Tested: cd desktop && bun run test -- --run src/components/workbench/WorkbenchPanel.test.tsx
Tested: cd desktop && bun run test -- --run src/components/layout/ContentRouter.test.tsx
Tested: cd desktop && bun run test -- --run src/stores/tabStore.test.ts
Tested: bun run check:desktop
Confidence: high
Scope-risk: moderate
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.
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.
Prevent Settings General checkbox focus/reflow from using off-row sr-only inputs, close stale native preview views when leaving session pages, and avoid Windows notification enable-time smoke side effects.
Tested: cd desktop && bun run test -- src/__tests__/generalSettings.test.tsx --reporter verbose
Tested: cd desktop && bun run test -- src/components/layout/ContentRouter.test.tsx --reporter verbose
Tested: cd desktop && bun run test -- src/lib/desktopNotifications.test.ts --reporter verbose
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
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
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
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
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
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
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
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
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.
Desktop destructive confirmations had split implementations: shared ConfirmDialog, hand-rolled modals, inline task popovers, and a diagnostics window.confirm path. This adds shared ActionDialog and ConfirmPopover primitives, keeps ConfirmDialog as the simple two-action wrapper, and migrates the known desktop confirmation flows onto those shared surfaces.
Constraint: Running-tab close needs three actions, so the two-button ConfirmDialog API was not enough.
Rejected: Force every confirmation through ConfirmDialog | would either lose the keep-running action or make ConfirmDialog too broad for inline popovers.
Confidence: high
Scope-risk: moderate
Directive: Add new desktop confirmation flows through ActionDialog, ConfirmDialog, or ConfirmPopover instead of hand-rolled overlays.
Tested: cd desktop && bun run test -- AdapterSettings.test.tsx mcpSettings.test.tsx TabBar.test.tsx PermissionModeSelector.test.tsx diagnosticsSettings.test.tsx
Tested: bun run check:desktop
Tested: git diff --check
Tested: Chrome DevTools smoke for provider delete and diagnostics clear-log confirmation dialogs
Not-tested: Agent Browser smoke because the local agent-browser CLI hung on open/snapshot/doctor/close commands.
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
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>
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
- Fix branch button hidden when AI is responding by decoupling
branchActionsDisabled from chatState (MessageList.tsx)
- Add sourceSessionId/sourceMessageId to SessionListItem type
(both server and desktop)
- Scan forkedFrom in listSessions to expose parent-child relationships
- Preserve sourceSessionId/sourceMessageId in branchSession optimistic store
- Add buildSessionTree utility for tree-flattened session list with
depth and isLastChild markers
- Render forked sessions indented with GitBranch icon in sidebar
- Add comprehensive tests (187 total: 81 desktop + 106 server)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Long transcripts were triggering avoidable layout work during tab switches and auto-scroll because MessageList read scroll geometry while the browser still had a large message tree to lay out. The tab bar also subscribed to full chat session state, so streaming payload churn could invalidate tab chrome even when running status did not change.
This keeps variable-height transcript rows fully mounted, adds browser offscreen rendering hints, scrolls to bottom through clamped large offsets instead of synchronous geometry reads, and narrows TabBar's chat-store subscription to running-state ids.
Constraint: Prior fixed-height virtualization left blank spacer gaps for variable-height chat rows.
Rejected: Reintroduce fixed-height virtualization | would regress existing long transcript rendering behavior.
Confidence: high
Scope-risk: moderate
Directive: Do not replace this with fixed row-height virtualization without measured row heights and scroll-restore coverage.
Tested: bun run check:desktop
Tested: Chrome DevTools trace on real session 055015e4-6093-418e-9944-19bba25ecb69 with INP 84ms to 43ms and ForcedReflow insight removed.
Not-tested: Packaged Tauri WebView runtime beyond the local web UI trace.
Multiple desktop sessions can run at the same time, so navigation surfaces need a compact shared signal instead of making users open each conversation to check progress. The sidebar and tab bar now derive running state from both persisted tab status and live chat state, while session rows keep worktree and updated-time metadata aligned on the right.
Constraint: Reuse existing tab/chat state without changing session persistence or server APIs
Rejected: Add another session status field | duplicated state would drift from chatStore and tabStore
Confidence: high
Scope-risk: narrow
Directive: Keep sidebar and tab running indicators sourced from the same state rules
Tested: cd desktop && bun run test src/components/layout/TabBar.test.tsx src/components/layout/Sidebar.test.tsx --pool forks --poolOptions.forks.singleFork=true
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: real desktop session in /private/tmp/cc-haha-real-status-fsip8S using gpt-5.5 Sub2API-ChatGPT; running markers appeared in sidebar and tab, then cleared after CCH_STATUS_REAL_DONE
Bulk tab actions reused a direct disconnect path instead of the single-tab running-session confirmation path. The tab bar now collects running sessions before closing any selected tab set, prompts when active work would be affected, and applies the user's stop-or-keep choice consistently across close all, close others, close left, and close right.
Constraint: Desktop users can run multiple sessions concurrently and must not lose running work through a bulk tab action without confirmation
Rejected: Patch only Close All | the neighboring bulk actions had the same direct disconnect bypass
Confidence: high
Scope-risk: narrow
Directive: Keep bulk tab closing routed through the same running-session policy as single tab closing
Tested: cd desktop && bun run test src/components/layout/TabBar.test.tsx --pool forks --poolOptions.forks.singleFork=true
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Manual desktop UI click-through
Bring back the narrow, previously validated sidebar recovery and Windows x64 sidecar compatibility fixes that were present on worktree branches but absent from the canonical local main checkout. The broader worktree branch is intentionally not merged because it would replay unrelated history and risk dropping current main work.
Constraint: The local main worktree already contains unrelated Settings and portable storage edits, so only the recovery hunks are staged.
Rejected: Merge 1506086927/main wholesale | its history includes broad rollback/replay commits that would remove current main changes.
Confidence: high
Scope-risk: narrow
Directive: Recover future worktree fixes by cherry-picking or staging narrow hunks; do not merge drifted worktree branches wholesale.
Tested: cd desktop && bun run test -- --run src/components/layout/Sidebar.test.tsx scripts/build-sidecars.test.ts
Not-tested: Full bun run verify due unrelated dirty Settings/portable-storage worktree changes.
The sidebar now groups conversations by project, so the embedded project picker in the search box duplicated the main navigation model and hid state inside a compact icon. Remove that picker and its store state, leaving search as plain text filtering and keeping the project header menu focused on organization and sorting.
The archive-all entry was also hidden because a broad destructive action does not belong in the lightweight project menu.
Constraint: Project grouping is now the primary project navigation surface.
Rejected: Keep the embedded project picker hidden in place | it would leave dead filtering state and a stale recovery path.
Rejected: Keep archive-all in the header menu | broad destructive actions are too risky for this surface.
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce broad session deletion or hidden project filters into the sidebar header without a dedicated reviewed management flow.
Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx src/stores/sessionStore.test.ts src/components/layout/TabBar.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
The session sidebar already uses project grouping, so the change adds header-level project actions instead of replacing project row controls. Sorting and organization choices are persisted through the existing desktop UI preferences file and mirrored into local cache for startup continuity.
Constraint: Keep the existing project tree, drag ordering, pinning, hiding, and Finder project-row actions intact.
Rejected: Replacing project row actions with the header menu | row-level Finder and per-project session actions are still useful and already covered by tests.
Confidence: high
Scope-risk: moderate
Directive: Do not store sidebar organization in global Claude settings; keep it under cc-haha desktop-ui preferences.
Tested: cd desktop && bun run test -- src/components/layout/Sidebar.test.tsx
Tested: bun test src/server/__tests__/desktop-ui-preferences.test.ts
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Sidebar project ordering, pinning, and hidden-project state must survive
browser access to the same local server, so the UI now stores these
preferences under the cc-haha config directory and keeps localStorage as
a migration/cache fallback.
Constraint: Browser and H5 localStorage is isolated from the Tauri WebView
Rejected: Keep sidebar project preferences only in localStorage | browser sessions would not share state
Rejected: Reuse cc-haha/settings.json | provider and H5 access settings should stay separate
Confidence: high
Scope-risk: moderate
Directive: Keep sidebar hide/remove semantics non-destructive; do not delete transcript files for project removal
Tested: cd desktop && bun run test -- src/components/layout/Sidebar.test.tsx --run
Tested: cd desktop && bun run lint
Tested: bun test src/server/__tests__/desktop-ui-preferences.test.ts
Tested: bun run check:persistence-upgrade
Tested: bun run check:server
The desktop sidebar needed a project-first navigation model that keeps worktree sessions attached to their source project instead of scattering them as separate paths. The UI now renders sessions under project groups with project-level actions and persisted ordering, while the server exposes a logical project root for worktree transcripts.
Constraint: Existing memory directory tree work must remain separate from the session sidebar behavior.
Rejected: Group by raw transcript projectPath | worktree paths fragment the same repository into multiple sidebar projects.
Confidence: high
Scope-risk: moderate
Directive: Keep future sidebar grouping keyed by projectRoot before projectPath so isolated worktrees stay under their source repository.
Tested: cd desktop && bun run test -- src/components/layout/Sidebar.test.tsx
Tested: bun test src/server/__tests__/sessions.test.ts
Tested: git diff --check
The detached worktree contains the verified desktop memory activity styling fix. Merge it into local main while preserving the existing main-line desktop changes and keeping the worktree commit as a separate decision record.
Constraint: Local main already diverged from the detached worktree and is ahead of origin
Rejected: Cherry-pick the worktree commit | would lose the explicit worktree merge topology the user requested
Confidence: high
Scope-risk: narrow
Directive: Preserve main-line desktop changes when resolving future memory-theme edits
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx --run
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Memory tool activity inherited the global warm brand treatment, which made the new desktop memory surface feel detached from white and dark themes. The component now uses memory-specific tokens with a quieter teal family, and the sidebar settings dock is made opaque so scrolling sessions cannot show through it.
Constraint: Desktop memory activity must remain readable across light, white, and dark themes without reusing the classic warm brand surface
Rejected: Keep using color-mix with --color-brand | preserves the original warm tint mismatch in white theme
Confidence: high
Scope-risk: narrow
Directive: Keep memory activity colors on memory-specific tokens instead of generic brand tokens
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx --run
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: Browser smoke screenshots for light, white, and dark memory activity previews
The previous tab strip height made session titles and toolbar affordances feel cramped. Increase the shared titlebar height token for tabs, overflow controls, and the native drag gutter so the top chrome reads as a deliberate desktop titlebar while preserving the existing drag, close, and toolbar interactions.
Constraint: Keep the change scoped to the desktop tab strip and avoid changing session state, ordering, or window-control behavior
Rejected: Increase only the outer bar height | tab hit targets and overflow controls would remain visually misaligned
Confidence: high
Scope-risk: narrow
Directive: Keep tab, overflow button, and drag-gutter heights aligned when changing this titlebar again
Tested: cd desktop && bunx vitest run src/components/layout/TabBar.test.tsx
Tested: bun run check:desktop
Tested: Browser smoke on Vite with tab bar measured at about 44px and toolbar controls centered
Not-tested: Native packaged Tauri window rendering on Windows titlebar controls
Several desktop copy entry points still called navigator.clipboard directly, which can fail or report success inconsistently in Tauri and browser test contexts. Route those copy actions through the existing textarea execCommand fallback helper and cover the fallback paths for startup diagnostics, markdown copy controls, and workspace file paths.
Constraint: Clipboard API is not reliable in every desktop/browser context.
Rejected: Leave direct navigator.clipboard calls in isolated components | repeats the diagnostics copy failure mode and creates inconsistent copy behavior.
Confidence: high
Scope-risk: narrow
Directive: New desktop copy controls should use copyTextToClipboard or CopyButton instead of calling navigator.clipboard directly.
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test -- --run src/components/markdown/MarkdownRenderer.test.tsx src/components/layout/StartupErrorView.test.tsx src/components/workspace/WorkspacePanel.test.tsx src/__tests__/diagnosticsSettings.test.tsx src/components/chat/MessageList.test.tsx
Tested: bun run check:desktop
Tested: bun run check:native
Tested: bun run check:coverage
Tested: bun run verify (passed=7 failed=0 skipped=3)
Not-tested: Live provider baseline; not required for this desktop-only clipboard fix.
PR #428 added a General Settings zoom slider after the desktop shortcut work already introduced a native-first app zoom controller. Keeping both paths would create double scaling and stale UI state, so the slider now routes through the existing controller and store state while the app shell no longer applies a second CSS zoom.
Constraint: UI zoom is device-local display state and should not be written into shared user settings.
Rejected: Keep cc-haha-ui-zoom plus AppShell style.zoom | it conflicts with shortcut zoom and multiplies visual scale.
Rejected: Persist zoom through /api/settings/user | it would sync display-specific state across machines.
Confidence: high
Scope-risk: moderate
Directive: Keep app zoom behind desktop/src/lib/appZoom.ts; do not add another storage key or DOM zoom application point without migration and shortcut sync tests.
Tested: cd desktop && bun run test -- --run src/lib/appZoom.test.ts src/hooks/useKeyboardShortcuts.test.tsx src/stores/settingsStore.test.ts src/__tests__/generalSettings.test.tsx src/lib/persistenceMigrations.test.ts src/lib/doctorRepair.test.ts
Tested: bun run check:desktop
Not-tested: Real Windows/Linux desktop runtime smoke.
- Add uiZoom field to UserSettings type and settingsStore with localStorage persistence
- Apply CSS zoom to AppShell root div for full-window scaling
- Add zoom slider in GeneralSettings (50%-200%, step 5%, centered at bottom-left)
- Export UI_ZOOM_MIN/MAX/STEP constants for consistent range control
- Add i18n support for UI zoom label and description (en/zh)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The first project opener UI used generic code glyphs for every IDE, which made the menu visually noisy and did not match the native desktop expectation. This changes the macOS path to expose transparent PNG icons generated from each detected local .app bundle and renders them directly in the toolbar/menu, falling back to the existing glyph only when an icon cannot be loaded.
Constraint: Do not redistribute third-party IDE trademark assets in the app bundle.
Constraint: Keep icon detection local to already detected open targets and cache only runtime PNG results.
Rejected: Bundling downloaded IDE logos | creates asset maintenance and trademark redistribution risk.
Rejected: Adding simple-icons as a dependency | it is not a faithful desktop app icon source and still requires brand permission checks.
Confidence: high
Scope-risk: narrow
Directive: Prefer local bundle icons on macOS; add curated official assets only as a cross-platform fallback layer.
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/stores/openTargetStore.test.ts src/components/layout/OpenProjectMenu.test.tsx src/components/layout/TabBar.test.tsx
Tested: cd desktop && bun run lint
Tested: bun -e openTargetService.getTargetIcon for vscode and finder returned PNG data
Tested: cd desktop && bun run build
Desktop sessions need a fast local escape hatch that opens the same materialized cwd the agent is editing, without showing unavailable IDE choices or persisting detection state. This adds a local open-targets API with silent in-memory detection for common IDEs and platform file managers, then wires a compact Codex-style toolbar menu into the desktop TabBar for active session workdirs.
Constraint: The first version is local IDE/editor and Finder/Explorer/file-manager only, no terminal targets or IDE plugin integration.
Constraint: The opened path must come from the active session workDir so isolated worktrees open the actual agent editing surface.
Rejected: Persisting detected applications | detection is cheap and temporary state avoids stale app inventory.
Rejected: Rendering unavailable IDEs as disabled menu rows | the user asked to show only detected targets and fall back to Finder/Explorer when no IDE is available.
Confidence: high
Scope-risk: moderate
Directive: Keep this path session-workdir based; do not switch it to repository root without proving isolated worktree behavior.
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/stores/openTargetStore.test.ts src/components/layout/OpenProjectMenu.test.tsx src/components/layout/TabBar.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: bun test src/server/__tests__/h5-access-auth.test.ts -t 'allows local desktop H5 access settings under explicit server auth with a valid bearer'
Not-tested: bun run check:server full suite had one unrelated H5 auth integration timeout in the full concurrent run; the timed-out test passed when rerun alone.
Desktop users can receive new sessions from IM adapters or scheduled tasks while the app stays open. The sidebar now refreshes on mount, visible focus, and a low-frequency visible-only interval, with a manual refresh control and in-flight request dedupe so the fix does not create avoidable polling pressure.
Constraint: Sessions can be created outside the desktop process by IM and scheduler entrypoints
Rejected: WebSocket push for this patch | broader server contract change than needed for the reported stale list
Confidence: high
Scope-risk: narrow
Directive: Keep session-list refresh visible-only and deduped before lowering intervals or adding more triggers
Tested: cd desktop && bunx vitest run src/components/layout/Sidebar.test.tsx src/i18n/index.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: Browser smoke on isolated desktop backend/frontend with refresh button click
Not-tested: Full coverage gate is blocked by unrelated root test port/timeouts; changed lines coverage reported 100% (59/59)
The previous batch selection state let adjacent selected sessions visually merge into a single block. This keeps the existing row structure but adds per-row spacing, a transparent baseline border, and a subtle selected-row shadow so the selection is easier to read without changing behavior.
Constraint: Keep the change local to the desktop sidebar and avoid new dependencies.
Rejected: Add global sidebar CSS tokens | unnecessary for this small component-level state and harder for changed-line coverage.
Confidence: high
Scope-risk: narrow
Directive: Keep batch selection spacing on the row wrapper so adjacent selected rows remain visually separated.
Tested: bun run verify
The age-based quick cleanup buttons overlapped poorly with the sidebar time groups and created ambiguity around what would be deleted. Keeping batch mode focused on all/selected sessions makes the destructive path easier to understand while preserving the existing confirmation and server deletion behavior.
Constraint: Desktop batch deletion should stay routed through the existing selected-session confirmation flow.
Rejected: Rename the age-based cleanup buttons | still leaves a low-value destructive shortcut in the primary batch panel.
Confidence: high
Scope-risk: narrow
Directive: Add future destructive cleanup shortcuts only when their target set is unambiguous in the current filtered session list.
Tested: cd desktop && bun run test src/components/layout/Sidebar.test.tsx
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Manual desktop click-through after removing the buttons.
The sidebar had only single-session deletion, which made stale or noisy
session lists expensive to maintain. This adds a batch-management lane in
the desktop UI and a server endpoint that deletes multiple sessions while
preserving per-session failure reporting and cleanup behavior.
Constraint: Session deletion must preserve existing transcript and adapter cleanup semantics
Constraint: Desktop UI should reuse the shared confirmation dialog instead of introducing a second modal surface
Rejected: Delete all selected sessions through repeated client DELETE calls | weaker partial-failure handling and duplicated cleanup orchestration
Rejected: Use a generic checkmark entry icon | it did not communicate batch deletion clearly enough
Confidence: high
Scope-risk: moderate
Directive: Keep batch deletion routed through the server batch endpoint so rollback and adapter cleanup stay centralized
Tested: bun test src/server/__tests__/sessions.test.ts -t batch-delete
Tested: bun run check:server
Tested: cd desktop && bun run check:desktop
Tested: browser UI smoke for group select, shift range select, delete confirmation, 7/30 day cleanup, Cmd+A filtered selection, and Escape exit
Not-tested: Live provider-backed session generation; this change covers session list management after sessions already exist
- Skip startDragging() on right-click to prevent Tauri IPC from
disrupting the contextmenu event on macOS
- Allow context menu to render when sidebar is collapsed
- Fix auto-dismiss effect to not self-clear contextMenu state
The mobile H5 shell was spending one row on the drawer button and another row on session metadata, which left less space for the actual chat on phones. The mobile shell now shares the drawer row with the active session title and metadata, while the session body suppresses its duplicate header on H5.
The mobile composer also restores horizontal breathing room and safe-area bottom padding so the input panel no longer sits flush against the viewport edge.
Constraint: Keep desktop/Tauri layout unchanged and scope the change to browser H5 mobile behavior.
Rejected: Keep the separate mobile menu row | it preserves the exact waste reported in phone screenshots.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce a second mobile session title row without checking a phone-sized H5 viewport.
Tested: cd desktop && bun run test src/components/layout/AppShell.test.tsx src/pages/ActiveSession.test.tsx src/components/chat/ChatInput.test.tsx --run
Tested: git diff --check
Tested: agent-browser 393x852 H5 layout smoke with screenshot /tmp/h5-layout-verify/mobile-h5-layout.png
Not-tested: cd desktop && bun run lint, blocked by pre-existing Settings.tsx responseLanguage/responseLangDraft errors outside this change
H5 browser use needs to keep local desktop WebUI testing open while giving remote phones a usable chat-first surface. This change scopes browser auth by origin and target host, hides non-chat desktop chrome on H5 mobile, compacts the empty-session composer, and unifies mobile selectors behind a full-width bottom sheet with an explicit close action.
Constraint: Local browser development may bypass H5 auth only for loopback or private LAN server hosts; public or remote hosts still require an H5 token.
Constraint: Mobile H5 scope is chat-first; settings and scheduled-task surfaces stay hidden instead of fully redesigned.
Rejected: Reuse desktop popovers on mobile | hover/anchored dropdown behavior leaves key actions hard to dismiss and cramped on narrow screens.
Rejected: Let any localhost origin bypass remote auth | it would let arbitrary local pages access public H5 servers without a token.
Confidence: medium
Scope-risk: moderate
Directive: Keep future H5 selector popups on MobileBottomSheet unless a selector needs a reviewed custom mobile interaction.
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run test -- src/pages/EmptySession.test.tsx src/components/chat/ChatInput.test.tsx src/lib/desktopRuntime.test.ts src/components/shared/RepositoryLaunchControls.test.tsx src/components/controls/PermissionModeSelector.test.tsx src/components/controls/ModelSelector.test.tsx src/components/shared/DirectoryPicker.test.tsx src/__tests__/pages.test.tsx
Tested: bun test src/server/__tests__/h5-access-auth.test.ts src/server/middleware/cors.test.ts
Tested: Chrome mobile viewport smoke for context, model, permission, project, branch, and worktree bottom sheets with no horizontal overflow.
Not-tested: Full bun run verify quality gate.
The browser H5 surface now switches to a phone-oriented shell: the sidebar becomes a closed-by-default drawer, chat stays primary, workspace and terminal panels stay off the mobile chat surface, composer controls use larger touch targets, and mobile menus avoid desktop-only widths and keyboard hints. Desktop and Tauri sidebar behavior remain on the existing store-driven path.
Constraint: H5 is personal/team browser access layered on the existing desktop web UI, so the normal desktop app must keep its current layout behavior.
Rejected: Share the global sidebarOpen default for mobile first paint | it can flash the drawer open before effects run on a phone.
Confidence: high
Scope-risk: moderate
Directive: Keep mobile-only layout branching behind useMobileViewport() && !isTauriRuntime() unless a future task explicitly redesigns the native desktop shell.
Tested: cd desktop && bunx vitest run src/hooks/useMobileViewport.test.tsx src/components/layout/AppShell.test.tsx src/components/layout/Sidebar.test.tsx src/pages/ActiveSession.test.tsx src/components/chat/ChatInput.test.tsx src/components/controls/PermissionModeSelector.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Remote browser bootstrap now keeps reverse-proxy path prefixes, recognizes IPv6 loopback as local, and converts remote health or verify failures into recoverable H5 connection prompts instead of falling back to the generic startup error surface. Token invalidation clears only the stored token so users can retry without re-entering the server address.
Constraint: Review fixes must stay inside the existing Task 4 browser-runtime scope without changing the Tauri localhost startup path
Rejected: Clear both stored server URL and token on remote bootstrap failures | it forces unnecessary re-entry after stale credentials
Directive: Any future browser-mode startup error for non-loopback backends should remain recoverable through H5ConnectionView unless the desktop/Tauri contract changes
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bunx vitest run src/api/client.test.ts src/api/websocket.test.ts src/lib/desktopRuntime.test.ts src/components/layout/AppShell.test.tsx
Tested: cd desktop && bun run lint
Tested: git diff --check -- desktop/src/api/client.ts desktop/src/api/websocket.ts desktop/src/lib/desktopRuntime.ts desktop/src/components/layout/H5ConnectionView.tsx desktop/src/components/layout/AppShell.tsx desktop/src/api/client.test.ts desktop/src/api/websocket.test.ts desktop/src/lib/desktopRuntime.test.ts desktop/src/components/layout/AppShell.test.tsx
Not-tested: Live reverse-proxy browser session against a real remote H5 server
Browser-mode startup now preserves the desktop localhost path while prompting for H5 credentials when a non-loopback backend requires verification. The API client and websocket layer share a central token source so REST and session sockets authenticate consistently without leaking the token into diagnostics.
Constraint: Preserve the Tauri startup path and default localhost browser behavior
Rejected: Reuse the generic startup error view for H5 auth failures | it blocks the credential-entry recovery path
Directive: Non-loopback browser startup is the only path that should require saved H5 verification without re-checking the desktop bootstrap contract
Confidence: high
Scope-risk: moderate
Tested: cd desktop && bunx vitest run src/api/client.test.ts src/api/websocket.test.ts src/components/layout/AppShell.test.tsx
Tested: cd desktop && bun run lint
Tested: git diff --check -- desktop/src/api/client.ts desktop/src/api/websocket.ts desktop/src/lib/desktopRuntime.ts desktop/src/components/layout/H5ConnectionView.tsx desktop/src/components/layout/AppShell.tsx desktop/src/api/client.test.ts desktop/src/api/websocket.test.ts desktop/src/components/layout/AppShell.test.tsx
Not-tested: Live remote H5 browser session against a running server