The chat add-to-session popover is anchored to viewport coordinates, so it can float over unrelated messages after the transcript scrolls. The shared selection-popover dismissal hook now treats captured scroll as a dismissal signal, matching the existing outside-click behavior and clearing the stale text selection.\n\nConstraint: Desktop selection popovers are owned by message/workspace child components, not by the transcript scroll container.\nRejected: Thread scroll state through every selectable message | would re-render message rows during scroll for a global dismissal concern.\nConfidence: high\nScope-risk: narrow\nDirective: Keep selection popover dismissal centralized unless a future menu needs scroll persistence explicitly.\nTested: bun run test -- src/components/chat/MessageList.test.tsx -t "dismisses the selected-message action when the message list scrolls"\nTested: bun run test -- src/components/chat/MessageList.test.tsx\nTested: bun run check:desktop
Browser screenshot capture reused composer prefill, which replaced local draft text and remained queued after the composer consumed it. The fix keeps rewind-style prefill as replacement while letting Browser screenshots append as one-shot composer attachments.
Constraint: Screenshot events can arrive while the chat composer is unmounted during Settings navigation.
Rejected: Store browser screenshots only in composerDraft | mounted composers would not update immediately.
Confidence: high
Scope-risk: narrow
Directive: Keep Browser screenshot capture on append mode; replacement prefill is for rewind-style composer restoration.
Tested: bun run test -- ChatInput.test.tsx previewEvents.test.ts --run
Tested: bun run check:desktop
Not-tested: Live Windows desktop/Tauri screenshot click
Project entry HTML often only mounts a dev-server app, while built output can be served statically. The desktop open-with policy now only offers the in-app browser for static HTML candidates, and preview-fs rewrites built HTML root-relative assets under the workspace preview path.
Constraint: Frontend project index.html needs a dev server instead of preview-fs static serving
Rejected: Treat every .html file as browser-previewable | project entry HTML can render blank or with missing assets
Confidence: high
Scope-risk: moderate
Tested: cd desktop && bun run check:desktop
Tested: bun run check:server
Tested: git diff --check
Not-tested: bun run verify was intentionally stopped at user request
Changed-file rows are easier to scan when previewable files are grouped before source-only files, and the file open-with menu should only expose useful preview, editor, and reveal actions. Output target cards also no longer need a separate copy control now that open/open-with are the primary actions.
Constraint: Keep the existing open-with API and menu components shared across changed files, workspace context menus, and output target cards.
Rejected: Keep system-default open for files | it duplicated less predictable platform behavior and added a low-value final menu item.
Confidence: high
Scope-risk: narrow
Directive: File open-with menus should stay ordered by immediate preview/open usefulness before editor and reveal actions.
Tested: cd desktop && bun run test src/lib/openWithItems.test.ts src/components/workspace/WorkspaceFileOpenWith.test.tsx src/components/chat/AssistantOutputTargetCard.test.tsx src/components/chat/CurrentTurnChangeCard.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
Preview selection turns should disclose the user's requested change on hover
or focus, while the browser capture controls should live in the address
toolbar like the referenced Codex UI. The message keeps a compact element tag
by default and exposes the note through a real tooltip instead of relying on a
native title-only hint.
Constraint: No issue number was provided for this task.
Constraint: Preserve the existing preview bridge protocol and direct-send selection flow.
Rejected: Native title-only hover text | it is too subtle and does not match the requested visible hover affordance.
Rejected: Keep a second preview action toolbar row | it contradicts the referenced right-aligned browser chrome.
Confidence: high
Scope-risk: narrow
Directive: Keep preview capture and picker actions in the browser address chrome unless the reference interaction changes.
Tested: cd desktop && bun run test --run src/components/chat/AttachmentGallery.test.tsx src/components/browser/BrowserSurface.test.tsx src/components/browser/BrowserAddressBar.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
Not-tested: Real Tauri webview visual smoke; browser automation startup failed, local Vite HTTP smoke returned 200.
Element selection from the local preview should preserve the visual target
the user picked and send the confirmed request directly to chat. The
screenshot now captures the visible viewport before drawing the marker, and
the selection event sends a model-facing prompt while the chat UI shows only
the annotated image and compact element label.
Constraint: No issue number was provided for this task.
Constraint: Keep the preview-agent protocol shape unchanged.
Rejected: Prefill the composer with the selection note | it keeps the old extra confirmation step and exposes implementation text in the input.
Rejected: Full-document annotation coordinates | body-relative captures drift from the viewport selection users see.
Confidence: high
Scope-risk: moderate
Directive: Do not route confirmed preview selections back through composer prefill without rechecking the Codex-style direct-send flow.
Tested: cd desktop && bun run test --run src/preview-agent/screenshot.test.ts src/lib/previewEvents.test.ts src/lib/selectionComposer.test.ts src/components/chat/AttachmentGallery.test.tsx src/stores/chatStore.test.ts
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build:preview-agent
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Real Tauri webview click-through smoke; in-app Browser automation timed out while local Vite HTTP smoke returned 200.
Inline image galleries live inside chat message shells that already use Tailwind group hover for message-level affordances. Reusing the unqualified group selector on image tiles made a parent message hover reveal every fullscreen affordance in the gallery at once. Naming the image tile group keeps the overlay state local to the hovered tile while preserving the existing layout and click behavior.
Constraint: Desktop chat message shells also use generic Tailwind group hover selectors.
Rejected: Track hovered image index in React state | unnecessary runtime state for a selector-scoping issue
Confidence: high
Scope-risk: narrow
Directive: Keep image-tile hover selectors scoped if this gallery remains nested inside message shells.
Tested: cd desktop && bun run test InlineImageGallery.test.tsx --run
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Manual desktop screenshot smoke
Synthetic API errors were carrying only English display text, so desktop chat could not distinguish our compatibility guidance from raw upstream failures and recovery logic depended on those English strings.
Add stable business error codes for media, PDF, request-size, prompt-length, and auto-mode errors. Desktop chat now resolves those codes through locale keys and suppresses the stale English fallback for known business errors, while normalizeMessagesForAPI still supports legacy text matching for old transcripts.
Constraint: Raw upstream provider details must remain available for diagnostics but should not drive localized business UX.
Rejected: Localize src/services/api/errors.ts strings directly | that would make persisted transcript text and media recovery depend on the active UI language.
Confidence: high
Scope-risk: moderate
Directive: Add new user-facing synthetic API errors with businessErrorCode values before localizing their display text.
Tested: bun test src/services/api/errors.test.ts tests/mediaRecoveryAndEstimation.test.ts src/server/__tests__/ws-memory-events.test.ts
Tested: cd desktop && bun run test -- run src/stores/chatStore.test.ts src/components/chat/MessageList.test.tsx
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: Manual desktop screenshot smoke for the rendered Chinese error card
Previewable changed-file rows no longer show a redundant workspace chevron when the row already has an Open With control. The unified Workbench now owns the Browser entry point, and opening Browser mode creates a blank address bar instead of navigating to a hard-coded localhost port.
Constraint: Keep explicit preview links and localhost output cards able to open the browser directly with their target URL.
Rejected: Keep the top toolbar browser button | it duplicated the Workbench Browser tab and silently opened localhost:5173.
Rejected: Use localhost:5173 as the blank browser default | it makes arbitrary Browser mode look tied to a dev server.
Confidence: high
Scope-risk: moderate
Tested: cd desktop && /Users/nanmi/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node ./node_modules/typescript/bin/tsc --noEmit
Tested: cd desktop && /Users/nanmi/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node ./node_modules/vitest/vitest.mjs run src/components/chat/CurrentTurnChangeCard.test.tsx src/stores/browserPanelStore.test.ts src/components/browser/BrowserAddressBar.test.tsx src/components/workbench/WorkbenchPanel.test.tsx src/components/browser/BrowserSurface.test.tsx src/components/layout/TabBar.test.tsx --pool=forks --poolOptions.forks.singleFork=true
Tested: git diff --check -- desktop/src/components/chat/CurrentTurnChangeCard.tsx desktop/src/components/chat/CurrentTurnChangeCard.test.tsx desktop/src/components/browser/BrowserAddressBar.tsx desktop/src/components/browser/BrowserAddressBar.test.tsx desktop/src/components/browser/BrowserSurface.tsx desktop/src/components/browser/BrowserSurface.test.tsx desktop/src/components/layout/TabBar.tsx desktop/src/components/layout/TabBar.test.tsx desktop/src/components/workbench/WorkbenchPanel.tsx desktop/src/components/workbench/WorkbenchPanel.test.tsx desktop/src/stores/browserPanelStore.ts desktop/src/stores/browserPanelStore.test.ts
Not-tested: Full desktop check:desktop gate.
Message actions still fade in on hover and focus, but their row now keeps a stable height so transcript content does not jump when the pointer enters or leaves a message.
Constraint: Preserve the existing hover-only visibility and copy/fork interaction model.
Rejected: Keep animating height from zero | it causes the visible transcript reflow this change is fixing.
Confidence: high
Scope-risk: narrow
Tested: cd desktop && /Users/nanmi/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin/node ./node_modules/typescript/bin/tsc --noEmit
Tested: git diff --check -- desktop/src/components/chat/MessageActionBar.tsx desktop/src/components/chat/MessageList.test.tsx
Not-tested: Focused Vitest could not complete in this local shell because the Vitest/esbuild startup process stalled or reported esbuild service stopped.
The previous fix moved the hover group onto the message shell, but the action row still occupied layout space while invisible. That invisible row kept expanding the shell hit area, so moving the pointer into the empty metadata strip could still reveal copy and timestamp controls.
Collapse the action row to zero height by default and expand it only while the message shell is hovered or focused. Keep message content spacing separate from the action row so hidden controls do not create a hover target.
Constraint: Copy/fork controls must remain reachable after the bubble reveals them.
Rejected: Use display:none for the action row | would remove the keyboard focus path entirely.
Confidence: high
Scope-risk: narrow
Directive: Keep hidden message metadata rows out of normal hover hit-testing; do not reintroduce invisible layout space around chat bubbles.
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run -t "keeps user actions anchored"
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: cd desktop && bun run test src/components/chat/UserMessage.test.tsx --run
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Live Tauri native hover smoke
Related: #642
WebKit (Tauri WKWebView) paints complex message DOM ~2x slower than Blink and stutters during scroll. Add content-visibility:auto + contain-intrinsic-size to fully-mounted (non-virtualized) rows so the engine skips off-screen rows without unmounting or re-measuring them.
Measured with Playwright WebKit on the reported laggy 79-message session (main): worst frame 279ms -> 18ms, jank 2 -> 0. Virtualization was ruled out — it made WebKit *worse* (tanstack 12-24fps) due to scroll-time measureElement/re-render cost.
Only non-virtualized rows get the class (no ResizeObserver); virtualized window items stay free of content-visibility to avoid the zero-height measurement regression. Tests guard both directions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Large Mermaid flowcharts were still hard to inspect in the desktop chat surface because ultra-wide SVGs could be squeezed or opened at an unhelpful crop. Normalize the rendered SVG, keep edge strokes visible while preserving author styles, and add fit-first overview rendering for both inline chat cards and the preview modal.
Constraint: Mermaid emits SVG sized for generic web pages, while desktop chat needs readable SVG behavior inside constrained WebView panels.
Rejected: Replace Mermaid with a node-canvas library | it would not be a drop-in renderer for Mermaid syntax and would add unnecessary dependency and migration risk.
Confidence: high
Scope-risk: moderate
Directive: Keep SVG normalization additive; do not override explicit linkStyle or marker paint without adding a regression test.
Tested: bun run check:desktop
Tested: Real desktop frontend visual QA with complex Mermaid screenshots at Fit, 25%, 50%, 75%, 100%, and 125%.
Not-tested: Packaged Tauri WebView smoke.
Message actions were still tied to the full message row, so moving the pointer into blank space beside a short chat bubble kept copy and timestamp controls visible. Move the hover group onto the actual message shell and keep full-width behavior only for document-style assistant replies.
Constraint: Message actions still rely on group-hover and focus-within for pointer and keyboard access.
Rejected: Remove focus-within reveal | would regress keyboard users who tab to copy or fork controls.
Confidence: high
Scope-risk: narrow
Directive: Keep normal bubble hover bounds tied to the message shell; document-layout replies intentionally remain full-width.
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: cd desktop && bun run test src/components/chat/UserMessage.test.tsx --run
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Live Tauri native hover smoke
Related: #642
Desktop reasoning effort now follows the selected session runtime instead of the General settings surface. Session metadata records provider, model, and effort so reconnects and prewarm restarts preserve the per-session runtime, while the default effort is max for new sessions.
Constraint: Existing transcript and localStorage runtime data must remain readable without a required migration.
Rejected: Keep effort as a General setting | it made one session change leak into every other session.
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce global desktop effort controls without proving multi-session isolation.
Tested: bun run check:server
Tested: bun run check:desktop
Tested: bun run check:native
Tested: bun run check:policy
Tested: bun run check:coverage
Tested: cd desktop && bun run test -- src/components/controls/ModelSelector.test.tsx src/stores/chatStore.test.ts
Tested: bun test src/server/__tests__/settings.test.ts --test-name-pattern "effort"
Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "runtime"
Not-tested: Live provider smoke for domestic providers without local credentials.
The "?? {}" fallback allocated a fresh object every render, defeating the React.memo barriers on ToolCallGroup and MessageBlock for sessions without agent task notifications, so every visible tool-call card re-rendered on each scroll frame. Reuse a module-level empty constant, mirroring EMPTY_MESSAGES.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Streaming assistant output can expose incomplete Mermaid fences before the
model has closed the diagram syntax. Keep those blocks in a generating state
until the assistant message finalizes, then render the completed diagram. Also
preserve Mermaid label content in sanitized SVG output so preview and expanded
views keep node text visible.
Constraint: marked treats unclosed fenced code as a code block during streaming
Rejected: Keep retrying Mermaid render on partial syntax | users still see parse errors while valid output is incomplete
Confidence: high
Scope-risk: narrow
Directive: Do not render parser-backed diagram components from streaming markdown without an explicit pending state
Tested: bun run check:desktop
Not-tested: live provider smoke after Codex host Crashpad renderer/utility dumps were observed
Message action controls stay visible through focus-within for keyboard navigation, but pointer clicks left the copy/fork buttons focused after the mouse moved away. Release pointer focus on message actions so hover controls return to their quiet state while keeping Tab focus behavior intact.
Constraint: Message actions use focus-within for keyboard accessibility and should not remove that path.
Rejected: Remove focus-within reveal | would regress keyboard users who tab to copy or fork.
Confidence: high
Scope-risk: narrow
Directive: Keep pointer-only blur scoped to message action controls; do not remove focus-within without a replacement keyboard affordance.
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: bun run check:desktop
Not-tested: Live Tauri manual hover smoke
Related: #642
Dark-mode Markdown text was still inheriting Tailwind Typography's default prose color, so normal and italic text could render dark while strong text used the app theme token.
Map markdown prose variables to the desktop theme tokens, move code-viewer hover and line-number styling onto theme tokens, and make Mermaid/Diff rendering follow the active app theme. Write/Edit/tool result and workspace render paths were audited; their core code and diff surfaces already use theme tokens, with the DiffViewer theme handoff tightened here.
Constraint: Mermaid theme customization requires the base theme and concrete color values, so CSS theme tokens are resolved to hex before rendering.
Rejected: Add per-element prose utility overrides only | misses Tailwind Typography defaults for quotes, counters, tables, and future prose elements
Confidence: high
Scope-risk: moderate
Directive: Keep markdown, code, diff, and diagram colors routed through shared desktop theme tokens; do not reintroduce fixed light backgrounds in chat renderers.
Tested: cd desktop && bun run test -- --run src/theme/globals.test.ts src/components/markdown/MarkdownRenderer.test.tsx src/components/chat/CodeViewer.test.tsx src/components/chat/MermaidRenderer.test.tsx src/components/chat/DiffViewer.test.tsx src/components/chat/chatBlocks.test.tsx src/components/workspace/WorkspacePanel.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
Not-tested: Full Tauri live desktop session with real provider output; browser broad-audit screenshot capture timed out, but computed colors were captured for all three themes.
Message rows should stay visually quiet until the user needs copy or fork controls. The action row now appears on message hover/focus, uses icon-sized controls, and shows the message timestamp beside the actions with locale-aware recent and historical formats.
Constraint: Desktop chat actions already own copy and fork affordances, so the change keeps that surface instead of adding a separate always-visible timestamp row.
Rejected: Keep text-labeled Copy buttons | the row remained too visually heavy for every message.
Confidence: high
Scope-risk: narrow
Directive: Keep copy, fork, and timestamp as one hover/focus metadata row unless the message layout is redesigned as a whole.
Tested: bun run check:desktop
Not-tested: Live Tauri native packaging
A turn can touch many files; an open-with affordance on every row is noise.
Restrict the per-file 打开方式 pill to rendered-previewable types via a new
isPreviewableChangedFile() predicate. Source rows keep their inline diff toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each file row now shows a material-symbol document-type icon, bold filename,
and a type subtitle (e.g. 文档·MD / 代码·TS) alongside a labeled 「打开方式 ⌄」
pill replacing the old open_in_new icon button; diff-expand is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the injection effect in AssistantMessage to also append a ▾ trigger
after inline <code> elements whose text is a browser-localhost or remote URL,
so AI-written run URLs like \`http://localhost:9527/\` get the same open-with
menu that markdown <a> links already receive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each file row in CurrentTurnChangeCard now has a sibling "open with" button
(open_in_new icon) that shows an OpenWithMenu, reusing buildOpenWithItems and
openWithContextForWorkspaceFile. HTML/XHTML files also get the in-app browser
option. Tests and i18n keys (openWith.title) included.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire L1 link routing into assistant message rendering. AssistantMessage now
accepts an optional sessionId and, when present, builds an onLinkClick that runs
handlePreviewLink against the running local server base (getServerBaseUrl) and
the browser/workspace panel stores, preventing default on handled links.
MessageList passes sessionId through in the assistant_text case.
Adds getServerBaseUrl() to desktopRuntime as a synchronous wrapper over the api
client's cached getBaseUrl().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Desktop users have different multiline habits, and issue #631 calls out that Enter-only submission is too easy to trigger. This adds a persisted General setting that keeps Enter-send as the default while allowing Ctrl/Cmd+Enter submission for users who want plain Enter to insert a newline.
Constraint: Preserve existing Enter-to-send behavior as the default
Rejected: Change the global default to Ctrl/Cmd+Enter | would disrupt existing users
Confidence: high
Scope-risk: moderate
Directive: Keep active and empty session composers using the shared send shortcut helper
Tested: cd desktop && bun run test -- --run src/components/chat/ChatInput.test.tsx src/__tests__/generalSettings.test.tsx src/stores/settingsStore.test.ts
Tested: bun run check:desktop
Tested: cd desktop && bun run lint
Tested: Browser smoke on http://127.0.0.1:5174/?serverUrl=http%3A%2F%2F127.0.0.1%3A3456
Not-tested: Packaged Tauri app runtime
Related: #631
getLocalizedFallbackCommands previously passed a constructed key to t()
without checking if the translation actually resolved. When the i18n
table lacked an entry, t() returned the raw key (e.g.
'slashCmd.clear.description'), which dosubot flagged in PR #593.
Fix: default to the static English description, only override when
t(key) returns a different string. Add two tests covering missing-key
fallback and partial-translation scenarios.
Co-Authored-By: qwen3.6-plus <QwenLM@claude-code-best.win>
This reverts commit 1b52da3587325715f06948d5d526f71b00c7d9fb.
The session parent-child tree display in sidebar is visually unappealing
and unnecessary. The branch button behavior during AI response is also
intentional - users should not switch branches mid-conversation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The workspace panel already had the attachment model for both files and directories, but directory rows lacked the same right-click path and there was no low-risk handoff from the panel into the active composer text. This keeps context attachment as the stable source of file content while adding a small composer insertion event for inline citations.
Constraint: Keep the existing attachment pipeline unchanged so file and directory contents still flow through the proven workspace reference path
Rejected: Build a rich inline chip editor immediately | larger composer and history-synchronization surface than these issues require
Confidence: high
Scope-risk: moderate
Directive: Do not replace workspace attachments with inline text-only mentions without rechecking model-content generation and history restore behavior
Tested: cd desktop && bunx vitest run src/components/workspace/WorkspacePanel.test.tsx
Tested: cd desktop && bunx vitest run src/components/chat/ChatInput.test.tsx
Tested: bun run check:desktop
Not-tested: Live provider response quality with inline citation wording
Windows WebView2 can report one-pixel content resize oscillations while a thinking or tool response is pinned to the bottom. That fed repeated bottom-scroll corrections back into the message list and made otherwise static chat content visibly move.
Ignore sub-2px content resize deltas before following live content growth, so real message growth still stays pinned without turning tiny layout jitter into scroll movement.
Constraint: Issue #603 reproduces on some Windows WebView2/display paths but not consistently across macOS or all Windows machines
Rejected: Disable bottom-following during thinking | would regress active response visibility when content actually grows
Confidence: medium
Scope-risk: narrow
Directive: Keep ResizeObserver follow behavior tolerant of sub-pixel or one-pixel WebView2 height jitter
Tested: cd desktop && bun run test -- --run src/components/chat/MessageList.test.tsx src/components/chat/virtualHeightCache.test.ts
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: Browser smoke at http://127.0.0.1:17999 loaded Claude Code Companion with no frontend error logs
Not-tested: Live Windows WebView2 runtime retest on reporter machine
Related: #603
The slash command menu in ChatInput and EmptySession was hard-coded to
English descriptions, so users running the desktop with locale=zh saw
English copy for the built-in commands the desktop owns (/clear,
/compact, /help, /mcp, /skills, /memory, /plugin, /doctor, ...).
This change:
- Adds slashCmd.<name>.description i18n keys for all 24 built-in slash
commands in en and zh locales.
- Introduces getLocalizedFallbackCommands(t) so React renders use the
active locale; the existing FALLBACK_SLASH_COMMANDS constant is kept
for non-React callers.
- Updates mergeSlashCommands so the localized fallback wins for
built-in command names while server/team-provided commands (e.g.
team:lark) still keep their own descriptions.
- Updates composerUtils.test.ts to reflect the new precedence and adds
a test that built-in commands prefer the localized description even
when the CLI broadcasts an English one.
- Adds CLAUDE.md and graphify-out/ to .gitignore (local AI assistant
artifacts that should not be committed).
Verification:
bun run check:desktop -> passed (lint + vitest + build, 32.9s)
changed-line coverage -> 100% (120/120)
Known environment-only blockers in this clone (unrelated to this diff):
bun run check:native -> rustc not installed in this WSL
bun run check:coverage -> 3 server-area suites hit
'error: An internal error occurred (WriteFailed)'
while writing very long stdout under WSL.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tab switches into a long session paid for everything that was already paid for
the last time the user was on that session. Specifically the per-message height
and metric maps were cleared on every switch, so the new commit could only feed
estimated heights into buildVirtualTranscriptWindow and then chase them with
ResizeObserver callbacks. The first commit also ran getBranchableMessageTargets
and getCompletedTurnTargets — two extra O(N) walks over the messages array —
synchronously, and scrollToBottom('auto') was called inside the switch's
useLayoutEffect, which (on JSDOM and depending on layout state) added a
scrollHeight read to that critical path.
Introduces virtualHeightCache, a small module-level LRU keyed by sessionId that
holds the height and metric maps. MessageList now reads the prior maps on
switch instead of clearing them, so revisiting a session uses real measured
heights from the previous visit and skips the estimate→measure-correction
cascade. tabStore.closeTab calls dropSession so closed tabs don't leak.
The two O(N) branch / completed-turn computations now read from
useDeferredValue(messages), so they run as a low-priority follow-up render
after the first paint instead of blocking the switch commit. The switch's
useLayoutEffect now writes the bottom sentinel directly instead of calling
scrollToBottom, keeping the layout-read path out of the synchronous switch.
Tested: 731/731 desktop vitest suites pass, including 5 new virtualHeightCache
LRU/isolation/restore cases
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two issues remained on long transcripts. (1) The markdown parse cache was 48
entries / 1.8MB and keyed by full content string, so a 2.6MB session thrashed
constantly and every streaming chunk evicted finalized history entries because
each delta produced a new key. (2) The virtualization spacers above and below
the active window were single huge divs, leaving the WebView nothing to paint
during reconciliation and producing the literal blank gaps users were seeing.
The markdown cache now keys on `${len}:${fnv1a}` and splits into a 200-entry /
8MB finalized cache plus a small 4-entry streaming cache that cannot evict
finalized parses. AssistantMessage forwards a `streaming` flag (and stops
disabling caching entirely while streaming, so revisiting a settled turn no
longer reparses). The spacers are now broken into ~800px chunks via a
VirtualSpacer component; each chunk uses `content-visibility: auto` with a
`contain-intrinsic-size` pinned to its real height, which is the combination
that lets the WebView paint placeholder boxes even when off-screen paint is
deferred — without restoring the regression the previous content-visibility
rollout caused on in-window items.
Tested: 726/726 desktop vitest suites pass, including 3 new markdown-cache cases
and 1 new spacer-chunk + in-window-no-content-visibility regression guard
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the virtualization window slid during scroll, every visible row reconciled
from scratch because the heavy chat-row subtree had no memo barriers, and
because renderTranscriptItem rebuilt fresh branchAction / toolResult object
literals each render that broke MessageBlock's existing memo.
Wraps AssistantMessage, UserMessage, ToolCallBlock, ToolCallGroup, ToolResultBlock,
and MarkdownRenderer in React.memo, and hoists the per-message branchAction and
toolResult lookups into useMemo'd Maps keyed by message id. Window slides now
keep referentially-stable props for unchanged rows, so reconciliation skips them
and only newly entering rows pay full render cost.
Tested: 722/722 desktop vitest suites pass
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Fix branch button hidden when AI is responding by decoupling
branchActionsDisabled from chatState (MessageList.tsx)
- Add sourceSessionId/sourceMessageId to SessionListItem type
(both server and desktop)
- Scan forkedFrom in listSessions to expose parent-child relationships
- Preserve sourceSessionId/sourceMessageId in branchSession optimistic store
- Add buildSessionTree utility for tree-flattened session list with
depth and isLastChild markers
- Render forked sessions indented with GitBranch icon in sidebar
- Add comprehensive tests (187 total: 81 desktop + 106 server)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Long desktop transcripts (797+ messages / 2.6MB) showed 1-2s blank frames while
scrolling. The previous strategy inflated virtualization overscan from 360px to
7200px on every wheel/touch/pointer event and routed scroll-handler state
updates through flushSync, which serialized large React reconciles onto WebKit's
compositor thread and starved paint.
Replaces the dynamic overscan + idle-timer state machine with a single stable
1200px overscan, and removes the flushSync barrier in updateAutoScrollState so
React 18's automatic batching handles the scroll-handler setStates. ResizeObserver
height callbacks now mark a pending bit and flush a single setMeasuredItemsVersion
per animation frame, instead of triggering a list-wide re-render per measured
item. Auto-scroll-on-measure is also removed so measurement feedback no longer
fights an active user scroll.
Tested: 722/722 desktop vitest suites pass
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>