文件最后提交记录最后更新时间
chore: clean up LOBE-XXX code annotations (#15135) * chore: clean up LOBE-XXX annotations from codebase comments - Remove 【LOBE-XXX】 bracket markers - Remove LOBE-XXXX references from inline comments - Clean up test descriptions containing LOBE identifiers - Preserve linear.app URLs and code-level regex patterns - Generated: 2026-05-23 02:30:09 * 🐛 fix(tests): restore () in arrow callbacks broken by annotation cleanup The LOBE-XXX annotation cleanup script over-matched `(LOBE-XXXX', () =>` and stripped the callback `()`, leaving invalid syntax like `describe(..., => {` and `it(..., async => {` across 24 test files. This caused parse failures in Test Packages, Test Desktop App, Test Database lint, and Test App shard runs. Restoring `()` / `async ()` unblocks the suites while keeping the ticket-text cleanup intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hintFormat-test): restore label + ellipsis in stripMarkdownLinks fixture The annotation cleanup stripped `LOBE-8516` from a markdown-link's *label* (`[LOBE-8516](/task/T-1)` → `[](/task/T-1)`), which then survived `stripMarkdownLinks` because the pattern requires non-empty link text — the test expected the link to disappear and asserted equality on a LOBE-free output. The same line also lost a `.` from the trailing `...` indicator in both input and expected strings. Substitute a neutral Chinese label (`发布计划`) so the link continues to exercise the multi-link substitution path, and restore the full `...` ellipsis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Arvin Xu <arvinxx@lobehub.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>20 小时前
✨ feat(hetero-agent): support AskUserQuestion tools for claude code (#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cdb7c — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>13 天前
✨ chore(heterogeneous-agent): integrate heterogeneous agents with claude code (#13754) * ♻️ refactor(acp): move agent provider to agencyConfig + restore creation entry - Move AgentProviderConfig from chatConfig to agencyConfig.heterogeneousProvider - Rename type from 'acp' to 'claudecode' for clarity - Restore Claude Code agent creation entry in sidebar + menu - Prioritize heterogeneousProvider check over gateway mode in execution flow - Remove ACP settings from AgentChat form (provider is set at creation time) - Add getAgencyConfigById selector for cleaner access - Use existing agent workingDirectory instead of duplicating in provider config Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> ✨ feat(acp): defer terminal events + extract model/usage per turn Three improvements to ACP stream handling: 1. Defer agent_runtime_end/error: Previously the adapter emitted terminal events from result.type directly into the Gateway handler. The handler immediately fires fetchAndReplaceMessages which reads stale DB state (before we persist final content/tools). Fix: intercept terminal events in the executor's event loop and forward them only AFTER content + metadata has been written to DB. 2. Extract model/usage per assistant event: Claude Code sets model name and token usage on every assistant event. Adapter now emits a 'step_complete' event with phase='turn_metadata' carrying these. Executor accumulates input/output/cache tokens across turns and persists them onto the assistant message (model + metadata.totalTokens). 3. Missing final text fix: The accumulated assistant text was being written AFTER agent_runtime_end triggered fetchAndReplaceMessages, so the UI rendered stale (empty) content. Deferred terminals solve this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> 🐛 fix(acp): eliminate orphan-tool warning flicker during streaming Root cause: LobeHub's conversation-flow parser (collectToolMessages) filters tool messages by matching `tool_call_id` against `assistant.tools[].id`. The previous flow created tool messages FIRST, then updated assistant.tools[], which opened a brief window where the UI saw tool messages that had no matching entry in the parent's tools array — rendering them as "orphan" with a scary "请删除" warning to the user. Fix: Reorder persistNewToolCalls into three phases: 1. Pre-register tool entries in assistant.tools[] (id only, no result_msg_id) 2. Create the tool messages in DB (tool_call_id matches pre-registered ids) 3. Back-fill result_msg_id and re-write assistant.tools[] Between phase 1 and phase 3 the UI always sees consistent state: every tool message in DB has a matching entry in the parent's tools array. Verified: orphan count stays at 0 across all sampled timepoints during streaming (vs 1+ before fix). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> 🐛 fix(acp): dedupe tool_use + capture tool_result + persist result_msg_id Three critical fixes to ACP tool-call handling, discovered via live testing: 1. **tool_use dedupe** — Claude Code stream-json previously produced 15+ duplicate tool messages per tool_call_id. The adapter now tracks emitted ids so each tool_use → exactly one tool message. 2. **tool_result content capture** — tool_result blocks live in `type: 'user'` events in Claude Code's stream-json, not in assistant events. The adapter now handles the 'user' event type and emits a new `tool_result` HeterogeneousAgentEvent which the executor consumes to call messageService.updateToolMessage() with the actual result content. Previously all tool messages had empty content. 3. **result_msg_id on assistant.tools[]** — LobeHub's parse() step links tool messages to their parent assistant turn via tools[].result_msg_id. Without it, the UI renders orphan-message warnings. The executor now captures the tool message id returned by messageService.createMessage and writes it back into the assistant.tools[] JSONB. Also adds vitest config + 9 unit tests for the adapter covering lifecycle, content mapping, and tool_result handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> ✨ feat(acp): integrate external AI agents via ACP protocol Adds support for connecting external AI agents (Claude Code and future agents like Codex, Kimi CLI) into LobeHub Desktop via a new heterogeneous agent layer that adapts agent-specific protocols to the unified Gateway event stream. Architecture: - New @lobechat/heterogeneous-agents package: pluggable adapters that convert agent-specific outputs to AgentStreamEvent - AcpCtr (Electron main): agent-agnostic process manager with CLI presets registry, broadcasts raw stdout lines to renderer - acpExecutor (renderer): subscribes to broadcasts, runs events through adapter, feeds into existing createGatewayEventHandler - Tool call persistence: creates role='tool' messages via messageService before emitting tool_start/tool_end to the handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: rename acpExecutor to heterogeneousAgentExecutor - Rename file acpExecutor.ts → heterogeneousAgentExecutor.ts - Rename ACPExecutorParams → HeterogeneousAgentExecutorParams - Rename executeACPAgent → executeHeterogeneousAgent - Change operation type from execAgentRuntime to execHeterogeneousAgent - Change operation label to "Heterogeneous Agent Execution" - Change error type from ACPError to HeterogeneousAgentError - Rename acpData/acpContext variables to heteroData/heteroContext Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: rename AcpCtr and acp service to heterogeneousAgent Desktop side: - AcpCtr.ts → HeterogeneousAgentCtr.ts - groupName 'acp' → 'heterogeneousAgent' - IPC channels: acpRawLine → heteroAgentRawLine, etc. Renderer side: - services/electron/acp.ts → heterogeneousAgent.ts - ACPService → HeterogeneousAgentService - acpService → heterogeneousAgentService - Update all IPC channel references in executor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🔧 chore: switch CC permission mode to bypassPermissions Use bypassPermissions to allow Bash and other tool execution. Previously acceptEdits only allowed file edits, causing Bash tool calls to fail during CC execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: don't fallback activeAgentId to empty string in AgentIdSync Empty string '' causes chat store to have a truthy but invalid activeAgentId, breaking message routing. Pass undefined instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: use AI_RUNTIME_OPERATION_TYPES for loading and cancel states stopGenerateMessage and cancelOperation were hardcoding ['execAgentRuntime', 'execServerAgentRuntime'], missing execHeterogeneousAgent. This caused: - CC execution couldn't be cancelled via stop button - isAborting flag wasn't set for heterogeneous agent operations Now uses AI_RUNTIME_OPERATION_TYPES constant everywhere to ensure all AI runtime operation types are handled consistently. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ✨ feat: split multi-step CC execution into separate assistant messages Claude Code's multi-turn execution (thinking → tool → final text) was accumulating everything onto a single assistant message, causing the final text response to appear inside the tool call message. Changes: - ClaudeCodeAdapter: detect message.id changes and emit stream_end + stream_start with newStep flag at step boundaries - heterogeneousAgentExecutor: on newStep stream_start, persist previous step's content, create a new assistant message, reset accumulators, and forward the new message ID to the gateway handler This ensures each LLM turn gets its own assistant message, matching how Gateway mode handles multi-step agent execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: fix multi-step CC execution and add DB persistence tests Adapter fixes: - Fix false step boundary on first assistant after init (ghost empty message) Executor fixes: - Fix parentId chain: new-step assistant points to last tool message - Fix content contamination: sync snapshot of content accumulators on step boundary - Fix type errors (import path, ChatToolPayload casts, sessionId guard) Tests: - Add ClaudeCodeAdapter unit tests (multi-step, usage, flush, edge cases) - Add ClaudeCodeAdapter E2E test (full multi-step session simulation) - Add registry tests - Add executor DB persistence tests covering: - Tool 3-phase write (pre-register → create → backfill) - Tool result content + error persistence - Multi-step parentId chain (assistant → tool → assistant) - Final content/reasoning/model/usage writes - Sync snapshot preventing cross-step contamination - Error handling with partial content persistence - Full multi-step E2E (Read → Write → text) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🔧 chore: add orphan tool regression tests and debug trace - Add orphan tool regression tests for multi-turn tool execution - Add __HETERO_AGENT_TRACE debug instrumentation for event flow capture Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ✨ feat: support image attachments in CC via stream-json stdin - Main process downloads files by ID from cloud (GET {domain}/f/{fileId}) - Local disk cache at lobehub-storage/heteroAgent/files/ (by fileId) - When fileIds present, switches to --input-format stream-json + stdin pipe - Constructs user message with text + image content blocks (base64) - Pass fileIds through executor → service → IPC → controller Closes LOBE-7254 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: pass imageList instead of fileIds for CC vision support - Use imageList (with url) instead of fileIds — Main downloads from URL directly - Cache by image id at lobehub-storage/heteroAgent/files/ - Only images (not arbitrary files) are sent to CC via stream-json stdin Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: read imageList from persisted DB message instead of chatUploadFileList chatUploadFileList is cleared after sendMessageInServer, so tempImages was empty by the time the executor ran. Now reads imageList from the persisted user message in heteroData.messages instead. Also removes debug console.log/console.error statements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * update i18n * 🐛 fix: prevent orphan tool UI by deferring handler events during step transition Root cause: when a CC step boundary occurs, the adapter produces [stream_end, stream_start(newStep), stream_chunk(tools_calling)] in one batch. The executor deferred stream_start via persistQueue but forwarded stream_chunk synchronously — handler received tools_calling BEFORE stream_start, dispatching tools to the OLD assistant message → UI showed orphan tool warning. Fix: add pendingStepTransition flag that defers ALL handler-bound events through persistQueue until stream_start is forwarded, guaranteeing correct event ordering. Also adds: - Minimal regression test in gatewayEventHandler confirming correct ordering - Multi-tool per turn regression test from real LOBE-7240 trace - Data-driven regression replaying 133 real CC events from regression.json Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ✨ feat: add lab toggle for heterogeneous agent (Claude Code) - Add enableHeterogeneousAgent to UserLabSchema + defaults (off by default) - Add selector + settings UI toggle (desktop only) - Gate "Claude Code Agent" sidebar menu item behind the lab setting - Remove regression.json (no longer needed) - Add i18n keys for the lab feature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: gate heterogeneous agent execution behind isDesktop check Without this, web users with an agent that has heterogeneousProvider config would hit the CC execution path and fail (no Electron IPC). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: rename tool identifier from acp-agent to claude-code Also update operation label to "External agent running". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ✨ feat: add CLI agent detectors for system tools settings Detect agentic coding CLIs installed on the system: - Claude Code, Codex, Gemini CLI, Qwen Code, Kimi CLI, Aider - Uses validated detection (which + --version keyword matching) - New "CLI Agents" category in System Tools settings - i18n for en-US and zh-CN Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: fix token usage over-counting in CC execution Two bugs fixed: 1. Adapter: same message.id emitted duplicate step_complete(turn_metadata) for each content block (thinking/text/tool_use) — all carry identical usage. Now deduped by message.id, only emits once per turn. 2. Executor: CC result event contains authoritative session-wide usage totals but was ignored. Now adapter emits step_complete(result_usage) from the result event, executor uses it to override accumulated values. Fixes LOBE-7261 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🔧 chore: gitignore cc-stream.json and .heterogeneous-tracing/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🔧 chore: untrack .heerogeneous-tracing/ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ✨ feat: wire CC session resume for multi-turn conversations Reads `ccSessionId` from topic metadata and passes it as `resumeSessionId` into the heterogeneous-agent executor, which forwards it into the Electron main-process controller. `sendPrompt` then appends `--resume <id>` so the next turn continues the same Claude Code session instead of starting fresh. After each run, the CC init-event session_id (captured by the adapter) is persisted back onto the topic so the chain survives page reloads. Also stops killing the session in `finally` — it needs to stay alive for subsequent turns; cleanup happens on topic deletion or app quit. * 🐛 fix: record cache token breakdown in CC execution metadata The prior token-usage fix only wrote totals — `inputCachedTokens`, `inputWriteCacheTokens` and `inputCacheMissTokens` were dropped, so the pricing card rendered zero cached/write-cache tokens even though CC had reported them. Map the accumulated Anthropic-shape usage to the same breakdown the anthropic usage converter emits, so CC turns display consistently with Gateway turns. Refs LOBE-7261 * ♻️ refactor: write CC usage under metadata.usage instead of flat fields Flat `inputCachedTokens / totalInputTokens / ...` on `MessageMetadata` are the legacy shape; new code should put usage under `metadata.usage`. Move the CC executor to the nested shape so it matches the convention the rest of the runtime is migrating to. Refs LOBE-7261 * ♻️ refactor(types): mark flat usage fields on MessageMetadata as deprecated Stop extending `ModelUsage` and redeclare each token field inline with a `@deprecated` JSDoc pointing to `metadata.usage` (nested). Existing readers still type-check, but IDEs now surface the deprecation so writers migrate to the nested shape. * ♻️ refactor(types): mark flat performance fields on MessageMetadata as deprecated Stop extending `ModelPerformance` and redeclare `duration` / `latency` / `tps` / `ttft` inline with `@deprecated`, pointing at `metadata.performance`. Mirrors the same treatment just done for the token usage fields. * ✨ feat: CC agent gets claude avatar + lands on chat page directly Skip the shared createAgent hook's /profile redirect for the Claude Code variant — its config is fixed so the profile editor would be noise — and preseed the Claude avatar from @lobehub/icons-static-avatar so new CC agents aren't blank. * 🐛 fix(conversation-flow): read usage/performance from nested metadata `splitMetadata` only scraped the legacy flat token/perf fields, so messages written under the new canonical shape (`metadata.usage`, `metadata.performance`) never populated `UIChatMessage.usage` and the Extras panel rendered blank. - Prefer nested `metadata.usage` / `metadata.performance` when present; keep flat scraping as fallback for pre-migration rows. - Add `usage` / `performance` to FlatListBuilder's filter sets so the nested blobs don't leak into `otherMetadata`. - Drop the stale `usage! || metadata` fallback in the Assistant / CouncilMember Extra renders — with splitMetadata fixed, `item.usage` is always populated when usage data exists, and passing raw metadata as ModelUsage is wrong now that the flat fields are gone. * 🐛 fix: skip stores.reset on initial dataSyncConfig hydration `useDataSyncConfig`'s SWR onSuccess called `refreshUserData` (which runs `stores.reset()`) whenever the freshly-fetched config didn't deep-equal the hard-coded initial `{ storageMode: 'cloud' }` — which happens on every first load. The reset would wipe `chat.activeAgentId` just after `AgentIdSync` set it from the URL, and because `AgentIdSync`'s sync effects are keyed on `params.aid` (which hasn't changed), they never re-fire to restore it. Result: topic SWR saw `activeAgentId === ''`, treated the container as invalid, and left the sidebar stuck on the loading skeleton. Gate the reset on `isInitRemoteServerConfig` so it only runs when the user actually switches sync modes, not on the first hydration. * ✨ feat(claude-code): wire Inspector layer for CC tool calls Mirrors local-system: each CC tool now has an inspector rendered above the tool-call output instead of an opaque default row. - `Inspector.tsx` — registry that passes the CC tool name itself as the shared factories' `translationKey`. react-i18next's missing-key fallback surfaces the literal name (Bash / Edit / Glob / Grep / Read / Write), so we don't add CC-specific entries to the plugin locale. - `ReadInspector.tsx` / `WriteInspector.tsx` — thin adapters that map Anthropic-native args (`file_path` / `offset` / `limit`) onto the shared inspectors' shape (`path` / `startLine` / `endLine`), so shared stays pure. Bash / Edit / Glob / Grep reuse shared factories directly. - Register `ClaudeCodeInspectors` under `claude-code` in the builtin-tools inspector dispatch. Also drops the redundant `Render/Bash/index.tsx` wrapper and pipes the shared `RunCommandRender` straight into the registry. * ♻️ refactor: use agentSelectors.isCurrentAgentHeterogeneous Two callsites (ConversationArea / useActionsBarConfig) were reaching into `currentAgentConfig(...)?.agencyConfig?.heterogeneousProvider` inline. Switch them to the existing `isCurrentAgentHeterogeneous` selector so the predicate lives in one place. * update * ♻️ refactor: drop no-op useCallback wrapper in AgentChat form `handleFinish` just called `updateConfig(values)` with no extra logic; the zustand action is already a stable reference so the wrapper added no memoization value. Leftover from the ACP refactor (930ba41fe3) where the handler once did more work — hand the action straight to `onFinish`. * update * ⏪ revert: roll back conversation-flow nested-shape reads Unwind the `splitMetadata` nested-preference + `FlatListBuilder` filter additions from 306fd6561f. The nested `metadata.usage` / `metadata.performance` promotion now happens in `parse.ts` (and a `?? metadata?.usage` fallback at the UI callsites), so conversation-flow's transformer layer goes back to its original flat-field-only behavior. * update * 🐛 fix(cc): wire Stop to cancel the external Claude Code process Previously hitting Stop only flipped the `execHeterogeneousAgent` operation to `cancelled` in the store — the spawned `claude -p` process kept running and kept streaming/persisting output for the user. The op's abort signal had no listeners and no `onCancelHandler` was registered. - On session start, register an `onCancelHandler` that calls `heterogeneousAgentService.cancelSession(sessionId)` (SIGINT to the CLI). - Read the op's `abortController.signal` and short-circuit `onRawLine` so late events the CLI emits between SIGINT and exit don't leak into DB writes. - Skip the error-event forward in `onError` / the outer catch when the abort came from the user, so the UI doesn't surface a misleading error toast on top of the already-cancelled operation. Verified end-to-end: prompt that runs a long sequence of Reads → click Stop → `claude -p` process is gone within 2s, op status = cancelled, no error message written to the conversation. * ✨ feat(sidebar): mark heterogeneous agents with an "External" tag Pipes the agent's `agencyConfig.heterogeneousProvider.type` through the sidebar data flow and renders a `<Tag>` next to the title for any agent driven by an external CLI runtime (Claude Code today, more later). Mirrors the group-member External pattern so future provider types just need a label swap — the field is a string, not a boolean. - `SidebarAgentItem.heterogeneousType?: string | null` on the shared type - `HomeRepository.getSidebarAgentList` selects `agents.agencyConfig` and derives the field via `cleanObject` - `AgentItem` shows `<Tag>{t('group.profile.external')}</Tag>` when the field is present Verified client-side by injecting `heterogeneousType: 'claudecode'` into a sidebar item at runtime — the "外部" tag renders next to the title in the zh-CN locale. * ♻️ refactor(i18n): dedicated key for the sidebar external-agent tag Instead of reusing `group.profile.external` (which is about group members that are user-linked rather than virtual), add `agentSidebar.externalTag` specifically for the heterogeneous-runtime tag. Keeps the two concepts separate so we can swap this one to "Claude Code" / provider-specific labels later without touching the group UI copy. Remember to run `pnpm i18n` before the PR so the remaining locales pick up the new key. * 🐛 fix: clear remaining CI type errors Three small fixes so `tsgo --noEmit` exits clean: - `AgentIdSync`: `useChatStoreUpdater` is typed off the chat-store key, whose `activeAgentId` is `string` (initial ''). Coerce the optional URL param to `''` so the store key type matches; `createStoreUpdater` still skips the setState when the value is undefined-ish. - `heterogeneousAgentExecutor.test.ts`: `scope: 'session'` isn't a valid `MessageMapScope` (the union dropped that variant); switch the fixture to `'main'`, which is the correct scope for agent main conversations. - Same test file: `Array.at(-1)` is `T | undefined`; non-null assert since the preceding calls guarantee the slot is populated. * 🐛 fix: loosen createStoreUpdater signature to accept nullable values Upstream `createStoreUpdater` types `value` as exactly `T[Key]`, so any call site feeding an optional source (URL param, selector that may return undefined) fails type-check — even though the runtime already guards `typeof value !== 'undefined'` and no-ops in that case. Wrap it once in `store/utils/createStoreUpdater.ts` with a `T[Key] | null | undefined` value type so callers can pass `params.aid` directly, instead of the lossy `?? ''` fallback the previous commit used (which would have written an empty-string sentinel into the chat store). Swap the import in `AgentIdSync.tsx`. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>1 个月前