FE-716: Walking skeleton chat runtime — inline secondary chats over chat/turn#141
FE-716: Walking skeleton chat runtime — inline secondary chats over chat/turn#141kostandinang wants to merge 51 commits into
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
b5e5c0e to
52ed431
Compare
7e3496a to
4dc1083
Compare
PR SummaryMedium Risk Overview Brings in the canonical Reviewed by Cursor Bugbot for commit b819928. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
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. |
8a9cbae to
8473675
Compare
ca088bb to
f125dbc
Compare
8716216 to
1da0c46
Compare
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
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>
…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>
… composer + turn-zero suggestions
- 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>
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>
1fc689c to
6d16b4f
Compare
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>

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 (seememory/PLAN.md→chat-runtime-secondary-chats).Walkthrough ⬇️
https://www.loom.com/share/6bba3940f05449cbbf092fc4653e66cc
Added
#REF-CODEmention resolutionanchored_item_ids)prefers-reduced-motionsupportUNIFIED_CHAT_UX.mddesign briefChanged
PendingReviewSectionsubstantive row now opens an inline secondary chatStructuredListViewitem-action rail now opens an inline secondary chatproducerChatId(Shape A seam)data-anchor-turn-id_viewandgraphroutesRemoved
SideChatPopoverandSideChatHost(superseded by unified shell)PendingReviewSectionandStructuredListViewOut 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.