* 👷 build(database): add messenger tables for IM bot integration
Adds three new tables to support the Messenger feature (Slack / Telegram
/ Discord / Feishu / MS Teams shared-bot integration):
- messenger_account_links: maps a LobeHub user to an IM account per
(platform, tenant); tracks the active agent for `/switch` flows.
- messenger_installations: per-tenant OAuth install records (Slack
workspaces, Feishu tenants, …); stores AES-GCM encrypted bot
credentials and the installer.
- system_bot_providers: deployment-wide App-level bot credentials
(one Discord App / Telegram bot / Slack App per deployment),
replacing the env-var-based config.
All sensitive credentials are encrypted via KeyVaultsGateKeeper, the
same gatekeeper used by `agent_bot_providers`. SQL is idempotent
(`IF NOT EXISTS` / `DROP CONSTRAINT IF EXISTS`) per repo convention.
Includes models with full test coverage. Schema and migration only —
no router / service wiring in this PR.
* 🐛 fix(database): bridge stale messenger_account_links missing tenant_id
Some envs deployed a pre-squash version of the messenger migrations
where `messenger_account_links` was created without `tenant_id` and
used the legacy 2-column unique indexes. CREATE TABLE IF NOT EXISTS is
a no-op on those tables, so the new 3-column unique index then fails
with `column "tenant_id" does not exist` (PG 42703).
Add the same bridge logic the original 0102 migration carried — ALTER
ADD COLUMN IF NOT EXISTS for tenant_id and DROP INDEX IF EXISTS for the
two legacy indexes. Idempotent on fresh DBs.
* Revert "🐛 fix(database): bridge stale messenger_account_links missing tenant_id"
This reverts commit d5232564e4.
* 💄 style: simplify onboarding agent identity intervention card
- Drop redundant "Onboarding approval" eyebrow, "Agent name"/"Agent avatar" field grid, and "Applies to" target chips — the description above already conveys scope, and the avatar+name preview already shows the new identity
- Rephrase title to first-person agent voice ("I'll update my name and avatar") so the card reads as the agent announcing what it will do, not a generic admin form
- Remove the now-dead applyHint line under the avatar
- Prune unused i18n keys (eyebrow / applyHint / name / emoji / targets / targetInbox / targetOnboarding) across default + en-US + zh-CN
- Update webOnboarding intervention test to match the simplified card
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: use field-aware title for partial saveUserQuestion approvals
The manifest routes name-only and emoji-only saveUserQuestion calls through the same intervention as the both-fields case, but the previous title hardcoded "I'll update my name and avatar". An emoji-only approval would over-promise a rename that never happens.
Pick titleNameOnly / titleAvatarOnly / title based on which fields are actually pending; cover all three branches in webOnboarding.test.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: drop redundant scope description from onboarding intervention
The field-aware title already says exactly what's about to change ("I'll
update my name" / "...avatar" / "...name and avatar"); the secondary line
explaining that the change applies to Inbox + the current onboarding chat
was extra reading without new information for someone mid-onboarding.
Remove the description Text + i18n key (default + en-US + zh-CN) and
collapse the now-single-child header Flexbox.
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(gateway): keep input loading on through execAgentTask round-trip
The Gateway branch in `sendMessageInternal` completed the parent
`sendMessage` op before awaiting `executeGatewayAgent`, so during the
`execAgentTask` network round-trip no operation was running. The send
button briefly flickered back to "send" until the child
`execServerAgentRuntime` op started.
Move `completeOperation` to after `executeGatewayAgent` resolves —
by then the child op is already running, so loading state never drops.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(gateway): hand off parent op inside executeGatewayAgent
Make `executeGatewayAgent` accept an optional `parentOperationId` and
complete it the instant phase-1 init finishes — right after the child
`execServerAgentRuntime` op starts and the assistant message is
associated. Previously the caller had to call `completeOperation` after
`await executeGatewayAgent(...)` returned, which was fragile: any future
`await` added between the child startOperation and the function return
would silently extend the parent op's lifetime past phase-1.
Also wires `parentOperationId` through to `startOperation` so the
parent/child lineage is recorded on the new op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): persist agentId so brief cards render the producing agent's avatar
`BriefCard` only renders the agent avatar when the enriched `brief.agent`
is non-null, which in turn requires `briefs.agentId` to be set. Several
brief creation paths (task lifecycle synthesize/error/review, and the
agent-driven `lobe-brief` tool runtime) were inserting briefs without
`agentId`, leaving the avatar slot empty in the Daily Brief card.
Pass `assigneeAgentId` from the task in `TaskLifecycleService` and
`context.agentId` from the tool execution context in the brief runtime.
No backfill — internal testing only, historical rows stay null.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): honor stop clicks during phase-1 init
With the parent `sendMessage` op kept running through the
`execAgentTask` round-trip (so the input loading state stays on),
clicking Stop now reaches `cancelOperation(sendMessage)` mid-await but
`executeGatewayAgent` was unaware of the abort: the request finished,
the server task got created, the WS opened, and the agent ran despite
the cancel.
Fixes:
- Plumb the parent op's AbortSignal into `aiAgentService.execAgentTask`
so the fetch itself aborts in-flight when cancel arrives during the
round-trip.
- After every await in phase-1 init, re-check `signal.aborted` and bail
out — the server task may already exist if cancel arrived after the
request resolved, so fire `interruptTask` best-effort before throwing.
- In the caller catch path, skip `failOperation` when op status is
already `cancelled` so we don't clobber the user-cancelled state with
`failed`.
Adds a regression test that pre-aborts the controller, awaits
`executeGatewayAgent`, and asserts the signal is forwarded, the server
task is interrupted, and the child op / message association / WS
connect / parent completion are all skipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(review): add branch-compare diff mode with base ref picker
Introduces a Branch mode in the agent Review panel that diffs the current
HEAD against the remote default branch (resolved via `refs/remotes/origin/HEAD`,
overridable via a per-repo base picker). Pulls the comparison data through a
new `getGitBranchDiff` IPC that streams `git diff base...HEAD` and reuses the
existing per-file split + size-cap path, plus `listGitRemoteBranches` for the
picker. Renders a GitHub-style `base ▾ ← head` label with shrink/ellipsis
behaviour, swaps the loading spinner for `NeuralNetworkLoading`, and persists
the user's base override in localStorage keyed by working directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-profile): hide right-panel toggle for heterogeneous agents
Heterogeneous runtimes (Claude Code, Codex, etc.) own their own toolchain
and don't surface the LobeHub right-panel content, so the toggle button is
a dead-end in their profile header.
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 assistant content blocks are split into answer and workflow segments,
each segment now receives explicit `contentOverride` and `hasToolsOverride`
props so that the rendered markdown matches the segment's own content
instead of all segments reading the same store subscription.
* ✨ feat(kb-tool): integrate BM25 search and docs_* read for inline documents
- searchKnowledgeBase now returns inline documents (BM25 over documents.content)
alongside file chunks (vector). Inline custom/document records created via
createDocument or `lh kb create-doc` are now discoverable through the agent tool.
- readKnowledge accepts both file_* and docs_* IDs. docs_* reads documents.content
directly (no S3 lookup, no parse).
- chunkRouter.semanticSearchForChat: dual-path with Promise.allSettled — failures
on either path no longer kill the whole call; surfaced via new `errors` field.
- formatSearchResults renders <files> and <documents> sections separately.
Fixes LOBE-8606
Fixes LOBE-8608
* style(TitleSection): add border radius to title input field
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(kb-tool): preserve search-path errors in zero-result responses
When semanticSearchForChat returns no hits but includes errors (e.g. vector
search fails and BM25 finds nothing), use formatSearchResults which renders
error notes, instead of promptNoSearchResults which drops them silently.
---------
Signed-off-by: Innei <tukon479@gmail.com>
Replace the inline `AgentTaskList` card on agent and inbox welcome
screens with a dedicated `Tasks` section in the agent sidebar that
groups items by status (Pending review / Backlog / In progress).
Sidebar fetch is scoped to active statuses only — `done` and
`canceled` are neither pulled nor rendered, and use a separate SWR
key from the kanban page so the two views don't trample each other's
state.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(agent): migrate remaining /api/agent routes to Hono
Move the static `route.ts` handlers under `src/app/(backend)/api/agent/`
into the existing Hono app at `src/server/agent-hono/`, leaving only the
SSE `stream` endpoint as a Next.js route. Behavior, URLs, and auth
semantics are unchanged.
- New middlewares: `qstashAuth` (QStash sig only) and `bearerSecretAuth`
(factory for arbitrary `Bearer <secret>` checks)
- Migrated handlers: `run`, `webhooks/bot-callback`, `gateway`,
`gateway/start`, `gateway/callback`, `webhooks/[platform]/[[...appId]]`
- `gateway/callback` keeps inline auth so the disabled-feature 204 still
short-circuits before any auth check
- `gatewayCron` keeps `next/server`'s `after()` for the 10-min poll loop
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🧪 test(agent-hono): cover migrated route handlers and new middlewares
Add unit tests for the handlers and middlewares introduced by the
/api/agent → Hono migration. Each test uses the same hand-built Hono
Context stub pattern as `toolResult.test.ts` (vitest can't resolve the
hoisted `hono` package, so a real Hono Context isn't available in
tests).
Coverage:
- middlewares/qstashAuth (sig pass/fail → next called/not, body forwarded
to verifier)
- middlewares/bearerSecretAuth (503/401/200 paths, lazy secret eval)
- handlers/runStep (validation, lock 429 + Retry-After, success shape,
upstash-retried header forwarding)
- handlers/botCallback (validation + service delegation + 500 on throw)
- handlers/gatewayCallback (disabled-feature 204, auth, zod validation,
state.status → BotRuntimeStatus mapping)
- handlers/gatewayStart (start/restart paths, stop-before-ensure
ordering, 500 on failure)
- handlers/platformWebhook (param validation, raw request passthrough)
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: polish onboarding agent welcome and name suggestions
- Float NameSuggestions above ChatInput (out of greeting message), match width via WideScreenContainer
- Compact suggestion cards: emoji and name on one row, smaller padding, ellipsis prompt
- Migrate suggestion data from i18n to a typed config (`nameSuggestions.config.ts`) with EN/ZH content
- Expand pool to 50 differentiated names; ZH uses native Chinese names, EN uses English; sample 3 random items per group, refresh excludes current ids
- Click a card to fill ChatInput instead of sending immediately
- Tighten welcome footer copy in EN/ZH
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: refine onboarding name suggestions and click-to-fill flow
- Click a suggestion fills ChatInput via editor.setDocument + focus instead of sending immediately
- Append localized avatar hint ("Use {emoji} as the avatar." / "头像用 {emoji}。") to the filled message
- Expand suggestion pool to 100 with bilingual EN/ZH content; mix 2/3/4-char Chinese names; rebalance emoji↔name pairings; tone the 4-char ZH names toward modern/youthful phrasing
- Update NameSuggestions.test.tsx to mock editor.setDocument/focus and i18n interpolation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-tools): add web-onboarding tool inspectors and write document render
- Add Inspector components for FinishOnboarding / ReadDocument / SaveUserQuestion / UpdateDocument / WriteDocument under @lobechat/builtin-tool-web-onboarding/client
- Add Render component for WriteDocument
- Wire WebOnboardingInspectors and WebOnboardingRenders into the central builtin-tools registries (inspectors.ts / renders.ts)
- Add tool display names (saveUserQuestion → "Recorded info", writeDocument → "Wrote a document") to AssistantGroup constants and chat locale
- Add plugin locale keys for docType (User Persona, SOUL.md) and pluralized inspector counters (chars / changes / interests); shorten saveUserQuestion API name to "Save"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: guard resolveNameSuggestion against undefined locale
When useTranslation is mocked without an i18n.language (e.g. Conversation.test.tsx), locale came in undefined and resolveNameSuggestion crashed on `.toLowerCase()`. Treat missing/unknown locales as a fallback to en.
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(conversation): reduce streaming re-renders with reference stabilization and self-subscribing components
- Add stabilizeReferences utility to pin unchanged subtrees to previous identity after parse()
- Make Tool, Tools, and MessageContent self-subscribe via store selectors instead of receiving data as props
- Stabilize handleExpandedChange and expandedKeys in WorkflowCollapse with useCallback/useMemo
- Add selectors: findBlockById, getToolsInBlock, getToolInBlock, getBlockContent, getBlockHasTools
* 🔧 chore(agent-mock): update todo-write-stress test case
* feat: refactor todo-write-stress case to utilize lobe-gtd API for task management and enhance workflow with structured plans and todos
- Updated tool steps to replace previous bash commands and file operations with lobe-gtd API calls for creating and updating todos and plans.
- Introduced structured plans for various phases of the migration process, enhancing clarity and organization.
- Implemented a breathing step to simulate processing between tool-call batches.
- Enhanced the overall flow of the todo-write-stress case to reflect a more realistic and organized task management approach.
refactor: optimize ContentBlocksScroll component with virtualized list for improved performance
- Added CSS styles to enable content visibility auto for off-screen workflow items, preserving React state while optimizing rendering.
- Updated Flexbox component to conditionally apply virtualized list styles based on the variant prop, enhancing layout performance.
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(conversation): remove virtualized list styles to improve rendering performance
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(conversation): address codex streaming review feedback
* ♻️ refactor(conversation): use query structural sharing helper
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): add afterMutateHandler for post-mutation synchronization
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): enhance beforeMutateHandler with context and add meaningful content check
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): improve data source validation and streamline command dispatch logic
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): add test for Page Agent editTitle behavior without sending content or editorData
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(editor-runtime): update LiteXML node extraction to include attributes and improve error logging
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix: use namespace import for GeneralChatAgent to fix vi.spyOn in tests
vi.spyOn on a module namespace object requires the production code to
access the class through the same namespace. Destructured imports capture
a direct binding that bypasses the spy, causing "Class constructor
GeneralChatAgent cannot be invoked without 'new'" in tests.
* 🐛 fix: replace vi.spyOn on class constructor with vi.mock for GeneralChatAgent
vi.spyOn wraps a class with a plain function that loses [[Construct]]
semantics in ESM, causing "Class constructor GeneralChatAgent cannot be
invoked without 'new'". Replace with vi.mock + hoisted mock constructor
that properly tracks calls while preserving new-ability.
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 💄 style: update heterogeneous agent ChatInput placeholder text
Change 'Ask {{name}} to do a task...' to 'Describe a task or ask a question to {{name}}' for a more natural prompt consistent with Claude Code style.
* fix: also update TypeScript locale source for sendPlaceholderHeterogeneous
* fix: unify casing for popup window labels and simplify folder chooser text
In execAgent/bot mode, `serverMessagesEngine` is called from
`RuntimeExecutors.ts` without several `{{VARIABLE}}` placeholders that
the client-side `contextEngineering.ts` correctly resolves via stores
and lambdaClient. This caused literal `{{CREDS_LIST}}`, `{{username}}`,
`{{language}}`, `{{memory_effort}}`, `{{sandbox_enabled}}`, and
`{{CRON_JOBS_LIST}}` strings to leak into LLM prompts.
Fix: resolve each missing variable before building `contextEngineInput`:
- `{{username}}` / `{{language}}`: `UserModel.getInfoForAIGeneration()`
- `{{sandbox_enabled}}`: check `lobe-cloud-sandbox` in enabled tools
- `{{memory_effort}}`: read from `agentConfig.chatConfig.memory.effort`
- `{{CREDS_LIST}}`: `MarketService.market.creds.list()` (lobe-creds gate)
- `{{CRON_JOBS_LIST}}`: `AgentCronJobModel.findWithPagination()` (lobe-cron gate)
All fetches are best-effort (try/catch → empty string fallback) so a
transient error never breaks agent execution.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass the user's preferred response language (from settings) to
chainTaskTopicHandoff and chainGenerateBrief so that task run titles
and briefs always output in the user's configured language instead of
following the agent's content language.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🐛 fix: add server runtime for lobe-agent-management tool
- Add `agentManagement.ts` server runtime in `serverRuntimes/`
- Implement all 9 API methods: `createAgent`, `updateAgent`, `deleteAgent`,
`getAgentDetail`, `duplicateAgent`, `updatePrompt`, `installPlugin`,
`searchAgent`, `callAgent`
- Uses `AgentModel` from `@lobechat/database` for agent CRUD
- Uses `DiscoverService` for marketplace search in `searchAgent`
- `callAgent` with `runAsTask: true` returns `execTask` state for task system
- Register `lobe-agent-management` in `serverRuntimes/index.ts`
Fixes LOBE-8434
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: address review feedback for agent-management server runtime
- callAgent: always use task path on server (no `registerAfterCompletion` available for synchronous execution)
- installPlugin: create `user_installed_plugins` DB record via PluginModel so manifest is discoverable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 💄 style(brief-card): mute brief icon when brief is resolved
Resolved briefs now render the leading icon with muted gray colors instead
of the type's accent color, matching the existing "已标记为已解决" pill so the
card visually reads as inactive at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(page-agent): add custom Render for modifyNodes tool
Wires page-agent renders into the central registry and adds a per-operation
list view for modifyNodes (action icon, position chip, litexml preview, and
per-op success/error from pluginState.results), replacing the JSON fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): set trigger='task' on briefs created from task lifecycle
Populate the existing `trigger` column on briefs emitted by the task
lifecycle (error, synthesized topic, auto-review pass/retry/force-pass)
and the heartbeat watchdog (workflow + tRPC), so consumers can filter
briefs by source module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief-card): show only the producing agent avatar
Stop fetching every agent in the task tree for brief cards. The stacked
Avatar.Group looked noisy for tasks with multiple subagents and didn't
convey ownership; render a single avatar for the agent that produced
the brief instead (`brief.agentId`).
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): add aiAgent.heteroIngest / heteroFinish procedures (LOBE-8535 phase 2a)
Wires `lh hetero exec` producer streams into the existing StreamEventManager
fanout: events flow CLI → tRPC → Redis Stream → gateway WS → renderer with
the same wire shape as gateway-driven runs.
- Reconcile server StreamEvent.type with @lobechat/agent-gateway-client's
AgentStreamEventType so tool_execute / tool_result land natively
- HeterogeneousAgentService skeleton with sequential publish (preserves
stepIndex ordering) + terminal agent_runtime_end fallback on finish
- Inline Zod schemas on aiAgentProcedure; topicId required (operationId
reverse-lookup unreliable per LOBE-8516 design decision)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): add HeterogeneousPersistenceHandler — server-side DB writes (LOBE-8535 phase 2b)
Mirrors src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts
(1.8k lines) for the DB concerns. Renderer keeps its own copy for
desktop-host concerns (IPC, store dispatch, notifications); cloud / CLI
ingest goes through this handler instead.
- 3-phase tool persist: pre-register tools[] → create role:'tool' message
→ backfill result_msg_id (mirrors persistToolBatch lines 319–411)
- Subagent threads: lazy-create on first tagged chunk + per-turn assistant
chaining + finalize on parent tool_result with terminal assistant
- Step boundaries: stream_start { newStep: true } flushes prior content
and chains a new assistant off the last tool message
- Per-turn metadata persistence (step_complete phase=turn_metadata)
- Module-level state map keyed on operationId; idempotency via
(stepIndex, type, timestamp). Multi-replica caveat documented — phase 3
sandbox owns the endpoint per-instance so sticky routing is implicit.
Tests:
- 13 unit tests with fake-models harness covering bootstrap, idempotency,
3-phase persist, step boundaries, subagent lifecycle, terminal events
- 2 fixture-driven tests replaying .heerogeneous-tracing/cc-streaming.json
(502 events, 71 tool uses) end-to-end with idempotency assertions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): restore runtime imports after lint auto-fix
ThreadStatus / ThreadType / AgentRuntimeErrorType are used as values, not
just types — the post-commit linter incorrectly converted the import to
`import type`, which broke the build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(hetero-agent): full renderer event-branch parity + session resume (LOBE-8535 phase 2b' + 2c)
Renderer-parity additions to HeterogeneousPersistenceHandler:
- Echo-suppression: when CC streams an AuthRequired error string into
`content` BEFORE emitting the structured error, the assistant ends up
with both. Mirror the renderer's `shouldSuppressTerminalErrorEcho` logic
(lines 113–130 of heterogeneousAgentExecutor.ts) so we keep only the
structured error in those cases. Trigger conditions: `AuthRequired` code
or explicit `clearEchoedContent` flag.
- 34 new branch-coverage tests against every event variant the renderer
dispatches on (step_complete phases, stream_start with/without newStep,
stream_chunk text/reasoning/tools_calling × main/subagent, all no-op
variants, terminal error echo handling, subagent edge cases).
Phase 2c — session id persistence + resume helper:
- ChatTopicMetadata.heteroSessionId docstring updated: it's now the shared
field for desktop and cloud paths (was tagged "desktop only").
- handler.finish() now accepts `sessionId` and writes it via
TopicModel.updateMetadata (merges, preserves runningOperation peer).
- HeterogeneousAgentService passes sessionId through, exposes
`getHeterogeneousResumeSessionId(topicId)` helper for phase 3 cloud
sandbox routing to inject `--resume <id>` on the next CLI spawn.
- 9 tests covering happy path, missing session id, error result still
persists, peer-field preservation, updateMetadata failure isolation,
and the resume helper's lookup paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): collision-safe idempotency key + mark-processed-after-success + portable fixture (PR #14444 review)
Three issues from PR review:
1. **Idempotency key collision** — the old `(stepIndex, type, timestamp)`
triple collided when CC bursts multiple `stream_chunk` events through
the same step within a single `Date.now()` millisecond. Later chunks
got dropped as duplicates → silent assistant truncation. Now keys
include a stable FNV-1a fingerprint of `event.data`, so distinct
payloads stay distinct even at the same timestamp.
2. **Mark-processed-before-handle** — `processedKeys.add(key)` ran BEFORE
`handleEvent`, and ingest swallowed throws. A transient DB error in
any per-event write was silently lost: the event was marked done,
the BatchIngester acked OK, retries skipped it, content was gone.
Now: mark only after successful handling + propagate throws all the
way to the BatchIngester so the batch retries. Idempotency map
dedupes the events that already succeeded earlier in the batch.
Knock-on: removed every `.catch(log)` from per-write paths. Renderer's
"log + continue" posture doesn't fit the server (authoritative for
cloud runs, silent partial writes diverge DB from WS view).
3. **Portable fixture** — `.heerogeneous-tracing/cc-streaming.json` is
gitignored and missing in CI, so the fixture-driven test couldn't run.
Replaced file IO with a synthetic stream that captures the same
characteristics (multi-step, bursty same-millisecond text chunks,
tool_use → tool_result pairs, step boundaries, terminal event). The
synthetic fixture is also more meaningful — it has explicit assertions
about chain-shape and bursty-text dedupe correctness.
Tooling adjustments to support the new contract:
- `persistToolBatch` restructured: payloads de-dup by id (so retries
don't duplicate); `persistedIds` populated only AFTER successful
per-tool create; phase 1 + phase 3 always run (idempotent re-writes)
so a partial-failure retry can complete missed phase 3 backfills.
- `ensureSubagentRun`: thread/user/first-assistant create errors throw
out instead of returning `undefined` and dropping the run.
`ThreadModel.create` already uses `onConflictDoNothing` on id, so
retrying the same generated id is safe.
Tests added (69 hetero-agent tests, was 66):
- Bursty same-timestamp distinct-content text chunks → all preserved
- Mark-processed-after-success retry contract (transient flake recovery)
- Synthetic fixture replays a multi-step CC-shaped run with chain-shape
+ idempotency + partial-batch retry assertions
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(time): extract useActivityTime hook and move time keys to common namespace
- Add `useActivityTime` hook wrapping `formatActivityTime` with i18n built in
- Move `time.formatThisYear/formatOtherYear/today/yesterday` from `discover` to `common` namespace
- Refactor chat header (hetero-agent), Task Activities, memory/home time, and Comment/Topic cards to use the hook so they show relative time (`5 minutes ago`) within 24h and absolute date afterwards
- Switch `PublishedTime` and `AgentTaskItem` to consume time keys from `common`
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): finalize trace snapshot on error path
Propagated errors from RuntimeExecutors (e.g. `markPersistFatal` from a
parent_id FK violation) used to skip snapshot finalization entirely:
the success-path `finalizeSnapshot` block lived inside the try, so the
catch threw without writing the canonical
`agent-traces/<agentId>/<topicId>/<op>.json`. The partial sat orphaned
at `_partial/<op>.json`, the final S3 path returned 404, and the failed
op was invisible in the trace bucket while still showing as `status:
'error'` in Redis. (LOBE-8533)
Extract the finalize block into `finalizeSnapshotForOperation` and call
it from both the success branch and the error catch. The error call
synthesizes a failed step (the real one never reached
`appendStepToPartial` — it threw before the partial push), so step
counts stay aligned with the assistant message that triggered the call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test: align expected strings with English-only labels and fix mobile router import sort
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): dedupe failed-step append and trust finalized step count
- finalizeSnapshotForOperation now merges the error event into an existing
step record when the synthetic failedStep collides with one already
written by the success-path append (e.g. saveAgentState or queue
scheduling threw post-append). Prevents duplicate stepIndex entries
that corrupt ordering and per-step metrics in trace reconstruction.
- totalSteps is derived from the finalized step array instead of
state.stepCount, so the synthesized failed step is reflected in the
snapshot total (Redis-loaded stepCount lags by one on the error path).
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): support multimodal input across CLI / shared spawn / desktop
`spawnAgent` and `lh hetero exec` could only take a flat string prompt, so
attaching images required bypassing the shared layer (which is what desktop
actually did). This adds a unified `AgentPromptInput` shape — string sugar or
an array of text/image content blocks — and lifts image handling into the
shared `@lobechat/heterogeneous-agents/spawn/input` module.
Image sources accept URL (with optional id for cache dedupe), local path, or
inline base64. The shared `normalizeImage` fetches/reads/decodes, with
optional on-disk caching keyed by `sha256(id || url)`. `materializeImageToPath`
writes buffers to a cache dir (used by Codex `--image <path>`), with byte-
signature sniffing fallback when MIME is generic. `buildAgentInput` is the
single source of truth for per-agent serialization: Claude Code receives base64
image blocks inline in stream-json; Codex receives text on stdin + repeatable
`--image <path>` flags.
CLI gets three input modes: `--prompt <text>` + `--image <path|url|data:>`
(repeatable), `--input-json <file|->` for full content-block JSON, and stdin
auto-detection (JSON vs plain text by first non-whitespace character).
Mutually-exclusive flag combinations error early.
Desktop's `HeterogeneousAgentCtr` drops ~100 lines of duplicated cache /
sniffing code; helpers (`buildStreamJsonInput`, `resolveCliImagePaths`) become
thin wrappers around the shared functions. Driver interface and IPC contract
are unchanged.
`spawnAgent` is now async (image normalization fetches/reads before spawn).
Verified end-to-end: `lh hetero exec --type claude-code --prompt ... --image
red.png` → CC replied "I see a solid red color." `--input-json` mode also
verified. 28/28 desktop tests, 11/11 CLI hetero tests, 22/22 spawn package
tests pass.
Refs LOBE-8523 (phase 1a follow-up before phase 1b ingest).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔧 chore(cli): include types/model-bank/business-const in workspace
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(types): inline crawler and python-interpreter types
Drop workspace deps on @lobechat/web-crawler and @lobechat/python-interpreter
from @lobechat/types by inlining CrawlSuccessResult / CrawlErrorResult /
CrawlUniformResult and PythonOutput / PythonResult into the relevant tool
type modules.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔖 chore(cli): bump @lobehub/cli to 0.0.10
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(github-tool): prefer description over command in inspector/render header
Show the human-readable `description` arg in the gh tool's collapsed
inspector chip and result-card header when provided; fall back to the
extracted subcommand. Full command is still visible in the expanded
Command code block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): treat generic Content-Type as unknown + handle async spawnAgent failures
Two issues raised on PR #14433 review:
**P1 — generic Content-Type bypassed sniffing in normalizeImage**
`fetchUrlImage` accepted any non-empty `Content-Type` as the final
`mediaType`, so CDN responses defaulting to `application/octet-stream` (or
`text/plain`) skipped URL/byte-based detection and forwarded an unrecognized
type into Claude Code's stream-json `media_type` field — Anthropic rejects
those even when the bytes are a valid PNG/JPEG. The same flaw existed for
base64 sources whose declared `mediaType` was generic.
Introduce `pickImageMediaType(headerType, url, buffer)`: the header value is
preferred only when it's a recognized `image/*` type we know how to extension-
map; otherwise it falls through to URL extension hint → byte-signature sniff
→ raw header → `image/png` final fallback. Applied uniformly to URL fetch,
URL cache hit, and base64 decode paths. Path sources are unchanged (their
"header" is the file extension, which is already authoritative when present).
**P2 — async spawnAgent rejections crashed the CLI**
`spawnAgent` is now async and can reject during image normalization (missing
local `--image` path, fetch failure, decode error). The CLI awaited it
outside any try/catch, so user-input errors surfaced as unhandled rejections
with stack traces instead of the friendly `log.error + process.exit` path
used for prompt validation.
Wrap the `await spawnAgent(...)` in try/catch, log the error message, exit 1
(matching the existing "Stream error from agent process" convention).
**Tests**
- `buildAgentInput.test.ts`: 3 new tests covering octet-stream URL
Content-Type → byte sniff, octet-stream base64 declared type → byte sniff,
generic header + URL extension hint preferred over header.
- `hetero.test.ts`: 1 new test verifying spawnAgent rejection produces clean
`exit(1)` instead of an unhandled rejection.
Manually verified:
`lh hetero exec --image /tmp/does-not-exist.png`
→ `[ERROR] Failed to start agent: ENOENT: no such file or directory…` + exit 1
Refs LOBE-8523.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Enable mobile app to access home.getSidebarAgentList for migrating
SessionList from sessionId to agentId (LOBE-8401).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🌐 i18n: add taskDetail.runAll keys for subtask dependency runner
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(cli): add `lh hetero exec` for standalone heterogeneous agent runs (LOBE-8523 phase 1a)
Phase 1a of LOBE-8516: a Node-side `spawnAgent()` plus the CLI command that
drives it. Standalone-only — no `--topic` / `--operation-id` / no server
ingest. Output is `AgentStreamEvent` JSONL on stdout, one event per line.
Why phase 1a is its own milestone: it lets us validate the producer pipeline
end-to-end (`spawn → JsonlStreamProcessor → adapter → toStreamEvent`) under a
plain Node process, get Device-mode + manual debugging unblocked, and ship
without waiting on phase 2's server `heteroIngest` procedures.
## Shared `spawnAgent({ agentType, prompt, resumeSessionId, cwd, command })`
- Lives in `@lobechat/heterogeneous-agents/spawn`. Pure Node — no Electron, no
image cache, no on-disk tracing, no proxy env composition. Desktop main keeps
its own bespoke spawn path for those host concerns; this minimal version is
what the CLI sandbox + terminal use case needs.
- CC: stream-json stdin format + the established preset flags. Codex: `exec` /
`exec resume` form with `--json --skip-git-repo-check --full-auto`.
- Returns `SpawnAgentHandle` with: async-iterable `events`, `exit` promise,
`kill(signal)` (Unix process-group kill, Windows direct), `pid`, raw `stderr`.
- Internally a single-queue async iterator coordinates between the stdout
listeners and the consumer — keeps backpressure simple, no extra deps.
## `lh hetero exec` command
```
lh hetero exec --type claude-code|codex
[--prompt - | --prompt <text>] # default stdin
[--resume <sessionId>]
[--cwd <path>] # default process.cwd()
[--command <bin>] # default `claude` / `codex`
[--operation-id <id>] # uuid v4 generated if omitted
```
- Reads prompt from stdin when omitted or `-`.
- Forwards child stderr to ours so users see auth prompts / missing-binary
errors.
- Ctrl-C → SIGINT to the child's process group (Unix); a second Ctrl-C
escalates to SIGKILL.
- Exit code passthrough: child code 0/non-0 stays as-is; SIGINT / SIGTERM /
SIGKILL map to POSIX 130 / 143 / 137.
## Out of scope (phase 1b — next PR)
- `--topic` / `--operation-id` flags as REQUIRED + the BatchIngester
- `--render none|jsonl` flag (phase 1a is implicit JSONL)
- trpc `aiAgent.heteroIngest` / `heteroFinish` calls
- Gateway WS interrupt subscription
## Validation
- `bunx vitest run packages/heterogeneous-agents` — 113 passing (8 new
spawnAgent tests + the 105 pre-existing on canary)
- `bunx vitest run apps/cli/src/commands/hetero.test.ts` — 7 passing
(all `--type` / `--prompt` / `--operation-id` / exit-code-passthrough /
SIGINT-mapping branches)
- Real end-to-end: `bun src/index.ts hetero exec --type claude-code --prompt
'Reply with exactly the word HELLO and nothing else.'` produced clean
AgentStreamEvent JSONL (stream_start → 2 stream_chunks → step_complete
turn_metadata → step_complete result_usage → stream_end → agent_runtime_end),
every line stamped with the same auto-generated operationId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(spawn): serialize pipeline pushes so flush waits for in-flight chunks
When stdout emits multiple chunks back-to-back — or `'end'` lands while an
earlier `pipeline.push()` is still awaiting the Codex tracker's filesystem
reads — the per-chunk `.then` handlers ran concurrently. Two consequences:
1. Out-of-order events. Push #2's events could resolve before push #1's,
so the JSONL stream came out shuffled.
2. Late-event loss. `'end'` would call `pipeline.flush()` and immediately
set `streamEnded = true` while prior pushes were still pending. The
async iterator could then return `{ done: true }` before those pushes
queued their events.
Fix: thread every `push()` / `flush()` / error-surface call through a single
`pipelineQueue` `Promise` chain, the same shape the desktop controller uses
for its broadcast queue. `flush()` now reliably runs after every queued
push has drained, so `streamEnded` is the very last write.
Two regression tests cover the failure modes by spying on
`AgentStreamPipeline.push` to inject deterministic delays:
- "preserves event ordering across async pipeline.push() calls" — chunk A
resolves slower than chunk B; without the chain B arrives first.
- "iterator drains slow in-flight pushes before flushing the stream" —
`'end'` fires while a 40 ms push is still pending; without the chain
the iterator returns done before the chunk's events queue.
Bisected: both tests fail without the chain, pass with it. E2E re-smoke
(`bun src/index.ts hetero exec --type claude-code` simple text + tool-using
prompt + stdin) still produces clean ordered JSONL.
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: remove dead Search Summary chain
Footer.tsx in web-browsing Search portal had near-zero usage. Removing it
makes the entire chain dead: triggerAIMessage, summaryPluginContent,
fillPluginMessageContent, saveSearchResult, plus the inSearchWorkflow param
threaded through internal_execAgentRuntime.
Part of LOBE-8519 — clears the path before introducing agentDispatcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add agentDispatcher.selectRuntimeType
Centralizes the client / gateway / hetero routing decision so every entry
point shares one source of truth. parentRuntime override lets sub-agent
dispatches inherit their parent operation's runtime.
Part of LOBE-8519 — call sites are migrated in following commits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route sendMessage through selectRuntimeType
Compute runtimeType once per sendMessage call and dispatch off it instead of
re-deriving the hetero/gateway/client decision inline. Behavior is identical;
this just centralizes the routing rule (LOBE-8519, A1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route regenerate / continue through selectRuntimeType
regenerateUserMessage and continueGenerationMessage in the conversation store
now consult selectRuntimeType for routing. Hetero variants of both are not yet
implemented (they currently fall through to client mode with a TODO + warning).
Also drops chatStore.continueGenerationMessage — the conversation-store version
is the only caller; the chat-store duplicate had zero production usage.
Part of LOBE-8519 (A2, B4 deletion, B5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route resume helpers through selectRuntimeType
approveToolCalling / rejectToolCalling / rejectAndContinueToolCalling now
consult selectRuntimeType (via #shouldUseGatewayResume) using the operation's
own ConversationContext, instead of the bare isGatewayModeEnabled() check.
Behavior is preserved (gateway resume vs. local resume); hetero resume is not
yet implemented and falls through to the client local path.
Part of LOBE-8519 (A3, A4, A5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: route sub-agent dispatch through selectRuntimeType
directMentionRoute and callAgent now consult selectRuntimeType using the
parent agent's config so sub-agent dispatches inherit the parent runtime.
Only the client path is wired today; gateway / hetero variants warn + fall
through with TODOs for follow-up.
Part of LOBE-8519 (B3, B6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor: rename internal_execAgentRuntime to executeClientAgent
Aligns the client runner's name with executeGatewayAgent and
executeHeterogeneousAgent so the three runtimes share a consistent
verb-noun pattern. Pure rename — no behavioral changes; log prefixes
and test mock variables follow the new name.
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(todo-progress): use colorFillSecondary so left/right borders are visible against QueueTray
The colorBorderSecondary stroke nearly vanished against the dark elevated bg, so the TODO card looked open on the sides when stacked under QueueTray. Match QueueTray's outer border token (colorFillSecondary) for a consistent visible seam; inner dividers keep colorBorderSecondary as a softer secondary level.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): extract producer pipeline into shared package
LOBE-8516 phase 0. Move the JSONL framing + adapter conversion + toStreamEvent
chain out of the renderer into a new `@lobechat/heterogeneous-agents/spawn`
entry, then have desktop main run it before broadcasting. Renderer now
consumes ready-made `AgentStreamEvent`s on `heteroAgentEvent`, dropping ~50
lines of in-renderer adapter wiring.
This unifies the wire shape across desktop main, the upcoming `lh hetero exec`
CLI, and the server `heteroIngest` handler — every consumer gets the same
stamped `AgentStreamEvent` with no per-consumer adapter step.
The desktop CC flow is unchanged behavior-wise: same adapter, same persistence
ordering, same step-boundary semantics; only the seam between main and
renderer moved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(hetero-agent): pull codex tracker into shared spawn, drop desktop's gateway-client dep
Two cleanups on top of the phase 0 refactor:
1. Move `CodexFileChangeTracker` (+ its test) out of `apps/desktop/src/main/modules/heterogeneousAgent/` into `packages/heterogeneous-agents/src/spawn/`. `AgentStreamPipeline` now auto-instantiates it when `agentType === 'codex'`, so the desktop controller (and the future `lh hetero exec` CLI) stays agent-agnostic — no more "if codex { wire tracker via transformPayload }" branching at the call site. The public `transformPayload` hook is removed since it had no other consumer.
2. Re-export `AgentStreamEvent` / `AgentStreamEventType` from `@lobechat/heterogeneous-agents/spawn` and drop `@lobechat/agent-gateway-client` from `apps/desktop/package.json`. The gateway-client package is a browser-side WebSocket client; producer-side callers (desktop main, sandbox CLI) shouldn't carry it as a direct dep — they only need the type, which now flows through the producer-side entry.
Type predicate on Codex payloads tightened to a non-`Required<>` shape so the moved file passes the root tsconfig's `strict: true` (apps/desktop's tsconfig was lax).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🧑💻 chore(local-testing): harden electron-dev.sh process management
Lifecycle improvements for the local-testing helper so smoke runs against the desktop dev session are reliable:
- `find_project_pids` now also catches user-started `bun run dev` Electron sessions (matches by project electron path, not just `--remote-debugging-port`), the launcher subshell saved to PIDFILE, and any process bound to the CDP port. Vite match tightened to `electron-vite[/.].*\bdev\b` so unrelated Vite invocations aren't swept up.
- `do_stop` expands seed PIDs into their descendant trees (DFS via `pgrep -P`), SIGTERMs the whole tree, waits 5s, then SIGKILLs survivors. Belt-and-suspenders sweep for stragglers + anything still bound to the CDP port. Closes the long-standing "Helper processes survive the kill" gotcha.
- `do_start` detects existing project Electron/vite before tearing it down so the user sees what's being killed; waits for port + user-data-dir locks to release before relaunching to avoid the "user data directory in use" race.
- `wait_for_cdp` uses an explicit deadline + early bail-out if the launcher PID dies, instead of the previous fixed-step loop. `wait_for_renderer` no longer pre-sleeps 10s.
`setsid` use is intentional; it puts the launched Electron in its own session so the whole tree shares a PGID we can signal in one shot. Note: `setsid` is GNU coreutils — on macOS without `brew install util-linux` the script will fail at the launch step. Documented as a known limitation; no fallback added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): gate session-complete on stdout fully drained
Node may emit `proc.on('exit')` BEFORE child stdio fully closes (documented
in child_process: "stdio streams might still be open"). Phase 0 of LOBE-8516
moved adapter ownership to main, so renderer no longer flushes its own
adapter on session-complete — meaning trailing events synthesized by
`pipeline.flush()` (e.g. Codex's `tool_end` for unfinished tool calls) would
race against, and lose to, the `heteroAgentSessionComplete` broadcast,
leaving renderer-side persistence to finalize on incomplete state.
Fix: in `proc.on('exit')`, await `streamFinished(stdout)` (covers `'end'`,
`'close'`, and `'error'`) BEFORE awaiting the broadcast queue. The first
await ensures the `stdout.on('end')` handler has had a chance to schedule
`pipeline.flush()` onto the queue; the second drains it. Only then do we
broadcast complete / error.
Regression test repros the documented Node race by emitting `exit` before
`stdout.end()` and asserts every `heteroAgentEvent` (including the
synthesized `tool_end` from `pipeline.flush()`) lands before
`heteroAgentSessionComplete`. Bisected: test fails without the gate, passes
with it.
Also: add `packages/heterogeneous-agents` to `apps/desktop/pnpm-workspace.yaml`
to mirror the new workspace dep added in the phase 0 refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(hetero-agent): drop builtin-tool-claude-code dep, inline the 3 CC wire shapes the adapter needs
Phase 0 added `@lobechat/heterogeneous-agents` as a runtime dep of the desktop
main process. That transitively pulled in `@lobechat/builtin-tool-claude-code`
(declared in the shared package's deps), which the desktop pnpm workspace
doesn't list — CI install on the desktop project fails:
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND In ../../packages/heterogeneous-agents:
"@lobechat/builtin-tool-claude-code@workspace:*" is in the dependencies but
no package named "@lobechat/builtin-tool-claude-code" is present in the
workspace
The dep is also a layer-violation: `heterogeneous-agents` is the producer
side (CLI stream → AgentStreamEvent), `builtin-tool-claude-code` is the UI
tool definition (renderers / inspectors / agent template). Producer
shouldn't depend on UI-tool packages, even if today the import is just
types/constants — the dep cascade still drags `shared-tool-ui` etc. into
every workspace that wants the adapter.
Fix: inline the three things the adapter actually uses (`'TodoWrite'` tool
name string, `TodoWriteArgs` interface, `ClaudeCodeTodoItem` interface).
They reflect upstream Claude Code's wire schema — if `claude` ever renames
`TodoWrite`, the adapter and the downstream renderers must both update
regardless of whether they share a constant. Renderer-side packages
(`builtin-tools/codex/TodoListRender`, etc.) keep importing the canonical
`ClaudeCodeApiName` from `@lobechat/builtin-tool-claude-code`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(local-system,cloud-sandbox): drop "Local" prefix from tool names
LLM-facing tool names dropped the redundant "Local" / "LocalFiles" prefix
to shrink manifest/system-prompt token footprint:
editLocalFile→editFile, globLocalFiles→globFiles, listLocalFiles→listFiles,
moveLocalFiles→moveFiles, readLocalFile→readFile,
searchLocalFiles→searchFiles, writeLocalFile→writeFile.
Also removed `renameLocalFile` entirely from the new surface — `moveFiles`
already covers in-place renames by changing only the filename in newPath.
Old long names are still recognised in the rendering path
(client Render/Inspector/Intervention/Streaming registries, placeholders,
workflow display labels, i18n keys) and in Gateway/CLI routing, so
historical messages and older Gateway versions keep working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(local-system): reuse LocalSystemApiName / LocalSystemIdentifier exports
Drop the inline LOCAL_SYSTEM_IDENTIFIER / READ_FILE / LIST_FILES consts in
the snapshot materializer and import the canonical values from the package.
Mark LocalSystemApiName `as const` (matching CloudSandboxApiName) so values
narrow to literal types and satisfy LocalSystemToolSnapshot.apiName.
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(builtin-tools): retire lobe-tools alias and slim lobe-notebook to render-only
- Drop the deprecated `'lobe-tools'` identifier alias from the inspector / render
registries plus its backward-compat checks in dbMessage selectors and the dev
RenderGallery fixtures.
- Hoist the only surviving notebook UI (the `createDocument` document card) into
`packages/builtin-tools/src/notebook/`, mirroring the github tool layout.
Marked the new module `@deprecated` with a ~3-month removal target.
- Delete `packages/builtin-tool-notebook/src/client/` entirely and unregister
notebook from the inspectors / interventions / placeholders / streamings
registries (it can no longer be invoked by the LLM, so those surfaces are dead
code). Manifest / executor / ExecutionRuntime stay so legacy tool calls keep
resolving.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔧 chore(builtin-tools): drop redundant antd peer dep
antd is already provided by the workspace and peered through
@lobehub/ui, so listing it explicitly on builtin-tools is noise.
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(builtin-tools): add UI render for github marketplace tool
Register an Inspector + Render for the marketplace `github` MCP tool
(single `run_command` API that wraps the `gh` CLI). Mirrors the codex
pattern under packages/builtin-tools/src/github/.
- Inspector: GitHub brand chip with the parsed gh subcommand and a
success/error indicator after the call resolves.
- Render: ToolResultCard with the full gh command (sh-highlighted) and
the output, auto-detected as JSON for `gh api` / `--json` calls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-tools): add inspector renders for moveLocalFiles and exportFile
Cloud-sandbox and local-system both expose moveLocalFiles, and cloud-sandbox additionally
exports exportFile, but none of these had inspector components registered, so the title
area in tool calls fell back to the default loading text. Add a shared
createMoveLocalFilesInspector factory and a cloud-only ExportFileInspector, then wire them
into both packages' inspector registries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(builtin-tools): drop redundant "GitHub:" prefix in github inspector
The chip already shows the GitHub icon and a `gh` prefix next to the subcommand,
so the leading "GitHub:" text was duplicating that signal. Always render the chip
(even when no subcommand has streamed yet) and remove the now-stale margin and
streaming-only branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(builtin-tools): hoist gh prefix out of github inspector chip
Move the literal `gh` text to plain leading copy with the GitHub icon as a separator,
and let the chip carry only the gh subcommand (e.g. `api /repos/...` or `search code ...`).
Reads more like the actual command and lets the verb stand out as the chip's first token.
Also seed a github run_command fixture in /devtools so the chip layout is preview-able.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(builtin-tools): hoist github icon out of chip too
Move the GitHub icon next to the literal `gh` prefix so the chip carries only the
gh subcommand (api /repos/..., search code ..., etc.). Reads as: [icon] gh [chip].
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(server,task): batch run subtasks in dependency order
Adds a "Run all" entry on the subtasks panel that kicks off the first
dependency layer; subsequent layers fire automatically as upstream tasks
complete. Layer planning (Kahn topo sort + cycle detection) lives in a
new TaskGraphService and runs server-side via two TRPC procedures.
Also fixes a pre-existing bug where `task.updateStatus(completed)` was
flipping unlocked dependents to `running` without ever invoking the
runner — leaving them in a phantom running state with no topic in
flight. Cascade now goes through TaskRunnerService.cascadeOnCompletion
from all three completion paths (TRPC updateStatus, brief approval,
judge auto-pass), so dependency chains advance end-to-end on their own.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(server,task): preserve edges to in-flight and out-of-scope upstreams
The graph used to drop any dependency edge whose upstream wasn't in the
runnable set. That silently freed two correctness-breaking cases:
- A backlog subtask that depends on a *running / scheduled* sibling
landed in layer 1 and got kicked off before its blocker finished.
- A descendant that depends on a task *outside the current subtree*
(allowed by the schema) lost its blocker entirely and ran prematurely.
Edges are now classified per dependency: terminal-OK upstreams drop the
edge; in-batch runnable upstreams keep their in-degree contribution; any
other status — in-flight, runnable but out of scope, or unknown — marks
the dependent as `blockedExternally` and excludes it from the layered
plan. External blockage propagates transitively through in-batch edges
so we never run a downstream of a blocked task either. `planForParent`
fetches statuses for cross-scope upstreams so the classifier has real
data to decide on.
The UI surfaces the new bucket via `RunSubtasksPreview` and keeps the
modal open (with the run button disabled) when a plan has nothing to
start but does have blocked tasks worth explaining.
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(gateway): complete local op on auth_failed to unstick input loading
When the gateway client receives `auth_failed` (server has GC'd the op or
the refreshed JWT no longer matches), the local op stayed `running`
forever — input kept the stop button, and `topic.metadata.runningOperation`
never cleared, so every revisit re-fired the same broken reconnect.
Treat `auth_failed` as session-terminal alongside `session_complete` so
`onSessionComplete` fires and `completeOperation` runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(gateway): support recoverable auth_expired with token refresh
When the JWT expires while the operation is still alive on the server,
sending `auth_failed` is wrong — the op is fine, only the credential
went stale. Treat that as a separate, recoverable signal instead.
Server (agent-gateway repo) emits a new `auth_expired` message and
keeps the WebSocket open. The client refreshes its JWT (via the
existing `aiAgentService.refreshGatewayToken`), updates the in-flight
client, and reconnects. `auth_failed` stays terminal for cases where
the op truly no longer exists.
Mirrors the device-gateway-client pattern (`auth_expired` event +
`updateToken` + `reconnect`). If no `tokenRefresher` is wired in (or
the refresh itself fails), we fall back to terminal so the input
doesn't stay stuck on the loading state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): disconnect ws on auth_expired without tokenRefresher
The server keeps the WebSocket open after `auth_expired` (so the client
can refresh and re-auth on the same connection). When no `tokenRefresher`
is wired in, we mark the local op complete but were leaving the socket —
heartbeat and autoReconnect kept running indefinitely after the op was
gone, leaking background connections.
Mirror the refresh-failure branch and call `client.disconnect()` before
firing onSessionComplete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(gateway): make tokenRefresher required on connectToGateway
Both real callers (executeGatewayAgent + reconnectToGatewayOperation)
already supply a refresher built from `aiAgentService.refreshGatewayToken`,
and there's no scenario where a Gateway op runs without a topic to refresh
against. The optional path was carrying its own foot-gun (socket leak if
forgotten) and a defensive ternary on `result.topicId` that the type
already rules out.
Required-only collapses both into the existing refresh-failure branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(gateway): inline token refresh, take topicId instead of refresher
Both callers of connectToGateway built identical refresher closures over
`aiAgentService.refreshGatewayToken(topicId)`. Pass `topicId` directly and
let connectToGateway call the service inline — gateway.ts already imports
aiAgentService for the cancel-handler path, so no new coupling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 chore(gateway): rewrite stale auth_expired comment
The "no refresher provided" branch is gone — fold that case out of the
comment and explain why the catch branch needs explicit disconnect().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): add max(50) pagination cap to file.recentFiles and file.recentPages
Fixes GHSA-jr3g-w7rp-fhm9: unbounded limit parameter in recentFiles
and recentPages endpoints allowed authenticated users to trigger
arbitrarily large DB queries (amplified 3x before the DB call).
Adds .max(50) Zod constraint to cap both endpoints at 50 items.
* fix(security): add pagination caps to topic.getTopics, rankTopics, recentTopics
Fixes GHSA-jr3g-w7rp-fhm9:
- getTopics.pageSize: .max(100)
- rankTopics input: .max(50)
- recentTopics.limit: .max(50)
* fix(security): add pagination caps to session.getSessions and rankSessions
Fixes GHSA-jr3g-w7rp-fhm9:
- getSessions.pageSize: .max(100)
- rankSessions input: .max(50) (multi-JOIN aggregate query)
* fix(security): add max(100) pagination cap to agent.queryAgents
Fixes GHSA-jr3g-w7rp-fhm9: unbounded limit parameter in queryAgents
allowed resource exhaustion via arbitrarily large DB queries.
* fix(security): add max(100) pagination cap to document.queryDocuments
Fixes GHSA-jr3g-w7rp-fhm9: unbounded pageSize parameter in queryDocuments
allowed resource exhaustion via arbitrarily large DB queries.
* 🐛 test(web-crawler): remove zhihu test cases after rule removal
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(builtin-tool-task): add Inspector + Render, batch createTasks/runTasks
Adds chip-style Inspector and per-API Render to the lobe-task tool, plus two
batch APIs (createTasks, runTasks) so an agent can plan or launch a set of
subtasks in a single call instead of calling createTask/runTask N times.
runTask/runTasks call taskService.run, actually triggering TaskRunnerService
and producing a topic+operationId — distinct from updateTaskStatus(running),
which only flips a flag. The system prompt now spells this out so the model
stops conflating the two. Already-running, missing-assignee, and per-item
failures surface back to the agent with clear messages.
Fixes LOBE-8438
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(server,task): implement createTasks/runTask/runTasks in server runtime
The manifest exposes these APIs to the model, but only the client-side
executor was implemented. Server-side tool execution (src/server/services/
toolExecution/builtin.ts) throws "Builtin tool ... is not implemented" when
the runtime is missing a method, so production paths that route through the
server runtime would fail at runtime.
- Extracted createTaskImpl as a reusable closure so createTasks loops can
reuse the parent-resolution + assignee-validation flow without copy-paste
- runTask / runTasks call taskCaller.run(...) which already routes to
TaskRunnerService — same execution path as the UI/CLI run buttons
- runTasks continues past per-item failures and reports them in the summary
(matching the client executor's behavior)
- Added 7 tests (20 total in this file) covering happy path, per-item
failure, missing identifier, and current-task fallback
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task-drawer): hide topic feedback input until run terminates
Feedback can only steer the next run, so showing the input while the
topic is pending/running was misleading — gate it on terminal status
(completed/failed/canceled/timeout).
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(builtin-tool-agent-documents): wire Inspectors into registry, switch to chip UI
The Inspector components for lobe-agent-documents existed but were never
registered in packages/builtin-tools/src/inspectors.ts, so the chat UI fell
back to the default "(id:316c6ad5-10e7-46ff-8ccf-15f2359c19...)" header
that shows raw param dumps. Registering them is the root fix.
While in there, refactored all 9 inspectors to the chip pattern used by the
other builtin tools — full UUIDs are noisy in a one-line header, so document
ids are truncated to their first 8 chars (prefixed ids like agd_… are left
intact since they're already short). Each inspector now surfaces the most
useful per-API context: title chip when known (Read/Create), id chip + new
title (Rename/Copy), op count + success ratio (Modify), char count
(Replace), target scope + doc count (List), rule type (UpdateLoadRule),
red dashed line-through (Remove). Shared chip styles live in one
_styles.ts so the visual language stays consistent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 📝 docs(.agents/skills): add builtin-tool skill
Self-contained reference for building/extending lobe-* builtin tools —
SKILL.md entry point plus architecture / tool-design / ui deep-dives.
Sits alongside the other agent skills.
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: sync DEFAULT_MODEL/DEFAULT_MINI_MODEL into desktop business-const stub
#14379 moved DEFAULT_MODEL and DEFAULT_MINI_MODEL into @lobechat/business-const,
but the desktop workspace stub at apps/desktop/stubs/business-const wasn't
updated, breaking the desktop client build with MISSING_EXPORT errors.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add feedback input at bottom of TopicChatDrawer (LOBE-8441)
Mount a comment box inside the Topic Run drawer so users can leave
feedback and trigger a follow-up topic run without leaving the drawer.
Send button calls addComment then runTask (without continueTopicId, so
a brand-new topic is started instead of resurrecting the completed one).
Existing AgentTaskDetail/CommentInput is untouched — the new component
lives next to TopicChatDrawer and stays separate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: close TopicChatDrawer after submitting feedback
Closing the drawer once the comment is persisted and the new run is
kicked off matches user expectation — leaving it open made it look
like the existing topic was the one being run again.
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(document-modal): show skeleton for title while document is loading
Replace the "Untitled" placeholder and AutoSaveHint with a skeleton in both the modal header and the in-page title editor while the document is still being fetched, so the empty fallback no longer flashes before content arrives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-detail): add run-now dropdown next to cancel-schedule button
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-artifacts): show created time and sort newest first
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔨 chore(release-template): drop Highlights from db-migration changelog
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔨 chore(release-template): drop version numbers from changelog templates
Patch releases auto-bump on merge, so the version isn't known when the
changelog is authored. Replace `# 🚀 LobeHub v<x.y.z> (YYYYMMDD)` with
`# 🚀 LobeHub Release (YYYYMMDD)` in all changelog examples and the
GitHub Release Changelog Template inside SKILL.md, and replace the
hard-coded `Since v...` / `Full Changelog: v...v...` lines in the
weekly-release example with the same `<previous-tag>` placeholder
already used by the SKILL.md template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Automatic sync from main to canary. Merge conflicts detected.
**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260501-25207007930
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.
Keep canary-side logic in useSend (active home agent), feedback action
planner procedure-state, useSend test mocks, and e2e Home chat-input
step. The main-side blocks referenced removed symbols and outdated
action-planning code that would break compile/tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): always synthesize a brief on scheduled-task ticks
Heartbeat ticks remain mid-loop nudges and are still skipped, but
schedule-mode tasks now bypass both the trivial-content rule gate and
the LLM emit-vote so each scheduled run produces a daily brief.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(taskDetail): switch activity timestamps to absolute date once gap exceeds one day
Adds formatActivityTime helper to @lobechat/utils/time: relative phrasing
under 24h, localized date (e.g. "4月29日" / "Apr 29") afterwards, with the
full datetime exposed via the native title attribute on hover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): fork chainGenerateBrief prompt so scheduled ticks always produce a brief
The default prompt instructs the LLM to pair `emit=false` with an empty
title, so even after we bypassed the emit-vote for scheduled tasks the
downstream `!title || !summary` guard could still drop the brief and
silently break the "every schedule tick must produce a brief" contract.
chainGenerateBrief now takes a forceEmit flag; when true it swaps to a
scheduled-tick prompt that removes the skip branch and mandates a
non-empty title/summary, including the "no new activity today" path.
synthesizeTopicBrief passes forceEmit=true for schedule-mode tasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update @google/genai version to ~1.50.1
* 💄 style(conversation): stack TodoProgress + QueueTray as a floating overlay above ChatInput
Move TodoProgress out of normal flow and render it together with QueueTray
inside ChatInput as a single absolute-positioned overlay anchored to the
input's top edge. The overlay no longer pushes ChatList up; instead it sits
as a "cover layer" above the scroll viewport.
To keep chat content reachable above the overlay, expose the overlay's
measured height via the conversation input store (ResizeObserver in
ChatInput) and have VList consume it as `paddingBottom = max(24, height +
12)` — the +12 compensates for ChatInput's `marginTop: -12`. BackBottom
also reads the same height via a new `bottomOffset` prop so the
back-to-bottom button lifts above the overlay instead of being occluded.
QueueTray sits on top, TodoProgress below; TodoProgress squares its top
corners (`topAttached`) when QueueTray is present so the two panels fuse
into a clean stack with no notches at the seams.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(utils): make formatActivityTime title assertion timezone-independent
The test hardcoded `2026-05-01 13:00:00` (UTC+8 author tz), so it failed in
UTC CI as `2026-05-01 05:00:00`. Derive the expected title via the same
dayjs format the implementation uses so the assertion holds regardless of
the runner's timezone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test(conversation): include chatInputOverlayHeight in store mock state
The store always initializes chatInputOverlayHeight to 0 via inputInitialState,
so the State type rightly keeps it required. The selectors test mock simply
missed the field after the slice gained it; supply 0 to match the real
initial state instead of weakening the type to optional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(brief): split judge from generate, persist decision on task topic
Split the brief-emission flow into two independent stages so judgment and
copy-generation are no longer entangled in a single LLM call (which made
the scheduled-tick fork necessary in the first place).
- Rule layer (`shouldEmitTopicBrief`) goes three-state: `'yes' | 'no' |
'unknown'`. Conclusive cases (error / review-handled / review-configured
/ heartbeat / trivial-non-scheduled / scheduled) bypass the LLM entirely;
only manual + non-trivial topics fall through to `'unknown'`.
- New `chainJudgeBriefEmit` (small chain, returns `{emit, reason}`) is
invoked ONLY on the `'unknown'` branch. Title/summary copy is no longer
in scope for this call.
- `chainGenerateBrief` drops the `forceEmit` fork and the `emit` field —
it now assumes the caller has already decided to emit and just produces
`{title, summary}`. Saves tokens on skip paths since we never draft copy
for a brief that won't be persisted.
- Every decision (rule or LLM) is persisted to
`taskTopics.handoff.briefDecision` via a new `updateBriefDecision` model
method using `jsonb_set + COALESCE` so existing handoff fields aren't
disturbed. Gives operators a per-topic audit trail of why a brief was
or wasn't produced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(brief): emit on errors, defer heartbeat to LLM judge
Two follow-up tweaks to the rule layer (`shouldEmitTopicBrief`):
- `reason === 'error'` is no longer a hard skip — the user must be told the
run failed. Returns `{emit: 'yes', reason: 'execution-error'}` so once
the error path is folded into `synthesizeTopicBrief` (separate
consolidation refactor) the verdict is correct without further changes.
Currently dead code: `onTopicComplete` still builds an urgent error
brief inline at the `else if (reason === 'error')` branch.
- Heartbeat ticks change from a hard `'no'` to `'unknown'`. Most ticks are
mid-loop noise but the occasional one warrants surfacing, and only the
LLM can read the content to tell. Heartbeat is at minimum 10 min so the
added judge call per tick is acceptable.
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(skill): skip OAuth redirectUri on desktop to prevent broken app:// navigation
On desktop (Electron), window.location.origin is app://renderer which the system browser cannot navigate to. Skip passing redirectUri so market shows a default success page instead, relying on existing window-close monitoring and fallback polling to detect OAuth completion.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): tokenize mdfind keywords, scope glob to home, align tool prompts
- mdfind treats free-form keywords as a single literal substring; "LobeHub
Financial Statement" never matches "Financial_Statement_LobeHub.pdf".
Split on whitespace and AND each token (still substring-matched) so
ordering doesn't matter.
- Unix/Windows glob fell back to process.cwd() — meaningless inside a
packaged Electron app. Default to os.homedir() instead so unscoped
patterns can actually find user files.
- systemRole/systemRole.desktop documented `query`/`onlyIn`/`path` for
searchLocalFiles/grepContent/globLocalFiles, but the manifest exposes
`keywords`/`scope`. The wrong names were silently dropped, so the LLM
could never scope its searches. Aligned the prompts with manifest and
noted the new keyword-tokenization semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): preserve glob/grep error in tool message content + tidy file row UI
Two independent bugs that combined to break Glob/Grep tool messages and
then made search hits look ugly in the result list.
Empty `content` on glob failure
- LocalSystemExecutionRuntime.normalizeResult dropped `raw.error` when
mapping `globLocalFiles`/`grepContent`, so a failure from the IPC layer
(e.g. fast-glob throwing EACCES while traversing the wrong cwd) became
`{ result: {...}, success: false }` with no error attached.
- ComputerRuntime.errorOutput then did
`result.error?.message || JSON.stringify(result.error)`. With error
undefined that yields the value `undefined` (not the string), which
collapsed into `content: ""` downstream — the chat store still saved
`pluginState` so users saw a tool message with state set but the
Response panel completely blank.
- Propagate `raw.error` through normalizeResult and harden errorOutput
with a "Tool execution failed" fallback so the LLM and the debug panel
always get a real string.
Search results layout
- FileItem stacked filename and a redundant full path on a single
baseline-aligned row, so the path column repeated the filename and
felt visually off-balance.
- Switch to a two-line layout: filename on top, parent directory only
(collapsed via displayRelativePath when available) underneath, both
vertically centered against the file icon.
- Promote the "open containing folder" action from hover-only to a
permanent right-side button so it's reachable in one click.
- Bump the SearchFiles scroll container so the taller rows still show a
reasonable number of hits before scrolling, and add a Downloads-style
fixture to the dev panel render gallery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-system): harden executor toResult to never emit empty content and to keep state on failure
The earlier fix patched normalizeResult and ComputerRuntime.errorOutput,
but the central funnel where every executor return is shaped —
LocalSystemExecutor.toResult — still trusted the runtime output blindly:
- the success=false branch dropped `state` entirely, which meant any
partial pluginState a runtime had built up was thrown away the moment
it reported an error (renderers then re-rendered as if the call had
produced nothing).
- both branches passed `output.content` through verbatim, so an
upstream regression that forgot to populate content (the recent Glob
EACCES path) would still surface as a blank Response panel.
Make toResult the strict gate it claims to be: derive a non-empty
content from `output.content -> output.error.message -> "Tool execution
failed"`, and always propagate `state` regardless of `success`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 chore(devtools): sanitize searchLocalFiles fixture to use synthetic data
Replace real-looking filenames, paths and corporate identifiers in the
RenderGallery fixture with neutral sample-user / sample-quarterly-report
placeholders. The fixture is checked into the repo and shipped to every
contributor's dev panel — it shouldn't carry data that resembles a
specific person's Downloads/iMessage/WeChat layout.
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(brief): keep recurring tasks active when resolving their result briefs
Approving a `result` brief on a recurring (`automationMode='schedule'`)
task was flipping the parent task to `completed`, which removed it from
the active board and stopped future scheduled runs from surfacing on it.
A daily brief is one occurrence — accepting it is a UI dismissal, not a
lifecycle terminal.
The discriminator is the **task's** automation mode, not the brief's
`cronJobId`. A manual run of a recurring task has `cronJobId=null` but
the task is still recurring, so a cronJobId-based check would let that
case slip through.
- Server: `BriefService.resolve` now loads the task and only completes
it when `automationMode !== 'schedule'`.
- Server: `enrichBriefsWithAgents` also batches the task lookup and
exposes `taskAutomationMode` on the listed briefs so the UI can label
the action correctly without an extra round-trip.
- UI: the result action label switches to "Mark as resolved" /
"标记为已解决" when `taskAutomationMode === 'schedule'`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): unify result brief action to "Confirm" and key off task status
Replace the dual confirmDone/markResolved labels with a single brief.action.confirm,
and gate task completion on task.status !== 'scheduled' so heartbeat-mode tasks
parked between ticks are also kept active when one of their result briefs is
approved.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): restore "Confirm complete" for terminal-accept; "Confirm" only for status='scheduled'
Bring back brief.action.confirmDone alongside the new brief.action.confirm.
The dual-label discriminator is the parent task's runtime status: tasks parked
at 'scheduled' show "Confirm" (dismiss-only — server keeps them active for the
next tick), all other states show "Confirm complete" since approving will flip
the task to completed. Server keeps its task.status !== 'scheduled' guard.
Threads taskStatus on BriefItem / BriefWithAgents (replacing the previously
removed taskAutomationMode) so the UI label matches the actual server effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): make BriefItem.taskStatus optional for locally-constructed briefs
TaskActivities.tsx builds a BriefItem from a TaskDetailActivity row and has no
task-status info to pass through. Marking the field optional matches the prop
shape on BriefCardActions and lets the activity feed compile again.
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(followUpAction): add shared types and JSON schema for follow-up chip extraction
* 🐛 fix(followUpAction): tighten JSON schema literal types with top-level as const
* ✨ feat(followUpAction): add base + onboarding prompt builders
* ✨ feat(followUpAction): add server service to extract chips via fast LLM
* 🐛 fix(followUpAction): drop empty chips and consolidate schemas in schema.ts
* ✨ feat(followUpAction): expose extract via lambda TRPC router
* ✨ feat(followUpAction): add client service wrapper around TRPC mutation
* ✨ feat(followUpAction): add zustand store with abort/timeout actions
* 🐛 fix(followUpAction): stabilize empty selector ref and abort on reset
* ✨ feat(followUpAction): add FollowUpChips component with reply icon style
* ✨ feat(followUpAction): add onboarding glue hook with phase/greeting guards
* ✨ feat(followUpAction): wire chips + glue hook into onboarding conversation
* 🐛 fix(followUpAction): drop unused eslint-disable directive in client service
* 🐛 fix(followUpAction): tighten types and align prompt with schema bounds
* 🐛 fix(followUpAction): use fresh phase for chip extraction across phase boundaries
* 🐛 fix(followUpAction): type SUGGESTION_RESPONSE_JSON_SCHEMA against GenerateObjectSchema
The earlier `as const` widened to readonly literal types, which is incompatible
with the mutable `GenerateObjectSchema` interface required by `generateObject`.
Replace with an explicit type annotation so the literal is checked at definition
and stays assignable at the call site.
* ⚡️ perf(followUpAction): only refresh user/agent caches at onboarding phase boundaries
The previous logic refreshed both useUserStore and the webOnboarding builtin
agent after every assistant turn, but their content only changes when the
phase advances or onboarding finishes. Compare prev vs next phase/finishedAt
from syncOnboardingContext and skip the two refresh calls when neither moved,
saving an RPC per intra-phase turn.
* 🐛 fix(followUpAction): read finishedAt from agentOnboarding subobject
* ♻️ refactor(followUpAction): take agentId from caller and resolve model from agent config
Drops the env-var override path on the server. The service is meant to be
generic across consumers, so the caller now passes the agentId of the
conversation context. The service resolves model/provider from
AgentModel.getAgentConfigById, falling back to DEFAULT_SYSTEM_AGENT_CONFIG.topic
when the agent has no explicit model. The onboarding caller passes the
webOnboarding builtin agent id; future consumers pass theirs.
* 🐛 fix(followUpAction): resolve latest text assistant message server-side via topicId
* ✨ feat(followUpAction): mirror assistant language and ban deferral chips
Two prompt rule changes:
1. Match the assistant message's language instead of forcing English. The
chip should be in the script the user would naturally reply in.
2. Prefer questions with explicit options when the message contains
several, and ban "Let me think / Skip / You decide / Let me explain"
style escape-hatch chips entirely. Every chip must be a concrete
reply the user might actually send; the user can always type
freely, so meta deferral chips just waste a slot.
* 🐛 fix(followUpAction): bump timeout to 20s and silence TRPC-wrapped abort
The previous 3s timeout aborted the LLM call before generateObject could
respond — a typical extract round-trip is ~10s. Bump to 20s.
Also silence the TRPCClientError that wraps the abort: TRPC re-throws
DOMException as TRPCClientError("signal is aborted ..."), so the
original `instanceof DOMException` check missed it and noise
`[FollowUpAction] extract failed` warnings hit the console on every
manual clear / new turn. Now we also short-circuit on `signal.aborted`.
* feat: enhance chat input functionality with new flags
- Added `disableMention` and `disableSlash` props to `ChatInput` and `StoreUpdater` to control mention and slash command triggers.
- Introduced `disableFollowUpVariant` and `disableQueue` props to manage placeholder behavior and message queuing during agent streaming.
- Updated `FollowUpChips` to handle topic IDs and prevent rendering during message generation.
- Refactored onboarding context retrieval to streamline fetching of user persona and state.
- Removed deprecated onboarding state API references and adjusted related tests.
- Improved follow-up action handling to discard stale results based on active request controllers.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat: enhance agent marketplace onboarding with summaries and improved state management
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 💄 style(brief): use Footprints icon and hide view-run until card hover
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): swap icon to Workflow for the View run shortcut
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(local-system): forward all search params and guard empty mdfind
- Pass through all resolved searchFiles params (keywords, fileTypes,
date range, scope, etc.) instead of dropping everything except
`directory`, which previously caused the executor to call mdfind
with no query.
- Surface missing fields (`keywords`, `fileTypes`, `contentContains`,
date range, sort, etc.) on `SearchFilesParams` so the cross-runtime
type matches the actual contract.
- Short-circuit Spotlight search when there is no query expression so
mdfind doesn't print its usage text and get parsed as phantom file
hits, and drop unstattable rows instead of fabricating 0-byte
placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(skills): guard empty command and forward description in desktop execScript
Desktop skills' execScript dropped `description` before IPC, so when an LLM tool call arrived without `command` (aborted stream, empty args, etc.) the runner crashed on `command.slice(0, 50)` and surfaced as "Failed to execute command: ...".
- runner.ts: return a proper error result when `command` is missing instead of throwing
- lobe-skills.desktop.ts: forward `options.description` to localFileService.runCommand for better logs and as a fallback when command is absent
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(local-system): show empty state when file search returns no results
Previously the SearchFiles result panel rendered an empty Flexbox when there were 0 hits, leaving the area visually blank below "Number of searches: 0". Reuse the same Block + Empty pattern as web-browsing search and the existing `search.emptyResult` i18n key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(local-file-shell): expand leading ~ in file operation paths
Node fs APIs don't expand `~` like a shell would, so paths supplied by
the LLM or pasted by users were failing with ENOENT. Apply expandTilde
across read/write/edit/move/rename/list/glob/grep/search and the desktop
search controller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(local-system): show empty state when listed directory has no files
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: inject skill instruction into tool system role
Consume the `instruction` field from market SDK's `listTools` response
and pass it as `systemRole` on the tool manifest, so the LLM receives
skill-level guidance documentation via `<tool.instructions>` in the
system prompt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: update market-sdk
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): open run topic drawer from daily brief card
Adds a "View run" shortcut to the brief card's actions row that opens
the corresponding topic chat drawer in place on the home page, so the
user can inspect the agent's actual run without navigating to the task
detail page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🌐 i18n(brief): refine zh-CN copy for view run action
"查看执行" was ambiguous (could read as "execute"); use "查看运行轨迹"
to make it clear the action opens the agent's actual run trace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⚡️ perf(agent,working-sidebar): cut Review tab open latency ~9× on large dirty trees
Two changes that together drop "open Review tab" from ~1.7s to ~190ms on a
working tree with 200+ dirty files:
- GitCtr.getGitWorkingTreePatches: replace N-parallel `git diff` subprocesses
with one bulk `git diff HEAD --` for tracked files (split per-file in JS) and
direct `fs.readFile` synthesis for untracked. Eliminates the main-process
fork storm and `.git/index` lock contention. IPC drops 635ms → ~160ms.
- Review/index.tsx: replace default-expand-all with a size budget
(≤100KB cumulative patch OR 50 files). Caps Shiki tokenizer cost on first
paint and removes the 1064ms renderer freeze; small-diff workflows still
get 50 panels open, big-refactor workflows clamp to 2–3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent,working-sidebar): handle special-char paths and bulk diff overflow
Address two P2 review issues on the perf refactor (#14338):
- Quote untracked paths in synthetic diff headers. Direct interpolation of
entry.filePath into `diff --git` / `+++` lines emitted malformed headers
for filenames containing TAB / LF / CR / quote / backslash, causing the
patch parser to choke (e.g. TAB-containing names triggered "bad git-diff -
inconsistent new filename"). New quoteGitPath mirrors git's own
quote_c_style: prefix lives inside the quotes, control bytes get octal
escapes. Plain ASCII spaces stay unquoted to match git's output.
- Replace fixed-buffer bulk diff with streamed spawn + per-file fallback.
The 64 MB execFile maxBuffer would reject the entire bulk diff on
overflow, leaving every tracked file as an empty placeholder. Now bulk
output streams via spawn (no ceiling), salvages partialStdout on failure,
and routes any uncovered tracked entry through fetchTrackedPatchPerFile
with concurrency 8 — restoring the per-file truncation/binary handling
the original implementation had.
Adds GitCtr.test.ts covering quote/dequote round-trips for the problem
characters the reviewer called out.
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(brief): show artifacts in card and extract DocumentModal
Wire `brief.artifacts` (already populated by topic-brief synthesis) into
TaskBriefCard and the home BriefCard so completed-topic deliverables
show up inline; clicking a doc card opens it in a modal.
The per-task PageModal becomes a reusable `DocumentModal` (props-based:
documentId/open/onClose), and the preview trigger state moves from task
store to a new `preview` slice in document store — any surface can now
call `useDocumentStore.openDocumentPreview(id)`.
Also:
- PageAgentPanelOverrideProvider: ephemeral right-panel state for
PageEditor in transient surfaces (modal); defaults collapsed and
doesn't write the persisted global preference.
- PageEditor.fullWidthHeader: layout flag so the modal's header spans
both columns instead of the left pane only.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(shared-tool-ui): unify label-to-content spacing in file inspectors
Replace trailing-space spacing with explicit 6px marginInlineEnd on the label
span in Read/Edit/Write/List inspectors so they match the 6px gap already used
by chip-based renderers (Bash, Grep, Glob).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): clear preview state on document modal teardown
`previewDocumentId` is global (`useDocumentStore`) and the modal opens on
any truthy value. Without cleanup, navigating away with the modal open
left a stale id behind, and the next surface that mounted a preview
modal (e.g. /home daily brief) would immediately reopen the old doc.
Extract a `<DocumentPreviewModal />` connector that resets the preview
state on unmount, and use it everywhere the global preview should be
rendered (TaskDetailPage, DailyBrief). Future mount points get the
cleanup for free.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): coerce globalExpand to boolean in panel control hook
`systemStatusSelectors.showPageAgentPanel` returns `boolean | undefined`
(zenMode short-circuit ANDs with an optional flag), but
`PageAgentPanelControl.expand` is `boolean`. Coerce with `!!` so the
non-override branch satisfies the type.
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(mobile-router): add task and brief routers to mobile tRPC router
Expose task and brief endpoints to the mobile client so the React Native
app can manage tasks and daily briefs via the same tRPC contract used by
the web client.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Automatic sync from main to canary. Merge conflicts detected.
**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260429-25113686179
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.
* 🐛 fix(model-runtime): preserve LLM finishReason through callbacks transformer
Soft interrupts from providers (Gemini RECITATION / MAX_TOKENS, etc.)
emit a `type: 'stop'` chunk carrying the finishReason string, but
`createCallbacksTransformer` was only using it as a terminal-event flag
and never aggregating the value. Downstream the `OnFinishData` payload
had no `finishReason` field, so RuntimeExecutors recorded an `llm_result`
event without it — the harness silently rendered an empty assistant
message even though tokens were billed.
Capture the value in the callbacks aggregator, surface it on
`OnFinishData`, and write it into the `llm_result` tracing event so
soft-interrupt cases are diagnosable.
Fixes LOBE-8403
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): keep first finishReason across multi-stop streams
Anthropic emits two `'stop'` chunks per stream — `message_delta` with
the real `stop_reason` (`end_turn` / `max_tokens` / `tool_use`) followed
by a `message_stop` sentinel. Last-write-wins clobbered the meaningful
reason with the sentinel string, defeating the very tracing signal this
fix is meant to provide.
Switch to first-non-empty-wins so the real provider reason survives.
The empty-string fallback covers cases where an early provider chunk
arrives before the reason is known.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
**Hotfix Scope:** Topic preservation across cold chat-entry routes
> Keeps newly created Topics visible when a first message is sent before
the destination chat route has fully hydrated.
- **Page Agent empty-session regression** — Sending the first message in
an empty Page Agent panel no longer clears the newly created Topic and
returns the panel to an empty state. (Resolves LOBE-8351)
- **Home cold-route send regression** — Sending from the Home default
Chat Input now routes to the newly created Inbox Topic even when
`/agent/:aid` has never been opened and the route chunk has no warm
cache.
- **Page-scoped Copilot consistency** — Page Copilot and File Copilot
share the same provider-level topic reset behavior, so stale Topics are
cleared only when entering or switching the scoped Agent.
- **Regression coverage** — Added focused unit coverage for Home default
sends, route parity coverage remains intact, and added an E2E scenario
for the no-cache Home send path.
- `bunx vitest run --silent='passed-only'
'src/routes/(main)/home/features/InputArea/useSend.test.ts'
'src/spa/router/desktopRouter.sync.test.tsx'
'src/routes/(main)/agent/features/Conversation/ChatHydration/index.test.tsx'
'src/routes/(main)/agent/_layout/AgentIdSync.test.tsx'`
- `BASE_URL=http://localhost:3007
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres bun
run test -- --tags '@HOME-CHAT-COLD-001'` from `e2e/`
- Self-hosted: pull the new image and restart. No schema or environment
changes.
- Cloud: ships through the normal hotfix deployment after merge.
@Innei
Fixes LOBE-8351
* ✨ feat(creds): add local/desktop credential injection guidance
Teach AI how to use credentials in non-sandbox (desktop/local) environments via
getPlaintextCred + runCommand inline env vars, alongside the existing sandbox flow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 🔒 fix(creds): use runCommand env param for secure credential passing
Inline secrets in the command string would be visible in the Intervention UI
and logs. Use runCommand's env parameter instead, and correct the misleading
file credential guidance (getPlaintextCred returns a fileUrl, not a local path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
✨ feat(agent,working-sidebar): add Review tab with bulk git working-tree diffs
Adds a Codex-style Review tab to the agent working sidebar (peer to the
existing Resources content, surfaced as Space). When the active topic has a
working directory bound, the sidebar shows two chip-style tabs — Space (left)
and Review (right) — and the Review pane lists every dirty file with its
unified diff rendered via PatchDiff.
A single new IPC method `git.getGitWorkingTreePatches(dirPath)` enumerates
the working tree once via `git status --porcelain -z`, then runs every
per-file `git diff` in parallel inside main; tracked entries hit
`git diff HEAD -- <file>` while pure untracked files use
`git diff --no-index /dev/null <file>`. Each patch is capped at 256 KB and
classified into added / modified / deleted with additions/deletions counts
parsed off the patch text, so the renderer needs exactly one round trip and
zero per-file fetches.
The Review pane defaults to all files expanded, with PatchDiff render gated
on the panel's expanded state so collapsed entries don't pay the shiki
highlight cost. Adds a unified/split viewMode toggle in the Review subheader,
shows an Unstaged-N chip alongside it, and ships a custom small expand caret.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
💄 style(daily-brief): add skeleton loading state for DailyBrief component
LOBE-8400
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat(task): add start-scheduling button in automation popover
Lets users mark a configured task as "scheduled" without firing an
immediate run, so the cron/heartbeat tick owns the first execution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): hide start-scheduling button in heartbeat mode
Heartbeat tasks are re-armed only by maybeRearmHeartbeat after a topic
completes — there is no dispatcher that picks up `scheduled` heartbeat
tasks, so the button would leave a paused/backlog task dormant.
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(task-lifecycle): auto-synthesize topic briefs (LOBE-8333)
Replaces agent-driven createBrief on the non-review "done" path with a
programmatic synthesis: rule-based decision + DB-collected artifacts +
a dedicated LLM for user-facing title/summary. Handoff and brief stay
separate (agent-internal vs user-facing language) and the new path is
gated behind task.config.brief.mode === 'auto' so existing tasks keep
the legacy tool-driven behavior until the GrowthBook flag flips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(generate-brief): let LLM gate emission per topic content
Pure rules can only skip the obvious cases (error, judge-handled,
automation tick, trivial content). They can't tell that "I clarified
my understanding and will start drafting next" is a working note, not
a delivery. Add an `emit: boolean` to GENERATE_BRIEF_SCHEMA and have
the prompt instruct the model to judge — emit=false discards the
brief without writing to the table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task-model): move topic-artifact query into TaskModel
DB queries belong on the model, not in a service helper. Replaces
the standalone collectTopicArtifacts() with TaskModel.getDocumentsPinnedSince(),
which lives next to pinDocument / getPinnedDocuments and returns
joined { id, kind, title } rows. synthesize.ts is now pure decision
logic — no more drizzle imports.
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: support UTF-16 encoded text files in TextLoader
The TextLoader previously hardcoded UTF-8 encoding when reading files,
causing UTF-16 encoded CSVs (e.g. Google Ads Keyword Planner exports)
to be parsed with null bytes, producing garbled content and database
insert failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor(file-loaders): tighten TextLoader UTF-16 detection
- Use TextDecoder('utf-16be') instead of manual byte-swap loop, which
also avoided in-place mutation of the read buffer.
- Replace the 2-byte heuristic with a 512-byte sample, count ASCII-pair
shape on both halves so UTF-16BE without BOM is detected too, and
files whose first character is non-ASCII no longer slip through.
- Add tests for UTF-8 BOM, UTF-16LE no-BOM, and UTF-16BE no-BOM.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
**Hotfix Scope:** Topic preservation across cold chat-entry routes
> Keeps newly created Topics visible when a first message is sent before
the destination chat route has fully hydrated.
## 🐛 What's Fixed
- **Page Agent empty-session regression** — Sending the first message in
an empty Page Agent panel no longer clears the newly created Topic and
returns the panel to an empty state. (Resolves LOBE-8351)
- **Home cold-route send regression** — Sending from the Home default
Chat Input now routes to the newly created Inbox Topic even when
`/agent/:aid` has never been opened and the route chunk has no warm
cache.
- **Page-scoped Copilot consistency** — Page Copilot and File Copilot
share the same provider-level topic reset behavior, so stale Topics are
cleared only when entering or switching the scoped Agent.
- **Regression coverage** — Added focused unit coverage for Home default
sends, route parity coverage remains intact, and added an E2E scenario
for the no-cache Home send path.
## ✅ Verification
- `bunx vitest run --silent='passed-only'
'src/routes/(main)/home/features/InputArea/useSend.test.ts'
'src/spa/router/desktopRouter.sync.test.tsx'
'src/routes/(main)/agent/features/Conversation/ChatHydration/index.test.tsx'
'src/routes/(main)/agent/_layout/AgentIdSync.test.tsx'`
- `BASE_URL=http://localhost:3007
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/postgres bun
run test -- --tags '@HOME-CHAT-COLD-001'` from `e2e/`
## ⚙️ Upgrade
- Self-hosted: pull the new image and restart. No schema or environment
changes.
- Cloud: ships through the normal hotfix deployment after merge.
## 👥 Owner
@Innei
Fixes LOBE-8351
* ✨ feat(agent-marketplace): implement onboarding agent marketplace picker
Adds a new builtin tool `@lobechat/builtin-tool-agent-marketplace` that
opens a categorized agent picker UI during web onboarding. The picker
fetches the live curated catalog from the marketplace API
(`/api/v1/agents/onboarding-full`) via a TRPC procedure that injects the
trust-token, and lets the user select template agents to install.
Highlights:
- Self-contained marketplace package with manifest, system role, executor,
and ExecutionRuntime
- React intervention component with category sidebar, skeleton loading
state, and avatar/empty/error UI; all user-visible strings i18n-driven
- Dependency-inverted fetcher: package exports `setAgentTemplatesFetcher`,
app registers a TRPC-backed implementation in AgentOnboardingPage
- New TRPC `market.agent.getOnboardingFull` proxies the upstream API with
trust-token authentication; client never sees secrets
- Splits the existing `saveUserQuestion` intervention into agent identity
and user profile cards for clearer onboarding approval UX
- Wires marketplace into `builtin-tools` registry, executor map, and
onboarding metrics; web-onboarding agent system prompt updated to
reference the picker
Closes LOBE-7801
* ✨ feat(onboarding): enhance early exit handling and marketplace integration in onboarding flow
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(agent-marketplace): register server runtime, scope picks per-topic, and harden onboarding handoff prompts
The summary phase silently skipped the marketplace handoff because the
server toolExecution registry had no runtime for `lobe-agent-marketplace`,
so every `showAgentMarketplace` call returned "not implemented" and the
agent fell through to `finishOnboarding`. The runtime-injected phase
guidance and action hints also instructed the agent to call
finishOnboarding directly after the summary, contradicting the new
system role.
- Register `agentMarketplaceRuntime` in
`src/server/services/toolExecution/serverRuntimes` so the executor
can actually run.
- Scope the in-memory `picks` map by `topicId` and reject a second
`showAgentMarketplace` call in the same conversation with a clear
"already opened, finish on next turn" message.
- Tighten the success content to instruct the model to STOP the current
turn after opening the picker and run closing + finishOnboarding on
the FOLLOWING user turn.
- Update `OnboardingActionHintInjector`, `PHASE_GUIDANCE.summary`,
`toolSystemRole` and `web-onboarding/systemRole` so all four prompt
layers agree: open the picker exactly once during summary, do not
call finishOnboarding in the same turn, and do not call the
submit/skip/cancel APIs ourselves.
- Stop treating short affirmations like "好的" / "行" / "ok" as
early-exit signals; they are confirmation of the summary and should
let the picker handoff proceed normally.
Verified end-to-end with `bun run agent-evals run onboarding/web-onboarding-v3
--case-id fe-intj-crud-v1 --model deepseek-v4-pro`: hard assertions all
pass, judge moves from 7/10 (premature finishOnboarding in same turn)
to 8/10 with picker opened once and finishOnboarding deferred to the
next turn.
* fix(ci): attempt 1 for PR #14286
Auto-generated by pr-dispatcher (task: 01KQBY8GAC1MNQCJ6T6X5DEP2F, attempt: 1).
Co-Authored-By: Claude <noreply@anthropic.com>
* 🐛 fix(agent-marketplace): wire picker submit + fix marketplace-already-opened detection
The marketplace picker confirm flow was sending the user's selection back as a
synthetic user message, and the action hint kept telling the model to open the
marketplace again — leading to a death loop where the agent re-opened the
picker instead of summarizing + persisting + finishing onboarding.
Two issues:
1. Pick confirm forwarded the selection as a user message instead of forking
the agents and resuming from the tool result. Wire `prepareCustomInteractionSubmit`
into the intervention's submit branch so it runs `installMarketplaceAgents`
client-side and returns a descriptive `toolResultContent`. Plumb a
`createUserMessage: false` + `toolResultContent` option through
`submitToolInteraction` (slice + chat store): when set, skip the synthetic
user message, override the tool message content, and resume runtime from the
tool message (`parentMessageType: 'tool'`) so the LLM sees the install
result and continues from there.
2. `OnboardingActionHintInjector.marketplaceAlreadyOpened` read `msg.tool_calls`,
but this provider runs in pipeline phase 4.5 (virtual tail guidance) BEFORE
`ToolCallProcessor` (phase 5) converts DB-shape `tools` → OpenAI-shape
`tool_calls`. Detection always returned false → the hint kept saying
"call showAgentMarketplace" → death loop. Fix: match on `tools[].apiName`
(with `tool_calls` kept as a fallback). Also rewrote the Summary-phase hints
to reflect the new flow (picker resolves directly via tool result, no
synthetic user reply needed).
Includes intervention bar portal-target plumbing for approval actions.
* ✨ feat(onboarding): wire marketplace picker analytics on agent onboarding page
Mount AnalyticsBridge under AgentOnboardingPage to inject useAnalytics() into
setOnboardingAnalyticsClient, so onboarding_marketplace_shown/picked events
emit through PostHog instead of being silently dropped. Adds spm fields to
align with onboardingFeedback's telemetry shape.
* ♻️ refactor: move DEFAULT_ONBOARDING_MODEL to business-const
Made-with: Cursor
* ✨ test(customInteractionHandlers): add tests for persisting marketplace picks and resolutions
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(onboarding): enhance agent marketplace integration with metadata persistence
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(agent): add web onboarding agent selectors and integrate into Actions and Usage components
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
KnowledgeRepo queries use COALESCE(d.id, f.id) as id, which returns the
document's `docs_xxx` ID when a document exists for the file. Using this
as the proxy URL path (`/f/docs_xxx`) fails because the file proxy route
looks up the `files` table by `file_xxx` ID.
Fix: use `item.fileId` (always the actual file ID) for proxy URLs in
`getKnowledgeItems` and `recentFiles` handlers.
Closes#12196
* feat: support Line
* chore: update Line docs
* feat: support line platform
* chore: update markdown files
* fix: lint error
* fix: home padding block
Daily/weekly schedules dedup'd by calendar day, so a manual "run now"
earlier in the day would advance lastHeartbeatAt and make the dispatcher
skip today's scheduled tick. Dedup now compares against today's target
H:M instead — a 21:00 schedule still fires after a 18:00 manual run,
while post-target runs and same-tick re-dispatch are still skipped.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Azure / OpenAI strict structured outputs require every key in `properties`
to appear in `required`; the schema only listed `title` and `summary`,
so every generateHandoff call returned 400 "Missing 'keyFindings'".
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
♻️ refactor(context-engine): drop ____builtin suffix from tool names
Builtin tools now generate two-segment names like documents____upsertDocumentByFilename instead of documents____upsertDocumentByFilename____builtin. The "default" plugin type was already suffix-less, and "default" is no longer in active use, so collapsing builtin into the same shape removes redundant LLM-facing tokens. resolve() falls back to type 'builtin' for two-segment names and still parses legacy three-segment ____builtin names from message history.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(lobehub-skill): add video/image model lookup guide to generate reference
* docs(lobehub-skill): add full model type list and default-type warning to model reference
* docs(lobehub-skill): fix incorrect tip about lh model list default behavior
* 🐛 fix(builtin-skills): close template literal in model reference
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: remove schedule config popup from task list item
The task list row should only display the schedule trigger tag, not act
as an entry point for editing the automation. Configuration stays
available on the task detail page via TaskProperties.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: mute BriefIcon when task is resolved
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: flatten task markdown card, drop container background and padding
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: expose task topic operationId and add copy menu item
Surfaces the persisted `task_topics.operationId` through the task detail API
so the topic card menu can offer a "Copy operation ID" entry alongside
"Copy topic ID", aiding debugging of completed runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: skip empty text block when Claude Code prompt is image-only
Anthropic rejects `{ text: '', type: 'text' }` with "messages: text content blocks must be non-empty", so uploading an image with no text would 400.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: add topic actions menu and share button to task topic drawer
- Add "..." dropdown next to title with Copy topic ID / Copy operation ID
- Add Share icon next to close button, reusing SharePopover and ShareModal
- Pass topicId through SharePopover so it works outside the chat store scope
- Use getContainer={false} on Drawer to escape App's isolation stacking context, letting popups render above the drawer
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: refactor home
* feat: add home agent id switch
* fix: useSend ensure agent map init
* feat: add custom image/video generation menu item
* chore: remove agent list ,group list and modetag
* fix: default home agent fallback
* fix: built in agent builder creation
* feat: add deepseek pro v4 hot picks
* chore: support agent select scrolling
* feat: add bot integration banner
* fix: lint error
* chore: update home page styles
* chore: adjust padding
* test: add image item to sidebar items test fixtures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: remove obsolete home starter e2e tests
The mode-tag buttons (Create Agent / Create Group / Write) no longer
exist after the Home refactor, so these scenarios cannot run.
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(home): collapse empty suggest questions wrapper on default home
Why: when enableAgentTask is on, SuggestQuestions and CommunityRecommend both render null on the default home view, but the AnimatePresence wrapper still mounted with marginTop:24 and produced a large empty gap between StarterList and DailyBrief.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): add cron-based task schedule dispatcher
Wires up backend execution for task-level cron schedules. Adds two
QStash workflows-hono routes:
- POST /api/workflows/task/schedule-dispatch — central sweep, point a
QStash Schedule (e.g. */30 * * * *) here. Loads all schedule-mode
tasks, filters by cron pattern + timezone + lastHeartbeatAt dedup,
and fans out per-task messages.
- POST /api/workflows/task/schedule-execute — internal per-task handler
that re-validates DB state and runs the task via TaskRunnerService.
Reuses existing schedulePattern / scheduleTimezone columns and
lastHeartbeatAt for dedup — no migration needed. Failure paths fall
through to the existing onTopicComplete error handling (urgent brief
+ paused).
* 💄 style(task): collapse resolved brief card on detail by default
Why: resolved briefs on the detail page rarely need re-reading; matching
home's collapse-when-resolved behavior keeps the activity feed compact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(agent-profile): make popup header navigate to agent profile
Click on the avatar/title in AgentProfilePopup now closes the popup and routes to /agent/:id/profile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): render task XML as a card in topic chat drawer
Why: the topic drawer's first user message is the task run prompt — a `<task>...</task>` XML blob (identifier, status, instruction, agent, …). Rendering it as raw XML buries the structure the user actually cares about.
- Add a `Task` markdown plugin (scope: user) that parses the `<task>` payload and renders an Artifacts-style card.
- Use a custom remark plugin so the block survives mdast splitting it across html + paragraph nodes.
- Gate the card UI behind a `TaskCardScope` React Context so it only activates inside `TopicChatDrawer`; everywhere else falls back to a plain `<pre>`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(claude-code): reuse result renders during streaming via wrapRender
Why: while a CC tool is still executing, the detail view fell back to a generic argument table for everything except `Agent`. Read/Write/Edit/Glob/Grep/Skill/Bash/TodoWrite already gracefully degrade their result Render when `content`/`pluginState` are absent, so the same component works for the live phase too.
- Add `wrapRender` helper that adapts a `BuiltinRender` into a `BuiltinStreaming` by passing `content: null`.
- Register Bash/Edit/Glob/Grep/Read/Skill/TodoWrite/Write streaming entries through `wrapRender`. `Agent` keeps its bespoke streaming view.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task-subtasks): drop legacy blockedBy flattening branch
Why: subtasks now always arrive as a real tree from the upstream service, so the fallback that re-built the tree from a flat list via `blockedBy` is dead code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(view-switcher): hide chat/task switcher for heterogeneous agents
Why: the chat/task view switcher in the agent header doesn't apply when the agent is heterogeneous (Claude Code / Codex / etc.) — those agents don't share the task topic flow, so showing the switch surfaces a non-functional control.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task-topic): show elapsed duration on completed topic runs
Mirror task_topics terminal transitions (completed / failed / canceled / timeout)
onto topics.completedAt so the activity feed can render elapsed time for
finished runs, not just for the live one. Thread completedAt through
findWithHandoff and the TaskDetailActivity payload, then extend TopicCard
to render formatDuration(completedAt - createdAt) for non-running statuses.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task-trigger-tag): respect automationMode when rendering schedule label
Heartbeat tasks were displaying cron schedule text when the DB still carried
a schedulePattern from a previous mode. Switch to automationMode as the
source of truth in TaskTriggerTag and pass it from all three call sites.
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(conversation): stop topic scroll restore from corrupting itself
The restore path called scrollTo(snapshot.offset) one rAF after a fresh
VList mount, when only viewport-visible items had laid out. virtua
clamped the target against the still-incomplete scrollSize and landed
at offset 0, then the resulting onScroll fed back into recordScroll and
overwrote the snapshot to offset 0 — locking the user at the top on
every revisit.
Two fixes:
- Add a restoringRef guard that suppresses recordScroll while a
programmatic restore is in flight, released after two rAFs.
- Poll virtua's scrollSize for up to 30 frames until it can accommodate
the target offset before issuing scrollTo, with a safety bail-out so
unreachable offsets still resolve.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): converge scroll snapshot to clamped offset on cap-out
When the saved offset is unreachable (e.g. messages were trimmed since
the snapshot was written), the polling loop hits its 30-frame cap and
falls through to scrollTo(targetOffset). Without this fix, the snapshot
keeps the stale unreachable offset, so every future revisit pays the
full polling delay before clamping again.
After the cap-out scrollTo lands, read the actual scrollOffset and
persist it (with a recomputed atBottom). Reachable-target restores still
leave the snapshot untouched so we don't churn writes for no reason.
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: auto-dismiss upload dock after completion
UploadDock now auto-removes all completed files and hides itself 3 seconds
after all uploads finish (or error). If new uploads start during the timer,
the timer is cancelled and the dock stays visible.
Closes#9605
* fix(ci): 将 `useRef<ReturnType<typeof setTimeout>>()` 改为 `useRef<ReturnType<typeof setTimeout> | null>(null)`。
Auto-generated by pr-dispatcher (task: 01KQ9ZB50GQXWTYADHAWEGTNQR, attempt: 1).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ci): Guarded `clearTimeout(autoDismissTimerRef.current)` calls with `if (autoDismissTimerRef.current)` checks in the UploadDock auto-dismiss effect.
Auto-generated by pr-dispatcher (task: 01KQA0NZB57SFPHP45227ENZAT, attempt: 1).
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 💄 style(task): replace page drawer with modal and rebuild artifact card
- Migrate page preview from a right-side drawer to a centered modal
(`PageModal`) with allow-fullscreen support; rename store state
`activePageDrawerPageId` → `activePageModalId` and the corresponding
`openPageDrawer` / `closePageDrawer` actions / selectors.
- Refresh artifact cards: collapse to a single-line layout (smaller
file icon, inline size + identifier tag) and add a remove action
that calls `unpinDocument` against the artifact's `sourceTaskId`
fallback chain (so artifacts pinned from another task unpin from
the right task, not just the active one).
- Surface `sourceTaskId` on `TaskDetailWorkspaceNode` /
`WorkspaceDocNode` and through the task service so the renderer
can resolve the owning task for the unpin call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): add delete action for brief cards
- `briefService.delete` calls `brief.delete` mutation; `deleteBrief`
store action removes the brief from the in-memory list after the
server roundtrip.
- `TaskBriefCard` exposes a `MoreHorizontal` dropdown with a danger
delete item gated by an `App.confirm` modal; `TaskActivities`
passes `onAfterDelete=refreshActiveTask` so the activity list
re-fetches once the brief is gone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): use local timezone over DB-default UTC on first schedule enable
The `tasks` table seeds `schedule_timezone` to `'UTC'` on row creation, so
even a task that has never been scheduled surfaces `timezone='UTC'`. The
previous "if timezone is missing, use local" check therefore never fired,
and first-time schedule enable always defaulted to UTC.
Treat a missing `pattern` as the reliable signal that the user has never
opened the schedule form, and override the DB-default UTC with the user's
local IANA zone in that case. A user-chosen timezone (with a real
pattern) is still preserved on subsequent re-entries.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task/scheduler): replace TimePicker with half-hour Select
- Cron storage rounds minutes to 0/30 (see `buildCronPattern`), so the
picker only ever needs 48 half-hour slots — flatten antd's
hour×minute grid into a single-column `Select`.
- Anchor every dropdown (`getPopupContainer`) inside the parent Base UI
Popover so option clicks aren't treated as outside-clicks (which
dismissed the popover before the selection committed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task/subtasks): wire context menu via Tree.onRightClick
`ContextMenuTrigger` was attached to each subtask title's inner
`Flexbox`, but antd `Tree`'s row-level `.ant-tree-node-content-wrapper`
only `preventDefault`s the contextmenu event when an `onRightClick`
handler is provided. Right-clicks landing in the row gap (anywhere
outside the title element) fell through to the browser's native menu.
- Refactor `useTaskItemContextMenu` into a shared
`useTaskContextMenuActions` factory exposing stable
`buildItems(task)` / `installKeyboardHandlers(task)`. Existing
`useTaskItemContextMenu(task)` API is preserved as a thin wrapper.
- `TaskSubtasks` now calls `Tree.onRightClick`, looks up the subtask
by `node.key` from a recursively-built map (subtasks are returned
as a nested tree, not flat), and calls `showContextMenu` plus the
keyboard-handler installer imperatively.
- The flat-map walk is recursive so right-click works on nested
children, not just top-level subtasks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task/topic): wrap dropdown to swallow card click + relabel topic ID
- Wrap the topic card's `MoreHorizontal` dropdown in a `Flexbox`
with `onClick={stopPropagation}` so menu interactions don't
bubble through to the card-level click handler.
- Fix the menu label fallback: `Copy run ID` → `Copy topic ID` to
match what the action actually copies.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task/artifacts): also refresh active task SWR after unpin
`unpinDocument` is called with `node.sourceTaskId` (the task that
owns the pin row, often a descendant DB id), but the open detail
page's SWR cache is keyed by `activeTaskId` (typically the parent
identifier from `/task/{identifier}`). Refreshing only the source
key left the parent's workspace stale until reload.
After the unpin succeeds, also revalidate the active key when it
differs from the source. The server call still uses the source id
because `model.unpinDocument` deletes by exact `(taskId, documentId)`
match — passing the parent identifier would no-op for docs pinned
by a subtask.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(panel): give page and task right panels independent visibility
Page editor and Task layout now read/write `showPageAgentPanel` /
`showTaskAgentPanel` (with matching `togglePageAgentPanel` /
`toggleTaskAgentPanel` actions) instead of sharing the global
`showRightPanel`, so toggling one no longer flips the other. Task panel
defaults to collapsed; page panel stays open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task/detail): tighten artifact size label and align activity card padding
- artifact size shows raw count with "字" instead of "1.4k 字符"
- swap artifact file icon to FileTextIcon (lucide), 18px
- BriefCard padding 12 → paddingInline 8 to align with CommentInput; BriefIcon 20 → 24
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task/page-modal): give modal its own header via PageEditor slot
PageEditor now accepts an optional `header` slot (undefined keeps the
built-in Header, null hides it). PageModal stops relying on antd's title
chrome and supplies its own header — title + autosave on the left, panel
toggle and close on the right — so the modal no longer stacks two
headers and owns its own composition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(page): mirror document into pageStore on standalone fetch
Document fetch now upserts the loaded `page`-source document into
pageStore via a new `upsertDocument` action. PageExplorer reads title
and emoji from pageStore selectors, so opening a page from a context
that never hit the page list (e.g. the task workspace modal) used to
show empty title/emoji until the list was visited.
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(task): polish schedule popover
Refresh the schedule popover after design review:
- Header: avatar with ⚡ icon + summary (e.g. "Runs every 10 min" / "Daily
at 09:00 · China Standard Time"); next-run preview block under the title.
- Segmented tabs gain Calendar / Refresh icons; Recurring tab drops the
Clear button + advanced section (only Schedule mode keeps advanced).
- Advanced settings is now an Accordion (matches lobehub patterns) and
hosts timezone + max executions.
- All inputs switch to variant="filled"; weekday picker uses
colorPrimaryBg + colorPrimary instead of solid primary to fix the
white-on-white "burned" active state.
- Popover surface uses colorBgContainer + colorBorderSecondary border +
12px radius for clearer elevation.
New `scheduler/helpers.ts` formats the cron summary, resolves IANA
timezone display names via Intl, and computes the next firing time for
both heartbeat and cron schedules (uses dayjs/plugin/timezone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): hide standalone "Brief" fallback in task list
When a brief activity has no title/summary AND no briefType, the latest
activity line on the task list rendered just "Brief" / "简要" — useless
text with no actual content. Return undefined in that case so the line
is omitted entirely.
Drops the now-unused `taskDetail.latestActivity.briefOnly` key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): navigate to /page/:id when clicking artifact tree
Drop `selectable={false}` on the workspace tree and wire `onSelect` to
push `/page/<documentId>`, so artifacts are openable from the task
detail page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): enforce 10-minute minimum on recurring interval
Drop the Seconds unit from the Recurring tab so users can't schedule
sub-minute intervals (which the runner can't keep up with anyway), and
clamp existing values that are smaller than 10 minutes to 10 minutes
when the popover opens.
Drops the now-unused `taskSchedule.seconds` key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): surface needs-review group above backlog in task list
Reorder the default kanban/list groups so `needsInput` (paused + failed)
sits at the top — the list view stacks groups vertically, and putting
actionable items first means users see what needs attention before
scrolling past long backlogs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): catch up next heartbeat firing past stale lastAt
When `lastAt + interval` already lies in the past (e.g. task was paused
for hours), step forward by whole intervals so the returned time is
strictly after now. Otherwise the popover would show a stale
"next run" timestamp until the next tick lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): open artifact pages in right-side drawer
Replace the `/page/:id` navigation from the artifact tree (a4af053338)
with a right-side drawer that shows the page in-place — the same UX
pattern as the chat document portal, so users keep the task context
while previewing artifacts.
- New `PageDrawer` mirrors `TopicChatDrawer` styling (right-anchored
floating drawer with rounded edges + shadow). Renders `PageExplorer`
inside.
- Task store gains `activePageDrawerPageId` state with
`openPageDrawer` / `closePageDrawer` actions; opening a page also
closes the topic drawer so the two don't stack on the same edge.
- `TaskArtifacts.onSelect` now calls `openPageDrawer(documentId)`
instead of pushing a new route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): seed defaults when entering an automation mode
Switching to a mode without persisting its core fields left the task in
a "mode enabled but unconfigured" state — the popover showed
"自动化未启用" / "Automation is off" because schedulePattern was still
null even though the Schedule tab was active, and the cron runtime had
nothing to fire.
`setAutomationMode` now seeds:
- `heartbeatInterval = 600` (10 min) when entering heartbeat without one
- `schedulePattern = '0 9 * * *'` + `scheduleTimezone = 'UTC'` when
entering schedule mode without them
Existing values are preserved on subsequent mode toggles.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): default scheduleTimezone to user's local IANA zone
Hardcoding `UTC` meant a user in Shanghai who picked "Daily 09:00" on a
fresh task would actually fire at 17:00 local. Resolve the user's local
zone via `Intl.DateTimeFormat().resolvedOptions().timeZone` (with a UTC
fallback for environments where Intl is unavailable) so the seeded
default matches what the user expects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): polish list, detail, and schedule UI
- Always show top-right + button in kanban view (no inline create input there)
- Unify subtasks/artifacts/activities section indicator on the Accordion arrow
- Refresh schedule popover nextRun every minute and move styling to staticStyles
- Move paused/failed groups ahead of running/backlog in task list ordering
- Color the scheduled status icon with colorWarning to match other active states
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(gateway): gate reconnect on server URL, not user toggle
Resuming a Gateway-running operation should depend on whether the server has
a Gateway URL configured — the user's lab toggle controls *new* requests, not
reattaching to an op that's already running.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): surface scheduled state with cancel action and countdown
- Reorder list view group ranks so paused/failed (待审阅) sit above
running and backlog, matching the kanban needsInput-first layout.
- Map `scheduled` task status to the running group so cron/heartbeat
tasks waiting between ticks no longer fall through to backlog.
- Render a muted "Scheduled" pill on task list rows so users can tell
scheduled (waiting) apart from running (executing now) at a glance.
- Add a "Cancel schedule" action and live countdown to the task detail
page when status=scheduled; cancel disables automation AND moves the
task back to backlog so the status badge updates immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): redesign artifact list as flat cards with file icons
Replace the antd Tree-based artifact view with a flat list of clickable
outlined cards. Each card uses FileIcon (resolves a real file glyph from
the title's extension) and shows the artifact title, size, and source
task tag inline. Removes the unused folder/tree visualization since
workspace nodes today are effectively flat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): use warning color for scheduled status icon
Promote the scheduled status icon from `colorTextDescription` to
`colorWarning` so it visually groups with `running` (also warning) — both
states represent "automation in progress" and now share a consistent
warm color, matching how kanban groups them in the same column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(topic): use shared MAIN_SIDEBAR_EXCLUDE_TRIGGERS constant
Replace the local EXCLUDE_TRIGGERS array with the canonical
MAIN_SIDEBAR_EXCLUDE_TRIGGERS exported from `@/const/topic` so the chat
sidebar and any other consumers stay aligned on which trigger types are
hidden from the main topic list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): rename artifact label from 作品 to 产物 in zh-CN
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): align artifact cards with activities content width
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): collapse resolved brief cards by default
Resolved brief cards now show only the header row with a "marked as resolved" badge and an expand chevron; clicking the chevron reveals the summary and actions. Also tightens the collapsed summary max-height from 240 to 180.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): show human-readable schedule on trigger tag
The list/properties trigger tag rendered the raw cron pattern
("0 9 * * * (Asia/Shanghai)") which is unreadable for non-engineers.
Reuse the popover's `formatScheduleDescription` + `formatTimezoneName`
helpers so the tag now reads as e.g. "每天 09:00 执行 · 中国标准时间".
The raw cron + IANA id moves into the tooltip for users who need it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): split timezone onto a smaller secondary line
The schedule summary used to read "每天 09:00 执行 · 中国标准时间" on a
single line, which crowded the popover header and the inline trigger tag
in TaskProperties. Move the timezone onto its own line below the
description with a smaller font and `colorTextDescription`, so the
primary information (when it fires) reads cleanly first.
For the compact pill (`mode='tag'`) used in the task list, drop the
visible timezone entirely — it stays accessible via the tooltip
alongside the raw cron pattern.
Drops the now-unused `taskSchedule.summary.schedule` interpolation key.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): default to schedule mode + reword automation copy
- Toggle "自动化" on now lands in the Schedule tab (cron) instead of the
Heartbeat tab. A scheduled run is the more common, predictable choice
— users who want fixed intervals can switch tabs from there.
- Rename the heartbeat tab from "循环任务"/"Recurring" to "心跳模式"/
"Heartbeat" so the term matches the underlying mechanism (and the
existing `taskSchedule.tag.heartbeat` copy).
- Replace 执行 with 运行 across the schedule UI strings (持续执行 → 持
续运行, 执行频率 → 运行频率, 下次执行 → 下次运行, etc.) for a more
natural "run" framing.
- Drop dead keys `taskSchedule.interval` and `taskSchedule.schedulerNotReady`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(brief): resolve brief and re-run task on free-form feedback
The SquarePen feedback editor only called addComment, leaving the
urgent brief unresolved — so the heartbeat re-arm gate kept skipping
the task with reason=human-waiting and the card never moved. Switch
the path to submitFeedback (resolveBrief + task.run) so the agent
picks up resolvedComment on the next turn.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): make trigger tag hover human-readable too
The pill already shows "每天 09:00 运行", but the tooltip still leaked
the raw cron + IANA id ("0 9 * * * (Asia/Shanghai)") on hover. Replace
it with a single readable line using "·" as separator, e.g.
"每天 09:00 运行 · 中国标准时间".
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: add userId and serverId tooltip guide
* feat: update built in message tool
* ✨ feat(cli): add bot dm-policy / allowlist subcommands (LOBE-8254)
Extend `lh bot update` with --dm-policy / --group-policy / --user-id /
--server-id, and add new `lh bot allowlist` and `lh bot group-allowlist`
subcommand groups (list/add/remove/clear). All write paths read existing
settings first and merge so unrelated keys aren't wiped by the partial
update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(channel): warn when a saved bot is missing the operator userId
Surface an inline alert and auto-expand the Advanced Settings group when an
existing bot has no settings.userId — without it AI tools can't push
notifications back to the operator and pairing approvals fail silently.
Skip on first-time configs and on platforms that don't expose userId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: optimize userId alert
* fix: test case
* fix: footer effective userId
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(utils): preserve JPEG format when compressing uploaded images
Images with dimensions > 1920px were always re-encoded as PNG regardless
of original format, inflating small JPEGs (100–200 KB) to 1 MB+ because
PNG is lossless while JPEG is lossy.
Fix: pass file.type to compressImage(), encode JPEG inputs as JPEG at
0.85 quality (not PNG), and derive File MIME type from the data URL
instead of hardcoding 'image/png'.
PNG and WebP inputs still compress to PNG as before.
Fixes#13485
* ✅ test(utils): add tests for JPEG format preservation in compressImage
Per @tjx666's request on #13585. Adds explicit coverage for the JPEG
format-preservation behaviour:
- compressImage with type='image/jpeg' calls toDataURL with quality 0.85
- compressImage with type='image/png' calls toDataURL without a quality arg
- compressImage with no type defaults to PNG
- compressImageFile preserves JPEG inputs as image/jpeg (regression fence
for the previously hardcoded 'image/png' MIME type in dataUrlToFile)
- compressImageFile keeps WebP inputs as PNG (documents the fallback)
The existing PNG tests are preserved to guard against regression in the
lossless path.
---------
Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
* ♻️ refactor(recent): rewrite queryRecent in Drizzle, exclude web-tool scrapes
- Replace raw SQL UNION with Drizzle's typed unionAll (topicArm/documentArm/taskArm)
- Hoist filter lists into named constants (SYSTEM_TOPIC_TRIGGERS,
TOOL_DOCUMENT_SOURCE_TYPES, TASK_FINAL_STATUSES) for readability
- Recent now excludes documents whose sourceType is in ('file', 'web') so
web-browsing tool scrapes stop leaking in alongside file uploads
- Add RecentModel test coverage
* 🐛 fix(recent): widen TOOL_DOCUMENT_SOURCE_TYPES to const tuple for inArray
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(task): support scheduled status for cron-driven automation
Adds the new `scheduled` task status to the type system, lifecycle, and
UI so cron-driven tasks can park between ticks instead of falling back
to `paused`. Replaces the SchedulerTab placeholder with a real cron
editor (frequency / weekday / time / timezone / max runs) and surfaces
the schedule config through TaskDetailData.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): show full execution history in detail
`findWithHandoff` defaulted to a limit of 4, which fits the prompt-build
case but truncated the activity feed in the task detail UI to the latest
4 runs. Make `limit` required and pass 100 from the detail service so
scheduled tasks display their full run history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(QueueTray): use elevated surface tokens
Switch the queue tray's border to colorFillSecondary and its background
to colorBgElevated so it visually sits above the chat input rather than
blending into the page background.
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(send-message): forward topic-list filter to server response
Without this, sending a message refreshes `topicDataMap` with an
unfiltered list, so completed/cron topics flash back into the sidebar
until the next SWR revalidation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(topic): preserve filter fields in internal_updateTopics
internal_updateTopics rewrote topicDataMap[key] from scratch and dropped
excludeStatuses / excludeTriggers, so #getTopicFilter returned undefined
on the second sendMessageInServer call and stopped forwarding the filter
to the server — completed/cron topics could leak in until SWR
revalidated. Carry the filter fields forward from currentData, matching
loadMoreTopics.
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(agent): clear stale topic in store when switching agents
Switching agents from `/agent/agt_A/tpc_X` to `/agent/agt_B` left the
previous topic's messages on screen and made *Start new topic* feel
inert. Two fixes:
- ChatHydration: replace the `useEffect`-based `useStoreUpdater` with
`useLayoutEffect` so the URL→store sync of `activeTopicId` /
`activeThreadId` runs before paint. Otherwise Conversation paints
one frame against the prior agent's `activeTopicId` and only catches
up on the next render. Also handles `null` (rather than `undefined`)
so the store actually clears instead of silently retaining the stale
id.
- AgentPage (desktop + web): drive the topic-popup guard from
`useParams().topicId` instead of the store, since URL is the source
of truth for which topic to render.
Drops the now-unnecessary `Portal` import from the desktop variant.
* 🐛 fix(conversation): update context handling and improve thread list visibility logic
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(topic): update ThreadList to accept topicId prop and improve thread visibility logic
Signed-off-by: Innei <tukon479@gmail.com>
* ✅ test(topic): align topic item thread list mock
* 🐛 fix(agent): show active thread title in conversation header
Header `Tags` always read `topicSelectors.currentActiveTopic(s)?.title`,
so when navigating into a subtopic (`activeThreadId` set via the
`?thread=...` URL sync) the title bar still showed the parent topic's
name. Read the matching thread from `s.threadMaps[s.activeTopicId]`
when `activeThreadId` is set and fall back to `chat:thread.title`
("Subtopic") for unnamed threads.
Signed-off-by: Innei <tukon479@gmail.com>
---------
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(topic): drop switchTopic race under rapid sidebar clicks
Share the single-click debounce timer at module level so a click on any
topic cancels a pending click from another, and add an epoch guard in
ChatTopicActionImpl.switchTopic so stale refresh continuations cannot
flip activeTopicId back to a superseded topic.
Fixes LOBE-7785
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(topic): yield before refresh so switchTopic epoch can skip stale fetches
The post-await epoch check was dead code: nothing followed the await.
Yield a microtask before the refresh so queued switchTopic sync bodies
can bump #switchTopicEpoch first, then bail the superseded caller before
its SWR mutate ever fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔖 chore(release): release version v2.1.53 [skip ci]
* fix(cli): improve gen status/download error message for wrong asyncTaskId
* docs(cli-skill): clarify asyncTaskId vs generationId in gen status/download
* fix(builtin-skills): clarify asyncTaskId vs generationId in gen status/download
* fix(cli): distinguish asyncTaskId not found vs generationId not found in error message
* Update package.json
---------
Co-authored-by: lobehubbot <i@lobehub.com>
* ✨ feat(tasks/progress): align workspace progress visibility with chat input
Switch the right-side ProgressSection to selectCurrentTurnTodosFromMessages so it appears and disappears in lockstep with the TodoProgress bar above ChatInput, instead of lingering on stale historical todos.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): promote tasks entry into top-level header nav
Place the Tasks entry directly under Home in the sidebar header alongside Search/Home, instead of letting it float inside the customizable body list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks/comment): use filled background for the task detail comment input
Switch the task detail comment input from a bordered card on `colorBgElevated` (which read as outline-only in light mode) to a `colorFillTertiary` filled card so it looks consistently filled in both light and dark themes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♿ fix(tasks/progress): restore keyboard toggle & uncap expanded list
Address two regressions introduced when ProgressSection moved off Accordion:
- Re-add keyboard/ARIA semantics on the toggle (role=button, tabIndex, aria-expanded, aria-controls, Enter/Space handler) so keyboard and screen-reader users can collapse/expand the panel. Toggle now lives on the header row only, so clicking todos no longer collapses the panel.
- Replace the `max-height: 600px` cap with the `grid-template-rows: 0fr → 1fr` pattern, letting the list grow to its natural height. Long todo plans are no longer clipped; the parent sidebar (already `overflow-y: auto`) handles scrolling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks/documents): auto-pin agent-created documents to current task
Why: taskDocuments table and TaskModel.pinDocument exist with pinnedBy='agent',
but the agent-documents runtime never called pinDocument, so documents written
during a task were never linked to that task's workspace.
How: In agentDocumentsRuntime factory, read context.taskId and pin the new
documentId after createDocument / createTopicDocument / copyDocument /
upsertDocumentByFilename. Idempotent via the existing (taskId, documentId)
unique constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks/artifacts): render task-level artifacts on the detail page
Why: The taskDocuments table now auto-populates when an agent writes a doc,
and the backend already serves the workspace tree (own task + descendants)
via getTaskDetail. The detail page just wasn't rendering it yet.
How: New TaskArtifacts component reads activeTaskWorkspace and shows a
collapsible tree (file/folder + size + source-task tag). Selectable is off
for now — click-through interaction will land in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(tasks/lifecycle): deliver onTopicComplete webhook via QStash
The hook was registered without `delivery: 'qstash'`, defaulting to plain
fetch. The target route `/api/workflows/task/on-topic-complete` is mounted
under `qstashAuth()`, which rejects unsigned requests with 401 in
production. `HookDispatcher.fetchDeliver` only logs failures, so the
webhook silently failed — leaving topic.status stuck at 'running' forever
for every heartbeat (and regular) task in production.
Same fix applied to all four agentEvalRun webhook registrations for
consistency, even though those routes are currently unauthenticated.
LOBE-8303
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(desktop/notification): deep-link notification click to source chat
Resolve the SPA path (group / 1:1 topic / agent root) from the conversation
context when posting a desktop notification, and forward it through the
existing main-broadcast `navigate` pipeline so clicking the notification
brings the user back to the originating chat instead of just focusing the
window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): move tasks tab back into the customizable sidebar
Removes 'tasks' from the fixed header nav and re-adds it as a default,
user-reorderable item under the body sidebar (alongside pages / recents).
Reverts the header-promotion from 287a3ac815 in favor of letting users
place / hide the tab themselves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks/detail): introduce TaskBriefCard, polish topic row layout
- Split a dedicated TaskBriefCard for the detail timeline so brief styling
there can diverge from the daily-brief card without conditionals.
- Promote the agent avatar (with profile popup) to the TopicCard header,
drop the redundant author chip and calendar icon next to the timestamp.
- Move the dashed divider from BriefCardSummary into BriefCard so any
consumer of the summary block doesn't get an unexpected leading rule.
- Tighten card padding (CommentCard / TopicCard) to align with the timeline
rhythm.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(agent/header): round segmented control items in ViewSwitcher
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(tasks): drop custom actions on result briefs & show trigger tag in subtasks
- Result briefs render a fixed single-button UI, so reject custom actions at
brief creation time and remove the unused defaults / lifecycle actions.
- Surface automation trigger (heartbeat / schedule) on subtask rows by
threading the fields through TaskService → TaskDetailSubtask → tree.
- Polish: tree title flex/overflow fix, QueueTray send icon swapped to ArrowUp.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(task): right-align subtask assignee avatar and make it clickable
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): standardize result brief actions to mark-as-done + edit
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): align decision brief icon with kanban pending-review column
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(brief): rename result brief primary action to "Confirm complete"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): wire passive Gateway WS reconnect for the task topic drawer
The task topic drawer rendered messages from the DB but never connected
to the Gateway, so a running task showed only the initial prompt and the
empty assistant placeholder. Server already writes runningOperation into
topic metadata; expose it through TaskDetailActivity and reuse the main
agent reconnect hook so the drawer establishes the WebSocket on open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(brief): mute Check icon on resolved success tag
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(recent): exclude system-trigger topics from the Recent sidebar
The Recent SQL union pulled every topic regardless of trigger, so cron,
eval, task_manager, and task-runner topics leaked into the main "最近"
list alongside ordinary chats. Filter them in the topics SELECT, and
align the long-stale `TopicTrigger.RunTask` constant with the literal
`'task'` that TaskRunnerService actually writes (the const was unused
so no DB migration is needed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 docs(skills): record contributor roster in version-release skill
- Add Contributor Ordering section with the canonical LobeHub team roster (10 handles) and a flat-list rule (community first, team after, sorted by PR count desc).
- Note the git-author-name vs GitHub-handle pitfall (e.g. YuTengjing -> @tjx666) and how to verify via gh CLI.
- Drop commits count from the changelog template's metadata and contributors lines; reword the contributors intro to a "Huge thanks to N contributors" pattern.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When users mention Klavis-managed services (Notion, Slack, Google Drive,
Airtable, Jira, Figma, etc.), the activator now recognizes these as
credential/connection intents and activates lobe-creds automatically.
This enables the full Klavis OAuth flow to be triggered inline without
requiring the user to manually navigate to settings.
Related to #14090
* ✨ feat(cmdk): show agent identity on topic search results
When two topics share the same title (e.g. customer email used as topic
name), the Cmd+K search results were indistinguishable. Surface the
owning agent's avatar + title before the date so users can tell them
apart at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔒 fix(cmdk): scope topic→agent join to current user
Prevent cross-tenant agent metadata (avatar / backgroundColor / title)
from leaking into Cmd+K topic search results when a topic row carries
an agentId that resolves to another user's agent — a state reachable
via crafted/migrated rows where topic creation persists input.agentId
even after resolveContext fails.
The agents JOIN now matches on (id AND agents.userId = current user);
mismatched rows fall through as null and the renderer omits the agent
chip rather than surfacing foreign data.
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(chat-input): drop @-mention hint from follow-up placeholder for heterogeneous agents
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(home): hide suggested questions when agent task flag is on
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(task): wire QStash-driven heartbeat self-rescheduling
Implements LOBE-8233: heartbeat tasks now self-arm via QStash delayed
publish (or LocalScheduler setTimeout in dev). After each topic completes,
TaskLifecycleService re-arms the next tick based on current DB state, with
a 3-strike fuse on consecutive errors and a skip-when-urgent-brief guard.
Adds /heartbeat-tick + /watchdog workflow handlers (signed) and extracts
TaskRunnerService from the task.run mutation so both router and tick
handler share one runner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(task): unblock heartbeat fuse + safe overlap handling + TaskItem typing
- TaskLifecycle re-arm now excludes type='error' urgent briefs from the
human-waiting check; the fresh error brief from onTopicComplete was
always present and stalled retries after the very first failure,
making the 3-strike fuse unreachable.
- TaskRunner only rolls back running→paused when *this* invocation
set the running state; heartbeatTick treats CONFLICT as a graceful
'in-flight' skip so overlapping ticks don't 500 or clobber the
in-flight run's status.
- buildTaskPrompt now types its task arg + getReviewConfig as TaskItem
(the prompts package already depends on @lobechat/types) so server
TaskModel methods are assignable without parameter contravariance
errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task): extract qstashAuth Hono middleware for webhook signature verification
Three handlers (on-topic-complete, heartbeat-tick, watchdog) duplicated the
same `c.req.text() → verifyQStashSignature → 401` boilerplate. Extracted to
src/server/workflows-hono/middlewares/qstashAuth.ts and mounted on the
routes; handlers now just `c.req.json()` (Hono cross-converts the cached
body so the middleware reading text() doesn't break json() in the handler).
Note: this is for one-shot QStash webhook receivers. Upstash *Workflow*
endpoints (memory-user-memory) keep using `serve()` from
`@upstash/workflow/hono`, which has its own built-in verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(task): move buildTaskPrompt back to server (it's a DB orchestrator, not a renderer)
Putting buildTaskPrompt under @lobechat/prompts was a layering mistake:
the function does ~10 DB calls (briefs / topics / subtasks / dep
identifier resolution / parent task assembly) and just maps the rows
through to buildTaskRunPrompt at the end.
The prompts package should stay pure rendering — buildTaskRunPrompt
already lives there as the actual renderer. Moving the orchestrator
back to src/server/services/taskRunner/ also lets it import model
classes directly instead of structurally-typed deps, dropping the
TaskPromptDeps abstraction.
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(conversation): persist per-topic chat scroll position to localStorage
Restores scroll position when switching back to a topic, keyed by
messageMapKey(context). Falls back to scroll-to-bottom for new topics or
when the user was already at the bottom. Storage is capped at 500 entries
with 30-day expiry and silent fallback on quota errors.
Fixes LOBE-8251
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔨 chore(conversation): rename scroll snapshot storage prefix to LOBEHUB
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔨 chore(conversation): use LOBEHUB_SCROLL as scroll snapshot key prefix
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): preserve scroll across draft-to-topic key transition
When a draft conversation (`*_new` key) gets promoted to a real topic via
onTopicCreated, the contextKey changes mid-stream for the same logical
conversation. Treating it as a topic switch loaded a missing snapshot and
fell back to scrollToIndex(end), yanking users away from content they
were reading.
Now we detect the draft-promotion shape, migrate the snapshot to the new
key, and skip the restore pass while data is already on screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔥 chore(settings): remove queryRewrite system agent
Removes the unused knowledge-base query rewrite system agent: settings UI in agent/service-model pages, type definition, default config, store selector, server env parser, locale strings across 18 languages, env-variable docs, and the now-orphan chainRewriteQuery prompt chain.
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(agent-runtime): scope pending-approval check to current assistant turn
A stale `pluginIntervention.status === 'pending'` row from a prior turn
(e.g. an abandoned approval flow whose user never clicked approve/reject)
gets loaded back into `state.messages` via `historyMessages`, hijacks every
subsequent `tool_result` / `tools_batch_result` phase, and parks the loop
in `waiting_for_human` forever — so after a tool call succeeds, the next
LLM call is never scheduled.
Scope the pending check to tool messages whose `parentId` matches the
current assistant turn (the most recent assistant with `tool_calls`).
* ✅ test(agent-runtime): cover persisted tools pending approvals
* ✨ feat(conversation): queue follow-up sends during running CC turns (Plan A)
Without this, a send fired while a Claude Code turn was running would spawn
a second `claude` process in parallel. Now CC participates in the same
soft-queue path that Client mode already uses: follow-ups are queued and
auto-drained into a fresh sendMessage once the current turn completes.
"Send now" remains a manual stop + send — no new UI, minimum architectural
diff vs. the persistent-stdin Plan B.
Refs LOBE-7346.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(conversation): use AI_RUNTIME_OPERATION_TYPES in queue filter
Replace inline `op.type === 'execAgentRuntime' || 'execHeterogeneousAgent'`
with the `AI_RUNTIME_OPERATION_TYPES` constant already used by cancelOperation,
loading-state selectors, and the plugin slice. Picks up `execServerAgentRuntime`
(Gateway) for free — same parallel-run risk as CC, now also queued.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(conversation): drain queue after heteroSessionId is persisted
The drain previously fired from inside onComplete on a fixed setTimeout(100),
racing with the post-sendPrompt updateTopicMetadata write that persists
adapter.sessionId as topic.metadata.heteroSessionId. On the very first queued
follow-up for a topic the metadata write could lose, leaving resolveHeteroResume
to start a fresh CLI session instead of resuming and breaking turn-to-turn
continuity.
Move the drain to run after `await updateTopicMetadata(...)`, so the next
sendMessage observes the just-finished session id. Drain still gated on
"not aborted, no terminal error" — manual stop preserves the queue.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(conversation): add Send-now to QueueTray + keep Stop visible while typing
Two changes for the queue UX:
1. QueueTray: per-row "Send now" icon between Edit and Delete. Clicking it
cancels the current AI runtime op for the context, removes that item from
the queue, and immediately fires sendMessage with its payload. Remaining
queue items stay in place — the new turn's drain picks them up after it
finishes.
2. ChatInput Stop button: previously flipped to Send the moment the composer
had any text during loading (`isInputLoading && isInputEmpty`), which read
as "agent finished" and made queued sends look like fresh sends. Now Stop
stays up for the whole loading window. Enter still enqueues; the QueueTray
Send-now icon is the explicit cancel+send escape hatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Object-typed JSON Schemas without `required` could be reserialized as
`required: null` by strict OpenAI-compatible upstreams (bailian / glm /
zhipu), which then reject the request with `at '/required': got null,
want array`. Default missing/non-array `required` to `[]` at the tool
generation boundary so the wire format stays consistent.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 fix(agent-runtime): tighten isCanUseVision default to false and add aggregator fallback
The runtime capability probe in RuntimeExecutors used `info?.abilities?.vision ?? true`,
which silently treated any model whose card omits the `vision` ability key as vision-capable.
This neutralised the LOBE-7214 downgrade pass for two real cases:
- Models present in the registry without an explicit `vision: true` (e.g. deepseek-v4-pro)
- Models routed through aggregator providers like `lobehub`, where `(model, providerId)` has
no direct registry hit so the lookup fell through to the default
Switch the default to `false` (matching `isCanUseVideo`) and add a cross-provider fallback
that resolves an aggregator-routed model id against its upstream model card.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to the await-review refactor (#14167):
P1: BriefService.resolve previously completed the task on `approve` of any
`decision` brief, but `decision` is also used for non-terminal mid-execution
checkpoints — approving a routine checkpoint shouldn't end the task. Limit
the accept-signal to `result` briefs. The review max-iterations path now
emits a `result` brief (it semantically *is* the final-but-imperfect
deliverable awaiting force-pass), keeping the existing approve→completed
wiring intact for that case.
P2: Judge-accepted result briefs (auto-review pass) were created unresolved,
so the UI rendered active approve/feedback buttons on a task that was
already `completed` — the same lifecycle/UI mismatch the original refactor
set out to remove. Mark the Judge-issued brief as resolved at creation
(`resolvedAction: 'auto-judge-pass'`).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✨ feat(conversation): per-phase workflow expand defaults for heterogeneous agents
Extend `defaultWorkflowExpandLevel` to accept either a single level (current
behavior) or an object split by phase (`streaming` / `completion`). Plain
string still applies to both phases.
Wires heterogeneous agents (Codex, Claude Code) to `{ streaming: 'full' }` so
all tool details stay visible while the turn is running, while keeping the
default collapse behavior once the turn finishes.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agent): surface Codex terminal errors and trace CLI output
- Map Codex `error` / `turn.failed` events to terminal error events
- Filter noisy WARN blocks from Codex stderr when reporting exit errors
- Persist CLI stdin/stdout/stderr to .heerogeneous-tracing/ in dev mode
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agent): skip trace when cwd is missing
`mkdir(dir, { recursive: true })` would otherwise materialize a stale or
typo'd cwd from scratch, swallowing the configuration error and running
the agent in an unintended empty directory. Probe `cwd` first and bail
out of trace setup so spawn() surfaces the real failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Treat agent-emitted `result` briefs as proposals, not completion signals.
Tasks now stay `paused` (await-review) until an explicit accept signal
arrives — user-clicked `approve` action on a `result`/`decision` brief, or
an auto-review (Judge) pass.
Closes LOBE-8223.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 fix(builtin-tool-memory): flatten searchUserMemory schema for OpenAI/xAI strict tool validation
Inline `definitions` and `$ref`, bound recursive `anchor` to one level, and
switch `oneOf`/`allOf` to `anyOf` so providers like grok-4 stop rejecting the
tool with "Invalid arguments passed to the model." (LOBE-8224).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔥 feat(tasks): drop per-agent /agent/:aid/tasks routes again
PR #13887 reintroduced the per-agent Tasks surface (sidebar entry, route
files, agentId-scoped breadcrumb/list/board, /agent/:aid/tasks/:taskId
navigation) that #14109 had removed in favor of unified /tasks and
/task/:id. Restore the unified-only model: drop the agent sidebar Tasks
nav item, delete the agent-scoped route files, strip agent-tasks blocks
from both desktopRouter configs, and revert the agentId props and
per-agent navigate paths in AgentTasksPage / KanbanBoard / Breadcrumb /
TaskDetailPage. Preserves #14137's canceled kanban column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent): redirect any agent sub-route before opening new topic
handleNewTopic only checked /profile and /channel, so on /agent/:aid/page,
/agent/:aid/cron/:cronId or other sub-routes the redirect was skipped and
mutate() opened a new topic on a non-chat screen — looking ineffective to
the user. Match useTopicNavigation's pattern: derive an agent base path
from params (with topicId when present) and treat anything longer than
that as a sub-route, so adding new sub-routes never re-introduces this gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent): always push agent chat route before opening new topic
The previous fix conditioned the redirect on isInAgentSubRoute, which
left the URL untouched on /agent/:aid/:topicId — opening a new topic
while the URL still pointed at the previous one. Drop the conditional
and always push /agent/:aid: it covers every sub-route (/profile,
/channel, /page, /cron/:cronId, …) and strips any stale :topicId so
the URL matches the freshly opened topic. Restores Nav.test.tsx.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🧹 chore: remove unused desktop upload IPC
* 🔥 feat(heterogeneous-agent): remove lab flag for GA rollout
External CLI agents (Claude Code, Codex) are now always available on desktop
without the lab toggle. Drops the `enableHeterogeneousAgent` preference,
selector, settings switch, locale strings, and menu-item gating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⬆️ chore(deps): bump @lobehub/ui to ^5.9.6 and @lobehub/editor to ^4.9.3
Unpin from exact versions so future patch/minor releases roll in automatically.
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(model-runtime): guard tool_use.input against non-object parsed arguments
Anthropic tool_use.input and Gemini functionCall.args both require a plain
object. Models occasionally emit malformed JSON whose top-level shape parses
into an array / null / primitive (e.g. unescaped quotes inside long string
args make the parser re-segment the payload). Previously we assigned the
parsed value directly, causing 400 "Input should be a valid dictionary".
Now guard the parsed value and fall back to {} with a console.warn carrying
tool id / name / parsed type, so we can monitor real-world frequency.
Refs: LOBE-8201
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): recover tool_call input from parsed[0] when arguments parse to an array
Previously fell back to {} when JSON.parse returned a non-object (array /
null / primitive). For the array case, prefer best-effort recovery from
element[0] instead — covers two real model failure modes:
* Single-element wrap: model emitted `[{...real args...}]` instead of
`{...}` → full recovery
* Unescaped quotes re-segmenting a long string arg into multiple objects
→ element[0] still carries the first legit key (e.g. `content` for
writeLocalFile), so partial intent is preserved instead of total loss
Falls back to {} for empty arrays, arrays whose first element isn't a
plain object, and the null/primitive cases (unchanged behavior).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote /devtools out of the main layout and break the monolithic gallery
into a layout + sidebar + per-tool detail route (/devtools/:identifier).
Each builtin-tool category (inspectors, interventions, placeholders,
streamings) now exposes a list*Entries registry helper so the sidebar can
enumerate them alongside the existing renders.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🐛 fix(model-runtime): use safety_identifier instead of user for OpenAI Responses API
OpenAI Responses API rejects the deprecated `user` parameter ("Unsupported
parameter: user"). Switch the three Responses API call sites
(generateObject, handleResponseAPIMode, generateObjectWithTools) to send
`safety_identifier` instead. Chat Completions paths are left untouched
since this factory backs many openai-compatible providers that still
accept `user`.
Fixes LOBE-8202
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): tolerate null function.name in streaming tool_call deltas
Some providers (NVIDIA NIM with z-ai/glm5 and qwen3.5-MoE, plus some
aihubmix-style proxies) open a streaming tool_call with
\`function.name = null\` as a start marker and supply the real name in a
later delta. The strict MessageToolCallSchema threw ZodError mid-stream
and killed the whole operation before any tokens were even recorded.
- parseToolCalls: coerce null/undefined name to '' before Zod parse;
merge name from subsequent deltas (previously only arguments merged).
- RuntimeExecutors: drop tool_calls whose name never resolved to a
non-empty string before pushing to state.messages, so they can't
poison subsequent history replays on strict providers.
Closes LOBE-8199.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💬 chore: trim RuntimeExecutors state-persist comment to the phenomenon
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(claude-code): polish ToolSearch inspector tag
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(tasks): preserve topic title when handoff is missing
Task activity rows rendered "Untitled" while the topic was still running
because the activity builder read `handoff.title` (populated post-summary)
and fell back straight to a hardcoded constant. Join `topics` in
`findWithHandoff` and fall through `handoff.title → topics.title → Untitled`
so running topics show the task name instead of "Untitled".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(conversation): add defaultWorkflowExpandLevel to control workflow fold default
Replace WorkflowCollapse.defaultStreamingExpanded (bool) with
defaultWorkflowExpandLevel ('collapsed' | 'semi' | 'full'), threaded
through MessageItem → AssistantGroup → Group → WorkflowCollapse and
exposed on ChatList (applies to the default item renderer only).
When set, pins both the initial state and post-completion reset so
'full' keeps tool-call groups expanded across streaming → complete;
pending intervention still forces expansion.
Apply 'full' in the task detail TopicChatDrawer so viewers see all
tool details by default. Migrate the Onboarding caller from
defaultWorkflowExpanded={false} to defaultWorkflowExpandLevel='collapsed'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(tasks): restart detail polling after data arrives
SWR's function-form refreshInterval is evaluated on effect mount and after each
timer fires. When the first call runs with cache.data=undefined, our function
returned 0 — so no timer was ever scheduled, and polling never started even
after the fetch populated the cache. Drive polling from a reactive zustand
selector instead, so refreshInterval is a stable number that flips once the
task/topic status is known.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): rename paused label to "Pending review"
"Paused" read like the task was stopped by the user. The actual semantic is
"agent has finished a run and is waiting for user to review and nudge it next" —
so rename the label in STATUS_META and the matching i18n keys (status.paused
and the kanban column needsInput). Also promote paused into USER_SELECTABLE_STATUSES
so users can explicitly park a task back into this state from the context menu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): add canceled kanban column
Expose a dedicated "Canceled" column in the kanban board so canceled tasks no
longer blend into the done column. Defaults to hidden (alongside done) to keep
the board compact, and maps the new column key through COLUMN_STATUS_ICON plus
the i18n table that KanbanColumn already referenced but was missing an entry
for.
* 💄 style(tasks): brighten priority icon and add label fallback
- Use colorTextSecondary (brighter than colorTextDescription) for non-urgent
priority icons so they read against the row background.
- Add a static label string to PRIORITY_META so callers can pass it as the
i18n defaultValue instead of an empty string — prevents unlocalised UI when
a translation is missing mid-rollout.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): route 1–N hotkeys to hovered status/priority submenu
The task context menu already supported number shortcuts to switch status. Extend
that to priority: when the user hovers the Priority submenu, pressing 1–5 picks
the corresponding priority level. A ref tracks which submenu is active (defaults
to Status on open) so the keydown handler knows which list to index into.
Also pick up meta.label as the i18n defaultValue for priority entries, matching
the new PRIORITY_META field so missing translations fall back to readable text
instead of an empty string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): drop column count from collapsed hidden panel header
The vertical collapsed header was getting noisy with "Hidden · 2" style
duplication — the count is already implied by the expanded tooltip, and the
vertical orientation makes the trailing number crowd the icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): show hotkey hint and check in status/priority menu
Surface the 1–N keyboard shortcuts next to each status/priority entry, with a
check icon on the currently selected value. Extract the render into a shared
menuExtra helper so TaskStatusTag and TaskPriorityTag share the same pattern
instead of each inlining its own layout.
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(FloatingChatPanel): add single-instance mount guard
* ✨ feat(FloatingChatPanel): add inner ChatBody layout
* ✨ feat(FloatingChatPanel): add reusable floating conversation panel
* ✅ test(FloatingChatPanel): add props wiring smoke tests
* Refactor agent topic and page routes
* Restore topic page routing for floating chat panel
* ✨ feat(FloatingChatPanel): enhance ChatBody and TopicItem for improved routing and styling
- Updated ChatBody to maintain scroll ownership while hiding overflow.
- Refactored TopicItem to correctly highlight active topics based on routing context.
- Added tests for TopicItem to ensure correct active state behavior.
- Introduced static styles for FloatingChatPanel to manage layout overflow.
Signed-off-by: Innei <tukon479@gmail.com>
* chore: help to merge & rebase
* chore: align merge with canary — drop pkg.pr.new ui, adopt canary useMenu, remove NotebookButton
* ✨ feat: add ViewSwitcher component and update localization for chat views
- Introduced a new ViewSwitcher component to toggle between chat, page, and task views in the conversation header.
- Updated English and Chinese localization files to include new labels for the view switcher options.
- Refactored the conversation header to integrate the ViewSwitcher, enhancing the user interface for better navigation.
Signed-off-by: Innei <tukon479@gmail.com>
* fix: update @lobehub/ui to version 5.9.1 and refactor FloatingChatPanel to use FloatingSheet component
- 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: add TopicCanvas and TitleSection components for topic management
- 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>
* ✨ feat(agent-page): bind documentId to URL and introduce HeaderSlot
- 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
* Use topic titles for auto-created page documents
* Add page-agent init gating and runtime diagnostics
* Support current-topic agent documents
* Implement Active Topic Document and Disabled Tool Call Filtering
- 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: enhance agent document management with LiteXML operations
- 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>
* 🐛 fix: apply agent document xml edits directly
* Refine document cache invalidation and editor hydration
* 🐛 fix: stabilize agent topic hydration
* fix: update @lobehub/editor dependency version and clean up test mocks
Signed-off-by: Innei <tukon479@gmail.com>
* Potential fix for pull request finding 'Useless assignment to local variable'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* 🐛 fix(document): preserve pending diff nodes through save path
Skip normalizeEditorDataDiffNodes on every autosave so diff nodes awaiting
user review survive persistence. Normalization now runs only on explicit
Accept/Reject via DiffAllToolbar. Also flip headless litexml ops to delay:true
to match the new review flow.
* 🐛 fix(agent): detect agent sub-route from URL params not cached topic
isInAgentSubRoute used routeTopicId (with activeTopicId fallback) as its
base path. On /agent/:aid/profile with a cached activeTopicId, the base
became /agent/:aid/:cachedTopicId which pathname cannot startsWith, so
sub-route detection returned false and sidebar topic clicks only called
switchTopic without routing back to chat — users stayed stuck on profile.
Derive the sub-route base from params.topicId directly so stale store
state cannot mask the check. routeTopicId export keeps the fallback for
sidebar highlighting.
* 🐛 fix(page): repair topic page document recovery
* 🐛 fix(page-agent): block tool calls when page editor is not mounted
scope is topic-bound not route-bound, so navigating from /agent/.../Page
to /agent/... keeps scope==='page' and PageAgentIdentifier stayed in the
injected plugin list. The LLM could still call initPage / modifyNodes /
etc. against a stale editor reference, returning misleading success
(e.g. nodeCount=0).
Two layers of guard:
- PageAgentExecutor wraps `invoke` and returns a structured
PAGE_EDITOR_NOT_MOUNTED / kind: 'replan' result when the runtime
editor is not mounted, pointing the LLM at lobe-agent-documents.
- streamingExecutor drops PageAgentIdentifier from the tool set via
the new `composeEnabledTools` pipeline when scope==='page' and
the page-agent runtime is not ready.
Also extract the tool-set composition (inject merge + runtime drops)
out of the ~320-line internal_createAgentState into
`mecha/toolSetComposer`, with unit tests.
* 🐛 fix(chat): unify message stream for /agent/:topicId and /page/:docId
Before this change a page-scoped conversation (FloatingChatPanel with
scope='page' in the /Page route) partitioned the client message store by
scope, so /agent/:topicId and /agent/:topicId/page/:docId each built their
own messagesMap slot and SWR cache — but the TRPC getMessages endpoint
ignores scope and returned the same messages for both, producing duplicate
fetches and a visible message-history split between the two surfaces.
Fixes by keeping scope='page' as a capability/surfacing marker only:
- messageMapKey: collapse 'page' to the default scope early in
toMessageMapContext, so threadId/groupId still win and only the
main/page pair actually unifies.
- useFetchMessages: build the SWR key from identity fields
(agentId, groupId, threadId, topicId) instead of the full
ConversationContext, so scope no longer partitions the cache.
agentConfigResolver/streamingExecutor/composeEnabledTools still read
scope='page' from operation.context for PageAgent injection and
initialContext.pageEditor wiring — the capability layer is unchanged.
Also fix two pre-existing test regressions surfaced by re-running the
impacted suites:
- streamingExecutor page-editor initialContext test now mocks
pageAgentRuntime.isReady() (required since the PageAgent editor-ready
guard landed).
- FloatingChatPanel default shell props test updated to match the
[180,320,520,800] snap points introduced in 62dc91e444.
* ♻️ refactor(FloatingChatPanel): read main slot without changing scope
Revert the global messageMapKey/SWR-key changes from b650cdc9d7 — the
global collapse over-reached and coupled message routing to scope in
ways other surfaces don't want. Instead, specialize only the place that
actually has the dual-role problem.
`scope` should be a capability marker (PageAgent tool + pageEditor
initialContext injection), not a message-list partition. Floating panel
on /agent/:topicId/page is the only caller that sets scope='page', and
its message list should mirror /agent/:topicId — the surfaces share a
topic.
Local collapse in FloatingChatPanel: compute chatKey with
`scope === 'page' ? 'main' : scope`, so messagesMap is read from the
main slot. The downstream ConversationContext keeps scope='page' for
the capability layer; only the slot lookup is specialized.
Kept from b650cdc9d7 (unrelated to the revert):
- streamingExecutor test mocks pageAgentRuntime.isReady() — required
by the PageAgent editor-ready guard in 01ef7bc142.
- FloatingChatPanel snap-points test matches [180,320,520,800] from
62dc91e444.
* 🐛 fix(FloatingChatPanel): simplify chat key computation for message retrieval
Signed-off-by: Innei <tukon479@gmail.com>
* 🐛 fix(index.desktop.test): update LocationProbe to reflect route changes and improve test accuracy
Signed-off-by: Innei <tukon479@gmail.com>
* Constrain agent header title under centered switcher
* 🐛 Fix conversation header view switcher layout
* 🐛 Fix agent topic path links and cmdk context
* 🐛 fix(test): align document history fixtures and layout ui mock
* 🐛 fix(e2e): support dialog-based topic rename
* ♻️ refactor(debug): use scoped debuggers for PR logging
---------
Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Neko Ayaka <neko@ayaka.moe>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* ✨ feat: polish task list id and date display
* ✨ feat: hide completed tasks from agent task card list
Completed tasks crowd the homepage card list and bury the ones that
still need attention; extract sort/limit into a testable helper so the
filter lives in one place.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(claude-code): render Agent tool streaming with instruction and subagent thread toggle
While a subagent is running (args parsed, tool_result not back) the CC
Agent tool fell back to the generic 参数列表 dump. Surface the instruction
markdown and, once the executor has created the subagent Thread, the
open/close subtopic button — so the user can jump into the live
conversation instead of waiting for the summary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): add /tasks sidebar entry and Linear-style item context menu
- Wire up /tasks as a top-level home sidebar item (gated on enableAgentTask) and register route metadata for Electron tab title
- Render a dashed UserRound placeholder when a task has no assignee, and add a search input + arrow-key navigation to the agent picker popover
- Wrap task list rows in a ContextMenuTrigger with status/priority submenus, copy id/link, and delete-with-confirm
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): unify task routes under /tasks and /task/:id, drop agent-scoped pages
Removes the per-agent `/agent/:aid/tasks` list and detail routes in favor of a
single cross-agent surface (`/tasks` list/kanban + `/task/:taskId` detail).
Kanban board now fetches across all agents via `useFetchTaskGroupList({ allAgents })`,
fixing the blank board on the `/tasks` route.
UI polish shipped alongside:
- Hidden kanban columns panel persists to global status, pinned to the right with
a swim-lane background to match other columns.
- Breadcrumb chevron margins tightened; separator, ancestors, and task detail
crumbs share the same compact styling.
- TaskDetailAssignee renders a clickable "Unassigned" placeholder when no agent
is set, so the selector is always reachable.
- Run button stays clickable without an assignee; falls back to the inbox agent
on click so users get a working default.
- Breadcrumb drops the per-agent tasks link; nav inside agents removes the now
dangling Tasks tab since `/tasks` is a top-level sidebar entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): hide completed & canceled tasks by default with Show footer
Hides completed/canceled tasks by default in the list view with a Linear-style "N tasks hidden by display options · Show" footer and a toggle in the display-options popover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): add copy id/link actions to task detail header, use app origin
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🔥 refactor(tasks): drop agentId plumbing from unified task detail route
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): float topic chat drawer with read-only messages
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): inline subtasks add button and run button loading state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(workflows): unify hono scaffold and add task on-topic-complete webhook
Consolidate workflow routes behind a single Hono app mounted at the
catch-all /api/workflows/[[...route]], with per-domain sub-apps. New
workflow segments now only need a folder under src/server/workflows-hono/
plus one app.route(...) line in the root — no new Next.js route files.
Also implements /api/workflows/task/on-topic-complete, which task.run
registers as the onComplete webhook. The handler wires the payload into
TaskLifecycleService.onTopicComplete; task.run now also includes
taskIdentifier in the webhook body so the handler skips a DB lookup.
LOBE-6659
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): align subtasks header pill with add button on same row
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(tasks): add AgentTaskManager side panel and polish task detail
- Mount AgentTaskManager conversation alongside the task detail route and
sync the task's assignee agent into chat store so the right panel talks
to the correct agent
- Reverse activities timeline to newest-first and float the comment input
on top with a card-styled container and guiding placeholder copy
- Redesign TopicCard with a live status icon, meta row, and dropdown
actions (open run / copy id); introduce shared TopicStatusIcon with
animated running state
- Swap task status palette: running uses warning+CircleDot, paused uses
info+Hand; show numeric shortcut extras on context menu status/priority
items alongside the checkmark for the current value
- Refresh hidden-columns panel to panel-open/close icons and inline the
count beside the header
- Drop fixed min height on create-task inline editor; tighten activity
row padding
- Fix Flexbox import in useTaskItemContextMenu (react-layout-kit → @lobehub/ui)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): show topic status icon in chat drawer title
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(tasks): drop stale AutoSaveHint on task list page
Task list does not save anything, but it reused the global taskSaveStatus from detail page — after editing a task, switching back to the list would still show "latest version loaded".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): drop redundant status tag in topic chat drawer title
Status is already expressed by the colored TopicStatusIcon next to the title.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): add tooltip hint for unassigned assignee
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): polish topic chat drawer border and spacing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(tasks): show check before shortcut in context menu extra
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(conversation): unify spacer + scroll-to-user hooks
Merge `useConversationSpacer` and `useScrollToUserMessage` into a single
`useConversationScroll` hook to eliminate the races that caused occasional
"send message but viewport doesn't pin to the new user message" regressions.
Race fixes:
- Single `prevLengthRef` and a single send-detection effect, replacing two
hooks with independent length tracking that could disagree across renders.
- `virtuaRef` is passed in and dereferenced at call time instead of reading
`virtuaRef.current?.scrollToIndex` during render — removes the window
where the ref hadn't been attached yet when a send fired.
- Pin state is an explicit `{ index, seenActive }` ref with three clear
transitions (send / layout-bump / user-scroll-up) instead of several
cooperating refs + derived flags.
- Retries are layout-driven: each `spacerLayoutVersion` bump re-fires
`scrollToIndex` exactly once. The old 0/32/96ms timer fan-out is gone.
Also bumps `AT_BOTTOM_THRESHOLD` 100 → 300 so `atBottom` stays stable
while the spacer is settling.
* ♻️ refactor(conversation): extract sub-hooks from useConversationScroll
Split the unified conversation scroll hook into four cooperating sub-hooks
in the same file so each layer has one clear concern:
- useSpacerLayoutSignal — ResizeObserver on the spacer node → version bumps
- useSpacerHeight — natural height / mount lifecycle / shrink state
- usePinController — pin state machine + virtua-aware scroll dispatch
- useScrollShrink — scrollOffset delta → cancel pin / shrink spacer
The main hook now owns just the send-detection effect, the pin re-fire on
layout settle, and derived output. Behavior is unchanged — same 15 tests
pass — but each piece is now readable in isolation.
* ⚡️ perf(conversation): narrow VirtualizedList subscription to a boolean
VirtualizedList only needs to know whether the second-to-last message is
the user's — the full displayMessages array was never used. Move the
derivation into `dataSelectors.isSecondLastMessageFromUser` so the
component re-renders on role transitions, not on every assistant token.
* ✅ test(e2e): cover conversation scroll behavior across the auto-scroll setting
Adds three scenarios under `@AGENT-SCROLL-*` that exercise the merged
`useConversationScroll` hook end-to-end through the real chat UI:
- AGENT-SCROLL-001 — with auto-scroll ON, the viewport ends up near the
bottom once a long response has finished streaming.
- AGENT-SCROLL-002 — with auto-scroll OFF, the user's message stays
pinned to the top and the viewport does not chase the assistant.
- AGENT-SCROLL-003 — with auto-scroll ON, scrolling up mid-stream cancels
the pin and the viewport is not yanked back to the bottom afterwards.
Also extends the LLM mock with `setConfig` / `resetConfig` so scenario 3
can slow the response down enough for the mid-stream manual scroll, and
adds `presetResponses.longScrollArticle` (long enough to overflow the
viewport so scroll assertions are meaningful).
* ✅ test(e2e): cover send-time pin-to-top as its own scenario
AGENT-SCROLL-004 exercises the core pin behavior of `useConversationScroll`
independent of the auto-scroll setting: after sending a message, the user's
turn must be anchored to the top of the scrollport. Uses the slow-response
mock so the assertion runs while the spacer is still mounted.
* ✅ test(e2e): tune scroll scenarios after runtime validation
Run outcomes against a cold Next dev server (paradedb + next dev -p 3006):
- AGENT-SCROLL-001 (enabled → viewport stays near bottom) — passing
- AGENT-SCROLL-002 (disabled → user msg pinned to top) — passing
- AGENT-SCROLL-004 (send pins user msg to top) — passing
- AGENT-SCROLL-003 (mid-stream scroll-up cancels pin) — skipped
Scenario 3 is marked `@skip` until the LLM mock supports truly chunked
SSE streaming. The current mock fulfils the whole body at once, which
collapses the "mid-stream" window to a handful of ms and makes the
manual-scroll timing race-prone. The cancel-pin path is already
covered at the unit level in `useConversationScroll.test.ts`, so the
e2e placeholder just keeps the scenario on the radar.
Other tweaks for dev-mode reliability:
- Bumped setting-toggle step timeout to 90 s (turbopack cold compile of
`/settings/chat-appearance` can exceed the default 30 s on first hit)
- Relaxed the inner `networkidle` / `toBeVisible` waits there to match
- Added a matching negative-path Then ("not pinned") that would power
the skipped scenario once the mock is upgraded
* 🐛 fix(conversation): rebind pin tracking on every new turn
The message index refs that drive `latestAssistantSignature` and the
messages `ResizeObserver` were plain `useRef`s updated inside the send-
detection effect. On the render triggered by spacer state updates right
after a send, `[dataSource, displayMessages]` could be unchanged, so the
signature memo returned its cached value and the observer effect never
rebound to the new turn's user/assistant DOM nodes. Under certain commit
orderings this left spacer height tracking the previous turn and let
the pin-to-user anchor drift.
Turn the indices into state, include `assistantMessageIndex` in the
signature memo's deps, and forward the state (not a ref) to
`useSpacerHeight`. The observer now reliably rebinds to the fresh
nodes on the very next render.
Adds a unit regression covering the observer-rebind path and an e2e
scenario (`AGENT-SCROLL-005`) that sends two consecutive turns and
checks that the second user message still pins to the top.
* feat: add the agent runtime tools call hooks
* feat: add more agent runtime hooks
* fix: add the lost hooks
* fix: add the agent runtimes hooks test
* fix: slove some error
* fix: change the as any to hooksEvent
* fix: slove the lint error
* fix: slove the lint error
* fix: slove the lint error
* fix: clean the code
* fix: change the toolCallCounts into all mode & add all hooks into qstash runtime way
* 🐛 fix: harden beforeToolCall mock validation and remove userId fallbacks
- dispatchBeforeToolCall returns { content, isMocked } instead of { content } | null
for explicit mock detection (avoids falsy content edge cases)
- mock() rejects invalid content: empty string, undefined, object, array, number, null
- Remove all `userId: ctx.userId || ''` fallbacks — userId absence should surface, not silently degrade
- beforeToolCall adds separate dispatch() observation path for QStash webhook delivery
- Add BeforeToolCallObservationEvent type for production webhook payload
- Add 3 unit tests for mock content validation edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat(builtin-skills): add bot platform setup guide reference
Add `references/bot-setup-guide` to the LobeHub skill with step-by-step
credential setup instructions for Discord, Slack, Telegram, Feishu, Lark,
QQ, and WeChat. Enables agents to guide users through platform bot
configuration end-to-end via the `lh bot` CLI workflow.
* ✨ feat(builtin-skills): split bot setup guide into per-platform references
Replace the single `bot-setup-guide` reference with 7 platform-specific
guides (Discord, Telegram, Slack, Feishu, Lark, QQ, WeChat), each with
detailed step-by-step credential setup instructions matched to the actual
schema fields. Also update the LobeHub skill description to trigger
activation when users mention connecting messaging platform bots.
* ♻️ refactor(builtin-skills): nest bot platform guides under references/bot/ directory
Move bot setup guide resource keys from flat `references/bot-*` to
nested `references/bot/*` so they appear as a subfolder in the
skill resource tree instead of a flat list.
* 🐛 fix(builtin-skills): fix Telegram --app-id and WeChat CLI setup guide
- Telegram: add required --app-id (numeric bot ID from token prefix)
to the lh bot add command; explain how to extract it from the token
- WeChat: remove incorrect CLI QR scan flow; lh bot connect only starts
an already-configured provider and does not perform QR auth.
Redirect users to Web UI for initial WeChat setup
* 📝 docs(builtin-skills): clarify WeChat setup steps with exact UI navigation
Guide users to click 消息频道 (Message Channel) in the left sidebar
then select WeChat to get the QR code, matching the actual UI layout.
* 💄 style: compact kanban card layout with variant prop
LOBE-8091
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 💄 style: reduce assignee avatar size from 22px to 18px
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expose aiAgent tRPC procedures (execAgent, interruptTask,
refreshGatewayToken) to the mobile client, enabling Gateway
mode for server-side agent execution with WebSocket streaming.
LOBE-8123
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat(desktop): gate screen capture on macOS recording permission
Prompt a native dialog before opening the capture overlay when macOS
Screen Recording permission is missing, with an Open Settings button
that deep-links to System Settings.
* 💄 style(desktop): add hint pill to screen capture overlay
Bottom-left pill with three grouped hints (hover to pick a window, drag
to crop a region, Esc to exit), sharing the WindowTag pill language.
Hidden during drag and after a selection so it doesn't clutter.
* 🚨 fix(test): mock MarketService in execGroupAgent integration test
The first test case was timing out (~9.5s) because execAgent makes a
real HTTP request to market.lobehub.com via MarketService.getLobehubSkillManifests().
Mock MarketService to return empty skill manifests, eliminating the
network dependency that caused the cold-start timeout in CI.
* ✨ feat(creds): integrate Klavis authorization status into lobe-creds system
Inject Klavis connected/available services into the creds systemPrompt so
agents are aware of Klavis-managed OAuth authorizations and stop asking
users for manual tokens. Add connectKlavisService API to allow agents to
initiate Klavis OAuth connections from within chat conversations.
Fixes LOBE-7243
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix(creds): cleanup dangling intervals and add server runtime for connectKlavisService
- Clear windowCheckInterval in cleanup to prevent dangling interval
- Add connectKlavisService to CredsExecutionRuntime for server-side support
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: prevent Markdown stream replay when vlist remounts streaming items
Long streaming replies replayed the token-by-token animation when users
scrolled them out of view and back. virtua VList was recycling streaming
items, so the Markdown component lost its animation state on remount.
- Pin currently-streaming messages via `keepMounted` on the VList so
their DOM stays mounted regardless of scroll position.
- Scope the `animated` flag to the last answer segment inside an
AssistantGroup. Finalized blocks now render as static markdown, so any
future remount cannot replay completed content.
* ♻️ refactor: drop redundant `animated` prop drilling in AssistantGroup
The store already exposes per-block streaming state via
`isMessageGenerating(blockId)`: the streaming write target's
DB message id (== block.id) is associated to the running operation,
so finalized blocks naturally resolve to `generating=false` and the
active block to `true`. The prop drilling added in the prior commit
only duplicated this and did not actually prevent replay on the
streaming block itself.
Keep the real fix (`keepMounted` on the VList) which pins the
streaming item so vlist recycling never resets the Markdown
animation state in the first place.
* ✨ feat: pin text-selection hosts in vlist keepMounted
Recycling a virtualized item whose node hosts a Selection anchor or
focus silently drops the user's highlight. Track message ids that
currently contain an active selection via a `selectionchange` listener
and merge their indices into `keepMountedIndices` alongside the
streaming pins.
- New hook `useSelectionMessageIds` walks Selection range endpoints up
to the nearest `[data-message-id]` host and returns a stable Set of
ids, returning the previous reference when the set is unchanged.
- VirtualizedList merges selection indices with streaming indices and
hands the union to VList's `keepMounted`.
Replace the awkward `from 'buffer/'` trailing-slash workaround with a
pnpm alias `"buffer.js": "npm:buffer@^6.0.3"`, so import sites read
`from 'buffer.js'`.
Dev server does not serve /manifest.webmanifest, which causes a console
404 in the browser. Add a shared dev-only Vite plugin that removes the
<link rel="manifest"> tag via transformIndexHtml for web/mobile/desktop.
* 🐛 fix: add env var support for missing Coding Plan providers
Add zod schema and runtimeEnv mappings for BailianCodingPlan,
GLMCodingPlan, MinimaxCodingPlan, and VolcengineCodingPlan in llm.ts.
These were missing when the providers were added in #13203, causing
them to fall back to OPENAI_API_KEY instead of their own env vars.
* 🐛 fix: add env var support for OpenCode Zen and OpenCode CodingPlan providers
Add zod schema and runtimeEnv mappings in llm.ts for OpenCodeZen and
OpenCodeCodingPlan providers introduced in #13943. Without these,
getParamsFromPayload falls back to OPENAI_API_KEY.
* ✨ feat: add OpenCode Zen and OpenCode Go providers
Add support for OpenCode Zen (dynamic model gateway) and OpenCode Go
(subscription-based coding plan) with full model definitions, runtime
implementations, and provider configurations.
- OpenCode Zen: curated models via single API key, dynamic model fetching
- OpenCode Go: coding models (GLM, Kimi, MiMo, Qwen, MiniMax)
- Both use @ai-sdk/openai-compatible runtime
- Go models include abilities, pricing, and extendParams settings
* ✨ feat: add 35 preset models to OpenCode Zen provider
Populate OpenCode Zen with all non-deprecated models from models.dev API
including Anthropic (9), OpenAI (13), Google (2), Zhipu GLM (2), Alibaba
Qwen (2), Kimi (1), MiniMax (2), Nvidia (1), and OpenCode (1). Switch
from dynamic model fetching to static model list.
* ♻️ refactor: migrate OpenCode Zen/Go to RouterRuntime and align extendParams
Migrate both providers from openaiCompatibleFactory to createRouterRuntime
to match OpenCode's native multi-SDK architecture:
Zen (4 routers):
- anthropic for Claude, google for Gemini, openai+Responses for GPT-5.x,
openai fallback for all others (GLM/Kimi/MiniMax/Qwen)
Go (2 routers):
- anthropic for MiniMax M2.5/M2.7, openai fallback for all others
Fix model-bank extendParams to match OpenCode variants() behavior:
- Remove extendParams from GLM/Kimi/MiniMax/BigPickle/Nemotron (variants return {})
- Change Qwen from enableReasoning+reasoningBudgetToken to reasoningEffort
- Change Go MiMo to reasoningEffort
* 🐛 fix: fix OpenCode Zen/Go Anthropic baseURL and remove Google router
- Add stripV1() to strip trailing /v1 from baseURL for Anthropic SDK
since it auto-appends /v1/messages to the base URL
- Remove Google router from Zen - Gemini models fall to openai-compatible
fallback as Zen Gateway does not support Google SDK format
- Keep user-configurable baseURL support while preventing /v1 duplication
* 🐛 fix: add missing package.json exports for opencode and stepfunCodingPlan
* ✨ feat: limit default enabled models to latest versions for OpenCode Zen/Go
Zen: claude-opus-4-7, gemini-3.1-pro, gpt-5.4, glm-5.1,
minimax-m2.5-free, nemotron-3-super-free, big-pickle
Go: glm-5.1, qwen3.6-plus, minimax-m2.7
* 🐛 fix: include opencodego in Coding Plan provider tag check
* ♻️ refactor: align model display names with official provider naming
Update Qwen3.6 Plus, Qwen3.5 Plus, and MiMo-V2 Omni display names
to use spaces instead of hyphens, matching the official provider naming
convention used in lobehub.
* ♻️ refactor: rename opencodego to opencodecodingplan for suffix consistency
Rename internal ID from opencodego → opencodecodingplan to align with
other Coding Plan providers. Display name remains "OpenCode Go".
This allows isCodingPlanProvider() suffix check to work without exceptions.
* 🐛 fix: remove broken stepfunCodingPlan export — file not on this branch
* ♻️ refactor: align MiMo-V2 Pro display name with official provider naming
* 🌐 i18n: add Chinese translations for OpenCode Coding Plan and Zen providers
* ✨ feat: add AgentTaskList component on agent welcome page (LOBE-6597)
- AgentTaskList with TaskListHeader, TaskItem, and styles
- Embedded in AgentWelcome below ToolAuthAlert
- Each task rendered as independent rounded card with status badge
- Status: green filled circle (Done), blue circle (In progress)
- Card width matches chat input (960px)
- i18n keys for taskList.title and taskList.viewAll
- Fix updateReview type to use TRPC-inferred type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add Tasks page at /agent/:aid/tasks with route, breadcrumb, and view toggle (LOBE-6597)
- Register tasks route in both desktopRouter.config.tsx and .desktop.tsx
- Thin route page at src/routes/(main)/agent/tasks/index.tsx
- Feature components in src/features/AgentTasks/: page, breadcrumb, header with list/kanban toggle, full task list
- Wire up "View All Tasks" navigation from AgentTaskList welcome card
- Add i18n keys (taskList.activeTasks, taskList.breadcrumb.task) and generate translations via pnpm i18n
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add Task detail page at /agent/:aid/tasks/:taskId (LOBE-6597)
- Register :taskId child route in both desktopRouter configs
- TaskDetailPage with auto-save hint, breadcrumb, and scrollable content
- TaskDetailHeader: editable title (borderless Input), Run/Pause button, status/priority tags, delete
- TaskInstruction: click-to-edit Markdown with debounced auto-save
- TaskSubtasks: sub-issues list with status badges
- TaskActivities: timeline with topic/brief/comment icons
- TaskItem now navigates to detail page instead of just setting activeTaskId
- Add taskDetail.* i18n keys with generated translations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add TaskModelConfig, TaskScheduleConfig, and refine Task detail UI (LOBE-6597)
Add model/provider selector and periodic execution config to Task detail page.
Refine TaskDetailHeader, TaskInstruction with auto-save and i18n support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: refine Task detail UI with Linear-style design (LOBE-6597)
- Redesign SubTasks with collapsible header, progress circle, hover + click navigation
- Redesign Activities with agent avatar, comment input box, and Linear-style layout
- Add TaskParentBar showing parent task relationship with sibling navigation popover
- Add delete confirmation modal using App.useApp().modal.confirm
- Move ModelSelect to separate row below action bar
- Fix zustand selector recreation in ActivityItem
- Replace hardcoded colors with cssVar tokens
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add Properties panel, parent link hover, activity icon, and lifecycle save status (LOBE-6597)
- Add TaskProperties sidebar with collapsible status/priority dropdowns
- Parent bar: clickable parent link with hover, sibling navigation popover on progress
- Activity title: add BotMessageSquare icon
- Fix lifecycle actions not updating taskSaveStatus (saving/saved indicator)
- Filter status dropdown to only user-selectable states (backlog/completed/canceled)
- Add test task creation script for dev
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add recursive tree view for subtasks with Linear-style connecting lines (LOBE-6597)
- Add buildTaskTree utility to convert flat getTaskTree API response into nested tree
- Implement SubtaskTreeItem recursive component with CSS connecting lines (├─ and └─)
- Fetch full task tree via taskService.getTaskTree for nested subtask display
- Show loading spinner during tree fetch, fallback to flat list on error
- Remove padding-inline from AgentTaskList container
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: address PR review — delete redirect, debounce cleanup, schedule resync (LOBE-6597)
- Redirect to task list after successful delete (P1)
- Clean up instruction debounce timer on unmount/task switch to prevent stale writes (P1)
- Resync TaskScheduleConfig local state when active task changes (P2)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor: use backend nested subtasks directly, remove buildTaskTree (LOBE-6597)
Backend now returns nested subtasks in task.detail (LOBE-6814).
Remove buildTaskTree utility, getTaskTree API call, and loading state.
Use TaskDetailSubtask from @lobechat/types instead of local interface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ⚡ perf: add optimistic update and save status for model config change (LOBE-6597)
updateTaskModelConfig now immediately reflects new model/provider in UI
via optimistic store dispatch, and tracks taskSaveStatus (saving/saved).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ⚡ perf: skip redundant refreshTaskDetail on successful model config update (LOBE-6597)
Optimistic update is trusted on success — no need for full detail re-fetch.
Aligns with updateTask pattern. Refresh kept only in error path for revert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: use backend author info for activities, fix AgentTaskList after AgentHome refactor (LOBE-6597)
- Activity: use act.author (TaskDetailActivityAuthor) from backend instead of agentMap lookup (LOBE-7013)
- AgentTaskList: fix agentId from useParams instead of useAgentStore.activeAgentId (was undefined)
- AgentHome: integrate AgentTaskList into new AgentHome layout (replaces old AgentWelcome)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: show participant avatars on task cards, use backend author for activities (LOBE-6597)
- TaskItem: display up to 3 participant avatars next to task title (LOBE-6805)
- Activity: use act.author from backend instead of agentMap lookup (LOBE-7013)
- AgentHome: integrate AgentTaskList into new AgentHome layout
- Revert AgentTaskList/TaskItem agentId back to useAgentStore (works correctly when mounted)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor: fix type safety, memoize participants filter, extract avatar styles (LOBE-6597)
- Use TaskParticipant type instead of `any` in filter/map
- Compute displayParticipants once with useMemo (was filtering twice per render)
- Move avatar overlap styles to CSS classes (was inline objects per render)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🔇 chore: hide kanban view toggle until implemented (LOBE-6597)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor: export TaskStatus/TaskPriority/TaskActivityType from @lobechat/types (LOBE-6597)
Replace hardcoded string/number types with shared type aliases:
- TaskStatus: 'backlog' | 'canceled' | 'completed' | 'failed' | 'paused' | 'running'
- TaskPriority: 0 | 1 | 2 | 3 | 4
- TaskActivityType: 'brief' | 'comment' | 'topic'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: update
* style: update
* style: update
* style: update
* style: update
* style: update
* style: update
* style: update
* style: update
* style: update
* ✨ feat: add Daily Brief module to homepage (#13851)
* ✨ feat: add Daily Brief module to homepage
Add a Daily Brief section below the chat input on the homepage that
displays unresolved briefs from the Agent Tasks system. Users can
resolve, comment, and provide feedback directly from the brief cards.
- Service: BriefService with listUnresolved, resolve, markRead, addComment
- Store: Independent Zustand store (src/store/brief/) with SWR data fetching
- Components: BriefCard, BriefCardActions (dynamic action buttons),
BriefCardSummary (Markdown with expand/collapse), CommentInput (@lobehub/editor)
- Three action types: resolve (closes brief), comment (resolve with text),
link (safe URL navigation with protocol validation)
- Fixed feedback button: adds task comment without resolving the brief
- Inline success state ("Feedback sent") with 1.5s auto-restore
- i18n: zh-CN + en-US translations
- Tests: 21 tests across service, store selectors, and components
- CLI: Register task and brief commands for local development
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add agent avatars to Daily Brief cards
Display stacked agent avatars next to brief card titles using the
new `agents` data from Arvin's enriched listUnresolved API (#13489).
- Add AgentAvatarInfo type and agents field to BriefItem
- Render overlapping circular avatars (20px, -6px overlap)
- Use cssVar.colorBgContainer for border (dark mode compatible)
- Extract avatar style to function to avoid inline object creation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor: clean up Daily Brief components
- Extract duplicate success state JSX into reusable SuccessTag component
- Remove redundant comments that describe what code does
- Use DEFAULT_AVATAR from @lobechat/const instead of hardcoded emoji
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: address PR review feedback for Daily Brief
- Use cssVar.colorBgBase instead of hardcoded #fff for primary button
text color (dark mode contrast fix)
- Add submitting state to CommentInput to prevent duplicate submissions
(disable buttons + show loading during async submit)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🌐 chore: generate i18n translations for Daily Brief
Run pnpm i18n to generate translations for all 18 locales.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor: use shared BriefType from @lobechat/types
Export BriefType union from packages/types and use it in
BRIEF_TYPE_COLOR and BRIEF_TYPE_ICON records for compile-time
key validation. Adding a new brief type now requires updating
the shared type, and TypeScript will flag missing mappings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: update
* style: update
* style: update
---------
Co-authored-by: Tsuki <976499226@qq.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: update
* style: update
* style: update
* style: update
* fix: stopPropagation
* fix: i18n
* 🐛 fix: wire comment inputs to editor instance so Send actually submits
CommentInput in AgentTasks and DailyBrief used antd TextArea inside
@lobehub/editor's ChatInput while reading content via
editor.getDocument('markdown'). The TextArea was never connected to the
editor instance, so getDocument always returned empty and handleSubmit
short-circuited silently — Send appeared to do nothing (no network
request fired).
Replace the TextArea with <Editor editor={editor} type="text"
variant="chat" /> so useEditor() actually drives the editable surface.
Keep plain-text behavior via markdownOption={false} +
enablePasteMarkdown={false}, and bind Cmd/Ctrl+Enter submit via
onPressEnter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: use participant.title after TaskParticipant schema rename (#13877)
PR #13877 renamed TaskParticipant.name → .title and added
.backgroundColor. Our branch's UI code (AgentAvatars, listViewOptions,
TaskList group header, Breadcrumb) was already written against the new
schema, but TaskProperties still read firstParticipant?.name — update
the last remaining call site so the type matches post-rebase.
backgroundColor is already plumbed through everywhere it applies within
#13877's scope; TaskActivities' TaskDetailActivityAuthor is a separate
type untouched by the PR and kept as-is.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: resolve type-check errors exposed after canary rebase
canary upgraded react-i18next to a version with typed i18n keys and
tightened @lobehub/editor's SendButton + IEditor APIs. Rebase pulled
these in, surfacing latent type errors in LOBE-6597 code.
- CommentInput: use editor.cleanDocument() (IEditor's actual API;
clearContent never existed).
- TaskActivities / TaskLatestActivity / TaskTriggerTag: type t as
TFunction<'chat'> so typed i18n accepts the known-literal keys used
inside module-level helpers.
- TaskPriorityTag / TaskStatusTag / listViewOptions: add
defaultValue: '' to dynamic-key t() calls (template literals and
Record lookups) to match the broad-key i18n overload.
- BriefCardActions: swap unusable <SendButton> (no children, no
iconPlacement) for <Button>; add defaultValue to the dynamic
brief-action key lookup; drop stale @ts-ignore.
- DailyBrief/CommentInput: drop unsupported children on SendButton;
keep label via title attribute.
- Recents/Item: type TYPE_ICON_MAP as Partial<Record<...>> so 'task'
(rendered via TaskStatusIcon elsewhere) is a safe absent key.
- brief/slices/list/action: cast briefService.listUnresolved() result
back to BriefItem[] (TRPC serialization widens BriefType to string).
- AgentTasks/TasksHeader: delete dead file — no importers and its
./style module was removed by an earlier refactor.
Also ran pnpm install to materialize the newly-extracted
@lobechat/agent-gateway-client workspace package (canary #13866),
clearing ~7 "cannot find module" errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor(builtin-tool-task): polish task tool paths (#13869)
* ✨ feat: navigate to task detail when clicking brief card header
Clicking the header row of a Daily Brief card (icon + title + time +
agent avatars) now jumps straight to the associated task, using the
brief's task-tree agent (with activeAgent / inbox as fallback).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: show parent task ids as clickable breadcrumb trail
Walk the cached parent chain from taskDetailMap and insert each ancestor's
identifier as a link between the "任务" entry and the current task name in
the task detail breadcrumb.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add cross-agent /tasks page with View All Tasks on Daily Brief
- Register `/tasks` route in desktop (web + Electron) and mobile router configs
- `useFetchTaskList` supports `allAgents` mode via options object API to fetch
tasks without agent filter; backend already supports optional assigneeAgentId
- `Breadcrumb` accepts optional `agentId`, renders "All tasks" crumb when absent
- `AgentTaskItem` navigation uses `task.assigneeAgentId` so clicks work from
the cross-agent page (falls back to `activeAgentId` for unassigned tasks)
- Extract `useScenarioEnabledTools` hook to share layout effect between
`/tasks/_layout` and `/agent/:aid/tasks/_layout`
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ♻️ refactor: use assigneeAgentId for task avatar instead of participants array
Replace AgentAvatars (took participants[]) with AssigneeAvatar (takes agentId,
resolves meta from agent store). This correctly represents that a task is
assigned to a single agent via assigneeAgentId/detail.agentId.
- New AssigneeAvatar component reads agent meta from agent store by ID
- TaskProperties reads activeTaskAgentId from task detail store
- listViewOptions uses task.assigneeAgentId directly for groupBy/sort
- Extract shared isInboxAgentId helper to eliminate 4x inline duplication
- Group headers resolve agent title at render time via AssigneeLabel component
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: enable vertical scrolling on cross-agent tasks page
Add overflowY and flex to WideScreenContainer wrapper so the task list
can scroll when content exceeds viewport height.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add re-assign task agent with popover selector
- Add AssigneeAgentSelector component with Popover agent list
- Extract useAgentDisplayMeta hook for consistent agent name/avatar resolution
- Fix optimistic update mapping assigneeAgentId → agentId in task store
- Disable reassignment for running tasks with tooltip hint
- Integrate selector into task list and task detail property panel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: reuse BriefCard in task detail activities & fix raw-id navigation
Render brief-type activities as full BriefCard (same as homepage) instead of
plain tree rows. Decouple BriefCardActions from useBriefStore for actions
lookup so it can be reused across pages. Fix infinite loading when navigating
to task detail via raw DB id (task_xxx) by storing detail under both the
identifier and the raw id key in taskDetailMap.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add TopicCard component for task detail activities
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: allow re-running completed tasks with dedicated button
Completed tasks now show a "Re-run" button (with rotate icon) instead of
hiding the action. The backend already supported this — only the frontend
selector gate needed updating.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: add create task modal with markdown editor
Add a "+" button on the tasks list page that opens a Linear-style modal
for manually creating tasks. The modal features a title input, a markdown
editor (EditorCanvas), and a bottom toolbar with priority and assignee
selectors. Existing tag components (TaskStatusTag, TaskPriorityTag,
AssigneeAgentSelector) are extended with an `onChange` controlled mode
so they can be used in creation context where no task exists yet.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 🐛 fix: suppress spurious updateTask on Task Detail page load
EditorDataMode was missing the contentChangeLockRef pattern that
DocumentIdMode already uses, causing Lexical's registerUpdateListener
to treat programmatic content hydration as a user edit and fire
onContentChange → updateTask on every page visit.
- Add contentChangeLockRef + lockIdRef staleness guard
- Extract loadContentWithLock to deduplicate lock/load/unlock logic
- Pass contentChangeLockRef to InternalEditor
- Remove unreachable dead code in loadEditorContent
Closes LOBE-7362
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ✨ feat: task detail comment CRUD and various UX improvements
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: move canceled status group to the end of task list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: polish task detail layout, title, and run button
- Title switched to auto-sizing TextArea so long names wrap (like Linear)
- Reduce title font-size from 32px to 24px and tighten paddings
- Make "运行任务" button small-sized to match the denser header
- Add 120px bottom padding for end-of-content scroll breathing room
- Default EditorCanvas paddingBottom trimmed from 64 to 32
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: refine task assignee, priority, and comment input
- Assignee block uses filled variant in dark mode for better contrast
- Urgent priority (level 1) renders in orange for quick scanning
- Comment input keeps SendButton slot reserved to prevent layout shift
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: task detail — inline subtasks, automation mode, chronological activity
- Inline subtask creation under a task via CreateTaskInlineEntry
(parentTaskId/autoFocus/onCollapse/placeholder), refreshes parent on create
- Track agent-created tasks via createdByAgentId through service, router,
types, and the builtin task executor
- Replace scheduler Segmented-only UI with an Enable switch + heartbeat/
schedule mode; persist via automationMode on the task
- Sort detail activities oldest → newest for a natural timeline reading
- Reducer patches nested subtask entries on updateTaskDetail so in-place
edits reflect in the parent's subtask tree
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: render activate-tool chips as rounded pills
Switch inspector tool chips from monospace code tags to filled rounded
pills with ellipsis overflow, making multi-tool rows scan better in tight
headers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: keep finished tool call out of loading state while siblings run
The message-level isAssistantMessageBusy flag stays true while sibling
tool calls are still running. Without guarding on this tool's own
result, a finished tool would flip back to "loading". Now a tool that
has a real result or error is never shown as calling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: use small Segmented in schedule config popover
Keeps the automation mode switcher visually aligned with the denser
popover controls.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: agent profile hover card on task activity author
- Extract shared AgentProfileCard + unified AgentProfilePopup (click / hover)
with lazy agent fetch; move out of group sidebar path.
- Wire activity author avatar + name to a hover card; brighten title on hover;
keep a small "agent" tag on the author row.
- Show inline skeletons (description + footer stats) while loading.
- Enrich subtask payload with assignee agent info for cleaner UI.
* ✨ feat: open task topic chat in side drawer
Click a topic row in the task detail activities to open a right-side drawer
showing the topic's full chat history. Messages stream in live via the existing
agent gateway pipeline (gateway events land in chatStore.dbMessagesMap keyed by
the topic context), so a running topic refreshes its drawer in real time without
a dedicated subscription.
Reuses the Conversation feature (ConversationProvider + ChatList) with an
isolated context (agentId + topicId + isolatedTopic), so the drawer never
touches the global active topic and multiple panels coexist cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style: outline activate-tool chip with subtle border
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat: show topic handoff summary on activity card
Pull `handoff.summary` through the task service into TaskDetailActivity and
render it under the title in TopicCard so completed topics surface what was
accomplished without opening the drawer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🎸 chore: gate agent task feature behind agent_task flag
Hide every client-side entry point to the Agent Task feature when the
`agent_task` flag (default `isDev`, off in prod) is disabled:
- Sidebar: task tab in the agent sidebar nav
- Routes: `/agent/:aid/tasks/*` and `/tasks/*` layouts redirect to `/` when
the flag is off (mobile router reuses the same layout)
- Home Recents: filter out `type='task'` items in both the list and the
"all recents" drawer
- Daily Brief: skip fetch + hide the entire panel (all briefs link to tasks)
Backend TRPC / lifecycle stays on — the feature is already live for CLI
usage. Flag name mirrors `agent_onboarding` for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix: prioritize includeTriggers in topic queries
* 🐛 fix: normalize task detail activity payloads
* ✨ feat: add Kanban board view for task list with drag-and-drop
LOBE-7493
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 💄 style: shorten schedule tag labels & fix time width in task cards
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* update i18n
* 💄 style: hide task tool from user selectors
* 💄 style: hide task skill from user selectors
---------
Co-authored-by: canisminor1990 <i@canisminor.cc>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
♻️ refactor: replace antd Modal with imperative base-ui createModal
Replace the declarative antd Modal in AttachKnowledgeModal with imperative
createModal from @lobehub/ui/base-ui. The antd Modal's event handling
conflicted with the three-dot DropdownMenu in the file list, causing the
menu to be unclickable in Group Chat context.
Closes#12389
🐛 fix(onboarding): show mode switch and skip footer based solely on AGENT_ONBOARDING_ENABLED
Remove route-based conditional so the footer visibility is controlled
entirely by the AGENT_ONBOARDING_ENABLED flag.
* 🐛 fix(agent-runtime): unwrap underlying PG error in formatErrorEventData
Drizzle wraps driver errors as "Failed query: insert into ..." and buries
the real PostgreSQL diagnostic fields (code, severity, detail, constraint,
column, table) in `.cause`. `formatErrorEventData` in RuntimeExecutors only
read the outer `.message`, so the agent-gateway dashboard saw nothing but
the SQL text — no way to bucket errors by SQLSTATE or tell apart a UTF-8
validation failure from a unique-constraint hit from a row-too-big.
Add a `pgError` util that walks `.cause` up to 5 layers, duck-types real
PG errors via `code` + a known `severity`, and exposes
`{ formatPgError, pgErrorType, unwrapPgError }`. `formatErrorEventData`
now invokes the unwrap as a last-step enrichment — only when no typed
errorType was identified — so typed errors like `ConversationParentMissing`
keep their clean business messages.
After this, the dashboard gets:
error: PG 22021 · ERROR · invalid byte sequence ... · table=message_plugins · column=state
errorType: pg_22021
instead of:
error: Failed query: insert into "message_plugins" ...
errorType: Error
Related: LOBE-7158, LOBE-7334
* 🐛 fix(agent-runtime): unwrap PG diagnostics for raw driver errors regardless of error.name
Review feedback on the prior commit: the enrichment branch only ran when
errorType was missing or exactly 'Error', so raw top-level driver errors —
`PostgresError` (postgres-js), `DatabaseError` (node-postgres), any
provider-specific subclass — kept their driver class name as errorType
and never reached the pg_<sqlstate> bucket. This defeated the new
classification for the exact case it was meant to catch: a PG error
surfacing directly from the driver without a Drizzle wrapper.
Fix: track whether `errorType` came from a business-typed field on the
error payload (step 1 — e.g. `ConversationParentMissing`) vs. from
`error.name` (step 3 — a driver class name). Only skip PG unwrap for
business-typed errors. Driver-named errors now fall through to unwrap
and emit `pg_<sqlstate>` when PG info is identifiable.
Also extract `formatErrorEventData` out of RuntimeExecutors.ts into its
own file so it can be unit-tested directly. The surrounding
RuntimeExecutors module pulls in workspace packages (`@lobechat/markdown-patch`,
`@lobechat/agent-gateway-client`, etc.) that don't resolve in the test
environment, blocking any test that imports from it.
Test coverage added (10 cases): top-level PostgresError class, plain
DatabaseError-shaped object, Drizzle .cause unwrap, ConversationParentMissing
preservation, custom errorType preservation, Node ENOTFOUND rejection,
null/non-object fallbacks, plain-string inputs, payload-with-only-message.
* 🐛 fix(conversation): pin user message to viewport top after spacer settles
Observing the spacer DOM via ResizeObserver lets us re-fire scrollToIndex
once virtua finishes measuring it and scrollSize actually expands, so the
sent user message lands flush against the viewport top instead of
trailing below by the spacer growth delta. Also drop the height
transition on mount/grow so scrollSize jumps in a single frame; only the
collapse-to-zero (unmount) still animates.
* 🐛 fix(vite): detach spawn for debug proxy so dev server isn't blocked
Swap execFile for a detached spawn with stdio ignored and unref, so the
opened browser process no longer keeps the Vite dev process alive. Falls
back to treating a 200ms "no error" window as success, and routes
diagnostics through the Vite logger instead of swallowing them.
* ✨ feat(conversation): fold long user messages so AI response stays visible
When a very long user message is pinned to the viewport top after send,
it can eat the entire viewport and leave no room for the AI reply.
Wrap the user text body in a CollapsibleContent that clamps content
past min(280px, 35vh) with a gradient mask and a Show more / Show less
toggle. Attachments, images and page selections stay fully visible.
* ♻️ refactor(conversation): scope spacer observer to this list via ref callback
ConversationProvider supports multiple conversation lists mounted at the
same time, so a document-wide querySelector would attach to whichever
spacer the DOM hands out first — possibly another panel's — and drive
spacerLayoutVersion from unrelated layout ticks. Switch to a ref
callback returned from useConversationSpacer and bound to the spacer div
rendered by the same VirtualizedList, guaranteeing the observer tracks
this instance's own spacer.
* 🐛 fix(conversation): cancel queued pin retries when user scrolls up
Clearing pendingScrollIndexRef alone wasn't enough — the retry wave fires
at 0/32/96ms, so if the user scrolled up between send and 96ms the
already-queued timers would still call scrollToIndex and yank the
viewport back down, contradicting the "don't fight user intent" rule.
Also invoke clearPendingPins in the same effect so the in-flight retry
window is cancelled along with the pending index.
Fix LOBE-7356 — PageEditor handleCopyLink used window.location.origin which resolves to app://renderer on desktop. Now uses electronSyncSelectors.remoteServerUrl on desktop, consistent with existing pattern in global.ts and Topic dropdown.
* feat: add screen capture functionality with overlay support
- Implemented ScreenCaptureManager to handle screen capture sessions.
- Added ScreenCaptureCtr for IPC methods related to screen capture.
- Created overlay.html and ScreenCaptureOverlay component for user interaction.
- Integrated window enumeration and capture logic using node-screenshots and get-windows.
- Updated menu options to include screen capture actions.
- Enhanced RendererUrlManager to support overlay routing.
- Introduced drag selection for capturing specific screen areas.
- Added necessary types and events for screen capture in electron-client-ipc.
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(desktop): refine screen capture overlay flow
* ✨ feat(desktop): refine screen capture overlay flow
* ⚡ feat(desktop): optimize screen capture overlay flow
* Delete apps/desktop/mockup/screen-capture-overlay.html
* ✨ feat(desktop): open mini toolbar via double Option
* 🐛 fix(desktop): separate quick composer hotkey
* 💄 fix(desktop): remove stale quick composer accelerator
* 🐛 fix(desktop): stabilize double option monitor
* 🐛 fix(desktop): read hardware option key state
* 🐛 fix(desktop): standardize path imports and improve error handling
- Replaced `join` imports with `path` imports for consistency across files.
- Enhanced error handling in various modules to include error causes for better debugging.
- Updated test files to reflect changes in variable naming and mock implementations.
Signed-off-by: Innei <tukon479@gmail.com>
* 🔥 chore(hotkey): drop orphan renderer quickComposer i18n entries
The `quickComposer` hotkey is registered only on the Electron side
(DESKTOP_GLOBAL_SHORTCUT_DEFAULTS + BrowserWindowsCtr.openQuickComposer);
the renderer never referenced these i18n keys, so the entries were dead.
`desktop.quickComposer` covers the app-level trigger.
* ⚡️ perf(screen-capture): parallelize overlay upload with route navigation
Overlay submit used to await screenshot upload before router.push,
blocking the main window for several seconds when the user was on an
unrelated page (e.g. /settings). Now we navigate immediately and run
upload in a background IIFE; MessageFromUrl waits on a new
`uploadStatus` field before calling sendMessage, so the chat page
mount and the upload proceed in parallel.
- Add `uploadStatus: 'uploading' | 'ready' | 'failed'` to
PendingOverlayDispatch; canConsumePendingOverlayDispatch blocks
while `'uploading'`.
- Store gains `markDispatchUploadComplete`; on failure it clears
screenshotFileNames so the prompt still delivers.
- Dispatcher drops stale prev search params on push to prevent
MessageFromUrl's message-param effect from double-firing.
* ⚡️ perf(screen-capture): pre-upload captures in overlay preview + per-thumbnail status
Move uploads from post-submit to preview time, bypassing dataUrl round-trips:
- Main process assigns captureId at preview time and ships the PNG bytes
as ArrayBuffer to the main renderer via `overlayUploadRequest`.
- Main renderer uploads through a dedicated pool (uploadWithProgress,
no chatUploadFileList pollution); reports status back to the overlay
through `overlayCaptureUploadStatus`.
- Overlay thumbnails render a spinner / error badge based on status;
the send button stays grey until every capture resolves to `ready`.
- Submit now carries only captureIds; MessageFromUrl awaits the pool
promises before sendMessage, removing the second upload pass.
- Carry overlay-selected modelId/provider into the agent config so the
first message actually uses the user-chosen model (fixes the bug where
switching the model on the overlay had no effect).
* update
* ✨ feat(popup): add Quick Chat tray entry backed by Inbox agent
Tray menu now exposes a "Quick Chat" action that opens (or focuses)
a single-instance popup window at `/popup/agent/inbox`. Each fresh
open starts with no active topic; the first message creates one
through the normal agent flow.
- New `PopupAgentQuickPage` resolves the inbox slug via
`builtinAgentSelectors.inboxAgentId` so `activeAgentId` points at
the real entity in `agentMap` (fixes the stuck-loading / skeleton
state from using the literal `'inbox'` slug).
- `BrowserManager.openQuickChatPopup` wraps
`createMultiInstanceWindow` with a fixed `topicPopup_quick_inbox`
uniqueId so repeat clicks focus rather than spawn.
- Wire the action into macOS / Windows / Linux tray menus and add
the `tray.quickChat` i18n key.
* Add quick chat shortcut and desktop hotkey support
* ✨ feat(screen-capture): enhance window enumeration with scale factor support
- Updated `enumerateWindows` to accept an optional `displayScaleFactor` parameter for improved window geometry normalization on high-DPI displays.
- Refactored `normalizeWindowBounds` to handle scaling based on the provided scale factor, ensuring accurate window dimensions across different platforms.
- Adjusted tests in `WindowSourceService.test.ts` to validate the new scaling behavior for both Windows and macOS environments.
- Minor adjustments in `ScreenCaptureManager` to accommodate the updated window enumeration logic.
---------
Signed-off-by: Innei <tukon479@gmail.com>
* ✨ feat(git-status): one-click pull/push from branch chip
Split the ahead/behind indicator out of the BranchSwitcher trigger so
↓N / ↑N become standalone action chips: clicking ↓ runs `git pull
--ff-only`, clicking ↑ runs `git push`. Each chip swaps to a spinning
LoaderIcon while the operation is in flight and refreshes branch /
working-tree / ahead-behind state on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(electron-ipc): extract Git IPC types into dedicated git.ts
Move GitBranchInfo / GitLinkedPullRequest(Result) / GitBranchListItem /
GitWorkingTree(Status|Files) / GitCheckoutResult / GitPullResult /
GitPushResult / GitAheadBehind out of system.ts into a sibling git.ts
so the system surface stays focused on system/window/theme types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(git-status): push chip failing under push.default=simple
Use `git push -u origin HEAD` instead of bare `git push` so the one-click
push action works on branches whose upstream name differs from the local
name (the common `git checkout -b feat/x origin/canary` workflow). Bare
`git push` refuses in that case under the default simple policy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(git-status): push tooltip lying about target ref
Push chip was reusing the pull upstream in its tooltip, which is wrong
when local branch name differs from upstream (e.g. feat/x tracking
origin/canary) — the push actually goes to origin/<local-name> per
our `git push -u origin HEAD`, not to the upstream.
Compute a separate `pushTarget` (`origin/<current-branch>`) and
`pushTargetExists` flag in getGitAheadBehind, and switch the push
tooltip to use that. When the target doesn't exist yet (one-click
creates a new remote branch) show a "(new branch)" variant so the
user knows what the click will do.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(git-status): ring spinner + clearer create-branch tooltip
- Swap the lucide LoaderIcon (with hand-rolled CSS spin) for the shared
RingLoadingIcon used in Topic items, so the in-flight pull/push chip
matches the rest of the app's spinner style.
- Reword the new-branch push tooltip from "push N commits to X (new
branch)" to "Click to create branch X" — the count is misleading when
the remote doesn't exist yet (the action is creating, not catching
up), and the shorter copy reads cleaner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Simplify comments in pushGitBranch method
Removed detailed comments about git push behavior.
* 🐛 fix(git-status): serialize pull/push on diverged branches
Block the opposite sync action while a git sync is running — both chips
go disabled whenever pulling or pushing is true. Previously on a
diverged branch (ahead > 0 and behind > 0) a user could start pull and
still click push before the first finished, launching concurrent git
operations against the same worktree and producing lock / non-FF errors
plus confusing double toasts for a single intent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(git-status): piggyback best-effort fetch on ahead/behind lookup
Problem: ahead/behind was computed purely against locally-cached refs, so
commits pushed to origin elsewhere (GitHub web UI, another machine) never
surfaced as ↓N until the user ran `git fetch` in a terminal.
Fix: run `git fetch --no-tags --quiet origin` at the start of
getGitAheadBehind with a 10s timeout; ignore failures and fall through
to compute against whatever refs we have. SWR's revalidateOnFocus
already re-invokes this IPC, so the fetch happens on window re-focus for
free — no new UI and no interval polling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ⬆️ chore(vite): migrate SPA build pipeline to Vite 8
* 🔧 chore(vite): patch inspector tooling and stabilize rolldown output
* 🐛 fix(vite): apply Vite 8 follow-up fixes and dev proxy polish
* 🩹 chore(vite): drop oversized code-inspector core patch
* 🐛 fix(desktop): support vite 8 electron build
* 🐛 fix(desktop): declare mac permissions types ambiently
* 🐛 fix(desktop): externalize mac permissions in main build
* ♻️ refactor(desktop): increase recent working directories from 5 to 20 with scroll container
* 🎨 style(branch-switcher): compact dropdown, immersive search, aligned icons
- Stop keydown propagation on inputs to bypass Base UI typeahead navigation
- Switch search input to borderless variant with bottom divider
- Align search prefix icon with list item icons at 12px
- Tighten item padding, line-height and meta spacing
- Match create-branch item radius to popup via calc(borderRadius - 4px)
* 🐛 fix(agent-runtime): sanitize invalid tool_call arguments to prevent history poisoning
When a model emits malformed JSON as tool_calls[].arguments (e.g. Qwen
producing `{, "description": ...}`), the raw string was persisted to
`messages.tools[].arguments` and replayed verbatim on every subsequent
turn. Strict providers (NVIDIA NIM) validate the full history and 400
the whole request, terminating the op and wasting all accumulated tokens.
Add a shared `sanitizeToolCallArguments` helper in @lobechat/utils and
wire it in at three layers so both new captures and already-poisoned DB
history are safe:
- Server entry (RuntimeExecutors onToolsCalling) — mirrors the frontend's
`internal_transformToolCalls` pattern; prevents new poisoning.
- Outbound context build (ToolCallProcessor) — last line of defense for
historical messages that were persisted before this fix.
- Agent-runtime core (call_tools_batch normalization) — covers the
old-format ToolsCalling[] path.
Behavior: valid JSON passes through unchanged (prompt cache stable);
partial-json recovers truncated streams; unrecoverable payloads fall
back to "{}" so the tool_call structure survives and the model can
replan on the next turn.
Fixes LOBE-7761
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): preserve INVALID_JSON_ARGUMENTS feedback when sanitizing
Sanitizing `tool_calls[].arguments` at capture (onToolsCalling) was too
early — the normalized "{}" reached `BuiltinToolsExecutor.execute` and
bypassed the `INVALID_JSON_ARGUMENTS` branch, so the model got a generic
"missing required field" error instead of the precise "your JSON syntax
was broken, fix it" feedback. That regressed the self-reflection signal.
Move sanitization to the persist boundaries only:
- DB write via `messageModel.update({tools: ...})`
- `state.messages` push for the assistant message's `tool_calls`
The execution path keeps the raw `arguments` string so the executor can
still emit its `INVALID_JSON_ARGUMENTS` tool-result with the original
malformed payload echoed back — exactly the frontend-symmetric self-
reflection flow.
Add a regression test pinning the LOBE-7761 Qwen shape so future changes
can't silently drop the feedback again.
Fixes LOBE-7761
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(agent-runtime): drop sanitize from runtime normalization to avoid undeclared @lobechat/utils dep
Review flagged that `runtime.ts` imported `sanitizeToolCallArguments` from
`@lobechat/utils` while `agent-runtime/package.json` doesn't list utils as
a runtime dependency — in strict/hermetic installs this resolves to
MODULE_NOT_FOUND before the runtime can start.
Rather than add a new dep just for a belt-and-suspenders path, drop the
sanitize on the old-format `call_tools_batch` normalization. The actual
LOBE-7761 bug is server-side history poisoning; that's fully covered by:
- RuntimeExecutors persist-boundary sanitize (DB write + state.messages)
- context-engine ToolCallProcessor outbound sanitize (handles any DB
history that was persisted before this fix)
Old-format agents in agent-runtime don't persist or replay to providers
on their own — sanitization is the consuming application's
responsibility and can live closer to its persistence layer.
Drops the dep-cycle-free path.
Related LOBE-7761
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(model-runtime): log tool_call parse errors in Anthropic adapter
The assistant→Anthropic conversion was swallowing `JSON.parse` errors
silently and falling back to empty `input: {}`. Combined with the
LOBE-7761 fix, bad arguments should always be sanitized upstream in
context-engine, so hitting this catch means something bypassed the
defense and we're about to send a tool_use with empty input to Claude.
That's worth knowing about.
Match the `console.error('parse tool call arguments error:', ...)`
pattern already used in openaiCompatibleFactory so logs are greppable.
Related LOBE-7761
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(claude-code): prefix Agent inspector with "Agent:" and drop chip 60% cap
Row visibly reads as a subagent dispatch, not a generic tool; chip no longer
ellipsizes when there is room to the right.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agents): unstick Read tool spinner on image results (LOBE-7338)
CC's `Read` on images returns a `tool_result` whose `content` is an `image`
block (base64). The generic array mapper had no branch for it so resultContent
collapsed to '' and the UI's StatusIndicator stuck on the spinner. Emit a
minimal `[Image: <media_type>]` placeholder so the tool ends in completed
state. Richer image echo (thumbnails) is tracked separately and needs
structured ToolResultData.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(claude-code): place "Agent:" prefix before the icon
Order is now `Agent: <icon> <subagent_type>` instead of `<icon> Agent: <subagent_type>` so the contextual label leads, the bot icon sits between as a visual separator, and the subagent name closes the row.
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(claude-code): render ScheduleWakeup / TaskOutput / TaskStop in inspector
CC emits three tool calls we were previously rendering as raw JSON:
`ScheduleWakeup` (self-paced /loop), `TaskOutput` (read from background
task), `TaskStop` (terminate background task). Add dedicated inspectors
and register them alongside the existing CC tool set.
`TaskStop` accepts both `task_id` and the legacy `shell_id` field name
since older CC builds still emit the latter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(chat-topic): stop completed topics from leaking past the sidebar filter
Two sibling components in each chat-topic sidebar were both calling
`useFetchTopics`, but with different args: the outer `Topic` passed the
preference-driven `excludeStatuses: ['completed']` filter while the
inner `List` / `TopicListContent` called it bare. Since `excludeStatuses`
is part of the SWR key, both calls fired independent requests whose
`onData` handlers wrote back to the same `topicDataMap[containerKey]`
slot — whichever response landed last won, and when the un-filtered
sibling won, completed topics reappeared in the sidebar despite the
"Include completed" preference being off.
Introduce `useFetchChatTopics` as the single call site for chat-topic
fetching. It reads `topicIncludeCompleted` from preferences and pins
`excludeTriggers` to the always-excluded cron/eval set, so every
sibling mounts with identical args, collapses onto one SWR key, and
SWR dedupes them to a single request. Group sidebars now also exclude
cron/eval triggers for parity with the agent sidebar (groups don't
produce either trigger today, so this is a no-op in practice but
prevents divergence if the rules change).
Popup and mobile-modal call sites keep using the raw `useFetchTopics`
because they deliberately need the unfiltered set — the popup has to
resolve a specific (possibly completed) topic's title from the map.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✨ feat(chat-input): heterogeneous-agent placeholder for Claude Code sessions
When the active agent is backed by a heterogeneous provider (currently
only `claude-code`), swap the generic "Ask, create, or start a task"
placeholder for a task-specific variant that names the provider
(e.g. "Ask Claude Code to do a task"). @-mention assignment hint is
suppressed in that mode since heterogeneous agents don't yet route to
sibling agents.
* 🌐 chore(i18n): translate sendPlaceholderHeterogeneous (en-US, zh-CN)
Local preview translations for the new heterogeneous-agent chat input
placeholder; en-US mirrors the default, zh-CN carries the Chinese
copy. CI regenerates locale JSON on release so this commit only seeds
dev preview.
* ♻️ refactor(workflow-summary): unify suffix to show total tool kinds and calls
Both branches of getWorkflowSummaryText now share the same suffix structure:
list · 共 N 种工具 · 共 X 次调用 · N 次失败. summaryMoreTools changes from
remaining count ("+N more" / "等 N 种工具") to total count, and the inline
(failed) per-tool marker is dropped in favor of the global error suffix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ♻️ refactor(workflow-summary): hide redundant kinds/calls suffixes
Show "N tool kinds" only when the displayed list is truncated, and "X calls
total" only when at least one tool was called more than once. Otherwise the
aggregates duplicate information already visible in the per-tool list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🎨 style(chat-input): drop hotkey suffix from heterogeneous placeholder
Heterogeneous-agent placeholder (e.g. "让 Claude Code 帮你完成任务…") no
longer trails the "press ⌘↵ to insert a line break" hotkey hint, which read
awkwardly attached to a short single-clause prompt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🎨 style(claude-code): align ScheduleWakeup/Task* inspectors with ToolSearch
Drop leading lucide icons, add `:` suffix so the label row reads like
ToolSearch, and promote ScheduleWakeup's `reason` into the chip with
`delaySeconds` trailing as secondary context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(heterogeneous-agents): retain subagent tool-call lookup across turn boundaries
`findRunByInnerToolCallId` consulted `run.state.persistedIds`, but that
set is wiped every time `ensureSubagentRun` advances `subagentMessageId`.
A `tool_result` delayed past the owning turn therefore failed the lookup
and skipped the thread-bucket `run.stream.update`, leaving the in-thread
tool bubble stuck on its loading spinner until the user re-opened the
Thread (main-topic `fetchAndReplaceMessages` doesn't rehydrate thread
buckets). Add a run-lifetime `lifetimeToolCallIds` set that only grows
and route the lookup through it; leave `state.persistedIds` as-is so
`persistToolBatch`'s turn-scoped dedupe is untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `ToolsCalling` -> `ChatToolPayload` mapping in `runtime.ts` explicitly
enumerated 5 fields and dropped `thoughtSignature`, while the type itself
never declared the field. As a result, any Gemini 3.x tool call beyond
the first one in a conversation would 400 with a misleading
"function call turn must come after user/function response turn" error —
Google's validator maps a missing signature to that generic ordering message.
Fix LOBE-7759.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 🐛 fix(context-engine): downgrade image_url parts when target model lacks vision
Historical messages persisted as multimodal parts (content is an array
with `image_url` entries, or assistant messages with `metadata.isMultimodal`)
bypassed the legacy `imageList` vision check and got forwarded verbatim to
the provider. DeepSeek rejects the `image_url` variant outright, so any
topic containing an image broke the moment the user switched to a
non-vision model.
Replace image parts with a textual placeholder so the conversation still
carries the signal that an image was sent, without including content
non-vision providers reject. Applies uniformly across user array content,
assistant multimodal content, and legacy `imageList` paths.
Fixes LOBE-7214.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ✅ test: update vision-disabled expectations after downgrade placeholder
Two tests in the app suite asserted the silent-drop behavior the
MessageContentProcessor used to exhibit for `imageList` + vision-off:
- src/services/chat/chat.test.ts
- src/services/chat/mecha/contextEngineering.test.ts
After this PR the processor appends the downgrade placeholder instead of
silently dropping the image, so the expected content grows by one line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 💄 style(context-engine): place vision downgrade placeholder before SYSTEM CONTEXT
The placeholder stands in for an image the user actually sent, so it
should sit adjacent to the user text rather than trailing after the
SYSTEM CONTEXT metadata block. Reorder so the payload reads:
<user text>
[image omitted: not supported by this model]
<!-- SYSTEM CONTEXT ... -->
Keeps the conversational flow intact and matches the semantic position
the image occupied in the original message.
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): 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>
description: "Agent runtime lifecycle hooks for observing and intercepting agent execution. Use when adding hooks to agent operations, mocking tool calls, logging step events, handling human intervention, sub-agent calls, context compression, or building eval/tracing integrations. Triggers on 'hooks', 'beforeToolCall', 'afterToolCall', 'beforeStep', 'afterStep', 'onComplete', 'onError', 'tool mock', 'agent lifecycle', 'human intervention', 'callAgent', 'compact'."
user-invocable: false
---
# Agent Runtime Hooks
Lifecycle hooks for observing and intercepting agent execution. Hooks are registered per-operation via `execAgent({ hooks })` and dispatched by `HookDispatcher`.
## Hook Types
16 hook types across 5 categories:
```
execAgent({ hooks })
│
├─ beforeStep ──────────── Before each step executes
│ │
│ ├─ [call_llm] LLM inference
│ │
│ ├─ [call_tool]
│ │ ├─ beforeToolCall ── Before tool executes (supports mocking)
│ │ ├─ (tool execution)
│ │ ├─ afterToolCall ─── After tool completes (observation only)
│ │ └─ onToolCallError ─ Tool threw an exception
│ │
│ ├─ [request_human_approve]
│ │ ├─ beforeHumanIntervention ── Before agent pauses
│ │ ├─ afterHumanIntervention ─── After approve/reject + resume
│ │ └─ onStopByHumanIntervention ── User rejected, agent halted
│ │
│ ├─ [compress_context]
│ │ ├─ beforeCompact ──── Before compression starts
│ │ ├─ afterCompact ───── After compression completes
│ │ └─ onCompactError ─── Compression failed
│ │
│ ├─ [callAgent] (via execSubAgentTask)
│ │ ├─ beforeCallAgent ── Before sub-agent starts
│ │ ├─ afterCallAgent ─── After sub-agent completes
│ │ └─ onCallAgentError ── Sub-agent failed
│ │
│ └─ afterStep ──────────── After step completes
│
├─ (next step...)
│
├─ onComplete ───────────── Operation reaches terminal state
└─ onError ──────────────── Error during execution
**`onCallAgentError`** — Sub-agent failed. Dispatched on **parent** operation.
```ts
// event: CallAgentErrorHookEvent
{
(operationId,agentId,error);
}
```
Note: CallAgent hooks require `parentOperationId` in `ExecSubAgentTaskParams`.
## Design Notes
- **Fire-and-forget**: All handlers return `Promise<void>`. Errors are non-fatal.
- **Exception**: `beforeToolCall` supports mock via `event.mock()` — uses `dispatchBeforeToolCall()` which returns the mock result.
- **Sequential**: Same-type hooks run in registration order.
- **Local only**: `beforeToolCall` mock only works in local mode (in-memory hooks). Webhook mode does not support mocking.
- **Scoped per operation**: Auto-cleaned via `hookDispatcher.unregister()` on completion.
- **Sandbox/MCP**: No separate hooks — they go through `executeTool`, so `beforeToolCall`/`afterToolCall` cover them. Use `event.identifier` to filter.
## Real-World Example: agent-evals
See `devtools/agent-evals/helpers/runner.ts` — `createEvalHooks()` uses `afterStep`, `onComplete`, `afterToolCall`, and `beforeToolCall` (for mock).
description: Build or extend LobeHub Agent Signal pipelines for background or quiet agent work driven by event sources, semantic signals, and action handlers. Use when adding a new Agent Signal source, signal or action type, policy, middleware handler, workflow handoff, dedupe or scope behavior, or observability around `src/server/services/agentSignal/**`, `packages/agent-signal`, or `packages/observability-otel/src/modules/agent-signal`.
---
# Agent Signal
Use this skill to implement event-driven background work for agents without coupling the work to the foreground chat request.
1. Read `references/architecture.md` to map the package boundary, runtime queue, scope model, and async workflow handoff.
2. Read `references/handlers.md` before writing any new policy, source handler, signal handler, or action handler.
3. Read `references/observability.md` when you need tracing, metrics, debugging, or workflow snapshot visibility.
## Use The Right Entry Point
- Use `emitAgentSignalSourceEvent(...)` when a server-owned producer should execute the pipeline immediately.
- Use `executeAgentSignalSourceEvent(...)` when a worker or controlled backend path already owns execution timing and may inject a runtime guard backend.
- Use `enqueueAgentSignalSourceEvent(...)` when the caller should return quickly and let Upstash Workflow process the event out-of-band.
- Use `emitAgentSignalSourceEventWithStore(...)` for isolated tests or evals that should avoid ambient Redis state.
Read:
-`src/server/services/agentSignal/index.ts`
-`src/server/workflows/agentSignal/index.ts`
-`src/server/workflows/agentSignal/run.ts`
## Core Model
-`source`: A normalized fact that happened. Sources come from producers such as runtime lifecycle events, user messages, or bot ingress.
-`signal`: A semantic interpretation derived from one source or from another signal. Signals express meaning, routing, or policy state.
-`action`: A concrete side effect planned from one signal. Actions do the work.
-`policy`: An installable middleware bundle that registers source, signal, and action handlers.
-`procedure`: Not a distinct runtime node. Treat "procedure" as the end-to-end flow for one use case: ingress source, matching handlers, planned actions, execution result, and observability.
Keep the boundaries strict:
- Add a new `source` when the outside world produced a new event.
- Add a new `signal` when the system needs a reusable semantic interpretation.
- Add a new `action` when the runtime needs a concrete side effect.
- Add or update a `policy` when you are wiring those pieces together.
## Implementation Workflow
1. Decide whether the use case is synchronous or quiet background work.
2. Define or reuse a source type in `src/server/services/agentSignal/sourceTypes.ts`.
3. Define or reuse signal and action types in `src/server/services/agentSignal/policies/types.ts`.
4. Implement handlers with `defineSourceHandler`, `defineSignalHandler`, or `defineActionHandler`.
5. Bundle handlers with `defineAgentSignalHandlers(...)`.
6. Register the policy in `src/server/services/agentSignal/policies/index.ts` and pass it into the runtime factory if needed.
7. Add or update ingress code that emits or enqueues the source event.
8. Add observability and tests before considering the flow complete.
- Reuse existing source, signal, and action types before adding new ones.
- Keep source handlers focused on interpretation and fan-out, not heavy side effects.
- Keep action handlers responsible for side effects, idempotency, and executor-style result reporting.
- Use stable ids and idempotency keys when the same source can arrive more than once.
- Preserve scope discipline. The runtime uses `scopeKey` to serialize related background work.
- Prefer the dedicated shared package types and builders from `@lobechat/agent-signal` for normalized nodes and result contracts.
- Add focused tests near the touched runtime, policy, or store module. Existing tests under `src/server/services/agentSignal/**/__tests__` are the reference pattern.
## References
- Architecture and boundaries: `references/architecture.md`
- Writing handlers and policies: `references/handlers.md`
- Observability, metrics, and debugging: `references/observability.md`
- a trace envelope with source, signals, actions, results, edges, and handler runs
- a compact telemetry record with dominant path, status breakdown, and chain metadata
This projection is built from:
- source node
- emitted signals
- planned actions
- executor results
## How To Inspect A Chain
Use this order:
1. Inspect the source type and payload.
2. Inspect emitted signals.
3. Inspect planned actions.
4. Inspect executor results.
5. Inspect projected edges and dominant path.
The helper `toAgentSignalTraceEvents(...)` flattens a chain into compact event records suitable for tracing snapshots.
## Workflow Snapshot Bridge
Workflow-triggered runs do not naturally pass through the normal foreground runtime snapshot path, so `runAgentSignalWorkflow` adds a development-only bridge into `.agent-tracing/`.
Read:
-`src/server/workflows/agentSignal/run.ts`
Use that path when:
- the source was enqueued with `enqueueAgentSignalSourceEvent(...)`
- you need local trace visibility for quiet background work
## Common Debug Questions
### The source emits but nothing happens
Check:
- feature gate enabled for the user
- source type matches a registered source handler
- dedupe or scope lock did not short-circuit generation
`supportsMarkdown=false` ⇒ outbound markdown is stripped to plain text via `stripMarkdown` and the AI is told not to use markdown. `supportsMessageEdit=false` ⇒ no progress edits — only the final reply is sent.
**Multi-mode connection** — Slack/Feishu/Lark/QQ shipped as websocket but support `webhook` per-provider via `settings.connectionMode`. Legacy rows without that field stay on `webhook` (see `LEGACY_WEBHOOK_PLATFORMS` in `platforms/utils.ts`) — **never add new platforms to that list**.
**Multi-mode connection** — Slack/Feishu/Lark/QQ ship as websocket but support `webhook` per-provider via `settings.connectionMode`. The runtime always merges schema defaults into stored settings before resolving the mode (`resolveBotProviderConfig` / `resolveConnectionMode` in `platforms/utils.ts`), so the schema's `field.default` is the source of truth — set it correctly when adding a new multi-mode platform.
## Inbound Flow (one webhook → reply)
@@ -76,7 +76,7 @@ The router caches loaded bots in memory. Cache is **invalidated** by `BotMessage
2. Calls `execAgent` with `stepWebhook` and `completionWebhook` pointing at `${INTERNAL_APP_URL ?? APP_URL}/api/agent/webhooks/bot-callback`, plus `webhookDelivery: 'qstash'`.
3. Returns immediately; the bridge `finally` block keeps the active-thread marker held until the `completion` callback fires.
`/api/agent/webhooks/bot-callback/route.ts` verifies the QStash signature and hands off to `BotCallbackService.handleCallback`:
`POST /api/agent/webhooks/bot-callback` (`src/server/agent-hono/handlers/botCallback.ts`) verifies the QStash signature via the `qstashAuth` middleware and hands off to `BotCallbackService.handleCallback`:
-`type: 'step'` → `handleStep` re-renders `renderStepProgress`, edits `progressMessageId` (skipped if `displayToolCalls=false` or platform `supportsMessageEdit=false`).
-`type: 'completion'` → `handleCompletion` writes the final reply (or error/interrupted message), removes the 👀 reaction, clears active-thread tracker, fires async `summarizeTopicTitle`.
@@ -140,12 +140,12 @@ Webhook platforms run fine in serverless functions. Persistent platforms (`webso
- On Vercel + webhook mode → start the client inline (one HTTP call).
- Off-Vercel → `GatewayManager` singleton holds long-lived clients in process.
- Iterates registered platforms and starts every enabled persistent provider with `durationMs = 10min`, then in `after(...)` polls `BotConnectQueue` every 30s for new connect requests, until the window expires.
-`getEffectiveConnectionMode(platform, settings)` is the only place that resolves per-provider mode — respect it everywhere.
**`POST /api/agent/gateway/start/route.ts`** is the non-Vercel `ensureRunning` entry point (`Bearer ${KEY_VAULTS_SECRET}`).
**`POST /api/agent/gateway/start`** (`src/server/agent-hono/handlers/gatewayStart.ts`) is the non-Vercel `ensureRunning` entry point (`Bearer ${KEY_VAULTS_SECRET}`).
**Runtime status** is stored in Redis at `bot:runtime-status:platform:appId` with TTL ≈ `durationMs + 60s`. States: `starting | connected | disconnected | failed | queued`. Updated by each `PlatformClient.start/stop` and by the gateway service.
@@ -166,7 +166,7 @@ Each platform exposes a `PlatformDefinition` registered in `platforms/index.ts`:
}
```
`schema` drives both server validation (`mergeWithDefaults`, `extractDefaults`) **and** the auto-generated UI form. Top-level keys `applicationId` / `credentials` / `settings` map to DB columns. Common settings fields live in `platforms/const.ts` (`displayToolCallsField`, `serverIdField`, `userIdField`).
`schema` drives both server validation (`mergeWithDefaults`, `extractDefaults`) **and** the auto-generated UI form. Top-level keys `applicationId` / `credentials` / `settings` map to DB columns. Common settings fields live in `platforms/const.ts` (`displayToolCallsField`, `makeServerIdField(platform?)`, `makeUserIdField(platform?)`). The `serverId` / `userId` factories take a platform identifier so the field's hint can render platform-specific "how to find this ID" guidance (Discord Developer Mode, Telegram @userinfobot, etc.); pass no argument to fall back to generic copy.
Each platform implements `PlatformClient` (see `platforms/types.ts`):
@@ -226,11 +226,11 @@ Client service: `src/services/agentBotProvider.ts`. Store actions: `src/store/ag
description: Build a new builtin tool package under `packages/builtin-tool-<name>/`. Use when adding a new agent-callable toolset, designing its API surface (manifest / ApiName / Params / State), implementing the Executor + ExecutionRuntime, building the Inspector / Render / Placeholder / Streaming / Intervention / Portal UI, or wiring a tool into the central registries (`packages/builtin-tools/src/{index,identifiers,inspectors,renders,placeholders,streamings,interventions,portals}.ts` and `src/store/tool/slices/builtin/executors/index.ts`). Triggers on "new builtin tool", "add a tool", "tool inspector", "tool render", "tool placeholder", "tool streaming", "tool intervention", "BuiltinToolManifest", "BaseExecutor", "ExecutionRuntime".
---
# Builtin Tool Authoring Guide
A builtin tool is a package the agent runtime can call. It ships **five faces**:
| Where do files live? What does each face do? Wiring? | [architecture.md](architecture.md) |
| How do I name the tool, design APIs, write the manifest, executor, ExecutionRuntime? | [tool-design.md](tool-design.md) |
| How do I build Inspector / Render / Placeholder / Streaming / Intervention / Portal? | [ui.md](ui.md) |
---
## When to Use This Skill
- Creating a new `packages/builtin-tool-<name>/` package
- Adding a new API method to an existing builtin tool
- Building or restyling any of the 6 client surfaces for a tool
- Wiring a tool into the central registries
- Debugging "tool not found / API not found / render not showing / placeholder stuck" errors
---
## Top-Level Design Principles
1.**`lobe-<domain>` identifier is permanent.** It's stored in message history. Renames need `@deprecated` aliases (see `packages/builtin-tools/src/inspectors.ts:88-89`). Get it right the first time.
2.**ApiName is an `as const` object**, not a TS enum. It doubles as the runtime list `BaseExecutor` iterates over.
3.**Three result fields, three audiences:**
-`content: string` → the LLM reads it
-`state: Record<…>` → the UI's `pluginState`; **result-domain only**, never echo all params back
-`error: { type, message, body? }` → both LLM and UI; `type` is a stable code
4.**Split execution from frontend wiring.**
-`src/ExecutionRuntime/` — pure runtime, no React, no Zustand, accepts services via constructor. **The default place for new logic.**
-`src/client/executor/` — `BaseExecutor` subclass that calls `ExecutionRuntime` (or stores/services directly when frontend-only).
5.**UI defaults to "do nothing".** Inspector is required (the header strip). Render/Placeholder/Streaming/Intervention/Portal are added **only when there's something specific to show** — empty registries are fine.
6.**Style with `createStaticStyles + cssVar.*`** (zero-runtime). Fall back to `createStyles + token` only when you genuinely need runtime values. Use `@lobehub/ui` components, not raw antd.
7.**i18n keys live in `src/locales/default/plugin.ts`.** Inspector titles must come from `t('builtins.<identifier>.apiName.<api>')` so something renders while args stream.
└── components/ # shared subcomponents used by the surfaces above
```
**Older packages** (`builtin-tool-task`, `builtin-tool-calculator`, etc.) still have `src/executor/` as a sibling of `src/client/`. That's grandfathered; **don't relocate without a deliberate refactor**. New packages and new APIs added to existing packages should follow the layout above.
| Pure-compute, no UI state | `packages/builtin-tool-calculator/` — `ExecutionRuntime` reuses executor (mathjs/nerdamer work everywhere) |
| CRUD over a domain entity | `packages/builtin-tool-task/` — full Inspector + Render set, batch variants |
| Heavy UI (Inspector/Render/Placeholder/Portal) | `packages/builtin-tool-web-browsing/` — search-style result UI, Portal for detail view |
| Desktop / filesystem with all surfaces (incl. Streaming + Intervention) | `packages/builtin-tool-local-system/` — `ExecutionRuntime` injects an `ILocalSystemService`, executor calls it |
| Server-side pure (no client executor) | `packages/builtin-tool-web-browsing/` — only `ExecutionRuntime` is exported; the chat client doesn't run it |
| Needs human approval before running | `packages/builtin-tool-local-system/src/client/Intervention/` — per-API approval components |
- Server bundles import only `./` and `./executionRuntime` and never touch React.
- Frontend bundles import `./client` and never touch Node-only services.
- The runtime is testable without React or Electron present.
---
## Why ExecutionRuntime is the Default Home for Logic
**Old pattern (grandfathered):** business logic in `src/executor/` directly. Examples: `builtin-tool-task`, older tools. Works, but the executor mixes runtime logic with frontend service plumbing — hard to reuse on the server.
**New pattern (preferred):** business logic in `src/ExecutionRuntime/`, frontend wiring in `src/client/executor/`. Examples: `builtin-tool-local-system`, `builtin-tool-web-browsing`, `builtin-tool-calculator`.
```
ExecutionRuntime
├─ accepts services via constructor (or `static create(opts)`)
├─ returns BuiltinServerRuntimeOutput (content + state + success)
└─ no React, no Zustand, no `@/services/...` direct imports
client/executor
├─ extends BaseExecutor<typeof <Name>ApiName>
├─ holds a `runtime = new <Name>ExecutionRuntime(realService)` instance
├─ each ApiName method:
│ 1. resolve scope / pull defaults from BuiltinToolContext
│ 2. call runtime.<method>(args)
│ 3. funnel through toResult() → BuiltinToolResult
└─ exported singleton: export const <name>Executor = new <Name>Executor()
```
### Service injection
`ExecutionRuntime` should declare a TypeScript interface for the services it needs and accept the implementation via constructor. Server callers wire in real implementations; tests wire in mocks. Example from `local-system`:
### When ExecutionRuntime is the only thing you ship
Some tools are server-only — there's no frontend executor. `builtin-tool-web-browsing` is the canonical example: only `./` and `./executionRuntime` are exported, no `./executor`, and the runtime is constructed by the server-side `ToolExecutionService`. Skip `client/executor/` entirely for those.
### When the executor reuses the runtime as-is
Pure-compute tools (`builtin-tool-calculator`) often have an executor whose ApiName methods call `executor.calculate(args)` and an `ExecutionRuntime` whose methods call `calculatorExecutor.calculate(args)` — same logic, two thin wrappers. That's fine; the duplication buys you the bundle split.
content: string;// the LLM-facing text — never undefined; default to error message
state?: any;// result-domain object the UI reads as pluginState
success: boolean;// mandatory
error?: any;// raw error; the executor will repackage
}
```
### `BuiltinToolResult` (what the executor returns to the runtime)
```ts
{
success: boolean;
content?: string;
state?: any;
error?:{type:string;message: string;body?: any};
metadata?: Record<string,any>;// rare; e.g. { agentCouncil: true }
stop?: boolean;// rare; halt the orchestration step
}
```
### The `toResult` funnel (mandatory)
Every executor method returns through a single `toResult()` to enforce two invariants:
1.**`content` is never undefined.** A missing content collapses downstream into `''`, leaving the Debug pane blank while `pluginState` was already saved. See the `globLocalFiles` regression in `local-system/src/client/executor/index.ts:60-84`.
2.**`state` survives failures.** Renderers can keep showing partial output even when `success: false`.
return(thisasany)[apiName](params,ctx);// method name MUST equal apiName value
```
So:
- **Method names must equal `<Name>ApiName` values, exactly.** A typo silently routes to "ApiNotFound".
- **Methods must be class fields, not class methods**, because `this` is lost when registry calls `executor.invoke(apiName, params, ctx)`. Always declare as `methodName = async (…) => { … }`.
- **Always destructure `apiEnum` and `identifier` as `readonly` instance fields**, not getters — `BaseExecutor.hasApi/getApiNames` reads them synchronously.
---
## `BuiltinToolContext` — What the Executor Receives
The runtime hands every executor method an optional `BuiltinToolContext` as the second argument:
For dev preview, also seed `locales/zh-CN/plugin.json` and `locales/en-US/plugin.json`. Run `pnpm i18n` before opening a PR — it's slow, so do it once at the end. (See the **i18n** skill for the full workflow.)
---
## Registry Wiring
Five core files plus optional ones. Miss any and you'll see "tool not found", a missing chip, a blank result card, a stuck spinner, or an approval dialog that never appears.
| `packages/builtin-tools/src/index.ts` | Import `<Name>Manifest`; push entry to `builtinTools`. Set `hidden`/`discoverable` flags. |
| `packages/builtin-tools/src/identifiers.ts` | Add `<Name>Manifest.identifier` to `builtinToolIdentifiers`. |
| `packages/builtin-tools/src/inspectors.ts` | Import `<Name>Inspectors, <Name>Manifest`; add to `BuiltinToolInspectors`. |
| `src/store/tool/slices/builtin/executors/index.ts` | Import `<name>Executor`; add to `registerExecutors([…])`. |
| **Conditional — add only if the surface exists** | |
| `packages/builtin-tools/src/renders.ts` | Add to `BuiltinToolsRenders` if any API has a Render. |
| `packages/builtin-tools/src/placeholders.ts` | Add to `BuiltinToolPlaceholders` if any API has a Placeholder. |
| `packages/builtin-tools/src/streamings.ts` | Add to `BuiltinToolStreamings` if any API has a Streaming renderer. |
| `packages/builtin-tools/src/interventions.ts` | Add to `BuiltinToolInterventions` if any API has an Intervention component. |
| `packages/builtin-tools/src/portals.ts` | Add to `BuiltinToolsPortals` if the tool has a Portal. |
| `packages/builtin-tools/src/displayControls.ts` | Add if Render must show/hide based on result content (rare; see ClaudeCode/Codex). |
### Optional flags in `packages/builtin-tools/src/index.ts`
```ts
{
identifier: TaskManifest.identifier,
manifest: TaskManifest,
type:'builtin',
hidden: true,// hide from chat-input Tools popover
discoverable: false,// exclude from agent builder / skill discovery
}
```
Lists in the same file you may need to touch:
-`defaultToolIds` — added to the agent's tool list by default
-`alwaysOnToolIds` — forced on regardless of user selection (use sparingly)
-`runtimeManagedToolIds` — enable state controlled by runtime, not user UI; **must mirror the rules map** in `src/server/modules/Mecha/AgentToolsEngine/index.ts` and `src/helpers/toolEngineering/index.ts`
This doc covers everything that **isn't UI**: the tool's identifier, API surface, manifest, types, system prompt, ExecutionRuntime, and the executor that wires it into the frontend.
For UI surfaces (Inspector / Render / Placeholder / Streaming / Intervention / Portal), see [ui.md](ui.md).
For where files live and how registries work, see [architecture.md](architecture.md).
- **`lobe-` prefix is mandatory** — many switches in the codebase key off it.
- Pick a **domain noun**, not a verb (`lobe-task`, not `lobe-task-manager`).
- The identifier is **persisted in message history** — renaming after release means the `@deprecated` alias trick (register the legacy identifier as a second key in `inspectors.ts` / `renders.ts` pointing at the new module). Get it right the first time.
- **Plural variant for batch** (`createTasks`, `runTasks`) — describe in the manifest description that it's preferred over multiple single calls. The system prompt should also push the batch form.
- Reserve **clear separation between mutating verbs** (`updateTaskStatus`, `editTask`) and **execution verbs** (`runTask`). The system prompt must warn the model when these are confusable — see `task` for the canonical "do NOT use updateTaskStatus(running) to start a task" warning.
Define `<Name>ApiName` as `as const` so it doubles as a runtime enum (used by `BaseExecutor`) and a literal type. Then declare `Params` and `State` per API.
```ts
exportconstTaskIdentifier='lobe-task';
exportconstTaskApiName={
createTask:'createTask',
createTasks:'createTasks',
listTasks:'listTasks',
/* …one entry per API, group logically (CRUD then run-style) */
**The result-domain rule for `State`** (memory: "pluginState is result-domain, not call-domain"):
- Include only fields the UI **renders after the call returns** — ids the LLM didn't have when calling, counts, summary numbers, server-assigned status.
- **Don't echo all params.** The Inspector/Render gets `args` for free.
- Keep batch results as `{ succeeded, failed, results }` so the Render can show a one-line summary plus a detail list.
description:'Detailed instruction for what the task should accomplish.',
},
parentIdentifier:{
type:'string',
description:
'Identifier of the parent task (e.g. "TASK-1"). If provided, the new task becomes a subtask.',
},
priority:{
type:'number',
description:'Priority level: 0=none, 1=urgent, 2=high, 3=normal, 4=low. Default is 0.',
},
},
},
},
/* …one entry per ApiName */
],
};
```
### Manifest writing checklist
- **Every API in `<Name>ApiName` has exactly one entry in `api[]`.** Easy to drift after a refactor.
- **`description` on each API is the model's only docs.** Make it long enough for the LLM to pick the right tool. Mention edge cases ("If you provide any filter, omitted filters are not applied implicitly"), defaults, and the relationship to sibling APIs ("To START a task, use runTask — updateTaskStatus only flips a flag").
- **`parameters` is JSON Schema** (`LobeChatPluginApi`). Use `enum`, `required`, `items`, `oneOf`, `additionalProperties: false` etc. — these survive into the LLM's tool spec.
- **Use `additionalProperties: false`** on parameter objects so the model can't sneak unknown fields past validation.
- **Number parameters with semantic values** (`priority: 0=none, 1=urgent, …`) should describe the mapping in the description. Don't rely on `enum` alone for numbers — the model often fills the wrong one.
- **`enum` arrays for known string sets** (statuses, categories, engines). Spread from a constants module (`enum: [...TASK_STATUSES]`) so the manifest stays in sync.
### Optional manifest fields
```ts
{
/* Where this tool can run.
'client' → Agent Gateway dispatches to the desktop client (filesystem, Electron only)
'server' → ToolExecutionService runs it on the server
omitted → server only */
executors:['client','server'],
/* Default human intervention policy for all APIs that don't specify one.
Pair with an Intervention component (see ui.md). */
Per-API `humanIntervention` and `renderDisplayControl` go inside each `api[]` entry.
---
## 4. `systemRole.ts` — Operator Instructions for the Model
This is appended to the agent system prompt whenever the tool is enabled. Treat it as a **how-to-use guide for the LLM**, not marketing copy.
```ts
exportconstsystemPrompt=`You have access to Task management tools. Use them to:
- **createTask**: Create a new task. Use parentIdentifier to make it a subtask.
- **createTasks**: Prefer this over multiple createTask calls when planning a batch
(e.g. all subtasks under one parent, or all chapters of an outline).
- **runTask**: Actually START a task — kicks off the agent in a new (or continued)
topic. Do NOT use updateTaskStatus(running) to start a task; that only flips a
flag without executing. The task must have an assigneeAgentId.
- **updateTaskStatus**: Change a task's status (completed/cancelled/paused/failed).
If you mark a task as failed, include an error message explaining why.
- ...
When planning work:
1. Create tasks for each major piece (use parentIdentifier to organize as subtasks).
2. Use editTask with addDependencies to control execution order.
3. Use updateTaskStatus to mark the current task completed when done.`;
```
### Patterns that work well
- **Bulleted list, bold the API name, one line per API.** The model picks tools by skimming.
- **Disambiguate confusable APIs explicitly** (`runTask` vs `updateTaskStatus`).
- **Push toward batched APIs** ("Prefer this when…").
- **End with a numbered workflow** if the tool has a typical sequence.
- **For tools with multiple environments** (e.g. desktop vs cloud), keep variants in `systemRole.ts` and `systemRole.desktop.ts` and pick at the manifest level. See `builtin-tool-local-system`.
### Dynamic system prompts
If the prompt depends on runtime state (current date, available models), export a function and call it in the manifest:
```ts
// systemRole.ts
exportconstsystemPrompt=(today: string)=>`Today is ${today}. You have web search tools…`;
Use when the same logic runs in browser and Node (e.g. mathjs, nerdamer). The runtime is a thin wrapper that imports the executor and re-types the state per API. See `builtin-tool-calculator/src/ExecutionRuntime/index.ts` for the canonical example.
### Pattern C: Extend a shared base
When you're implementing a domain that already has a base runtime (file ops via `ComputerRuntime`), extend and only override `callService` + result normalization. See `builtin-tool-local-system/src/ExecutionRuntime/index.ts`.
### Runtime contract
Every method returns:
```ts
{
content: string;// LLM-facing — never undefined; default to error message
state?: any;// result-domain — what the UI's pluginState becomes
success: boolean;// mandatory
error?: any;// raw error object; the executor will repackage
}
```
Use `@lobechat/prompts` formatters (`searchResultsPrompt`, `crawlResultsPrompt`, `formatTaskCreated`, etc.) to produce structured `content`. They emit XML/markdown that's already tuned for token efficiency.
The executor's job is to **resolve frontend defaults** (current agent, current task, scope) and **call the runtime**. It then funnels through `toResult()` into the `BuiltinToolResult` shape.
1.**Methods are class fields** (`name = async (…) => {…}`), not class methods. The registry calls `(executor as any)[apiName](params, ctx)`; arrow-function fields keep `this` bound.
2.**`identifier` and `apiEnum` are `readonly` instance fields**, not getters — `BaseExecutor.hasApi/getApiNames` reads them synchronously at registration time.
3.**Default missing params from `ctx`**, but never silently override explicit values. Use `params.foo ?? ctx?.foo`, not `ctx?.foo ?? params.foo`.
4.**One funnel for all returns.** Either always return through `toResult(runtime.x())` (when delegating) or through `errorResult(…)` for the catch arm. Never inline `{ success: false, content: '' }` — `content: ''` collapses the Debug pane to blank.
5.**`debug('lobe-<name>:executor')`.** Match the namespace to the identifier minus `lobe-` when convenient.
6.**Singleton export.**`export const <name>Executor = new <Name>Executor()` — the registry imports the instance, not the class.
### When the executor delegates to ExecutionRuntime
The `toResult` funnel is **mandatory**: it enforces never-undefined `content` and partial-state preservation. Both invariants caught real production bugs (`globLocalFiles` Response empty, `editLocalFile` partial state lost).
---
## 7. `index.ts` — Package Entry Point
Keep it pure data + the manifest. **No React, no stores, no Node-only imports.**
**Why peer not direct deps for client libs:** the `./` and `./executionRuntime` entry points must be importable from server code. Listing React etc. as peer deps prevents bundlers from following them when only the runtime is consumed.
**Skip `./executor`** if the package has no frontend executor (server-only tools like `builtin-tool-web-browsing`).
| "ApiNotFound" at runtime | Method name in executor doesn't match `ApiName` value (typo, wrong case) |
| Method works once, then "this is undefined" | Method declared as `async fn() {}` instead of `fn = async () => {}` — `this` lost when registry invokes |
| Debug "Response" pane blank but `pluginState` populated | Returning `content: ''` or letting `output.content` be undefined — use the `toResult` funnel |
| Partial result vanishes on failure | `toResult` discarded `state` when `success: false`; preserve it |
| Tool shows up but doesn't run on desktop | `executors` in manifest doesn't include `'client'` (or vice versa for server-only) |
| Same tool registered twice / legacy identifier ghost | Identifier collision; check `@deprecated` aliases in `inspectors.ts`/`renders.ts` |
| Manifest test fails after adding API | Forgot to add the corresponding i18n `apiName.<api>` key |
| TypeScript error on `BaseExecutor<typeof X>` | `X` declared with `enum` instead of `as const` object — must be the const-object form |
A builtin tool can ship up to **six client-side surfaces**, each with a different role in the chat UI. Only `Inspector` is required; the other five are added on demand and registered in their own central files.
| Surface | Required? | When the chat shows it | Registered in |
Fall back to `createStyles + token` only when you need runtime token computation (rare). Inline `style={{ color: cssVar.colorTextSecondary }}` is fine for one-off dynamic values.
### 0.3 Use `@lobehub/ui`, not raw `antd`
`Block`, `Text`, `Flexbox`, `Highlighter`, `Alert`, `Tooltip`, `Skeleton` all come from `@lobehub/ui`. Modals come from `@lobehub/ui/base-ui` (`createModal`, `useModalContext`, `confirmModal`) — see the **modal** skill.
Memory note: `@lobehub/ui`'s `<Text type='secondary'>` is a lighter shade than `colorTextSecondary`. If you need that exact token color, write `<Text style={{ color: cssVar.colorTextSecondary }}>`.
### 0.5 Always type with `BuiltinXProps<Args, State>` generics
Don't widen to `any`. The Args generic is the JSON Schema params, the State generic is the executor's `state` field. The two should match `<Name>Params` and `<Name>State` from `types.ts`.
### 0.6 Pull strings from `t('plugin')`
```tsx
const{t}=useTranslation('plugin');
t('builtins.<identifier>.apiName.<api>');
```
Every Inspector should default to `t('builtins.<identifier>.apiName.<api>')` so it shows something while args stream in.
### 0.7 Read store state from `@/store/chat`, not props
Tool surfaces sometimes need cross-cutting state (loading, streaming buffer). Read it inside the component via Zustand selectors, not from props — props only carry args/state/messageId.
---
## 1. Inspector — Header Chip (required)
**Lifecycle:** Inspector renders for **every phase** of a tool call: while args are streaming in, while the executor is running, and after results come back. It's the only surface that's always visible.
**Goal:** keep it to a single line. Show what's happening with as much context as is currently available.
| Args streaming, no useful field yet | `isArgumentsStreaming === true`, `partialArgs.X` undefined | Just the API title with `shinyTextStyles.shinyText` |
| Args streaming, key field arrived | `partialArgs.X` populated | Title + key field chip, still pulse-animated |
| Args complete, executor running | `args` populated, `isLoading === true` | Same as above, still pulse-animated |
| Result arrived | `pluginState` populated, `isLoading === false` | Title + chips + result summary (count, identifier, status) |
- Wrap the whole row with `inspectorTextStyles.root` (provides correct flex / line-height baseline).
- Pulse with `shinyTextStyles.shinyText` whenever `isArgumentsStreaming || isLoading`.
- Show the i18n title first so the row is non-empty during the earliest streaming phase.
- Read both `args?.X` and `partialArgs?.X` together — `args` is final, `partialArgs` is in-stream.
- Use chips/tags for distinct facets (identifier, name, parent, status, count). Each chip should clip with `text-overflow: ellipsis` and have a `max-width` so long values don't blow out the chat bubble.
- Append `pluginState`-derived suffixes only **after** loading finishes — count or "(no results)" should not appear while still searching.
**Lifecycle:** rendered **once the result arrives** (after Placeholder/Streaming hand off). Sits below the Inspector header.
**Skip if** the API is read-only or the result is just text — the framework already shows the executor's `content` string. Add a Render only when there's a structured artifact worth seeing: a card, a chart, a diff, a list of files.
If the Render should hide for certain results (e.g. ClaudeCode's TodoWrite hides when the agent is mid-stream), add a `RenderDisplayControl` to `packages/builtin-tools/src/displayControls.ts`. See `ClaudeCodeRenderDisplayControls` for the pattern.
---
## 3. Placeholder — Skeleton Between Args and Result (optional)
**Lifecycle:** rendered when the args have finished streaming but the executor hasn't returned yet. Disappears when `pluginState` arrives. Bridges the moment of perceived lag.
**Add for** APIs with noticeable execution time: web search, network crawl, file list, large grep. **Skip for** instant ops (status flips, calculator).
- **Mirror the eventual Render's layout.** When the result arrives the Placeholder unmounts and the Render mounts; if they share dimensions, the chat doesn't jump.
- Use `Skeleton.Block` / `Skeleton.Button` from `@lobehub/ui` for placeholder shapes.
- Embed any args you have (e.g. the query text) — context helps the user know what's loading.
- Pulse with `shinyTextStyles.shinyText` if the Placeholder includes literal text.
## 4. Streaming — Live Output During Execution (optional)
**Lifecycle:** rendered **while the executor is still running** for APIs that emit incremental output. The component is responsible for fetching the in-flight stream from the chat store and rendering it.
messageId: string;// use to fetch the streaming buffer from store
toolCallId: string;
}
```
Note there's **no `state` or `result` prop** — the Streaming component is for the in-flight phase. It pulls the live buffer from the store itself (typically via `chatToolSelectors.streamingContent(messageId)` or similar).
**Lifecycle:** rendered **before the executor runs** for APIs whose manifest sets `humanIntervention`. The user sees a preview of the args, can edit them, then approves or skips/cancels.
- **Show a preview, not a form by default.** Editing UI is opt-in via `onArgsChange` and is usually inline (click to edit a code block, etc.).
- For args with debounced edit state (text fields), use `registerBeforeApprove(id, flushFn)` so the approve action waits for the debounce to flush. Always return the cleanup function.
- Call `onInteractionAction({ type: 'submit', payload })` when the user approves; `'skip'` if they skip with a reason; `'cancel'` if they cancel the whole turn.
- Add a corresponding `interventionAudit.ts` in the package root if the tool needs scope/path validation before approval (see `local-system/src/interventionAudit.ts`).
**Lifecycle:** rendered when the user opens the tool message in a side panel or full-screen modal. One Portal per **tool**, not per API — the Portal switches on `apiName` internally.
**Add for** tools whose results deserve a deep-dive view: search results with editable filters, page content with reader mode, code interpreter sessions.
| Portal opens but blank | Switch in `Portal/index.tsx` doesn't cover the apiName | | |
| Strings show as `builtins.lobe-foo.apiName.bar` | Missing i18n key in `src/locales/default/plugin.ts` (or not seeded in dev locale files) | | |
| Wrong color shade on `<Text type="secondary">` | `type='secondary'` is lighter than `colorTextSecondary` — pass via `style={{ color: cssVar.colorTextSecondary }}` | | |
-`src/server/routers/lambda/video/index.ts` — video creation (uses `authedProcedure` + `serverDatabase`)
-`src/server/routers/lambda/generation.ts` — status checking
-`packages/database/src/models/asyncTask.ts` — `AsyncTaskModel` including `checkTimeoutTasks`
**Note**: Image/video routes do NOT use the `keyVaults` middleware — they read API keys from the database via `initModelRuntimeFromDB` or `createAsyncCaller`.
description: Guide for implementing and debugging LobeHub heterogeneous agent integrations such as Claude Code, Codex, and future external CLI agents. Use when working on adapter event mapping, Electron IPC transport, renderer persistence, tool-call chaining, subagent threads, resume/session handling, or regressions like mixed multi-tool messages, broken step boundaries, stuck tool loading, and orphan tool messages. Triggers on 'heterogeneous agent', 'hetero agent', '异构 agent', 'claude code adapter', 'codex adapter', 'external agent CLI', '孤立 tool 消息', 'raw Codex trace', or adapter/executor bugs.
---
# Heterogeneous Agent Development
Use this skill when the bug or feature lives in the external CLI agent pipeline, not the normal server-side agent runtime.
## Use This Skill For
- Adding or changing a driver under `apps/desktop/src/main/modules/heterogeneousAgent/drivers/`
- Editing an adapter under `packages/heterogeneous-agents/src/adapters/`
- Debugging `heteroAgentRawLine` transport, `window.__HETERO_AGENT_TRACE`, or `executeHeterogeneousAgent`
- Fixing Claude Code stream-json bugs such as duplicate partial/full chunks, broken `message.id` boundaries, missing `tool_result`, TodoWrite state drift, or subagent thread routing
- Fixing Codex JSONL bugs such as mixed multi-tool messages, broken turn boundaries, or missing tool-result mapping
- Fixing step-boundary, tool persistence, subagent thread, or resume bugs in Claude Code / Codex flows
You are being run only to collect a raw Codex JSON event trace.
Do not modify any files.
Use at least 4 separate shell tool invocations, one invocation per command.
Run a short sequence of read-only repo checks and then reply with a one-sentence summary.
EOF
```
What to look for in the JSONL:
-`thread.started`
-`turn.started`
-`item.started` / `item.completed`
-`item.type === 'command_execution'`
-`item.type === 'agent_message'`
-`turn.completed`
If raw Codex already merges tools into one item, the adapter is innocent. If raw Codex emits independent items but UI collapses them, the bug is downstream.
If the repo already contains useful traces under `.heerogeneous-tracing/`, inspect them before reproducing.
### Claude Code raw NDJSON
Mirror the arguments from `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts`.
-`-p`
-`--input-format stream-json`
-`--output-format stream-json`
-`--verbose`
-`--include-partial-messages`
-`--permission-mode bypassPermissions`
You can capture a local raw trace like this:
```bash
ts=$(date +%Y%m%d-%H%M%S)
out=".heerogeneous-tracing/claude-${ts}.ndjson"
cat << 'EOF' | claude -p \
--input-format stream-json \
--output-format stream-json \
--verbose \
--include-partial-messages \
--permission-mode bypassPermissions \
> "$out"
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Do a few read-only repo checks, use several tool calls, and then summarize briefly."}]}}
EOF
```
What to look for in Claude Code raw traces:
-`type: 'system', subtype: 'init'`
-`type: 'assistant'` blocks for `thinking`, `tool_use`, and `text`
-`type: 'user'` blocks containing `tool_result`
-`type: 'stream_event'` with `message_start`, `content_block_delta`, and `message_delta`
-`type: 'result'`
-`type: 'rate_limit_event'`
Important Claude Code semantics:
- Each content block often arrives as its own assistant event.
- Multiple assistant events can share the same `message.id`; that is still one turn.
-`message.id` change is the main-step boundary.
- Partial deltas arrive before the later full assistant block.
-`message_delta.usage` is the authoritative per-turn usage.
- Subagent events are tagged with `parent_tool_use_id`.
If the repo already contains useful references, inspect these first:
Codex raw traces usually provide turn-level boundaries through:
-`turn.started`
-`turn.completed`
The executor only cuts a new assistant message when it receives a step-boundary signal it understands. If the adapter emits `stream_start` without `newStep`, multiple Codex tools and text chunks can accumulate under the same assistant longer than intended.
@@ -30,6 +30,74 @@ This is NON-NEGOTIABLE. Skipping Linear comments is a workflow violation.
When creating issues with `mcp__linear-server__create_issue`, **MUST add the `claude code` label**.
## Language
Issue titles, descriptions, and comments **MUST follow the language of the current conversation**, not default to English.
- Conversation in 中文 → issue body in 中文;technical terms (file paths, identifiers, library names, commands, error messages) stay in English.
- Conversation in English → issue body in English.
- Code blocks, file paths, and quoted strings always stay in their original form regardless of surrounding language.
- This applies equally to **updates** — when editing an existing issue (description **and titles**), preserve the language of the conversation that triggered the edit; do not switch the issue language during a refactor (Chinese → English or vice versa).
Rationale: the issue is a continuation of the conversation. Forcing English when the discussion is in Chinese creates translation friction for the collaborator who came from that thread.
## Creating Sub-issue Trees
When breaking a parent issue into a tree of sub-issues (e.g., task decomposition for LOBE-xxx), follow these rules — they work around real limitations of the Linear MCP tools.
### 1. ALWAYS prefix titles with an ordering index
The Linear Sub-issues panel displays children by `sortOrder`, which **defaults to newest-first** (most recently created appears on top). Neither parallel nor serial creation will produce the intended top-to-bottom reading order, and the MCP `save_issue` tool does **not expose a `sortOrder` parameter** — you cannot set order at create time.
**Workaround**: encode execution order in the title itself:
```plaintext
[1] [db] add schema fields
[2] [db] new table + repository
[3] [service] business logic layer
[4] [api] REST endpoints
[4.1] [sdk] client SDK wrapper
[4.1.1] [app] consumer integration
[4.1.2] [app] UI surface
[4.2] [ui] dashboard page
```
Even when the panel shuffles, the reader can mentally reconstruct the dependency graph at a glance. Dotted numbering `[n.m.k]` should mirror the parent-child nesting so the index and the tree agree.
### 2. Nest sub-issues by logical parent-child, not flat under the root
Linear supports **unlimited sub-issue depth**. A flat list of 8+ siblings under one root is hard to scan. Group by main-subordinate logic:
- Core service → its SDK → SDK consumers
- Don't create a sibling when a child is more accurate
Use `parentId: "LOBE-xxxx"` at creation (or `save_issue` to move). Moving an issue's parent does not disturb its `blockedBy` relations.
### 3. Sub-issue creation order is dictated by `blockedBy`
`blockedBy` requires the blocker to exist first (you need its LOBE-id). So:
1.**Topologically sort** the DAG — leaves (no deps) first, roots last
2. Create issues with zero deps in the first wave
3. Create dependent issues only after collecting the blocker IDs from prior responses
4.`blockedBy` is **append-only**; passing it again does not overwrite — safe to re-run
### 4. Don't waste rounds trying to parallelize
MCP tool calls in a single message look parallel but execute sequentially on the server, and you still need blocker IDs from earlier responses. Just issue calls in dependency order; optimizing for parallelism gains nothing here.
### 5. Keep each sub-issue description self-contained
Each sub-issue should state:
- Goal (1–2 lines)
- Key files to touch
- Concrete changes / acceptance criteria
- Dependencies (link to blocker issues by `LOBE-xxxx`)
- Validation steps
The implementer may open only the sub-issue, not the parent — don't rely on context that lives only in the parent description.
## Completion Comment Format
Every completed issue MUST have a comment summarizing work done:
description: 'Code review checklist for LobeHub. Use when reviewing PRs, diffs, or code changes. Covers correctness, security, quality, and project-specific patterns.'
name: review-checklist
description: 'Common recurring mistakes in LobeHub code review — console leftovers, missing return await, hardcoded secrets, hardcoded i18n strings, desktop router pair drift, antd vs @lobehub/ui, non-idempotent migrations, cloud impact red flags. Use as a quick checklist when reviewing PRs, diffs, or branch changes.'
---
# Code Review Guide
# Review Checklist
## Before You Start
1. Read `/typescript` and `/testing` skills for code style and test conventions
2. Get the diff (skip if already in context, e.g., injected by GitHub review app): `git diff` or `git diff origin/canary..HEAD`
## Checklist
### Correctness
## Correctness
- Leftover `console.log` / `console.debug` — should use `debug` package or remove
- Missing `return await` in try/catch — see <https://typescript-eslint.io/rules/return-await/> (not in our ESLint config yet, requires type info)
- Can the fix/implementation be more concise, efficient, or have better compatibility?
### Security
## Security
- No sensitive data (API keys, tokens, credentials) in `console.*` or `debug()` output
- No base64 output to terminal — extremely long, freezes output
- No hardcoded secrets — use environment variables
### Testing
## Testing
- Bug fixes must include tests covering the fixed scenario
- New logic (services, store actions, utilities) should have test coverage
- Existing tests still cover the changed behavior?
- Prefer `vi.spyOn` over `vi.mock` (see `/testing` skill)
### i18n
## i18n
- New user-facing strings use i18n keys, not hardcoded text
- Keys added to `src/locales/default/{namespace}.ts` with `{feature}.{context}.{action|status}` naming
- For PRs: `locales/` translations for all languages updated (`pnpm i18n`)
### SPA / routing
## SPA / routing
- **`desktopRouter` pair:** If the diff touches `src/spa/router/desktopRouter.config.tsx`, does it also update `src/spa/router/desktopRouter.config.desktop.tsx` with the same route paths and nesting? Single-file edits often cause drift and blank screens.
### Reuse
## Reuse
- Newly written code duplicates existing utilities in `packages/utils` or shared modules?
- Copy-pasted blocks with slight variation — extract into shared function
Use this skill when the user asks to run the migrated source command `dedupe`.
## Command Template
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
---
Found 3 possible duplicate issues:
1. <link to issue>
2. <link to issue>
3. <link to issue>
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
@@ -189,7 +189,7 @@ If metrics cannot be reliably computed, omit unknown numbers instead of guessing
Follow this section order unless the user asks otherwise:
1.`# 🚀 LobeHub v<x.y.z> (<YYYYMMDD>)`
1.`# 🚀 LobeHub Release (<YYYYMMDD>)`
2. Metadata lines:
-`Release Date`
-`Since <Previous Version>` metrics
@@ -238,13 +238,34 @@ Use `---` separators between major blocks for long releases.
- Keep concise.
- Must include `Migration overview`, operator impact, and rollback/backup note.
### Contributor Ordering
Render contributors as a **single flat list** (no separate "Community" / "Core Team" subsections). Order: **community contributors first, team members after**. Within each group, sort by PR count desc. Bots (`@lobehubbot`, `renovate[bot]`) go on a separate "maintenance" line.
**LobeHub team roster** — anyone in this list is a team member; anyone not in this list is a community contributor:
-@arvinxx
-@Innei
-@tjx666 (commit author name: YuTengjing)
-@LiJian
-@Neko
-@Rdmclin2
-@AmAzing129
-@sudongyuer
-@rivertwilight
-@CanisMinor
> **Resolving handles** — git author names (e.g. `YuTengjing`) are not always the GitHub handle. Verify via `gh pr view <PR> --json author` or `gh api search/users -f q='<email>'` before listing.
If a new contributor appears who is not on this list, treat them as community by default and ask the user whether to add them to the roster.
**Migration Scope:** Agent benchmark data model bootstrap (5 new tables, 2 new indexes)
@@ -7,14 +7,6 @@
---
## ✨ Highlights
- **Benchmark Lifecycle Schema** — Added a relational model that tracks benchmark setup, runs, per-topic execution, and record outputs end-to-end.
- **Queryability Upgrade** — Added indexes for run status and benchmark-topic joins, improving operational queries in dashboard and debugging workflows.
- **Safer Operator Rollout** — Migration is startup-driven and backward-compatible with existing non-benchmark chat workflows.
**Hotfix Scope:** Agent topic-switching regression — stale chat state on agent change
> Clears residual topic state when navigating between agents and restores blank-canvas behavior on agent switch.
## 🐛 What's Fixed
- **Stale topic on agent switch** — Switching from `/agent/agt_A/tpc_X` to `/agent/agt_B` no longer leaves the previous topic's messages on screen, and _Start new topic_ responds again. (#14231)
- **Header & sidebar consistency** — Conversation header now shows the active subtopic's title, and the sidebar keeps the parent topic's thread list expanded while a thread is open.
## ⚙️ Upgrade
- Self-hosted: pull the new image and restart. No schema or env changes.
- Cloud: applied automatically.
## 👥 Owner
@{pr-author}
> **Note for Claude**: Replace `{pr-author}` with the actual PR author. Retrieve via `gh pr view <number> --json author --jq '.author.login'`. Do not hardcode a username.
> This weekly release focuses on reducing friction in everyday agent work: faster model routing, smoother gateway behavior, stronger task continuity, and clearer operator diagnostics when something goes wrong.
2.**Create PR to main** with a gitmoji prefix title (e.g. `🐛 fix: description`)
3.**After merge**: auto-tag-release detects `hotfix/*` branch → auto patch +1.
3.**Write a short hotfix changelog** — See `changelog-example/hotfix.md`. Keep it minimal: scope line, 1-3 fix bullets (symptom + fix in one sentence), upgrade note, owner. No long root-cause section — that lives in the commit message.
- **Hotfix owner**: Use the actual PR author (retrieve via `gh pr view <number> --json author --jq '.author.login'`), never hardcode a username.
4.**After merge**: auto-tag-release detects `hotfix/*` branch → auto patch +1.
- **@ONLY-yours**: Performance, streaming, settings, general bugs, web platform, marketplace, agent builder, schedule task
- **@tjx666**: Model providers and configuration, new model additions, image/video generation, vision, cloud version, documentation, TTS, auth, login/register, database
This document serves as a comprehensive guide for all team members when developing LobeHub.
## Project Description
You are developing an open-source, modern-design AI Agent Workspace: LobeHub (previously LobeChat).
Guidelines for using AI coding agents in this LobeHub repository.
## Tech Stack
- **Frontend**: Next.js 16, React 19, TypeScript
-**UI Components**: Ant Design, @lobehub/ui, antd-style
-**State Management**: Zustand, SWR
-**Database**: PostgreSQL, PGLite, Drizzle ORM
-**Testing**: Vitest, Testing Library
-**Package Manager**: pnpm (monorepo structure)
- Next.js 16 + React 19 + TypeScript
-SPA inside Next.js with `react-router-dom`
-`@lobehub/ui`, antd for components; antd-style for CSS-in-JS — **prefer `createStaticStyles` with `cssVar.*`** (zero-runtime); only fall back to `createStyles` + `token` when styles genuinely need runtime computation. See `.cursor/docs/createStaticStyles_migration_guide.md`.
-react-i18next for i18n; zustand for state management
-SWR for data fetching; TRPC for type-safe backend
SPA-related code is grouped under `src/spa/` (entries + router) and `src/routes/` (page segments). We use a **roots vs features** split: route trees only hold page segments; business logic and UI live in features.
- **`src/spa/`** – SPA entry points (`entry.web.tsx`, `entry.mobile.tsx`, `entry.desktop.tsx`, `entry.popup.tsx`) and React Router config (`router/`, with `desktopRouter.config.*`, `mobileRouter.config.tsx`, `popupRouter.config.tsx`). Keeps router config next to entries to avoid confusion with `src/routes/`.
- **`src/routes/` (roots)**\
Only page-segment files: `_layout/index.tsx`, `index.tsx` (or `page.tsx`), and dynamic segments like `[id]/index.tsx`. Keep these **thin**: they should only import from `@/features/*` and compose layout/page, with no business logic or heavy UI.
- **`src/features/`**\
Business components by **domain** (e.g. `Pages`, `PageEditor`, `Home`). Put layout chunks (sidebar, header, body), hooks, and domain-specific UI here. Each feature exposes an `index.ts` (or `index.tsx`) with clear exports.
When adding or changing SPA routes:
1. In `src/routes/`, add only the route segment files (layout + page) that delegate to features.
2. Implement layout and page content under `src/features/<Domain>/` and export from there.
3. In route files, use `import { X } from '@/features/<Domain>'` (or `import Y from '@/features/<Domain>/...'`). Do not add new `features/` folders inside `src/routes/`.
4.**Register the desktop route tree in both configs:**`src/spa/router/desktopRouter.config.tsx` and `src/spa/router/desktopRouter.config.desktop.tsx` must stay in sync (same paths and nesting). Updating only one can cause **blank screens** if the other build path expects the route. `desktopRouter.sync.test.tsx` guards this invariant — keep it passing.
See the **spa-routes** skill (`.agents/skills/spa-routes/SKILL.md`) for the full convention and file-division rules.
## Development
### Starting the Dev Environment
```bash
# SPA dev mode (frontend only, proxies API to localhost:3010)
bun run dev:spa
# Full-stack dev (Next.js + Vite SPA concurrently)
bun run dev
```
After `dev:spa` starts, the terminal prints a **Debug Proxy** URL:
Open this URL to develop locally against the production backend (app.lobehub.com). The proxy page loads your local Vite dev server's SPA into the online environment, enabling HMR with real server config.
### Git Workflow
- **Branch strategy**: `canary` is the development branch (cloud production); `main` is the release branch (periodically cherry-picks from canary)
- New branches should be created from `canary`; PRs should target `canary`
- Use rebase for git pull
-Git commit messages should prefix with gitmoji
-Git branch name format: `feat/feature-name`
- Use `.github/PULL_REQUEST_TEMPLATE.md` for PR descriptions
- **Protection of local changes**: Never use `git restore`, `git checkout --`, `git reset --hard`, or any other command or workflow that can forcibly overwrite, discard, or silently replace user-owned uncommitted changes. Before any revert or restoration affecting existing files, inspect the working tree carefully and obtain explicit user confirmation.
- Use rebase for `git pull`
-Commit messages: prefix with gitmoji
-Branch format: `<type>/<feature-name>`
### Package Management
- Use`pnpm`as the primary package manager
- Use`bun` to run npm scripts
- Use`bunx`to run executable npm packages
-`pnpm`for dependency management
-`bun` to run npm scripts
-`bunx`for executable npm packages
### Code Style Guidelines
#### TypeScript
- Prefer interfaces over types for object shapes
### Testing Strategy
### Testing
```bash
# Web tests
bunx vitest run --silent='passed-only''[file-path-pattern]'
# Run specific test (NEVER run `bun run test` - takes ~10 minutes)
bunx vitest run --silent='passed-only''[file-path]'
# Package tests (e.g., database)
cd packages/[package-name]&& bunx vitest run --silent='passed-only''[file-path-pattern]'
# Database package
cd packages/database&& bunx vitest run --silent='passed-only''[file]'
```
**Important Notes**:
-Wrap file paths in single quotes to avoid shell expansion
- Never run `bun run test` - this runs all tests and takes \~10 minutes
### Type Checking
- Use `bun run type-check` to check for type errors
- Prefer `vi.spyOn` over `vi.mock`
- Tests must pass type check: `bun run type-check`
-After 2 failed fix attempts, stop and ask for help
### i18n
-**Keys**: Add to `src/locales/default/namespace.ts`
-**Dev**: Translate `locales/zh-CN/namespace.json` locale file only for preview
- DON'T run`pnpm i18n`, let CI auto handle it
-Add keys to a namespace file under `src/locales/default/` (e.g. `agent.ts`, `auth.ts`)
-For dev preview: translate `locales/zh-CN/` and `locales/en-US/`
-`pnpm i18n` is slow; run it manually when locale keys need updating (e.g. before opening a PR).
## SPA Routes and Features
### Code Review
- **`src/routes/`** holds only page segments (`_layout/index.tsx`, `index.tsx`, `[id]/index.tsx`). Keep route files **thin** — import from `@/features/*` and compose, no business logic.
- **`src/features/`** holds business components by **domain** (e.g. `Pages`, `PageEditor`, `Home`). Layout pieces, hooks, and domain UI go here.
- **Desktop router parity:** When changing the main SPA route tree, update **both**`src/spa/router/desktopRouter.config.tsx` (dynamic imports) and `src/spa/router/desktopRouter.config.desktop.tsx` (sync imports) so paths and nesting match. Changing only one can leave routes unregistered and cause **blank screens**.
- See the **spa-routes** skill (`.agents/skills/spa-routes/SKILL.md`) for the full convention and file-division rules.
## Skills (Auto-loaded)
All AI development skills are available in `.agents/skills/` directory and auto-loaded by Claude Code when relevant.
**IMPORTANT**: When reviewing PRs or code diffs, ALWAYS read `.agents/skills/code-review/SKILL.md` first.
Before reviewing a PR / diff / branch change, read the **review-checklist** skill (`.agents/skills/review-checklist/SKILL.md`) — it lists the recurring mistakes specific to this codebase.
- **misc**: clear stale topic when switching agents from a topic route.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: clear stale topic when switching agents from a topic route, closes [#14231](https://github.com/lobehub/lobe-chat/issues/14231) ([deeb97a](https://github.com/lobehub/lobe-chat/commit/deeb97a))
Guidelines for using Claude Code in this LobeHub repository.
## Tech Stack
- Next.js 16 + React 19 + TypeScript
- SPA inside Next.js with `react-router-dom`
-`@lobehub/ui`, antd for components; antd-style for CSS-in-JS — **prefer `createStaticStyles` with `cssVar.*`** (zero-runtime); only fall back to `createStyles` + `token` when styles genuinely need runtime computation. See `.cursor/docs/createStaticStyles_migration_guide.md`.
- react-i18next for i18n; zustand for state management
- SWR for data fetching; TRPC for type-safe backend
SPA-related code is grouped under `src/spa/` (entries + router) and `src/routes/` (page segments). We use a **roots vs features** split: route trees only hold page segments; business logic and UI live in features.
- **`src/spa/`** – SPA entry points (`entry.web.tsx`, `entry.mobile.tsx`, `entry.desktop.tsx`) and React Router config (`router/`). Keeps router config next to entries to avoid confusion with `src/routes/`.
- **`src/routes/` (roots)**\
Only page-segment files: `_layout/index.tsx`, `index.tsx` (or `page.tsx`), and dynamic segments like `[id]/index.tsx`. Keep these **thin**: they should only import from `@/features/*` and compose layout/page, with no business logic or heavy UI.
- **`src/features/`**\
Business components by **domain** (e.g. `Pages`, `PageEditor`, `Home`). Put layout chunks (sidebar, header, body), hooks, and domain-specific UI here. Each feature exposes an `index.ts` (or `index.tsx`) with clear exports.
When adding or changing SPA routes:
1. In `src/routes/`, add only the route segment files (layout + page) that delegate to features.
2. Implement layout and page content under `src/features/<Domain>/` and export from there.
3. In route files, use `import { X } from '@/features/<Domain>'` (or `import Y from '@/features/<Domain>/...'`). Do not add new `features/` folders inside `src/routes/`.
4.**Register the desktop route tree in both configs:**`src/spa/router/desktopRouter.config.tsx` and `src/spa/router/desktopRouter.config.desktop.tsx` must stay in sync (same paths and nesting). Updating only one can cause **blank screens** if the other build path expects the route.
See the **spa-routes** skill (`.agents/skills/spa-routes/SKILL.md`) for the full convention and file-division rules.
## Development
### Starting the Dev Environment
```bash
# SPA dev mode (frontend only, proxies API to localhost:3010)
bun run dev:spa
# Full-stack dev (Next.js + Vite SPA concurrently)
bun run dev
```
After `dev:spa` starts, the terminal prints a **Debug Proxy** URL:
Open this URL to develop locally against the production backend (app.lobehub.com). The proxy page loads your local Vite dev server's SPA into the online environment, enabling HMR with real server config.
### Git Workflow
- **Branch strategy**: `canary` is the development branch (cloud production); `main` is the release branch (periodically cherry-picks from canary)
- New branches should be created from `canary`; PRs should target `canary`
- Use rebase for `git pull`
- Commit messages: prefix with gitmoji
- Branch format: `<type>/<feature-name>`
### Package Management
-`pnpm` for dependency management
-`bun` to run npm scripts
-`bunx` for executable npm packages
### Testing
```bash
# Run specific test (NEVER run `bun run test` - takes ~10 minutes)
bunx vitest run --silent='passed-only''[file-path]'
# Database package
cd packages/database && bunx vitest run --silent='passed-only''[file]'
```
- Prefer `vi.spyOn` over `vi.mock`
- Tests must pass type check: `bun run type-check`
- After 2 failed fix attempts, stop and ask for help
### i18n
- Add keys to `src/locales/default/namespace.ts`
- For dev preview: translate `locales/zh-CN/` and `locales/en-US/`
- Don't run `pnpm i18n` - CI handles it
## Skills (Auto-loaded by Claude)
Claude Code automatically loads relevant skills from `.agents/skills/`.
logger.debug('Failed to get claude auth status:',error);
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.