moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
# PR3 Recon — SCREEN 02 · Chat (ship Variation B "Split work canvas")
Branch: `feature/warm-hive-redesign`. Maps the **current Chat/agent-runtime** implementation
in `apps/web` against `docs/design_handoff_waggle_app/SCREENS.md` §02 + `design-files/screens/chat.html`.
**Headline:** the conversation surface, activity stream (steps), tool cards, approval card, model
pill, persona pill, composer, artifact card, and provenance primitive **all already exist** and are
functional. The **right work-canvas (Variation B's ~42% live-drafting side panel) does NOT exist**
it must be built. There is also **no provenance pill inside the activity stream**, and the agent's
streamed "thinking" steps render as a **flat inline list, not the design's collapsible Activity
card with `⬡ provenance` pills**. The visual styling is current "Hive DS" (primary/secondary/muted,
emerald/amber/sky/violet) — it needs the warm-token migration but the structure is mostly there.
---
## 1. Current structure (with line refs)
### Component tree
```
ChatHost.tsx keep-alive portal host: 1 ChatWindowInstance per visited workspace
└ ChatHostInstance portals into per-workspace container; composes title bar
└ ChatWindowInstance.tsx data wiring: useChat + useSessions + model/team fetch
└ ChatApp.tsx ALL of the chat UI (793 lines) — single big component
├ <header> (chat-header) persona pill · Memory chip · storage/team chips · autonomy · model pill
├ Agent Profile panel (collapsible)
├ Pins bar
├ <div scrollRef> thread messages.map → bubbles
│ └ BlockRenderer (per assistant msg with blocks)
│ ├ TextBlock / StepBlock / ToolUseBlock / ArtifactBlock / ModelSwitchBlock
│ └ ToolCard (legacy tools[] path)
│ └ FeedbackButtons · suggested-action chips
│ └ ApprovalGate (pendingApproval)
└ composer (textarea + slash menu + attach + send)
```
### Key file:line anchors
- **ChatHost.tsx** — keep-alive via React portals. `ChatSlot` (`:85`) is the seam node
`WorkspaceRoute` passes into `WorkspaceDesktopApp`'s `chatSlot` prop. `ChatHostInstance` (`:96`)
portals `<ChatWindowInstance>` into a stable per-workspace `<div>` (`getChatContainer` `:57`), kept
alive (hidden, not unmounted) so in-flight SSE survives navigation. **The split canvas must live
INSIDE this kept-alive subtree** (either in ChatApp or a wrapper it renders), or the canvas state
is lost on navigation. ChatHost itself only does title + keep-alive; it is NOT the place to add a
sibling canvas pane.
- **ChatWindowInstance.tsx** — thin data layer. `FALLBACK_MODELS` (`:14`); calls `useChat` (`:98`)
and `useSessions` (`:97`); fetches model list/current-model with a 20s retry loop (`:126211`);
`handleModelChange` (`:213`). Passes ~20 props straight into `ChatApp` (`:221`).
- **ChatApp.tsx** — the entire rendered surface:
- Header bar: `:746` (`data-testid="chat-header"`). Persona picker `:765`; **Memory chip** `:815`
(always-visible trust signal, `Brain` icon); storage badge `:829`; team presence `:843`;
overflow `⋯` menu in compact mode `:867`; **AutonomyToggle** `:920`; **model picker** `:931`.
- `ToolCard` (`:99`) — legacy `msg.tools[]` render path (only used when a msg has no `blocks`).
- `ApprovalGate` (`:223`) — the inline approval card with RiskBadge + Allow once / Always allow /
Deny / Show details.
- `AutonomyToggle` (`:339`) — Ask first / Trusted / Autopilot chip + TTL dropdown.
- Thread render `:10501191`; per-message bubble `:1093`; `BlockRenderer` invocation `:1115`;
plain-content fallback `:1121`; suggested-action chips `:1160`.
- Composer `:11941236`; slash menu `:1195`; textarea `:1219` ("Message Waggle... (/ for commands)").
- `WorkspaceBriefing` empty-state `:1051` (renders when `messages.length === 0`).
- **chat-blocks/** (the progressive-disclosure renderers):
- `BlockRenderer.tsx` — switch over block.type; routes completed file-writes to `ArtifactBlock`
(`:34`), else `ToolUseBlock`.
- `StepBlock.tsx` — a single agent "thinking" step: spinner/check + description. **Flat inline
row — NOT wrapped in a collapsible Activity card and has NO provenance pill.**
- `ToolUseBlock.tsx` — collapsible tool row (status icon + name + input summary + duration + raw
JSON on expand).
- `ArtifactBlock.tsx` — the Cowork "artifact card": icon + filename + "Created/Updated by the
agent" + **Open in Files** (stashes deep-link + fires `waggle:open-app`). `isArtifactBlock` (`:21`).
- `ModelSwitchBlock.tsx` — fallback-model banner.
- **chat-header-layout.ts** — pure `shouldCollapseChatHeader(width)` decision (threshold 480px,
`:16`); `CHAT_HEADER_OVERFLOW_CONTROLS` / `CHAT_HEADER_PRIMARY_CONTROLS` classify which chips fold.
### Container / layout the canvas must slot into
`WorkspaceDesktopApp.tsx:841``{/* Body: main canvas + right context panel */}` is a
`flex-1 flex overflow-hidden` row. The chat tab renders `chatSlot ?? <placeholder>` at `:882902`
inside `<main className="flex-1 min-w-0 overflow-auto">`. The chat slot is given the full main
column. **The split canvas should be implemented as a horizontal flex INSIDE ChatApp's own root
`<div className="flex h-full relative">` (`:697`)** — append the canvas `<aside>` as a sibling of
the existing chat `<div className="flex flex-col flex-1 min-w-0">` (`:744`). That keeps it within
the kept-alive portal subtree and reuses ChatApp's existing flex root.
---
## 2. Data contract — how streamed turns / activity / artifacts arrive
### Source of truth: `useChat.ts` (apps/web/src/hooks/useChat.ts)
- Returns `{ messages, isLoading, sendMessage, clearHistory, pendingApproval, approveAction }` (`:325`).
- `sendMessage` (`:93`) appends a user `ChatMessage` then an empty assistant `ChatMessage`
(`blocks: []`), then **iterates `adapter.sendMessage(...)` as an async generator of `StreamEvent`**
(`:125`), reducing each event into the LAST assistant message's `blocks[]` immutably (`:130262`).
- **StreamEvent types** (`lib/types.ts:616`): `'token' | 'step' | 'tool_start' | 'tool_end' | 'done'
| 'error' | 'approval_request' | 'approval_required' | 'model_switch' | 'notification'`.
Adapter maps SSE event names to these in `adapter.ts:722731` (`tool`→`tool_start`,
`tool_result`→`tool_end`, etc.).
- **Event → block reduction** (`useChat.ts`):
- `token` (`:141`) → appends/extends the trailing `TextContentBlock`.
- `step` (`:152`) → marks prior running steps done, pushes a new `StepContentBlock{status:'running'}`.
**This is the "activity/thinking" stream — currently a flat sequence of StepBlocks, not grouped.**
- `tool_start` (`:167`) → pushes a `ToolUseContentBlock{status:'running'}` AND mirrors into legacy
`tools[]`.
- `tool_end` (`:183`) → flips the matching tool block to `done` with `result`/`duration`.
- `model_switch` (`:214`) → `ModelSwitchContentBlock`.
- `error` (`:225`) → `ErrorContentBlock`.
- `done` (`:231`) → marks all running blocks done; appends final text if none present.
- `approval_request` / `approval_required` (`:248`) → `setPendingApproval(data)` — does NOT mutate
blocks; surfaced as a single `pendingApproval` slot (one at a time).
- `content` is kept in sync via `flattenBlocks` (`:45`) for copy/pins/search.
- **History load**: `getHistory(workspaceId, sessionId)` → `ensureBlocks` backfills `blocks[]` for
legacy messages (`:14`, `:8391`).
- **Adapter wire**: `adapter.sendMessage` (`adapter.ts:686`) POSTs `/api/chat` with
`{workspaceId, message, sessionId, persona, autonomy, shape}` and parses SSE lines into StreamEvents.
### ContentBlock shapes (`lib/types.ts:461503`)
`TextContentBlock{type,blockId,content}` · `StepContentBlock{type,blockId,description,status}` ·
`ToolUseContentBlock{type,id,name,input?,status,result?,duration?}` ·
`ModelSwitchContentBlock{type,blockId,from,to,reason}` · `ErrorContentBlock{type,blockId,message}`.
`ChatMessage{id,role,content,blocks?,timestamp,tools?,feedback?,pinned?,persona?}` (`:366`).
`ApprovalRequest{requestId,toolName,description,input,riskLevel?,approvalClass?,trustSource?,
explanation?,...}` (`:387`).
### Artifacts — how they arrive
There is **no dedicated artifact/canvas stream channel.** An "artifact" today is derived purely from
a completed `write_file`/`edit_file`/`file_write` tool block (`ArtifactBlock.isArtifactBlock` `:21`)
and rendered as an inline card. The design's **live-drafting `teardown.md` canvas has no backing
data source** — the streamed events carry no document body, only the tool's `input.path` + opaque
`result` string. **Building the canvas means either** (a) deriving its content from the most recent
file-write tool block's `input.content`/`result`, or (b) adding a new stream channel /
artifact-fetch. This is the single biggest data gap (see §5).
### Provenance data
The activity steps from the server are plain text descriptions — **the SSE `step` payload carries no
structured `source`/`provenance` field.** The design's `⬡ mem://hive · provenance kept` pill has no
backing data today; it would need either a richer `step` payload or a client-side heuristic. A reusable
provenance UI primitive already exists: `components/ui/evidence-chip.tsx` (`EvidenceChip`).
---
## 3. Test contract (must not break)
### `apps/web/src/test/chat-artifact-block.test.tsx`
- `BlockRenderer` renders `chat-artifact-block` for a completed `write_file` with a path; shows the
basename and "Created by the agent".
- `edit_file` → "Updated by the agent".
- running/error file-writes keep the generic tool row (NOT an artifact card).
- `isArtifactBlock`: false for non-file tools / missing path; true for a done write_file.
- **Open in Files** stashes the path deep-link and fires `waggle:open-app` with `appId:'files'`.
- **Contract for PR3:** keep `data-testid="chat-artifact-block"` + `chat-artifact-open`, the
Created/Updated copy, and the `isArtifactBlock` routing predicate intact. The canvas is ADDITIVE —
the inline artifact card stays.
### `apps/web/src/test/p1a-chat-state.test.tsx`
- `useChatWidgetState` persistence (persona, autonomy TTL + 10s auto-revert, P4 defaultAutonomy
inheritance) under `waggle-chat-state-v1`.
- `seedChat`/`takeChatSeed` one-shot semantics; `composeChatTitle` formatting.
- **WorkspaceDesktopApp two-seam edit (§5.2):** seam (a) controlled `activeTab` + `onTabChange`
(test IDs `ws-tab-chat`, `ws-tab-tasks`, `ws-tasks-tab`); **seam (b) the chat tab renders the
provided `chatSlot` instead of the placeholder** (test IDs `chat-slot-stub`, and asserts
`ws-chat-tab-open` is absent when a slot is given).
- **Contract for PR3:** the `chatSlot` seam + `activeTab`/`onTabChange` props are load-bearing — the
split canvas must NOT change `WorkspaceDesktopApp`'s public chatSlot contract; it lives inside the
slot's subtree (ChatApp), not as a new prop on the desktop.
### Other related tests (grep before editing)
`chat-blocks/TextBlock.test.tsx`; `context-rail-fetch.test.ts` (the `onContextRail` double-click path
in ChatApp `:1095`); header-layout behavior is pinned via `chat-header-layout.ts` consumers.
---
## 4. Design spec — Variation B, with exact copy
### Context header (replaces current `chat-header` styling)
- `wmark` hex "C" (honey gradient) + workspace name **"Competitive Intelligence"** + mono sub
**"workspace · 142 memories · 9 sources"**.
- **Model pill** (right): pill, `--surface` bg, `--line` border, healthy live dot, title
"Waggle picked the model — click to override", text **"Model: auto · Claude Sonnet"** (`b` on
"auto"). → maps to current model picker (`ChatApp.tsx:931`) but restyled as a pill with a live dot
and the "auto ·" prefix.
- **Memory icon button** (right) — `iconbtn`, brain glyph, title "Memory & context". → current Memory
chip (`:815`) becomes this icon button.
### Thread (max-width 760px; **narrows to ~620px when canvas open**)
- **User message:** `--surface` bubble, **asymmetric radius `4px 14px 14px 14px`**, 15.5px/1.55.
(Current user bubble is `bg-primary rounded-xl` `:1108` — needs warm surface + asymmetric radius.)
- **Bot message:** hex "W" avatar (honey gradient); meta line **"Waggle · Analyst · Claude Sonnet"**
(`who` `b` on "Waggle"). Prose 15.5px/1.62; bullet lists use honey `` markers.
- **Activity stream card (the "magic"):** collapsible, `--bg-2` bg, `--line-soft` border, radius
`--r`. Header: violet **spark** icon + **"Worked across memory, web & files · 6 steps · 38s"**
+ chevron (rotates 90° when open). **Default-open on the active turn.** Each step row (`.astep`):
colored dot + text + optional **provenance pill** — mono, `--intel` color, `--intel-wash` bg,
format **`⬡ mem://hive · provenance kept`**. Exact step copy from chat.html:
- (intel dot) "Recalled **6 memories** from your hive — last quarter's landscape, the Mem0
teardown, your pricing notes." → prov `⬡ mem://hive · provenance kept`
- (web/cyan dot) "Searched **9 competitor sites** for pricing & positioning `(mem0, letta,
langmem, notion…)`"
- (work/blue dot) "Compared against your **last teardown** — flagged 3 changes since March."
- (healthy dot) "Drafted **teardown.md** — exec summary, landscape table, opening."
- **Bot prose after activity:** "Done. Here's the shape of it — full draft is in **teardown.md**."
+ 3 honey-bullet items + "Want me to turn the opening into a one-page brief for the board, or
export the table to the pricing sheet?"
- **Approval card** (`--honey-wash` bg, `--attention` border, warning icon):
- Title: **"Approve before I leave your machine"**
- Body: "This step writes to an external system — `Salesforce Q2 Pricing` (mono). Everything
else stayed local. I'll export the 9-row table and nothing else."
- Actions: **"Approve & export"** (honey) / **"Not now"** (ghost).
- → maps to current `ApprovalGate` (`:223`) — restyle to honey-wash; keep RiskBadge + the
Always-allow gating logic.
### Composer
- Rounded box, `--surface`, `:focus-within` → `--honey-line` + `--honey-glow`. Textarea placeholder
**"Reply, or ask Waggle to take the next step…"**. Tool chips row: **Attach** / **Persona: Analyst**
/ **Tools**; spacer; mono hint **"⏎ send · ⌘K commands"**; honey send button (``, 40px).
→ current composer (`:1194`) restyle; placeholder + hint copy change; persona/tools become chips.
### Variation B — right work canvas (~42% width)
- `<aside class="canvas">` `--bg-2`, `--line-soft` left border, `width:42%` (transition .25s).
- Canvas head: title **"teardown.md"** + healthy mono tag **"● live draft"** + an "Open in
Artifacts" icon button (external-link glyph).
- Canvas body = `.doc`: H1 **"Q2 Competitive Teardown"**; mono docsub **"draft · 9 competitors ·
updated just now by Waggle"**; honey mono H2 section labels ("Executive summary", "Landscape");
a landscape `<table>` (Player / Memory / Local-first: Mem0 Cloud No · Letta Agent-centric Partial
· LangMem Toy-tier No · Notion AI Doc-scoped No); a trailing paragraph with a **blinking honey
type-cursor** (``, `@keyframes bl` 1s steps(2)).
- Responsive: `@media (max-width:820px)` hides the canvas (chat goes full width).
### Interactions
- Clicking an activity header toggles its steps (current StepBlocks have no grouping/toggle — new).
- Canvas type-cursor blinks on the live draft.
---
## 5. Gap table (current → design)
| # | Area | Current | Design (Variation B) | Gap / action |
|---|------|---------|----------------------|--------------|
| 1 | **Right work canvas** | **Does not exist** anywhere (`grep canvas` → only WorkspaceDesktopApp layout comments + an unrelated KG viz). Chat is single-pane. | ~42% live-drafting `teardown.md` aside with header, doc body, blinking honey cursor; thread narrows to 620px when open. | **BUILD NEW.** New `<ChatWorkCanvas>` aside as sibling of ChatApp's chat column (`:744`). Needs open/close state + a data source for the doc body (none exists — see #2). |
| 2 | **Canvas content source** | No artifact body in the stream — only tool `input.path` + opaque `result`. | Live document with sections/table that updates as the agent drafts. | **DATA GAP.** Derive from latest file-write tool block's `input.content`/`result`, OR add a stream channel / artifact fetch. Simplest PR3: show the last completed artifact's rendered content; "live drafting" cursor is cosmetic. |
| 3 | **Activity stream grouping** | Flat `StepBlock` rows inline (`StepBlock.tsx`), no card, no toggle, no header summary. | Collapsible Activity card with violet spark + "Worked across … · N steps · Ns" header + chevron; default-open on active turn. | **BUILD.** New `ActivityCard` that groups consecutive `step` (and tool) blocks; BlockRenderer must collapse a run of steps into one card. Derive "N steps · Ns" from the grouped blocks. |
| 4 | **Provenance pill in steps** | None (SSE `step` carries no source field). `EvidenceChip` primitive exists but unused in chat. | `⬡ mem://hive · provenance kept` mono `--intel` pill per step. | **DATA + UI GAP.** No backing data. PR3 can render the pill only when a step/tool exposes a source; otherwise omit (don't fabricate). Reuse `EvidenceChip` styled to `--intel`/`--intel-wash`. |
| 5 | **Model pill** | Picker button: `Cpu` icon + raw model string + chevron (`:931`). | Pill w/ healthy live dot + "Model: auto · Claude Sonnet"; honey-line hover. | **RESTYLE.** No "auto" sentinel surfaced today; show "auto ·" when model is the default/unset, plus a live dot. |
| 6 | **User bubble radius/color** | `bg-primary text-primary-foreground rounded-xl` (`:1108`). | `--surface` bubble, asymmetric `4px 14px 14px 14px`. | **RESTYLE** to warm surface + asymmetric radius. |
| 7 | **Bot meta line** | Avatar + Sparkles prefix; no "Waggle · Analyst · Claude Sonnet" meta row. | hex W avatar + meta "Waggle · Analyst · Claude Sonnet". | **ADD** meta row above bot prose (persona name + model). |
| 8 | **Approval card** | `ApprovalGate` honey/amber-ish, RiskBadge, Allow once/Always allow/Deny/Show details (`:223`). | honey-wash card, "Approve before I leave your machine", "Approve & export"/"Not now". | **RESTYLE + copy.** Keep RiskBadge + Always-allow gating; warm tokens; external-write framing copy. |
| 9 | **Composer copy + chips** | placeholder "Message Waggle... (/ for commands)"; Paperclip + Send only; no persona/tools chips, no "⏎ send · ⌘K" hint. | placeholder "Reply, or ask Waggle…"; Attach/Persona/Tools chips + mono "⏎ send · ⌘K commands" hint; honey ↑ send. | **RESTYLE + ADD** chips row + hint; new copy. |
| 10 | **Header sub / context** | persona pill + Memory chip + storage/team chips + autonomy + model (no "142 memories · 9 sources" sub, no workspace hex). | hex avatar + name + mono "workspace · 142 memories · 9 sources" + model pill + memory icon. | **RESTYLE.** Add hex avatar + memory/sources sub (data from workspace context). Autonomy/storage/team chips not in design header — relocate or drop into ⌘K/overflow. |
| 11 | **Warm tokens** | Hive DS classes (`bg-primary`, `text-emerald-400`, `bg-secondary`, `border-border`, etc.). | Warm graphite/paper tokens (`--surface`, `--honey`, `--intel`, `--bg-2`, `--line-soft`). | **MIGRATE** color classes to the PR1 warm tokens (already in `index.css`). |
| 12 | **Activity default-open on active turn** | StepBlocks always visible (no collapse). | Activity card default-open on the active turn, collapsed on prior turns. | **BEHAVIOR.** Track which turn is active; collapse historical activity cards. |
**Not gaps (already correct / reusable):** SSE streaming + block reduction (`useChat`), tool cards,
artifact card + Open-in-Files, feedback buttons, slash menu, pins, persona picker, autonomy toggle,
keep-alive across navigation, the `chatSlot` seam, `EvidenceChip` provenance primitive, warm tokens
in `index.css`.
---
## 6. Reuse + build
### Reuse as-is
- **`useChat.ts`** — the entire stream/reduction contract is design-compatible. The Activity card +
canvas are pure RENDER concerns over the existing `blocks[]`; no hook change required for a basic
build (canvas content derivation may want a small selector helper).
- **`ChatHost.tsx` keep-alive + `ChatSlot` seam** — unchanged. Canvas state lives inside the
kept-alive ChatApp subtree, so it survives navigation for free.
- **`ArtifactBlock.tsx`** — keep the inline card AND reuse its `iconFor`/`isArtifactBlock`/path logic
to feed the canvas (open the latest artifact in the canvas instead of/in addition to "Open in Files").
- **`EvidenceChip`** (`components/ui/evidence-chip.tsx`) — base for the `⬡ provenance` pill (restyle
to `--intel`).
- **`ApprovalGate`** + `RiskBadge` + `canAlwaysAllow` — keep logic, restyle to honey-wash.
- **`AutonomyToggle`**, slash menu, pins, `FeedbackButtons`, `WorkspaceBriefing` — keep.
- **`chat-header-layout.ts`** — keep the overflow decision (chips not in the design header fold here).
### Build new
- **`ChatWorkCanvas` aside** — append as a sibling of ChatApp's chat column inside ChatApp root
(`ChatApp.tsx:697` flex root; chat column `:744`). Props: `open`, `artifact` (path + body), `onClose`.
Width 42%; thread column `max-width` drops to ~620px when open. Honor `@media (max-width:820px)` →
hide canvas. This keeps it within the keep-alive portal subtree (do NOT add it as a `WorkspaceDesktopApp`
pane — that breaks the `chatSlot` test contract and lives outside keep-alive).
- **`ActivityCard`** — a grouping wrapper in `chat-blocks`: BlockRenderer collapses a consecutive run
of `step` (+ optionally tool) blocks into one collapsible card with the "Worked across … · N steps
· Ns" header (derive count from grouped steps; duration from summed/last tool durations if present),
violet spark icon, chevron toggle, default-open when `isStreaming`/active turn.
- **Canvas content selector** — derive the canvas doc from the most recent completed
`write_file`/`edit_file` block (path + `input.content`/`result`); render markdown via the app's
existing markdown path (TextBlock already renders prose — reuse its renderer for the doc body).
The blinking type-cursor is cosmetic, shown while `isStreaming`.
- **Provenance data** — render the step provenance pill ONLY when the data exists; do not fabricate.
Flag to backend owners that the SSE `step` payload needs a structured `source` field to fully match
the design (`docs/redesign-warm-hive/pr3-recon` follow-up).
### Is there already an artifact-canvas / side panel? — NO.
Confirmed by grep across `apps/web/src` for `canvas` / `work canvas` / `artifact-canvas`: the only
matches are `WorkspaceDesktopApp.tsx` layout comments (an unrelated "right context panel" on the
Overview tab, NOT a chat canvas) and a knowledge-graph canvas in Memory. The chat surface
(`ChatApp.tsx`) is strictly single-pane (`flex h-full` with one `flex-col flex-1` column, `:697`/`:744`).
**The split work canvas must be built from scratch.**
### Risk notes
- Canvas MUST live inside the kept-alive subtree (ChatApp), else its state resets on every nav
(`ChatHost.tsx` keep-alive only covers the portal subtree).
- Do not change `WorkspaceDesktopApp`'s `chatSlot`/`activeTab` props — `p1a-chat-state.test.tsx`
seam (a)/(b) pins them.
- Keep `chat-artifact-block` / `chat-artifact-open` test IDs + Created/Updated copy + `isArtifactBlock`
predicate — `chat-artifact-block.test.tsx` pins them; the canvas is additive.

View File

@@ -0,0 +1,293 @@
# PR3 Recon — SCREEN 01 · Home / Cockpit (ship Variation A "Editorial")
Maps the **current** Home/Cockpit implementation against the warm-Hive design spec for
SCREEN 01. Scope: what renders today, the live data contract, the test contract that
must keep passing, the Editorial target with exact copy, the gap list, and the
reuse/build plan.
Primary files:
- Current UI: `apps/web/src/components/os/apps/HomeCockpit.tsx` (the real Home; **this is the one PR3 rebuilds**)
- Route wrapper: `apps/web/src/routes/HomeRoute.tsx`
- `CockpitApp.tsx` (`apps/web/src/components/os/apps/CockpitApp.tsx`) is a **separate** system-health dashboard (Mission-Control-style), **NOT** the Home surface — see note in §1.
- Server contract: `packages/server/src/local/routes/home.ts`
- FE types: `apps/web/src/lib/types.ts:266-331`
- Test contract: `apps/web/src/test/p2-home-desktop.test.tsx`
- Design: `docs/design_handoff_waggle_app/SCREENS.md` §"01 · Home / Cockpit" + `design-files/screens/home.html` (view `#view-a`)
---
## 1. Current structure — what `HomeCockpit.tsx` renders today (section by section)
`HomeCockpit` is the default `/home` surface (`HomeRoute.tsx:13`). It is a single
scrolling column, `p-6 max-w-3xl mx-auto` on the sub-states and `max-w-4xl mx-auto` on
the normal render (`HomeCockpit.tsx:580`). Data loads in `load()`
(`HomeCockpit.tsx:467-497`): `adapter.getHomeBriefing()` then a best-effort
`adapter.getHomeOvernight()`; deferred until `useService().connecting` settles
(cold-load 401 race guard, `:499-507`).
**Render states (root, `:509-650`):**
1. **Loading**`CockpitSkeleton` (`:84-98`), `data-testid="home-cockpit-loading"`.
2. **Permission denied (403)**`:514-527`, `data-testid="home-cockpit-permission-denied"`.
3. **Load error / offline**`:531-554`, `data-testid="home-cockpit-error"` + Retry.
4. **First-run empty**`FirstRunEmpty` (`:101-125`), `data-testid="home-cockpit-empty"`, "Create your first workspace" CTA.
5. **Normal**`:579-649`, `data-testid="home-cockpit"`.
**Normal render, top to bottom:**
- **`GreetingHeader`** (`:136-159`, rendered `:581`): H1 = `briefing.greeting` (`text-2xl font-display font-bold`), sub = `formatBriefingDate(briefing.date)` rendered as a human date (`:130-134`). Right side: optional offline "Local only" pill + a static `Ctrl+K` chip (`:153-156`). **No mono date row, no live dot, no streak chip.**
- **Attention banner** (`:583-599`, `data-testid="home-cockpit-attention-banner"`): only when `overnight.failures.length > 0`. Honey-tinted `role="alert"`.
- **J08 review banner** (`:601-625`, `data-testid="home-cockpit-review-banner"`): only when `briefing.needsReviewCount > 0`; "N imported memories need your review" + Review CTA that dispatches `waggle:open-app {appId:'memory', filter:'unreviewed'}` (`:573-577`).
- **`RecentWorkspacesPanel`** (`:162-227`, rendered `:627-632`): section head **"You were working on"** (`:174-176`), 2-col grid (`sm:grid-cols-2`). Each card: name + `group` chip, optional 2-line `summary`, `lastActive` relative time + `pendingCount` "pending" warning, a **"Continue →"** button (`:208-215`), and a `WorkspaceActionsMenu` kebab (G1 rename/archive/delete). Card body click → `onOpenDesktop`. Returns null when 0 cards.
- **`OvernightPanel`** (`:230-288`, rendered `:637`, suppressed when offline): section head **"Overnight"**, a 3-counter grid (Memories consolidated / Artifacts created / Automations completed), then a failures block (deep-links each failure to the Automation Center logs, `:271-280`). Hidden when no activity.
- **`SuggestedActionsPanel`** (`:321-343`, rendered `:639`): section head **"Suggested next actions"**, wrap of pill buttons from `briefing.suggestedActions`, each → `onContinue(a.workspaceId, a.sessionId)`.
- **`UpNextPanel`** (`:291-318`, rendered `:643`): section head **"Up next"**, list of up to 6 items (event/task/schedule icons). Returns null when 0 items.
- **`QuickCapturePanel`** (`:346-434`, rendered `:645`): "Quick capture" — a 4-kind segmented selector (note/task/link/file) + text input + Capture button → `adapter.quickCapture()`. **Not in the Editorial design.**
- **`ActiveModelsTile`** (`:437-448`, rendered `:647`): tiny "Active models: …" chips row when `briefing.activeModels` present.
**Styling today:** uses shadcn semantic classes (`bg-secondary/30`, `border-border/30`,
`text-muted-foreground`) and inline `var(--sem-intelligence)` / `--sem-attention` /
`--sem-work` / `--sem-healthy` / `--sem-risk` semantic tokens. Layout is **dense and
utilitarian** (10-13px text, `rounded-xl`, compact panels) — the opposite of the
Editorial spec's calm, large-type, story-led 920px column.
> **Note on `CockpitApp.tsx`:** despite the name, this is the system-health/ops
> dashboard (System Health, Cost, Memory Weaver, Cron, Connectors, Audit Trail,
> ComplianceDashboard) on a 30s refresh. It is **not** the Home surface and **not in
> scope for SCREEN 01** — it corresponds to "Mission Control" (SCREENS §16) /
> "surfaces" (§08). Listed in the task only to disambiguate; PR3 Home work happens
> entirely in `HomeCockpit.tsx`.
---
## 2. Data contract — `HomeBriefing` shape + feeding routes (real vs mocked)
### Types (FE mirror `apps/web/src/lib/types.ts:266-331`; server `home.ts:43-95`)
```ts
interface HomeBriefing {
greeting: string; // server builds via buildTimeAwareGreeting + personalizeGreeting (home.ts:401-407)
userName?: string; // from IdentityLayer on personal mind (home.ts:261-270); optional
date: string; // now.toISOString() (home.ts:412) — RAW ISO, FE must humanize
recentWorkspaces: RecentWorkspaceCard[];
suggestedActions: SuggestedAction[];
upNext: UpNextItem[];
activeModels?: string[]; // OPTIONAL — server briefing NEVER populates it (no producer in home.ts)
isFirstRun: boolean; // ranked.length === 0 (home.ts:418)
needsReviewCount?: number; // J08 unreviewed personal-mind frames (home.ts:382-389)
}
interface RecentWorkspaceCard {
id; name; group; // group = workspace.group string
summary?; // state.recentDecisions[0].content (home.ts:314-316)
lastActive: string; // ISO; ranking timestamp
pendingCount: number; // state.pending.length + state.blocked.length
continueSessionId?; // NOTE: server never sets it (home.ts card builder omits) → Continue has no session seed
}
interface SuggestedAction { label; workspaceId; sessionId?; kind; } // kind always 'next-action' from server
interface UpNextItem { id; label; workspaceId?; at?; kind: 'event'|'task'|'schedule'; }
// server only ever emits kind:'schedule' from cron; 'event'/'task' are type-only
// server NEVER sets `at` → UpNextPanel's time column is always empty today
interface OvernightSummary { // GET /api/home/overnight (separate call)
consolidated; artifactsCreated; automationsCompleted;
failures: OvernightFailure[]; // {id,label,automationId?,error,at} from cron execution history
window?: { from; to };
}
```
### Routes feeding Home
| Route | Method | Adapter | Source / realness |
|---|---|---|---|
| `/api/home/briefing` | GET | `adapter.getHomeBriefing()` (`adapter.ts:612-615`) | **Real.** Server-side cross-workspace fan-out (`home.ts:257-423`); personal-only (A2), excludes team/archived. Reuses `buildWorkspaceState`, `buildTimeAwareGreeting`, `buildUpcomingSchedules`, `IdentityLayer`, cron store, audit DB. |
| `/api/home/overnight` | GET | `adapter.getHomeOvernight(since?)` (`adapter.ts:617-621`) | **Real.** Audit-event counts (`memory_write` → consolidated; file-write `tool_call` → artifacts) + cron execution history → automations/failures (`home.ts:425-503`). |
| `/api/quick-capture` | POST | `adapter.quickCapture()` (`adapter.ts:623-627`) | **Real.** Used by `QuickCapturePanel` (not in Editorial layout). |
### Real vs mocked / missing, per design need
- **REAL:** greeting, userName, date (ISO), recentWorkspaces (name/group/summary/lastActive/pendingCount), suggestedActions, upNext (schedule labels only), overnight counters + failures, needsReviewCount.
- **NOT produced by the server (treat as absent):**
- `continueSessionId` — never set → "Continue" can't target a session (already a known gap, `HomeRoute.tsx:16-19`).
- `UpNextItem.at` (time) and `event`/`task` kinds — never set; only `schedule` from cron.
- `activeModels` — typed-optional, **no producer**; tile never shows from the real route.
- **NO DATA ANYWHERE (must mock for the Editorial design):**
- **🔥 streak chip** — `grep` across `packages/server/src` and `apps/web/src` finds **no** `streak` field. Habit-loop spec (SCREENS §15) says the streak lives on Home but it is unimplemented. **Needs-mock** (or a follow-up backend field).
- **Overnight "story" sentence** + **run chips with labels** ("Teardown drafted · 9 competitors") — server gives raw counts, not a composed narrative or per-run labels. The narrative must be **composed client-side** from `OvernightSummary` (counts + failures), and the first "Teardown drafted" run chip has **no backing field** (would need the top suggestedAction/workspace summary as a proxy) → **partial mock**.
- **Workspace card hex avatar glyph / "agent live" badge** — no `glyph`/`live` field; derive glyph from `name[0]`, and there is no per-card live-agent flag → **derive / mock badge**.
---
## 3. Test contract — `apps/web/src/test/p2-home-desktop.test.tsx`
These assertions constrain the rebuild (the `describe('HomeCockpit (P2)')` block,
`:73-129`). All must keep passing. The harness renders `HomeCockpit` bare with mocked
`adapter`, `useService` (`{connecting:false, connected:true}`), offline=false, and a
stub `ShellContext` (`:38-46`).
Required behaviors / DOM contract:
1. **Root testid** — after load, `screen.getByTestId('home-cockpit')` must exist (`:81`). **Keep `data-testid="home-cockpit"` on the normal-render root.**
2. **J08 review banner** (`:84-98`) — with `needsReviewCount:3`, `data-testid="home-cockpit-review-banner"` renders text containing **"3 imported memories need your review"**, and `data-testid="home-cockpit-review-cta"` click dispatches exactly `waggle:open-app` with detail `{ appId:'memory', filter:'unreviewed' }`. **Keep the banner, its copy pattern, the CTA testid, and the event payload.**
3. **Banner omitted at 0/undefined** (`:100-103`) — no `home-cockpit-review-banner` when `needsReviewCount` is undefined.
4. **Up next omitted when empty** (`:105-111`) — no `home-cockpit-upnext` when `upNext` is `[]` or undefined.
5. **Up next present with items** (`:113-118`) — `home-cockpit-upnext` contains the item label ("Weekly digest") when ≥1 item.
6. **Human date** (`:120-128`) — the raw ISO (`2026-06-11T07:42:13.512Z`) must **not** appear; the date must render via `new Date(iso).toLocaleDateString(undefined, {weekday:'long', month:'long', day:'numeric'})`. **Keep `formatBriefingDate` semantics; the mono date row must humanize, not print ISO.**
The mocked briefing factory (`:57-67`) defines the minimum shape the component must
tolerate: `{greeting, userName:'Marko', date, recentWorkspaces:[], suggestedActions:[],
upNext:[], isFirstRun:false, needsReviewCount:0}`. The Editorial rebuild must still
render `home-cockpit` with all-empty arrays (no crash on empty overnight/workspaces).
**Implication:** PR3 may freely restyle and re-lay-out, but must preserve these
testids + behaviors: `home-cockpit`, `home-cockpit-review-banner`,
`home-cockpit-review-cta` (+ event payload), `home-cockpit-upnext` (present/absent
rules), and human-date rendering. The `up next` empty/present rule means the Editorial
"Up next" (if kept) stays conditional. Other testids (`home-cockpit-overnight`,
`home-cockpit-suggested`, `home-cockpit-continue-*`, quickcapture testids) are **not**
asserted in this file — they can be renamed/removed if their features are reshaped.
---
## 4. Design spec (Editorial / Variation A) — target sections + exact copy
From `SCREENS.md` §01 (ship = Variation A) and `home.html` `#view-a` (`:157-186`).
**Layout:** single centered column, **max-width 920px, 46px top padding**
(`home.html:31`). Sections top to bottom:
### 4.1 Greeting (`home.html:158-161`)
- **Mono date row** (`--honey`, uppercase, `.1em` tracking) with a **live dot**
(`.dot-live`, `--healthy`) on the left, and a **right-aligned 🔥 streak chip**
(pill, `--honey-wash` bg, `--honey-line` border, `--honey` text).
- **H1** Hanken 600, `clamp(34px,5vw,52px)`, line-height 1.02; the keyword **"ahead"**
is honey (`em`, not italic; `--honey`).
- **Exact copy:** date `"Friday · June 14 · 8:42"` (compose from `briefing.date`);
streak `"🔥 12-day streak"`; H1 = **"Good morning, Mara."** / **"You're _ahead_ of yesterday."**
- Maps to: greeting → `briefing.greeting` (real, already personalized server-side; the design's literal "Good morning, Mara." is `buildTimeAwareGreeting`'s output). "You're ahead of yesterday" second line + the honey "ahead" → **needs-mock / composed** (no "ahead vs yesterday" signal exists). Date → **real** (`briefing.date`, reformatted with time). Streak → **needs-mock**.
### 4.2 Overnight hero card (`home.html:163-172`)
- Radius `--r-xl`, gradient `--surface → --surface-2`, `--shadow`, soft honey radial
glow top-right (`::after`). Mono eyebrow with `--intel` dot.
- **Eyebrow (exact):** "While you slept".
- **Story line (exact):** Hanken 600, clamp 21→28px, max-width 30ch; honey key numbers (`b`):
**"Waggle finished the _Q2 competitor teardown_, folded _14 new memories_ into the hive, and ran into _one snag_ worth a look."**
- **Run chips (exact, status dot + label):**
- `"Teardown drafted · 9 competitors"` (dot `--healthy`)
- `"14 memories consolidated"` (dot `--intel`)
- `"2 artifacts created"` (dot `--work`)
- `"1 export failed"` (dot `--risk`)
- Maps to: counters are **real** (`overnight.consolidated`, `.artifactsCreated`, `failures.length`). The **composed sentence** + the **"Teardown drafted · 9 competitors"** label are **needs-mock/composed** (no narrative producer; no per-run "9 competitors" field). Build the sentence from counts + failures client-side; degrade gracefully when overnight is null/empty (must still render `home-cockpit`).
### 4.3 "Pick up where you left off" (`home.html:174-175, 287-299`)
- Section head = mono uppercase `--text-dim` with trailing hairline rule (`.sec-h`).
- 2-col grid (`.ws-grid`) of `.ws-card`: **hex avatar** (honey gradient, glyph =
first letter), **title** (16px, 650), **time** (mono `--text-dim`, e.g. "2h ago"),
**summary** (13.5px `--text-muted`), footer = **"Continue →"** (honey) + optional
**status badge** (`pend` honey-wash "3 to review" / `live` healthy-wash "agent live").
Hover: honey border + `translateY(-3px)` + `--shadow`.
- Maps to: **real**`recentWorkspaces[].name`, `.summary`, `formatRelative(lastActive)`, `.pendingCount` (→ "N to review"/"N pending"). Hex glyph = derive from `name[0]`. "agent live" badge → **no field, mock/omit**. **Section head copy must change** "You were working on" → **"Pick up where you left off"**.
### 4.4 "Waggle suggests" (`home.html:177-178, 301-306`)
- Section head "Waggle suggests". Stacked **`.move` rows**: tinted icon tile
(`--*-wash` bg), **title** (14.5px 600) + **sub** (12.5px `--text-muted`), arrow
that slides on hover.
- **Exact sample copy (design data):** "Review the competitor teardown" / "9
competitors · ready since 02:40 · ~6 min read"; "Send the board update" / "Draft
built from this week's work in Q2 Board Deck"; "Confirm 3 imported memories" / "From
Tuesday's pricing call — Waggle wants your sign-off".
- Maps to: title → **real** `suggestedActions[].label`. The **sub-line** has no
backing field (server emits label only, `kind:'next-action'`) → **needs-mock/derive**
(e.g. workspace name + relative time). Section head copy "Suggested next actions" →
**"Waggle suggests"**. Rows become **stacked** (not pills).
### 4.5 Ask bar (`home.html:180-185`)
- Full-width pill (`.ask`), honey **"+"** icon left, text input
(placeholder **"Start something new — "draft the board update from this week's
work"…""**), mono **"⌘K"** hint, honey round **send** button ("→"). Focus → honey
border + glow.
- Maps to: **net-new on Home.** No current "ask bar" on HomeCockpit. Wire send →
`onContinue`/new-chat or open ⌘K (PR2 `CommandCenter`). This **replaces** the
current QuickCapturePanel as the primary input affordance. The "+" can keep a
quick-capture role, but the design's primary intent is "start a task".
### 4.6 Not in Editorial (drop or relocate)
- **Quick capture segmented panel** — replaced by the ask bar; the
`adapter.quickCapture` API can be kept behind the "+" icon or dropped from Home.
- **Active models tile** — not in Editorial (and route never populates it). Drop.
- **Attention/review banners** — not literally in the Editorial mock, but the **J08
review banner is test-locked** (keep it; can be styled as a run-chip-adjacent
attention row or kept above the hero). The overnight-failure attention banner maps
naturally onto the "1 export failed" run chip + (optionally) Variation C's risk
alert pattern.
---
## 5. Gap list
| Design section | Current state | Gap | Data available? | Severity |
|---|---|---|---|---|
| 920px centered column, 46px top pad | `max-w-4xl mx-auto p-6` dense | Re-layout to 920px / `pt-[46px]`, larger type scale | n/a (layout) | MED |
| Mono date row + live dot | Plain `text-sm` sub under H1, no dot | Add mono uppercase honey date row + `.dot-live` | date = real; format with weekday+time | MED |
| 🔥 streak chip (right pill) | Absent | Add streak chip | **No data** — needs-mock or new backend field | HIGH |
| H1 with honey "ahead" keyword | H1 = raw greeting, no honey span | Two-line H1; honey-span a keyword | greeting real; "ahead of yesterday" composed | MED |
| Overnight **hero** card (gradient + glow) | `OvernightPanel` = 3 plain counters + failures, suppressed offline, hidden when empty | Rebuild as hero card w/ eyebrow + story + run chips; must still render when empty/null | counts real; **story + run-chip labels composed/mock** | HIGH |
| Run chips (4, status-dotted) | Counter tiles only | Render chips w/ status dots; map counts | 3 of 4 real (consolidated/artifacts/failed); "Teardown drafted · 9 competitors" mock | MED |
| "Pick up where you left off" head | "You were working on" | Copy + `.sec-h` hairline style | n/a (copy) | LOW |
| Workspace cards: hex avatar + 16px title + Continue→ + badge | Cards w/ name+group chip, Continue button, kebab | Add hex avatar (glyph from name), restyle; keep Continue + kebab | name/summary/time/pending real; glyph derived; "agent live" mock/omit | MED |
| "Waggle suggests" stacked rows w/ sub-line | "Suggested next actions" pills, label-only | Copy + restyle to `.move` rows; add sub-line | label real; **sub-line composed/mock** | MED |
| Ask bar (pill, +/⌘K/send) | None on Home (QuickCapture panel instead) | Net-new ask bar; wire to chat/⌘K | n/a (action wiring) | MED |
| `continueSessionId` for targeted resume | Plumbed but server omits | Continue lands at chat root, not a session | **Not produced** by `home.ts` | LOW |
| Quick capture panel | Present | Remove/relocate behind "+" | n/a | LOW |
| Active models tile | Present, never populated | Remove from Home | route never sets `activeModels` | LOW |
| J08 review banner (test-locked) | Present + correct | Preserve testids/copy/event while restyling | needsReviewCount real | (keep) |
| Overnight-failure attention banner | Present | Reconcile with "1 export failed" run chip | failures real | LOW |
**Gap count (distinct design-vs-impl gaps): 11** (excludes the two "keep as-is"
test-locked rows and the layout-only re-layout row counted once).
---
## 6. Reuse + build
### 6.1 Reuse (already shipped in PR1 — do NOT recreate)
All warm-Hive tokens the design references already exist in
`apps/web/src/index.css` and `apps/web/src/waggle-theme.css`:
- **Surfaces/lines/text:** `--bg --bg-2 --surface --surface-2 --line --line-soft --line-strong --text-2 --text-muted --text-dim` (`index.css:149-153`).
- **Honey + washes:** `--honey --honey-bright --honey-deep --honey-wash --honey-line --honey-glow` (`index.css:99,155-159`).
- **Semantics + washes:** `--work --intel --healthy --attention --risk` + each `*-wash` (`index.css:161-169`).
- **Radii:** `--r:12 --r-lg:18 --r-xl:26` (`index.css:177-179`).
- **Shadows:** `--shadow --shadow-sm --shadow-lg --shadow-pop` (`index.css:171-174`).
- **Fonts:** Tailwind `font-display` / `font-sans` → Hanken Grotesk, `font-mono` → JetBrains Mono (`tailwind.config.ts:87-90`). Hanken/JetBrains `@import` already in `index.css`.
- **Utilities:** `.hex` clip-path (`waggle-theme.css:140`), `.comb` honeycomb bg (`:143-146`), `.dot-live` + `@keyframes breathe` (`index.css:428-435`), `.kbd`/mono helper (`waggle-theme.css:161`).
- **Existing component primitives:** `WorkspaceActionsMenu` (kebab CRUD — keep on cards), `formatRelative` / `formatBriefingDate` (`HomeCockpit.tsx:69-81,130-134`), the adapter trio (`getHomeBriefing`/`getHomeOvernight`/`quickCapture`), `useOfflineStatus`, `useService().connecting` race guard, shadcn `ui/` (button/input/card available if wanted), lucide icons, `waggle:open-app` deep-link shim. ⌘K lives in `CommandCenter.tsx` (PR2) — wire the ask-bar "⌘K" hint / send to it.
### 6.2 Net-new (build for Editorial)
- **Mono date row** with live dot + **🔥 streak chip** (streak value mocked/constant until a backend field exists — flag in code).
- **Honey-keyword H1** (two-line; second line composed; honey-span helper).
- **Overnight hero card** — a new presentational component: eyebrow "While you slept" + composed story sentence (from `OvernightSummary` counts + failures) + 4 run chips. Must render gracefully when overnight is null/empty (no crash; the test renders with `getHomeOvernight → null`).
- **Run-chip** component (status dot + label).
- **Hex-avatar workspace card** restyle (`.ws-card` look: hex glyph, hover lift, "Continue →", badge).
- **`.move` suggestion row** restyle (icon tile + title + sub + sliding arrow); compose sub-line from workspace/time.
- **Ask bar** (pill input + honey "+" + "⌘K" hint + honey send) — replaces QuickCapture as the primary input; wire send to chat/⌘K.
### 6.3 Recommended build approach
1. **Keep `HomeCockpit.tsx` as the component** (route + props + load() + all five render
states + the test-locked testids/banners stay). Restyle/replace only the **normal
render body** (`:579-649`) and the sub-state shells to the warm Editorial look.
2. **Container:** swap `max-w-4xl mx-auto p-6` → a centered **`max-w-[920px] mx-auto px-8 pt-[46px] pb-20`** wrapper; optionally drop a `.comb` honeycomb layer behind it.
3. **Extract small presentational subcomponents** within the file (or co-located
`home/` dir, per CLAUDE.md "many small files"): `GreetingHeader` (rework),
`OvernightHero` (new, replaces `OvernightPanel`), `RunChip`, `WorkspaceCard`
(hex restyle of the existing card), `SuggestRow` (restyle of `SuggestedActionsPanel`),
`AskBar` (new). Keep `UpNextPanel` conditional (test 4/5) or fold it into the dash —
simplest is to **retain it** below suggestions to satisfy the present/absent tests.
4. **Compose, don't fetch:** the overnight **story sentence** and **run-chip labels**
are derived client-side from the existing `OvernightSummary` + top `suggestedAction`
/workspace summary. Mark the **streak** and **"ahead of yesterday"** as explicit
`// TODO(backend): no data source yet` constants so the mock is honest.
5. **Preserve behavior:** J08 banner (copy + `home-cockpit-review-cta` event payload),
human-date, `home-cockpit` root testid, `home-cockpit-upnext` present/absent rule.
6. **Dark-first** (per BUILD-PLAN §6); verify light via the existing ratchet
(`light-mode-tokens.test.ts`). Gates: `tsc -p apps/web/tsconfig.app.json` 0,
`npm run test` (FE) green incl. `p2-home-desktop.test.tsx`, lint clean.
> Honesty flags for the rebuild: (a) **streak** has no data anywhere — it is a pure
> mock until a backend field lands; (b) the overnight **narrative + "9 competitors"**
> run-chip and the suggestion **sub-lines** are composed/mocked, not server-provided;
> (c) `continueSessionId` is still omitted server-side so "Continue" lands at chat root.

View File

@@ -0,0 +1,200 @@
# PR3 Recon — Warm-Hive primitives & global patterns
> One shared vocabulary for the three PR3 screen builders (Home / Chat / Workspace).
> Post-PR1 inventory of what already exists in `apps/web` + the design's global
> patterns extracted from `docs/design_handoff_waggle_app/`. **Use the token names
> and components below — do NOT invent ad-hoc colors or re-build atoms that exist.**
Sources of truth:
- Warm tokens: `apps/web/src/index.css` (`@layer base`) + `apps/web/src/waggle-theme.css`
(note: file is at `src/`, NOT `src/styles/`).
- Tailwind utilities: `apps/web/tailwind.config.ts`
- Design global: `docs/design_handoff_waggle_app/README.md` §6/§7 + `SCREENS.md` 01/02/03
---
## 1. Warm token cheatsheet
Dark is the default `:root`; `:root[data-theme="light"]` overrides every color. Radii /
type / shadow tokens are theme-independent. Prefer the **named warm token** (`var(--x)`)
for screen chrome; the **shadcn HSL utility** (`bg-card`, `text-foreground`) for any
component that already derives from the HSL core.
### Surfaces (`apps/web/src/index.css:20-44, 149-153`)
| Token | Dark hex | Tailwind / usage |
|---|---|---|
| `--bg` | `#14110b` | `bg-background` — app shell |
| `--bg-2` | `#1a160f` | rails, recessed panels (sidebar uses `bg-[var(--bg-2)]`) |
| `--surface` *(= `--card`)* | `#1f1a12` | `bg-card` — cards |
| `--surface-2` *(= `--secondary`)* | `#272117` | `bg-secondary` — hover/insets |
| `--surface-3` *(= `--muted`)* | `#322a1d` | `bg-muted` — chips, icon tiles |
| `--line` | `#38301f` | `border-border` ≈ — default border |
| `--line-soft` | `#2a2417` | subtle dividers (card border in design = `--line-soft`) |
| `--line-strong` | `#4a4030` | emphasized border / kbd / scrollbar |
### Text (`index.css:21,36,89,153` + `waggle-theme.css:24-29`)
| Token | Tailwind / alias |
|---|---|
| `--text` *(= `--foreground`)* `#f6f1e4` | `text-foreground` |
| `--text-2` `#c8bfa9` | secondary copy |
| `--text-muted` *(= `--muted-foreground`)* `#948a73` | `text-muted-foreground` |
| `--text-dim` *(= `--hive-500` `#6b6250`)* | labels, meta, mono section labels |
### Honey — the ONE accent (`index.css:91-100, 155-159`)
| Token | Dark hex | Notes |
|---|---|---|
| `--honey` *(= `--honey-500`, `--primary`)* | `#e9a52c` | `bg-primary` / `text-honey-500`; primary btn, active nav, key numbers, focus |
| `--honey-bright` *(= `--honey-400`)* | `#f6c45a` | gradient top of hex avatar |
| `--honey-deep` *(= `--honey-600`)* | `#c07e16` | gradient bottom of hex avatar |
| `--honey-wash` | `rgba(233,165,44,.10)` | tinted fills (active nav bg, streak/honey chips) |
| `--honey-line` | `rgba(233,165,44,.28)` | tinted borders (card hover, chip border) |
| `--honey-glow` | `rgba(233,165,44,.12)` | soft glow / `--shadow-honey` |
| **On-honey ink** | `#1a1407` | text/icon color on any honey fill (matches `--primary-foreground`) |
### Semantics — desaturated, STATUS ONLY (never decoration) (`index.css:161-169`)
| Token | Dark / Light | Meaning | Wash |
|---|---|---|---|
| `--work` | `#7aa6d6` / `#3f72b0` | tasks, workspaces (blue) | `--work-wash` |
| `--intel` | `#b196dd` / `#7d57b8` | memory / intelligence / **provenance** (violet) | `--intel-wash` |
| `--healthy` | `#6cb78c` / `#3c8a5f` | complete / healthy (sage) | `--healthy-wash` |
| `--attention` | `#e9a52c` / `#b57d12` | attention / automation (= honey) | (use `--honey-wash`) |
| `--risk` | `#db8068` / `#c0573c` | risk / failure (terracotta) | `--risk-wash` |
> There is also a parallel `--sem-*` alias set (`--sem-work/-intelligence/-healthy/-attention/-risk`,
> `index.css:110-114`) wired into the EXISTING primitives (StatusBadge, ConfidenceBadge).
> The vivid `--status-*` / `bg-status-*` tokens (`#34d399` etc.) are the legacy palette —
> **prefer the desaturated `--work/--intel/...` (or `--sem-*`) for warm-Hive screens.**
### Shadows / radii / fonts (`index.css:171-183`)
| Token | Value |
|---|---|
| `--shadow-sm` / `--shadow` / `--shadow-lg` / `--shadow-pop` | card → overlay elevation |
| `--shadow-honey` | `0 0 0 1px rgba(233,165,44,.25), 0 8px 30px -10px rgba(233,165,44,.35)` |
| `--r-sm` 8px · `--r` 12px · `--r-lg` 18px · `--r-xl` 26px · pills `999px` | `rounded-sm/md/lg/xl` map to the shadcn radius scale, NOT these raw px — use `rounded-[18px]`/`rounded-[var(--r-lg)]` for design-exact cards |
| `--sans` Hanken Grotesk · `--mono` JetBrains Mono | `font-sans` / `font-mono`; `--serif` = `--sans` (no book-serif) |
> Caveat for builders: Tailwind `rounded-lg` = `--radius` (0.75rem/12px = design `--r`),
> NOT `--r-lg` (18px). Design **cards** want 18px → use `rounded-[18px]`. Design hero
> card wants 26px → `rounded-[26px]`. Pills → `rounded-full`.
### Utility classes already shipped (`index.css` + `waggle-theme.css`)
- `.hex` (`waggle-theme.css:140`) — hex clip-path for brandmark/avatars/tiles.
- `.hex-avatar` (`index.css:377`) — same clip path (duplicate; either works).
- `.comb` (`waggle-theme.css:143`) + `.honeycomb-bg` (`index.css:369`) — subtle hex mesh bg.
- `.dot-live``@keyframes breathe` 2.4s (`index.css:429-435`) — live/active status dot.
- `.heartbeat` (2s), `.honey-pulse`, `.float`, `.hex-cursor` (streaming type cursor),
`.token-stream`, `.send-flash`, `.card-enter`, `.hex-spin` — all in `index.css:408-503`
and mirrored as Tailwind `animate-*` in `tailwind.config.ts:107-156`.
- `.pill` (`waggle-theme.css:151`) — status/filter chip base.
- `.kbd` (`waggle-theme.css:160`) — keyboard hint chip.
- `.glass` / `.glass-strong` / `.glow-primary` / `.text-glow` (`index.css:334-358`).
- `.waggle-card-lift` (`waggle-theme.css:112`) — hover `translateY(-2px)` + honey border
(matches design card hover). `.waggle-interactive`, `.waggle-nav-hover`, `.waggle-press`.
- Focus: global `:focus-visible { outline: 2px solid var(--honey-500); offset 2px }`
(`index.css:389`). Selection = honey @ .28 (`index.css:383`).
---
## 2. Existing reusable components (REUSE — do not rebuild)
| Component | Path | Props | Reuse for which PR3 screen |
|---|---|---|---|
| `ConfidenceBadge` | `apps/web/src/components/ui/confidence-badge.tsx:22` | `value?: number; compact?: boolean; className?` — 0-100 → High/Med/Low band via `--sem-*`, never color-only | Workspace "What Waggle knows" fact rows; Chat memory-write steps |
| `EvidenceChip` | `apps/web/src/components/ui/evidence-chip.tsx:16` | `label: string; title?; onClick?; className?` — the inline provenance pill promoted to a primitive | Chat activity-stream provenance pills; Workspace fact/artifact `⬡ source · when` |
| `EvidencePanel` | `apps/web/src/components/ui/evidence-panel.tsx:18` | `source?; sourceId?; sourceUrl?; evidence?: string[]; className?` — "Provenance & evidence" block of chips | Workspace memory detail; Chat memory-write detail |
| `StatusBadge` | `apps/web/src/components/ui/status-badge.tsx:31` | `tone: 'healthy'\|'attention'\|'risk'\|'info'\|'neutral'; label: string; icon?; className?` — always-labeled `--sem-*` pill | Home run-chip status dots, Workspace status card, all status pills |
| `ApprovalModal` | `apps/web/src/components/ui/approval-modal.tsx:60` | `request: ApprovalRequest\|null; approveLabel?; busy?; onApprove; onCancel` (`ApprovalRequest = {action, scope[], riskLevel, approvalClass?, trustSource?}`) | Chat **inline approval card** is a different surface — but reuse this modal for the same flow + share `RISK_LABELS`/`risk-display.tsx` |
| `DetailDrawer` | `apps/web/src/components/ui/detail-drawer.tsx:22` | `open; onOpenChange; title; subtitle?; headerExtra?; footer?; children; className?` — right sheet for object detail | Workspace fact/artifact detail; Chat artifact detail |
| `BuilderStepper` | `apps/web/src/components/ui/stepper.tsx:29` | `steps: BuilderStep[]; ...` body-portaled focus-trapped modal stepper | not core to Home/Chat/Workspace; available |
| shadcn primitives | `apps/web/src/components/ui/` | card, button, badge, tabs, dialog, tooltip, popover, command, scroll-area, separator, avatar, input, textarea, switch, sheet, sonner/toast, hover-card, dropdown-menu, alert-dialog, progress, skeleton, table, +30 more | Workspace **tab bar** = `tabs.tsx`; ⌘K already on `command.tsx`; cards/buttons everywhere; toasts via `sonner` |
| `Sidebar` (calm spine) | `apps/web/src/components/os/Sidebar.tsx:44` | `workspaceName; spine: SidebarNavItem[]; pinned?; onOpen*; userName; tierLabel` — already implements **active = left honey bar + `--honey-wash` + honey icon** (`Sidebar.tsx:82-91`) and the **hex avatar** inline (`:116`) | Already the shell; reuse its active-state recipe + hex pattern verbatim |
| ⌘K catalog | `apps/web/src/lib/command-catalog.ts:58` | `buildCommandCatalog(ctx)` → plain-name + mono-subtitle groups (Pinned/Jump/Do/Power) | Chat/Home "⌘K" hints route here; don't re-author the vocabulary |
> shadcn `button.tsx` `default` variant = `bg-primary text-primary-foreground` = honey
> bg / `#1a1407` ink → already matches the design primary button. `ghost`/`outline`
> variants cover the design "ghost" button. shadcn `badge.tsx` is **color-capable but
> not always-labeled** — prefer `StatusBadge` when conveying status (a11y).
---
## 3. Missing primitives to build in PR3 (NET-NEW shared components)
These appear across Home/Chat/Workspace and have **no component today** (the hex avatar
exists only as inline markup in `Sidebar.tsx:116`). Build them once as shared atoms.
**Suggested home: `apps/web/src/components/os/warm/`** (new folder for warm-Hive-specific
composite atoms) — keep generic, token-driven, a11y-labeled primitives in `ui/` and the
opinionated warm compositions in `os/warm/`.
| Net-new primitive | Where | Minimal prop API | Used by |
|---|---|---|---|
| `HexAvatar` | `os/warm/HexAvatar.tsx` | `label: string; size?: number; gradient?: boolean; className?``.hex` clip + honey gradient `linear-gradient(150deg,var(--honey-bright),var(--honey-deep))` + `#1a1407` initial; extract from `Sidebar.tsx:116` | workspace switcher, Home workspace cards, Chat context header, Workspace header (46px), bot avatar |
| `DotLive` | `os/warm/DotLive.tsx` | `tone?: 'healthy'\|'attention'\|'risk'\|'work'\|'intel'\|'honey'; className?` — colored dot + `.dot-live` breathe; honor `prefers-reduced-motion` | Home greeting live dot, Workspace "1 agent live", status cards |
| `RunChip` | `os/warm/RunChip.tsx` | `label: string; tone?: StatusTone` — status dot + label inline chip ("Teardown drafted · 9 competitors") | Home overnight hero run-chip row |
| `StreakChip` | `os/warm/StreakChip.tsx` | `days: number; weekDots?: boolean[]; className?` — 🔥 + "12-day streak", `--honey-wash` bg / `--honey-line` border | Home greeting (§ habit-loop mechanic lives on Home, not a page) |
| `SectionLabel` | `os/warm/SectionLabel.tsx` | `children; className?` — 11px mono, uppercase, `.12-.14em` tracking, `--text-dim`, trailing hairline rule | every screen section header (README §6) |
| `ScreenHead` (`.shead`) | `os/warm/ScreenHead.tsx` | `title; subtitle?; action?: ReactNode` — H1 (Hanken 600 ~24-28px) + subtitle + primary action row | Workspace header; power surfaces; generic screen chrome (README §6 / SCREENS §08) |
| `ProvenanceLine` | `os/warm/ProvenanceLine.tsx` | `source: string; when?: string; onClick?``⬡ source · when` in mono `--intel` | Workspace fact rows + recent-work rows; Chat activity steps (thin wrapper over `EvidenceChip` styled to `--intel` mono) |
| `ModelPill` | `os/warm/ModelPill.tsx` | `mode?: string; model: string; onClick?` — "auto · Claude Sonnet" pill in Chat header | Chat context header |
| `ActivityStream` | `os/warm/ActivityStream.tsx` | `summary: string; durationMs?; steps: {tone, text, provenance?}[]; defaultOpen?` — collapsible `--bg-2` card, violet spark, per-step colored dot + `ProvenanceLine` | Chat "the magic" activity card (default-open on active turn) |
| `InlineApprovalCard` | `os/warm/InlineApprovalCard.tsx` | reuse `ApprovalRequest`; `onApprove; onDecline; alwaysAllow?``--honey-wash` bg, attention border, warning icon (NOT a modal — inline in thread) | Chat approval card (shares risk vocab w/ `ApprovalModal`) |
| `OvernightHero` | `os/warm/OvernightHero.tsx` (composite, Home-only) | `eyebrow; statement: ReactNode; runs: RunChipProps[]``--r-xl` gradient card + honey radial glow | Home (composes RunChip) |
| `AskBar` | `os/warm/AskBar.tsx` | `placeholder?; onSubmit; cmdkHint?: boolean` — full-width pill, honey `+`, ⌘K hint, honey send | Home (and Chat composer reuses the send affordance) |
| `HexCheckTile` | `os/warm/HexCheckTile.tsx` | `tone?; size?` — small `.hex` tile w/ check, for fact rows | Workspace "What Waggle knows" fact rows |
| `IconTile` | `os/warm/IconTile.tsx` | `icon: ElementType; tone?: StatusTone` — tinted (`*-wash`) rounded square icon tile | Home "Waggle suggests" rows; Workspace recent-work ext tiles |
> Build order suggestion: `HexAvatar`, `SectionLabel`, `ProvenanceLine`, `DotLive`,
> `RunChip`, `IconTile` first (shared by all three screens), then the screen-specific
> composites. Keep each <80 LOC, token-driven, `prefers-reduced-motion`-safe.
---
## 4. Global pattern rules (apply on every PR3 screen)
From `README.md` §6 and `SCREENS.md`:
1. **Density / scale:** body 16px / line-height 1.55 (set on `body`, `index.css:321`);
**never below 12px**. Honey words in headlines are honey-colored, **not italic**,
same weight. Headlines Hanken 600, `letter-spacing -0.02em` (already on `h1-h6`,
`index.css:327`).
2. **Section labels:** 11px **mono**, uppercase, `.12-.14em` tracking, `--text-dim`,
trailing hairline rule. (Sidebar zone label is the reference: `Sidebar.tsx:102`,
`9.5px mono uppercase tracking-[0.14em] text-[var(--text-dim)]`.)
3. **One accent — honey, sparingly:** primary buttons, active nav, **key numbers**,
focus only. Semantics are desaturated and status-only, never decoration.
4. **Cards:** `bg-card` (`--surface`), `1px solid` **`--line-soft`**, radius **18px**
(`--r-lg`), hover → `--honey-line` border + `translateY(-2px)` + `--shadow`. Use
`.waggle-card-lift` for the hover recipe.
5. **Active nav state:** **left honey bar** (`absolute -left-3 h-[18px] w-[3px] bg-[var(--honey)]`)
+ `--honey-wash` bg + honey icon — already implemented in `Sidebar.tsx:82-91`; mirror
on the Workspace tab bar as a **honey underline** on the active tab.
6. **Provenance everywhere:** any memory / fact / artifact shows `⬡ source · when` in
**mono / `--intel`**. Core trust pattern — never drop it. Use `ProvenanceLine` /
`EvidenceChip`.
7. **Buttons:** primary = honey bg / `#1a1407` ink (shadcn `default`); ghost = `bg-card`
/ `--line-strong` border, hover honey border. Radius 9-13px.
8. **Hex motif:** brandmark + agent/workspace avatars + icon tiles use `.hex` clip with
the honey gradient. Honeycomb (`.comb`/`.honeycomb-bg`) is decorative-only at ~.05.
9. **Motion:** entrances `cubic-bezier(.16,1,.3,1)` ~.7s, **content visible without JS /
reduced-motion**. Hover transitions .14-.18s. Live status = `.dot-live` (breathe 2.4s).
**Honor `prefers-reduced-motion`** on every animated atom.
10. **Focus / a11y:** `2px solid --honey`, 2px offset (global). Status is **never
color-only** — always a text label (StatusBadge/ConfidenceBadge pattern). Keep
keyboard access on ⌘K, tabs, composer.
11. **Dark + light:** both warm (graphite ↔ paper) via `:root[data-theme="light"]`;
drive through the app theme provider (`apps/web/src/providers/ThemeProvider.tsx`),
NOT localStorage scaffolding. Every new color must resolve from a token so light
mode inherits for free (light-mode AA is test-guarded).
### Per-screen anchor (ship-variation)
- **Home → Variation A (Editorial):** centered column max-w 920px; greeting (mono date +
DotLive + StreakChip) → OvernightHero (run chips) → "Pick up where you left off" 2-col
workspace cards (HexAvatar) → "Waggle suggests" rows (IconTile) → AskBar. (`SCREENS.md` 01)
- **Chat → Variation B (Split work canvas):** context header (HexAvatar + name + memory
count + ModelPill) → thread (user bubble asymmetric radius `4px 14px 14px 14px`; bot
hex avatar; ActivityStream w/ ProvenanceLine; InlineApprovalCard) → composer; right
work-canvas (~42%) with `.hex-cursor` live draft. (`SCREENS.md` 02)
- **Workspace → Variation A (Overview + tabs, Memory = a tab):** ScreenHead (breadcrumb +
46px HexAvatar + meta) → Tabs (Overview/Chat/Memory/Artifacts/Files/Team, honey
underline) → 2-col (1.7fr/1fr): left = summary + "What Waggle knows" (HexCheckTile +
ProvenanceLine fact rows) + "Recent work" (IconTile rows); right = Status card +
Up-next + Team. **Do NOT make the graph the default.** (`SCREENS.md` 03)

View File

@@ -0,0 +1,276 @@
# PR3 Recon — SCREEN 03 · Workspace (Variation A: Overview + tabs)
Maps the **current** Workspace surface against the warm-Hive design (`workspace.html`,
`SCREENS.md` §03). Ship target = **Variation A "Overview + tabs"**, **Memory stays a
tab** (do NOT make the knowledge-graph the default — that's the alternate Variation B,
deferred).
Primary file: `apps/web/src/components/os/apps/WorkspaceDesktopApp.tsx` (1020 LOC).
Route wrapper: `apps/web/src/routes/WorkspaceRoute.tsx`.
Design ref: `docs/design_handoff_waggle_app/design-files/screens/workspace.html`.
---
## 1. Current structure (with line refs)
`WorkspaceDesktopApp.tsx` is a fixed-layout shell (no drag/resize grid, per its header
comment lines 9-21). Composition top-to-bottom:
- **Tab model** — 8 tabs, `WorkspaceTabId` union + `TABS` array
(`WorkspaceDesktopApp.tsx:58-77`): `overview · chat · research · artifacts · memory ·
tasks · timeline · settings`. Each tab is `{ id, label, icon }` (lucide icons). **No
per-tab counts.**
- **Header** (`:759-811`) — flat row: `<h2>` workspace name (`:761`), a **type pill**
(`:762-766`), a **status pill** (`statusPillClass`, `:767-772` + helper `:117-126`),
an inline "N running" agent indicator (`:773-778`), a **members avatar stack**
(`:783-798`, initials, max 5 + overflow), and the **`WorkspaceActionsMenu`**
(`:800-809`, rename/archive/restore/export/delete). **No breadcrumb. No hex avatar.
No meta row** (memories/sources/updated). **No "Memory"/"Continue" header buttons.**
- **Tab bar** (`:814-839`) — horizontal scroll nav, `role=tablist`. Active tab =
`border-primary` bottom border + `text-foreground` (`:827-831`); honey comes via the
`--primary` token so the underline is already honey-tinted. Icon + label per tab, **no
count badges**.
- **Body = main canvas + right context panel** (`:842-969`):
- **Overview** (`OverviewTab`, `:518-545`) — a **widget grid**
(`grid-cols-1 md:grid-cols-2 xl:grid-cols-3`, fixed 16rem row height, `:530-533`):
5 `WidgetCard`s — `ChatPreviewWidget` (read-only thread preview, `:184-232`),
`ArtifactsWidget` (file registry, `:241-283`), `TasksWidget` (pending+blocked from
state, `:286-333`), `MemoryHighlightsWidget` (decisions + "I remember", `:335-377`),
`ActivityWidget` (audit feed, `:379-401`). Empty-state when everything empty
(`:850-868`).
- **Other tabs** — `chat` renders the `chatSlot` or a deep-link placeholder
(`:882-902`); `tasks``<TasksTab>` (`:904`); `memory`
`<MemoryCenterTab mind="workspace">` (`:937-941`); `research`/`timeline`/`settings`
are `<TabPlaceholder>` stubs (`:906-957`); `artifacts` is an inline file-card grid
or placeholder (`:914-932`).
- **Right context panel** (`WorkspaceInfoPanel`, `:405-514`, `hidden lg:flex w-72`):
Workspace block (name/desc + memory/session counts, `:422-439`), **Team members**
(`:441-460`, global roster — see §2), **Last activity** + "N agents running"
(`:462-481`), **Quick actions** (Open chat / View memory / Review tasks, `:483-511`).
- **Whole-screen states** — loading (`:691-698`), `permission`/`notfound`/`offline`
error variants (`:700-754`) via `FullScreenState` (`:1000-1018`), empty-workspace
(`:850-868`).
- **Route wrapper** (`WorkspaceRoute.tsx`) — `/workspaces/:workspaceId/:tab?`; the URL
drives `activeTab` (controlled seam §5.2a, `:43-54`); `onTabChange` navigates
(overview = bare `/workspaces/:id`, `:52-53`); `chatSlot` portals the live chat
(`:57`); URL→shell sync effect (`:34-38`). The 8 tabs are pinned in `WS_TABS`
(`:20-22`).
- **`WorkspaceBriefing.tsx`** (284 LOC) — a SEPARATE component, shown **inside ChatApp**
when a thread has no messages (header comment `:1-5`). Reads the SAME
`getWorkspaceContext` (`:57`). Holds "I Remember" / "Recent Decisions" / "Recent
Conversations" / suggested-prompt chips / stats bar. **It is not part of the Workspace
Desktop shell** — it's the chat empty-state. Useful as a copy/data reference for the
Overview's "What Waggle knows" rows but is not the screen being rebuilt. Its
collapsed-state persistence is `lib/workspace-briefing-state.ts` (tested — see §3).
---
## 2. Data contract — what's REAL vs mocked
The Overview's three data feeds come from real, populated endpoints (verified in
`packages/server/src/local/routes/workspaces.ts`). The DESIGN, however, surfaces several
fields the contract does NOT yet provide. Table below; "✔ real" = endpoint returns it
today, "✖ mock" = design shows it but no field exists, "~ derivable" = computable from
existing data.
### Endpoints + adapter methods (all real, no envelope)
| Adapter (`lib/adapter.ts`) | Route (`workspaces.ts`) | Returns |
|---|---|---|
| `getWorkspaceContext(id)` `:582` | `GET /:id/context` `workspaces.ts:364-655` | summary, recentMemories, recentDecisions, recentThreads, suggestedPrompts, `stats{memoryCount,sessionCount,fileCount}`, greeting, pendingTasks, workspace{type,status,description} |
| `getWorkspaceState(id)` `:601` | `GET /:id/state` `:663-693` | active/openQuestions/**pending**/**blocked**/completed/stale/recentDecisions/nextActions (WorkspaceStateView) |
| `getWorkspaceActivity(id,limit)` `:606` | `GET /:id/activity` `:699-742` | `{events:[{id,ts,type,actor?,summary}]}` from audit_events |
| `getWorkspaceFiles(id)` `:587` | `GET /:id/files` `:744-756` | `{files:[…]}` from file registry (interim "artifacts") |
| `getTeamMembers()` `:2160` | (global team roster) | `[{id,name,status,avatar?}]`**NOT workspace-scoped** (`WorkspaceDesktopApp.tsx:660-666` TODO) |
### Design field → reality
| Design element (workspace.html / SCREENS §03) | Source | Status |
|---|---|---|
| **Header meta: "142 memories"** | `ctx.stats.memoryCount` | ✔ real |
| **Header meta: "9 sources"** | — | ✖ mock (no `sourceCount`; harvest sources exist in mind but not exposed on context) |
| **Header meta: "1 agent live"** | `useRoomState().workspaceMap.get(id).live.length` (`:603-607`) | ✔ real (live SSE) |
| **Header meta: "updated 2h ago"** | `ctx.lastActive` (`:592,646`) | ~ derivable (have `lastActive`; relativeTime helper exists `:135-147`) |
| **Tab counts (Chat 3 · Memory 142 · Artifacts 7 · Files 12 · Team 4)** | memory=`stats.memoryCount` ✔; sessions=`stats.sessionCount` ✔; files=`stats.fileCount` ✔; team=`getTeamMembers().length` ✔(global); **artifacts** ✖ (no distinct artifact entity — interim = files) | ~ mostly derivable; artifacts count is the gap |
| **Left: summary card** | `ctx.summary` (`composeWorkspaceSummary` `workspaces.ts:31-75`) | ✔ real |
| **"What Waggle knows" fact rows** | `ctx.recentMemories` (content+importance+date) and/or `ctx.recentDecisions` | ✔ content real; **✖ per-fact provenance `⬡ source · when`** (frames have no `source` surfaced — design's "web · mem0.ai", "teardown.md" are mock; `date` IS present so "·when" is real) |
| **"Recent work" artifact rows (ext tile + name + provenance + time)** | `getWorkspaceFiles``normalizeArtifacts` (`:977-998`): name, mimeType/modifiedAt | ✔ name+time real; **✖ provenance source** ; ext-tile derivable from filename |
| **Right Status: agent live + name** | `useRoomState` live list — name not in the view-model today | ~ "live" real, agent NAME ✖ mock |
| **Right Status: model "auto · Claude Sonnet"** | `ctx.workspace.model` (`workspaces.ts:625`) | ✔ real (model id; "auto" + friendly name is display) |
| **Right Status: "+6 today"** | — | ✖ mock (no per-day memory delta on context; overnight delta exists on Home `OvernightSummary.consolidated` but not per-workspace) |
| **Right Status: "3 to review / Needs review"** | `HomeBriefing.needsReviewCount` exists at HOME scope (J08); per-workspace ✖ | ✖ mock at workspace scope |
| **Right "Up next" rows** | `state.pending` + `state.blocked` + `state.nextActions` + cron `upcomingSchedules` | ~ derivable (TasksWidget already uses pending/blocked; nextActions+schedules unused on this screen) |
| **Right Team avatar rows** | `getTeamMembers()` | ✔ real but **global roster, not per-workspace** (documented TODO `:660-662`) |
**Net:** the three load-bearing columns (summary, knowledge/memory facts, recent
work/artifacts, status counts, up-next, team) are all **backed by real endpoints**. The
**provenance `⬡ source · when` line** (a core trust pattern, README §6) is the single
biggest data gap — memory frames have a `source` column server-side
(`workspaces.ts:291` writes `source`, `:413` SELECTs around it) but it is **not
projected into `recentMemories`/`recentDecisions`** today, nor onto file rows. "9
sources", "+6 today", per-workspace "N to review", and a distinct **artifacts** entity
(vs files) are genuinely absent.
---
## 3. Test contract
What PR3 must NOT break (existing tests touching this surface):
- **`apps/web/src/test/p1a-workspace-route.test.tsx`** — pins the **route ↔ shell
contract**, NOT the visual layout. Asserts `WorkspaceRoute` calls
`selectWorkspace(routedId)` on deep-link / Back-Forward, skips when already active,
and **never** syncs the `local-default` placeholder (`:56-80`). It mocks
`WorkspaceDesktopApp` to a stub (`:30-32`) and `ChatHost`/`ChatSlot` (`:33-36`). The
route renders under `path="workspaces/:workspaceId/:tab?"` (`:44`). **Constraint:** the
`:tab?` param, the `selectWorkspace`-on-route effect, and the `local-default` guard
must survive any rewrite of `WorkspaceRoute`.
- **`apps/web/src/lib/workspace-briefing-state.test.ts`** — pins the per-workspace
briefing-collapsed localStorage helpers (key prefix `waggle:workspace-briefing-
collapsed:`, sanitisation, per-id isolation, no colon-boundary leak). Only relevant if
the briefing collapse behavior is carried into the new Overview; the helpers themselves
can be reused as-is.
- **Implicit `data-testid` contract** (consumed by live-smoke / Playwright + the empty
flows): `ws-desktop-root`, `ws-tab-bar`, `ws-tab-<id>`, `ws-tab-panel`,
`ws-overview-grid`, `ws-widget-{chat,artifacts,tasks,memory,activity}`,
`ws-info-panel`, `ws-status-pill`, `ws-members-stack`, `ws-agents-running`,
`ws-desktop-{loading,permission-denied,notfound,offline,retry}`, `ws-memory-tab`.
Changing the tab set (8→6) removes `ws-tab-{research,tasks,timeline,settings}` — grep
for those test ids before deleting (none appear in the two test files above, so the
risk is in untracked Playwright smokes, not unit tests).
There is **no test that pins the 8-tab set, the widget-grid layout, or the right-panel
contents** — so the Overview re-layout (grid → 2-col 1.7fr/1fr) and the tab reduction
are free to change as long as the route/shell contract and the load-bearing test ids are
preserved.
---
## 4. Design spec — Variation A (exact copy + structure)
From `SCREENS.md` §03 + `workspace.html` Variation A markup:
**Header** (`workspace.html:138-155`):
- **Breadcrumb** (`.crumbs`, mono 11.5px, `--text-dim`): `Home **Competitive
Intelligence**` (current workspace bold).
- **46px hex avatar** (`.wmark.hex`, 46×52, honey gradient `--honey-bright → --honey-deep`,
`#1a1407` glyph) showing the workspace initial.
- **H1** title (Hanken 650, 28px, `-0.02em`).
- **Meta row** (`.wmeta`, 13px `--text-muted`, gap 14px): `● 1 agent live` (live dot
`--healthy`) · `142 memories` · `9 sources` · `updated 2h ago`.
- **Header actions** (right): `Memory` (ghost button) + `Continue →` (honey primary).
**Tab bar** (`.tabs`, `workspace.html:157-164`): `Overview · Chat 3 · Memory 142 ·
Artifacts 7 · Files 12 · Team 4`. Active tab = `--text` + **2px honey bottom-border**
(`.tab.on`, `:44`). Each count is a mono 11px `--text-dim` `.cnt` span.
**Overview content** — 2-col grid `1.7fr / 1fr`, gap 22px (`.grid`, `:49`):
- **Left col:**
- **Summary card** (`.card`, `.summary` 15.5px/1.6, honey-bold keywords). Exact sample
copy: *"This workspace tracks the **persistent-memory competitor landscape** for the
Q2 board cycle. Waggle has mapped **9 rivals**, pulled current pricing, and drafted a
teardown — the live thread is mid-flight on turning the opening into a board brief."*
- **"What Waggle knows"** section (`.sec-h` mono uppercase label + brain icon, `:177`)
→ `.knows` list of `.fact` rows (`:61-67`): each = a **30px hex check tile**
(honey-gradient, `#1a1407` checkmark) + fact text (honey-bold spans) + a
**provenance line** `**source** · when` (`.prov`, mono 10.5px, `.src` =
`--intel`). Sample facts (`:279-284`): "Mem0 is cloud-only and raised prices ~15%…"
(`web · mem0.ai · 2h ago`), "Only 2 of 9 rivals ship local-first memory."
(`teardown.md · 2h ago`), "Mara wants the board brief to lead with the regulated-
industries opening." (`chat · Tue · 2d ago`), etc.
- **"Recent work"** section (file icon, `:182`) → `.arts` list of `.art` rows
(`:71-78`): **32px ext tile** (tinted by type color, mono ext label e.g. `MD`/`XLS`/
`PDF`) + name (`<b>`) + subtitle + right-aligned mono `.when` time. Samples (`:288-291`):
`teardown.md` "Q2 competitive teardown · 9 competitors" `2h ago`,
`pricing-landscape.xlsx`, `mem0-teardown.pdf`. Rows hover → `--honey-line` border.
- **Right col** (three `.card`s):
- **Status** (`:188-194`): `.stat-line` rows — `Agent` → `● Research-synth · live`
(`--healthy`); `Model` → `auto · Claude Sonnet`; `Memories` → `142 **+6 today**`
(delta `--healthy`); `Needs review` → `3 memories` (`--attention`).
- **Up next** (`:196-201`): `.agentrow`s — colored dot + label + mono status: `Board
brief from teardown` (draft, `--work`); `Export table → Salesforce` (awaiting you,
`--attention`); `Weekly digest` (`17:00`, `--healthy`).
- **Team** (`:203-206`): `.person` rows — 30px round avatar (initial, colored bg) +
name + role: `Mara K. · Owner`, `Research-synth · Agent · live`, `Deck-builder ·
Agent · idle`, `Jonas P. · Editor`.
**Non-Overview tabs** in this concept pass are a single empty placeholder (`.emptytab`,
`:211-216`): *"This tab is wired in the full prototype — Overview is the focus of this
concept pass."* — i.e. the design only fully specs Overview; the other tabs route to the
**real existing screens** (see §6).
**Memory stays a tab** (SCREENS §03 emphatic, README §4/§5 table). Variation B
(memory-forward knowledge-graph as the default view, `workspace.html:219-265`) is an
**alternate to defer**, NOT this PR.
---
## 5. Gap table (current → design)
| # | Area | Current | Design (Var A) | Severity |
|---|------|---------|----------------|----------|
| G1 | **Tab set** | 8 tabs: Overview/Chat/**Research**/Artifacts/Memory/**Tasks**/**Timeline**/**Settings** (`:68-77`) | 6 tabs: Overview/Chat/**Memory**/Artifacts/**Files**/**Team** | HIGH — drop Research/Tasks/Timeline/Settings from the bar; add Files + Team; reorder |
| G2 | **Tab counts** | none | per-tab mono count (Chat N·Memory N·Artifacts N·Files N·Team N) | MED — wire from `stats` + members; artifacts count gap |
| G3 | **Header avatar** | none | 46px hex avatar, honey gradient, workspace initial | MED — reuse `.hex` clip-path + initialsOf |
| G4 | **Breadcrumb** | none | `Home Workspace` mono crumb | LOW |
| G5 | **Header meta row** | type pill + status pill + "N running" | `● agent live · N memories · N sources · updated Xago` | HIGH — restyle to meta row; "sources" is a data gap (§2) |
| G6 | **Header actions** | WorkspaceActionsMenu (kebab) | `Memory` ghost + `Continue →` honey | MED — add the two buttons; KEEP the actions menu (real feature, not in mock) |
| G7 | **Overview layout** | 3-col equal-height widget grid | 2-col 1.7fr/1fr: left summary+knows+recent-work, right status+upnext+team | HIGH — full re-layout |
| G8 | **"What Waggle knows" rows** | `MemoryHighlightsWidget` (plain list, no provenance, no hex tile) | `.fact` rows: hex check tile + honey-bold text + `⬡ source · when` provenance | HIGH — new row component; **provenance source is a data gap** |
| G9 | **"Recent work" rows** | `ArtifactsWidget` (file icon + name) | `.art` rows: ext tile + name + subtitle + mono time + hover border | MED — restyle; ext-tile + provenance |
| G10 | **Status card** | scattered in right panel (counts + last activity) | one Status card: agent/model/memories+delta/needs-review | MED — consolidate; "+6 today" & "needs review" are data gaps |
| G11 | **Up next card** | none (tasks live in a widget + Tasks tab) | `.agentrow` list (draft/awaiting-you/scheduled) | MED — derivable from pending/blocked/nextActions/schedules |
| G12 | **Team card** | right-panel "Team members" (global roster) | Team card with role labels (Owner/Agent·live/Editor) | LOW — restyle; still global-roster-backed |
| G13 | **Right context panel** | persistent `w-72` aside (info/team/last-activity/quick-actions) | folded INTO the Overview right column; no separate aside | MED — the aside's content moves into the grid's right col |
| G14 | **Provenance pattern** | absent on this screen | `⬡ source · when` on every fact + artifact (core trust pattern) | HIGH (trust) — needs `source` projected from frames (§2) |
| G15 | **Tokens/typography** | shadcn `text-foreground/muted-foreground`, `font-display`, 10-12px dense | warm tokens, Hanken H1 28/650, 13-16px body, mono labels | MED — apply warm-Hive tokens (already shipped, §6) |
| G16 | **Memory tab** | `<MemoryCenterTab mind="workspace">` already embedded (`:937-941`) | Memory stays a tab | ✅ already correct — keep |
---
## 6. Reuse + build — tab bar → existing routes
The 6 design tabs map cleanly onto surfaces that **already exist**; PR3 rebuilds the
SHELL + Overview, and the other 5 tabs embed/deep-link the real screens:
| Design tab | Maps to | Exists? | How to wire |
|---|---|---|---|
| **Overview** | `WorkspaceDesktopApp` Overview canvas | ✔ (re-layout) | Rebuild as 2-col grid; this is the bulk of PR3 |
| **Chat** | `ChatSlot` (live per-workspace chat) | ✔ — `WorkspaceRoute.tsx:57` already portals `<ChatSlot workspaceId>` into the `chat` tab via the `chatSlot` seam (`WorkspaceDesktopApp.tsx:112,885`) | reuse as-is |
| **Memory** | `MemoryCenterTab` | ✔ — already embedded `WorkspaceDesktopApp.tsx:937-941` (`mind="workspace"`, `consumeDeepLinks={false}`) | reuse as-is (✅ G16) |
| **Artifacts** | `ArtifactsRoute`/Artifact Center | ✔ route exists (`routes/index.ts:36`); workspace tab currently inlines a file-card grid (`:914-932`) | embed the Artifact Center component, or keep the file grid until the real artifact entity lands (§2 gap) |
| **Files** | `FilesRoute` | ✔ route exists (`routes/index.ts:37`) | NEW tab — embed the Files surface scoped to the workspace; data via `getWorkspaceFiles` (already used) |
| **Team** | `TeamRoute` | ✔ route exists (`routes/index.ts:48`) | NEW tab — embed Team; or a workspace-scoped panel. NB roster is global today (§2) |
**Drop from the bar** (no longer top-level per design): **Research** (placeholder stub
only `:906-912`), **Tasks** (still a real `<TasksTab>` `:904` — relocate to the
Overview "Up next" card + keep reachable, don't delete the component), **Timeline**
(stub `:943-948` — lives at `/settings/timeline` per `routes/index.ts:29`), **Settings**
(stub `:951-957` — lives at `/settings`). Update `WS_TABS` in `WorkspaceRoute.tsx:20-22`
and the `WorkspaceTabId` union to the 6-tab set; unknown `:tab?` already falls back to
overview (`WorkspaceRoute.tsx:43-45`), so stale `/workspaces/:id/tasks` links degrade
gracefully.
**Reuse without change:**
- **Warm tokens already shipped** (PR1): `apps/web/src/index.css` defines `--honey`,
`--honey-wash`, `--honey-line`, `--honey-bright/-deep`, `--intel`, `--work`,
`--healthy`, `--bg`, `--surface*`, `--text*` (verified `:149-167`) + the shadcn HSL
core derives from them (`--primary` = honey `:29`). The Overview can use these
directly. `apps/web/src/waggle-theme.css` is the companion sheet.
- **Helpers in-file:** `initialsOf` (`:128-133`, for hex avatar + team), `relativeTime`
(`:135-147`, for "updated Xago" + artifact times), `normalizeArtifacts` (`:977-998`),
`humanizeActivitySummary` (`lib/activity-labels`).
- **Hex motif:** README §7 `.hex { clip-path: polygon(...) }` — add a Tailwind/utility
class for the 46px header avatar + the 30px fact check-tiles + 32px ext tiles.
- **Data plumbing:** the existing `useEffect` load (`:613-679`) already fetches context/
state/activity/members/files — **no new adapter calls needed** for Overview except
optionally projecting `source` onto memory rows (server change, §2/G14) and a per-
workspace artifacts/sources count (G2/G5).
- **`WorkspaceBriefing.tsx`** importance pills + "I Remember"/"Recent Decisions" copy
(`:181-199,164-178`) are a good reference for the "What Waggle knows" fact rows, but
the briefing itself stays the chat empty-state — don't fold it into the shell.
**Build new:** the 2-col Overview grid + `FactRow` (hex check tile + provenance),
`ArtRow` (ext tile + provenance + time), `StatusCard`, `UpNextCard`, `TeamCard`, the
46px hex header avatar + breadcrumb + meta row, and the `Memory`/`Continue →` header
buttons. Fold the current right `aside` content into the grid's right column (G13).