- Updated API names for clarity, changing 'patchDocument' to 'modifyNodes'.
- Introduced LiteXML operation schema for document modifications.
- Implemented new mutation for modifying document nodes via LiteXML.
- Enhanced document retrieval methods to support format options (XML, Markdown, Both).
- Added support for editor data snapshots and normalization of diff nodes.
- Improved document history management to handle editor data with diff nodes.
- Created tests for new features and ensured existing functionality remains intact.
Signed-off-by: Innei <tukon479@gmail.com>
- Introduced ActiveTopicDocumentContextInjector to inject context for active topic documents into user messages.
- Added DisabledToolCallFilter to remove historical tool calls for disabled tools in the current runtime scope.
- Updated MessagesEngine to utilize the new context injectors and filters.
- Enhanced tests to verify the correct injection of active topic document context and filtering of disabled tool calls.
This update improves the handling of document editing contexts and tool management in the conversation flow.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(heterogeneous-agents): stream subagent Thread + fix parallel-tool orphan
When a main-agent step emits a parallel tool_use (e.g. `[Grep, Agent]`),
the gateway handler's stream_chunk branch was forwarding the subagent's
inner `tools_calling` chunks onto `currentAssistantMessageId` (main),
overwriting main.tools[] with subagent tools — main's own Task/Agent
tool_use then had no matching entry and every tool message under it
rendered with the "orphan tool call" banner.
Two coordinated changes:
1. Main-bucket isolation: the executor now drops subagent-tagged
`stream_chunk` events before forwarding to the gateway handler. DB
persistence continues via `persistSubagent*Chunk` so the subagent
content is never lost; only the main-handler in-memory dispatch is
suppressed for subagent chunks.
2. Thread-bucket streaming: `internal_dispatchMessage` now accepts a
`threadId` override that snaps scope to `thread`, routing
create/update payloads to the thread's `messagesMap` bucket. Each
`SubagentRunState` carries a thread-scoped dispatcher; ensureSubagentRun
seeds user + assistant on lazy Thread creation and at turn boundaries,
persistToolBatch gets an `onToolCreated` hook that the subagent path
uses to seed role:'tool' rows, persistSubagent*Chunk dispatches
tools[] / content / reasoning updates on every chunk, and the
tool_result branch mirrors subagent tool_result content (+ pluginState)
into the thread bucket. Thread view now streams token-by-token with
the same cadence as the main bubble.
Tests:
- `does NOT forward subagent-tagged stream_chunks to the gateway handler`
— asserts main bucket isolation under parallel main+subagent tool use.
- `streams subagent create/update dispatches into the thread messagesMap
bucket` — asserts user/assistant/tool createMessage dispatches land in
the thread scope, plus streaming updateMessage for tools[], content,
and tool_result, with no bleed into the main bucket.
Local repro verified end-to-end: main assistant.tools=[Grep, Agent]
stays intact across two parallel runs, thread bucket populates 14 rows
(user + 2 subagent assistants with Bash/Glob then Read×8 + 10 tool
results) during the run, `mainOrphans`/`threadOrphans`/
`threadIntoMainBleed` all empty, orphan warning DOM count = 0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(heterogeneous-agents): route subagent stream through a per-spawn sub-operation
Replace the threadId-override on `internal_dispatchMessage` with a
proper per-spawn child operation, eliminating the second context
expression at the dispatch boundary.
The previous design accepted `{ operationId, threadId? }` and snapped
scope to `'thread'` when the override was present. That was a leaky
parallel path to the operation registry — the same "which messagesMap
bucket should this dispatch hit?" question got answered two different
ways. `startOperation` already supports `parentOperationId` + context
inheritance + recursive cancel cascade, so the right move is to model
the subagent run as a first-class child op and let
`internal_getConversationContext` do its normal job.
Changes:
- Add `'subagentThread'` to `OperationType` (NOT in
`AI_RUNTIME_OPERATION_TYPES` — it's a context container, not an
independent loading state, so it shouldn't double-count for spinners).
- `executeHeterogeneousAgent` opens the sub-op in `beginSubagentRun`
via `startOperation({ type: 'subagentThread', parentOperationId,
context: { ...context, threadId, scope: 'thread' } })` and binds a
thread-scoped dispatcher to that sub-op's id.
- `SubagentRunState.subOperationId` carries the id so `finalizeSubagentRun`
can mark it completed when the spawn's tool_result arrives (or on the
`onComplete` fallback for crash/abort paths). Cancel cascade + cleanup
flow through the existing parent/child op linkage.
- Revert the `threadId` override in `internal_dispatchMessage` — the
store boundary is back to a single context expression
(`{ operationId? }`).
Test:
- Add `startOperation` mock to `createMockStore` (returns monotonic
`sub-op-N` ids).
- Update the streaming regression to identify the sub-op via the
`startOperation` call with `type: 'subagentThread'`, assert the
sub-op's parent + context shape, filter Thread bucket dispatches by
`ctx.operationId === subOperationId`, and verify
`completeOperation(subOperationId)` fires when the run finalizes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agents): drain subagent buffers only after DB flush confirms
`finalizeSubagentRun`'s buffer reset used to run unconditionally after
the flush try/catch, so a transient `messageService.updateMessage`
failure silently wiped the accumulated streamed text/reasoning — the
later `onComplete` fallback then had nothing left to retry, leaving the
subagent's streamed content absent from persisted thread history.
Move the clear into the success branch. A second concern surfaces once
the clear moves: after the flush block, the `resultContent` branch
advances `currentAssistantMsgId` to the newly created terminal
assistant, so a naive retry that reads `currentAssistantMsgId` would
overwrite the authoritative terminal content with the leftover streamed
buffer — corrupting the subagent summary with stale partial text.
Pin the flush target via a new `SubagentRunState.pendingFlushTarget`:
captured before the DB attempt, carried on the run when the flush
fails, cleared alongside the buffers on success. The retry uses the
pinned target instead of the live `currentAssistantMsgId`, so leftover
streamed buffers always land on the streaming turn's assistant — never
on the terminal row.
Test: `retains subagent buffers + pinned target when the finalize flush
fails` stubs `updateMessage` to throw once for the subagent streaming
write, runs streamed text → spawn `tool_result` → `onComplete`, and
asserts (1) the leftover content eventually reaches DB across ≥2
write attempts and (2) every attempt targets the streaming turn's
assistant — not the terminal row created by `resultContent`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ 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 #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 042e48c733.
* ♻️ 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>
* ✨ feat(topic): add completed status with dropdown action and filter
- Surface ChatTopicStatus (active/completed/archived) on topic list items and pass to dropdown menu
- Add markTopicCompleted / unmarkTopicCompleted store actions wired into the topic item dropdown
- Show CheckCircle2 icon on completed topics in the sidebar list
- Add topicIncludeCompleted user preference (default false) and an "Include Completed" toggle in the topic filter menu (agent + group routes)
- Wire excludeStatuses and triggers filters through TopicModel, TRPC router, service, and store SWR keys so completed topics are excluded by default
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🌐 i18n(topic): add zh-CN/en-US for completed status keys
Translate actions.markCompleted / actions.unmarkCompleted and filter.filter / filter.showCompleted for dev preview. CI's pnpm i18n will fill in remaining locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(topic): scope completed exclusion to routes with the toggle
Move the topicIncludeCompleted preference read out of the chat-store useFetchTopics action and into the (main) agent/group sidebars where the "Include Completed" filter actually lives. Popup and mobile topic views call useFetchTopics without excludeStatuses, so completed topics remain reachable on surfaces that don't expose the toggle (e.g. the popup window for a deep-linked completed topic, the mobile TopicModal).
Also switch ChatTopicStatus imports in the topic item / dropdown files to @lobechat/types to match the rest of the topic-feature imports.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(topic-model): cover excludeStatuses + triggers filters
Add cases to the TopicModel.query suite for the new params introduced alongside the topic.status column:
- triggers (positive trigger filter) on the container branch
- excludeStatuses on the container, agent, and groupId branches (verifies null status rows are still returned)
- status / completedAt are populated on returned items
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(topic): move "Mark Completed" to top of agent topic dropdown
Promote the completed-status toggle to the first menu item, with a divider before favorite, so the most-used status action sits at the top of the dropdown.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When users say "daily task" or "routine", the model confused lobe-gtd (one-time todos) with lobe-cron (recurring automation), often falling back to user-memory or GTD instead of cron.
Fixes LOBE-7486
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: inject timezone and cron jobs list into cron tool system prompt
Add {{timezone}} to cron systemRole session_context so the model knows
the user's local timezone when creating scheduled tasks. Wire up the
{{CRON_JOBS_LIST}} placeholder that was already referenced in the
systemRole but never populated — now fetches the agent's existing cron
jobs via tRPC and injects them, following the same pattern as CREDS_LIST.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: limit cron jobs context to 4 items to save context window
Only inject a preview of up to 4 cron jobs into the system prompt.
When there are more, append a hint directing the model to call
listCronJobs API for the full list. This avoids bloating the context
window for agents with many scheduled tasks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: fallback to skill activation when activateTools cannot find identifier
When an LLM calls activateTools with a skill identifier (e.g. "lobehub"),
the tool lookup fails with "Not found" because skills and tools are separate
registries. Now activateTools falls back to activateSkill for identifiers
not found as tools, so skills can be activated regardless of which API the
LLM chooses to call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: fallback to skill activation when activateTools cannot find identifier
When an LLM calls activateTools with a skill identifier (e.g. "lobehub"),
the tool lookup fails because skills and tools are separate registries.
Two changes:
1. ActivatorExecutionRuntime.activateTools() now falls back to activateSkill
for identifiers not found as tools
2. selectActivatedSkillsFromMessages() now also extracts skills from
activateTools messages (pluginState.activatedSkills[]), so downstream
stepContext and execScript zip resolution work correctly
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clear residual list-container margin/border when collapsed and slightly
increase bottom padding so the header sits on the bar's visual center.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(models): update AI models with new capabilities and pricing adjustments
* ✨ feat(aiModels): add new AI models Kimi K2.6 and GLM-5.1 to ollamaCloud; enhance siliconCloud with Qwen3.6 35B A3B and update pricing and settings
* ✨ feat(heterogeneous-agents): preserve CC subagent lineage in adapter
Claude Code tags subagent events (Agent / Task tool spawns) with
parent_tool_use_id pointing back at the outer tool_use. The adapter
used to flatten these, breaking the main-agent step tracker — each
subagent turn introduces a NEW message.id, which the adapter read as
"new main-agent step" and forced stream_end + stream_start(newStep),
producing orphan assistant bubbles and double-counted usage.
- ToolCallPayload.parentToolCallId carries the pointer to downstream
consumers so they can group subagent inner tools under their parent.
- claudeCode.ts reads raw.parent_tool_use_id and:
* skips main-agent step boundary on subagent message.id changes
* skips model tracking for subagent events (the result event has
the authoritative usage, would double-count otherwise)
* drops subagent text / reasoning in this adapter pass — the
subagent's final answer is delivered via the outer tool_result;
verified against a real CC trace where 76 subagent assistant
events carried only tool_use, zero text / thinking
* stamps parentToolCallId onto subagent tool_use payloads
- 6 new unit tests cover lineage propagation, no newStep for subagent
message.id changes, no turn_metadata emission, text/reasoning drop,
main-agent resuming step boundary, and subagent tool_result
passthrough.
Refs LOBE-7319, LOBE-7260
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(types): foundation types for CC Task block (LOBE-7392)
Sets up the data shape for rendering CC subagent spawns as inline
`task` blocks inside the parent assistantGroup, replacing the
role:'task' message intermediary that was previously proposed in
PR #13928. Pure data layer — no DB schema migration, no new
columns.
- TaskBlock + AssistantContentBlock.tasks?: derived view that the
MessageTransformer will populate by joining Threads onto the
parent message's tool_use entries (follow-up commit). Carries
threadId, subagentType, description, status — enough for the
folded inline header without re-fetching the thread on every
render pass.
- ThreadMetadata gains sourceToolCallId, subagentType, description.
sourceToolCallId disambiguates parallel subagents that share a
sourceMessageId (one assistant turn can spawn multiple Task
tool_uses in one batch).
- CreateThreadParams.id + zod schema field + thread router
passthrough lets clients allocate the threadId synchronously
before the create mutation resolves. The CC adapter emits
Task tool_use synchronously while the create call is async, so
having the id up-front lets us persist subagent inner messages
with the right threadId without a queue or blocking the stream.
- ClaudeCodeApiName.Task + TaskArgs match the CC tool_use shape
(description, prompt, subagent_type) so executor / renderer can
type the input safely.
Refs LOBE-7392
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: extract subagent assistant handler + drop ThreadMetadata.description
Two review-feedback cleanups on the LOBE-7392 foundation:
1. **Adapter — early-return + shared helper.** The main-agent path no
longer carries `if (!isSubagentEvent)` guards; subagent events short-
circuit into a dedicated `handleSubagentAssistant` that only extracts
`tool_use` blocks, and both paths share a new `emitToolChunk` helper
for the `tools_calling` + `tool_start` emission. Adding a new
subagent suppress-rule (no model / no text / no step) now lives in
one method instead of sprinkling guards across the main handler.
2. **ThreadMetadata — drop `description`, use `Thread.title`.** Thread
already has a `title` column; storing the CC Task `description`
input there is the canonical spot and removes the redundant metadata
field. `TaskBlock.description` is collapsed into `TaskBlock.title`
(single source), and the MessageTransformer will populate it from
`thread.title` at read time. Also adds `status?: ThreadStatus` on
`TaskBlock` so the renderer gets the processing / completed / failed
state without a separate lookup.
Behavior unchanged — all 56 adapter tests still pass.
Refs LOBE-7392, LOBE-7319
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(thread-router): translate id-collision into CONFLICT error
ThreadModel.create uses onConflictDoNothing() and returns undefined
when a caller-provided id collides with an existing row. With the
new client-side id passthrough (introduced in 16d73261f9 to let the
CC subagent executor allocate threadId synchronously), the original
router would silently insert a follow-up message with
threadId: undefined and return { threadId: undefined } — a data-
integrity regression flagged in PR review.
Translates the model's undefined return into TRPCError(CONFLICT) at
the router boundary so callers see an explicit error and can
regenerate their id and retry. The model layer is untouched —
onConflictDoNothing remains the right primitive for server-generated
ids where collisions are unreachable; the new validation only
applies when the router is the entry point.
- ensureThreadCreated helper extracted; both createThread and
createThreadWithMessage routes funnel through it
- New thread model tests document the conflict behavior and
caller-provided id passthrough that the router relies on (16/16
pass)
Refs LOBE-7392
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 feat(chat-minimap): user-message peek with in-place hover preview
- Filter ticks to user messages; fall back to last user when viewport is on assistant reply
- Replace per-tick popovers with one in-place panel that crossfades from rail center
- Drop arrow nav buttons (hover panel makes them redundant)
- Smooth sqrt width curve (5–16px) so short messages cluster naturally
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(claude-code-todo): chip-style detail in inspector, plain header in render
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⏪ revert(heterogeneous-agents): pull CC adapter subagent-lineage changes
The CC subagent-lineage adapter work (parent_tool_use_id routing,
parentToolCallId on ToolCallPayload, dedicated handleSubagentAssistant /
emitToolChunk helpers, 6 subagent tests) would ship before the thread
backend changes in this PR are deployed — online flows would see the new
payload field with no server to receive it.
Holding this PR to thread-router + foundation types only. The adapter
work is preserved on feat/lobe-7392-cc-adapter-followup and will ship
as a separate PR after this one is deployed.
Refs LOBE-7392, LOBE-7319
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(electron): use colorBgElevated for active title-bar tab
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 fix(bot): show operation id instead of raw error in IM failure reply
Replace the error message content in bot-facing failure replies with the
operation id so end users don't see raw runtime errors; errors are still
logged server-side for debugging and correlation via operation id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): extract tool_name from ToolSearch tool_reference blocks
CC CLI returns ToolSearch results as `tool_reference` content blocks with
only a `tool_name` field — no `text`/`content` — so the generic array
mapper collapsed every entry to '' and persisted empty content, keeping
the UI tool StatusIndicator stuck on the spinner (LOBE-7369).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When businessMenuItems (from cloud deployment) returns items that
include a trailing divider, and getDesktopApp prepends its own divider,
two dividers appear back-to-back between Credits and Get Desktop App.
Add a post-filter on mainItems that strips any consecutive divider,
regardless of which module injected them.
- Add nested /agent/:aid/:topicId/page/:docId route with PageRedirect for bare /page
- Introduce useAutoCreateTopicDocument with module-level inflight de-dup
- Lift Portal + WorkingSidebar to (chat) layout; keep ChatHeader in left column
- Sidebar document clicks on page route navigate to /page/:docId instead of opening Portal
- Add HeaderSlot (context + createPortal) as a reusable header injection point
- Mount AutoSaveHint via HeaderSlot; register Files hotkey scope in TopicCanvas so Cmd+S triggers manual save
- Sync desktopRouter.config.tsx and desktopRouter.config.desktop.tsx
- Extend RecentlyViewed plugin to round-trip optional docId segment
* ✨ feat(onboarding): structured hunk ops for updateDocument
Extend `updateDocument` (and the underlying `@lobechat/markdown-patch`) with
explicit hunk modes so agents can unambiguously express deletes and inserts
instead of encoding them as clever search/replace pairs.
Modes: `replace` (default, backward-compatible), `delete`, `deleteLines`,
`insertAt`, `replaceLines`. Line-based modes use 1-based inclusive ranges
and are applied after content-based hunks, sorted by anchor line descending
so earlier lines stay stable. New error codes: `LINE_OUT_OF_RANGE`,
`INVALID_LINE_RANGE`, `LINE_OVERLAP`.
Onboarding document injection now prefixes each line with its 1-based number
(cat -n style) so the agent can cite line numbers when issuing line-based
hunks. Tool description, system role, and per-phase action hints updated to
teach the new shape.
* 🐛 fix(onboarding): align patchOnboardingDocument zod schema with structured hunks
The tRPC input schema still accepted only the legacy `{search, replace}` shape,
so agent calls using the new `insertAt`/`delete`/`deleteLines`/`replaceLines`
hunk modes were rejected before reaching `applyMarkdownPatch`. Switch to a
z.union matching MarkdownPatchHunk.
* 🐛 fix(markdown-patch): validate line ranges before overlap detection
Previously the overlap loop ran before per-hunk range validation, so an
invalid range (e.g. startLine=0 or endLine<startLine) combined with another
line hunk would be misreported as LINE_OVERLAP instead of the real
LINE_OUT_OF_RANGE / INVALID_LINE_RANGE. Validate each line hunk against the
baseline line count first, then run overlap detection on valid ranges only.
Automatic sync from main to canary. Merge conflicts detected.
**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260420-24659236264
git merge origin/main
# Resolve conflicts
git add -A && git commit
git push
```
> Do NOT merge canary into a main-based branch — always merge main INTO
the canary-based branch to keep a clean commit graph.
The 200-char truncation is no longer needed as the caller
already handles length limits.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 💄 style(topic): darken project group folder label in sidebar
Previous `type='secondary'` on the group title was too faint against the
sidebar background; promote the text to default color for better
legibility and keep the folder icon at tertiary so it stays subtle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(topic): use colorTextSecondary for project group title
Text's `type='secondary'` resolves to a lighter token than
`colorTextSecondary`; apply `colorTextSecondary` directly so the title
lands at the intended shade (darker than before, lighter than default).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(electron): show blue unread dot on tab when agent has unread badge
Mirror the sidebar agent unread badge on the corresponding browser-like tab as a subtle blue dot, so unread completions are visible even when the sidebar is out of view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(electron): forward proxy env vars to spawned agent CLI
The main-process undici dispatcher set by ProxyDispatcherManager only
covers in-process requests — child processes like claude-code CLI never
saw the user's proxy config. Extract a shared `buildProxyEnv` so any CLI
spawn can merge HTTP(S)_PROXY / ALL_PROXY / NO_PROXY into its env.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(electron): close active tab on Cmd+W when multiple tabs are open
Cmd/Ctrl+W now closes the focused tab first and only closes the window when
a single tab (or none) remains.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(electron): add Cmd+T shortcut to open a new tab
Reuses the active tab's plugin context to create a same-type tab, mirroring
the TabBar + button behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(electron): use container color for active tab background
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(electron): update Close menu item expectations for smart Cmd+W
Tests now assert the CmdOrCtrl+W accelerator and click handler instead of
the legacy role: 'close'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(electron): drop const/store import from HeterogeneousAgentCtr
The controller previously pulled defaultProxySettings from @/const/store,
which chain-loads @/modules/updater/configs and electron-is — that breaks
any unit test that mocks `electron` without a full app shim. Make
buildProxyEnv accept undefined and read the store value directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reject avatar values that aren't a base64 data URL, an absolute http(s) URL,
or an internal /webapi/user/avatar/<userId>/ path for the caller. Also
require the old avatar URL to live under the caller's own prefix (and
contain no '..') before removing it from S3.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(todo-progress): replace green bar with inline progress ring
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(chat-input): split branch and diff blocks, add changed-files popover
Branch now has its own hover tooltip for the full name; the diff stat is a
sibling block that opens a lazy-loaded popover listing changed files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(chat-input): show ahead/behind commit count vs upstream
Adds a badge next to the branch chip showing commits pending push (↑, blue)
and pull (↓, red) against the branch's upstream tracking ref. Hidden when
no upstream is configured or both counts are zero. Refreshed on focus,
after checkout, and on manual refresh from the branch switcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(desktop): extract git IPC methods into dedicated GitController
Moves detectRepoType, getGitBranch, getLinkedPullRequest, listGitBranches,
getGitWorkingTree{Status,Files}, getGitAheadBehind, and checkoutGitBranch out
of SystemCtr into a new GitCtr (groupName = 'git'). Shared helpers (resolveGitDir
/ resolveCommonGitDir / detectRepoType) become pure functions under utils/git.ts
so SystemCtr's selectFolder can still probe the picked folder without crossing
controller boundaries. Renderer side: new electronGitService wraps ipc.git.*,
and all six chat-input hooks plus BranchSwitcher are switched over.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(chat-input): inline ahead/behind arrows into branch chip
Moves the ↑/↓ counts out of a separate status block and inside the branch
trigger next to the label, so they sit with the branch they describe instead
of after the file-change badge. Tooltip folds into the branch tooltip (full
name · N to push · M to pull) so a single hover covers both pieces of info.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(desktop): parse git status with -z to avoid filename misparse
The previous getGitWorkingTreeFiles split every line on ' -> ' to detect
renames, but only R/C status codes emit that delimiter. Legitimate filenames
containing ' -> ' (or spaces, or embedded newlines) were misparsed — the
popover would report a truncated path or lose the entry entirely.
Switch both getGitWorkingTreeStatus and getGitWorkingTreeFiles to
`git status --porcelain -z`: NUL-terminated records, no C-style quoting,
no \n splitting hazards. Rename/copy entries emit two NUL-separated tokens
(DEST\0SRC) which we consume as a pair so counts and paths stay correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(todo-progress): hide stale todos when a new user turn starts
Add `selectCurrentTurnTodosFromMessages` that scopes the todos lookup
to messages after the last user message. The inline TodoProgress
component now uses it, so a completed 8/8 progress bar from a previous
operation no longer lingers across the next user turn.
The original `selectTodosFromMessages` is unchanged because the agent
runtime step context still needs cross-turn visibility of the plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 fix(desktop): tighten GitHub remote detection to host position
Replace substring check `config.includes('github.com')` with a regex
anchored to URL host position so look-alikes like `evilgithub.com` and
`github.com.attacker.com` no longer classify as GitHub. Closes CodeQL
"Incomplete URL substring sanitization" on PR #13980.
Not a real security issue (the config file is local and the
classification only drives a UI icon), but the tightened check is
strictly more correct and silences the scanner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 fix(electron): align TabBar left padding with NavPanel width on initial load
Defer DraggablePanel mount in NavPanelDraggable until `isStatusInit` flips true
so defaultSize captures the hydrated `leftPanelWidth` instead of the pre-hydration
default. Before hydration, render a placeholder div matching the store's current
width so NavigationBar's live-read width stays aligned with the DOM. Also adds
a small paddingRight to NavigationBar for visual balance.
Without this, the TabBar's left edge drifted away from the NavPanel's right edge
whenever the user's persisted panel width differed from the 320px default.
* ✨ feat(electron): add + button to TabBar to open new topic in active context
Introduce a pluggable `createNewTabAction` extension on RecentlyViewed
plugins so each page type can decide whether (and how) to spawn a new
tab from the active tab. Implemented for agent / agent-topic /
group / group-topic — clicking `+` creates a fresh topic under the
current agent/group and opens it as a new tab; other page types hide
the button by default.
* ✨ feat(electron): support new tab from page context
Page plugin now implements `createNewTabAction`, creating a fresh
untitled document via `usePageStore().createPage` and opening it as
a new `page` tab.
* 🐛 fix(electron): refresh page list after creating a new page via TabBar +
`createPage` only hits the service; without refreshing the documents
list, the sidebar / PageExplorer wouldn't show the freshly-created
page until the next full reload.
* 🐛 fix(electron): highlight new page in sidebar when opened via TabBar +
Switch to `createNewPage`, which runs the full optimistic flow —
dispatches the new document into the sidebar list and sets
`selectedPageId` — so the nav item active state stays in sync with
the freshly-opened page tab.
* 🐛 fix(electron): dispatch real page doc into sidebar list for TabBar +
The earlier `createNewPage` approach relied on an optimistic temp
document that SWR revalidation can clobber before the real doc
replaces it, leaving the new page absent from the sidebar. Create
the page via `createPage` first, then synthesize a `LobeDocument`
from the server response and dispatch it into the list alongside
setting `selectedPageId` — the nav item now appears and highlights
in sync with the new tab.
* ✨ feat(onboarding): add preset agent naming suggestions
* 🐛 fix(test): align AgentDocumentsGroup test assertions with title-first rendering
#13940 changed DocumentItem to prefer title over filename, but the
AgentDocumentsGroup tests from #13924 were still asserting on filename
strings. Update all text matchers to use titles (Brief / Example).
* fix: local webhook typing
* feat: add dormant status
* feat: add bot status tag
* feat: add bot connection status and refresh status
* feat: support bot status list refresh
* fix: bot status
* chore: add test timeout
* 🐛 fix(desktop): detect repo type for submodule and worktree directories
Route detectRepoType through resolveGitDir so directories where `.git`
is a pointer file (submodules, worktrees) are correctly identified as
git/github repos instead of falling back to the plain folder icon.
Fixes LOBE-7373
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(desktop): reprobe repo type for stale recent-dir entries
The recents picker rendered `entry.repoType` directly from localStorage,
so any submodule/worktree entry cached while `detectRepoType` still
returned `undefined` stayed stuck on the folder icon even after the
main-process fix. Wrap each row icon in a component that calls
`useRepoType`, which re-probes missing entries and backfills the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(chat-input): clear autocomplete hint on IME start to prevent freeze
Dispatch KEY_ESCAPE_COMMAND on compositionstart so the autocomplete
plugin removes PlaceholderInline/PlaceholderBlock nodes before the IME
begins composing. Composing next to those placeholder nodes caused the
editor to freeze during pinyin input with a visible hint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(topic-sidebar): split project grouping into ByProjectMode
Extracts project-specific group rendering from ByTimeMode into its own ByProjectMode folder, with a shared GroupedAccordion container. Project groups get a folder-icon column aligned with the topic item layout and a "new topic in {directory}" action.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(desktop): read config via commondir for linked worktrees
`resolveGitDir` returns `.git/worktrees/<name>/` for linked worktrees —
that dir has its own `HEAD` but no `config`, so `detectRepoType` still
returned `undefined` and worktrees missed the repo icon. Resolve the
`commondir` pointer first so `config` is read from the shared gitdir.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tab-bar): blend inactive tabs with titlebar, show close icon by default
Inactive tabs now use a transparent background and gain a subtle hover fill,
matching Chrome's tab chrome so the titlebar feels visually unified. The close
icon is always visible instead of fading in on hover, so users don't have to
hunt for it on narrow tabs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(desktop): CMD+N now actually clears active topic on agent page
Previously the File → 新建话题 (CMD+N) handler only `navigate()`d to the
agent base path. When the user was on `/agent/:aid?topic=xxx`, this stripped
the URL param but `ChatHydration`'s URL→store updater skips `undefined`
values, so `activeTopicId` in the chat store was never cleared and the
subscriber would push the stale topic right back into the URL.
Call `switchTopic(null)` on the store directly when an agent is active so
the change propagates store→URL via the existing subscriber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): don't surface self-cancelled exits as runtime errors
User-initiated cancel/stop and Electron before-quit kill the agent process
with SIGINT/SIGTERM, producing non-zero exit codes (130/143/137). Mark
these via session.cancelledByUs so the exit handler routes them through
the complete broadcast — otherwise a user cancel or app shutdown would
look like an agent failure (e.g. "Agent exited with code 143" leaking
into other live CC sessions' topics).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tab-bar): show running indicator dot on tab when agent is generating
Adds a useTabRunning hook that reads agent runtime state from the chat
store for agent / agent-topic tabs, and renders a small gold dot over
the tab avatar/icon while the conversation is generating. Other tab
types stay unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(claude-code): render ToolSearch select: queries as inline tags
Parses select:A,B,C into individual tag chips (monospace, subtle pill
background) instead of a comma-joined string, so the names of tools
being loaded read more clearly. Keyword queries keep the existing
single-highlight rendering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(git-status): show +N ±M -K diff badge next to branch name
Surface uncommitted-file count directly in the runtime-config status bar
so the dirty state is visible at a glance without opening the branch
dropdown. Each segment is color-coded (added / modified / deleted) and
hidden when zero; a tooltip shows the verbose breakdown.
Implementation:
- Backend buckets `git status --porcelain` lines into added / modified /
deleted / total via X+Y status pair
- New always-on useWorkingTreeStatus SWR hook (focus revalidation, 5s
throttle) shared by GitStatus and BranchSwitcher — single fetch path
- BranchSwitcher's "uncommitted changes: N files" now reads `total`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(assistant-group): show only delete button while tool call is in progress
When the last child of an assistantGroup is a running tool call, `contentId`
is undefined and the action bar fell through to a branch that dropped the
`menu` and `ReactionPicker`, leaving a single copy icon with no overflow.
Replace the legacy `continueGeneration / delAndRegenerate / del` bar with a
del-only bar in this state — delete is the only action that makes sense
before any text block is finalized.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation-flow): aggregate per-step nested metadata.usage in assistantGroup
After hetero-agent moved to per-step usage writes (`metadata: { usage: {...} }`),
the assistantGroup virtual message stopped showing the cumulative token total
across steps and instead surfaced only the last step's numbers.
Root cause: splitMetadata only recognised the legacy flat shape
(`metadata.totalTokens`, etc.) and didn't read the new nested shape, so each
child block went into aggregateMetadata with `usage: undefined`. The sum was
empty, and the final group inherited a single child's metadata.usage purely
because Object.assign collapsed groupMetadata down to the last child.
- splitMetadata now reads both nested (`metadata.usage` / `metadata.performance`)
and flat (legacy) shapes; nested takes priority
- Add `'usage'` / `'performance'` to the usage/performance field sets in parse
and FlatListBuilder so the nested objects don't leak into "other metadata"
- Regression test: multi-step assistantGroup chain sums child usages
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(hetero-agent): tone down full-access badge to match left bar items
The badge was shouting in colorWarning + 500 weight; reduce to
colorTextSecondary at normal weight so it sits at the same visual rank
as the working-dir / git buttons on the left. The CircleAlert icon
still carries the warning semantics. Also force cursor:default so the
non-interactive label doesn't pick up an I-beam over its text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): synthesize pluginState.todos from CC TodoWrite
Adapter now translates Claude Code's declarative TodoWrite tool_use input into the shared StepContextTodos shape and attaches it to tool_result. Selector drops the GTD identifier filter so any producer honoring pluginState.todos lights up the TodoProgress card.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): skip TodoWrite pluginState synthesis on error results
A failed TodoWrite (is_error=true) means the snapshot was never applied on CC's side. Since selectTodosFromMessages now picks the latest pluginState.todos from any producer, leaking a failed-write snapshot could overwrite the live todo UI with changes that never actually happened. Drain the cache either way so a retry with a fresh tool_use id doesn't inherit stale args.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): prefer topic-level cwd on send; route UI changes to active topic
Topic-level workingDirectory now takes priority over agent-level on the
send path, matching what the topic is actually pinned to. The UI picker
writes to the active topic's metadata (not the agent default), and warns
before switching when doing so would invalidate an existing CC session.
* ✨ feat(tab): reset tab cache when page type changes to stop stale metadata bleed
Switching a tab from one page type to another (e.g. agent → home) kept
the previous page's cached title/avatar, so the new page rendered with
the wrong header. Reset the cache on type change; preserve the merge
only when the type stays the same.
* 🐛 fix(hetero-agent): kill CC process tree on cancel so tool children exit
SIGINT to just the claude binary was leaving bash/grep/etc. tool
subprocesses running, which kept the CLI hung waiting on them. Spawn
the child detached (Unix) so we can signal the whole group via
process.kill(-pid, sig); use taskkill /T /F on Windows. Escalate
SIGINT → SIGKILL after 2s for tool calls that swallow SIGINT, and do
the same tree kill on disposeSession's SIGTERM path.
* ✨ feat(hetero-agent): show "Full access" badge in CC working-directory bar
Claude Code runs locally with full read/write on the working directory
and permission mode switching isn't wired up yet — the badge sets that
expectation up-front instead of leaving users guessing. Tooltip spells
out the constraint for anyone who wants detail.
* ♻️ refactor(agent-list): show runtime name (Claude Code/Codex) instead of generic "External" tag
The "External" tag on heterogeneous agents didn't tell users which
runtime backs the agent — multiple CLI runtimes (Claude Code, Codex, …)
looked identical in the sidebar. Map the heterogeneous type to its
display name so the tag identifies the actual runtime, with the raw
type as a fallback for any future provider we haven't mapped yet.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): persist accumulated text alongside tools[] writes
Carry the latest streamed content/reasoning into the same UPDATE that
writes tools[], so the DB row stays in sync with the in-memory stream.
Without this, gateway `tool_end → fetchAndReplaceMessages` reads a
tools-only row and clobbers the UI's streamed text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(workflow-summary): collapse summary when many tool kinds
When a turn calls >4 distinct tool kinds, list only the top 3 by count
and append "+N more · X calls total[ · Y failed]". Keeps the inline
summary scannable on long tool-heavy turns instead of running off the
line. Short turns keep the existing full list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(claude-code): use chip style for Skill inspector name
Replace the colon+highlight text with a pill-shaped chip containing the
SkillsIcon and skill name. Gives the Skill activation readout visual
parity with other tool chips and prevents long skill names from
overflowing the inspector line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(agent-documents): assert on rendered title, not filename
#13940 changed DocumentItem to prefer document.title over filename, but
the sidebar test still expected 'brief.md' / 'example.com'. Align the
assertions with the current behavior so the suite is green on canary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tab-bar): show agent avatar on agent/topic tabs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): persist per-step usage to each step assistant message
Previously, usage tokens from a multi-step Claude Code run were accumulated
across all turns and written only to the final assistant message, leaving
intermediate step messages with no usage metadata.
Each Claude Code `turn_metadata` event carries per-turn token usage
(deduped by adapter per message.id), so write it straight through to the
current step's assistant message via persistQueue (runs after any in-flight
stream_start(newStep) that swaps currentAssistantMessageId). The `result_usage`
grand-total event is intentionally dropped — applying it would overwrite the
last step with the sum of all prior steps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): normalize usage inside CC adapter (UsageData)
Follows the same principle as LOBE-7363: provider-native shape knowledge
stays in the adapter, executor only sees normalized events. The previous
commit left Anthropic-shape fields (input_tokens, cache_creation_input_tokens,
cache_read_input_tokens) leaking into the executor via `buildUsageMetadata`.
Introduce `UsageData` in `@lobechat/heterogeneous-agents` types with LobeHub's
MessageMetadata.usage field names. The Claude Code adapter now normalizes
Anthropic usage into `UsageData` before emitting step_complete, for both
turn_metadata (per-turn) and result_usage (grand total). Executor drops
`buildUsageMetadata` and writes `{ metadata: { usage: event.data.usage } }`
directly. Future adapters (Codex, Kimi-CLI) normalize their native usage into
the same shape; executor stays provider-agnostic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): persist per-step provider alongside model
CC / hetero-agent assistant messages were writing `model` per step but
leaving `message.provider` NULL, so pricing/usage lookups could not key on
the adapter (e.g. `claude-code`, billed via CLI subscription rather than
raw Anthropic API rates).
CC adapter now emits `provider: 'claude-code'` on every turn_metadata event
(same collection point as model + normalized usage). Executor tracks
`lastProvider` alongside `lastModel` and writes it into:
- the step-boundary update for the previous step
- `createMessage` for each new step's assistant
- the onComplete write for the final step
Provider choice is the CLI flavor (what the adapter knows), not the wrapped
model's native vendor — CC runs under its own subscription billing, so
downstream pricing must treat `claude-code` as its own provider rather than
conflating with `anthropic`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): read authoritative usage from message_delta, not assistant
Under `--include-partial-messages` (enabled by the CC adapter preset), Claude
Code echoes a STALE usage snapshot from `message_start` on every content-block
`assistant` event — e.g. `output_tokens: 8` or `1` — and never updates that
snapshot as more output tokens are generated. The authoritative per-turn
total arrives on a separate `stream_event: message_delta` with the final
`input_tokens` + cache counts + cumulative `output_tokens` (e.g. 265).
The adapter previously grabbed usage from the first `assistant` event per
message.id and deduped, so DB rows ended up with `totalOutputTokens: 1` on
every CC turn.
Move turn_metadata emission from `handleAssistant` to a new `message_delta`
case in `handleStreamEvent`. `handleAssistant` still tracks the latest model
so turn_metadata (emitted later on message_delta) carries the correct model
even if `message_start` doesn't.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(extras-usage): fall back to metadata.usage when top-level is absent
The assistant Extras bar passes `message.usage` to the Usage component,
which conditionally renders a token-count badge on `!!usage.totalTokens`.
Nothing in the read path aggregates `message.metadata.usage` up to
`message.usage`, so the top-level field is always undefined for DB-read
messages — the badge never shows for CC/hetero turns (and in practice also
skips the gateway path where usage only lands in `metadata.usage`).
Prefer `usage` when the top-level field is populated, fall back to
`metadata.usage` otherwise. Both fields are the same `ModelUsage` shape, so
the Usage/TokenDetail components don't need any other change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(extras-usage): promote metadata.usage inside conversation-flow parse
The previous fix spread a `usage ?? metadata?.usage` fallback across each
renderer site that passed usage to the Extras bar. Consolidate: `parse`
(src/store → packages/conversation-flow) is the single renderer-side
transform every consumer flows through, so promote `metadata.usage` onto the
top-level `usage` field there and revert the per-site fallbacks.
UIChatMessage exposes a canonical `usage` field, but no server-side or
client-side transform populated it — executors write to `metadata.usage`
(canonical storage, JSONB-friendly). Doing the promotion in parse keeps the
rule in one place, close to where display shapes are built, and covers both
desktop (local PGlite) and web (remote Postgres) without a backend deploy.
Top-level `usage` is preserved when already present (e.g. group-level
aggregates) — `metadata.usage` is strictly a fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Introduced TopicCanvas component to serve as a document canvas for topics, integrating an editor and title section.
- Added TitleSection component for managing topic titles and emojis, enhancing user interaction with a dedicated UI.
- Updated FloatingChatPanel to accommodate the new TopicCanvas, ensuring a cohesive layout in the topic page.
- Enhanced tests to verify the integration of TopicCanvas within the topic page route.
Signed-off-by: Innei <tukon479@gmail.com>
- Updated the @lobehub/ui dependency in package.json to version 5.9.1.
- Refactored FloatingChatPanel to utilize the new FloatingSheet component, enhancing its layout and state management.
- Introduced a new ChatLayout component for better organization of chat-related UI elements.
- Adjusted routing configuration to incorporate the new ChatLayout for agent chat pages.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(desktop): add dedicated topic popup window with cross-window sync
Introduce a standalone Vite entry for the desktop "open topic in new window"
action. The popup is a lightweight SPA (no sidebar, no portal) hosting only
the Conversation, and stays in sync with the main window through a
BroadcastChannel bus.
- Add popup.html + entry.popup.tsx + popupRouter.config.tsx
- Add /popup/agent/:aid/:tid and /popup/group/:gid/:tid routes
- Reuse main Conversation/ChatInput; wrap in MarketAuth + Hotkeys providers
- Pin-on-top button in the popup titlebar (new windows IPC: set/isAlwaysOnTop)
- Group topic "open in new window" now uses groupId (previously misused agentId)
- Cross-window sync: refreshMessages/refreshTopic emit via BroadcastChannel;
subscriber revalidates local SWR caches with echo-loop suppression
- Hide WorkingPanel toggle inside /popup (no WorkingSidebar present)
- RendererUrlManager dispatches /popup/* to popup.html in prod; dev middleware
rewrites SPA deep links while skipping asset/module requests
* 💄 style(desktop): restore loading splash in popup window
* ♻️ refactor(desktop): replace cross-window sync with popup-ownership guard
The BroadcastChannel-based bidirectional sync between the main SPA and the
topic popup window had edge cases during streaming. Drop it in favour of a
simpler ownership model: when a topic is already open in a popup, the main
window shows a "focus popup" redirect instead of rendering a second
conversation.
- Remove src/libs/crossWindowBus.ts and src/features/CrossWindowSync
- Remove postMessagesMutation/postTopicsMutation calls from refresh actions
- Add windows.listTopicPopups + windows.focusTopicPopup IPC
- Main process broadcasts topicPopupsChanged on popup open/close; parses
(scope, id, topicId) from the popup window's /popup/... path
- Renderer useTopicPopupsRegistry subscribes to broadcasts and fetches the
initial snapshot; useTopicInPopup selects by scope
- New TopicInPopupGuard component with "Focus popup window" button
- Desktop-only index.desktop.tsx variants for (main)/agent and (main)/group
render the guard when the current topic is owned by a popup
- i18n: topic.inPopup.title / description / focus in default + en/zh
* 🐛 fix(desktop): re-evaluate popup guard when topic changes
Subscribe to the popups array and derive findPopup via useMemo so scope changes (e.g. switching topic in the sidebar while a popup is open) correctly re-compute the guard and let the main window render the newly active topic.
* 🐛 fix(desktop): focus detached topic popup from main window
* ✨ feat(desktop): add open in popup window action to menu for active topic
Signed-off-by: Innei <tukon479@gmail.com>
* 🎨 style: sort imports to satisfy simple-import-sort rule
* ✨ feat(error): add resetPath prop to ErrorCapture and ErrorBoundary for customizable navigation
Signed-off-by: Innei <tukon479@gmail.com>
* ♻️ refactor: restore ChatHydration in ConversationArea for web/mobile routes
Reintroduce ChatHydration component to agent and group ConversationArea
so that URL query sync (topic/thread) works on web and mobile routes,
not only on desktop entry files.
* ✨ feat(electron): enforce absolute base URL in renderer config to fix asset resolution in popup windows
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>