Skip to content

FE-716: Walking skeleton chat runtime — inline secondary chats over chat/turn#141

Open
kostandinang wants to merge 51 commits into
mainfrom
ka/fe-716-chat-runtime-unified-secondary-chats
Open

FE-716: Walking skeleton chat runtime — inline secondary chats over chat/turn#141
kostandinang wants to merge 51 commits into
mainfrom
ka/fe-716-chat-runtime-unified-secondary-chats

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented May 15, 2026

Why

Today's side-chat (V3.1) is a popover bolted onto the workspace. The unified chat shell (docs/design/UNIFIED_CHAT_UX.md) makes anchored sub-conversations first-class: durable, switchable, layout-aware, and able to stage edits. V1 scope = re-surface every existing side-chat behavior on the new shape; nothing more (see memory/PLAN.mdchat-runtime-secondary-chats).

Walkthrough ⬇️

https://www.loom.com/share/6bba3940f05449cbbf092fc4653e66cc

Added

  • Durable secondary chats over chat/turn substrate (no new tables)
  • Ask / Edit modes with per-mode tool gating
  • Streaming SSE endpoint for secondary chats
  • #REF-CODE mention resolution
  • Per-chat patch staging strip
  • Lightweight reconciliation panel inside secondary chats
  • Per-item chat dedupe (one chat per anchored item)
  • Anchored items persistence (anchored_item_ids)
  • Unified chat shell with three layout modes: Compact / Side-docked / Maximize
  • Chat switcher dropdown when 2+ chats exist
  • Layout mode persistence (localStorage, per spec)
  • Esc-decrement through layout tiers
  • X-close (removes shell) vs Minimize-pill (preserves state) semantics
  • Jump-to-anchor button (scrolls workspace to invoking turn)
  • Motion springs + prefers-reduced-motion support
  • Kind chip (Edit / Ask) on collapsible header
  • UNIFIED_CHAT_UX.md design brief

Changed

  • PendingReviewSection substantive row now opens an inline secondary chat
  • StructuredListView item-action rail now opens an inline secondary chat
  • Patch list partitioned by producerChatId (Shape A seam)
  • Workspace artifact rows expose data-anchor-turn-id
  • Unified shell mounts on both _view and graph routes

Removed

  • SideChatPopover and SideChatHost (superseded by unified shell)
  • Popover-based side-chat trigger paths in PendingReviewSection and StructuredListView

Out of scope

$ thread mentions, full reconciliation target-grouped UX, Shift+Tab mode toggle, Ladle prototype, agent-run inline rendering (C7 — blocked on a producer).

Testing

npm run verify — 108 test files / 1273 tests green.

@kostandinang kostandinang changed the title FE-716: Plan inline-secondary-chats frontier with V1 card queue FE-716: Chat runtime — unified chat surface with inline secondary chats May 15, 2026
Copy link
Copy Markdown
Contributor Author

kostandinang commented May 15, 2026

@kostandinang kostandinang force-pushed the ka/fe-716-chat-runtime-unified-secondary-chats branch 2 times, most recently from b5e5c0e to 52ed431 Compare May 15, 2026 18:25
@kostandinang kostandinang changed the title FE-716: Chat runtime — unified chat surface with inline secondary chats FE-716: Walking skeleton chat runtime — inline secondary chats over chat/turn May 17, 2026
@kostandinang kostandinang force-pushed the ka/fe-716-chat-runtime-unified-secondary-chats branch 2 times, most recently from 7e3496a to 4dc1083 Compare May 17, 2026 17:20
@lunelson lunelson changed the base branch from ln/fe-709-reconciliations to graphite-base/141 May 18, 2026 13:19
@kostandinang kostandinang marked this pull request as ready for review May 18, 2026 14:10
@cursor
Copy link
Copy Markdown

cursor Bot commented May 18, 2026

PR Summary

Medium Risk
Introduces multiple new chat table columns and indexes (plus a new JSON-text field) that will affect persistence and query patterns; although changes are additive, they carry migration/backfill and runtime-readiness risk. Remaining changes are documentation/planning updates and a minor HTML title tweak.

Overview
Adds the initial secondary chat substrate by extending the chat table with parent/anchor fields (parent_chat_id, invoked_in_turn_id), pinning metadata (pinned_item_id, pinned_span_hint, pinned_reconciliation_need_id), a persisted mode (mode), and an anchored_item_ids JSON-text list, plus indexes to support lookup.

Brings in the canonical UNIFIED_CHAT_UX.md design brief and a large memory/CARDS.md execution queue, and updates memory/PLAN.md to mark FE-716 as V1 complete. Also lowercases the app <title> in index.html.

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

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented May 18, 2026

This pull request is abnormally large and would use a significant amount of tokens to review. If you still wish to review it, comment "augment review" and we will review it.

Comment thread src/client/components/secondary-chat-trigger.tsx
Comment thread src/client/components/secondary-chat-host.tsx Outdated
Comment thread src/client/components/unified-chat-shell.tsx
Comment thread src/client/components/__tests__/chat-shell-patch-panel.test.tsx
Comment thread src/client/components/use-chat-layout-mode.ts Outdated
@kostandinang kostandinang force-pushed the ka/fe-716-chat-runtime-unified-secondary-chats branch from 8a9cbae to 8473675 Compare May 19, 2026 07:39
Comment thread src/client/components/unified-chat-shell.tsx
Comment thread src/client/components/unified-chat-shell.tsx
Comment thread src/client/components/__tests__/chat-shell-presence.test.tsx Outdated
@kostandinang kostandinang force-pushed the ka/fe-716-chat-runtime-unified-secondary-chats branch from ca088bb to f125dbc Compare May 19, 2026 08:59
Comment thread src/client/components/unified-chat-shell.tsx Outdated
Comment thread src/client/components/secondary-chat-host.tsx Outdated
Comment thread src/client/components/use-chat-layout-mode.ts
Comment thread src/client/components/unified-chat-shell.tsx
Comment thread src/client/components/unified-chat-shell.tsx Outdated
Comment thread src/client/components/unified-chat-shell.tsx
@kostandinang kostandinang self-assigned this May 19, 2026
@kostandinang kostandinang requested a review from lunelson May 19, 2026 14:06
@kostandinang kostandinang force-pushed the ka/fe-716-chat-runtime-unified-secondary-chats branch from 8716216 to 1da0c46 Compare May 19, 2026 14:08
Comment thread src/client/components/unified-chat-shell.tsx Outdated
Comment thread src/client/components/unified-chat-shell.tsx
kostandinang added a commit that referenced this pull request May 20, 2026
Post-V1 deepening (from FE-716 PR #141 ln-review):

- Aα: extract useMasterChatBootstrap hook from <UnifiedChatShell>
  with byte-equivalent semantics (latch ref + mutation ref + per
  (specId, parentChatId) key). Shell shrinks by ~30 LOC to a single
  hook call. New unit test covers no-op-on-hasMaster,
  no-op-on-null-parent, fire-once-per-pair, latch-release on null
  result, per-pair re-fire on parentChatId change.

- B: extract extractStagedIntents(messages, ctx) → ToolCallDecision[]
  out of useSecondaryChatStream into a pure adapter at the new private
  sub-tree src/client/components/secondary-chat-host/. ToolCallDecision
  is a discriminated union of {stage, intent} vs {skip|defer, reason}.
  Hook shrinks from a 58-LOC if-ladder to a 19-LOC dispatch loop owning
  only dedupe + patchList.stage. 11 new pure unit tests; no React in
  the extractor.

C32 (workspace footer chat) — direction reversed:

- Slice 4b (transcript popover anchored to active tab): reverted.
  Landed briefly then rolled back after the broader 'no expandable
  shell' premise was reversed. Inline always-visible transcript stays
  canonical. Leaf-component props from Slice 4b (chat-tabs.tsx
  popoverAnchorActiveTab/onActiveTabClick/data-active-chat-tab,
  secondary-chat-host.tsx transcriptPortalTarget/onSendMessage) remain
  as unused entrypoints; chat-transcript-popover.tsx + its test exist
  as orphan modules. Open coordination item documents the standing
  decision (default disposition: delete on next sweep).

- Slice 5 (replace <UnifiedChatShell> with <ChatWorkspaceFooter>):
  retired. The expandable shell stays as the primary chat surface.

- Slice 6 (patches + needs permanent home outside the shell):
  retired. Premise removed by Slice 5's retirement; patches + needs
  continue to live inside the shell body per C29/C30.

- C32 main card status flipped 'in progress' → 'partially landed
  (Slices 1–4b shipped; Slices 5 + 6 retired)'.

Cleanup:

- Deleted orphan secondary-chat-staging-strip.tsx (no callers; was on
  the retired Slice 5 deletion list).
- Updated misleading comment in chat-transcript-popover.tsx.

PLAN.md:

- Design A follow-on note flags Aβ's premise change (workspace-footer
  mount target was retired with Slice 5; re-scope candidate is
  <ChatShellPresenceProvider> at the spec route).

SPEC.md:

- Adds V1 anchor/handle implementation detail to invariant 154 (pin =
  birth identity, anchors = transcript-projected runtime context, not
  chat-row state). Adds 'pin (chat)' and 'anchor (chat)' to the
  domain vocabulary table.

Verification: npm run check clean (0 errors, 6 pre-existing warnings
unchanged). Affected vitest suites green: unified-chat-shell.test.tsx
(27/27), use-master-chat-bootstrap.test.tsx (5/5),
chat-shell-presence.test.tsx (2/2).

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e411d-0e14-73bd-9f0c-bba32fd39bea
kostandinang added a commit that referenced this pull request May 20, 2026
Address PR #141 high-severity bug 3266972144: switching the active
chat unmounted SecondaryChatHost from the active slot and mounted a
new instance under bg-{id} (or the reverse). Each mount created a
fresh useChat session, aborting in-flight streams and losing per-host
composer state — exactly what the background mounts were meant to
prevent.

Unify all hosts at a single, stable mount point. Keys are per chat.id
(no bg-/min-/active prefixes) so React preserves useChat across:
- tab switches (only renderTranscript/renderComposer + portal targets
  change, the host itself stays mounted),
- minimize/expand transitions (the hosts list is rendered alongside
  both branches of the appearance switch).

The active+expanded host portals its transcript into a new
transcriptSlot (mirrors the existing composerSlot pattern); background
chats render nothing visible but keep firing streaming/unread
callbacks.

Verified: unified-chat-shell.test.tsx (27/27), chat-shell-presence
(2/2), use-master-chat-bootstrap (5/5), secondary-chat-host (10/10),
chat-shell-patch-panel (4/4) all pass. The 2 pre-existing flaky
failures in router.test.tsx and structured-list-view.test.tsx are
unrelated to this change (verified by stashing the diff).

Co-authored-by: Amp <amp@ampcode.com>
Comment thread src/server/db/specification-store.ts
kostandinang and others added 28 commits May 20, 2026 11:39
…cher

C17 — Reshape the unified chat shell header to three layout
affordances (Minimize · Side-docked · Compact↔Maximize toggle);
hide Full mode (clamped on read in `useChatLayoutMode`); lift
close-vs-minimize into `ChatShellPresenceProvider` as
`appearance: 'expanded' | 'minimized' | 'closed'`. X close removes the
shell entirely so the workspace reclaims the pane; Minimize collapses
to a bottom-right "Ask Brunch" pill while preserving chat state.

Extract the layout dispatch into `chat-shell-layout.tsx` and apply
it on both the chat sub-routes (`_view/route.tsx`) and the graph
route (`graph.tsx`), so the unified shell ships on both surfaces.
Compact dock height raised to `h-[78vh]` to match the retired
side-chat popover footprint. Drop the navigate-on-create fallback
from the structured-list trigger handlers.

C18 — Add `chat.anchored_item_ids text NOT NULL DEFAULT '[]'`
(drizzle/0023). Bundle projects `anchoredItemIds: number[]` on each
secondary-chat entry.

C26 — Replace the single-scratch behavior with per-item dedupe:
`getOrCreateItemSecondaryChat(db, specId, input)` finds the existing
chat for `(parent_chat_id, pinned_item_id, pinned_reconciliation_need_id IS NULL)`
or creates a new one. New `<ChatSwitcher>` dropdown component mounts
in the shell header when 2+ item-anchored chats exist; active chat
= `presence.focusedChatId` with most-recent fallback. Reconciliation
chats stay hidden from the switcher until Track 3 defines their UX.

`npm run verify` — 108 test files / 1279 tests pass; build clean.

Co-Authored-By: Claude <noreply@anthropic.com>
C20 — Adopt `<Conversation>` + `<Message>` for turn rendering. Persisted
turns and the streaming pulse now route through ai-elements; assistant
text renders via `<MessageResponse>` (markdown-aware), user text stays
plain.

C21 — Replace the bespoke composer with `<PromptInput>`. Mode toggle
moves into the composer footer's leading-edge tools slot; Shift+Tab
inside the textarea flips Ask↔Edit. The header keeps a read-only kind
chip so collapsed state still surfaces kind.

C22 — Streaming live-state uses ai-elements `<Reasoning isStreaming>`;
prefers-reduced-motion short-circuits to a static text block. Drop the
bespoke `motion` import from the collapsible.

C23 — Turn-zero `<SecondaryChatSuggestions>` row keyed by `(mode,
reconciliation-kind)`. Six static prompt arrays — generic + supersedes
+ needs_confirmation for each of Ask/Edit. Click writes into the
lifted composer draft; hides once `turns.length > 0`.

C24 — `secondary-chat-route.ts` rebuilt on `pipeUIMessageStreamToResponse`
+ `createUIMessageStream` + `validateUIMessages` from the AI SDK. The
client transport drops `streamSecondaryChatMessage` (deleted) in favor
of `useChat<BrunchUIMessage>` mounted per chat in `SecondaryChatHost`.
Typed-data-parts substrate (`brunchDataPartSchemas`) now carries the
secondary-chat surface.

C25 — `#` mention autocomplete on the composer textarea. New
`secondary-chat-mention-popup.tsx` exposes `<SecondaryChatMentionPopup>`
+ `computeMentionQuery` / `handleMentionPopupKey` / `insertMention`
helpers. Reads the spec's intent graph for refcode + kind; inserts a
`#REF-CODE` token at the cursor on Enter. Server-side resolution (C6)
unchanged.

C27 — Proposed (not yet implemented). UI polish pass: selective
`kindAccentHex` tinting on switcher/host/focus-ring/workspace-row
states + modern shell vocabulary (rounded-xl shell, pill compose,
trimmed header). Inspiration only from the HASH SgAI figma; gradient
wash, tab-based switcher, agent-run narration, and "+ New chat"
affordance all deferred.

C28 — Sticky composer at the bottom of the chat surface; autoscroll to
latest message as turns arrive or streaming text grows. New
`bottomAnchorRef` + `useEffect` keyed on `turns.length` and
`streamingAssistantText.length` calls `scrollIntoView({ block: 'end' })`.
Composer wrapped in `sticky bottom-0 bg-background/95 backdrop-blur-sm
border-t border-rule/40` via the negative-margin trick. Pause-on-
scroll-up + "Jump to latest" button deferred to V2.

Follow-up: `router.test.tsx` shows 7–8 failures in the full verify
suite but passes when run in isolation — test pollution from the
parallel ai-elements work to be debugged separately. Build-boundary
test also exhibits the same isolation/pollution divergence. None
involve the C28 sticky-composer edit.

Co-Authored-By: Claude <noreply@anthropic.com>
Selective kind-accent tinting (accents only, never full backgrounds):
- <ChatSwitcher> trigger + active row replace the flat bg-tint/60 fill
  with a 3px left-border in kindAccentHex[chat.pinnedItemKind]; both
  carry data-accent-hex for downstream snapshot/diff debugging.
- <SecondaryChatCollapsible> body gains a 2px left accent strip in the
  chat's kindAccentHex; the collapsible trigger's focus ring borrows
  the same colour at ~30% opacity (--tw-ring-color override).
- StructuredListView rows whose item id matches the active chat's
  pinned_item_id or any anchored_item_ids render with a 2px left-border
  in the active chat's kindAccentHex. Foundation slice — C19's full
  selection-styling pass can build on data-graph-row-chat-anchored.

Modern shell vocabulary (chat surface chrome only):
- <UnifiedChatShell> header drops the literal 'CHAT' uppercase prefix
  in favour of just the truncated spec name; row clamped to h-8 to
  trim vertical chrome.
- Inner cards bumped from rounded-md → rounded-lg on the collapsible
  shell, reconciliation panel, and staging strip.
- Composer textarea gains rounded-full px-4; Send button becomes a
  dark pill (rounded-full bg-ink text-background hover:bg-ink/90).

Out of scope per the CARDS.md C27 parking lot: soft gradient wash on
the chat panel background, '1 Queued' indicator, agent-run progress
narration, tab-based switcher, '+ New chat' affordance, mention chip
styling, and workspace-center polish — all stay deferred to follow-up
frontiers.

Tests
- New chat-switcher.test.tsx (3 tests): empty render, trigger accent
  hex/border, active-row accent + selection forwarding.
- structured-list-view.test.tsx: existing -specification-data.js mock
  extended to stub useSpecificationBundleData so the new active-chat
  anchor lookup runs without a QueryClient harness.

Verification
- npm run fix clean (only pre-existing 6 'rendered is declared' warns
  in InterviewView.test.tsx, unchanged).
- npm run verify green: 1302 tests pass, build clean.

Co-authored-by: Amp <amp@ampcode.com>
Promotes C27's left-border foundation to a full selection state on the
StructuredListView. Rows whose ids appear in the focused secondary
chat's pinned_item_id or anchoredItemIds render as selected:

- 2px left-border in kindAccentHex[item.kind]
- ~10% kind-accent background tint (`${kindAccentHex[item.kind]}1A`)

Per the C19 spec the accent colour is resolved *per row* against the
item's own kind ("matching the item's kind"), not the active chat's
pinnedItemKind. C27's earlier choice (active-chat accent) is dropped in
favor of the C19 directive — for the pinned item the colour is the
same; for items mentioned via #REF-CODE (in anchoredItemIds) the row
now lights up in its own kind colour, which makes mixed-kind anchored
sets legible at a glance.

ItemActionRail flips its open-inline-chat trigger's aria-label between
'Anchored to active chat' and 'Open inline chat about this item' (plus
aria-pressed=true and data-chat-anchored='true' for snapshot/test
access). The click handler is unchanged — C26 server-side per-item
dedupe makes re-triggering on the anchored item idempotent, returning
the existing chat id rather than creating a new one.

Tests
- 4 new cases in structured-list-view.test.tsx:
  - No chat focused → no data-graph-row-chat-anchored on any row.
  - Pinned-item row gets selection styling (border + tint); sibling
    rows stay neutral.
  - anchoredItemIds entries get the same selection styling as the
    pinned item.
  - aria-label / aria-pressed / data-chat-anchored flip when a row is
    in the active chat's anchor set.
- Bundle and presence mocks extended with mutable mockSecondaryChats /
  mockFocusedChatId so individual tests can name an active chat without
  a full QueryClient or ChatShellPresenceProvider harness; reset in
  beforeEach.

Out of scope (per CARDS.md C19 §Out of scope)
- Anchor-removal UX — defer until walkthrough demands it.
- Selection in the graph view — separate frontier (item-anchored badge
  in graph view lives on the deferred parking lot).
- Optional shell-header 'Anchored: A12, GOAL3' mini-band — defer.

Verification
- npm run fix clean (only pre-existing 6 'rendered is declared' warns
  in InterviewView.test.tsx, unchanged).
- npm run verify green: 1306 tests pass (was 1302; +4 new C19 tests),
  110 test files, build clean.

Co-authored-by: Amp <amp@ampcode.com>
…ent mode label, sticky composer)

Walkthrough feedback on the C27/C19 selection-styling pass surfaced four
concrete fixes; all are scoped tweaks against the same files.

1. **Border colors — match the figma theme / side-chat color schema.**
   The original C27/C19 implementation used solid 100%-saturation kindAccentHex
   for the chat-switcher trigger and active row (3px left-border) and for the
   chat panel (2px left strip). Replaced with the side-chat color schema used
   by the legacy SideChatPopover and the existing kindColor chips:
   - `${accent}14` (~8% alpha) tinted background + accent-coloured text.
   - `${accent}33` (~20% alpha) for the accent border.
   - `${accent}0a` (~4% alpha) where a softer fill is preferred.
   - Focus ring stays at `${accent}4D` (~30% alpha).
   Applied to <ChatSwitcher> trigger + active row, <SecondaryChatCollapsible>
   outer panel, and the structured-list chat-anchored rows.

2. **Structured-list row tint dropped.**
   Per follow-up feedback ("remove the selected item in the graph background
   accent"), the chat-anchored row only renders the accent border now — the
   ~8% background tint was visually noisy against the graph view's already
   dense kind-colored chips. Border-only stays as the selected affordance.

3. **Ask/Edit → "Agent" mode toggle with hover tooltips.**
   SecondaryChatModeToggle now prefixes the segmented control with an
   "Agent" label and adds title hover copy on each button:
   - Ask: "Ask — discuss the item, get analysis, no changes to the spec"
   - Edit: "Edit — agent proposes structured changes you can review and apply"
   Underlying mode values (`explore` | `edit`) and testids stay unchanged so
   server contract + existing tests keep working; only the visible labeling
   + title attributes change.

4. **Input always at the bottom of the chat surface.**
   The composer was `sticky bottom-0` but the parent flex chain stopped at
   the SecondaryChatHost's motion.div, so when the conversation was shorter
   than the available height the composer sat just below the messages
   instead of at the bottom of the viewport. Threaded
   `flex min-h-0 flex-1 flex-col` through motion.div → Collapsible →
   CollapsibleContent, and added `mt-auto` on the composer wrapper. The
   composer now pushes to the bottom on short conversations and stays
   sticky-pinned during scroll on long ones.

5. **Send button — side-chat dark filled rounded-md.**
   Reverted the C27 `rounded-full bg-ink` Send button to the legacy
   SideChatPopover styling: `rounded-md bg-[#202020] text-white` with the
   matching shadow + disabled-state styling. Keeps the affordance
   consistent with what users are accustomed to from the popover era.

Tests
- chat-switcher.test.tsx (3 tests): assertions migrated from
  `borderLeftWidth: '3px'` to chip-schema (`backgroundColor` +
  `color` + `borderColor` all non-empty for active, empty for
  inactive).
- structured-list-view.test.tsx C19 cases: assertion migrated from
  `borderLeftWidth: '2px'` to `borderColor` non-empty (border only);
  the dropped row tint is explicitly asserted (`backgroundColor: ''`
  on anchored rows).

Verification
- npm run fix clean (only the pre-existing 6 `rendered is declared`
  warnings in InterviewView.test.tsx, unchanged).
- npm run verify: 1306 tests pass overall. `router.test.tsx` shows
  intermittent failures under full-suite parallelism — passes 12/12
  in isolation. Documented as a pre-existing pollution flake in
  CARDS.md C24d, not caused by these changes.
- Build clean.

Co-authored-by: Amp <amp@ampcode.com>
…blue accent)

Walkthrough revision on top of the previous "Agent label + Ask/Edit"
shape:
- Drop the "Agent" prefix label.
- Two halves of a single segmented pill: Chat (explore) + Agent (edit).
- Icons inside each half: MessageSquare (Chat), Sparkles (Agent).
- Active half is filled with the blue accent (#2563eb = kindAccentHex.goal)
  + white text so toggling reads as one switch flipping sides; the
  inactive half stays transparent + text-hint with hover lift to text-ink.
- One rounded-full border-rule container hosts both halves so the whole
  control reads as a segmented toggle, not two independent chips.
- Title hover copy updated: 'Chat — discuss the item, get analysis, no
  changes to the spec' / 'Agent — proposes structured changes you can
  review and apply'.

Underlying mode values (`explore`, `edit`) and testids unchanged so
server contract + existing tests keep working.

Verification: npm run fix clean; secondary-chat-collapsible.test.tsx
passes (31/31).

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Amp <amp@ampcode.com>
Default to no comments unless the WHY is non-obvious (per CLAUDE.md).
Sweep across the FE-716 branch removes:

- Task/issue references in code comments and test names (`FE-716 Cxx`,
  `V3.1 Card N`, `FE-665 follow-up`, `Shape A`, `D131`, `D132`, etc.).
- WHAT comments that restate well-named code or test setup.
- Section banners (`// ---- Helpers ----`, `// ---- Setup ----`).
- "Migrated from X" / "replaces Y" notes that rot as the codebase moves.
- JSDoc that just restates the prop/field name in English.

Kept:
- WHY comments documenting non-obvious workarounds (Radix happy-dom
  pointer-event quirk, PromptInput Promise.all timing, useChat mock
  boundary rationale, `producerChatId` partition-seam invariant, etc.).
- `// @vitest-environment` and lint-disable directives.
- JSDoc on exported API where it adds load-bearing detail beyond the type.

35 files touched; ~470 lines removed, ~142 added (net -328 from
substantive comments tightened rather than fully deleted).

Out of scope (left uncommitted for the next FE-716 commit): the C29
`<ChatShellPatchPanel>` work — `unified-chat-shell.tsx`,
`secondary-chat-host.tsx`, `secondary-chat-mention-popup.tsx`,
`route.tsx`, `unified-chat-shell.test.tsx`, the new
`chat-shell-patch-panel.{tsx,test.tsx}` files, and `memory/CARDS.md`.

Verification
- `npm run check`: 0 errors, 6 pre-existing warnings unchanged.
- Scoped suites under each touched directory pass (client tests 210/210,
  client production 251/251, routes 222/222, server/shared 720/720).
- Full-suite runs occasionally time out under parallelism on `cli.test.ts`,
  `build-boundary.test.ts`, `walkthrough.test.ts` — all pass in isolation;
  unrelated to comment removal.

Co-Authored-By: Claude <noreply@anthropic.com>
- Add <ChatShellPatchPanel> mounted in <UnifiedChatShell> body. Built
  on ai-elements <Task>/<TaskTrigger>/<TaskContent>/<TaskItem>/
  <TaskItemFile> with <ContentDiff> + <ImpactChip>. Header: bulk Apply
  all + Undo; per-row: kind chip + summary + diff + Discard. Stays
  mounted while canUndo so Undo survives post-apply (mirrors the
  overlay's saved-toast Undo). Optional isStreaming prop, defaulted
  off; defaultOpen surfaces patches as soon as they arrive.
- Hide (do not delete) <PatchListOverlay /> mount at route.tsx:67 with
  a single-comment revert path. Component file + tests untouched.
- Retire <SecondaryChatStagingStrip> render from <SecondaryChatHost>.
  producerChatId partition seam in patch-list-reducer + usePatchList
  ForChat() preserved for future per-chat decoration / Track 3.
- Tests: new chat-shell-patch-panel.test.tsx (6 tests covering null
  empty-state, count copy, bulk Apply all, post-apply Undo presence,
  Discard sibling isolation, ContentDiff conditional rendering).
  unified-chat-shell.test.tsx adds 2 mount tests (panel absent when
  empty, present inside body when patches staged).
- Mark C29 done in memory/CARDS.md.

Co-authored-by: Amp <amp@ampcode.com>
- Theme the mention popover (popover surface, accent-colored refCode
  badges via knowledgeKindReferencePrefixes, scrollable list, no kind
  label on the right).
- Mouse picks fire on onMouseDown so the textarea keeps focus and the
  outside-click dismiss no longer races the selection.
- Arrow keys navigate, Enter picks the highlighted item, Escape
  dismisses; highlight resets on query/items change.
- Render inline #REFCODE mentions in user turns as colored chip spans,
  wrapped in a single span so the flex-col MessageContent does not
  stretch them to full width.
- Tighten composer rhythm: gap-2 between suggestions and input, pt-3
  above the sticky composer, no extra margin around the chip.
- Clicking a suggestion now submits immediately instead of populating
  the draft; test updated accordingly.

Co-authored-by: Amp <amp@ampcode.com>
… color pass

C30 — Quick-win same as C29, applied to <PendingReviewSection>:

- Mount <PendingReviewSection /> inside <UnifiedChatShell> body above
  <ChatShellPatchPanel>. Conversational input (needs → side-chat
  triggers) sits upstream of output (proposed edits).
- Hide (do not delete) workspace mount + import in
  -structured-list-view.tsx with a 2-line uncomment revert path.
- chat-shell-presence test now stubs useSpecificationOpenReconciliation
  Needs at the module boundary; bundle invalidation was firing a stray
  reconciliation-needs refetch through the shared fetchMock and
  consuming the queued POST/bundle responses.
- unified-chat-shell.test adds 2 mount tests (empty/null vs seeded via
  makeNeed()).

Color/UX pass on both shell surfaces:

- <ChatShellPatchPanel>: Apply all → dark composer-send (#202020).
  Undo → muted hint→ink outline. Kind label + anchor refcode → plain
  mono hint (dropped per-kind accent border/bg).
- <PendingReviewSection>: amber wash container → tint/40 + rounded-lg
  border matching the patch panel. Per-row amber rail dropped.
  Per-row action buttons (Confirm / Apply / View / Skip / Open side-
  chat / Resolve) condensed to icon-only with title tooltips and
  preserved aria-labels; primary actions reuse one dark
  PRIMARY_ICON_BUTTON_CLASS, secondaries reuse a single neutral
  ICON_BUTTON_CLASS. Bulk header buttons all share PRIMARY_BUTTON_
  CLASS. ClassificationChip kept (per-variant accents encode semantic
  state, not decoration). Edit form: tinted ring/bg → border-rule +
  bg-background. DiffPopover kindAccent overrides removed.

All 1316 tests green. Mark C30 done in memory/CARDS.md.

Co-authored-by: Amp <amp@ampcode.com>
…crointeractions

Threads the pinned knowledge-item accent through more of the secondary-chat
surface and tightens chrome across the shell.

Accent threading (composer + bubbles):
- Composer focus ring + textarea selection highlight retint to pinnedAccent.
- User message bubble gets a soft accent wash (~8% fill, ~25% border).
- Send button background uses pinnedAccent when enabled; disabled state
  falls back to semantic bg-tint / text-hint tokens so the icon stays
  legible (was a washed-out icon over a colored fill).
- Mode toggle + segmented pill keep the accent family.

Header (Linear-style minimal controls):
- Flat icon buttons sharing one shape — no segmented-pill container,
  no border, no shadow at rest. Hover lifts to text-ink + bg-tint with
  active:scale-95 press feedback.
- Dock ↔ Compact merged into one toggle (PanelRight ↔ PictureInPicture2)
  so the row carries one icon per layout family.
- Maximize → renders chat full-screen ('full' mode), Restore returns to
  side-docked. Dropped clampDisabledMode so 'full' is a real persisted
  mode.
- Thin separator before the close button distinguishes destructive intent.

Persistent 'Ask Brunch' affordance:
- X (close) collapses to the same minimized pill as the minus button — the
  chat entry point is always reachable instead of disappearing on close.
- Pill shows a badge with the count of open per-item subchats.
- Hover lifts the pill (-translate-y-0.5 + shadow-lg) and the Send icon
  tilts -8° via group-hover; active:scale-95 on press.

Title chip:
- Removed the chat-mode icon from the title (redundant with the
  ChatSwitcher tab icon + composer toggle). Title now carries only the
  pinned-item refCode chip + trimmed name.

ChatSwitcher:
- Swapped MessageCircleQuestion → MessageSquare and PencilLine → Sparkles
  to match the composer toggle; icons inherit currentColor so dropdown
  rows stay neutral and only the active row picks up the accent.
- Tightened icon size 3.5 → 3 and chevron opacity 70 → 60.

Send button:
- Fully circular (rounded-full), arrow-up icon (ArrowUp 2.5 stroke).
- Dropped the heavy 1px black ring; soft drop shadow grows on hover.
- Hover lifts the button (scale-105) and nudges the arrow icon up
  (group-hover:-translate-y-0.5); active:scale-95 snap on press.

Pre-stream affordance:
- SecondaryChatStreamingAssistant shows a Shimmer 'Thinking…' line when
  isStreaming && text is empty so users have immediate feedback before the
  first token arrives. Reduced-motion users get a static hint label.

Classification chip → icon-only with tooltip:
- Dropped the inline UPPERCASE label; chip is now a size-4 accent-tinted
  square holding only the variant icon. Radix Tooltip shows the
  capitalized label on hover/focus; for failed rows, the agent error
  message renders as a secondary body line. Label moved to sentence case
  (Auto-confirm, Substantive, Queued, Failed). title/aria-label/data-attrs
  mirror the tooltip body.

Pending-review + chat-shell-patch-panel row controls:
- Disabled primary buttons swap bg-[#e3e3e3] / text-[#a6a6a6] for the
  semantic bg-tint / text-hint tokens.
- Added transition-[transform,…] active:scale-95 across all three button
  shapes for tactile press feedback. Resting icon-button opacity 60 → 70
  for slightly more discoverability.

Mention popup positioning fix:
- For above-placement, anchor by 'bottom' instead of 'top'. Previously the
  popup pre-allocated its full maxHeight above the textarea, so a popup
  with one filtered match floated far above the composer with a big
  visible gap. Now the popup's bottom edge hugs the input regardless of
  rendered height.

Tests:
- Updated unified-chat-shell tests for the new dock-toggle, full-mode
  maximize behaviour, persistent close-pill, and open-chat count badge.
- Updated classification-chip tests for icon-only chrome (label moved to
  aria-label/tooltip, sentence-case labels).
- Updated secondary-chat-collapsible tests for the removed title chip.

Co-authored-by: Amp <amp@ampcode.com>
Restores the C32 unified chat shell architecture and lands the round of UX
polish discussed in the last session.

Restored / hardened:
- Consolidated active-chat host into one SecondaryChatHost so transcript +
  composer share the same useChat instance (fixes streaming when the
  composer's send fired on a different hook than the transcript was reading).
- HOME-only top-strip + ChatSwitcher dropdown for every item chat
  (maxVisibleItems=0).
- ChatShellAppliedToast with Undo, X dismiss, and 5s auto-dismiss.
- buildRefCodeByItemId helper threaded through the host so anchor chips show
  human-meaningful refCodes instead of raw numeric ids.

Pending review:
- Bulk reconcile aria-label → "Reconcile pending reviews"; per-row → "Reconcile
  pending review {id}".
- Amber pulse dot next to the collapsed count so the surface stays
  discoverable while minimized.
- Row meta is now a button: clicking it navigates the workspace via
  `navigate({ to: '.', hash: target_reference_code })`, reusing
  StructuredListView's useGraphHashAnchor scroll mechanism.
- Row label renders `G3` (or `#20` fallback) instead of always raw id.
- Disabled when target_reference_code is null.

Turn-zero / hero:
- Moved turn-zero suggestions out of the composer overlay into the centered
  SecondaryChatFreshStateHero with a contextual title ("Where would you like
  to begin?" / "How would you like to change this?" / "Ask Brunch about
  anything").
- Hero always renders SecondaryChatSuggestions; fresh-start chips remain as
  a generic fallback when there's no pinned context.

Composer:
- ProposeChangeChips (Edit / Connect / Drill down) now render inline directly
  above the textarea — no wrapping band, no background, no shadow. The
  composer-sticky parent already sits above the transcript so they read as
  floating chips above the scroll area.
- ComposerAnchorChip drops the «…» quote wrappers; the Highlighter icon is
  the only marker. Added an X (visible on hover/focus) that locally dismisses
  the highlighted-text chip without touching server-pinned pinned_span_hint.

Title strip / anchor:
- ChatSwitcher trigger now renders the active chat's anchor inline before the
  label: kind-accent dot + refCode (e.g. "G1") and any additional anchored
  refCodes in muted opacity. Plain mono badges — no dashed underline, no
  leading "+" glyphs.
- Removed the separate `unified-chat-shell-active-anchor` pill (anchor info
  now lives inside the trigger).

Shell:
- Transcript side padding bumped: compact mode `px-1.5` → `px-3`; default
  `px-3` → `px-4`. Sticky-overlay band negative margins/padding match the
  new edges.

Selection / impact / overlay:
- Selection menu rename Annotate → "Add to Notes" (tests updated).
- ImpactChip labels simplified to "Hard / Soft / None" (legacy overlay test
  updated to match `^soft$`).
- Various comment cleanups — removed "Per user feedback" meta-narration,
  consolidated multi-paragraph design comments.

Test wiring:
- Mocked `useNavigate` in pending-review-section + unified-chat-shell tests.
- Test harness for SecondaryChatCollapsible wires `onPickStartSuggestion` to
  `onSubmitMessage` so the hero-mounted suggestions surface correctly in
  isolated component tests.
- Fixed pre-existing TS error in pending-review tests (`source_item_kind`
  is not a field).

254/254 client component tests pass; `npm run fix` clean.

Co-authored-by: Amp <amp@ampcode.com>
- Type CreateSecondaryChatResponse.kickoffTurnId as number | null
  (server returns null when reusing an existing chat)
- Resolve propose_edge targetReferenceCode to a real anchor via
  buildAnchorByRefCode; drop proposals that would create self-edges
- Add producerChatId: null to chat-shell-patch-panel test fixtures
- Hoist localStorage.setItem out of setState updater in
  useChatLayoutMode (avoid double-fire in React Strict Mode)

Co-authored-by: Amp <amp@ampcode.com>
- Keep SecondaryChatHost instances mounted as hidden background hosts
  while the shell is minimized/closed, so in-flight assistant streams
  don't abort and background tabs continue to fire streaming/unread
  callbacks.
- Seed a master chat in the presence/trigger integration test so the
  shell's auto-create-master effect can't race the trigger's POST and
  steal its fetchMock response.

Co-authored-by: Amp <amp@ampcode.com>
Replace the small accent-colored dot in the chat-switcher trigger with
the mode-aware glyph (MessageSquare for Ask, Sparkles for Agent/Edit),
tinted with the anchor accent. The active tab selection at the top of
the shell now communicates what the chat *does*, not just which item
it's anchored to.

Co-authored-by: Amp <amp@ampcode.com>
- Replace the verbose 'Anchored to <snippet>' / 'Editing <snippet>'
  kickoff turn with a friendly greeting that names the anchor by its
  reference code, e.g. 'Hi! How can I help with **#G1**?' (explore)
  and 'Hi! What would you like to change about **#G1**?' (edit). Applies
  to both the item-anchored and reconciliation-pinned paths so every
  secondary chat opens with the same simple frame.
- In the chat-switcher trigger, switch the anchor refCode container
  from items-baseline to items-center and add leading-none on the
  refCode spans so the mode glyph (Ask/Agent) sits flush with the
  refCode text instead of dropping to the text baseline.

Co-authored-by: Amp <amp@ampcode.com>
- Default chat layout mode → side-docked (matches FE-716 brief; was
  compact, which dropped users into the floating dock on first visit).
- Stabilize the auto-create-master effect: keep the mutation in a ref
  so the effect's dep array doesn't churn each render, and add a
  per-(specId, parentChatId) latch so we only ever attempt one POST
  per mount even if the bundle refresh is slow.
- Include reconciliation-pinned chats in `visibleChats` so
  `focusChat(reconciliationChatId)` after creating a substantive
  reconciliation side chat actually surfaces it instead of being
  silently swallowed.

Co-authored-by: Amp <amp@ampcode.com>
- Release the master-create latch when the POST fails so a subsequent
  render can retry, instead of stranding the shell on "Opening chat…"
  until full remount.
- Mount the post-apply Undo toast outside the expanded shell body so
  users who minimize right after Apply still see the 5s undo window
  (the toast renders null when nothing is undoable, so the floating
  wrapper costs nothing in the common case).

Co-authored-by: Amp <amp@ampcode.com>
Post-V1 deepening (from FE-716 PR #141 ln-review):

- Aα: extract useMasterChatBootstrap hook from <UnifiedChatShell>
  with byte-equivalent semantics (latch ref + mutation ref + per
  (specId, parentChatId) key). Shell shrinks by ~30 LOC to a single
  hook call. New unit test covers no-op-on-hasMaster,
  no-op-on-null-parent, fire-once-per-pair, latch-release on null
  result, per-pair re-fire on parentChatId change.

- B: extract extractStagedIntents(messages, ctx) → ToolCallDecision[]
  out of useSecondaryChatStream into a pure adapter at the new private
  sub-tree src/client/components/secondary-chat-host/. ToolCallDecision
  is a discriminated union of {stage, intent} vs {skip|defer, reason}.
  Hook shrinks from a 58-LOC if-ladder to a 19-LOC dispatch loop owning
  only dedupe + patchList.stage. 11 new pure unit tests; no React in
  the extractor.

C32 (workspace footer chat) — direction reversed:

- Slice 4b (transcript popover anchored to active tab): reverted.
  Landed briefly then rolled back after the broader 'no expandable
  shell' premise was reversed. Inline always-visible transcript stays
  canonical. Leaf-component props from Slice 4b (chat-tabs.tsx
  popoverAnchorActiveTab/onActiveTabClick/data-active-chat-tab,
  secondary-chat-host.tsx transcriptPortalTarget/onSendMessage) remain
  as unused entrypoints; chat-transcript-popover.tsx + its test exist
  as orphan modules. Open coordination item documents the standing
  decision (default disposition: delete on next sweep).

- Slice 5 (replace <UnifiedChatShell> with <ChatWorkspaceFooter>):
  retired. The expandable shell stays as the primary chat surface.

- Slice 6 (patches + needs permanent home outside the shell):
  retired. Premise removed by Slice 5's retirement; patches + needs
  continue to live inside the shell body per C29/C30.

- C32 main card status flipped 'in progress' → 'partially landed
  (Slices 1–4b shipped; Slices 5 + 6 retired)'.

Cleanup:

- Deleted orphan secondary-chat-staging-strip.tsx (no callers; was on
  the retired Slice 5 deletion list).
- Updated misleading comment in chat-transcript-popover.tsx.

PLAN.md:

- Design A follow-on note flags Aβ's premise change (workspace-footer
  mount target was retired with Slice 5; re-scope candidate is
  <ChatShellPresenceProvider> at the spec route).

SPEC.md:

- Adds V1 anchor/handle implementation detail to invariant 154 (pin =
  birth identity, anchors = transcript-projected runtime context, not
  chat-row state). Adds 'pin (chat)' and 'anchor (chat)' to the
  domain vocabulary table.

Verification: npm run check clean (0 errors, 6 pre-existing warnings
unchanged). Affected vitest suites green: unified-chat-shell.test.tsx
(27/27), use-master-chat-bootstrap.test.tsx (5/5),
chat-shell-presence.test.tsx (2/2).

Co-authored-by: Amp <amp@ampcode.com>
Address PR #141 high-severity bug 3266972144: switching the active
chat unmounted SecondaryChatHost from the active slot and mounted a
new instance under bg-{id} (or the reverse). Each mount created a
fresh useChat session, aborting in-flight streams and losing per-host
composer state — exactly what the background mounts were meant to
prevent.

Unify all hosts at a single, stable mount point. Keys are per chat.id
(no bg-/min-/active prefixes) so React preserves useChat across:
- tab switches (only renderTranscript/renderComposer + portal targets
  change, the host itself stays mounted),
- minimize/expand transitions (the hosts list is rendered alongside
  both branches of the appearance switch).

The active+expanded host portals its transcript into a new
transcriptSlot (mirrors the existing composerSlot pattern); background
chats render nothing visible but keep firing streaming/unread
callbacks.

Verified: unified-chat-shell.test.tsx (27/27), chat-shell-presence
(2/2), use-master-chat-bootstrap (5/5), secondary-chat-host (10/10),
chat-shell-patch-panel (4/4) all pass. The 2 pre-existing flaky
failures in router.test.tsx and structured-list-view.test.tsx are
unrelated to this change (verified by stashing the diff).

Co-authored-by: Amp <amp@ampcode.com>
Address PR #141 medium-severity bug 3272179870: getOrCreateItemSecondaryChat
returned an existing per-item chat without updating invoked_in_turn_id
or pinned_span_hint, so re-triggers from a later turn silently kept the
chat pinned to the first opening turn. Jump-to-anchor (presence's
`jumpToAnchor(turnId)` helper, currently exposed but not yet wired
into a caller) would land on a stale anchor.

When the dedupe branch sees a request with a different invoked_in_turn_id
or pinned_span_hint, refresh both columns on the existing row so the
persisted anchor tracks the latest invocation context. Same chat id is
returned, kickoffTurnId stays null (no duplicate greeting turn).

New regression test in app.test.ts: open the chat from firstTurn with
`spanHint: 'first hint'`, re-trigger from secondTurn with
`spanHint: 'second hint'`, assert the bundle row reflects the second
turn + hint while the chat id is unchanged.

Verified: 15/15 `POST /secondary-chats` route tests pass. Existing
pre-existing flaky failures in router.test.tsx and
structured-list-view.test.tsx are unrelated (confirmed by stashing).

Co-authored-by: Amp <amp@ampcode.com>
Address PR #141 medium-severity bug 3272354434: item-anchored and
reconciliation-pinned chats with a server kickoff rendered BOTH the
kickoff line ('Hi! How can I help with #G1?') AND the fresh-state
hero title ('Where would you like to begin?'), surfacing two
competing turn-zero prompts.

Keep the hero shell + suggestion chip row (they're still the only
surface for chat-context-specific suggestions on item-anchored
turn-zero chats), but suppress the redundant title whenever
hasPinnedContext is true (i.e. a kickoff or reconciliation panel is
already greeting the user). Master chats with no pinned context
continue to show 'Ask brunch about your spec' as before.

New regression test asserts:
- the kickoff line still renders,
- the redundant secondary-chat-fresh-state-title is absent,
- the hero shell stays mounted so the suggestion chips remain.

Verified: 34/34 in secondary-chat-collapsible.test.tsx, 262/262 in
client component tests pass.

Co-authored-by: Amp <amp@ampcode.com>
Address PR #141 medium-severity bug 3272392796: the transcript reads
`secondaryChat.turns` from the persisted bundle, but the client only
invalidates the bundle in useChat's `onFinish`. The server persists
the user turn before streaming, so the user's message vanished from
the transcript for the entire submit → assistant-finish window —
sometimes 5–30+ seconds for long responses.

Surface the in-flight user text from `useChat.messages` as a new
`pendingUserText` field on the stream state; render it as a user
bubble between the persisted turns and the streaming assistant. The
hook filters out cases where the bundle already reflects the same
text (post-onFinish window), so there's no duplicate render. Falls
back to null when not streaming.

New regression test asserts that a pending-user bubble surfaces with
the right text when `pendingUserText` is set during streaming.

Verified: 70/70 tests in the three touched test files (collapsible,
host, unified-chat-shell) pass.

Co-authored-by: Amp <amp@ampcode.com>
Restore files that diverged during PLAN/SPEC conflict resolution on main.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kostandinang kostandinang force-pushed the ka/fe-716-chat-runtime-unified-secondary-chats branch from 1fc689c to 6d16b4f Compare May 20, 2026 09:40
StructuredListView now resolves activeChat from the full secondaryChats
bundle when focusedChatId is set, matching UnifiedChatShell so
reconciliation-pinned chats anchor the correct graph rows.

Co-authored-by: Cursor <cursoragent@cursor.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