feat(web): /specs route — list + markdown editor (no AI drawer) #1126
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/agent-hooks!1126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "code-lead/1118"
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 #1118.
Summary
Replaces the redirect-only
specs.tsx/specs.index.tsx/specs.$specName.tsxshells with first-class list + editor surfaces so spec authoring lives on its own
page instead of riding along with
/workspace(wsplit-2)./specslayout — hosts the sticky?repo=<owner/name>selector and renders<Outlet />for the list / editor children./specslist —SpecsListfetchesspecs/*.mdviafetchArchitectFiles(repo)filtered by thespecs/root. Empty state surfaces a "New spec" button that opens a Base UI Dialog →saveSpec→ pushes the operator into/specs/<name>so they land on the editor with the file already on disk./specs/$specNameeditor — loads body viafetchArchitectFileContentand mounts the existingSpecEditor. New unsaved-changes guard wires TanStack Router'suseBlockerplus abeforeunloadhandler; the discard prompt is a Base UI Dialog (specs-discard-dialog).useSpecsKeymaphook — extracts ⌘/Ctrl+S to a window-level capture listener so save fires regardless of focus inside the markdown editor.SpecEditornow exposesonDirtyChangeso the route knows when to block navigation.specs.test.tsxcovers list render + row href + new-spec dialog flow + invalid-name error + editor body load + autosave firessaveSpec+ the navigation-guard dialog.spec-editor.test.tsxadds anonDirtyChangeassertion and re-points the Ctrl+S test atwindow(matches the new capture binding).workspace.test.tsxstill passes pre-split.Out of scope
/workspace(wsplit-5).Test plan
bun x vitest run src/routes/specs.test.tsx— 13 passing.bun x vitest run src/components/spec-editor/spec-editor.test.tsx— 8 passing.bun x vitest run src/routes/workspace.test.tsx— 9 passing (no regression).just qa(typecheck + lint + format + full test suite + sql/i18n/paraglide/flow-schema checks)./specsloads the list, "New spec" round-trip lands on/specs/<name>, ⌘+S surfaces "Saved", navigating away with unsaved edits prompts the discard dialog.🤖 Generated with Claude Code
✅ APPROVED — CI green, all AC met
Solid, well-scoped implementation. Walked through every acceptance criterion from #1118 against the diff; all pass. A few minor observations below, none of them blockers.
AC checklist
specs.index.tsxrendersspecs/*.mdlist viafetchArchitectFiles/specs/<name>?repo=<o/n>selector, both list and editor/specs/$specNamespecs.$specName.tsxmountsSpecEditorwithfetchArchitectFileContentbodysaveSpec+role="status" aria-live="polite"pillSpecEditor)useBlocker+beforeunload+ Base UI DialoguseSpecsKeymapwindow-level capture hookspecs.tsxlayout<Outlet />+validateSearchacceptsrepoAppShellchrome on both list and editor routesspecs.test.tsxcovers rows / hrefs / dialog / save / guard / clean-editorworkspace.test.tsxNotes (non-blocking)
KEBAB_REaccepts trailing and consecutive hyphensfoo-andfoo--barpass validation. The error message advertises "kebab-case" but the regex doesn't enforce no trailing/double hyphen. In practice the operator is unlikely to type those, and the resulting filenames are valid. If you want strict kebab:/^[a-z0-9]+(-[a-z0-9]+)*$/. Worth revisiting in wsplit-4 when the dialog may see more traffic.errorIdis hardcoded, notuseId()SpecEditorusesuseId()for its status region;NewSpecDialoghard-codes the id. No collision risk since the dialog is a singleton, but it's inconsistent with the component's own conventions. Trivial to fix whenever the dialog is touched next.New-spec template title is not title-cased
The test even asserts
stringContaining("# multi tenant auth")so this is deliberate, but it looks a bit rough in the editor.name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())would give "# Multi Tenant Auth".useSpecsKeymapcallsstopPropagation()in capture phaseThis means any other
windowcapture listener added later (e.g. in wsplit-3's AI drawer keymap) won't seeCmd+S. Since this surface is isolated that's fine for now, but worth a comment if wsplit-3/4 add more window-level shortcuts — a shared keymap registry likeuseWorkspaceKeymapmight be cleaner long-term.Triplicated
validateSearchspecs.tsx,specs.index.tsx, andspecs.$specName.tsxall repeat the identical{ repo: string | undefined }shape. That's the correct TanStack Router pattern (children must re-declare parent search), just noting it as a copy-paste footgun if therepofield type changes — the type won't catch mismatches between layout and children.Approved and CI is green, but
mergeable: false— there's a conflict withmainthat needs to be resolved before I can squash-merge. Please rebase ontomainand force-push the branch.22b59b20e50d99d4f073