mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
canary
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6c8976b641 |
Update dependency vitest to v3.2.6 [SECURITY] (#15698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> |
||
|
|
6953f188c1 |
✨ feat(platform-agent): openclaw/hermes agent creation UI, device guard, and remote dispatch backend (#15065)
* ♻️ refactor(agent-invocation): add AgentInvocationIntent + unified non-hetero dispatcher (LOBE-8927/8928) Introduce a shared invocation contract and unified dispatcher for the non-hetero, non-group agent call paths (callAgent speak mode and @agent direct mentions). Removes the implicit client-only fallback that existed in both entry points. Changes: - agentDispatcher.ts: add AgentInvocationIntent interface as the unified intent type for callSubAgent / callAgent / @agent invocations - nonHeteroSubAgentDispatcher.ts (new): dispatchNonHeteroSubAgent() resolves child runtime via selectRuntimeType and routes to executeClientAgent (client) or executeGatewayAgent (gateway); throws for hetero (out of scope per LOBE-8926) - conversationLifecycle.ts #executeDirectMentionRoute: replace hardcoded executeClientAgent + TODO fallback with dispatchNonHeteroSubAgent call - builtin-tool-agent-management executor.ts callAgent speak mode: replace hardcoded executeClientAgent + TODO fallback with dispatchNonHeteroSubAgent call Fixes LOBE-8927 Fixes LOBE-8928 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ✨ feat(platform-agent): openclaw/hermes agent creation UI, device guard, and remote dispatch backend - Add CreatePlatformAgent 3-step creation modal (type select → config → bind device) - Add RemoteAgentConfigCard to agent profile editor for openclaw/hermes config - Add device guard banner in HeterogeneousChatInput for offline/unavailable devices - Add useRemoteAgentDeviceGuard hook for real-time device status polling - Fix backend dispatch: openclaw/hermes now use executeToolCall(runHeteroTask) instead of dispatchAgentRun (lh connect only handles tool_call_request) - Add agentNotify router for lh notify → DB write + gateway stream event - Add device.checkCapability endpoint for platform availability probe - Add notify_update event type to gateway stream and event handler - Add sendDoneSignal in heteroTask.ts for clean openclaw exit signaling - Unify non-hetero sub-agent dispatch via dispatchNonHeteroSubAgent (LOBE-8927) - Route openclaw/hermes to gateway runtime; keep claude-code/codex on hetero/client paths - Add i18n keys for platform agent UI and device guard banners Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🐛 fix(agentNotify): reuse execAgent placeholder message on first lh notify call Instead of creating a second empty bubble, the first assistant notify without a messageId now updates the placeholder assistantMessageId that execAgent already seeded in runningOperation.assistantMessageId. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ✨ feat(agentNotify): cancel openclaw/hermes process on interruptTask - Store deviceId + heteroType in topic.metadata.runningOperation at dispatch time - interruptTask now dispatches cancelHeteroTask tool call to the bound device when topicId reveals a remote hetero operation, sending SIGINT to the process - Pass topicId from gateway cancel callback to interruptTask - Add topicId to InterruptTaskSchema and InterruptTaskParams Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ♻️ refactor(hetero-agent): consolidate remote/local type classification into heterogeneous-agents package - Add RemoteHeterogeneousAgentConfig, REMOTE_HETEROGENEOUS_AGENT_CONFIGS, isRemoteHeterogeneousType, and derived type aliases (HeterogeneousAgentType, LocalHeterogeneousAgentType, RemoteHeterogeneousAgentType) to packages/heterogeneous-agents/src/config.ts - Extend HETEROGENEOUS_TYPE_LABELS to cover remote platform types (openclaw, hermes) via REMOTE_HETEROGENEOUS_AGENT_CONFIGS - Replace all inline `=== 'openclaw' || === 'hermes'` checks and local Sets/type aliases across aiAgent service, ProfileEditor, HeterogeneousChatInput, useRemoteAgentDeviceGuard, CreatePlatformAgent, RemoteAgentConfigCard, and deviceProxy with the shared utility - Show OpenClaw/Hermes display name in assistant message model tag (Usage component) by setting provider=heteroType on placeholder message and using HETEROGENEOUS_TYPE_LABELS for rendering - Fix ReferenceError: move remoteDeviceId declaration before updateMetadata call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add the platform agents get profiles * 🐛 fix(platform-agent): routing, security, and i18n issues from review - Route openclaw/hermes to gateway on desktop (P1): add isRemoteHeterogeneousType check in selectRuntimeType before desktop hetero branch — remote agents never use local desktop IPC, no special-casing needed - Fix race in heteroTask: sendAutoNotify → sendDoneSignal now sequential via .finally() so error message is written before agent_runtime_end is published - Security: validate messageId belongs to topicId in agentNotify before MessageModel.update to prevent cross-conversation data corruption - Clear capability/device/profile state on platform change in creation modal (P2) - Derive PLATFORM_DEFS from REMOTE_HETEROGENEOUS_AGENT_CONFIGS — new platforms automatically appear in the modal without code changes - Use HETEROGENEOUS_TYPE_LABELS for platform names in HeterogeneousChatInput and RemoteAgentConfigCard (remove hardcoded PLATFORM_NAMES map) - i18n: platform card descs, 'online'/'offline' tags, 'Select a device' placeholder, checkFailed error — all now use i18n keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ♻️ refactor(platform-agent): derive remote platform enum from config + fix test - device.ts: replace hardcoded z.enum(['hermes','openclaw']) with a zod enum derived from REMOTE_HETEROGENEOUS_AGENT_CONFIGS so new platforms are automatically covered without touching this file - heteroTask.ts / getAgentProfile.ts: use RemoteHeterogeneousAgentType instead of literal 'hermes' | 'openclaw' union for the same reason - gateway.test.ts: update cancel-handler assertion to include topicId which was added to the interruptTask call in the previous commit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ✨ feat(platform-agent): gate creation entry behind labs flag + expand dispatcher tests - Add enablePlatformAgent lab preference (default false) — the "Add Platform Agent" menu item is hidden until the user opts in via Settings → Advanced → Labs - Wire toggle in settings/advanced with labs i18n key (en/zh) - createPlatformAgentMenuItem returns null when flag is off - agentDispatcher.test: add remote hetero cases (openclaw/hermes → gateway on both web and desktop) to cover the routing fix added earlier Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🐛 fix(lint): merge duplicate import + sort interface props in nonHeteroSubAgentDispatcher Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 💄 feat(platform-agent): disable Hermes option in creation modal (coming soon) Hermes is not yet ready for production. Mark it as coming-soon in the platform selection step: grayed-out card, not clickable, "Coming Soon" tag next to the name. To enable Hermes when ready: remove 'hermes' from COMING_SOON_PLATFORMS in CreatePlatformAgent/index.tsx. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ✅ fix(test): mock CreatePlatformAgentModal in ModalProvider.test The modal always mounts (open=false) and calls lambdaQuery.useQuery which requires a tRPC context not present in the test environment. Mock it out the same way as ChatGroupWizard and EditingPopover. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ✅ fix(test): mock useUserStore + labPreferSelectors in useCreateMenuItems.test Adding useUserStore to useCreateMenuItems triggered user store initialization in tests, which pulled in @lobechat/const and failed because the existing mock only exports isDesktop. Mock the store and selectors directly instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🐛 fix(platform-agent): hide divider when platform agent entry is disabled The divider before 'Add Platform Agent' was unconditional — it showed even when the labs flag was off. Conditionally include both the divider and the menu item together so no orphaned separator appears. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
5f24d179d4 |
✨ 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
|
||
|
|
fe65741a32 |
♻️ refactor(hetero-agent): extract producer pipeline into shared package (#14425)
* 💄 style(todo-progress): use colorFillSecondary so left/right borders are visible against QueueTray The colorBorderSecondary stroke nearly vanished against the dark elevated bg, so the TODO card looked open on the sides when stacked under QueueTray. Match QueueTray's outer border token (colorFillSecondary) for a consistent visible seam; inner dividers keep colorBorderSecondary as a softer secondary level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(hetero-agent): extract producer pipeline into shared package LOBE-8516 phase 0. Move the JSONL framing + adapter conversion + toStreamEvent chain out of the renderer into a new `@lobechat/heterogeneous-agents/spawn` entry, then have desktop main run it before broadcasting. Renderer now consumes ready-made `AgentStreamEvent`s on `heteroAgentEvent`, dropping ~50 lines of in-renderer adapter wiring. This unifies the wire shape across desktop main, the upcoming `lh hetero exec` CLI, and the server `heteroIngest` handler — every consumer gets the same stamped `AgentStreamEvent` with no per-consumer adapter step. The desktop CC flow is unchanged behavior-wise: same adapter, same persistence ordering, same step-boundary semantics; only the seam between main and renderer moved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(hetero-agent): pull codex tracker into shared spawn, drop desktop's gateway-client dep Two cleanups on top of the phase 0 refactor: 1. Move `CodexFileChangeTracker` (+ its test) out of `apps/desktop/src/main/modules/heterogeneousAgent/` into `packages/heterogeneous-agents/src/spawn/`. `AgentStreamPipeline` now auto-instantiates it when `agentType === 'codex'`, so the desktop controller (and the future `lh hetero exec` CLI) stays agent-agnostic — no more "if codex { wire tracker via transformPayload }" branching at the call site. The public `transformPayload` hook is removed since it had no other consumer. 2. Re-export `AgentStreamEvent` / `AgentStreamEventType` from `@lobechat/heterogeneous-agents/spawn` and drop `@lobechat/agent-gateway-client` from `apps/desktop/package.json`. The gateway-client package is a browser-side WebSocket client; producer-side callers (desktop main, sandbox CLI) shouldn't carry it as a direct dep — they only need the type, which now flows through the producer-side entry. Type predicate on Codex payloads tightened to a non-`Required<>` shape so the moved file passes the root tsconfig's `strict: true` (apps/desktop's tsconfig was lax). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🧑💻 chore(local-testing): harden electron-dev.sh process management Lifecycle improvements for the local-testing helper so smoke runs against the desktop dev session are reliable: - `find_project_pids` now also catches user-started `bun run dev` Electron sessions (matches by project electron path, not just `--remote-debugging-port`), the launcher subshell saved to PIDFILE, and any process bound to the CDP port. Vite match tightened to `electron-vite[/.].*\bdev\b` so unrelated Vite invocations aren't swept up. - `do_stop` expands seed PIDs into their descendant trees (DFS via `pgrep -P`), SIGTERMs the whole tree, waits 5s, then SIGKILLs survivors. Belt-and-suspenders sweep for stragglers + anything still bound to the CDP port. Closes the long-standing "Helper processes survive the kill" gotcha. - `do_start` detects existing project Electron/vite before tearing it down so the user sees what's being killed; waits for port + user-data-dir locks to release before relaunching to avoid the "user data directory in use" race. - `wait_for_cdp` uses an explicit deadline + early bail-out if the launcher PID dies, instead of the previous fixed-step loop. `wait_for_renderer` no longer pre-sleeps 10s. `setsid` use is intentional; it puts the launched Electron in its own session so the whole tree shares a PGID we can signal in one shot. Note: `setsid` is GNU coreutils — on macOS without `brew install util-linux` the script will fail at the launch step. Documented as a known limitation; no fallback added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): gate session-complete on stdout fully drained Node may emit `proc.on('exit')` BEFORE child stdio fully closes (documented in child_process: "stdio streams might still be open"). Phase 0 of LOBE-8516 moved adapter ownership to main, so renderer no longer flushes its own adapter on session-complete — meaning trailing events synthesized by `pipeline.flush()` (e.g. Codex's `tool_end` for unfinished tool calls) would race against, and lose to, the `heteroAgentSessionComplete` broadcast, leaving renderer-side persistence to finalize on incomplete state. Fix: in `proc.on('exit')`, await `streamFinished(stdout)` (covers `'end'`, `'close'`, and `'error'`) BEFORE awaiting the broadcast queue. The first await ensures the `stdout.on('end')` handler has had a chance to schedule `pipeline.flush()` onto the queue; the second drains it. Only then do we broadcast complete / error. Regression test repros the documented Node race by emitting `exit` before `stdout.end()` and asserts every `heteroAgentEvent` (including the synthesized `tool_end` from `pipeline.flush()`) lands before `heteroAgentSessionComplete`. Bisected: test fails without the gate, passes with it. Also: add `packages/heterogeneous-agents` to `apps/desktop/pnpm-workspace.yaml` to mirror the new workspace dep added in the phase 0 refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): drop builtin-tool-claude-code dep, inline the 3 CC wire shapes the adapter needs Phase 0 added `@lobechat/heterogeneous-agents` as a runtime dep of the desktop main process. That transitively pulled in `@lobechat/builtin-tool-claude-code` (declared in the shared package's deps), which the desktop pnpm workspace doesn't list — CI install on the desktop project fails: ERR_PNPM_WORKSPACE_PKG_NOT_FOUND In ../../packages/heterogeneous-agents: "@lobechat/builtin-tool-claude-code@workspace:*" is in the dependencies but no package named "@lobechat/builtin-tool-claude-code" is present in the workspace The dep is also a layer-violation: `heterogeneous-agents` is the producer side (CLI stream → AgentStreamEvent), `builtin-tool-claude-code` is the UI tool definition (renderers / inspectors / agent template). Producer shouldn't depend on UI-tool packages, even if today the import is just types/constants — the dep cascade still drags `shared-tool-ui` etc. into every workspace that wants the adapter. Fix: inline the three things the adapter actually uses (`'TodoWrite'` tool name string, `TodoWriteArgs` interface, `ClaudeCodeTodoItem` interface). They reflect upstream Claude Code's wire schema — if `claude` ever renames `TodoWrite`, the adapter and the downstream renderers must both update regardless of whether they share a constant. Renderer-side packages (`builtin-tools/codex/TodoListRender`, etc.) keep importing the canonical `ClaudeCodeApiName` from `@lobechat/builtin-tool-claude-code`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
41719dfd29 |
🐛 fix(gateway): unstick input loading on auth_failed + recoverable auth_expired (#14419)
* 🐛 fix(gateway): complete local op on auth_failed to unstick input loading When the gateway client receives `auth_failed` (server has GC'd the op or the refreshed JWT no longer matches), the local op stayed `running` forever — input kept the stop button, and `topic.metadata.runningOperation` never cleared, so every revisit re-fired the same broken reconnect. Treat `auth_failed` as session-terminal alongside `session_complete` so `onSessionComplete` fires and `completeOperation` runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(gateway): support recoverable auth_expired with token refresh When the JWT expires while the operation is still alive on the server, sending `auth_failed` is wrong — the op is fine, only the credential went stale. Treat that as a separate, recoverable signal instead. Server (agent-gateway repo) emits a new `auth_expired` message and keeps the WebSocket open. The client refreshes its JWT (via the existing `aiAgentService.refreshGatewayToken`), updates the in-flight client, and reconnects. `auth_failed` stays terminal for cases where the op truly no longer exists. Mirrors the device-gateway-client pattern (`auth_expired` event + `updateToken` + `reconnect`). If no `tokenRefresher` is wired in (or the refresh itself fails), we fall back to terminal so the input doesn't stay stuck on the loading state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(gateway): disconnect ws on auth_expired without tokenRefresher The server keeps the WebSocket open after `auth_expired` (so the client can refresh and re-auth on the same connection). When no `tokenRefresher` is wired in, we mark the local op complete but were leaving the socket — heartbeat and autoReconnect kept running indefinitely after the op was gone, leaking background connections. Mirror the refresh-failure branch and call `client.disconnect()` before firing onSessionComplete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(gateway): make tokenRefresher required on connectToGateway Both real callers (executeGatewayAgent + reconnectToGatewayOperation) already supply a refresher built from `aiAgentService.refreshGatewayToken`, and there's no scenario where a Gateway op runs without a topic to refresh against. The optional path was carrying its own foot-gun (socket leak if forgotten) and a defensive ternary on `result.topicId` that the type already rules out. Required-only collapses both into the existing refresh-failure branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(gateway): inline token refresh, take topicId instead of refresher Both callers of connectToGateway built identical refresher closures over `aiAgentService.refreshGatewayToken(topicId)`. Pass `topicId` directly and let connectToGateway call the service inline — gateway.ts already imports aiAgentService for the cancel-handler path, so no new coupling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 chore(gateway): rewrite stale auth_expired comment The "no refresher provided" branch is gone — fold that case out of the comment and explain why the catch branch needs explicit disconnect(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd81642d83 |
♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package (#13866)
* ♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package Move the Agent Gateway WebSocket client from src/libs/agent-stream/ into a standalone workspace package at packages/agent-gateway-client/. This eliminates the duplicate AgentStreamEvent type in apps/cli and provides a single source of truth for the Gateway WS protocol types shared by SPA, server, and CLI consumers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * add agent-gateway-client --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |