api_retry heartbeats already reach the desktop status bar, but the
streaming-to-non-streaming fallback had no signal at all: the CLI only
flipped an internal flag, and the one-shot fallback response can take
minutes with zero incremental output, so the UI showed a bare spinner
the whole time.
- CLI: yield a {type:'system', subtype:'streaming_fallback', cause}
message at both fallback sites (stream error/watchdog and 404 stream
creation), mirroring the existing api_error -> api_retry path through
query.ts passthrough, QueryEngine SDK output, and the SDK schema.
- Server: translate it to a streaming_fallback ServerMessage;
unrecognized causes normalize to 'unknown' instead of dropping the
event.
- Desktop: track it as active-turn state (cleared at the same 12 sites
as apiRetry; a fallback notice supersedes the stale retry banner) and
render a neutral pill with the turn timer - expected state, not an
error, so no amber styling and nothing in the diagnostics panel.
- Retry banner now shows "retrying now" once the countdown elapses
instead of sticking at "waiting 0s".
With 62241a31 disabling the non-streaming fallback for desktop CLI
sessions, this notice mainly covers the 404 gateway path (which skips
the disable check), callers that re-enable fallback via env, and
non-desktop SDK consumers.
Constraint: Retries and fallbacks are expected states per the
diagnostics severity standard - lightweight active-turn UI only, no
error-panel entries, no transcript persistence.
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run test -- --run
Tested: cd desktop && bun run lint
Not-tested: live provider outage reproduction; verified via unit
coverage of the translation, store lifecycle, and indicator rendering.
The context usage indicator stayed at the pre-compact percentage (e.g.
100%) until the next API response arrived. Two stacked causes:
- The CLI anchors the displayed total to max(local estimate, last API
usage) so the meter never drops mid-turn — but preserved messages
(SM-compact / partial compact) still carried the pre-compact usage,
pinning the meter after compaction. buildPostCompactMessages now
zeroes token usage on preserved assistant copies, the established
stale placeholder that getCurrentUsage() skips, covering every
compaction path in one place. Originals are not mutated, so
transcript and cost accounting are unaffected.
- Right after compaction the CLI is often still busy, so the
indicator's refresh timed out ("Request timed out after 30s") and the
catch kept the stale context on screen with no later retry (auto
refresh is throttled to 10s and stops once the session goes idle).
compact_boundary now bumps a per-session compactCount; the indicator
force-refreshes on that nonce — bypassing the throttle and any
in-flight request that may still hold pre-compact data — and retries
once after 5s if the forced refresh fails.
Tested: bun test src/services/compact/
Tested: cd desktop && bun run test -- src/components/chat/ContextUsageIndicator.test.tsx src/stores/chatStore.test.ts
Tested: cd desktop && bun run lint
Confidence: high
Scope-risk: narrow
Desktop injected a far stricter timeout stack than the terminal CLI, so
healthy-but-slow third-party providers (sensenova/bailian/zhipu) died at
exactly API_TIMEOUT_MS while the UI showed "running" forever:
- API_TIMEOUT_MS is the SDK client's time-to-first-byte budget for
streaming requests; these gateways send zero bytes (no headers, no
SSE ping) until prefill finishes, which takes minutes at large
contexts. Raise the default from 120s to the SDK's own 600s and widen
the configurable range to 30-1800s.
- Widen the desktop-forced stream watchdog idle window to 240s so
silent thinking/prefill phases stop tripping the 90s default.
- Disable the non-streaming fallback for desktop CLI sessions: a
non-streaming request only responds after the FULL generation, so it
can never finish inside the same budget and loops timeout aborts
forever while the UI spins (also avoids double tool execution,
upstream inc-4258). All three knobs respect caller env overrides.
Repro: mock upstream whose SSE stays silent for 150s before a complete
event sequence — terminal env completes; desktop env aborts at exactly
120s (client timeout) or 90s watchdog + non-streaming fallback loop;
the fixed env completes both variants.
Tested: bun test src/server/__tests__/network-settings.test.ts src/server/__tests__/conversation-service.test.ts src/server/__tests__/proxy-network-settings.test.ts
Tested: cd desktop && bun run test -- src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx
Tested: cd desktop && bun run lint
Confidence: high
Scope-risk: medium
Add pending user-message queue controls while a desktop turn is running, replay queued prompts into the transcript when the CLI accepts them, and flush remaining prompts after turn completion.
Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx
Tested: cd desktop && bun run test -- src/stores/chatStore.test.ts
Tested: bun test src/server/__tests__/ws-memory-events.test.ts
Tested: bun run check:desktop
Not-tested: bun run check:server remains blocked by unrelated E2E CORS preflight expectation, expected 403 but got 204.
Confidence: medium
Scope-risk: moderate
Increase the desktop session list fetch window so noisy observer sessions are less likely to hide real user sessions from the sidebar.
Tested: cd desktop && bun run test -- src/stores/sessionStore.test.ts src/components/layout/Sidebar.test.tsx
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Token counts were rendered with five inconsistent formats across the
chat UI (header "181,117 t", in-progress bare "↓ 2514", background
agents "12,345 tokens", compact summary "1.5k", trace "1.2k"), and the
in-progress count was actually stale: the server never set the status
event's tokens field, so the indicator showed the previous turn's value
(hence "first message shows nothing", "sometimes appears, never moves").
- Add shared desktop lib/formatTokenCount; route header, streaming
indicator, background agents, compact summary, and trace through it.
- Header shows compact "124.3k tokens" with the exact count on hover.
- Add common.tokens i18n key (en/zh/zh-TW/jp/kr).
- Estimate the in-progress count from streamed chars (÷4, mirroring the
CLI spinner) via a new streamingResponseChars per-session field, reset
on each send; drop the dead status.tokens/elapsed fields.
- CLI spinner rows use formatTokens to drop the "1.0k" artifact.
Show ExitPlanMode approvals as a rendered plan preview in desktop chat,
forward plan feedback and requested prompt permissions through the desktop
WebSocket permission response, and keep permission-mode restoration owned by
the CLI runtime.
Tested: bun run verify
Confidence: high
Scope-risk: moderate
Expose the existing CLI Auto-dream switch in General settings with an explicit opt-in confirmation before enabling background memory consolidation.
Tested: bun run check:desktop
Tested: ./bin/claude-haha --help
Tested: CLAUDE_CONFIG_DIR temp settings true/false autoDreamEnabled check
Tested: real provider Auto-dream trigger against temporary memory copy
Scope-risk: narrow
Confidence: high
Prevent stale same-version update metadata from surfacing another install prompt, avoid showing a fake desktop version fallback, and configure the Windows NSIS installer to expose install directory selection.
Fixes#801
Tested: bun run verify
Confidence: high
Scope-risk: moderate
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
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
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
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
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)
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>
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
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
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.
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.
Dismissed completed task lists are a user visibility choice for that completed cycle, so polling the same persisted tasks during the next chat turn should not resurrect the bar. The store now keeps the dismissed completion key while the remote reset is in flight, and the markdown renderer gives default lists enough inset so bullets stay inside message cards.
Constraint: Preserve future task cycles when new TodoWrite or task updates arrive.
Rejected: Clear only the visible task array | polling can rehydrate the same completed list before remote reset finishes
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bun run test -- run src/stores/cliTaskStore.test.ts src/components/markdown/MarkdownRenderer.test.tsx
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Live Tauri chat session task creation and dismissal
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.
Move update downloads into the background and prompt only after the update is ready, so the global update UI no longer pins a progress panel over the app. Keep the About page as the detailed status surface and make failed downloads or installs retryable without losing the prepared update.
Constraint: Tauri updater install still needs an explicit user restart action after download
Rejected: Keep showing global download progress | it preserves the blocking top-right experience that triggered the complaint
Confidence: high
Scope-risk: moderate
Directive: Do not show the global update prompt before status is downloaded without rechecking the desktop UX complaint
Tested: bun run test -- src/stores/updateStore.test.ts src/components/shared/UpdateChecker.test.tsx --run
Tested: bun run check:desktop
Tested: bun run check:native
Tested: bun run check:coverage
Tested: bun run verify
Not-tested: Real signed release artifact update from an installed older desktop build
MCP settings could remount into a full-page spinner even when cached server data was available, and overlapping refreshes could let stale responses replace newer state. Historical chat sessions also had no explicit history-loading state, so slow transcript reads looked like a blank session.
This keeps cached MCP/session lists visible during refresh, ignores stale list responses, clears the selected MCP server when returning to the list, and gives historical sessions explicit loading/error states with de-duped history loads.
Constraint: Fix must stay local-state focused and avoid changing MCP config persistence.
Rejected: Add a broad transcript/session indexing layer | too large for the issue-level stall fix.
Confidence: high
Scope-risk: moderate
Directive: Do not remove the stale-response guards without re-testing rapid settings navigation and session switching.
Tested: cd desktop && bun run test -- mcpSettings.test.tsx
Tested: cd desktop && bun run test -- sessionStore.test.ts
Tested: cd desktop && bun run test -- chatStore.test.ts
Tested: cd desktop && bun run test -- ActiveSession.test.tsx
Tested: bun test src/server/__tests__/sessions.test.ts src/server/__tests__/mcp.test.ts
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run check:desktop
Tested: bun run check:server
Not-tested: Manual Windows desktop reproduction for issue #648.
Routes preview://event (navigated/ready) from Rust into the browser store via a new subscribePreviewEvents subscriber; adds title field and setNavigated/setReady reducers to BrowserSessionState; BrowserSurface subscribes on mount to replace M1 optimistic nav-state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-session in-app mini-browser state: current URL, back/forward
history stack (historyIndex with forward-truncation on navigate),
loading flag, and element-picker toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Permission mode is a per-session runtime choice, while scheduled tasks cannot rely on a human approval loop. This removes the General settings permission surface, persists session permission metadata, and forces scheduled tasks to run with bypass permissions. Runtime permission changes now handle both directions across bypass boundaries, including startup/prewarm races, by persisting first and restarting only when the CLI launch mode must change.
Constraint: Scheduled tasks must be able to execute without a human standing by for authorization.
Constraint: The CLI only honors bypass permissions when launched with the skip-permissions flag, so switching to or from bypass requires a restart.
Rejected: Keep a global General permission default | it can leak across sessions and scheduled runs in ways the user cannot reason about.
Confidence: high
Scope-risk: broad
Directive: Do not reintroduce a global UI permission selector without proving it cannot affect unrelated sessions or automations.
Tested: bun test src/server/__tests__/conversations.test.ts -t "permission" --timeout 30000 (10 pass, 0 fail)
Tested: bun run check:server (858 pass, 0 fail)
Tested: cd desktop && bun run check:desktop (760 tests plus production build passed)
Tested: desktop/scripts/build-macos-arm64.sh and codesign verification passed
Tested: Real DeepSeek smoke validated plan, bypass, and scheduled task permission behavior
Not-tested: Did not repeat the full DeepSeek smoke after the final startup-race hardening; mock WebSocket permission regression and full server gate were rerun after that change.
Fixes#632
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
Users can be typing in the docked terminal when they hide the panel or promote it into a tab. The old component-owned lifecycle treated those UI moves as terminal teardown and spawned a fresh shell afterward.
This moves terminal ownership into a small runtime registry keyed by panel or tab identity, keeps docked terminals mounted while hidden, and transfers the runtime id to the terminal tab when promoted. Closing the owning tab or session still releases the PTY.
Constraint: Keep native PTY behavior unchanged and fix this in the desktop React lifecycle layer.
Rejected: Persist terminal tabs through localStorage | runtime PTYs are process-local and should not be restored after app restart.
Confidence: high
Scope-risk: moderate
Directive: Do not tie terminal process lifetime to panel visibility; only explicit owning-surface close/restart should destroy it.
Tested: cd desktop && bun run test -- src/pages/TerminalSettings.test.tsx src/pages/ActiveSession.test.tsx src/components/layout/ContentRouter.test.tsx src/stores/tabStore.test.ts
Tested: bun run check:desktop
Not-tested: Manual Tauri window PTY smoke.
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
Desktop MCP local and project scopes were still anchored to the active session workDir when adding or editing servers, which made a selected session worktree look like the target project. The form now treats the target project as explicit input for local/project scopes, and the server update path can remove the old scoped config before writing to the newly selected directory.
Constraint: Local MCP scope is private user config keyed by project path; project scope writes the selected project's .mcp.json
Rejected: Keep using the active session workDir as the implicit target | it can point at transient session worktrees and hides where config is written
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce implicit active-session cwd writes for MCP create/edit; local and project scopes need an explicit target project
Tested: cd desktop && bun run test -- mcpSettings.test.tsx
Tested: bun test src/server/__tests__/mcp.test.ts
Tested: cd desktop && bun run check:desktop
Tested: bun run check:server
Tested: bun run quality:gate --mode baseline --allow-live --only 'provider-smoke:*' --provider-model deepseek:main:deepseek-main
Tested: Live filesystem MCP probes with DeepSeek deepseek-v4-pro across local/project scopes; see artifacts/quality-runs/mcp-live-split-2026-05-25T09-15-18-245Z/summary.json
Not-tested: Full bun run verify was not rerun after the live MCP smoke
Related: #585
Desktop MCP settings were using the process startup directory for local
project config and treated new custom servers as user-global. The desktop
API now carries the active cwd into local MCP reads, writes, and enablement
state, and the settings page exposes the same local/project/user scopes that
the CLI supports.
Constraint: Claude Code MCP scope semantics are local, project, and user.
Constraint: Desktop sessions can switch projects inside one long-lived server process.
Rejected: Only clear getProjectPathForConfig cache | still leaves request-scoped desktop API calls tied to getOriginalCwd
Confidence: high
Scope-risk: moderate
Directive: Do not route MCP local config or disabledMcpServers through ambient original cwd in desktop APIs.
Tested: bun test src/server/__tests__/mcp.test.ts
Tested: cd desktop && bun run test -- mcpSettings.test.tsx
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: Live provider smoke; the change is config/control-channel scoped.
Sleep/wake reconnects can resend persisted assistant text as live output
without transcript ids. Treat that output as replay when it already exists
at the hydrated chat tail, and collapse duplicate transcript-backed text
after history refresh so the UI does not append repeated replies or send
new completion notifications for old turns.
Constraint: Keep the fix inside desktop chat state; do not change the WebSocket protocol or transcript storage shape.
Rejected: Add a macOS sleep/wake listener | it would not address replayed stream events from other reconnect/resume paths.
Confidence: high
Scope-risk: narrow
Directive: Do not remove replay dedupe without testing sleep/wake reconnect output against transcript id hydration.
Tested: bunx vitest run src/stores/chatStore.test.ts --testNamePattern "replay|collapses duplicate"
Tested: bun run test -- --run src/stores/chatStore.test.ts
Tested: bun run check:desktop
Not-tested: Real macOS sleep/wake desktop smoke with native notifications.
H5 access broke after switching Wi-Fi: the previously saved private-LAN host
(e.g. 192.168.1.207) was no longer bound to any interface on the current
network (which had moved to 192.168.0.x), but commit da8308de only refreshed
the stale *port* and kept the stale *hostname*. The QR code therefore pointed
at an IP this machine no longer had, and phones got TCP-refused.
Two changes:
1. resolveEffectiveH5PublicBaseUrl now accepts the set of local IPv4 hosts
and, when the stored URL is a plain private-LAN HTTP URL whose hostname
is no longer on any interface, falls back to the auto-discovered URL —
without overwriting the stored value, so switching back to the original
network restores it automatically.
2. H5AccessService.updateSettings now validates the publicBaseUrl on save.
Plain LAN URLs whose host is not on any local interface are rejected with
a 400 + suggested LAN IP. Reverse-proxy URLs (https, custom path, hostname
targets) are accepted unchanged because reachability is owned by the
user's tunnel / nginx / cloudflared setup, not the desktop.
GET /api/h5-access also returns a diagnostics block (storedHostStaleness,
suggestedHost, localInterfaceHosts, effectivePublicBaseUrl) so the desktop
Settings page can render a warning banner with a one-click switch to the
current LAN IP, plus a quieter note for proxy URLs.
Constraint: Reverse-proxy users may legitimately point H5 at a hostname that
is not on this machine's network adapters; we must not regress that path.
Rejected: Reachability-probe arbitrary public URLs from the server | the
desktop cannot reliably round-trip through the user's external tunnel and
should not pretend to validate it.
Rejected: Overwrite the stored host with the auto host | users on multi-
network laptops would lose their pinned WSL/Docker-aware choice on every
Wi-Fi switch.
Confidence: high
Scope-risk: moderate
Directive: Do not narrow validateH5PublicBaseUrl to reject reverse-proxy
URLs without restoring an explicit opt-in path for them.
Tested: bun test src/server/__tests__/h5-access-service.test.ts (21 pass)
Tested: bun test src/server/__tests__/h5-access-api.test.ts src/server/__tests__/h5-access-auth.test.ts src/server/__tests__/h5-access-policy.test.ts (57 pass)
Tested: bun run check:server (840 pass)
Tested: bun run lint && bun test --run in desktop/ (734 pass, 91 files, +3 new H5 banner / proxy-note / save-error cases)
Not-tested: Real phone scan against a packaged desktop build after a Wi-Fi switch.
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>
- 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>