FE-656: Side-chat V1.1 β design spec + Explore vertical slice#81
Conversation
FE-656 Side chat
Problem / Motivation
Acceptance criteria:
|
This stack of pull requests is managed by Graphite. Learn more about stacking. |
9183dc1 to
3f1db1e
Compare
PR SummaryMedium Risk Overview On the server, introduces a new side-chat route with Zod validation, spec/item resolution, prompt construction ( Reviewed by Cursor Bugbot for commit 9a94392. Bugbot is set up for automated code reviews on this repo. Configure here. |
π€ Augment PR SummarySummary: Adds a proposed design spec for the new side-chat surface (FE-656) and ships the V1.1 βExploreβ vertical slice end-to-end. Changes:
Technical Notes: The side-chat stream is intentionally separate from the main interview workflow (preserving D113 invariants) and currently implements Explore-only behavior with pending/error message states on the client. π€ Was this summary useful? React with π or π |
Addresses Cursor + Augment review feedback on PR #81: - SideChatHost: tag each session with an id, hold an AbortController ref, abort on openFor/dismiss/unmount, and gate stream callbacks on the captured sessionId so stale chunks can't corrupt a swapped pinned item. Async work moved out of the setActiveSideChat updater. - streamSideChatResponse: throw when response.body is null instead of silently returning, so callers surface an error state. - Server SSE: AbortController wired to res 'close', abortSignal threaded into streamText so model generation halts on client disconnect. - side-chat-route: switch zod import to 'zod/v4' to match the rest of the codebase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Cursor review feedback on PR #81: the UI rendered a multi-turn thread but each request only carried the latest message, so the LLM had no memory of prior exchanges. - Server schema accepts an optional history: { role, text }[] array; empty-text entries are rejected. - buildSideChatPrompt threads prior turns into the messages array, with the pinned-item context still anchored on the first user turn and the new user message appended at the end. - streamSideChatResponse forwards history to the route. - SideChatHost collects finalized turns (pending + error placeholders filtered out) and sends them on every submit. Tests cover the new shape end-to-end: prompt builder, route schema + forwarding, streaming-helper body, and a multi-turn scenario in the SideChatHost integration test (including error-retry hygiene). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
β Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cedca3a. Configure here.
Merge activity
|
Adds docs/design/SIDE_CHAT.md as the canonical design for the side-chat β a popover-to-panel chat anchored to spec items in the structured spec view, with patch-list staging in the persistent app top-bar. Subsumes the prior trigger-popover composer, revisit / edit mode, and D128 chat-with seam into one user-driven mutation surface. memory/PLAN.md - Replace trigger-popover composer (Next) with side-chat - Remove revisit / edit mode + cascade preview from Horizon (subsumed) - Add architect / generator loop to Horizon - Update dependency graph memory/SPEC.md - Add Requirement 34 (side-chat surface) - Add A71 (patch / event-stream model), A72 (item versioning), A73 (architect loop) β status: future - Add D130 (side-chat as unified user-driven mutation surface) - Add D131 (patch list canonical staging in top-bar) - Soften D80 to acknowledge chat-level branching and replace the modal secondary thread with the side-chat panel mode docs/design/REVISIT_MODULE.md - Mark as subsumed; cascade lifecycle remains valid as the V3 hard-edit path inside the side-chat panel Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each version introduces a meaningfully different lifecycle/persistence seam, passing the anti-fragmentation test as separate frontier items: - V1 (Next 4): panel surface + Explore + Annotate. Establishes the popover-to-panel chat, top-bar patch summary scaffold, comment-store extension for annotations, and the floating selection menu. - V2 (Next 5): Edit / Drill-down / Propose-edge. Activates cross-surface intent emission to the turn machinery and D125's typed relation policy. Soft-impact edits apply directly via soft-recompute; hard-impact defers to a placeholder until V3. - V3 (Horizon): Hard edit absorbs REVISIT_MODULE β cascade preview inline, batch-resolution secondary-thread mode in the panel. V1 stays in one branch (FE-656 currently); subsequent versions get their own Linear issues + branches when their turn arrives. Updates Track A dependency graph and the leading rationale paragraph.
V1.1's vertical slice decomposes into 5 sub-cards (A: prompt builder, B: backend endpoint, C: popover skeleton, D: end-to-end wiring, E: polish). A, B, C are independently scopable now; D and E are tentative anchors to re-scope after AβC land. Captured here so the next /ln-build session has a ready run-list with acceptance criteria and promotion checklists per card.
Pure `buildSideChatPrompt(item, message, specContext)` returning the
`{ system, messages }` payload that biases the model to discuss the
pinned spec item without injecting interviewer phase-stage tone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires `POST /api/specifications/:id/side-chat` as a thin I/O shell: validates spec + (itemKind, itemId) lookup, builds the prompt via the Card A pure function, and streams the Anthropic response as SSE. The route never enters the chat-route transition or observer paths, so the D113 zero-turns / zero-observer invariant holds structurally without needing a new seam-level rule on top of D113. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Controlled popover surface with a pinned-item header, empty message log, message input (auto-focused on mount), disabled-when-empty send button, and a close button placed DOM-last so the input is the first focusable element in tab order. Esc and click-outside both fire onDismiss; Tab is trapped in both directions. Inside-out skeleton only β graph view wiring (Card D) and persistence + anchoring (Card E) re-scope on top of this surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Activates the previously-disabled chat-with button on every item row in
the structured-list view (when specificationId is provided). Clicking
mounts SideChatPopover anchored to the row; typing and submitting a
message POSTs to /api/specifications/:id/side-chat; the SSE response
streams chunk-by-chunk into the popover's message log.
Inside-out:
- side-chat-stream.ts pure parser handles cross-chunk reassembly of
the `data: {...}\n\n` ... `data: [DONE]` framing emitted by Card B.
- streamSideChatResponse async helper drives the fetch + ReadableStream
reader chain, accepts an injected fetch for unit tests.
- SideChatPopover skeleton from Card C extended with messages,
pendingAssistantText, and onSubmit props; Enter submits, Shift+Enter
inserts a newline; the input clears after submit and the send button
is disabled while a stream is in-flight.
- StructuredListView owns the active-popover state, threads onChatWith
to ItemActionRail, and accumulates streamed deltas into pendingText.
Bumps the build-boundary test timeout from 30s to 60s β under suite
load with the new lib + component + test files, two back-to-back real
Vite builds were exceeding the 30s envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SideChatPopover dropped the parallel `pendingAssistantText: string | null` prop; SideChatMessage gained optional `pending?: true`. The popover now derives `isStreaming` from `messages.some(m => m.pending)` and renders the pending row inline with the rest of the message list. StructuredListView's orchestration composes the new shape: submit appends a pending assistant message, deltas mutate its text via `replacePendingText`, and completion drops the pending flag (or removes empty pending messages) via `finalizePending`. Eliminates the type-system-meaningless "messages ending in assistant turn + non-null pendingAssistantText" combination noted in the V1.1 review. Behavior-preserving β same 687 tests pass, no semantic change. First commit of the side-chat session boundary refactor; the second commit will extract a SideChatHost provider so the activation gate becomes a tree-mount fact rather than a derived optional prop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦ StructuredListView The new SideChatHost component owns the active-session state, the streaming side-effect, and the popover render; descendants read the open callback via a useSideChat() hook backed by React context. StructuredListView loses its specificationId prop, its activeSideChat state, the submitSideChatMessage orchestration with its replacePendingText and finalizePending helpers, and the inline popover mount. ItemActionRail consults useSideChat() directly: present and non-null β button is active; absent or null β disabled placeholder. graph.tsx wraps the structured list in SideChatHost. The activation gate is now a tree-mount fact rather than an optional-prop derivation chain β the type system can no longer represent the previously-allowed almost- active states. Closes out the V1.1 review-driven refactor (findings #1, #2, #4). Behavior-preserving β same 687 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦ V1.1 polish ln-sync: - Drop completed Active items: workflow ownership extraction (FE-616 landed 2026-04-29) and graph view structured-list (FE-643 landed 2026-04-30, closed by V1.1 chat-with activation). - Add V1.1 / FE-643 / FE-616 to Recently Completed; archive older Distribution hardening + JSON payload entries to PLAN_HISTORY. - Promote Side-chat V1 to the single Active frontier item (V1.1 done on branch, V1.2 Annotate + Card E polish remain). - Update intro paragraph and Dependencies diagram. ln-scope: - Replace the V1.1 A/B/C/D queue (exhausted) with the V1.1 polish queue: E1 (lift SideChatHost to spec-level layout) Β· E2 (corner-anchored popover per design doc Β§11.5) Β· E3 (render side-chat errors). Each is a light scope card; row-anchoring explicitly deferred to V2/V3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦yout The host moves from graph.tsx up to route.tsx so an open side-chat session survives in-spec navigation (e.g. graph β grounding β graph round-trip). graph.tsx renders StructuredListView directly; the host wraps the spec workspace's <Outlet /> instead. Existing structured-list-view tests continue to pass β they wrap the view in <SideChatHost> at the test boundary, so the move is invisible to the test surface. Matches design doc Β§2: "the panel persists for the spec session; navigating within the spec preserves the panel and its thread." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦tates
E2 β anchor the popover as a fixed top-right corner panel per design
doc Β§11.5:
- fixed top-4 right-4 z-50 w-[360px], rounded-2xl, backdrop-blur,
border-rule + ring-foreground/5 + shadow-xl
- message rows align user-right / assistant-left; close button
absolute-positioned top-right inside the dialog
- data-side-chat-anchor="top-right" attribute for selection
- row-anchoring (popover follows the clicked row through scroll)
explicitly deferred to V2/V3 per Β§11.5
E3 β render stream rejections as a visible error in the message log:
- SideChatMessage gains optional `error?: true`; popover renders
error rows with bg-red-50 / text-red-900 / ring-red-200 and
data-message-error="true"
- SideChatHost's catch block replaces the pending row with an
error-flagged assistant message ("Something went wrong β try again.")
via a new failPending helper, clears pending, re-enables sending
Combined into one commit because both touch SideChatPopover and the
error-row class branch reuses the shared className computation
introduced by the corner-anchor styling. CARDS.md queue exhausted β
deleting the file per ln-build retire-derivative-artifacts discipline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Cursor + Augment review feedback on PR #81: - SideChatHost: tag each session with an id, hold an AbortController ref, abort on openFor/dismiss/unmount, and gate stream callbacks on the captured sessionId so stale chunks can't corrupt a swapped pinned item. Async work moved out of the setActiveSideChat updater. - streamSideChatResponse: throw when response.body is null instead of silently returning, so callers surface an error state. - Server SSE: AbortController wired to res 'close', abortSignal threaded into streamText so model generation halts on client disconnect. - side-chat-route: switch zod import to 'zod/v4' to match the rest of the codebase. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses Cursor review feedback on PR #81: the UI rendered a multi-turn thread but each request only carried the latest message, so the LLM had no memory of prior exchanges. - Server schema accepts an optional history: { role, text }[] array; empty-text entries are rejected. - buildSideChatPrompt threads prior turns into the messages array, with the pinned-item context still anchored on the first user turn and the new user message appended at the end. - streamSideChatResponse forwards history to the route. - SideChatHost collects finalized turns (pending + error placeholders filtered out) and sends them on every submit. Tests cover the new shape end-to-end: prompt builder, route schema + forwarding, streaming-helper body, and a multi-turn scenario in the SideChatHost integration test (including error-retry hygiene). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Kostandin Angjellari <kostandinang@users.noreply.github.com>
Co-authored-by: Kostandin Angjellari <kostandinang@users.noreply.github.com>
The previous merge resolution put provider-setup decisions at D132-D135, but PR #88 (ka/fe-656-side-chat-v1-2, stacked above this branch) already uses D132 for the PatchListProvider module and D133 for the new annotation entity with side-chat semantics. Shift provider items to D134-D137 so PR #88's existing numbering can ride through the rebase without a new conflict. - D134..D137 = first-run setup / provider seam / UI credentials / gitignore - A74-A76 dependency refs updated - I106/I107 traceability refs updated - PLAN.md horizon traceability refs updated Verified: npm run verify (726 tests pass, build clean).
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
6133314 to
9a94392
Compare
β¦ity, queue cards Kicks off V1.2 (Side-chat Annotate vertical slice) on a stacked branch over V1.1's PR #81. The /ln-design synthesis settled the patch-list module shape and the annotation entity model; /ln-scope queued the three vertical-slice cards (server seam, client module, end-to-end wiring) in CARDS.md. memory/SPEC.md - Add D132: patch-list module is React-context-native publicly, event-log- shaped internally. Provider + 3 hooks; closed discriminated union for patch kinds; useReducer over PatchEvent log; appliers prop forces V2 kinds to typecheck-fail at mount until supplied. Internal events shaped to match A71's eventual server-side primitive β migration is reducer swap, not API rewrite. - Add D133: annotation is a new durable entity with its own table. Item- anchored in V1; `selection_start`/`selection_end` columns are part of the schema from day one but stay NULL until V2/V3 lights up span anchors. Supersedes the optimistic "comment-store extension" framing β the spike showed there is no prior comment store; per-turn `itemComments` on review responses is a separate seam and stays unchanged. memory/PLAN.md - Update Active 1 to reflect V1.2 in progress on `ka/fe-656-side-chat-v1-2`, reference the CARDS.md vertical-slice queue, and correct the "comment- store extension" framing per D133. - Add D132 / D133 to traceability; record both branches under Linear FE-656. memory/CARDS.md (new) - Card A: annotation server seam β drizzle migration, table with FK + cascade, POST/GET/DELETE endpoints, server-only tests. - Card B: PatchListProvider client module β sibling to SideChatHost, hooks for mutations + reactive reads + filtered selectors, internal reducer + pure fold tests. - Card C: end-to-end annotate wiring β annotate composer in side-chat header, in-panel inline patch list, in-panel Apply hits the new endpoint, Undo round-trips. Top-bar canonical UI, floating selection menu, and multi-pin re-scope after Card C lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
https://www.loom.com/share/b0d96bc2f0a849d8b57b043bb648a9aa ## Stack Context This PR is the side-chat **V1.2 vertical slice** under FE-656, stacked on **PR #81** (V1.1 β Class 1 Explore: chat anchored to spec items). V1.1 proved the chat seam; **V1.2 proves the durable mutation seam** β the side-chat can now produce real, persistent changes to a spec. PR #88 is sized as one coherent slice. **V1.2-D** (top-bar canonical Apply/Undo), **V1.2-E** (floating selection menu), and **V1.2-F** (multi-item pinning) are deferred to follow-up PRs per `memory/PLAN.md`. ## What End-to-end Annotate flow: the user opens the side-chat on a knowledge item, types a note, hits Save, and it persists to the database. Existing notes for the pinned item render above the chat log as a collapsible `Notes` section. The slice lands as three cards plus follow-on UX work: - **A** β Annotation server seam: new `annotation` table + REST endpoints. - **B** β `PatchListProvider` client module: React provider + hooks for staging and applying patches. - **C** β End-to-end wiring: side-chat composer talks to the patch list and the server. - **Auto-apply + UX coherence** β user-typed annotations save on submit (no extra Apply click); three clear states for in-flight / saved / failed. - **C2 β Show existing annotations** β collapsible `Notes` section above the chat log. ## Why V1.1 vertically proved the chat seam end-to-end (graph β side-chat β SSE β streaming reply). Without V1.2, the side-chat is read-only β Class 4 Annotate, V2 Edit / Drill-down / Propose-edge, and V4 architect-loop emissions all need somewhere to write. V1.2 establishes that surface. Two design choices worth attention before merge: ### D132 β Patch-list module shape `/ln-design` explored four shapes in parallel (minimal-API, plug-in registry, event-log-public, react-context-native). The synthesis takes the React-native public surface (mirrors `SideChatHost` exactly) but uses an event-log internal primitive. Rationale: - **Idiomatic for this codebase** β Zero contributor-onboarding cost. - **A71-ready internally** β When the future `appendPatch(spec, patch[])` server primitive lands, migration is "swap the reducer," not "rewrite the public API." - **Closed discriminated union** β V2 patch kinds force a typecheck failure at the provider mount until their applier is supplied. Silent drift impossible. ### D131 β User-driven annotations auto-apply Originally framed as "uniform staging required for all patch kinds." User feedback during this PR pushed back: re-reviewing what you just typed adds nothing for annotations. Distinction is now explicit in D131: - **Annotate** (user-driven, low-stakes, just-typed) β auto-apply on Save. The patch list still records the patch, surfaces it transiently, and provides Undo on the resulting batch. - **Edit / Drill-down / Propose-edge** (V2, mutates durable content) β keep the explicit Apply step. Review-before-commit matters when the operation has cascading effects. - **Architect-loop emissions** (V4, system-driven) β keep the explicit Apply step. The user wasn't in the moment of authoring; review is the whole point. The patch-list module supports both paths β kind-specific behavior is wired in `SideChatHost` via the auto-apply `useEffect`, not in the module itself. ## Spec / plan deltas `memory/SPEC.md`: - **D132** β patch-list module shape (React-context-native public, event-log-shaped internally) - **D133** β annotation as a new durable entity, item-anchored in V1, span-anchor-ready in schema - **D131** revised β adds the user-driven vs review-required distinction; supersedes the "uniform staging required" framing `memory/PLAN.md`: - Active 1 reflects V1.2 vertical slice landed; V1.2-D / E / F still owed. - Earlier "comment-store extension" framing corrected β D133 supersedes it. ## Stacked on - **PR #81** (`ka/fe-656-side-chat`, V1.1 Explore) β must merge first. - **PR #82** (`ka/fe-656-side-chat_v4`, V4 patch / event-stream data model) β sibling on the same parent, independent of V1.2. ## Linear [FE-656](https://linear.app/hashintel/issue/FE-656) β same issue as V1.1 (one Linear issue per frontier item per `CLAUDE.md`). ## Test plan **Automated** β `npm run verify` passes (793/793 tests, includes the 9 V1.1-polish tests merged in). **Manual walkthrough** (in browser, dev server running): - [ ] **Happy path.** Open a spec β graph view β click `π¬` on any item row β click `Annotate` β type Summary + Body β click `Save`. Expect: brief `Saving annotationβ¦`, then `β Annotation saved` with Undo. New note appears in the `Notes (1)` section above the chat log. - [ ] **Persistence.** Reload the page. Re-open chat on the same item. Note still there. - [ ] **Multiple annotations.** Save a second note on the same item. `Notes (2)` lists both, each independently expandable. - [ ] **Cross-item isolation.** Save a note on item A, switch chat to item B. B's panel does not show A's notes. - [ ] **Undo.** Save a note β click Undo. Notes section refetches; the just-undone note is gone. - [ ] **Composer validation.** `Save` disabled until both Summary and Body are non-empty (whitespace-only doesn't count). - [ ] **Cancel.** `Esc` or `Cancel` button in composer returns to chat without saving. - [ ] **Streaming exclusivity.** While a chat reply is streaming, the `Annotate` button is disabled. - [ ] **Notes collapse.** Click the `Notes (N) βΊ` chevron to collapse the entire section. Click individual rows to expand bodies. - [ ] **Failure path** *(stop dev server mid-save).* Stuck-staged panel appears with `Retry` and `Γ Discard`. Restart server β `Retry` succeeds. **Server sanity:** ```bash curl -s http://localhost:3000/api/specifications/<id>/annotations | jq npm run studio # query the `annotation` table directly ``` ## Known transitional gaps (deferred to follow-ups) - **Top-bar canonical surface** (V1.2-D). Until this lands, in-panel Undo is the only undo affordance. Per D131 the in-panel list is "convenience UI, not source of truth"; V1.2-D moves canonical Undo to the persistent app top-bar. - **Span-anchored annotations** (V2/V3). Schema reserves `selection_start` / `selection_end` columns; V1 leaves them NULL. - **Annotation deletion UI** beyond Undo. Broader management lands in a later card. π€ Generated with [Claude Code](https://claude.com/claude-code)


What
Adds the canonical design spec for the side-chat (FE-656) and ships the V1.1 Explore vertical slice β
chat-withfrom graph view β SideChatHost provider β/side-chatSSE β streaming response.The side-chat is a popover-to-panel chat surface anchored to spec items. Two entry modes (per-row
chat-withbutton and selection floating menu); three intents (Explore Β· Edit Β· Annotate); proposed changes stage in a top-bar patch list. Edit is internally a router to Refine / Soft / Hard tiers.This design subsumes:
chat-withplaceholder)docs/design/REVISIT_MODULE.md)Why
Today, all interaction with the spec runs through one long interview thread. When the user notices something in the structured spec view they want to discuss or edit, they have to navigate back to the chat and reintroduce the topic. The side-chat closes that gap: chat about an item, edits to an item, and annotations on an item all converge through the same review surface.
Phasing
chat-withfrom graph view, corner-anchored popover,/side-chatSSE, streaming response, SideChatHost lifted to spec-route layoutScope of this PR
Design + planning
docs/design/SIDE_CHAT.mdβ canonical design specmemory/PLAN.mdβ replace trigger-popover composer with side-chat; remove revisit/edit mode from Horizon; add architect/generator loop to Horizonmemory/SPEC.mdβ Requirement 34, A71/A72/A73 (status: future), D130, D131; soften D80docs/design/REVISIT_MODULE.mdβ mark as subsumed; cascade lifecycle remains valid as the V3 hard-edit pathV1.1 implementation
/side-chatSSE endpointpendingAssistantTextinto messages; extract SideChatHost providerView in action:
https://www.loom.com/share/84c413168ba34c0ab794a54c9f552fc3
Test plan
Design review
docs/design/SIDE_CHAT.mdend-to-endmemory/PLAN.md+memory/SPEC.mddeltas capture full subsumptionfuture, not blocking V1V1.1 functional
chat-withon a graph row β popover anchors to row, opens to corner