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.