feat(planner): architect chat UI with streaming + slash + @file (M18-5) #192
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!192
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "boss/166"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ships
/app/planner— the operator-facing chat surface against the host-modearchitect agent from #165.
Closes #166
Summary
New-session button clears the selection so the empty state surfaces the
read-only "System" block (default prompt from
GET /architect/config).Delete button opens a Base UI
Dialogconfirm modal.useArchitectStream(taskId)subscribes to/architect/stream/:task_id. Tool-call events collapse into titled pills(click to expand input JSON); assistant text streams through
react-markdown+remark-gfmwith a blinking-cursor tail.shikiand render with thetokyo-nighttheme. Plain<pre>fallback before shiki resolves keepsthe happy-dom tests (and the first paint) free of WASM dependencies.
/trigger opens the slash palette (/spec,/breakdown,/assign);@opens a file picker backed byGET /architect/files(allowlist:specs/*.md+config/agents.json).On send,
expandFileMentionsinlines the contents of@specs/foo.mdmentions before POSTing the turn.
apps/server/src/architect.ts—/architect/config(surfaces default prompt + model),/architect/files(picker list),
/architect/files/content?path=(with a strictresolveAllowlistedFileguard: no.., no absolute paths, allowlistgated to
specs/*.md+config/agents.json).packages/shared/src/architect.tscarries the wirecontract (session view, list entry, chat response, config, files). Both
apps import through
@claude-hooks/shared.AppShellnav tabs; Vite dev proxypicks up
/architectand/whoami.Test plan
bun x turbo run typecheck lint test— 768 server tests + 37 webtests pass, Biome clean.
bun x turbo run build— web bundle builds (shiki langs code-splitper language).
persisted markdown render against a fixture stream.
probePalette(slash vs file trigger,URL / email false-positives), keyboard send, and disabled state.
attachments.test.tscoversfindFileMentions+ fetch-failurehandling in
expandFileMentions.architect.test.tsgains coverage for/architect/config,/architect/files,/architect/files/content(happy path, badpath, missing file), plus
resolveAllowlistedFileallowlist shape.planner.spec.ts— mocks/architect/*, sends a message,asserts the tool-call pill toggles + the assistant markdown bubble
paints
<strong>bold.🤖 Generated with Claude Code
d7af16089e881b99ff83Review — M18-5 Architect Chat UI
CI ✅ green. All AC from #166 is met: layout, streaming, tool-call pills, markdown, session management, system-prompt block, slash palette, @file expansion, and the server-side
resolveAllowlistedFilepath-traversal guard. One correctness issue needs fixing before merge.❌ EventSource not closed on terminal
result— spurious reconnection loopFile:
apps/web/src/lib/architect.ts, insideuseArchitectStream'ses.onmessagehandler.What happens today: When the per-task SSE stream emits a
resultevent, the hook correctly setsphase → "done"in React state, but it does not close theEventSource. The browser's built-in EventSource auto-reconnection kicks in as soon as the server closes its end of the per-task stream (which happens immediately after the terminal event). The hook relies on the parent component eventually callingsetStreamingTaskId(null)— but that requires: state update → React re-render → effect fires → parent state update → another render →useEffectin the hook re-runs →es.close(). During those async ticks the browser has already issued 1–2 reconnect attempts to/architect/stream/:task_id.Why this matters: The
onerrorguard (if (prev.phase === "done") return prev) prevents the phase from regressing, but it does not prevent the EventSource from reconnecting. Each reconnect round-trips the server. More critically: if the server replays the terminalresultevent on reconnect (reasonable behaviour for a task that's already complete),isArchitectTaskEventpasses, theresultgets pushed intoeventsagain, and any consumer iteratingstream.eventssees duplicates. Even without replay, there's a dangling open connection.Fix — two lines inside
onmessage, whereev.type === "result"is handled:Note:
esis captured in the closure of theuseEffectthat creates it, so it's safe to reference directly here. TheesRef.current === esguard mirrors the existing cleanup teardown pattern in the effect's return function, preventing a stale ref from clobbering a newer stream's handle.Everything else looks good: the
resolveAllowlistedFilebelt-and-braces path traversal check is solid, thecanUseToolarchitect-specific denies are correct and tested, the palette dismiss token logic is a reasonable heuristic, and thewasStreamingRefpattern in the route component correctly prevents double-invalidation. The test coverage is thorough across unit, component, and E2E layers.Fixed in
3bc763b.useArchitectStreamnow callses.close()inline after observing aresultevent, guarded withesRef.current === esto mirror the existing cleanup teardown pattern. Addedapps/web/src/lib/architect.test.ts(5 tests) covering the eager close, the onerror-after-done phase preservation, taskId-change teardown, and the null-idle path. Full QA green: 768 server + 70 web tests, Biome clean, typecheck clean.Review — M18-5 Architect Chat UI (round 2)
CI ✅ green (run #1802, 3m13s, SHA
3bc763b).My round-1 finding is fully addressed. In
apps/web/src/lib/architect.ts,useArchitectStream'sonmessagehandler now callses.close()(+ theesRef.current === esstale-ref guard) synchronously on the terminalresultframe, outside thesetStateupdater — exactly the fix I prescribed. The spurious EventSource reconnect loop and the potential event-replay/duplication are both closed off.Nothing else changed in the delta. ✅ APPROVED.