Skip to content

✨ feat(heterogeneous-agent): support CC subagent rendering#14001

Merged
arvinxx merged 15 commits into
canaryfrom
feat/lobe-7319-cc-adapter-subagent-lineage
Apr 21, 2026
Merged

✨ feat(heterogeneous-agent): support CC subagent rendering#14001
arvinxx merged 15 commits into
canaryfrom
feat/lobe-7319-cc-adapter-subagent-lineage

Conversation

@arvinxx

@arvinxx arvinxx commented Apr 20, 2026

Copy link
Copy Markdown
Member

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 existing threads table 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 chunk data, never nested inside ToolCallPayload:

  • SubagentEventContext + SubagentSpawnMetadata exported from the package
  • CC adapter caches every main-agent tool_use input and pulls description / prompt / subagent_type from whichever spawn-tool variant was used (Task, Agent, future)
  • handleSubagentAssistant emits text / reasoning / tool_use β€” all tagged with subagent peer so the executor routes them to the in-thread assistant instead of the main bubble
  • 61 adapter tests pass

Executor (src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts) β€” Thread as a recursive container:

  • persistToolBatch shared helper runs the existing 3-phase main flow for both main and subagent scopes (threadId optional)
  • ensureSubagentRun lazy-creates the Thread on the first subagent event (seeds role:'user' from the Task prompt, opens the first in-thread assistant, triggers store.refreshThreads() so the sidebar updates immediately)
  • Turn boundaries mirror main-agent step handling β€” new subagentMessageId flushes the prior turn's content and opens a new in-thread assistant
  • finalizeSubagentRun fires when the main agent receives the spawn tool's tool_result, ensuring the in-thread assistant's final text lands in DB before fetchAndReplaceMessages
  • onComplete fallback flush for un-finalized runs (CLI crash mid-subagent)
  • 31 executor tests pass

E2E verified in Electron

Real CC session: 请用 Task tool ε§”ζ‰˜δΈ€δΈͺ subagent ζ‰§θ‘Œ pwd 命什... β†’ Thread thd_... created with title="Run pwd command", seeded role:'user' with the Task prompt, in-thread role:'assistant' with Bash tool + its output. Sidebar auto-refreshes mid-stream.

What's NOT in this PR (Layer 3 β€” separate work)

  • Inline TaskBlock renderer in the main topic's ContentBlock.tsx (to show the outer Agent tool_use as a compact subagent-spawn card instead of a plain tool row)
  • Thread-status lifecycle on the DB side (Completed / Cancel / Failed transitions on finalize / stop / error)

Test plan

  • 61 adapter tests + 31 executor tests
  • Full type-check clean
  • Electron E2E: real CC session with subagent spawn β†’ Thread persisted correctly, sidebar auto-refreshes
  • Follow-up: Layer 3 inline TaskBlock renderer

Refs LOBE-7319, LOBE-7260, LOBE-7492

πŸ€– Generated with Claude Code

@vercel

vercel Bot commented Apr 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lobehub Ready Ready Preview, Comment Apr 21, 2026 8:56am

Request Review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ’‘ 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".

Comment thread packages/heterogeneous-agents/src/adapters/claudeCode.ts Outdated
Comment thread packages/heterogeneous-agents/src/types.ts Outdated
Base automatically changed from feat/lobe-7392-cc-task-block to canary April 20, 2026 17:27
@arvinxx arvinxx force-pushed the feat/lobe-7319-cc-adapter-subagent-lineage branch from 743431b to 7b54493 Compare April 20, 2026 17:33
@codecov

codecov Bot commented Apr 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.38739% with 42 lines in your changes missing coverage. Please review.
βœ… Project coverage is 66.82%. Comparing base (595193c) to head (2907db2).
⚠️ Report is 6 commits behind head on canary.

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               
Flag Coverage Ξ”
app 59.43% <87.38%> (?)
database 92.25% <ΓΈ> (ΓΈ)
packages/agent-runtime 79.72% <ΓΈ> (ΓΈ)
packages/context-engine 83.18% <ΓΈ> (ΓΈ)
packages/conversation-flow 92.40% <ΓΈ> (ΓΈ)
packages/file-loaders 87.02% <ΓΈ> (ΓΈ)
packages/memory-user-memory 74.74% <ΓΈ> (ΓΈ)
packages/model-bank 99.86% <ΓΈ> (ΓΈ)
packages/model-runtime 84.22% <ΓΈ> (ΓΈ)
packages/prompts 69.08% <ΓΈ> (ΓΈ)
packages/python-interpreter 92.90% <ΓΈ> (ΓΈ)
packages/ssrf-safe-fetch 0.00% <ΓΈ> (ΓΈ)
packages/utils 87.95% <ΓΈ> (ΓΈ)
packages/web-crawler 88.66% <ΓΈ> (ΓΈ)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Ξ”
Store 66.40% <86.30%> (βˆ…)
Services 51.73% <33.33%> (βˆ…)
Server 66.77% <ΓΈ> (βˆ…)
Libs 51.32% <ΓΈ> (βˆ…)
Utils 80.59% <ΓΈ> (-12.89%) ⬇️
πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@arvinxx arvinxx changed the title ✨ feat: CC subagent lineage in heterogeneous-agents adapter ✨ feat: CC subagent rendering via Thread container (LOBE-7492) Apr 21, 2026
const subAssistantMsg = mockCreateMessage.mock.calls.find(
([p]: any) => p.role === 'assistant' && p.threadId === threadId,
);
const subAssistantId = subAssistantMsg?.[0];
arvinxx and others added 10 commits April 21, 2026 15:38
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>
…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>
arvinxx and others added 4 commits April 21, 2026 15:38
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>
@arvinxx arvinxx force-pushed the feat/lobe-7319-cc-adapter-subagent-lineage branch from ad8e629 to ab78b38 Compare April 21, 2026 07:39
@arvinxx arvinxx changed the title ✨ feat: CC subagent rendering via Thread container (LOBE-7492) ✨ feat: support CC subagent rendering Apr 21, 2026
@arvinxx arvinxx changed the title ✨ feat: support CC subagent rendering ✨ feat(heterogeneous-agent): support CC subagent rendering Apr 21, 2026
…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>
@arvinxx arvinxx merged commit b02b727 into canary Apr 21, 2026
35 checks passed
@arvinxx arvinxx deleted the feat/lobe-7319-cc-adapter-subagent-lineage branch April 21, 2026 09:48
arvinxx added a commit that referenced this pull request Apr 27, 2026
# πŸš€ 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
github-actions Bot added a commit to techflowasia/verve-lobe-client that referenced this pull request May 18, 2026
…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>
github-actions Bot added a commit to techflowasia/verve-lobe-client that referenced this pull request May 18, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant