Skip to content

FE-710: Chat runtime β€” unified surface with threads as primitive#138

Closed
kostandinang wants to merge 21 commits into
ka/fe-709-continuous-workspacefrom
ka/fe-710-chat-runtime-threads
Closed

FE-710: Chat runtime β€” unified surface with threads as primitive#138
kostandinang wants to merge 21 commits into
ka/fe-709-continuous-workspacefrom
ka/fe-710-chat-runtime-threads

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented May 14, 2026

Summary

Delivers the FE-710 chat-runtime-threads frontier β€” Track 2 of the Conversational Workspace Runtime umbrella.

Substrate (D153): new thread table sits between chat and turn (option q). chat becomes a pure container; turn.thread_id replaces turn.chat_id; chat.kind and chat.active_turn_id retire. Five thread kinds: interview / side / reconciliation / qa / agent_run. Flat threads via thread.invoked_in_turn_id (D154; no nested threads in V1).

Rendering: ThreadCollapsible interleaves 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: SideChatPopover deleted (βˆ’3,300 LOC across 5 files); side-chat-host shrunk 940 β†’ 95 LOC. Eager thread creation route + openFor always 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

  • thread table with five kinds
  • turn.thread_id replaces turn.chat_id; chat.kind / chat.active_turn_id retire
  • Threads render inline as collapsibles (interactive for side; scaffolded for others)
  • SideChatPopover retires as cutover
  • In-thread mutation state in ThreadCollapsible (criterion reframed β€” the global PatchListOverlay strip remains as a deliberate cross-thread summary surface, not a regression; in-thread mutation state is the per-thread replacement)
  • Turn-zero universal entry across kinds
  • Agent runs render inline via thread.invoked_in_turn_id

Known 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

  • PendingReviewSection retirement β†’ reconciliation-runtime (Track 3)
  • Mention chip + autocomplete (# / $ / !)
  • Layout state header control (Compact / Side-docked / Maximize / Full); default = Side-docked
  • "Reconcile Now" sidebar affordance
  • Reconciliation thread in-stream UX (target-grouped, classifier states)
  • QA thread composer UX
  • Ladle design prototype (scenes A / B / C / D)

References

  • Linear: FE-710
  • Design brief: docs/design/UNIFIED_CHAT_UX.md
  • Substrate decisions: memory/SPEC.md β€” Req 45, D153 / D154, I111
  • Umbrella: docs/design/CONVERSATIONAL_WORKSPACE_RUNTIME.md Β§3.2
  • PLAN.md frontier: chat-runtime-threads (moved to Recently Completed)

Test plan

  • npm run verify passes
  • Manual: create a spec, advance through grounding turns, open a side thread on an item, send messages, verify streaming + persistence
  • Manual: edit an item with impact > soft, verify staged patches appear in-thread
  • Manual: refresh mid-stream, confirm history reconstructs correctly
  • Manual: agent-run thread renders inline with Sparkles chip when invoked
  • Known regression β€” "Chat" from structured-list view does not surface the thread (see above)

πŸ€– Generated with Claude Code

@cursor
Copy link
Copy Markdown

cursor Bot commented May 14, 2026

PR Summary

High Risk
High risk because it changes core persistence schema (thread table, turn.thread_id, chat reshaping) and removes the existing side-chat UI/bridging logic, which can impact data migrations and chat/edit flows across the app.

Overview
Delivers the thread substrate by adding a new thread table (with interview/side/reconciliation/qa/agent_run kinds), migrating turn records to reference thread_id, and simplifying chat to a container-only table; turn.phase is also made nullable for non-interview threads.

Cuts over the UI away from SideChatPopover by deleting the popover/host tests and removing the overlay scoping/undo bridge (PatchListOverlayBridgeProvider, PatchListUndoProvider), leaving PatchListOverlay to always apply/undo globally and shrinking SideChatHost to an inline-thread launcher that creates/focuses side threads.

Updates planning/spec docs (memory/SPEC.md, memory/PLAN.md) and adds a new UX design brief (docs/design/UNIFIED_CHAT_UX.md) to define the unified inline-thread chat surface.

Reviewed by Cursor Bugbot for commit 5a9ef97. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown
Contributor Author

kostandinang commented May 14, 2026

@kostandinang kostandinang changed the title FE-710: Settle thread substrate as option (q) in SPEC/PLAN FE-710: Chat runtime β€” unified surface with threads as primitive May 14, 2026
@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 14, 2026

πŸ€– Augment PR Summary

Summary: This PR updates the planning/spec artifacts to settle the chat-runtime-threads substrate decision (option (q): a new thread table between chat and turn) and track the work under FE-710.

Changes:

  • Moves chat-runtime-threads to memory/PLAN.md Active and links it to FE-710, updating objective/acceptance/traceability.
  • Adds an explicit β€œcurrent execution pointer” describing the initial substrate-landing slice (schema + migration, no UI cutover).
  • Extends memory/SPEC.md with Requirement 45 and updates Requirement 39 to β€œone chat per specification; multiple threads per chat”.
  • Adds Decisions 153–154 and Assumption A94 to capture the chosen thread model and the β€œflat threads + invoked_in_turn_id” stance.
  • Updates I111 and the lexicon to reflect chat as a pure container and thread as the conversational-scope primitive.

πŸ€– Was this summary useful? React with πŸ‘ or πŸ‘Ž

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. No suggestions at this time.

Comment augment review to trigger a new review at any time.

@kostandinang kostandinang force-pushed the ka/fe-710-chat-runtime-threads branch from acd5110 to 73081ce Compare May 14, 2026 15:12
Comment thread src/server/capabilities.ts Outdated
Comment thread src/server/capabilities.ts
Comment thread src/server/db.ts Outdated
Comment thread src/server/core.ts
Comment thread src/client/components/thread-collapsible.tsx
Comment thread src/server/side-chat-route.ts
@kostandinang kostandinang changed the base branch from ka/fe-709-continuous-workspace to graphite-base/138 May 15, 2026 11:23
@kostandinang kostandinang requested a review from lunelson May 15, 2026 11:27
@kostandinang kostandinang force-pushed the ka/fe-710-chat-runtime-threads branch from b3ab28e to 1d21d5c Compare May 15, 2026 14:02
@kostandinang kostandinang changed the base branch from graphite-base/138 to ka/fe-709-continuous-workspace May 15, 2026 14:03
kostandinang and others added 14 commits May 15, 2026 16:06
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>
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>
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>
kostandinang and others added 7 commits May 15, 2026 16:06
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>
…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>
Two stacked fixes for the known regression where clicking 'Chat' on a
knowledge item in structured-list view produced no visible feedback:

1. Stream projector: free-floating threads (null invoked_in_turn_id)
   now append at the end of the transcript instead of being filtered
   out. Covers eagerly-created threads from specs with no active turn.

2. Structured-list view: ItemActionRail navigates to the chat view
   (/specification/$id β†’ redirects to current phase) after openFor,
   so the ThreadCollapsible is visible. focusedThreadItemId handles
   scroll + focus once it renders.

Amp-Thread-ID: https://ampcode.com/threads/T-019e2841-4e0a-7208-8a96-6877a33cc868
Co-authored-by: Amp <amp@ampcode.com>
- Stream projector: threads with null invoked_in_turn_id append at the
  end of the transcript instead of being filtered out
- Remove floating panel from SideChatHost β€” the chat-from-graph-view
  entry bridge is a known regression that resolves when the UX layout
  system (UNIFIED_CHAT_UX.md Β§4) lands
- Revert structured-list navigation (no route change on Chat click)

Amp-Thread-ID: https://ampcode.com/threads/T-019e2841-4e0a-7208-8a96-6877a33cc868
Co-authored-by: Amp <amp@ampcode.com>
@kostandinang kostandinang force-pushed the ka/fe-710-chat-runtime-threads branch from 1d21d5c to 5a9ef97 Compare May 15, 2026 14:06
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5a9ef97. Configure here.

// and how many of them we mark `inContext` in the rendered thread. Single source of truth
// referenced from both the request builder and the threadItems derivation.
const MAX_ACTIVE_ANNOTATIONS = 8;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition allows duplicate thread creation on rapid clicks

Medium Severity

The openFor callback checks the query cache synchronously for an existing open thread, then fires an async fetch to create one if none is found. There is no in-flight guard, so rapid clicks (or multiple calls before the first fetch resolves and invalidateQueries propagates) will each see no existing thread in the stale cache and each create a new one. The old SideChatHost used activeRef and session tracking to prevent this. The new code has no debounce, no optimistic local state, and no ref-based in-flight flag, so duplicate side threads for the same item can be created.

Fix in CursorΒ Fix in Web

Reviewed by Cursor Bugbot for commit 5a9ef97. Configure here.

(t) => t.kind === 'side' && t.target_item_id === item.id && t.status === 'open',
);
if (existingThread) {
setFocusedThreadItemId(item.id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Focus signal uses item ID, expanding multiple collapsibles

Low Severity

focusedThreadItemId is set to item.id (the knowledge-item ID), not the thread's own ID. In ThreadCollapsible, the focus effect matches on targetItemId. If multiple threads share the same target_item_id (e.g. a closed side thread and a newly opened one for the same item), all matching ThreadCollapsible instances will call setIsExpanded(true) in the same React batch before clearFocusedThread propagates, causing all of them to expand instead of only the intended one.

Additional Locations (1)
Fix in CursorΒ Fix in Web

Reviewed by Cursor Bugbot for commit 5a9ef97. Configure here.

@kostandinang
Copy link
Copy Markdown
Contributor Author

Closing without merge. Substrate decision reversed per #139: schema-level thread is deferred and sub-chats will use the existing chat substrate instead. The Linear issue this PR resolved (FE-710) is cancelled and superseded by FE-716.

This PR is retained as historical reference β€” the UX work (inline collapsibles, popover retirement, design brief, kickoff turn, streaming, mode toggle, patch staging) will be harvested into the new branch on top of #139 with the corrected substrate.

kostandinang added a commit that referenced this pull request May 15, 2026
Copy docs/design/UNIFIED_CHAT_UX.md verbatim from PR #138 as the in-tree
UX ceiling. Body unedited per user directive; a prepended FE-716 reading
note documents vocabulary (thread = secondary chat) and substrate (D153
defers a schema-level thread table; FE-716 uses chat columns + existing
chat.kind).
kostandinang added a commit that referenced this pull request May 15, 2026
Copy docs/design/UNIFIED_CHAT_UX.md verbatim from PR #138 as the in-tree
UX ceiling. Body unedited per user directive; a prepended FE-716 reading
note documents vocabulary (thread = secondary chat) and substrate (D153
defers a schema-level thread table; FE-716 uses chat columns + existing
chat.kind).

Co-authored-by: Amp <amp@ampcode.com>
kostandinang added a commit that referenced this pull request May 20, 2026
Copy docs/design/UNIFIED_CHAT_UX.md verbatim from PR #138 as the in-tree
UX ceiling. Body unedited per user directive; a prepended FE-716 reading
note documents vocabulary (thread = secondary chat) and substrate (D153
defers a schema-level thread table; FE-716 uses chat columns + existing
chat.kind).

Co-authored-by: Amp <amp@ampcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant