refactor(web): /workspace chat-only — strip spec mode, drop ?spec/?task params #1129
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/agent-hooks!1129
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/1121"
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?
Closes #1121.
Summary
/workspaceis now the architect chat surface only. Spec authoring lives at/specs/$specName(wsplit-2). Old?spec=<name>/?task=<id>bookmarks land on the chat page —validateSearchsilently drops both fields, no redirect, no crash.routes/workspace.index.tsxshrank from 1856 → 677 lines.SessionsSidebar,WorkspaceToolbar,EmptyState,BreakdownDrawer,DeleteSessionDialog, andExportSessionMenumoved tocomponents/workspace/*. The streaming-task lifecycle moved tolib/use-architect-chat-stream.ts./spec <name>slash command intercepts on send and navigates to/specs/$specName(falls back to/specsif name omitted)./breakdown specs/<name>.mdopens the in-routeBreakdownPreviewdrawer./assignand any future slash commands still pass through to the architect.WorkspaceCommandPalettetrimmed: Specs section dropped (the in-chat/specslash command is the workspace shortcut for jumping to a spec; the/specsroute owns navigation).?task=was dropped — the streaming task id lives in component state. Mid-turn reload no longer reconnects to the SSE stream (operator decision, 2026-05-12).Test plan
just qa— typecheck + lint + format + i18n + paraglide + flow-schema all greenbun x vitest run src/routes/workspace.test.tsx— 29 cases pass/workspacein browser:lg+and as a drawer below/spec authnavigates to/specs/auth/spec(no arg) navigates to/specs/breakdown specs/foo.mdopens the breakdown drawer/assign 42 devis forwarded to the architect verbatim/architect/chatand stream the reply/workspace?spec=auth&task=t1URL renders the chat page (no crash)/specs/authseeds the composer with@specs/auth.mdon the workspace mount🤖 Generated with Claude Code
Review —
refactor(web): /workspace chat-onlyCI ✅ green. The refactor is well-executed — 1856 → 677 lines in the route file, clean component extraction, comprehensive test suite (29 cases). One real bug to fix before merge; everything else is minor.
🐛 Bug: missing unmount cleanup in
use-architect-chat-stream.tsThe 2 s fallback
setTimeoutis set inside the firstuseEffectbut the effect never returns a cleanup function. If the workspace unmounts while the timer is pending (e.g. the user navigates away within 2 seconds of a turn completing), the timer fires and callssetStreamingTaskId(null)on the unmounted component.React 18 silently ignores the state update, but the timer object itself is a resource leak, and in strict-mode double-invocation the missing cleanup causes a spurious second invocation.
Fix — add a return to that effect:
⚡ Minor:
groupedmap recomputed on every render insessions-sidebar.tsxfilteredSessionsis correctly memoised, but thegroupedmap built from it (lines 93–99) lives in the render body and is recreated on every re-render (e.g. on hover state changes fromgroup-hover/row). Wrap it inuseMemo([filteredSessions]):This is low-impact for typical session counts but is inconsistent with the surrounding
useMemousage.✅ Everything else looks good
validateSearchsplit — both layout and index route defining identicalsession/reposhapes is intentional belt-and-suspenders for TanStack Router type inference. The test coverage for the legacy?spec=/?task=drop is solid.parseLeadingSlash/normaliseSpecArg— exported helpers with full unit tests. The regex^(\/[a-zA-Z]+)(?:\s+([\s\S]*))?$correctly rejects/123and bare/. Good.(navigate as any)casts — three occurrences, all with identical explanatory comments about the locale-prefix dynamic-params limitation. Acceptable and consistent.biome-ignorefordraftKeydeps — the stale-closure reasoning in the comment is correct; includingdraftKeywould clobber the new session's draft on switch.SessionsSidebar—<aside aria-label="Workspace sessions">on desktop, plain<div>inside the dialog drawer for embedded mode. Correct.SessionModelPicker—sr-only+aria-hiddenlabel pattern is correct; screen readers announce "Model" without the colon.DeleteSessionDialog—tone="error"on the destructive confirm button matches the design-system convention.workspace_sessions_search_placeholderadded to bothen.jsonandfr.json.ExportSessionMenureuse — shared cleanly between toolbar and per-session sidebar rows. Consistenttestidconventions.consume()correctly takes priority overlocalStorage.specs.$specName.tsx—repo: undefinedin the navigate call was already there before this PR; no regression.Fix the two items above (the cleanup is the blocker) and this is good to merge.
Both review items addressed in
598ceaa:Bug fix —
use-architect-chat-stream.tscleanupAdded the suggested
returnfrom the firstuseEffectso the 2 s fallbacksetTimeoutis cleared on unmount (and on dep changes, which is fine —wasStreamingRefis already nulled after the first entry into thedonebranch, so the timer is never re-set on a stale render).Perf nit —
groupedmap insessions-sidebar.tsxWrapped in
useMemo([filteredSessions])as suggested. Matches the surrounding pattern.just qagreen (3305 server tests, 1139 web tests).Review — refactor(web): /workspace chat-only (#1121)
CI ✅ green. 29 unit tests pass. Full diff read.
What this does
workspace.index.tsxshrinks from 1 856 → 677 lines by:specmode entirely (SpecEditor, NewSpec dialog, SaveAsSpec dialog, Breakdown drawer wired to?spec=, spec section in the sidebar and command palette).components/workspace/*and the streaming lifecycle tolib/use-architect-chat-stream.ts.?task=URL state with component state (operator decision documented).parseLeadingSlash+normaliseSpecArghelpers with/spec→ navigate and/breakdown→ drawer interception.Positives
Decomposition is clean. Each extracted component has one clear job and a self-contained props interface. The route file now reads as data-flow + routing; the markup moved where it belongs.
promoteToSession+toastError+invalidateSessioneliminate the three-way duplication that existed acrosssendMutation/regenerateMutation/editMutationsuccess/error handlers. Good DRY.useArchitectChatStreamis a faithful lift. Both effects, both refs, the 2 s fallback timer, and the cleanup on unmount are all preserved. ThewasStreamingRef→expectingRefetchRef→baselineMsgsRefdance is identical to the original.URL contract is double-gated correctly. Both
workspace.tsx(layout) andworkspace.index.tsx(index) have matchingvalidateSearchthat omitspecandtask, so old bookmarks are stripped before any component code runs. Belt-and-suspenders but explicit.Test rewrite is comprehensive. Unit tests for
parseLeadingSlashandnormaliseSpecArgare a good choice (regression on those helpers silently routes/spec//breakdownto the architect as prose). Component-level tests cover the/spec → navigate,/breakdown → drawer, pass-through, composer-handoff, and ⌘K paths.groupedis nowuseMemoinSessionsSidebar(the oldWorkspaceSidebarrecomputed it on every render). Improvement.Observations (non-blocking)
/breakdowndoesn't clear the composer draft on success./spec authcallssetDraft("")+localStorage.removeItem(draftKey)before navigating./breakdown specs/auth.mdopens the drawer but leaves/breakdown specs/auth.mdin the textarea. The breakdown drawer is additive (not navigational) so this may be intentional — the user might want to type a follow-up. Worth a conscious decision, but it's a product call, not a bug.buildSpecScaffoldFromTranscriptis gone without a migration. The export + tests were removed; the "Save as spec…" flow is gone, so this is correct. Just confirming the/specsroute owns its own scaffolding if it ever needs one — nothing broken here.filesquery lost its explicit generic annotation (useQueryinstead ofuseQuery<ArchitectFileEntry[]>). Type is still inferred fromfetchArchitectFiles's return type; no runtime impact, just a style note.Verdict
APPROVED. The decomposition is solid, the streaming hook extraction is correct, the URL-contract change is clearly documented and tested, and the new test file is better than what it replaces. The one behavioural observation (
/breakdowndraft retention) appears intentional given the drawer-vs-navigation distinction.