Message action controls stay visible through focus-within for keyboard navigation, but pointer clicks left the copy/fork buttons focused after the mouse moved away. Release pointer focus on message actions so hover controls return to their quiet state while keeping Tab focus behavior intact.
Constraint: Message actions use focus-within for keyboard accessibility and should not remove that path.
Rejected: Remove focus-within reveal | would regress keyboard users who tab to copy or fork.
Confidence: high
Scope-risk: narrow
Directive: Keep pointer-only blur scoped to message action controls; do not remove focus-within without a replacement keyboard affordance.
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: bun run check:desktop
Not-tested: Live Tauri manual hover smoke
Related: #642
Dark-mode Markdown text was still inheriting Tailwind Typography's default prose color, so normal and italic text could render dark while strong text used the app theme token.
Map markdown prose variables to the desktop theme tokens, move code-viewer hover and line-number styling onto theme tokens, and make Mermaid/Diff rendering follow the active app theme. Write/Edit/tool result and workspace render paths were audited; their core code and diff surfaces already use theme tokens, with the DiffViewer theme handoff tightened here.
Constraint: Mermaid theme customization requires the base theme and concrete color values, so CSS theme tokens are resolved to hex before rendering.
Rejected: Add per-element prose utility overrides only | misses Tailwind Typography defaults for quotes, counters, tables, and future prose elements
Confidence: high
Scope-risk: moderate
Directive: Keep markdown, code, diff, and diagram colors routed through shared desktop theme tokens; do not reintroduce fixed light backgrounds in chat renderers.
Tested: cd desktop && bun run test -- --run src/theme/globals.test.ts src/components/markdown/MarkdownRenderer.test.tsx src/components/chat/CodeViewer.test.tsx src/components/chat/MermaidRenderer.test.tsx src/components/chat/DiffViewer.test.tsx src/components/chat/chatBlocks.test.tsx src/components/workspace/WorkspacePanel.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: git diff --check
Not-tested: Full Tauri live desktop session with real provider output; browser broad-audit screenshot capture timed out, but computed colors were captured for all three themes.
Message rows should stay visually quiet until the user needs copy or fork controls. The action row now appears on message hover/focus, uses icon-sized controls, and shows the message timestamp beside the actions with locale-aware recent and historical formats.
Constraint: Desktop chat actions already own copy and fork affordances, so the change keeps that surface instead of adding a separate always-visible timestamp row.
Rejected: Keep text-labeled Copy buttons | the row remained too visually heavy for every message.
Confidence: high
Scope-risk: narrow
Directive: Keep copy, fork, and timestamp as one hover/focus metadata row unless the message layout is redesigned as a whole.
Tested: bun run check:desktop
Not-tested: Live Tauri native packaging
A turn can touch many files; an open-with affordance on every row is noise.
Restrict the per-file 打开方式 pill to rendered-previewable types via a new
isPreviewableChangedFile() predicate. Source rows keep their inline diff toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each file row now shows a material-symbol document-type icon, bold filename,
and a type subtitle (e.g. 文档·MD / 代码·TS) alongside a labeled 「打开方式 ⌄」
pill replacing the old open_in_new icon button; diff-expand is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the injection effect in AssistantMessage to also append a ▾ trigger
after inline <code> elements whose text is a browser-localhost or remote URL,
so AI-written run URLs like \`http://localhost:9527/\` get the same open-with
menu that markdown <a> links already receive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each file row in CurrentTurnChangeCard now has a sibling "open with" button
(open_in_new icon) that shows an OpenWithMenu, reusing buildOpenWithItems and
openWithContextForWorkspaceFile. HTML/XHTML files also get the in-app browser
option. Tests and i18n keys (openWith.title) included.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes preview://event (navigated/ready) from Rust into the browser store via a new subscribePreviewEvents subscriber; adds title field and setNavigated/setReady reducers to BrowserSessionState; BrowserSurface subscribes on mount to replace M1 optimistic nav-state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire L1 link routing into assistant message rendering. AssistantMessage now
accepts an optional sessionId and, when present, builds an onLinkClick that runs
handlePreviewLink against the running local server base (getServerBaseUrl) and
the browser/workspace panel stores, preventing default on handled links.
MessageList passes sessionId through in the assistant_text case.
Adds getServerBaseUrl() to desktopRuntime as a synchronous wrapper over the api
client's cached getBaseUrl().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Permission mode is a per-session runtime choice, while scheduled tasks cannot rely on a human approval loop. This removes the General settings permission surface, persists session permission metadata, and forces scheduled tasks to run with bypass permissions. Runtime permission changes now handle both directions across bypass boundaries, including startup/prewarm races, by persisting first and restarting only when the CLI launch mode must change.
Constraint: Scheduled tasks must be able to execute without a human standing by for authorization.
Constraint: The CLI only honors bypass permissions when launched with the skip-permissions flag, so switching to or from bypass requires a restart.
Rejected: Keep a global General permission default | it can leak across sessions and scheduled runs in ways the user cannot reason about.
Confidence: high
Scope-risk: broad
Directive: Do not reintroduce a global UI permission selector without proving it cannot affect unrelated sessions or automations.
Tested: bun test src/server/__tests__/conversations.test.ts -t "permission" --timeout 30000 (10 pass, 0 fail)
Tested: bun run check:server (858 pass, 0 fail)
Tested: cd desktop && bun run check:desktop (760 tests plus production build passed)
Tested: desktop/scripts/build-macos-arm64.sh and codesign verification passed
Tested: Real DeepSeek smoke validated plan, bypass, and scheduled task permission behavior
Not-tested: Did not repeat the full DeepSeek smoke after the final startup-race hardening; mock WebSocket permission regression and full server gate were rerun after that change.
Fixes#632
Desktop users have different multiline habits, and issue #631 calls out that Enter-only submission is too easy to trigger. This adds a persisted General setting that keeps Enter-send as the default while allowing Ctrl/Cmd+Enter submission for users who want plain Enter to insert a newline.
Constraint: Preserve existing Enter-to-send behavior as the default
Rejected: Change the global default to Ctrl/Cmd+Enter | would disrupt existing users
Confidence: high
Scope-risk: moderate
Directive: Keep active and empty session composers using the shared send shortcut helper
Tested: cd desktop && bun run test -- --run src/components/chat/ChatInput.test.tsx src/__tests__/generalSettings.test.tsx src/stores/settingsStore.test.ts
Tested: bun run check:desktop
Tested: cd desktop && bun run lint
Tested: Browser smoke on http://127.0.0.1:5174/?serverUrl=http%3A%2F%2F127.0.0.1%3A3456
Not-tested: Packaged Tauri app runtime
Related: #631
getLocalizedFallbackCommands previously passed a constructed key to t()
without checking if the translation actually resolved. When the i18n
table lacked an entry, t() returned the raw key (e.g.
'slashCmd.clear.description'), which dosubot flagged in PR #593.
Fix: default to the static English description, only override when
t(key) returns a different string. Add two tests covering missing-key
fallback and partial-translation scenarios.
Co-Authored-By: qwen3.6-plus <QwenLM@claude-code-best.win>
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.
The shared directory picker is used from right-aligned controls such as the MCP target project selector. Its portal menu previously used the trigger's raw left coordinate, so a trigger near the window edge could render the 400px menu partly off-screen.
Clamp the fixed-position menu against the viewport while preserving the existing width where space allows, and cover the right-edge case with a focused component test.
Constraint: DirectoryPicker is shared by MCP settings, task prompts, adapter settings, and launch controls.
Rejected: Special-case the MCP form layout | the overflow comes from the shared portal positioning and would remain in other right-aligned uses.
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bun run test -- src/components/shared/DirectoryPicker.test.tsx
Tested: bun run check:desktop
Not-tested: Manual browser smoke; local agent-browser automation hung during startup/command evaluation.
Workspace file-tree actions should default to project-relative paths because users usually operate inside the selected work directory. The menu now keeps the common copy action relative, preserves absolute copying as an explicit secondary action, and removes the duplicate inline citation entry in favor of Add to chat.
Constraint: Desktop workspace context menu should stay compact and avoid duplicate chat-reference actions
Rejected: Keep Copy path as absolute | makes the common selected-project workflow copy a less useful path
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bunx vitest run src/components/workspace/WorkspacePanel.test.tsx
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Manual browser smoke; component interaction tests cover the changed menu behavior
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
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
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
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.
The slash command menu in ChatInput and EmptySession was hard-coded to
English descriptions, so users running the desktop with locale=zh saw
English copy for the built-in commands the desktop owns (/clear,
/compact, /help, /mcp, /skills, /memory, /plugin, /doctor, ...).
This change:
- Adds slashCmd.<name>.description i18n keys for all 24 built-in slash
commands in en and zh locales.
- Introduces getLocalizedFallbackCommands(t) so React renders use the
active locale; the existing FALLBACK_SLASH_COMMANDS constant is kept
for non-React callers.
- Updates mergeSlashCommands so the localized fallback wins for
built-in command names while server/team-provided commands (e.g.
team:lark) still keep their own descriptions.
- Updates composerUtils.test.ts to reflect the new precedence and adds
a test that built-in commands prefer the localized description even
when the CLI broadcasts an English one.
- Adds CLAUDE.md and graphify-out/ to .gitignore (local AI assistant
artifacts that should not be committed).
Verification:
bun run check:desktop -> passed (lint + vitest + build, 32.9s)
changed-line coverage -> 100% (120/120)
Known environment-only blockers in this clone (unrelated to this diff):
bun run check:native -> rustc not installed in this WSL
bun run check:coverage -> 3 server-area suites hit
'error: An internal error occurred (WriteFailed)'
while writing very long stdout under WSL.
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>