Normalize generated flowchart labels before Mermaid rendering so Markdown previews handle labels with HTML breaks, braces, and bracketed type text.
Tested: bun run check:desktop
Scope-risk: narrow
Keep live trace polling sensitive to in-place call changes, scope detail section collapse state by trace session, and expose trace rows with list/button semantics.
Tested: cd desktop && bun run test -- TraceList.test.tsx TraceSession.test.tsx
Tested: cd desktop && bun run lint
Tested: bun test src/server/__tests__/trace-capture.test.ts
Confidence: medium
Scope-risk: narrow
Trace is a low-frequency diagnostic feature, so its entry points no
longer sit in primary chrome:
- Remove the Trace nav item above the session list in the sidebar
- Remove the trace open/open-window buttons from the session header
- Add a Trace tab in Settings between Token usage and Diagnostics,
embedding the existing TraceList page full-bleed
- Add settings.tab.trace and drop unused sidebar.traces across locales
UI rebuild (desktop):
- TraceSession: replace 3-column layout with two panes — turn-grouped
timeline tree (draggable splitter, search/filter, keyboard nav) and a
section-flow detail panel (Response / Messages / System Prompt /
Tools / Parameters / Raw), collapse state persists across spans
- Render LLM requests/responses semantically: messages as role-colored
conversation with tool_use/tool_result pairing instead of raw JSON
dumps; Raw fallback via CodeViewer for legacy truncated records
- TraceList: row-style list with model chips, mono metrics, hover
actions; content-visibility rows (no virtualization, WebKit-safe)
- i18n synced across zh/en/jp/kr/zh-TW (+25/-55 keys)
Data & capture (server):
- Capture full bodies: preview cap 2048 -> 240k chars, stream cap
256KB -> 1MB; list API trims previews to keep polling light; new
GET /api/sessions/:id/trace/calls/:callId returns the full record
- Extract per-call token usage at read time (SSE + JSON + proxy
wrapped); mtime-keyed read cache for the polling path
- Fix sensitive-key regex redacting *_tokens count fields, which made
token stats always report 0
Frontend data layer:
- SSE stream reassembly (Anthropic + OpenAI chat) adapted from
claude-tap (MIT, attribution in THIRD_PARTY_LICENSES.md), request/
response body parsers, shared formatters, on-demand call detail
cache; traceViewModel gains tokenUsage/isLifecycleNoise, drops
fullRaw
Tested:
- bun run check:server (1201 pass)
- bun run check:desktop (lint + 1358 tests + build)
- Chromium walkthrough against real local traces: list, session tree,
LLM semantic detail (new format), legacy fallback, tool detail
Restore visual-selection prompts from persisted transcript history as annotated screenshot attachments instead of exposing the model-facing selector prompt after reopening a session.
Tested: cd desktop && bun run test -- --run src/stores/chatStore.test.ts -t "restores visual selection history" --reporter verbose
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Put Telegram first in the IM adapter settings, add a single documentation link in the setup copy, and keep per-platform settings focused on configuration.
Tested: cd desktop && bun run test AdapterSettings.test.tsx
Tested: bun run check:desktop
Scope-risk: narrow
Confidence: high
Tested: cd desktop && bun run test -- src/__tests__/memorySettings.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
Tested: bun run check:desktop
Tested: bun run check:coverage
Tested: Chrome CDP smoke confirmed the memory editor textarea expands to 592px tall
Confidence: high
Scope-risk: narrow
Use Electron WebContentsView capturePage for browser preview screenshots and selection annotations so captured images match the rendered preview.
Tested:
- cd desktop && bun run test -- electron/services/preview.test.ts src/lib/previewEvents.test.ts src/components/browser/BrowserSurface.test.tsx src/preview-agent/screenshot.test.ts src/preview-agent/picker.test.ts src/preview-agent/editBubble.test.ts
- cd desktop && bun run lint
- cd desktop && bun run build
- bun run check:desktop
- bun run check:electron
- bun run check:native
Not-tested:
- Real GUI click-through for Screenshot / Select Element; Electron runtime launch was blocked in this shell and packaged build did not enter the smoke path.
Constraint: GUI click-through smoke was blocked by local Electron launch behavior; package smoke passed but does not launch the app.
Confidence: medium
Scope-risk: narrow
Tested:
- cd desktop && bun run test -- --run src/pages/TraceList.test.tsx
- bun test src/server/__tests__/trace-capture.test.ts
- bun run check:desktop
- bun run check:server
Scope-risk: broad
Fixes#652.
Add desktop/server output style settings APIs and a General Settings picker
that mirrors the Claude CLI outputStyle sources.
Persist active-project choices to .claude/settings.local.json and global
choices to user settings, and route /config to the local settings UI.
Tested:
- bun test src/server/__tests__/settings.test.ts
- cd desktop && bun run test -- src/components/chat/composerUtils.test.ts src/stores/settingsStoreOutputStyle.test.ts src/pages/SettingsOutputStyle.test.tsx
- cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx
- bun run check:desktop
- bun run check:server
- bun run verify
Confidence: high
Scope-risk: moderate
Add a WhatsApp adapter backed by Baileys linked-device auth, plus desktop QR binding UI, server config endpoints, sidecar startup wiring, tests, and documentation.
Constraint: Uses WhatsApp Web linked-device auth, not Meta WhatsApp Business Cloud API.
Tested:
- cd adapters && bun run check:adapters
- bun test src/server/__tests__/adapters.test.ts
- cd desktop && bun run check:desktop
- bun run check:native
- bun run check:persistence-upgrade
- bun run check:docs
Not-tested:
- Live WhatsApp QR pairing, because no WhatsApp account/device was exercised here.
- bun run check:server, because the existing src/server/__tests__/conversations.test.ts timeout still fails independently.
Confidence: medium
Scope-risk: moderate
Surface expanded error output for tool cards whose previews previously returned before rendering the result body. Keep successful Bash/Read/Edit/Write outputs hidden as before while exposing error details with wrapping and copy support.
Tested: cd desktop && bun run test -- src/components/chat/chatBlocks.test.tsx
Tested: bun test src/server/__tests__/conversations.test.ts -t "should switch from bypass permissions back to default without restarting" --timeout=20000
Tested: bun run verify
Confidence: high
Scope-risk: narrow
Tested:
- cd desktop && bun run test -- src/__tests__/pluginsSettings.test.tsx src/stores/pluginStore.test.ts --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- bun run check:desktop
- git diff --check
Scope-risk: moderate
Confidence: high
Commit 449ff0b0 changed completed thinking blocks to render the
'thinking.labelDone' title ("Thought"/"已思考") instead of the static
"Thinking" label, and updated ThinkingBlock.test.tsx but not
chatBlocks.test.tsx. The three inactive/default-state cases there still
queried the toggle button by /Thinking/ and failed. Match the new
done-state label (/Thought/); the active-state case is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Main added two TranslationKeys after this branch's base — 'thinking.labelDone'
(the "Thought" label shown after thinking completes) and
'slashCmd.agent.description' — which the jp/kr/zh-TW locale files did not yet
cover, so the Record<TranslationKey, string> contract failed tsc after merging
main. Add both keys to each new locale:
- jp: '思考完了' / '選択した Agent でプロンプトを実行'
- kr: '사고 완료' / '선택한 Agent로 프롬프트 실행'
- zh-TW: '已思考' / '使用指定 Agent 執行提示'
en/zh are unchanged. `tsc --noEmit` is now clean and the i18n + timestamp
suites pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The desktop notification poller fired on mount, racing the bootstrap that
resolves the dynamic server URL and confirms /health. Its first requests hit
the uninitialized default base URL and failed with "Failed to fetch", logging
spurious client_api_request_failed warnings to the diagnostics panel.
Add a whenDesktopServerReady() signal resolved once initializeDesktopServerUrl
sets the base URL and the healthcheck passes, and gate the poller on it so it
only starts once the server is reachable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The thinking block title always rendered the static "思考中"/"Thinking"
label; isActive only toggled the animated dots. Once thinking finished
the dots disappeared but the in-progress text stayed. Switch the label
to "已思考"/"Thought" when the block is no longer active.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Indent session rows under their project group and switch project folder icons between open and closed states when collapsed.
Tested: cd desktop && bun run test --run src/components/layout/Sidebar.test.tsx -t "groups sessions by project|collapses a project group"
Tested: cd desktop && bun run test --run src/components/layout/Sidebar.test.tsx
Tested: bun run check:desktop
Tested: Browser plugin smoke on http://127.0.0.1:3456/ confirmed first project open -> closed state, icon state, and pl-6 session indentation.
Confidence: high
Scope-risk: narrow
Normalize image blocks loaded from transcript history back into renderable data URLs and keep generated image metadata out of the visible user bubble. Image source metadata is applied in order so multi-image messages retain the correct paths and names.
Tested: bun run test -- --run src/stores/chatStore.test.ts
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
(cherry picked from commit 82381e8d647fa19e20853a96b9681fb548c17f0a)
Use frameless Electron chrome only on Windows and remove the native Windows application menu so the packaged app matches the previous Tauri-style desktop surface.
Add a Windows-only manual drag fallback for desktop drag regions, while excluding tab reorder targets and preserving macOS/Linux native chrome and menu behavior.
Tested: cd desktop && bun run test --run electron/services/menu.test.ts electron/services/windows.test.ts src/hooks/useElectronWindowDragRegions.test.tsx src/components/layout/TabBar.test.tsx src/components/layout/Sidebar.test.tsx src/lib/desktopHost/electronHost.test.ts electron/ipc/capabilities.test.ts
Tested: cd desktop && SKIP_INSTALL=1 bun run build:windows-x64
Tested: Computer Use Windows packaged app smoke verified no native menu, custom controls, drag regions, tab reorder, close-to-background, and deepseek-v4-pro provider response FINAL_WINDOWS_ELECTRON_REAL_PROVIDER_OK.
Not-tested: full bun run verify.
Constraint: keep custom frameless behavior scoped to win32 so macOS and Linux native chrome paths remain intact.
Confidence: high
Scope-risk: moderate
Use container queries for the activity summary grid so the Token usage panel responds to its own width instead of the full desktop viewport. This prevents loose medium-width layouts and over-compressed five-column cards when the settings page is shown with sidebars.
Tested:
- cd desktop && bun run test -- src/pages/ActivitySettings.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- cd desktop && bun run test -- src/theme/globals.test.ts --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- bun run check:desktop
- Electron smoke at 1180px and 900px against the local dev server
Not-tested:
- Coverage report; this is a presentation-only desktop UI change.
Confidence: high
Scope-risk: narrow
The sidebar title area also needs to participate in Electron's CSS app-region chrome. Mark only the sidebar title/header region as draggable and keep the full sidebar body out of the drag region so session rows, search controls, and project actions remain normal interactions.
Constraint: Electron window dragging is driven by CSS app-region regions after the migration
Rejected: Keep the sidebar-wide startDragging mouse handler | Electron does not use that as the active drag path and it misses the left title gap
Confidence: high
Scope-risk: narrow
Directive: Keep sidebar body interactions outside the drag region unless a real titlebar affordance is added there
Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx --run
Tested: bun run check:desktop
Bare absolute paths like /icons/bilibili.svg failed under Electron's
file:// protocol. Route them through publicAssetPath so they resolve
against the relative base URL, matching the GitHub and app icons.
Electron handles custom draggable chrome through CSS app-region rules, not the old runtime startDragging path. Mark the tab strip and empty scroll gutter as native drag regions while keeping tab items and controls explicitly no-drag so tab reordering and close/tool buttons keep receiving pointer events.
Constraint: Electron drag regions swallow pointer events unless interactive children are marked no-drag
Rejected: Keep calling startDragging from the empty gutter | Electron desktopHost does not expose that as the active migration path
Confidence: high
Scope-risk: narrow
Directive: Do not mark tab items themselves as drag regions without revalidating tab reorder behavior
Tested: cd desktop && bun run test src/components/layout/TabBar.test.tsx --run
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
The chat reference action was still tied too closely to message-local
mouseup timing, and the floating fixed-position control stayed inside
virtualized message DOM. In Electron Chromium that made multi-line ranges
race selection settlement and let transformed ancestors offset the button
away from the viewport position we calculated.
This reads settled document selections after pointerup/selectionchange,
portals the action to document.body, and prefers right-side placement for
multi-line selections.
Constraint: Electron Chromium selectionchange can settle after message-local mouse events
Rejected: Keep the popover inside the message node | transformed/virtualized ancestors offset fixed positioning in real browser layout
Confidence: high
Scope-risk: narrow
Directive: Keep chat selection actions portaled; do not reparent under virtualized message rows without browser-coordinate verification
Tested: cd desktop && bun run test -- src/components/chat/MessageList.test.tsx
Tested: bun run check:desktop
Tested: Playwright Chromium smoke for multi-line selection, single-line selection, and outside-click dismissal on local desktop harness
Add jp / kr / zh-TW translations and wire them into the i18n runtime so the
desktop app can switch among English, Simplified Chinese, Traditional Chinese,
Japanese, and Korean.
- new locale files jp.ts / kr.ts / zh-TW.ts with full TranslationKey coverage
- register the locales in the i18n index and extend the Locale union
- accept the new codes in settingsStore.getStoredLocale (default stays zh)
- add language-switcher entries (简体中文 / 繁體中文 / 日本語 / 한국어)
- localize chat message timestamps: Han 年月日 for zh/zh-TW/jp, Intl ko-KR for kr
- extend the DATE_LOCALES map in ActivitySettings (ja-JP / ko-KR / zh-TW)
- tests: formatMessageTimestamp locale branches + i18n locale resolution
Korean terminology follows the Microsoft Korean localization style guide
(새로 고침 / 사용·사용 안 함 / 이름 바꾸기, 합니다-style sentences).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chat selection references could miss drag gestures that ended outside the
message bubble after the Electron migration. The chat transcript now tracks
selection gestures at the document pointer-up boundary and repositions the
shared selection action near the selected text with above-then-right placement.
Constraint: Electron/WebView selection ranges are not always stable during the message-local mouseup event
Rejected: Add a new selection popover system | the existing shared selection hook already covers chat and workspace dismissal behavior
Confidence: high
Scope-risk: narrow
Directive: Keep chat and workspace selection popovers on the shared positioning/dismissal hook unless their behavior intentionally diverges
Tested: bun run test -- src/components/chat/MessageList.test.tsx -t "selected-message action"
Tested: bun run test -- src/components/chat/MessageList.test.tsx
Tested: bun run check:desktop
Tested: in-app browser smoke at http://127.0.0.1:5181/ with zero console errors
Not-tested: Packaged Electron manual drag selection smoke
Related: #351
Selecting a concrete /agent entry from the composer suggestions should prepare the command and leave focus in the editor so the user can type the required prompt. The Enter send shortcut still applies after the suggestion menu is closed, and exact non-agent slash commands keep their existing direct execution path when they remain highlighted.
Constraint: The desktop composer supports configurable Enter vs Ctrl/Cmd+Enter sending.
Rejected: Special-case only /agent entries | the highlighted suggestion should consistently win while the menu is open.
Confidence: high
Scope-risk: narrow
Directive: Keep ChatInput and EmptySession slash-key handling aligned with shared send shortcut behavior.
Tested: cd desktop && bun run test -- --run src/components/chat/composerUtils.test.ts src/components/chat/ChatInput.test.tsx src/pages/EmptySession.test.tsx
Tested: bun run check:desktop
Not-tested: Live backend agent-list integration beyond mocked desktop composer coverage.
Complete the Electron replacement boundary before merging by removing the renderer-side Tauri host fallback, tightening H5/browser access so only desktop navigation is tokenless, and moving desktop release publication to a tag-driven GitHub Actions matrix with a single final publish job.
Constraint: H5/browser capability access must not gain tokenless access through localhost or retired Tauri origins
Constraint: Desktop release artifacts must be built by GitHub Actions from version tags, not treated as local build outputs
Rejected: Keep localhost browser origins trusted for convenience | local browser contexts can access loopback services and must use the H5 token path
Rejected: Publish from each matrix job | partial releases can be created before all platforms finish
Confidence: high
Scope-risk: broad
Directive: Do not reintroduce Tauri origins or localhost browser origins into the trusted desktop origin set without a reviewed security design
Tested: bun test src/server/__tests__/h5-access-policy.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/diagnostics-service.test.ts src/server/middleware/cors.test.ts
Tested: bun test scripts/pr/release-workflow.test.ts scripts/release-update-metadata.test.ts
Tested: bun run check:desktop
Tested: bun run check:native
Tested: git diff --check
Not-tested: bun run check:server is blocked by expired quarantine entries server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
Electron desktop runs network-sensitive OpenAI OAuth token exchange in the sidecar process, so the sidecar now receives system proxy env derived from Electron's cross-platform proxy resolver and the OAuth token client uses the existing proxy fetch options. General manual proxy settings also document and preserve authenticated proxy URLs.
The macOS fullscreen black-screen path is addressed by avoiding native fullscreen Spaces for app fullscreen toggles and by leaving fullscreen before hiding or closing the window.
Constraint: Electron packaged apps may not inherit shell HTTPS_PROXY env when launched from Finder.
Constraint: Manual authenticated proxies must remain standard HTTP(S) proxy URLs for Bun/undici compatibility.
Rejected: Store proxy username and password as separate fields | would require new secret-storage semantics and migration beyond this bugfix.
Rejected: Use native macOS fullscreen Spaces for the desktop app | reproduced black-screen behavior when hiding/closing from fullscreen.
Confidence: high
Scope-risk: moderate
Directive: Do not remove sidecar proxy env injection without retesting OpenAI OAuth from a packaged app launched outside a shell.
Tested: bun test src/services/openaiAuth/client.test.ts src/server/__tests__/haha-openai-oauth-service.test.ts
Tested: bun test src/server/__tests__/network-settings.test.ts
Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx --run
Tested: cd desktop && bun run check:electron
Tested: git diff --check
Not-tested: bun run check:server blocked by expired quarantine entries server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
Not-tested: live OpenAI OAuth through a corporate authenticated proxy
Introduce the Electron desktop shell alongside the existing React renderer and local Bun server boundary. The migration keeps the DesktopHost contract explicit across Tauri, Electron, and browser runtimes while adding Electron main/preload services for dialogs, shell, notifications, updates, tray/window lifecycle, terminal, preview WebContentsView, app mode, and release/package validation.
The commit also carries the latest local main desktop command updates, including agent slash entries and hidden-by-default markdown thinking details, so the packaged Electron build matches the current main UX surface.
Constraint: React renderer, local Bun server, REST/WebSocket, and sidecar boundaries must remain reusable during the migration
Constraint: macOS dev packages are ad-hoc signed and cannot prove Developer ID notarization or Gatekeeper release launch
Rejected: Browser-only smoke validation | it cannot exercise native dialogs, keychain prompts, notification behavior, or packaged app startup
Confidence: medium
Scope-risk: broad
Directive: Do not remove Tauri host support until signed Electron release artifacts pass native OS smoke on macOS, Windows, and Linux
Tested: bun run check:desktop
Tested: cd desktop && bun run check:electron
Tested: CSC_IDENTITY_AUTO_DISCOVERY=false bun run electron📦dir
Tested: bun run test:package-smoke --platform macos --package-kind dir --artifacts-dir desktop/build-artifacts/electron
Tested: Computer Use read packaged Electron app window at desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app
Not-tested: Developer ID signed/notarized Gatekeeper launch
Not-tested: Real OS notification click-to-session action
Not-tested: Windows and Linux packaged app smoke on real hosts
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
The desktop app can quit while a preview-driven Bash task is still running npm/vite. Bash commands are spawned as detached process groups, so killing only the CLI or shell process can leave descendant dev servers alive and make later app launches look stuck.
This gives the CLI enough shutdown budget to run cleanup, lets the native sidecar wait long enough for that server cleanup, kills detached Bash process groups before the existing tree-kill fallback, and closes the native preview WebView instead of hiding it on workbench unmount.
Constraint: CLI gracefulShutdown has a 5s failsafe, so outer desktop shutdown windows must not SIGKILL it after 2-3s
Rejected: Treat the browser preview as the dev-server owner | it only opens URLs and does not spawn npm/vite
Confidence: high
Scope-risk: moderate
Directive: Do not shorten desktop/server shutdown windows below the CLI cleanup budget without reproducing BashTool background-task teardown
Tested: bun test src/utils/ShellCommand.test.ts src/server/__tests__/conversation-service.test.ts
Tested: cd desktop && bun run test -- --run src/components/browser/BrowserSurface.test.tsx src/components/workbench/WorkbenchPanel.webview.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:server
Tested: SKIP_INSTALL=1 ./desktop/scripts/build-macos-arm64.sh
Tested: Opened canonical app and generated DMG app; both showed visible/frontmost window and left no app/sidecar/vite processes after quit
Not-tested: Notarized release install path
Preview annotations should keep the selected element readable. The selected rectangle now carries a stronger translucent blue fill, while the numbered badge is a separate high-contrast fixed overlay placed outside the selected rect and clamped to the viewport.
Constraint: No issue number was provided for this task.
Constraint: Preserve the DOM-captured annotation path introduced for viewport-safe screenshots.
Rejected: Put the badge inside the selected box | it obscures compact buttons and labels.
Rejected: Return to canvas-side badge drawing | that reintroduces the separate coordinate system.
Confidence: high
Scope-risk: narrow
Directive: Keep numbered badges outside compact selected targets unless visual evidence shows a better non-obscuring placement.
Tested: cd desktop && bun run test --run src/preview-agent/screenshot.test.ts src/preview-agent/picker.test.ts src/preview-agent/metadata.test.ts src/preview-agent/protocol.test.ts src/preview-agent/bridge.test.ts src/preview-agent/editBubble.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 visual smoke.
The profile edit affordance is useful but low-frequency, and it was always visible in the token activity header. Hide it by default so shared screenshots keep the profile header clean while hover and keyboard focus still expose the action.
Constraint: The edit action must remain discoverable for pointer users and reachable for keyboard users.
Rejected: Remove the edit button entirely | profile customization still needs an obvious local entry point.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: cd desktop && bun run test -- src/pages/ActivitySettings.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
Tested: git diff --check -- desktop/src/pages/ActivitySettings.tsx desktop/src/pages/ActivitySettings.test.tsx
Not-tested: Manual packaged desktop screenshot capture.
Selection screenshots were drawing the marker after html2canvas finished by
mapping DOM coordinates onto a separate canvas. That leaves room for drift when
html2canvas renders at a different output size, the viewport is resized, or the
thumbnail is compressed. The marker is now a fixed DOM overlay captured with the
page itself, so the chosen element and its marker share the same viewport,
layout, crop, and scale path.
Constraint: No issue number was provided for this task.
Constraint: Preserve the existing selection event payload and preview bridge protocol.
Rejected: Keep canvas post-processing with more scale math | it still maintains a second coordinate system beside the rendered page.
Rejected: Keep the unused annotate module | it would preserve the stale path that caused this class of drift.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce canvas-side selection marker math unless it is proven against resized and scaled viewport captures.
Tested: cd desktop && bun run test --run src/preview-agent/screenshot.test.ts src/preview-agent/picker.test.ts src/preview-agent/metadata.test.ts src/preview-agent/protocol.test.ts src/preview-agent/bridge.test.ts src/preview-agent/editBubble.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 visual smoke.
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