Version. v0.1
Date. 2026-06-26
Build target. loomworks Operator-Layer frontend, branch feat-companion-surface (off cr-2026-125-companion-surface-phase1, the known-good focus-integration foundation — no drift).
Against. loomworks-companion-build-spec-v0_1.md + mockups …-surface-desktop-… (1) and …-surface-mobile-… (2), read as a specification.
Verification. Vitest suite 438 / 76 files green (32 new conformance tests for this surface) + live render of the real engagement E0007 "Goosey the Moose" (held=2, settled=13, renders=13) at desktop (1280) and mobile (390) widths via Playwright + the dev session. Screenshots in ~/Downloads/companion-inengagement-*-render-v0_1.png.
Legend: ✅ CONFORMS · ◐ CONFORMS w/ noted real-data or honest-empty fill · ⚠ DEVIATION (with reason)
| # | Element (spec) | Status | Evidence / note |
|---|---|---|---|
| D1 | Dual-pane frame: left conversation 46%, right workspace 54%, divider between | ✅ | InEngagementSurface grid minmax(0,46fr) minmax(0,54fr); left pane border-r. Live render shows the split + divider. |
| D2 | Header left: "Loomworks · {engagement name}" (no "Engagement:" label) | ✅ | InEngagementHeader: {BRAND.productName} · {engagementName}. Live: "Loomworks · Goosey the Moose". |
| D3 | Header right: focus chip "here: {n} · {name} · {E####}" | ◐ | Live: "here: 7 · Goosey the Moose · E0007". Name is the engagement's real title (mockup's "Goosey" was illustrative shorthand) — real-content fill per spec. Never the UUID (verified: no uuid-secret in DOM). |
| D4 | Header right: sort-order toggle (CONFIRMED KEEP, not in mockup) | ✅ | data-testid=sort-order-toggle, Icon flips with order; click → PUT /me/settings (optimistic). |
| D5 | Header right: Operator avatar | ✅ | "MP" initials from person.display_name, info-wash circle. |
| D6 | NO "Speak to Companion" pill | ✅ | Absent by construction. |
| D7 | NO big engagement-name title inside the left pane | ✅ | Left pane is conversation only; name lives in header + chip. |
| D8 | Left pane = vertical column, two children | ✅ | ConversationPane: scroll child + input-zone child. |
| D9 | Child 1 conversation: flex:1; overflow-y:auto (THIS scrolls) | ✅ | conversation-scroll = flex-1 overflow-y-auto. |
| D10 | Operator messages right-aligned, info-blue bubbles | ✅ | OperatorBubble: self-end bg-info-wash text-info. Live confirms. |
| D11 | Companion messages left-aligned, "Companion" label above, secondary-bg bubble | ✅ | CompanionBubble: self-start, label text-content-3, bubble bg-surface-2. |
| D12 | Inline action buttons on Companion messages where applicable | ◐ | Wired to the engine's real suggested_actions (the API-supported per-turn affordance). The mockup's literal "Yes, commit ↗ / Not yet" is illustrative — see DEV-1. |
| D13 | Child 2 input zone: border-top, secondary-bg, NO flex-grow, FIXED SIBLING that does NOT scroll | ✅ | input-zone = shrink-0 border-t bg-surface-2, sibling of (not inside) the scroll area. Test asserts scroll.contains(input)===false. |
| D14 | Round mic on the LEFT (●, 38px, info fill/border/glyph) | ✅ | 38×38 bg-info-wash border-info-line text-info. Live confirms purple/info circle. |
| D15 | One field filling the rest, placeholder "Speak, or type a note or command…" | ✅ | composer textarea, SURFACE.inputPlaceholder. |
| D16 | NO Send button (Enter sends) | ✅ | Test: queryByRole("button",{name:/send/i}) is null. |
| D17 | Suggestion chips below the input row | ✅ | SURFACE.suggestionChips(...); live shows three chips. |
| D18 | Right pane: tab strip (Memory · Manifestation · Shaping · Rendering), click switches room | ✅ | WorkspacePane role=tablist, 4 tabs, onRoomChange. |
| D19 | Active room indicated | ⚠→✅ | The mockup CSS marks active as a bordered pill on surface-1 (its .room.active), NOT "underlined" as the spec prose says. Conformed to the mockup CSS (authoritative artifact). See DEV-2. |
| D20 | Workspace body flex:1; overflow-y:auto rendering per-room cards | ✅ | workspace-body = flex-1 overflow-y-auto. |
| # | Item | Status | Note |
|---|---|---|---|
| D21 | Memory: "Held for you ({n})" + "Settled ({n})" sections, real engine assertions | ✅ | Live: "Held for you (2)" + "Settled (13)" from real E0007 data. |
| D22 | Held card: content + Definition/Rule type chip + Required/Recommended chip where applicable + contributor + Confirm action | ✅ | Live held card: "Held" + "Definition" + "Held 15" + "Confirm ↗" + content + "noted by Marvin Percival (Operator)". |
| D23 | Confirm = Operator's reserved act (Held→Settled live, Operator-approves, not automatic) | ✅ | commitAssertion through the Operator's own session; test asserts call + refetch. |
| D24 | Honest empty states ("Nothing held right now — the Companion isn't waiting on you") | ✅ | SURFACE.heldEmpty(companionName) / settledEmpty; test asserts + no dead buttons. |
| D25 | Rendering: produced renders to download/view + candidates; badge treatments | ◐ | Live: real renders with "Render" + "HTML" badges + "View ↗" (opens the produced HTML). Candidate cards: none (no candidate endpoint) → honest. |
| D26 | Manifestation / Shaping room shapes | ◐ | No engine endpoint exists for these rooms (confirmed). Honest empty states (composeEmpty / shapeEmpty) rather than fake cards — spec: "honest empty states where it doesn't [exist]". |
| D27 | Badge treatments (Held/In record/Question/Shape/Type/Render/Candidate) | ✅ | WorkCard BadgeTone map = the mockup's badge() colour table as design-token classes. |
| # | Element/behavior (spec) | Status | Evidence / note |
|---|---|---|---|
| M1 | Single column; header: engagement name + compact context line "here · {n} · {E####}" | ✅ | Live: "Goosey the Moose" + "here · 7 · E0007" + MP avatar. |
| M2 | NO desktop labels (minimal header) | ✅ | Compact header branch in InEngagementHeader. |
| M3 | Room stack flex:1; overflow-y:auto, one room at a time | ✅ | mobile-stack = flex-1 overflow-y-auto; renders the active room only. |
| M4 | Room name + dots indicator + chevron prev/next | ✅ | mobile-room-name, 4 dots (active wider, info), ‹ / › buttons. |
| M5 | Bottom bar flex-shrink:0; border-top — FIXED SIBLING, does NOT scroll | ✅ | mobile-bottom-bar = shrink-0 border-t, sibling of stack. Test asserts stack.contains(bar)===false. |
| M6 | Bottom bar: field (flex:1) on left + large round mic (48px) on RIGHT; hint line above | ✅ | Live: hint "swipe to move · hold to speak" + field + 48px info-blue mic on the right. |
| M7 | The four rooms are the swipe plane (mockup has no conversation tab) | ◐ | Conformed to the mockup's actual arrangement: swipe plane = the four rooms, no transcript. See DEV-3. |
| # | Behavior (spec) | Status | Evidence |
|---|---|---|---|
| B1 | Input zone never scrolls — fixed sibling, both desktop & mobile | ✅ | D13 + M5 (tests assert sibling-not-descendant on both). |
| B2 | Room switching (tab desktop / chevron+swipe mobile) re-renders that room; active indicated | ✅ | Tests: tab click → onRoomChange; chevron + swipe → onRoomChange. |
| B3 | Mobile swipe between rooms (\|dx\|>50 && \|dx\|>\|dy\|); vertical scroll = within room; gestures coexist | ✅ | MobileSurface touchend matches the mockup rule. Tests: left-swipe moves; vertical drag does not. |
| B4 | Mic toggle: not-listening = info-blue; listening = iron-oxide + hint "listening… '{example}'" | ✅ | Live: idle = info-blue; listening = iron-oxide + placeholder listening… "show me my outputs". |
| B5 | Suggestion chips send their text on click | ✅ | Test: chip click → send(chipText). |
| B6 | Enter sends a typed message (no Send button) | ✅ | Tests (desktop + mobile): Enter → send; Shift+Enter does not. |
| B7 | Conversation order: default + sort-order toggle flips it (CONFIRMED KEEP) | ✅ | Baseline oldest_first (newest at bottom near input); header toggle + Companion tune_setting both flip; stored pref honoured. |
suggested_actions. So inline Companion-message buttons are wired to suggested_actions (they send on click), and the structured commit affordance lives where the API supports it — the Memory-room held card's Confirm (the Operator's reserved act). Behaviorally conformant ("inline action buttons where applicable"); the specific labels were illustrative..room.active) renders the active tab as a bordered pill on surface-1. Conformed to the mockup CSS (the authoritative artifact), not the prose word.h-[100dvh], layout switches on WIDTH at 900px). Conformed to structure + behavior; the fixed dimensions were illustration scaffolding.SURFACE.suggestionChips(heldCount) already supports the contextual "Confirm N held notes" variant; it is fed the standing set at the desktop pane pending a shared held-count source. Per spec, chip wording is Operator-refined later; built functional now.person.companion_name); avatar initials from person.display_name.
| Requirement | Status | Note |
|---|---|---|
| Identity: number + name + E####, never UUID | ✅ | Chip + context line; DOM has no UUID (asserted). |
| Plain-terms: Memory/Manifestation/Shaping/Rendering as room labels; wire-tokens forbidden outside adapters | ✅ | room-labels.ts (vocab-wall room-label exception); components dispatch on a wall-safe content discriminator, never the room-key literals. |
| Only show what's available (no disabled/grayed) | ✅ | DEV-4. |
| Responsive: layout switches on WIDTH | ✅ | useIsWide(900); verified at 1280 + 390. |
| Template-clean: tokens (no raw hex), strings via registry, Companion name from data | ✅ | New semantic palette tokens; all strings in SURFACE/BRAND/FOCUS; companion name from person.companion_name. Vocab-wall + template-clean guards green. |
| Companion is the entity; AI invisible | ✅ | No model disclosure anywhere. |
DUNIN7 — Done In Seven LLC — Miami, Florida Companion in-engagement surface — conformance checklist — v0_1 — 2026-06-26