Desktop sessions can switch provider and model while a CLI subprocess is already alive, so the server now serializes runtime restarts and marks provider-managed launches to prevent stale settings env from overriding the selected provider. Provider settings also write API key env consistently and clear stale managed keys before syncing.
This includes the related desktop/docs brand asset refresh and keeps the desktop locale default in Chinese, with tests updated to match the current provider semantics.
Constraint: Session-scoped model selection must win over cc-haha/settings.json and inherited ANTHROPIC_* values.
Rejected: Store the selected model as a global provider activeModel | chat runtime selection is per session.
Confidence: high
Scope-risk: moderate
Directive: Do not remove CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST without validating Desktop provider switching against stale settings env.
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/conversations.test.ts src/server/__tests__/providers.test.ts src/server/__tests__/providers-real.test.ts
Tested: cd desktop && bun run test src/stores/settingsStore.test.ts
Tested: cd desktop && bun run lint
Tested: git diff --check
Not-tested: Full desktop production package/signing.
Desktop plugin details now route into the shared Skills, Agents, and MCP management surfaces instead of maintaining separate read-only drilldowns. This also extends the desktop/server skill aggregation so plugin-provided skills appear in the shared list, groups MCP entries by source, and preserves detail-view back navigation based on where the user entered the page.
The implementation keeps plugin detail as the high-level capability hub while pushing real inspection into the existing management pages. Disabled plugins no longer expose false navigation paths into shared views, and the agent-browser regression script was expanded to exercise the new end-to-end flows.
Constraint: Shared Agents data only includes enabled plugin agents, so disabled plugins cannot deep-link into agent detail
Rejected: Keep duplicating full Skills/Agents/MCP detail inside Plugin detail | creates divergent UI flows and stale data paths
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: If a detail view can be opened from multiple entry points, keep the return target in store state rather than hardcoding a single back destination
Tested: desktop vitest for plugins/skills/agents/mcp; desktop tsc --noEmit; desktop vite build; server skills API test; agent-browser web regression on plugin->skill/mcp and plugin->agent back navigation
Not-tested: packaged desktop app regression after rebuilding the Tauri bundle
The desktop app already had solid session streaming, permission, and tool-rendering flows, but extension setup still forced users into manual forms or external shell work. This change adds an Install Center in Settings that reuses the session chat pipeline for natural-language installs, adds installer-specific guidance for plugin and skill URLs, and includes an agent-browser E2E script for real UI validation.
Constraint: Must reuse the existing session/chat execution path instead of introducing a second install runtime
Constraint: Plugin installs need real CLI commands while skill installs may come from published install commands on third-party pages
Rejected: Separate terminal-only install surface | duplicates session UX and weakens permission/tool visibility
Rejected: Pure form-based installer expansion | too much friction for plugin, MCP, and skill onboarding
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Keep installer prompts aligned with the actual CLI install surfaces; do not let the installer fall back to slash-command syntax inside Bash
Tested: cd desktop && bun run lint
Tested: Real UI automation via agent-browser for Telegram plugin install flow through Settings > Install, verified Plugins page shows telegram enabled
Tested: Real UI automation via agent-browser for ui-ux-pro-max skill install flow through Settings > Install, verified ~/.claude/skills/ui-ux-pro-max and Skills page visibility
Not-tested: Full e2e-install-center-agent-browser.sh script as a single uninterrupted green run after the latest stability tweaks
The desktop client already had a conversation-level rewind UI concept on the
CLI side, but the web/desktop surface lacked the protocol, session trimming,
and file checkpoint restore path needed to make rewind trustworthy. This change
adds a desktop-specific rewind API, wires the message-level UI affordance and
confirmation modal, enables SDK file checkpointing for desktop sessions, and
covers the restore path with service tests plus a real agent-browser workflow.
Constraint: Desktop sessions run the CLI in SDK/print mode, so file checkpointing had to be enabled explicitly for that path
Constraint: main branch is checked out in a separate worktree, so merge-back must happen from the primary worktree after commit
Rejected: UI-only rewind that only trims local state | would leave persisted transcript and disk state inconsistent after refresh
Rejected: Reuse getLastSessionLog as the sole snapshot source | active rewind must read file-history metadata directly from the session file
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep desktop rewind keyed to persisted user-message order unless the UI model starts carrying stable transcript UUIDs end-to-end
Tested: bun test src/server/__tests__/conversation-service.test.ts src/server/__tests__/sessions.test.ts; desktop MessageList vitest; desktop tsc no-emit; live agent-browser E2E on isolated ports with file edit then rewind
Not-tested: Browser E2E matrix for multi-file and second-edit scenarios is still covered at service-test level rather than full UI level
Windows releases were shipping both NSIS exe and MSI bundles, which made the
release page harder to understand and increased the chance that users would
pick the wrong asset. Restricting Windows output to MSI narrows the installer
surface while the release asset naming now spells out platform, architecture,
and bundle type directly in every published filename.
Constraint: Remote desktop packaging is triggered by GitHub Actions releases, not local uploads
Constraint: Updater metadata must keep latest.json stable for existing clients
Rejected: Keep publishing both NSIS and MSI with better docs | still leaves the higher-risk installer on the release page
Rejected: Rename assets manually after upload | easy to drift from workflow output and updater metadata
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: If Windows exe bundles are reintroduced later, reassess SmartScreen and AV impact before publishing them again
Tested: YAML parse for both desktop workflows; git diff --check on modified files; manual review of PowerShell changes
Not-tested: End-to-end GitHub Actions release run; PowerShell parser validation on a Windows host
Concurrent writes to the same settings file could interleave and leave user
preferences in a partially updated state, and a restarted conversation session
could still be torn down by an older process exit callback. This serializes
per-file settings writes, hardens temp-file handling, adds regression coverage
for both service behaviors, and drops an unused sidecar external from the
build exclude list.
Constraint: Settings writes must remain atomic while allowing multiple service entry points to update the same JSON file
Rejected: Keep fire-and-forget writes with unique temp names only | still allows stale reads and last-writer races between callers
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Preserve the per-file write lock when adding new settings mutation paths so concurrent writes keep one serialization point
Tested: bun test src/server/__tests__/settings.test.ts src/server/__tests__/conversations.test.ts (new settings/conversation cases passed)
Not-tested: Root lint script is unavailable; Models API tests in src/server/__tests__/settings.test.ts still fail in this checkout with changed default model/effort expectations
The Feishu and Telegram IM adapters used to be standalone Bun processes
that the user had to launch manually with bun + .ts source on disk —
which meant they were effectively unreachable from the bundled desktop
app, since the user wouldn't have bun or the source tree.
This adds them as a third mode of the merged claude-sidecar binary:
claude-sidecar adapters --app-root <path> [--feishu] [--telegram]
The launcher pre-checks credentials via the same `loadConfig()` the
adapters use internally, then conditionally `await import()`s each
enabled adapter whose creds are present. Adapters with missing creds
are warned and skipped, so a partially-configured user (e.g. only
Feishu set up, no Telegram bot token) still gets the working adapter
started cleanly instead of having Telegram's top-level
`process.exit(1)` kill the whole process.
Adapter source code is unchanged — the adapters still self-start at
top-level via Lark.WSClient.start() / grammy bot.start(). Their
SIGINT handlers also still register independently. The only thing
gating runtime is whether bun's static-import follows the dynamic
specifier into adapters/feishu/index.ts and adapters/telegram/index.ts,
which it does.
Bundle impact
=============
metric P0+P2 only +adapters mode delta
claude-sidecar 66 MB 68 MB +2 MB
.app total 87 MB 88 MB +1 MB
.dmg 37 MB 37 MB 0 MB
Both adapter SDKs (@larksuiteoapi/node-sdk and grammy) statically
inline into the binary at a +2 MB cost, fully absorbed by DMG
compression. Compared to the alternative (a separate ~60 MB sidecar
binary per adapter, or even one combined ~60 MB adapter binary)
this is essentially free.
Verification
============
* `claude-sidecar server` regression test still passes (boots, /api/sessions
→ 200, CronScheduler runs)
* `claude-sidecar cli --version` returns 999.0.0-local
* `claude-sidecar adapters` (no flags) → exit 2 with usage error
* `claude-sidecar adapters --feishu --telegram` (no creds) → both warned and
skipped, exit 1
* `claude-sidecar adapters --feishu` (FEISHU_APP_ID=cli_fake creds) →
Feishu adapter boots, Lark client `client ready`, attempts API connect,
fails with 400 from feishu API and gracefully retries (correct
behavior — fake creds)
* `claude-sidecar adapters --telegram` (TELEGRAM_BOT_TOKEN=fake:token) →
grammy bot.start() called, getMe API hits with 404, throws GrammyError
(correct — fake creds)
* `bun test adapters/` → 299 pass / 0 fail
* `bun test src/` → 358 pass / 45 fail / 2 errors, identical to baseline
Scanner change
==============
desktop/scripts/scan-missing-imports.ts now also walks adapters/ in
addition to src/, so any future feature() gated stubs in the adapter
tree get auto-stubbed. As of this commit, adapters/ has 0 missing
imports — all clean.
Next step (UI integration, not done here)
==========================================
To actually wire this into the desktop UX, the Tauri main process needs:
- A "Configure IM adapters" settings page (App ID/Secret, bot token,
allowed users) that writes ~/.claude/adapters.json
- A "Start/stop Feishu" / "Start/stop Telegram" toggle that spawns
`claude-sidecar adapters --feishu` (or both) as a managed sidecar,
monitors lifecycle, restarts on crash
The runtime infrastructure is now in place — that work is purely UI +
Rust spawn glue and can be done independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the two ~65MB bun-compiled sidecar binaries with a single
~66MB merged binary. The bun runtime + shared dependency code (anthropic
SDK, MCP SDK, ws, undici, etc) was previously duplicated across both —
merging eliminates that duplication entirely.
Combined with the previous P0 commit (static-import inlining + drop
src/ + node_modules/ from Resources), this brings the macOS .app from
the original 435MB baseline down to 87MB (-80%), and the DMG from 113MB
to 37MB (-67%).
Final breakdown of the 87MB .app:
Contents/MacOS/claude-sidecar 66MB (was 57+57=114MB)
Contents/MacOS/claude-code-desktop 18MB (Tauri Rust main)
Contents/Resources/icon.icns 2MB
+ plist + frameworks ~1MB
This is essentially the floor — bun runtime + Tauri main + minimum
overhead. Going lower would require swapping toolchains.
Implementation
==============
* desktop/sidecars/claude-sidecar.ts (new): single entrypoint that
takes a positional mode argument ("server" or "cli") then dispatches
via `await import('../../src/server/index.ts').startServer()` or
`await import('../../src/entrypoints/cli.tsx')`. Same env / argv setup
pattern as the old launchers.
* desktop/sidecars/server-launcher.ts + cli-launcher.ts: deleted.
* desktop/scripts/build-sidecars.ts: compiles only claude-sidecar now.
* desktop/src-tauri/tauri.conf.json: externalBin → ["binaries/claude-sidecar"]
* desktop/src-tauri/src/lib.rs: spawns sidecar with leading "server"
positional arg.
* src/server/services/conversationService.ts resolveBundledCliPath /
resolveCliArgs: when current process is claude-sidecar, reuses the
same exe and spawns it with leading "cli" positional arg. Backward
compat path for old claude-server / claude-cli pair preserved for the
bin/claude-haha dev mode.
Verification
============
* claude-sidecar cli --version → 999.0.0-local (Claude Code) ✓
* claude-sidecar cli --help → full Commander spec ✓
* claude-sidecar server --port N → HTTP listening, CronScheduler running ✓
* All three above run in /tmp with no src/ or node_modules/ on disk
* bun test on src/ → 358 pass / 45 fail / 1 error, identical to baseline
(44 fails are pre-existing on main, unrelated to this change)
* Full DMG round-trip via build-macos-arm64.sh succeeds; new .app
installs cleanly in /Volumes/
Bundle size summary (vs original baseline)
==========================================
metric baseline final delta
.app total 435 MB 87 MB -348 MB (-80%)
.dmg 113 MB 37 MB -76 MB (-67%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cuts the macOS .app from 435MB → 152MB (-65%) and the DMG from 113MB → 60MB
(-47%) by inlining src/server and src/entrypoints/cli into the bun-compiled
sidecar binaries instead of dynamic-importing them from disk at runtime.
Architectural change
====================
Before:
desktop/sidecars/server-launcher.ts → bun build --compile (≈57MB shell)
└─ at runtime: dynamic file:// import of <appRoot>/src/server/index.ts
which transitively requires ALL of src/ + the entire root node_modules/
to be shipped as Resource. tauri.conf.json copied 254M of node_modules
and 47M of src/ into Contents/Resources/app/ on every build.
After:
desktop/sidecars/server-launcher.ts → bun build --compile (≈65MB)
└─ uses `await import('../../src/server/index.ts')` with a literal
specifier so bun's bundler walks the whole graph statically and
inlines everything into the binary.
Same treatment for cli-launcher.ts → src/entrypoints/cli.tsx.
Resolver gymnastics
===================
This fork carries dozens of ant-internal feature() gated require/import
calls referencing modules that simply don't exist on disk
(cachedMicrocompact, devtools, proactive, coordinator, etc). Bun's resolver
walks the static dep graph BEFORE bun:bundle macro DCE, so even though
the dead branches never execute at runtime, they still fail to resolve
at compile time.
Two complementary mechanisms:
1. desktop/scripts/scan-missing-imports.ts walks src/, regex-greps every
relative import / require / type-import specifier, and writes a Proxy
noop stub for any target that doesn't exist on disk. Stubs are tagged
with "@generated stub from scan-missing-imports" for idempotency. Text
resources (.md / .txt / .json) get appropriate format-specific stubs.
Runs as a pre-step inside build:sidecars.
2. desktop/scripts/build-sidecars.ts adds an `external: [...]` list for
bare-package optional deps not in package.json (OTLP exporters,
@aws-sdk/*, @anthropic-ai/{bedrock,vertex,foundry,mcpb}-sdk,
@azure/identity, fflate, turndown, sharp, react-devtools-core).
These remain runtime imports, fail benignly when their gating env
var or feature flag is off.
Tauri side
==========
- desktop/src-tauri/tauri.conf.json: dropped all `resources` entries.
Was 7 entries totaling ≈301MB. Now `{}`.
- desktop/src-tauri/src/lib.rs `resolve_app_root` no longer calls
BaseDirectory::Resource (the app/ resource dir doesn't exist anymore);
instead returns the directory of the current sidecar exe. The launchers
still accept --app-root for backward compat with conversationService's
CLI subprocess spawn.
Optimisations
=============
- bun build now uses minify whitespace+identifiers+syntax. Saved another
≈16MB across both binaries (server: 72MB→65MB, cli: 75MB→66MB).
Bonus fix
=========
src/services/remoteManagedSettings/index.ts had a typo importing
'./securityCheck.jsx' instead of '.js'. Bun's runtime resolver tolerated
it; bun build didn't.
Verification
============
- Both binaries boot successfully in /tmp with no src/ or node_modules/
on disk. Verified `claude-cli --version` returns the build version,
`claude-cli --help` prints the full Commander spec, and claude-server
starts CronScheduler + listens on the requested port.
- bun test on src/ shows 358 pass / 45 fail / 2 errors vs main baseline
of 359 / 44 / 2 — net 0 new failures (1 different flake direction).
All 44 baseline failures pre-exist on main and are unrelated.
- Full DMG round-trip via build-macos-arm64.sh succeeds; new bundle
installs cleanly in /Volumes/.
Bundle size summary
===================
metric baseline after P0 delta
Resources/app/ 301 MB 0 MB -301 MB
MacOS/claude-server 57 MB 65 MB +8 MB
MacOS/claude-cli 57 MB 66 MB +9 MB
MacOS/claude-code-desktop 18 MB 18 MB —
─────────────────────────────────────────────
.app total 435 MB 152 MB -283 MB (-65%)
.dmg 113 MB 60 MB -53 MB (-47%)
Generated stub files (173 of them under src/) are committed for
reproducibility — the scanner is idempotent and will re-create them
identically on every build, but tracking them avoids dirty working trees
on first compile.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This folds together the desktop-side fixes needed before broader rollout.
Session resume no longer deadlocks waiting on init, Mermaid and inline image
output render inside chat, task and sub-agent state stay visible during
execution, local build/release paths are safer, and Feishu/Telegram now expose
lightweight mobile commands (/help, /status, /clear) without adding a new
adapter-specific protocol.
Constraint: Desktop releases must publish updater artifacts from non-draft GitHub releases
Constraint: IM commands need short, phone-friendly responses and low operational complexity
Rejected: Add a dedicated IM command API surface | re-used existing slash commands and session/task REST endpoints to keep adapters thin
Rejected: Wait for task_update push events in WebUI | added low-risk polling because the current frontend ignores that event path
Confidence: medium
Scope-risk: broad
Reversibility: clean
Directive: Keep IM command replies terse and mobile-first, and merge local fallback slash commands when server-provided lists are partial
Tested: cd desktop && bun x vitest run src/components/chat/MermaidRenderer.test.tsx src/components/markdown/MarkdownRenderer.test.tsx
Tested: cd desktop && bun x vitest run src/components/chat/composerUtils.test.ts src/pages/ActiveSession.test.tsx src/stores/chatStore.test.ts
Tested: cd desktop && bun run lint
Tested: bun test src/server/__tests__/conversations.test.ts --test-name-pattern "SDK init arrives only after the first user turn" --timeout 60000
Tested: cd adapters && bun test common/ feishu/ telegram/
Tested: cd adapters && bunx tsc --noEmit
Not-tested: Full GitHub Actions release run on all three desktop platforms
Not-tested: Local DMG packaging end-to-end on Apple Silicon
Not-tested: Real Feishu/Telegram device sessions against a live adapter process
- Add Tauri sidecar architecture: Rust shell spawns claude-server binary,
dynamic port allocation, health-check wait loop, graceful shutdown
- Fix CORS middleware to accept `tauri://localhost` and `https://tauri.localhost`
origins from Tauri WebView, and add CORS headers to /health endpoint
- Enable native macOS window decorations (traffic lights) with Overlay title bar,
add data-tauri-drag-region on sidebar for window dragging
- Conditionally apply desktop-only padding (44px for traffic lights) vs web (12px)
- Generate brand identity: light-background app icon, horizontal logo, full icon
set (icns/ico/png) for Tauri bundle
- Add brand mark + GitHub link in sidebar, replace mascot SVG with app icon
in EmptySession page
- Update README (zh/en) and docs hero image with new branding
- Add sidecar build scripts and launcher entry points
- Gitignore Rust target/, Tauri gen/, and brand-assets candidates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>