# VC 145 — canvas + project switcher + persistence investigation (READ-ONLY)
## Verdict
The operator's hypothesis is correct: the canvas is a same-page `#fv-main-canvas` div rendering the FreshVibeOrigin template with default data, NOT the active project. The project switcher shows 4 hardcoded projects pointing at deployed `*.freshvibeapps.com` URLs that the canvas never fetches. Chat build output (VC 142's "Velvet Brew Coffee House" / "Brewed Bliss Coffee Shop") went NOWHERE on disk — it came back as chat-text only. `mountCms()` is a stub.
## Three-question answers
**(a) Canvas loads** — `FreshVibeOrigin template` (default placeholder data, NOT active project origin)
- `src/host/host-boot.js:913-934` injects `
` into body
- `host-boot.js:943` `renderOriginPage(canvas, null, 'operator')` renders template with `data=null` → DEFAULT_DATA (`origin.js:22-40`) = "Welcome to FreshVibe" + 3 items etc.
- On project switch (`host-boot.js:1080-1093`), canvas re-renders the SAME template. Comment b2928 confirms "renderOriginPage() handles per-project data via WDM getActiveProject()" — but the function does NOT actually read WDM, it uses `data=null` → defaults.
- `cms-negotiator.ts:58-71` `mountCms()` is a STUB: `// TODO (option A): actually dispatch the 4-script mount sequence. // For now, this is a no-op that records intent.`
- So switching projects updates localStorage + fires event, but the canvas always shows the template placeholder. The CMS panel is independent (reads WDM on its own).
**(b) Project switcher reads** — `localStorage fvos:workspace:v1:projects` (seeded by hardcoded bootstrapBuiltInProjects)
- `src/host/header/ProjectSwitcher.tsx:82` `readWorkspace()?.wm.listProjects() ?? []`
- `src/host/workspace/workspace-manager.ts:149-151` `Object.values(this.snap.projects).sort(...)`
- `src/host/workspace/registry.ts:35-38` `Object.values(reg.projects).sort(...)` where reg = `JSON.parse(localStorage.getItem('fvos:workspace:v1:projects'))`
- Registry is seeded ONCE on empty by `bootstrapBuiltInProjects()` (`registry.ts:108-160`). Four HARDCODED built-ins:
1. `vibecoder-self` → `https://vibecoder.freshvibeapps.com` (cmsVersion 2.0.0, mountContract: 'editor')
2. `oscar-tree-academy` → `https://oscarcms4.freshvibeapps.com` (cmsVersion 1.2.25, mountContract: 'oscar')
3. `oscar-static-source` → `https://oscar-static-cms-v0-8-0.freshvibeapps.com` (cmsVersion 2.0.0, mountContract: 'oscar')
4. `hopfan-europe` → `https://hopfan-europe.freshvibeapps.com` (cmsVersion 0.8.0)
- ALL four are deployed `*.freshvibeapps.com` domains.
**(c) Chat build goes** — `NOWHERE` (no auto-persist path)
- VC 142's smoke returned 37,362-byte chat reply with "Velvet Brew Coffee House" / "Brewed Bliss Coffee Shop" HTML — but cascade-fallback.jsonl + VPS file scan: ZERO traces on disk.
- Cascade pipeline (`bridge/lib/chat-handler.cjs:170-200`, vibe-agents/*) has NO post-build hook that calls `write_file` with the HTML. The HTML returns to chat text only.
- Bridge DOES have a `write_file` tool (`bridge/lib/chat-handler.cjs:1476-1478`, `bridge/lib/fs-safe.cjs:17-49`) which writes to `workspaceRoot + path` (default `'/var/www/freshvibeapps/clients/vibecoder'` per `chat-handler.cjs:1093`). But the cascade does NOT auto-call it after a build.
- VibeChat has 6 tools (`src/vibechat/tools.ts:361-440`): `workspace_context`, `switch_project`, `switch_surface`, `mount_cms`, `register_project`, `panel_layout`. None operates on a build output. `register_project` requires an EXISTING origin URL (calls `detectProject(origin)` to fetch `/FV-CMS-VERSION.txt`, `/FV-CMS-MANIFEST.json`, `/annotation.json` from the URL — `registry.ts:62-102`) — it does NOT take HTML content.
## Coffee house: found on disk?
NO. Searched `/var/www` and `/opt` for `velvet`, `coffee`, `brew`:
- 3 matches in `assets/coffee-CfnpWUYo.js` — Vite's lazy-loader for the CoffeeScript language (system asset, not user content)
- 4 matches in `monaco-editor/.../coffee.*` — Monaco's CoffeeScript syntax highlighting (node_modules)
- 1 match in `oscar-wp-sandbox-130800/wp-admin/css/colors/coffee` — WP admin color theme
Zero user-generated HTML from VC 142's smoke. The 37KB reply was sent over the chat socket to the operator's UI; nothing written to filesystem.
## Why the operator's "no coffee house project" is correct
The switcher CANNOT show a "coffee house" project because:
1. There is no path for a chat build to create a project — no `create_project` tool, no `save_build` hook.
2. The only registration path is `register_project(origin, name)` which needs an existing deployed URL with `/FV-CMS-MANIFEST.json`. Chat builds return chat-text HTML, not deployed URLs.
3. The built-in 4 projects are baked into source code (`registry.ts:108-160`); operator can only ADD via `register_project` against a deployed origin.
So the operator's chat-built "coffee house" never had a chance to appear in the switcher. The switcher's 4 entries are the only ones possible unless the operator manually deploys first, then registers.
## What would need to change to support chat-built projects (NOT IMPLEMENTED — read-only)
1. Build cascade pipeline needs a post-cascade hook: when build-mode returns HTML, if `mode === 'build' && reply.length > 1000`, extract HTML and call write_file at `/var/www/freshvibeapps/clients/
/index.html`.
2. New tool: `create_project({name, html})` that does: register_project(origin=`.freshvibeapps.com`, name) then upload HTML to `/var/www/...`.
3. The project switcher reads from localStorage, not the filesystem, so creating a workspace dir doesn't add it to the switcher automatically — needs the WDM call too.
4. The current `mountCms()` is a TODO stub; full mount logic lands in "Option A" refactor.
## Source-of-truth pointers
- Canvas mount: `src/host/host-boot.js:913-934, 1068-1093`
- Canvas content: `src/gallery/origin/freshvibe-origin/origin.js:42-98`
- Project switcher: `src/host/header/ProjectSwitcher.tsx:79-378` (lines 82, 117 for read paths)
- Workspace manager: `src/host/workspace/workspace-manager.ts:115-137, 172-189`
- Project registry: `src/host/workspace/registry.ts:35-160`
- CMS mount stub: `src/host/workspace/cms-negotiator.ts:58-71` (lines 67-69 = TODO)
- VibeChat tools: `src/vibechat/tools.ts:361-440`
- Bridge write_file: `bridge/lib/chat-handler.cjs:1476-1478` + `bridge/lib/fs-safe.cjs:17-49`
- Bridge workspaceRoot hardcode: `bridge/lib/chat-handler.cjs:1093-1125` (all 5 read tools pin to `/var/www/freshvibeapps/clients/vibecoder`)
## VA status
Vibe Agents DOWN (probe returned `(None, None)` at dispatch start; re-tested end of dispatch same). All work native. No retries attempted (no 400/#227 encountered).
## Repo currency
Local HEAD `3e4621d` (VC 142 merge). Origin `6324ae0` (2 commits ahead: VC 141 ladder honest criteria + merge). Local is BEHIND origin but working tree clean (only `.mavis/` untracked). Per READ-ONLY constraint, did NOT pull. Read latest source via `git show origin/main:`.
## New reflexes (9- namespace)
- **9-#226 (VC 145)**: When asked to trace "where does X load / read / write", check (a) what UI element renders it, (b) the data source function, (c) the persistence layer (localStorage key / DB table / file path), (d) any post-event hooks that re-render. In this dispatch: canvas = div#fv-main-canvas with `renderOriginPage(canvas, null /* data */, 'operator')` — the `null` data arg means defaults render regardless of project.
- **9-#227 (VC 145)**: A `mountXxx()` function with a `// TODO` comment that says "no-op for now" or "records intent" is NOT mounted. It's a STUB. Look for the TODO marker before concluding the system works. `cms-negotiator.ts:67-69` is the canonical example.
## Deliverables
- Bulletin: b004038
- HQ Doc: this file (d000290)
- Notion: skipped (no API key)
- Local: `/workspace/vibecoder-standalone/.mavis/dispatches/VC-145/`
- Git: no commits (READ-ONLY)