Add a Codex-style global search dialog (Cmd+K / sidebar button) that
full-text searches across all session transcripts, replacing the old
title-only sidebar filter.
Backend: rewrite searchService.searchSessions as a two-phase engine —
ripgrep finds candidate files + matched lines, then those lines are
parsed to keep only user/assistant text, re-confirmed against the
cleaned text to drop JSON/UUID/base64 false positives, and windowed into
highlighted snippets. Results carry real session titles (new
sessionService.getSessionTitleAndMeta reusing the list title
precedence), project path, mtime, role and match counts; falls back to a
JS scan when ripgrep is unavailable.
Frontend: new GlobalSearchModal (debounced, stale-response-safe, keyboard
nav, role badges, highlighting, recent-chats empty state); Cmd+K now
opens it; the sidebar input is replaced by a search trigger button.
Tests: 15 backend cases (searchService.sessions) + 12 frontend cases
(GlobalSearchModal); existing Sidebar/pages tests updated for the new
trigger.
Prefer the live git diff for currently changed workspace files so DIFF previews do not get stuck on stale transcript snapshots. Refresh an existing workspace preview tab when it is opened again instead of only reactivating cached content.
Tested: bun test src/server/__tests__/workspace-service.test.ts
Tested: cd desktop && bun run test src/stores/workspacePanelStore.test.ts --run
Tested: bun run check:server
Tested: bun run check:desktop
Confidence: high
Scope-risk: narrow
Third-party gateways can trickle content deltas (e.g. a large tool_use
input_json_delta) just under the idle-watchdog window and never send
message_stop. Each delta resets the idle timer, so the in-kernel watchdog
never fires and the request hangs forever — the desktop shows "running"
with slowly growing tokens and a permanently-pending trace span (#766).
Add an overall-duration watchdog (CLAUDE_STREAM_MAX_DURATION_MS) that is
armed once and never reset by incoming chunks; the desktop injects 600000ms
via buildChildEnv. Orthogonal to the idle watchdog: idle catches a fully
silent stream, this catches an endless trickle that never completes. CLI
default stays 0 (disabled) for backward compatibility.
Verified end-to-end by replaying the issue's real captured SSE against the
actual kernel: trickle hangs without the cap, aborts cleanly with it.
Keep provider ordering compatible with older desktop store state, keep workspace traversal blocked even when external changed-file roots are registered, and align the quality-contract test with the current AGENTS wording.
Tested: bun test scripts/pr/quality-contract.test.ts
Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx src/__tests__/skillsSettings.test.tsx src/__tests__/pluginsSettings.test.tsx src/__tests__/diagnosticsSettings.test.tsx --run
Tested: bun run check:policy
Tested: bun run check:desktop
Tested: bun test src/server/__tests__/workspace-service.test.ts -t 'does not allow relative traversal'
Tested: bun test src/server/__tests__/sessions.test.ts -t 'workspace/file and tree should reject traversal|workspace/diff should reject traversal' --timeout=20000
Confidence: high
Scope-risk: narrow
Fix four root causes in the desktop preview pipeline, surfaced when the
model writes the files the user pointed it at:
- Output chips guessed paths from prose and could point at a missing file.
They are now reconciled against the turn's real changed files: a bare
`index.html` resolves to the `todo-app/index.html` actually written, and
mentions the turn never changed are dropped.
- A standalone single-page index.html got no browser preview (mistaken for a
Vite template). It is now only routed to the source view when a
package.json/vite.config ships in the same change-set.
- Files written outside the session workdir (another folder, or another drive
on Windows) failed to preview with 'Path is outside workspace'. The turn's
changed-file directories are registered as filesystem access roots; html
serves via /local-file and other files via a workdir-relaxed read.
- The visual-selection prompt leaked as a raw bubble on Windows because the
server-appended '[Image source: ...]' line broke replay dedupe. Replay text
is now metadata-normalized before comparison (affects any image message).
Adds unit tests for each: htmlPreviewPolicy, assistantOutputTargets
reconciliation, replay dedupe + stripGeneratedImageMetadataLines, filesystem
access roots, and workspace outside-workdir reads.
Render provider settings through dnd-kit sortable rows, include official providers in the same order model, and persist providerOrder across server and desktop state.
Tested:
- bun test src/server/__tests__/providers.test.ts src/server/__tests__/persistence-upgrade.test.ts
- cd desktop && bun run test -- --run src/stores/providerStore.test.ts src/__tests__/generalSettings.test.tsx -t "providerStore reorderProviders|Settings > Providers tab"
- cd desktop && bun run lint
- cd desktop && bun run build
- bun run check:persistence-upgrade
Not-tested:
- bun run check:server (broad suite hit environment failures during this run: MCP stdio zshrc timeout, adapter dependency gap at the time, and e2e cascade)
Confidence: high
Scope-risk: moderate
When an upstream request was aborted mid-stream (SDK client timeout,
stream idle watchdog, non-streaming fallback timeout, or user
cancellation), the trace fetch hook waited on a clone of the response
body that could hang forever, so the call never left "pending" in the
trace panel — exactly the silent stall that misled the #766 report.
- captureResponseTraceSnapshot reads the body with abort awareness:
reader.cancel() on abort keeps the partial body, with a 2s grace
backstop for runtimes where cancel cannot wake a hung read.
- The fetch hook now records an error-state call on abort with the
abort reason (e.g. the watchdog's stream idle timeout), duration,
partial response body, and an api_call_aborted event; non-abort
capture failures also record an error instead of inferring ok, and
pre-response fetch rejections carry an aborted flag.
- The trace detail panel shows an "Aborted" badge plus guidance for
aborted calls, and labels the new api_call_aborted phase in all
locales.
Tested: bun test src/server/__tests__/trace-capture.test.ts
Tested: bun run check:server
Tested: cd desktop && bun run test -- --run && bun run lint
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
Reveal-in-Explorer/Finder rejected ~-prefixed paths because no layer
expanded the tilde to the home directory. Expand it in the three path
normalization entry points: server validateOpenPath, frontend
resolveAbsolute, and Electron normalizeOpenPath. Tilde expansion is
platform-aware (~\ only on win32, where backslash is a separator).
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
Ensure Electron sidecar launch and Windows taskkill calls hide console windows, and pass the same hidden-window spawn option through desktop CLI and scheduled-task subprocess launches.
Tested: cd desktop && bun test ./electron/services/sidecarManager.test.ts
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/cron-scheduler-launcher.test.ts
Tested: bun run check:native
Tested: bun run check:server
Not-tested: Windows GUI quit smoke
Confidence: medium
Scope-risk: narrow
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
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
Ensure custom OpenAI-compatible provider tests explicitly request non-stream responses for both direct connectivity checks and the proxy pipeline. This avoids default SSE responses being parsed as empty JSON by provider validation.
Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "requests non-stream OpenAI Chat responses during provider tests"
Tested: bun test src/server/__tests__/providers.test.ts
Tested: bun test src/server/__tests__/proxy-transform.test.ts
Tested: bun run check:server
Confidence: high
Scope-risk: narrow
Prevent desktop prewarm launches from inheriting interrupted-turn resume state, while leaving explicit user-message startup unchanged. Also stop server-side background schedulers during shutdown so app quit cannot leave scheduled task runners alive.
Tested:
- bun test src/server/__tests__/conversation-service.test.ts
- bun test src/server/__tests__/conversations.test.ts -t "prewarm"
- bun test src/server/__tests__/conversations.test.ts -t "permission"
- bun test src/server/__tests__/diagnostics-service.test.ts -t "keeps fatal startup errors visible on stderr while recording diagnostics"
- bun test desktop/electron/services/windows.test.ts desktop/electron/services/tray.test.ts desktop/electron/services/sidecarManager.test.ts src/server/__tests__/server-shutdown.test.ts
- bun run check:server
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
The diagnostics panel filled with red ERROR/WARN entries during normal use
because the capture layer treated "wrote to console" as "something failed",
with no severity gatekeeping. Much of the noise was also test runs leaking
into the user's real ~/.claude/cc-haha/diagnostics.
- diagnosticsService: default unclassified events to info (not error);
drop writes under NODE_ENV=test when no CLAUDE_CONFIG_DIR is set; document
the console-capture contract (expected states use console.debug/info).
- index: don't install console/process capture under bun test.
- api/diagnostics: an ingested event with missing severity defaults to info.
- oauthRefreshLog (new): token refresh failure is gracefully handled and is
never an error — expected expiry (401/403/revoked) logs at debug, anything
else at warn. Wired into both Haha OAuth services.
- conversationService: classify cli_runtime_exit severity by exit code, so
clean/SIGTERM/SIGKILL exits are info and only abnormal codes stay error
(real "chat died" crashes remain perceivable).
- ws/handler: streaming partial tool-input JSON is normal — debug, not warn.
Tests: oauth-refresh-log + cli-exit-severity unit tests; diagnostics-service
covers the info default and the test-isolation guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Async title refresh runs after assistant output, so provider prompts can be biased by assistant language and English title examples. The title language now resolves from the first meaningful user message before consulting the response-language fallback, then retries once when a generated title violates that language.
Constraint: Desktop response-language settings still control assistant prose, not automatic title metadata when the first user message has a clear language
Rejected: Follow response-language setting for every generated title | Chinese sessions flip to English when replies are configured in English
Rejected: Persist mismatched generated titles after retry failure | preserves a bad UX over the safer first-message placeholder
Confidence: high
Scope-risk: moderate
Directive: Do not change automatic title language precedence without re-testing image/text first-turn sessions and English-response-language sessions
Tested: bun test src/server/__tests__/title-service.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts -t "refreshes the first-turn AI title"
Tested: git diff --check
Tested: real MiniMax-M3 /tmp session 58a47fcd-6f9d-4685-bc73-f9317e3f330e with temporary response language english; assistant replied English and final aiTitle was 标题语言验证
Not-tested: bun run check:server blocked before tests by expired quarantine entries server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
Desktop title generation previously upgraded the first sidebar title from the raw user prompt before assistant output existed. That made image-plus-text sessions vulnerable to keeping a title based only on the user's initial wording. This moves polished title generation to completed turns, reuses the CLI title prompt and transcript extractor, and routes title calls through the configured haiku model with a retry when disabled thinking is rejected.
Constraint: The first user message still needs an immediate placeholder title so the sidebar is responsive.
Rejected: Generate the AI title before assistant output | image-derived context is only available after the assistant turn completes.
Confidence: high
Scope-risk: moderate
Directive: Keep polished automatic titles tied to completed transcript turns, not pre-response user prompts.
Tested: bun test src/server/__tests__/title-service.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts -t "refreshes the first-turn AI title"
Tested: git diff --check
Tested: Real MiniMax-M3 /tmp session f89073ed-3467-492a-b63c-e1886c146a3a with image attachment wrote ai-title "Validate session title generation"
Not-tested: bun run check:server is blocked by existing expired quarantine manifest entries
Desktop chat can fall back to persisted transcript estimates when live inspection context is unavailable. That path previously only used built-in model windows, so custom provider settings such as MiniMax-M3 with a 1,000,000-token window still displayed the default 200,000-token estimate. Resolve transcript context windows from the session or active provider runtime env first, then fall back to built-ins.
Constraint: Provider model context windows are stored in provider runtime env, not always in the server process env.
Rejected: Change the built-in MiniMax-M3 window to 1,000,000 | upstream preset correctly keeps MiniMax-M3 at 204,800 by default, while user overrides must remain provider-specific.
Confidence: high
Scope-risk: moderate
Directive: Keep transcript estimates aligned with session/provider runtime env before consulting built-in model defaults.
Tested: git diff --check
Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "active provider model context windows|Sonnet 4.6 transcript usage|low-trust media"
Tested: bun test src/server/__tests__/provider-presets.test.ts src/server/__tests__/provider-runtime-env.test.ts src/server/__tests__/providers.test.ts src/utils/__tests__/context.test.ts src/utils/__tests__/contextBudget.test.ts
Tested: bun test src/server/__tests__/conversations.test.ts src/server/__tests__/conversation-service.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/provider-presets.test.ts src/server/__tests__/provider-runtime-env.test.ts src/utils/__tests__/context.test.ts src/utils/__tests__/contextBudget.test.ts
Not-tested: bun run check:server blocked before execution by expired quarantine entries: server:cron-scheduler, server:providers-real, server:tasks, server:e2e:business-flow, server:e2e:full-flow
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
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
Packaged desktop shutdown was killing the server sidecar with SIGKILL on Unix, which skipped server-side cleanup for CLI subprocesses spawned by active chat sessions. The native layer now gives sidecars a SIGTERM grace window, and the server waits for active CLI sessions to exit before completing signal-driven shutdown.
Constraint: Tauri shell CommandChild::kill maps to SIGKILL on Unix.
Rejected: Only clean stale processes on next launch | leaves user-owned sessions and sockets in an unknown state after every unclean app exit.
Confidence: high
Scope-risk: moderate
Directive: Do not replace the SIGTERM grace path with direct CommandChild::kill without rechecking packaged-app process-tree shutdown.
Tested: bun test src/server/__tests__/conversation-service.test.ts
Tested: bun run check:server
Tested: bun run check:native
Tested: git diff --check
Not-tested: Fresh DMG install plus manual quit/reopen process-tree smoke.
Changed-file menus pass file paths while the workspace header passes directories. The server-side open-target path validation only accepted directories, so file-level editor launches failed before reaching the existing desktop open-with integration.
Constraint: Preserve the existing open-target API and frontend menu contract.
Rejected: Add a desktop-only file-opening endpoint | duplicates the shared open-target launch path.
Confidence: high
Scope-risk: narrow
Directive: Keep file-manager behavior platform-aware; Finder and Explorer can reveal/select files, while Linux xdg-open falls back to the parent directory.
Tested: bun test src/server/__tests__/open-target-service.test.ts src/server/__tests__/open-target-api.test.ts
Tested: cd desktop && bun run test src/components/chat/CurrentTurnChangeCard.test.tsx src/lib/openWithItems.test.ts
Tested: bun run check:server
Tested: git diff --check
The token usage profile header had too much top spacing, the edit action competed with the content from the top-right corner, and the profile subtitle was fixed to the default project link. This keeps the profile controls close to the identity block, moves the modal scrim to the document body so it covers the whole desktop shell, and persists a user-editable second line with URL auto-linking.
Constraint: Preserve existing profile preferences and backfill older records that do not have a subtitle field.
Rejected: Keep the top-right edit button | it kept the action visually detached from the profile content.
Rejected: Store the second line only in desktop local state | the preference already has a server-backed profile shape.
Confidence: high
Scope-risk: narrow
Directive: Keep profile preference schema changes backward-compatible with older desktop-ui preference files.
Tested: cd desktop && bun run test --run src/pages/ActivitySettings.test.tsx src/api/desktopUiPreferences.test.ts
Tested: bun test src/server/__tests__/desktop-ui-preferences.test.ts
Tested: bun run check:persistence-upgrade
Tested: bun run check:server
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Full Tauri native shell smoke; renderer flow was smoke-tested in browser only.
Add a local profile surface to the desktop token usage page, including avatar
persistence, display-name editing, cumulative usage metrics, and daily, weekly,
and cumulative heatmap views. Align heatmap color ramps and hover treatment with
the supported white, warm, and dark desktop themes.
Constraint: User profile data must stay local under cc-haha desktop preferences.
Rejected: Keep a separate profile edit panel | the requested design favors a compact profile header with modal editing.
Rejected: Use a blue heatmap ramp | it conflicted with the app theme palettes.
Confidence: high
Scope-risk: moderate
Tested: bun run check:desktop
Tested: bun test src/server/__tests__/desktop-ui-preferences.test.ts
Tested: bun run check:persistence-upgrade
Tested: bun run check:coverage
Not-tested: Full bun run verify
Unsupported image rejections from text-only compatible providers should not
poison the session, and low-trust multimodal usage spikes should not make
context indicators report a full window.
Constraint: Third-party Anthropic-compatible providers may report encoded media bytes as usage tokens.
Rejected: Trust all provider usage uniformly | third-party media responses can pin context to 100% incorrectly.
Confidence: high
Scope-risk: moderate
Directive: Do not remove the media-aware fallback without checking text-only provider recovery and desktop context indicators.
Tested: bun run check:server
Tested: focused media/context regression suite
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.
Large desktop installs can accumulate huge JSONL transcripts, and the sidebar session list previously loaded each requested transcript into memory on cache misses. The main branch cache from #651 handles repeated hot refreshes, so this change keeps that cache and replaces cache-miss list metadata extraction with a sequential streaming summary scan for paginated files only. It also makes Windows sidecar shutdown prefer taskkill /T so stale child processes are less likely to survive app stop paths.
Constraint: Must preserve main's short session-list cache from #651 while reducing cache-miss memory growth.
Rejected: Rely only on the #651 cache | cache expiry, first load, and pagination still hit full JSONL reads.
Rejected: Add a persistent summary index | larger persistence migration risk than needed for this issue.
Confidence: medium
Scope-risk: moderate
Directive: Do not replace the streaming scanner with full readJsonlFile calls without re-running large-transcript pressure tests.
Tested: bun test src/server/__tests__/sessions.test.ts
Tested: bun run check:server
Tested: bun run check:native
Tested: bun run check:desktop
Tested: synthetic pressure test with 140 sessions x 1200 JSONL entries showed lower RSS growth than main baseline.
Not-tested: Windows live taskkill /T process-tree cleanup.
The desktop session sidebar can poll /api/sessions while long live-model turns are streaming. Re-reading the same paginated JSONL files on every request adds avoidable server pressure, so list metadata is cached briefly and invalidated when session metadata changes.
Constraint: Session transcripts remain the source of truth and can be large during long real-model conversations.
Rejected: Cache transcript parses indefinitely | stale titles and counts would be harder to reason about when external writers touch JSONL files.
Confidence: medium
Scope-risk: moderate
Directive: Keep the list-session cache TTL short unless every transcript writer routes through SessionService.
Tested: bun test src/server/__tests__/sessions.test.ts --timeout 30000
Tested: bun run check:server
Tested: DeepSeek deepseek-v4-pro and Sub2API gpt-5.5 long real-model repro runs for #651
Not-tested: Windows 11 TCP reset reproduction; local macOS runs did not reproduce the exact hang.
Related: #651
Desktop image attachments were sent as @path references, which made the model invoke Read on the original file before the existing CLI image resizing path could run. This aligns desktop attachment materialization with the CLI behavior by resizing image data locally and sending SDK image content blocks, while preserving @path fallback for non-image files and failed image normalization.
Constraint: Desktop still needs local source paths for UI/context metadata while model input should avoid first-turn Read for images
Rejected: Route all attachments through @path | keeps the oversized image failure path for desktop images
Confidence: high
Scope-risk: moderate
Directive: Keep desktop image attachments aligned with CLI pasted-image processing before changing this path
Tested: bun test src/server/__tests__/conversation-attachments.test.ts
Tested: bun run check:server
Not-tested: Live GPT-5.5 proxy request with real provider credentials
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 sessions already had CLI stream-idle recovery code available, but
child CLI processes did not enable it by default. A provider stream that
stopped producing chunks without closing could therefore leave the desktop UI
showing a running turn indefinitely.
Enable the existing watchdog for desktop-launched CLI sessions and lock the
launch environment with a focused regression test.
Constraint: Desktop provider streams must surface a terminal state when upstream TCP stalls silently.
Rejected: Add a frontend-only running-state timeout | it would mask the hung CLI stream without recovering the turn.
Confidence: high
Scope-risk: narrow
Directive: Keep desktop stream-stall handling at the CLI stream boundary unless the transport contract changes.
Tested: bun test src/server/__tests__/conversation-service.test.ts
Tested: bun run check:server
Normalize realpath results for Windows drive roots so D:\ does not round-trip to drive-relative D: and resolve back into the current repository.
Also tighten Sidebar hidden-project matching so a drive root does not match every child project on the same drive.
Tested: bun test src/server/__tests__/windows-drive-path.test.ts src/server/__tests__/sessions.test.ts src/server/__tests__/workspace-service.test.ts
Tested: cd desktop && bun run test -- --run src/components/layout/Sidebar.test.tsx
Tested: cd desktop && bun run build:windows-x64
Tested: bun run check:desktop
Not-tested: bun run check:server (failed on existing/environment failures including the pre-existing untracked FileReadTool test)
Confidence: high
Scope-risk: narrow
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>
Windows folder selection can surface drive roots as D:, which Node treats as drive-relative instead of the absolute D:\ root. Normalize drive-root inputs before resolving launch directories, filesystem access roots, transcript metadata, and workspace paths. Use path.relative containment checks so drive roots and child projects remain distinct in session grouping and file access.
Constraint: Windows drive-root inputs may arrive as C:, D:, or any other single-letter drive prefix
Rejected: Special-case only D: | every Windows drive letter has the same drive-relative semantics
Confidence: high
Scope-risk: moderate
Directive: Keep Windows drive-root normalization centralized; do not reintroduce string-prefix containment checks for workspace roots
Tested: bun test src/server/__tests__/windows-drive-path.test.ts src/server/__tests__/filesystem.test.ts src/server/__tests__/sessions.test.ts src/server/__tests__/workspace-service.test.ts
Tested: cd desktop && bun run test -- --run src/components/layout/Sidebar.test.tsx
Tested: bun run check:server
Tested: cd desktop && bun run lint
Not-tested: Real Windows desktop smoke on a physical Windows machine
Related: #601
Portable desktop installs can mutate CLAUDE_CONFIG_DIR from an embedded or external CLI while the desktop server is already running. The server skill surface now rereads plugin-related state before listing skills, and /reload-plugins refreshes plugin skills alongside plugin commands so terminal sessions see the same enabled skills without a restart.
Constraint: Portable mode expects CLI, desktop API, and terminal sessions to share the selected CLAUDE_CONFIG_DIR even when writes happen from separate processes
Rejected: Require a desktop restart after plugin install | leaves the portable install workflow stale and contradicts the existing /reload-plugins handoff
Confidence: high
Scope-risk: moderate
Directive: Keep skill discovery and reload summaries counting plugin skills separately from plugin slash commands
Tested: bun test src/server/__tests__/skills.test.ts
Tested: bun test src/server/__tests__/plugins.test.ts
Tested: bun test src/server/__tests__/sessions.test.ts
Tested: bun run check:server
Tested: CLAUDE_CONFIG_DIR=/tmp portable smoke with anthropics/skills, two cwd /api/skills checks, and DeepSeek skill slash-command calls
Tested: bun run quality:gate --mode baseline --allow-live --provider-model deepseek:main:deepseek-main --only provider-smoke:*
Not-tested: Packaged desktop UI visual smoke
Desktop portable mode depends on CLI-driven installs using the same filesystem root as the app. The installed terminal launcher now carries CLAUDE_CONFIG_DIR into macOS/Linux shell invocations and uses a Windows cmd wrapper so plugin, skill, and MCP operations do not fall back to the native home config. Moved portable bundles also rebase cached plugin install paths when the cache exists under the current config root.
Constraint: Desktop portable bundles are file-system based and must survive zip/unzip relocation.
Rejected: Copy the sidecar binary directly into user bin | it cannot inject the selected portable config directory for future CLI installs.
Rejected: Rewrite plugin install paths unconditionally | missing cache directories should remain visible as broken state instead of being silently retargeted.
Confidence: high
Scope-risk: moderate
Directive: Keep desktop launcher wrappers as env-carrying sh/cmd files; do not revert to raw sidecar copies without portable install verification.
Tested: bun test src/server/__tests__/desktop-cli-launcher.test.ts src/utils/plugins/installedPluginsManager.test.ts
Tested: cargo test portable --manifest-path desktop/src-tauri/Cargo.toml
Tested: bun run check:server
Tested: bun run check:native
Tested: macOS launcher fixture plus Computer Use Finder verification of portable marker output.
Not-tested: Windows physical desktop execution; Windows wrapper behavior is covered by unit test only.
File browsing and workspace trees were treating every dot-prefixed entry as hidden, which made project files such as .env.example impossible to select through the desktop file surfaces. The fix keeps normal dotfiles and project folders visible while still hiding VCS metadata directories such as .git.
Constraint: VCS internals should stay out of user-facing file trees even when dotfiles are visible
Rejected: Add a UI toggle for hidden files | issue asks for file lists to include dot-prefixed project entries by default
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce blanket dotfile filtering on filesystem browse, workspace tree, or path completion paths
Tested: bun test src/server/__tests__/filesystem.test.ts src/server/__tests__/workspace-service.test.ts src/utils/suggestions/directoryCompletion.test.ts
Tested: bun run check:server
Not-tested: desktop browser visual smoke
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>
- 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>
Desktop AskUserQuestion waits on the SDK permission bridge, and a transient renderer disconnect could previously let the short no-client cleanup kill the CLI. That turned a live user question into an AbortError and left history restore showing a stale interactive card.
Track live SDK permission requests, replay them to reconnecting clients, and give sessions waiting on user input a longer cleanup grace. Render aborted terminal results as completed history instead of an unanswered prompt.
Constraint: SDK permission requests are live in-memory control messages until answered or aborted.
Rejected: Only de-dupe history cards in MessageList | would not keep the CLI alive or restore the active permission request.
Rejected: Disable disconnect cleanup globally | would leak abandoned CLI processes for ordinary disconnected sessions.
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Do not shorten pending-permission cleanup without testing AskUserQuestion reconnect and abort paths.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: git diff --check
Not-tested: Live Sub2API provider reconnect repro.