The workspace panel already had the attachment model for both files and directories, but directory rows lacked the same right-click path and there was no low-risk handoff from the panel into the active composer text. This keeps context attachment as the stable source of file content while adding a small composer insertion event for inline citations.
Constraint: Keep the existing attachment pipeline unchanged so file and directory contents still flow through the proven workspace reference path
Rejected: Build a rich inline chip editor immediately | larger composer and history-synchronization surface than these issues require
Confidence: high
Scope-risk: moderate
Directive: Do not replace workspace attachments with inline text-only mentions without rechecking model-content generation and history restore behavior
Tested: cd desktop && bunx vitest run src/components/workspace/WorkspacePanel.test.tsx
Tested: cd desktop && bunx vitest run src/components/chat/ChatInput.test.tsx
Tested: bun run check:desktop
Not-tested: Live provider response quality with inline citation wording
Desktop MCP local and project scopes were still anchored to the active session workDir when adding or editing servers, which made a selected session worktree look like the target project. The form now treats the target project as explicit input for local/project scopes, and the server update path can remove the old scoped config before writing to the newly selected directory.
Constraint: Local MCP scope is private user config keyed by project path; project scope writes the selected project's .mcp.json
Rejected: Keep using the active session workDir as the implicit target | it can point at transient session worktrees and hides where config is written
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce implicit active-session cwd writes for MCP create/edit; local and project scopes need an explicit target project
Tested: cd desktop && bun run test -- mcpSettings.test.tsx
Tested: bun test src/server/__tests__/mcp.test.ts
Tested: cd desktop && bun run check:desktop
Tested: bun run check:server
Tested: bun run quality:gate --mode baseline --allow-live --only 'provider-smoke:*' --provider-model deepseek:main:deepseek-main
Tested: Live filesystem MCP probes with DeepSeek deepseek-v4-pro across local/project scopes; see artifacts/quality-runs/mcp-live-split-2026-05-25T09-15-18-245Z/summary.json
Not-tested: Full bun run verify was not rerun after the live MCP smoke
Related: #585
Windows WebView2 can report one-pixel content resize oscillations while a thinking or tool response is pinned to the bottom. That fed repeated bottom-scroll corrections back into the message list and made otherwise static chat content visibly move.
Ignore sub-2px content resize deltas before following live content growth, so real message growth still stays pinned without turning tiny layout jitter into scroll movement.
Constraint: Issue #603 reproduces on some Windows WebView2/display paths but not consistently across macOS or all Windows machines
Rejected: Disable bottom-following during thinking | would regress active response visibility when content actually grows
Confidence: medium
Scope-risk: narrow
Directive: Keep ResizeObserver follow behavior tolerant of sub-pixel or one-pixel WebView2 height jitter
Tested: cd desktop && bun run test -- --run src/components/chat/MessageList.test.tsx src/components/chat/virtualHeightCache.test.ts
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: Browser smoke at http://127.0.0.1:17999 loaded Claude Code Companion with no frontend error logs
Not-tested: Live Windows WebView2 runtime retest on reporter machine
Related: #603
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 MCP settings were using the process startup directory for local
project config and treated new custom servers as user-global. The desktop
API now carries the active cwd into local MCP reads, writes, and enablement
state, and the settings page exposes the same local/project/user scopes that
the CLI supports.
Constraint: Claude Code MCP scope semantics are local, project, and user.
Constraint: Desktop sessions can switch projects inside one long-lived server process.
Rejected: Only clear getProjectPathForConfig cache | still leaves request-scoped desktop API calls tied to getOriginalCwd
Confidence: high
Scope-risk: moderate
Directive: Do not route MCP local config or disabledMcpServers through ambient original cwd in desktop APIs.
Tested: bun test src/server/__tests__/mcp.test.ts
Tested: cd desktop && bun run test -- mcpSettings.test.tsx
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: Live provider smoke; the change is config/control-channel scoped.
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.
IM adapters could keep in-process bridge state after desktop session deletion removed the persisted mapping or after the server no longer had the mapped session. The restore path now validates the stored mapping against bridge memory and the server session before reusing it, then clears stale transient state before creating a replacement session.
Constraint: Issue #574 is intermittent and tied to adapter-created sessions; live reporter Telegram credentials are not available locally.
Rejected: Keep separate recovery checks in each IM adapter | duplicates the same stale-state logic across Telegram, Feishu, WeChat, and DingTalk.
Confidence: high
Scope-risk: moderate
Directive: Do not use bridge.hasSession() as the sole restore check; verify adapter-sessions state and server session existence first.
Tested: bun run check:adapters (364 pass, 0 fail)
Tested: cd adapters && bunx tsc --noEmit
Tested: bun test src/server/__tests__/sessions.test.ts src/server/__tests__/websocket-handler.test.ts --timeout 30000 (110 pass, 0 fail)
Tested: git diff --check
Not-tested: Live Telegram bot/provider reproduction on the reporter machine
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
The bypass-permission confirmation was resolving its displayed workspace from the session list's activeSessionId, while the chat composer sends runtime updates to the currently active tab. When those two UI states diverged after tab restores or project switches, the confirmation could show a parent or stale workspace path even though the chat was operating in another project.
Constraint: Desktop tabs and session-list selection can diverge during restore and multi-tab workflows.
Rejected: Thread workDir through every chat-composer call site | the selector already has the active tab context needed for uncontrolled usage.
Confidence: high
Scope-risk: narrow
Directive: Keep uncontrolled permission-mode actions keyed to the same active tab used for set_permission_mode.
Tested: cd desktop && bun run test -- src/components/controls/PermissionModeSelector.test.tsx --run
Tested: cd desktop && bun run lint
Not-tested: Live Windows desktop packaging smoke.
Sleep/wake reconnects can resend persisted assistant text as live output
without transcript ids. Treat that output as replay when it already exists
at the hydrated chat tail, and collapse duplicate transcript-backed text
after history refresh so the UI does not append repeated replies or send
new completion notifications for old turns.
Constraint: Keep the fix inside desktop chat state; do not change the WebSocket protocol or transcript storage shape.
Rejected: Add a macOS sleep/wake listener | it would not address replayed stream events from other reconnect/resume paths.
Confidence: high
Scope-risk: narrow
Directive: Do not remove replay dedupe without testing sleep/wake reconnect output against transcript id hydration.
Tested: bunx vitest run src/stores/chatStore.test.ts --testNamePattern "replay|collapses duplicate"
Tested: bun run test -- --run src/stores/chatStore.test.ts
Tested: bun run check:desktop
Not-tested: Real macOS sleep/wake desktop smoke with native notifications.
The beforeEach hook saved but did not clear the env var, so tests
that expected publicBaseUrl=null got the auto-discovered LAN URL
instead when CLAUDE_H5_AUTO_PUBLIC_URL=1 was set in the environment.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
When the dropdown opens above the trigger button, use CSS bottom instead
of top to prevent a gap between the dropdown and the button when content
is shorter than maxHeight.
Co-Authored-By: claude-opus-4-6 <noreply@anthropic.com>
Only show Claude Official and ChatGPT Official model groups in the
runtime-scoped ModelSelector dropdown when the corresponding OAuth
session is active. This prevents users from selecting official models
they cannot actually use.
- Import useHahaOAuthStore and useHahaOpenAIOAuthStore into ModelSelector
- Fetch OAuth status on mount
- Pass login flags to buildProviderChoices to conditionally include sections
- Add test case verifying hidden sections when not logged in
Co-Authored-By: claude-opus-4-6 <noreply@anthropic.com>
Bump version from 0.2.9 to 0.3.0 across desktop/package.json,
tauri.conf.json, and Cargo.toml. Add v0.3.0 release notes covering
desktop long-session performance, streaming tool previews,
AskUserQuestion reliability, multi-client streaming, and H5 network
fixes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tab switches into a long session paid for everything that was already paid for
the last time the user was on that session. Specifically the per-message height
and metric maps were cleared on every switch, so the new commit could only feed
estimated heights into buildVirtualTranscriptWindow and then chase them with
ResizeObserver callbacks. The first commit also ran getBranchableMessageTargets
and getCompletedTurnTargets — two extra O(N) walks over the messages array —
synchronously, and scrollToBottom('auto') was called inside the switch's
useLayoutEffect, which (on JSDOM and depending on layout state) added a
scrollHeight read to that critical path.
Introduces virtualHeightCache, a small module-level LRU keyed by sessionId that
holds the height and metric maps. MessageList now reads the prior maps on
switch instead of clearing them, so revisiting a session uses real measured
heights from the previous visit and skips the estimate→measure-correction
cascade. tabStore.closeTab calls dropSession so closed tabs don't leak.
The two O(N) branch / completed-turn computations now read from
useDeferredValue(messages), so they run as a low-priority follow-up render
after the first paint instead of blocking the switch commit. The switch's
useLayoutEffect now writes the bottom sentinel directly instead of calling
scrollToBottom, keeping the layout-read path out of the synchronous switch.
Tested: 731/731 desktop vitest suites pass, including 5 new virtualHeightCache
LRU/isolation/restore cases
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two issues remained on long transcripts. (1) The markdown parse cache was 48
entries / 1.8MB and keyed by full content string, so a 2.6MB session thrashed
constantly and every streaming chunk evicted finalized history entries because
each delta produced a new key. (2) The virtualization spacers above and below
the active window were single huge divs, leaving the WebView nothing to paint
during reconciliation and producing the literal blank gaps users were seeing.
The markdown cache now keys on `${len}:${fnv1a}` and splits into a 200-entry /
8MB finalized cache plus a small 4-entry streaming cache that cannot evict
finalized parses. AssistantMessage forwards a `streaming` flag (and stops
disabling caching entirely while streaming, so revisiting a settled turn no
longer reparses). The spacers are now broken into ~800px chunks via a
VirtualSpacer component; each chunk uses `content-visibility: auto` with a
`contain-intrinsic-size` pinned to its real height, which is the combination
that lets the WebView paint placeholder boxes even when off-screen paint is
deferred — without restoring the regression the previous content-visibility
rollout caused on in-window items.
Tested: 726/726 desktop vitest suites pass, including 3 new markdown-cache cases
and 1 new spacer-chunk + in-window-no-content-visibility regression guard
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the virtualization window slid during scroll, every visible row reconciled
from scratch because the heavy chat-row subtree had no memo barriers, and
because renderTranscriptItem rebuilt fresh branchAction / toolResult object
literals each render that broke MessageBlock's existing memo.
Wraps AssistantMessage, UserMessage, ToolCallBlock, ToolCallGroup, ToolResultBlock,
and MarkdownRenderer in React.memo, and hoists the per-message branchAction and
toolResult lookups into useMemo'd Maps keyed by message id. Window slides now
keep referentially-stable props for unchanged rows, so reconciliation skips them
and only newly entering rows pay full render cost.
Tested: 722/722 desktop vitest suites pass
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Fix branch button hidden when AI is responding by decoupling
branchActionsDisabled from chatState (MessageList.tsx)
- Add sourceSessionId/sourceMessageId to SessionListItem type
(both server and desktop)
- Scan forkedFrom in listSessions to expose parent-child relationships
- Preserve sourceSessionId/sourceMessageId in branchSession optimistic store
- Add buildSessionTree utility for tree-flattened session list with
depth and isLastChild markers
- Render forked sessions indented with GitBranch icon in sidebar
- Add comprehensive tests (187 total: 81 desktop + 106 server)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Long desktop transcripts (797+ messages / 2.6MB) showed 1-2s blank frames while
scrolling. The previous strategy inflated virtualization overscan from 360px to
7200px on every wheel/touch/pointer event and routed scroll-handler state
updates through flushSync, which serialized large React reconciles onto WebKit's
compositor thread and starved paint.
Replaces the dynamic overscan + idle-timer state machine with a single stable
1200px overscan, and removes the flushSync barrier in updateAutoScrollState so
React 18's automatic batching handles the scroll-handler setStates. ResizeObserver
height callbacks now mark a pending bit and flush a single setMeasuredItemsVersion
per animation frame, instead of triggering a list-wide re-render per measured
item. Auto-scroll-on-measure is also removed so measurement feedback no longer
fights an active user scroll.
Tested: 722/722 desktop vitest suites pass
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bring the detached-worktree desktop Writer preview patch into the canonical local main checkout so long pending Write calls show visible progress from main.
Constraint: Local main had advanced beyond the detached worktree base, so this landing keeps an explicit merge boundary.
Confidence: high
Scope-risk: moderate
Directive: Preserve bounded pending Writer rendering; do not replace it with live diff rendering without performance evidence.
Tested: bun run check:desktop in detached worktree before merge.
Not-tested: Post-merge desktop gate before this merge commit was created.
Long Write tool inputs can spend minutes streaming the file body before the tool executes, so the desktop transcript now decodes the pending content field into a lightweight Writer preview. The preview intentionally uses a bounded plain-text window while the tool is pending, then keeps the existing full diff rendering once the Write call completes.
Constraint: Write.content arrives as streaming tool input before the filesystem write executes.
Rejected: Render a live DiffViewer for every input delta | repeated diff and syntax work would reintroduce long-session jank.
Confidence: high
Scope-risk: moderate
Directive: Keep pending Writer rendering lightweight and bounded; reserve full diff rendering for completed Write calls.
Tested: cd desktop && bun run test src/components/chat/chatBlocks.test.tsx -- --runInBand
Tested: cd desktop && bun run test src/stores/chatStore.test.ts -- --runInBand
Tested: bun run check:desktop
Not-tested: Live provider long Write smoke after the UI patch.
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.
Large desktop sessions were still paying too much render and paint cost on tab switch, while the previous content-visibility approach could defer WebView painting enough to show white gaps during scroll. This moves long transcripts to a measured dynamic-height window, keeps small transcripts fully mounted, and caches completed Markdown parses so remounted assistant messages do not pay repeated parse cost.
Constraint: Tauri/WebView must not depend on content-visibility for transcript correctness because deferred offscreen paint can surface as blank regions
Rejected: Keep content-visibility:auto on every chat item | it improves synthetic DOM cost but matches the user's white-gap failure mode
Rejected: Fixed-height transcript virtualization | variable-height chat rows and tool groups can create spacer gaps and broken scroll positions
Confidence: high
Scope-risk: moderate
Directive: Do not reintroduce deferred-paint transcript optimization without testing the real Tauri/WebView scroll path on large sessions
Tested: bun run check:desktop
Tested: Chrome DevTools real-session smoke for 055015e4-6093-418e-9944-19bba25ecb69, switch trace max RunTask about 158ms and scroll blankSamples=0 across 34 sampled positions
Not-tested: Packaged Tauri binary smoke on WebKit/WebView2
Bring the validated detached-worktree desktop performance patch into the canonical local main checkout so long-session tab and scroll fixes are available from the main working copy.
Constraint: Local main had advanced independently beyond the detached worktree base, so fast-forward was not the correct landing shape.
Confidence: high
Scope-risk: moderate
Directive: Verify ancestry for detached-worktree landings because main may advance after the feature worktree is created.
Tested: bun run check:desktop in detached worktree before merge.
Not-tested: Post-merge desktop gate before this merge commit was created.
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.
Bring the detached worktree fix back to local main so H5 QR and copied LAN links no longer keep stale sidecar ports after app restarts.
Constraint: Local main already had newer unrelated work after the detached worktree base.
Confidence: high
Scope-risk: narrow
Directive: Keep H5 LAN URL host preservation separate from reverse proxy URL preservation.
Tested: bun test src/server/__tests__/h5-access-service.test.ts
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
Tested: bun run check:server
Not-tested: Full desktop packaged build after merge.
H5 access can preserve a manually selected private LAN host, but the desktop sidecar binds a fresh dynamic port on restart. Refresh plain HTTP private-LAN URLs to the current auto-discovered port while leaving reverse proxy URLs intact.
Constraint: Desktop sidecar ports are dynamic across app launches.
Rejected: Preserve the complete private-LAN URL unchanged | this keeps stale ports like 5179 in QR links after restart.
Confidence: high
Scope-risk: narrow
Directive: Do not treat plain private-LAN H5 URLs as reverse proxy URLs unless they carry a custom scheme or path.
Tested: bun test src/server/__tests__/h5-access-service.test.ts
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
Tested: bun run check:server
Tested: manual smoke with stored :5179 returning the active :45679 publicBaseUrl
Not-tested: Real phone scan against a packaged desktop build after reinstall.
Desktop already receives tool_use start and input deltas before the final tool_use_complete event, but the chat transcript only rendered a visible tool card after the complete event. This makes long Write payloads look frozen while the model is still generating the tool input. The store now upserts a pending tool_use message on stream start, updates a lightweight input preview from deltas, and resolves that same message when the complete event arrives.
Constraint: Long Write calls spend most perceived time streaming JSON tool input, not executing the filesystem write
Rejected: Show only the global active tool label | it does not anchor progress in the transcript where the user is looking
Confidence: high
Scope-risk: moderate
Directive: Keep pending tool_use messages keyed by toolUseId so the complete event updates in place instead of duplicating cards
Tested: bun run check:desktop
Not-tested: Live provider smoke with a real 10k-word Write stream
Bring the IM streaming repair from the ea41 worktree into local main so Telegram keeps receiving live content when the desktop opens the same active session. The merged fix also accumulates Telegram thinking deltas instead of replacing the preview with the latest short chunk.
Constraint: Local main already contains independent worktree label cleanup changes, so this merge preserves both histories
Rejected: Cherry-pick the fix | the user asked to merge the worktree back into local main
Confidence: high
Scope-risk: moderate
Directive: Keep multi-client session streaming covered before changing WebSocket output callback ownership
Tested: bun run check:server (828 pass); bun run check:adapters (358 pass); git diff --check
Not-tested: Live Telegram Bot API smoke against the packaged app
Telegram and desktop can attach to the same session while a turn is streaming. The WebSocket handler now tracks output callbacks per client and broadcasts session messages instead of replacing the previous subscriber. Telegram thinking deltas are accumulated before editing the placeholder so the preview does not collapse to the latest tiny chunk.
Constraint: Desktop and IM adapters may observe the same active session concurrently
Rejected: Keep a single session output callback | a later desktop view can steal Telegram's live content and completion events
Confidence: high
Scope-risk: moderate
Directive: Do not collapse session output callbacks back to one callback without a multi-client streaming regression
Tested: bun run check:server (828 pass); bun run check:adapters (358 pass); git diff --check
Not-tested: Live Telegram Bot API smoke against the packaged app
Bring the detached worktree fix into the canonical local checkout so desktop worktree sessions hide internal branch labels while preserving git-info metadata separation.
Constraint: User requested local main integration, not push or PR
Confidence: high
Scope-risk: narrow
Tested: Clean local main worktree before merge; feature commit contains server and desktop verification evidence
Not-tested: Full quality gate after merge
Desktop worktree sessions should show the source project and worktree marker without surfacing implementation refs like worktree-desktop-* as user-facing branch state. The git-info response now keeps launch branch metadata separate from worktree identity, and the desktop chip hides branch and slug labels in isolated worktree mode.
Constraint: Git worktrees need an internal branch for isolated execution, but that ref is product plumbing rather than useful UI context
Rejected: Show both launch branch and worktree slug | duplicated noisy identifiers and confused the session location
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/sessions.test.ts -t "git-info"; cd desktop && bun run test src/components/shared/ProjectContextChip.test.tsx; bun run check:server; bun run check:desktop; git diff --check
Not-tested: Live desktop screenshot smoke
Merge the worktree fix that preserves CLI AskUserQuestion toolUseResult metadata across history replay and live WebSocket translation, then filters stale unresolved cards when no pending permission is active.
Constraint: main had advanced past the worktree base, so this requires a non-fast-forward local merge.
Confidence: high
Scope-risk: moderate
Directive: Keep AskUserQuestion rendering driven by CLI transcript metadata instead of desktop-only lifecycle inference.
Tested: git merge-tree 3f384a1d5f5f48f0c7eb848b2dfacb4527fd5059 main 853e3db6ecbeb9a39f333c65aa7eddc32d613d7b
Tested: worktree commit already passed server focused tests, desktop focused tests, check:server, desktop lint, desktop build, and git diff --check.
Not-tested: Re-running full verification after the merge commit.
The CLI transcript already records each AskUserQuestion once, but the desktop bridge dropped the structured toolUseResult answers from both history replay and live WebSocket tool results. That made answered question cards look unresolved and allowed stale cards to reappear during the gap before the next permission request.
Constraint: Desktop should render the CLI question stream directly without inventing separate question lifecycle state.
Rejected: Hide all previous AskUserQuestion cards after submit | would remove resolved answer history instead of preserving transcript context.
Confidence: high
Scope-risk: moderate
Directive: Preserve toolUseResult metadata when adapting CLI transcript entries; AskUserQuestion answer state depends on the structured answers object, not the prose tool_result string.
Tested: bun test src/server/__tests__/sessions.test.ts --timeout 30000
Tested: bun test src/server/__tests__/ws-memory-events.test.ts --timeout 30000
Tested: cd desktop && bun run test src/stores/chatStore.test.ts
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx
Tested: bun run check:server
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
Not-tested: Full bun run verify and live desktop browser smoke.
When the model streams `tool_use ... text ... tool_use` interleaved blocks
within one turn (e.g. parallel TaskCreate calls plus a mid-stream
explanation), the previous merge logic preserved the literal stream order
and persisted it to history. On the next request, Bedrock's stricter
history validator inspects the trailing run of `tool_use` blocks: text
between tool_uses makes the earlier tool_uses no longer count as
"trailing", so they are reported as missing tool_result and the API 400s
even though all tool_results were actually returned. See
CLIENT_TOOL_USE_BUG_REPORT.md from the worldRouter proxy team for the
captured request body and Bedrock error.
This fix:
- Adds `reorderAssistantToolUseBlocks` that moves non-tool_use blocks out
of the tool_use cluster (preserving tool_use id order, thinking/
redacted_thinking positions, and dropping no blocks). No-op when zero
or one tool_use, or when tool_uses are already contiguous.
- Applies it in `mergeAssistantMessages` so live stream-merge produces
clean history.
- Applies it as a pass in `normalizeMessagesForAPI` so sessions resumed
from disk that were persisted before this fix also get cleaned up.
- Adds 9 bun:test cases covering the no-op, hoist, head/tail
preservation, idempotence, and merge cases (including the exact
pattern from the upstream bug report).
OpenAI-compatible streaming requests previously used the shared AI request timeout as a full-body AbortSignal. That made long SSE generations stop mid-answer after the configured timeout even when the upstream had already started streaming.
The proxy now applies the configured timeout only while opening streaming upstream requests and keeps non-streaming requests on the existing full-request timeout.
Constraint: The global AI request timeout is documented as covering provider requests and streaming first responses, not total stream duration.
Rejected: Increase the default timeout | long plan-mode answers can still exceed any fixed full-stream limit.
Confidence: high
Scope-risk: narrow
Tested: bun test src/server/__tests__/proxy-network-settings.test.ts
Tested: bun run check:server
Bring in the selected-workspace filesystem access fix from the detached
investigation worktree so Windows projects outside the user home can use
desktop @ file search.
Constraint: Preserve local main history while landing the detached worktree fix
Confidence: high
Scope-risk: moderate
Tested: bun test src/server/__tests__/filesystem.test.ts
Tested: bun run check:server
Not-tested: Windows packaged desktop smoke after merge
Desktop file mentions should follow the workspace the user opened, but
the filesystem browser only trusted the home and temp roots. Register
workspace roots after repository context, session creation, and session
git-info resolution so Windows projects on another drive can be searched
without turning the browse API into arbitrary disk access.
Constraint: Windows users can open repositories outside C:\\Users, such as D:\\workspace\\code\\cc-haha
Constraint: Filesystem browse must not become an unbounded local disk reader
Rejected: Add a global user-configurable filesystem whitelist | broader product and persistence surface than this bug needs
Rejected: Allow every requested browse path | would bypass the intended filesystem boundary
Confidence: high
Scope-risk: moderate
Directive: Register only workspace roots that the server has already resolved through session or repository flows
Tested: bun test src/server/__tests__/filesystem.test.ts
Tested: bun run check:server
Not-tested: Windows packaged desktop smoke
Manual H5 LAN hosts should survive auto LAN discovery, because the detected adapter can be a WSL or Docker virtual interface instead of the physical network reachable by a phone. The settings UI now asks for the host or IP separately and reuses the current service port for the normal LAN workflow, while still accepting full URLs for reverse proxy setups.
Constraint: H5 auto discovery may see multiple private IPv4 adapters on Windows hosts.
Rejected: Treat all private URLs as stale auto-discovery output | this overwrites intentional LAN choices.
Rejected: Let users manually type the port for the normal LAN path | the server already owns the bound port and the UI can reuse it.
Confidence: high
Scope-risk: moderate
Tested: bun test src/server/__tests__/h5-access-service.test.ts
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
Tested: bun run check:server
Tested: bun run check:desktop
Not-tested: Real Windows plus WSL/Docker plus phone LAN smoke.
Bring the desktop compact state repair from the validation worktree into
local main after focused, full local, packaged-app, and coverage checks.
Constraint: Local main was clean and shared the same base commit as the validation worktree.
Confidence: high
Scope-risk: moderate
Directive: Keep compact status null handling and tail compact-card cleanup together.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: bun run check:policy
Tested: bun run check:native
Tested: bun run check:coverage
Tested: SKIP_INSTALL=1 PRESERVE_TAURI_TARGET=1 ./desktop/scripts/build-macos-arm64.sh
Not-tested: Full live auto-compact threshold trigger in packaged app; Tauri sidecar did not inherit CLAUDE_AUTOCOMPACT_PCT_OVERRIDE during manual smoke.
Related: cb922b01
Desktop compact events can arrive as a start status followed by a null
CLI status or by a summary boundary. Preserve the visible transcript while
making either completion path clear transient compacting UI so the session
cannot remain stuck after auto-compact or cancellation.
Constraint: Desktop receives compact state through translated CLI SDK status events.
Rejected: Collapse old transcript into a single compact card | hides useful history and caused issue #568.
Confidence: high
Scope-risk: moderate
Directive: Do not drop status=null compact exits without validating desktop chat state transitions.
Tested: bun run check:desktop
Tested: bun run check:server
Tested: bun run check:policy
Tested: bun run check:native
Tested: bun run check:coverage
Tested: SKIP_INSTALL=1 PRESERVE_TAURI_TARGET=1 ./desktop/scripts/build-macos-arm64.sh
Tested: Real packaged macOS app with GPT-5.5 completed two live turns without staying in running state.
Not-tested: Full live auto-compact threshold trigger in packaged app; Tauri sidecar did not inherit CLAUDE_AUTOCOMPACT_PCT_OVERRIDE during manual smoke.
Related: https://github.com/NanmiCoder/cc-haha/issues/567
Related: https://github.com/NanmiCoder/cc-haha/issues/568
Computer Use now rejects Python runtimes below 3.9 before running pip,
keeps Pillow on the Python 3.9-compatible 11.x line, and falls back from
the configured mirror to the default PyPI index when dependency installs
fail.
Constraint: Pillow 11.3 requires Python 3.9+, while user machines may resolve a different Python than expected
Rejected: Downgrade Pillow to the Python 3.8-compatible 10.x line | current product direction assumes modern Python installs
Confidence: high
Scope-risk: narrow
Directive: Keep runtime package ranges aligned with the setup Python minimum before changing either side
Tested: bun test src/server/__tests__/computer-use-api.test.ts src/server/__tests__/computer-use-requirements.test.ts src/server/__tests__/computer-use-python.test.ts src/utils/computerUse/pipInstall.test.ts
Tested: bun run check:server
Tested: bun run check:coverage
Tested: bun run check:policy
Not-tested: Real Windows 10 machine dependency install against the reported user environment
Bring the detached worktree fix back to the local main branch so modern WebUI keeps Shiki rendering while legacy WebKit can fall back before loading Shiki.
Constraint: Local main already contains unreleased commits ahead of origin/main.
Confidence: high
Scope-risk: moderate
Tested: Worktree commit passed focused CodeViewer test, desktop lint, desktop build, startup bundle scan, and real WebUI Shiki smoke before merge.
Not-tested: Re-running the full desktop gate after merge.
Code blocks should keep the richer Shiki rendering in modern WebUI while avoiding the old WebKit startup crash path by loading Shiki lazily and falling back to Prism only when the runtime cannot safely parse or execute it.
Constraint: Issue #461 is triggered by older Safari/WebKit parsing modern RegExp syntax during startup.
Rejected: Replace Shiki entirely with Prism | visual quality regressed for normal WebUI usage.
Confidence: high
Scope-risk: moderate
Directive: Keep Shiki out of the startup path unless macOS 12 WebView compatibility is re-verified.
Tested: cd desktop && bun run test -- --run src/components/chat/CodeViewer.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: Real WebUI smoke on http://127.0.0.1:1420/ verified six code blocks rendered with data-highlight-engine=shiki.
Not-tested: Physical macOS 12 WebView runtime.
Related: https://github.com/NanmiCoder/cc-haha/issues/461
The Telegram adapter fix was developed in a detached worktree from a shared base while local main already contained newer desktop fixes. Merge the narrow worktree commit into main so the local checkout keeps those existing commits and gains the Telegram streaming behavior.
Constraint: Local main had already advanced independently after the worktree base.
Rejected: Rebase local main onto the worktree commit | rewrites the user's local main history unnecessarily.
Confidence: high
Scope-risk: narrow
Directive: Keep this as a local integration merge; do not push unless explicitly requested.
Tested: bun run check:adapters
Tested: cd desktop && bun run build:sidecars
Not-tested: Live Telegram Bot API run with a real bot token.