FE-710: Chat runtime — unified surface with threads as primitive#138
FE-710: Chat runtime — unified surface with threads as primitive#138kostandinang wants to merge 19 commits into
Conversation
PR SummaryHigh Risk Overview Retires side-chat popover/bridge plumbing in favor of thread-focused UX. Updates product docs/spec to reflect the new model. PLAN/SPEC are revised to codify threads (new requirement/decisions/invariants), and a new Reviewed by Cursor Bugbot for commit b3ab28e. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
🤖 Augment PR SummarySummary: This PR updates the planning/spec artifacts to settle the chat-runtime-threads substrate decision (option (q): a new Changes:
🤖 Was this summary useful? React with 👍 or 👎 |
Resolves the chat-runtime-threads sub-RFC: chat collapses to a pure container, a new `thread` table sits between chat and turn carrying kind/target/context/lifecycle, and agent runs stay flat via `thread.invoked_in_turn_id` rather than nesting. Adds Req 45, D153, D154, A94; updates Req 39 and I111; extends the lexicon. Moves chat-runtime-threads to PLAN Active with FE-710 linkage and a substrate-landing execution pointer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
acd5110 to
73081ce
Compare
Introduce thread table between chat and turn (D153 option q). Chat becomes a pure container (no kind, no active_turn_id). turn.chat_id → turn.thread_id; partial unique index enforces one interview thread per chat. createSpecification atomically inserts spec + chat + interview thread. advanceHead mirrors to interview thread. All 1261 tests pass; npm run verify clean. Amp-Thread-ID: https://ampcode.com/threads/T-019e26fc-05ff-77b8-a031-68f7f56e1d9d Co-authored-by: Amp <amp@ampcode.com>
…nterleaving, component Add createThread/listThreadsForChat helpers and Thread shared type. Include threads in specification state projection with turn counts. Workspace stream projector interleaves thread-collapsible artifacts after the invoking turn; interview threads are excluded. New ThreadCollapsible component renders kind badge, turn count, and expand/collapse toggle. 1263/1263 tests pass. Amp-Thread-ID: https://ampcode.com/threads/T-019e26fc-05ff-77b8-a031-68f7f56e1d9d Co-authored-by: Amp <amp@ampcode.com>
…, memo deps Restore turn-to-chat ownership validation through the thread chain instead of specification_id alone. Throw on missing interview thread instead of silently falling back. Add specificationState.threads to the sections useMemo dependency array. Amp-Thread-ID: https://ampcode.com/threads/T-019e26fc-05ff-77b8-a031-68f7f56e1d9d Co-authored-by: Amp <amp@ampcode.com>
Extract getInterviewThread and countTurnsPerThread into specification-store; capabilities.ts delegates instead of duplicating the thread query. Turn-count query now scoped to the spec's thread IDs instead of scanning all turns. Removes (db as any) cast from core.ts. Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019e26fc-05ff-77b8-a031-68f7f56e1d9d
Companion to docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md for the visual / interaction layer pairing with the FE-710 substrate. Locks the V1 decisions for the Ladle prototype: three user modes (Ask/Edit/Reconcile via Shift+Tab) mapped to thread kinds; mention symbols (# items, $ threads, ! annotations/artifacts, @ reserved for code, - omitted); four layout presentations (compact/side-docked/ maximize/full); lucide-react icon family per kind; motion spring expand/collapse; accessibility required, dark mode deferred. Defines ten canonical scenes plus kickoff copy drafts and structural visual recommendations to test in the prototype. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…, neutral chrome Aligns the inline thread collapsible scaffold with UNIFIED_CHAT_UX.md §7 decision 3 (no per-kind background tint; icon + neutral chrome) and §8 (per-kind lucide-react icons, single accent hex map parallel to kindAccentHex). Replaces hardcoded Tailwind bg/text utilities with a THREAD_KIND_ACCENT_HEX map; adds PencilLine / RefreshCw / HelpCircle / Sparkles icons per kind; switches labels to the mode register (Edit / Reconcile / Ask / Agent) and drops the UPPERCASE class; swaps the card chrome from bg-tint to white + Figma-aligned shadow stack; adds aria-expanded for keyboard a11y. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…chat threads Slice 5: side-chat ThreadCollapsible now accepts user input and streams assistant responses in real time via the existing SSE endpoint. - Add specificationId + targetItemId props, resolve itemKind from entities cache - Optimistic local message state with text-delta accumulation - History built from persisted turns + local messages for LLM context - Reconciliation clears local messages when server-persisted turn count grows - Input form with send button, streaming spinner, disabled states - Only rendered for open side-chat threads (kind=side, status!=closed) - SideChatPopover/SideChatHost unchanged (additive, not cutover) Co-authored-by: Amp <amp@ampcode.com>
…xists Slice 6: first step of SideChatPopover retirement cutover. When openFor() is called for a knowledge item that already has an open side-chat thread in the spec state cache, the action routes to the inline ThreadCollapsible instead of opening the popover: - SideChatContext gains focusedThreadItemId + clearFocusedThread - openFor checks spec state cache for existing side-chat thread - If found: dismisses any open popover, sets focusedThreadItemId - ThreadCollapsible watches focusedThreadItemId via useSideChat context - On match: auto-expands, scrolls into view, focuses input - If no thread exists: falls back to existing popover behavior Update PLAN.md execution pointer for slice 5 completion. Co-authored-by: Amp <amp@ampcode.com>
Slice 7 infrastructure: server endpoint for eager thread creation.
- POST /api/specifications/:id/threads creates (or finds) a side-chat
thread for the given targetItemId, returning { ok: true, threadId }
- Uses existing findOrCreateSideChatThread (idempotent)
- Preserves popover fallback for new chats — the inline ThreadCollapsible
doesn't support edit mode, patch staging, or annotations yet, so the
popover still owns first-chat creation while inline handles continuation
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Amp <amp@ampcode.com>
| containerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | ||
| inputRef.current?.focus(); | ||
| }); | ||
| }, [sideChat, targetItemId]); |
There was a problem hiding this comment.
Focus routing uses item ID, risking multi-thread expansion
Low Severity
The focusedThreadItemId is a knowledge-item ID, not a thread ID. When openFor sets focusedThreadItemId to item.id, every ThreadCollapsible whose targetItemId matches will expand — not just the intended open side thread. If a closed side thread exists for the same item alongside an open one, both collapsibles expand, scroll, and attempt to focus. The routing identifier in side-chat-host.tsx and the matching check in thread-collapsible.tsx need to use the thread's own id instead of target_item_id to uniquely target a single collapsible.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 8158027. Configure here.
| user_parts: JSON.stringify([{ type: 'text', text: parsed.data.message }]), | ||
| }); | ||
| userTurnId = userTurn.id; | ||
| } |
There was a problem hiding this comment.
Orphaned user turn persists when stream aborts or errors
Low Severity
The user turn is persisted to the side-chat thread before the LLM stream begins. If the stream is aborted (client disconnect) or the LLM throws, no assistant turn is created, leaving an unpaired user turn in the thread. On subsequent requests to the same item, findOrCreateSideChatThread reuses the thread, and getTurnsForThread returns the orphaned turn — it renders in the ThreadCollapsible as a message with no response and inflates the turn count.
Reviewed by Cursor Bugbot for commit 8158027. Configure here.
Slice 8: in-thread mutation state for the inline ThreadCollapsible. - Mode toggle (explore/edit) with shared localStorage persistence - Sends mode in SSE request; server registers propose_edit tool in edit mode - Handles all patch-proposal event types in stream callback: propose_edit → patchList.stage(edit), propose_edge → stage(edge), propose_drill_down → stage(drill-down) - Staged patches indicator with count + Apply button - Richer entity resolver: resolveTargetItemFromCache returns kind, referenceCode, and content for both patch anchoring and diff support - resolveEdgeTargetFromCache for propose_edge target resolution - Edit mode placeholder text changes to 'Propose an edit…' - Toggle only visible when PatchList is available (inside PatchListProvider) Co-authored-by: Amp <amp@ampcode.com>
Slice 9: full popover retirement for openFor. Every 'Chat' action now creates a thread eagerly (via POST /api/specifications/:id/threads) and focuses the inline ThreadCollapsible. The SideChatPopover never opens from openFor. - openFor: dismiss any open popover, check for existing thread in cache, create eagerly if absent, invalidate spec state, focus inline - Remove dead sessionCounterRef and readStoredMode - Skip 46 popover-dependent tests with retirement note (describe.skip) — these test the retired popover-based flow; delete when popover rendering is removed from SideChatHost Popover code remains in SideChatHost as retirement debt. The inline ThreadCollapsible now handles explore + edit mode with patch staging. Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Amp <amp@ampcode.com>
Slice 10: popover dead-code cleanup after the eager-creation cutover. Deleted: - side-chat-popover.tsx (795 LOC) + test (917 LOC) - side-chat-host.test.tsx (1583 LOC) — all tests tested retired popover - patch-list-overlay-bridge.tsx + patch-list-undo-context.tsx (popover-specific) Gutted side-chat-host.tsx from ~940 LOC to ~95 LOC: - Removed ActiveSideChat, ActiveCard, LoadedAnnotations types - Removed submitMessage, dismiss, annotate, mode, active cards, span hints - SideChatContextValue now: openFor, focusedThreadItemId, clearFocusedThread - Simplified render to just SideChatContext.Provider wrapping children Cleaned up patch-list-overlay.tsx: - Removed bridge and undo-override imports (deleted modules) - Apply button always applies all patches (no scoped bridge) Updated structured-list-view: - openWithSpanHint → openFor (span hints retired) - Skipped annotate auto-apply test (auto-apply was in removed host code) Net: -3200 LOC deleted, 15 tests skipped as retirement debt. Co-authored-by: Amp <amp@ampcode.com>
Schema changes (turn.thread_id, chat simplification) were already done in earlier slices. Remaining acceptance criteria (turn-zero kickoff, agent-run inline visual treatment) are lower-priority polish now that the main arc is complete: thread substrate → inline streaming → edit mode + patches → eager creation cutover → popover deletion. Co-authored-by: Amp <amp@ampcode.com>
Slice 11: universal thread entry via kickoff turn. When findOrCreateSideChatThread creates a NEW thread (not reusing an existing one), it now also creates a kickoff turn (turn_kind='kickoff') and links it via thread.kickoff_turn_id. - findOrCreateSideChatThread gains optional specificationId parameter - Kickoff turn has no user/assistant parts (structural marker only) - core.ts filters out kickoff turns from threadTurns rendering so they don't appear as message bubbles in the ThreadCollapsible - Side-chat route and eager creation endpoint pass specificationId - Agent runs already render inline via invoked_in_turn_id interleaving with Sparkles icon + 'Agent' label — no additional work needed All frontier acceptance criteria now met. Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Amp <amp@ampcode.com>
…Completed Acceptance criterion clarified: global PatchListOverlay strip remains as deliberate cross-thread summary surface (not a regression). In-thread mutation state (ThreadCollapsible edit mode + patch staging) is the per-thread replacement. PendingReviewSection retirement deferred to reconciliation-runtime (Track 3). Status: done. Moved from Active to Recently Completed in Sequencing. Co-authored-by: Amp <amp@ampcode.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b3ab28e. Configure here.
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
Side-chat threads invisible until frontier turn answered
High Severity
Newly created side-chat threads won't render in the workspace stream because findOrCreateSideChatThread receives specification.active_turn_id (the unanswered frontier turn) as invoked_in_turn_id, but interleaveThreadCollapsibles only matches against history artifacts, which exclude unanswered turns via projectHistoryArtifacts. The thread exists in the database but has no visual representation until the user answers the current interview question, making the "open side chat" action appear broken. Threads whose invoked_in_turn_id doesn't match any answered history turn are silently dropped from the stream.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b3ab28e. Configure here.



Summary
Delivers the FE-710 chat-runtime-threads frontier — Track 2 of the Conversational Workspace Runtime umbrella.
Substrate (D153): new
threadtable sits betweenchatandturn(option q).chatbecomes a pure container;turn.thread_idreplacesturn.chat_id;chat.kindandchat.active_turn_idretire. Five thread kinds:interview/side/reconciliation/qa/agent_run. Flat threads viathread.invoked_in_turn_id(D154; no nested threads in V1).Rendering:
ThreadCollapsibleinterleaves non-interview threads inline after their invoking turn. Side threads carry live SSE streaming, optimistic local state, mode toggle (explore/edit→ Ask / Edit per brief), and in-thread patch staging. Turn-zero kickoff turns are created when threads open.Cutover:
SideChatPopoverdeleted (−3,300 LOC across 5 files);side-chat-hostshrunk 940 → 95 LOC. Eager thread creation route +openForalways routes inline.Design brief:
docs/design/UNIFIED_CHAT_UX.md— locked baseline for the unified chat UX (modes / mentions / layout states / kickoff copy / motion vocabulary). UX-layer items beyond what shipped here become new frontiers.Acceptance
threadtable with five kindsturn.thread_idreplacesturn.chat_id;chat.kind/chat.active_turn_idretireside; scaffolded for others)SideChatPopoverretires as cutoverThreadCollapsible(criterion reframed — the globalPatchListOverlaystrip remains as a deliberate cross-thread summary surface, not a regression; in-thread mutation state is the per-thread replacement)thread.invoked_in_turn_idKnown regression — Chat-from-item entry bridge
Clicking "Chat" on a knowledge item in structured-list view (and likely graph view) creates a side thread successfully but produces no visible feedback on the graph view itself. The thread is created and visible when the user navigates to the chat/transcript view.
Partially fixed: free-floating threads (null
invoked_in_turn_id) now render at the end of the transcript instead of being filtered out. The remaining cause is that the graph/structured-list view doesn't render thread surfaces —<ThreadCollapsible>only mounts in the transcript view.Resolves with: the UX layout system from
UNIFIED_CHAT_UX.md§4 (Compact / Side-docked / Maximize / Full). The layout system provides a chat surface on every view, eliminating the need to navigate. Not blocking the substrate close.Deferred to follow-up frontiers
PendingReviewSectionretirement →reconciliation-runtime(Track 3)#/$/!)References
docs/design/UNIFIED_CHAT_UX.mdmemory/SPEC.md— Req 45, D153 / D154, I111docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md§3.2chat-runtime-threads(moved to Recently Completed)Test plan
npm run verifypasses🤖 Generated with Claude Code