β¨ feat(heterogeneous-agent): support CC subagent rendering#14001
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
π‘ Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 743431b535
βΉοΈ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with π.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
743431b to
7b54493
Compare
Codecov Reportβ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## canary #14001 +/- ##
===========================================
- Coverage 85.95% 66.82% -19.13%
===========================================
Files 599 2100 +1501
Lines 49998 179430 +129432
Branches 8547 21162 +12615
===========================================
+ Hits 42975 119902 +76927
- Misses 6899 59404 +52505
Partials 124 124
Flags with carried forward coverage won't be shown. Click here to find out more.
π New features to boost your workflow:
|
f94ba67 to
c0cb6c9
Compare
Restores the CC subagent-lineage adapter work that was held back from #LOBE-7392 until the thread-router backend changes ship. This PR targets the LOBE-7392 branch so the adapter diff stays isolated from the thread/UI foundation β GitHub will auto-retarget to canary once LOBE-7392 merges. Original scope (unchanged from the held-back commits): - ToolCallPayload.parentToolCallId carries parent tool_use id downstream so consumers can group subagent inner tools under their spawning parent. - claudeCode.ts routes raw.parent_tool_use_id events through handleSubagentAssistant so the main-agent step tracker is not advanced on subagent message.id changes, usage is not double-counted, and subagent text / reasoning are dropped (their final answer flows back via the outer tool_result). - emitToolChunk helper shared by main-agent and subagent paths so new suppress-rules live in one place. - 6 subagent-lineage tests: lineage propagation, no newStep on subagent message.id change, no turn_metadata emission, text/reasoning drop, main-agent step boundary resumes after subagent, subagent tool_result passthrough. Refs LOBE-7319, LOBE-7260 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pass the fullscreen toggle as AccordionItem action so the built-in chevron indicator (same as TopicList) sits inline with the title on the left, with Maximize2/Minimize2 on the right. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a main-agent tool_use spawns a subagent, the executor now sync-
allocates a threadId and creates a Thread, routing subsequent subagent
inner tool_uses (tagged with `parentToolCallId` by the adapter) into
that thread instead of the main assistant's tools[].
The "this tool_use spawns a subagent" decision lives entirely in the
adapter layer via a new `ToolCallPayload.subagentSpawn` descriptor
(`description`, `subagentType`). The CC adapter populates it on every
`Task` tool_use; when Codex (or any other CLI) grows a subtask concept,
its adapter populates the same field and the executor needs zero
changes. The executor never checks `identifier === 'claude-code'` or
`apiName === 'Task'` β it just reacts to the presence of
`subagentSpawn`.
- `ToolCallPayload.subagentSpawn?: { description?, subagentType? }`
in `packages/heterogeneous-agents/src/types.ts` β adapter-agnostic
spawn signal, paired with the existing `parentToolCallId` (which
marks tool_uses BELONGING to a subagent). Together they cover both
directions of the lineage.
- `claudeCode.ts` stamps `subagentSpawn` on main-agent `Task` tool_uses
using the already-parsed `block.input` β no redundant JSON.parse.
- `ThreadService.createThread` helper wraps the sync-id TRPC mutation
shipped in #14000. `generateThreadId()` mirrors the server's
`idGenerator('threads', 16)` shape (`thd_<16 chars>`) so caller-
provided ids match the schema pattern.
- `persistNewToolCalls` splits fresh tools into main/subagent groups:
Phase 1 (pre-register assistant.tools[]) and Phase 3 (backfill
result_msg_id) run for main tools only. A new Phase 1b creates the
Thread per `subagentSpawn` β guarded on `context.topicId` (required
for Thread creation; missing falls back to normal tool rendering).
Phase 2 writes tool messages for both groups, attaching `threadId`
to subagent writes. Orphaned subagent events (parent spawn never
registered) warn + drop instead of leaking into the main timeline.
- `taskThreadMap` lives at executor scope (not on ToolPersistenceState
which resets per step) so pathological orderings that straddle the
main-agent step boundary can't lose the parentβthread mapping.
7 new tests: 2 adapter-level (subagentSpawn stamped on Task,
NOT stamped on Read) + 5 executor-level (Thread creation, threadId
propagation onto subagent tool messages, main assistant.tools[]
isolation, orphan drop + warn, topicId-missing fallback).
Refs LOBE-7319, LOBE-7392
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `parentToolCallId` and `subagentSpawn` as first-class optional fields on `ChatToolPayload` + `ChatToolPayloadSchema`, so the adapter- emitted lineage metadata survives the TRPC `update-message` gate instead of being silently stripped by zod's default strip behavior. Reviewer-flagged bug: `UpdateMessageParamsSchema.tools` runs each payload through `ChatToolPayloadSchema`, which previously only whitelisted `apiName / arguments / id / identifier / intervention / result_msg_id / thoughtSignature / type`. Any adapter-level extension (subagent spawn marker, parent-child pointer) was dropped before it ever reached the `messages.tools` JSONB column, so lineage only lived in transient stream events and vanished on the first `tool_end β fetchAndReplaceMessages`. Downstream consumers that wanted to key off `tool.subagentSpawn` to render a TaskBlock, or follow `tool.parentToolCallId` to reconstruct the spawning parent, had nothing to work with. - `SubagentSpawnInfo` + `SubagentSpawnInfoSchema` defined in `packages/types/src/message/common/tools.ts` as the canonical shape. Structurally identical to the same-named type in `@lobechat/heterogeneous-agents` (which stays self-contained by design) β TypeScript structural typing handles the bridge. - Both new fields are optional on the interface and the zod schema, so existing callers continue to parse unchanged. - Jsonb column accepts any shape, so no DB migration β the only missing piece was the schema gate. 3 new regression tests next to the executor's subagent-thread-routing suite, asserting `ChatToolPayloadSchema.parse()` preserves both fields and the same fields survive through `UpdateMessageParamsSchema` (the actual TRPC gate that was stripping them before). Refs LOBE-7319 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦load schema" This reverts commit 042e48c.
β¦r fields `ToolCallPayload` is "one tool call" β it shouldn't carry stream-level lineage (parent spawn id, subagent turn id). That info describes the containing event/chunk and should live as a peer field on the event `data`, not nested inside each payload. Event model changes: - New `SubagentEventContext` + `SubagentSpawnMetadata` types. Events originating from a subagent stream (CC Task, future Codex subtask, etc.) carry `data.subagent` as a peer field next to `toolsCalling` / `toolCallId`. Covers `stream_chunk` (tools_calling), `tool_start`, `tool_end`, and `tool_result`. - `SubagentEventContext.spawnMetadata` appears ONLY on the first event for each new parent β lets the executor lazy-create the subagent Thread on first sight without needing to know CC-specific argument shapes or to re-parse `tool_use.input`. Subsequent events for the same parent carry just the lineage ids. - `ToolCallPayload` is back to its minimal form (`apiName / arguments / id / identifier / type`). No `parentToolCallId`, no `subagentSpawn` β those were the wrong abstraction level; removing them also sidesteps the `ChatToolPayloadSchema` strip-on-persist issue (the fields never need to survive DB roundtrip because Thread container persistence expresses the lineage). CC adapter (`claudeCode.ts`): - `handleSubagentAssistant` emits tools through a shared `emitToolChunk` that stamps the `subagent` peer field on the chunk + each tool_start. The FIRST subagent chunk for a new parent gets `spawnMetadata` pulled from a new adapter-internal `taskArgsById` cache β description / prompt / subagentType β announced exactly once via `announcedSpawns`. - `handleUser` stamps `subagent.parentToolCallId` on `tool_result` + `tool_end` when the user event carries `parent_tool_use_id` (CC's shape for subagent inner tool_results). - Main-agent tool_use handling no longer stamps lineage on payloads. Adapter tests updated β 4 rewrites in the subagent suite: - assert chunk-level peer fields (not payload-nested lineage) - assert `spawnMetadata` on first subagent event, absent on subsequent - assert main-agent tool_uses don't get `subagent` context - assert subagent `tool_result` + `tool_end` carry the peer 59 adapter tests pass (52 existing + 7 covering the new peer contract). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Subagents now materialize as a nested conversation inside a Thread,
shaped identically to the main topic:
Thread
ββ user (content = Task prompt, threadId=thread.id)
ββ assistant#1 (tools[] = subagent turn 1 tool_uses, threadId)
ββ tool (parentId=assistant#1, threadId)
ββ assistant#2 (tools[] = subagent turn 2 tool_uses, threadId)
ββ tool (parentId=assistant#2, threadId)
Same schema as a main topic, just rooted at a Thread instead of a
Topic. No new persistence shape, no new renderer β the existing
`query({ threadId })` read path reconstructs the subagent's full
conversation when the UI expands the TaskBlock.
Executor changes:
- `ToolPersistenceState` shrinks to `{ payloads, persistedIds }` β the
`tool_use.id β tool message DB id` map moves to executor scope as
one global `toolMsgIdByCallId` shared across main + every subagent
run. `tool_result` lookups don't care which scope created the row.
- `persistNewToolCalls` β renamed `persistToolBatch` and made scope-
agnostic (takes an optional `threadId` + the global id map). Runs
the same 3-phase flow (pre-register β create β backfill) whether
target is main assistant or in-thread subagent assistant.
- New `persistSubagentToolChunk` handles the subagent path: reads the
adapter's `SubagentEventContext` peer field off the chunk, lazy-
creates the Thread + user message on the FIRST chunk for each
parent (using `spawnMetadata`), opens a new in-thread assistant on
`subagentMessageId` change (same shape as main-agent step
boundary), then delegates to `persistToolBatch`.
- `SubagentRunState` tracks per-parent Thread id, current in-thread
assistant, `currentSubagentMessageId`, chain parent, and its own
`ToolPersistenceState`. Lives at executor scope so subagent events
straddling a main-agent step boundary keep their mapping.
- Step-boundary parent lookup reads from `toolState.payloads` (not
the global id map) so main-agent chain doesn't accidentally pick
up a subagent tool's msg id as the step parent.
- Executor has NO CC-specific knowledge β it never checks
`identifier`, `apiName`, or parses `tool_use.arguments`. All CC
quirks live in the adapter; new CLIs (Codex subtask, ...) plug in
by emitting the same `SubagentEventContext` peer.
Test rewrite β 6 tests under "CC subagent thread-container":
- Task tool_use alone does NOT create a Thread (lazy)
- First subagent event creates Thread + `role:'user'` seeded with
the Task prompt + first in-thread `role:'assistant'`
- Subagent inner tools persist as `role:'tool'` messages with
threadId set and parentId chained to the in-thread assistant
- `subagentMessageId` change opens a new in-thread assistant
- Main `assistant.tools[]` carries Task only; subagent inner tools
appear on the in-thread assistant's `tools[]`
- Missing topicId gracefully skips Thread creation
25 executor tests pass (19 existing + 6 rewritten for new shape).
Refs LOBE-7319, LOBE-7392
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦read view Electron E2E surfaced two gaps in the Thread-container model shipped in the previous commit: 1. **Subagent user-message content empty.** Real CC emits `Agent` as the spawn-tool name for general-purpose subagents (not only `Task` as the spec documents). My earlier `taskArgsById` cache keyed off `ClaudeCodeApiName.Task` only, so `spawnMetadata.prompt` was undefined when the user watched the actual app β the Thread's `role:'user'` message landed with empty content and the thread view looked like a tool call floating alone. 2. **No closing summary in the Thread.** The adapter dropped subagent text/reasoning per an earlier comment claiming the subagent's final answer arrives via the outer tool_result. That's true for the MAIN timeline (the outer spawn tool's result content = the subagent's summary), but the THREAD view is a standalone conversation β dropping the subagent's final text left it ending on a bare tool call with no assistant conclusion. Adapter changes (`claudeCode.ts`): - Rename `taskArgsById` β `mainToolInputsById` and cache EVERY main-agent tool_use input (not just `Task`). `emitToolChunk` looks up the parent's input by `parent_tool_use_id` on the first subagent event and extracts `description` / `prompt` / `subagent_type` defensively β any CC spawn-tool variant that shares this input shape (`Task`, `Agent`, future ones) gets spawn metadata for free. - `handleSubagentAssistant` stops filtering `tool_use` only. Text and `thinking` blocks now emit as `stream_chunk` events with the `subagent` peer field attached β routed to the in-thread assistant, NOT the main assistant's accumulators. Executor changes (`heterogeneousAgentExecutor.ts`): - `SubagentRunState` gains `accumulatedContent` + `accumulatedReasoning`, mirroring main-agent content tracking. - Extract `ensureSubagentRun` helper so text chunks and tool chunks share the Thread / user / assistant lifecycle logic. On turn boundary (`subagentMessageId` change), flush the prior turn's accumulated content before creating the next in-thread assistant β covers text-only turns that never hit `persistToolBatch`. - New `persistSubagentTextChunk` accumulates text/reasoning onto the run; `persistToolBatch` writes content alongside tools[] so DB sees both in one update (same pattern as main agent). - New `finalizeSubagentRun` flushes pending content when the main- agent receives the spawn tool's `tool_result` β ensures the closing summary lands before `fetchAndReplaceMessages` refreshes from stale DB state. - `onComplete` iterates `subagentRuns.keys()` and flushes any un-finalized runs, covering the CLI-crashed-mid-subagent edge case. Tests: - Adapter: replaced the "drops subagent text" test with two tests asserting text/reasoning ARE emitted with correct `subagent` peer context. New test covers the `Agent` spawn-tool variant. - Executor: 4 new tests cover the Thread user message content population, subagent text accumulation into the in-thread assistant, non-leakage into main assistant content, and tool_result-triggered finalization. Total 29 executor tests pass. E2E verified via Electron + CDP: fresh CC session β `Agent`-based subagent β Thread created with `title="Run pwd command"`, `metadata.subagentType="general-purpose"`, `role:'user'` seeded with the Task prompt, Bash tool_use + result inside the thread. Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦ is lazy-created Earlier Electron E2E repro: a subagent Thread born mid-stream landed in DB correctly, but the topic sidebar only picked it up after the user manually navigated topics / called `refreshThreads()` β the SWR cache for the thread list (`SWR_USE_FETCH_THREADS`) wasn't invalidated, so the new Thread stayed invisible until the next cold fetch. - `ensureSubagentRun` now accepts an optional `onThreadCreated` callback fired once per lazy Thread create. Kept as a callback (not a direct `store.refreshThreads` call) so the executor persistence logic stays decoupled from the Zustand store shape. - `persistSubagentToolChunk` + `persistSubagentTextChunk` thread the callback through to `ensureSubagentRun`. - Executor defines `onSubagentThreadCreated` once at run scope and passes it into all three subagent persist call sites. Calls `get().refreshThreads()` fire-and-forget β it's a no-op when the user has navigated away from the topic, so no need to block persist on cache refresh. Two regression tests: - Subagent-spawning run β `refreshThreads` called exactly once - Non-subagent run (plain tool only) β `refreshThreads` NOT called Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦ + Render CC's subagent-spawn tool arrives as `tool_use.name: 'Agent'`, not `Task` β rename the apiName so the Inspector/Render registry actually matches the stream. Inspector switches icon/label by `subagent_type` (Explore / Plan / general-purpose / statusline-setup), with `description` surfaced in a chip; new Render shows `prompt` and tool_result as labelled Markdown blocks that can't fit in the folded header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the hand-rolled motion span + role="button" / keyboard-handler expand toggle with a single @lobehub/ui ActionIcon β fewer a11y edge cases to maintain and the icon/title/blockSize layout matches other toolbar buttons in the group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Give the Edit render a small inline padding so the CodeDiff lines up with the rest of the tool renders; zero-width flush-left was awkward against the surrounding labelled blocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦ator
ContentLoading now renders "{name} is running" / "{name} θΏθ‘δΈ" for
heterogeneous agent execution β previously it collapsed to the generic
"External agent running" so a user watching a long CC run couldn't tell
which external CLI was working (mattered once Codex landed as a sibling
adapter).
- Share `HETEROGENEOUS_TYPE_LABELS` (claude-code / codex) out of the
heterogeneous-agents package so all consumers read one map; home
Sidebar AgentItem switches to it and drops its inline copy.
- `conversationLifecycle.startOperation` passes
`metadata.heterogeneousType` on the heterogeneous-exec operation so
ContentLoading can resolve the label from the running op without
re-deriving the adapter type from session state.
- New `operation.heterogeneousAgentFallback` key covers the (rare) case
where the metadata is absent β keeps the dot loader labelled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the viewing loop for CC subagent runs: the main-topic Agent tool
row now links into the spawned Thread, the Thread's Portal view renders
with provenance + read-only affordances, and the sidebar surfaces which
entries are subagent-produced.
UX:
- Agent render gains a trailing "View / Collapse full subagent
conversation" toggle. It looks up the Thread by
`metadata.sourceToolCallId === toolCallId` and calls
openThreadInPortal / closeThreadPortal β hidden until the executor
lazy-creates the Thread on the first subagent event, so it never
renders as a no-op.
- Portal Thread Header shows a `[icon] subagentType` Tag next to the
title ("Explore" / "General purpose" / ...). Inspector's folded row
already exposes the same detail, so the icon + label stays
consistent across the two surfaces.
- Portal Thread Chat flips into read-only mode when
`metadata.sourceToolCallId` is set: ChatInput is hidden (the
external CLI owns the session β new turns have nowhere to go),
`disableEditing` propagates to every message (no double-click to
edit, no user action bar), and `useThreadActionsBarConfig` wipes
`bar` + `menu` across assistant / assistantGroup / user roles.
- Sidebar ThreadItem on both /agent and /group routes renders a plain
"Subagent" badge next to the title when
`metadata.subagentType` is present. The type detail deliberately
lives on the Thread Header, not here β sidebar space is tight.
Shared resolver:
- `CC_SUBAGENT_TYPES` + `resolveCCSubagentType` move out of the
Inspector into `packages/builtin-tool-claude-code/src/client/
subagentTypes.ts` and re-export from the `/client` entry. Inspector
+ Portal Thread Header both consume it, so the icon/label stay in
sync. Kept UI-level (LucideIcon | FC) rather than pushed into
heterogeneous-agents, which is a pure-data package.
- Root package.json adds a direct dep on
`@lobechat/builtin-tool-claude-code` so Portal Thread Header can
import from `/client` (previously only transitive via builtin-tools).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ad8e629 to
ab78b38
Compare
β¦m action slot
After the expand-toggle refactor to ActionIcon + the `action` prop on
AccordionItem, the test's module mocks were missing both: ActionIcon
wasn't exported from the @lobehub/ui mock, and AccordionItem dropped
`action` on the floor so the toggle never made it into the rendered
DOM. Restore both β ActionIcon renders as a real \`button\` with
aria-label so \`getByRole('button', { name })\` can still target it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# π LobeHub v2.1.53 (20260427) **Release Date:** April 27, 2026 **Since v2.1.52:** 194 merged PRs Β· 17 contributors > Introduce Heterogeneous Agent β Claude Code and Codex run as first-class desktop runtimes, paired with a new Agent Signal package, sharper desktop UX, and a wave of flagship model additions. --- ## β¨ Highlights - **Introduce Heterogeneous Agent** β Claude Code and Codex run as first-class desktop agents: subagent rendering, partial-message streaming, multi-turn resume, terminal error surfacing, rich tool inspectors, and runtime polish. (#14162, #13754, #14067, #14001, #13970, #13942) - **Screen capture & Quick Chat tray** β New desktop screen capture overlay (macOS permission-gated) with Quick Chat tray and upload pipeline improvements; chat input auto-focuses on overlay mount. (#13818, #14097, #14105) - **Desktop topic & tab UX** β Dedicated topic popup window with cross-window sync, Cmd+W/Cmd+T tab shortcuts, TabBar polish, recent working directories expanded to 20, and human approval notifications. (#13957, #13983, #13972, #14036, #14092) - **Git workflow built-in** β One-click pull/push from the branch chip, ahead/behind badge, and submodule/worktree repo detection. (#14041, #13980, #13978) - **Agent Signal package** β New `@lobechat/agent-signal` runtime for dynamic memory feedback signals, with OTel metrics and self-iteration in Lab. (#14157, #14170, #14159, #14169, #14187) - **New models** β Claude Opus 4.7 with `xhigh` effort tier, GPT-5.5, DeepSeek V4 Flash/Pro with reasoning slider, Kimi K2.6, MiMo-V2.5/Pro, gpt-image-2, Qwen3.6 Flash/Plus, and Pixverse-c1. (#13903, #14147, #14114, #14004, #14089, #14039, #13923) - **New providers** β OpenCode Zen, OpenCode Go, and Azure OpenAI Router runtime. (#13943, #14064, #13823) - **Mobile settings overhaul** β Full settings menu and responsive profile layout for mobile. (#14019) --- ## ποΈ Heterogeneous Agent - Claude Code runtime, working-directory awareness, and sidebar polish. (#13970) - CC subagent rendering with persistent streamed text; parallel-tool orphan fix. (#14001, #13968, #14024) - Per-step usage persisted to each step assistant message. (#13964) - Per-phase workflow expand defaults; full-expand toggle with three-level expansion. (#14171, #13906) - Hetero-mode actions bar; tool inspector polish. (#13963, #14034, #14030) - Codex desktop integration with rich tool rendering and devtools preview. (#14067, #14100) - Codex terminal error surfacing and CLI output tracing. (#14166) - Tighten `isCanUseVision` default and add aggregator fallback. (#14172) - Persist `ccSessionId` in topic metadata for CC multi-turn resume. (#13902) - CC account card, topic filter, and integration polish. (#13955, #13942, #13950) - Token-level deltas streamed via `--include-partial-messages`. (#13929) --- ## π§ Agent Signal & Self-Iteration - New `@lobechat/agent-signal` package with dynamic feedback signals. (#14157) - AgentSignalRuntime wired through agent-tracing and observability-otel metrics. (#14170, #14159) - Self-iteration feature flag added to Lab; front-side flag check. (#14169, #14186) - Signal policy for receiving memory feedback dynamically. (#14187) --- ## π¬ Conversation - Queue follow-up sends during running CC turns. (#14179) - Persist per-topic chat scroll position; pin user message + fold long messages. (#14191, #14056) - Inline resend when editing last user message. (#14080) - Disable first-block markdown streaming to prevent flicker. (#14193, #13904) - Prevent Markdown stream replay when vlist remounts streaming items. (#14086) - Stop repinning after manual scroll; unify scroll-to-user + spacer hooks. (#14099, #14132) --- ## π± Platforms & Integrations ### Desktop / Electron - Screen capture overlay, Quick Chat tray, and upload pipeline improvements. (#13818) - macOS permission gate for screen capture; auto-focus chat panel input. (#14097, #14105) - Dedicated topic popup window with cross-window sync. (#13957) - TabBar polish: `+` button for new topic, dark theme blend, close icon by default. (#13972, #14203, #13973) - Recent working directories expanded from 5 to 20; submodule/worktree repo detection. (#14036, #13978) - Cmd+W / Cmd+T tab shortcuts and global shortcut consolidation. (#13983, #13880) - Linux icon configuration; human approval desktop notifications. (#14042, #14092) ### Git Workflow - One-click pull/push from branch chip; ahead/behind badge with refactored GitCtr. (#14041, #13980) ### Mobile - Full settings menu and responsive profile layout. (#14019) - Agent route added to mobile router; mobile agent topic route registered. (#14103, #14158) - Session list skeleton row layout corrected. (#14040) ### Bot / Messaging - DM strategy support; bot emoji and markdown render optimization. (#14201, #14091, #14140) - Slack webhook fix; bot platform setup guide reference. (#14052, #14121) --- ## π€ Models & Providers ### New models - **Claude Opus 4.7** with `xhigh` effort tier; strip temperature/top_p. (#13903, #13909) - **GPT-5.5**. (#14147) - **DeepSeek V4** Flash/Pro cards with reasoning slider; cache-hit and Pro discount pricing. (#14114, #14209, #14196, #14131) - **Kimi K2.6** model with LobeHub-hosted card. (#14004, #14006) - **MiMo-V2.5 / V2.5-Pro**. (#14089) - **gpt-image-2**, **Qwen3.6 Flash/Plus**, **Pixverse-c1**. (#14039, #13923) ### New providers - **OpenCode Zen** and **OpenCode Go** with env-var support. (#13943, #14064) - **Azure OpenAI Router** runtime support. (#13823) - Model alias mapping for image and video runtimes. (#13896) - Seedance video models migrated to Dreamina. (#14144) ### Runtime reliability - Sanitize invalid tool_call arguments to unbreak strict providers. (#14033) - Tolerate null `function.name` in streaming tool_call deltas. (#14139) - Preserve Gemini 3 `thoughtSignature` in `call_tools_batch` normalization. (#14032) - Downgrade `image_url` parts when target model lacks vision. (#14029) - Preserve Cloudflare provider error context. (#14136) - Use `safety_identifier` for OpenAI Responses API. (#14148) - Unwrap underlying PG error in `formatErrorEventData`. (#14038) --- ## π₯οΈ User Experience - **Onboarding** β Preset agent naming suggestions, structured hunk ops for `updateDocument`, persona analytics snapshot, footer promotion pipeline, wrap-up button. (#13931, #13989, #13930, #13853, #13934) - **Document workflow** β Agent documents promoted as primary workspace panel; history management and compare workflow; web-crawl docs associated with agent documents. (#13924, #13725, #13893) - **cmdk** β Agent identity surfaced on topic search results; topic/message search scoped to current agent. (#14204, #13960) - **Floating chat panel** and workspace improvements. (#13887) - **Topic completion status** with dropdown action and filter. (#14005) --- ## π§ Tooling - Redis-backed feature flag provider for runtime config. (#14098) - Vite upgraded to 8.0.0 with Rolldown strict execution order. (#12720, #14058) - `@lobechat/model-bank` automated npm release with provenance. (#14015, #14017, #14018) - Skill activation fallback when `activateTools` cannot find identifier. (#14010) - Cron tool: timezone and existing jobs injected into system prompt; clarified `lobe-gtd` and `lobe-cron` descriptions. (#14012, #14013) --- ## π Security & Reliability - **Security:** uuid bumped to v14 (advisory). (#14083) - **Security:** validate avatar URL and scope old-avatar deletion to owner. (#13982) - **Security:** clear OIDC sessions on better-auth signout; return 401 (not 500) for expired OIDC JWT. (#13916, #14014) - **Reliability:** scope pending-approval check to current assistant turn. (#14182) - **Reliability:** sanitize heterogeneous-agent attachment cache filenames. (#13937) - **Reliability:** reduce subagent task status error noise. (#14026) --- ## π₯ Contributors Huge thanks to **17 contributors** who shipped **194 merged PRs** this week. @hardy Β· @shaun0927 Β· @hezhijie0327 Β· @sxjeru Β· @arvinxx Β· @Innei Β· @tjx666 Β· @lijian Β· @neko Β· @rdmclin2 Β· @AmAzing129 Β· @sudongyuer Β· @CanisMinor Β· @rivertwilight Plus @lobehubbot and renovate[bot] for maintenance. --- **Full Changelog**: v2.1.52...v2.1.53
β¦4001) * β¨ feat(heterogeneous-agents): preserve CC subagent lineage in adapter Restores the CC subagent-lineage adapter work that was held back from #LOBE-7392 until the thread-router backend changes ship. This PR targets the LOBE-7392 branch so the adapter diff stays isolated from the thread/UI foundation β GitHub will auto-retarget to canary once LOBE-7392 merges. Original scope (unchanged from the held-back commits): - ToolCallPayload.parentToolCallId carries parent tool_use id downstream so consumers can group subagent inner tools under their spawning parent. - claudeCode.ts routes raw.parent_tool_use_id events through handleSubagentAssistant so the main-agent step tracker is not advanced on subagent message.id changes, usage is not double-counted, and subagent text / reasoning are dropped (their final answer flows back via the outer tool_result). - emitToolChunk helper shared by main-agent and subagent paths so new suppress-rules live in one place. - 6 subagent-lineage tests: lineage propagation, no newStep on subagent message.id change, no turn_metadata emission, text/reasoning drop, main-agent step boundary resumes after subagent, subagent tool_result passthrough. Refs LOBE-7319, LOBE-7260 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π style(workflow-collapse): move expand toggle to action slot Pass the fullscreen toggle as AccordionItem action so the built-in chevron indicator (same as TopicList) sits inline with the title on the left, with Maximize2/Minimize2 on the right. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): route CC Task tool_use to subagent Thread When a main-agent tool_use spawns a subagent, the executor now sync- allocates a threadId and creates a Thread, routing subsequent subagent inner tool_uses (tagged with `parentToolCallId` by the adapter) into that thread instead of the main assistant's tools[]. The "this tool_use spawns a subagent" decision lives entirely in the adapter layer via a new `ToolCallPayload.subagentSpawn` descriptor (`description`, `subagentType`). The CC adapter populates it on every `Task` tool_use; when Codex (or any other CLI) grows a subtask concept, its adapter populates the same field and the executor needs zero changes. The executor never checks `identifier === 'claude-code'` or `apiName === 'Task'` β it just reacts to the presence of `subagentSpawn`. - `ToolCallPayload.subagentSpawn?: { description?, subagentType? }` in `packages/heterogeneous-agents/src/types.ts` β adapter-agnostic spawn signal, paired with the existing `parentToolCallId` (which marks tool_uses BELONGING to a subagent). Together they cover both directions of the lineage. - `claudeCode.ts` stamps `subagentSpawn` on main-agent `Task` tool_uses using the already-parsed `block.input` β no redundant JSON.parse. - `ThreadService.createThread` helper wraps the sync-id TRPC mutation shipped in lobehub#14000. `generateThreadId()` mirrors the server's `idGenerator('threads', 16)` shape (`thd_<16 chars>`) so caller- provided ids match the schema pattern. - `persistNewToolCalls` splits fresh tools into main/subagent groups: Phase 1 (pre-register assistant.tools[]) and Phase 3 (backfill result_msg_id) run for main tools only. A new Phase 1b creates the Thread per `subagentSpawn` β guarded on `context.topicId` (required for Thread creation; missing falls back to normal tool rendering). Phase 2 writes tool messages for both groups, attaching `threadId` to subagent writes. Orphaned subagent events (parent spawn never registered) warn + drop instead of leaking into the main timeline. - `taskThreadMap` lives at executor scope (not on ToolPersistenceState which resets per step) so pathological orderings that straddle the main-agent step boundary can't lose the parentβthread mapping. 7 new tests: 2 adapter-level (subagentSpawn stamped on Task, NOT stamped on Read) + 5 executor-level (Thread creation, threadId propagation onto subagent tool messages, main assistant.tools[] isolation, orphan drop + warn, topicId-missing fallback). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(types): persist subagent lineage fields on ChatToolPayload schema Add `parentToolCallId` and `subagentSpawn` as first-class optional fields on `ChatToolPayload` + `ChatToolPayloadSchema`, so the adapter- emitted lineage metadata survives the TRPC `update-message` gate instead of being silently stripped by zod's default strip behavior. Reviewer-flagged bug: `UpdateMessageParamsSchema.tools` runs each payload through `ChatToolPayloadSchema`, which previously only whitelisted `apiName / arguments / id / identifier / intervention / result_msg_id / thoughtSignature / type`. Any adapter-level extension (subagent spawn marker, parent-child pointer) was dropped before it ever reached the `messages.tools` JSONB column, so lineage only lived in transient stream events and vanished on the first `tool_end β fetchAndReplaceMessages`. Downstream consumers that wanted to key off `tool.subagentSpawn` to render a TaskBlock, or follow `tool.parentToolCallId` to reconstruct the spawning parent, had nothing to work with. - `SubagentSpawnInfo` + `SubagentSpawnInfoSchema` defined in `packages/types/src/message/common/tools.ts` as the canonical shape. Structurally identical to the same-named type in `@lobechat/heterogeneous-agents` (which stays self-contained by design) β TypeScript structural typing handles the bridge. - Both new fields are optional on the interface and the zod schema, so existing callers continue to parse unchanged. - Jsonb column accepts any shape, so no DB migration β the only missing piece was the schema gate. 3 new regression tests next to the executor's subagent-thread-routing suite, asserting `ChatToolPayloadSchema.parse()` preserves both fields and the same fields survive through `UpdateMessageParamsSchema` (the actual TRPC gate that was stripping them before). Refs LOBE-7319 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "β¨ feat(types): persist subagent lineage fields on ChatToolPayload schema" This reverts commit 042e48c. * β»οΈ refactor(heterogeneous-agents): lift subagent context to event-peer fields `ToolCallPayload` is "one tool call" β it shouldn't carry stream-level lineage (parent spawn id, subagent turn id). That info describes the containing event/chunk and should live as a peer field on the event `data`, not nested inside each payload. Event model changes: - New `SubagentEventContext` + `SubagentSpawnMetadata` types. Events originating from a subagent stream (CC Task, future Codex subtask, etc.) carry `data.subagent` as a peer field next to `toolsCalling` / `toolCallId`. Covers `stream_chunk` (tools_calling), `tool_start`, `tool_end`, and `tool_result`. - `SubagentEventContext.spawnMetadata` appears ONLY on the first event for each new parent β lets the executor lazy-create the subagent Thread on first sight without needing to know CC-specific argument shapes or to re-parse `tool_use.input`. Subsequent events for the same parent carry just the lineage ids. - `ToolCallPayload` is back to its minimal form (`apiName / arguments / id / identifier / type`). No `parentToolCallId`, no `subagentSpawn` β those were the wrong abstraction level; removing them also sidesteps the `ChatToolPayloadSchema` strip-on-persist issue (the fields never need to survive DB roundtrip because Thread container persistence expresses the lineage). CC adapter (`claudeCode.ts`): - `handleSubagentAssistant` emits tools through a shared `emitToolChunk` that stamps the `subagent` peer field on the chunk + each tool_start. The FIRST subagent chunk for a new parent gets `spawnMetadata` pulled from a new adapter-internal `taskArgsById` cache β description / prompt / subagentType β announced exactly once via `announcedSpawns`. - `handleUser` stamps `subagent.parentToolCallId` on `tool_result` + `tool_end` when the user event carries `parent_tool_use_id` (CC's shape for subagent inner tool_results). - Main-agent tool_use handling no longer stamps lineage on payloads. Adapter tests updated β 4 rewrites in the subagent suite: - assert chunk-level peer fields (not payload-nested lineage) - assert `spawnMetadata` on first subagent event, absent on subsequent - assert main-agent tool_uses don't get `subagent` context - assert subagent `tool_result` + `tool_end` carry the peer 59 adapter tests pass (52 existing + 7 covering the new peer contract). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): persist subagent runs as Thread containers Subagents now materialize as a nested conversation inside a Thread, shaped identically to the main topic: Thread ββ user (content = Task prompt, threadId=thread.id) ββ assistant#1 (tools[] = subagent turn 1 tool_uses, threadId) ββ tool (parentId=assistant#1, threadId) ββ assistant#2 (tools[] = subagent turn 2 tool_uses, threadId) ββ tool (parentId=assistant#2, threadId) Same schema as a main topic, just rooted at a Thread instead of a Topic. No new persistence shape, no new renderer β the existing `query({ threadId })` read path reconstructs the subagent's full conversation when the UI expands the TaskBlock. Executor changes: - `ToolPersistenceState` shrinks to `{ payloads, persistedIds }` β the `tool_use.id β tool message DB id` map moves to executor scope as one global `toolMsgIdByCallId` shared across main + every subagent run. `tool_result` lookups don't care which scope created the row. - `persistNewToolCalls` β renamed `persistToolBatch` and made scope- agnostic (takes an optional `threadId` + the global id map). Runs the same 3-phase flow (pre-register β create β backfill) whether target is main assistant or in-thread subagent assistant. - New `persistSubagentToolChunk` handles the subagent path: reads the adapter's `SubagentEventContext` peer field off the chunk, lazy- creates the Thread + user message on the FIRST chunk for each parent (using `spawnMetadata`), opens a new in-thread assistant on `subagentMessageId` change (same shape as main-agent step boundary), then delegates to `persistToolBatch`. - `SubagentRunState` tracks per-parent Thread id, current in-thread assistant, `currentSubagentMessageId`, chain parent, and its own `ToolPersistenceState`. Lives at executor scope so subagent events straddling a main-agent step boundary keep their mapping. - Step-boundary parent lookup reads from `toolState.payloads` (not the global id map) so main-agent chain doesn't accidentally pick up a subagent tool's msg id as the step parent. - Executor has NO CC-specific knowledge β it never checks `identifier`, `apiName`, or parses `tool_use.arguments`. All CC quirks live in the adapter; new CLIs (Codex subtask, ...) plug in by emitting the same `SubagentEventContext` peer. Test rewrite β 6 tests under "CC subagent thread-container": - Task tool_use alone does NOT create a Thread (lazy) - First subagent event creates Thread + `role:'user'` seeded with the Task prompt + first in-thread `role:'assistant'` - Subagent inner tools persist as `role:'tool'` messages with threadId set and parentId chained to the in-thread assistant - `subagentMessageId` change opens a new in-thread assistant - Main `assistant.tools[]` carries Task only; subagent inner tools appear on the in-thread assistant's `tools[]` - Missing topicId gracefully skips Thread creation 25 executor tests pass (19 existing + 6 rewritten for new shape). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): subagent prompt + closing summary in Thread view Electron E2E surfaced two gaps in the Thread-container model shipped in the previous commit: 1. **Subagent user-message content empty.** Real CC emits `Agent` as the spawn-tool name for general-purpose subagents (not only `Task` as the spec documents). My earlier `taskArgsById` cache keyed off `ClaudeCodeApiName.Task` only, so `spawnMetadata.prompt` was undefined when the user watched the actual app β the Thread's `role:'user'` message landed with empty content and the thread view looked like a tool call floating alone. 2. **No closing summary in the Thread.** The adapter dropped subagent text/reasoning per an earlier comment claiming the subagent's final answer arrives via the outer tool_result. That's true for the MAIN timeline (the outer spawn tool's result content = the subagent's summary), but the THREAD view is a standalone conversation β dropping the subagent's final text left it ending on a bare tool call with no assistant conclusion. Adapter changes (`claudeCode.ts`): - Rename `taskArgsById` β `mainToolInputsById` and cache EVERY main-agent tool_use input (not just `Task`). `emitToolChunk` looks up the parent's input by `parent_tool_use_id` on the first subagent event and extracts `description` / `prompt` / `subagent_type` defensively β any CC spawn-tool variant that shares this input shape (`Task`, `Agent`, future ones) gets spawn metadata for free. - `handleSubagentAssistant` stops filtering `tool_use` only. Text and `thinking` blocks now emit as `stream_chunk` events with the `subagent` peer field attached β routed to the in-thread assistant, NOT the main assistant's accumulators. Executor changes (`heterogeneousAgentExecutor.ts`): - `SubagentRunState` gains `accumulatedContent` + `accumulatedReasoning`, mirroring main-agent content tracking. - Extract `ensureSubagentRun` helper so text chunks and tool chunks share the Thread / user / assistant lifecycle logic. On turn boundary (`subagentMessageId` change), flush the prior turn's accumulated content before creating the next in-thread assistant β covers text-only turns that never hit `persistToolBatch`. - New `persistSubagentTextChunk` accumulates text/reasoning onto the run; `persistToolBatch` writes content alongside tools[] so DB sees both in one update (same pattern as main agent). - New `finalizeSubagentRun` flushes pending content when the main- agent receives the spawn tool's `tool_result` β ensures the closing summary lands before `fetchAndReplaceMessages` refreshes from stale DB state. - `onComplete` iterates `subagentRuns.keys()` and flushes any un-finalized runs, covering the CLI-crashed-mid-subagent edge case. Tests: - Adapter: replaced the "drops subagent text" test with two tests asserting text/reasoning ARE emitted with correct `subagent` peer context. New test covers the `Agent` spawn-tool variant. - Executor: 4 new tests cover the Thread user message content population, subagent text accumulation into the in-thread assistant, non-leakage into main assistant content, and tool_result-triggered finalization. Total 29 executor tests pass. E2E verified via Electron + CDP: fresh CC session β `Agent`-based subagent β Thread created with `title="Run pwd command"`, `metadata.subagentType="general-purpose"`, `role:'user'` seeded with the Task prompt, Bash tool_use + result inside the thread. Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π fix(heterogeneous-agents): refresh thread list when subagent Thread is lazy-created Earlier Electron E2E repro: a subagent Thread born mid-stream landed in DB correctly, but the topic sidebar only picked it up after the user manually navigated topics / called `refreshThreads()` β the SWR cache for the thread list (`SWR_USE_FETCH_THREADS`) wasn't invalidated, so the new Thread stayed invisible until the next cold fetch. - `ensureSubagentRun` now accepts an optional `onThreadCreated` callback fired once per lazy Thread create. Kept as a callback (not a direct `store.refreshThreads` call) so the executor persistence logic stays decoupled from the Zustand store shape. - `persistSubagentToolChunk` + `persistSubagentTextChunk` thread the callback through to `ensureSubagentRun`. - Executor defines `onSubagentThreadCreated` once at run scope and passes it into all three subagent persist call sites. Calls `get().refreshThreads()` fire-and-forget β it's a no-op when the user has navigated away from the topic, so no need to block persist on cache refresh. Two regression tests: - Subagent-spawning run β `refreshThreads` called exactly once - Non-subagent run (plain tool only) β `refreshThreads` NOT called Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(builtin-tool-claude-code): specialize Agent subagent Inspector + Render CC's subagent-spawn tool arrives as `tool_use.name: 'Agent'`, not `Task` β rename the apiName so the Inspector/Render registry actually matches the stream. Inspector switches icon/label by `subagent_type` (Explore / Plan / general-purpose / statusline-setup), with `description` surfaced in a chip; new Render shows `prompt` and tool_result as labelled Markdown blocks that can't fit in the folded header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π style(workflow-collapse): unify expand toggle with ActionIcon Replace the hand-rolled motion span + role="button" / keyboard-handler expand toggle with a single @lobehub/ui ActionIcon β fewer a11y edge cases to maintain and the icon/title/blockSize layout matches other toolbar buttons in the group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π style(builtin-tool-claude-code): inline-pad Edit diff container Give the Edit render a small inline padding so the CodeDiff lines up with the rest of the tool renders; zero-width flush-left was awkward against the surrounding labelled blocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): interpolate agent name in running indicator ContentLoading now renders "{name} is running" / "{name} θΏθ‘δΈ" for heterogeneous agent execution β previously it collapsed to the generic "External agent running" so a user watching a long CC run couldn't tell which external CLI was working (mattered once Codex landed as a sibling adapter). - Share `HETEROGENEOUS_TYPE_LABELS` (claude-code / codex) out of the heterogeneous-agents package so all consumers read one map; home Sidebar AgentItem switches to it and drops its inline copy. - `conversationLifecycle.startOperation` passes `metadata.heterogeneousType` on the heterogeneous-exec operation so ContentLoading can resolve the label from the running op without re-deriving the adapter type from session state. - New `operation.heterogeneousAgentFallback` key covers the (rare) case where the metadata is absent β keeps the dot loader labelled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(claude-code): CC subagent Thread rendering pipeline Closes the viewing loop for CC subagent runs: the main-topic Agent tool row now links into the spawned Thread, the Thread's Portal view renders with provenance + read-only affordances, and the sidebar surfaces which entries are subagent-produced. UX: - Agent render gains a trailing "View / Collapse full subagent conversation" toggle. It looks up the Thread by `metadata.sourceToolCallId === toolCallId` and calls openThreadInPortal / closeThreadPortal β hidden until the executor lazy-creates the Thread on the first subagent event, so it never renders as a no-op. - Portal Thread Header shows a `[icon] subagentType` Tag next to the title ("Explore" / "General purpose" / ...). Inspector's folded row already exposes the same detail, so the icon + label stays consistent across the two surfaces. - Portal Thread Chat flips into read-only mode when `metadata.sourceToolCallId` is set: ChatInput is hidden (the external CLI owns the session β new turns have nowhere to go), `disableEditing` propagates to every message (no double-click to edit, no user action bar), and `useThreadActionsBarConfig` wipes `bar` + `menu` across assistant / assistantGroup / user roles. - Sidebar ThreadItem on both /agent and /group routes renders a plain "Subagent" badge next to the title when `metadata.subagentType` is present. The type detail deliberately lives on the Thread Header, not here β sidebar space is tight. Shared resolver: - `CC_SUBAGENT_TYPES` + `resolveCCSubagentType` move out of the Inspector into `packages/builtin-tool-claude-code/src/client/ subagentTypes.ts` and re-export from the `/client` entry. Inspector + Portal Thread Header both consume it, so the icon/label stay in sync. Kept UI-level (LucideIcon | FC) rather than pushed into heterogeneous-agents, which is a pure-data package. - Root package.json adds a direct dep on `@lobechat/builtin-tool-claude-code` so Portal Thread Header can import from `/client` (previously only transitive via builtin-tools). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β test(workflow-collapse): mock @lobehub/ui ActionIcon + AccordionItem action slot After the expand-toggle refactor to ActionIcon + the `action` prop on AccordionItem, the test's module mocks were missing both: ActionIcon wasn't exported from the @lobehub/ui mock, and AccordionItem dropped `action` on the floor so the toggle never made it into the rendered DOM. Restore both β ActionIcon renders as a real \`button\` with aria-label so \`getByRole('button', { name })\` can still target it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
β¦4001) * β¨ feat(heterogeneous-agents): preserve CC subagent lineage in adapter Restores the CC subagent-lineage adapter work that was held back from #LOBE-7392 until the thread-router backend changes ship. This PR targets the LOBE-7392 branch so the adapter diff stays isolated from the thread/UI foundation β GitHub will auto-retarget to canary once LOBE-7392 merges. Original scope (unchanged from the held-back commits): - ToolCallPayload.parentToolCallId carries parent tool_use id downstream so consumers can group subagent inner tools under their spawning parent. - claudeCode.ts routes raw.parent_tool_use_id events through handleSubagentAssistant so the main-agent step tracker is not advanced on subagent message.id changes, usage is not double-counted, and subagent text / reasoning are dropped (their final answer flows back via the outer tool_result). - emitToolChunk helper shared by main-agent and subagent paths so new suppress-rules live in one place. - 6 subagent-lineage tests: lineage propagation, no newStep on subagent message.id change, no turn_metadata emission, text/reasoning drop, main-agent step boundary resumes after subagent, subagent tool_result passthrough. Refs LOBE-7319, LOBE-7260 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π style(workflow-collapse): move expand toggle to action slot Pass the fullscreen toggle as AccordionItem action so the built-in chevron indicator (same as TopicList) sits inline with the title on the left, with Maximize2/Minimize2 on the right. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): route CC Task tool_use to subagent Thread When a main-agent tool_use spawns a subagent, the executor now sync- allocates a threadId and creates a Thread, routing subsequent subagent inner tool_uses (tagged with `parentToolCallId` by the adapter) into that thread instead of the main assistant's tools[]. The "this tool_use spawns a subagent" decision lives entirely in the adapter layer via a new `ToolCallPayload.subagentSpawn` descriptor (`description`, `subagentType`). The CC adapter populates it on every `Task` tool_use; when Codex (or any other CLI) grows a subtask concept, its adapter populates the same field and the executor needs zero changes. The executor never checks `identifier === 'claude-code'` or `apiName === 'Task'` β it just reacts to the presence of `subagentSpawn`. - `ToolCallPayload.subagentSpawn?: { description?, subagentType? }` in `packages/heterogeneous-agents/src/types.ts` β adapter-agnostic spawn signal, paired with the existing `parentToolCallId` (which marks tool_uses BELONGING to a subagent). Together they cover both directions of the lineage. - `claudeCode.ts` stamps `subagentSpawn` on main-agent `Task` tool_uses using the already-parsed `block.input` β no redundant JSON.parse. - `ThreadService.createThread` helper wraps the sync-id TRPC mutation shipped in lobehub#14000. `generateThreadId()` mirrors the server's `idGenerator('threads', 16)` shape (`thd_<16 chars>`) so caller- provided ids match the schema pattern. - `persistNewToolCalls` splits fresh tools into main/subagent groups: Phase 1 (pre-register assistant.tools[]) and Phase 3 (backfill result_msg_id) run for main tools only. A new Phase 1b creates the Thread per `subagentSpawn` β guarded on `context.topicId` (required for Thread creation; missing falls back to normal tool rendering). Phase 2 writes tool messages for both groups, attaching `threadId` to subagent writes. Orphaned subagent events (parent spawn never registered) warn + drop instead of leaking into the main timeline. - `taskThreadMap` lives at executor scope (not on ToolPersistenceState which resets per step) so pathological orderings that straddle the main-agent step boundary can't lose the parentβthread mapping. 7 new tests: 2 adapter-level (subagentSpawn stamped on Task, NOT stamped on Read) + 5 executor-level (Thread creation, threadId propagation onto subagent tool messages, main assistant.tools[] isolation, orphan drop + warn, topicId-missing fallback). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(types): persist subagent lineage fields on ChatToolPayload schema Add `parentToolCallId` and `subagentSpawn` as first-class optional fields on `ChatToolPayload` + `ChatToolPayloadSchema`, so the adapter- emitted lineage metadata survives the TRPC `update-message` gate instead of being silently stripped by zod's default strip behavior. Reviewer-flagged bug: `UpdateMessageParamsSchema.tools` runs each payload through `ChatToolPayloadSchema`, which previously only whitelisted `apiName / arguments / id / identifier / intervention / result_msg_id / thoughtSignature / type`. Any adapter-level extension (subagent spawn marker, parent-child pointer) was dropped before it ever reached the `messages.tools` JSONB column, so lineage only lived in transient stream events and vanished on the first `tool_end β fetchAndReplaceMessages`. Downstream consumers that wanted to key off `tool.subagentSpawn` to render a TaskBlock, or follow `tool.parentToolCallId` to reconstruct the spawning parent, had nothing to work with. - `SubagentSpawnInfo` + `SubagentSpawnInfoSchema` defined in `packages/types/src/message/common/tools.ts` as the canonical shape. Structurally identical to the same-named type in `@lobechat/heterogeneous-agents` (which stays self-contained by design) β TypeScript structural typing handles the bridge. - Both new fields are optional on the interface and the zod schema, so existing callers continue to parse unchanged. - Jsonb column accepts any shape, so no DB migration β the only missing piece was the schema gate. 3 new regression tests next to the executor's subagent-thread-routing suite, asserting `ChatToolPayloadSchema.parse()` preserves both fields and the same fields survive through `UpdateMessageParamsSchema` (the actual TRPC gate that was stripping them before). Refs LOBE-7319 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "β¨ feat(types): persist subagent lineage fields on ChatToolPayload schema" This reverts commit 042e48c. * β»οΈ refactor(heterogeneous-agents): lift subagent context to event-peer fields `ToolCallPayload` is "one tool call" β it shouldn't carry stream-level lineage (parent spawn id, subagent turn id). That info describes the containing event/chunk and should live as a peer field on the event `data`, not nested inside each payload. Event model changes: - New `SubagentEventContext` + `SubagentSpawnMetadata` types. Events originating from a subagent stream (CC Task, future Codex subtask, etc.) carry `data.subagent` as a peer field next to `toolsCalling` / `toolCallId`. Covers `stream_chunk` (tools_calling), `tool_start`, `tool_end`, and `tool_result`. - `SubagentEventContext.spawnMetadata` appears ONLY on the first event for each new parent β lets the executor lazy-create the subagent Thread on first sight without needing to know CC-specific argument shapes or to re-parse `tool_use.input`. Subsequent events for the same parent carry just the lineage ids. - `ToolCallPayload` is back to its minimal form (`apiName / arguments / id / identifier / type`). No `parentToolCallId`, no `subagentSpawn` β those were the wrong abstraction level; removing them also sidesteps the `ChatToolPayloadSchema` strip-on-persist issue (the fields never need to survive DB roundtrip because Thread container persistence expresses the lineage). CC adapter (`claudeCode.ts`): - `handleSubagentAssistant` emits tools through a shared `emitToolChunk` that stamps the `subagent` peer field on the chunk + each tool_start. The FIRST subagent chunk for a new parent gets `spawnMetadata` pulled from a new adapter-internal `taskArgsById` cache β description / prompt / subagentType β announced exactly once via `announcedSpawns`. - `handleUser` stamps `subagent.parentToolCallId` on `tool_result` + `tool_end` when the user event carries `parent_tool_use_id` (CC's shape for subagent inner tool_results). - Main-agent tool_use handling no longer stamps lineage on payloads. Adapter tests updated β 4 rewrites in the subagent suite: - assert chunk-level peer fields (not payload-nested lineage) - assert `spawnMetadata` on first subagent event, absent on subsequent - assert main-agent tool_uses don't get `subagent` context - assert subagent `tool_result` + `tool_end` carry the peer 59 adapter tests pass (52 existing + 7 covering the new peer contract). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): persist subagent runs as Thread containers Subagents now materialize as a nested conversation inside a Thread, shaped identically to the main topic: Thread ββ user (content = Task prompt, threadId=thread.id) ββ assistant#1 (tools[] = subagent turn 1 tool_uses, threadId) ββ tool (parentId=assistant#1, threadId) ββ assistant#2 (tools[] = subagent turn 2 tool_uses, threadId) ββ tool (parentId=assistant#2, threadId) Same schema as a main topic, just rooted at a Thread instead of a Topic. No new persistence shape, no new renderer β the existing `query({ threadId })` read path reconstructs the subagent's full conversation when the UI expands the TaskBlock. Executor changes: - `ToolPersistenceState` shrinks to `{ payloads, persistedIds }` β the `tool_use.id β tool message DB id` map moves to executor scope as one global `toolMsgIdByCallId` shared across main + every subagent run. `tool_result` lookups don't care which scope created the row. - `persistNewToolCalls` β renamed `persistToolBatch` and made scope- agnostic (takes an optional `threadId` + the global id map). Runs the same 3-phase flow (pre-register β create β backfill) whether target is main assistant or in-thread subagent assistant. - New `persistSubagentToolChunk` handles the subagent path: reads the adapter's `SubagentEventContext` peer field off the chunk, lazy- creates the Thread + user message on the FIRST chunk for each parent (using `spawnMetadata`), opens a new in-thread assistant on `subagentMessageId` change (same shape as main-agent step boundary), then delegates to `persistToolBatch`. - `SubagentRunState` tracks per-parent Thread id, current in-thread assistant, `currentSubagentMessageId`, chain parent, and its own `ToolPersistenceState`. Lives at executor scope so subagent events straddling a main-agent step boundary keep their mapping. - Step-boundary parent lookup reads from `toolState.payloads` (not the global id map) so main-agent chain doesn't accidentally pick up a subagent tool's msg id as the step parent. - Executor has NO CC-specific knowledge β it never checks `identifier`, `apiName`, or parses `tool_use.arguments`. All CC quirks live in the adapter; new CLIs (Codex subtask, ...) plug in by emitting the same `SubagentEventContext` peer. Test rewrite β 6 tests under "CC subagent thread-container": - Task tool_use alone does NOT create a Thread (lazy) - First subagent event creates Thread + `role:'user'` seeded with the Task prompt + first in-thread `role:'assistant'` - Subagent inner tools persist as `role:'tool'` messages with threadId set and parentId chained to the in-thread assistant - `subagentMessageId` change opens a new in-thread assistant - Main `assistant.tools[]` carries Task only; subagent inner tools appear on the in-thread assistant's `tools[]` - Missing topicId gracefully skips Thread creation 25 executor tests pass (19 existing + 6 rewritten for new shape). Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): subagent prompt + closing summary in Thread view Electron E2E surfaced two gaps in the Thread-container model shipped in the previous commit: 1. **Subagent user-message content empty.** Real CC emits `Agent` as the spawn-tool name for general-purpose subagents (not only `Task` as the spec documents). My earlier `taskArgsById` cache keyed off `ClaudeCodeApiName.Task` only, so `spawnMetadata.prompt` was undefined when the user watched the actual app β the Thread's `role:'user'` message landed with empty content and the thread view looked like a tool call floating alone. 2. **No closing summary in the Thread.** The adapter dropped subagent text/reasoning per an earlier comment claiming the subagent's final answer arrives via the outer tool_result. That's true for the MAIN timeline (the outer spawn tool's result content = the subagent's summary), but the THREAD view is a standalone conversation β dropping the subagent's final text left it ending on a bare tool call with no assistant conclusion. Adapter changes (`claudeCode.ts`): - Rename `taskArgsById` β `mainToolInputsById` and cache EVERY main-agent tool_use input (not just `Task`). `emitToolChunk` looks up the parent's input by `parent_tool_use_id` on the first subagent event and extracts `description` / `prompt` / `subagent_type` defensively β any CC spawn-tool variant that shares this input shape (`Task`, `Agent`, future ones) gets spawn metadata for free. - `handleSubagentAssistant` stops filtering `tool_use` only. Text and `thinking` blocks now emit as `stream_chunk` events with the `subagent` peer field attached β routed to the in-thread assistant, NOT the main assistant's accumulators. Executor changes (`heterogeneousAgentExecutor.ts`): - `SubagentRunState` gains `accumulatedContent` + `accumulatedReasoning`, mirroring main-agent content tracking. - Extract `ensureSubagentRun` helper so text chunks and tool chunks share the Thread / user / assistant lifecycle logic. On turn boundary (`subagentMessageId` change), flush the prior turn's accumulated content before creating the next in-thread assistant β covers text-only turns that never hit `persistToolBatch`. - New `persistSubagentTextChunk` accumulates text/reasoning onto the run; `persistToolBatch` writes content alongside tools[] so DB sees both in one update (same pattern as main agent). - New `finalizeSubagentRun` flushes pending content when the main- agent receives the spawn tool's `tool_result` β ensures the closing summary lands before `fetchAndReplaceMessages` refreshes from stale DB state. - `onComplete` iterates `subagentRuns.keys()` and flushes any un-finalized runs, covering the CLI-crashed-mid-subagent edge case. Tests: - Adapter: replaced the "drops subagent text" test with two tests asserting text/reasoning ARE emitted with correct `subagent` peer context. New test covers the `Agent` spawn-tool variant. - Executor: 4 new tests cover the Thread user message content population, subagent text accumulation into the in-thread assistant, non-leakage into main assistant content, and tool_result-triggered finalization. Total 29 executor tests pass. E2E verified via Electron + CDP: fresh CC session β `Agent`-based subagent β Thread created with `title="Run pwd command"`, `metadata.subagentType="general-purpose"`, `role:'user'` seeded with the Task prompt, Bash tool_use + result inside the thread. Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π fix(heterogeneous-agents): refresh thread list when subagent Thread is lazy-created Earlier Electron E2E repro: a subagent Thread born mid-stream landed in DB correctly, but the topic sidebar only picked it up after the user manually navigated topics / called `refreshThreads()` β the SWR cache for the thread list (`SWR_USE_FETCH_THREADS`) wasn't invalidated, so the new Thread stayed invisible until the next cold fetch. - `ensureSubagentRun` now accepts an optional `onThreadCreated` callback fired once per lazy Thread create. Kept as a callback (not a direct `store.refreshThreads` call) so the executor persistence logic stays decoupled from the Zustand store shape. - `persistSubagentToolChunk` + `persistSubagentTextChunk` thread the callback through to `ensureSubagentRun`. - Executor defines `onSubagentThreadCreated` once at run scope and passes it into all three subagent persist call sites. Calls `get().refreshThreads()` fire-and-forget β it's a no-op when the user has navigated away from the topic, so no need to block persist on cache refresh. Two regression tests: - Subagent-spawning run β `refreshThreads` called exactly once - Non-subagent run (plain tool only) β `refreshThreads` NOT called Refs LOBE-7319, LOBE-7392 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(builtin-tool-claude-code): specialize Agent subagent Inspector + Render CC's subagent-spawn tool arrives as `tool_use.name: 'Agent'`, not `Task` β rename the apiName so the Inspector/Render registry actually matches the stream. Inspector switches icon/label by `subagent_type` (Explore / Plan / general-purpose / statusline-setup), with `description` surfaced in a chip; new Render shows `prompt` and tool_result as labelled Markdown blocks that can't fit in the folded header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π style(workflow-collapse): unify expand toggle with ActionIcon Replace the hand-rolled motion span + role="button" / keyboard-handler expand toggle with a single @lobehub/ui ActionIcon β fewer a11y edge cases to maintain and the icon/title/blockSize layout matches other toolbar buttons in the group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * π style(builtin-tool-claude-code): inline-pad Edit diff container Give the Edit render a small inline padding so the CodeDiff lines up with the rest of the tool renders; zero-width flush-left was awkward against the surrounding labelled blocks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(heterogeneous-agents): interpolate agent name in running indicator ContentLoading now renders "{name} is running" / "{name} θΏθ‘δΈ" for heterogeneous agent execution β previously it collapsed to the generic "External agent running" so a user watching a long CC run couldn't tell which external CLI was working (mattered once Codex landed as a sibling adapter). - Share `HETEROGENEOUS_TYPE_LABELS` (claude-code / codex) out of the heterogeneous-agents package so all consumers read one map; home Sidebar AgentItem switches to it and drops its inline copy. - `conversationLifecycle.startOperation` passes `metadata.heterogeneousType` on the heterogeneous-exec operation so ContentLoading can resolve the label from the running op without re-deriving the adapter type from session state. - New `operation.heterogeneousAgentFallback` key covers the (rare) case where the metadata is absent β keeps the dot loader labelled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β¨ feat(claude-code): CC subagent Thread rendering pipeline Closes the viewing loop for CC subagent runs: the main-topic Agent tool row now links into the spawned Thread, the Thread's Portal view renders with provenance + read-only affordances, and the sidebar surfaces which entries are subagent-produced. UX: - Agent render gains a trailing "View / Collapse full subagent conversation" toggle. It looks up the Thread by `metadata.sourceToolCallId === toolCallId` and calls openThreadInPortal / closeThreadPortal β hidden until the executor lazy-creates the Thread on the first subagent event, so it never renders as a no-op. - Portal Thread Header shows a `[icon] subagentType` Tag next to the title ("Explore" / "General purpose" / ...). Inspector's folded row already exposes the same detail, so the icon + label stays consistent across the two surfaces. - Portal Thread Chat flips into read-only mode when `metadata.sourceToolCallId` is set: ChatInput is hidden (the external CLI owns the session β new turns have nowhere to go), `disableEditing` propagates to every message (no double-click to edit, no user action bar), and `useThreadActionsBarConfig` wipes `bar` + `menu` across assistant / assistantGroup / user roles. - Sidebar ThreadItem on both /agent and /group routes renders a plain "Subagent" badge next to the title when `metadata.subagentType` is present. The type detail deliberately lives on the Thread Header, not here β sidebar space is tight. Shared resolver: - `CC_SUBAGENT_TYPES` + `resolveCCSubagentType` move out of the Inspector into `packages/builtin-tool-claude-code/src/client/ subagentTypes.ts` and re-export from the `/client` entry. Inspector + Portal Thread Header both consume it, so the icon/label stay in sync. Kept UI-level (LucideIcon | FC) rather than pushed into heterogeneous-agents, which is a pure-data package. - Root package.json adds a direct dep on `@lobechat/builtin-tool-claude-code` so Portal Thread Header can import from `/client` (previously only transitive via builtin-tools). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * β test(workflow-collapse): mock @lobehub/ui ActionIcon + AccordionItem action slot After the expand-toggle refactor to ActionIcon + the `action` prop on AccordionItem, the test's module mocks were missing both: ActionIcon wasn't exported from the @lobehub/ui mock, and AccordionItem dropped `action` on the floor so the toggle never made it into the rendered DOM. Restore both β ActionIcon renders as a real \`button\` with aria-label so \`getByRole('button', { name })\` can still target it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Full Layer 2 implementation of LOBE-7260 β CC subagent runs now materialize as a Thread that is structurally identical to a main topic (user β assistant with tools[] β tool β β¦), recursively. No new message roles, no
role:'task'placeholder on the main timeline β just the existingthreadstable hosting a nested conversation.Tracked by LOBE-7492 (Layer 2 sub-issue under LOBE-7260 documenting the shipped design + divergence from original plan).
What's in this PR
Adapter (
packages/heterogeneous-agents) β subagent context lives as event-peer fields on chunkdata, never nested insideToolCallPayload:SubagentEventContext+SubagentSpawnMetadataexported from the packagedescription/prompt/subagent_typefrom whichever spawn-tool variant was used (Task,Agent, future)handleSubagentAssistantemits text / reasoning / tool_use β all tagged withsubagentpeer so the executor routes them to the in-thread assistant instead of the main bubbleExecutor (
src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts) β Thread as a recursive container:persistToolBatchshared helper runs the existing 3-phase main flow for both main and subagent scopes (threadId optional)ensureSubagentRunlazy-creates the Thread on the first subagent event (seedsrole:'user'from the Task prompt, opens the first in-thread assistant, triggersstore.refreshThreads()so the sidebar updates immediately)subagentMessageIdflushes the prior turn's content and opens a new in-thread assistantfinalizeSubagentRunfires when the main agent receives the spawn tool'stool_result, ensuring the in-thread assistant's final text lands in DB beforefetchAndReplaceMessagesonCompletefallback flush for un-finalized runs (CLI crash mid-subagent)E2E verified in Electron
Real CC session:
θ―·η¨ Task tool ε§ζδΈδΈͺ subagent ζ§θ‘ pwd ε½δ»€...β Threadthd_...created withtitle="Run pwd command", seededrole:'user'with the Task prompt, in-threadrole:'assistant'with Bash tool + its output. Sidebar auto-refreshes mid-stream.What's NOT in this PR (Layer 3 β separate work)
ContentBlock.tsx(to show the outer Agent tool_use as a compact subagent-spawn card instead of a plain tool row)Test plan
Refs LOBE-7319, LOBE-7260, LOBE-7492
π€ Generated with Claude Code