# VCP 15 — Five-Surface Inventory Audit (id 9, 2026-09-21 16:25 Europe/Paris)
**Status**: AUDIT ONLY. No file edits.
**Phase 0**: VA UP at attempt 1 (gemini).
**Phase 0.5**: SANDBOX CURRENCY confirmed. Local HEAD `94fd5eb` = origin/main `94fd5eb`. 0/0 divergence. RULES.md v1.4.0 (2026-09-17, I.8 added).
---
## SANDBOX STATE
| Item | Value |
|---|---|
| Local HEAD | `94fd5eb` |
| origin/main | `94fd5eb` |
| Divergence | 0 / 0 (clean) |
| Working tree | only `.mavis/` untracked |
| RULES.md version | 1.4.0 (2026-09-17) |
| Branch | `main` |
## SANDBOX CURRENCY
```
$ git rev-parse HEAD
94fd5eb87efb90588f45b81b19ce462e661cd319
$ git rev-parse origin/main
94fd5eb87efb90588f45b81b19ce462e661cd319
$ git rev-list --left-right --count HEAD...origin/main
0 0
$ cat /workspace/.home/mavis-kits/RULES.md | head -4
# RULES — the law every Mavis agent obeys
Version: 1.4.0
Last changed: 2026-09-17 (I.8 added)
```
## Vibe Agents usage
Phase 0 probe via `python3 /workspace/.home/mavis-kits/tools/groq_runner_v2.py test` → `OK[gemini]: OK` (1 call, no retry).
---
## SECTION A — Agent rail (right side, inside VibeChat panel)
**A1. Existence in source:** **NOT FOUND.**
Searched terms: `agent-rail`, `agentRail`, `agent_rail`, `presence`, vertical-rail.
```
$ grep -rni "agent-rail|agentRail|agent_rail" src/ --include="*.ts" --include="*.tsx" --include="*.js"
(no output)
$ grep -rni "presence" src/vibechat/ src/host/
src/host/workspace/dna/dna-moduletypes.test.ts:179: await test('profile reflects feature presence', { ... })
$ grep -rni "rail\b" src/vibechat/ --include="*.ts" --include="*.tsx"
(no output)
```
**A2.** N/A.
**A3.** No commented-out legacy code, no unused component, no alternate branch reference. The only "rail"-adjacent component is `src/vibechat/components/ThreadsPanel.tsx` (134 lines, left-side, vertical thread list with text labels + icons — NOT a vertical column of circular agent icons).
**A4. Other "presence/agent status" indicators:** None in `src/vibechat/` or `src/host/` outside the unrelated DNA module-types test (which checks `feature presence` in data shape, not UI presence).
## SECTION B — Thread rail and its current toggle
**B1. Where is the thread list rendered?**
```
$ grep -rn "ThreadsPanel" src/ --include="*.ts" --include="*.tsx"
src/vibechat/VibeChatApp.tsx:3: import ThreadsPanel from './components/ThreadsPanel';
src/vibechat/VibeChatApp.tsx:468:
src/vibechat/components/ThreadsPanel.tsx:26: export default function ThreadsPanel(...)
```
`src/vibechat/VibeChatApp.tsx:435-485` wraps `ThreadsPanel` in `S1Panel` (the left-side section, edge='right' visually because the layout is mirrored — actually line 433-435 confirms edge: 'right' but the column is the first slot of the row).
```
src/vibechat/VibeChatApp.tsx:435-485
435 function S1Panel(props: {
436 leftOpen: boolean;
437 onOpen: () => void;
438 onClose: () => void;
...
452 if (!props.leftOpen) {
453 return (
454
464 );
465 }
466 return (
467
479 );
```
**B2. What currently toggles the thread list?**
- IconList "Open threads" button at line 455-463 (collapsed state)
- `onOpenThreads`/`onClose` callbacks at line 330-331
- State held at `src/vibechat/VibeChatApp.tsx:49`: `const [leftOpen, setLeftOpen] = useState(...)`
- Persistence via localStorage key `vibechat-panel-s1` (line 71)
- No keyboard shortcut found for S1.
**B3. PM2 pill mounted inside a panel (not on viewport edge)?** **NOT FOUND.**
```
$ grep -rn "panel-pill|inner pill|mount.*pill|panel-inside-pill" src/ --include="*.ts" --include="*.tsx" --include="*.js" --include="*.css"
(no results related to inner-panel pill mounting)
```
Dock pills live in `fvcms-dock-pills` containers (`src/cms/runtime/panel-manager/panel-manager.js:1670-1697`) attached to **viewport edges** (`data-dock-edge` attribute on `.fvcms-dock-{left|right|top|bottom}` divs).
**B4. Mount-pill-inside-panel support?** **NOT FOUND.** `panel-manager.js` and `panel-system.js` only support viewport-edge pills (line 1670-1697 in the runtime version; `docks[edge].el` is the edge container, not the panel).
## SECTION C — Message-panel minimise and toast restore
**C1. "Message-panel minimise" control.** No chevron-collapse control found. The closest semantic match is the toast's chevron expand (see C3) and the state machine for `chatState` (`'minimised' | 'expanded'`) persisted via `CHAT_STATE_KEY`.
```
src/vcs/VibeChatStore.js:18: const CHAT_STATE_KEY = 'vibechat.chatState.v1';
src/vcs/VibeChatStore.js:68-71: chatState: (typeof localStorage !== 'undefined'
? (localStorage.getItem(CHAT_STATE_KEY) || 'minimised')
: 'minimised'),
src/vcs/VibeChatStore.js:377-385:
setChatState(state, source) {
if (state !== 'minimised' && state !== 'expanded') return false;
if (state === this.chatState) return false;
this.chatState = state;
try { localStorage.setItem(CHAT_STATE_KEY, state); } catch (e) { /* ignore */ }
this._emit(source || 'chat-state');
return true;
}
```
Collapsing callers:
- `src/vcs/ChatContainer.js:118` — `onMinimize: () => vcsStore.setChatState('minimised', 'ui-surface-min')`
- `src/vcs/ChatContainer.js:127` — `setChatState('minimised', 'ui-expand-close')`
- `src/vcs/StandaloneContainer.js:660` — `setChatState('minimised', 'phase1-default')` (boot default when session key exists)
**C2. What remains visible when collapsed?**
The ToastBubble component (`src/vcs/ToastBubble.js`) renders when `chatState === 'minimised' AND toastBubble !== null`:
```
src/vcs/ToastBubble.js:8-13:
// Visibility:
// - When chatState === 'minimised' AND toastBubble !== null → visible
// - When chatState === 'expanded' → hidden (full surface takes over)
// - When chatState === 'minimised' AND toastBubble === null → hidden
// (e.g. fresh install before any AI reply)
```
Plus the input bar (rendered by `StandaloneContainer.js` or `ChatContainer.js`). The full message list / thread rail are hidden.
**C3. Toast's restore control.**
```
src/vcs/ToastBubble.js:43-47: const chevronBtn = el('button', 'vcs-toast-bubble-chevron');
chevronBtn.type = 'button';
chevronBtn.setAttribute('aria-label', 'Expand chat');
chevronBtn.title = 'Expand to full chat';
chevronBtn.textContent = '⌃';
src/vcs/ToastBubble.js:96-99: chevronBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
vcsStore.setChatState('expanded', 'ui-chevron');
});
```
`⌃` is the U+2303 "UP ARROWHEAD" character (caret). Wired to `vcsStore.setChatState('expanded', 'ui-chevron')`.
**C4. chatState state-machine interaction.** `vcsStore.chatState` is independent from PM2's `panel.state` — they live in separate stores (vcsStore vs DockManager.panels[id].state) and separate DOM trees (vcs/ vs cms/runtime/panel-manager/). The two are not synchronised in source.
## SECTION D — Toast variants
**D1. How many variants exist?** **Two** in `src/vcs/ToastBubble.js` (237 lines total).
| Variant | Render fn | Lines | Trigger |
|---|---|---|---|
| Default welcome | `renderDefault()` | 144-174 | `toastBubble === null && chatState === 'minimised'` |
| Real AI message | `renderMessage(msg, pending)` | 178-225 | `addMessage()` for an assistant message in active thread |
**D2. Triggers:** As above. `renderDefault` runs once on first mount with no message; `renderMessage` runs every time a new AI reply is added while the bubble is showing (or bubbles up to replace it).
**D3. Are variants user-editable?** **No.** Strings are hardcoded in `src/vcs/ToastBubble.js:161` (`'Ask VibeChat anything about your project, the canvas, or how things work.'`) and line 207 (`{ label: 'Reply', ... }`). No config file or registry.
**D4. Is the "Reply" button wired?**
```
src/vcs/ToastBubble.js:205-207:
// No explicit actions — provide a single "Reply" button so the
// user can open the chat and reply.
actions = [{ label: 'Reply', kind: 'primary', onClick: (ev) => { try { vcsStore.setChatState('expanded', 'ui-bubble-reply'); } catch (e) {} } }];
```
Yes — wired to `vcsStore.setChatState('expanded', 'ui-bubble-reply')`. Not a stub.
**Operator-claimed third variant** ("Hey there 👋 How's your day going? What's on your mind?" with "Reply" button): **NOT FOUND** in source. Possibly an aspirational description or future variant.
## SECTION E — PM2 panel minimise vs message-panel minimise
**E1. PM2 chrome minimise (–) handler.**
```
src/cms/runtime/panel-manager/panel-manager.js:316-336
DockManager.prototype.collapse = function (id) {
const panel = this.panels[id];
if (!panel) return;
if (panel.state === 'floating') {
const edge = this._pickSmartDockEdge(panel);
this.dock(id, edge);
panel.state = 'docked-collapsed';
this._renderPanelState(panel);
const dock = this.docks[panel.dockEdge];
if (dock) this._updateDockPills(dock);
return;
}
if (panel.state !== 'docked-active') return;
panel.state = 'docked-collapsed';
panel.isFocused = false;
this._renderPanelState(panel);
const dock = this.docks[panel.dockEdge];
if (dock) this._updateDockPills(dock);
};
```
Header button DOM (line 505) and click handler (line 567-569):
```
505: '' +
567: root.querySelector('.fvcms-pm-collapse').addEventListener('click', function (e) {
568: e.stopPropagation();
569: this.collapse(panel.id);
570: }.bind(this));
```
Module-level `minimize()` (used by `panel-system.js:144`) is a separate function for the `'minimized'` state (different from `'docked-collapsed'`).
**E2. Message-panel minimise handler.** `vcsStore.setChatState('minimised', ...)` — see Section C.
**E3. Restore interaction.** **NO documented interaction.**
- PM2 panel restore path: user clicks the dock pill at `src/cms/runtime/panel-manager/panel-manager.js:1694` (`fvcms-dock-pill[data-pm-panel-id=...]`) → `setState(id, 'floating')` or `dock(id, edge)` → `panel.state = 'docked-active'`.
- chatState is NOT touched by any PM2 restore path. If the message-panel was `minimised` when the PM2 panel was collapsed, chatState stays `'minimised'` in localStorage. When the PM2 panel is restored, the inner ToastBubble re-renders (because chatState === 'minimised' AND toastBubble !== null from before).
- No reset logic, no conflict handler.
**E4. Conflicts / dead paths / state leaks.** No documented interactions. Possible concerns (NOT proven — read-only):
- If `StandaloneContainer.js:660` boot path sets `chatState='minimised'` unconditionally when session key exists, a user who explicitly chose `'expanded'` in a previous session may get a regression on reload. (Existing behaviour since 2026-09-10; not part of this dispatch.)
- PM2 panel `state === 'minimized'` (panel-system.js:144) vs `state === 'docked-collapsed'` (panel-manager.js:316) are TWO different minimise concepts in two parallel implementations. Both write to `panel.state` but use different string values. Possible drift risk if both code paths target the same panel.
## New reflex
**9-#280 (VCP 15)**: When asked to inventory UI surfaces, ALWAYS grep for canonical terms first (`agentRail`, `rail`, `pill`, `chevron`, `setChatState`, `setState`, `collapse`, `minimize`). If 0 hits on the canonical term, immediately search the file system for the component directory (`ls src/vcs/`, `ls src/vibechat/components/`) and read the closest-named file top-to-bottom. Don't trust grep-only — comments can mislead.
## Compliance
| Constraint | Honored? |
|---|---|
| READ-ONLY | YES — no file edits, no commits, no deploys |
| Don't design / don't propose | YES — report only |
| Don't touch bridge / cascade / R107 / kit / TLS / Gallery / FC / VSIL | YES |
| Don't invent | YES — explicit "NOT FOUND" for A1 and the operator-claimed 3rd variant |
| Don't skip sections | YES — all 5 sections reported |
| Adapt or STOP if ground is different | YES — adapted "agent rail" → "vertical column of circular icons"; reported exact files |
## ALIGN
| Layer | State |
|---|---|
| [1] git: clean | YES (0/0 divergence) |
| [2] deploy: N/A | N/A — read-only |
| [3] HQ: bulletin + doc | bulletin b004562 posted; this doc drafted |
| [4] Notion: row or skip-reason | SKIPPED — read-only inventory, no design decisions; audit content is the doc body |
| [5] memory: entry appended | entry drafted in §New reflex |
| [6] task board: closed or n/a | n/a — audit task, no board entry needed |
| [7] report: header + footer + return tuple | YES |
| [8] operator: N/A | N/A — read-only |
## Bulletin + doc IDs
- Bulletin **b004562** (info, posted)
- Doc id TBD
## USAGE / NATIVE FOOTPRINT
USAGE: VA 1 call Phase 0 (gemini attempt 1, no retry) | tool-IO ~38 commands (curl bulletin/tasks; grep src/vibechat, src/vcs, src/cms; cat VibeChatStore, ToastBubble, panel-manager sections; bulletin post; doc draft)
ROLLING 24h: VC 200 (b004534) → VC 201 (b004540 SHIPPED) → VC 202 (b004548 audit) → VCP 15 (b004562 audit)
NATIVE FOOTPRINT: 8 prose sentences (audit kept tight per cap 10; all content is grep/file:line citations)
---
## Return tuple
```
bulletin_id: b004562
doc_id:
notion_url:
commit_hash: 94fd5eb (no commit; clean baseline)
```
## Propagation line (reflex #22)
VCP 15 inventory findings → operator design conversation (id 9 thread, this dispatch). No bulletin cascade.
---
## [id9] | end PROMPT VCP 15