This bundles the pending desktop/server team-session fixes with the local adapter recovery changes already in the worktree. The team path now keeps teammate membership stable under concurrent spawns, surfaces real teammate identities in the desktop UI, and allows direct interaction with member transcripts. The adapter changes recover automatically when stale thinking signatures invalidate an existing session.
Constraint: Team config writes can happen concurrently while multiple reviewers spawn in parallel
Constraint: Desktop member views must follow mailbox/transcript semantics rather than hijacking teammate runtime sessions
Rejected: Keep relying on config.json alone for member discovery | in-process teammates can be lost after concurrent writes
Rejected: Open teammate sessionIds as normal desktop sessions | would attach a second CLI instead of the running teammate
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Preserve locked team-file mutation for any future teammate registration path and keep teammate labels sourced from member names before agent types
Tested: bun test src/server/__tests__/teams.test.ts src/server/__tests__/team-watcher.test.ts
Tested: cd desktop && bun run test --run src/stores/chatStore.test.ts src/pages/ActiveSession.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Not-tested: Manual end-to-end validation against a live Agent Teams run in the desktop app
- security: XSS sanitization with DOMPurify in Markdown/Mermaid/PermissionDialog;
path whitelist in filesystem API; fake keys in test/config files
- perf: fine-grained Zustand selectors in Sidebar/StatusBar/ContentRouter;
50ms throttle on streaming deltas; React.memo + useMemo in MessageList;
useRef for frequent keyboard shortcut state; AbortController 30s timeout
- leaks: WS session TTL timers (5-min cleanup on close); batch splice for
sdkMessages/stderrLines; folderPath validation in cronScheduler
- quality: optimistic update rollback in settingsStore; error state in
providerStore/teamStore; i18n for all hardcoded English strings
- docs: desktop architecture and features docs updated; VitePress nav fixed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the default macOS about dialog with a custom menu that emits
a Tauri event, navigating to the Settings > About tab within the React app.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14" MacBook-friendly default size (was 1280×800). New session tabs now display
the same icon + title welcome screen as the landing page until the first message.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slims the macOS .app from 435MB → 88MB (-80%) and the DMG from 113MB →
37MB (-67%) by inlining src/server + cli into bun-compiled sidecars
instead of shipping src/ + node_modules/ as Resources, then merging
server / cli / IM adapters into a single claude-sidecar binary so they
share one bun runtime.
Also wires Feishu / Telegram adapters as a built-in third mode of the
merged sidecar (auto-spawned by Tauri main on launch, hot-restarted on
config save), removing the need for the user to ever launch them
manually.
Commits:
662485c experiment(desktop): static-import sidecars + drop src/ + node_modules/ from bundle
3c5549e experiment(desktop): merge server + cli into one sidecar binary
e58ec4f experiment(desktop): add adapters mode to claude-sidecar (Feishu + Telegram)
3e3c2bc experiment(desktop): auto-spawn adapter sidecar on launch + restart on save
Bundle size summary:
metric baseline after delta
.app total 435 MB 88 MB -347 MB (-80%)
.dmg 113 MB 37 MB -76 MB (-67%)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires the new claude-sidecar adapters mode into the actual desktop UX:
* On app launch, Tauri main process now spawns the adapter sidecar right
after the server sidecar comes up. The sidecar reads ~/.claude/adapters.json
and connects whichever of Feishu / Telegram has credentials configured;
if neither does, it warns + skips + exits cleanly (treated as expected).
* When the user saves credentials in the existing AdapterSettings page,
the frontend store invokes restart_adapters_sidecar after the PUT
/api/adapters succeeds. Tauri kills the old child and spawns a new one,
which picks up the fresh config and establishes the WebSocket connection
to Feishu / Telegram immediately — no app restart needed.
End-to-end behavior is now: install app → open → configure credentials in
settings → click save → IM bot is live.
Implementation
==============
* desktop/src-tauri/capabilities/default.json: replace the stale
binaries/claude-server allowlist with binaries/claude-sidecar across
shell:allow-execute, shell:allow-spawn, plus a new shell:allow-kill
entry needed for the restart path. (P2 changed externalBin to
claude-sidecar but missed updating capabilities, which is why the prior
bundle worked at all in dev mode but would have failed in production.)
* desktop/src-tauri/src/lib.rs:
- new AdapterState that holds an Option<CommandChild>
- start_adapters_sidecar() spawns `claude-sidecar adapters --feishu --telegram`
with ADAPTER_SERVER_URL env var pointing at the dynamic server port
(converted http://→ws:// since WsBridge does `new WebSocket(url)`
directly without protocol translation)
- spawn_and_track_adapters_sidecar() handles spawn + state insertion
- stop_adapters_sidecar() kills + clears state
- new #[tauri::command] restart_adapters_sidecar that calls stop+spawn
- sidecar Terminated events are info-logged, not treated as errors,
so the credential-missing path doesn't show up as a crash
- setup() spawns the adapter sidecar after server startup completes
- RunEvent::Exit cleanup also kills adapter sidecar
* desktop/src/stores/adapterStore.ts: after every successful PUT
/api/adapters, dynamic-import @tauri-apps/api/core and call
invoke('restart_adapters_sidecar'). Wrapped in try/catch so non-Tauri
test environments fall through quietly. Triggers on every config
change (including pairing code generation, paired-user removal) by
design — keeps the rule simple and guarantees any save takes effect.
* desktop/src/pages/AdapterSettings.tsx: removed the stale "Server URL"
text input. The field defaulted to ws://127.0.0.1:3456 but the actual
server uses a dynamic port chosen at startup. Even when filled in
correctly, loadConfig() in adapters/common/config.ts gives env var
priority over file value, so this UI control had zero effect inside
the desktop app. Standalone-mode adapter users can still edit the
field directly in adapters.json if they need to.
Bundle size: unchanged at 88 MB .app / 37 MB DMG. The Rust changes
add only a few KB to the desktop main binary.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Feishu cards used to sit on "正在思考中..." for the entire turn and
then dump everything at message_complete, while Telegram showed the
reasoning and tool calls progressively. Two underlying causes:
1. handleServerMessage ignored `thinking`, `content_start{tool_use}`
and `tool_use_complete` entirely, so nothing was fed into the card
between turns of text output.
2. StreamingCard.performFlush silently set `cardKitStreamActive=false`
on any non-rate-limit error, after which all middle frames were
skipped and only finalize touched the card — exactly the
"long wait → all at once" symptom.
StreamingCard now tracks accumulatedReasoningText and a toolSteps
list, with appendReasoning / startTool / completeTool methods that
trigger throttled flushes like appendText. renderedText composes
tools → reasoning → answer in plain markdown (no blockquotes / lists
that historically tripped Feishu's parser). consecutiveStreamFailures
threshold (3) keeps a single transient error from killing the stream.
terminalText() is used by finalize so the final card holds only the
answer text, matching the Desktop UI's clean post-completion view.
handleServerMessage wires the new events to the existing per-chat
StreamingCard via the streamingCards map (read-only — never
auto-creates cards from thinking/tool events to avoid empty cards
for /clear-style commands).
Test coverage: realistic event-stream regression (thinking → tool_use
→ text → finalize), first-frame-failure recovery, finalize drops
intermediate state, plus per-method tests for the new APIs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Re-implements the inbound and outbound attachment paths on top of the
StreamingCard rewrite (ae586b0) instead of the legacy MessageBuffer
pipeline. Replaces the dangling extractText reference and ports the
behavior originally drafted on feat/im-attachments to the new structure:
Inbound (user → Claude):
- handleMessage now uses extractInboundPayload to detect text and any
PendingDownloads (image/file/file_archive/post-embedded media)
- safeMessageId captured outside enqueue to keep TS narrowing happy
- All command branches gated on `!hasAttachments` so attachment-bearing
messages bypass /new, /clear, /stop, /projects, etc.
- Per-message Promise.allSettled download via FeishuMediaService;
per-attachment checkAttachmentLimit; partial-failure user hint
- AttachmentRef[] forwarded through bridge.sendUserMessage(...)
- effectiveText guard prevents empty content from being sent to Claude
when all attachments are rejected and the user supplied no text
Outbound (Claude → user):
- content_delta handler feeds the streaming text into per-chat
ImageBlockWatcher and dispatches each new PendingUpload via
dispatchOutboundImage (fire-and-forget)
- dispatchOutboundImage handles all three source kinds (base64/path/url),
applies the same checkAttachmentLimit, dedupes by fingerprint, and
publishes the image as an independent im.message.create({msg_type:'image'})
alongside the streaming card text
- Card-embedded `{tag:'img', img_key}` rendering is intentionally deferred
to a follow-up PR — the MVP keeps streaming text and outbound images as
separate messages
Lifecycle: clearTransientChatState and startNewSession now also delete
imageWatchers and uploadedImageKeys for the chat so a new session never
inherits stale fingerprints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- attachment-store.ts: pin Dirent<string>[] and pass `encoding: 'utf8'`
so newer @types/node doesn't infer Dirent<NonSharedBuffer>
- feishu/media.ts: drop Readable.from(buffer) and pass Buffer directly
to im.image.create / im.file.create — Lark SDK already accepts Buffer
and the stream wrapper trips the stricter Buffer | ReadStream union
used on the main branch's tsconfig
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lists inbound/outbound attachment coverage, local staging directory,
size limits, and the file/module additions from the im-attachments
feature branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements TDD Task 9: wraps grammY bot.api.getFile/sendPhoto/sendDocument
with local attachment staging via AttachmentStore.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>