feat(web): breakdown toolbar + open-in-chat on /specs/$specName #1128

Merged
reviewer merged 1 commit from code-lead/1120 into main 2026-05-13 15:12:18 +00:00
Collaborator

Summary

Closes #1120 — wsplit-4 toolbar additions on the focused spec editor.

  • Breakdown (ListTodo) button opens a right-side <Drawer> hosting the existing <BreakdownPreview> panel. The preview now runs against the live in-memory draft: <SpecEditor> mirrors its body up through a new onDraftChange callback and fetchBreakdownPreview forwards it as an optional spec_content field on POST /architect/breakdown-preview (allowlist gate unchanged). A banner inside the drawer warns when the editor is dirty.
  • Open in chat (MessageSquare) button stages an @specs/<name>.md mention in a new single-shot Zustand store (useComposerHandoffStore) and navigates to /workspace?session=new. The workspace's draft-restore effect drains the store on mount — no URL leak, no localStorage spill.

The legacy /workspace breakdown drawer and the in-chat /breakdown slash command both stay; this is purely additive.

Acceptance criteria

  • routes/specs.$specName.tsx toolbar has a "Breakdown" button (lucide ListTodo) that opens <BreakdownPreview> inside a right-side <Drawer>.
  • BreakdownPreview is invoked with the current spec body — even if dirty, uses the in-memory draft (banner warns the operator when unsaved).
  • "Create issues" inside the drawer behaves identically to its current /workspace invocation (same <BreakdownPreview> component + props).
  • Drawer is dismissable via Esc / backdrop click (inherited from the shared <Drawer> primitive).
  • Toolbar "Open in chat" button (lucide MessageSquare) navigates to /workspace?session=new.
  • Spec is pre-inlined as @specs/<name>.md mention via a transient Zustand key (useComposerHandoffStore) — not a URL param.
  • On /workspace mount, if the handoff key holds a value, the composer draft is seeded and the key cleared.
  • Tests: drawer opens + calls fetchBreakdownPreview with the spec body + renders preview rows.
  • Tests: Open-in-chat navigates with session=new and handoff store holds the expected mention.
  • Tests: workspace mount drain — renderHook proof that the effect consumes a staged handoff on first render.

Out of scope (per the issue)

  • AI drawer integration (wsplit-3 — already landed).
  • Removing breakdown from /workspace (operator decision: both stay).

Test plan

  • just qa — typecheck + Biome + sql-layer-check + paraglide-check + 3305 server tests + 1119 web tests all pass.
  • Manual smoke: open /specs/<name> → click Breakdown → drawer shows preview rows; edit body → reopen → banner appears, preview reflects unsaved edits.
  • Manual smoke: click Open-in-chat → lands on /workspace?session=new with composer pre-filled @specs/<name>.md; navigate away and back → no stale handoff.

🤖 Generated with Claude Code

## Summary Closes #1120 — wsplit-4 toolbar additions on the focused spec editor. - **Breakdown** (`ListTodo`) button opens a right-side `<Drawer>` hosting the existing `<BreakdownPreview>` panel. The preview now runs against the **live in-memory draft**: `<SpecEditor>` mirrors its body up through a new `onDraftChange` callback and `fetchBreakdownPreview` forwards it as an optional `spec_content` field on `POST /architect/breakdown-preview` (allowlist gate unchanged). A banner inside the drawer warns when the editor is dirty. - **Open in chat** (`MessageSquare`) button stages an `@specs/<name>.md` mention in a new single-shot Zustand store (`useComposerHandoffStore`) and navigates to `/workspace?session=new`. The workspace's draft-restore effect drains the store on mount — no URL leak, no localStorage spill. The legacy `/workspace` breakdown drawer and the in-chat `/breakdown` slash command both stay; this is purely additive. ## Acceptance criteria - [x] `routes/specs.$specName.tsx` toolbar has a "Breakdown" button (lucide `ListTodo`) that opens `<BreakdownPreview>` inside a right-side `<Drawer>`. - [x] `BreakdownPreview` is invoked with the current spec body — even if dirty, uses the in-memory draft (banner warns the operator when unsaved). - [x] "Create issues" inside the drawer behaves identically to its current `/workspace` invocation (same `<BreakdownPreview>` component + props). - [x] Drawer is dismissable via Esc / backdrop click (inherited from the shared `<Drawer>` primitive). - [x] Toolbar "Open in chat" button (lucide `MessageSquare`) navigates to `/workspace?session=new`. - [x] Spec is pre-inlined as `@specs/<name>.md` mention via a transient Zustand key (`useComposerHandoffStore`) — not a URL param. - [x] On `/workspace` mount, if the handoff key holds a value, the composer draft is seeded and the key cleared. - [x] Tests: drawer opens + calls `fetchBreakdownPreview` with the spec body + renders preview rows. - [x] Tests: Open-in-chat navigates with `session=new` and handoff store holds the expected mention. - [x] Tests: workspace mount drain — `renderHook` proof that the effect consumes a staged handoff on first render. ## Out of scope (per the issue) - AI drawer integration (wsplit-3 — already landed). - Removing breakdown from `/workspace` (operator decision: both stay). ## Test plan - [x] `just qa` — typecheck + Biome + sql-layer-check + paraglide-check + 3305 server tests + 1119 web tests all pass. - [ ] Manual smoke: open `/specs/<name>` → click Breakdown → drawer shows preview rows; edit body → reopen → banner appears, preview reflects unsaved edits. - [ ] Manual smoke: click Open-in-chat → lands on `/workspace?session=new` with composer pre-filled `@specs/<name>.md`; navigate away and back → no stale handoff. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): breakdown toolbar + open-in-chat on /specs/$specName (#1120)
All checks were successful
qa / sql-layer-check (pull_request) Successful in 12s
qa / dockerfile (pull_request) Successful in 12s
qa / i18n-string-check (pull_request) Successful in 15s
qa / db-schema (pull_request) Successful in 18s
qa / qa-1 (pull_request) Successful in 3m36s
qa / qa (pull_request) Successful in 0s
61a53eafd7
Adds the wsplit-4 toolbar buttons on the focused spec editor:

- **Breakdown** (`ListTodo`) opens a right-side `<Drawer>` hosting the
  existing `<BreakdownPreview>` panel. The preview is computed from the
  live in-memory draft — `<SpecEditor>` now mirrors its body up via a
  new `onDraftChange` callback, and `fetchBreakdownPreview` forwards it
  to the server as a new optional `spec_content` field (allowlist gate
  unchanged). A banner warns the operator when the editor is dirty.
- **Open in chat** (`MessageSquare`) stages an `@specs/<name>.md`
  mention in a new single-shot Zustand store
  (`useComposerHandoffStore`) and navigates to `/workspace?session=new`.
  The workspace's draft-restore effect drains the store on mount —
  no URL leak, no localStorage spill.

The legacy `/workspace` breakdown invocation and the in-chat
`/breakdown` slash command both stay; this is purely additive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
reviewer approved these changes 2026-05-13 15:12:11 +00:00
reviewer left a comment

Review — APPROVED

CI green. All 10 ACs verified against the diff.

What this does

Adds two toolbar buttons to /specs/$specName:

  • Breakdown (ListTodo) — opens a right-side <Drawer> hosting <BreakdownPreview> driven by the live in-memory draft (not the last saved disk state). Unsaved-draft banner warns when dirty.
  • Open in chat (MessageSquare) — stages @specs/<name>.md in a new single-shot Zustand store (useComposerHandoffStore) and navigates to /workspace?session=new. The workspace mount drains the store before the localStorage fallback path.

Server gains an optional spec_content field on POST /architect/breakdown-preview; allowlist gate on spec_path is preserved even for inline content.


Positives

  • Security gate preserved — the spec_path allowlist check runs regardless of whether spec_content is supplied (both local and remote branches). No bypass possible.
  • Correct fallback orderingliveBody = draftBody || data?.content || "" handles the cold path (editor hasn't fired onDraftChange yet) without ever sending an empty string when loaded content is available.
  • Store contract is cleanconsume() atomically reads and clears; buildSpecMention is exported for test assertability; single-slot design is well-motivated in the module comment.
  • Workspace drainconsume() runs before the localStorage fallback and the early return prevents the stale-draft path from clobbering the handoff. composerFocusTrigger bump is in the right branch.
  • Test coverage — store unit tests, route integration tests (loading / success / dirty / clean), and a renderHook drain proof all land in the same PR as the feature. Good.
  • i18n — both en.json and fr.json updated; French copy is idiomatic.
  • Additive only — workspace breakdown drawer and /breakdown slash command are untouched.

Minor observations (non-blocking)

  1. Focus-trap comment on the query keybreakdownPreviewQuery keys on liveBody, so any onDraftChange while the drawer is open would fire a new request. This is safe today because <Drawer> applies a focus trap (per CLAUDE.md) so the user can't type in the editor while the drawer has focus. A one-line comment on the queryKey line noting this invariant would protect a future refactor from accidentally regressing it.

  2. Missing error-state test — the suite covers loading, success, dirty-draft, and clean-draft, but not breakdownPreviewQuery.isError. The path is a simple <p> render so the risk is low, but a rejected-mock test would complete the matrix.

  3. useState<string>("") — the type annotation is redundant given the string initialiser. Cosmetic only.

Neither observation blocks merge.

## Review — APPROVED ✅ CI green. All 10 ACs verified against the diff. ### What this does Adds two toolbar buttons to `/specs/$specName`: - **Breakdown** (`ListTodo`) — opens a right-side `<Drawer>` hosting `<BreakdownPreview>` driven by the live in-memory draft (not the last saved disk state). Unsaved-draft banner warns when dirty. - **Open in chat** (`MessageSquare`) — stages `@specs/<name>.md` in a new single-shot Zustand store (`useComposerHandoffStore`) and navigates to `/workspace?session=new`. The workspace mount drains the store before the localStorage fallback path. Server gains an optional `spec_content` field on `POST /architect/breakdown-preview`; allowlist gate on `spec_path` is preserved even for inline content. --- ### Positives - **Security gate preserved** — the `spec_path` allowlist check runs regardless of whether `spec_content` is supplied (both local and remote branches). No bypass possible. - **Correct fallback ordering** — `liveBody = draftBody || data?.content || ""` handles the cold path (editor hasn't fired `onDraftChange` yet) without ever sending an empty string when loaded content is available. - **Store contract is clean** — `consume()` atomically reads and clears; `buildSpecMention` is exported for test assertability; single-slot design is well-motivated in the module comment. - **Workspace drain** — `consume()` runs before the `localStorage` fallback and the early `return` prevents the stale-draft path from clobbering the handoff. `composerFocusTrigger` bump is in the right branch. - **Test coverage** — store unit tests, route integration tests (loading / success / dirty / clean), and a `renderHook` drain proof all land in the same PR as the feature. Good. - **i18n** — both `en.json` and `fr.json` updated; French copy is idiomatic. - **Additive only** — workspace breakdown drawer and `/breakdown` slash command are untouched. --- ### Minor observations (non-blocking) 1. **Focus-trap comment on the query key** — `breakdownPreviewQuery` keys on `liveBody`, so any `onDraftChange` while the drawer is open would fire a new request. This is safe today because `<Drawer>` applies a focus trap (per `CLAUDE.md`) so the user can't type in the editor while the drawer has focus. A one-line comment on the `queryKey` line noting this invariant would protect a future refactor from accidentally regressing it. 2. **Missing error-state test** — the suite covers loading, success, dirty-draft, and clean-draft, but not `breakdownPreviewQuery.isError`. The path is a simple `<p>` render so the risk is low, but a rejected-mock test would complete the matrix. 3. **`useState<string>("")`** — the type annotation is redundant given the string initialiser. Cosmetic only. Neither observation blocks merge.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/agent-hooks!1128
No description provided.