Compare commits

...

2170 Commits

Author SHA1 Message Date
yutengjing 5b19ca3990 💄 style: extend artifact code background 2026-06-12 11:29:19 +08:00
yutengjing c8b5e337c0 🐛 fix: keep artifact code panel scrolled 2026-06-12 11:12:34 +08:00
Arvin Xu 61586b9377 🐛 fix(agent): persist & deliver image attachments for device/sandbox hetero runs (#15685)
* 🐛 fix(agent): persist file attachments in hetero early-exit user message

The hetero-agent early exit in execAgent created the user message without
the `files` relation, so attachments sent from the SPA gateway path
(executionTarget=device / sandbox) were never linked via messagesFiles and
disappeared once the optimistic client message was replaced by the server
snapshot. Attach the deduped `fileIds` the same way sendMessageInServer
does on the local-mode path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): deliver image attachments to device/sandbox hetero runs

Persisting the messagesFiles relation fixed display, but the dispatched
CLI still never saw the image — local mode feeds the persisted imageList
into sendPrompt for vision, while the device/sandbox dispatch protocols
(agent_run_request / sandbox runner) only carried a text prompt.

- resolve attached images into signed URLs in the hetero early exit
  (metadata-only, non-fatal) and carry them through heteroParams
- add imageList to the agent_run_request wire type and dispatchAgentRun
  params (gateway client + server service)
- extract buildHeteroExecStdinPayload into @lobechat/heterogeneous-agents
  so the three dispatch sites (desktop spawnLhHeteroExec, lh connect
  daemon, server sandbox runner) build the same content-block payload:
  systemContext, prompt, then image blocks
- lh hetero exec already coerces image blocks via coerceJsonPrompt and
  normalizeImage (url → base64 for Claude Code, materialized path for
  Codex), so no CLI consumer changes are needed

openclaw/hermes (runHeteroTask) keep text-only prompts — their dispatch
goes through a separate one-shot tool protocol.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(heterogeneous-agents): move exec stdin wire contract to a pure /protocol entry

The server sandbox runner imported `buildHeteroExecStdinPayload` through the
`/spawn` barrel, which (with no `sideEffects` hint) bundles the whole spawn
machinery into the Next.js server chunk. Its `process.cwd()`-rooted dynamic
fs calls then make Vercel's output file tracing glob the entire repo source
tree into every serverless function (+~69 MB each), pushing the 4 largest
functions past the 250 MB uncompressed limit and failing the deployment.

Split the dispatch wire contract (stdin payload builder + content-block
types) into a new pure, isomorphic `/protocol` export and point all three
dispatch sites (server sandbox runner, desktop main, `lh connect` daemon) at
it. `/spawn` re-exports the moved symbols so executor-side callers are
unaffected. Also declare `sideEffects: false` for the package.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:02:51 +08:00
Arvin Xu eca449e4e2 feat(skills): agent-testing iteration after first real-world run (#15700)
* 📝 docs(skills): make agent-testing Step 0 an env-setup + auth checklist

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

*  feat(skills): agent-testing probes, GIF evidence, and report-language rule

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:52:25 +08:00
renovate[bot] 6c8976b641 Update dependency vitest to v3.2.6 [SECURITY] (#15698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-11 23:34:38 +08:00
Arvin Xu 60d9d3c3c7 ♻️ refactor(skills): merge local-testing and cli-backend-testing into agent-testing (#15699)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 23:14:45 +08:00
Arvin Xu 2dd4cf7a1d fix(agentDocument): replace getDocuments with listDocuments in useFetchAgentDocuments to avoid over-fetching (#15301)
* fix(agentDocument): listDocuments returns templateId and derived fields

* fix(agentDocument): useFetchAgentDocuments use listDocuments instead of getDocuments

* fix(agentDocument): derive AgentDocumentItem from listDocuments return type

* fix(agentDocument): export AgentDocumentListItem type

* 🐛 fix(agentDocument): align list projections and consumers after rebase onto canary

- listDocumentsForTopic now returns the same projection as listDocuments
  (derived fields + templateId), so the tRPC union no longer collapses
  the inferred client type to the old 8-field shape
- add description/updatedAt to both projections for sidebar consumers
- AgentDocumentsGroup switches getDocuments -> listDocuments (it already
  shared the documentsList SWR key)
- makePendingDocument trimmed to the lean list item shape
- update useFetchAgentDocuments test to the listDocuments behavior

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(agentDocument): migrate agentDocumentSkills sync to slim listDocuments

The tool store's skill registry sync shared agentDocumentSWRKeys.documentsList
with the working sidebar and the new useFetchAgentDocuments hook, but still
fetched the full getDocuments payload. Sharing one SWR key across different
payload shapes made the cached result order-dependent: whichever consumer
mounted first decided whether the cache held the heavy full documents or the
slim list items. Migrate the skills sync to listDocuments, whose projection
covers every field mapDocsToSkills reads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:41:24 +08:00
Arvin Xu 575ef1e8ee ♻️ refactor(agent): single-track device-tool injection via execution plan (#15683)
* ♻️ refactor(agent): single-track device-tool injection via execution plan

P3 follow-up to #15669 — downstream layers now consume the resolved
ExecutionPlan instead of re-deriving device capability:

- ExecutionPlan carries the effective `target`; persisted into
  state.metadata.executionPlan via createOperation
- call_llm executor gates buildStepToolDelta's activeDeviceId signal on
  the plan (none/sandbox can never re-inject local-system mid-run)
- AgentToolsEngine consumes the plan's target; redundant rule-level
  canUseDevice checks removed (physical manifest walls remain)
- builtin agent runtime config can now override agencyConfig
  (web-onboarding pins executionTarget=none)
- hetero desktop 'local' selection persists this desktop's deviceId so
  opening the agent from web dispatches to the same machine via gateway
- 'local' vs 'device' stay distinct user choices even for the same
  machine: gateway dispatch streams progress to all clients (mobile),
  IPC is faster but desktop-session-only — guarded by a regression test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(agent): enforce device access policy on hetero dispatch

resolveDeviceAccessPolicy now runs BEFORE the hetero early exit and feeds
canUseDevice into the hetero execution plan: a denied sender (external
bot user) degrades local/device-bound CLI hetero runs to the cloud
sandbox instead of dispatching to the owner's machine, and requestedDeviceId
cannot bypass the policy. Remote hetero agents (openclaw/hermes) are
device-only with no sandbox fallback, so denied senders are refused
outright.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 style(agent): fix interface field order in RuntimeSelectionContext

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 22:39:11 +08:00
YuTengjing ba6976c063 🐛 fix: pause input completion after errors (#15692) 2026-06-11 22:05:45 +08:00
Innei bfdfd3bca3 🐛 fix(desktop): adjust mac fullscreen titlebar spacing (#15693) 2026-06-11 22:02:48 +08:00
YuTengjing f6c23e3654 🐛 fix(agent-runtime): persist assistant reasoning to DB (#15690) 2026-06-11 21:05:23 +08:00
Arvin Xu 813d756b9c 🐛 fix(editor-canvas): re-check editor init state before subscribing (#15686)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:42:28 +08:00
renovate[bot] 671bc26e0d Update opentelemetry-js-contrib monorepo (#13582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-11 20:41:48 +08:00
renovate[bot] 309c25cb44 Update dependency code-inspector-plugin to v1.3.6 (#14612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-11 20:41:40 +08:00
Arvin Xu a810bf3dcd 🐛 fix(agent-runtime): always persist assistant reasoning to DB (#15687)
* 🐛 fix(agent-runtime): always persist assistant reasoning to DB

PR #13494 gated message reasoning persistence behind preserveThinking
(agent chatConfig + model extendParams / qwen|zhipu fallback). That gate
is only meant to control whether reasoning is replayed into the next LLM
payload — applying it to the DB write dropped thinking content for every
non-qwen/zhipu reasoning model in server-side agent mode: reasoning
streamed live via stream_end but vanished after refresh.

Restore unconditional reasoning persistence in messageModel.update and
keep the preserveThinking gate only for state.messages payload replay.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 style(i18n): localize callSubAgent tool labels

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:41:29 +08:00
Arvin Xu 7d6be512b8 🐛 fix(model-runtime): align tool-calling fallback tests & surface missing tool call as error (#15691)
*  test(model-runtime): align tool-calling fallback tests with new return shape

#15680 changed generateObject's tool-calling fallback to return the parsed
schema object (same shape as the json_schema path) instead of an array of
tool calls, and reworked its error handling, but left the pre-existing
"tool calling fallback" block in index.test.ts asserting the old behavior,
breaking CI on canary:

- result is now the parsed object, not [{ name, arguments }]
- the no-tool-call path returns undefined via debug log without console.error
- the parse-failure path logs the single matched tool call, not the array

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(model-runtime): surface missing tool call in generateObject fallback as error

tool_choice forces the structured-output function, so a response without a
tool call means the provider misbehaved. #15680 routed this branch to a
debug-namespace log that is invisible in production, leaving callers with
an unexplained undefined. Log it via console.error with the response
message as context, matching the parse-failure branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 20:40:12 +08:00
LiJian 1130f7df32 feat(devices): add browser device pairing flow (#15678)
*  feat: add browser device pairing flow to /settings/devices

- Add "Via Browser" tab to ConnectDeviceModal with pairing code display and input
- Add "Register this browser as a device" callout card above DeviceList
- Support ?pair=<code> URL param to auto-open browser pairing modal with pre-filled code
- Improve DeviceList empty state with method cards (Desktop + CLI)
- Ship en-US and zh-CN i18n keys for all new browser/sync strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 fix(devices): fix lint warnings — import sort order and empty catch block

* fix(devices): add pair API route and invalidate device list cache

- Create /api/devices/pair POST handler that authenticates the user via
  Better Auth session, validates the code against the user's registered
  devices via DeviceModel.findByDeviceId, and returns JSON.
- Replace the setListKey/key-prop re-mount trick with
  lambdaQuery.useUtils().device.listDevices.invalidate() so the tRPC
  React Query cache is properly busted after a successful pair (fixes
  staleTime: 30s preventing the new device from appearing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(devices): drop browser pairing, fix modal close, redesign UI

- Remove the "Via Browser" pairing flow entirely: browser tab in
  ConnectDeviceModal, the "register this browser" callout card, the
  ?pair=<code> deep-link, and the /api/devices/pair stub route. Only the
  real Desktop and CLI connection methods remain.
- Fix the modal that couldn't be closed: @lobehub/ui Modal closes via
  onCancel (antd), not onClose — the X button was a no-op.
- Redesign the connect modal (segmented tabs, numbered steps, command
  blocks with copy, security footer) and the empty state (onboarding
  hero with Desktop/CLI options + capability cards).
- Clean up browser/sync i18n keys; add capabilities + footer keys for
  en-US and zh-CN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(devices): apply card radius — cssVar.borderRadius already has unit

The radius tokens (cssVar.borderRadius / borderRadiusLG) already include
their unit, so the trailing `px` produced `var(--…)px`, which browsers
drop — leaving the cards with sharp corners. Drop the `px` so the cards
pick up the same rounded radius as the appearance settings FormGroup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-11 19:50:28 +08:00
Arvin Xu e20496e444 🐛 fix(codex): persist model metadata and file diffs (#15672)
* 🐛 fix(codex): persist model metadata

* 🐛 fix(codex): show file change diffs
2026-06-11 19:15:45 +08:00
Innei dbc8d76c8d feat(desktop): restore cloud desktop builds (#15666) 2026-06-11 19:14:26 +08:00
renovate[bot] ecfdac5395 Update dependency @opentelemetry/sdk-node to ^0.217.0 [SECURITY] (#14687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-11 18:51:20 +08:00
YuTengjing 5f4bec347b 🐛 fix(model-runtime): improve DeepSeek structured output (#15680) 2026-06-11 16:57:57 +08:00
Arvin Xu 77e4d0492b ♻️ refactor(agent): resolve device routing via a single execution plan (#15669)
- add resolveExecutionPlan as THE device decision (none/sandbox never
  route to a device; offline bindings stay unrouted; single-online-device
  auto-activation only for device-capable targets)
- fix executionTarget=none being bypassed by single-device auto-activation
  (background runs executed device tools despite 无设备)
- stop exposing the remote-device proxy in none/sandbox sessions
- converge native execAgent, hetero dispatch fork and client
  selectRuntimeType onto the shared resolution
- drop the legacy per-platform chatConfig.runtimeEnv.runtimeMode fallback
  entirely (no migration: unset targets resolve to platform defaults)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:29:37 +08:00
Neko a60d11df48 🐛 fix(chat): preserve message order after tool results (#15657) 2026-06-11 16:18:18 +08:00
YuTengjing 14501ea69a 🐛 fix: keep model guard in provider grouping (#15681) 2026-06-11 15:35:15 +08:00
Arvin Xu b76992e581 feat(file-preview): support remote read-only local previews (#15673)
*  feat(file-preview): support remote read-only local previews

*  feat(local-file): identify tabs by context

* ♻️ refactor(file-preview): route previews through project file service

* 🐛 fix(desktop): clamp nav panel width

*  feat(file-preview): improve local preview controls

* 🐛 fix(file-preview): reload html after refresh completes
2026-06-11 15:10:25 +08:00
Arvin Xu 97e4e345d1 🔨 chore(codecov): update coverage grouping (#15650)
🔨 chore: update codecov coverage grouping
2026-06-11 14:40:06 +08:00
cokeSEE1 c609a60f0e 🔨 chore(ci): bump outdated action versions to latest (#15655)
- actions/checkout@v4 -> @v6 in issue-auto-comments.yml
  (last remaining @v4 usage; all other 48 uses are already @v6)
- actions/github-script@v7 -> @v8 in release-desktop-canary.yml
  (last remaining @v7 usage; all other 4 uses are already @v8)

Co-authored-by: 章岚 <zhanglan@datagrand.com>
2026-06-11 09:54:53 +08:00
renovate[bot] 06bf82f3e0 Update dependency node to v24.16.0 (#14621) 2026-06-11 09:24:21 +08:00
Zhijie He 3ccc23152c 💄 style: add sensenova-6.7-flash-lite & sensenova-u1-fastsupport (#15306) 2026-06-11 09:22:49 +08:00
Zhijie He 3a780a62f6 feat: add AntGroup (蚂蚁百灵) provider support (#13713) 2026-06-11 09:21:54 +08:00
Zhijie He e98ad7edca 💄 style: update models for Longcat, support api fetch model list (#15134) 2026-06-11 09:20:55 +08:00
Arvin Xu 686778fe51 feat(file-preview): render HTML files inline (#15671)
 feat(file-preview): render html files inline
2026-06-11 02:39:05 +08:00
Arvin Xu 914976a52f feat(model-bank): knowledgeCutoff batch 2, metadata skill & always-visible tab bar (#15663)
*  feat(model-bank): backfill knowledgeCutoff batch 2 and restore lost Anthropic values

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 📝 docs(skills): add model-bank-metadata skill for cutoff/family backfill

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(model-bank): Claude Fable 5 belongs to the claude-mythos family

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 💄 style(desktop): always surface the tab bar by creating a tab on first navigation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ♻️ refactor(model-bank): family is the product lineage (claude-opus/sonnet/haiku), not the brand

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(agent): backfill activeAgentId before paint on tab/route switches

Tab switches are plain route navigations, so leaving an agent page cleared
activeAgentId via a passive useUnmount and the next page re-set it in a
passive useEffect — the first painted frame always had no active id, flashing
a skeleton even when agentMap already cached the config. Move both the
backfill and the unmount clear to layout effects: removed-tree layout
cleanups run before new-tree layout effects in one commit, so the clear can
never wipe a freshly synced id and the id is in place before paint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

*  feat(agent): surface agent config fetch errors with a retry action

isAgentConfigLoading only knows "no data yet", so a failed fetch (e.g. a 401
that SWR deliberately does not retry, with no focus revalidation inside a
single Electron window) left the agent page on a skeleton forever — only a
manual reload recovered. Record per-agent fetch errors in
agentConfigErrorMap (set by onError, cleared on data / retry), expose
currentAgentConfigError / isAgentConfigError selectors, add a
retryAgentConfigFetch action that revalidates the agent's SWR entries, and
show an error alert with a retry button above the main chat input while the
config is still missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(ci): sync model metadata test expectations

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 01:29:17 +08:00
Arvin Xu fdd955404d feat(codex): add collab tool render (#15662)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 01:15:29 +08:00
LiJian 6d47c1d07e feat(connector): fold OAuth into the custom MCP (PluginDevModal) form (#15661)
*  feat(connector): support API key / custom header / OAuth auth in custom connector

Make the connector backend a full replacement for the legacy custom-MCP plugin form:

- connector create/update now accept bearer/apikey/header credentials (encrypted at rest);
  oauth2 stays callback-only
- map apikey → bearer auth and header → request headers in both the sync path
  (syncTools + callTool) and the agent-runtime manifest path
- pass custom HTTP headers through to the MCP client
- AddConnectorModal becomes a rich form: MCP type (HTTP/STDIO), auth type
  (None / API Key / Custom Headers / OAuth), reusing the plugin form inputs;
  OAuth keeps the existing popup authorize flow, others create + sync directly

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(connector): fold OAuth into the PluginDevModal MCP form

Pivot the custom-MCP entry to reuse the rich PluginDevModal / MCPManifestForm
instead of a bespoke connector modal, and add OAuth as an auth type inside it:

- MCPManifestForm: gated `enableOAuth` adds an "OAuth" auth type with
  Client ID / Secret (optional) + redirect-URI hint. Only the custom-connector
  entry enables it, so plain custom-plugin DevModal callers (editing plugins,
  agent tools, …) are unaffected.
- DevModal: opens the OAuth popup synchronously on the save click (browsers
  block window.open once an async boundary is crossed), validates, then hands
  the popup to onSave which navigates it to the authorize URL.
- New CustomConnectorModal wraps DevModal and persists every auth type onto the
  connector backend (none / bearer / custom headers → create + sync; OAuth →
  create with OIDC config + run the authorize popup).
- settings/skill entry now opens CustomConnectorModal; the standalone
  AddConnectorModal rich rewrite from the previous commit is reverted to the
  canary original (it is only referenced by the unused ConnectorList).
- i18n: dev.mcp.auth.oauth* keys (default + en-US + zh-CN).

Backend stays as in the prior commit (connector create/update accept
bearer/apikey/header credentials; sync + manifest paths apply them).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(connector): route the OAuth auth type through the authorize flow, not the token-less manifest test

Selecting OAuth and clicking "Test connection" called the plugin manifest test
(getStreamableMcpServerManifest), which connects with no token and 401s on any
OAuth-gated server (e.g. Linear MCP / DCR). For OAuth there is nothing to test
without authorizing first, so the button now becomes "Authorize & Connect" and
runs the connector OAuth flow (discovery + DCR + authorize popup), shared with
the footer save button via DevModal.runOAuthFlow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(connector): make connector.create idempotent on (user, identifier)

Re-adding or re-authorizing a custom connector with an existing identifier hit
the user_connectors unique constraint and 500'd. Now an existing row is updated
(reset to disconnected, refreshed name/url/oidcConfig/credentials) and its id
reused, instead of inserting a duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(skill-store): route Add Custom MCP through the connector modal, drop the Custom tab

- Skill Store "Add → Add Custom MCP Skill" now opens CustomConnectorModal
  (connector backend + OAuth), matching the settings/skill entry, instead of
  the legacy plugin DevModal (installCustomPlugin + togglePlugin).
- Remove the now-redundant "Custom" tab from the Skill Store (custom MCP lives
  in the connector list now): drop SkillStoreTab.Custom, its tab option,
  CustomList render, and the matching search branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 01:00:38 +08:00
renovate[bot] c65cf8c2a0 Update dependency @opentelemetry/auto-instrumentations-node to ^0.76.0 [SECURITY] (#14686)
Update dependency @opentelemetry/auto-instrumentations-node to ^0.75.0 [SECURITY]

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-11 00:09:31 +08:00
Arvin Xu 981c57d6f9 🐛 fix(codex): scope repeated tool results (#15659)
* 🐛 fix(codex): scope repeated tool results

* 💄 style(codex): refine local file link states
2026-06-10 23:22:56 +08:00
Arvin Xu 87eba86514 chore(model-bank): backfill knowledgeCutoff + family/generation data (#15642)
*  feat(model-bank): backfill knowledgeCutoff for OpenAI/Claude/Llama/Phi families (batch 1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

*  feat(model-bank): add family/generation fields with rule-derived data for chat models

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

*  feat(model-bank): add canonical knowledge-cutoff map with build-time backfill

Adds MODEL_KNOWLEDGE_CUTOFFS (canonical id → YYYY-MM, all values verified
against official provider docs) plus normalizeModelIdForCutoff, which reduces
provider-specific spellings (openrouter/bedrock prefixes, dated snapshots,
-thinking/-fast/-latest/-preview variants, claude dot-versions) to canonical
ids. buildDefaultModelList backfills knowledgeCutoff from the map when a model
card has no inline value, so all aggregator providers inherit cutoffs
automatically; inline values always win.

Covers Anthropic (incl. legacy 3.x), OpenAI, Google Gemini/Gemma, xAI Grok,
Meta Llama, Amazon Nova, and Cohere. DeepSeek/Qwen/GLM/Kimi/MiniMax/Mistral
publish no official cutoffs and are intentionally absent. Anthropic inline
PoC entries migrate into the map (single source of truth).

Cross-checked against the batch-1 inline backfill: 0 value mismatches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(model-bank): correct Claude Sonnet 4.6 cutoff

*  test(model-bank): sync metadata expectations

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:59:36 +08:00
Rdmclin2 09e6f02e45 🔨 chore: modify workspace sidebar (#15658)
* chore: change back to user style sidebar panel

* chore: optimize personal menu

* chore: update i18n files
2026-06-10 22:21:27 +08:00
Arvin Xu a2ea314cd8 feat(codex): refine Codex tool renders (#15651)
* 💄 style(codex): refine file change tool render

*  feat(codex): add web search tool render

*  feat(codex): add mcp tool render

*  feat(codex): improve tool command display

* 💄 style(files): refine explorer tree icons

*  test: fix local file link render props
2026-06-10 22:13:56 +08:00
Arvin Xu e2be720726 🐛 fix(agent-runtime): keep async sub-agent stream alive (#15646)
* 🐛 fix: keep async sub-agent stream alive

* 🐛 fix: preserve async tool resume parent chain
2026-06-10 22:12:22 +08:00
Arvin Xu 8b6905ec7e 💄 style(desktop): tighten tab close button right padding (#15636)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:12:02 +08:00
Arvin Xu e4830943cf 🔨 chore(model-bank): add knowledgeCutoff field to model cards (#15640)
*  feat(model-bank): add knowledgeCutoff field with Anthropic models as PoC

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

*  feat(model-bank): add family/generation fields to model card types

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:02:34 +08:00
Arvin Xu 5dfb6fc288 chore: clean [LOBE-XXX] code annotations (2026-06-10) (#15623)
chore: clean up [LOBE-XXX] code annotations (2026-06-10)

Remove LOBE-XXX markers from comments and URLs across 7 files:
- apps/cli/hetero.ts & hetero.test.ts: Remove LOBE-10157 markers, keep context
- apps/server/ModelRuntime: Remove LOBE-10056, keep PK migration note
- packages/database/rbac.ts: Remove LOBE-9193, keep API doc
- scripts/codemodWorkspaceNav.ts: Remove LOBE-9024 from description
- parse.ts & parse.test.ts: Replace LOBE-10141/LOBE-123 with generic IDs

Co-authored-by: lobehub-bot <lobehub-bot@users.noreply.github.com>
2026-06-10 19:59:54 +08:00
Arvin Xu 94ea3f6a34 🚀 release: 20260610 (#15647)
# 🚀 LobeHub Release (20260610)

**Release Date:** June 10, 2026  
**Since v2.2.2:** 131 merged PRs · 13 contributors

> This weekly release strengthens agent collaboration across cloud,
desktop, CLI, and workspace flows, with steadier runtime behavior and a
broader foundation for workspace-scoped data.

---

##  Highlights

- **Agent execution across devices** — Unifies per-device working
directories, project skill discovery, and sub-agent suspend/resume
behavior across server, QStash, and device RPC flows. (#15543, #15566,
#15481, #15620, #15591)
- **Connector and sandbox platform** — Expands connector permissions,
custom OAuth MCP connector onboarding, sandbox provider support, and
user-uploaded file sync into cloud sandbox runs. (#15463, #15546,
#15184, #15550)
- **Desktop and CLI reliability** — Fixes desktop cold-start,
auto-update, Windows build, CLI skill discovery, and `lh connect` agent
dispatch paths. (#15547, #15525, #15527, #15562, #15632, #15634)
- **Pages and sharing** — Refreshes topic sharing, improves Page Editor
layout behavior, and routes Page Agent tool execution through the
server-side editor path. (#15581, #15556, #15588, #15023, #15610)
- **Model availability and provider updates** — Adds user-scoped LobeHub
model availability, Claude Fable 5, Qwen thinking preservation, and
MiniMax M3 updates. (#15590, #15639, #13494, #15376)

---

## 🏗️ Core Product & Architecture

### Agent Runtime & Heterogeneous Agents

- Improves sub-agent lifecycle handling, including async suspend/resume,
queue-mode QStash resume delivery, and blocking nested sub-agent calls.
(#15481, #15620, #15575)
- Stabilizes heterogeneous agent ingestion and streaming with raw stream
dumps, per-turn usage, image forwarding on regenerate, and
duplicate-text fixes. (#15602, #15577, #15592, #15585)
- Adds execution-device and working-directory controls across device
RPC, legacy defaults, and remote-spawned Claude Code sessions. (#15543,
#15566, #15591, #15572)
- Improves runtime diagnostics and compatibility, including Gemini
multimodal output capture, abort stream semantics, and trace quality
analysis. (#15535, #13677, #15508)

---

## 📱 Platforms, Integrations & UX

### Connectors, Sandbox & Tools

- Ships API-level connector tool permissions, custom OAuth MCP connector
onboarding, and connector-first runtime execution. (#15463, #15546)
- Adds sandbox provider support, cloud sandbox file sync, and safer
external URL file input handling with SSRF validation. (#15184, #15550,
#12657)
- Improves tool visibility and execution with pinned app-fixed tools,
ANSI output rendering, gateway-tunneled MCP calls, and automatic
headless tool runs. (#15509, #15516, #15469, #15492)

### Desktop, CLI & Web UX

- Restores desktop startup and reload behavior, preserves IPC error
causes, and keeps the tab bar new-tab action visible across routes.
(#15547, #15597, #15638)
- Fixes desktop update and build stability for browser quit guards,
macOS update signing, and Windows Visual Studio detection. (#15525,
#15527, #15562)
- Shows the plan-limit upgrade UI on desktop builds. (#15628)
- Adds the Agent Run delivery checker and fixes CLI device dispatch plus
skill list/search output. (#15489, #15634, #15632)
- Refreshes onboarding, auth source preservation, topic UI states,
referral/Fable campaign copy, and chat-input control bar behavior.
(#15629, #15544, #15573, #15614, #15616, #15617, #15622, #15643)

---

## 🔒 Security, Reliability & Rollout Notes

- External URL file input now includes SSRF validation for safer Google
file handling. (#12657)
- Database workspace-scope migrations are part of this release;
self-hosted operators should run the normal migration path before
serving the updated app. (#15446, #15465, #15468, #15472)
- The release branch was re-cut from `canary` and includes the latest
`main` release-version commit so `v2.2.2` is the verified compare base.

---

## 👥 Contributors

@ONLY-yours, @sxjeru, @hardy-one, @xujingli, @hezhijie0327, @Coooolfan,
@arvinxx, @tjx666, @Innei, @rivertwilight, @rdmclin2, @cy948,
@AmAzing129

**Full Changelog**:
https://github.com/lobehub/lobehub/compare/v2.2.2...release/weekly-20260610-recut-3
2026-06-10 19:35:47 +08:00
YuTengjing b8339abc76 🐛 fix: show plan limit upgrade UI on desktop builds (#15628) 2026-06-10 18:19:25 +08:00
Innei c037609b8b 💄 style(chat-input): fix control bar height jump when TokenTag appears (#15643) 2026-06-10 17:43:13 +08:00
René Wang b8b37cffa3 feat: refresh topic sharing experience (share page + popover) (#15581) 2026-06-10 17:43:02 +08:00
Rdmclin2 e8e4b2e822 feat: support workspace lobehub (#13977)
feat: support workspace (full) — store→business-hook + workspace router
2026-06-10 17:34:12 +08:00
Arvin Xu c02e5720c2 feat(model-bank): add claude-fable-5 to Anthropic models (#15639)
*  feat(model-bank): add claude-fable-5 to Anthropic models

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(agent): allow adding directory topics on web when agent targets a bound device

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:57:57 +08:00
Arvin Xu 3fb732da66 💄 style(desktop): keep tab bar new-tab button visible on every route (#15638)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:01:38 +08:00
Arvin Xu fdb529d598 🐛 fix(agent): deliver sub-agent resume bridge via QStash webhook in queue mode (#15620)
* 🐛 fix(agent): deliver sub-agent resume bridge via QStash webhook in queue mode

The callSubAgent completion bridge was a handler-only hook, which lives in
process memory: in queue mode (AGENT_RUNTIME_MODE=queue) HookDispatcher only
delivers webhook-configured hooks, so the bridge never fired — the parent op
stayed parked in waiting_for_async_tool forever after all sub-agents finished.

- Give the bridge hook a webhook config (delivery: qstash) targeting the new
  /api/agent/webhooks/subagent-callback endpoint; local mode keeps the
  in-process handler. Both paths converge on
  AgentRuntimeService.completeSubAgentBridge (backfill + barrier/CAS resume).
- Park-time self-check: after the parked state and operation row are
  persisted, re-run the resume barrier once to recover children that
  completed before the parent finished parking.
- One-shot verify watchdog: when a completion finds the parent not yet
  resumable, schedule a delayed verifyAsyncToolBarrier re-check (no step
  lock, CAS-idempotent, never re-arms).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 📝 docs(agent): correct verify-watchdog rationale comment

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 📝 docs(agent): clarify eventFields trimming rationale

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* ♻️ refactor(agent): align subagent-callback with workspace-scoped step worker

Post-rebase adaptation to canary's runtime restructure (#15609):

- Route the webhook bridge through AiAgentService (like the /run step
  worker) so the runtime's models stay workspace-scoped — a bare
  AgentRuntimeService would be personal-scoped and the tool-message
  backfill / resume barrier could miss workspace-scoped rows.
- Extract SubAgentBridgeParams into agentRuntime/types and add the
  completeSubAgentBridge passthrough next to executeStep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* 🐛 fix(agent): fail sub-agent callback loudly on backfill or delivery failure

Address two review findings on the resume bridge:

- completeSubAgentBridge now checks updateToolMessage's { success } result
  (it swallows transaction errors instead of throwing) and propagates all
  infrastructure failures. The webhook endpoint then returns non-2xx so
  QStash redelivers the whole bridge — previously a failed backfill was
  acked with 200 and the parent stayed parked forever, since the verify
  recheck only re-reads the barrier and cannot retry the backfill.
- New AgentHookWebhook.fallback: 'none' opts a qstash-delivered hook out of
  the unsigned plain-fetch fallback, which can never authenticate against a
  QStash-signed endpoint and only masked publish failures as silently
  dropped 401s. The bridge hook uses it; dispatch escalates such delivery
  failures to console.error instead of the debug namespace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:00:17 +08:00
Arvin Xu 4c5c8795ef 🐛 fix(model-runtime): emit stop:abort instead of error when stream is aborted (#13677)
* 🐛 fix(model-runtime): emit stop:abort instead of error when stream request is aborted

When user cancels a streaming request, the provider SDK throws abort errors
(e.g. "Request was aborted"). Previously these were propagated as error chunks,
causing the client to display a provider error message. Now abort errors emit
a stop:abort event through the SSE pipeline, allowing the client to handle
cancellation gracefully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): fix type error in abort pipeline test

Use `as const` for type literal to satisfy StreamProtocolChunk union type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(fetch-sse): add planUpgradeAfterFinish to onFinish expectations

#15616 added planUpgradeAfterFinish to the onFinish context but missed
updating fetchSSE.test.ts, breaking 13 tests on canary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(model-runtime): harden abort detection against non-Error throws

isAbortError assumed error.message is always a string, but catch
clauses receive unknown — a non-Error throw (string, object without
message) would make the abort check itself throw inside the stream
error handler, swallowing both ABORT_CHUNK and the first-chunk error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-10 15:56:39 +08:00
YuTengjing 8b342c600f feat: land new signups directly on onboarding (#15629) 2026-06-10 15:31:32 +08:00
LiJian 723c4d6daa 🐛 fix(cli): handle agent_run_request in lh connect so device dispatch doesn't time out (#15634)
* 🐛 fix(cli): handle agent_run_request in `lh connect` so device dispatch doesn't time out

`lh connect` auto-registers the CLI as a device, so the gateway can pick it
as the dispatch target for a heterogeneous agent run (`agent_run_request`).
But the connect daemon only listened for `system_info_request` and
`tool_call_request` — it never handled `agent_run_request`, so it never sent
`agent_run_ack`. The gateway waited out its ack window and returned
`{error:'TIMEOUT',success:false}`, surfaced server-side as "Hetero agent
device dispatch failed".

Add an `agent_run_request` handler mirroring the desktop app: spawn
`lh hetero exec` fire-and-forget and ack `accepted` immediately. The spawned
process owns the full execution + server-ingest pipeline. It re-invokes the
current CLI entry (process.execPath + argv[1]) rather than relying on `lh`
being on PATH, so it works inside the detached daemon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: bump the cli version

* chore: bump the cli manifest

* 🐛 fix(cli): ack agent run only after spawn succeeds, reject on spawn error

`child_process.spawn` reports a missing/inaccessible cwd asynchronously via
the child's `error` event, after the handler had already sent an `accepted`
ack. The gateway/server then recorded dispatch success while no `lh hetero
exec` process existed to emit `heteroFinish`, leaving the assistant message
stuck instead of surfacing a failure.

`spawnHeteroAgentRun` now resolves on the child's outcome: `accepted` on the
`spawn` event (stdin is written only then), `rejected` on an early `error`. A
rejected ack returns the gateway 422 → execAgent writes a ServerAgentRuntimeError
onto the assistant message, so a failed dispatch is visible. Still resolves in
milliseconds, well within the gateway's 10s ack window.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 15:19:01 +08:00
LiJian 5b02563659 🐛 fix(cli): skill list/search commands returning empty results (#15632)
🐛 fix: skill list/search commands returning empty results

tRPC endpoints return { data, total } but CLI was treating the result as
an array; switch to result?.data ?? [] and update mocks to match.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 14:20:35 +08:00
YuTengjing a5f16c1184 🐛 fix: import button from ui root (#15599) 2026-06-10 14:19:04 +08:00
YuTengjing 7641cda958 💄 style: update i18n locales (#15630) 2026-06-10 14:02:02 +08:00
Arvin Xu 9ef76475c2 💄 style: add fable promo locale keys for plans page (#15622) 2026-06-10 07:59:15 +08:00
YuTengjing 1ed93b6a24 🐛 fix: type fable starter config (#15618) 2026-06-10 06:05:49 +08:00
Arvin Xu 004027ffdd 💄 style: update free credit badge copy and add cta/dismiss keys (#15617)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 06:05:28 +08:00
Arvin Xu 0434953053 chore: add home free credit badge business slot (#15615)
 feat: add home free credit badge business slot

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 05:47:37 +08:00
YuTengjing 4b7ef28e46 🐛 fix: support fable campaign UI (#15616) 2026-06-10 05:46:31 +08:00
Arvin Xu 437b4c8968 💄 style: update referral copy for pay-to-unlock reward (#15614)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 05:14:49 +08:00
Arvin Xu fdb4f37053 ♻️ refactor(hetero-agent): shared subagent-run coordinator + fix device-mode subagent streaming (#15613)
* ♻️ refactor(hetero-agent): shared subagent-run coordinator + fix device-mode subagent streaming

Remote-device (gateway) hetero runs corrupted SubAgent text on the wire: the
CLI `SerialServerIngester`'s main-agent text-snapshot coalescing was subagent-
unaware, so subagent full-block text got mixed into the main accumulator and
re-`append`ed as `replace` snapshots server-side. Fix: exclude `data.subagent`
text from the coalescer so it forwards raw (the server appends it once).

The deeper cause was duplication: the renderer executor and the server
persistence handler each hand-wrote the SAME subagent-run state machine (lazy
thread create, turn-boundary cut, finalize, orphan drain, chain parenting) —
the epicenter of past hetero subagent bugs. Extract it into ONE pure,
transactional reducer (`reduceSubagentRuns`) in `@lobechat/heterogeneous-agents`
that emits declarative intents; each engine keeps a thin interpreter for its
own I/O (renderer: messageService + live store dispatch; server: messageModel).

The reducer pre-allocates ids so intents carry parentId chains with no
create→backfill round-trip; this needs `messageService.createMessage` to accept
a caller id (threaded through; the model already supported it). Also widened the
message nanoid 14→18 for the higher per-run id volume.

Behavior unifications (vs the two old copies):
- transactional commit-on-success subsumes the renderer's `pendingFlushTarget`
  (a failed flush leaves the run intact for the onComplete-drain retry; the
  renderer keeps a local pending-flush map pinned to the original assistant).
- finalize DELETES the run (server-style); a second finalize / orphan drain is
  a clean no-op with the same DB end-state.

Scoped to subagent runs only; main-agent persistence stays per-engine. A future
pass can absorb the main-agent path into a unified agent-event reducer.

Tests: reducer 13, CLI hetero 22, server hetero 84, renderer executor 58.

Refs: LOBE-10175

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(hetero-agent): strengthen subagent flush-retry assertion

The earlier rewrite of this assertion (caused by ids moving from server-
generated to caller-pre-allocated) weakened it to "all streamed writes share
one id", which would also pass if they all wrongly hit the terminal row. Pin it
back to the test's real intent: resolve the FIRST streaming-turn assistant by
its create payload and assert every streamed write targets it AND that it
differs from the terminal assistant's id — so `resultContent` is never clobbered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(hetero-agent): honor commit-on-success for renderer subagent intents + fix stale id-length tests

- renderer interpreter: createThread / createMessage failures now rethrow so
  reduceAndApplySubagent skips the state commit — the next event retries the
  lazy create / turn boundary instead of orphaning the run (review P2)
- catch around the intent loop so a failed intent can't poison persistQueue
- regression test: transient createThread failure retries on next event
- update message id length assertions 18 → 22 (nanoid widened 14→18 + msg_)
- update messageService.createMessage spy assertions for the new (params, id) call

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 05:09:43 +08:00
Arvin Xu 1260756246 feat(agent): block nested sub-agent calls (#15575)
*  feat(agent): block nested sub-agent calls

Sub-agents must not recursively spawn further sub-agents. Plumb an
`isSubAgent` flag from the spawning thread through the conversation /
operation / tool-call metadata, and refuse nested dispatch at every layer:

- streamingExecutor marks the spawned sub-agent context with `isSubAgent`
- aiAgent strips the LobeAgent tool from a sub-agent's plugin config
- client builtin-tool executor + server tool runtime return a clear error
- RuntimeExecutors blocks both single and batch sub-agent dispatch

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(test): align execSubAgentTask expectation with isSubAgent appContext

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent): don't mark group sub-agent tasks as isSubAgent

Group sub-agents are real agent dispatches and must keep the ability to
spawn their own sub-agents; only the LobeAgent-tool virtual sub-agent
path should carry isSubAgent. Drop the flag from execSubAgentTask.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 04:00:23 +08:00
YuTengjing cb769534d3 ♻️ refactor: parse Claude model ids for runtime checks (#15601) 2026-06-10 02:55:34 +08:00
Arvin Xu de1a5c88e4 test(database): cover more model/repository gaps (client-db 95.4%→95.7%) (#15612)
Extend tests toward full coverage of PGlite-reachable code:
- agentEval/runTopic (batchMarkAborted, deleteByRunAndTestCase) → 100%
- agentEval/run (benchmarkId filter branch) → 100%
- verifyCheckResult (createMany empty, findById, update, backfillTracingId) → 100%
- asyncTask, document, systemBotProvider, dataImporter — additional branches

Remaining client-db gaps are BM25/pg_search paths (run only in server-db/CI)
and real-Postgres-error / defensive fallbacks not reachable under PGlite.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 02:48:58 +08:00
Arvin Xu 5b4b50e050 🐛 fix(page-agent): inject active documentId into context on send (#15610)
* 🐛 fix(page-agent): inject active documentId into context on send

Page-scoped conversations never carried the open document id to the
agent runtime. At send time `operationContext` only had agentId/scope/
topicId, so the gateway's `appContext.documentId` was undefined and the
server-side PageAgent runtime threw "received a tool call without
documentId in context".

Inject the live document id from the page editor runtime
(`pageAgentRuntime.getCurrentDocId()`) into `operationContext` when
scope is `page`, so it flows through `execAgentTask` → server
`state.metadata.documentId` → tool execution context.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(page-agent): pass new document id explicitly in sendAsWrite to avoid stale injection

The page-scoped documentId fallback reads the page editor runtime
singleton, which is only authoritative once the active page's editor has
mounted. `sendAsWrite` creates a document, navigates, and sends
immediately — before the new editor mounts — so the singleton may still
be bound to the previously open page, scoping server-side PageAgent
tools to the wrong document.

Thread the freshly created `newDoc.id` through the conversation context;
the existing `!context.documentId` guard then skips the singleton
fallback entirely. Document the constraint at the fallback site.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 02:30:33 +08:00
YuTengjing 1d619ad507 feat: add user-scoped LobeHub model availability (#15590) 2026-06-10 02:19:14 +08:00
Arvin Xu 3ce3b5388f test(database): raise model/repository coverage to 95%+ and document DB test conventions (#15611)
*  test(database): raise model/repository coverage to 95%+ and document DB test conventions

Raise @lobechat/database client-db coverage 89.11% -> 95.36%:
- New integration tests for connector, connectorTool, workspaceMember (were 0%)
- Extend task, workspace, rbac, notification, userMemory/query, file,
  agentSignal/reviewContext, verifyRubric, brief, taskTopic, dataImporter,
  messengerAccountLink, home

Fix client-db (PGlite) test failures: BM25 search lacks the pg_search
extension under PGlite, so wrap session.queryByKeyword and home.searchAgents
in describe.skipIf(!isServerDB), matching the existing convention.

Document DB model/repository testing conventions so new models ship with tests:
- Rewrite testing skill's db-model-test.md (getTestDB integration pattern,
  client-vs-server-db split, BM25 skipIf guard, schema gotchas, user isolation)
- Surface the rule in testing/SKILL.md, cross-link from drizzle/SKILL.md,
  review-checklist/SKILL.md, and models/_template.ts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(database): make verifyRubric/brief ordering tests deterministic

These models order by `updatedAt`/`createdAt` desc with no id tiebreaker, and
the tests created rows back-to-back relying on default `now()` — when two rows
land in the same millisecond the order is non-deterministic, causing flaky CI
failures. Set explicit, well-separated timestamps instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 01:42:08 +08:00
Innei 991c2f79e8 🐛 fix(desktop): trace Session Expired cause and resume onboarding at Login (#15604)
- Carry a `reason` payload on the `authorizationRequired` IPC event so the
  cause behind the Session Expired modal (proxy 401, refresh non-retryable,
  startup proactive refresh exception, etc.) lands in `electron-log` and the
  renderer debug namespace for postmortem.
- On 401 + `X-Auth-Required`, enrich the reason with `hadToken`, the upstream
  `www-authenticate` header and a truncated body snippet so OAuth/tRPC error
  details are captured without consuming the forwarded stream.
- Fix returning users (token refresh failed -> active=false -> relaunch)
  landing on the Welcome screen of desktop onboarding. Persist an
  `everCompleted` flag in localStorage and resume at the Login screen for
  anyone who has already completed onboarding once.
- Extract the screen-resolution logic into a pure `resolveInitialScreen`
  helper with unit tests; cover the new storage flag and reason payload in
  AuthCtr / BackendProxy tests.
2026-06-10 01:06:00 +08:00
Arvin Xu c329696dc2 🐛 fix(hetero): chain step boundary off tool row when tools[] backfill is unseen (#15607)
* 🐛 fix(hetero): chain step boundary off tool row when tools[] backfill is unseen

On a warm replica that did not drain the prior step's `tools_calling` (or
before the assistant's `tools[]` JSONB has its `result_msg_id` backfilled),
the in-memory tool state is empty, so the step boundary falls back to the
previous assistant and forks the wire into two disconnected bubbles.

Fall back to the authoritative anchor — the `role:'tool'` rows themselves,
committed in Phase 2 independently of the JSONB mirror's Phase-3 backfill —
via a new `MessageModel.getLastChildToolMessageId`. Excludes subagent tool
rows (threadId set) so they never anchor the main-agent wire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(hetero): write per-device cwd when adding topic from project group

The sidebar "+ new topic in this directory" action wrote the working
directory to the legacy per-agent slot (localAgentWorkingDirectoryMap),
which sits below agencyConfig.workingDirByDevice in the resolution
precedence. Once a directory had been picked via the ControlBar (which
writes workingDirByDevice), the "+" action was silently shadowed and the
new topic was created with the previously-picked directory instead.

Route the action through useCommitWorkingDirectory.commitAgentDefault so
it writes the same high-precedence per-device slot the picker uses,
keeping the two write paths from drifting again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(hetero): cover MessageModel.getLastChildToolMessageId

The fallback anchor query added in 599eea5bda had no DB-level test — the
persistence handler mocks it, so its real SQL was never exercised and
patch coverage dropped. Add direct PGlite tests covering all branches:
latest-tool ordering, no-tool → undefined (ignoring non-tool children),
subagent thread exclusion (threadId IS NULL), and ownership isolation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:42:37 +08:00
Arvin Xu 4b5e001934 🐛 fix(server): restore sub-agent forking in QStash step worker (#15609)
* 🐛 fix(server): restore sub-agent forking in QStash step worker

In QStash mode every agent step runs in a fresh HTTP request via the
hono `runStep` handler, which built a bare AgentRuntimeService without
the `execSubAgent` fork callback. As a result `lobe-agent.callSubAgent`
failed with SUB_AGENT_UNAVAILABLE in cloud (the in-process callback
never survives the queue boundary).

Step through AiAgentService.executeStep instead, reusing its internal
runtime that is already wired with the fork callback — no second runtime,
no manual rebinding.

Also rename the internal `execSubAgentTask` → `execSubAgent` (method,
runtime/tool context fields, options, ExecSubAgent{Params,Result} types)
to separate the "task" concept from "sub-agent", and make the method an
auto-bound arrow field so it no longer needs `.bind(this)`. The external
lambda procedure name (`execSubAgentTask`) and the client service are
left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(server): group runtime upward-calls into an AgentRuntimeDelegate

`execSubAgent` was a loose top-level option on AgentRuntimeService, which
hid that it is not ordinary config but an upward call: the low-level
runtime, mid-step, triggering a high-level pipeline that lives in
AiAgentService (the layer above it).

Introduce `AgentRuntimeDelegate` as the single named home for these
upward-call capabilities, and inject it as `delegate: { execSubAgent }`.
The interface doc states the convention so future "runtime must trigger a
higher-layer pipeline" capabilities land in the same place instead of
sprawling as ad-hoc options.

Scope is deliberately the injection surface (options + service field +
AiAgentService wiring). The downstream executor/tool context keeps its
flat `execSubAgent` field — the tool runner wants the unpacked capability,
not the whole delegate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 00:41:01 +08:00
Arvin Xu aa46864df6 ♻️ refactor(lobe-agent): remove callSubAgents in favor of parallel callSubAgent calls (#15608)
The lobe-agent manifest exposed `callSubAgents` (parallel multi-task
dispatch), but the server runtime only implemented `callSubAgent`. When an
agent run executed server-side and the model invoked `callSubAgents`, the
builtin executor threw "Builtin tool lobe-agent's callSubAgents is not
implemented".

The server already supports parallel sub-agents natively: a batch parks on
all deferred tools (`pendingToolsCalling`) and `tryResumeParentFromAsyncTool`
enforces a K=N barrier, resuming the parent only once every pending
tool_result is fulfilled. So emitting multiple `callSubAgent` calls in one
turn is equivalent to the old `callSubAgents` — making the plural API
redundant and the source of a server/client inconsistency.

Remove `callSubAgents` end to end (manifest, types, client executor,
Inspector/Render/Streaming components + registries, locale keys, display-name
map, dev fixture) and update the system prompt to guide the model to fan out
via multiple `callSubAgent` calls.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:59:20 +08:00
Arvin Xu af3f0ea171 🐛 fix(desktop): preserve Error cause across IPC so renderer sees real failure reason (#15597)
* 🐛 fix(desktop): preserve Error cause across IPC so renderer sees real failure reason

Electron's IPC error serialization carries an Error's message/stack/name plus
its enumerable own properties, but a standard `cause` (set via
`new Error(msg, { cause })`) is non-enumerable — so the real failure reason
(e.g. undici wrapping ENOTFOUND/ECONNREFUSED under a generic
`TypeError: fetch failed`) was dropped on the way to the renderer.

- IPC base: re-expose `cause` as an enumerable, clone-safe field in the central
  handler catch (nested Errors flattened to { name, message, code }) so every
  IPC method's error carries it.
- Heterogeneous agent executor: include `cause` in the ChatMessageError body so
  the surfaced error structure exposes the underlying reason alongside message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(desktop): ferry IPC error cause via a serializable envelope

Making `cause` enumerable before rethrowing didn't actually reach the renderer:
Electron's `ipcRenderer.invoke` rebuilds a thrown handler error from its *string*
form (`Error invoking remote method '<channel>': <String(error)>`), so the
original error object — and any `cause` — never crosses the boundary.

Switch to an explicit serializable envelope:
- `~common/ipcError`: `toIpcErrorEnvelope` (clone-safe plain object, recursively
  captures name/message/stack/code/cause) + `isIpcErrorEnvelope` /
  `fromIpcErrorEnvelope` to rebuild a real Error.
- IPC base handler: return the envelope instead of throwing.
- preload `invoke`: detect the envelope and re-throw a rebuilt Error (with
  `cause`), preserving the "promise rejects on failure" contract.
- hetero executor: flatten the Error cause to a plain object for the
  DB-persisted `ChatMessageError.body`.

Adds unit tests for the envelope round-trip and the preload unwrap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:58:50 +08:00
Arvin Xu 84a7b5c7c8 📝 docs(agents): replace device-gateway with server in apps tree (#15606) 2026-06-09 22:55:32 +08:00
Arvin Xu e01cadb779 feat(hetero): add --raw-dump to persist agent raw stream-json for debugging (#15602)
*  feat(hetero): add --raw-dump to persist agent raw stream-json for debugging

The remote-device path (`spawnLhHeteroExec`) leaves no local execution
record: `lh hetero exec` consumes the agent's stdout internally and only
POSTs adapted events to the server, so a misbehaving remote run can't be
inspected. The adapted/ingested view also can't distinguish a CC-side
empty `tool_result` from an adapter extraction bug.

Add `lh hetero exec --raw-dump <dir>`: spawnAgent gains an `onRawStdout`
tee that captures the child's untouched stdout BEFORE the adapter; the
CLI writes it (plus stderr + a meta.json) to
`<dir>/<timestamp>-<operationId>/`, one file pair per spawn attempt.
Fully best-effort — a dump failure never affects the run or exit code.

Wire the desktop device path to pass `--raw-dump` (gated by the existing
`shouldTraceCliOutput` toggle, into `resolveTraceRootDir`), so remote-device
CC runs now leave a raw stream on the device — the same toggle/location the
local trace path already uses. Reusable later for the server sandbox path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔖 chore(cli): bump version to 0.0.27

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:16:05 +08:00
Arvin Xu ce5833cb67 feat(file): persist image dimensions into file metadata (#15594)
*  feat(file): persist image dimensions into file metadata

Record intrinsic width/height for uploaded images so consumers can
reserve layout space (avoid CLS) without loading the file first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  test(file): assert persisted dimensions in upload createFile payload

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔖 chore(cli): bump version to 0.0.26 and regenerate man page

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(file): record image aspect ratio alongside width/height

Compute intrinsic aspect ratio (width / height, rounded) at extraction
time and persist it into file metadata so consumers can group/reserve
layout by orientation without recomputing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 22:11:15 +08:00
Arvin Xu 5b534f45d1 ♻️ refactor(chat-input): rename RuntimeConfig→ControlBar, WorkingDirectoryBar→HeteroControlBar (#15545)
* ♻️ refactor(chat-input): rename RuntimeConfig to ControlBar

The bar below the chat input now composes mode switcher, execution
device + working directory, approval mode and context window — "runtime
config" no longer matches. Rename the directory, component, and the
showRuntimeConfig / runtimeConfigSlot props (→ showControlBar /
controlBarSlot) across all call sites. Reads as a sibling of ActionBar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): rename WorkingDirectoryBar to HeteroControlBar

Make the heterogeneous chat-input bar a symmetric sibling of ControlBar:
both compose the shared WorkspaceControls, so naming should match. Rename
the file, component and displayName, and update the controlBarSlot usage.
2026-06-09 20:21:05 +08:00
Innei e692448346 🔨 chore(deps): pin @lobehub/editor to stable ^4.17.1 (#15600)
Switch from the pkg.pr.new preview snapshot back to the published 4.17.1 release.
2026-06-09 20:09:27 +08:00
Rylan Cai 3fe5b62cbe 🐛 fix: relax clear todo intervention (#15598)
🔒 Relax clear todo intervention
2026-06-09 19:55:20 +08:00
Arvin Xu 6c6c8698d3 🐛 fix(hetero): forward user images on regenerate so vision input isn't dropped (#15592)
* 🐛 fix(agent): resolve working directory by target device instead of legacy-only

The chat-input directory picker writes the selection to
`agencyConfig.workingDirByDevice[deviceId]`, but the send / regenerate /
streaming / placeholder paths resolved the agent working directory via
selectors that only read the legacy `localAgentWorkingDirectoryMap`. So a
freshly picked directory was silently dropped and execution fell back to a
default cwd (the app's own repo), losing the user's project and `--resume`.

Make both `getAgentWorkingDirectoryById` and `currentAgentWorkingDirectory`
device-aware: per-device choice > legacy > desktop/home, with the target
device resolved from a passed-in `currentDeviceId` (kept out of the selector
so hook callers stay reactive). Update all call sites to supply the device id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(hetero): forward user images on regenerate so vision input isn't dropped

The hetero regenerate/resend path (`runHeterogeneousFromExistingMessage`)
only forwarded the text prompt to `executeHeterogeneousAgent`, never the
original user message's `imageList`. The send path reads imageList off the
persisted user message and passes it along; this path must too. Without it,
regenerating an image turn re-ran the CLI with no attachments (fully lost
when the session couldn't be resumed, e.g. cwd changed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:50:55 +08:00
Arvin Xu cdbef3f72e 🐛 fix(agent): resolve working directory by target device instead of legacy-only (#15591)
The chat-input directory picker writes the selection to
`agencyConfig.workingDirByDevice[deviceId]`, but the send / regenerate /
streaming / placeholder paths resolved the agent working directory via
selectors that only read the legacy `localAgentWorkingDirectoryMap`. So a
freshly picked directory was silently dropped and execution fell back to a
default cwd (the app's own repo), losing the user's project and `--resume`.

Make both `getAgentWorkingDirectoryById` and `currentAgentWorkingDirectory`
device-aware: per-device choice > legacy > desktop/home, with the target
device resolved from a passed-in `currentDeviceId` (kept out of the selector
so hook callers stay reactive). Update all call sites to supply the device id.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:45:36 +08:00
YuTengjing 71030c6e21 ♻️ refactor(auth): remove email harmony plugin (#15589) 2026-06-09 19:18:56 +08:00
LiJian adf49db7c4 🐛 fix: activator tool discovery for cloud-sandbox and local-system (#15586)
* 🐛 fix: activator tool discovery for cloud-sandbox and local-system

- P0: Explicitly inject LocalSystemManifest when device gateway is configured
  (discoverable: isDesktop is always false on server, so it never enters
  the discovery loop. The explicit injection mirrors the canUseDevice guard.)

- P1: Skip CloudSandboxManifest when runtimeMode is not 'cloud'
  (resolveRuntimeMode unifies executionTarget='sandbox' and legacy
  chatConfig.runtimeEnv.runtimeMode paths, so agents with sandbox
  disabled correctly exclude the cloud-sandbox tool.)

Both fixes operate at the manifest-map build stage, consistently affecting
all downstream consumers (activator discovery, availableTools, etc.)

* 🐛 fix: remove cloud-sandbox manifest when runtime is not sandbox

The initial manifest seed via getEnabledPluginManifests includes
defaultToolIds (which contains lobe-cloud-sandbox), so the manifest
was already in toolManifestMap before the allowedBuiltinTools loop's
continue guard. This made lobe-cloud-sandbox activatable even when
sandbox was disabled.

Add a delete right after resolveRuntimeMode to cover both the
manifestMap seed and the allowedBuiltinTools loop in one place.

Co-authored-by: chatgpt-codex-connector[bot]

* 🐛 fix: gate local-system injection by runtimeMode === 'local'
2026-06-09 19:03:25 +08:00
Innei 69cefce3d9 🐛 fix(page-editor): align table bleed with controllers (#15588) 2026-06-09 19:02:47 +08:00
Arvin Xu b295265f25 🐛 fix(hetero): stop cross-message text duplication in server-ingest mode (#15585)
🐛 fix(hetero): reset per-message text accumulator at message boundaries

In server-ingest mode (remote-device CC and cloud sandbox both run
`lh hetero exec`), SerialServerIngester's `accumulatedText` spanned the
whole run and never reset across assistant-message boundaries. Combined
with `snapshotMode: 'replace'`, every later message's snapshot re-emitted
all prior messages' text verbatim, which the server persisted into the
new DB message — producing cross-message text duplication.

Reset `accumulatedText` on `stream_start` / `stream_end` (emitted by the
adapter's `openMainMessage`) after flushing the just-ended message's
snapshot, so each message snapshots only its own text.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:27:47 +08:00
Innei 1a4005c7b9 ♻️ refactor: extract server into apps/server + root namespaces into packages (#14949)
* ♻️ refactor(server-deps): extract envs/trpc/config/locales/business-server into packages

* ♻️ refactor: relocate src/server backend modules to apps/server package

Rebuilt on current canary: git mv the 8 server subtrees (services, routers,
modules, globalConfig, utils, runtimeConfig, workflows, featureFlags) into
@lobechat/server, with @/server/* dual-path alias, database vitest aliases,
and instrumentation import fixup.

* 📝 docs(skills): update src/server path refs to apps/server/src after relocation
2026-06-09 18:09:26 +08:00
sxjeru 64d3bdb978 💄 style: add preserve thinking feature for Qwen3.7 Max model (#13494)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-06-09 17:21:39 +08:00
Arvin Xu 434532ce36 🐛 fix(heterogeneous-agents): emit per-turn usage for batch-mode Claude Code (#15577)
* 🐛 fix(heterogeneous-agents): emit per-turn usage for batch-mode Claude Code

Device + sandbox runs spawn Claude Code via the `lh hetero exec` CLI in BATCH
mode (no `--include-partial-messages`), unlike the desktop driver which always
streams partial messages. In batch mode CC emits no `message_delta`, and the
adapter deliberately skipped usage on `assistant` events (assuming the stale
`message_start` echo that only exists in partial mode). The grand-total
`result_usage` is intentionally ignored to avoid double-counting, so batch runs
ended up persisting NO usage at all — the model tag showed no token count.

Track whether any `stream_event` was seen (partial mode); when none has been
(batch mode), emit per-turn usage from the `assistant` event as turn_metadata.
The assistant event's usage is authoritative in batch mode, not a stale echo.

This also fixes the model tag showing `claude-opus-4-8[1m]`: the `[1m]` 1M-context
beta marker only appears in the `system init` model field, while `assistant`
events report the canonical `claude-opus-4-8`. The new turn_metadata carries the
clean id, which supersedes the init-captured one (and matches the id ModelIcon /
pricing lookups expect).

Partial mode (desktop/local) is unchanged — `message_delta` still owns usage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(heterogeneous-agents): update batch-mode E2E for assistant usage

The multi-step E2E fixture has no `stream_event` records (batch mode) and 5
assistant events with `message.usage`, so the new batch-mode path now emits 5
turn_metadata events. Update the expectation from 0 — this validates the fix on
a realistic device/sandbox session: per-turn usage lands with the canonical
model id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(heterogeneous-agents): stop leaking host Anthropic creds into spawned CLI

The local CLI spawn forwarded the entire `process.env` to `claude`, so a
developer with `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL`
exported in their shell had it inherited by the CLI — overriding its own
subscription login and surfacing as a baffling "Invalid API key" + non-zero
exit on every message.

Strip those three vars from the inherited env via `buildInheritedSpawnEnv`.
`session.env` is still spread last, so an agent that explicitly configures an
API key continues to win. Adds regression tests for both the strip and the
override.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 16:51:33 +08:00
YuTengjing 23120f26e4 💄 style: update referral backfill copy (#15583) 2026-06-09 16:40:35 +08:00
sxjeru 77dbe4b7b3 🔨 chore(google): Support External URL file input with SSRF validation to optimize transmission (#12657)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: yutengjing <ytj2713151713@gmail.com>
2026-06-09 16:13:54 +08:00
LiJian 1ccc86e589 🐛 fix(skill): consolidate add-skill button into header dropdown (#15578)
* 🐛 fix(skill): consolidate add-skill button into header dropdown

Move the standalone 'AddSkillButton' from SkillList sidebar into the
header '+' dropdown, providing a unified entry point for all add-skill
actions (import from URL/GitHub, upload zip, custom connector).
Replace legacy 'Add Custom MCP' with the new Connector flow.

* 🐛 fix(skill): fix lint - remove unused ChevronDown import, sort imports
2026-06-09 16:07:36 +08:00
Rdmclin2 ccb33fa48c feat: workspace backend service slice (#15560)
Backend-only slice of the workspace feature (server routers/services, database models with workspaceId threading, openapi middleware, business/server stubs, const/types). Excludes all UI (features/routes/store/hooks). Deploys dark behind the workspace feature flag.

Includes open-source stub fixes: workspaceCreds router stub, ChargeParams workspaceId, usage.ts null-coalesce, DBMessageItem.workspaceId.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 15:54:26 +08:00
YuTengjing 082481c35d 🔇 chore: silence noisy dev console logs (#15548) 2026-06-09 14:55:37 +08:00
Arvin Xu 441e0c5b7c 🐛 fix(heterogeneous-agents): refine execution target + topic sidebar attention grouping (#15574)
* 🐛 fix(heterogeneous-agents): hide "no device" execution target for hetero agents

Heterogeneous agents (Claude Code / Codex) bring their own toolchain and must
execute somewhere, so the 'none' (plain chat) execution target is invalid for
them. Hide the option in the device switcher and never resolve/display 'none'
for hetero agents — fall back to local (desktop) or sandbox (web) instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(topic): use colorText for titles and move "Needs attention" below favorites

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(chat-input): improve runtime config bar layout on narrow screens

Keep chips on a single line (no per-character wrapping), truncate long
labels (working dir / branch / device name) with ellipsis, and let the
workspace cluster scroll horizontally instead of wrapping. On a narrow
bar the hetero "full access" badge collapses to its icon (hover tooltip
still explains it) via a container query.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(topic): show project directory under topic items in by-status mode

Surface each topic's working directory as a muted second line in the
by-status grouping, where rows otherwise carry no project context. Data
is already on the topic metadata, so no extra fetch.

- NavItem: add opt-in `description` slot (single-line layout unchanged)
- DirIcon: convert `renderDirIcon` function into a memo component, add
  `size` prop, rename file to PascalCase, migrate all call sites

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 14:18:18 +08:00
Arvin Xu 0a6b02ccb5 💄 style(topic): show error alert icon with tooltip on failed topics (#15573)
* 💄 style(topic): show error alert icon with tooltip on failed topics

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(topic): merge attention-needing topics into one "Needs attention" group

Collapse the unread-completion, failed, and waitingForHuman states into a single
top "pending" status bucket (待处理 / Needs attention) so the sidebar surfaces
everything that needs the user's attention in one place.

- groupTopicsByStatus now buckets those three states into `pending`, taking a new
  `unreadTopicIds` set (unread completions are a client-only state).
- Server STATUS_SORT_RANK floats `failed` to the top alongside `waitingForHuman`
  so failed topics stay on the first page and don't drop out of the group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(topic): pin the "Needs attention" group above favorites

The pending bucket already sorts above running, but the synthetic favorite group
was prepended ahead of it. Hoist pending to index 0 so attention-needing topics
sit at the very top of the sidebar, above both favorites and running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(heterogeneous-agents): pin resolved cwd onto remote-CC new topics

Remote CC dispatched the run with the correct working directory (the
precedence chain falls back to the agent's per-device pick), but a
brand-new topic was created without `metadata.workingDirectory`, so the
sidebar grouped it under "No directory" / 无目录.

Unify the three drifting server-side cwd-precedence sites behind one
pure helper (`resolveDeviceWorkingDirectory`) and persist the resolved
cwd back onto a freshly-created topic so grouping, next-turn reuse, and
workspace-init scan all agree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:24:42 +08:00
LiJian 5dd0f0c0c9 feat: specialize Market auth modal copy per capability scene (#15569)
Introduce a MarketAuthScene ('default' | 'sandbox' | 'mcp' | 'publish') so the
Market authorization modal can show capability-specific copy instead of the
generic "Create Community Profile" wording, while falling back to the generic
copy for unknown scenes.

- Reactive (401) path: infer scene from the tRPC procedure path in the error
  link and carry it on the market-unauthorized event.
- Proactive path: callers pass the scene to signIn() (publish buttons, MCP/skill
  install, in-chat market tool auth).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:39:33 +08:00
LiJian dfb70c1e87 🐛 fix(skills): inject pinned skill content into the system prompt (#15568)
* 🐛 fix(skills): inject pinned skill content into the system prompt

Pinned skills (ids in agentConfig.plugins) were marked activated by
SkillResolver but never carried their content, because resolveClientSkills
dropped the `content` field when mapping store skills to metas. As a result
SkillContextProvider's `s.activated && s.content` filter skipped them, so the
agent had to call activateSkill to use a pinned skill instead of it being
force-injected.

- builtin skill content is already in the store: carry it through.
- pinned DB skill content is fetched on demand (store cache first), only for
  pinned ids to avoid bulk network calls when auto mode exposes every skill;
  a failed fetch degrades gracefully to a content-less listing.
- resolveClientSkills becomes async; contextEngineering awaits it.
- add skillEngineering tests covering both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(skills): mark pinned skills activated and fix test types

The MessagesEngine path passes skillsConfig.enabledSkills straight to
SkillContextProvider without running SkillResolver, so the metas must carry
`activated` themselves — content alone is not enough (the provider only injects
`s.activated && s.content`). Mark pinned skills activated in resolveClientSkills,
guarded by content presence so a content-less pinned skill still falls back to
the <available_skills> list instead of disappearing.

Also widen the test helper's param type so `content`/`activated` are accessible
(fixes TS2339 in CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(skills): don't pre-activate ZIP-bundled pinned skills

Server-side bundle mounting for execScript / readReference is keyed off
stepContext.activatedSkills, which is populated only by the activateSkill tool
call — operation-level pinning never seeds it. So pre-injecting the content of a
ZIP-bundled DB skill would tell the model to run scripts from an unmounted bundle.

Gate the content pre-injection on the absence of a zipFileHash: bundled skills
stay in <available_skills> and are activated via the tool (which mounts the
bundle), while pure-content skills (builtin Artifacts, bundle-free DB skills)
are still force-injected when pinned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:38:59 +08:00
Arvin Xu 7ad6e2aa25 🐛 fix(agent): make working-directory Clear actually clear legacy / default-sourced cwd (#15571)
* 🐛 fix(agent): make working-directory Clear actually clear legacy / default-sourced cwd

The "Clear" action in the working-directory picker was a no-op whenever the
shown directory came from a precedence level that clear() never touched:

- clear() only removed the topic override and the agent's per-device choice
  (workingDirByDevice), but the button's visibility was gated on selectedDir,
  which also resolves from legacyAgentWorkingDirectory (pre-migration
  localStorage pick) and deviceDefaultCwd (device-wide default). When the cwd
  came from either, clear() deleted an already-empty higher level → nothing
  changed.

Fixes:
- useCommitWorkingDirectory: when clearing at the agent-default scope, also drop
  the legacy per-agent value (localStorage-only, no network round-trip).
- WorkingDirectoryPicker: gate the Clear button on hasClearableSelection
  (topic / agent choice / legacy) instead of selectedDir, so it no longer
  renders as a dead button when the cwd comes solely from the device default
  (which isn't clearable from the agent picker).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(claude-code): slow token count-up animation to 2000ms

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:22:40 +08:00
Arvin Xu 3986223b25 🐛 fix(heterogeneous-agents): show real CLI model on remote-spawned Claude Code (#15572)
Remote/device-spawned CC runs persist via the server-side
HeterogeneousPersistenceHandler (the executing device is not the viewing
client), and the assistant placeholder was created with the agent's
configured chat model/provider (e.g. deepseek-v4-pro). That value leaked
into the model tag and was re-applied at terminal, so the model tag showed
the wrong model instead of the real Claude Code model.

- Create the hetero placeholder with `provider: heteroType` for ALL hetero
  agents (not just remote openclaw/hermes) and no model, mirroring the
  client path. The real model is reported by the CLI and backfilled.
- Capture the CLI's authoritative model/provider from the first
  `stream_start` (CC system/init) and backfill the placeholder, so the real
  model lands from the first turn even without usage-bearing turn_metadata.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 12:08:00 +08:00
Arvin Xu ea246d6e17 feat(agent): list project skills over device RPC in the sidebar (#15566)
*  feat(agent): list project skills over device RPC in the sidebar

The right-sidebar 技能 (project skills) tab only read skills over local
Electron IPC, so in device mode (working dir on a bound remote device, or
the web client) the list was always empty — unlike the Files / Review tabs
which already branch on `deviceId`.

Add a `listProjectSkills` device RPC mirroring `getProjectFileIndex`:
- types: `DeviceProjectSkillItem` / `DeviceListProjectSkillsResult`
- `deviceGateway.listProjectSkills` via the generic `invokeRpc` relay
- TRPC `device.listProjectSkills` + `GatewayConnectionCtr` dispatch to
  `WorkspaceCtr.listProjectSkills`
- renderer chokepoint `projectSkillService` branches on `deviceId`
- `useProjectSkills(dir, deviceId?)`; remote mode lists but doesn't open
  previews (parity with the Files tab)
- thread `remoteDeviceId` through `SkillsGroup`

No device-gateway repo change needed — the RPC relay is method-agnostic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): list project skills over device RPC for homogeneous agents too

Thread `deviceId` through the homogeneous resources path
(`AgentDocumentsGroup` → `ProjectLevelSkills`) so a device-bound homogeneous
agent's 技能 tab populates over RPC, matching the heterogeneous `SkillsGroup`.
`useProjectSkills` already accepts `deviceId`; this just wires it in and
OR-s `deviceId` into the `showProjectSkills` gate.

(The large AgentDocumentsGroup diff is prettier re-indentation from wrapping
the outer memo() once the param list crossed the print width.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent): resolve per-device cwd in ResourcesSection so device-mode skills load

ResourcesSection computed its working directory with the legacy
`topicCwd || agentCwd` selector, which misses `workingDirByDevice[deviceId]`
and `device.defaultCwd`. For a device-bound agent the cwd lives in that
per-device map, so it resolved to `undefined` — the project-skills SWR key
was null and the fetch never fired even though `deviceId` was set (the 技能
tab showed "暂无可用技能"). Switch to `useEffectiveWorkingDirectory`, the
same resolver the runtime bar / WorkingSidebar use. Fixes both the hetero
SkillsGroup and the homogeneous AgentDocumentsGroup paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(agent): show loading state for project skills while switching path

On a working-directory switch the project-skills SWR key changes, so items
go empty while the new scan is in flight. The homogeneous skills panel was
flashing the empty placeholder instead of a loader. Surface
`useProjectSkills().isLoading` and render NeuralNetworkLoading when project
skills are the only source and still loading. (The hetero SkillsGroup already
shows it via SkillSection's isLoading.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:58:55 +08:00
Arvin Xu f5458e1ad9 ♻️ chore: replace LOBE-XXX markers with inline migration context (#15567)
♻️ chore: replace LOBE-XXX markers with inline migration context in 0110 SQL
2026-06-09 10:54:22 +08:00
LiJian 251e2ede5e feat(sandbox): sync user-uploaded files into the cloud sandbox (#15550)
*  feat(sandbox): sync user-uploaded files into the cloud sandbox

Pre-load the files a user attached in a conversation (topic message files +
session files) into the cloud sandbox the first time it is used, and tell the
agent they are available.

- FileModel.findFilesToInitInSandbox: merge messages_files (by topic) and
  files_to_sessions (by the topic's session), de-duped by file id
- SandboxMiddlewareService.ensureFilesInitialized: on first tool call, presign
  download URLs and run an idempotent curl bootstrap into /mnt/data; guarded by
  an in-sandbox marker and a short-lived Redis hint, best-effort so it never
  blocks the actual tool call (caps: 50 files / 100MB / 120s)
- Agent awareness via {{sandbox_uploaded_files}} in the cloud-sandbox systemRole,
  populated by both the server (RuntimeExecutors) and client (contextEngineering)
  placeholder generators

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(sandbox): make file sync work on all server runtimes & keep prompt consistent

Address review feedback on the uploaded-files sync:

1. (high) The sync was a no-op on the cloudSandbox server runtime and the skills
   runtime because createSandboxService() was called without serverDB, so
   ensureFilesInitialized() returned early. Thread serverDB through both.
   (heterogeneous sandboxRunner is intentionally left out: it runs a coding agent
   in /workspace and does not use the cloud-sandbox systemRole.)

2. (medium) Drop the Redis "already initialized" hint. The in-sandbox marker is
   now the single source of truth for idempotency, so a recycled sandbox always
   re-syncs instead of being skipped by a stale 5-min Redis key.

3. (medium) Apply the 50-file / 100MB caps inside formatUploadedFilesPrompt (via
   the shared selectSandboxInitFiles), so the files the prompt advertises match
   exactly what the bootstrap downloads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 10:40:34 +08:00
Innei 337e7f244c 🐛 fix(market-auth): skip auth flow when LobeChat session is missing (#15532)
Guard `signIn()` and the market.* 401 handlers on `isSignedIn` so the
Create Community Profile modal no longer pops up for unauthenticated
users. Routing the user back to LobeChat sign-in is not MarketAuth's
responsibility — callers handle that.
2026-06-09 10:16:44 +08:00
Arvin Xu eae47f527c feat(markdown): render GitHub / Linear / external links as rich chips (#15561)
*  feat(heterogeneous-agents): default Codex exec to bypass approvals/sandbox

Switch the default Codex execution mode from --full-auto to
--dangerously-bypass-approvals-and-sandbox, and share the execution-mode
constants from @lobechat/heterogeneous-agents/spawn so the desktop driver
and spawnAgent stay in sync. An explicit execution flag in extraArgs still
wins. Also fix the Codex adapter step tracking so consecutive agent_message
items stay in one step, stale tool completions don't start a new step, and
turn completion drains pending tools before emitting stream_end +
agent_runtime_end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(shared-tool-ui): unwrap shell-wrapper commands in RunCommand UI

Codex execs commands wrapped as `/bin/zsh -lc '...'`; surface the inner
command in the RunCommand inspector and render. Also switch Unix glob
fallback from `find` to `fast-glob` to preserve globstar semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(markdown): render GitHub / Linear / external links as rich chips

Add a markdown Link plugin that rewrites anchor elements into rich inline
chips: GitHub repo/PR/issue/commit/user, Linear issues, npm packages, Figma
files, mailto, and any other external link (favicon + full URL). Citation,
footnote, anchor and relative links keep the default renderer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ⬆️ chore(deps): bump @lobehub/editor to 4.17.0 and @lobehub/ui to 5.15.10

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:22:35 +08:00
Arvin Xu dfdf844761 🐛 fix(desktop): bump node-gyp to 12.x so Windows build finds Visual Studio 2026 (#15562)
GitHub redirects the `windows-2025` runner to the new `windows-2025-vs2026`
image, which ships Visual Studio 2026. node-gyp 11.5.0 only recognizes VS
2019/2022, so `electron-builder install-app-deps` fails to rebuild the native
`get-windows` module with "Could not find any Visual Studio installation".
node-gyp 12.x adds VS 2026 detection. Override it in both the root workspace
and the isolated apps/desktop install.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 02:36:10 +08:00
Arvin Xu cca01451f9 feat(heterogeneous-agents): default Codex exec to bypass approvals/sandbox (#15557)
*  feat(heterogeneous-agents): default Codex exec to bypass approvals/sandbox

Switch the default Codex execution mode from --full-auto to
--dangerously-bypass-approvals-and-sandbox, and share the execution-mode
constants from @lobechat/heterogeneous-agents/spawn so the desktop driver
and spawnAgent stay in sync. An explicit execution flag in extraArgs still
wins. Also fix the Codex adapter step tracking so consecutive agent_message
items stay in one step, stale tool completions don't start a new step, and
turn completion drains pending tools before emitting stream_end +
agent_runtime_end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(shared-tool-ui): unwrap shell-wrapper commands in RunCommand UI

Codex execs commands wrapped as `/bin/zsh -lc '...'`; surface the inner
command in the RunCommand inspector and render. Also switch Unix glob
fallback from `find` to `fast-glob` to preserve globstar semantics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 02:03:35 +08:00
Innei d2cd9ef023 feat(page-editor): enable block plugin with shared inline padding (#15556)
*  feat(page-editor): enable block plugin with shared inline padding

Mount `ReactBlockPlugin` on the page editor with `anchorPadding={0}` so
the editor root no longer reserves its default 54 px gutters, and apply
`DEFAULT_BLOCK_ANCHOR_PADDING` as `paddingInline` on the `Flexbox`
wrapping `TitleSection` + `EditorCanvas`. This keeps the title and
editor content aligned while leaving the same 54 px of room for the
floating block menu / drag handle to render in.

Requires `@lobehub/editor` with `anchorPadding` support and the
exported `DEFAULT_BLOCK_ANCHOR_PADDING` constant.

* 🐛 fix(page-editor): drop redundant overflowY on editor content wrapper

`editorContent` previously declared `overflowY: 'auto'`, which created
a second scroll container nested inside `.contentWrapper` (already
`overflowY: 'auto'`). With the new inline padding from
`DEFAULT_BLOCK_ANCHOR_PADDING`, the nested scroller clipped the
floating block menu / drag handle that the editor renders in the
inline-padding gutter. Let the outer wrapper own scrolling so the
gutter overflow stays visible.
2026-06-09 01:04:10 +08:00
Arvin Xu ea3ae583d6 feat(agent): unified per-device working directory + execution-device UI (#15543)
*  feat(agent): unified per-device working directory + execution-device UI

Client UI consuming the backend contract (#15542). User-facing — validate
before merge.

- New `src/store/device` (SWR fetch + cwd writes) — single source of device data;
  `deviceCwd` helper moves here from the chat-input feature layer.
- One `WorkingDirectoryPicker` for local + remote (native dialog vs manual path).
- Shared `WorkspaceControls` strip composed by both chat-input bars.
- GitStatus reads remote git via `useDeviceGitInfo` (read-only).
- Execution-device switcher graduates out of labs → writes only executionTarget.
- One-time migration of legacy localStorage recents into device.workingDirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): wire executionTarget→runtimeMode + workingDirByDevice cwd

The runtime-decision wiring, kept out of the backend contract PR so it's
reviewed/validated together with the UI that drives it.

- `helpers/executionTarget`: resolveRuntimeMode / executionTarget resolvers.
- server tool gate (AgentToolsEngine) derives runtimeMode from
  `agencyConfig.executionTarget`, with a no-regression fallback to the legacy
  per-platform runtimeMode.
- server cwd precedence (aiAgent resolveWorkspaceInit + hetero dispatch) now
  consumes `workingDirByDevice[targetDeviceId]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(agent): cover executionTarget + workingDir helpers; drop dead lab key

- Unit-test resolveRuntimeMode / resolveExecutionTarget and the working-dir
  precedence (locks the web default→cloud graduation + legacy fallback)
- Remove the now-unused `executionDeviceSwitcher` lab i18n keys (toggle deleted)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): guide web users to the desktop app in the device switcher

On web with no remote device, replace the muted "no devices" dead-end with a
prominent, clickable download-desktop card (and drop the now-duplicate header
link). Desktop keeps the muted hint since local execution is already available.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): fix execution-device copy for desktop + web

- Desktop "no devices" hint no longer tells an already-on-desktop user to
  "install the desktop app" — just points at `lh connect`.
- Tighten the web download-card description to the desktop's real benefit
  (run on your computer with local file access).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): flatten the web download card to a plain row

Drop the outer border/background so it reads as a normal menu row (like the
sandbox option), and shorten the description to a single line so the row stops
being taller than its neighbours.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): reword download-card desc to "access to your computer"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): add "no device" execution target (plain chat, no run tools)

Restores the option to run an agent with no execution environment, lost when
the per-platform runtimeMode was unified into executionTarget. Adds `none` to
HeteroExecutionTarget (→ runtimeMode `none`), surfaces it at the top of the
switcher on both web + desktop, and flips the web default back to `none` so an
unconfigured web agent is plain chat again (desktop still defaults to local).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): rename HeteroExecutionTarget→DeviceExecutionTarget, reorder switcher

- Rename the type (it now carries `none`, so "device" target fits better than
  "hetero") across types + helpers + dispatcher + switcher.
- Move "no device" to the bottom of the list (real targets first, opt-out last).
- Reword the download card to "let agents connect directly to your computer".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): move "no device" back to top, restore EN download copy

"No device" sits above the dynamic device rows; keep the EN download-card
wording as "Run agents with access to your computer".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): swap switcher icons — MonitorOff for "no device", Box for sandbox

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): clarify execution-device info tooltip + "no device" desc

- Info tooltip now explains the cloud sandbox is provided by the centralized
  LobeHub Marketplace, and that picking a device makes it the agent's runtime
  for reading/writing files and operating the computer.
- "No device" description now conveys "no device enabled, can't operate a
  computer" instead of "plain chat".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): move info icon beside the title, shorten "no device" desc

- Info tooltip trigger now sits next to the "Execution Device" title instead of
  right-aligned; the download link stays on the right.
- "No device" description trimmed to just "No device enabled".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): zh tooltip wording — "提供服务"

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): reorder tooltip — device runtime first, marketplace last

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): trim tooltip — drop "设备"/devices and trailing period

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): tag the current machine's device row, drop duplicate "This device"

When the desktop's own machine appears in the device list, badge that real row
with a "This device" tag and hide the generic "This device" (local) option —
no more two entries for the same machine. The local option still shows as a
fallback when the machine isn't enrolled in the list yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(agent): hoist this-machine device above sandbox + auto-bind on first run

Switcher-only (no routing/dispatch changes):
- Order is now: no device → this device → cloud sandbox → other devices.
- On desktop, when this machine is enrolled and online and the agent has no
  explicit target yet, default to it and persist the binding once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): widen gap between execution-device rows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): hide "Get Desktop App" link on desktop

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): capitalize "Cloud Sandbox" label

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(agent): web working-dir entry via "Add folder" modal instead of inline input

The browser folder picker can't yield an absolute path (sandboxed handle), so
on web / a remote device the working directory is entered manually. Replace the
inline input with an "Add folder…" row that opens a modal for absolute-path
entry; the local desktop machine still opens the native folder dialog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): split working-dir footer into local/remote row components

Replace the scattered `isLocalDevice ?` forks (icon, label, handler) with one
branch that picks between two self-contained rows: ChooseLocalFolderRow (native
dialog) and AddRemoteFolderRow (absolute-path modal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): use the device default cwd as the add-folder placeholder

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): validate manually-entered working dir via device statPath RPC

Web / remote clients can't browse the target device's filesystem, so the
"Add folder" modal now checks the typed path on the device before binding it.
New `statPath` device RPC mirrors gitInfo end-to-end:
- desktop WorkspaceCtr.statPath (fs.stat → exists / isDirectory) + RPC dispatch
- server deviceGateway.statPath + device.statPath tRPC (invokeRpc relay)
- modal blocks on a definitive negative (not found / not a directory); an
  unreachable device is treated as "can't verify" and allowed through

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): route statPath through deviceService, not lambdaClient

Components shouldn't import lambdaClient directly — add a thin deviceService
wrapping device.statPath, and call it from the working-dir picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(i18n): move working-directory strings from plugin to a device ns

The working-directory / git control-bar strings (53 keys) were lumped under the
`plugin` namespace. Move them to a dedicated `device` namespace and drop the
now-redundant `localSystem.` prefix (`plugin:localSystem.workingDirectory.X` →
`device:workingDirectory.X`). Updates the 4 consumer components; the `device`
ns auto-registers via defaultResources.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): route all device TRPC calls through deviceService

Components/hooks/stores shouldn't reach into lambdaClient.device.* directly.
Expand deviceService with listDevices/updateDevice/listGitBranches/
checkoutGitBranch/checkCapability/getAgentProfile and migrate every imperative
call site (device store, BranchSwitcher, CreatePlatformAgent, the remote-agent
guard, RemoteAgentConfigCard) + the DeviceListItem type. lambdaQuery.device.*
React-Query hooks are left as-is (a different pattern).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): pull/push a remote device's branch over RPC

Wire git pull/push through the device's pullGitBranch/pushGitBranch RPC so the
web/remote GitStatus bar can sync, not just the local desktop over IPC. Shows
the pull/push affordances for remote devices too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): route git pull/push through deviceService too

Add pullGitBranch/pushGitBranch to deviceService and switch GitStatus off the
direct lambdaClient.device.* calls, so no component reaches the device router
directly anymore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent): detect repoType for manually-added working dirs

A directory added via the "Add folder" modal committed without a repoType, so a
GitHub repo showed a plain folder icon. statPath now also returns the git repo
type (detected on the target device); the modal threads it into the committed
entry. Collapses the modal's separate validate+submit into one onSubmit that
validates and enriches in a single round-trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(agent): create new branch via a modal instead of inline footer

"Checkout new branch…" now opens a focused modal (branch-name input + create)
rather than expanding an inline footer inside the branch dropdown. Always
creates + checks out the branch — no checkout/overwrite options. Errors show
inline in the modal; drops the dead inline-create state/styles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): route all git ops through a unified gitService

Pick Electron IPC vs device RPC inside the service so UI / store / hooks
stay transport-agnostic. Replace the bundled `gitInfo` device RPC with
granular reads (branch / linked PR / working-tree / ahead-behind) that
mirror the local IPC methods one-to-one, and move the git read SWR hooks
into the device store (useFetchGitInfo / WorkingTreeStatus / AheadBehind).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): route Review git ops through device RPC (remote-capable)

Extend the device-RPC git pipeline to the 4 ops the Review panel needs
(getGitWorkingTreePatches / getGitBranchDiff / listGitRemoteBranches /
revertGitFile), mirroring the listGitBranches pattern end-to-end: desktop RPC
dispatch → deviceGateway → device.* tRPC → gitService. Adds minimal DeviceGit*
mirror types to @lobechat/types. Review (useReviewPatches / useGitRemoteBranches
/ FileItem) now goes through gitService with a deviceId, dropping the isDesktop
gate so web/remote devices get the diff + revert too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent): resolve repoType from device store so remote Review tab shows

useRepoType now reads the persisted workingDirs[].repoType from the device
store (keyed by deviceId), so a remote device's git/github type — and thus the
Review tab visibility — resolves without a local-only IPC probe. The IPC probe
+ localStorage fallback are kept only when the target is the local machine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(agent): optimistic branch switch in the branch switcher

Flip the displayed branch the instant a checkout is clicked (or a new branch
created) instead of waiting for the IPC/RPC round-trip + gitInfo refetch. The
git-info SWR cache is optimistically updated and reconciled on completion — a
failed checkout rolls the label back and toasts the error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat: support remote device files panel

* 💄 style: restore desktop this-device option

* 🐛 fix: keep files panel local for this device

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 23:27:52 +08:00
Arvin Xu a75eba5a4f 💄 style(chat-input): use compact stats footer for skill tools popover (#15552)
* 💄 style(chat-input): use compact stats footer for skill tools popover

- Replace the two full-width footer rows (store / management) with a
  compact stats footer: pinned / auto counts on the left, an
  "Add Skills / Connector" store button (icon + label) and a settings
  icon button on the right.
- Right-align each item's type tag (MCP / Skills / builtin) so badges sit
  flush next to the row action instead of trailing the name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(aiAgent): mock deviceGateway in connectorOverlap exec test

execAgent reads `deviceGateway.isConfigured`, which under the happy-dom
test environment hits real t3-env and throws "server-side env var on the
client". Mock `@/server/services/deviceGateway` like the sibling device
tests do so the connector/plugin overlap cases run in isolation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 22:23:38 +08:00
Innei 9eff025787 💄 style(modal): use base-ui Button in custom modal footers (#15444)
💄 style(modal): use @lobehub/ui/base-ui Button in modal footers

Align custom-footer button padding/font with base-ui Modal's built-in
OkBtn/CancelBtn (32h / 14 / 13) for consistent visual rhythm. Affects
AuthRequiredModal footer and TaskTemplateDetailModal content button.
2026-06-08 21:31:34 +08:00
Innei 9b19ebb2c6 🐛 fix(desktop): unbreak dev cold-start + restore UI language across reloads (#15547)
* 🐛 fix(desktop): unbreak dev cold-start on non-default UI languages

`ViteRendererFallback` now proxies via globalThis `fetch` (Node undici) instead
of Electron `net.fetch`, and Vite dev server is pinned to IPv4 listen. The
main-process Chromium `net` pool is small and surfaces `ERR_INSUFFICIENT_RESOURCES`
under cold-start module bursts + ~50 i18n namespace fan-out under non-en-US
locales. undici queues internally and avoids that pool entirely; v4 listen avoids
happy-eyeballs dual-stack connect storms. A Semaphore(64) still caps in-flight
fetches so the OS socket layer never gets buried.

Fixes LOBE-10086

* 🐛 fix(desktop): restore persisted UI language across renderer reloads

The renderer's `<html lang>` was being computed from `?lng=` (injected by the
main process at `loadURL` time) with `navigator.language` as fallback. On
`Cmd+R` the webContents reload reuses the prior URL without rebuilding it
against `storeManager.locale`, so users who changed their language after
launch got dropped back to the OS locale on every reload (white screen, then
English). Read the i18next localStorage cache first — that's the actual
persisted user setting written by the language switcher — and fall back to the
URL param + navigator as before.

*  test: mock device gateway in connector overlap spec
2026-06-08 21:21:24 +08:00
YuTengjing a2fd98a2d1 🐛 fix: restore file URLs in context prompts (#15549) 2026-06-08 19:26:16 +08:00
Arvin Xu 235a16fc11 chore(agent): agencyConfig contract + git-over-RPC backend (#15542)
*  feat(agent): agencyConfig contract — workingDirByDevice + executionTarget

Type-only contract for the unified per-device working-directory work. Adds
`workingDirByDevice` (per-device cwd) and `executionTarget` to agencyConfig.
No runtime logic consumes them yet — the server/client wiring lands in the UI
PR so it can be validated as one unit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): device gitInfo over RPC + shared local-file-shell git impl

Backend/RPC capability for "git branch / changes / PR for remote devices".
Dormant — no client caller yet; merging changes no existing behavior.

- `@lobechat/local-file-shell/git`: repoType + branch / linked-PR / working-tree
  / ahead-behind + `gitInfo` aggregate + `DeviceGitInfo` type (desktop + CLI).
- desktop `GitCtr.gitInfo()` (@IpcMethod) delegates to it; registered in
  GatewayConnectionCtr's RPC dispatch. `utils/git` re-exports the helpers.
- server: `deviceGateway.gitInfo()` wrapper + `device.gitInfo` TRPC query.
- `@lobechat/types`: `DeviceGitInfo` shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(desktop): fix stale mocks after git impl moved to local-file-shell

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(server): extract DeviceGateway into its own service dir

deviceGateway is a device-scoped gateway client (status/list/tool-call/git/
workspace RPC), not tool-execution-specific. Move it out of toolExecution/
into its own services/deviceGateway/ and update all import sites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 18:09:09 +08:00
LiJian ee65cf2a0f feat(connector): custom OAuth MCP connectors — onboarding, runtime execution & connector-first (LOBE-9983) (#15546)
*  feat(connector): wire custom MCP OAuth — Pre-registration & DCR (LOBE-9983)

Connect the two OIDC schemes designed in LOBE-9736 (oidcConfig) end-to-end so
users can add a custom OAuth MCP server from /settings/skill. Until now the DB
schema, models, and tool-permission UI existed, but nothing ran the OAuth
authorization flow — syncTools only worked when a token already existed.

Flow (shared pipeline, branches only on where client_id comes from):
- Add modal (client_id present → Pre-registration; absent → DCR/RFC 7591)
- startOAuth: probe MCP URL → RFC 9728 protected-resource metadata → RFC 8414
  AS metadata; DCR-register the client when no client_id; persist resolved
  oidcConfig; build PKCE authorize URL, stash verifier in Redis keyed by state
- /oauth/connector/callback: consume state → exchange code → store encrypted
  tokens (KeyVaultsGateKeeper) + tokenExpiresAt + status=connected → postMessage
- syncTools lazily refreshes the access token before connecting

Built on @modelcontextprotocol/sdk OAuth helpers (discover/register/start/
exchange/refresh) — no hand-rolled protocol code.

Security:
- Wire KeyVaultsGateKeeper into ConnectorModel so OAuth tokens are encrypted at
  rest (previously the router passed no gatekeeper → plaintext)
- Strip decrypted credentials and oidcConfig.clientSecret from the list response

UI:
- "+" button in /settings/skill Connectors tab opens the Add modal
- SkillList surfaces custom connectors from the connector store
- Modal wires the client secret field, infers the scheme, and shows the
  redirect URI to register

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(connector): request server-advertised scopes in OAuth flow

The authorize request sent an empty scope list, so providers that require a
scope (e.g. Linear MCP advertises scopes_supported ["read","write"]) issued a
useless token or rejected the flow. Default to the authorization server's
advertised scopes_supported when the user did not specify any, and use them for
both DCR registration and the authorize request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(connector): let OAuth callback bypass SPA rewrite and auth gate

/oauth/connector/callback is a backend route handler reached via a cross-site
redirect from the OAuth provider, so the proxy middleware broke it two ways:

1. It was not in the backend passthrough list, so it got rewritten to the SPA /
   locale shell instead of running the route handler (307 → blank).
2. It was not in isPublicRoute, so BetterAuth treated it as protected; the
   cross-site top-level navigation doesn't reliably carry the SameSite session
   cookie, so it redirected to sign-in (307).

Add /oauth/connector to backendApiEndpoints and /oauth/connector/callback to
isPublicRoute (the handler validates its own single-use state, so it must not be
session-gated). Scoped so /oauth/callback/success|error SPA pages are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(connector): execute connector tools server-side + agent-runtime wiring

Make custom OAuth MCP connectors actually callable, and sync their tools as
soon as authorization completes.

- callback: after token exchange, sync the tool list server-side via a shared
  syncConnectorToolsById — the connector is usable without a client round-trip
- sync.ts: extract buildConnectorMcpParams (http+auth / stdio), shared by
  syncTools and the new callTool
- connector router: add `callTool` (resolve connector, hard-block disabled
  tools, refresh token, call the remote MCP with decrypted credentials)
- aiAgent runtime: pass a KeyVaultsGateKeeper when resolving connectors so OAuth
  tokens decrypt (otherwise tool calls 401); surface connectors in the
  agent-management availablePlugins as a new 'connector' type
- AgentManagementContextInjector: render a <connector_plugins> section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(connector): wire connectors into the classic client chat path

The front-end chat orchestrates tools client-side (via /webapi/chat proxy),
separate from the server agent runtime. Connectors were invisible and
unexecutable there. Wire them in, connector-first.

- toolEngineering: build connector manifests from the store and inject them into
  createToolsEngine; drop plugins sharing a connector identifier (connector wins)
- buildClientConnectorManifests: store rows → type 'mcp' manifests (no token; the
  client has none) with permission → humanIntervention mapping
- mcpService.invokeMcpToolCall: route connector tool calls to connector.callTool
  before the plugin path (only connectors with a real MCP endpoint, so
  Lobehub/Klavis skills keep their executor)
- DeferredStoreInitialization: fetch connectors post-login so chat sees them
- AddConnectorModal: refresh after OAuth regardless of popup outcome
- chat-input skills picker: surface custom connectors in the auto group

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(connector): open OAuth popup synchronously + escape callback HTML (codex P1)

- AddConnectorModal: open the OAuth popup synchronously inside the click handler
  (before any await), then navigate it to the authorize URL. Browsers block
  window.open once an async boundary is crossed, which left popup=null and the
  poll loop never resolving — the Add modal hung. Null popup now fails fast with
  a "allow popups" message.
- callback route: escape the postMessage payload for `<script>` context
  (`<`, `>`, `&`, U+2028/U+2029 → \uXXXX). A malicious OAuth server could put
  `</script>...` in the error param and execute script on the app origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(connector): tighten execution boundary + surface OAuth failures + tests

Address review: enforce the same constraints at the call site that the manifest
layer enforces, and stop swallowing OAuth failures.

- isEnabled on BOTH sides: invokeMcpToolCall only routes enabled connectors
  (a disabled connector no longer steals a same-name plugin's call), and the
  server rejects calls to a disabled connector. Matches buildClientConnectorManifests
  which only exposes enabled connectors.
- callTool requires the toolName to exist in the synced user_connector_tools
  list — unsynced / hand-crafted tool names are rejected instead of being
  forwarded blindly to the remote MCP.
- extract callConnectorToolById (typed ConnectorToolCallError → tRPC codes) so
  the gates are unit-testable.
- AddConnectorModal: distinguish success / provider-error (show the reason) /
  user-dismissed instead of collapsing every failure into a silent close.
- tests: exec gates (not-found / disabled connector / unknown tool / disabled
  tool / success / token-refresh) + buildClientConnectorManifests mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(connector): align redirect URI, connector-override & partial-failure UX

Second review round.

- redirect URI: the modal showed a client-origin URI while the server sent an
  APP_URL one — register-vs-use mismatch broke the callback. Add a
  `connector.getRedirectUri` query (server source of truth) and show exactly
  that in the modal.
- execAgent: derive the plugin-override set from the connectors that ACTUALLY
  produce a manifest (enabled + with tools), not the raw endpoint-having set —
  a disabled / not-yet-synced same-named connector no longer evicts the plugin
  and leaves the runtime with no tools. Matches the client-chat behaviour.
- partial failure: when code exchange succeeds but the tool sync fails, the
  callback now reports `synced: false`; the modal shows "authorized but tools
  could not be synced" instead of a false "connected".

Tests: execAgent overlap regression (disabled / 0-tool keeps the plugin; real
tools replace it) + callback partial-failure (synced:false on sync error).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor(connector): name the availablePlugins source 'custom' not 'connector'

The agent-management availablePlugins types describe a tool's SOURCE
(builtin / klavis / lobehub-skill); 'connector' named the storage system
instead. Once plugins migrate to the connector table everything is a connector,
so the source-based label is what matters. Rename to 'custom' to align with
ConnectorSourceType.custom (single source of truth); section is <custom_plugins>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(connector): enforce connector permissions for community MCP plugins

Community MCPs execute via the plugin path (not connector.callTool), so the
per-tool permissions a user sets in the new Connectors UI weren't surfaced:
needs_approval didn't trigger the approval prompt on either runtime. (disabled
was already hard-blocked at execution by ToolExecutionService and the mcp
router.)

- extract patchManifestWithPermissions into a pure, client-safe module
  (patchManifestPermissions.ts); connectorPermissionCheck.ts re-exports it.
- execAgent: also patch community-plugin manifests (pluginsWithoutConnectors)
  with their connector permissions, alongside lobehub/klavis.
- client createToolsEngine: patch community-plugin manifests with connector
  permissions from the store so needs_approval surfaces as humanIntervention
  in the classic chat path too.
- unit tests for the shared patch function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  fix(connector): tolerate uninitialized connectors slice in selectors

createToolsEngine now reads connectorSelectors.{customConnectors,connectorList};
toolEngineering/index.test.ts mocks getToolStoreState without `connectors`, so
the selectors hit `undefined.filter`. Guard with `?? []` (the real store always
seeds connectors:[] via initialState) and add connectors:[] to the test mock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  fix(connector): guard every connector selector against an uninitialized slice

mcp.test.ts mocks the tool store without `connectors`, and invokeMcpToolCall
calls connectorByIdentifier → `s.connectors.find` threw. The previous fix only
guarded connectorList/customConnectors; harden all of them (find/filter) so any
partial-store mock is safe. The real store always seeds connectors:[].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:01:41 +08:00
Arvin Xu 0ac53b4e80 🐛 fix(agent-runtime): capture Gemini multimodal content_part/reasoning_part output (#15535)
Gemini 2.5+/3 thinking streams deliver assistant text and reasoning as
content_part/reasoning_part events instead of plain text/reasoning. The
runtime registered no onContentPart/onReasoningPart handlers, so the text
was silently dropped: onCompletion still reported usage tokens, the
empty-completion guard saw outputTokens > 0, and the turn finalized to a
blank `done` (lost in DB, client stream and trace alike).

Add the two handlers, mirroring onText/onThinking for text parts so
streaming, persistence and tracing all capture the content. Image parts
are uploaded to object storage and serialized as multimodal content
(text + image URLs, in order) — never persisting raw base64.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 14:55:37 +08:00
René Wang 91588bfdf8 📝 docs: add June 8 weekly changelog (#15537)
* 📝 docs: add June 8 weekly changelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs: add June 8 changelog cover and register index entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 13:57:37 +08:00
LiJian 927a79c3fb feat(auth): preserve utm_source through the OIDC sign-in/sign-up flow (#15544)
When Market kicks off OIDC against LobeHub, unauthenticated users are
redirected by the auth middleware to /signin (and onward to /signup).
The utm_source param sent on the original /oidc/auth request was only
buried inside callbackUrl and never surfaced on the sign-up page.

Carry utm_source as a first-class query param through the auth detour,
mirroring how the `hl` locale param is already preserved:
- middleware lifts utm_source from the request onto the /signin URL
- sign-in forwards utm_source to /signup in both navigation paths

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:24:53 +08:00
Arvin Xu c5c047e4b5 🐛 fix(desktop): misc independent fixes (vite fetch cap, gateway loading, token animation) (#15541)
* 🐛 fix(desktop): bound concurrent Vite dev-server fetches

Since #15304 unified dev under app://, every renderer asset round-trips
through the main-process net stack. A cold start (thousands of module
requests) or a non-default UI language (~50 i18n namespaces over HTTP at
once) could exhaust the net request pool and surface as
ERR_INSUFFICIENT_RESOURCES. Gate Vite dev-server fetches behind a FIFO
semaphore (cap 64), holding each slot until the response body is fully
drained so streaming responses count for their whole lifetime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(desktop): add trailing inset to tab title

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix: eliminate blank loading state during Gateway/ServerRuntime execution

When sending a message in Gateway (ServerRuntime) mode, the UI showed
a blank state between 'Sending message' and 'Task is running in server'
because the new execServerAgentRuntime operation was associated with the
server-created message ID, while the UI was still rendering the temp
message ID. The temp ID had no running operation, so ContentLoading
returned null.

Fix: pass temp message IDs to executeGatewayAgent and associate them
with the gateway operation alongside the server message ID. This ensures
ContentLoading finds a running operation regardless of which message ID
the UI is currently rendering.

*  feat(agent): animate subagent token count with count-up effect

Promote a shared AnimatedNumber into @lobechat/shared-tool-ui/components and
use it for the subagent metrics token total so it rolls up smoothly while
streaming instead of jumping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 13:09:45 +08:00
LiJian 419aca2c59 🐛 fix(skill): stop OAuth connectors duplicating into the Skills tab (#15510)
The unified /settings/skill manager renders the Connectors and Skills
sub-tabs from one SkillList via viewMode. Lobehub/Klavis OAuth connectors
(type 'lobehub' | 'klavis') belong only in the Connectors view, but the
Skills view's "Community Skill" section still mapped them alongside the
market agent skills — so Gmail, Notion, Google Drive, etc. showed up in
both tabs.

Render only market agent skills in the Skills view; OAuth connectors stay
exclusively under the Connectors view's "OAuth Connectors" group.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:26:14 +08:00
Arvin Xu f0f8ecd64d 🧹 chore: clean LOBE marker comments from aiInfra schema (2026-06-08) (#15536)
🧹 chore: replace LOBE-10056 markers with inline context in aiInfra schema comments
2026-06-08 11:07:18 +08:00
René Wang b19008ed24 💄 style: bring various details for better experience (#15486) 2026-06-08 10:55:46 +08:00
Arvin Xu dbf743cc12 feat(verify): Agent Run delivery checker system (#15489)
* 🗃️ feat(database): add verify system tables for agent run delivery checker

Implement the database layer for the Agent Run delivery checker (Verify System).

Reuse / definition layer:
- verify_criteria: a single reusable pass/fail standard (atomic unit), carrying
  its verifier config + onFail default and bound to a document for judging
  guidance (iteration history reuses document_history; no version columns)
- verify_rubrics: a named group that aggregates criteria — the reusable unit
- verify_rubric_criteria: junction, which criteria a rubric aggregates
  (criteria are reusable across rubrics)

Mounted onto an agent via the existing agency config jsonb:
- agencyConfig.verifyRubricId: a reusable rubric (criteria template)
- agencyConfig.verifyCriteriaIds: ad-hoc one-off criteria
A run's plan instantiates the union of both. No dedicated bindings table.

Snapshot + result layer:
- agent_operations.verify_plan (jsonb) + verify_plan_confirmed_at: the per-run
  immutable check-item snapshot lives ON the operation (1:1 — auto-repair spawns
  a new operation), instead of a separate plans table
- agent_operations.verify_status: denormalized rollup for list-page badges
- verify_check_results: per-criterion result with the Toulmin model
  (verdict/confidence as columns, narrative in a typed toulmin jsonb), N:1
  verifier_tracing_id for batch judging, FP/FN flags for the data flywheel;
  relates to the plan via operation_id + stable check_item_id

Ref: LOBE-10019

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(verify): add Agent Run delivery checker backend + frontend module

Implements the verify system on top of the schema (PR #15480):
- models: verifyCriterion / verifyRubric (+junction) / verifyCheckResult;
  agentOperation verify plan/status methods
- services/verify: AI plan generation (auto-create criteria), executor with
  LLM Toulmin judge (per-criterion + batch), program placeholder, agent &
  auto-repair spawner seams, rollup chokepoint, feedback fp/fn, completion
  lifecycle bridge
- lambda verify router (criteria/rubric CRUD, plan, results, feedback)
- frontend feature module: service, SWR hooks, CheckerDock state machine,
  RunArtifact, verify i18n namespace
- tracing scenarios: VerifyPlanGen / VerifyJudge

Live UI mount (dock/artifact into chat) pending server operationId source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(verify): persist delivery-checker verdicts via async tracing backfill

The LLM judge produced valid verdicts but they were never persisted, leaving
every run stuck at `verifying`. Two root causes:

1. FK ordering: `writeVerdict` stamped `verifier_tracing_id` synchronously, but
   the `llm_generation_tracing` row is written asynchronously (best-effort,
   after the response) — so the hard FK was violated every time and the verdict
   write was rolled back. Now the verdict is written with a null link, and the
   tracing id is backfilled by an `onPersisted` callback that fires only after
   the tracing row commits (still non-blocking). If tracing is disabled the link
   simply stays null.

2. Verdict parse: the judge JSON schema is non-strict, so the provider returns
   optional Toulmin fields as explicit `null`. The Zod validator used
   `.optional()` (accepts undefined, not null), so any null failed the whole
   `safeParse` and discarded the batch. Switched to `.nullish()`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(cli): add `verify` command for the delivery checker

Adds `lh verify` covering the full delivery-checker chain — criteria & rubric
CRUD, per-run plan (generate/state/confirm/skip), execute (LLM judge), results,
and feedback — calling the `verify` lambda router. Enables end-to-end backend
testing of the verify system.

Also adds the missing `tool-runtime` / `prompts` / `const` workspace entries to
the CLI's `pnpm-workspace.yaml` so the standalone package installs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(verify): add verify message role + delivery-checker card UI

Make the delivery-checker renderable in chat:

- Fix the `features/Verify` components so they compile: flatten the `verify`
  locale to the repo's flat-dotted-key convention (keySeparator: false), import
  `Flexbox`/`TextArea` from `@lobehub/ui` (react-layout-kit is no longer a dep),
  and the token cast.
- Add a `verify` UI message role + a `VerifyMessage` card that renders the
  Run Artifact + checker dock from `metadata.verifyOperationId`, wired into the
  message renderer switch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): add lobe-agent `generateVerifyPlan` tool (server runtime)

Lets an agent set up the delivery checker for its run: the agent calls
`generateVerifyPlan` early (per the new `<delivery_checker>` system-role
guidance), which instantiates the rubric / ad-hoc criteria into a frozen plan on
the current `agent_operations` row. Executed server-side only — the executor is
dispatched via `runtime[apiName]` with `operationId` threaded through the tool
execution context; the client `BaseExecutor` gracefully no-ops it.

Also registers the metadata fields (`verifyOperationId`/`verifyRound`) on the
message metadata zod schema so the role='verify' card can carry its operation id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): surface role=verify card on run completion (LOBE-10051)

Connect the delivery checker to the conversation: when an Agent Run with a
verify plan completes, `CompletionLifecycle` inserts a persisted `role='verify'`
message (parented to the assistant, carrying `metadata.verifyOperationId`) that
renders the checker card. Self-guarded — no plan → no card, failures never
affect the run.

`role='verify'` behaves like a `user` leaf message everywhere it flows
(persistence + conversation-flow pass it through unchanged); only the
context-engine treats it specially: a new `VerifyMessageProcessor` drops it from
the model context (UI-only card, not a valid model role). Adds `verify` to
`CreateMessageRoleType`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(verify): merge run-artifact + checker into one card

The role=verify message rendered two stacked cards (Run Artifact summary +
Delivery Checker) that duplicated the check-item list. Merge into a single card:
the `Run Artifact · Round N` header, then the checker results + actions, then the
snapshot note. RunArtifact/CheckerDock gain an `embedded` prop (header-only /
body-only, no card chrome) and VerifyMessage composes them under one border.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): derive generateVerifyPlan rubric from agencyConfig

A real agent calls `generateVerifyPlan` with just a `goal` and doesn't know
rubric ids. When `rubricId`/`criteriaIds` params are absent, derive the mounted
rubric + ad-hoc criteria from the executing agent's
`agencyConfig.verifyRubricId / verifyCriteriaIds`. Params still win when given.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): surface agent gateway WebSocket close code + reason

The `onclose` handler logged `String(event)` → the useless "[object
CloseEvent]". Surface `event.code` (+ `event.reason` when present) so a gateway
disconnect before completion is actually diagnosable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(verify): rename "Run Artifact" → "Verification", drop failed red border

- The kicker said "Run Artifact" — it's automated verification, not an artifact.
  Renamed to "Verification · Round N".
- Removed the red error border on a failed check — a normal card reads better.
- Fixes a render crash (`useVerifyState is not defined`): the border removal left
  a dangling reference after the import was dropped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(cli): poll run status when the agent stream drops

When the live stream (gateway WebSocket / SSE) closes before the run finishes,
the run is still executing server-side — so instead of hard-exiting, fall back to
polling `aiAgent.getOperationStatus` every 10s until the run reaches a terminal
state (or is no longer tracked). Pairs with surfacing the WS close code/reason.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(verify): add Render for generateVerifyPlan tool call

The generateVerifyPlan tool call rendered as the default param/result dump. Add a
Render that lists the generated delivery checks (title + gate/auto-fill tag), and
surface the items on the tool state so the Render can read them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): auto-confirm generated plan so checks run on completion

The agent generated a plan but it stayed `planned`/unconfirmed, so the completion
hook (which gates on a confirmed plan) never ran the checks — the card was stuck
at "awaiting confirmation" with no pass/fail. In the headless agent flow there's
no one to click Confirm, so `generateVerifyPlan` now auto-confirms the plan it
generates; the checks then run automatically on completion. (An interactive
"review before run" gate is a future enhancement.)

Also: the verify card header disappeared in the draft/planned phase
(`phaseToArtifact.draft` was null). Give it a header so the card always shows its
"Verification · Round N" heading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-tracing): only count opaque/presentational attrs as structural noise

The first structuralNoiseRatio charged ALL markup (every <...> tag) as noise,
which over-penalized legitimately structured results 3x. Grounding against real
web-search output (`<item title="…" url="…">snippet</item>`) showed the tags and
the title=/url= attributes ARE the signal the model reads.

Now only opaque/presentational attribute names (id, class, style, data-*, aria-*,
role, on*) count as noise; semantic element tags and content-bearing attributes
(title, url, href, name…) are kept. On a 57-op user-interrupted sample this drops
web-search noise 42%→0% and overall estimated waste 16%→5%, leaving large-payload
(readDocument) and high error-rate tools as the real signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): model-authored criteria with name/description/instruction-in-document + agent verifier

Restructure the generateVerifyPlan tool to a createDocument-style full-create flow
and wire up the agent verifier path:

- criteria now = title + description (required one-liner) + instruction (required
  detailed rubric); instruction lives in a linked document (verify_criteria.documentId),
  description is a new verify_criteria column (migration 0111). verifierConfig no
  longer holds description/instruction.
- generateVerifyPlan creates verify_criteria + a rubric, snapshots the plan onto
  the operation and confirms it; judge resolves the instruction from the document.
- agent-type checks run as verifier sub-agents (execAgent + isolated thread) whose
  onComplete hook parses a VERDICT and writes it back to verify_check_results
  (renamed AgentVerifierSpawner → VerifierAgentRunner).
- UI: custom Inspector for the tool header; check list shows per-verifier-type icons
  (llm/agent/program) + description + required/optional tag; i18n en/zh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ️ perf(verify): run program/llm/agent checks concurrently on completion

The three verifier kinds are independent; previously the agent spawn waited for
the batched LLM judge to finish. Run them via Promise.all so agent sub-agents
start immediately alongside the LLM batch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): dedicated builtin verify-agent + writeback tool, role=verify message, portal check editor

- Add `@lobechat/builtin-tool-verify` (submitVerifyResult) + builtin `verify-agent`;
  agent-type checks now run as the dedicated verify agent (not the user's agent),
  which investigates and writes its verdict back via the tool during its run.
- Verifier inherits the parent run's model/provider (builtin default may be
  unconfigured locally).
- role=verify completion message no longer requires an assistantMessageId, so the
  delivery-checker card always surfaces when a plan exists.
- Portal editor for verify checks (title/description/instruction/verifier/onFail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(verify): restrict verify-agent to its writeback tool; fix running loader icon

Root cause of stuck `running` agent checks: the verify-agent ran in agent mode and
inherited all default tools (web-browsing, cloud-sandbox, skills, activator), so it
went off web-searching/crawling to "investigate" and never called submitVerifyResult.

- Run the verify-agent in chat mode (enableAgentMode: false, searchMode: off) — the
  strict whitelist — and whitelist `lobe-verify` for chat mode so the verifier gets
  ONLY its writeback tool.
- Sharpen the verify systemRole: judge from the provided deliverable/instruction
  (no external tools), always reach a verdict, and always call submitVerifyResult.
- CheckerDock: running check now uses the standard RingLoadingIcon (warning ring),
  matching the app's loader instead of a blue spinner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): auto-repair loop — re-run the agent with failure feedback on failed checks

When required checks fail with onFail=auto_repair, automatically run a second
iteration instead of ending at `failed`:

- createRepairRunner: re-runs the SAME agent in the same topic with the failure
  feedback as the prompt, re-snapshots the plan onto the repair operation and
  confirms it so it re-verifies on completion (the next round). Capped at
  MAX_REPAIR_ROUNDS via parent-chain depth to prevent runaway loops.
- maybeAutoRepair: fires only once every required check has a terminal result, so
  it works for inline LLM checks (triggered from lifecycle) and async agent checks
  (triggered from the verify tool's writeback path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): open check result detail in portal & rename artifact→result

- add a VerifyResult portal view: clicking any check row opens that result's
  detail (verdict, confidence, Toulmin sections, suggestion) on the right; agent
  checks expose their execution trace from inside the panel
- CheckerDock rows are all clickable now (chevron affordance), status shown by
  icon only; verify card uses colorBgElevated
- rename the run-result surface from "artifact" to "result" everywhere: RunArtifact
  → RunResult, phaseToArtifact → phaseToResult, and all `artifact.*` i18n keys →
  `result.*`
- ship verify namespace zh-CN / en-US locales

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): enrich check result portal — criterion stepper, richer detail view

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): rubric run-policy config + repair feedback on the verify card

Auto-repair feedback now lives on the failed round's role=verify message
(content), and the VerifyMessageProcessor surfaces it into the repair run's
context as a tagged user turn — so the repair op runs off history via a new
execAgent `suppressUserMessage` path instead of injecting a synthetic user
message. createVerifyMessage is awaited before verification to avoid a race.

maxRepairRounds becomes a rubric-level config: new `verify_rubrics.config`
jsonb column, read live at repair time via the plan's sourceRubricId. Adds a
RubricConfig portal panel (reachable from the plan card's settings affordance)
to view/edit it, wired through the verify store + TRPC.

Verify domain types/vocab/config are extracted from the DB schema into
@lobechat/types as the single source of truth; schema and consumers import
from there.

Tests: VerifyMessageProcessor dual behavior; VerifyRubricModel config
round-trip; MessageModel.findVerifyMessageByOperationId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🗃️ refactor(verify): squash the 3 verify migrations into one

Collapse 0110 (tables) + 0111 (criteria.description) + 0112 (rubrics.config)
into a single regenerated 0110_add_verify_tables so the PR ships one clean,
idempotent migration. No schema change vs the three combined.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(cli): verify rubric run-policy config commands + shrink judging-rule editor font

CLI: `verify rubric create --max-repair-rounds`, `verify rubric view`, and
`verify rubric update` exercise the rubric config endpoints end-to-end; adds a
mocked command test. UI: judging-rule editor font 16px → 14px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(verify): editable rubric name in the config panel + default 3 repair rounds

Add a name (title) field to the RubricConfig portal, persisted via a new
updateRubricTitle store action + service (optimistic + debounced, alongside
the config write-back). Bump DEFAULT_MAX_REPAIR_ROUNDS 2 → 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(verify): extract generateVerifyPlan into installable lobe-delivery-checker tool

Move the delivery-checker plan-creation flow out of the always-on lobe-agent
tool into a new standalone, installable builtin tool `lobe-delivery-checker`
(Skill Store, opt-in per agent — not loaded by default). lobe-agent no longer
ships generateVerifyPlan.

- new packages/builtin-tool-lobe-delivery-checker (manifest/types/systemRole +
  client Render/Inspector/Portal moved wholesale from lobe-agent)
- new serverRuntimes/lobeDeliveryChecker.ts (generateVerifyPlan moved out of
  lobeAgent.ts), registered alongside verifyResult
- registered installable in builtin-tools (no hidden/discoverable:false, not in
  defaultToolIds/alwaysOnToolIds/runtimeManagedToolIds); renders/inspectors/
  portals/identifiers wired; lobe-agent portal entries removed
- i18n keys moved builtins.lobe-agent.verifyPlan.* → builtins.lobe-delivery-checker.*

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(agent): add `custom` tool mode; verify agent uses it instead of chat-mode

Chat mode's contract is to strip ALL user/agent plugins (strict KB/memory/web
allow-list) — so the verify sub-agent couldn't get its writeback tool without a
leaky blanket rule. Introduce a third tool mode `custom` where the toolset is
EXACTLY the agent's declared plugins (no always-on, no defaults, no activator),
for focused builtin sub-agents.

- chatConfig.toolMode: 'agent' | 'chat' | 'custom' (overrides enableAgentMode)
- AgentToolsEngine: custom branch (defaultToolIds = plugins, rules = plugins-on,
  allowExplicitActivation only in agent mode); chatModeRules restored to strict
- verify agent → toolMode: 'custom'; lobe-verify dropped from chatModeAllowedToolIds
- test: custom mode enables exactly the declared plugin, no always-on / defaults

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:16:35 +08:00
Arvin Xu fc0daa7604 💄 style(conversation): show running indicator after a settled inline tool while generating (#15528)
 feat(conversation): show running indicator after a settled inline tool while generating

Heterogeneous agent turns render a single tool call inline (no
WorkflowCollapse chrome). Once that tool settles but the run is still
generating the next step, the inline path showed nothing below it — a
blank gap that reads as "stuck". Render the same turn-start "running"
indicator at the segment tail for this case. Multi-tool segments keep
WorkflowCollapse's own streaming header; a tool still executing is
already covered by its loading placeholder.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 02:03:50 +08:00
Arvin Xu df72bc335e 🎨 refactor(local-system): preserve ANSI escape codes in command output (#15529)
* 🎨 refactor(local-system): preserve ANSI escape codes in command output

The client now renders ANSI sequences, so stripping color codes from
shell command output is no longer needed. Drop the stripAnsi helper and
let truncateOutput keep the raw colored output intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(local-system): drop dangling ANSI escape and reset open SGR state before truncation notice

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 01:37:34 +08:00
Innei e855fcc0b8 ♻️ refactor(desktop): move backend URL rewrite into main process (#15304)
* ♻️ refactor(desktop): move backend URL rewrite into main process

Renderer code no longer needs `withElectronProtocolIfElectron` to rewrite
backend URLs to `lobe-backend://`. The Electron main process now diverts
backend-prefixed paths (`/trpc`, `/webapi`, `/api/auth`, `/market`) to the
remote LobeHub server in two places:

- prod: `RendererProtocolManager` (`app://` handler) delegates to
  `BackendProxyProtocolManager.proxy(request, session)` after the existing
  hostname guard.
- dev: `Browser.setupRemoteServerRequestHook` registers a
  `webRequest.onBeforeRequest` listener that redirects
  `http://localhost(:*)/<backend-prefix>...` to `lobe-backend://lobe<path>`.

`BackendProxyProtocolManager` keeps a per-session `WeakMap<Session, Context>`
and exposes `proxy(request, session)` so the same OIDC token / Vercel cookie
/ 401 debounce / `X-Auth-Required` pipeline serves both entry points.

The helper and ~35 call sites in `src/services/_url.ts` and the three tRPC
clients are removed. `ELECTRON_BE_PROTOCOL_SCHEME` stays for the main
process; new `BACKEND_PATH_PREFIXES` + `isBackendPath` predicate live in
`apps/desktop/src/main/const/protocol.ts`.

* ♻️ refactor(desktop): decouple renderer protocol from backend proxy via interceptor pipeline

`RendererProtocolManager` no longer imports `BackendProxyProtocolManager` or
`isBackendPath`. It exposes a generic `addRequestInterceptor(fn)` hook and
runs interceptors in order inside the `app://` handler — first non-null
Response short-circuits the file pipeline.

`BackendProxyProtocolManager.createAppRequestInterceptor()` owns the
"what counts as a backend path" knowledge and returns a 502 for backend
prefixes when no proxy context is wired up (must not fall through to SPA
HTML).

Wiring happens in `App.ts` after `RendererUrlManager` construction —
composition root knows both modules so neither has to know the other.

* ♻️ refactor(desktop): unify dev/prod renderer under app:// and drop lobe-backend://

Dev mode no longer uses `http://localhost:<port>` as the renderer origin; the
BrowserWindow now loads `app://renderer/` in both dev and prod. Non-backend
requests fall through to a strategy:

- prod: `StaticRendererFallback` serves the static export from `rendererDir`
  (Range support, SPA HTML fallback, 404 handling)
- dev:  `ViteRendererFallback` proxies to the electron-vite dev server via
  `net.fetch('http://localhost:5173/<path>')`; HMR WebSocket connects
  directly (configured via `server.hmr.{host,clientPort}` + `strictPort`)

`lobe-backend://` is gone — the scheme, its privileged registration, the
`session.protocol.handle('lobe-backend', ...)` call, and the dev
`webRequest.onBeforeRequest` trampoline are all removed.
`BackendProxyProtocolManager` now only stores per-session context and
exposes `createAppRequestInterceptor()` for the `app://` pipeline.

Dev userData is pinned to `<appData>/lobehub-desktop-dev` via a new
`pre-app-init.ts` that runs before `@/const/dir` captures
`app.getPath('userData')` — necessary because dev and prod now share the
`app://renderer` origin and would otherwise collide on localStorage /
cookies / IndexedDB.

Also adds `stream: true` to the `app` scheme privilege so dev media Range
requests survive forwarding.
2026-06-08 00:49:33 +08:00
Arvin Xu ee6a74ba06 🗃️ feat(db): verify delivery-checker schema + ai_providers/ai_models _id column (#15526)
🗃️ feat(db): delivery-checker schema + ai_providers/ai_models surrogate `_id`

The DB layer, split out so it merges ahead of its callers (services / TRPC /
store / UI ship in a follow-up stacked PR). One consolidated, idempotent
migration (0110_add_verify_tables_and_ai_infra_id):

- verify delivery-checker: verify_criteria / verify_rubrics (+ config) /
  verify_rubric_criteria / verify_check_results tables + verify_status /
  verify_plan / verify_plan_confirmed_at columns on agent_operations; plus the
  verify domain types/vocab/config in @lobechat/types the schema imports.
  All four verify tables carry a workspace_id FK + index (cascade on workspace
  delete), matching documents / agent_operations. verify_check_results has a
  UNIQUE (operation_id, check_item_id) index — one lifecycle row per plan item
  per run, so a retry / concurrent worker can't create conflicting duplicates.
- ai-infra (LOBE-10072): nullable `_id uuid DEFAULT gen_random_uuid()` on
  ai_providers / ai_models, written as the safe two-step form (ADD nullable,
  then SET DEFAULT) to avoid a full-table rewrite + ACCESS EXCLUSIVE lock;
  backfill + NOT NULL are later manual steps (LOBE-10073 / LOBE-10074)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 23:03:50 +08:00
Arvin Xu 20cea3a6bf feat(page-agent): execute tools server-side via HeadlessEditor (#15023)
*  feat(page-agent): execute tools server-side via HeadlessEditor

Page-agent tools (initPage / editTitle / getPageContent / modifyNodes /
replaceText) now run on the server against a `@lobehub/editor/headless`
instance and persist through `DocumentService.updateDocument`, instead
of executing inside the renderer's Lexical instance. The renderer
applies the resulting snapshot via the builtin-tool `onAfterCall` hook,
so the document store stays in sync without an extra fetch.

This makes page-agent execution independent of the client lifecycle
(editor unmount, tab switch, network blip), gives us full server-side
tracing for free (OTel gen-ai + agent-signal + documentHistories), and
exposes a `silent-no-op` / `unexpected-mutation` invariant when the
exported editorData hash diverges from what the handler reported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(page-agent): decouple EditorRuntime from @lobehub/editor side-effecting bundle

EditorRuntime statically imported LITEXML_*_COMMAND from @lobehub/editor,
which pulls ReactSlashPlugin and crashes Node (`document is not defined`)
in any server-side test that transitively touched the runtime. The same
import also dispatched the wrong command identity on HeadlessEditor's
kernel — pnpm resolves @lobehub/editor to a different module copy than
the headless bundle, so dispatchCommand would silently no-op server-side.

Introduce a LiteXMLAdapter strategy: renderer wires command dispatch
against the live editor; server wires HeadlessEditor.applyLiteXMLBatch
/ applyLiteXML so the correct headless-bundle symbols are used.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(page-agent): restore client-side mutate handlers on PageEditor mount

The main commit dropped `setBeforeMutateHandler`/`setAfterMutateHandler`
under the assumption that page-agent tools always execute server-side.
But the chat-store path (`invokeBuiltinTool` → `PageAgentExecutor.modifyNodes`
→ `EditorRuntime.modifyNodes`) still routes through the client-bound
runtime whenever the LLM dispatcher is the chat slice — it does not
consult `manifest.executors`. Without the handlers, that path mutates
the live editor but skips both `documentHistoryQueueService.enqueueEditorSnapshot`
(loses undo baseline) and `commitEditorMutation(saveSource: 'llm_call')`
(row never persists).

Re-wire both handlers. Server-runtime path is unaffected: it instantiates
its own `EditorRuntime` against `HeadlessEditor` and never sees the
client's StoreUpdater wiring, so the two paths can coexist without
double-writing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(editor-runtime): split client / server entries so renderer gets adapter for free

Renderer call sites shouldn't have to opt in to the obvious default
(dispatch LITEXML_*_COMMAND on the live editor). Split the package into
two entries:

- `@lobechat/editor-runtime` — renderer entry; constructor auto-wires
  the LiteXML adapter from `@lobehub/editor`. Static-importing this
  from Node still crashes (ReactSlashPlugin), so it's the right shape
  for the browser only.
- `@lobechat/editor-runtime/server` — server-safe entry; exports the
  bare class without touching `@lobehub/editor`. Callers (currently
  only the page-agent server runtime) supply their own HeadlessEditor-
  backed adapter.

Drops the renderer-side setLiteXMLAdapter patch and a stale comment
block in StoreUpdater.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(page-agent): drop LiteXMLAdapter, dispatch commands directly

`@lobehub/editor` 4.16.1 ships the LiteXML command identities through the
side-effect-free `@lobehub/editor/litexml-commands` subpath, so a single command
object is shared across the browser and node bundles and can be imported in Node
without pulling the DOM-dependent editor bundle.

`EditorRuntime` now imports `LITEXML_MODIFY_COMMAND` / `LITEXML_APPLY_COMMAND`
from that subpath and dispatches them straight onto the editor kernel. This
removes the `LiteXMLAdapter` strategy object (`setLiteXMLAdapter` /
`getLiteXMLAdapter`) — a leaky abstraction whose only purpose was to keep the
crash-on-Node command import out of the shared base.

- editor-runtime: dispatch `LITEXML_*_COMMAND` directly; delete the adapter
  interface, field, setter and runtime-throw guard.
- Collapse the client/server entry split (its sole reason — isolating the
  DOM-crashing import — is gone); both entries now re-export the isomorphic base.
- pageAgent server runtime: drop the HeadlessEditor-backed adapter wiring.
- Bump `@lobehub/editor` to ^4.16.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(editor-runtime): drop redundant /server entry

Now that `EditorRuntime` is isomorphic (LiteXML commands come from the DOM-free
`@lobehub/editor/litexml-commands` subpath), the `./server` entry is byte-for-byte
identical to the root `.` entry. Remove it and point the only consumer
(pageAgent server runtime) at the root entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:33:41 +08:00
Arvin Xu 78657d496e 🐛 fix(desktop): pin electron-builder to 26.14.0 to fix broken macOS update signing (#15527)
electron-builder was floating on `^26.8.1` and the repo commits no lockfile,
so each CI build resolved a fresh version. The canary.12 build (2026-06-07)
picked up 26.15.0, which regressed macOS .app bundle signing: codesign reports
"bundle format is ambiguous (could be app or framework)" and Squirrel.Mac
rejects the update during code-signature validation, so the app never quits
to install — surfacing as "auto-update does nothing".

26.15.0 introduced the two suspect changes (mac signing rework #9822 and the
full app-builder-bin Go→TS replacement #9829). 26.14.0 predates both and does
not touch macOS app-bundle signing/layout. Pinning the exact version cascades
to app-builder-lib / dmg-builder / builder-util (electron-builder pins those
exactly), stopping the toolchain from floating across CI installs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:20:26 +08:00
Arvin Xu 2453fc3515 🐛 fix(desktop): skip browser beforeunload guard so auto-update can quit (#15525)
On desktop the chat-loading beforeunload guard (preventLeavingFn) blocks
window.close() during quitAndInstall, so the app fails to quit & install
the update. The main process already manages close/quit via keepAlive +
isQuiting, so short-circuit the guard on desktop.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:42:33 +08:00
Coooolfan a28fd30719 feat: suppport sandbox provider (#15184)
*  feat(cloud-sandbox): add Onlyboxes provider support for self-hosted sandbox (#15136)

- Add `SANDBOX_PROVIDER` env var (market | onlyboxes) to select sandbox backend
- Add Onlyboxes-specific env vars: `ONLYBOXES_BASE_URL`, `ONLYBOXES_API_TOKEN`, `ONLYBOXES_LEASE_TTL_SEC`
- Create `SandboxService` abstraction layer with `MarketSandboxService` and `OnlyboxesSandboxService` implementations
- Add `createSandboxService` factory that routes to configured provider
- Migrate `execInSandbox` and `exportFile` t

*  feat(sandbox): improve Onlyboxes export flow

* 🐛 fix(sandbox): pass presigned upload headers to Onlyboxes

*  test(sandbox): import tool runtime package

* 🐛 fix(sandbox): preserve Market export errors

* 🐛 fix(sandbox): allow empty docker env defaults

* 🔒 fix: redact sandbox auth params in logs

* 🐛 fix: address sandbox provider review comments

* 🔐 feat: use onlyboxes jit tokens

* 📝 docs: clarify cloud sandbox provider config

* 🐛 fix: align cloud sandbox timeout defaults

* 🐛 fix(sandbox): lower default Onlyboxes lease TTL to 15 minutes

* 🐛 fix(sandbox): cap Onlyboxes task wait time

* ♻️ refactor: split sandbox env config
2026-06-07 12:18:39 +08:00
Arvin Xu c711279edf feat(tools): show app-fixed tools in the chat-input Pinned section (#15509)
*  feat(tools): show app-fixed tools in the chat-input Pinned section

Surface always-on, runtime-owned tools (lobe-agent + always-on infra) read-only
at the top of the Tools popover "Pinned" group, so users can see what the app
keeps active for every conversation. These have no toggle — a Pin indicator with
a hint replaces the per-tool policy menu.

- builtin-tools: add `fixedDisplayToolIds` ([lobe-agent, ...alwaysOnToolIds])
- builtin selectors: add `fixedDisplayMetaList` (reads hidden tools by id)
- useControls: render read-only fixed items, prepend to Pinned, fold into counts
- i18n: add `tools.activation.fixed.hint` + `tools.builtins.lobe-agent.*`

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(tools): make lobe-agent actually always-on; gate fixed display to runtime

The Pinned section was rendering tools that aren't enabled every turn:
- lobe-agent was only enabled when injected into plugins/runtime ids (it has no
  rule in the engine, so it defaulted to disabled) — showing it as "always on"
  was a UI lie.
- manual skill-activate mode strips manualModeExcludeToolIds (activator,
  skill-store) from the defaults, so they're off — but they still showed as fixed.

Fixes:
- Add lobe-agent to alwaysOnToolIds so its core capabilities (plan/todo, sub-agent
  dispatch, visual-media fallback) are genuinely on every agent-mode turn. Chat
  mode still drops alwaysOn entirely.
- Derive fixedDisplayToolIds from alwaysOnToolIds (single source of truth, no drift).
- Make fixedDisplayMetaList mode-aware: drop manualModeExcludeToolIds in manual mode
  so the Pinned list matches what the engine actually enables.
- Update engine tests that asserted the old "lobe-agent off by default" behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ♻️ refactor(tools): drop fixedDisplayToolIds alias, use alwaysOnToolIds directly

fixedDisplayToolIds was just `= alwaysOnToolIds`; collapse it. The selector now
reads alwaysOnToolIds directly and still applies the manual-mode exclusion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:10:32 +08:00
Arvin Xu e7c73bd4ce 💄 style: support show CC subagent metrics chip (#15217)
*  feat(cc): show tool count + token + model metrics on Agent inspector chip

Surface per-subagent progress on the inline Agent inspector row so users can
see how much work has happened without expanding the thread:

- Inspector chip renders `[count] tools · [tokens]` after the description
  chip, with the model name in a Tooltip. Tool count = count of `role==='tool'`
  child messages; tokens = LAST subagent assistant's `metadata.usage.totalTokens`
  (CC's per-turn `message.usage` already includes the full prior context,
  so summing would double-count the shared history — the final turn's value
  matches the main-agent message-footer convention).
- New `threadSelectors.getThreadDbMessages` reads the raw DB-shape child
  messages from `dbMessagesMap[thread_*]` (the display-bound `messagesMap`
  bucket only holds the parent + a virtual `assistantGroup`).
- `BuiltinInspectorProps` carries `toolCallId` so the chip can join to its
  subagent Thread via `metadata.sourceToolCallId`; propagated from both the
  chat Inspector caller and the DevPanel `ToolInspectorSlot`.

Adapter / executor changes so subagent token usage actually flows in:
- `claudeCode.ts` `handleSubagentAssistant` emits a
  `step_complete{phase:turn_metadata, subagent}` event when
  `raw.message.usage` is present. Subagent assistant events are not
  partial-streamed (unlike main-agent), so `message.usage` is
  authoritative — no de-stale logic needed. The subagent ctx tag lets
  the executor route the usage write onto the in-thread assistant
  instead of the main agent's, so CC's `result_usage` grand-total
  semantics aren't double-counted.
- Renderer + server `step_complete{turn_metadata}` branches check for
  `event.data.subagent` and route to the run's `currentAssistantMsgId`.
  Renderer mirrors the write into `dbMessagesMap` via `run.stream.update`
  so the chip's selector picks up usage as it lands.

Server-side finalize rolls totals onto `thread.metadata` for the
historical-view cold-load path: tool count from `lifetimeToolCallIds.size`,
tokens from the last in-thread assistant's `metadata.usage.totalTokens`,
plus `completedAt` / `duration`. Done via the existing `threadModel.update`
with an inline metadata read-merge — no new `ThreadModel.updateMetadata`
method or `threadRouter.updateThreadMetadata` endpoint introduced.

i18n: 5 keys under `chat.thread.subagentMetrics.*` in `chat.ts` + zh-CN +
en-US.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(cc): persist subagent metrics so the inspector chip survives cold-load

The metrics chip (tool count · tokens, model in tooltip) only rendered while
the run streamed — after a reload it vanished on desktop. Two gaps:

- The renderer `heterogeneousAgentExecutor.finalizeSubagentRun` never rolled
  totals onto `thread.metadata` (only the server `HeterogeneousPersistenceHandler`
  did). On cold-load the child messages aren't hydrated, so the live selector
  had nothing to read and the chip's `hasAny` went false. Added the symmetric
  rollup (`totalToolCalls` / `totalTokens` / `completedAt` / `duration`),
  re-sending the create-time `sourceToolCallId` / `subagentType` / `startedAt`
  since `updateThread` replaces the whole metadata column.
- Subagent assistant messages carried no `model`, so the tooltip's model line
  never showed. The subagent `turn_metadata` branch now writes `model` /
  `provider` onto the in-thread assistant (live tooltip) and persists `model`
  onto `thread.metadata.model` (cold-load tooltip); the chip selector falls
  back to `thread.metadata.model`.

Also fixes a latent bug both paths shared: finalize read `totalTokens` off
`currentAssistantMsgId`, which by then points at the freshly-created terminal
assistant (no usage), so it always resolved `undefined`. Now tracks the last
non-zero per-turn `totalTokens` on the run — matching the live selector's
"last turn, not a sum" convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(cc): derive subagent chip metrics on read, drop run-state tracking

The chip's tool-count / token / model metrics were captured incrementally on
the subagent run (`lastTurnTokens` / `subagentModel`) and denormalized onto
`thread.metadata` at finalize — in BOTH the renderer executor and the server
handler, so the rule lived in three places and the two finalize paths had to
be kept in sync by hand.

Derive them on read instead, from the child messages (the single source of
truth):

- `aggregateSubagentMetrics(messages)` (new, `src/utils`) is the one rule:
  COUNT `role='tool'`, SUM every assistant turn's `usage.totalTokens`, pin the
  model. SUM (not last-turn) matches the project's token-usage heatmap
  convention — "total tokens processed".
- The chip selector aggregates the in-memory child messages live, falling back
  to `thread.metadata.*` on cold-load.
- `threadModel.queryByTopicId` computes the SAME projection in SQL (LEFT JOIN +
  GROUP BY, reusing the `usage->totalTokens` index, with a legacy
  `metadata.usage` fallback) and folds it onto `metadata`, so cold-load reads a
  server-derived value without hydrating the child messages.

Both finalize paths drop the metadata rollup and now only flip thread status
Active; `lastTurnTokens` / `subagentModel` run-state fields are gone. Each
subagent turn still writes its `usage` + `model` onto the in-thread assistant —
those rows are what the read-time aggregation sums over.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-07 02:21:53 +08:00
Arvin Xu 28f0117932 💄 style(tool-ui): render ANSI escape codes in RunCommand output (#15516)
 feat(tool-ui): render ANSI escape codes in RunCommand output

Parse ANSI SGR sequences in shell stdout/stderr with anser and emit
styled spans for fg/bg colors, dim, bold, italic, underline, strikethrough.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 01:34:16 +08:00
Arvin Xu 573cc5b798 💄 style(desktop): move panel toggle into titlebar top-left (#15515)
*  feat(desktop): move panel toggle into titlebar top-left

Place a persistent collapse/expand toggle at the titlebar's top-left
corner on desktop, to the right of the macOS traffic lights. The
NavigationBar now splits into a left group (toggle) and a right group
(back / forward / clock) with space-between: expanded, the right group
hugs the sidebar's right edge; collapsed, the controls cluster at the
left edge like codex.

ToggleLeftPanelButton gains an optional `id` prop so the titlebar
instance can opt out of the shared TOGGLE_BUTTON_ID, avoiding a
duplicate DOM id and NavPanelDraggable's hover-reveal CSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(desktop): expand untracked directories in git status

`git status --porcelain` defaults to `--untracked-files=normal`, which
collapses whole untracked directories into a single `?? path/` entry.
That trailing-slash path then flowed into `readUntrackedAsPatch` as if
it were a file — `stat()` reported `isFile()=false`, an empty patch was
returned, and the Review panel rendered "无法加载该文件的 diff" against
a directory row. Pass `-u` so git expands those directories into their
individual files; each file then produces a real synthetic patch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(desktop): scope titlebar toggle to macOS, hide in-page toggles there

The persistent titlebar toggle now renders only on macOS; Windows/Linux
keep the original right-aligned navigation controls and their in-page
toggles.

On macOS desktop, ToggleLeftPanelButton instances hide themselves (the
titlebar owns the control) unless `forceVisible` is set, removing the
now-redundant sidebar-header and content-header toggles. NavHeader also
skips rendering its empty toggle-only bar in this case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:42:57 +08:00
Rdmclin2 7b54edc665 🐛 fix(database): scope ai-infra upsert conflict targets to workspace (precursor for 0110) (#15507)
🐛 fix(database): scope ai-infra upsert conflict targets to personal partial index

The 0110 migration replaces the (id, user_id) / (id, provider_id, user_id)
primary keys with partial unique indexes (WHERE workspace_id IS NULL). A bare
ON CONFLICT target can no longer infer a partial index, so add
`targetWhere: isNull(workspaceId)` (and `where` for onConflictDoNothing) to
every personal-scope upsert. Keeps existing provider/model toggling, ordering
and batch upserts working after the migration.
2026-06-07 00:40:08 +08:00
Arvin Xu b6ae130c97 feat(agent): auto-scan project workspace (skills + AGENTS.md) for server agents (#15512)
*  feat(agent): auto-scan project workspace (skills + AGENTS.md) for server agents

When a server agent runs against a bound project directory, scan it server-side
at run start for project skills (.agents/skills + .claude/skills) and root
AGENTS.md/CLAUDE.md, cache the result on devices.workingDirs[].workspace (1h TTL),
surface skills in <available_skills>, and inject instructions into the system role.
Replaces the desktop-only client pre-scan so it works for any run initiator.

- Generic device RPC channel (invokeRpc / rpc_request) for server-internal device
  methods, separate from the LLM-facing tool-call path
- New desktop WorkspaceCtr owns project-skill / workspace scanning

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent): preserve workspace-init cache on device cwd save

device.updateDevice validates workingDirs as { path, repoType } only, so zod
strips the server-written workspace / workspaceScannedAt cache — an ordinary cwd
pick wiped the 1h workspace-init cache (and web reuse), forcing every later run
to rescan. The cache is server-owned, so re-attach it by path from the stored
row instead of trusting the client to round-trip it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:26:48 +08:00
Arvin Xu 5b5794baa4 ♻️ refactor(server): rename deviceProxy → deviceGateway (#15513)
Pure mechanical rename of the server device-relay module/class/singleton
(deviceProxy → deviceGateway, file included) to match the underlying
GatewayHttpClient naming. No behavior change. Split out of the workspace-init
feature PR (lobehub/lobehub#15512) to keep that diff reviewable.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:07:30 +08:00
Arvin Xu 04700bed52 feat(agent-runtime): server callSubAgent async suspend/resume (#15481)
*  feat(agent-runtime): add waiting_for_async_tool parked state for deferred tools

Add a dedicated `waiting_for_async_tool` operation status that mirrors
`waiting_for_human` as a non-terminal, resumable pause, and migrate the
client-tool execution pause off `interrupted` onto it — so `interrupted`
once again means only user-initiated cancellation.

Also add the AgentOperationModel primitives the upcoming server sub-agent
bridge needs: queryByParentOperationId (reconcile child ops) and
tryResumeFromAsyncTool (atomic single-fire CAS).

Foundation for the server sub-agent suspend/resume mechanism (LOBE-9763).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): extract isParkedStatus / isBlockedStatus predicates

Replace the repeated `status === 'waiting_for_human' || ... === 'waiting_for_async_tool' || ... === 'interrupted'`
chains with named predicates so the parked/blocked semantics live in one place
(runtime step-loop break, completion lifecycle completedAt, executeSync pause,
operation isActive).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(aiAgent): rename execSubAgentTask -> execSubAgent

Full rename of the service method, its `ExecSubAgentTaskParams`/`ExecSubAgentTaskResult`
types, the tRPC endpoint, the injected `RuntimeExecutorContext`/`AgentRuntimeServiceOptions`
callback, and tests. Group-mode `execGroupSubAgent*` identifiers are intentionally left
untouched. Prep for the server sub-agent suspend/resume work (LOBE-9763).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Revert "♻️ refactor(aiAgent): rename execSubAgentTask -> execSubAgent"

This reverts commit f1ea407d74.

*  feat(agent-runtime): add deferred-tool park infrastructure

Introduce a generic `deferred` result flag (BuiltinServerRuntimeOutput /
ToolExecutionResult). When a tool returns deferred, call_tool parks the
operation (waiting_for_async_tool + pendingToolsCalling) without writing a
tool_result — mirroring the client-tool pause — so the result can be
delivered out-of-band later by a completion bridge. Thread the existing
execSubAgentTask DI seam into ToolExecutionContext so async tools can spawn
a child op without a circular import.

Part of the server sub-agent suspend/resume mechanism (LOBE-9763).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(agent-runtime): park call_tools_batch on deferred tools

Mirror the call_tool deferred-park on the parallel path: deferred (async)
tools are collected during the concurrent batch and, once server tools
settle, the operation parks (waiting_for_async_tool + pendingToolsCalling)
alongside any client tools — so K parallel sub-agents in one round all
resolve before the parent resumes.

Part of the server sub-agent suspend/resume mechanism (LOBE-9763).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(agent-runtime): server callSubAgent async suspend/resume bridge

Turn the server `callSubAgent` path from fire-and-forget into a real
deferred-tool suspend/resume loop (LOBE-9763 Phase 2):

- lobeAgent server runtime: add `callSubAgent` executor returning a
  `deferred` result via an injected `ctx.subAgent` runner
- RuntimeExecutors: build a per-tool-call server sub-agent runner that
  creates the pending placeholder tool message (anchoring the isolation
  thread) and kicks off the child op
- aiAgent.execSubAgentTask: register an onComplete bridge hook that
  backfills the placeholder and resumes the parent
- AgentRuntimeService: `tryResumeParentFromAsyncTool` (barrier over
  pendingToolsCalling + single-fire CAS + schedule), `refreshMessagesFromDB`,
  and the `resumeAsyncTool` branch in executeStep
- queue/local: forward `payload` to the execution callback so local/in-memory
  resumes (and human-approval) no longer drop their signal

Tests: callSubAgent executor unit tests, tryResumeParentFromAsyncTool
barrier/CAS unit tests, and a server suspend/resume integration test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-runtime): keep hooks across waiting_for_async_tool park

The async sub-agent resume reuses the SAME operationId, but dispatchHooks
fired onComplete and unregistered all hooks on every non-continue step —
including the waiting_for_async_tool park. That made completion consumers
(webhooks, bot promises, eval snapshots) fire prematurely on the park and
miss the real terminal state after resume.

For waiting_for_async_tool, persist the parked status (the resume CAS reads
it) but skip onComplete and keep hooks registered, so the eventual resume
under the same op still notifies consumers. waiting_for_human is unchanged
(its resume runs under a new operationId).

Found via the server-subagent agent-eval (real LLM, in-memory runtime):
parent now correctly reaches `done` after the sub-op completes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-runtime): unwrap QStash body.payload in runStep handler

QStashQueueServiceImpl nests resume/intervention fields under `body.payload`
(operationId/stepIndex/context stay top-level), but the runStep handler
destructured them from the top level. In production/QStash the resumed step
therefore saw `resumeAsyncTool` (and approvedToolCall/toolMessageId/…) as
undefined and never ran the waiting_for_async_tool DB-refresh/clear-pending
branch — the parent op would stay parked forever. The local queue spreads
payload itself, which masked this in local/eval runs.

Merge `body.payload` over the top-level body so both shapes work. Adds a
handler test asserting the QStash-nested payload reaches executeStep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-runtime): unpark parent when callSubAgent fails to start

When a server callSubAgent child op fails to start, no completion bridge
ever fires, so the parent stayed parked in `waiting_for_async_tool`
forever. The runner now drops the placeholder and signals `started:false`
so callSubAgent surfaces an inline tool error instead of parking the
parent — the batch continues (or parks only for genuinely-deferred
siblings, whose barrier already counts this error result).

Also:
- add isParkedStatus/isBlockedStatus to the @lobechat/agent-runtime test
  mock — persistCompletion/getOperationStatus call isParkedStatus, so the
  missing export crashed dispatchHooks (swallowing onComplete) and
  getOperationStatus, failing 3 AgentRuntimeService tests.
- fix completion-bridge totalToolCalls path (finalState.session.toolCalls
  → finalState.usage.tools.totalCalls; the former never existed).
- remove dead AgentOperationModel.queryByParentOperationId (zero callers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 22:46:10 +08:00
Arvin Xu ad87e43b2e feat(agent-tracing): tool-result feedback quality analysis (tq command) (#15508)
*  feat(agent-tracing): add tool-result feedback quality analysis (tq command)

Adds a shared, no-LLM analyzer that scores how "clean / LLM-friendly" the
environment feedback (tool return content) is, plus an `agent-tracing tq`
CLI command to preview it over a snapshot corpus.

- src/analysis/toolFeedback.ts: pure analysis lib (reusable core) — per
  tool-result metrics (tokens, self-redundancy, structural-noise ratio,
  error flag/size, format) + op-level and corpus-level rollups.
- src/cli/tool-quality.ts: `tq` (alias `tool-quality`) — token-size
  histogram, dirty leaderboard ranked by token-weighted waste, single-op
  drill-down, and --json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(agent-tracing): guard against undefined histogram bucket in buildCorpusReport

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:31:06 +08:00
Arvin Xu 32c293f8c0 feat(claude-code): add per-question custom input to askUserQuestion (#15506)
*  feat(claude-code): add per-question custom input to askUserQuestion

Let users write their own answer as the trailing item in each question's
option list, beside picking a numbered choice. Single-select treats the two
as mutually exclusive; multi-select appends the custom text as an extra
entry. Merged into the question's answer at submit, so the bridge formatter
and completed Render need no changes. Draft round-trips via a __custom__:
prefix on the existing askUserDraft map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(claude-code): split askUserQuestion form & drop draft key prefix

Break the single ~530-line AskUserQuestion.tsx into a folder:
- draft.ts        pure helpers (read/buildSubmitPayload/isQuestionAnswered)
- useAskUserForm.ts  all state + handlers + draft persistence
- OptionCard.tsx / QuestionPanel.tsx  presentational pieces
- index.tsx       thin view

Also drop the `__custom__:<question>` draft-key prefix: persist the draft as
a typed object { picks, custom, escapeText, escapeActive } instead of a flat
string-keyed map. The picks/custom split now lives in named fields, so the
only sentinel left is `__freeform__` — and only in the submit payload, which
is the actual bridge contract. No behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(claude-code): make AskUserDraft assignable to setInterventionDraft

`setInterventionDraft` takes `Record<string, unknown>`; an `interface` isn't
assignable to it (open to declaration merging, so no implicit index
signature). Switch `AskUserDraft` to a `type` alias, which is closed and
satisfies the index signature. Fixes the tsgo TS2345 in CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:30:19 +08:00
LiJian 6f5a633c9f feat(connector): Connectors system — API-level tool permissions with plugin fallback (#15463)
*  feat(connector): add ConnectorModel, ConnectorToolModel, tRPC router, and inferCrudType util (LOBE-9984, LOBE-9985)

- packages/database/src/models/connector.ts: ConnectorModel with create/delete/query/queryByIdentifiers/findById/update/updateStatus
- packages/database/src/models/connectorTool.ts: ConnectorToolModel with upsertMany (preserves user permission on sync), updatePermission, queryByConnector, queryByConnectorIds
- src/libs/mcp/utils.ts: inferCrudType() — name-based CRUD type inference (delete > update > read > write)
- src/server/routers/lambda/connector.ts: tRPC router with list/create/update/delete/syncTools/updateToolPermission
- src/server/routers/lambda/index.ts: register connectorRouter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): runtime integration — connector-first tool resolution with plugin fallback (LOBE-9986)

- src/libs/mcp/buildConnectorManifests.ts: converts user_connector_tools rows into LobeToolManifest entries; maps permission → humanIntervention ('needs_approval' → 'required', 'disabled' → excluded)
- src/server/services/aiAgent/index.ts:
  - queryByIdentifiers(agentPlugins) to find matching connectors first
  - filter installedPlugins to exclude connector-covered identifiers
  - inject connectorManifests as additionalManifests into createServerAgentToolsEngine
  - add connector stdio tools to client executor map

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): add connector Zustand store slice (LOBE-9987)

- src/store/tool/slices/connector/: new slice with ConnectorState, ConnectorAction, connectorSelectors
  - fetchConnectors, createConnector, deleteConnector, syncConnectorTools, disconnectConnector
  - updateToolPermission with optimistic update + rollback
  - connectorToolsGrouped selector splits tools into read / write groups
- Wired into ToolStore (initialState + store.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): add Connectors UI feature — list, detail, tool permission editor (LOBE-9988)

- src/features/Connectors/: new feature with two-panel layout (list + detail)
  - ConnectorList: groups connectors by Connected / Not connected, Add button
  - ConnectorDetail: sync button, disconnect, tool permission groups (read/write)
  - ToolPermissionGroup: collapsible with batch set (auto/approval/disable all)
  - ToolPermissionRow: three-state toggle auto(✓) / needs_approval() / disabled(🚫)
  - AddConnectorModal: name + MCP URL input via @lobehub/ui/base-ui Modal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): add Connectors tab to Agent customization panel (LOBE-9989)

- src/store/global/initialState.ts: add ChatSettingsTabs.Connector = 'connector'
- src/features/AgentSetting/AgentCategory/useCategory.tsx: add Connectors tab with LinkIcon
- src/features/AgentSetting/AgentConnectors/: new component listing user connectors with toggle
  - toggle calls toggleAgentPlugin(connector.identifier) — reuses agents.plugins[] field
  - shows per-connector tool count
- src/features/AgentSetting/AgentSettingsContent.tsx: render AgentConnectors for Connector tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): wire Connectors feature to /settings/connector route

- src/store/global/initialState.ts: add SettingsTabs.Connector = 'connector'
- src/routes/(main)/settings/hooks/useCategory.tsx: add Connectors item (LinkIcon) after Skills in AI config group
- src/routes/(main)/settings/features/componentMap.ts: map SettingsTabs.Connector → '../connector'
- src/routes/(main)/settings/features/SettingsContent.tsx: render Connector tab full-width (no SettingContainer), same as Provider
- src/routes/(main)/settings/connector/index.tsx: route page rendering the Connectors feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): use cssVar.property syntax in createStaticStyles (not function call)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): refactor /settings/skill to unified master-detail tool manager

## Backend
- connector.ts: add syncBuiltinTool — bootstraps user_connectors from builtin manifest api[]
- connector.ts: add syncPluginTools — bootstraps user_connectors from user_installed_plugins manifest
- connector.ts: upsertConnectorEntry helper + resolveDefaultPermission (maps humanIntervention → permission)
- connectorTool.ts: SyncToolInput.defaultPermission — per-tool default for new rows, existing rows preserved

## Store
- connector/selectors.ts: add connectorByIdentifier, connectorToolsGroupedByIdentifier, isSyncingByIdentifier
- connector/action.ts: add syncBuiltinTool, syncPluginTools (idempotent — safe to call on panel open)

## /settings/skill refactor
- index.tsx: two-panel master-detail layout (left: 300px skill list, right: detail + permissions)
- SkillList: add onSelect + selectedIdentifier props, pass through to builtin/mcp items
- BuiltinSkillItem: add onSelect + isSelected (selection highlight, click triggers right panel)
- McpSkillItem: add onSelect + isSelected
- SkillDetail (new): auto-syncs connector entry on mount, then renders ConnectorDetail permission editor
- SettingsContent: Skill tab now renders full-width (same as Provider/Connector)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(skill): createStaticStyles returns static object, not a hook

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(skill): wire onSelect to all skill item types — LobehubSkillItem, KlavisSkillItem + error handling in SkillDetail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): use createStaticStyles correctly — static object, not hook; use string concat instead of cx()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): whole row clickable in list mode, hide action buttons when onSelect provided

All 5 item types (Builtin/Mcp/Lobehub/Klavis/AgentSkill):
- When onSelect is provided (list mode): entire row is clickable, action buttons hidden
- When onSelect is not provided (other usages): original behavior preserved
- Added onSelect/isSelected to AgentSkillItem + wired in SkillList for all agent skill types
- SkillDetail: show friendly message instead of error when skill has no tool permissions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): route sync action by sourceType; improve no-tools skill UI

ConnectorDetail:
- builtin → Reset (syncBuiltinTool from local manifest, resets permissions to defaults)
- marketplace → Refresh (syncPluginTools from installed plugin manifest)
- custom MCP → Sync (syncTools via remote MCP server, existing behavior)
- Hide Disconnect button for builtin/marketplace (only MCP connectors can disconnect)
- Show 'No tool permissions' message when connector has 0 tools
- Fix hooks-rules violation: move useCallback before early return

SkillDetail:
- Catch sync failure cleanly — shows graceful 'no tool permissions' panel
- Show skill identifier as title even when no tools available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): inline AgentSkillDetail for agent skills; clean ConnectorDetail layout

SkillDetail:
- Add 'agent-skill' ToolDetailType — renders AgentSkillDetail inline (no modal, no connector sync)
- All hooks called before conditional returns (fixes rules-of-hooks)

SkillList:
- Pass type='agent-skill' for market/user agent skills (UUID identifiers, not plugin identifiers)

ConnectorDetail:
- Remove 'Tool permissions / Choose when AI...' subheader — tool groups render directly
- Cleaner layout: name → sync/disconnect buttons → tool groups

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): description in ConnectorDetail header + builtin-skill detail panel

Backend (connector.ts):
- syncBuiltinTool: store manifest meta.description + meta.avatar in connector.metadata
- syncPluginTools: same for plugin manifest meta
- upsertConnectorEntry: always update metadata on re-sync (keeps description fresh)

ConnectorDetail:
- Show connector.metadata.description below name in header

SkillDetail:
- Add 'builtin-skill' ToolDetailType for builtinSkills (Artifacts, Task, AgentBrowser)
  → Shows avatar + name + description panel; no connector sync needed (prompt-based)
- Add 'builtin-skill' type: reads from store builtinSkills array by identifier

SkillList:
- builtinAgent items → pass type='builtin-skill' (not 'builtin') to SkillDetail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): fix crudType for camelCase, show skill content, compact items + categorized groups

inferCrudType (utils.ts):
- Fix: use prefix ^ anchoring instead of \b word boundary
- getReactions/listPins/searchMessages now correctly → 'read' (not 'write')
- \b fails on camelCase: 'getreactions' has no boundary after 'get' (both \w chars)

SkillDetail:
- builtin-skill type: render builtinSkill.content via <Markdown variant='chat'>
- Artifacts/Task/LobeHub skills now show their full markdown content in right panel

style.ts:
- Compact skill items: icon 48→36px, padding-block 12→6px

SkillList:
- Remove old flat renderIntegrations() + Divider
- Add categorized sections with headers:
  LobeHub 内置 Tools | 内置 Skill | 社区 Skill | 社区 Tools | 自定义
- Add sectionHeader style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): collapsible sections, compact items matching reference design

style.ts:
- icon: 28→24px, no background (reference style: plain icon, no container bg)
- padding-block: 4→3px, font-size: 13px
- sectionHeader: collapsible with hover state

SkillList:
- Sections are collapsible — click header to toggle
- ChevronDown/ChevronRight icons on section headers
- All renderSection calls now pass a unique key

All item components (Builtin/Mcp/Lobehub/Klavis/AgentSkill):
- gap: 16→8px (tighter horizontal spacing)
- avatar/icon: 32→22px (matches reference ~24px icon)
- In list mode (onSelect): tag moves to RIGHT side of row
- In list mode: remove tag from title area, status text below title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): default select first item; + button opens Add custom connector modal

index.tsx:
- Auto-select first installed builtin tool (or first builtin skill) on page load
- + button → opens AddConnectorModal (add custom MCP connector)
- 技能商店 button → still opens skill store (unchanged)

AddConnectorModal:
- Add Advanced settings section (collapsible chevron)
- OAuth Client ID field → stored in oidcConfig.clientId
- OAuth Client Secret field (UI only, encryption path TBD)
- Clear all fields on cancel/submit

Connectors/index.ts: export AddConnectorModal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): reference-quality UI polish + Connectors/Skills tab switcher

Style polish (matching linear-tool-permissions demo):
- style.ts: icon 20px, padding-block 6px, font-size 14px (no bold)
- All item avatars: 16px
- ToolPermissionRow: py-10px px-12px, font-mono tool names, 15px icons, hover bg
- ToolPermissionGroup: rounded badge for count, outline 'Custom ▾' batch button
- ConnectorDetail: restore 'Tool permissions' h3 + subtitle

Connectors/Skills tab switcher:
- Top of left panel: Connectors tab | Skills tab
- Connectors: builtin tools + OAuth connectors + community/custom MCPs
- Skills: builtin agent skills + community/user agent skills
- Switching tabs resets selection and auto-selects first item in new view
- + button only shown in Connectors view

SkillList: add viewMode='connector'|'skill' prop with filtered section display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(skill): active permission state + Lobehub OAuth skill tools sync

ToolPermissionRow:
- btnActive: use primary color + primaryBg background (clearly visible selected state)

connector router:
- Add syncToolsFromClient: accepts client-provided tool list for skills that already
  have their tool list fetched (Lobehub OAuth skills, etc.)

Store action:
- Add syncToolsFromClient action

SkillDetail:
- Add 'lobehub-connector' ToolDetailType
- For lobehub-connector: reads server.tools from lobehubSkillStore (already populated
  after OAuth connect) and syncs via syncToolsFromClient — no remote MCP call needed

SkillList:
- Pass type='lobehub-connector' for Lobehub OAuth items (was 'plugin', wrong path)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(connector): replace 'Tool permissions' header with connector description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): show disabled tools in settings UI (only filter at runtime)

connectorToolsGrouped: remove permission !== disabled filter — all tools should
be visible in ConnectorDetail so users can re-enable them. Disabled filtering
already happens at runtime in buildConnectorManifests and queryByConnectorIds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(skill): section lowercase, 4-group tools, remove tags in list mode

SkillList: remove text-transform: uppercase from sectionHeader
ConnectorDetail: split tools into 4 groups — Read / Create / Update / Delete
  (maps to crudType: read / write / update / delete)
connectorToolsGrouped selector: return { readTools, createTools, updateTools, deleteTools }
All item components: remove SkillSourceTag in list mode (onSelect provided)
  — tags are redundant when section headers already provide categorization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): add Reset permissions button — restore all tools to auto

connector router: resetPermissions endpoint — sets all connector's tools to 'auto'
store: resetConnectorPermissions action
ConnectorDetail:
- Add 'Reset permissions' button — resets ALL tools back to auto (fully open)
- Rename 'Reset'/'Refresh' button to 'Refresh' — clarifies it syncs tool list only
- Two separate concerns: Refresh (tool list) vs Reset permissions (all → auto)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): use excluded.* in onConflictDoUpdate to ensure crudType updates + add description to tool rows

connectorTool.ts:
- Use sql`excluded.crud_type` etc. instead of table.column refs in onConflictDoUpdate
- table.column in set generates self-reference (no-op) in some Drizzle versions
- Now correctly updates crudType when Refresh is clicked (read/update/delete groups will show correctly)

ToolPermissionRow:
- Add description below tool name: 11px, tertiary color, single-line truncate with ellipsis
- Tooltip shows full description on hover (mouseEnterDelay: 0.5s)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): createStaticStyles returns static object not hook in ConnectorItem

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🗑️ chore(settings): remove /settings/connector route — Connectors are in /settings/skill

- Remove src/routes/(main)/settings/connector/index.tsx
- Remove SettingsTabs.Connector from enum and componentMap
- Remove Connectors item from settings sidebar useCategory
- Remove Connector from full-width list in SettingsContent
- Remove unused LinkIcon import from useCategory

ChatSettingsTabs.Connector (agent panel) is separate and unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): disabled tools stay in manifest with blocking description + hard-block at callTool

buildConnectorManifests:
- Disabled tools are now INCLUDED in the manifest (not excluded)
- Description replaced with: '[TOOL DISABLED] The user has disabled this tool and it cannot be executed...'
- humanIntervention: 'required' set for disabled tools so AI is explicitly warned
- AI can inform user the tool is disabled instead of silently not knowing it exists

mcp.callTool:
- Pre-call permission gate: query ConnectorModel + ConnectorToolModel by connector identifier
- If tool.permission === 'disabled': return immediately with "disabled by user" message
- MCP server is never called — the block is enforced server-side regardless of what AI attempts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): add permission gate to klavis.callTool for disabled tools

Gmail (and other Klavis-sourced connectors) use tools.klavis.callTool,
not tools.mcp.callTool, so the previous MCP permission gate didn't apply.

Fix: Add serverDatabase to klavisProcedure, extract connector identifier from
toolName prefix, query user_connector_tools, hard-block if permission=disabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🗑️ chore(skill): hide + button (custom MCP connector creation — OAuth flow TBD)

Remove AddConnectorModal entry point from /settings/skill header.
Custom HTTP MCP connectors require OAuth (Pre-registration / DCR) which
is not yet fully implemented. Will be re-added in a future PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): only replace plugins with connectors that have a real MCP endpoint

Root cause: Lobehub/Klavis OAuth skills are synced into user_connectors via
syncToolsFromClient with mcpServerUrl=null. buildConnectorManifests generates
mcpParams={url:''} for them. After humanIntervention approval, the runtime calls
tools.mcp.callTool({url:''}) → fails silently → empty result.

Fix: only use connectorsMcp (connectors with mcpServerUrl or stdio config) to
replace installedPlugins and build connector manifests. Connectors without a real
MCP endpoint (Lobehub/Klavis) fall back to their original plugin executor path,
preserving the Klavis callTool execution chain and fixing needs_approval flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(connector): centralized tool permission enforcement across all execution paths

connectorPermissionCheck.ts (new shared utility):
- getConnectorToolPermission(): look up permission by identifier + toolName
- buildBlockedToolResponse(): standardized "disabled by user" response
- patchManifestWithPermissions(): patch manifest api[] with DB permissions

ToolExecutionService.executeTool() — centralized disabled gate:
- Queries DB at execution entry for ALL tool types (Lobehub skills, Klavis,
  MCP connectors, builtin plugins, and qstash/execAgent async path)
- Hard-blocks 'disabled' tools before any executor runs
- needs_approval handled by manifest humanIntervention (not blocked here)

aiAgent/index.ts — manifest patching for Lobehub/Klavis:
- After fetching lobehubSkillManifests + klavisManifests, query connector tools
- Patch manifests: needs_approval → humanIntervention:'required' (pauses for approval)
- Patch manifests: disabled → blocking description (AI informed, executor blocks)
- humanIntervention system already handles headless auto-reject for qstash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): invokeBuiltinTool falls back to store lookup when payload.source is undefined

Root cause: when a tool call is re-invoked after humanIntervention approval,
the payload comes from the DB-stored message which does NOT persist the `source`
field. `internal_transformToolCalls` sets source correctly but it only runs for
LLM-generated tool calls, not for the approval re-invocation path.

Fix: in `invokeBuiltinTool`, if `payload.source` is undefined, do a live lookup
from the tool store (klavisAsLobeTools / lobehubSkillAsLobeTools) to determine
the correct executor. Applies to Klavis (Gmail, etc) and LobeHub Skills alike.

Also: remove all temporary [DEBUG] console.log statements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 chore: fix TypeScript errors and test failures after canary rebase

- buildConnectorManifests: LobeToolManifest → ToolManifest (correct export name)
- connectorPermissionCheck: cast permission string to ConnectorToolPermission
- connector.ts model: guard encryptCredentials against null credentials
- ConnectorDetail: String() cast for unknown metadata.description
- AddConnectorModal: move loading to Modal.confirmLoading (correct prop)
- connector/action.ts: break circular ToolStore type reference with Pick<Impl>
- execAgent.disableTools.test.ts: mock ConnectorModel/ConnectorToolModel DB deps

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(connector): P1/P3 fixes + test mock coverage after code review

P1 — real MCP disabled tools now appear in manifest:
- ConnectorToolModel.queryAllByConnectorIds: new method without disabled filter
- aiAgent.ts: uses queryAllByConnectorIds for manifest building so buildConnectorManifests
  receives ALL tools (including disabled) and can emit blocking descriptions
- queryByConnectorIds (non-disabled filter) retained for runtime hot-path

P1 — Klavis gate works for hyphenated identifiers (google-calendar, etc):
- klavis.ts: replace split('_')[0] prefix hack with direct findByToolName DB lookup
- ConnectorToolModel.findByToolName: query user_connector_tools by userId + toolName

P3 — queryByConnector adds userId filter:
- Prevents leaking tool metadata to wrong user if connector UUID is known

Tests — mock ConnectorModel/ConnectorToolModel in all execAgent test files:
- execAgent.builtinRuntime.test.ts
- execAgent.deviceToolPipeline.test.ts
- execAgent.disableTools.test.ts (queryAllByConnectorIds added to mock)

TypeScript — ConnectorDetail metadata.description:
- Use typeof === 'string' type guard to narrow unknown → string for JSX render

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 fix(connector): precise Klavis permission gate + update stale disabled comments

Klavis gate — identifier + toolName (precise, no same-name collision risk):
- CallKlavisToolParams: add identifier? field
- klavisExecutor: pass identifier to callKlavisTool
- callKlavisTool store action: thread identifier through to tRPC mutate
- klavis.callTool router: accept optional identifier in input schema
- Permission gate: when identifier present, do queryByIdentifiers + queryByConnector
  + find by toolName for a precise 2-field lookup; fall back to findByToolName for
  legacy callers without identifier

Comments updated to reflect current disabled behavior:
- buildConnectorManifests.ts: disabled → injected with blocking description
- connector.ts schema: same correction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:03:56 +08:00
AmAzing- 485d664589 💬 style: rebrand platform agent copy to Connect Agent (#15498) 2026-06-06 09:55:34 +08:00
Arvin Xu b1ada9e5fc 🐛 fix(conversation): hide Usage extra for local hetero agents until model arrives (#15501)
Local CLI hetero agents (claude-code, codex) only report `model` after
turn_metadata lands mid-stream. The previous `showUsage` check used the
broad `HETEROGENEOUS_TYPE_LABELS` lookup which matches both local and
remote types, so it returned true with an empty model. Usage then fell
through to the `ModelIcon` path (Usage uses the narrower
`isRemoteHeterogeneousType` for the brand-label branch) and rendered a
lone empty-model placeholder icon under the message.

Align the gate with Usage's internal branching: only bypass `!!model`
for remote hetero (openclaw, hermes) which never expose a real model id.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 01:49:49 +08:00
Arvin Xu 5dc769d135 🐛 fix(agent-signal): attribute self-iteration run trace to reviewed agent & isolate memory runs (#15479)
Background Agent Signal runs (memory / skill / self-reflection) execute under a
builtin agent slug. Two attribution gaps caused their traces to surface in the
wrong place:

- execAgent persisted the run's user + assistant message rows under the builtin
  slug's agent id, while the operation row, isolated thread, and receipts all
  attribute to the reviewed user agent on `marker.agentId`. The trace therefore
  "hung" under the builtin reflection/skill agent. Persist messages under
  `marker.agentId` when present, falling back to the executing agent otherwise.

- The memory run only created its isolated thread when an `assistantMessageId`
  could be extracted from a `clientRuntimeComplete` source id
  (`${assistantMessageId}:completion:${parentMessageId}`). Any other source left
  it undefined, skipping thread creation so the memory-agent messages leaked
  into the active conversation. Fall back to the triggering user `messageId` so
  a child thread is still created.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 01:42:48 +08:00
Arvin Xu 64b7ab2f17 💄 style(topic): one-click collapse/expand all topic groups (#15484)
*  feat(topic): add one-click collapse/expand all groups in topic sidebar

Add a toggle button in the topic sidebar header (next to Filter and the
more-actions menu) that collapses or expands all topic groups at once.
It reuses the existing `expandTopicGroupKeys` global status, so it stays
in sync with manual per-group toggling, and hides itself when there are
fewer than two groups (e.g. flat mode).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(topic): hide group toggle in flat mode

In flat mode, groupedTopicsForSidebar falls through to time grouping so
the computed group count can exceed one, but List renders FlatMode with
no accordion for the toggle to affect. Hide the control explicitly when
topicGroupMode === 'flat' instead of relying on the group count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(topic): use 2-corner minimize/maximize icons for group toggle

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 01:33:32 +08:00
Arvin Xu 9c4dadda4c feat(task-detail): replace inline comment input with ChatInput that triggers a new run (#14873)
*  feat(task-detail): split task panel comment from topic-thread reply

CommentInput in TaskActivities stays as-is on canary — avatar + EditorCanvas
+ attachment + send button, posting a plain task-level comment.

TopicChatDrawer footer becomes a FeedbackInput that calls the in-scope
ConversationProvider's sendMessage, continuing the existing topic
conversation instead of attaching a comment + restarting the run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(task-detail): keep FeedbackInput visible while topic is running

Drop the canLeaveFeedback gate so the in-thread reply box renders even
when the topic is pending/running. ConversationStore.sendMessage already
queues messages during an in-flight stream, so this just exposes the
queue affordance to the user — letting them steer the next step
without waiting for the current run to terminate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(task-detail): collapse FeedbackInput behind a follow-up button + add attach action

FeedbackInput now starts collapsed as a full-width "Send follow up message"
button. Click expands a ChatInput shell with EditorCanvas inside and a footer
that carries an AttachmentUploadButton on the left (+ icon) and the send
button on the right. Files are inserted inline into the editor (same
pattern as CommentInput) so they ride along on sendMessage's editorData.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(task-detail): tighten CommentInput card & switch follow-up button to filled

- CommentInput card: padding-block 8px → 4px, editor placeholder fontSize 14px
- FeedbackInput collapsed button: default size + variant="filled" for a less
  obtrusive look that sits flush in the chat footer

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(task-detail): drop top padding above FeedbackInput in topic drawer

Use paddingBlock="0 12px" so the follow-up button hugs the last message
instead of floating with a 12px gap above.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(task-detail): clear FeedbackInput editor before awaiting sendMessage

Previously the editor cleanup ran after the awaited sendMessage call, so
the box kept the just-sent text on screen until the entire send + stream
lifecycle resolved. Move clearContent / collapse before the await so the
input feels responsive (sendMessage already snapshots markdown and
editorData for its optimistic update).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(task-detail): keep FeedbackInput expanded after sending

Drop the setExpanded(false) call in handleSubmit so the ChatInput
remains open once the user has opened it. Collapsing it back to the
"Send follow up message" button right after every reply was disruptive
mid-conversation; the button only makes sense as the initial resting
state of the drawer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(chat): add forceRuntime override to SendMessageParams

Plumb a new optional forceRuntime field through SendMessageParams →
ConversationLifecycle.sendMessage → selectRuntimeType(parentRuntime).
parentRuntime already wins over every other signal in the dispatcher,
so callers can pin a send to 'gateway' / 'client' / 'hetero' regardless
of the agent's local/cloud config.

Also propagate forceRuntime through the message queue (QueuedMessage +
MergedQueuedMessage + mergeQueuedMessages + both drain sites in the
client and hetero executors) so a follow-up queued during an in-flight
run keeps its runtime pin when it eventually fires.

FeedbackInput in TopicChatDrawer passes forceRuntime: 'gateway' so
task-topic follow-ups stay on the server-side path that runTask
originally used, even if the user's global runtime preference is local.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 01:26:25 +08:00
AmAzing- ab7cb07ae5 🐛 fix: type errors in oidc http-adapter test breaking CI lint (#15499) 2026-06-06 01:24:12 +08:00
Rylan Cai 596440901d 🐛 fix: auto-run required tools in headless mode (#15492) 2026-06-06 00:40:24 +08:00
YuTengjing 2b9f08a43b 🐛 fix: timeout Market connection listing (#15487) 2026-06-05 13:27:08 +08:00
YuTengjing 95a0cf1264 🐛 fix: handle runtime request errors (#15478) 2026-06-05 13:13:56 +08:00
Innei 65ba086685 🐛 fix(agent-documents): render system docs in editor (#15462)
* 🐛 fix(agent-documents): render system docs in editor

*  feat(agent-documents): autosave highlight editor with safe unmount flush

Add debounced autosave to the non-markdown highlight editor and a StrictMode-safe
unmount flush via queueMicrotask, plus a beforeunload guard against dirty buffers.

*  test: fix agent document PR type checks
2026-06-05 10:22:31 +08:00
Zhijie He 25635ddb38 feat(task): auto-ensure qstash schedule for task system (#14771)
*  feat(task): auto-ensure qstash schedule

chore: cleanup code

chore: cleanup code

chore: cleanup code

* chore: migrate qstash init workflow to startServer

chore: migrate qstash init workflow to startServer

* fix: set default QSTASH_URL to eu region, same as SDK

fix: set default QSTASH_URL to eu region, same as SDK
2026-06-05 02:07:03 +08:00
Arvin Xu f5d78d3d28 feat(device): switch device cwd handling to structured workingDirs (#15353)
Consume the `working_dirs` column: model `updateDevice`, tRPC `updateDevice`
input + `listDevices` output, and the client cwd pickers now operate on
`WorkingDirEntry[]` instead of the flat `recentCwds: string[]`.

- model / tRPC: `workingDirs` (input capped at 20, validated `{ path, repoType? }`)
- client `deviceCwd`: `nextRecentCwds` → `nextWorkingDirs`
- UI: DeviceWorkingDirectory / WorkingDirectory / DeviceDetailPanel / DeviceItem
  render the detected repo type via the shared `renderDirIcon`

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:27:03 +08:00
Hardy f7c46a30a4 feat(opencode-go) add MiniMax M3, remove deprecated models, rework model fetch logic (#15376)
* 🗑️ chore(opencode-go): remove MiMo V2 Omni and MiMo V2 Pro models

*  feat(opencode-go): fetch model list from API with models.dev enrichment

- Try API /models first for real-time available models
- Enrich with models.dev data (pricing, abilities, SDK routing)
- Fallback to models.dev + model-bank if API fails
- Dynamic Anthropic SDK routing via provider.npm field

* 💰 fix(opencode-go): update MiMo pricing to match models.dev

- mimo-v2.5: input $0.14, output $0.28, cache_read $0.0028
- mimo-v2.5-pro: input $1.74, output $3.48, cache_read $0.0145

*  feat(opencode-go): add MiniMax M3 and remove deprecated Qwen3.5 Plus

- Add minimax-m3: 512K context, vision support (image+video), 131K output,
  pricing 0.6/2.4/0.12 USD per M tokens, released 2026-05-31
- Remove qwen3.5-plus: marked deprecated in models.dev

* 🐛 fix(opencode-go): restore Anthropic routing fallback when models.dev is unreachable

Codex P2 review on #15376:
- `routers` is called with `ClientOptions` (no `client` field), so
  `options.client?.models.list?.()` silently returned `undefined` via
  optional chaining; the `catch` never ran and `modelIds` stayed `[]`.
- In API + models.dev double-failure scenarios, `getAnthropicModels([])`
  returned an empty list, regressing Anthropic SDK routing for MiniMax /
  Qwen models.

Fix:
- Make `getAnthropicModels` self-contained: takes no parameters.
- Fallback chain: models.dev → static model-bank prefix match → `[]`.
- `routers` no longer touches `options.client`.

*  feat(opencode-go): enrich model list with models.dev metadata

The model list pipeline previously forwarded only `{ id }` from the API
and models.dev, so displayName / pricing / context / modalities all came
from the static model-bank. When models.dev disagrees with model-bank
(e.g. a price update or new model), the runtime would show stale data.

Map models.dev fields into the flat shape that `processModelCard`
understands, so each card is enriched with:
  - displayName (dev.name)
  - contextWindowTokens / maxOutput (dev.limit)
  - releasedAt (dev.release_date)
  - functionCall / reasoning / vision / structuredOutput (dev.flags +
    dev.modalities.input)
  - pricing (dev.cost → flat input/output/cachedInput/writeCacheInput;
    processModelCard's formatPricing converts it to units)

Fields models.dev doesn't have (description, organization, settings
.extendParams, etc.) still fall back to the model-bank entry via
processModelCard's knownModel lookup, keeping the static config as the
source of truth for UX-only fields.

*  feat(opencode-go): drive reasoning_content handling from models.dev

The `reasoningInterleavedModels` list was hardcoded and drifted from
models.dev:
  - Missing: kimi-k2.5, kimi-k2.6, mimo-v2-omni, mimo-v2-pro
  - Stale: qwen3.7-max (no longer has `interleaved` in models.dev)

Move the source of truth into the models.dev cache. `fetchModelsDevData`
now also builds an `interleavedIds: Set<string>` from `m.interleaved.field`
alongside `anthropicModels`, so every derived field stays in sync with
a single fetch.

The new `getInterleavedModelIds` sync accessor lets `buildOpenAIPayload`
keep its sync signature; it returns the cached set when populated and
falls back to a hardcoded snapshot of the last-known models.dev state on
the very first chat request before any fetch has run.
2026-06-05 01:11:40 +08:00
Arvin Xu f77f31efc0 🔨 chore(database): re-tighten getBuiltinAgent onConflict after 0109 (#15475)
🔨 chore(database): re-tighten getBuiltinAgent onConflict to the 0109 partial index

Now that migration 0109 has flipped agents_slug_user_id_unique to a partial
index (WHERE workspace_id IS NULL) in all environments, restore the precise
conflict arbiter { target: [slug, userId], where: isNull(workspaceId) } so
unexpected unique violations surface instead of being silently swallowed by the
bare onConflictDoNothing() transition form.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 01:07:57 +08:00
Rylan Cai cd171d3510 🐛 fix: bypass audits for headless tool calls (#15406)
* 🐛 fix: bypass audits for headless tool calls

* 🐛 fix: block high-risk headless tools at execution

* Revert "🐛 fix: block high-risk headless tools at execution"

This reverts commit 1d4b534e7a36757bfea0ab229b45a7da647898a3.

* 🐛 fix: restore headless audit bypass

* 🐛 fix: resolve headless blocked tools

* 🐛 fix: simplify blocked tool results

* 🧹 chore: remove unrelated prompt diff

* 🐛 fix: narrow blocked tool instruction type

* 🐛 fix: split security blacklist policies

* 🐛 fix: simplify security blacklist policy rules

* 💄 style: tighten security blacklist diff

* 💄 style: reduce agent config doc diff

* 💄 style: tighten headless audit diff

* 💄 style: minimize audit policy diff

* 💄 style: clarify global audit match naming

* 🐛 fix: auto-run required global audits in headless

* 💄 style: clarify headless intervention comments

* 💄 style: clarify headless global audit comment

* 💄 style: use blocked tool instruction type

* 💄 style: clarify headless audit tests

* 💄 style: annotate headless blocked tool tests

* 🐛 fix: type security blacklist policy filter

* 💄 style: clarify local system 403 guidance

* 🐛 fix: use current persist error helper
2026-06-04 23:42:21 +08:00
YuTengjing b7e2663079 ♻️ refactor: expose email harmony options slot (#15477) 2026-06-04 23:06:14 +08:00
René Wang 537c39f771 💄 style(chat-input): rework Plus menu with toggle switches and grouped submenus (#15433) 2026-06-04 21:24:28 +08:00
Arvin Xu ed47d9ece5 🗃️ build(database): migrate unique constraints to workspace scope (#15472)
* 🗃️ db(database): migrate unique constraints to workspace scope (migration 0109)

Replace the legacy user-scoped UNIQUE constraints with workspace-scoped
partial unique indexes across agents, agent evals, agent skills,
documents, sessions, tasks, and rbac roles/user-roles. Adds migration
0109_migrate_unique_constraints and updates the affected schemas.

* 🐛 fix(database): match partial unique index in getBuiltinAgent upsert

Migration 0109 turned `agents_slug_user_id_unique` into a partial index
(WHERE workspace_id IS NULL). A plain `ON CONFLICT (slug, user_id)` no longer
matches it (Postgres 42P10), breaking getBuiltinAgent. Add the same predicate
via onConflictDoNothing's `where` option; builtin agents are always
workspace-less so the predicate always holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔨 chore(database): use bare onConflictDoNothing in getBuiltinAgent for 0109 transition

Index-shape-agnostic upsert so the builtin-agent path works whether
agents_slug_user_id_unique is the legacy full unique or the 0109 partial,
removing the deploy-ordering coupling. Re-tighten to { target, where } in a
follow-up once 0109 has flipped the index everywhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:08:36 +08:00
Arvin Xu 2bb39f470a feat(gateway): add explicit type discriminator to tunneled tool calls (#15473)
*  feat(gateway): add explicit type discriminator to tunneled tool calls

The device-gateway relays builtin local-system calls and tunneled stdio MCP
calls over one `tool-call` channel. The device was meant to tell them apart by
sniffing whether `toolCall.params` exists — fragile: any future builtin tool
that grows a `params` field would be misrouted to the MCP client.

Add an explicit `toolCall.type` discriminator (`'builtin' | 'mcp'`). The HTTP
client stamps it: `executeToolCall` → `'builtin'`, `executeMcpCall` → `'mcp'`.
The device routes on `type`, never on payload shape. Optional + back-compatible:
an older server that omits it is treated as `'builtin'`.

The desktop receiver switches to this discriminator in a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(desktop): execute tunneled stdio MCP calls from the gateway (#15470)

Receiving half of the gateway stdio-MCP work. When the cloud server tunnels a
stdio MCP tool call to this device (a `tool_call_request` carrying
`mcpParams`), run it locally instead of falling through to the builtin
local-system tool switch (which keys on apiName and has no MCP context, so it
rejected these as "not available on this device").

- `gatewayConnectionSrv`: add a dedicated `mcpCallHandler` + `setMcpCallHandler`;
  `handleToolCallRequest` routes on the presence of `toolCall.mcpParams`,
  sharing the existing response-envelope path.
- `GatewayConnectionCtr`: wire `setMcpCallHandler` → `executeMcpCall`, which
  maps the wire payload to `McpCtr.runStdioMcpTool`.
- `McpCtr`: extract `runStdioMcpTool` core from the `callTool` IPC method so
  both the renderer and the gateway tunnel share one stdio execution path
  (no SuperJSON round-trip for the in-process caller).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:04:09 +08:00
Johnny 92ec067718 fix: prefer INTERNAL_APP_URL for ComfyUI server calls (#15387)
🐛 fix: prefer internal app url for comfyui calls
2026-06-04 19:37:39 +08:00
Arvin Xu 8f19fde3e7 🗃️ build(database): add workspace_id indexes (#15468)
* 🗃️ db(database): add workspace_id indexes (migration 0108)

Phase 3 of the workspace DB migration (LOBE-9961). Adds a btree index on
workspace_id to 70 tenant tables, plus 7 workspace-scoped partial unique
indexes (WHERE workspace_id IS NOT NULL) that pre-build the "new" side of the
Phase 4 (0109) unique-constraint cutover.

A separate production-safe runbook (0108_concurrent.sql, CREATE INDEX
CONCURRENTLY, ordered smallest->largest) is intentionally NOT committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🗃️ db(database): make 0108 index migration idempotent

Add IF NOT EXISTS to all 70 CREATE INDEX + 7 CREATE UNIQUE INDEX statements,
per the db-migrations standard flow (defensive/idempotent SQL), matching how
0107 used DROP CONSTRAINT IF EXISTS. Safe to re-run and safe if the concurrent
runbook already built the indexes before the auto-migrator reaches 0108.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:03:00 +08:00
Arvin Xu f35f984268 feat(gateway): tunnel stdio MCP tool calls to the device (#15469)
Stdio MCP servers live on the user's machine, but in gateway (cloud) mode
the agent runs server-side and `executeMCPTool` tried to spawn the stdio
binary on the cloud server — which has neither the binary nor access to the
user's machine, so local MCP tools (e.g. tasks calling a local kimi-datasource
MCP) always failed.

Add a dedicated `executeMcpCall` path that forwards the stdio connection
params (command/args/env) to a connected device, which spawns the MCP server
and runs the call locally. It rides the existing `/api/device/tool-call`
relay — the gateway forwards `toolCall` opaquely — so the device-gateway
worker needs no changes; the device routes on the presence of
`toolCall.mcpParams`.

Server-side only: when no device is connected, behavior is unchanged
(standalone Electron still spawns in-process). The desktop-side receiver that
runs the forwarded call lands in a follow-up.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:02:11 +08:00
YuTengjing b9fbad7f02 ♻️ refactor(ai-chat): remove simple turn fast path (#15471) 2026-06-04 17:58:57 +08:00
YuTengjing e165b6424b 📝 docs: clarify drizzle raw sql guidance (#15467) 2026-06-04 17:00:42 +08:00
YuTengjing bab3ff4a7a 🐛 fix: reduce agent document context latency (#15436) 2026-06-04 16:23:51 +08:00
Arvin Xu 1e2c1aacd5 🗃️ build(database): add workspace_id FK constraints (#15465)
* 🗃️ db(database): add workspace_id FK constraints (migration 0107)

Phase 2 of workspace_id rollout: add the FK constraint on the 70 tables
that gained a bare `workspace_id` column in Phase 1 (0106), referencing
workspaces(id) ON DELETE CASCADE.

- schema: add `.references(() => workspaces.id, { onDelete: 'cascade' })`
  to all 70 nullable workspace_id columns
- 0107_add_workspace_id_fk.sql: idempotent drizzle migration
  (DROP CONSTRAINT IF EXISTS + ADD), runs in CI / dev / self-host
- 0107_concurrent.sql: production-safe out-of-band runbook
  (NOT VALID + VALIDATE) to avoid write-blocking locks on large tables;
  NOT run by drizzle

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔥 db(database): remove stray 0107_concurrent migration file

* 🐛 fix(database): break user/workspace schema circular dependency

Move userInstalledPlugins from user.ts into connector.ts to break the
user.ts <-> workspace.ts import cycle flagged by dpdm. connector.ts
already imports both users and workspaces, and consumers import the
table from the schemas barrel, so no call sites change.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:23:14 +08:00
Arvin Xu 475f391d97 ♻️ refactor(message): prefer dedicated usage column over metadata.usage (#15457)
* ♻️ refactor(message): prefer dedicated usage column over metadata.usage

Token usage was promoted out of metadata.usage into a dedicated messages.usage
column, but nothing populated it and all reads still went through metadata.usage.

- Centralize write-side promotion in the DB model (update / updateMetadata /
  create), so all executor callers populate the usage column from a top-level
  usage payload, falling back to metadata.usage. metadata.usage stays dual-written
  for backward-compatible reads.
- Reads prefer the usage column and fall back to metadata.usage: message queries,
  getTokenHeatmaps, recomputeTopicUsage, the usage record service, and context
  token accounting.
- Add top-level usage to UpdateMessageParams + DBMessageItem types.
- Mark metadata.usage and the legacy flat token fields as @deprecated, pointing
  to the top-level usage field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(message): dual-write metadata.usage for top-level usage updates

When a caller passed the new top-level `usage` param without also sending
`metadata.usage`, the update wrote only `messages.usage` and left
`metadata.usage` stale/absent — legacy readers and rollback paths still consume
it during the dual-write transition. Fold the resolved usage into the metadata
patch so `metadata.usage` stays in sync regardless of how usage was passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:14:11 +08:00
Arvin Xu 133675adda 🗃️ db(database): add workspace_id columns to existing tables (#15446)
* 🗃️ feat(database): add workspace_id columns to existing tables

Add a nullable `workspace_id text` column to user-owned business tables
(agents, sessions, topics, messages, files, tasks, RAG/eval, RBAC, devices,
connectors, etc.) so records can later be scoped to a workspace. Workspace
tables themselves already landed on canary via 0105_add_usage_agent_share_workspace.

Also folds in the additive device schema from #15356: the structured
`working_dirs` jsonb column + `WorkingDirEntry` type (recent_cwds kept,
now @deprecated).

Scope is deliberately column-only — the lowest-risk slice:
- migration 0106 is pure `ADD COLUMN IF NOT EXISTS` (metadata-only, ~ms locks
  per table, online-safe, no app code change since columns are all NULL).
- FKs, btree indexes, and the per-user→workspace-scoped unique-constraint
  conversions are intentionally deferred to follow-up PRs so each can use the
  production-safe execution path Drizzle can't express (NOT VALID + VALIDATE,
  CREATE INDEX CONCURRENTLY, atomic unique swap).

Scoping notes:
- devices / user_connectors / user_connector_tools: scoped (user-owned resources).
- push_tokens: left user/device-level — an Expo token is one per app install and
  receives a person's notifications across all their workspaces.
- agent_shares: no workspace_id — scoped transitively via agent_id → agents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(database): satisfy inferred row types after adding workspace_id

Adding workspace_id made it a required key in the Drizzle-inferred row types
($inferSelect), breaking call sites that build those shapes by hand:
- rbac.getUserRoles: include workspace_id in the explicit select projection
- session action: add workspaceId to the constructed chat-group literal
- test mocks (apiKey / generation / generationBatch / generationTopic): add
  workspaceId: null

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  test(database): use toMatchObject for topic.create row assertions

The two `expect(createdTopic).toEqual({ ...full literal })` snapshots broke
on every new column (here: workspace_id). Switch them to toMatchObject so the
returned row may carry extra columns without churning the expected literal.
The dbTopic↔createdTopic strict comparisons are left as toEqual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 13:18:16 +08:00
Arvin Xu e8b914feef ♻️ refactor(agent-signal): S6 — migrate skillManagement to execAgent builtin agent (#15443)
Move the self-iteration skill-management action off the inline policy
implementation onto an execAgent-dispatched builtin agent (slug
`skill-management`), mirroring the S3/S4 memoryWriter + self-iteration
migration. Adds the `agentSignalSkillManagement` serverRuntime, the
builtin-tool-agent-signal skill-management manifest/systemRole, and the
builtin-agents skill-management agent; strips the ~3.5k-line inline
skillManagement policy down to the dispatch shim.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:49:26 +08:00
Hardy 7f3f1278e4 feat(prompts): use XML format for topic title generation to improve DeepSeek compatibility (#15413) 2026-06-04 12:42:11 +08:00
Arvin Xu 951561f685 ️ perf(database): add optional statement_timeout to server DB connections (#15445)
Long-running queries (e.g. an insert stuck for 700s on lock contention)
could block indefinitely because Postgres' statement_timeout defaults to
0 (no limit) and neither the node nor neon pool configured one.

Add an optional DATABASE_STATEMENT_TIMEOUT env (milliseconds, no default)
applied to both NodePool and NeonPool as statement_timeout and
idle_in_transaction_session_timeout, so Postgres aborts a stuck statement
or idle transaction on the server side. Unset keeps the previous behavior.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:34:23 +08:00
lobehubbot 248a4dcab5 🔖 chore(release): release version v2.2.2 [skip ci] 2026-06-04 03:59:37 +00:00
lobehubbot d3eebd3994 Merge remote-tracking branch 'origin/main' into canary 2026-06-04 03:57:53 +00:00
Arvin Xu 6532cd1ee0 🚀 release: 20260604 (#15447)
# 🚀 LobeHub Release (20260604)

**Release Date:** June 4, 2026  
**Since v2.2.1:** 88 merged PRs · 11 contributors

> This week brings Execution Devices out of the lab — run agents and
Claude Code on any configured local or remote machine — alongside Claude
Opus 4.8, token-usage analytics, and Page sharing.

---

##  Highlights

- **Execution Devices** — Pick where an agent runs. Desktop and CLI
devices auto-register with a stable machine ID, route through the
gateway by channel, and surface a device switcher in the chat input. Run
remote Claude Code on a configured device, with a recent-directory
picker you can drag to reorder. (#15300, #15315, #15322, #15343, #15351,
#15371)
- **Claude Opus 4.8** — Day-one support for Anthropic's latest model.
(#15314)
- **Token-usage analytics** — A new token-usage mode on the activity
heatmap, backed by a denormalized topic usage/cost rollup so totals stay
accurate without recomputing from messages. (#15365, #15417, #15425)
- **Page sharing** — Share a Page through a dedicated document share
flow, plus new Workspace and Agent share tables. (#15309, #15439)
- **Self-iteration agents** — Agent Signal's execAgent migration lands a
server-runtime bridge, async memory writer, and a registered
self-iteration tool package, with a CLI trigger command for testing.
(#15360, #15364, #15392)
- **Knowledge search** — BM25 search now extends to file-backed
documents, and the portal ships an editable CodeMirror viewer for local
files with document highlighting. (#15247, #15298)

---

## 🏗️ Core Agent & Architecture

### Agent Signal & Runtime

- **execAgent migration** — Server-runtime bridge, completion
projection, async memory writer, and removal of the legacy
`executeSelfIteration` path. (#15392)
- Registered the self-iteration builtin tool package and restored the
three mode-specific self-iteration agent slugs. (#15202, #15364)
- Added a CLI trigger command with a golden-snapshot fixture for Agent
Signal. (#15360)
- **Skill priority** — Agent Builder now emits a skill-priority
instruction with matching server runtime. (#15409)
- Retry empty LLM completions instead of silently finishing the turn.
(#15355)
- Classify topic/agent/session foreign-key violations as
`ConversationParentMissing` for clearer recovery. (#15408)
- Persist canonical nested usage/performance on assistant messages, and
re-link orphan tool messages at the raw bucket write boundary. (#15359,
#15438)
- Guard `createAgent` against LLM double-encoded array fields. (#15381)

---

## 🖥️ Execution Devices & Gateway

- Auto-register desktop and CLI devices with a stable machine ID, and
add the `@lobechat/device-identity` package. (#15300, #15321)
- New Devices settings page behind the Execution Device Switcher lab,
with a device switcher shown for all agents in the chat input. (#15315,
#15371)
- `connectionId` + channel routing across the gateway client and device
list; preset the local device on the first LLM request for the 本机
target. (#15322, #15435)
- Run remote Claude Code on a configured device, with drag-to-reorder
recent-directory management and client renders for device tool results.
(#15343, #15351, #15437)
- Preserve content and state across gateway tool calls, and prevent
duplicate streaming from stale reconnects. (#15114, #15354)

---

## 🖥️ CLI & Desktop

- Preserve content/state for connect local file and shell tools; render
the `runCommand` tool result card. (#15441, #15442)
- New `lh topic view` command; CLI now auto-registers its device on
login, matching desktop. (#15340, #15377)
- Resolve CLI tools from the shell `PATH`, and clarify local command
session handling. (#15368, #15389)
- Relocate visual-ref helpers to `@lobechat/const` to fix a renderer
crash; upload `.blockmap` files to S3 for differential updates. (#15326,
#15369)
- Fix a market OAuth expiry that triggered the wrong re-login modal, and
kill dev child processes on parent shutdown. (#15246, #15290)

---

## 🗂️ Pages, Library & Knowledge

- Document share flow with business slot stubs, plus Workspace and Agent
share tables. (#15309, #15439)
- Export Agent profiles as Markdown, preserving an empty agent prompt on
export. (#15312, #15316)
- Editable CodeMirror viewer for local files with document highlighting;
BM25 search extended to file-backed documents. (#15247, #15298)
- Default new Agent-doc files to `.md` and preserve IME composition;
refresh folder data on slug switch and dedupe breadcrumb fetches.
(#15335, #15427)

---

## 💬 Chat & User Experience

- Group-by-status mode for the Topic sidebar; dropped the legacy
session→agentId compatibility path from Topic queries. (#15366, #15378)
- Restore editor focus after the file picker closes, and close the skill
dropdown before navigating to settings. (#15391, #15394)
- Strip markdown tokens from fallback Topic titles; keep an open
ActionBar popup when hovering another message. (#15303, #15372)
- Stabilize home starter loading and stop transliterating model names in
the home starter; show artifact source while streaming. (#15310, #15324,
#15386)
- Group the sidebar spacer with recents and agents. (#15373)

---

## 📊 Analytics, Tasks & Notifications

- Token-usage mode on the activity heatmap, backed by a denormalized
topic usage/cost rollup. (#15365, #15417, #15425)
- Push: new `PushChannel`, receipt cron, and `pushToken` tRPC API.
(#15233)
- Tasks now support file and image attachments. (#15141)

---

## 🧩 Models & Providers

- Support Claude Opus 4.8 and configurable model routing with starters.
(#15314, #15384)
- MiniMax M3: new model entry and an Anthropic video runtime. (#15380,
#15403)
- Add `intern-s2-preview` with `thinking_mode`, and `step-3.7-flash`
support. (#15308, #15317)
- Block disabling the official provider; fix default provider setup in
business mode. (#15379, #15382)

---

## 🎨 UI & Modals

- Migrate modals to `@lobehub/ui/base-ui` (LOBE-9711 + eval batch),
including the create-custom-model and feedback/changelog modals.
(#15401, #15416)
- Restructure confirmModal title and content across deletion flows;
polish the service-model form and migrate its Switch to base-ui.
(#15426, #15440)
- Wrap the BlueBubbles bridge config into a connection card; update
`@lobehub/ui` to v5.15.5. (#15325, #15342)

---

## 🔒 Reliability

- Replace hardcoded `session_context` values with template variables in
credentials. (#15352)
- Point `CHANGELOG_URL` to `/changelog`. (#15428)

---

## 👥 Contributors

Huge thanks to **11 contributors** who shipped **88 merged PRs** this
cycle.

@hezhijie0327 · @qybaihe · @sxjeru · @arvinxx · @Innei · @tjx666 ·
@LiJian · @sudongyuer · @cy948 · @rivertwilight · @AmAzing129

Plus @lobehubbot and renovate[bot] for maintenance.

---

**Full Changelog**: v2.2.1...release/weekly-20260604
2026-06-04 11:56:58 +08:00
AmAzing- 54e1b59ce6 feat(agent-management): paginate searchAgent with real totals + wire 8 packages into CI (#15448)
*  feat(agent-management): paginate searchAgent with real totals and cap notice

The searchAgent tool silently clamped limit to 20 with no pagination and
reported totalCount as the returned page size, so models (and users) could
never discover agents beyond the 20 most recently updated ones.

- AgentModel: extract shared where builder, add countAgents (same
  conditions as queryAgents)
- lambda router + client agent service: expose countAgents
- server tool runtime & AgentManagerRuntime: pass offset through, report
  real totals (workspace + marketplace), emit explicit notes when the
  requested limit is capped and when more pages exist, explain
  out-of-range offsets instead of claiming no matches
- manifest: add offset param, document pagination
- agent-manager-runtime: add vitest config + test scripts (suite was
  previously unrunnable), repair stale store mocks

* 👷 build(ci): wire 8 tested packages into the package test workflow

An audit found 8 packages carrying test:coverage scripts that were never
added to the CI PACKAGES allowlist, so their suites never ran:

- agent-gateway-client, device-gateway-client, device-identity,
  eval-dataset-parser: already green, added as-is
- eval-rubric, fetch-sse: had no package-level vitest config, so vitest
  fell back to the root config whose setup/aliases break outside src/ —
  added minimal configs
- heterogeneous-agents: one assertion drifted (labels registry gained
  amp/hermes/openclaw/opencode) with nobody noticing — updated
- agent-manager-runtime: wired in the previous commit

All 8 verified locally with the exact CI command
(bun run --filter <pkg> test:coverage).

*  test(agent-management): cover searchAgent error path and market totalCount fallback

Codecov flagged 3 uncovered lines in the patch: the searchAgents catch
block (2 misses) and the totalCount ?? items.length fallback (1 partial).
Add the missing failure-path and fallback tests on both execution paths
(client AgentManagerRuntime + server tool runtime).
2026-06-04 10:52:25 +08:00
Arvin Xu 72ea0f94f7 🐛 fix(cli): preserve content/state for connect local file/shell tools (#15442)
* 🐛 fix(cli): preserve content/state for connect local file/shell tools

Route file/shell tool calls in connect mode through LocalSystemExecutionRuntime
so the result carries formatted prompt `content` plus structured `state`, and
forward `state` over the gateway tool-call response — aligning the CLI with the
desktop gateway path (PR #15114).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): preserve getCommandOutput timeout when polling running commands

Routing getCommandOutput through the runtime dropped the per-call/gateway
timeout: the CLI mapping didn't forward it and LocalSystemExecutionRuntime's
denormalizeParams stripped it before ShellProcessManager.getOutput, so polling
fell back to the 30s default and could block past the gateway budget. Carry
timeout through the runtime param type, denormalize, and the CLI mapping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 01:28:11 +08:00
Arvin Xu a3a08c2395 🐛 fix(chat): re-link orphan tool messages at the raw bucket write boundary (#15438)
A fast hetero-agent (Claude Code) tool can have its parent assistant's
`tools[]` momentarily dropped (stale/out-of-order `replaceMessages` snapshot,
or an optimistic `updateMessage{tools}` on the wrong assistant during a step
boundary) while the `role:'tool'` row + parentId survive. Since conversation-
flow binds a tool into its assistant solely via `assistant.tools[].id`, the
tool then renders as a top-level orphan bubble (`inspector.orphanedToolCall`).

Fix at the RAW `dbMessagesMap` write boundary — shared by `replaceMessages`
and `internal_dispatchMessage` (the optimistic-update path) — so the Source of
Truth stays consistent for optimistic updates, not just the parsed display.
`reconcileAssistantToolLinks` re-attaches the missing `tools[]` entry for any
present tool row whose parentId resolves to an assistant in the same bucket;
it only acts on present rows (never resurrects deletions) and never removes or
reorders entries.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:59:12 +08:00
Arvin Xu 643ad16a5d 🐛 fix(github): render runCommand tool result card (#15441)
The github render/inspector were registered under the snake_case
`run_command` key, but the tool call emits the camelCase `runCommand`
apiName, so the lookup missed and fell back to the generic collapsed
pill. Register both casings so the custom card renders.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:33:28 +08:00
Arvin Xu 5761d20637 feat(db): add workspace and agent share table (#15439)
*  feat(db): add usage column to messages table

Promote token usage/cost out of `metadata.usage` into a dedicated
`messages.usage` jsonb column, with btree expression indexes on
`usage.cost` and `usage.totalTokens`. Additive only — no data backfill;
`metadata.usage` stays the source of truth during the transition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(db): add agent share schema (picked from #15430)

Bring the agent-share schema layer over from #15430: new `agent_shares`
table + `topics.sender_id` column/index, schema relations and barrel
export. Migration renumbered to 0106 to sit after the usage column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(db): add workspace schema (picked from #15414)

Bring over only the standalone `workspace.ts` schema from #15414 — the
workspaces / workspace_members / workspace_invitations / workspace_audit_logs
tables (self-contained, FK to users only). None of #15414's workspaceId
column additions across other tables are included. Migration is 0108-safe,
renumbered to 0107.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🗃️ chore(db): squash usage/agent-share/workspace into one migration

Collapse the three stacked migrations (0105 usage, 0106 agent_share,
0107 workspace) into a single idempotent 0105_add_usage_agent_share_workspace.
Schema source is unchanged; only the migration files/snapshot/journal are
consolidated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(db): add senderId to expected topic shape in create test

The picked agent-share schema added topics.senderId, so the created row
now returns it; update the two toEqual assertions accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:24:09 +08:00
Innei fd3c6cf8fc 🐛 fix(ui): restructure confirmModal title and content across deletion flows (#15440)
🚸 fix(ui): restructure confirmModal title and content across deletion flows

Move long warning sentences from `title` into `content` and use short verb titles
("Delete", "Uninstall", "Wipe Data", etc.). Add `okText`/`cancelText` i18n for all
fixed sites so confirm buttons match the action language.

Covers topic/thread/agent/group/library/file/model/skill/storage delete flows.
2026-06-04 00:23:58 +08:00
Arvin Xu d81e5e703e feat(remote-device): add client renders for device tool results (#15437)
*  feat(remote-device): add client renders for listOnlineDevices and activateDevice

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(utils): make SVG event-handler stripping engine-independent

DOMPurify's FORBID_ATTR / SVG-profile allowlist path relies on the underlying DOM's
attribute + namespace handling, which differs across engines (jsdom vs happy-dom) and
DOMPurify versions — in some CI environments on* handlers on SVG-namespaced nodes slipped
through. Add a scoped uponSanitizeAttribute hook to drop every on* attribute deterministically,
and assert by security property instead of exact serialization to drop whitespace brittleness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(remote-device): render activation failure content when no device state

activateDevice returns success:false with explanatory content but no error and no state when
the target is offline/unknown. The tool detail view only skips custom rendering when result.error
is set, so the custom renderer's `return null` rendered a blank result. Fall back to the failure
content so the user/model still sees the message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(utils): deterministically scrub SVG on* handlers via post-pass

The DOMPurify uponSanitizeAttribute hook still failed in CI: <script> is removed (tag filtering)
but on* handlers survive, because the attribute-sanitization phase doesn't run for SVG-namespaced
nodes in CI's DOM engine — so the hook never fires. Replace it with an explicit regex scrub on the
serialized output, which strips every on* event-handler attribute independent of the DOM engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔒 fix(utils): loop SVG on* scrub until stable to close recombination bypass

A single-pass regex replace can leave a fresh handler behind when removing one splices the
surrounding text back together (` on onclick="x"click="y"` → ` onclick="y"`) — the CodeQL
js/incomplete-multi-character-sanitization case. Repeat the scrub until the string stops changing
so no on*= token can survive. Adds a regression test for the recombination input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 23:59:35 +08:00
Innei 2a4b6e4974 🐛 fix(agent-doc): default new files to .md and preserve IME composition (#15427)
* 🐛 fix(agent-doc): default new files to .md and preserve IME composition

- Append `.md` to newly-created agent documents; pre-select only the stem
  in the inline rename input so the extension stays intact.
- Wire `useIMECompositionEvent` on the explorer container so Enter pressed
  during IME composition (e.g. Chinese pinyin) no longer commits the
  half-formed name through pierre/trees' shadow-DOM input.

* 🐛 fix(agent-doc): use native capture listener for IME guard

React `onKeyDownCapture` can lose to pierre/trees' bubble handler in some
event ordering edge cases, and the original guard missed IMEs that report
`keyCode === 229` or fire Enter just after compositionend in the same task.

- Bind a native `keydown` capture listener on the container so we can
  inspect `composedPath()` and confirm the keydown originated inside the
  shadow-DOM rename input.
- Extend the IME guard with an `imeSessionRef` that stays true through one
  extra microtask after compositionend.
- Drop the React `onKeyDownCapture` prop in favour of the native listener.

*  revert(agent-doc): drop IME guard pending pierre/trees upstream fix

The inline rename input lives in pierre/trees' shadow DOM and we can't
reliably suppress its IME-composing Enter commit from the outside. Roll
back the local hack and track the issue upstream instead. The default
`.md` extension and stem-only selection on rename stay in place.

*  feat(agent-doc): preselect stem on inline rename too

Existing files entering inline rename (right-click → Rename, or F2) now
narrow the selection to the stem after pierre/trees' `input.select()`,
matching the new-file flow so the user never has to retype `.md`.

* 🐛 fix(agent-doc): preserve extension on filename collisions
2026-06-03 23:54:39 +08:00
Arvin Xu 2fb0970cf9 💄 style(stats): add token-usage mode to activity heatmap (#15425)
* 💄 feat(stats): ladder shorten number up to B and T tiers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(stats): move token summary below overview and surface cumulative tokens

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(stats): add 12px gap between overview cards and token summary

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(stats): move heatmap summary under the activity title

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 21:51:57 +08:00
Arvin Xu 7a93df9e44 ️ perf(device): preset local device on first LLM request for 本机 target (#15435)
* ️ perf(device): preset local device on first LLM request for 本机 target

When the desktop runs an agent against the local machine (executionTarget
'local'), resolve this desktop's own gateway deviceId client-side and pass it
as the run's `deviceId`. The server then presets `activeDeviceId` and injects
`lobe-local-system` into the very first LLM payload, skipping the extra
`activateDevice` round-trip the model was forced to make whenever more than one
device was online.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(device): cover local deviceId resolution in executeGatewayAgent

Asserts the client forwards this desktop's deviceId only for the local (本机)
target — including the unset-on-desktop fallback — and never for sandbox,
explicit remote device, or off-desktop runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(device): gate local-device binding on effective runtime mode

`resolveLocalDeviceId` defaulted an unset `agencyConfig.executionTarget` to
'local' and sent this desktop's deviceId. But the legacy ModeSelector writes
only `runtimeMode`, leaving executionTarget unset — so an explicit cloud/none
run would still get a deviceId, which the server turns into activeDeviceId and
injects lobe-local-system, wrongly routing a cloud run to the local machine.

Gate on `isLocalSystemEnabledById` (effective runtimeMode === 'local'), the
source of truth both selectors agree on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚨 fix(test): use import-type alias instead of inline import() type

Satisfies @typescript-eslint/consistent-type-imports (CI lint).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:31:46 +08:00
Arvin Xu d9673c3c41 ♻️ refactor(agent-signal): execAgent migration — serverRuntime bridge + completion projection + async memoryWriter + executeSelfIteration removal (#15392)
* 🚧 wip(agent-signal): S1 — self-iteration tools as static primitives, no side-channel

Rewrite all three self-iteration execAgent tool surfaces (review / reflection /
feedback-intent) as static, named primitives instead of reusing the dynamic
createServerToolSet / createToolSet factory (which carries the legacy
reserveOperation / receipt / completeOperation side channel the migration removes).

Package (builtin-tool-agent-signal):
- AgentSignalToolService.invoke (generic bag) → AgentSignalRuntimeService, a
  narrow named DB-primitive seam (skillManagement precedent). Artifact recorders
  echo their input; reads/mutations route to one primitive each. The runtime
  carries no dedupe / receipt / operation-state side channel — idempotency and
  receipt projection live on the completion path, not the tool call.

Server primitives (pure live-DB reads + writes, keyed to api names):
- review/server.ts createReviewRuntimePrimitives — proposal lifecycle + resource
  tools, parameterized by window scalars from the operation marker, reusing the
  existing snapshot/preflight/projection/brief helpers.
- tools/runtimePrimitives.ts createResourceRuntimePrimitives — the skill-read /
  skill-write / writeMemory surface shared by reflection and feedback-intent.
- No context blob and no getEvidenceDigest: evidence is embedded in the agent
  prompt, so tools only touch live state.

serverRuntimes: agentSignalReview / agentSignalReflection / agentSignalFeedbackIntent
thin factories wiring ToolExecutionContext → primitives → package runtime, all
registered. createServerToolSet / createToolSet left untouched (legacy
executeSelfIteration path, removed in S4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S2 — completion-path receipt projection from finalState

Replace the in-runtime receipt accumulator with finalState-driven projection on
the completion path. finalState is only in hand inside the completion lifecycle
(S3 final snapshots are write-only — get() is a null stub; the operation row has
no messages; prod webhook hooks strip finalState), so receipts must be projected
from the one point state exists.

- CompletionLifecycle.emitSignalEvents: extract the compact, kind-tagged tool
  outcomes from the terminal state (extractSelfIterationCompletionPayload) and
  carry them on the agent.execution.completed payload — only for marked
  self-iteration runs, never the full message history.
- completionPolicy: forward the payload to onSelfIterationCompleted.
- completion/buildSelfIterationReceipts: project mutations + artifacts into
  user-visible receipts, mirroring the legacy createReceipts kind/status/target
  mapping. Deterministic receipt ids (sourceId + tool call id) → idempotent
  re-projection; the store dedupes by id.
- completion/selfIterationCompletionHandler: build + persist receipts.
- orchestrator: wire the handler into createDefaultAgentSignalPolicies.
- agent-signal source type: add an opaque selfIteration field to the
  agent.execution.completed payload.

Inert until the dispatch side stamps the operation marker (S3 / S4): without a
marker the extractor returns undefined and the handler no-ops.

Tests: buildSelfIterationReceipts (5) + extractCompletionPayload (4); completion
policy + CompletionLifecycle + orchestrator suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S3 part 1 — completion-side memory receipt support (inert)

Foundation for migrating the memory writer to the async execAgent path: teach
the completion path to project a memory receipt from a finished memory-writer
run. Inert until the dispatch side stamps a kind:'memory' marker (part 2).

- completion routing is now keyed on the operation MARKER (the selfIteration
  payload), not the agent slug — a memory writer runs as the user's own agent,
  so a slug check would miss it. completionPolicy gates on payload presence;
  agentId loosened to string.
- extractCompletionPayload: for a kind:'memory' run, synthesize a writeMemory
  mutation from the run's finalState (the memory builtin tool results are not
  kind-tagged, so extractMutations finds nothing) via resolveMemoryActionResultFromState.
- buildSelfIterationReceipts: a memory run surfaces as just its action receipt,
  no aggregate review summary.
- extract the pure memory finalState parsers into a dependency-light
  ./memoryActionResult module so the completion lifecycle can reuse them without
  dragging the heavy memory-runner module (ModelRuntime/AgentService/…) into its
  graph. userMemory re-exports them for backward compat.
- bump a too-tight (5s) timeout on the real-orchestration integration test.

Tests: completion (12) + completionPolicy (8) + userMemory (12) green; agentSignal
policies + orchestrator suites (138) green; type-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent-signal): S3 — migrate memoryWriter to async execAgent + completion receipt

Flip the memory-writer action from a blocking executeSync run to an async
queued operation (autoStart) stamped with an agent-signal `memory` marker.
The user-visible "memory saved" receipt is no longer projected synchronously
from the action result — it is projected on the completion path from the run's
finalState (extractMemoryMutations → buildSelfIterationReceipts), so the receipt
appears a few seconds later once the run completes.

- userMemory.ts: add `dispatch` path enqueuing via createOperation(autoStart),
  stamping appContext.agentSignal so completion can project the receipt.
- receiptService.ts: drop the synchronous memory receipt projection (would
  duplicate the async one, with a premature empty target).
- types.ts: add `agentSignal` marker to OperationCreationParams.appContext.
- tests: cover the memory-kind completion loop end-to-end (single memory
  receipt, correct target + anchor, no aggregate summary).

Note: the memory run uses createOperation (not execAgent), so it never
synthesises a user message and cannot recurse into analyzeIntent — no
suppressSignal needed on this path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S4 step 0 — forward agentSignal marker through execAgent

Foundation for migrating self-iteration onto execAgent: let a background run
carry its agent-signal marker so the S2 completion path can project receipts.

- Move AgentSignalOperationMarker / AgentSignalOperationKind into @lobechat/types
  (ExecAgentAppContext can now reference it); operationMarker.ts re-exports the
  type and keeps the runtime parse/validate helpers.
- ExecAgentAppContext: add `agentSignal?` field.
- execAgent: forward `appContext.agentSignal` into createOperation's appContext
  (it was dropped by the curated passthrough), so it lands in
  state.metadata.agentSignal — the key the completion extractor reads.

No behaviour change yet: nothing sets appContext.agentSignal on the execAgent
path until the self-iteration dispatch helper lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🚧 wip(agent-signal): S4 step 0b — self-iteration execAgent dispatch helper

Shared primitive for migrating the 3 self-iteration modes off the hand-rolled
runtime onto async execAgent (used by reflection/feedback/nightly-review next).

- enqueueSelfIterationRun(): create an isolated thread (when anchored), then
  execAgent the builtin slug with suppressSignal + the agent-signal marker on
  appContext, autoStart, headless. Returns immediately (fire-and-forget).
- marker: add `agentId` (the reviewed user agent). A slug run resolves the
  operation agentId to the builtin agent, so receipts must attribute to the
  reviewed agent carried on the marker.
- buildSelfIterationReceipts: attribute to `marker.agentId ?? agentId` (memory
  runs leave it unset and fall back to the run agentId — unchanged).

Not wired into the mode handlers yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent-signal): S4 — migrate executeSelfIteration to async execAgent

Replace the hand-rolled `executeSelfIteration` runtime (new AgentRuntime +
custom call_llm executor + 6 closure side-channels) with the standard async
`execAgent` queue path. nightly-review / self-reflection / self-feedback-intent
now enqueue via `enqueueSelfIterationRun → execAgent` and project their
receipts/briefs on the `agent.execution.completed` completion path.

- Delete `execute.ts` (1500 lines) + `execute.test.ts`; gut the three server
  adapters (review/reflection/feedback) to drop the synchronous run path and
  the legacy receipt/runtime wiring.
- `aiAgent`: background runs execute under a builtin slug but attribute their
  resource tools + receipts to the *reviewed* user agent via the run marker.
- Drop the orchestrator's `writeDailyBrief` default — nightly review writes its
  brief in-run via the builtin review serverRuntime primitive.
- Add `ReviewRunStatus.Dispatched` for enqueued background runs.
- Completion-path debug logging across CompletionLifecycle / completionPolicy /
  completion handler.

Part of LOBE-9434 (S4 · LOBE-9876).

* 🐛 fix(agent-signal): make execAgent resolve builtin slugs + give self-iteration agents a mini model

Live-testing the S4 self-iteration → execAgent path surfaced two gaps that kept
background runs (nightly-review / self-reflection / self-feedback-intent) from
ever dispatching:

- execAgent threw `Agent not found: <slug>` when addressed purely by a builtin
  slug (the self-iteration dispatch path) because getAgentConfig only resolves
  persisted rows. Lazily materialize the virtual builtin row via
  AgentModel.getBuiltinAgent — mirrors the inbox/task precedent — then re-resolve.
- The three self-iteration builtin agents had no `persist` model, so runs fell
  back to the user's default chat model. Give them `persist: { DEFAULT_MINI_MODEL,
  DEFAULT_MINI_PROVIDER }` (gpt-5.4-mini), matching the legacy executeSelfIteration
  behavior.

Verified live: self-reflection now dispatches, the async operation reaches `done`,
and a `review` completion receipt is projected on the completion path. Adds two
execAgent.builtinRuntime tests (builtin-slug materialization + unknown-id still
throws).

Part of LOBE-9434 (S4).

* 🚨 fix(agent-signal): use type-only import for createServerSelfReviewBriefWriter

After the S4 gutting, review/server.ts only uses createServerSelfReviewBriefWriter
in a `ReturnType<typeof ...>` position — split it into a type-only import to
satisfy @typescript-eslint/consistent-type-imports (the lone lint:ts error).

* 🐛 fix(agent-signal): carry tool apiName in result content so action receipts project

The agent runtime persists tool messages with only content/role/tool_call_id (no
message-level apiName), so the completion extractor's `message.apiName` read was
always undefined in live runs — buildSelfIterationReceipts then dropped every
mutation via `if (!apiName) return []`, so durable skill/proposal writes produced
no action receipt (only the summary survived; memory was exempt via a hard-coded
apiName).

Fix the extraction channel, not the shared runtime:
- ExecutionRuntime stamps `apiName` into the result content alongside `kind`.
- extractFromFinalState reads apiName from the content (message.apiName fallback).

Tests reworked to the real persisted shape (apiName in content, no message-level
apiName) — the prior mocks hid the bug.

Part of LOBE-9434 (S4).

* 🐛 fix(agent-signal): persist run marker to operation metadata for server tools

Self-iteration server tools (nightly-review etc.) read the run marker from
`agent_operations.metadata` via readAgentSignalMarker, but recordStart only
persisted a trimmed appContext and never wrote metadata — so in live runs the
marker was always undefined and review/proposal writes fell back to a 1970
window/localDate + operationId source (non-idempotent).

recordStart now persists `metadata: { agentSignal }` from appContext.agentSignal,
so the tool path matches the completion path (which reads it from finalState).

Part of LOBE-9434 (S4).

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:12:59 +08:00
sxjeru dd2e32cf6f 💄 style: Add new MiniMax-M3 model (#15403)
*  feat(minimax): add MiniMax M3 model with pricing and update tests

* Update minimax.ts

* fix test
2026-06-03 19:29:02 +08:00
YuTengjing a5ab99f055 📝 docs: add agent code style guidance (#15434)
* 📝 docs: add code style guidance for hook extraction and file splitting

* 📝 docs: tighten file-splitting guidance

* 📝 docs: clarify agent guidance wording
2026-06-03 18:45:40 +08:00
Arvin Xu 41bccc4aa8 chore: remove LOBE-XXX markers from code comments (#15422)
chore: remove LOBE-XXX markers from code comments

- match.test.ts: replace (LOBE-9913) marker with inline comment context
- nightly-review.golden.json: replace (LOBE-9434) marker with execAgent migration context

Co-authored-by: Arvin Xu <arvin@lobehub.com>
2026-06-03 17:02:24 +08:00
AmAzing- 1ce4e026a7 🐛 fix(const): point CHANGELOG_URL to /changelog (#15428) 2026-06-03 15:25:48 +08:00
Innei 89c55bf658 💄 style(service-model): polish form layout & migrate Switch to base-ui (#15426)
- align memory model InputNumber height (32px) with base-ui Select trigger via local ConfigProvider
- stack optional features as switch above model select, switch right-aligned
- migrate optional features Switch from antd to @lobehub/ui/base-ui
2026-06-03 14:18:36 +08:00
Arvin Xu 2eb9e34fda feat(stats): add daily token-usage mode to activity heatmap (#15417)
*  feat(stats): add daily token-usage mode to activity heatmap

Add a Messages/Tokens toggle to the stats activity heatmap. The token
mode sums assistant messages' `metadata.usage.totalTokens` (the source of
truth for usage) bucketed by the day each message was created, so tokens
land on the day they were actually consumed rather than on a topic's
creation date. Aggregation runs in SQL (SUM over the jsonb path, GROUP BY
date) and levels are scaled relative to the busiest day.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 💄 feat(stats): format heatmap token counts and add token stat row

- Format tooltip token counts compactly (e.g. 44.2K, 12.5M) via the chart's
  customTooltip; message counts get thousand separators.
- Add a token-dimension summary row (cumulative / peak daily / current streak
  / longest streak) shown in token mode, derived client-side from the heatmap
  data over the past year.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(stats): add longest-task duration to token heatmap stats

Add the "longest task" figure to the token-mode stats row, computed from
the longest wall-clock agent operation (completedAt - startedAt) over the
past year — MAX in SQL on the agent_operations table, scoped by user and
using the (user_id, created_at) index. Rendered as a compact 1h 15m / 45s
duration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 💄 feat(stats): default heatmap to token mode and move toggle beside title

- Token is now the first/default segmented option (Messages second); the
  share card keeps Messages as its default.
- Move the Messages/Tokens toggle next to the section title (left) via a new
  StatsFormGroup `afterTitle` slot; day tags stay on the right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:54:07 +08:00
Innei 13ce3c52ec ♻️ refactor: migrate modals to @lobehub/ui/base-ui (LOBE-9711 + eval batch) (#15416)
* ♻️ refactor: migrate modals to @lobehub/ui/base-ui (LOBE-9711 + eval)

Move 5 root createModal sites (LibraryModal/AddFilesToKnowledgeBase,
LibraryModal/CreateNew, Electron/AuthRequiredModal, SkillStore,
SkillStore/SkillDetail) to base-ui imperative createModal. Drop
allowFullscreen/destroyOnHidden/getContainer (base-ui handles them),
rename children→content, afterClose→onOpenChangeComplete, styles.body
→styles.content.

For AuthRequiredModal, base-ui imperative ModalInstance.update only
accepts Partial<BaseModalProps>, so the previous closable/keyboard
dynamic lock is reduced to maskClosable only — Esc/X close cannot be
blocked during sign-in.

Convert 11 declarative <Modal open … /> sites under eval/bench to
imperative createXxxModal factories, splitting each into Content.tsx
(body) + index.tsx (factory). Update callers in eval/index.tsx,
bench/[id]/{datasets/[id],features/{BenchmarkHeader,DatasetsTab,
RunsTab,TestCasesTab},runs/[id]/{index,features/RunHeader}} to call
factories on click instead of toggling local open state.

Delete unused TestCasePreviewModal.tsx (dead code); extract the
inline preview Modal from TestCasesTab into a new
TestCasePreviewModal feature folder.

* ♻️ refactor: move modal actions to base-ui footer slot, drop content padding overrides

Per @innei feedback on PR #15416:

- base-ui's ModalContent already has 12px/16px default padding; remove
  manual paddingBlock/paddingInline wrappers in Content components and
  drop styles.content.padding=0 overrides in factories.
- Move OK/Cancel (and other actions) into the createModal({footer}) slot
  using base-ui's ModalFooter atom for proper flex/justify-end styling.
- Form submit wired via antd Form's name + Button form=name htmlType=submit
  so the footer button outside Form can submit it. Shared loading state
  flows from Content to Footer via a per-modal closure that calls
  instance.update({footer: ...}).

New helper src/utils/createFormModal.tsx encapsulates the common pattern
for plain form modals (Cancel + Submit). Custom factories (RunCreate
split-button, BatchResume with selection counter, DatasetImport step-aware
footer, AuthRequired sign-in flow) use inline closure plumbing.

Touched files: 11 modal pairs (Content + Footer + index) + 1 helper.

* 🔥 chore: remove dead AddFilesToKnowledgeBase modal

`useAddFilesToKnowledgeBaseModal` exported from
`src/features/LibraryModal/AddFilesToKnowledgeBase/` had no callers in
the main codebase — only its own test referenced it. Remove the entire
folder (index, SelectForm, index.test) and drop the re-export from
`LibraryModal/index.ts`.

* 💄 style: bleed SkillStore scroll viewport past modal content padding

base-ui ModalContent has 12px/16px default padding, which insets the
SkillStore scroll viewport and makes the scrollbar look blocked. Pull
the body wrapper out with negative margins (marginInline: -16,
marginBlockEnd: -12) so the inner scroll container sits flush with the
modal edge. Grid items inside the scroll keep their own 16px padding.

* 🐛 fix: cast Modal.update to ImperativeModalProps for footer typing

base-ui's ModalInstance.update is typed as Partial<BaseModalProps>,
which excludes the `footer` and `content` fields that only
ImperativeModalProps carries. At runtime the imperative updateModal
spreads any shape, so the cast is sound — narrow it at each call site.

Also delete src/routes/(main)/eval/bench/[benchmarkId]/features/
DatasetRunCreateModal/, an orphaned re-export of RunCreateModal's
removed default export.
2026-06-03 00:17:48 +09:00
YuTengjing f9eb48feea feat: add limited offer & original price locale keys for top-up (#15415) 2026-06-02 21:00:12 +08:00
YuTengjing 8dee729f9f feat: add storage pay-as-you-go stubs and locale keys (#13501) 2026-06-02 20:45:15 +08:00
LiJian 359b348989 feat(agent-builder): add skill priority instruction and server runtime (#15409)
*  feat(agent-builder): add skill priority instruction and server runtime

- Add <skill_coexistence> section to agent-builder system prompt so the
  model always prefers Agent Builder tools over LobeHub skills for
  agent configuration tasks when both are active simultaneously
- Add agentBuilder server runtime to support background (QStash)
  execution: implements updateConfig, updatePrompt, searchMarketTools,
  getAvailableModels (DB-backed, LobeHub provider first, max 20 chat
  models), and installPlugin (market source only; official/OAuth tools
  return a clear unsupported error)
- Register agentBuilderRuntime in the server runtime registry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(agent-builder): fix identity confusion when user provides agent name/purpose

Add <identity_boundary> section and example to prevent the AgentBuilder
from roleplaying as the agent being configured. Short phrases like
"健康助手,咨询健康问题" must be interpreted as configuration requests,
not service requests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agent-builder): address three server runtime issues

- getAvailableModels: use AiInfraRepos instead of raw AiProviderModel +
  AiModelModel so builtin providers (DEFAULT_MODEL_PROVIDER_LIST) are
  included even when the user has no DB-customized providers
- installPlugin (official): allow builtin tools (lobe-web-browsing etc.)
  to be enabled directly; only block OAuth-requiring tools (Klavis,
  LobehubSkill) that cannot be installed in background context
- installPlugin (market): fetch and persist the marketplace MCP manifest
  on install so server tool discovery can find and execute the plugin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 17:10:30 +08:00
Arvin Xu 0c3450de7c test(model-runtime): classify ollamacloud "context window exceeds limit" as ExceededContextWindow (#15411)
* 🐛 fix(model-runtime): classify ollamacloud "context window exceeds limit" as ExceededContextWindow

ollamacloud surfaces context-window overflow as a generic 400 the upstream
labels ProviderBizError. Document the ollamacloud provenance on the existing
`context window exceeds` ECW pattern and add a regression test asserting the
message wins over the 400 / ProviderBizError catch-alls.

Fixes LOBE-9913

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔥 chore(model-runtime): drop redundant ollamacloud note on ECW pattern

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 16:07:43 +08:00
René Wang cbc259094d 📝 docs: add Codex & Claude Code agent guides; merge image+video generation (#15407)
- New docs/usage/agent/{codex,claude-code}.{mdx,zh-CN.mdx} cover how to
  delegate the Codex and Claude Code CLIs from the LobeHub desktop app
  (install, sign-in, working-directory pinning, in-chat tool renderers,
  resume behavior, execution targets, limitations).
- Rename docs/usage/getting-started/image-generation.{mdx,zh-CN.mdx} to
  generation.{mdx,zh-CN.mdx} and expand to cover the Video workspace
  alongside Image.
- Update <Card> links in sibling resource/vision docs to point at the
  new /generation slug.
2026-06-02 13:55:32 +08:00
Arvin Xu ccf33e8b98 🐛 fix(agent-runtime): classify topic/agent/session FK violations as ConversationParentMissing (#15408)
When a user deletes a topic (or agent/session/thread) while an agent operation
is still running, the assistant/tool-message INSERT fails with a Postgres 23503
foreign_key_violation on the corresponding `messages` FK. The persist-error
guard only recognised the `messages_parent_id_messages_id_fk` self-FK, so every
other reference deletion slipped through as a raw `Failed query: insert into
"messages"` 500 — surfacing to the user as a driver/SQL error and polluting the
error dashboard as DatabasePersistError noise (one of the longest-standing
top error categories).

Generalise `isParentMessageMissingError` → `isMidOperationReferenceMissingError`
to match a 23503 violation on any of the mid-operation-deletable `messages`
references (parent / quota message, topic, agent, session, thread). These all
mean "the referenced context was deleted mid-flight" — a lost race against the
user, not a runtime failure — so they are normalised to the typed, user-side
`ConversationParentMissing` error like the parent case already was.

Out-of-scope FKs (e.g. `messages_user_id_users_id_fk`, other tables) stay real
failures.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:23:19 +08:00
YuTengjing d1a6ffaf30 🔨 chore: tighten skill descriptions for triggering (#15397) 2026-06-02 13:00:52 +08:00
qybaihe 66c9339e98 fix(desktop): resolve CLI tools from shell PATH (#15368)
* fix(desktop): resolve CLI tools from shell PATH

* fix(desktop): validate resolved CLI with fallback PATH
2026-06-02 11:29:57 +08:00
LiJian 857aaf4766 feat(chat-input): show execution-device switcher for all agents (#15371)
 feat(chat-input): show execution-device switcher for all agents and add desktop download link

- Remove `!isHeterogeneous` guard so the device switcher surfaces for every agent type (not just non-heterogeneous), controlled by the existing Lab toggle
- Make the sandbox/runtime-env mode selector mutually exclusive with the device switcher: hide it when `enableExecutionDeviceSwitcher` is on
- Add a "下载桌面端 / Get Desktop App" quick link in the execution-device popover header (right side) linking to https://lobehub.com/downloads

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 11:20:52 +08:00
Innei 4e91a3181d ♻️ refactor(modal): convert create custom model modal to base-ui imperative API (#15401)
* ♻️ refactor(modal): convert create custom model modal to base-ui imperative API

* ♻️ refactor(modal): convert edit model modal to base-ui imperative API

* 🐛 fix: make extend params preview read-only
2026-06-02 12:18:27 +09:00
Arvin Xu c9ca46e1e0 chore: remove LOBE-XXX annotations from code comments (#15398)
Replaced all LOBE-XXX references in comments with descriptive context
instead of internal Linear issue markers. As an open-source project, these
internal references should not be exposed.

Changes:
- LOBE-9834 (empty completion): replaced with inline descriptions of the
  "empty completion" failure mode
- LOBE-6587 (task scheduler): replaced with "task scheduler infra" ref
- LOBE-6634 (getTaskDetail model/provider): updated TODO description
- LOBE-9434 #5/#7 (execAgent migration): removed issue markers

12 files changed

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2026-06-02 10:46:44 +08:00
Innei 37db828c17 ♻️ refactor(modal): convert feedback & changelog modals to base-ui imperative API
Migrate `FeedbackModal` and `ChangelogModal` from declarative `@lobehub/ui`
modals + a `useFeedbackModal` zustand store to the `@lobehub/ui/base-ui`
imperative `createModal()` API. Call sites now invoke `openFeedbackModal()`
/ `openChangelogModal()` directly — no more open/close state plumbing
through `(main)/_layout` or `(mobile)/me/(home)`. The `useFeedbackModal`
hook is removed.

Also:
- Wrap the email address in `feedback.emailContact` with a `<email>` tag
  (Trans component → mailto link); all 18 locale files updated.
- Restore the external link on the changelog modal header pointing to
  `CHANGELOG_URL`; the previous Button used `onClick={onClose}` despite
  the ArrowUpRight icon suggesting an external navigation.
- Footer test mocks updated to match the new module exports.
2026-06-02 03:00:48 +09:00
Innei 0208c0adfe 🐛 fix(chat-input): restore editor focus after file picker closes (#15394)
Picking files via the antd `Upload` dropdown (paperclip / plus menus) and via
the AgentTasks attachment helpers leaves focus on `document.body` once the OS
file picker dismisses, so the editor cursor disappears and users do not know
they can keep typing.

Refocus the editor right after the file picker yields:

- `ChatInput/ActionBar/Upload`: subscribe to `editor` from the chat input
  store and call `editor?.focus()` in the three `beforeUpload` handlers
  (image / file / folder).
- `ChatInput/ActionBar/Plus`: same fix for the unified file-or-image upload
  entry; add `editor` to the items `useMemo` deps.
- `EditorCanvas/editorAttachments`: refocus inside `insertFilesIntoEditor`
  so every AgentTasks composer (CommentInput, CommentCard, FeedbackInput,
  CreateTask*, TaskInstruction via `pickAndInsertAttachments`) recovers
  the cursor too.

Fixes LOBE-9862
2026-06-02 01:19:36 +08:00
Rylan Cai 09a57d4618 🐛 fix: clarify local command session handling (#15389) 2026-06-02 00:55:17 +08:00
Arvin Xu 73dd0ef136 🔖 chore(cli): bump @lobehub/cli to 0.0.24 (#15393)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 00:45:24 +08:00
Innei d2e4833f1e 🐛 fix(chat-input): close skill dropdown before navigating to settings (#15391)
The skill market dropdown's settings button navigates to /settings/skill
but does not close the controlled dropdown first, so the popup persists
after the trigger has unmounted (LOBE-9852).

Also restore the negative margins on the skill market footer (regressed
in #15214 when bumping @lobehub/ui to 5.15.1) so the stats row aligns
with the dropdown's outer padding.

Apply the same defensive close-before-navigate to ModelSwitchPanel:
- ListItemRenderer no-provider / empty-model rows previously navigated
  without calling onClose at all.
- Footer and GenerationListItemRenderer now close before navigate
  instead of after, for a consistent ordering.

Closes LOBE-9852
2026-06-02 00:32:12 +08:00
Innei 5119c0802d 🐛 fix(topic): strip markdown tokens from fallback titles (#15372)
Sliced raw user input was leaking syntax (#, **, ``` etc.) into topic / thread / agent / group / document titles whenever LLM summarization had not yet produced a clean title. Run the source string through `markdownToTxt` (remove-markdown) before slicing so the visible fallback is plain text.
2026-06-01 23:22:46 +08:00
Innei 3e51b87b1e 💄 style(sidebar): group spacer with recents and agents (#15373) 2026-06-01 23:22:07 +08:00
Arvin Xu 1e8b5959da ♻️ refactor(topic): drop legacy session→agentId compatibility from topic queries (#15378)
* ♻️ refactor(topic): drop legacy session→agentId compatibility in topic queries

Topic ownership is fully migrated to `topics.agentId`, so the
`agentsToSessions` lookup that mapped a legacy `sessionId` back to an agent
is no longer reachable in practice. Remove it from the agent query, count,
and batch-delete paths — they now match `topics.agentId` directly.

- `query()`: drop the `agentsToSessions` pre-query and the `sessionId` OR
  branch; keep the inbox fully-orphan fallback (all owner columns null),
  which is unrelated to session linkage.
- `count()` / `batchDeleteByAgentId()`: match `topics.agentId` only.
- Remove the now-unused `agentsToSessions` import.

Tests updated to assert session-only legacy topics are no longer matched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): make topic rank/recent agent-centric, drop returned sessionId

Topic ownership is `topics.agentId`, so the topic ranking and recent-topic
queries no longer need to expose or resolve a legacy `sessionId`.

- `TopicModel.rank()` now selects `topics.agentId` instead of `sessionId`;
  `TopicRankItem.sessionId` → `agentId`.
- `TopicModel.queryRecent()` stops selecting `sessionId`.
- `recentTopics` TRPC procedure: drop the `agentsToSessions` batch resolve
  and the `after()` runtime agentId backfill — both keyed off the legacy
  session mapping. Agent topics now map straight through `topic.agentId`.
- Topic ranking UI navigates to `SESSION_CHAT_TOPIC_URL(agentId, topicId)`
  (`/agent/:aid/:topicId`), falling back to the inbox agent id when a topic
  has no agentId, replacing the old `/agent?session=...` query-param link.

Rank test asserts `agentId`; the broader `getTopics` session-resolution
path is intentionally left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  revert(topic): restore session→agentId resolution in query/count/delete

The integration tests (topic.integration.test.ts) showed this compatibility
is still load-bearing: the topic write path (createTopic / batchCreateTopics
/ updateTopic) persists `sessionId` with `agentId = null`, so dropping the
read-side session→agentId resolution made freshly-created topics
unqueryable/undeletable by agentId.

Revert the read-side removal from `query()` / `count()` /
`batchDeleteByAgentId()` (and their tests) until the write path is migrated to
store `agentId` directly. The agent-centric `rank()` / `queryRecent()` /
`recentTopics` surface changes are kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): drop session→agentId compatibility from topic read paths

Topic ownership is fully migrated to `topics.agentId` (old data backfilled,
new app no longer depends on sessionId), so the legacy session resolution in
the topic read paths is dead and can go.

- `query()` / `count()` / `batchDeleteByAgentId()`: match `topics.agentId`
  directly; drop the `agentsToSessions` lookup + `topics.sessionId` OR branch.
  The inbox fully-orphan fallback (all owner columns null) is kept.
- `getTopics` TRPC procedure: drop the `after()` runtime agentId backfill and
  the now-unused `AgentMigrationRepo` wiring / `after` import. The sessionId→
  agentId reverse-resolution of the query *filter* is kept for clients that
  still pass a sessionId.
- Update topic integration + model tests to agent-native fixtures; remove the
  legacy-session and runtime-migration cases that exercised the removed path.

The write path (createTopic/batchCreate/updateTopic) is intentionally left
unchanged per scope; no data migration is performed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): keep getTopics runtime agentId backfill during transition

Restore the `after()` runtime migration in `getTopics` (and the
`AgentMigrationRepo` wiring / `after` import). The read paths no longer
resolve sessionId, but the backfill is still needed to migrate straggler
legacy (sessionId-only) topics over the transition window; a legacy topic is
backfilled on first query and becomes agentId-queryable thereafter.

Restore the migration integration tests, adjusted: they assert the agentId
backfill happens after the query rather than expecting legacy rows in the
first (now agentId-only) response.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(topic): keep recentTopics runtime agentId backfill

Restore the recentTopics session→agentId backfill removed earlier: re-select
`sessionId` in `queryRecent` (internal only — not exposed in the RecentTopic
response) and re-add the `batchResolveAgentIdFromSessions` resolution + the
`after()` migrateAgentId backfill. Like the getTopics backfill, this keeps
migrating straggler legacy (sessionId-only) topics during the transition.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 chore(topic): drop unnecessary comment churn in topic router/model

Revert the migration/backfill comments to their original wording so the
restored getTopics/recentTopics blocks are byte-identical to canary, and drop
the extra queryRecent select comment. No logic change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(agent): replace session-based assistant ranking with agent-native rank

The assistant usage ranking was session-centric (SessionModel.rank joining
agentsToSessions, returning a sessionId; UI linked /agent?session=...). Rework
it as agent-native:

- Add `AgentRankItem` type (id = agentId); remove `SessionRankItem`.
- Add `AgentModel.rank`: count topics grouped by `topics.agentId`, joined to
  agents for avatar/title, ordered by count. Mirrors the recents filter
  (real agents + inbox, excluding other virtual agents). No sessions involved.
- Add `agent.rankAgents` TRPC procedure + `agentService.rankAgents`; remove
  `session.rankSessions`, `sessionService.rankSessions`, `SessionModel.rank/_rank`.
- AssistantsRank UI: navigate to `SESSION_CHAT_URL(agentId)` → `/agent/:aid`,
  resolving the inbox title via the store's inboxAgentId.

Move the rank tests from session.test.ts to agent.test.ts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:51:28 +08:00
Rylan Cai 5b25b8d8bb ️ perf: skip user count on api key checks (#15385) 2026-06-01 22:25:38 +08:00
YuTengjing fd82f6fd0e 🐛 fix: add restore subscription copy (#15388) 2026-06-01 21:31:22 +08:00
YuTengjing 80c11a09e2 🐛 fix: stabilize home starter loading (#15386) 2026-06-01 20:48:48 +08:00
Arvin Xu c8096590c4 feat(topic): add group-by-status mode to topic sidebar (#15366)
*  feat(topic): add group-by-status mode to topic sidebar

Add a new "By status" grouping option to the agent topic sidebar. Topics
bucket into fixed-priority groups — waitingForHuman first, then running,
then active, with the remaining states below. Topics without a status are
treated as active. Only non-empty groups render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(topic): resolve group-by-status ordering on the server

The sidebar only loads the first page of topics, so grouping that partial
list client-side could hide high-priority topics (awaiting-human / running)
that live on a later page. Push the ordering to the query instead.

- Add `sortBy: 'updatedAt' | 'status'` to the topics query (TRPC + model).
  `status` orders by a priority CASE (waitingForHuman → running → active →
  paused → failed → completed → archived) before the updatedAt tiebreaker,
  so the most important topics always land on the first page.
- Plumb `sortBy` through the service, store fetch action (incl. SWR key),
  and the shared topic hooks; `useFetchChatTopics` requests `status` ordering
  only when the resolved agent group mode is `byStatus` (group sessions keep
  the default). The client still buckets for display, now over a correctly
  ordered page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(topic): bucket streaming topics under "running" in group-by-status

A topic generating a response shows the loading ring via the client-only
`topicLoadingIds` state, not a persisted `status`, so it was landing in the
"active" group. Mirror the sidebar TopicItem icon precedence when bucketing:
waitingForHuman wins, then a topic that is streaming on this client (or
persisted as running) goes to "running", then the persisted status.

The loading overlay stays client-side (the server can't know a given client
is mid-stream); the selector passes `topicLoadingIds` into the byStatus
grouping only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 19:44:38 +08:00
YuTengjing dda527926d feat: support configurable model routing and starters (#15384) 2026-06-01 18:46:00 +08:00
YuTengjing 5f0fa7bf50 🐛 fix: block disabling official provider (#15382) 2026-06-01 17:39:54 +08:00
YuTengjing c50d790feb 🔨 chore: pin Vitest 3.2.4 (#15383) 2026-06-01 17:30:31 +08:00
LiJian 4d030e9db1 🐛 fix(agent-manager): guard createAgent against LLM double-encoded array fields (#15381)
* 🐛 fix(agent-manager): guard createAgent against LLM double-encoded array fields

When an LLM calls createAgent, it can send plugins/tags/openingQuestions
as a JSON string (e.g. '["lobe-cloud-sandbox"]') instead of a proper
array. This caused jsonb to store a double-serialized string rather than
an array, breaking downstream ETL queries with "cannot get array length
of a scalar".

updateAgentConfig already had this guard (line 130+); apply the same
parseArrayParam helper to all three array fields in createAgent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agent-manager): guard server-side createAgent against double-encoded array fields

Same LLM double-encoding guard applied to the server-side execution path
(src/server/services/toolExecution/serverRuntimes/agentManagement.ts),
which directly calls agentModel.create() and was equally vulnerable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 17:00:42 +08:00
YuTengjing 56ce192c61 🐛 fix: default provider setup in business mode (#15379) 2026-06-01 16:03:14 +08:00
YuTengjing 21a73b22b2 feat: support MiniMax M3 Anthropic video runtime (#15380) 2026-06-01 16:00:06 +08:00
LiJian 818e67d1f0 🐛 fix(gateway): prevent duplicate streaming from stale reconnects (#15354)
* 🐛 fix(gateway): prevent duplicate streaming from stale reconnects

When a new agent execution starts for a topic that has a stale
`runningOperation` in its metadata, `useGatewayReconnect` would still
attempt to reconnect to the old operation concurrently with the new one,
producing duplicate streaming events.

Fix by:
1. Optimistically updating the topic's `runningOperation` to the new op
   immediately after `executeGatewayAgent` creates it, and disconnecting
   any live reconnect connection for the stale op.
2. Skipping `connectToGateway` in the reconnect path when the topic
   already has a newer running operation ID.

* 🐛 fix(gateway): add post-refresh stale-op guard and fix test mocks

Two issues fixed:

1. Race condition: the `reconnectToGatewayOperation` guard only ran before
   `refreshGatewayToken`. A stale reconnect that passed the initial check
   could still proceed after the token refresh if `executeGatewayAgent`
   started a new operation during the await. Re-check `runningOperation`
   after the token refresh to bail out in that case.

2. Test failures: the `executeGatewayAgent` mock state was missing
   `topicDataMap`, causing `topicSelectors.getTopicById` to crash with
   "Cannot read properties of undefined". Added `topicDataMap: {}` and
   `internal_dispatchTopic` to both mock setups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 15:18:06 +08:00
Arvin Xu e14f2e96f6 🐛 fix(cli): auto-register device on login, matching desktop (#15377)
* 🐛 fix(cli): auto-register device on login, matching desktop

Device registration previously only ran in `lh connect`, so `lh login`
left no device row until the user separately connected the gateway. The
desktop app registers on login; this aligns the CLI.

Extract the shared identity-resolution + register logic into
`device/register.ts` (`resolveDeviceIdentity` + `registerDevice`) and call
it from `login` right after auth (best-effort, non-fatal). `connect` keeps
its own call as an idempotent fallback for `--token` sessions that never
went through login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(cli): skip login-time device registration for fallback identities

When node-machine-id can't read a machine id, deriveDeviceId returns a
fresh random id with identitySource 'fallback'. Since `lh login` has no
--device-id and persists no fallback id, registering it on every login
spawns orphan device rows that never match the id a later `lh connect`
resolves. Defer registration to connect in that case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 13:24:22 +08:00
Arvin Xu cf5ec7b96a test(hetero-agent): use canonical usage fields in persistence fixtures (#15375)
The HeterogeneousPersistenceHandler fixtures wrote `metadata.usage` as
`{ inputTokens, outputTokens }` — a shape the het adapters never emit. Both
claudeCode/codex build canonical `UsageData`
(`totalInputTokens`/`totalOutputTokens`/`totalTokens`) via `toUsageData()` and
`handleTurnMetadata` persists it unchanged, so production het messages already
carry canonical fields that the topic usage rollup sums correctly.

The unrealistic alias fixtures made it look like rollups would store
`total_* = 0` for Claude Code/Codex topics. Align the fixtures with real
adapter output.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:51:31 +08:00
Arvin Xu c3f91f10ac feat(database): maintain denormalized topic usage/cost rollup from messages (#15365)
*  feat(database): maintain denormalized topic usage/cost rollup from messages

Topics carry usage/cost aggregate columns (total_input_tokens /
total_output_tokens / total_tokens / total_cost / usage / cost / model /
provider) mirroring agent_operations, but nothing populated them. Add a
canonical derived-projection rollup maintained live from the topic's messages.

- `recomputeTopicUsage(trx, userId, topicId)` (new `models/topicUsage.ts`):
  sums the topic's `role='assistant'` messages (thread messages included — they
  carry topic_id too) over the canonical `metadata.usage`, grouped by
  (provider, model). Writes the same shape as agent_operations: scalar totals,
  a flat `usage` jsonb ({ llm:{ apiCalls, processingTimeMs, tokens }, tools,
  humanInteraction }), and a `cost` jsonb ({ total, currency, llm:{ byModel[] },
  tools }) — NULL when no model reported cost. `model`/`provider` = the
  dominant model by total tokens. Pure derived & idempotent: resets to NULL
  when no measurable usage remains, so deletes/regenerations are reflected.

- Hook it into MessageModel at the shared chokepoints, inside the existing
  transactions: `update()` (only when the incoming payload carries
  `metadata.usage`, i.e. assistant finalize / hetero step — streaming
  content-only updates don't trigger it) and `deleteMessage()` /
  `deleteMessages()` (recompute affected topics). This covers all LLM-call
  write paths since they funnel through MessageModel.update.

- `TopicModel.recomputeUsage(id)` wraps the canonical fn in a transaction for
  external callers (e.g. the historical backfill).

Tool/human-interaction sub-totals are left as a zero skeleton (not
reconstructable from assistant messages); the historical backfill will reuse
the same aggregation via raw SQL without bumping updated_at.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(database): cover topic usage/cost rollup recompute

Add tests for the denormalized topic usage rollup: core
`recomputeTopicUsage` aggregation (per-model grouping, dominant model,
cost-null path, userId scoping, NULL reset), the `TopicModel.recomputeUsage`
wrapper, and the `MessageModel` update/delete hooks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:21:47 +08:00
Arvin Xu 650a178709 feat(agent-signal): register self-iteration builtin tool package (#15364)
Part of LOBE-9434 (#3). Gives the three (currently dormant) self-iteration
builtin agents a declarative tool surface so they no longer run with no tools.

One package `@lobechat/builtin-tool-agent-signal` with internal subdirs:
- `shared/`: the 3 stable identifiers, JSON-schema specs for the full tool
  surface (resource / review / reflection), a result-kind map (read | artifact
  | mutation — the LOBE-9434 #5 discriminator), `createAgentSignalManifest`,
  and one shared `AgentSignalToolExecutionRuntime` that dispatches per api name
  and stamps every result with its kind so `extractFromFinalState` can
  partition outcomes from a persisted snapshot.
- `review/` `reflection/` `feedback-intent/`: per-mode manifests assembled from
  the shared specs + a mode-specific system prompt, exported under their three
  stable identifiers. Review = resource + proposal/idea tools; reflection &
  feedback-intent share the resource + reflection-recorder surface.

Registered all three manifests in `@lobechat/builtin-tools`. `executors` is
omitted on purpose — BuiltinToolManifest defaults to server-only execution.

The server-side execution bridge (wiring the ExecutionRuntime to the existing
createToolSet(adapters) from ToolExecutionContext) lands with the
executeSelfIteration -> execAgent migration (#7); the ExecutionRuntime is
exported (./executionRuntime) and ready for it. No production self-iteration
path is touched — executeSelfIteration still serves all current runs.

Tested: shared ExecutionRuntime (dispatch + kind stamping + error handling) and
review manifest structure. bun run type-check clean for everything touched.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 12:09:06 +08:00
Arvin Xu e1d6b30127 🐛 fix(desktop): relocate visual-ref helpers to @lobechat/const to fix renderer crash (#15369)
🐛 fix(desktop): relocate visual-ref helpers to @lobechat/const

PR #15114 added @lobechat/prompts + @lobechat/tool-runtime to the desktop
nested pnpm workspace. pnpm then linked their @lobechat/types dependency to
the desktop type-only stub (apps/desktop/stubs/types) inside the SHARED
packages/*/node_modules, which the renderer/web build also consumes. The
runtime value import `createVisualFileRef` (in prompts/files/{image,video}.ts)
resolved to the stub — which only surfaces types — so the renderer crashed on
boot with `SyntaxError: ... does not provide an export named createVisualFileRef`.

The stub is load-bearing: pointing the desktop workspace at the real
@lobechat/types fails install (model-bank@workspace:* dependency cascade), so
the stub must stay. Fix the contract instead: visual-ref helpers are runtime
logic, not types, so they don't belong in @lobechat/types. Move the
zero-dependency helpers to @lobechat/const/visualRef (already a real desktop
member, no cascade) and import them via the narrow subpath. prompts/tool-runtime
now only `import type` from @lobechat/types, so the stub link is harmless.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:46:03 +08:00
Arvin Xu 7d1086b096 🐛 fix(remote-device): preserve content/state across gateway tool calls (#15114)
The cloud→gateway→desktop path was JSON.stringify-ing the entire IPC result
into `content`, dropping `state` and leaking `{success: true, ...}` into the
LLM-facing prompt. Routes remote tool calls through `LocalSystemExecutionRuntime`
(same runtime the renderer uses) so `content` is the formatted prompt and
`state` flows through `DeviceProxy` → `RuntimeExecutors` into `pluginState`.

Also moves `LocalSystemExecutionRuntime` from `@lobechat/builtin-tool-local-system`
(renderer-coupled, React/antd peers) into `@lobechat/tool-runtime` so the
desktop main process can reuse it without pulling UI deps.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 11:01:25 +08:00
LiJian 58c671b7ac 🐛 fix(agent-builder): explicitly sync editing agent ID to chatStore (#15357)
The Agent Builder reads the wrong agent's context because
`getChatStoreState().activeAgentId` — which the chat service uses to
build `agentBuilderContext` — can drift from the agent currently open in
the profile editor under certain timing conditions (SWR cache hits,
navigation order, React effect scheduling).

Fix: `AgentBuilderProvider` now accepts an `editingAgentId` prop and
writes it to `chatStore.activeAgentId` in a `useEffect`. This makes
the data flow explicit instead of relying on `AgentIdSync` alone.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 10:19:38 +08:00
Arvin Xu e0ead38c20 ♻️ refactor(agent-signal): restore 3 mode-specific self-iteration agent slugs (#15202)
The Phase 1 consolidation into a single `self-iteration` slug (PR #15187,
inheriting commit 627f899895 from the closed #15116) conflated three
distinct background flows that have:

- Independent receipt tables and idempotency Redis namespaces
- Different preflight / brief projection paths
- Different audit pipelines

`one identifier = one behavior` is a load-bearing contract once these
agents are routed through the standard execAgent plugin lookup. Restore
the 3 mode-specific slugs so each agent declares its own tool surface:

| slug                    | future plugin identifier        |
| ----------------------- | ------------------------------- |
| `nightly-review`        | `agent-signal-review`           |
| `self-reflection`       | `agent-signal-reflection`       |
| `self-feedback-intent`  | `agent-signal-feedback-intent`  |

`SELF_ITERATION_AGENT_SLUGS` now contains all three; `completionPolicy`
dispatches on slug membership rather than equality; callback receives
the resolved `agentId` so mode-specific bookkeeping can route from it.

Plugin arrays reference the future identifiers but the tool packages
are not yet registered — invoking any of these agents today runs the
LLM with no tools (dormant by design). Tool-package registration
follows in a separate PR.

No behavior change for existing callers (none invoke these slugs yet).
2026-06-01 10:03:42 +08:00
René Wang f71be63bea 📝 docs: add May 26 weekly changelog (#15183) 2026-06-01 09:36:32 +08:00
Tsuki 4d840e9071 feat(push): add PushChannel, receipt cron, and pushToken tRPC API (#15233)
Send-side machinery for mobile push notifications (LOBE-8771), stacked
on top of the schema PR (#15186).

### tRPC
- `pushToken.register` / `pushToken.unregister` exposed on both
  `MobileRouter` and `LambdaRouter`.

### `PushChannel`
- Structurally compatible with cloud's `NotificationChannel` so cloud
  can register it without casts.
- Fans a single notification out to all of a user's tokens, chunks via
  `expo-server-sdk`, respects the 600 msg/sec project limit with 100ms
  throttle between chunks.
- Embeds `(ticketId, expoToken)` pairs in `providerMessageId` for
  receipt reconciliation.
- Returns `no_tokens` / `invalid_tokens` / `rate_limited` /
  `all_send_failed` so callers can distinguish.

### `processPushReceipts`
- Pure helper to be called by cloud's Vercel cron (companion PR).
- Polls Expo receipts in parallel (`Promise.all` across chunks),
  updates `notification_deliveries` in bulk, prunes `push_tokens` rows
  flagged `DeviceNotRegistered`.
- Configurable lookback window + min-age guard (default: 24h / 15min).

### Dev tooling
- `/api/dev/test-push` (404s in production) lets you fire a real push
  directly to a user's registered tokens, bypassing `NotificationService`.
  Useful for end-to-end verification before cloud submodule sync.

### Types
- `NotificationSettings` gains an optional `push` channel.

Tests: 21 added (router 7, PushChannel 7, processPushReceipts 7).

Linear: https://linear.app/lobehub/issue/LOBE-8771

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 01:49:04 +08:00
Arvin Xu d382df1b2c ♻️ refactor(agent-runtime): persist canonical nested usage/performance on assistant messages (#15359)
 feat(agent-runtime): persist canonical nested usage/performance on assistant messages

The standard agent chat path (RuntimeExecutors) only flattened token usage
onto message metadata and never persisted performance metrics, while the
heterogeneous and client store paths already wrote the canonical nested
`metadata.usage` / `metadata.performance`. Converge the server path so all
writers produce the same shape:

- capture `data.speed` (ModelPerformance) from the model-runtime onCompletion
  callback and write `metadata.performance`
- write nested `metadata.usage` alongside the existing flat fields (kept for
  backward-compatible readers) on both the normal and interrupted finalize
- read usage/performance from the nested shape first (flat fallback) in the
  usage service

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:28:53 +08:00
Zhijie He d75e193ea0 💄 style: add intern-s2-preview support, support thinking_mode (#15308)
* style: add intern-s2-preview support, support thinking_mode

* chore: remove stream limited with tools

* fix: fix search missing for intern-s1-pro

* chore: migrate to processModelList for model fetch

fix: fix ci error
2026-06-01 01:20:00 +08:00
Arvin Xu 7989952d2e feat(agent-signal): add CLI trigger command + golden snapshot fixture (#15360)
Part of LOBE-9434 (#1 / LOBE-9435). Adds the local verification tooling the
execAgent migration depends on.

- `lh agent-signal trigger`: enqueue any producer-side Agent Signal source
  event for the authenticated user (nightly_review / self_reflection /
  self_feedback_intent / user.message / tool.outcome.*)
- server: `buildTriggerSourceEvent` default-payload builder +
  `AGENT_SIGNAL_TRIGGER_SOURCE_TYPES` allowlist, surfaced through a new
  authed `agentSignal.triggerSourceEvent` tRPC procedure that re-derives
  userId from context (owner-scoped, override can't repoint it)
- golden snapshot fixture + dependency-free `assertGoldenFinalState`
  structural assertion (ideas/intents/writeOutcomes >= 1, brief non-empty)
  for use by the migration regression tests
- builder unit tests + offline/live e2e, regenerated man page

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 01:06:09 +08:00
Tsuki 480f6a8e7b feat(task): support file & image attachments (#15141)
*  feat(task): support file & image attachments (LOBE-8967)

Adds attachment / image upload to all four Task input surfaces (Create
Modal, Inline Entry, Task Instruction, Comment Input, Feedback Input)
plus comment edit. Attachments persist in `tasks.editor_data` /
`task_comments.editor_data` as part of the Lexical JSON state and flow
into agent runs via `execAgent.fileIds` — images as multimodal vision
content, documents through `documentService.parseFile` for text
extraction.

Server-side fileId resolution rides on the editor's
`extractMediaFromEditorState` (`@lobehub/editor/headless` 4.15.1), so
no junction tables are needed — editor_data is the single source of
truth. The /f/{fileId} proxy URL contract from the file router stays
the bridge between editor URLs and backend file lookup.

Five UI surfaces share `EditorCanvas` + `editorAttachments` for inline
attachment insertion. Comment display renders the Lexical state via
`@lobehub/editor/renderer`'s `LexicalRenderer` so image sizes round-
trip without the EditorCanvas hydration flash.

DB schema (`tasks.editor_data jsonb` column) landed separately via
#15280.

Fixes LOBE-8967

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): correct fileId prefix + accept nodes without status

Real-world editor_data exposed two bugs in the regex-based extract:

1. `fileId` prefix was wrong — the regex looked for `fle_…` but
   `idGenerator('files')` actually produces `file_…`, so every proxy
   URL `/f/file_…` silently failed to match.
2. `@lobehub/editor`'s `extractMediaFromEditorState` requires
   `status === 'uploaded'` strictly. Editor data from the cloud upload
   path and from historical inserts omits the `status` field entirely,
   so the upstream helper silently dropped everything. Walk the tree
   ourselves and treat a missing `status` as uploaded.

Verified against real `tasks.editor_data` rows: T-6 (proxy URL form)
now extracts `file_…` correctly. T-8 (cloud R2 signed URL form) still
returns `[]` — that requires either aligning cloud's `createFile` to
return the proxy URL or adding a DB-fallback resolver, tracked as a
follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): resolve fileIds from pre-signed editor URLs via files.url lookup

Root cause: `fileService.getFileAccessUrl()` returns different URL forms
depending on the environment:

- prod / non-dev → `getFileProxyUrl(fileId)` = `${APP_URL}/f/{fileId}`
- dev → `getFullFileUrl(file.url)` = a pre-signed R2/S3 URL

The dev branch is intentional so remote model providers can fetch the
file directly (proxy URLs point to localhost and aren't reachable). But
the pre-signed URL doesn't contain the fileId anywhere, so our regex
extract silently returned [] for every local upload — agent never saw
any attached image.

Same shape happens for historical cloud data where the editor stored
pre-signed URLs.

Fix: make `extractFileIdsFromEditorData` async and take a `{ db, userId }`
context. Fast path stays the proxy-URL regex; URLs that don't match fall
back to a single batched `SELECT id FROM files WHERE user_id = ? AND url
IN (…)` keyed on the storage path extracted from each URL's pathname.

Verified against real local data:

  T-6 (proxy URL form)         → file_2vFD2sdzW9VO   (regex fast path)
  T-8 (pre-signed R2 URL)      → file_cAQ4naT8G8r5   (DB fallback)
  T-9 (pre-signed R2 URL × 2)  → file_…, file_…      (DB fallback)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): dedupe fileIds by storage key in DB fallback

Same bytes re-uploaded by the same user produce multiple `files` rows
with identical `url` + `file_hash`. The DB fallback in
`extractFileIdsFromEditorData` was returning every matching row, so a
task with one inline image but three historical upload attempts fed
the agent three copies of the same image — wasteful multimodal tokens
and noisy provider input.

Group results by `files.url` and keep the first row per key. Verified
against real local data:

  T-6  (1 img, 1 upload)              → 1 fileId
  T-8  (1 img, 1 upload)              → 1 fileId
  T-9  (1 img, 2 dup uploads)         → 1 fileId (was 2)
  T-10 (1 img, 3 dup uploads)         → 1 fileId (was 3)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(editor): render inline file nodes as block-level cards

The default @lobehub/editor `ReactFile` decorator paints file attachments
as a tiny inline pill (icon + filename in monospace, inline-block with
0.4em padding), so a single PDF on its own line looked cramped and
hugged the surrounding text.

Override the upstream styling via the `className` prop the plugin
already exposes: full-width flex row, 10px gap, 14px padding,
`borderRadiusLG` corner, subtle hover, primary tint on `.selected`.
Aligns the editor's file attachment row with the Linear attachment
card look — and with the LexicalRenderer card the comment thread
already uses, so the same file looks consistent across surfaces.

The upstream component still only renders icon + name (no size), but
the layout change is the main UX win.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(editor): Linear-style file card with hover download

Replace the upstream inline pill FileNode UI with a full-width card
(icon + name + size + hover-revealed download button) wired in both the
live editor and the read-only LexicalRenderer for saved comments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(editor): use existing editor:file.* keys for file card states

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 00:34:18 +08:00
Arvin Xu 45a6f2b440 🐛 fix(agent-runtime): retry empty LLM completions instead of silent done (#15355)
When a stalled tool loop made the model return an empty completion (no
content, no tool calls, ~0 output tokens), the harness finalized the
operation to `done` and persisted a blank assistant message — an empty
bubble with `status=done, error=null`, completely silent.

The call_llm executor now detects this "gave up" turn and throws
`ModelEmptyError`, which its existing LLM retry loop catches and re-issues
(a retry usually yields real content). Empty completions use a dedicated
retry budget (EMPTY_COMPLETION_MAX_RETRIES) so the branded provider — which
has 0 general retries because its own fallback chain re-routes failed
requests — still re-issues an HTTP-200-but-empty turn (the LOBE-9834 repro
path). If every retry is also empty, it propagates to a readable,
dashboard-visible terminal error (`ModelEmptyCompletion`, E8014, provider
attribution, countAsFailure) instead of a silent done.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 23:37:36 +08:00
LiJian 4bc77fc103 🐛 fix(creds): replace hardcoded session_context values with template variables (#15352)
* 🐛 fix(creds): replace hardcoded session_context values with template variables

- Replace hardcoded `Current user`, `Session date`, `Sandbox mode` in
  systemRole.ts with {{username}}, {{session_date}}, {{sandbox_enabled}}
- Inject {{session_date}} via Intl.DateTimeFormat in RuntimeExecutors
- Remove isCredsEnabled gate so {{CREDS_LIST}} / {{KLAVIS_SERVICES_LIST}}
  are always substituted when userId is available, regardless of execution path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🧪 test(creds): mock klavisEnv to prevent t3-oss jsdom throws in tests

klavisEnv uses @t3-oss/env-nextjs which throws in jsdom (vitest treats
it as a client context). Previously the isCredsEnabled gate short-circuited
before the access; now that the gate is removed, the mock is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(creds): add client-side generators and restore isCredsEnabled gate

- Add session_date and sandbox_enabled variable generators to
  contextEngineering.ts so client-side renders substitute them correctly
- Restore isCredsEnabled gate in RuntimeExecutors to avoid fetching creds
  on every call_llm step; now checks both enabledToolIds (client-activated
  path) and manifestMap (execAgent path) to cover all execution paths

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 chore(creds): revert isCredsEnabled gate in RuntimeExecutors

Remove the isCredsEnabled OR-condition that caused execAgent test failures.
Keep session_date, sandbox_enabled, and always-inject CREDS_LIST/KLAVIS_SERVICES_LIST.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 20:03:51 +08:00
Arvin Xu e4d5017e76 feat(device): add recent directory management with drag-to-reorder (#15351)
Add a full-width "Add directory" button to pick a folder via the native
picker, make the recent directories list reorderable via SortableList, and
drop the Save button so all device edits (name, default cwd, recent dirs)
persist immediately.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:22:34 +08:00
Zhijie He 27121a6f1a 💄 style: add step-3.7-flash support (#15317)
* style: add step-3.7-flash support

* chore: support step-3.5 reasoning effort
2026-05-31 17:02:32 +08:00
Arvin Xu 373b5e90b2 style(device): run remote CC on a configured device (#15343)
*  feat(device): run remote CC on a configured device with cwd + device context

Make `claude-code`/`codex` dispatched to an `lh connect` device (executionTarget
='device') run in the user's configured directory with a device-appropriate
system context, instead of inheriting the cloud-sandbox setup.

3a — server cwd passthrough:
- resolve the run cwd in the useDevice branch: topic-level workingDirectory
  override > the bound device's `defaultCwd` (read from DB via DeviceModel; the
  gateway only knows live connections, not the user-owned cwd), and pass it to
  dispatchAgentRun.

3b — device-specific systemContext, end to end:
- new `buildRemoteDeviceHeteroContext` — strips the cloud-sandbox boilerplate
  (ephemeral /workspace, pre-cloned repos, commit-or-lose warnings) that would
  mislead an agent on the user's own persistent machine; keeps agent static
  context + resumed conversation history + a minimal cwd note.
- thread `systemContext` through the contract: AgentRunRequestMessage,
  GatewayHttpClient.dispatchAgentRun, deviceProxy.dispatchAgentRun.
- desktop: spawnLhHeteroExec now injects systemContext as the first text block
  of a content-block array on stdin (mirrors spawnHeteroSandbox); previously it
  wrote only the bare prompt, so any context was silently dropped.

The gateway relays unknown fields transparently (`...runParams`), so no gateway
change is needed.

Tests: buildRemoteDeviceHeteroContext unit (6) + GatewayConnectionCtr forwards
cwd/systemContext. type-check clean; existing device/desktop/pkg suites green.

Part of LOBE-9579 (Step 3a/3b). Old ephemeral boundDeviceId migration (3d) and
the web cwd picker (3c) are out of scope here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(device): optimistic device cwd persistence (defaultCwd + recentCwds)

Foundation for the device-scoped cwd picker (executionTarget=device): persist a
working-directory pick to the bound device's registry record so the server's
hetero dispatch (which reads device.defaultCwd) stays in sync and the picker can
offer recent dirs.

- nextRecentCwds: pure most-recent-first / dedupe / cap-20 list builder (the
  server stores recentCwds verbatim, so the client owns this) — unit tested.
- useUpdateDeviceCwd: optimistic `device.updateDevice` — patches the listDevices
  cache in onMutate for instant UI, invalidates onSettled to re-sync truth (self-
  corrects a failed write without manual rollback).

Not yet wired into a picker — the target=device recentCwds-list + manual-input
picker mode that consumes this is the next step.

Part of LOBE-9579 (Step 3c, data layer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(device): gate send on bound-device online for device-targeted hetero

Extend the pre-send device guard from remote-only agents (openclaw / hermes) to
any hetero agent whose run dispatches to a device — i.e. claude-code / codex with
executionTarget='device'. If the bound device is offline (or none is bound), the
send button is disabled and a guard alert is shown, instead of letting the run
fail at dispatch time.

- new selector currentAgentExecutionTarget
- isDeviceExecution = remote-typed OR executionTarget==='device'; drives the
  guard's enabled flag, the blocked state, and the alert.
- device execution no longer requires cloud credentials (it doesn't use the
  cloud sandbox), so the cloud-not-configured gate now exempts it.

The guard hook already handled non-remote types (online check only, no platform
capability probe), so no hook change is needed.

Part of LOBE-9579 (Step 3, device online guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(tool-render): flatten nested-background tool renders into single-layer surfaces

Remove the card-in-card look across builtin tool renders by dropping the outer
colorFillQuaternary container fill (the framework tool card already provides the
surface) and keeping at most one delineated inner box.

- claude-code AskUserQuestion: rebuilt as a flat Question / divider / Selected
  layout; add i18n keys (question/selected/reply/noAnswer)
- claude-code Skill, local-system WriteFile: flat container + single previewBox
- agent-management CreateAgent/GetAgentDetail: flat container, keep outlined
  systemRole block
- web-onboarding SaveUserQuestion: drop the redundant inner value box

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs(builtin-tool): document single-layer surface rule for tool renders

Add §0.8 "stay single-layer — don't nest filled cards": the framework tool
card is already the surface, so the Render's outer wrapper carries no fill and
at most one filled box delineates real content. Cross-link from §2 Render rules
and the diagnostic table, and note the deliberate outlined-panel exception
(TodoWrite / Task).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs(builtin-tool): consolidate fragmented UI shared-style rules

The §0 shared rules had drifted into 8 one-line subsections (0.1–0.8). Fold the
five mechanical "every file looks like this" rules ('use client', memo +
displayName, BuiltinXProps generics, t('plugin'), store reads) into a single
annotated component skeleton (0.1), merge the two styling rules into 0.2, and
keep the single-layer surface rule as 0.3. Update the §0.8 cross-references in
§2 and the diagnostic table to §0.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs(builtin-tool): split UI reference into a per-topic ui/ folder

The single 770-line ui.md had grown unwieldy. Break it into references/ui/
with a README index and one file per topic: principles, shared-rules, the six
surfaces (inspector/render/placeholder/streaming/intervention/portal),
composition, and diagnostics. Convert in-doc §-number cross-refs to cross-file
links and repoint SKILL.md + tool-design.md at the new folder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(device): device-scoped cwd picker for executionTarget=device

When a hetero run is bound to a remote device, the device's filesystem isn't
browsable from here, so the local folder picker doesn't apply. Add
DeviceWorkingDirectory — a self-contained bar item (chip + popover) sourced from
the bound device's recentCwds plus a manual path input.

- Picking/typing a cwd pins it to the active topic (override) and persists it to
  the device via useUpdateDeviceCwd (optimistic defaultCwd + recentCwds), which
  is exactly what the server's device-dispatch branch reads back.
- Same per-cwd CC-session-reset confirm as the local picker.
- WorkingDirectoryBar routes to it when executionTarget==='device' (both web —
  replacing CloudRepoSwitcher — and desktop, replacing the local picker +
  GitStatus); local/sandbox paths are unchanged.
- Reuses existing i18n keys (recent / noRecent / placeholder).

Completes LOBE-9579 Step 3c. type-check clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(tool-render): flatten ToolResultCard + de-duplicate Read header

ToolResultCard was the card-in-card shared component (colorFillQuaternary
wrapper around a colorBgContainer box) behind CC Read/Grep/Glob/Write/WebSearch/
WebFetch. Flatten it to single-layer (flat wrapper, one colorFillTertiary
content box) so all consumers stop stacking fills inside the framework tool card.

CC Read header showed the filename strong-label and then dumped the full
absolute path whose tail repeated the same basename, end-truncated so the
meaningful suffix was hidden. Show the directory only (filename stays the
strong label), and drop the conflicting word-break so the dir ellipsizes on one
line.

Note ToolResultCard in the skill as the canonical single-layer header+content
card to reuse.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(device): mark current device, native cwd browse, fix edit Save button

Settings → Devices page polish:
- Badge the row for the machine you're on ("This device"), resolved from the
  desktop gateway's own deviceId (web has no current device → no badge).
- For the current device, the edit modal's Default working directory gains a
  native folder picker (electronSystemService.selectFolder) next to the manual
  input — you can't browse a remote device's filesystem, only your own.
- Edit modal footer now uses real Button components (Cancel + primary Save)
  instead of the base-ui Modal's default okText, which rendered with the wrong
  (non-primary) color.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(device): neutral current-device tag + per-channel tags

- "This device" badge uses the default neutral tag instead of success green.
- Show each live connection's channel as a small tag (desktop / cli) so a
  multi-channel device's connections are individually legible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(devtools): add API jump-list column to the render gallery

The render gallery stacked all of a toolset's API previews in one scroll column
(67 for Claude Code), making any specific render slow to find. Add a middle
column listing the toolset's apiNames: clicking scrolls the matching preview
card into view (landing below the sticky lifecycle bar via scroll-margin), and
an rAF-throttled scrollspy highlights the API the reader is on and keeps that
item visible in the list. A leading dot marks APIs that ship a Render. The
content area now owns its own scroll so the list stays pinned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(devtools): make the API jump-list readable + deep-linkable

The jump-list was a wall of identical `mcp__claude_ai_Linear__…` truncations and
the active item barely differed from hover. Show just the trailing action for
mcp__ tools (full id in a title tooltip + the preview card header), render names
in monospace, and give the active item a primary left-accent so it reads as
selected. Clicking now pins a `#api-<name>` hash (deep-linkable / shareable) and
loading a hashed URL jumps straight to that card below the sticky bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(devtools): add an Aggregate message-flow preview tab

The gallery only previewed each API in isolation. Add a View tab (By API /
Aggregate): Aggregate stitches every render-bearing API into one compact
content + tool message flow, so renders can be judged in conversational context
across any lifecycle mode. Inspector-only MCP tools are dropped to keep the
thread about the renders, and the API jump-list column hides in this view.

Extract the Inspector/Body surface rendering out of ToolPreview into shared
ToolInspectorSlot / ToolBodySlot (toolSurfaces.tsx) so both tabs derive props
identically and never drift. View choice persists to localStorage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(devtools): densify API list + keep mcp prefix visible

The earlier "shorten mcp names" change solved the wrong problem and hid the
`mcp__` prefix, so MCP tools no longer read as MCP. The actual complaint was row
height. Restore the full identifier and instead middle-elide it
(`mcp__claude_ai_Li…get_diff`) so both the muted `mcp` namespace and the
distinguishing trailing action stay visible; full id remains in the title
tooltip. Drop row height to a fixed dense 22px (flex-shrink:0 so it scrolls
instead of squishing) to fit far more APIs per screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(devtools): render Aggregate tab through the real Conversation renderer

The hand-rolled MessageList only approximated the chat. Replace it with the
actual shipping renderer: seed a `ConversationProvider` (skipFetch) with fixture
`assistantGroup` messages and map each render-bearing API to a real tool
payload, then render the real `MessageItem` for each. Tool state is driven
purely by the message shape — `result` → success, `result.error` → error,
`intervention.pending` → intervention, unterminated `arguments` JSON →
streaming — so the preview is byte-for-byte what users see in chat. Skips the
virtualized `ChatList` (and its data fetches) by mapping `MessageItem` directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(device): device detail drawer (channels + recent dirs + config)

Clicking a device row now opens a right-side detail drawer instead of a small
edit modal:
- Connections: render every live connection from the `channels` array, each
  with its channel tag (desktop / cli) + connected-since.
- Name + default working directory (native folder browse on the current
  device); saving a default cwd also seeds the recent list.
- Recent directories: list `recentCwds`, click to reuse, × to remove — this is
  where you can see and manage the recent list (previously not surfaced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(device): record recentCwds on the local device picker

Local-mode runs execute on this machine, but the local working-directory picker
only persisted to a desktop-local recents store — the dir never reached the
device registry, so the settings detail view (and a future device-mode picker)
couldn't see it.

- WorkingDirectory.selectDir now also records the chosen dir into the current
  device's recentCwds (resolved from the gateway's own deviceId).
- useUpdateDeviceCwd gains a { setDefault } option so local mode records
  recentCwds without repointing the device's defaultCwd.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🩹 fix(devtools): thread Aggregate preview messages via parentId

Each fixture turn was an orphaned message with no parentId, so the renderer saw
a pile of disconnected messages rather than one conversation. Chain every turn
onto the previous one (`parentId` = prior message id) so they read as a single
linear thread.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(devtools): seed flat messages so conversation-flow groups the Aggregate

The previous version hand-built `role: 'assistantGroup'` messages, bypassing the
real grouping. Seed the flat DB-shaped messages instead — an `assistant` message
carrying the tool_use plus a linked `role: 'tool'` result message per API — and
let conversation-flow's `parse()` synthesize the assistantGroup exactly as it
does in chat. The consecutive tool turns now collapse into one real workflow
group (one avatar, N content+tool blocks) instead of N hand-rolled groups.
Lifecycle state rides the tool message the same way production carries it
(content/pluginState = success, pluginError = error, pluginIntervention = pending).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 refactor(device): inline master-detail device settings; drop uppercase labels

Per feedback:
- Replace the floating edit Drawer with an inline right-hand detail panel —
  the devices page is now a master-detail layout (device list on the left,
  selected device's detail on the right), like the rest of settings.
- Drop the ALL-CAPS section labels (no more text-transform: uppercase /
  letter-spacing) — labels use natural case + a muted color.

DeviceItem becomes a selectable list row (no own modal); DeviceDetailPanel
renders the detail inline (connections per channel, name, default cwd + browse,
recent dirs). Keyed on deviceId so the form resets on selection change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 refactor(device): detail panel opens on click, not by default

Per feedback — mirror the memory-preferences master-detail pattern:
- No device is selected by default; the right detail panel only renders once a
  row is clicked (clicking the selected row again closes it). Panel has its own
  close (×).
- List flexes to fill when nothing is selected; the detail appears as a right
  column on selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(devtools): bind render gallery to viewport height so columns scroll

The page root used height:100%, which only resolves when an ancestor route
provides a bounded height — under mounts that don't, the whole page grew to
content height and the API list never scrolled internally. Bind the root to
100dvh directly and add min-height:0 to the flex chain (main + the API list)
so the scroll container engages regardless of how the route is mounted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  feat(devtools): add WebFetch / WebSearch fixtures so they render

Both APIs had no fixture, so the gallery fell back to schema-sampled args with no
content and the renders drew empty (just the icon). Add fixtures with realistic
args + content: WebFetch (url + prompt + markdown answer), WebSearch (query +
allowed_domains + results), plus their apiList descriptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(device): render connections straight from device.channels[]

Drop the device.online-based synthetic single-channel fallback — the connection
rows now come purely from the device.channels[] array (one row per live
connection), with offline = empty array.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(hetero): distinguish CC server throttle from user quota limit

A 429 "Server is temporarily limiting requests (not your usage limit)"
was classified as a user rate_limit, rendering the misleading "Claude
Code usage limit reached" reset-time guide. Key the rate_limit vs
overloaded decision on the structured rate_limit_event reset window
(resetsAt / rateLimitType) instead of the HTTP status, so 429/529 with
no quota signal fall through to the overloaded (retry) UX.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(devtools): loosen the API list density

22px rows at 12px overcorrected into a cramped sidebar. Relax to 30px rows,
13px label, a small inter-row gap, and a touch more vertical padding so the
jump-list reads comfortably.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(device): align connection rows in the list item (drop 30px indent)

The connection rows had a 30px inline-start padding that pushed them right of
the cwd line; align them with the rest of the device info.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(device): move connection status dot to the first line

The online/offline status now sits as a dot next to the device name + badges
(with the connected / last-active time as a tooltip), instead of a separate
third line. Per-channel connection detail still lives in the detail panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 feat(devtools): show the Aggregate preview as "Lobe AI"

The seeded preview conversation resolved its avatar/name through an agentId that
wasn't in the agent store, so every turn fell back to the unresolved-agent
"Unnamed Assistant" / UN avatar. Seed agentMap with a Lobe AI meta
(DEFAULT_INBOX_AVATAR + title) for the devtools agentId, shared via
DEVTOOLS_AGENT_ID / DEVTOOLS_AGENT_META so MessageList's context and the store
seed stay in sync. Restored on unmount.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(devtools): carry tool result state in BuiltinInspectorProps

The Aggregate preview passes `result.state` to inspectors, matching the
real runtime, but the canonical `result` type omitted `state` — failing
type-check. Add `state?: any` so devtools and runtime agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 fix(device): pin topic cwd and add hetero-tracing toggle

- Prefer the topic's own `metadata.workingDirectory` over the device
  default when dispatching, so an existing topic keeps its pinned cwd
- Add `heteroTracingEnabled` store flag to trace CLI raw streams in
  packaged builds (Help menu checkbox)
- Reorder the connection status dot ahead of badges in DeviceItem

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

*  feat(device): add Help-menu toggle to record hetero-agent CLI traces in production

Packaged builds previously never wrote hetero-agent (CC / Codex) CLI traces,
so production issues couldn't be captured. Add a persisted `heteroTracingEnabled`
toggle in the Help menu (all 3 platforms) plus an "Open HeteroAgent Directory"
entry. Dev still always traces to `cwd/.heerogeneous-tracing`; packaged builds,
when enabled, centralize traces under `<appStoragePath>/heteroAgent/tracing`
(sibling to the existing files cache) via shared dir constants.

Closes LOBE-9828

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 📝 docs(skills): fold stacked-prs guidance into the pr skill

Merge the standalone `stacked-prs` skill into `pr` as a supplementary section
(ordering rule, file placement, git split recipe, dependency verification,
Linear bookkeeping, gotchas) and absorb its triggers into the pr description,
rather than keeping a separate skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(devtools): chain RenderGallery previews into one assistantGroup

Unfinished tool states (streaming / loading) now emit a paired tool result
message with `LOADING_FLAT` content instead of none, and every assistant turn
chains onto the previous message's id. The tool_use → tool_result link is what
lets conversation-flow merge the turns into one assistantGroup; without it the
unfinished modes rendered as one orphaned group per tool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(device): key hetero trace location off the toggle, not isPackaged

`resolveTraceRootDir` now centralizes traces under
`<appStoragePath>/heteroAgent/tracing` whenever `heteroTracingEnabled` is on,
instead of gating on `isPackaged`. Packaged behavior is unchanged (it only
traces when the toggle is on), and a dev who opts in now also gets the
centralized dir reachable from the Help-menu entry. Plain dev runs keep
writing to `cwd/.heerogeneous-tracing`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🐛 fix(device): move hetero dir consts to a side-effect-free module

Importing the new `HETERO_AGENT_*` constants from `@/const/dir` dragged that
module's load-time `app.getPath()` / `app.getAppPath()` calls into the menu and
controller import graphs, breaking menu/controller suites whose electron mocks
or partial `@/const/dir` mocks didn't anticipate it. Relocate the pure path
segments to `@/const/heteroAgent` (no electron import) and point the controller
+ all three menu impls there. Also add the now-required `storeManager.get/set`
to the menu test app mocks (the Help-menu tracing checkbox reads it at build).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(devtools): refine RenderGallery surfaces and fix local-system fixtures

- flatten the active ApiList item (drop accent bar) and the ToolPreview card shadow
- give the Aggregate thread a white container surface
- hide deprecated lobe-notebook toolset and legacy *Local* aliases from the gallery
- re-key local-system fixtures to current API names + add missing call args
- backfill agent-management call args so inspectors render their argument rows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

*  test(desktop): default global electron mock so import-time app access is safe

`@/const/dir` reads `app.getAppPath()` / `app.getPath()` at module load — fine
in production (app is ready), but it forced every test that transitively imports
it to stub those basics, which is the real root of the recent breakages.

Register a default `electron` mock in the global vitest setup, giving every
suite a ready `app` (paths + readiness) plus light stubs for the common
namespaces. Suites that need specific behavior still declare their own
`vi.mock('electron', …)`, which overrides this per-file. This keeps production
free to use plain value-style path constants instead of lazy getter functions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:11:55 +08:00
Arvin Xu 3caa3efb18 feat(device): auto-register desktop & CLI devices with stable machine ID (#15300)
 feat(device): auto-register desktop & CLI devices; send connectionId + channel

App layer — wires desktop and `lh connect` to the device registry and the
connection-routing scheme. Depends on @lobechat/device-identity and the
gateway-client connectionId/channel options (earlier PRs in this stack), plus
the device.register / listDevices endpoints (already on canary).

- desktop derives the stable deviceId on gateway connect (old per-install random
  UUID demoted to the routing `connectionId`), registers via device.register,
  and tags channel `desktop` / `desktop-dev`
- `lh connect` derives + registers before opening the WS (explicit --device-id
  still pins a VM); channel `cli` (env-overridable); connectionId persisted in
  `~/.lobehub/connection-id`
- CLI api client preserves explicit --token connects during registration

Part of LOBE-9572. Closes LOBE-9576 / LOBE-9577.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:35:09 +08:00
Arvin Xu c27b62e10c 💄 style(imessage): wrap BlueBubbles bridge config into a connection card (#15342)
* 💄 style(imessage): wrap BlueBubbles bridge config into a connection card

Regroup the iMessage BlueBubbles bridge settings into a single bordered
card with a clearer top status / middle form / bottom action layout:

- Header shows the connection title + overall test status badge
  (Pending Test / Connected / Failed), with breathing room before the
  form fields.
- Server URL field gains an inline hint box (127.0.0.1 vs LAN IP).
- A full-width bridge service bar at the bottom: running/stopped status
  with the listening address on the left, the primary Enable Bridge
  toggle on the right, and the less-frequent Refresh / Test actions on a
  second row.

Test status is tracked locally and reset on any field edit so the badge
never shows a stale pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(claude-code): fix WebFetch inspector URL truncation and align chip with Bash

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(imessage): use BlueBubbles logo for the bridge status icon

Swap the generic plug glyph for the BlueBubbles app logo so the bridge
service card reads more recognizably. The icon sits in a white rounded
tile; the running state is already conveyed by the Running tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(imessage): move BlueBubbles logo to the connection header

Promote the BlueBubbles logo next to the section title so it identifies
the integration up front, and drop the icon tile from the bridge service
row — the running/stopped state reads fine as text + status tag there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 style(imessage): enlarge bridge logo, fix disabled status, clarify relay copy

- Logo now spans both header lines (44px) for a stronger section anchor.
- Bridge status reflects this config's Enable toggle (running && enabled),
  so flipping it off no longer keeps showing "Running" until the next save.
- Service descriptions now explain the bridge relays iMessage messages to
  LobeHub, so the local server's purpose is clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ♻️ refactor(imessage): make Electron main the SoT for the bridge status

Read the bridge status via SWR (revalidates on focus + after each mutation)
instead of caching a divergent copy, and drop the manual Refresh button.

- `enabled` / `running` / `serverUrl` / `passwordSet` now derive from the
  main-process status, not local form state.
- Enable is a write-through toggle: it auto-persists the current Server URL +
  password and starts/stops the bridge immediately (option B), surfacing real
  connection errors on enable.
- Test is ungated from enable — it pings BlueBubbles directly and only needs a
  Server URL + password.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 20:31:40 +08:00
Arvin Xu a9d74bb143 chore: remove LOBE-XXX auto-link pattern from WelcomeText (#15336)
Remove the LOBE-\d+ regex from AUTO_LINK_PATTERNS since LOBE issue references
should not appear in an open-source codebase. Only GitHub issue references (#\d+)
remain auto-linked.

Co-authored-by: arvinxx <arvinxx@lobehub.com>
2026-05-30 19:26:33 +08:00
Arvin Xu e1fe37933d feat(cli): add lh topic view command (#15340)
* feat(cli): add `lh topic view` command to display topic details and messages

* test(cli): add unit tests for `lh topic view` command

* fix(cli): improve topic view - fix --no-messages bug, add tool calls, threads, pagination

* test(cli): update view tests - fix mock, add tool/thread/pagination cases

* feat(topic): add getTopicDetail trpc procedure for structured topic metadata

* refactor(cli): use getTopicDetail for view command metadata, show full fields

* test(cli): update view tests to use getTopicDetail mock
2026-05-30 19:13:34 +08:00
Innei 1c3e973bab 🐛 fix(library): refresh folder data on slug switch and dedupe breadcrumb fetch (#15335)
Resource Explorer kept showing the previous folder's items when sidebar
hierarchy clicks switched the URL slug. SWR `onSuccess` only fires after
revalidate completes, so cache-hit navigations could not update the
zustand mirror that the Explorer reads from.

- Move SWR data → store sync into a `useEffect` so cache hits also push
  fresh items into `useFileStore` immediately, while keeping the 30s
  deduping window to avoid wasted background revalidations.
- Reuse the Breadcrumb SWR cache in `LibraryHierarchy`: replace
  `tree.navigateTo(slug)` (which fetched the breadcrumb directly) with
  `tree.expandAncestors(ids)`, and let `useFetchFolderBreadcrumb` feed
  the ids so a folder switch no longer issues two parallel
  `document.getFolderBreadcrumb` requests.

Fixes LOBE-4293
2026-05-30 17:27:07 +08:00
Innei 22c264bb77 feat(page-share): add document share flow with business slot stubs (#15309)
*  feat(page-share): add document share flow

*  improve page share probe fallback

* ♻️ refactor(page-share): extract to business slot stubs

* ♻️ refactor(page-share): move shared-page viewer to /share/page/:id

- Drop anonymous handling on /page/🆔 revert middleware allowlist, main
  layout PageShareLayout wrap, and outlet-context probe branch
- Add /share/page/:id route under share tree (parallel to /share/t/:id),
  registered in desktop/desktop-vite/mobile router configs
- New PublishedShell business slot stub (pass-through); cloud provides the
  marketing banner + chrome
- Align SharePopover i18n schema with the topic-share pattern

* 🐛 fix(page-share): provide pageShare router stub procedures for OSS type-check

The /share/page/:id route calls lambdaClient.pageShare.getSharedDocument;
the empty router({}) stub left the OSS standalone type-check unable to
resolve it. Stub now declares all three procedures (getShareSettings,
updateShareSettings, getSharedDocument) with cloud-matching inputs and
throws NOT_FOUND when invoked without the cloud override.
2026-05-29 22:36:51 +08:00
Innei 1736faf3af 📝 docs(spa-routes): document .desktop.{ts,tsx} variant pattern (#15327)
Extend the spa-routes skill so agents catch all `.desktop` colocated
variants under `src/routes/`, not just the desktopRouter pair. Adds a
new "3b. Other .desktop variants" section listing the current known
cases (settings componentMap, agent index, group index), spells out
the drift risk for each, and lists the rules for editing/adding/
removing variant pairs. Also updates the skill description so the
trigger glob covers `componentMap.desktop`, `index.desktop.tsx`, and
`.desktop.tsx variant`.
2026-05-29 17:50:41 +08:00
Innei 6c58af9c84 🐛 fix(desktop): upload .blockmap files to S3 for differential updates (#15326)
The S3 publish action was missing *.blockmap from its upload glob,
causing electron-updater to always fall back to full downloads.
2026-05-29 17:23:19 +08:00
Innei 0139c054a2 ⬆️ chore: update @lobehub/ui to v5.15.5 (#15325)
Bump @lobehub/ui from the pkg.pr.new preview to the released v5.15.5,
and switch the community user list search inputs from antd Input.Search
to @lobehub/ui SearchBar to align height with the status Select.
2026-05-29 17:10:54 +08:00
Arvin Xu 063fa61c49 feat(device): connectionId + channel routing in gateway client & device list (#15322)
*  feat(device): connectionId + channel routing in gateway client & device list

Shared client + server + settings-UI half of decoupling the gateway connection
routing key from the stable deviceId (the gateway DO change lives in the
device-gateway repo).

- GatewayClient gains `connectionId` (per-install routing UUID) + `channel`
  (freeform label) options, both sent on the WS URL; `currentConnectionId` getter
- consume the gateway's device-centric `/api/device/devices` shape: deviceProxy
  maps it to runtime devices + nested channels (tolerant of a legacy flat shape
  via `?? []`); device.listDevices flattens channels; DeviceItem shows the label

Part of LOBE-9572. Closes LOBE-9781.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 🔥 chore(device): remove unused in-repo apps/device-gateway

`apps/device-gateway` was a stale, non-deployed mirror of the device-gateway
Cloudflare worker (the real one lives in its own repo and already diverged —
it has AdminDO / geo / message-api / the tool-call-timeout refactor this copy
never got, and no CI here deploys this directory). Keeping it around just makes
the in-repo gateway look like it ignores the connectionId/channel this client
now sends. Drop it; the gateway contract is owned by the service repo.

- delete apps/device-gateway/**
- drop its tsconfig `exclude` entry
- retarget the protocol-mirror comment in device-gateway-client to the service

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 16:03:40 +08:00
YuTengjing dc3186a990 🐛 fix: preserve empty agent prompt export (#15316) 2026-05-29 14:06:47 +08:00
YuTengjing 50d7b126c8 🐛 fix: stop transliterating model names in home starter (#15324) 2026-05-29 14:00:01 +08:00
Innei 42487663b9 🐛 fix(scripts): kill dev child processes on parent shutdown (#15246)
Detach next/vite children into their own process group so process.kill(-pid)
reaps the whole tree (npm -> vite, etc.). Forward SIGHUP, escalate to SIGKILL
after a timeout, and add uncaughtException / 'exit' fallbacks to avoid
leaving orphan processes when the dev startup script is killed.
2026-05-29 13:55:14 +08:00
Arvin Xu 94c7fa4d76 chore(device): add @lobechat/device-identity (#15321)
 feat(device): add @lobechat/device-identity (stable machine-derived deviceId)

New shared package: `deriveDeviceId` hashes the OS machine id with the userId
(+ salt) so one machine + one user → one stable, user-scoped deviceId that
survives LobeHub reinstalls. Falls back to a caller-supplied random UUID (flagged
via `identitySource: 'fallback'`) when the machine id is unavailable.

Foundational layer — no consumers yet; desktop/CLI wire it up in a later PR.

Part of LOBE-9572. Closes LOBE-9574.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 13:28:10 +08:00
lobehubbot 049c81d53b 🔖 chore(release): release version v2.2.1 [skip ci] 2026-05-29 01:54:38 +00:00
LiJian 2461709de4 🐛 fix(desktop): market OAuth expiry triggers wrong re-login modal (#15290)
🐛 fix(desktop): market OAuth expiry no longer triggers LobeHub re-login modal

When sandbox tools (Document Writing, Agent Browser) encountered a
Market OAuth token expiry on desktop, the server threw UNAUTHORIZED
which caused responseMeta to set X-Auth-Required: true, triggering the
LobeHub cloud re-login modal instead of the Market OAuth dialog.

- Add MARKET_AUTH_REQUIRED_MESSAGE sentinel to desktop-bridge
- market.ts uses this message for Market auth TRPCErrors
- responseMeta skips X-Auth-Required for Market auth errors
- MarketAuthProvider on desktop now calls handleUnauthorized() when
  silent token refresh fails, correctly opening the Market OAuth flow

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 09:54:37 +08:00
lobehubbot 5609b6313b Merge remote-tracking branch 'origin/main' into canary 2026-05-29 01:52:45 +00:00
Arvin Xu 53e13ea3b1 🚀 release: 20260528 (#15302)
# 🚀 LobeHub Release (20260528)

**Release Date:** May 28, 2026  
**Since v2.2.0:** 220 merged PRs · 15 contributors

> This cycle brings heterogeneous "platform agents" you can dispatch to
local or remote devices, a rebuilt onboarding flow, document-centric
chat, and a unified model-runtime error model — with new DeepSeek V4 and
Gemini 3.5 Flash support along the way.

---

##  Highlights

- **More Hetero Agents (OpenClaw / Hermes)** — Create heterogeneous
agents and dispatch them to local or remote devices through the device
gateway, with an execution-target switcher in the composer and
persistent CLI sessions. (#15065, #15179, #15022)
- **iMessage on Desktop** — New iMessage setup and bridge on desktop,
plus bot attachments across every platform. (#15228, #15227, #15029)
- **Skills in the Composer** — Drag skill chips into chat, trigger
installed skills from the slash menu mid-line, and surface project-level
skills in the homogeneous agent runtime. (#15095, #15061, #15110)
- **New Models** — DeepSeek V4 Flash/Pro and Gemini 3.5 Flash across
providers, with thinking params for structured output and chat cost
estimates. (#15031, #15001, #15051, #14876)
- **Agent Runtime Observability** — OpenTelemetry GenAI semantic
conventions plus per-call generation tracing. (#15123, #15124)

---

## 🤖 Agents & Heterogeneous Runtime

- **Platform agent creation** — OpenClaw/Hermes creation UI, device
guard, and remote dispatch backend. (#15065)
- **Execution-target switcher** — Pick local vs remote execution
directly in the composer; device-selection UX with actionable guidance.
(#15179, #15111)
- **CLI hetero dispatch** — OpenClaw/Hermes dispatch with persistent
sessions and a notify protocol. (#15022)
- **Gateway snapshot as source of truth** — Consume the gateway
`uiMessages` snapshot at step boundaries to keep chat state consistent.
(#15153, #15152)
- **Client sub-agent as a normal tool call** — Simplifies the sub-agent
execution path. (#15281)
- **Hermes agent chain** — Implements the Hermes agent chain logic.
(#15189)
- **Device registry** — TRPC endpoints to register, list, update, and
remove devices. (#15299)
- **Desktop device routing** — Route gateway agent runs through `lh
hetero exec`; restore `userId` in gateway dispatch and gate local-system
by execution target. (#15132, #15232)
- **Agent signals** — Anchor agent-signal receipts to messages and
isolate memory-agent messages into a child thread. (#14969, #14921)

---

## 🚀 Onboarding

- **Simplified first screen** — Defer topic creation to first send.
(#15090)
- **Market Agent Picker** — Added as a classic onboarding step, with
template prefetch. (#14980, #15041)
- **Welcome guidance** — Show agent welcome guidance on first run.
(#15098)
- **Mobile** — Adapt agent onboarding UI and restore Classic-step
padding on mobile. (#15019, #15032)
- **Discovery** — Streamline discovery to a single profession question.
(#14987)
- **Analytics** — Track onboarding step events and create-agent modal
source. (#15133, #15028)

---

## 📄 Documents, Pages & Knowledge

- **Thread chat in preview** — Embed thread chat in the document preview
portal. (#15216)
- **Non-markdown rendering** — Render non-markdown docs as a read-only
highlight. (#15272)
- **Multi-select** — Multi-select delete in the document tree. (#15125)
- **Page-agent streaming** — Preview `initPage` streaming arguments.
(#15039)
- **Per-agent topics** — Per-agent topic management page. (#15207)
- **Server-side category** — Derive document category server-side and
drop frontend predicates. (#15076)

---

## 🧩 Skills & Tools

- **Drag skill chips** — Drag skills into chat input and register
agent-document skills. (#15095)
- **Slash menu** — Installed skills appear in the slash menu with a
mid-line trigger. (#15061)
- **Project skills** — Recognize project-level skills in the homogeneous
agent runtime and surface them regardless of active device. (#15110,
#15177)
- **VFS archiving** — Archive oversized tool results to VFS instead of
truncating. (#15074)
- **@localFile mentions** — Drag folders into chat input as `@localFile`
mentions on desktop. (#15071)

---

## 🧠 Model Runtime & Providers

- **Error spec registry** — Unify error codes into a spec + pattern
registry, split `ProviderBizError` into finer codes, classify Cloud-only
codes via a tier digit, and add `DatabasePersistError`. (#15262, #15286,
#15278, #15279)
- **New models** — DeepSeek V4 Flash/Pro (opencode-go) and Gemini 3.5
Flash; DeepSeek V4 Pro on SiliconCloud. (#15031, #15001, #15017, #15267)
- **Structured output** — Thinking params for structured output, Bedrock
structured generation, and DeepSeek `generateObject` tool choice.
(#15051, #15174, #15054)
- **Cost** — Chat cost estimate support; preserve usage cost in custom
streams. (#14876, #15218)

---

## 💬 Chat & User Experience

- **Follow-up chips** — Extend follow-up chip suggestions to general
chat with scene-specific model config. (#15101, #14797)
- **Input drafts** — Persist unsent input drafts across tab switches and
prevent repeated draft restore. (#14992, #15024)
- **Command menu** — Order topic/message search by recency and promote
inline type filters. (#15094, #14986)
- **Zoom HUD** — Show a zoom-level HUD on Cmd +/− and Cmd 0. (#15294)
- **Copy** — Unescape markdown escapes when copying user messages.
(#15253)

---

## 🖥️ Desktop

- **App Nap fix** — Prevent App Nap from dropping the gateway WebSocket
during display sleep. (#14994)
- **File preview** — Preview `.cjs`/`.mjs`/no-extension files instead of
binary fallback and expand `~` when opening local files. (#15168,
#15284)
- **Cross-platform settings** — Open settings via main-window navigation
on Windows/Linux and restore the route after an update restart. (#15036,
#14922)
- **Token refresh** — Prevent frequent logout from token-refresh
retries. (#14928)

---

## 📊 Observability

- **OTel GenAI** — Instrument Agent Runtime with OpenTelemetry GenAI
semantic conventions. (#15123)
- **Generation tracing** — Per-call `llm_generation_tracing` with a
pre-allocated tracingId and recordFeedback router. (#15124, #15146)
- **Error classification** — Persist `ERROR_CODE_SPECS` classification
on operation errors. (#15273)

---

## 🗃️ Database Migrations

- **Batch migrations** — Topic usage stats, push tokens,
`tasks.editor_data`, and document shares. (#15280)
- **Tracing & eval tables** — Add `llm_generation_tracing` and agent
eval experiment tables. (#15126)

> Self-hosted operators should run the database migration (`pnpm
db:migrate`, or restart with auto-migrate enabled) after upgrading. The
changes are additive and backwards-compatible.

---

## 🔒 Security & Reliability

- **Security:** Remove the `getPlaintextCred` tool to prevent plaintext
credential exposure. (#14998)
- **Security:** Prompt account selection for Google OAuth and add
`prompt=consent` to the OIDC authorization URL to fix missing refresh
tokens. (#15234, #15010)
- **Reliability:** Preserve streamed content across a mid-stream cancel.
(#15173)
- **Reliability:** Bound the Redis command timeout and configure the
Anthropic client timeout. (#15091, #15042)
- **Reliability:** Prevent infinite recursion in the assistant chain.
(#15288)

---

## 👥 Contributors

Huge thanks to **15 contributors** who shipped **220 merged PRs** this
cycle.

@AnotiaWang · @sxjeru · @algojogacor · @hardy-one · @arvinxx · @Innei ·
@tjx666 · @LiJian · @AmAzing129 · @Rdmclin2 · @Neko · @cy948 ·
@CanisMinor · @sudongyuer · @rivertwilight

Plus @lobehubbot and renovate[bot] for maintenance.

---

**Full Changelog**: v2.2.0...release/weekly-20260528
2026-05-29 09:51:56 +08:00
Arvin Xu 21aceb6fee feat(settings): add Devices settings page behind Execution Device Switcher lab (#15315)
Add a "Devices" tab under the General settings group (above Hotkeys) that
lists the user's registered devices. Each device is keyed by deviceId; the
gateway's live WS connections are nested as channel rows under their device
rather than shown as separate devices. The tab is gated behind the
`enableExecutionDeviceSwitcher` lab flag.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 03:02:48 +08:00
YuTengjing 2657b667be feat: export agent profiles as Markdown (#15312) 2026-05-29 02:45:25 +08:00
YuTengjing f042dd352e feat: support Claude Opus 4.8 (#15314) 2026-05-29 02:19:05 +08:00
Innei 15cb3be9cc 🐛 fix(conversation): keep open ActionBar popup when hovering another message (#15303)
* 🐛 fix(conversation): keep open ActionBar popup when hovering another message

When a dropdown inside the singleton message ActionBar is open, hovering
another message used to move the singleton host's DOM and swap the rendered
actionType, which unanchored or unmounted the open popup. Freeze both the
host placement target and the rendered actionType while any descendant has
`data-popup-open`, and re-commit the latest live values once the popup
closes (observed via MutationObserver).

* ♻️ refactor(conversation): freeze message ActionBar subtree while popup is open

Replace the manual committed-state freeze with `@lobehub/ui` `Freeze`:
split the host migration effect + portal render into `ActionBarBody`, and
wrap it with `<Freeze frozen={isPopupOpen}>` in `SingletonMessageActionsBar`.

While any descendant of the host has `data-popup-open`, the inner body is
suspended — its migration effect doesn't run and its render is paused, so
hovering another message no longer DOM-moves the trigger or unmounts the
dropdown's React subtree. Once the popup closes, the body resumes with the
latest live `actionType` / `portalElement` and migrates the host normally.

* Revert "♻️ refactor(conversation): freeze message ActionBar subtree while popup is open"

This reverts commit a8d47bedbb.
2026-05-29 01:31:41 +08:00
Innei 65113ca2a7 feat(kb): extend BM25 search to file-backed documents (#15247)
`searchKnowledgeBaseDocuments` only matched inline `custom/document`
pages, so parsed PDFs and other file-backed documents never surfaced
via the BM25 path — vector search was the sole way to retrieve them.

Run two scoped ParadeDB queries in parallel (inline via
`documents.knowledge_base_id`, file-backed via a `knowledge_base_files`
join) and merge by score in JS. A single OR-ed predicate trips
ParadeDB's `Unsupported query shape` because `paradedb.score()`
requires a conjunctive tantivy scan.

Folder rows are excluded; hits now carry an optional `fileId` so the
agent can read with either `docs_*` or `file_*` ids. The XML formatter
exposes the new attribute downstream.
2026-05-29 01:01:47 +08:00
YuTengjing 2194b23390 🐛 fix: show artifact source while streaming (#15310) 2026-05-29 00:52:13 +08:00
YuTengjing 234c87dd9d 🐛 fix: restore file access URL policy (#15305) 2026-05-28 22:51:32 +08:00
Innei 9945cecf87 feat(portal): editable CodeMirror viewer for LocalFile + Document highlight (#15298)
*  feat(portal): editable CodeMirror viewer for LocalFile + Document highlight

Replace the read-only Highlighter in the LocalFile portal preview and the
Document portal highlight mode with a shared `CodeEditorPane` powered by
`@lobehub/editor/codemirror`. Pane supports inline editing, Cmd/Ctrl+S to
save, lobeTheme tokens, and language-aware syntax highlighting.

LocalFile flow
- Track per-path edit buffers + save action in the chat portal store
  (`dirtyLocalFileContents`, `setLocalFileBuffer`, `saveLocalFile`).
- Show a filled dot on the tab close button when the file is dirty;
  hovering still reveals the X. Closing a dirty tab (via X or the context
  menu's "Close") prompts a confirmation modal via `confirmModal` from
  `@lobehub/ui/base-ui`.
- After save, mutate the SWR cache to the just-saved content before
  clearing the buffer so CodeMirror does not see a stale `value` prop and
  reset the cursor.

Document flow
- For non-markdown documents (`getDocumentRenderMode` → `highlight`),
  render `CodeEditorPane` with a local edit buffer keyed by `documentId`.
- Save calls `documentService.updateDocument({ saveSource: 'manual' })`,
  mutates the document-meta SWR cache, then clears the buffer.

Bump `@lobehub/editor` to ^4.15.0 to pick up the new
`@lobehub/editor/codemirror` subpath export.

* 🐛 fix(portal): force read-only on truncated local file previews

When a file exceeds MAX_PREVIEW_CHARS the preview only holds the first
500k character prefix. Editing and saving against that prefix would
silently overwrite the rest of the file with the truncated content.

Pass `readOnly={truncated}` to the editor, ignore any stale buffer when
truncated, and short-circuit handleSave so Cmd/Ctrl+S is a no-op in this
mode.

* ♻️ refactor(portal): drop MAX_PREVIEW_CHARS truncation for local files

Always pass the full file content to the editor instead of slicing at
500k characters. The truncation existed only to avoid losing data when
saving the previously-Highlighter-rendered prefix, but with full content
available the editor can both display and persist the file safely.

Removes the `truncated` / `truncatedLabel` plumbing, the truncated
banner, and the associated read-only short-circuit in handleSave.

*  test(portal): update document body highlight editor test
2026-05-28 22:42:25 +08:00
Arvin Xu 671b2527b8 feat(device): device registry TRPC (register / list / update / remove) (#15299)
Server-side foundation for the device registry. Builds on the `devices` table
(already on canary) so devices persist beyond the gateway's in-memory WS
sessions and stay visible/bindable while offline.

- new DeviceModel: register upserts on (userId, deviceId) and only refreshes
  machine-reported fields + lastSeenAt, so user-owned friendlyName / defaultCwd
  / recentCwds survive re-registration
- device.* router gains register / updateDevice / removeDevice (DB row only, no
  OIDC token revocation); listDevices is rewritten as a DB ∪ online union so
  offline devices stay listed and not-yet-registered online devices surface as
  transient entries
- HeteroDeviceSwitcher adapts to the richer listDevices shape (null-safe
  platform, prefers friendlyName)

Desktop / CLI auto-registration ships in a follow-up PR that depends on this.

Part of LOBE-9572. Closes LOBE-9575.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:51:35 +08:00
Arvin Xu 6d94635631 feat(bot): add iMessage Desktop setup and bridge (#15228)
 feat(bot): add iMessage Desktop bridge with Labs gate

Desktop-side BlueBubbles bridge for the iMessage channel:

- Bridge runtime (ImessageBridgeCtr/Srv) + gateway message_api_request routing;
  chat-adapter-imessage api lists all webhooks instead of the 500-prone url
  filter (first-time save no longer fails).
- iMessage channel UI: desktopDeviceId + webhookSecret are auto-filled/generated
  (not user fields); a single "Save Configuration" persists both the cloud
  provider and the local bridge via a post-save extension point — no separate
  "Save Bridge" button.
- Gated behind the `enableImessage` Labs preference (off → "Coming Soon").
- Group local-testing bot skills into per-channel folders + add iMessage
  bridge/outbound regression scripts.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:48:44 +08:00
Innei 109545c3b1 feat(desktop): show zoom level HUD on Cmd+/- and Cmd+0 (#15294)
*  feat(desktop): show zoom level HUD on Cmd+/- and Cmd+0

Replace Electron built-in zoomIn/zoomOut/resetZoom menu roles with custom
handlers backed by a new ZoomService, which clamps the zoom level to
[-3, +3] and broadcasts zoom:changed to the renderer. The renderer mounts
a macOS-style frosted HUD that fades in for 1.5s after each zoom change
so users can see the resulting percentage and confirm when they're back
to 100%.

* ⌨️ fix(desktop): preserve plus zoom shortcut
2026-05-28 21:24:56 +08:00
Arvin Xu 47daf09be1 Revert "🐛 fix: resolve file access urls via file service (#15295)"
This reverts commit 41172a6740.
2026-05-28 20:51:35 +08:00
YuTengjing 41172a6740 🐛 fix: resolve file access urls via file service (#15295) 2026-05-28 20:26:46 +08:00
Arvin Xu caa7905be2 🔨 feat(db): batch topic usage stats, push tokens, tasks editor_data & document shares migrations (#15280)
* 🔨 feat(db): batch topic usage stats, push tokens, tasks editor_data & document shares

Bundle four independent schema changes onto one migration branch:

- 0104 topics: add usage/cost aggregate columns (total_cost, token totals,
  cost/usage jsonb, model, provider) + model/provider indexes
- 0105 push_tokens: new table for Expo push notification tokens
- 0106 tasks: add editor_data jsonb column
- 0107 document_shares: new table for document share flow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔨 chore(db): combine batch schema changes into a single migration

Squash the four sequential migrations (0104-0107) into one 0104 SQL file
containing all DDL: topic usage/cost columns, push_tokens table,
tasks.editor_data column, and document_shares table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔨 chore(db): make push_tokens unique constraint device-only

Drop the userId prefix from the push_tokens unique index — one row per
device, reassigned to the new user on switch (upsert by deviceId).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(db): add user_connectors and user_connector_tools schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(db): add user_connectors and user_connector_tools schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(db): merge connectorTool schema into connector.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  revert(db): restore push_tokens unique constraint to (userId, deviceId)

This reverts commit addf14c2a6 (device-only unique index).

The device-only index conflicts with #15186's pushToken upsert, whose
onConflict target is (userId, deviceId). Restore the composite unique
index so the upsert lands consistently with both PRs.

Also re-point 0105 snapshot prevId to the restored 0104 id and carry the
(userId, deviceId) index forward so the migration chain stays consistent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(db): add devices table and consolidate batch migration into 0104

Add the `devices` identity anchor (surrogate uuid PK + unique(userId, deviceId))
as the stable, reinstall-proof base for binding agent runtime instances per
machine. Fold the prior 0104/0105 migrations and the new table into a single
idempotent 0104 migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  test(db): add topic usage/cost columns to topic.create assertions

The batch added 8 nullable topic columns (totalCost/usage/model/...) but
topic.create.test.ts still asserted the pre-batch 19-field shape via toEqual.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(db): use uuid primary key for document_shares

Align document_shares.id with the other new batch tables (uuid defaultRandom);
table has no consumers yet so no compat impact. Regenerated 0104 + snapshot.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: ONLY-yours <1349021570@qq.com>
2026-05-28 19:36:45 +08:00
Arvin Xu a7f38114d5 ♻️ refactor(bot): slim iMessage setup schema to user-edited fields (#15291)
♻️ refactor(bot): drop iMessage desktopDeviceId + webhookSecret from user schema

These are not user-supplied: the Desktop client fills the device id from the
local gateway and generates the webhook secret on first save. Removing them
from the platform schema keeps the iMessage setup form to the fields the user
actually edits.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 17:48:16 +08:00
Arvin Xu 1b74566b4c feat(model-runtime): split ProviderBizError into finer codes + reclassify catch-all at write time (#15286)
*  feat(model-runtime): split ProviderBizError into finer codes + reclassify catch-all at write time

Add UpstreamGatewayError (E8010), UpstreamMalformedResponse (E8011), and
UpstreamHttpError (E8012), migrating the matching patterns out of the
ProviderBizError catch-all. Add a refineErrorCode() step (message-pattern match
+ HTTP-status fallback) wired into formatErrorForState so generic ProviderBizError
is reclassified into the correct existing code (rate-limit / quota / network /
service-unavailable / model-not-found) instead of collapsing into one opaque
8xxx bucket. Production sampling showed ~72% of ProviderBizError actually belongs
to existing codes and only ~5% is a true residual.

*  feat(model-runtime): add isFallback flag to mark catch-all error buckets

Add an `isFallback` boolean to ErrorCodeSpec / ChatMessageError, set on the
catch-all codes (ProviderBizError, UpstreamHttpError, AgentRuntimeError,
DatabasePersistError). It flows onto agent_operations.error via the write-path
enrichment so monitoring can track how much volume still lands in fallback
buckets — the signal for where finer codes are still worth carving out.

*  test(model-runtime): add refineErrorCode to @lobechat/model-runtime mocks

formatErrorForState now imports refineErrorCode, so the partial module mocks in
AgentRuntimeService / RuntimeExecutors must expose it or vitest throws on access.

*  test(model-runtime): bump UpstreamGatewayError numericId to 8011 after canary 8010 collision

canary claimed 8010 for ProviderContentPolicyViolation, so the Upstream* codes
shifted to 8011/8012/8013 during rebase; update the refinement test assertion.
2026-05-28 17:02:39 +08:00
LiJian 1024ee961b 🐛 fix(cc-adapter): emit reasoning chunk before text in batch mode (#15289)
In the batch path (CLI / sandbox without --include-partial-messages),
the adapter extracted thinking and text from the complete assistant
block and emitted text first, reasoning second. This reversed order
caused `gatewayEventHandler` to call `startReasoningIfNeeded()` AFTER
text had already been dispatched, making the brain icon appear below
the rendered text content instead of preceding it.

Fix: swap the emission order so reasoning is always emitted before
text in both the main-agent and subagent batch paths, matching Claude's
natural output order (thinking → response) and the streaming delta path.

The desktop driver uses --include-partial-messages (partial deltas
arrive in correct order naturally), so it is unaffected.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 17:00:54 +08:00
Innei 980c2e74d8 🐛 fix(desktop): expand ~ when opening local files and folders (#15284)
shell.openPath() does not perform tilde expansion, so paths like
~/git/work failed silently. Run expandTilde() (shared with the rest
of LocalFileCtr) on the incoming path before handing it to the OS.
2026-05-28 16:05:48 +08:00
Innei 84598524df 🐛 fix(chat-input): keep input mounted while intervention panel is shown (#15283)
* 🐛 fix(chat-input): keep input mounted while intervention panel is shown

Conditional render swapped <DesktopChatInput> with <InterventionBar>,
unmounting the Lexical editor and wiping any unsent draft. Wrap the
input area in a display: contents | none container so the editor's
React subtree stays mounted and its in-memory document survives.

* 🐛 fix: hide expanded chat input during interventions
2026-05-28 16:05:39 +08:00
Arvin Xu 5e2ef88c13 🐛 fix(conversation-flow): prevent infinite recursion in assistant chain (#15288)
* 🐛 fix(conversation-flow): guard collectAssistantChain against cyclic chains

collectAssistantChain checked `processedIds` for loop protection but never
populated it, so when a topic contains duplicated tool_call_ids (the same
tool result reachable from multiple assistant messages) the assistant→tool→
assistant walk revisited already-seen assistants and recursed without bound,
crashing the conversation view with "Maximum call stack size exceeded".
Mark each assistant visited up front.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  test(conversation-flow): cover collectAssistantChain cycle guard

Regression test for the duplicate-tool_call_id cycle that previously
overflowed the stack: two assistant turns declaring the same tool_call_id
make one turn's tool result resolvable from the other, so the
assistant→tool→assistant walk revisits an already-collected assistant.
Asserts the walk terminates and collects each assistant once, plus a
control case for a normal acyclic chain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(conversation-flow): skip already-visited followers in collectAssistantChain

The cycle guard stopped the infinite recursion but, with a duplicated
tool_call_id, collectToolMessages can surface an earlier turn's tool result
before the current assistant's own. Its child is an already-visited assistant,
so the recursive call is a no-op — yet the unconditional return after it made
the walk stop there and silently drop the current turn's real continuation
under a later tool. Skip already-processed followers so the loop advances to
the current assistant's own tool result.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:53:05 +08:00
YuTengjing 403de538d6 🐛 fix: improve Google image error handling (#15285) 2026-05-28 15:40:15 +08:00
Arvin Xu 8949e89535 ♻️ refactor(agent): run client sub-agent as a normal tool call (#15281)
* ♻️ refactor(agent): run client sub-agent as a normal tool call

Make lobe-agent callSubAgent/callSubAgents execute the sub-agent in an
isolated thread via the current client runtime (executeClientAgent +
threadId + isSubAgent) and return a normal tool result, instead of the
stop:true + exec_sub_agent instruction + polling detour. UI now mirrors
the Claude Code Agent tool: a collapsed tool row that opens the sub-agent
thread in the portal. No more role='task' messages on the lobe-agent path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(agent): refine sub-agent tool UI and unify subagent thread display

- Inspector mirrors the Claude Code Agent tool: leading bot icon, "Call SubAgent" / "Call SubAgents" label, description as a chip, and a compact run-stats tail (model · tools · tokens)
- callSubAgents collapses to the first description + "等 X 个" beyond 2, with per-row stats
- rename the open-thread action to "View Detail"
- unify subagent-thread detection on ThreadType.Isolation so lobe-agent sub-agent threads indent in the sidebar and render read-only like CC subagents
- fix: refresh threads right after creating the client sub-agent thread so the "View Detail" button and sidebar entry appear immediately instead of only after a topic switch

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(agent): unify sub-agent workflow group label to "Call SubAgent"

Align the collapsed workflow group summary (workflow.toolDisplayName) with the
inspector copy so callSubAgent / callSubAgents read "Call SubAgent" / "Call
SubAgents" instead of "Dispatched a sub-agent".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 15:14:08 +08:00
Arvin Xu 8aa075cd80 feat(model-runtime): add DatabasePersistError code for failed DB queries (#15279)
*  feat(model-runtime): add DatabasePersistError code for failed DB queries

Drizzle stringifies a failed query/transaction as `Failed query: <sql>
params: <values>`. These are harness-side persistence failures, but they
were landing in the operation dashboards as `unknown` — and worse, the
embedded SQL/parameter text (model names, error_log rows, user messages)
contains substrings that trip unrelated provider patterns, so naive
message-matching misclassified them as CapabilityNotSupported /
InsufficientQuota / ModelNotFound.

- `agentRuntime.ts` — new `DatabasePersistError` code.
- `specs.ts` — E7004 under the 7xxx Stream/Runtime (harness) bucket,
  `attribution: harness`, `countAsFailure: true`, httpStatus 500.
- `patterns.ts` — `Failed query:` substring pattern placed **first** in the
  registry. matchErrorPattern is first-match-wins, so claiming it up front
  both classifies these correctly and stops the embedded blob from matching
  anything below.
- `match.test.ts` — assert the wrap classifies as DatabasePersistError and
  that a blob embedding `InsufficientQuota` / `context length exceeded` still
  resolves to DatabasePersistError.
- `modelRuntime.ts` — en-US `DatabasePersistError` copy (others auto-translate).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(model-runtime): add StateStorePersistError; stop classifying Redis aborts as provider-network

`Command aborted due to connection close` is an ioredis error — the
Redis/Upstash agent-state store dropping a queued command, not the LLM
provider's network. It was mapped to `ProviderNetworkError`, which
misattributed our own infra failures to upstream providers.

- `agentRuntime.ts` — new `StateStorePersistError` (sibling of
  `DatabasePersistError`: DB layer vs state-store layer).
- `specs.ts` — E7005 under 7xxx Stream/Runtime (harness), countAsFailure true.
- `patterns.ts` — repoint `Command aborted due to connection close` to
  StateStorePersistError, and add the other Upstash state-store signatures
  (`max request size exceeded`, `database has been suspended`).
- `match.test.ts` + `modelRuntime.ts` — test + en-US locale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(model-runtime): add ContextEnginePipelineError + harness JS-crash patterns

Classify the harness-side crashes that were landing as `unknown`:

- `ContextEnginePipelineError` (E7006, 7xxx Stream/Runtime, harness) — the
  context-engine pipeline processor crash, surfaced as "Processor [<name>]
  execution failed". The context-engine throws `PipelineError` (its
  `error.name`), so a CODE_ALIASES entry resolves `PipelineError` →
  ContextEnginePipelineError for stored / live records.
- patterns: `Processor [` → ContextEnginePipelineError, placed before the
  generic JS-crash fallbacks so a processor crash with a nested TypeError is
  attributed to the pipeline, not the bare `Cannot read properties` rule.
- patterns: bare V8 crashes (`is not a function`, `Cannot read properties of`,
  `Maximum call stack size exceeded`) → AgentRuntimeError, kept LAST so
  specific provider/harness patterns win first.
- test + en-US locale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): reattribute ConversationParentMissing to user

The broken conversation chain (`parent_id` no longer exists) is usually the
user deleting the topic / parent message mid-operation — an expected race,
not a harness bug. Flip attribution harness → user, countAsFailure
true → false (so it drops out of failure metrics), severity error → warning.

numericId 7003 / category `stream` stay put (append-only); attribution and
category are orthogonal, so a stream-bucket code can be user-attributed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(model-runtime): classify "[object Object]" messages as AgentRuntimeError

A message of literally "[object Object]" means the harness stringified an
error object instead of extracting its message — a harness serialization bug.
Add it to the JS-crash fallbacks (last, lowest priority) so it resolves to
AgentRuntimeError instead of staying unknown.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:21:07 +08:00
Arvin Xu 9cc5f9e1a0 feat(model-runtime): classify Cloud-only error codes via numericId tier digit (#15278)
The three Cloud-only `ChatErrorType` codes (`FreePlanLimit`,
`InsufficientBudgetForModel`, `LobeHubModelDeprecated`) were emitted by the
managed gateway but had no spec, so they showed up unclassified on the
operation dashboards.

Rather than add a 10th `ErrorCategory` (the single-digit category prefix
1-9 is exhausted, and a 10th would break the 4-digit numericId scheme +
its validation tests), encode the OSS-vs-Cloud distinction in the
**second digit** of `numericId`: `0` = open-source runtime, `9` = Cloud-only.
Every existing code already has tier digit 0, so this is purely additive —
the category leading-digit invariant, 4-digit range, and `E####` regex all
hold unchanged.

- `taxonomy.ts` — document the tier digit, add `CLOUD_TIER_DIGIT = 9`.
- `specs.ts` — widen the spec key/`code` type to `SpecErrorCode`
  (`ILobeAgentRuntimeErrorType | CloudErrorCode`); add the three entries
  under their semantic categories with tier-9 ids: `FreePlanLimit` E2901 &
  `InsufficientBudgetForModel` E2902 (quota), `LobeHubModelDeprecated` E4901
  (request). All `attribution: user`, `countAsFailure: false`.
- `match.test.ts` — assert every spec's tier digit is 0 or 9, and the three
  Cloud codes resolve under the cloud tier.

Locale keys (`response.<code>`) for all three already exist. The
agent-gateway mirror is updated separately.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:28:50 +08:00
AnotiaWang bcf97d9487 💄 style: add DeepSeek V4 Pro to SiliconCloud model list (#15267)
*  feat(model-bank): add DeepSeek V4 Pro to SiliconCloud model list

Co-authored-by: AnotiaWang <AnotiaWang@users.noreply.github.com>

* 💰 pricing(siliconcloud): add cache hit price for DeepSeek V4 Flash

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: AnotiaWang <AnotiaWang@users.noreply.github.com>
2026-05-28 11:07:20 +08:00
Arvin Xu 3e4b81d2cc chore(channel): register iMessage platform with coming-soon UI gate (#15276)
 feat(channel): register iMessage platform with coming-soon UI gate

Activate the server-side iMessage registration that was previously
landed but un-registered, and let coming-soon entries take precedence
over server platforms with the same id so the platform stays hidden
until the desktop bridge UI ships.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:54:20 +08:00
Arvin Xu 651d1a203a Revert " feat(desktop): support cloud desktop builds (#14498)"
This reverts commit 0c5ccc8770.
2026-05-28 10:40:59 +08:00
Arvin Xu 4c29515e4c ♻️ refactor(locales): split model-runtime errors into modelRuntime namespace (#15269)
Until now, every runtime error code (InvalidProviderAPIKey, ProviderBizError,
ExceededContextWindow, …) lived under `error.response.<X>` — mixed in the
same file with HTTP statuses, Plugin*, Cloud business errors, and
GoogleAIBlockReason subkeys. The `response.` prefix is a lobehub-specific
convention that has nothing to do with the underlying ErrorCode, which
made it awkward for external consumers and noisy for maintainers.

This change carves out a dedicated `modelRuntime` i18next namespace:

- `src/locales/default/modelRuntime.ts` — 34 keys, one per
  `AgentRuntimeErrorType` (or deprecated alias `QuotaLimitReached`).
  Key = the bare ErrorCode (no `response.` prefix).
- `src/locales/default/error.ts` — runtime keys removed. The file keeps
  HTTP statuses (response.400 - response.524), Plugin*, Cloud-only
  business errors (FreePlanLimit, SubscriptionPlanLimit, etc.),
  GoogleAIBlockReason.*, and the various UI-flow strings.
- Registered `modelRuntime` in `src/locales/default/index.ts` so the
  namespace appears in the typed resources map.
- Generated `locales/en-US/modelRuntime.json` + updated
  `locales/en-US/error.json` — other languages need `pnpm i18n`.

New helper `src/utils/locale/runtimeErrorMessage.ts`:

```ts
getRuntimeErrorMessage(t, code, vars)
```

Routes via `getErrorCodeSpec(code)`: returns `t('modelRuntime:<code>')`
when the code is in `ERROR_CODE_SPECS`, otherwise falls back to
`t('response.<code>')`. Callers add `'modelRuntime'` to their
`useTranslation()` namespace list.

UI consumer migrations (5 dynamic lookup sites):

- `features/Conversation/Messages/AssistantGroup/Tool/Detail/ErrorResponse.tsx`
- `features/Conversation/Error/index.tsx`
- `routes/(main)/settings/provider/features/ProviderConfig/Checker.tsx`
  (incl. the static `t('response.ConnectionCheckFailed')` call)
- `routes/(main)/(create)/video/features/GenerationFeed/VideoErrorItem.tsx`
- `routes/(main)/(create)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx`

`Description.tsx` (HTTP status renderer) stays on `response.<X>` since
its inputs are always HTTP status numbers, never runtime ErrorCodes.

Stacks on top of #15262 (the unified errors PR introduces
`getErrorCodeSpec` / `ERROR_CODE_SPECS`); base this PR there until
#15262 merges, then it auto-rebases onto canary.

Tests: lobehub type-check clean; model-runtime 3908 pass / 1 skip / 164 files.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:04:07 +08:00
Innei b4b1205ee9 ♻️ refactor(modal): migrate confirm modals to @lobehub/ui/base-ui (Phase 1) (#15259)
* ♻️ refactor(modal): migrate confirm modals to @lobehub/ui/base-ui

Replace all `App.useApp().modal.confirm`, `Modal.confirm` and `AntModal.confirm`
call sites with the headless `confirmModal` from `@lobehub/ui/base-ui`, dropping
antd-only props (`centered`, `type`, `width`, `okButtonProps.type='primary'`,
`okButtonProps.loading`, `classNames.root`) that the base-ui imperative API does
not accept.

- 82 files touched; `modal.confirm`/`Modal.confirm` call sites now zero
- `PageEditor/store/action.ts`: drop `modal` arg from `handleDelete`
- `ResourceManager/useUploadFolder`: replace dynamic `import('antd').Modal`
- `Eval/DatasetsTab`: migrate `modal.success` to `confirmModal`

Part of LOBE-9645 Phase 1.

* ♻️ refactor(ui): migrate select/modal call sites to @lobehub/ui/base-ui

- Convert imperative-modal factories (createXxxModal + Content split) for apikey,
  creds (Create/Edit/View), provider (CreateNewProvider), and messenger LinkModal.
- Switch Select usages to base-ui Select (Messenger AgentSelect, provider sdkType).
- Restructure CreateNewProvider form to vertical layout with manual section titles
  for tighter spacing; drop FormModal/Form group nesting.
- Standardize small ActionIcon sizing via DESKTOP_HEADER_ICON_SMALL_SIZE
  (WideScreenButton, ToggleRightPanelButton, ContextDropdown, AddNewProvider).
- Fix missing title on ResourceManager delete confirm modal so the header
  (title + close X) renders.
- Update react skill and AGENTS.md to require base-ui priority over root @lobehub/ui
  / antd; expand component table and Common Mistakes with explicit base-ui rules.

* ♻️ refactor(ui): swap antd Select to base-ui Select and migrate createStyles to createStaticStyles

*  test: update test mocks for base-ui confirmModal migration

*  test(e2e): switch delete confirm selector to base-ui dialog role
2026-05-28 02:46:27 +08:00
Arvin Xu 8c0e66b633 feat(agent-runtime): persist ERROR_CODE_SPECS classification on operation errors (#15273)
*  feat(agent-runtime): persist ERROR_CODE_SPECS classification on operation errors

Look up the runtime error's spec in `ERROR_CODE_SPECS` at the single catch
chokepoint and merge `attribution` / `category` / `severity` / `httpStatus`
/ `retryable` / `countAsFailure` / `numericId` onto the normalized
`ChatMessageError`. The enriched object flows through to all three
downstream sinks — `agent_operations.error` JSONB, S3 trace snapshot,
and the agent-gateway WS push — without each consumer having to re-run
pattern matching.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(agent-runtime): enrich inner-step error path too

Model-runtime failures caught inside `runtime.step()` resolve normally with
`newState.status = 'error'` instead of throwing, so the prior commit's outer
`executeStep` catch never sees common provider errors like
`InvalidProviderAPIKey` / `InsufficientQuota`. Those were reaching
`agent_operations.error` JSONB and the success-path trace snapshot raw —
without `attribution` / `category` / `severity` / …

Run `formatErrorForState` on `stepResult.newState.error` immediately after
`runtime.step()` returns, before the state is saved to Redis, hooks are
dispatched, or the trace is finalized. Made the helper idempotent (recognizes
already-normalized `ChatMessageError` shape) so a second pass through the
outer catch can't collapse it back to `AgentRuntimeError`. Success-path
`traceRecorder.finalize` now forwards the classification fields too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 02:25:16 +08:00
Innei 1ae8498fc7 feat(agent-document): render non-markdown docs as readonly highlight (#15272)
* 🐛 fix(tool-archive): use .txt extension for archived tool results

Tool result content is raw output (logs, JSON, stack traces) rather than
structured markdown. Saving as .md misrepresents the format and triggers
markdown parsing downstream. Switch the archive filename to .txt to match
the actual content type.

*  feat(agent-document): render non-markdown docs as readonly highlight

Agent documents whose filename does not resolve to markdown (e.g. archived
tool results saved as .txt, future .json / .yaml entries) are now rendered
through @lobehub/ui Highlighter with the inferred language, replacing the
markdown editor surface that misinterpreted raw text as syntax.

- Extract the filename→language map from FileViewer Code renderer into a
  shared util so the document viewer reuses the same mapping.
- Introduce getDocumentRenderMode: SKILL.md and .md keep the editor; all
  other extensions resolve to a Highlighter, which is naturally readonly.
- Hide the auto-save hint in Header when the document is rendered as a
  Highlighter (no editor, nothing to save).

* 🐛 fix(agent-document): render notebook documents as editor when filename is absent

Notebook documents store the markdown signal in `fileType` + `title` and never set a
`filename`. `getDocumentRenderMode` was falling back to `title` for language
inference, which resolved free-form titles like "Meeting notes" to `txt` and routed
them into the readonly Highlighter (also hiding the autosave hint).

Treat filename-absent documents as editor mode directly; filename remains the only
source for code-language inference.
2026-05-28 01:37:58 +08:00
Arvin Xu c4b147554b ♻️ refactor(model-runtime): unify error codes into spec + pattern registry (#15262)
*  feat(model-runtime): unify error codes into spec + pattern registry

Add a single source of truth for runtime error classification under
`packages/model-runtime/src/errors/`:

- `taxonomy.ts` — category / severity / attribution dimensions
- `specs.ts` — ERROR_CODE_SPECS: per-code httpStatus / retryable /
  countAsFailure / attribution (user | provider | harness | system)
- `patterns.ts` — ERROR_PATTERNS: substring/regex registry consolidating
  the 5 separate isXxxError lists and the upstream provider message
  patterns previously kept only in agent-gateway
- `match.ts` — matchErrorPattern() + isUserSideError()

Wire-up:
- Add 8 codes to AgentRuntimeErrorType (ProviderServiceUnavailable,
  ProviderNetworkError, NoAvailableChannel, ContentModeration,
  CapabilityNotSupported, InvalidRequestFormat, UserConfigError,
  OperationInactivityTimeout) plus their en-US locale keys
- Rewrite isExceededContextWindow / isQuotaLimit / isInsufficientQuota /
  isAccountDeactivated as one-line wrappers around matchErrorPattern
- errorResponse.ts getStatus() now reads ERROR_CODE_SPECS, removing the
  hardcoded switch

Tests: 167 model-runtime test files (3916 pass / 1 skip) including 13
new match.test.ts cases and all 42 isXxxError snapshots unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(model-runtime): add numericId (E1001) + ErrorClassifier namespace

Numeric reference codes for external surfaces (open-source consumers, docs
anchors, support tickets):

- `ErrorCodeSpec.numericId` (required, 4-digit). Append-only contract: once
  assigned, a (code, numericId) pair never changes even if the string `code`
  is renamed.
- Format: `E<numericId>` (e.g. `E1001` InvalidProviderAPIKey, `E3001`
  QuotaLimitReached, `E7002` OperationInactivityTimeout).
- First digit encodes category via `CATEGORY_NUMERIC_PREFIX`:
  1=auth, 2=quota, 3=capacity, 4=request, 5=safety, 6=network, 7=stream,
  8=provider, 9=config.
- Helpers: `formatErrorRef(code) → 'E1001'`, `parseErrorRef('E1001') → code`.
- Test guards: numericId is unique across specs; leading digit matches the
  declared category for every entry.

Consolidate classification predicates:

- New `ErrorClassifier` namespace bundles `isExceededContextWindow` /
  `isInsufficientQuota` / `isQuotaLimitReached` / `isAccountDeactivated`
  behind a single discoverable import.
- The 4 scattered `is*Error.ts` utilities are now `@deprecated`; kept as
  shims for callers that aren't migrated yet.
- Parity test asserts ErrorClassifier and the legacy utils return the same
  boolean on a curated sample set.

Tests: 168 files / 3928 pass / 1 skip. +12 new tests for numericId contract,
ref formatting, and classifier parity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): rename QuotaLimitReached → RateLimitExceeded

The legacy name conflated two distinct semantics: short-window rate limit
(429-class, transient, retryable, provider-attributed) vs. long-term
account-level quota exhaustion (`InsufficientQuota`, user-attributed).
Surface code readers hit this confusion the moment they look at the spec
table — the name reads like a 2xxx quota code but the spec sits in 3xxx
capacity.

- Add `AgentRuntimeErrorType.RateLimitExceeded` as the canonical name.
- Keep `AgentRuntimeErrorType.QuotaLimitReached` as a `@deprecated` alias
  (same string value preserved for legacy stored data on the dashboard
  side) — `CODE_ALIASES` map in `specs.ts` ensures `getErrorCodeSpec` /
  `isUserSideError` resolve both old and new strings to the canonical
  E3001 spec.
- `ErrorClassifier`: new `isRateLimitExceeded` is canonical;
  `isQuotaLimitReached` kept as deprecated alias.
- Refresh patterns.ts (~24 entries) + isQuotaLimitError util.
- Locale: add `response.RateLimitExceeded` next to the kept legacy
  `response.QuotaLimitReached`.
- Match.ts now reads via `getErrorCodeSpec` so alias resolution flows
  through one place.

Tests: 3930 model-runtime tests pass (+2 explicit alias-resolution cases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(AgentRuntime): wire classifyLLMError to ERROR_CODE_SPECS

The runtime retry loop's STOP_ERROR_TYPES was a hardcoded set that didn't
move with the unified error scheme. New codes added in #15262
(ContentModeration, InvalidRequestFormat, UserConfigError, NoAvailableChannel,
OperationInactivityTimeout, CapabilityNotSupported, LocationNotSupportError,
ExceededToolLimit, …) all carry `retryable: false` in the spec, but an
error arriving with one of these `errorType` values **and no HTTP status**
(e.g. a gateway-classified moderation message like "Content Exists Risk")
fell through to the classifier's default `retry` branch, producing pointless
retry storms for requests the spec says should stop.

Fix:

- Derive `STOP_ERROR_TYPES` / `RETRY_ERROR_TYPES` from `ERROR_CODE_SPECS` at
  module load. Future codes added to the spec table now classify
  automatically — no second source of truth.
- Keep a tight `RETRY_OVERRIDES` set for the 4 legacy codes
  (`AgentRuntimeError` / `OllamaServiceUnavailable` / `ProviderBizError` /
  `StreamChunkError`) that the runtime intentionally retries even though
  the spec marks them non-retryable; these are catch-all / harness-level
  failures often transient in practice.
- Resolve through `getErrorCodeSpec` before set lookup so the deprecated
  `QuotaLimitReached` alias classifies the same as its canonical
  `RateLimitExceeded`.
- Export the `errors/` module from `@lobechat/model-runtime` root barrel.

Tests: 31 cases (+12) including `it.each` coverage of all 8 newly-stop
codes and 3 newly-retry codes, plus explicit guards for the legacy retry
overrides and the QuotaLimitReached → RateLimitExceeded alias.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): consolidate isXxxError utils into ErrorClassifier

Three structural cleanups on top of the unified error scheme:

1. **Reorder `ERROR_CODE_SPECS` strictly by `numericId`.** Previously the
   spec table followed the original loose category groupings, which left
   stragglers like `InvalidOllamaArgs` (E9001, config) wedged into the
   1xxx auth section. Now entries appear in 1001 → 9005 order with
   numeric-prefix section dividers. Added `it('spec entries appear in
   source order sorted by numericId')` as a lint guard so future
   additions stay sorted (JS preserves object-literal insertion order).

2. **Migrate all production callers from `isXxxError` utils to
   `ErrorClassifier` namespace.** Touched 4 files, 13 call sites:
   - `core/anthropicCompatibleFactory/index.ts` (6)
   - `core/openaiCompatibleFactory/index.ts` (4)
   - `providers/bedrock/index.ts` (1)
   - `utils/googleErrorParser.ts` (2)

3. **Delete the 4 deprecated util files + their tests.** With no
   production callers left, the shim layer is dead code. Classifier
   tests now stand on their own (no parity comparison against the
   deleted utils).

Also mirror the spec ordering to `agent-gateway/src/errors/specs.ts`
(separate commit on that repo).

Tests: 164 files / 3908 pass / 1 skip (was 168 / 3930 — the delta is the
4 removed `isXxxError.test.ts` files, ~42 tests, net of new classifier
coverage).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(AgentRuntime): stub ERROR_CODE_SPECS in test mocks of @lobechat/model-runtime

`classifyLLMError` now reads `ERROR_CODE_SPECS` + `getErrorCodeSpec` at
module-load time to derive the STOP / RETRY sets. Two test suites mock
`@lobechat/model-runtime` sparsely (only `consumeStreamUntilDone` or
`getModelPropertyWithFallback`), so those new exports were undefined and
the module-eval crashed with `No "ERROR_CODE_SPECS" export is defined on
the "@lobechat/model-runtime" mock`.

Fix: add the two symbols to the mocks. Used empty stubs rather than
`importOriginal` so the mocks stay small and don't transitively pull
the entire model-runtime package (which would then expect every other
mocked package — e.g. `model-bank.AiModelTypeSchema` — to be complete).

Neither suite exercises the runtime retry classifier, so empty
`ERROR_CODE_SPECS` and `getErrorCodeSpec` returning `undefined` are
behaviorally equivalent to the pre-PR baseline.

Verified locally:
- `bunx vitest run src/server/modules/AgentRuntime/__tests__/RuntimeExecutors.test.ts` — 102 tests pass
- `bunx vitest run src/server/services/agentRuntime/AgentRuntimeService.test.ts` — 60 tests pass

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 00:54:59 +08:00
Hardy 5fb1f339a7 feat(opencode-go): align model configs with models.dev API, add DeepSeek V4 Flash/Pro, improve reasoning runtime (#15031) 2026-05-28 00:52:27 +08:00
Rdmclin2 81fc1aaf7f 🐛 fix: telegram messager attachments (#15268)
* fix: telegram messager installation

* fix: lint error

* fix: telegram resolve Credentials first
2026-05-27 23:31:47 +07:00
LobeHub Bot b14f1dba5c 🌐 chore: translate non-English comments to English in openapi-types-common (#15255)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:19:31 +08:00
Arvin Xu 1d2b32bafc 🔖 chore(cli): bump @lobehub/cli to 0.0.22 (#15254)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:18:37 +08:00
Innei 347e2eec0c 💄 style(desktop/overlay): replace native select with @base-ui/react primitives (#15266)
* 💄 style(desktop/overlay): replace native select with @base-ui/react primitives

The overlay window's agent/model pickers use native `<select>` elements,
which render poorly on Windows. Switch to `@base-ui/react/select` primitives
directly, styled with the existing overlay vanilla-extract tokens.

The overlay is a bare-React tree (no SPA provider stack) intentionally
optimised for cold-start, so we cannot afford to mount `@lobehub/ui`'s
`ThemeProvider` just to use its `Select` wrapper — that path adds ~250ms
of bundle parse + ~117ms of React mount in dev mode. Using the underlying
primitive instead keeps the increase to ~119ms over native.

Mirror the overlay theme CSS variables onto `document.documentElement` so
the portaled popup (rendered outside the panel subtree) inherits them.

Also add a small gated benchmark utility (`perfMark.ts`, enabled via
`localStorage.lobe-overlay-bench=1` or `?bench`, zero overhead otherwise)
for measuring overlay cold-start segments. Call `__OVERLAY_BENCH__()`
in DevTools to dump the timeline.

* 🔥 chore(desktop/overlay): drop bench instrumentation, lower popup z-index

- Remove perfMark utility and its call sites — benchmarking is done, no
  need to ship the bench harness.
- Drop popup z-index from int32-max to 114514 (sufficient on its own
  stacking context; saner number).
2026-05-27 22:01:11 +08:00
Arvin Xu e8275a93ff 🐛 fix(hetero-agent): hide device switcher in regular agent chat input (#15257)
The HeteroDeviceSwitcher is meant for heterogeneous agents only and is
already rendered by HeterogeneousChatInput/WorkingDirectoryBar. Remove
it from the regular RuntimeConfig so it no longer appears for normal
agents.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:54:42 +08:00
Innei 49d191d2a7 🐛 fix: unify TypeScript peer resolution on 6.x (#15263) 2026-05-27 19:22:35 +08:00
Arvin Xu 35052416cc chore: clean up LOBE-XXX code annotations (#15249) 2026-05-27 18:09:06 +08:00
Innei 0c5ccc8770 feat(desktop): support cloud desktop builds (#14498)
*  feat(desktop): support cloud desktop builds

* 🐛 fix: open payment navigations externally in desktop
2026-05-27 16:22:48 +08:00
Innei c8ff3ac43d feat: gate agent document floating chat panel (#15260) 2026-05-27 14:02:14 +08:00
Innei 718096e306 💄 style(settings): unify select width and migrate to base-ui Select on service-model (#15248)
* 💄 style(settings): unify select width and migrate to base-ui Select on service-model

- Migrate `Select` from deprecated `@lobehub/ui` (antd-based) to `@lobehub/ui/base-ui` on STT/OpenAI/const
- Fix inconsistent select widths on service-model page: all selects now fixed at 448px
- Pull Switch out of width-constrained Flexbox in optional features row so the inner ModelSelect stays at 448px
- Drop per-item `minWidth: undefined` overrides and let Form-level `itemMinWidth={undefined}` make control col fit-content

* 💄 style(settings): move enable Switch before Select in optional features

Putting Switch in front of the Select aligns all selects on the page at the
same right edge — previously Switch trailing the Select pushed its right edge
56px to the left of other rows.
2026-05-27 12:44:35 +08:00
LiJian f0eded2941 feat(onboarding): skip redirect when landing on agent/inbox with message param (#15256)
*  feat(onboarding): skip redirect when landing on agent inbox with message param

New users arriving via /agent/inbox?message=... (e.g. Skills Marketplace
"Try in LobeHub" links) were being redirected to /onboarding before their
message could be sent, breaking the intended flow.

When the user lands on /agent/inbox with a message param, skip the onboarding
redirect so MessageFromUrl can immediately deliver the message. The user will
be prompted to complete onboarding on their next regular visit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(onboarding): broaden agent inbox guard to cover AgentIdSync slug rewrite

The previous guard matched only /agent/inbox, but AgentIdSync rewrites the
builtin slug to the resolved real agent ID (/agent/{uuid}) before the
useInitUserState callback fires — so pathname.startsWith('/agent/inbox')
was false by the time the check ran.

Widen the guard to any /agent/* path with a message param. The message
query param is the "send immediately" signal so the guard remains narrow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:58:59 +08:00
LiJian 1f6d350dca 🐛 fix(copy): unescape markdown escapes when copying user messages (#15253)
* 🐛 fix(copy): unescape markdown escapes when copying user messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 refactor(copy): extract unescapeMarkdown util and skip code spans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:09:51 +08:00
LiJian 5eee6d21e3 🐛 fix(hetero-agent): hide sandbox selector when device switcher is visible and sync runtimeMode (#15252)
* 🐛 fix(hetero-agent): hide runtimeMode selector when device switcher is visible and sync runtimeMode on target change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-agent): persist executionTarget and runtimeMode atomically to avoid abort-signal race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 11:05:06 +08:00
Arvin Xu bcc31ca331 feat(bot): add hidden iMessage backend foundation (#15227)
*  feat(bot): add hidden iMessage backend foundation

* 🐛 fix(bot): align iMessage search totals and attachment timeout

* ♻️ refactor(bot): derive gateway runtime user from provider

*  feat(device): add message API calls
2026-05-27 02:21:43 +08:00
Innei 72d34046c0 🐛 fix(tabbar): debounce route meta publish to avoid tab item flicker (#15238)
* 🐛 fix(tabbar): debounce route meta publish to avoid tab item flicker

Desktop tab title and avatar could switch multiple times during page
navigation as agent/chat store data settled. Two coordinated fixes:

- Short-circuit `setCurrentRouteMeta` on shallow-equal meta + url so
  repeated identical writes never trigger downstream re-renders.
- Wrap the publish in a trailing 80ms debounce inside `RouteMetaBridge`
  and cancel it on route change/unmount so stale resolves from the
  previous route cannot overwrite the new one. Local `setDynamic`
  (driving document.title) stays synchronous.

* 🐛 fix(route-meta): keep previous dynamic meta during in-route navigation to stop title flicker

Dynamic state was keyed by `routeId + currentUrl`, so switching between
two topics (same route, different params) immediately invalidated the
previously resolved meta. The title fell back to the static `titleKey`
for one render before the new resolution arrived, producing an "A →
Chat → B" flash.

Key the cached meta by `routeId` alone. While navigating within the
same route family, the previous title persists until the new dynamic
resolution overwrites it; navigating to a different route still clears
correctly via the existing reset effect.
2026-05-26 22:09:23 +08:00
Innei 60f08f58e4 🐛 fix(electron-tab): update inactive tab title when topic is auto-named (#15244)
Run DynamicMetaRunner for every tab via TabCacheBridges so background
tabs receive auto-named topic titles instead of staying on "Default Topic".

Fixes LOBE-9492
2026-05-26 21:42:23 +08:00
Innei 202f062a0d feat(portal): embed thread chat in document preview portal (#15216)
*  feat(portal): embed thread chat in document preview portal

Embed FloatingChatPanel at the bottom of the Document preview portal so
users can converse with the agent about the document they are viewing
without leaving the portal.

Key changes:

- Remove the unused `/agent/:aid/:topicId/page/:docId` route and its
  supporting modules (TopicCanvas, Page, PageRedirect, topicPageRouteMeta,
  `agent/page` redirect). The route had no remaining UI entry point.
- Revive FloatingChatPanel as a thread-scoped side chat. Replace the
  hardcoded MainChatInput with `@/features/Conversation`'s ChatInput so
  the embedded composer no longer fights the main-page input for the
  global `mainInputEditor` slot.
- Default the panel's context to `scope='thread' + isNew: true` so a
  fresh ephemeral thread can be created on first send.
- Thread an `agentDocumentId` field through ConversationContext,
  ExecAgentAppContext, the Document portal payload, `openDocument` and
  callers (AgentDocumentsGroup, DocumentExplorerTree,
  AgentSignalReceiptList) so the in-portal chat always knows the
  agent_documents row id for the document in view.
- Rewrite the server `activeTopicDocument` resolver to use a single
  indexed `findRowByDocumentId(agentId, documentId)` lookup. This
  validates any caller-supplied row id and recovers the row when one
  was not provided, fixing cross-topic documents (skills, web docs)
  whose row id was previously missing — preventing the LLM from passing
  a `documents.id` into `readDocument({ id })` and triggering a failed
  query against `agent_documents.id`.

*  feat(portal): persist document portal chats as real threads

Anchor the in-portal `FloatingChatPanel` on the topic's last main-scope
message so the first send goes through `conversationLifecycle.ts`'s
`newThread` branch and the server actually creates a thread row. The
resulting thread now shows up in the left sidebar's `ThreadList` under
the parent topic.

- Read `sourceMessageId` from the latest non-thread message in
  `dbMessagesMap[messageMapKey({ agentId, topicId })]`; pair it with
  `ThreadType.Standalone` in the conversation context when `isNew`.
- Track the active thread in panel-local state. On
  `onAfterMessageCreate({ createdThreadId })` we refresh threads /
  messages and pivot the context from `isNew` to the persisted
  `threadId` in place — without calling `openThreadInPortal`, which
  would push a Thread view onto the portal stack and cover the document
  the user is reading.
- When the topic has no messages yet (no anchor), fall back to the
  previous ephemeral behavior (still leaks to main on first send;
  needed for empty-topic scenarios).

*  feat(portal): isolate document portal thread chat from main topic

Make the Document portal's `FloatingChatPanel` a truly doc-anchored side
conversation — independent of the main topic history and surviving the
mid-send pivot from `_new` → persisted thread key without the AI stream
disappearing.

- Subscribe to `chatStore.portalThreadId` instead of a panel-local
  `internalThreadId`. `lifecycle.ts:syncThreadInPortal` writes the new
  thread id into the portal slice *before* stream chunks arrive, so this
  panel's chatKey pivots in time to render the streaming response — the
  old `onAfterMessageCreate` hook only fired after the stream resolved,
  leaving the panel blank for the whole turn.
- Clear any stale `portalThreadId` left by a sibling portal on mount so a
  fresh `(agentId, topicId, documentId)` opens in `isNew` state.
- Pass `skipFetch` + a filtered `messages` prop to ConversationProvider.
  Without `skipFetch` the provider's own `useFetchMessages` pulled the
  main-topic history into this panel; with the doc-anchored A-mode we
  show only rows whose `threadId` matches the active thread (or nothing
  before the first send).
- Split `openThreadInPortal` into two actions: keep the original (push
  Thread view + sync state) for the main-page "create subtopic" flow,
  and add `syncThreadInPortal` that only mutates the portal slice.
  `lifecycle.ts` now picks one based on the current portal view type so a
  panel-hosted ConversationProvider in the Document portal no longer
  triggers a Thread view that covers the document.
- Add `key={agentId:topicId:documentId}` on `FloatingChatPanel` inside
  `Portal/Document/Body.tsx` so panel-local state (snap point, open,
  etc.) resets when conversation coordinates change.
- Anchor new threads on the topic's last main-scope message, paired with
  `ThreadType.Standalone`, so first send actually creates a thread row
  rather than leaking into the main topic.
2026-05-26 20:58:23 +08:00
LiJian be81c35e94 🐛 fix(exec-agent): gate CREDS_LIST/KLAVIS substitution on manifestMap instead of enabledToolIds (#15240)
* 🐛 fix(exec-agent): gate CREDS_LIST fetch on manifestMap instead of enabledToolIds

In execAgent mode, lobe-creds is added to toolManifestMap for activator
discovery but never into enabledToolIds, so the previous check
`resolved.enabledToolIds.includes(CredsIdentifier)` was always false
while the system role (containing {{CREDS_LIST}}) was already injected.
Gating on manifestMap presence aligns the variable substitution with the
actual system-role injection condition.

Also applies the same fix to {{KLAVIS_SERVICES_LIST}} which shares the
same isCredsEnabled gate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(exec-agent): gate KLAVIS_SERVICES_LIST substitution on KLAVIS_API_KEY presence

When KLAVIS_API_KEY is not configured the Klavis API client throws and
none of the advertised services are actually usable. Populate
{{KLAVIS_SERVICES_LIST}} only when the key is present, mirroring the
client-side enableKlavis check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 18:11:24 +08:00
LiJian 14357a3f51 🐛 fix(create-platform-agent): wrap long version string in capability status (#15237)
🐛 fix(create-platform-agent): wrap long version string in capability status tag

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:17:19 +08:00
LiJian 0561a1d7eb 🐛 fix(hetero-agent): skip LOADING_FLAT placeholder when restoring accumulatedContent (#15236)
* 🐛 fix(hetero-agent): skip LOADING_FLAT placeholder when restoring accumulatedContent

When the cloud/IM Claude Code path cold-starts (Vercel serverless), it reads
the pre-created assistant message from DB to restore accumulatedContent.  That
message initially holds LOADING_FLAT ('...'), which was being treated as real
text — causing every first-turn response to start with '...'.

Subsequent turns were unaffected because handleStepStart (triggered by
--resume's newStep:true) always resets accumulatedContent to '' and creates a
fresh message with empty content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-agent): also strip LOADING_FLAT in ingest() DB refresh path

The previous commit guarded loadOrCreateState but the ingest() method
re-reads the assistant row from DB immediately after and adopts the DB
value when it is longer than in-memory.  On a cold-start first turn the
DB still holds LOADING_FLAT ('...', length 3) while in-memory was just
reset to '' (length 0), so the "adopt if longer" branch overwrote the
fix and put '...' back into accumulatedContent.

Apply the same LOADING_FLAT → '' normalisation to the refresh read so
both paths are consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:01:21 +08:00
Rylan Cai 3e0a396968 ♻️ refactor: + run command observation timeout (#15195)
*  add command observation timeout handling

*  hide shell observation timeout from model

* 🐛 restore shell observation compatibility

* 🐛 clear device proxy caller timeout timer

* 🐛 separate shell observation and caller timeouts

* 📝 clarify shell output wait semantics

* 🐛 align shell manifest timeout semantics

* 🐛 restore incremental shell output semantics

* 🐛 restore get command output manifest wording

* 🐛 restore get command output prompt wording

* 🐛 remove running state from command output

* 🐛 restore local system server runtime passthrough

* 🐛 restore device proxy timeout passthrough

* ♻️ tighten shell observation implementation

* ♻️ defer completed shell cleanup policy

* ♻️ simplify shell observation wait

* ♻️ read shell exit code from child process

* ♻️ tighten shell output exit code handling

* ♻️ clarify shell observation wait race

* 🐛 add device gateway HTTP call timeout

* 📝 clarify shell command session prompts

*  use incremental shell session ids

*  pass execution timeout through local system tool chain

* 🚑 fix local system timeout CI coverage

*  fix desktop shell controller tests
2026-05-26 14:53:34 +08:00
Arvin Xu 5f27cd8f26 💄 polish(agent-topic-manager): lighter bulk-bar shadow, transparent tool-auth alert, preserve sub-route (#15224)
* 💄 polish(agent-topic-manager): lighter bulk-bar shadow, transparent tool-auth alert, preserve sub-route on agent switch

- BulkActionBar: tone down the floating pill shadow from a heavy 24%/16%
  stack to a softer 8%/6% pair so it stops competing with the list rows.
- ToolAuthAlert: drop the secondary-tint fill (`background: transparent`)
  so the panel reads as a calm hint, not a warning. Reword the hint copy
  to "技能未授权或未配置时,相关技能无法使用,可能导致助理能力受限或报错" /
  matching EN.
- Sidebar agent switcher: clicking Lobe AI (Inbox) from `/agent/X/topics`
  now lands on `/agent/inbox/topics` instead of dropping back to the
  default chat URL. Extracts the existing `AgentItem` preservation logic
  into a `usePreservedAgentUrl` hook so both items share it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 polish(bulk-bar): use cssVar.boxShadowSecondary token

Replace the hand-tuned `box-shadow` stack with the existing
`boxShadowSecondary` design token — matches the floating-overlay
pattern used by Notification, CommandMenu, etc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(inspector): add X (Twitter) inspector

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 polish(linear-inspector): use secondary text color in chips

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 polish(linear-inspector): only dim the Linear wordmark, keep chip text primary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 polish(twitter-inspector): only dim the X (Twitter) wordmark, keep chip text primary

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:36:43 +08:00
YuTengjing 1c80146a07 🐛 fix(auth): prompt account selection for Google OAuth (#15234) 2026-05-26 12:54:47 +08:00
Innei 1d4d5c1c73 🐛 fix(library): add CTA in folder hierarchy empty state (#15220)
Previously the sidebar tree showed a blank panel when a library had no files or folders, leaving users with no entry point. Now an empty state surfaces an icon, title, hint, and the existing AddButton dropdown (new page / new folder / upload / Notion import).
2026-05-26 12:50:48 +08:00
Innei d45257615a 🐛 fix(sidebar): respect customize sidebar order across the bottom spacer (#15222)
The home sidebar previously split items into hard-coded top/bottom buckets,
so reordering an item across the bottom spacer in the Customize Sidebar
modal had no visible effect. Introduce a sentinel spacer slot in
`sidebarItems` (draggable in the modal as a divider row, rendered as a
flex:1 occupant in the sidebar) and remove the hard split — the sidebar
now follows the persisted order verbatim.
2026-05-26 12:50:40 +08:00
Arvin Xu b3cbc9a710 🐛 fix(prompts): keep input_completion system prompt stable across invocations (#15230)
* 🐛 fix(prompts): keep input_completion system prompt stable across invocations

Move the per-conversation context block out of the system message and into
a dedicated user message. The tracing `promptHash` is computed over the
system prompt, so embedding the rolling conversation window in it produced
a fresh hash on nearly every keystroke (1000+ unique hashes observed),
defeating per-prompt grouping.

Bumps `INPUT_COMPLETION_PROMPT_VERSION` to v1.1 so tracing can distinguish
the two message layouts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(prompts): build inputCompletion messages array declaratively

Replace successive `messages.push(...)` mutations with a single array
literal using a conditional spread for the optional context message.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:50:00 +08:00
LiJian e295f80235 🐛 fix: restore userId in gateway dispatch, gate local-system by executionTarget, add device switcher for regular agents (#15232)
- Fix GatewayHttpClient.dispatchAgentRun stripping userId from request body,
  causing 'Missing userId' error when routing Claude Code to desktop device
- Gate activeDeviceId=undefined when executionTarget='sandbox' so local-system
  tools are not injected in sandbox mode
- Add HeteroDeviceSwitcher to RuntimeConfig for regular agents (lab flag gated)
  so users can select a desktop device for local-system tool execution

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 12:00:05 +08:00
Arvin Xu 5cd02b937b feat(topics): add per-agent topic management page (#15207)
*  feat(agent-topics): add per-agent topic management page

Add /agent/:aid/topics — a dedicated management surface for browsing,
filtering, and bulk-operating on an agent's topics. Card grid view by
default with list view toggle, status / project / trigger / time filters,
keyword search, and multi-select bulk favorite / archive / delete.

A new "All Topics" entry in the agent sidebar (above the Topic accordion)
opens the page.

Frontend-only — no new TRPC procedures. Wires to the existing
useFetchTopics / useSearchTopics / favoriteTopic / updateTopicStatus /
removeTopic actions. Filters that the existing backend doesn't natively
support (project, time range, multi-sort) apply client-side on the loaded
page (default pageSize 100). Bulk favorite / archive loops single-action
calls; a proper batchUpdate procedure is left as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(agent-topics): UX iteration — sidebar entry, breadcrumb, byProject grouping, floating bulk bar

Major refinements after design review on PR #15207:

- Sidebar entry: moved from in-accordion to top nav between Profile and
  Channels, renamed "All Topics" → "Topics", uses MessagesSquare icon
- Header: breadcrumb (Agent / Topics) replaces standalone title; search
  bar moves into the NavHeader center slot; "New chat" + "Select" header
  buttons removed (selection enters via card hover-checkbox)
- Card refresh: compact layout (no fixed min-height, removed "No preview"
  fallback), favorite star moves to title prefix, hover reveals
  top-right checkbox, status renders as subtle StatusDot instead of
  saturated Tag, time uses platform `useActivityTime` (relative <24h,
  absolute date otherwise)
- Grouping: defaults to byTime; adds byProject + flat options matching
  the sidebar accordion modes; section titles in normal case
- Toolbar: status chips become a single Segmented control; Trigger
  dropdown items get icons (Chat/API/Scheduled/Eval); default trigger
  filter = ['chat'] so cron/api/eval noise hides by default
- List view: grid-template `minmax(0, 1fr)` + per-cell `min-width: 0`
  so long titles ellipsize instead of pushing other columns
- Layout: content max-width 1440, centered; grid `minmax(min(280px,
  100%), 1fr)` wraps cleanly when the agent sidebar expands
- Infinite scroll: IntersectionObserver sentinel + `loadMoreTopics`,
  PAGE_SIZE 30, shimmer text via `shinyTextStyles`
- BulkActionBar: floating pill at bottom-center (position: fixed,
  pointer-events isolated), ActionIcon buttons instead of full Buttons
- i18n: `management.*` namespace fleshed out across en/zh; zh "活跃"
  for active status (not "进行中")
- Backend: `topic.getTopics` SELECT now includes `description`;
  `ChatTopic` type adds `description?: string | null`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(agent-topics): bulk actions, stats columns, sticky header, list polish

Second iteration on PR #15207:

Backend (`topic.getTopics`)
- SELECT now returns `firstUserMessage` (correlated subquery, indexed via
  `messages_topic_id_idx`), `messageCount`, and `trigger`
- Mock `tokenUsage` / `cost` via `hashtext(topic.id)` so values are stable
  across refetches but look varied; will be replaced once real aggregation
  lands
- `ChatTopic` type adds matching optional fields

Page
- `ToolbarActions` (⋯ menu next to Sort): one-click "Archive topics
  inactive for 3+ months" (client-side iterate → `updateTopicStatus →
  completed`, with confirm and noneFound/done toasts), and an
  "Auto-generate summaries" entry stubbed to a Coming Soon toast until a
  topic-summary endpoint exists
- Status Segmented: drop `archived` and `favorite` (favorite isn't a
  status — keep the star indicator on the card/list instead); add
  `running` as its own slot
- `matchesTrigger` detects cron-spawned topics via `metadata.cronJobId`
  when `trigger` is null, so Daily Brief style data doesn't leak into the
  default Chat filter
- `clearFilters` resets to All instead of Active so users can confirm an
  empty result really is empty across the whole dataset
- Infinite-scroll: `IntersectionObserver` now uses the scroll container
  as `root` (was viewport — broken inside a nested scroller); sentinel +
  shimmer text rendered only when topics are actually present

Card
- Preview fallback chain `description → historySummary → firstUserMessage`
- Footer shows `messageCount` / `tokenUsage` (formatTokenNumber) / `cost`
  (formatPrice) alongside the activity time

List view
- Sticky header (`position: sticky; inset-block-start: 0`) with opaque
  `colorBgElevated` so scrolled rows don't bleed through
- "Select all" checkbox in header with indeterminate state; auto-enters
  selectMode on first activation
- Trigger column localized via `t('management.filters.trigger.*')`;
  Updated column right-aligned
- Grid template back to 6 columns (favorite star is now inline before
  the title)

Sidebar
- The Topic accordion's "Load more" entry (`FlatMode` + `GroupedAccordion`)
  now navigates to `/agent/:aid/topics` instead of opening the legacy
  `AllTopicsDrawer`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(agent-topics): infinite scroll, status counts, task trigger filter

- Per-agent paged fetch via new agentTopicsViewMap (action + selectors + initial state) with `withDetails` opt-in for card columns
- Toolbar status segmented control surfaces live counts; trigger filter switches `cron` → `task` (matches TaskRunnerService output) with ListTodo icon

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(agent-topic-manager): rename folder, swap to LobeUI Checkbox

- Rename `AgentTopics` → `AgentTopicManager` (folder, displayNames, route import)
- Replace hand-rolled card checkbox with `@lobehub/ui` Checkbox (size 18, lighter border via colorBorder); list view also uses `@lobehub/ui` instead of antd
- Fix topic.query withDetails correlated subqueries: qualify column refs so `topic_id = topics.id` resolves correctly (drizzle `${table.col}` renders unqualified — previously matched against messages.id). Add covering tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🔧 chore(topic-query): drop mock cost/tokenUsage from withDetails, polish card

- topic.ts: stop emitting hashtext-mocked `cost` / `tokenUsage` in the
  `withDetails` branch — they need a real schema migration before they
  can be backed by actual numbers. Real aggregates (firstUserMessage,
  messageCount) and existing columns (description, trigger) still come
  back as before.
- Update test + JSDoc to match. The card already gracefully drops the
  cost row via `cost > 0` since the field is now undefined.
- TopicCard: drop the redundant `$` text before `formatPrice` — the
  CircleDollarSign icon already conveys the currency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🙈 hide(agent-topic-manager): hide auto-summarize entry until migration lands

The auto-summarize menu item depends on the same schema migration that
gates cost / tokenUsage in the topic.query withDetails path. Drop it
from the ToolbarActions dropdown for now; i18n keys stay in place so
re-enabling is just adding the item back.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  test(agent-sidebar-nav): add MessagesSquareIcon to lucide-react mock

Nav.tsx now renders the agent-topic-manager entry via `MessagesSquareIcon`;
the test mock listed only the previous three icons, so the component
threw on render.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:07:28 +08:00
Innei cce2741de3 🐛 fix(chat-input): disable automatic URL highlighting (#15219) 2026-05-26 01:54:09 +08:00
YuTengjing 362d137a2b 🐛 fix(model-runtime): preserve usage cost in custom streams (#15218) 2026-05-26 01:13:39 +08:00
Innei 6859ee2374 feat(page-agent): preview initPage streaming arguments (#15039) 2026-05-26 01:08:08 +08:00
Arvin Xu d6e641b790 🐛 fix(model-runtime): capture useful errorCode from generateObject failures (#15209)
The catch in ModelRuntime.generateObject only read `error.code`, but
neither lobehub's structured ChatCompletionErrorPayload nor Vercel
AI SDK errors expose that field — provider wrappers set `errorType`
(InvalidProviderAPIKey / ModelNotFound / ExceededContextWindow / …)
and AI SDK errors set `name` (AI_TypeValidationError /
AI_NoObjectGeneratedError / AI_RateLimitError / …). As a result every
tracing row landed with `error_code = null`, displayed downstream as
"unknown" and defeating the error-type classifier in dashboards.

Walk the chain `errorType → code → name → constructor.name` so the
most descriptive identifier wins. Add three test cases covering each
branch.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 21:28:49 +08:00
Innei 2ee53bcd60 ⬆️ chore(deps): bump @lobehub/ui to 5.15.1 (#15214)
5.15.1 adds `&[data-has-header] { padding-block-start: 0 }` and
`&[data-has-footer] { padding-block-end: 0 }` on the menu popup, so the
4px block padding the slot content used to bleed into no longer exists.
Drop the `margin-block-*: -4px` compensations on the Plus menu's tools
search box, stats footer, and knowledge "view more" button to avoid
content being clipped by the popup's `overflow: hidden`.
2026-05-25 21:11:48 +08:00
Innei 8b96d14347 💄 style(explorer-tree): align file icons with folder icons (#15205)
Drop the `compact` density override on the two PierreFileTree consumers
(DocumentExplorerTree, WorkingSidebar Files) so rows breathe like the
SkillsList. Reserve a chevron-sized slot on file rows when the tree
contains any folder so file icons line up with the folder glyph, mirroring
SkillsList's `reserveChevronSlot`.

Pierre's `unsafeCSS` is captured at FileTree construction with no public
setter, so the offset is driven by a CSS custom property the wrapper sets
inline. Custom properties cascade through the shadow DOM, so toggling the
flag when the last folder is deleted reflows the offset live.
2026-05-25 19:58:29 +08:00
Arvin Xu 248d6ecf76 feat(observability): instrument Agent Runtime with OTel GenAI semantic conventions (#15123)
*  feat(observability): add Agent Runtime OTel spans per GenAI semantic conventions

Introduces a new `@lobechat/observability-otel/modules/agent-runtime` module
with `gen_ai.*` attribute helpers (aligned with OTel GenAI semconv v1.41) and
LobeHub-specific `lobehub.*` extensions, then instruments the core execution
path with four span types:

- `invoke_agent {agent.name}` around `AgentRuntimeService.executeStep`,
  carrying `gen_ai.agent.*`, `gen_ai.conversation.id`, accumulated token
  usage and `lobehub.agent.completion_reason`.
- `chat {model}` around the LLM call in `RuntimeExecutors.call_llm`,
  including `gen_ai.response.time_to_first_chunk` captured on the first
  text/reasoning chunk, finish reasons, and per-call token breakdown.
- `execute_tool {tool.name}` per tool call in both `call_tool` and the
  concurrent `call_tools_batch`, with `gen_ai.tool.type` mapped from
  LobeHub `ToolSource` and `lobehub.tool.success` / `lobehub.tool.attempts`.
- `context_engineering` around `serverMessagesEngine` invocations, with
  message/token/knowledge/memory/tool-count metadata.

Spans are no-ops when OTEL is not initialized (the `@opentelemetry/api`
default provider), so runs without `ENABLE_TELEMETRY` keep their previous
cost profile.

Refs LOBE-5594.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(observability): align agent runtime GenAI attributes

* test(agent-runtime): stabilize agent signal hook integration

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:43:46 +08:00
LiJian d4e8d6df6e 🐛 fix: desktop device hetero task — correct notify URL, auth header, child env (#15206)
🐛 fix: desktop hetero task notify — correct URL, auth header, and child env

Three bugs prevented openclaw results from reaching the UI when dispatched
via the desktop device (vs. the CLI which worked):

1. `sendNotify` posted to `/trpc/agentNotify.notify` — missing `/lambda/`
   segment, causing every done/error signal to hit a 404.
2. `sendNotify` sent `Authorization: Bearer <token>`; the lambda tRPC context
   only recognises `Oidc-Auth` (and `X-API-Key`), so every call was UNAUTHORIZED.
3. Spawned openclaw/hermes processes inherited bare `process.env` with no
   credentials, so `lh notify` inside the child had no auth to call back.

Fix: inject `LOBEHUB_JWT` + `LOBEHUB_SERVER` into child env from desktop's
stored credentials, and use the correct `/trpc/lambda/` URL + `Oidc-Auth`
header (matching what the CLI does).

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 18:34:42 +08:00
Innei 4c6a3999c1 🐛 fix(agent): hold per-agent working directory in store (#15204)
Previously `getAgentWorkingDirectoryById` read directly from localStorage
and `updateAgentRuntimeEnvConfigById` wrote via `setLocalAgentWorkingDirectory`
without going through zustand's `set`. With no store mutation, subscribers
were never notified, so components that re-render only via store updates
(e.g. AgentWorkingSidebar's Files tab) kept showing stale data while the
picker itself appeared to work — the popover close re-rendered the bar,
masking the bug.

Hold the per-agent working directory in `localAgentWorkingDirectoryMap`
on the agent store (hydrated from localStorage at init). Writes now go
through `#set` in addition to localStorage, so all subscribers see the
change. Selectors read from the store map.
2026-05-25 18:19:43 +08:00
Arvin Xu 506b96af64 🐛 fix(agent-runtime): strip heavy fields off finalState in stream events (#15190)
🐛 fix(agent-runtime): strip heavy fields off finalState in stream events (LOBE-9544)

Long topics with `compressedGroup` envelopes can serialize a full
`AgentState.messages` array that exceeds Upstash Redis's 10 MB single-
request limit on xadd, crashing `agent_runtime_stream:<opId>` writes
and surfacing as a misleading watchdog "Operation idle" timeout on
the gateway side.

LOBE-9110 already removed `contextEngine.input` + `toolsetBaseline`
from the state blob. `messages` (especially compressedGroup envelopes
that preserve full original-message arrays alongside the LLM summary)
is the remaining size driver. A diagnosed case (op_177967426) was
20 MB, of which 15 MB lived in 3 compressedGroup envelopes holding
752 raw messages.

Approach: centralize the strip at the `publishStreamEvent` chokepoint.
Every stream-event publish in the runtime — `publishAgentRuntimeEnd`,
the per-step `step_complete` in `AgentRuntimeService.executeStep`, the
two terminal `step_complete` sites in `RuntimeExecutors` — flows
through this single method. Putting the strip there means call sites
stay dumb and any future direct user of `publishStreamEvent` gets the
size protection automatically.

The same strip is mirrored in `InMemoryStreamEventManager.publishStreamEvent`
(test-mode parity) and `GatewayStreamNotifier.pushEvent` (gateway WS
push channel — separate HTTP POST that would otherwise re-introduce
the same multi-megabyte serialization).

Fields stripped (mirrors OperationTraceRecorder's `done`-event strip
from LOBE-9110, kept in sync intentionally):

- `messages` — canonical copy lives in DB rows / in-memory state;
  in-process consumers (e.g. `execSubAgentTask.onComplete`) receive
  the full state via the local `HookContext` channel, not via the
  stream
- `operationToolSet`, `toolManifestMap`, `toolSourceMap`, `tools`
  — operation-level snapshot already covered by LOBE-9110

`finalState` itself stays in the payload so existing consumers that
read lightweight fields (`status`, `cost`, `usage`, `error`, …) keep
working. Verified no consumer reads the stripped fields off the
wire — `gatewayEventHandler` only reads `reason` + `uiMessages`,
`runAgent.ts` reads `finalState.status` which survives the strip,
CLI / agent-gateway-client / hetero adapters / agent-mock have no
`finalState` references at all.

Tests:
- New `publishAgentRuntimeEnd` integration test with a fat finalState
  asserts heavy fields stripped + lightweight fields preserved +
  `reasonDetail` derivation still sees the un-stripped error message
- New `stripFinalStateInEventData` unit tests cover the helper
  contract (no-op when absent / falsy, strips correctly, defensive
  on non-object input)
- Existing tests pass unchanged — their mock `finalState` objects
  don't carry `messages`, so the strip is a no-op for them, which
  is exactly the chokepoint contract: invisible to callers that
  don't pass heavy state

306 tests pass (StreamEventManager / InMemoryStreamEventManager /
GatewayStreamNotifier / RuntimeExecutors / AgentRuntimeService /
AgentRuntimeCoordinator / runAgent / gatewayEventHandler).

Follow-up (out of scope): catch the xadd 500 inside the DO and
publish an `op_crashed_redis_overflow` event so the gateway surfaces
"state payload exceeded" instead of the misleading watchdog idle
timeout.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 18:00:47 +08:00
LiJian 924ae8bf1f 🐛 fix: pass assistantMessageId through sandbox env to eliminate heteroIngest race (#15197)
* 🐛 fix: pass assistantMessageId through sandbox env to eliminate heteroIngest race

Before this change, `HeterogeneousPersistenceHandler.loadOrCreateState` always
read `topic.metadata.runningOperation` from the DB to obtain `assistantMessageId`.
On Vercel serverless, the first `heteroIngest` batch could arrive on a cold Lambda
that read from a replica before the orchestrator's `updateMetadata` write was
visible, causing a hard throw and BatchIngester exhausting all 5 retries — leaving
the assistant message stuck as LOADING_FLAT with no user feedback.

Fix: orchestrator passes `assistantMessageId` via `LOBEHUB_ASSISTANT_MESSAGE_ID`
env var → CLI → `TrpcIngestSink` → `heteroIngest` payload → `loadOrCreateState`.
When present, the DB lookup is skipped entirely for state initialisation, matching
the frontend `createGatewayEventHandler` pattern which always receives
`assistantMessageId` in-memory before any events are processed.

The `topic.metadata` DB read is kept as a fallback for desktop/old-CLI callers
that do not send the field, and is still needed to restore `heteroCurrentMsgId`
for mid-conversation cold-start reconstruction on step boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-agent): snapshot text ingests and ignore stale batches

* chore: publish the cli to 0.0.21

* 🐛 fix(hetero-agent): validate seeded assistant binding

* fix: fixed the little types error

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:43:02 +08:00
Innei 302755057e ️ perf(vite): prewarm more route chunks (#15142) 2026-05-25 16:48:55 +08:00
Innei eea9464b04 🌐 fix(locale): add missing follow-up i18n keys (#15201)
🌐 fix(locale): add missing follow-up i18n keys for zh-CN and en-US
2026-05-25 16:45:02 +08:00
Arvin Xu 82cc885394 🐛 fix(llm-generation-tracing): backfill task_brief / task_brief_judge scenario (#15198)
🐛 fix(llm-generation-tracing): backfill task_brief/task_brief_judge scenario

Brief generation and judge call sites only set `metadata.trigger`, so the
tracing hook fell back to `scenario='unknown'` for every row. Surfaced via
the unknown-scenario cleanup pass: 433 task-brief + 26 task-brief-judge
rows landed in unknown, alongside 434 task-handoff rows that still used
the dashed trigger string.

- Add `task_brief` and `task_brief_judge` to `TRACING_SCENARIOS`
- Add `_PROMPT_VERSION` + `_SCHEMA_NAME` constants for both brief chains,
  matching the existing `TASK_TOPIC_HANDOFF_*` convention
- Wire explicit `tracing: { promptVersion, scenario, schemaName }` at all
  three task-lifecycle generateObject call sites
- Normalize `metadata.trigger` to underscored ids
  (`task_handoff` / `task_brief` / `task_brief_judge`) to match the
  `RequestTrigger` enum convention
2026-05-25 16:40:37 +08:00
Arvin Xu e4ad195df9 🐛 fix: silence Turbopack project-wide glob warning (#15194)
`path.join(this.root, sub)` still tripped Turbopack's static file-pattern
analyzer because `safeSegment`'s `|| 'unknown'` fallback gave the analyzer
a finite alternation, fanning out into a project-wide glob that matched
11k+ files at build time. Hand-roll the join with `path.sep` so the
analyzer can't see it as a path pattern; output is byte-identical to
`path.join` on both Unix and Windows.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:36:40 +08:00
LiJian 47b6f3503a feat(hermes): implement hermes agent chain logic (#15189)
*  feat(hermes): implement hermes agent chain logic

Replace the broken HTTP gateway approach with direct CLI spawn (matching
openclaw's architecture). Hermes chat -q --quiet outputs session_id +
response to stdout — we capture it and relay via sendAutoNotify/sendDoneSignal,
no buildNotifyProtocol injection needed.

- heteroTask (CLI): spawn hermes chat -q --quiet [--resume <id>], capture
  stdout, persist session_id to ~/.lobehub/hermes-sessions.json per topicId,
  kill concurrent same-topic tasks by PID before spawning
- GatewayConnectionCtr (desktop): mirror CLI logic, store hermes session IDs
  in-memory hermesSessionMap, remove unused HTTP gateway helpers
- getAgentProfile: implement hermes profile fetch via `hermes profile list`
  + SOUL.md description parsing
- checkPlatformCapability: fix hermes check to use `hermes --version`
  instead of non-existent HTTP /health endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hermes): fix CodeQL incomplete sanitization and tilde path expansion

- readHermesSoulDescription: loop comment-stripping regex until stable to
  prevent residual `<!--` from malformed/nested sequences (CodeQL High)
- getHermesProfilePath: expand leading `~` via os.homedir() before fs.join
  in case hermes profile show returns a tilde-prefixed path (CLI + desktop)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hermes): strip residual angle brackets to satisfy CodeQL sanitization check

After stripping HTML comments, add .replaceAll(/[<>]/g, '') inside the
loop to ensure no partial `<!--` delimiters survive, resolving the CodeQL
'Incomplete multi-character sanitization' High warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(platform-agent): enable Hermes + add Amp/OpenCode as coming-soon platforms

- Enable Hermes in CreatePlatformAgent (remove from COMING_SOON_PLATFORMS)
- Add Amp and OpenCode to REMOTE_HETEROGENEOUS_AGENT_CONFIGS (coming-soon disabled)
- Extend RemoteHeterogeneousAgentType and HeterogeneousProviderConfig.type
- Use isRemoteHeterogeneousType() in HeterogeneousAgentStatusCard to future-proof
  remote agent detection guard (removes hardcoded openclaw/hermes check)
- Export isRemoteHeterogeneousType from heterogeneous-agents/client entrypoint
- Broaden agentType to string in device-gateway-client (AgentRunRequestMessage,
  dispatchAgentRun) so new remote types pass without package updates
- Add i18n keys for amp/opencode platform descriptions (en-US, zh-CN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(test): add isRemoteHeterogeneousType to heterogeneous-agents/client mock

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 14:09:21 +08:00
YuTengjing bb4924fc5b 🐛 fix(image): explain text-only image responses (#15181) 2026-05-25 14:05:43 +08:00
Arvin Xu 46f884d5ed chore(llm-generation-tracing): pre-allocate tracingId + recordFeedback router (#15146)
*  feat(llm-generation-tracing): pre-allocate tracingId + recordFeedback router

Wire up the per-call feedback loop foundation.

1. **Pre-allocate tracingId (plan A2)**
   - `TracingOptions.tracingId?: string` — optional caller-supplied UUID.
   - `LLMGenerationTracingService.record` generates one via `randomUUID()`
     when the caller doesn't supply one, so the id is always known
     before DB insert.
   - `LlmGenerationTracingModel.record` accepts an optional `id` and
     forwards it to the insert (Drizzle still autogens when omitted).
   - `aiChat.outputJSON` allocates the id up-front, threads it through
     `tracing.tracingId`, and returns `{ data, tracingId }` so the
     client can wire feedback against the id even though
     `service.record` runs inside Next's `after()`.
   - `aiChatService.generateJSON` consumers (InputEditor, supervisor)
     unwrap the envelope.

2. **New `llmGenerationTracingRouter.recordFeedback`**
   - Scenario-agnostic feedback endpoint at `lambda.llmGenerationTracing`.
   - Validates `{ tracingId (uuid), signal (positive|negative|neutral),
     source, score?, data? }` and forwards to
     `LLMGenerationTracingService.recordFeedback`.

Follow-up issues already filed:
- LOBE-9488 — `@lobehub/editor` AutoCompletePlugin needs
  `onAccept`/`onReject`/`onCancel` callbacks before the client side can
  capture Tab/Esc/keep-typing signals against the returned tracingId.
- LOBE-9489 — session-level signal modeling (multi-suggestion typing
  sessions) — deferred until per-row feedback data lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(llm-generation-tracing): surface feedback write failures instead of silent ok

The recordFeedback mutation used to always return `{ ok: true }` even when
the underlying write was silently dropped — `LLMGenerationTracingService`
swallowed both DB-init/update throws and the no-op case where the WHERE
clause (id + userId) matched zero rows. Callers couldn't tell
"persisted" from "lost", which would skew tracing-feedback metrics and
prevent reasoned retry/error handling.

Fix:

- `LlmGenerationTracingModel.updateFeedback` now returns
  `{ updated: boolean }` (via `.returning({ id })`), so the caller knows
  whether the WHERE clause actually matched a row.
- `LLMGenerationTracingService.recordFeedback` throws a typed
  `LLMGenerationFeedbackError` with `kind: 'not_found' | 'db_failure'`
  instead of swallowing — stops logging-only behaviour for DB errors and
  promotes the 0-rows case to an explicit signal.
- `llmGenerationTracingRouter.recordFeedback` catches that error and
  translates to `TRPCError({ code: 'NOT_FOUND' })` for stale-id and
  `INTERNAL_SERVER_ERROR` for DB outages — `{ ok: true }` only flows
  back when a row was actually patched.

Tests:
- Model: assert `{ updated: true/false }` for happy / cross-user / missing-id
- Service: assert throws on both not_found scenarios
- Router: assert TRPCError code translation for both error kinds

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(input-completion): wire Tab/Esc/typing feedback to recordFeedback

- bump @lobehub/editor to ^4.12.0 for AutoComplete onSuggestion{Accepted,Rejected}
- add llmGenerationTracingService wrapping lambda.llmGenerationTracing.recordFeedback
- InputEditor: map suggestionId→tracingId, fire positive on accept, negative on
  esc, neutral on typing/cursor-move/blur/other; recode IME-driven escape as
  neutral/autocomplete_ime so CJK input doesn't poison the signal

Closes LOBE-9488

* ♻️ refactor(input-completion): fold recordTracingFeedback into aiChatService

Single trpc mutation didn't warrant a dedicated service file; aiChatService
already owns the paired `outputJSON` call that mints the tracingId, so
recordTracingFeedback belongs alongside it.

* 💄 style(llm-generation-tracing): tag task-handoff scenario + prompt version (#15191)

* 💄 style(QueueTray): use borderless variant for queued file preview

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(llm-generation-tracing): tag task-handoff scenario + prompt version

Task topic handoff was tracing as scenario=unknown / promptVersion=v0 because the
generateObject call only set metadata.trigger and that trigger isn't in the
registry. Add a TaskHandoff scenario const, version the prompt next to its
definition, and pass tracing options explicitly at the call site (mirroring
followUpAction).

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(llm-generation-tracing): validate caller-supplied tracingId as UUID

The `outputJSON` route echoed `tracing.tracingId` back to clients without
checking the shape. Because the surrounding `tracing` record is free-form,
a malformed value passed request validation, then failed DB insertion on
the uuid PK and was later rejected by `recordFeedback` (`z.string().uuid()`),
so callers could receive a tracingId unusable for the feedback flow.

Tighten `StructureOutputSchema.tracing` to a `z.object({ tracingId: uuid }).catchall(unknown)`
so the validation happens at the request boundary; the route can then drop
the redundant `typeof === 'string'` guard.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:59:28 +08:00
Arvin Xu 0fcc21895e 🧹 chore(skills): audit pass — normalize, dedupe, and fix project-overview (#15193)
* 🧹 chore(skills): consolidate, normalize, and add audit skill

Findings from the first skills audit on the 36 project-local skills:

- `source-command-dedupe` was a verbatim duplicate of the global `dedupe` skill (same description, same procedure). Deleted.
- `data-fetching` only covered the pipeline (Service + Zustand Store + SWR),
  not Zustand itself. Renamed to `data-fetching-architecture` so the scope
  is clear next to the standalone `zustand` skill. Cross-ref in
  `store-data-structures` updated.
- 9 skills had inconsistent description format (numbered lists, missing
  `Triggers on`, `MUST use when` opener, `Triggers:` colon vs `Triggers on`,
  etc). Normalized to the template:
  `{Topic + key conventions}. Use when {scenarios}. Triggers on {symbols, phrases, 中文}.`
  Skills touched: docs-changelog, pr, project-overview, react, review-checklist,
  spa-routes, chat-sdk, upstash-workflow, store-data-structures.
  User-invoked-only skills (`disable-model-invocation: true`) intentionally
  skipped — they don't need trigger keywords.

Adds a new `skills-audit` skill that codifies the weekly check (inventory,
overlap detection, description-template validation, stale-skill check,
cross-reference integrity) so future audits don't have to re-derive the
process.

Skill count: 36 → 36 (-1 deleted, +1 added).

* 📝 docs(skills): rewrite project-overview from open-source repo perspective

The skill previously described the private cloud repo (cloud root + `lobehub/`
submodule + override mechanism), which doesn't apply here — this is the
open-source root. Rewrite the directory map and description for the flat
`apps/` + `packages/@lobechat/*` + `src/` layout, and append a Cloud Repo
note explaining how the cloud SaaS repo mounts this as a submodule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:32:02 +08:00
Arvin Xu 3c52998157 feat(hetero-agent): execution target switcher in composer (#15179)
*  feat(hetero-agent): add execution target switcher in composer

Add a chip in the chat composer toolbar that lets users pick where a
heterogeneous agent (claude-code / codex) executes: on this desktop, in
a cloud sandbox, or on an `lh connect` remote device. Persists the
choice via a new `agencyConfig.executionTarget` field paired with the
existing `boundDeviceId`. Server dispatch wiring will land separately.

* 🐛 fix(hetero-agent): mount execution target switcher in hetero composer

The hetero `ChatInput` replaces `RuntimeConfig` with `WorkingDirectoryBar`
via `runtimeConfigSlot`, so the new chip added in the previous commit
was never reached for hetero agents. Mount `HeteroDeviceSwitcher` in
`WorkingDirectoryBar` directly (both desktop and web branches).

* 💄 style(hetero-agent): polish execution target popover

- Drop uppercase + letter-spacing from section titles for normal sentence case
- Add a green status dot next to "Online" on device rows
- Rename "Remote devices (lh connect)" to "Other devices" with a clarifying
  subtitle so it covers both desktop-app and `lh connect` machines

* 💄 style(hetero-agent): use OS-specific icons for devices

Replace the generic bot avatar in device rows (and the chip) with the
machine's actual OS icon — Apple for darwin, Linux for linux, Microsoft
for win32, generic monitor as fallback. Matches the same icon set
already used in MCP plugin deployment.

* 💄 style(hetero-agent): unify execution targets into a single list

- Flatten This device / Cloud sandbox / remote devices into one list
- Add an info ⓘ icon in the popover header explaining when to pick a
  remote device vs This device; drop the inline section description
- Remove the "Other devices" rename and keep the original "Remote
  devices" terminology in the empty hint

* 💄 style(hetero-agent): rename popover title to Execution Device

* 💄 style(agent-signal): refine skill receipt card with self-evolution copy

- Render SkillsIcon for skill receipts and let PortalResourceCard accept a ReactNode icon
- Square 64x64 avatar, 12px corner radius, larger icon, drop the RadioTower marker
- Move the receipt card below the Usage row so it reads as metadata, not body content
- Reword the skill receipt to convey self-evolution ("Auto-learned a new skill" / "已自动习得新技能")

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(hetero-agent): keep working-directory controls in RuntimeConfig

Revert the early-return I added in `RuntimeConfig.rightContent` for
heterogeneous agents. Hetero agents are mounted via `HeterogeneousChatInput`
which already replaces `RuntimeConfig` with `WorkingDirectoryBar` (where
the `HeteroDeviceSwitcher` lives), so the branch here was dead code — but
it dropped the `!isDesktop` gate and would have skipped the desktop
working-directory picker for any edge case that still falls through this
path (popup/share/embed). Restore the original web-only condition.

*  feat(hetero-agent): fork dispatch by executionTarget for local CLI hetero

Local CLI hetero (claude-code / codex) used to dispatch to a device only
when topic.metadata.boundDeviceId was set, otherwise always spawned a
cloud sandbox — ignoring agencyConfig.executionTarget entirely.

Now resolve in this order:
  1. requestedDeviceId (topic-level override) → device dispatch, always wins
  2. agencyConfig.executionTarget = 'device' → dispatch to boundDeviceId;
     error out if no device is bound (no silent sandbox fallback, since
     the user explicitly chose this mode)
  3. otherwise (sandbox / local / unset) → cloud sandbox

'local' mode falls back to sandbox on the server since in-process spawn
only makes sense inside the Electron client; that path is owned by the
desktop and doesn't reach this code today.

*  feat(hetero-agent): route runtime by executionTarget for local CLI hetero

Frontend complement to the previous server-side dispatch fork. Without
this change the chip's choice on desktop was a no-op: selectRuntimeType
hard-routed local CLI hetero to 'hetero' (desktop IPC) whenever
isDesktop, bypassing the server entirely — so 'device' / 'sandbox' picks
never reached the new server-side fork.

Now selectRuntimeType reads agencyConfig.executionTarget:
  - 'device'  → 'gateway' (server dispatches to bound lh connect device)
  - 'sandbox' → 'gateway' (server spawns cloud sandbox)
  - 'local'   → 'hetero' on desktop, 'gateway' on web (fallback)
  - unset     → legacy default (desktop = hetero, web = gateway)

All four runtime-selection call sites pass executionTarget through; the
non-hetero sub-agent dispatcher is unaffected since heteroProvider is
always undefined there.

*  feat(chat-input): add Advanced Parameters entry to Plus menu

- New menu item toggles the right working sidebar's params tab, mirroring the agent header's ParamsPanelToggle
- Simplify the format-toolbar item label to a fixed "Show formatting toolbar" with a checkmark indicating active state
- Widen the active-label gap so the checkmark sits comfortably away from the text

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🚩 feat(hetero-agent): gate execution-device switcher behind a lab flag

Add `enableExecutionDeviceSwitcher` to UserLabSchema (default off) and gate the heterogeneous WorkingDirectoryBar's HeteroDeviceSwitcher on it, so the new switcher can ship to canary without exposing it to all users until ready. Expose the toggle in Settings → Advanced → Labs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:50:41 +08:00
Arvin Xu 8d4c48749f feat(agent-signal): add execAgent plumbing for self-iteration migration (#15187)
Phase 1 of LOBE-9434: introduces dormant plumbing for converging
agent execution onto execAgent. No behavior changes for any existing
caller — every piece is a no-op until later phases wire it up.

- Add `ExecAgentAppContext.suppressSignal` flag and `sourceMessageId`
- Add `shouldSuppressSignal` helper; gate the `agent.user.message`
  re-emission in `aiAgent.execAgent` so future builtin/background runs
  cannot recurse into the analyzeIntent pipeline
- Register `self-iteration` builtin agent + `SELF_ITERATION_AGENT_SLUGS`
- Add `finalStateExtractor` (`extractFromFinalState` /
  `extractMutations` / `extractArtifacts`) for reading tool-result kind
  partitions off a persisted AgentState snapshot
- Register a no-op `completionPolicy` listener on
  `agent.execution.completed` with an optional
  `onSelfIterationCompleted` callback (undefined by default)

Tests: 17 new unit tests across suppressSignal, finalStateExtractor,
and completionPolicy.
2026-05-25 11:40:23 +08:00
Arvin Xu 26aa28c263 chore: clean up LOBE-XXX code annotations (2026-05-25) (#15182)
chore: clean up LOBE-XXX code annotations

- Removed LOBE-9501 markers (assistantGroup clobber fix — gateway
  pushes UIChatMessage snapshot as SoT at step boundaries)
- Removed LOBE-9523 markers (mid-stream cancel fix — skip uiMessages
  for interrupted status; partial-finalize accumulated content in
  executor catch block)
- Removed LOBE-9378 markers (local-system template variable injection
  — unified activeDeviceId resolution for regular chat)
- Preserved all descriptive comments; only removed issue ID tokens
- No behavior changes

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
2026-05-25 10:43:43 +08:00
Neko f3d5d03cf5 ♻️ refactor(userMemories): support resolving agent config from ServiceModel (#15138)
* ♻️ refactor(userMemories): support resolving agent config from ServiceModel

* ♻️ refactor(userMemories): share memory analysis service model
2026-05-25 04:06:50 +08:00
YuTengjing d71686ba88 🐛 fix: normalize image MIME from bytes (#15172) 2026-05-25 00:32:55 +08:00
Arvin Xu f16c280e93 🐛 fix(agent): surface projectSkills regardless of activeDeviceId (#15177)
The merge gate in execAgent silently dropped client-provided
projectSkills whenever activeDeviceId couldn't be resolved
(multi-device-no-bind, bound-device-offline, disableTools=true, no
DEVICE_GATEWAY_URL). The client having scanned `.agents/skills` /
`.claude/skills` and sent them up is itself proof that a device is
reachable now — gating availability on a multi-device-routing decision
conflated two concerns and produced "I sent skills but the model never
sees them" with no log to diagnose.

Drop the activeDeviceId precondition so projectSkills always populate
`<available_skills>`. Whether the readFile can actually resolve at
activation time stays gated at `serverRuntimes/skills.ts`, where a
missing `deviceFileAccess` naturally fails `activateSkill` instead of
silently hiding the option.

Also add a one-line merge log so future "why didn't my skill show up"
investigations land on the answer immediately.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 00:20:59 +08:00
YuTengjing be62847e00 🐛 fix: support Bedrock structured generation (#15174) 2026-05-25 00:15:50 +08:00
Arvin Xu a8faccff66 🐛 fix(agent-runtime): preserve streamed content across mid-stream cancel (#15173)
* 🐛 fix(agent-runtime): preserve streamed content across mid-stream cancel

LOBE-9523

Mid-stream STOP currently collapses the in-memory streamed assistant
content back to the LOADING_FLAT placeholder (cLen 5182 → 3 observed in
the agent-gateway probe dump at `.agent-gateway/caseD-prerefresh-…json`),
and a subsequent reload returns the same placeholder from DB so the
content is **permanently lost**.

Root cause (matrix-tested via Electron + probe, see updated LOBE-9523
description): when the user clicks STOP, `interruptOperation` flips
state.status to 'interrupted' and `coordinator.saveAgentState` publishes
`agent_runtime_end` carrying the `uiMessages` snapshot. The executor's
post-stream finalize at `RuntimeExecutors.call_llm:1078` hasn't run yet,
so the assistant row is still the empty placeholder — that placeholder
gets pushed to the client as SoT and clobbers the streamed content.

Three coordinated fixes:

1. **Executor partial-finalize on interrupt** (`RuntimeExecutors.ts`
   inner catch). When `isOperationInterrupted` is true AND the
   `onText`/`onThinking`/`onToolsCalling` callbacks accumulated partial
   content, do an extra `messageModel.update` before rethrowing. This
   makes the DB row carry the real partial content, so a later reload
   shows the streamed answer instead of an empty placeholder.

2. **Coordinator skips uiMessages on interrupted** (`AgentRuntimeCoordinator.ts`
   `resolveUiMessages`). Short-circuit when `state.status === 'interrupted'`
   so the agent_runtime_end payload omits `uiMessages` entirely. The
   executor's partial-finalize update from (1) is racy with this publish
   path — leaving the field undefined lets the client preserve its
   in-memory state instead of pulling whatever's in DB at publish time.

3. **Client skips DB refetch on `reason='interrupted'`** (`gatewayEventHandler.ts`
   agent_runtime_end case). The existing fallback at L540 does a
   `fetchAndReplaceMessages` whenever uiMessages is absent, which would
   defeat fix (2) by reading the still-pre-finalize DB row. Add a
   third branch: when reason='interrupted' AND no uiMessages, keep the
   in-memory state — the next explicit refresh (route change, user-driven
   mutate, page reload) will pick up the finalized partial content from
   (1).

Test matrix (5 new tests):
- `RuntimeExecutors`: persists on interrupt-with-content / skips on
  empty-interrupt / skips on non-interrupt error
- `AgentRuntimeCoordinator`: resolver not called on saveAgentState /
  saveStepResult when status='interrupted'
- `gatewayEventHandler`: no refetch + no replaceMessages when reason=
  'interrupted' and uiMessages absent / SoT still consumed when server
  did include uiMessages on an interrupted run (forward-compat)

Manual verification (probe dumps in `.agent-gateway/`):
- Case A/B/C/E (clean stream, mid-stream tab-switch, post-stream
  tab-switch, post-stream reload) all remain  — no regression
- Case D (long stream → STOP) currently shows
  `cLen[gRojDUMG] 5182→3 near-event:[agent_runtime_end]` rollback;
  with this patch the client retains 5182 chars and the DB carries the
  same partial content for reload

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(chat-store): only skip interrupt refetch after stream progressed

Reviewer caught a regression in PR #15173's agent_runtime_end change:
unconditionally skipping the DB fallback when `reason === 'interrupted'`
leaves the optimistic `tmp_*` placeholder messages stuck in the store
when cancel arrives BEFORE any server state landed (no step_start, no
stream_start with server id, no chunks). Previously the fallback
`fetchAndReplaceMessages` cleaned those up by replacing them with the
server-side rows.

Track `hasStreamedContent` in the handler closure and flip it to true on:
- `stream_start` switching to a server-assigned assistant id
- `stream_chunk` dispatching text / reasoning / tools_calling

Gate the interrupted-skip on this flag:
- `hasStreamedContent === true`  → keep in-memory state (mid-stream cancel)
- `hasStreamedContent === false` → fall back to refetch (cancel-before-stream)

New test for the cancel-before-stream path; existing
"NOT refetch when reason=interrupted" test renamed and updated to set up
prior stream activity before sending the cancel.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 00:13:53 +08:00
Arvin Xu 63d8e07453 chore: clean up LOBE-xx comment (2026-05-24) (#15158)
chore: 清理 LOBE-9110 代码注释 (2026-05-24)

- 将 RuntimeExecutors / AgentRuntimeService / OperationTraceRecorder 中的 "See LOBE-9110" 引用替换为实际的架构决策说明
- 补充 context engine payload 脱离 Redis state pipeline 的上下文(Upstash 10MB 限制根因)
- 保留 WelcomeText 中的 /LOBE-\d+/ 正则(功能性代码,用于动态内容自动链接)

Co-authored-by: Arvin Xu <arvinx@lobehub.com>
2026-05-25 00:13:14 +08:00
Arvin Xu 44e69af6cc 🐛 fix(desktop): preview .cjs/.mjs/no-ext files instead of binary fallback (#15168)
* 🐛 fix(desktop): sniff unknown extensions instead of mislabeling as binary

The local file preview pipeline used a hand-maintained extension whitelist
in `apps/desktop/src/main/utils/mime.ts` and fell back to
`application/octet-stream` for anything unmapped. `.cjs`, `.mjs`,
`.editorconfig`, `.lock`, and any other extension not in the table got
classified as binary by the renderer and showed "二进制文件 — 无法预览",
even though the contents were plain text.

Add `resolveLocalFileMimeType(filePath, buffer)`: whitelist hit first for
known source/image extensions; otherwise run `sniffBinaryBuffer` (from
`@lobechat/file-loaders`, already a desktop dep) on the first 8KB.
Text → `text/plain; charset=utf-8`, binary → `application/octet-stream`.
`getExportMimeType` is left untouched for `RendererProtocolManager`
because the bundled-asset extension set there is closed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(desktop): short-circuit known-binary extensions before sniff

The sniff fallback in `resolveLocalFileMimeType` only flags a buffer as
binary on a null byte or >30% non-printable chars in the first 8KB. PDF
files (and many archives/executables/media containers) start with a long
printable-ASCII prefix — header + xref + dictionary for PDF — so the sniff
returns text and the renderer hands the buffer to the text highlighter,
producing garbled output and unnecessary decode cost.

Add a `KNOWN_BINARY_EXTENSIONS` set checked before the sniff. Common
binary formats (PDF, zip/tar/gz/7z, exe/dll/dylib/so/wasm, audio/video,
sqlite, design files) short-circuit to `application/octet-stream`. The
set is intentionally narrow — uncommon binary blobs with early null bytes
still fall through to the sniff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 00:02:40 +08:00
Arvin Xu eedf46a11d ♻️ refactor(agent-runtime): route desktop callers through device-gateway (#15157)
Removes the Phase 6.4 `clientRuntime === 'desktop'` short-circuit so the
desktop UI, web UI, and IM/Bot callers all converge on a single tool
dispatch path: the device-gateway proxy to a registered device. The
Agent Gateway WS-back-to-caller mechanism is deprecated.

This is the second half of LOBE-9378. PR #15087 fixed the IM/Web
single-online-device auto-activate so `deviceSystemInfo` was fetched
and the `<user_context>` Mustache template substituted (`{{hostname}}`,
`{{workingDirectory}}`, `{{homePath}}`). But on cloud canary the desktop
Electron client took the Phase 6.4 branch instead — `lobe-local-system`
was enabled via `hasClientExecutor` and `executor:'client'` was stamped
on the manifest, bypassing both `activeDeviceId` resolution AND
`fetchDeviceSystemInfoForTemplate`. So `state.metadata.deviceSystemInfo`
stayed undefined and the literal `{{workingDirectory}}` reached the LLM
even after the LOBE-9378 fix shipped. With this refactor, the desktop
client registers with device-gateway like the CLI does, gets picked up
by `queryDeviceList`, auto-activates as the single online device, and
the existing template substitution kicks in unchanged.

Changes:
- AgentToolsEngine: drop `hasClientExecutor` / `clientRuntime` param.
  `platform` is now `hasDeviceProxy ? 'desktop' : 'web'`. LocalSystem
  enable rule is the single device-gateway path; RemoteDevice no longer
  has the `!hasClientExecutor` carve-out.
- aiAgent.execAgent: drop `clientRuntime` param. `shouldDispatchToClient`
  collapses to `!gatewayConfigured`, preserving the standalone-Electron
  path where there is no gateway and tools run in-process.
- tRPC input + shared types (`packages/types/src/agentExecution`,
  `src/services/aiAgent.ts`) drop the `clientRuntime` field.
- Store: stop sending `clientRuntime: isDesktop ? 'desktop' : 'web'`.
- Tests: remove the Phase 6.4 describe blocks and the
  `clientRuntime`-forwarding tests; add coverage that local-system /
  stdio MCP `executor` stays unset when the gateway is configured so
  routing goes through Remote Device.
- `executors` doc on builtin tool manifests rewritten to describe the
  remaining standalone path (no more "client dispatched via Agent
  Gateway WS").

The unrelated `clientRuntimeStart` / `clientRuntimeComplete` agent
signal source-types are about run lifecycle events, not request runtime,
and are untouched.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:01:12 +08:00
YuTengjing ff61f4b3fa 💄 style: add Qwen3.7 Max locale (#15150) 2026-05-24 21:49:34 +08:00
Innei 192111840c 💄 style(workflow): normalize block spacing (#15169) 2026-05-24 20:17:30 +08:00
Arvin Xu 837a3daa58 feat(chat): consume gateway uiMessages snapshot as SoT at step boundaries (#15153)
* ♻️ refactor(chat-store): useFetchMessages accepts options object

LOBE-9501

Replace the positional `skipFetch?: boolean` second argument with an
`options?: { skipFetch?, revalidateOnFocus? }` object on both
`useChatStore.useFetchMessages` and `useConversationStore.useFetchMessages`.
Plumb `revalidateOnFocus` through to the underlying SWR config so callers
can suppress focus revalidate per-call (default behaviour unchanged).

Mechanically migrate all 7 call sites to the new shape. No behaviour
change in this commit — the streaming-aware `revalidateOnFocus: false`
follow-up lives in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(chat): consume gateway uiMessages snapshot as SoT at step boundaries

LOBE-9501

Server attaches the canonical UIChatMessage[] snapshot to step_start and
agent_runtime_end events (#15152). The client now uses that pushed payload
as the source of truth instead of refetching from DB:

- step_start handler calls replaceMessages(uiMessages, { context }) when
  the snapshot is present, so the assistant tab-switch / next-step path
  no longer issues a refetch that returns a stale assistant placeholder.
- agent_runtime_end handler does the same for the terminal step — the
  last step has no later step_start to carry a fresh snapshot, so this
  branch is the only one that reconciles the final commit.
- step_complete on phase=tool_execution stops calling refreshMessages.
  That refetch was the direct cause of the assistantGroup→assistant
  clobber regression captured by the agent-gateway probe scripts.
- ChatList disables SWR revalidateOnFocus while the current topic is
  streaming (via operationSelectors.isAgentRuntimeRunningByContext) and
  automatically restores it after the run ends. Tab-focus during a run
  no longer triggers the stale DB read.

Doesn't touch streamingExecutor.ts (homogeneous runtime — parallel path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-store): wire gateway handler to consume server-pushed uiMessages SoT

LOBE-9501

#15152 (server) attaches the canonical UIChatMessage[] snapshot to both
the Redis SSE channel and the gateway /push-event channel. The earlier
client patch wired the consumer into `runAgent.ts`, but that file only
runs on the Group Chat SSE path. The actual gateway entry point
(`createGatewayEventHandler` in `gatewayEventHandler.ts`, used by single
agent, sub-agent, and hetero-CLI flows) ignored the field entirely and
kept refetching from DB.

Fix the gateway handler:

- step_start: consume `event.data.uiMessages` and replaceMessages with
  the pushed SoT. Skipped when absent — hetero adapters don't emit
  step_start at all (HeterogeneousEventType excludes it), so the new
  branch is invisible to hetero.

- agent_runtime_end: same SoT consumption; the existing
  `fetchAndReplaceMessages` becomes the fallback for events without the
  field. Claude Code adapter emits agent_runtime_end with empty data,
  so hetero terminal behavior is preserved by the fallback.

- stream_start: gate the DB fetch on `!newAssistantMessageId`. Native
  gateway streams carry `assistantMessage.id` (the preceding step_start
  also delivered the SoT), so the await is unnecessary — AND it was
  blocking the enqueue chain. Live chunks queued behind that await
  could not dispatch, which manifested as "streaming content never
  lands in messagesMap" during tab-switch and slow-network repros.
  Hetero CLI streams never set `assistantMessage.id`, so the fetch
  still runs for them on every stream_start.

Verified with the agent-gateway probe (separate commit): chunks now
land in real time (cLen grows 3 → 529 monotonically), and tab-switch
mid-stream no longer rolls the streamed assistantGroup back to the
LOADING placeholder (ROLLBACKS=none in the analyzer output).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🧪 chore(local-testing): rewrite agent-gateway probes in TS + add CLI

LOBE-9501

Convert the local-testing agent-gateway probes from .js/.mjs to TypeScript
and add a unified `run.ts` CLI that bundles via Bun.build (no extra
deps) and persists dumps to a gitignored `.agent-gateway/` directory for
use as streaming-replay test fixtures.

- types.ts: shared dump shape (ProbeStreamEvent / ProbeTimelineSample /
  ProbeDump) and `declare global` for the `window.__PROBE_*` surface
- probe-events.ts: WebSocket + fetch interception (gateway WS captures
  any socket with `operationId=`; fetch captures `/api/agent/stream` for
  direct SSE). Per-key timeline samples every 200ms so we can see
  which messagesMap key streaming chunks actually land in
- probe-dump.ts: stops the timeline timer and stashes JSON dump on
  `window.__PROBE_LAST_DUMP_JSON` (runner returns that global)
- analyze-events.ts: stream events (non-chunk) + chunks summary +
  action-call stacks + correlation + per-key assistant growth +
  rollback detection. Per-key growth was added specifically to
  diagnose "chunks arrive but assistant cLen never moves"
- run.ts: `install` | `dump [name]` | `analyze [path]` CLI. Bundles via
  Bun.build, wraps as IIFE with explicit return, pipes to
  `agent-browser eval --stdin`. Dumps land at
  `.agent-gateway/<name>-<YYYYMMDD-HHmmss>.json`

`.agent-gateway/` is gitignored so dumps accumulate across debugging
sessions without polluting git.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(local-testing): repair run.ts after autofix mangled path imports

LOBE-9501

The eslint --fix run during the previous commit applied the unicorn
`import-style` rule and renamed every `join(` / `dirname(` / `resolve(`
to `path.join(` / `path.dirname(` / `path.resolve(`, but the replacement
was a naive text substitution that:

1. rewrote `array.join('\n')` to `array.path.join('\n')` — broke bundle
   error reporting (would TypeError on the build-failure path)
2. produced `const path = path.join(DUMP_DIR, filename)` inside cmdDump
   — shadowed the `path` module with itself, ReferenceError on every
   dump invocation

Rename the local `path` to `dumpPath` and drop the spurious `.path`
prefix on the array `.join`. Verified round-trip: install + dump now
write a valid capture to `.agent-gateway/`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🧪 chore(local-testing): capture per-call message snapshot in probe

LOBE-9501

The probe's `replaceMessages` wrapper used to record only `count` and
`params` — enough to see "two messages were written" but not WHICH two.
For post-stream collapse debugging we need to see whether each call
restored streamed content (cLen=N) or wiped to LOADING_FLAT (cLen=3).

Two changes:

- Capture `snapshot` field on every replaceMessages call: last 2
  messages' id / role / cLen / rLen / updatedAt. The analyzer prints
  this inline next to each call so reviewers can see content drift /
  collapse without re-reading the dump.

- Make wrapping idempotent across re-installs. The old guard
  `chat.__probeWrapped = true` froze the first-installed wrapper across
  re-installs, so updates to the probe body had no effect without a
  page reload. Stash the originals on
  `window.__PROBE_ORIG_REFRESH_MESSAGES` /
  `window.__PROBE_ORIG_REPLACE_MESSAGES` and re-wrap from those on
  every install.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🧪 chore(local-testing): add mutation log + dispatchMessage wrap to probe

LOBE-9501

The replaceMessages-only wrap couldn't catch chunk-level writes (those go
through internal_dispatchMessage) or attribute post-stream collapses to a
specific writer. Add:

- `__PROBE_MUTATIONS` — unified ordered log of every dbMessagesMap[key]
  reference change, with `last`/`prevLast` summaries and a `delta` field
  that tags interesting transitions (`cLen↓N→M`, `rLen↓`, `id:A→B`,
  `n↓prev→cur`). Both writers — replaceMessages AND internal_dispatchMessage
  — push to the same buffer so a single timeline shows all stores writes.

- Idempotent action wrapping. Originals are stashed on
  `window.__PROBE_ORIG_*` and re-wrapped from there on every install, so
  probe edits take effect without a page reload (previous
  `chat.__probeWrapped` flag froze the first wrapper).

- Snapshot field on replaceMessages — last 2 messages'
  id/role/cLen/rLen/updatedAt — so reviewers can see WHICH content each
  call is writing instead of just the count.

- Dump file now carries the `mutations` array alongside streamEvents,
  actionCalls, timeline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(chat-store): gate SWR onData by isStreaming for streaming topic

LOBE-9501

Backstop for the post-stream cLen collapse that survives even with the
gateway SoT consume in place. Reproduction (confirmed):

1. Send a stream that lands lots of WS chunks into ChatStore
2. Immediately reload the page

If the page reload races against server-side chunk fan-out into Postgres,
SWR's fresh fetch returns the assistant row in its LOADING_FLAT placeholder
state (cLen=3) and writes that to ChatStore via the conversation-store
mirror — even though the WS push at agent_runtime_end carried the
correct full content moments earlier.

`mergeFetchedMessagesWithLocalState`'s updatedAt tie-breaker handles
this for in-session repros (local message wins when its updatedAt is
newer), but it degenerates when:

- The SoT consume just wrote server's snapshot updatedAt onto the local
  message, equalising the timestamps so the next stale DB fetch wins
- The user reloads (no local state to merge against — fresh fetch wins
  outright)

Add a gate at the bottom of `ConversationStore.useFetchMessages.onData`:
while `isAgentRuntimeRunningByContext(context)` is true, drop the SWR
write entirely. SWR's own cache still updates, so once streaming ends a
normal revalidate writes through correctly.

This is layered defense — it does NOT fix the underlying server-side
fan-out lag (filed as separate Linear issue). It does prevent the
client-side flash users currently see during the lag window.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🧪 test(chat-store): align gateway handler tests with SoT contract

The previous assertions still expected `stream_start` to issue a DB refetch
on every native gateway stream — the very behaviour LOBE-9501 removes
(`acb9523a04`). Update the three failing cases to the new contract:

- `stream_start > should associate new message with operation`:
  assert `messageService.getMessages` is NOT called when
  `assistantMessage.id` is present (the SoT snapshot from the preceding
  `step_start` already pre-populated `dbMessagesMap`).
- `sequential processing`: rewrite around the surviving ordering guarantee
  — `associate` (stream_start) must precede `dispatch` (stream_chunk) so
  the chunk targets the new id. Add a sibling case for hetero CLI streams
  (no `assistantMessage.id` → DB fetch is still mandatory).
- `multi-step integration > full LLM → tools → LLM cycle`: keep the
  post-`tool_end` `replaceMessages` assertion (tool_end still refreshes
  from DB), invert the post-`stream_start` assertion for step 2.

42 tests passing (was 41 + 1 new hetero fallback test).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:05:58 +08:00
AmAzing- 5f6f053039 🐛 fix(agent): hide community publish for heterogeneous agents (#15166) 2026-05-24 18:39:05 +08:00
AmAzing- 775be47513 🐛 fix(agent): align settings defaults and locale state (#15163) 2026-05-24 16:29:22 +08:00
Arvin Xu 2f265a9307 🐛 fix(conversation): only swap model name for remote hetero agents in Usage (#15156)
* 🐛 fix(conversation): only swap model name for remote hetero agents in Usage

Local CLI hetero agents (claude-code, codex) report their actual model
id on `turn_metadata` and persist it on the assistant message, but the
Usage extra was unconditionally replacing it with the provider brand
label ("Claude Code" / "Codex") whenever `HETEROGENEOUS_TYPE_LABELS`
had an entry. Gate the swap to remote platform agents (openclaw,
hermes) — those don't expose a real model id — so CC/Codex turns show
the underlying model again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  test(desktop): update GatewayConnectionCtr tests for lh hetero exec route

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 13:08:21 +08:00
Arvin Xu 0fa2e2349c 🐛 fix(desktop): route gateway agent runs through lh hetero exec (#15132)
* feat(desktop): route gateway agent runs through lh hetero exec

Replace the desktop-side GatewayConnectionCtr.executeAgentRun() flow
(startSession -> sendPrompt with local AgentStreamPipeline) with a direct
lh hetero exec spawn. The lh CLI handles spawn -> adapt -> BatchIngester ->
heteroIngest/heteroFinish, matching the cloud sandbox path exactly.

Changes:
- HeterogeneousAgentCtr: add spawnLhHeteroExec() method
- GatewayConnectionCtr: executeAgentRun() now delegates to the new method

* 🐛 fix(desktop): remove duplicate lh token from hetero exec args

spawn('lh', args) already invokes the lh binary, so the leading 'lh'
in args made the effective command `lh lh hetero exec ...` and failed
before heteroIngest could run, breaking the gateway-triggered agent
run flow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: LobeHub Agent <agent@lobehub.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 02:54:00 +08:00
Arvin Xu 930344ae23 feat(agent-runtime): push UIChatMessage snapshot at gateway step boundaries (#15152)
* 🧪 chore(local-testing): add agent-gateway probe scripts for stream SoT validation

Probe + tab-switch + analyzer scripts under .agents/skills/local-testing/scripts/agent-gateway/
to capture in-browser snapshots of the message store during gateway streaming and detect
regressions where assistantGroup messages get clobbered by stale DB refetches.

Used to verify LOBE-9501.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(agent-runtime): push canonical UIChatMessage snapshot at step boundaries

LOBE-9501

Gateway-mode streaming previously let the client refetch from DB on every
step_complete or tab-focus; with stream chunks landing before the DB write
fans out, the refetch returned a stale assistant placeholder that clobbered
the in-memory streamed assistantGroup (reasoning / tool calls / content).

Server now attaches the canonical UIChatMessage[] snapshot to step_start
and agent_runtime_end events so the client can use the pushed payload as
Source of Truth instead of refetching:

- step_start now loads agent state first, queries messages, and attaches
  uiMessages to the event data when topic context is known
- publishAgentRuntimeEnd signature switched to a params object (additive
  uiMessages field) and the coordinator resolves the snapshot through an
  optional uiMessagesResolver hook before publishing terminal events
- AgentRuntimeService wires the resolver through a lazily-instantiated
  MessageService so tests without S3 env still construct cleanly
- MessageService.queryMessages exposes the same read path as the
  message.getMessages trpc lambda (FileService postProcessUrl included)

Pure additive on the wire: legacy consumers see new uiMessages field, old
finalState payload unchanged. Existing call sites in agentNotify and
aiAgent migrated to the params shape. Failures in the resolver fall back
to publishing without uiMessages so streaming never fails the step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): forward uiMessages in gateway /push-event payload

LOBE-9501

GatewayStreamNotifier.publishAgentRuntimeEnd was delegating uiMessages to
the inner manager (Redis SSE) but reconstructing its own push-event data
object that only carried { errorType, finalState, reason, reasonDetail }.
In gateway mode, clients consume /push-event rather than Redis directly,
so the canonical UIChatMessage[] snapshot never reached them at terminal
state — and the final step has no later step_start to carry a fresh one.

Forward uiMessages via the same conditional-spread pattern used in the
inner managers; add two tests covering the present/absent branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:23:21 +08:00
Arvin Xu 538195dfb4 🐛 fix(agent-runtime): route context engine payload out of the events stream (#15151)
* 🐛 fix(agent-runtime): route context engine payload out of the events stream

`call_llm` previously pushed a `context_engine_result` event carrying the
full `contextEngineInput` (agentDocuments, systemRole, knowledge, …) into
the per-step events array. That array is the same one persisted into
Redis `agent_runtime_events`, so every step shipped the heavy CE payload
into the state pipeline even though the only consumer was the trace
recorder, which extracted CE into the typed `contextEngine` snapshot
field and immediately filtered the event back out.

Wire a typed `recordContextEngine` callback through
`RuntimeExecutorContext` instead. `AgentRuntimeService.executeStep`
buffers the call per step and hands it to
`OperationTraceRecorder.appendStep` via a new `contextEngine` param.
Trace snapshots are byte-identical; the events stream — and therefore
the Redis state blob — no longer carries CE.

Step toward LOBE-9110 (split state vs trace pipeline). Viewer keeps
the legacy `context_engine_result` reader for back-compat with older
on-disk snapshots.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🎨 refactor(agent-runtime): rename recordContextEngine to tracingContextEngine

The callback name now signals its role as the trace-pipeline channel,
matching the `tracing` prefix used elsewhere for non-state observability
wiring. Pure rename, no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 01:14:12 +08:00
Arvin Xu b3d2d2fdbd feat(review-panel): group review changes by submodule (#15148)
* 🐛 fix(claude-code): show task subject in TaskUpdate inspector & header

A TaskUpdate that only sets `subject` (no status flip) was falling
through to the aggregate `Todos: x/y` chip and burying the per-call
signal. Surface the new subject like the status branch already does:
"Task updated: <subject>".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(review-panel): group changes by submodule with per-group collapse

Surface dirty submodules as their own groups in the agent Review panel so
users working in a parent repo with submodules see each repo's changes
clustered together (mirrors WebStorm's per-repo commit grouping). Both
Unstaged and Branch modes apply the same grouping — submodules with internal
working-tree changes (unstaged) or branch diffs against their own
origin/HEAD (branch) surface as separate groups, each tagged with its own
branch label and file/diff totals.

Backend (`GitCtr`):
- `getGitWorkingTreePatches` and `getGitBranchDiff` extracted into private
  recursive helpers that detect submodules via `git submodule status`,
  partition pointer-bump entries out of the parent's flat patches, and
  recurse one level for each dirty submodule's own patches + branch info.
- Nested submodules are not traversed (phase 1); revert routes through each
  group's absolute path so submodule files revert inside the submodule.

Renderer:
- New `GroupHeader` and `FileRow` subcomponents split out of `Review`.
  `GroupHeader` is sticky with a chevron + name + file count + diff totals +
  branch; clicking collapses the group's rows. A hover-revealed `ActionIcon`
  on the right expands/collapses all file diffs in that group
  (`e.stopPropagation` keeps it from also collapsing the surrounding header).
- Fixed `block-size: 32px` on the header so toggling the fold button on/off
  doesn't jitter the sticky height.
- Single-repo working trees keep the previous flat layout when no submodule
  groups exist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(review-panel): scan all submodules in branch mode

Previously branch mode only surfaced a submodule group when the parent's
diff against base ref contained a `Subproject commit` pointer bump for it.
This missed the common case where the user has committed work in a
submodule on a feature branch but the parent's pointer hasn't yet moved
relative to its base — the submodule's own branch differences stayed
invisible in the Review panel.

`collectBranchDiff` now recurses into every registered submodule (single
level, in parallel) and keeps a group when EITHER its pointer differs in
the parent OR its own branch diverges from its own origin/HEAD. Clean-on-
both-axes submodules are dropped so the panel stays quiet for repos where
the submodule isn't actively being worked on.

Submodule count is small in practice (single digits), so the extra
per-submodule fetch + diff in parallel is an acceptable cost.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(agent-documents): hide .tool-results archive from user-facing lists

Auto-created tool-result archive folder and its children are now filtered
out of getAgentDocuments. Agents still discover them via the tool-oriented
listDocuments paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(review-panel): drop "file not found in project index" toast

Reveal-in-tree now silently no-ops when the path isn't indexed (e.g.
submodule files) instead of nagging the user with a warning toast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(review-panel): keep submodule groups visible on pointer-only bumps

`isEmpty` was derived solely from `totalEntryCount`, which counts file
patches across groups. A pointer-only submodule bump (parent patch
filtered out, submodule group present but internally clean) produced
`totalEntryCount === 0`, so the panel rendered the global empty state
and silently skipped the submoduleClean group rendering — even though
git was dirty.

Now `isEmpty` also requires zero submodule groups, so pointer-only bumps
keep their GroupHeader + "submodule clean" line. The fold-all button
visibility switches to `totalEntryCount > 0` so it stays hidden when
there's nothing foldable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 00:29:22 +08:00
Arvin Xu cce14911d1 feat: per-call llm_generation_tracing observability (#15124)
*  feat(database): add llm_generation_tracing schema + tracing package (LOBE-9462)

Foundation layer for per-call observability of `generateObject` calls.

- New Drizzle table `llm_generation_tracing` with identity / context / model /
  result / usage / storage / feedback / audit columns and full single-column
  index coverage (Postgres bitmap-scan friendly). Migration 0103 is idempotent
  (CREATE TABLE/INDEX IF NOT EXISTS) for safe re-runs.
- `LlmGenerationTracingModel` with `record` / `updateFeedback` / `findById` /
  `listRecent`, all userId-scoped to prevent cross-user leaks.
- New package `@lobechat/llm-generation-tracing` mirroring agent-tracing's
  shape: `ITracingStore` interface, `FileTracingStore` (local/dev, scenario
  subfolders + latest.json symlink), `computePromptHash` (6-char sha256 of
  systemPrompt + schema), and `TRACING_SCENARIO_REGISTRY` + `resolveScenario`
  with explicit scenario override.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(model-runtime): wire llm_generation_tracing into ModelRuntime.generateObject (LOBE-9462)

Per-call interception layer — one hook covers all generateObject callers.

- New `onGenerateObjectComplete` hook on `ModelRuntimeHooks`: always fires
  (success or failure) with latency, usage, output/error. Fixes the gap where
  `onGenerateObjectFinal` only fires when the runtime invokes `onUsage`.
- `S3TracingStore` (zstd level 3, key
  `llm-generation-tracing/{scenario}/{v}-{hash}/{date}/{id}.json.zst`) and
  `LLMGenerationTracingService` that does DB insert → store.save → patch
  storage_key. Store failures preserve the row with `metadata.store_error`.
- `createLLMGenerationTracingHook` + `mergeModelRuntimeHooks` wired into
  `initModelRuntimeFromDB`; tracing runs alongside business (billing) hooks
  via `next/server.after()` when available, microtask fallback otherwise.
  Unknown metadata keys (e.g. `parent_memory_trace_key`) pass through.
- Memory extractor accepts `parentMemoryTraceKey` option for the job-level
  backlink. Follow-up-action caller given an explicit `scenario: 'follow_up'`
  metadata override — it was the only OSS caller missing trigger metadata.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  test(llm-generation-tracing): type vi.fn mocks so tsgo accepts mock.calls indexing

The hook + service tests destructured `mock.calls[0][0]` and accessed nested
fields, which tsgo flagged as TS2493 / TS18046 because `vi.fn()` defaults to a
zero-arg signature. Add explicit type parameters to the mocks so tsgo can
infer the call tuple, and cast `call.payload` at the access point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(model-runtime): move mergeModelRuntimeHooks into the package

It's a generic utility for composing `ModelRuntimeHooks` instances — same
import surface as `ModelRuntime` and the hooks interface — so it belongs
alongside them rather than tucked under a server-side consumer.

- New `packages/model-runtime/src/core/mergeHooks.ts` exports
  `mergeModelRuntimeHooks` and is re-exported from the package index.
- Move the unit tests to `packages/model-runtime/src/core/mergeHooks.test.ts`,
  including a new case covering the "a throws → b is skipped" load-bearing
  semantics.
- `src/server/services/llmGenerationTracing/hook.ts` drops the local copy and
  the consumer (`src/server/modules/ModelRuntime/index.ts`) imports from
  `@lobechat/model-runtime`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(llm-generation-tracing): version lives with the prompt, not in a central table

`promptVersion` was baked into `TRACING_SCENARIO_REGISTRY`, far from any
prompt definition — editing a prompt + forgetting to bump the entry in a
completely different file was an obvious foot-gun.

- Registry is now `Record<string, string>` mapping trigger → scenario only;
  it's the stable concern that rarely changes.
- `resolveScenario` always passes `promptVersion` through from the caller,
  defaulting to `UNKNOWN_PROMPT_VERSION` ('v0') when absent.
- Each call site declares its own `*_PROMPT_VERSION` constant next to the
  prompt it describes. `followUpAction` ships the first one:
  `FOLLOW_UP_PROMPT_VERSION` in `prompts/index.ts`, threaded through
  `metadata.promptVersion` at the `generateObject` call. Other callers can
  add the same constant when they next touch their prompts.

The 6-char prompt hash on the row still catches forgotten bumps.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(input-completion): wire prompt-version metadata at the auto-complete call site

Aligns input auto-complete with the FOLLOW_UP_PROMPT_VERSION convention so
each prompt iteration is recordable as the chat-side tracing lands.

- `INPUT_COMPLETION_PROMPT_VERSION = 'v1.0'` declared next to
  `chainInputCompletion` — bump together with the prompt body.
- `fetchPresetTaskResult` accepts optional `metadata` and forwards it to
  `getChatCompletion`; the existing chat path already plumbs metadata to
  `ModelRuntime.chat` options.
- `InputEditor` call site passes
  `{ scenario: 'input_completion', promptVersion }`.

Note: `llm_generation_tracing` currently only fires from
`onGenerateObjectComplete`. Input completion is a `chat` call, so this
metadata is forward-looking until a chat-side tracing hook lands.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(llm-generation-tracing): collapse bucketDir path.join args to silence turbopack glob warning

Turbopack's static analyzer treats `path.join(root, dyn1, dyn2)` as a
multi-segment glob pattern and warned that it could match ~12k files in
the project. Compose the relative subdir as a single string first, so
`path.join` only sees one dynamic segment.

Behavior unchanged — the resulting path is identical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(input-completion): route auto-complete through generateObject for tracing

Auto-complete is the first preset-task caller migrated to the structured-
output path so it lands in `llm_generation_tracing` via the existing
`onGenerateObjectComplete` hook. No new server hook, no global chat-side
tracing.

- `chainInputCompletion` now returns `{ messages, schema }` with a minimal
  `{ completion: string }` schema and a stable `INPUT_COMPLETION_SCHEMA_NAME`
  constant. JSON wrapping costs ~15-30 tokens against a 100-token completion
  budget — negligible for the observability win.
- `StructureOutputSchema` / `StructureOutputParams` accept optional
  `metadata`; `aiChatRouter.outputJSON` merges caller metadata over the
  default trigger so `{ scenario, promptVersion, schemaName }` reach
  `ModelRuntime.generateObject` options unchanged.
- `IStructureSchema.description` is now optional to match the zod schema —
  previously the TS type was stricter than runtime validation accepted.
- `InputEditor` switches from `chatService.fetchPresetTaskResult` to
  `aiChatService.generateJSON`, reading `response.completion`. Streaming
  is dropped because auto-complete already buffers the full result before
  inserting; no UX change.
- Reverts the unused `metadata` field that was added to
  `fetchPresetTaskResult` in the previous commit — no current caller needs
  it now that input completion uses the generateObject path.

Bumps `INPUT_COMPLETION_PROMPT_VERSION` to v2.0 because the system prompt
gained an "output the completion field" instruction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(aiGeneration): extract the runtime-init + generateObject dance into a service

Every server-side caller that produces structured output was repeating the
same two-step ritual: `initModelRuntimeFromDB(...)` → `runtime.generateObject(payload, { metadata })`.
`AiGenerationService` collapses it into one call so future cross-cutting
concerns (default metadata, retry, observability hooks) have one place to
land.

- New `src/server/services/aiGeneration/index.ts` exposes
  `generateObject<T>(input, options)` and is unit-tested for provider
  resolution + payload/metadata pass-through.
- `aiChatRouter.outputJSON` and `FollowUpActionService.extract` migrated to
  the service (other callers move organically when next touched).
- Drops the unused `keyVaultsPayload` field from `StructureOutputParams`
  and the placeholder at the InputEditor call site — key vaults are
  server-resolved from DB, the client never supplies them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(tracing): centralize TRACING_SCENARIOS const + inject AiGenerationService via trpc ctx

- New `packages/const/src/llmGenerationTracing.ts` exports `TRACING_SCENARIOS`
  + `TracingScenario` type — the single directory where every known scenario
  name lives. Adds `@lobechat/const` as a workspace dep on llm-generation-
  tracing so `TRACING_SCENARIO_REGISTRY` can reference the same literals.
- Callers (FollowUpActionService, InputEditor) replace `'follow_up'` /
  `'input_completion'` string literals with `TRACING_SCENARIOS.FollowUp` /
  `.InputCompletion`, so a typo or a rename fails the type-check instead of
  silently drifting on the row.
- `AiGenerationService` is now injected into the `aiChatProcedure` ctx
  middleware alongside `aiChatService`; `outputJSON` consumes it via
  `ctx.aiGenerationService` instead of new-ing it inside the handler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(llm-generation-tracing): add lt/llm-tracing CLI + drop local-only storage_key

- Add `lt` / `llm-tracing` CLI under @lobechat/llm-generation-tracing with
  `list` (recent records, --scenario filter, --json) and `inspect` (by
  tracing_id prefix or latest, --full, --json).
- `FileTracingStore.save` now returns `{ key: null }` so dev DB rows leave
  `storage_key` empty instead of recording a non-resolvable local path; S3
  store remains the source of truth for the real key. Add helpers
  `findByTracingId` / `getLatest` used by the CLI.
- Wire `agentId` and `topicId` into `input_completion` tracing metadata
  from the chat input auto-complete call site.
- Default `FileTracingStore` whenever NODE_ENV=development (drop the
  ENABLE_LLM_GENERATION_TRACING_LOCAL opt-in env var).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(llm-generation-tracing): prettier CLI output (tree + colors)

Mirror the @lobechat/agent-tracing viewer style:

- Inline ANSI color helpers (dim/bold/cyan/magenta/green/yellow/red).
- Compact single-line header with id, scenario, version, model, status,
  time — replaces the multi-line bullet list.
- Tree structure with `├─`/`└─` connectors instead of `── section ──`
  banners.
- input arrays render per-message (role + char count + preview) rather
  than dumping raw JSON.
- Small single-key outputs (e.g. `{ completion: "怎么样" }`) collapse
  to inline `key: "value"`.
- `lt list` switches to a colored, properly padded table.

Default view stays compact; --full expands system_prompt / input /
schema bodies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(llm-generation-tracing): split `tracing` config out of `metadata`

`options.metadata` was overloaded — half tracing-specific structured fields
(scenario / promptVersion / schemaName / agentId / topicId / ...), half
free-form jsonb passthrough. Callers couldn't tell which was which, and the
inputHint was always auto-extracted (useless when the prompt wraps the user's
text in a template).

This commit introduces a dedicated `tracing` option:

- Add `TracingOptions` to @lobechat/llm-generation-tracing — the typed shape
  callers import (agentId / topicId / inputHint / scenario / promptVersion /
  schemaName / systemPrompt / parentTracingId / metadata).
- Add loose `tracing?: Record<string, unknown>` to GenerateObjectOptions and
  StructureOutputParams / StructureOutputSchema so the field flows through
  the runtime + TRPC.
- Tracing hook now reads `context.options.tracing` for structured fields; it
  still falls back to `metadata.trigger` for the cross-cutting trigger string
  (ModelRuntime itself uses metadata.trigger for timing logs, so trigger
  stays on metadata).
- Service `record()` accepts an explicit `inputHint`; otherwise falls back
  to auto-extraction from the first user message. Always truncated.
- Free-form jsonb fields move to `tracing.metadata` (was unknown-key passthrough
  on `metadata`).
- Call sites updated:
  - FollowUpAction now passes `tracing: { scenario, promptVersion, schemaName,
    topicId }` (previously `metadata`).
  - InputCompletion now passes `tracing: { agentId, topicId, inputHint: input,
    scenario, promptVersion, schemaName }` — `inputHint` is the user's actual
    typed text, not the wrapper prompt's first user message.
  - `aiChat.outputJSON` router forwards both metadata and tracing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update inputCompletion.ts

* 🐛 fix(llm-generation-tracing): stop duplicating provider into the row's metadata jsonb

`provider` is already a first-class column on the `llm_generation_tracing`
row, so auto-stamping it into the `metadata` jsonb column on every call was
pure noise. The hook now writes the caller-supplied `tracing.metadata`
verbatim — empty/undefined when the caller had nothing to add.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:14:23 +08:00
Arvin Xu ddb5794826 chore: clean up LOBE-XXX code annotations (#15135)
* chore: clean up LOBE-XXX annotations from codebase comments

- Remove 【LOBE-XXX】 bracket markers
- Remove LOBE-XXXX references from inline comments
- Clean up test descriptions containing LOBE identifiers
- Preserve linear.app URLs and code-level regex patterns
- Generated: 2026-05-23 02:30:09

* 🐛 fix(tests): restore () in arrow callbacks broken by annotation cleanup

The LOBE-XXX annotation cleanup script over-matched `(LOBE-XXXX', () =>`
and stripped the callback `()`, leaving invalid syntax like
`describe(..., => {` and `it(..., async => {` across 24 test files.

This caused parse failures in Test Packages, Test Desktop App, Test
Database lint, and Test App shard runs. Restoring `()` / `async ()`
unblocks the suites while keeping the ticket-text cleanup intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hintFormat-test): restore label + ellipsis in stripMarkdownLinks fixture

The annotation cleanup stripped `LOBE-8516` from a markdown-link's
*label* (`[LOBE-8516](/task/T-1)` → `[](/task/T-1)`), which then survived
`stripMarkdownLinks` because the pattern requires non-empty link text —
the test expected the link to disappear and asserted equality on a
LOBE-free output. The same line also lost a `.` from the trailing
`...` indicator in both input and expected strings.

Substitute a neutral Chinese label (`发布计划`) so the link continues
to exercise the multi-link substitution path, and restore the full
`...` ellipsis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Arvin Xu <arvinxx@lobehub.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 17:18:18 +08:00
Innei f685d5c217 feat(agent-explorer): support multi-select delete in document tree (#15125)
*  feat(agent-explorer): support multi-select delete in document tree

- Right-click on a multi-selected row deletes the whole selection; dedupe descendants when an ancestor folder is also selected
- Reserve chevron slot in SkillsList rows so atomic and bundled skills align
- Centralize EMPTY_ARRAY (typed `never[]`, frozen) in @lobechat/const

* ♻️ refactor: migrate delete confirm dialog from antd modal to confirmModal

*  test: stabilize bun vitest environment

* 🔧 ci: avoid authenticated checkout for PR tests
2026-05-23 16:44:00 +08:00
LobeHub Bot 7eee016abe 🌐 chore: translate non-English comments to English in agent-skills-identifiers (#15137)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 12:42:23 +08:00
AmAzing- 36cc836f2b 💄 style(settings): clean up settings page copy and entries (#15117) 2026-05-23 10:04:08 +08:00
AmAzing- 1c24b9e677 feat(analytics): track onboarding step events (#15133) 2026-05-23 09:40:39 +08:00
AmAzing- a22ea78460 🧹 chore(analytics): remove unused PostHog component (#15131) 2026-05-23 02:58:58 +08:00
YuTengjing b50acaca40 🐛 fix: pin baseline-browser-mapping (#15130) 2026-05-23 01:15:12 +08:00
Arvin Xu d3faa70c94 Revert "fix(github): support both runCommand and run_command in render matching"
This reverts commit 6770d8f321.
2026-05-23 01:04:44 +08:00
Innei 8cd03c8013 ️ perf: warm route chunks after idle (#15109)
* ️ perf: warm route chunks after idle

* 🐛 fix: normalize platform route chunk ids

* ️ perf: refine route chunk preloading

* 🔧 chore: keep desktop renderer preload unchanged

* ️ perf: skip renderer chunks in route warmup

* ️ perf: preload agent route dynamic chunks

* ️ perf: align route preload deployment urls

* ️ perf: coalesce stable vendor chunks

* ️ perf: group shared data runtime chunks

* ️ perf: group model runtime chunks

* ️ perf: trim initial route preloads

* ️ perf: limit idle route micro preloads

* ️ perf: strip tiny html modulepreloads

* ️ perf: prune redundant route chunk imports

* ️ perf: enable rolldown devtools

* ️ perf: gate vite devtools output

* ️ perf: optimize react-scan integration and update global types

Signed-off-by: Innei <tukon479@gmail.com>

* ️ perf: support cloud route chunk preload

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-23 01:00:53 +08:00
Innei 8a6545f799 🐛 fix(docker): make prepare script tolerant when git is unavailable (#15129)
The `prepare` script runs `git config core.hooksPath .githooks`, which
fails inside Docker build where neither `.git` nor `git` exists, causing
`pnpm i` to abort. Guard with `git rev-parse --git-dir` and a `|| true`
fallback so the script silently no-ops outside a git working tree while
still installing the local hook path for normal development.
2026-05-23 00:39:14 +08:00
Innei de9f7e092a feat(follow-up): extend follow-up chip suggestions to general chat (#15101)
*  feat(follow-up): add foundation types for chat follow-up chips

- FollowUpExtractInput.threadId for portal thread isolation
- UserSystemAgentConfig.followUpAction (global enable + model)
- LobeAgentChatConfig.enableFollowUpChips (per-agent opt-in)
- ConversationHooks.onAssistantTurnSettled first-class member
- Remove dead onGenerationStart/Complete/Cancelled hooks
- DEFAULT_SYSTEM_AGENT_CONFIG.followUpAction off by default
- DEFAULT_AGENT_CHAT_CONFIG.enableFollowUpChips false default

* ♻️ refactor(follow-up): key follow-up store by conversation for concurrency

- Convert useFollowUpActionStore from single-slot to slots map
- conversationKey = messageMapKey(agentId, topicId, threadId?) for parity with chat store
- contextSelectors.conversationKey exposes the key from ConversationProvider
- FollowUpChips and ChatItem consume conversationKey
- Onboarding hook adopts the new keyed API
- Pass threadId through to extract (server filter lands in T3)

* 🐛 fix(follow-up): address T2 code review feedback

- Restore design-intent comments for 20s timeout and race guard
- Remove dead pendingMessageId field from FollowUpActionSlot
- Remove unused slotFor selector
- Trim chipsFor / FollowUpActionSlot JSDoc to design intent only
- Gate useOnboardingFollowUp against missing onboardingAgentId
- removeSlot uses destructure; slotStatus uses ?? for falsy safety

*  feat(follow-up): filter extract by threadId for portal thread isolation

- FollowUpActionService.extract honours optional threadId
- threadId provided → eq(messages.threadId, threadId)
- threadId absent → isNull(messages.threadId) so main topic never surfaces thread replies
- Tests cover both branches

*  feat(conversation): emit onAssistantTurnSettled hook from provider

- AssistantTurnSettledWatcher fires hooks.onAssistantTurnSettled(messageId, { reason }) once per turn
- Reason derived from the most recent terminal Operation for the message id
- Reason mapping: cancelled → stopped, type=regenerate → regenerated, type=continue → continued, else → completed
- Settlement gated on idle + no pending tool intervention (mirrors Onboarding's logic)
- Tests cover all four reason branches + intervention gating + no double-fire + fallback log
- Onboarding bespoke prop untouched (migrates in T6)

* 🐛 fix(conversation): scope settlement reason to turn-level operations

- TURN_LEVEL_TYPES filter excludes child sub-ops (callLLM, executeToolCall, etc.) before sorting by endTime
- Prevents successful regenerate/continue being misreported as 'completed' when a child finishes after the parent
- Tests cover parent/child ordering for all reason branches

*  feat(follow-up): add useChatFollowUp hook and wire chat mount sites

- New mergeConversationHooks composes multiple hooks with boolean short-circuit
- useChatFollowUp computes effective enable (global × per-agent × valid model)
- Registers onBeforeSendMessage/Continue/Regenerate to clear slot and onAssistantTurnSettled to extract
- Mount sites: agent route ConversationArea, FloatingChatPanel, Portal Thread Chat (last in chain per §4.6)
- Skips on reason='stopped'; skips when effective is false
- Group chat intentionally not mounted

* ♻️ refactor(onboarding): migrate settlement to ConversationHooks first-class

- Drop bespoke onAssistantTurnSettled prop and duplicate useEffect from AgentOnboardingConversation
- useOnboardingFollowUp returns ConversationHooks { onBeforeSendMessage, onAssistantTurnSettled }
- Split settlement work: context-sync + builtin refresh runs first, chip extract runs after
- Phase snapshot captured at memoize time preserves original prevPhase semantics
- Settlement detection now lives solely in AssistantTurnSettledWatcher

*  feat(settings): add Follow-up suggestions controls (global + per-agent)

- Global System Agent page: new Follow-up Suggestions panel (model picker + enable toggle)
- Per-agent chat controls: enableFollowUpChips toggle with hint when global not configured
- i18n keys: setting.systemAgent.followUpAction.*, setting.settingChat.enableFollowUpChips.*
- Hint surfaces when user toggles per-agent ON but global is disabled/unmodeled

* 🔧 chore(follow-up): T8 — scoped lint cleanup and comment discipline pass

* 🐛 fix(follow-up): align conversationKey selector with callsite + wrap single hook

- contextSelectors.conversationKey forwards full context (scope/isNew/groupId/subAgentId) so portal-thread NEW state matches callsite-computed keys
- ConversationArea wraps chat-follow-up via mergeConversationHooks for spec §4.6 ordering robustness
- Both per final-review Important concerns

*  test(settings): update follow-up defaults snapshots

*  feat(follow-up): surface model in service-model page + default to mini

- Add followUpAction to /service-model OPTIONAL_FEATURE_ITEMS so model/provider and enable Switch render alongside inputCompletion and promptRewrite
- Seed DEFAULT_FOLLOW_UP_ACTION_SYSTEM_AGENT_ITEM with DEFAULT_MINI model/provider so out-of-box config has a valid model; users only need to flip enabled
- Sync settings selector snapshot
2026-05-23 00:31:15 +08:00
Arvin Xu 6770d8f321 fix(github): support both runCommand and run_command in render matching 2026-05-22 16:16:48 +00:00
Arvin Xu b01e4dc257 🔨 feat(db): add llm_generation_tracing and agent eval experiment tables (#15126)
🔨 chore(db): combine llm_generation_tracing and agent eval experiment tables into 0103

Merges the schema work from #14990 with the new llm_generation_tracing
table into a single idempotent 0103 migration so the two streams can
land together without a migration-number conflict.

Also adds user_id (FK + index) to agent_eval_experiment_benchmarks so
the junction table is scoped per user, matching agent_eval_run_topics.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 00:05:15 +08:00
YuTengjing 0e346c5b72 ♻️ refactor: add shared guard helpers (#15122) 2026-05-22 23:27:26 +08:00
AnotiaWang 55452cdf42 🐛 fix(web-crawler): support Jina CN domains (#14916)
Co-authored-by: AnotiaWang <AnotiaWang@users.noreply.github.com>
2026-05-22 23:05:27 +08:00
AnotiaWang 94bd7b2f6b 🐛 fix: preserve topic pagination state after topic actions and new topic creation (#13463)
* fix: topic drawer behavior after deleting topics

* fix: `hasMoreTopics` selector

* 🐛 fix: refine topic sidebar hasMore and filter-aware pagination

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: AnotiaWang <AnotiaWang@users.noreply.github.com>
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2026-05-22 23:04:05 +08:00
Rylan Cai b09d744231 🐛 fix(cli): catch promise error to avoid agent run crash in WS mode (#14830)
* 🐛 fix cli websocket agent run crash handling

* ♻️ chore trim unrelated bm-36 diff

* ♻️ chore minimize bm-36 websocket diff
2026-05-22 22:35:33 +08:00
YuTengjing 5fe9afc681 🐛 fix: preserve Gemini image diagnostics (#15120) 2026-05-22 22:03:21 +08:00
Arvin Xu 857cf9582a 💄 style(workflow): show check with warning badge for partial-success runs (#15119)
* 💄 style(workflow): show check with warning badge for partial-success runs

When a turn finishes with a mix of successful and failed tool calls, the
overall workflow now reads as "done" (green check) with a small warning
triangle pinned to the bottom-right of the status block, instead of
flipping the whole indicator to warning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(workflow): shrink and tuck partial-status warning badge

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:53:44 +08:00
AmAzing- acd3da8059 🐛 fix: guard restricted default provider selection (#15118) 2026-05-22 21:01:38 +08:00
Arvin Xu 7cad53d878 🐛 fix(agent-runtime): inject local-system template vars for regular chat (#15087)
* 🐛 fix(agent-runtime): inject local-system template vars for regular chat

Before this fix, the lobe-local-system system prompt's `<user_context>`
template (`{{workingDirectory}}` / `{{hostname}}` / `{{homePath}}`)
reached the LLM as literal `{{...}}` strings whenever a user chatted in
the regular Web UI without binding a device. The model couldn't see cwd,
home, or hostname and wasted the first N steps groping for paths
(observed: 16 wasted steps in one 120-step, 1281s op).

Root cause: `activeDeviceId` resolution at execAgent had an IM/Bot
limitation — only `(discordContext || botContext) && length===1` would
auto-activate. Regular Web chat fell to `undefined`, which gated out the
`deviceSystemInfo` fetch and left the Mustache template variables empty.
The PlaceholderVariables renderer keeps `{{...}}` literals when a
generator is missing, so the placeholders reached the LLM intact.

Fix (LOBE-9378):
- Remove the IM/Bot restriction. Regular chat and IM/Bot now share the
  same single-device auto-activate rule. Multi-device users still need
  to bind explicitly — picking by recency would be a guess that could
  route tool calls to the wrong machine.
- Extract `deviceSystemInfo` fetching into a `fetchDeviceSystemInfoForTemplate`
  helper so the template-rendering decision is structurally decoupled
  from the routing decision (future fallback policies belong in the
  helper, not in activeDeviceId resolution).

* 🐛 fix(test): assert new autoActivated field on deviceContext

The PR added `autoActivated` to the deviceContext shape forwarded to
`createServerAgentToolsEngine`. The deviceToolPipeline test in a
sibling file still used a strict `toEqual` against the old three-field
shape — single online device + no binding now auto-activates, so the
assertion missed the new field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 20:59:38 +08:00
Arvin Xu a0fac0b700 feat(skills): recognize project-level skills in the homogeneous agent runtime (#15110) 2026-05-22 19:22:41 +08:00
LiJian a35877f676 feat(platform-agent): improve device selection UX with actionable guidance (#15111)
*  feat(platform-agent): improve device UX — copyable lh connect cmd + version-too-low hint

- No-device state now shows a copyable `lh connect` command with clearer guidance to run it on the target machine then click Refresh
- Capability check failure caused by outdated lh desktop now shows a user-friendly "lh version is too low" alert with a copyable `npm install -g @lobehub/cli` upgrade command instead of the raw internal error string
- Changed no-device alert type from warning → info (absence of device is expected, not an error)
- Add en-US / zh-CN locale keys: noDevicesCmd, versionTooLow, versionTooLowHint, upgradeCmd

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 📝 fix(platform-agent): correct platform card descriptions — connect not run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(platform-agent): desktop capability check + improved no-device onboarding

- Add checkPlatformCapability / getAgentProfile handlers in GatewayConnectionCtr so desktop devices no longer return "tool not available" error
- Redesign no-device alert: primary CTA is Desktop App download (https://lobehub.com/downloads), secondary is copyable lh connect CLI command
- Add 5 tests for new capability probing handlers (43 total, all pass)
- Add missing execa/fast-glob/fflate mocks to unblock test suite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(platform-agent): route openclaw/hermes to correct binary in executeAgentRun

Previously all non-codex agent types defaulted to the `claude` command.
Now maps claude-code → `claude`, all other types (openclaw, hermes, …) → their
own binary name, which matches the pattern used by checkPlatformCapability.

Also adds 6 agent-run-routing tests covering openclaw/hermes/codex/claude-code
command mapping, accepted ack + sendPrompt wiring, and rejected ack on
startSession failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(platform-agent): wire runHeteroTask/cancelHeteroTask on desktop gateway

The server dispatches openclaw/hermes via executeToolCall('runHeteroTask'),
not agent_run_request. The CLI (lh connect) handles this in its methodMap;
now the desktop gateway does too.

- Port runHeteroTask + cancelHeteroTask from CLI to GatewayConnectionCtr
  - openclaw: spawn detached process, save PID, inject notify protocol on
    first turn, send done signal via sendNotify on close
  - hermes: ensure gateway daemon is running, POST to /message endpoint
- Add in-memory platformTasks registry for cancel support
- Add sendNotify helper — calls agentNotify.notify tRPC endpoint directly
  using desktop token (desktop counterpart to `lh notify`)
- Port buildNotifyProtocol inline so desktop and CLI stay in sync
- Add resolveLhPath, openclawSessionExists, getHermesPort helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(heteroTask): always inject notify protocol and kill concurrent openclaw processes

- Remove openclawSessionExists check: always inject buildNotifyProtocol
  into every turn so openclaw can report back even after a failed session
- Before spawning openclaw, kill any existing process for the same
  topicId to prevent session file lock conflicts (exit code 1)
- Apply same fixes to both CLI (heteroTask.ts) and desktop
  (GatewayConnectionCtr.ts) to keep behaviour in sync
- Add CLI unit tests (heteroTask.test.ts, 7 cases)
- Extend desktop tests to cover always-inject and kill-concurrent
  behaviours (52 total, up from 49)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔀 chore(cli): resolve version conflict — keep 0.0.19

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔖 chore(cli): bump version to 0.0.20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(desktop): implement getAgentProfile via openclaw agents list --json

Port getAgentProfile from CLI (getAgentProfile.ts) to desktop gateway:
- calls `openclaw agents list --json` to get name + emoji
- reads workspace IDENTITY.md / SOUL.md for description fallback
- falls back to 🦞 emoji when no identityEmoji set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(desktop): make getAgentProfile async to satisfy methodMap Promise return type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 18:44:55 +08:00
LiJian d15651bbec 🐛 fix(hetero): fix cloud CC agent execution failures and improve error messages (#15107)
* 🐛 fix(hetero): auto-retry on stale --resume session when cloud sandbox is recycled

Cloud sandboxes are ephemeral (~1h idle TTL). When a new container is
spawned for the next conversation turn, the previous CC session files under
~/.claude/projects/<cwd>/ are gone, so --resume <staleId> fails with
"No conversation found with session ID".

Two-layer fix:

CLI (lh hetero exec)
- Detect resume-not-found errors from stream error events and stderr
- Intercept the error event (withheld from the ingester so the server
  never sees a terminal error) and transparently retry without --resume
- The retry emits a fresh CC session id via heteroFinish, replacing the
  stale heteroSessionId in topic metadata and breaking the failure loop

Server (HeterogeneousPersistenceHandler)
- When result=error and no sessionId was produced (CC never emitted
  system.init, typical for resume failures), clear the persisted
  heteroSessionId from topic metadata as a safety net
- When CC ran successfully but produced an error result, sessionId IS set
  so the valid session is preserved for resume on the next turn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero): handle context-overflow resume failure + inject conversation history

Extends the resume auto-retry to also cover the "long conversation →
immediate next turn → Agent execution failed" scenario:

CLI (hetero exec)
- Renames RESUME_NOT_FOUND_PATTERNS → RESUME_RETRY_PATTERNS and adds
  context-overflow patterns (`/prompt.*too long/i`, `/context.*too long/i`,
  etc.) so CC's API-level "prompt too long" error triggers the same
  retry-without-resume path as the sandbox-recycled case.
- Adds a test case that verifies the context-overflow error retries cleanly.

Server (cloudHeteroContext + aiAgent)
- Exports ConversationHistoryEntry from cloudHeteroContext.ts and adds
  a conversationHistory? param that renders a <previous_conversation> block
  (user turns ≤ 1 KB, assistant turns ≤ 2 KB) in the system context.
- In execAgent, when resumeSessionId is set, fetches the last 200 messages
  for the topic, filters to the last 30 user/assistant turns, and passes
  them as conversationHistory to buildCloudHeteroContext.  This gives CC
  context about prior turns even when the native session file was reset.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero): fix SIGTERM handler leak + remove unused ingestError binding

- Store the SIGTERM callback in a variable and process.off() it in the
  finally block alongside SIGINT, so the first run's handler is removed
  before the retry run registers its own (fixes duplicate sink.finish
  calls on SIGTERM mid-retry).
- Remove unused `ingestError` from the result destructuring (downstream
  code already uses result.ingestError directly).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero): surface CC stderr in error message instead of generic fallback

Always collect stderr from the agent process (cap 8 KB) and pass its
tail (last 1 KB) as the `error` param to `heteroFinish` when the run
fails.  The persistence handler's `flushFinalState` overwrites the
generic "Agent execution failed" fallback with the actual CC stderr,
giving users and operators a meaningful error message.

Previously:
  {"message":"Agent execution failed","type":"AgentRuntimeError"}

After this fix, e.g.:
  {"message":"Error: API error: context window exceeded (200 000 tokens)",
   "type":"AgentRuntimeError"}

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 chore(cli): bump version to 0.0.18

* 🐛 fix(lint): replace inline import() type with static import type

* 🐛 fix(lint): fix import sort order for ConversationHistoryEntry

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 17:38:31 +08:00
Neko fd985d0b69 🐛 chore(builtin-tool-memory): missing sourceIds in manifest causing memory failure (#15113) 2026-05-22 17:34:49 +08:00
Innei cec72199bb 🐛 fix(onboarding): prevent agent identity from using user name (#15112) 2026-05-22 17:09:01 +08:00
Arvin Xu 1a340deb75 ♻️ refactor(local-file-shell): sink desktop search modules into shared package (#14972)
* ♻️ refactor(local-file-shell): sink desktop contentSearch + fileSearch modules

Move the entire `apps/desktop/src/main/modules/contentSearch/` and
`apps/desktop/src/main/modules/fileSearch/` trees into the shared
`@lobechat/local-file-shell` package so desktop, CLI, and cloud-sandbox
runtimes share one platform-aware implementation instead of maintaining
parallel copies that drift apart (the `.github/workflows/*.yml` hidden-segment
bug fixed in #14965 had to be patched in two places).

What moves
- `contentSearch/{base,impl/{unix,linux,macOS,windows},index}.ts` → factory
  `createContentSearchImpl()` with rg → ag → grep → nodejs fallback
- `fileSearch/{base,types,impl/{unix,linux,macOS,windows},index}.ts` →
  factory `createFileSearchModule()` with fd → find → fast-glob (Unix),
  mdfind override on macOS, fd → PowerShell → fast-glob on Windows
- All 7 corresponding test files

Abstractions introduced
- `src/logger.ts`: `Logger` interface + debug-backed `createDefaultLogger`
  (namespace `lobe-local-file-shell:*`) and a `setLoggerFactory()` escape
  hatch so desktop can keep routing through electron-log if it wants
- `src/toolDetector.ts`: minimal `ToolDetector` interface
  (`getBestTool(category): Promise<string|null>` only) — desktop's
  `ToolDetectorManager` already satisfies it structurally and is injected
  lazily via `setToolDetector()`

Type-source consolidation
- `GrepContentParams/Result`, `GlobFilesParams/Result` now live in
  `@lobechat/local-file-shell/types`; `@lobechat/electron-client-ipc`
  re-exports them so the IPC contract, the desktop service, and the CLI
  share one source of truth (with legacy aliases `cwd`, `filePattern`,
  `directory` kept for back-compat)

Desktop services collapse to thin adapters
- `contentSearchSrv.ts` / `fileSearchSrv.ts` now just delegate to the
  factories; the old `apps/desktop/src/main/modules/contentSearch/` and
  `fileSearch/` directories are deleted entirely (≈4000 LoC removed)

Legacy `globLocalFiles` / `grepContent` / `searchLocalFiles` thin functions
keep their existing lightweight fast-glob / spawned-rg implementations
(unchanged semantics for CLI + cloud-sandbox callers), but now share the
`hasHiddenSegment` helper with the factory so dot-segment fixes only need
to be applied once.

Tests
- local-file-shell: 167/167
- desktop services: 58/58
- CLI file: 7/7
- builtin-tool-local-system: 64/64

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(local-file-shell): route sunk search logs through desktop's electron-log

Reviewer caught a regression: after #14972 sank `contentSearch` and `fileSearch`
into `@lobechat/local-file-shell`, the package's default debug-only logger took
over — so search warnings/errors no longer landed in the electron-log file that
users attach for support. The desktop `setLoggerFactory()` was defined but
never called.

Two-part fix:

1. `local-file-shell/logger.ts` — the `Logger` returned by `createLogger()` is
   now a thin proxy that re-resolves the current factory on every method call
   (with a per-namespace cache). This means `setLoggerFactory()` works even
   after module-level `const logger = createLogger('...')` declarations have
   already run — important because `local-file-shell`'s search modules are
   imported (and their loggers created) before the desktop bootstrap finishes.

2. `apps/desktop/src/main/utils/logger.ts` — calls `setLoggerFactory(createLogger)`
   as a module-load side effect, so anyone importing `@/utils/logger` (which
   App.ts does) automatically rewires the package logger into electron-log.

Tests: 169/169 in local-file-shell (added `logger.test.ts` covering the late-bind
and cache-per-namespace behaviour); desktop services 58/58.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(electron-client-ipc): keep package leaf — declare grep/glob types locally

Reviewer feedback: `@lobechat/electron-client-ipc` is an IPC contract package
and shouldn't reverse-depend on the business package `@lobechat/local-file-shell`
just to share four type aliases. Declare them locally instead — the two
copies must stay structurally compatible (they describe the same IPC payload
either way), but the dependency arrow now points only one direction.

Changes
- `electron-client-ipc/src/types/localSystem.ts` — re-declare GrepContentParams,
  GrepContentResult, GlobFilesParams, GlobFilesResult locally
- `electron-client-ipc/package.json` — drop the `@lobechat/local-file-shell`
  dependency
- `local-file-shell/types.ts` — tighten `success` and `total_files`/
  `total_matches` from optional to required so the two type definitions stay
  structurally interchangeable (the IPC version had them required all along)
- `local-file-shell/file/glob.ts` + `grep.ts` — thin wrappers fill in the now-
  required `engine` / `success` / `total_files` / `total_matches` fields

Tests: local-file-shell 169/169, desktop services 58/58, CLI 7/7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:23:42 +08:00
Arvin Xu eb1ba56024 ♻️ refactor(heterogeneous-agents): align CC adapter preset with actual spawn flags (#15102)
* ♻️ refactor(heterogeneous-agents): align CC adapter preset with actual spawn flags

The CC adapter's `claudeCodePreset` hard-coded `--include-partial-messages`
and `--permission-mode acceptEdits`, but runtime spawn args come from
`spawnAgent`'s `CLAUDE_CODE_BASE_ARGS` (with partial-messages opt-in and
permission mode chosen per-caller). CLI / sandbox runs default to no
partial deltas; only the desktop driver opts in. Trim the preset to the
invariant flags so it stops implying spawn-site-specific behavior, and
fix the matching adapter / test comments that called partial-messages
"our default".

* 🔥 chore(heterogeneous-agents): remove unused CLI preset infrastructure

`claudeCodePreset` / `codexPreset` and the `AgentCLIPreset` type were
registry metadata never consumed at runtime — the actual spawn args come
from `spawnAgent`'s `CLAUDE_CODE_BASE_ARGS` / `CODEX_REQUIRED_ARGS`. The
preset field on registry entries and the `getPreset` accessor were only
reached from `registry.test.ts`. Cloud repo and downstream consumers have
zero references.

Drop the presets, the preset field on registry entries, `getPreset`, the
`AgentCLIPreset` type, related re-exports, and the orphaned tests. The
registry now just maps agent type → adapter constructor.
2026-05-22 15:51:50 +08:00
Arvin Xu 902eb9f863 🐛 fix: add pre-flight tool-limit check for GitHub Copilot (#14909)
* fix: add pre-flight tool-limit check for GitHub Copilot (128 tools)

- Add maxToolCount / maxToolPayloadBytes to AIChatModelCard
- Set maxToolCount=128 on all githubCopilot models
- Add ExceededToolLimit error type
- Create validateToolLimits utility
- Integrate pre-flight check into LobeGithubCopilotAI

Closes LOBE-8660
Part of LOBE-8678

* refactor: lift Copilot tool limit to provider settings + map ExceededToolLimit to 400

- Move maxToolCount/maxToolPayloadBytes from AIChatModelCard to AiProviderSettings; the 128-tool cap applies to every GitHub Copilot model, so a single provider-level field replaces the per-model duplication.
- Rewrite validateToolLimits to read limits from DEFAULT_MODEL_PROVIDER_LIST by providerId.
- Add ExceededToolLimit to getStatus in errorResponse.ts (alongside ExceededContextWindow) so the pre-flight error returns HTTP 400 instead of throwing RangeError from new Response(..., { status: 'ExceededToolLimit' }).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test: add coverage for validateToolLimits / assertToolLimits

- ToolLimitExceededError: count overage message, payload-size message (KB rounding), combined overage, field assignment.
- validateToolLimits: empty tools, provider without declared limits, unregistered provider, count under cap, count exceeding the real GitHub Copilot 128 limit, payload-size enforcement via a synthetic provider pushed into DEFAULT_MODEL_PROVIDER_LIST.
- assertToolLimits: re-throws as a structured AgentRuntimeError chat payload with errorType ExceededToolLimit; no-op when limits are not exceeded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:19:57 +08:00
Arvin Xu a41fd95eb5 feat(skills): drag skill chips + register agent-document skills (#15095)
*  feat(skills): drag skill chips from the working sidebar into the chat input

Pick a project skill from the right Skills panel and drop it onto the
chat input to insert a `/<skill-name>` action tag — the same end state
as picking it from the `/` slash menu.

- `SKILL_DRAG_MIME` lives in `@lobechat/const` so both the producer
  (sidebar) and the consumer (input drop handler) share one source of
  truth.
- `skillDragData.ts` owns the drag payload and a custom drag image: a
  themed "icon + name" chip centered above the cursor. The native drag
  image is suppressed by an invisible 1×1 ghost — the OS bakes its own
  drop shadow into it which no CSS can remove. Token values are resolved
  via `getComputedStyle` against the dragged row so the chip stays
  themed even though it mounts on `document.body`.
- `useSkillDrop` listens on the input container and only reacts to the
  `application/x-lobe-skill` MIME, so it never interferes with the
  file-upload drop zone (which keys off `Files`).
- `ProjectLevelSkills` and `SkillsGroup` wire drag-start with the
  `projectSkill` category, matching the existing slash-menu behaviour
  (markdown serializes to `/<skill-name>`).

Agent-document skills (the 智能体 Skills group) are not wired here —
they need to be registered as first-class skills in the runtime
registry first; that work is tracked separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(i18n): localize Skills label to 技能 across working sidebar and mention menu

- zh-CN: workingPanel.skills.* and resources.filter.skills now use 技能
  (covers the Space tab pill plus the agent/project skill section headers)
- Wire SkillStore tab and ChatInput mention categories through t() instead
  of hardcoded English labels; add mention.category.* keys for the five
  @-menu groups (Agents / Members / Topics / Skills / Tools)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(skills): register agent-document skill bundles in the skill registry

Agent-document skill bundles (the "智能体 Skills" panel group, stored as
isSkillBundle documents in agent_document) become first-class runtime
skills end-to-end, so the slash menu / drag chip / model activation all
share one source of truth.

Identifier convention: `agent-document:<filename>` (where `<filename>`
is the bundle's slug — `validateSkillName`-validated on the server). The
prefix prevents collisions with builtin / DB skill names; mirrors the
`project:<name>` convention used for filesystem project skills.

Server:
- `aiAgent/index.ts` SkillEngine assembly: query
  `agentDocumentsService.getAgentDocuments(resolvedAgentId)`, filter
  `isSkillBundle`, and merge into the skills array so the model sees
  them in `<available_skills>`.
- `toolExecution/serverRuntimes/skills.ts` factory: when an `agentId`
  is in the request context, load the bundles + their SKILL.md index
  children and shape them as `BuiltinSkill` entries, then concat with
  `filterBuiltinSkills(builtinSkills)` before constructing
  `SkillsExecutionRuntime`. The runtime resolves builtins by `name`
  with no DB lookup — so `activateSkill('agent-document:<filename>')`
  now returns the SKILL.md content for free, no `SkillRuntimeService`
  extension needed. `source: 'builtin'` on these entries is a
  type-system carrier shape, not a claim that they're real builtins.

Client:
- New tool-store slice `agentDocumentSkills` (per-agent scoped, cleared
  on agent switch). `useFetchAgentDocumentSkills(agentId)` is the SWR
  hook that keeps the registry hydrated; shares the SWR key with the
  working-sidebar panel so we never double-fetch.
- `useInstalledSkillsAndTools` now reads from the new slice and triggers
  the SWR hook with the active agent's id, so the `/` menu and any
  consumer that goes through that hook see agent-doc skills alongside
  builtin / lobehub / market / user skills.
- `AgentDocumentsGroup` wires `onSkillDragStart` on its SkillsList: the
  payload uses the runtime identifier (`agent-document:<filename>`),
  while the chip label keeps the human-readable title.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(skills): rename agent-doc skill prefix to agent-skills + render <skill> tags

Three intertwined fixes around the agent-document skill registry that
the earlier commit (331eed1e9c) shipped half-baked:

1. **Prefix renamed `agent-document:` → `agent-skills:`** and extracted to
   `@lobechat/const` (`AGENT_SKILLS_IDENTIFIER_PREFIX`,
   `buildAgentSkillIdentifier`, `parseAgentSkillIdentifier`). The new
   prefix mirrors the unified VFS skill namespace path
   `./lobe/skills/agent/skills/<name>` flattened to one token, and
   single-sourcing it through const stops drift between the server
   resolver and the client drag wiring.

2. **`AgentDocumentsService.getAgentSkills(agentId)`** — one place to
   query bundles, filter `isSkillBundle`, resolve the `SKILL.md` index
   child, and build the runtime identifier. Both the SkillEngine
   assembly in `aiAgent/index.ts` and the `SkillsExecutionRuntime`
   factory in `serverRuntimes/skills.ts` call it instead of each
   re-implementing the prefix + bundle → index lookup (which was how
   the two sides drifted last round).

3. **`<skill>` / `<tool>` markdown plugins** (`plugins/Skill`,
   `plugins/Tool`) so the chat bubble renders these tags as the same
   chip the editor uses, instead of leaving the literal
   `<skill name="…" />` text in the message. Fixes a pre-existing bug
   that affected all registered skills (builtin / lobehub / DB / agent-
   document) — only the bare-text `projectSkill` flavour rendered
   correctly before because it serializes to `/<name>` instead.

Note: the client drag wiring in `AgentDocumentsGroup.tsx` and the
client tool-store slice action import the new const helpers, but
landing the *category* refactor (`'skill'` → `'agentSkill'`) and the
shared `@/features/SkillsList` extraction is intentionally kept out of
this commit so it can ship with its own ActionTag work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(skills): extract SkillsList feature + add agentSkill chip category

- New src/features/SkillsList/ bundle: SkillsList moved here from
  AgentDocumentsExplorer, joined by a shared SkillSection wrapper (optional
  collapsible sectionHeader prop unifies the Accordion / flat-header
  variants) and a useProjectSkills hook (SWR + open handlers).
- AgentDocumentsGroup / ProjectLevelSkills / SkillsGroup now consume that
  bundle and drop ~340 lines of duplicated SWR + section UI.
- ActionTag gains an 'agentSkill' UI category (types, mention card, style,
  en/zh editor copy) so agent-document skill chips render with their own
  tooltip / label while still serializing as <skill name="agent-skills:..."
  /> on the wire — the runtime keys off the identifier prefix, so no new
  XML tag is needed. The XML reader detects the prefix on parse to keep
  the chip's category across save/reload.
- AgentDocumentsGroup drag uses category='agentSkill', backed by the
  shared buildAgentSkillIdentifier helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  feat(hetero-agent): classify Claude Code 529 overload as structured error

Adapter previously surfaced overload (`api_error_status: 529` /
`overloaded_error`) as a plain `{ error, message }` payload, so the
executor fell through to the unstructured branch and the UI rendered
the raw text instead of a typed `HeterogeneousAgentSessionError`. Add
a dedicated `overloaded` code + StatusGuide state with a Retry action
so the common transient failure has a recoverable, branded surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(skills): drop text/plain fallback + custom drag image — they broke every skill drag

`writeSkillDragData` also set `text/plain` to the chip label, and
`setSkillDragImage` swapped in a custom cursor-following preview. The
combination races the Lexical chat input's own drop handling: it reacts
to `text/plain` and the suppressed-native-image sequence intermittently
aborts the dragstart, leaving `useSkillDrop` to never fire. Net result
was that every skill drag (project + agent-document) silently failed.

Strip both back to the minimum that's known to work:

- `writeSkillDragData` writes only the custom `application/x-lobe-skill`
  MIME + `effectAllowed = 'copy'`. Drops on non-editor targets now do
  nothing instead of degrading to plain text — acceptable trade-off.
- Native browser drag image is back. The OS drop shadow on the ghost
  is ugly but not a regression worth losing the drag for.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(skills): drop agent-doc skill fetch from useInstalledSkillsAndTools

The earlier commit (331eed1e9c) wired the agent-document skill registry
into `useInstalledSkillsAndTools` by calling the SWR hook directly off
the tool-store selector:

    useToolStore((s) => s.useFetchAgentDocumentSkills)(activeAgentId);

That extra hook indirection — invoking a function selected out of
zustand on each render of the slash-menu consumer — was throwing /
breaking React's hook tracking at render time. The slash menu and every
drag-into-input flow rely on `useInstalledSkillsAndTools` resolving
cleanly, so the breakage cascaded into `/skills` not rendering and
every skill drag silently failing.

Revert to the pre-331eed1e9c shape: only the four already-working
sources (builtin / lobehub / market / user) feed the slash + mention
list. Agent-document skills are still in the tool store (server side
registers them in SkillEngine via `agent-skills:<filename>`) — they
just won't show up in the `/` autocomplete until we hydrate the slice
through a safer path (e.g. an effect in the agent route root, or
shared SWR from the panel).

Drag from the working sidebar continues to work because the wiring is
local to `AgentDocumentsGroup`, not to `useInstalledSkillsAndTools`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 💄 style(skills): restore custom drag image (white floating chip above cursor)

Brings back the cursor-following white rounded chip (icon + name) and
suppresses the native OS drag ghost. Earlier reverted along with the
`text/plain` fallback when we were narrowing down the drag breakage,
but the real culprit turned out to be the `useFetchAgentDocumentSkills`
hook indirection in `useInstalledSkillsAndTools` (fixed in 1ccdfc5821),
not the drag-image code itself.

`text/plain` stays removed — that one really does race with Lexical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:13:18 +08:00
Arvin Xu a27ea18dfb 💄 style(builtin-tool): switch Task inspector copy by phase (#15104)
Inspector chips stay in chat history, so a settled TaskCreate row that still reads "Creating task" looks like the call is still running. Split lobe-claude-code task labels into .loading / .completed pairs and pick based on isArgumentsStreaming || isLoading. Documented the rule in the builtin-tool ui skill so new tools follow the same convention.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:12:21 +08:00
AmAzing- 875e2ffb87 🐛 fix(i18n): add provider description fallbacks (#15103) 2026-05-22 14:18:21 +08:00
LiJian 6953f188c1 feat(platform-agent): openclaw/hermes agent creation UI, device guard, and remote dispatch backend (#15065)
* ♻️ refactor(agent-invocation): add AgentInvocationIntent + unified non-hetero dispatcher (LOBE-8927/8928)

Introduce a shared invocation contract and unified dispatcher for the
non-hetero, non-group agent call paths (callAgent speak mode and @agent
direct mentions). Removes the implicit client-only fallback that existed
in both entry points.

Changes:
- agentDispatcher.ts: add AgentInvocationIntent interface as the unified
  intent type for callSubAgent / callAgent / @agent invocations
- nonHeteroSubAgentDispatcher.ts (new): dispatchNonHeteroSubAgent()
  resolves child runtime via selectRuntimeType and routes to
  executeClientAgent (client) or executeGatewayAgent (gateway);
  throws for hetero (out of scope per LOBE-8926)
- conversationLifecycle.ts #executeDirectMentionRoute: replace hardcoded
  executeClientAgent + TODO fallback with dispatchNonHeteroSubAgent call
- builtin-tool-agent-management executor.ts callAgent speak mode:
  replace hardcoded executeClientAgent + TODO fallback with
  dispatchNonHeteroSubAgent call

Fixes LOBE-8927
Fixes LOBE-8928

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(platform-agent): openclaw/hermes agent creation UI, device guard, and remote dispatch backend

- Add CreatePlatformAgent 3-step creation modal (type select → config → bind device)
- Add RemoteAgentConfigCard to agent profile editor for openclaw/hermes config
- Add device guard banner in HeterogeneousChatInput for offline/unavailable devices
- Add useRemoteAgentDeviceGuard hook for real-time device status polling
- Fix backend dispatch: openclaw/hermes now use executeToolCall(runHeteroTask) instead of dispatchAgentRun (lh connect only handles tool_call_request)
- Add agentNotify router for lh notify → DB write + gateway stream event
- Add device.checkCapability endpoint for platform availability probe
- Add notify_update event type to gateway stream and event handler
- Add sendDoneSignal in heteroTask.ts for clean openclaw exit signaling
- Unify non-hetero sub-agent dispatch via dispatchNonHeteroSubAgent (LOBE-8927)
- Route openclaw/hermes to gateway runtime; keep claude-code/codex on hetero/client paths
- Add i18n keys for platform agent UI and device guard banners

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agentNotify): reuse execAgent placeholder message on first lh notify call

Instead of creating a second empty bubble, the first assistant notify
without a messageId now updates the placeholder assistantMessageId that
execAgent already seeded in runningOperation.assistantMessageId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(agentNotify): cancel openclaw/hermes process on interruptTask

- Store deviceId + heteroType in topic.metadata.runningOperation at dispatch time
- interruptTask now dispatches cancelHeteroTask tool call to the bound device
  when topicId reveals a remote hetero operation, sending SIGINT to the process
- Pass topicId from gateway cancel callback to interruptTask
- Add topicId to InterruptTaskSchema and InterruptTaskParams

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(hetero-agent): consolidate remote/local type classification into heterogeneous-agents package

- Add RemoteHeterogeneousAgentConfig, REMOTE_HETEROGENEOUS_AGENT_CONFIGS, isRemoteHeterogeneousType, and derived type aliases (HeterogeneousAgentType, LocalHeterogeneousAgentType, RemoteHeterogeneousAgentType) to packages/heterogeneous-agents/src/config.ts
- Extend HETEROGENEOUS_TYPE_LABELS to cover remote platform types (openclaw, hermes) via REMOTE_HETEROGENEOUS_AGENT_CONFIGS
- Replace all inline `=== 'openclaw' || === 'hermes'` checks and local Sets/type aliases across aiAgent service, ProfileEditor, HeterogeneousChatInput, useRemoteAgentDeviceGuard, CreatePlatformAgent, RemoteAgentConfigCard, and deviceProxy with the shared utility
- Show OpenClaw/Hermes display name in assistant message model tag (Usage component) by setting provider=heteroType on placeholder message and using HETEROGENEOUS_TYPE_LABELS for rendering
- Fix ReferenceError: move remoteDeviceId declaration before updateMetadata call

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: add the platform agents get profiles

* 🐛 fix(platform-agent): routing, security, and i18n issues from review

- Route openclaw/hermes to gateway on desktop (P1): add isRemoteHeterogeneousType
  check in selectRuntimeType before desktop hetero branch — remote agents never
  use local desktop IPC, no special-casing needed
- Fix race in heteroTask: sendAutoNotify → sendDoneSignal now sequential via
  .finally() so error message is written before agent_runtime_end is published
- Security: validate messageId belongs to topicId in agentNotify before
  MessageModel.update to prevent cross-conversation data corruption
- Clear capability/device/profile state on platform change in creation modal (P2)
- Derive PLATFORM_DEFS from REMOTE_HETEROGENEOUS_AGENT_CONFIGS — new platforms
  automatically appear in the modal without code changes
- Use HETEROGENEOUS_TYPE_LABELS for platform names in HeterogeneousChatInput
  and RemoteAgentConfigCard (remove hardcoded PLATFORM_NAMES map)
- i18n: platform card descs, 'online'/'offline' tags, 'Select a device'
  placeholder, checkFailed error — all now use i18n keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(platform-agent): derive remote platform enum from config + fix test

- device.ts: replace hardcoded z.enum(['hermes','openclaw']) with a
  zod enum derived from REMOTE_HETEROGENEOUS_AGENT_CONFIGS so new
  platforms are automatically covered without touching this file
- heteroTask.ts / getAgentProfile.ts: use RemoteHeterogeneousAgentType
  instead of literal 'hermes' | 'openclaw' union for the same reason
- gateway.test.ts: update cancel-handler assertion to include topicId
  which was added to the interruptTask call in the previous commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat(platform-agent): gate creation entry behind labs flag + expand dispatcher tests

- Add enablePlatformAgent lab preference (default false) — the
  "Add Platform Agent" menu item is hidden until the user opts in
  via Settings → Advanced → Labs
- Wire toggle in settings/advanced with labs i18n key (en/zh)
- createPlatformAgentMenuItem returns null when flag is off
- agentDispatcher.test: add remote hetero cases (openclaw/hermes →
  gateway on both web and desktop) to cover the routing fix added earlier

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(lint): merge duplicate import + sort interface props in nonHeteroSubAgentDispatcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💄 feat(platform-agent): disable Hermes option in creation modal (coming soon)

Hermes is not yet ready for production. Mark it as coming-soon in the
platform selection step: grayed-out card, not clickable, "Coming Soon"
tag next to the name.

To enable Hermes when ready: remove 'hermes' from COMING_SOON_PLATFORMS
in CreatePlatformAgent/index.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  fix(test): mock CreatePlatformAgentModal in ModalProvider.test

The modal always mounts (open=false) and calls lambdaQuery.useQuery
which requires a tRPC context not present in the test environment.
Mock it out the same way as ChatGroupWizard and EditingPopover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  fix(test): mock useUserStore + labPreferSelectors in useCreateMenuItems.test

Adding useUserStore to useCreateMenuItems triggered user store
initialization in tests, which pulled in @lobechat/const and failed
because the existing mock only exports isDesktop. Mock the store and
selectors directly instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(platform-agent): hide divider when platform agent entry is disabled

The divider before 'Add Platform Agent' was unconditional — it showed
even when the labs flag was off. Conditionally include both the divider
and the menu item together so no orphaned separator appears.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 14:04:08 +08:00
Arvin Xu 063c0b7a21 🐛 fix(command-menu): order topic/message search results by recency (#15094)
CommandK search surfaced stale topics/messages because results were ranked
purely by BM25 score across three sort layers that ignored recency:

- SearchRepo: topics/messages were limited to top-N by score, dropping newer
  items entirely. Now fetch a larger candidate pool (limit * 4) by score, then
  order topics by updatedAt DESC and messages by createdAt DESC before slicing.
- SearchRepo.search() / search router: both re-sorted the merged list by
  relevance, undoing the per-type recency order. Drop the relevance sort — the
  command palette groups results by type, so per-type order is what matters.
- cmdk client: with shouldFilter on, cmdk re-ranks items (incl. force-mounted)
  by fuzzy match against the query, overriding server order. Add a custom filter
  that returns a constant for "search-result" items so cmdk's stable sort keeps
  the server order, while built-in commands keep default fuzzy ranking.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 12:58:43 +08:00
Arvin Xu 16b932278e 🐛 fix(chat): persist topic status when run completes after agent switch (#15084)
`updateTopicStatus` looked up the topic via `getTopicById`, which only
searches the *currently active* agent's bucket. When an agent run
finishes after the user has switched to another agent, the topic isn't
in that bucket — the guard bailed early and the DB write was skipped
along with the in-memory dispatch, leaving the sidebar stuck on
"running" forever.

- Discover the owning bucket by scanning `topicDataMap` for the topicId
  (topicIds are globally unique), independent of `activeAgentId`.
- Run the DB write unconditionally so the next refetch picks up the
  persisted status even if no bucket is loaded in memory yet.
2026-05-22 12:57:59 +08:00
Arvin Xu 97111fc99d 🐛 fix(context-engine): guard placeholder log preview against undefined content (#15097)
A tool error result (e.g. budget-exceeded) can arrive with
`content: undefined`. The processor's logging step called
`JSON.stringify(undefined).slice(...)`, which throws because
`JSON.stringify(undefined)` returns `undefined`, not a string — crashing
the whole processor before any message was processed.

Coerce the preview to a string before slicing.

Fixes LOBE-9408
2026-05-22 12:46:52 +08:00
Arvin Xu 219f44c6e8 🐛 fix(agent-tasks): show 404 fallback when task does not exist (#14893)
* 🐛 fix(agent-tasks): show 404 fallback when task does not exist

Previously TaskDetailPage relied on the `isTaskDetailLoading` selector,
which returns true whenever the task is missing from the store map.
When the backend returns NOT_FOUND, the task never enters the map and
the page stays stuck on the loading spinner.

Switch to SWR's `isLoading` + `error` directly and render a NotFound
state (with a Back to all tasks action) when the fetch errored or the
task is still absent after loading completes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-tasks): preserve task detail on transient fetch errors

The not-found check included `!!error`, so any SWR revalidation failure
(focus/reconnect refresh, polling, temporary 5xx/network error) flipped a
cached, valid task to the 404 fallback and removed the editor until the
next successful revalidation.

Key the fallback solely off the absence of cached detail
(`!isLoading && !hasTaskDetail`), so a transient error on an
already-loaded task keeps the editor mounted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:45:23 +08:00
LiJian 99ec113e75 💄 style(community): use landing URL for agent share link (#15099)
Change share URL from app.lobehub.com/community/agent/{id} to
lobehub.com/agent/{id} using the existing AGENTS_OFFICIAL_URL constant.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 12:20:39 +08:00
AmAzing- bf294e2df9 feat(onboarding): show agent welcome guidance (#15098) 2026-05-22 12:02:02 +08:00
YuTengjing 066c77fad7 🐛 fix: disable reasoning for Responses structured outputs (#15092)
* 🐛 fix: disable reasoning for Responses structured outputs

* 🐛 fix: preserve GPT-5 Pro Responses reasoning effort

* 🐛 fix: support GPT-5 Pro-family reasoning defaults
2026-05-22 11:25:39 +08:00
YuTengjing 8c40ff90ea ♻️ refactor: rename proLLM locale key to advancedLLM (#15093) 2026-05-22 11:11:31 +08:00
Innei 029d442992 feat(onboarding): simplify first screen and defer topic creation to first send (#15090) 2026-05-22 11:10:41 +08:00
YuTengjing 422ccc9f58 🐛 fix: bound redis command timeout (#15091) 2026-05-22 11:09:02 +08:00
Arvin Xu 83b8aa5a04 🐛 fix(agent-document): propagate sourceType and dedupe web crawls (#15088) 2026-05-22 08:40:26 +08:00
Arvin Xu e37cca70c5 chore(agent-tracing): resolve partial op id by _remote/ cache prefix (#15015)
*  feat(agent-tracing): resolve partial op id by _remote/ cache prefix

`agent-tracing inspect op_<timestamp>` used to fail with "Snapshot not found"
because the CLI only accepted the full `op_<ts>_agt_..._tpc_..._<suffix>` id.
Now when the input starts with `op_` but isn't a full id, scan the local
`_remote/` cache and resolve a unique prefix match automatically; on multiple
matches, list them and exit so the user can pick the full id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-tracing): preserve FileSnapshotStore fallback for op_ prefixes

The previous commit routed partial `op_<timestamp>` ids straight at the
`_remote/` cache, bypassing `FileSnapshotStore.get(...)`. That meant
in-progress local `_partial/` snapshots (which `FileSnapshotStore.get`
finds via substring match through `getPartial`) were no longer reachable
by prefix; users hit `Snapshot not found` even when the partial existed
on disk. Try the file store first, then fall back to the remote cache
prefix scan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 02:18:26 +08:00
Innei c056760414 feat(tool): archive oversized tool results to VFS instead of truncating (#15074)
* 📝 docs: add tool result archive design

*  feat(tool): archive oversized tool results to VFS instead of truncating

When tool execution results exceed the configured max length, the full
content is now persisted to the agent's VFS under ./.tool-results/ and
the LLM receives a truncated preview with an archive path pointer.

Key changes:
- Add archiveToolResultIfNeeded() to persist oversized results via VFS
- Add skipResultTruncation flag to ToolExecutionContext so the runtime
  can receive full content for archival before truncation
- Add line-range (loc) support to VFS reads for inspecting archived files
- Extend AgentDocumentReadResult with line/char count and loc metadata
- Wire archival into both single-tool and batch-tool executor paths

*  feat(tool-archive): cover webapi client tool path and bypass agent-documents reads

Server-only AgentRuntime archive missed the main webapi chat loop where tool
execution happens in the browser. Route oversized tool results from the client
plugin executors through a new aiChat.archiveToolResult tRPC mutation that
reuses archiveToolResultIfNeeded, so calculator/MCP/klavis/lobehub-skill calls
all archive to the VFS instead of just being truncated.

Flatten the archive layout to ./.tool-results/<topicId>_<toolCallId>.md to dodge
a nested-folder edge case in the VFS resolver, surface the agent_documents.id
in the model-facing hint so the LLM can call lobe-agent-documents.readDocument
directly, and bypass archive entirely for lobe-agent-documents tool results so
reading the archive does not loop back into another archive write.

Also harden truncateToolResult against splitting a UTF-16 surrogate pair: when
the cutoff lands on a high surrogate, step back one code unit so JSON.stringify
no longer emits a lone \\uD83D escape that DeepSeek / Anthropic reject as
'unexpected end of hex escape'.

Includes a small ApprovalMode dropdown placement + trigger styling tweak.

* 🔨 chore: untrack docs/superpowers from git

The path is already excluded by .gitignore line 149; the design spec was only
in the index because an earlier commit forced it in. Remove it from tracking
while keeping the local copy so the ignore rule actually takes effect.

* 🧪 test(truncate-tool-result): exhaustive cutoff sweep over a ZWJ-composed emoji

A single surrogate pair was easy to get right; the real-world worry is ZWJ
sequences like 👨‍👩‍👧‍👦 where four surrogate pairs are stitched with ZWJs
into one grapheme. Sweep every cutoff position across that family emoji and
assert the result never leaves a lone high surrogate and always round-trips
through JSON.stringify / JSON.parse.

* 🐛 fix(thinking): drop stale loading when stream cancelled or ended

Thinking accordion and assistant content loading dot kept spinning after
the user aborted a stream or the run ended without closing the inline
`<think>` tag. Gate the markdown thinking plugins on
`isMessageGenerating(id)` and bail out of `ContentLoading` when no
running operation exists for the message.
2026-05-22 02:07:28 +08:00
YuTengjing aca724c430 🐛 fix: resolve browser model config import (#15089) 2026-05-22 01:46:51 +08:00
AmAzing- b45cb41d4b 🐛 fix(agent-builder): open panel after blank agent creation (#15085) 2026-05-22 01:06:38 +08:00
YuTengjing 736eb570af 🐛 fix: sanitize DeepSeek surrogate payloads (#15086)
* 🐛 fix: sanitize DeepSeek surrogate payloads

* Revert "🔨 chore: add DeepSeek payload diagnostics (#15062)"

This reverts commit d96912dae7.

* 🐛 fix: sanitize DeepSeek Anthropic tool inputs
2026-05-22 00:40:24 +08:00
YuTengjing af785466d1 🐛 fix: add signup email review spend locale (#15082) 2026-05-21 23:36:08 +08:00
Arvin Xu 869f10a44c 💄 style(skills-list): use colorTextSecondary by default with hover swap (#15078)
* 💄 style(skills-list): use colorTextSecondary by default with hover swap

Skill / folder / file name Text in the agent documents explorer rendered as
colorText because @lobehub/ui Text applies its own default color class that
beats the parent container's color. Set inline `color: 'inherit'` so the
existing parent secondary→text hover transition flows through.

* 💄 style(working-sidebar): replace antd Spin with NeuralNetworkLoading

The Space tab's resources loaders used antd's generic Spin dots. Swap to
NeuralNetworkLoading for consistency with the rest of the agent loading
states (content loading, context compression). Inline loader under the
Skills header uses size=24; the full-panel non-hetero loader uses size=32.
2026-05-21 23:25:24 +08:00
YuTengjing 7e78453ae3 🐛 fix: preserve current turn with zero history (#15080) 2026-05-21 23:19:21 +08:00
YuTengjing 874cf39ef3 🐛 fix: add signup email review trigger (#15079)
🐛 fix: add signup email review request trigger
2026-05-21 23:07:29 +08:00
Arvin Xu d3b6f74672 ♻️ refactor(agent-document): derive category server-side, drop frontend predicates (#15076)
* ♻️ refactor(agent-document): derive category + tab flags server-side

Add `category: 'skill' | 'document' | 'web'` plus `isFolder` /
`isSkillBundle` / `isSkillIndex` to `AgentDocumentWithRules` as server-
computed fields and inject them through `projectDocuments` so every
endpoint returning the agent-document shape gets them for free.

Drop the matching frontend categorization predicates (`isSkillBundleItem`,
`isSkillIndexItem`, `isManagedSkillItem`, `isFolderItem`) and the
duplicated `FOLDER_FILE_TYPE` / `SKILL_*` / `AGENT_SKILL_TEMPLATE_ID`
constants from `src/features/AgentDocumentsExplorer/types.ts`. The
remaining relationship helpers (`hasSkillIndexChild`,
`isOrphanSkillBundleItem`, `isProtectedManagedSkillItem`) now read the
server-derived flags directly. UI callers (`AgentDocumentsGroup`,
`DocumentExplorerTree`, `useDocumentTreeOps`, `canDrop`,
`pendingDocument`) switch to the new fields.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* ♻️ refactor(agent-document): consolidate skill taxonomy constants in db schemas

Move SKILL_BUNDLE_FILE_TYPE, SKILL_INDEX_FILE_TYPE, AGENT_SKILL_TEMPLATE_ID
(and the related SKILL_MANAGEMENT_SOURCE / SKILL_INDEX_FILENAME) into
packages/database/src/schemas/file.ts alongside DOCUMENT_FOLDER_TYPE — that
file is already the source of truth for the fileType column values, and
having the constants there lets deriveAgentDocumentFields import them
instead of re-declaring local copies.

src/server/services/skillManagement/constants.ts now re-exports from the
database package, so existing call sites (skillManagementService, the
agent-signal VFS providers, integration tests, etc.) keep their imports
unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* 🐛 fix(deepseek): satisfy thinking input type when disabling reasoning

`ChatStreamPayload['thinking']` now requires `budget_tokens` even when
`type: 'disabled'`. The generateObject test passed a bare
`{ type: 'disabled' }` input and broke `tsgo --noEmit` on CI.

Pass `budget_tokens: 0` in the input — the runtime still strips
`budget_tokens` from the disabled payload (see `index.ts` line 161 in
`buildDeepSeekAnthropicPayload`), so the assertion stays as
`{ type: 'disabled' }`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 23:01:02 +08:00
Innei f8142de9a2 feat(chat-input): add installed skills to slash menu with mid-line trigger (#15061)
 feat: add installed skills to slash menu and support mid-line trigger

- Surface installed skills (builtin / lobehub / market / user agent) in the slash popup, reusing the action tag pipeline shared with @ mention
- Allow `/` to trigger mid-line when preceded by whitespace; in that position only skills are shown (commands stay line-start only)
- Suppress the menu inside paths/URLs (e.g. http://, a/b) by requiring line-start or whitespace before `/`
- Align ActionTag chip with surrounding text via vertical-align
2026-05-21 21:09:43 +08:00
Innei b22ac0f266 feat: drag folders into chat input as @localFile mentions on desktop (#15071)
When the agent's runtime mode is `local` (or it's a heterogeneous agent),
dragging a folder into the conversation now inserts a `<localFile path="..."
isDirectory />` mention at the editor cursor instead of recursively uploading
its contents. Mixed drops route folders to mentions and files to the existing
upload pipeline in drop order.

The drag overlay detects content kind on `dragenter` via `webkitGetAsEntry`
and swaps the title/desc/icon between "Upload Files", "Reference Folder", and
the mixed variant.

Also aligns the @ mention search and server-side local file materialization
gates with the same condition (`isLocalSystemEnabled || isHeterogeneous`)
since `lobe-local-system` plugin presence is already overridden in
toolEngineering — runtime mode is the only real gate.
2026-05-21 21:09:19 +08:00
YuTengjing b358b0b2d1 🐛 fix: handle deprecated runtime models (#15064) 2026-05-21 17:15:06 +08:00
Innei 9fb3038615 🐛 fix(onboarding): enforce response language in server runtime (#14793) 2026-05-21 16:39:26 +08:00
YuTengjing d96912dae7 🔨 chore: add DeepSeek payload diagnostics (#15062)
* 🔨 chore: add DeepSeek Anthropic payload diagnostics

* 🔨 chore: expand DeepSeek payload diagnostics
2026-05-21 16:32:48 +08:00
Innei 56cbf7a3f3 🐛 fix: prevent scrollbar from overlapping ScrollArea content (#15060)
🐛 fix: update @lobehub/ui to version 5.14.1 and add disableContentFit to ScrollArea components

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-21 16:20:57 +08:00
YuTengjing 3680e5efe6 🐛 fix: guard system agent model config (#15058)
* 🐛 fix: guard system agent model config

* 🐛 fix: allow legacy system agent settings

*  test: fix disabled thinking payload type

* 🐛 fix: allow thinking without budget tokens
2026-05-21 16:16:50 +08:00
Arvin Xu e78cbaf945 💄 style(space-panel): split agent resources into Skills / Documents / Web tabs (#15057)
* ♻️ refactor(space-panel): split resources into Skills / Documents / Web tabs

Replace the All / Documents / Web filter on the agent Space panel with
three dedicated tabs (Skills / Documents / Web, default Skills) and give
the Skills tab a folder-style list with expand-to-children rows that
matches the heterogeneous agent's skills panel. Extract the row primitive
into a shared `SkillsList` component so both panels render the same UI.
Skill bundles and their `SKILL.md` index are filtered out of the
Documents tree; web items live on their own tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

*  test(space-panel): mock router and skills empty state in WorkingSidebar test

`AgentDocumentsGroup` now calls `useNavigate`/`useMatch` at the top level
and defaults to the Skills tab, so the parent `AgentWorkingSidebar` test
needs a `react-router-dom` mock and the Skills empty-state i18n key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:05:30 +08:00
Innei 3859b7ca51 🐛 fix(desktop): open settings via main window navigation on Windows/Linux (#15036)
The File → Preferences and Tray → Settings menu items on Windows and
Linux were calling `retrieveByIdentifier('settings').show()`, but no
browser window with the `settings` identifier exists in `appBrowsers`.
Clicking either entry threw `Browser settings not found and is not a
static browser` from `BrowserManager.retrieveByIdentifier`.

Align both platforms with the macOS implementation: show the main window
and broadcast a `navigate` event to `/settings`.
2026-05-21 15:57:10 +08:00
LiJian 9a4c8d5590 🐛 fix: hetero agent cloud credential alert flash and width misalignment (#15056)
🐛 fix: hetero agent alert flash and width misalignment

- Treat `isCredsLoading` as configured in `useHeteroAgentCloudConfig` so the
  "cloud credentials required" alert is hidden during the initial query, preventing
  the flash-then-disappear effect when credentials are already set up.
- Wrap the alert in `WideScreenContainer` in `HeterogeneousChatInput` so its
  width and centering match the chat input below it.

Co-authored-by: LobeHub Bot <bot@lobehub.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 15:40:58 +08:00
YuTengjing 1466d6eb51 feat: support thinking params for structured output (#15051)
*  feat: support thinking params for structured output

* 🐛 fix: scope generate object thinking params

* 💡 docs: clarify generate object thinking scope

* 🐛 fix: forward DeepSeek generateObject effort
2026-05-21 15:27:54 +08:00
YuTengjing ba358bf3fc 🐛 fix: support DeepSeek generateObject tool choice (#15054) 2026-05-21 12:03:59 +08:00
AmAzing- 516a2651f4 💬 chore(onboarding): refine agent setup copy (#15048) 2026-05-21 11:25:26 +08:00
YuTengjing 0911c2a94c ♻️ refactor: load models through model bank slot (#14877)
* ♻️ refactor: load models through model bank slot

* ♻️ refactor: remove static LobeHub model cards

* ♻️ refactor: share OpenAI image parameters

* 🐛 fix: load async LobeHub model config in server paths

* 🐛 fix: repair model bank CI follow-ups

* 🐛 fix: avoid repeated model bank fallback loads

* 🐛 fix: resolve business model config import in browser

* 🐛 fix: align Nano Banana 2 resolution default

* ♻️ refactor: move model loader slot under client

*  test: move model bank aiModels spec out of build entries

* 🐛 fix: use business model config for mixed provider parsing

* ♻️ refactor: consolidate model bank provider utilities

* 🐛 fix: preserve Nano Banana 2 raw resolution

* 🐛 fix: avoid generated locale sync for raw resolution

* 🌐 style: add Nano Banana 2 resolution locales

* 🌐 style: add online LobeHub model locales

* 🐛 fix: guard optional model provider loaders

* 🐛 fix: prevent sitemap build from hanging

* 🐛 fix: clear sitemap timeout after model load
2026-05-21 10:35:14 +08:00
YuTengjing fc088773bd 🐛 fix: configure anthropic client timeout (#15042) 2026-05-21 02:20:29 +08:00
Rdmclin2 1698b7e77d feat: support bot attachments across all platforms (#15029)
* feat: support bot attachments across all platforms

Squashed from feat/support-bot-attachments (15 commits):
- Wechat adapter attachment support (image/video/voice/file via iLink CDN)
- All-platform attachments: Discord, Telegram, Slack, Feishu/Lark, LINE, QQ
- Messager + CLI sendMessage/sendDirectMessage/replyToThread attachment params
- System Bot messenger installs as outbound channels + listOutboundChannels
- Onboarding messager integration + feedback commands
- AI-side attachment ingestion across platforms
- Updated builtin-tool-message systemRole / manifest / types

* chore: unify client and runtime adapter

* feat: support system bot messenger and cli

* chore: remove unnecessary listOutboundChannels

* chore: add test and prompts
2026-05-21 01:14:50 +07:00
AmAzing- b8c4df5a13 feat(onboarding): prefetch agent marketplace templates (#15041) 2026-05-21 01:55:37 +08:00
Innei 7b7690fbb6 ♻️ refactor(desktop): unify TabBar registration into a cross-platform route-meta layer (#14995)
* ♻️ refactor(desktop): unify TabBar registration into a cross-platform route-meta layer

Replace the desktop TabBar plugin registry with route-co-located metadata.

Previously four parallel registries (the RecentlyViewed plugin registry,
routeMetadata.ts, getRouteById icons, and the router config) had to be kept
in sync by hand; forgetting to register a page made its tab silently break.

Now every route declares its metadata once via `handle.meta`:
- New `routeMeta.ts` declaration types + a cross-platform `<RouteMetaBridge>`
  that resolves the active route's meta and drives `document.title`.
- Tab identity moves from semantic ids to normalized URLs (`TabItem`).
- Background-tab titles fall back through a guarded snapshot so cold-start
  store-data gaps never blank or clobber a tab.
- Deletes the 11 plugins, the registry, usePluginContext, routeMetadata.ts
  and cachedData.ts; `<PageTitle>` is removed from the (main) route tree.

*  feat(desktop): define route-meta title for task workspace routes

* ♻️ refactor(settings): create settingsRouteMeta for dynamic tab titles in settings

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor(RouteMetaBridge): enhance dynamic route meta handling and state management

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: scope route meta to tab url

* ♻️ refactor(PopupLayout): remove unused RouteMetaBridge component

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor(route-meta): centralize web title updates

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-21 01:14:53 +08:00
YuTengjing c7976ce7f7 🐛 fix: return bad request for malformed auth JSON (#15038) 2026-05-21 00:56:57 +08:00
AmAzing- 45e07a9584 🐛 fix(onboarding): skip pro settings without Klavis (#15033) 2026-05-21 00:04:19 +08:00
Innei 55623c5661 🐛 fix(onboarding): restore mobile padding on Classic steps (#15032)
* 🐛 fix(onboarding): restore mobile padding on Classic steps

After the layout removed outer padding and inner border on mobile to
let the Agent conversation go full-bleed, Classic step content stuck
to the viewport edges. Add inline padding on the Classic Flexbox for
mobile only; Agent remains full-bleed.

* 💄 style(onboarding): inline chip-row refresh action to prevent title wrap
2026-05-20 21:45:40 +08:00
arya rizky 95c27bd748 fix: add LaTeX extensions (.tex, .sty, .cls, .bib, .bbl) to recognized text file types (#15008)
fix: add LaTeX extensions to recognized text file types

Add .tex, .sty, .cls, .bib, and .bbl to TEXT_READABLE_FILE_TYPES.
These are plain-text UTF-8/ASCII files used in LaTeX documents and should
not be treated as binary by lobe-local-system.

Closes #14917
2026-05-20 20:36:14 +08:00
Innei 67cd059340 🔨 chore: replace husky with native git hooks (#14941) 2026-05-20 20:30:49 +08:00
Innei c261c06098 feat(onboarding): adapt agent onboarding UI for mobile (#15019)
- Welcome.mobile: dedicated mobile greeting, push to bottom, static text (no typewriter)
- NameSuggestions: chips variant for mobile (horizontal scroll, emoji + name only)
- LobeMessage: add align/horizontal/disableTypewriter props, default flex-start
- CompletionPanel: explicit align=center, mobile-friendly sizes and block button
- ModeSwitch: mobile media query — avoid input area via safe-area-inset-bottom
- _layout: remove inner border/radius and outer padding on mobile
- Classic: gate ModeSwitch behind isDev (align with Agent page)
2026-05-20 19:37:05 +08:00
AmAzing- 2b2abca0ae feat(analytics): track create agent modal source (#15028) 2026-05-20 18:45:53 +08:00
YuTengjing 2eb860b59d 🐛 fix: discourage redundant visual tool calls (#15025)
🐛 fix: discourage redundant visual analysis tool calls
2026-05-20 17:19:42 +08:00
Innei 3b3632b419 🐛 fix(chat-input): prevent repeated draft restore (#15024) 2026-05-20 17:12:46 +08:00
YuTengjing b68760d0ca 💄 style: add Gemini 3.5 Flash to LobeHub provider (#15017)
- Add gemini-3.5-flash card to the LobeHub-hosted Google provider
- Fix missing structuredOutput ability on gemini-3.5-flash (google.ts, vertexai.ts)
- Fix missing image/video/audio input pricing units on gemini-3.5-flash,
  which caused multimodal input tokens to be billed at $0
2026-05-20 16:37:07 +08:00
Arvin Xu 71dd287001 ♻️ refactor(creds): remove getPlaintextCred tool to prevent plaintext credential exposure (#14998)
* refactor(creds): remove getPlaintextCred tool to prevent plaintext credential exposure

* refactor(creds): remove getPlaintextCred tool to prevent plaintext credential exposure

* refactor(creds): remove getPlaintextCred tool to prevent plaintext credential exposure

* refactor(creds): remove getPlaintextCred tool to prevent plaintext credential exposure

* refactor(builtin-tool-creds): remove getPlaintextCred from ExecutionRuntime and ICredsService

* refactor(builtin-tool-creds): remove getPlaintextCred from systemRole prompt and local_integration section

* fix(builtin-tool-creds): escape backticks in systemRole template literal
2026-05-20 16:31:37 +08:00
LiJian e87eb8c033 feat(cli): integrate OpenClaw/Hermes hetero-agent dispatch with persistent sessions and notify protocol (#15022)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 16:26:17 +08:00
sxjeru 63ced8167d 💄 style: add new Gemini 3.5 Flash model (#15001) 2026-05-20 14:38:03 +08:00
Innei 7cf5616638 🐛 fix(chat-input): persist unsent input drafts across tab switches (#14992)
* 🐛 fix(chat-input): persist unsent input drafts across tab switches

Switching desktop tabs remounts the conversation route, recreating the
ConversationStore and editor instance and discarding any unsent text.

Persist the editor JSON state per conversation context to localStorage:
save debounced on change (flushed on blur), restore on editor init,
and clear on a successful send. Covers both agent and group main chat,
which share the Conversation ChatInput.

* 🐛 fix(chat-input): flush draft save on unmount
2026-05-20 14:07:12 +08:00
LiJian 621b36e752 🐛 fix(hetero-finish): use heteroCurrentMsgId for lastAssistantContent (#15012)
runningOperation.assistantMessageId is the initial placeholder created at
run start. The persistence handler updates topic.metadata.heteroCurrentMsgId
on each step boundary to track the latest assistant message. Reading from
the initial placeholder produces only first-step content, causing IM to
receive a truncated reply (just the first sentence).

Fix: prefer heteroCurrentMsgId.msgId (when it matches the current operationId)
so BotCallbackService.handleCompletion receives the full final content.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:17:36 +08:00
LiJian c38e6db65c 🐛 fix(market-auth): add prompt=consent to OIDC authorization URL to fix missing refresh token (#15010)
🐛 fix(market-auth): add prompt=consent to OIDC authorization URL

Without prompt=consent the OIDC provider can skip the consent screen on
repeat logins, which causes oidc-provider to silently strip offline_access
from the granted scopes. No offline_access → no refresh_token → users are
forced to re-authenticate once the access token expires.

Co-authored-by: LobeHub Agent <agent@lobehub.dev>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:05:28 +08:00
CanisMinor 3740791573 📝 docs: add ph #1 badge (#15007)
* docs: add ph #1 badge

* docs: add ph #1 badge

* docs: add ph #1 badge
2026-05-20 12:00:22 +08:00
Arvin Xu 61f4bda987 🐛 fix(desktop): prevent App Nap from dropping gateway WebSocket during display sleep (#14994)
* fix(desktop): add powerSaveBlocker when gateway is connected

* fix(desktop): stop powerSaveBlocker on any non-connected status

* test(desktop): add powerSaveBlocker to electron mock in GatewayConnectionCtr tests
2026-05-20 10:49:39 +08:00
Arvin Xu 3bcf6a8d72 ♻️ refactor(agent-settings): consolidate Chat tab into Params popover, drop dead auto-topic feature (#14885)
* 🔥 chore(agent-config): drop dead enableAutoCreateTopic feature

Drop enableAutoCreateTopic + autoCreateTopicThreshold end-to-end. No
business code consumed these fields anymore — only types, defaults,
locale copy, UI form items, agent-builder LLM prompts, and test
fixtures kept the dead config alive.

Sweep:
- types & zod schema (LobeAgentChatConfig, AgentChatConfigSchema, openapi)
- DEFAULT_AGENT_CHAT_CONFIG constant
- locale keys in default + 18 translations
- agent-builder system prompts & tool manifests
- AgentChat form items (auto-topic switch + threshold slider)
- test fixtures & integration tests (replaced sample boolean key in
  parser tests with enableHistoryCount)
- docs/self-hosting env-var examples
- settings.test snapshot

dataImporter JSON fixtures keep the legacy keys on purpose — they
simulate historical user exports and the zod schema strips unknowns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(chat-input): move inputTemplate + autoScroll into Params popover

Surface the User Input Preprocessing template (inputTemplate) and
Auto-scroll During AI Response toggle (enableAutoScrollOnStreaming) in
the chat-input Params popover, alongside compression / history /
max_tokens. Drop the matching form items from AgentChat — the popover
is now the single entry point for these two agent-level preferences.

ControlRow's action prop becomes optional so inputTemplate can render
as a label + TextArea without a Switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔥 refactor(agent-settings): drop AgentChat tab in favor of Params popover

Remove the now-redundant Chat Preferences tab from agent settings:

- delete src/features/AgentSetting/AgentChat/
- drop ChatSettingsTabs.Chat enum and its three registrations
  (useCategory, AgentSettingsContent, profile Content)
- drop agentTab.chat locale key in default + 18 translations
- drop MessagesSquare / MessagesSquareIcon imports that became unused

History/compression/auto-scroll/inputTemplate already live in the
chat-input Params popover, so this tab carried no unique
functionality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(chat-input): surface enableStreaming + reasoning_effort + disabledParams in Params popover

Bring the Model tab's controls into the chat-input Params popover so the
popover can become the single entry point for agent-level params.

- enableStreaming Switch at the top of Advanced (treats undefined as on,
  matching `chatConfig.enableStreaming !== false` in chat service)
- reasoning_effort row after max_tokens (Select tied to
  chatConfig.enableReasoningEffort / params.reasoning_effort, matching
  the agentConfigResolver gating)
- per-model disabledParams filter on the 4 sampling sliders (e.g. Claude
  Opus 4.7 hides temperature/top_p), via aiModelSelectors.modelDisabledParams
- max_tokens defaults to 4096 on toggle-on (parity with AgentModal),
  matching the AgentModal UX
- drop the !enableAgentMode gate on Advanced so agent-mode users still
  reach the model params once the Model tab is gone

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔥 refactor(agent-settings): drop AgentModal tab in favor of Params popover

Now that the chat-input Params popover surfaces enableStreaming,
reasoning_effort, the 4 sampling params (model-aware via
disabledParams), and max_tokens, the Model Settings tab carries no
unique behavior. Remove it:

- delete src/features/AgentSetting/AgentModal/ (index + ModelSelect)
- drop ChatSettingsTabs.Modal enum and its three registrations
  (useCategory, AgentSettingsContent, profile Content)
- drop agentTab.modal locale key in default + 18 translations
- drop BrainCog / BrainIcon imports that became unused
- simplify the profile Content inbox-default fallback to Opening
  (Content menu no longer carried Modal at all)

settingModel.* locale keys are kept — Controls still reads them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-input): keep !enableAgentMode gate on Advanced sampling params

Walk back the gate removal from the prior commit. Agent mode is meant
to manage temperature / top_p / penalties / reasoning_effort itself;
exposing user overrides there contradicts the design.

- Move enableStreaming out of Advanced into the common section so it
  stays visible in both modes (streaming is a UI behavior, not a
  sampling param).
- Re-wrap the SectionHeader + sampling sliders + max_tokens +
  reasoning_effort with `{!enableAgentMode && (...)}`, restoring the
  prior visibility rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:27:35 +08:00
YuTengjing 7144e9de28 🐛 fix: resolve desktop visual media urls (#14989) 2026-05-20 01:16:54 +08:00
Rdmclin2 0195f42daa 🐛 fix: onboarding im integration (#14988)
* feat: support onboarding messager

* chore: remove telegram CN screenshots

* feat: add feedback commands

* fix: bot feedback commands

* chore: optimize messenger intergration

* chore: update onboarding style

* feat: support wechat adapter attachments

* feat: support ai attachments

* chore: update i18n files

* fix: bot message image attachment
2026-05-19 22:51:38 +07:00
Innei 2a66071210 ♻️ refactor(onboarding): streamline discovery to a single profession question (#14987)
* ♻️ refactor(onboarding): streamline discovery to a single profession question

*  test(onboarding): update structured field fixtures
2026-05-19 22:46:46 +08:00
Neko f9b611bc69 🐛 fix(agent-signal,app): anchor agent signal receipts to messages (#14969) 2026-05-19 21:07:36 +08:00
YuTengjing 29623c4ab6 feat(profile): optimistic interests update + clickable auth logo (#14984) 2026-05-19 20:49:41 +08:00
YuTengjing d2d3888f43 🐛 fix(command-menu): promote inline type filters from setSearch (#14986) 2026-05-19 20:46:20 +08:00
Innei e7524c4f1a 🐛 fix(nav): align home sidebar layout (#14974)
* 🐛 fix(nav): align home sidebar layout

* 🐛 fix(nav): preserve sidebar bottom grouping
2026-05-19 20:19:57 +08:00
René Wang 632c1e6c49 📝 docs: add May 19 weekly changelog (#14973) 2026-05-19 19:18:57 +08:00
YuTengjing d3973a5cc0 feat: add chat cost estimate support (#14876) 2026-05-19 19:14:47 +08:00
Innei 6ab1fb2a77 feat(onboarding): add Market Agent Picker as a classic onboarding step (#14980)
*  feat(onboarding): add Market Agent Picker as a classic onboarding step

- Add AgentPickerStep as the final classic onboarding step (step 4)
- Agent onboarding skip now routes to the picker step instead of finishing
- Hide the footer skip link on the classic flow
- Relocate installMarketplaceAgents to src/services for shared use
- Map collected interests to marketplace category hints

* 💄 style(onboarding): widen agent picker step and polish card layout

- Widen the classic picker step container to 780px (other steps stay 600px)
- Left-align the LobeMessage logo to match the title
- Always reserve the agent card check slot to avoid text reflow on select
2026-05-19 18:56:58 +08:00
AmAzing- 6a7a20176a 🐛 fix(agent-builder): open builder panel after prompt creation (#14978) 2026-05-19 18:23:33 +08:00
YuTengjing a91385aabc 🐛 fix: nano banana 4K resolution dropped when aspect ratio is auto (#14977) 2026-05-19 17:30:05 +08:00
YuTengjing 1285f601df 🔨 chore: skip branded provider llm retries (#14975) 2026-05-19 16:58:20 +08:00
LiJian e5c9a1a054 🐛 fix(hetero-agent): fire IM bot-callback webhook from heteroFinish (#14968)
* 🐛 fix(hetero-agent): fire IM bot-callback completion webhook from heteroFinish

When an IM bot triggers a heterogeneous agent (Cloud Claude Code / Codex),
the execAgent hetero early-exit path discards all registered hooks, so the
`bot-completion` webhook registered by AgentBridgeService is never fired
and the IM user never receives a response.

Fix:
- Persist the `onComplete` webhook config into `topic.metadata.runningOperation.completionWebhook`
  when the hetero operation starts, alongside the existing `operationId` / `assistantMessageId`.
- In `heteroFinish`, read the stored webhook and deliver it via the existing
  `deliverWebhook` helper (export it from HookDispatcher), which honours
  QStash vs fetch delivery and resolves relative URLs with APP_URL.
- Add `completionWebhook` to the `runningOperation` Zod schema in the topic
  tRPC router and to the `ChatTopicMetadata` TypeScript interface.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(hetero-finish): fix idempotency + clear runningOperation + import AgentHookWebhook

Three follow-up fixes from self-review of the completionWebhook change:

1. Idempotency — heteroFinish can be called more than once (signal path
   sends cancelled, normal exit sends the real result, transport retries).
   Now reads completionWebhook and clears runningOperation in the same
   block before delivery, so a second call finds runningOperation already
   null and skips the webhook.

2. Clear runningOperation — the normal LLM path clears this field in
   RuntimeExecutors after completion to prevent page-reload reconnects.
   The hetero path never did. Now cleared unconditionally in heteroFinish.

3. Payload order — align with HookDispatcher convention: spread
   hook.webhook.body last so it can override base fields if needed.
   (Was: `{ ...body, hookId, hookType }`. Now: `{ hookId, hookType, ...body }`)

4. Import AgentHookWebhook from hooks/types instead of inlining the type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-finish): skip completionWebhook delivery on cancelled result

heteroFinish can be called twice: once with result=cancelled (from
termination signal) and once with result=success (from normal process exit).
The previous guard cleared runningOperation before delivering, so the first
call (cancelled) would fire the webhook with truncated content, and the
second call (success) would find runningOperation=null and skip delivery —
leaving the IM user with a partial response.

Fix: skip webhook delivery when result=cancelled. The subsequent success
or error call delivers the complete content. Transport-level retries of
the same result are accepted; BotCallbackService reads the latest DB
content on each invocation so duplicate deliveries are idempotent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-finish): include lastAssistantContent and reason in completionWebhook payload

BotCallbackService.handleCompletion checks lastAssistantContent before
sending — without it the handler logs "no lastAssistantContent, skipping"
and returns, leaving the IM user with no reply despite the fix reaching
the delivery point.

Changes:
- Add messageModel field to HeterogeneousAgentService (reused by
  HeterogeneousPersistenceHandler so no extra DB connection)
- Read assistantMessageId from runningOperation before clearing it
- Fetch the final assistant message content via messageModel.findById
- Include lastAssistantContent, operationId, and reason (mapped from
  hetero result: success→done, error→error) in the webhook payload
- Include errorMessage/errorType on error result so handleCompletion
  can render the agent error card
- Spread completionWebhook.body last, matching HookDispatcher convention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(hetero-finish): don't clear runningOperation on cancelled result

When heteroFinish is called with result=cancelled (signal path) followed
by result=success (normal exit), the previous code cleared runningOperation
on the cancelled call. The subsequent success call then found runningOperation
already null, couldn't read completionWebhook or assistantMessageId, and
skipped delivery — leaving the IM user with no final reply.

Fix: early-return on result=cancelled without touching runningOperation,
so the subsequent success/error call still finds the stored webhook config.
runningOperation is only cleared on the delivering call (success/error).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:36:35 +08:00
Arvin Xu 03c79bfb62 🐛 fix: surface stderr in errorOutput fallback and add UNKNOWN_EXEC_ERROR prefix (#14964)
* fix: surface stderr in errorOutput fallback and add UNKNOWN_EXEC_ERROR prefix

When a shell command fails with a non-zero exit code (e.g. git commit
with nothing to commit), the runner puts the error message in stderr
but does not set the error field. This caused errorOutput() to fall
through to the hardcoded 'Tool execution failed' string, losing the
actual error.

Changes:
- errorOutput() now checks state.stderr and state.error before the
  final fallback, so real error messages from stderr are surfaced
- Final fallback changed from 'Tool execution failed' to
  '[UNKNOWN_EXEC_ERROR] Tool execution failed' for easier grepping
- Same prefix applied to toResult() in the executor for consistency

* fix: pass stderr/stdout into errorOutput state for runCommand failures

runCommand() called errorOutput() with a state that only contained
{ error, isBackground, success }, missing result.result.stderr.
Since normalizeResult() stores the shell stderr under result.result.stderr
(not result.error), the state.stderr fallback in errorOutput() was
never reached for non-zero exit commands like 'git commit' with
nothing to commit.
2026-05-19 15:16:28 +08:00
Arvin Xu cf16737668 🐛 fix(local-file-shell): auto-enable hidden matching for dot-prefixed patterns (#14965)
🐛 fix(local-file-shell): auto-enable hidden matching for dot-prefixed glob/grep patterns

When callers passed patterns like `.github/workflows/*.yml` to `globLocalFiles`,
`searchLocalFiles`, or `grepContent`, the underlying engines (`fast-glob` with
`dot: false` and `rg` without `--hidden`) silently skipped dot-prefixed
directories and returned zero results — making it look like the file didn't
exist.

Detect when the pattern explicitly references a hidden segment (`.foo/...` or
`foo/.bar/...`, excluding `./` and `../` relative indicators) and auto-enable
hidden matching. A `hint` field on the result explains the auto-adjustment so
the agent doesn't treat an empty match as failure. The same fix is applied to
the desktop `contentSearch` rg/ag argument builder.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:15:06 +08:00
Innei d6dae46261 🐛 fix(document): reject unsupported file parser types (#14966) 2026-05-19 15:09:42 +08:00
YuTengjing 48ac76815d 🐛 fix: normalize Anthropic-compatible base URLs (#14960) 2026-05-19 14:45:14 +08:00
Arvin Xu d35ee849dd chore: streamline issue triage to core business labels (1-3 per issue) (#14962)
* refactor: streamline issue triage labels

---------

Co-authored-by: lobehubbot <i@lobehub.com>
2026-05-19 13:37:15 +08:00
YuTengjing 391b16e082 ️ perf: optimize chat bootstrap persistence (#14934) 2026-05-19 12:53:32 +08:00
AmAzing- 97ea30e48b 💬 fix(messenger): standardize platform preposition copy (#14959) 2026-05-19 12:40:11 +08:00
YuTengjing fd0d208152 💄 style(subscription): update budget recovery copy (#14875) 2026-05-19 11:44:27 +08:00
Arvin Xu 500a02bd88 🔒 chore: remove compromised actions-cool/issues-helper@v3 (#14956)
* fix: remove compromised actions-cool/issues-helper@v3

* fix: remove actions-cool/issues-helper

* fix: pin actions-cool/issues-helper to safe commit SHA in sync.yml
2026-05-19 11:42:01 +08:00
LobeHub Bot 8ddd8e2cff 🌐 chore: translate non-English comments to English in tests-utils and heterogeneous-agents (#14914) 2026-05-19 10:13:35 +08:00
Arvin Xu 62187d55c5 🐛 fix(portal): make markdown preview scrollable in LocalFile portal (#14919) 2026-05-19 10:11:51 +08:00
AmAzing- c68eb07a91 🐛 fix(sidebar): restore home nav for task workspace (#14945) 2026-05-19 01:46:53 +08:00
Innei 2dc812ac97 ♻️ refactor(onboarding): group chat input feature switches (#14943)
* ♻️ refactor(onboarding): group chat input feature switches

*  test(onboarding): satisfy chat input prop ordering lint
2026-05-19 01:27:42 +08:00
AmAzing- c21076eec4 🐛 fix(tasks): preserve agent context in task routes (#14926) 2026-05-18 22:54:25 +08:00
Innei b3a31ec2ee 💄 refactor(ToolTag): always use filled variant regardless of dark mode (#14937) 2026-05-18 22:04:39 +08:00
Innei c9505f7ea2 feat(follow-up): allow scene-specific model config for follow-up action extraction (#14797)
*  feat(follow-up): allow scene-specific model config for follow-up action extraction

Add optional modelConfig to FollowUpExtractInput so callers (e.g. the
onboarding agent) can specify which model/provider to use for chip
generation instead of always falling back to the generic topic system
agent.

Priority chain: caller-provided config > env overrides > default system
agent config.

*  Use scene model config for follow-up actions
2026-05-18 21:36:38 +08:00
Innei c6d3633337 🐛 fix(desktop): prevent frequent logout from token refresh retry (#14928)
* 🐛 fix(desktop): prevent frequent logout from token refresh retry

The OIDC server rotates refresh tokens and revokes the whole grant when a
consumed refresh token is reused. The desktop refresh wrapper retried the
token request up to 4 times reusing the same stored refresh token, so any
failure after the server had already consumed it (lost response, timeout,
parse error) guaranteed an invalid_grant on the next attempt and logged the
user out.

- RemoteServerConfigCtr: drop the in-line retry — refresh is now a single
  attempt; transient failures recover on the next refresh cycle
- AuthCtr: refresh proactively only when the access token is near expiry
  instead of on every launch/activation, cutting refresh-token rotations
  from dozens a day to roughly one a week
- remove the now-unused async-retry dependency

* 🐛 fix(desktop): use a small buffer for proactive token refresh checks

isTokenExpiringSoon() defaults to a 24h buffer. An OIDC server issuing
access tokens with a lifetime <= 24h would be treated as "expiring soon"
right after login, refreshing on every launch/activation and recreating
the refresh-token rotation churn this branch removes.

Pass an explicit 10-minute buffer at all three call sites (auto-refresh
timer, startup init, app activation) so the behaviour no longer depends
on the server's access-token lifetime.
2026-05-18 20:17:19 +08:00
Innei ae4145ba12 🐛 fix(desktop): restore route after update restart (#14922)
* 🐛 fix(desktop): restore route after update restart

When the desktop app installs an update and restarts via quitAndInstall, the main window always reloaded path '/', dropping whatever route the user was on. Capture the active route in installNow() and restore it on the next launch (consume-once).

* 🐛 fix(desktop): consume update restore route once
2026-05-18 19:50:12 +08:00
LiJian 8a2d05d64e 🐛 fix(market): map getUserByUsername 404 to NOT_FOUND instead of 500 (#14929)
🐛 fix(market): map 404 from market API to NOT_FOUND instead of 500

When a user hasn't set up a market username yet, getUserByUsername returns
404 — an expected first-login scenario. The backend was wrapping this as
INTERNAL_SERVER_ERROR (500), causing SWR to retry 3× per component and
flooding server logs with false-alarm 500s.

- server: catch MarketAPIError status 404 and re-throw as TRPCError NOT_FOUND
- client: add shouldRetryOnError to useMarketUserProfile so SWR does not
  retry on NOT_FOUND, eliminating log noise from UserAvatar / MarketAuthProvider

Co-authored-by: LobeHub Bot <bot@lobehub.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:48:46 +08:00
LiJian d359a83ade 🐛 fix: wire server-side exec_task/exec_tasks for callAgent async mode (#14913)
* 🐛 fix: wire server-side exec_task/exec_tasks for callAgent async mode

When a parent agent runs as a server-side QStash task and calls
`lobe-agent-management.callAgent(agentId, { runAsTask: true })`, the
sub-agent was silently never spawned.

Root cause (three missing links):
1. `RuntimeExecutors.ts` `call_tool` did not set `stop: true` in the
   `tool_result` payload when the tool returned an `execTask`/`execTasks`
   state, so `GeneralChatAgent` fell through to the normal LLM-call path
   instead of emitting an `exec_task` instruction.
2. No `exec_task` / `exec_tasks` executor existed in `RuntimeExecutors.ts`,
   so even if the instruction had been emitted the runtime would have thrown
   `No executor found for instruction type: exec_task`.
3. `AiAgentService` did not inject an `execSubAgentTask` callback into
   `AgentRuntimeService`, so the executors had no way to spawn the child
   operation.

Fix:
- Detect `execTask` / `execTasks` state type in `call_tool` and forward
  `stop: true` so `GeneralChatAgent` routes correctly.
- Add server-side `exec_task` and `exec_tasks` executors that create a
  task message and fire `execSubAgentTask` via an injected callback, then
  return a `task_result` / `tasks_batch_result` context so the parent agent
  can do a final LLM summary call.
- Extend `AgentRuntimeServiceOptions` with `execSubAgentTask` callback and
  propagate it through the executor context.
- Wire `this.execSubAgentTask` into `AgentRuntimeService` from
  `AiAgentService` constructor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor: simplify execSubAgentTask injection + sync canary renames

- Remove bespoke ExecSubAgentTaskCallbackParams interface; reuse
  ExecSubAgentTaskParams from @lobechat/types directly (structurally
  identical, avoids duplication)
- Use this.execSubAgentTask.bind(this) instead of lambda wrapper in
  AiAgentService constructor
- Sync instruction/state type renames from canary:
    exec_task → exec_sub_agent
    exec_tasks → exec_sub_agents
    execTask state → execSubAgent
    execTasks state → execSubAgents
    task_result phase → sub_agent_result
    tasks_batch_result phase → sub_agents_batch_result
    AgentInstructionExecTask → AgentInstructionExecSubAgent
    AgentInstructionExecTasks → AgentInstructionExecSubAgents

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  test: add unit tests for server-side exec_sub_agent executor

Three cases covering the callAgent async fix:
1. call_tool sets stop:true when tool returns execSubAgent state
2. exec_sub_agent creates task message + calls execSubAgentTask callback
3. exec_sub_agent gracefully skips dispatch when callback not injected

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(exec-sub-agent): report actual dispatch outcome instead of callback existence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(test): add as const to toolCalling.type to satisfy ToolManifestType

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:45:37 +08:00
CanisMinor 519e755aff 📝 docs: LobeHub Your Chief Agent Operator (#14924)
* style: update readme

* style: update readme

* style: update readme
2026-05-18 15:09:47 +08:00
Arvin Xu 652005ed21 🐛 fix(agent-signal): isolate memory-agent messages into a child thread (#14921) 2026-05-18 14:47:16 +08:00
Tsuki 27f97b2e52 🐛 fix(agent-tasks): prevent schedule pill from wrapping in Kanban card (#14923)
The schedule pill (TaskTriggerTag in tag mode) had a fixed 24px height
but no single-line constraint on its inner Text, so long descriptions
like "每周 日/一/二/六 09:00 运行" wrapped to two lines and broke the
row layout in the Kanban card. Force single-line + ellipsis truncation
and let the existing tooltip surface the full string + timezone.

Also hoist inline style objects to module scope so React.memo on
Block/Flexbox/Text isn't defeated as the Kanban re-renders many cards.

Fixes LOBE-9149

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:30:12 +08:00
Rdmclin2 6f42386345 🐛 fix: sidebar new agent (#14920)
fix: sidebar new agent
2026-05-18 11:54:10 +07:00
lobehubbot 694a25822f 🔖 chore(release): release version v2.2.0 [skip ci] 2026-05-18 04:43:53 +00:00
lobehubbot 1792752231 Merge remote-tracking branch 'origin/main' into canary 2026-05-18 04:42:33 +00:00
Arvin Xu 46818e9571 🚀 release: v2.2.0 (#14915)
# 🚀 LobeHub Release (20260518)

**Release Date:** May 18, 2026  
**Since v2.1.58:** 208 merged PRs · 209 commits · 16 contributors

> v2.2.0 introduces the **Chief Agent Operator** — an agent that runs
itself end-to-end. It self-iterates against its own output, assembles
sub-agent teams on demand through the heterogeneous runtime, and drives
a unified task system that knows when to pause for a human. Self-review,
AssistantGroup, and tasks/scheduling all converge into one operator
surface.

---

##  Highlights

### 🎩 Chief Agent Operator

- **Self-iteration exits Lab** — Agent Signal's self-review pipeline
ships proposal actions straight into briefs and auto-executes the
approved follow-ups, with prompts hardened against eval. The operator
now critiques and re-runs its own work without a human in the loop.
(#14769, #14583, #14647, #14882)
- **Auto-formed agent teams** — Heterogeneous AssistantGroup gains
Monitor-style signal callbacks, read-only SubAgent threads with
breadcrumb headers, and a thread switcher. The operator dispatches
sub-agents and you can step into any branch to see what the team is
doing. (#14859, #14658, #14845, #14715)
- **Task system as the operator's runway** — Claude Code surfaces task
tools, AskUserQuestion freeform notes, and a dedicated `waitingForHuman`
topic status; `lobe-task` exposes `setTaskSchedule`; the scheduler is
hardened (maxExecutions cap, sub-10min heartbeat block, race-free
SchedulerForm). Long-running operator runs no longer go silent and stop
themselves when human input is needed. (#14870, #14639, #14713, #14865,
#14853)

### 🚀 Cloud & runtime

- **Cloud Claude Code V3** — Repo picker, GitHub token flow, and
sandbox-aware context bring cloud-hosted Claude Code to feature parity
with local; cloud sandbox completion now triggers the task lifecycle
end-to-end. (#14568, #14822, #14681)
- **Heterogeneous agent multi-replica safety** — Subagent threads,
ingest refresh, and parallel-tool counts now survive replica swaps
without losing parent_id or rolling back tool state. (#14897, #14631,
#14806, #14838)
- **Built-in tool lifecycle hooks** — `onBeforeCall` / `onAfterCall`
land on the built-in tool runtime; sub-agent dispatch moves to
`lobe-agent`; self-iteration aligns with the shared inspector pattern.
(#14719, #14715, #14827)
- **Knowledge base RAG unified** — Client and server share one
`KnowledgeBaseSearchService`; KB files preserved on `NoSuchKey` instead
of silently lost. (#14673, #14501)

### 💬 Workspace experience

- **Home daily brief + recommendations** — The home screen opens with a
linkable welcome, paired input hint, and a recommendations module
sourced from the operator's hetero action library. (#14589, #14645,
#14770)
- **Chat mode + redesigned action bar** — The chat input gains a
Chat/Agent mode toggle and a re-pitched action bar with icon-and-color
action tag chips. (#14774, #14903, #14846)
- **Documents tree, optimistic** — Document tree creates, deletes, and
inline renames now apply optimistically; the agent-documents index hides
web crawls and switches to a table layout. (#14714, #14292)
- **Branded MCP inspectors** — Linear MCP tool calls render with the
same branded inspector as the built-in Linear skill; CC MCP and built-in
skills now share inspector code. (#14864, #14884)
- **Bot identity gating** — Device tools are gated by sender identity,
the activator bypass is closed, and Slack mpim plus Discord DM
regressions are fixed. (#14634, #14664, #14733)

---

## 🏗️ Core Agent & Signal Pipeline

### Self-iteration & Agent Signal

- Self-iteration graduates out of Lab, with service, tool, name, and
concept structure unified across `agent-signal`, `prompts`, `database`,
and `builtin-tool-self-iteration`. (#14699, #14769)
- Self-review now proposes actions to briefs and auto-executes the
approved set, with eval-verified prompt hardening. (#14583, #14657,
#14647)
- Self-iteration built-in tool aligns with the shared runtime +
inspector patterns. (#14827)
- Agent Signal prompts adapt their response language and avoid blocking
agent execution. (#14890, #14775, #14882)
- Receipt descriptions now carry an Agent Signal marker, and self-review
hinted skill documents route correctly. (#14764, #14895)

### Heterogeneous agent runtime

- Subagent threads render read-only with a breadcrumb header and thread
switcher; SUBAGENT badge dropped, indentation tightened. (#14658,
#14845, #14783)
- Multi-replica safety: ingest refresh restores tools/model from DB to
fix parent_id breaks; new-step assistants sync across replicas;
subagent-tagged events no longer leak into the main gateway handler.
(#14897, #14631, #14838)
- Fetch-triggering events are deferred to keep parallel tool counts from
rolling back. (#14806)
- AskUserQuestion is wired for Claude Code, with auto-decline disabled
and a freeform note input on the cloud side; `waitingForHuman` is a
first-class topic status. (#14639, #14629, #14870)
- AssistantGroup gains Monitor-style signal callbacks; project skills
surface in the working sidebar and markdown preview. (#14859, #14896)
- Cloud Claude Code V3 — repo picker, GitHub token, sandbox context;
credentials alert and disabled input when not configured. (#14568,
#14822)
- Cloud sandbox completion now triggers the task lifecycle end-to-end.
(#14681)

### Agent runtime & context engine

- Built-in tool runtime gets `onBeforeCall` / `onAfterCall` lifecycle
hooks. (#14719)
- `CompletionLifecycle`, `HumanInterventionHandler`, and
`stepPresentation` are extracted from the runtime monolith. (#14441)
- Per-tool timeout is honored end-to-end for client tool dispatch.
(#14817)
- Compression budget accounts for `tool_calls`, reasoning content, and
tool defs; `call_llm` forwards tools into the budget. (#14813, #14837)
- Pre-flight context check now fails fast for OpenAI-compatible
providers. (#14824)
- Malformed `tool_call` names are recovered instead of finishing the
step silently. (#14577)
- Sub-agent dispatch moves from `lobe-gtd` to `lobe-agent`. (#14715)
- Hidden built-in tools now appear in the system prompt @-mention list.
(#14823)

### Agent tracing & operations

- New `agent_operations` table and runtime persistence for every
hetero-agent operation. (#14416, #14736)
- `signOperationJwt` issues 4-hour signed operation tokens. (#14586)
- S3 trace snapshots are zstd-compressed; DB `trace_s3_key` aligns with
the `.json.zst` suffix; legacy `.json` fallback preserved on fetch.
(#14807, #14860, #14826)

---

## 📱 Platform & Integrations

### Bot / Channels

- Device tools are gated by sender identity. (#14634)
- Activator bypass closed and device-access checks converged. (#14664)
- Slack mpim supported; Discord DM regression fixed; Slack connect +
slash commands repaired. (#14733, #14591)
- Bot channels, bot watch, bot callback service, and system bot
reliability fixes. (#14847, #14796, #14570, #14784, #14649)
- Online Messager scaffolding. (#14755)

### Onboarding

- Home daily brief with linkable welcome and paired input hint. (#14589)
- Recommendations module sourced from the hetero agent action library.
(#14645)
- Chat onboarding passes request triggers via metadata and preserves the
resume request. (#14770, #14798)
- Discovery turn progress gated by phase, with a reminder on stalled
discovery. (#14842, #14833)
- FullNameStep back button rejoins the shared prefix; ModeSwitch hidden
in production. (#14898, #14760)
- Agent marketplace folds into the web onboarding tool. (#14578, #14672)
- Onboarding interests stored as keys instead of free text; early-exit
skips marketplace and drops CJK prompts. (#14624, #14598)

### Model providers

- Gemini 3.1 Flash-Lite cards; Gemini schema sanitizer drops
non-compliant `enum` / `required`; zero `cachedContentTokenCount`
handled in usage conversion. (#14604, #14740, #14567)
- DeepSeek-V4 model cards and pricing restored to official rates.
(#14110, #14911)
- ernie-5.1 and spark-x2-flash support; Grok 4.3 `reasoning_effort`
support. (#14643, #14731, #14642)
- SiliconCloud catalog synced with API; duplicates removed; reasoning
params adjusted. (#14464)
- Minimax derives `max_tokens` from context window to avoid
`ExceededContextWindow`. (#14814)
- aihubmix uses the full models endpoint for a complete list; stale
empty-apiKey test dropped. (#14511, #14669)
- Stream parse errors are enriched with provider + model context.
(#14636)
- Visual content parts are consumed in the server runtime; video image
references move to a JSON object. (#14637, #14900)
- Google function call magic `thoughtSignature` now attaches to every
part, not just the last turn. (#14904)
- Service model assignments settings added; model extend-param options
removed. (#14712, #14607)

### Built-in tools & knowledge base

- `lobe-task` exposes `setTaskSchedule`; task scheduler hardened
(maxExecutions cap, sub-10min heartbeat blocked, SchedulerForm race fix,
rapid automation-mode toggle stabilized). (#14713, #14865, #14853,
#14801)
- KnowledgeBaseSearchService shares RAG runtime across client and
server. (#14673)
- KB files preserved on `NoSuchKey` and orphan documents/tasks cleaned.
(#14501)
- Document tree gets optimistic create/delete + inline rename. (#14714)
- agent-documents index hides web crawls and switches to a table layout.
(#14292)
- `lobe-clarify` and SKILL.md frontmatter parsing/edit validation are
unified. (#14566)
- AnalyzeVisualMedia inspector + Portal HTML preview refactor; HTML
preview restored for AssistantGroup messages. (#14777, #14811)
- Branded inspector shared between CC MCP and built-in Linear skill.
(#14884, #14864)

---

## 🖥️ CLI & User Experience

### Chat & Conversation

- Chat mode toggle and redesigned chat input action bar. (#14774)
- Action tag chips switch to icon + colored label; ActionDropdown closes
on sibling-open and focus-out; submenu uses native header/footer slots.
(#14903, #14802, #14901)
- Action bar padding equalized around the send button; skeleton shows in
action bar while config loads. (#14846, #14656)
- `useCmdEnterToSend` is respected in thread & task inputs; send button
enables after pasting into thread/comment input. (#14850, #14816)
- TopicChatDrawer state preserved during close animation. (#14803)
- Only the last assistant block animates during markdown streaming.
(#14906)
- Right working panel no longer auto-collapses on chat mount; home agent
config fetched so knowledge toggles reflect in UI. (#14883, #14834)

### Tasks

- Task scheduler, hotkey, comment, and TodoList polish. (#14707)
- Add Subtask button & card baseline aligned; activity card stop run;
task agent manager polish. (#14848, #14559, #14569)
- Task template skeleton CLS reduced; task page placeholder copy
refreshed. (#14788, #14704)
- Task agent model snapshotted into `task.config` at create time.
(#14670)
- User-feedback card, task card polish, and Run-now context menu in
markdown. (#14727)
- Inline skill auth in recommended task templates. (#14676)

### Navigation & Layout

- Tab bar gains a Chrome-style divider between inactive tabs. (#14892)
- SideBarDrawer & header layout polish; nav ActionIcon sizing unified;
TodoList encapsulation improved. (#14762, #14692)
- Desktop header icons, sidebar density, and task menus polished.
(#14724)
- Standardized header action icon sizes. (#14717)
- Chat topic title length increased; copy session ID added to topic
dropdown menu. (#14659, #14595)
- Heterogeneous agent topic rows regain indentation. (#14783)

### Other polish

- Usage token details shortened; tool execution time formatted as `Xmin
Ys`. (#14849, #14641)
- Tool arguments display gets word-wrap toggle; long tool-call params
wrap instead of truncate. (#14706, #14640)
- Editor stops showing per-line placeholder once content is present.
(#14852)
- Visible divider between queued messages; intervention confirmation bar
polished. (#14593, #14587)
- Credit top-up copy refreshed; auth captcha retry copy refreshed; brief
recommendations layout polished. (#14821, #14561, #14871)

---

## 🔧 Tooling & Developer Experience

- Dev-only feature flag override panel. (#14565)
- `__DEV__` define replaces `process.env.NODE_ENV` in the SPA. (#14696)
- Agent-settings drops Meta/Documents tabs and restores `inputTemplate`.
(#14874)
- `local-system` forwards all `grepContent` params and moves the
executor to `/client`. (#14888)
- `lobe-task` and `setTaskSchedule` exposed. (#14713)
- Memory user-memory benchmark agent config and source-id extraction
schemas. (#14779, #14778)
- CLI man page drops stale cron entry; `clearMessages` hotkey removed.
(#14709, #14906)
- Skill docs simplified; cloud heteroContext gains sandbox TTL +
public-repo fork push guide. (#14785, #14761)

---

## 🔒 Security & Reliability

- **Security:** Sensitive comments and examples sanitized from the
production JS bundle. (#14557)
- **Security:** Inactive OIDC access rejected. (#14674)
- **Security:** CASC `new Function()` template replaced with safe string
builders. (#14751)
- **Security:** Sign-in captcha flow removed in favor of safer flow.
(#14573)
- **Security:** Desktop local file previews restricted to safe roots.
(#14789)
- **Security:** Image binary capped at 3.75 MB so base64 payload stays
under the Anthropic 5 MB limit. (#14711)
- **Reliability:** Neon/Node pools get error listeners to prevent Lambda
crashes. (#14606)
- **Reliability:** `paradedb.match(...)` replaces hardcoded normalizer
in memory search. (#14590)
- **Reliability:** `PlaceholderVariablesProcessor` errors carry
diagnostic context. (#14741)
- **Reliability:** File storage upload checks are serialized; multiple
account link bug fixed. (#14829, #14562)
- **Reliability:** `ScrollShadow` replaced with `ScrollArea` to fix a
React infinite render loop (error code 185). (#14689)
- **Reliability:** Embedding token cap enforced — long memory queries
are limited and truncated before search. (#14757)
- **Reliability:** Embed binary blob guard + oversized output cap in
`local-system.readFile`. (#14602)
- **Reliability:** Windows npm CLI shims resolved before spawning
agents. (#14772, #14720)
- **Reliability:** Vite pinned to 8.0.12 to avoid the rolldown 1.0.1
preload regression; desktop runtime externals split from native deps.
(#14804, #14776)
- **Reliability:** Old lobehub cron job removed; WeChat URL rules
dropped from web crawler. (#14630, #14633)

---

## 👥 Contributors

Huge thanks to **16 contributors** who shipped **208 merged PRs** this
cycle.

@hezhijie0327 · @sxjeru · @hardy-one · @Bianzinan · @brone1323 · @YuSaZh
· @Wxh16144 · @arvinxx · @Innei · @tjx666 · @Neko · @LiJian · @Rdmclin2
· @sudongyuer · @AmAzing129 · @rivertwilight

Plus @lobehubbot for maintenance translations.

---

**Full Changelog**:
https://github.com/lobehub/lobe-chat/compare/v2.1.58...v2.2.0
2026-05-18 12:41:47 +08:00
AmAzing- d6b5e81a57 🐛 fix(agent-signal): persist memory receipt routing metadata (#14912) 2026-05-18 11:41:33 +08:00
YuTengjing e5666882d4 💄 style(pricing): restore DeepSeek models to official pricing (#14911) 2026-05-18 11:05:47 +08:00
Arvin Xu 469a8e6661 🐛 fix(conversation): animate only the last markdown block + drop clearMessages hotkey (#14906)
* 🐛 fix(conversation): animate only the last assistant block markdown streaming

Switch `withMarkdownStreamingState` from disabling the first block to
disabling every block except the last one. The previous logic let middle
blocks keep `animated=true` during generation, so any remount mid-stream
replayed the typewriter from scratch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔥 chore(hotkey): remove clearCurrentMessages shortcut

Drop the Alt+Shift+Backspace binding from the chat scope. The eraser
button in ActionBar still works; only the keyboard shortcut, registry
entry, hotkey i18n and docs row are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 10:59:13 +08:00
Arvin Xu 7798e4b0b5 💄 style(chat-input): switch action tag chips to icon + colored label (#14903)
* 💄 style(chat-input): switch action tag chips to icon + colored label

Replace the filled Tag chip with an inline icon + colored label so skill
and command references read like prose instead of UI badges.

- Use SkillsIcon for skill / projectSkill (both green via colorSuccess)
- Use TerminalIcon for command (cssVar.purple token, theme-aware)
- Use WrenchIcon for tool (cssVar.colorInfo)
- Preserve selection outline on .selected for the editor

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(chat-input): rename ActionTagView to ActionMention

The component no longer renders a Tag chip — it renders an inline icon
with colored label representing a mentioned/inserted action reference.
"Mention" matches how these are inserted in the editor (via slash menu or
@-mention) and reads better in the user-message renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(chat-input): drop borders on @mention and @topic chips

@-mention (from `@lobehub/editor`) and @-topic refer chips both had
outlined borders; switch them to a borderless filled look so they sit
quietly inline with surrounding text — matching the new ActionMention.

- `ReferTopicView`: `variant="outlined"` → `variant="filled"`
- Add `mentionFilledClassName` (`.editor_mention { border: none }`) and
  apply it on both the editor (`InputEditor` className) and the rendered
  user message (`RichTextMessage` LexicalRenderer className) so input
  and read-back look the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(agent-sidebar): allow message channel for Claude Code hetero agents

Codex and other hetero providers still hide the channel entry; Claude Code agents can now use it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-input): satisfy strict types for icon map and mention className

CI failures from the previous commits:

- `ActionMention` typed CATEGORY_ICON as `ComponentType<any>` which is a
  superset of `LucideIcon | FC<any> | ReactNode` accepted by `<Icon>` —
  narrow to `FC<any>` so SkillsIcon and lucide icons type-check.
- `mentionFilledClassName` was a `SerializedStyles` from `css\`\``; wrap
  in `cx()` so it serializes to a `string`, which `LexicalRenderer`'s
  `className` prop requires.
- Update `Nav.test.tsx` mock to expose the new
  `currentAgentHeterogeneousProviderType` selector that landed in 89d7515.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): keep reasoning state live during gateway streaming

The gateway event handler only accumulated reasoning text into `message.reasoning`
without ever creating a `type: 'reasoning'` operation, so `isMessageInReasoning`
was always `false`. The Thinking UI then rendered the "已深度思考" completed title
and stayed collapsed for the entire stream. Mirror `StreamingHandler`'s lifecycle:
start a reasoning sub-op on the first thinking chunk and end it on text /
tools_calling / stream_end / stream_start (next step) / agent_runtime_end / error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 03:03:48 +08:00
Arvin Xu 654035e7b0 🐛 fix(google): add magic thoughtSignature to all functionCall parts, not just last turn (#14904)
Previously the magic signature was only applied when the last message was a
tool message and only to functionCall parts after the last user message. This
missed cross-provider scenarios (e.g. OpenAI GPT-5 → Gemini switch) where
historical tool_calls lack thoughtSignature, causing Gemini API warnings:

  Function call is missing a thought_signature in functionCall parts.

Now we unconditionally iterate all model-role contents and add the magic
signature to any functionCall part that doesn't have one, ensuring Gemini's
thought signature validator is always satisfied regardless of conversation
history origin.

See LOBE-8662
2026-05-18 02:38:02 +08:00
Innei eb39f193c9 ♻️ refactor(chat-input): adopt native submenu header/footer slots for skill menu (#14901)
* ♻️ refactor(chat-input): adopt native submenu header/footer slots for skill menu

The skill menu in the Plus dropdown pinned its search bar and stats footer as faux menu items held by position:sticky CSS hacks (data-fixed-menu-footer / data-skill-menu-search / data-skill-stats). @lobehub/ui 5.14.0 adds native header/footer slots to submenu popups, so move the search bar and stats row onto those slots and drop the hacks.

* ♻️ refactor(knowledge-controls): integrate footer into useControls and update PlusAction to utilize new structure

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-18 00:55:49 +08:00
YuTengjing 7e514ac3e3 🐛 fix: use JSON object for video image reference (#14900) 2026-05-18 00:55:29 +08:00
Zhijie He f3f2bda880 💄 style: add ernie-5.1 support (#14643) 2026-05-18 00:44:49 +08:00
Arvin Xu 6434ee9a5d 🐛 fix(agent): stop auto-collapsing right working panel on chat mount (#14883)
* 🐛 fix(agent): stop auto-collapsing right working panel on chat mount

ChatConversation had a mount effect that forcibly toggled showRightPanel
off whenever status init completed, so switching to a new topic (which
remounts the route subtree) would close the user's Workspace panel.
Drop the effect and default showRightPanel to false instead — the
persisted user preference is now the single source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent): keep right-panel toggles usable before status hydration

INITIAL_STATUS.showRightPanel now defaults to false, which means
WorkingPanelToggle / ToggleRightPanelButton / ParamsPanelToggle render
their "open" button during the pre-hydration window. But
updateSystemStatus bails early while isStatusInit is false, so the very
first click was silently dropped and the panel stayed closed even after
hydration when storage was empty.

Defer rendering these toggles until isStatusInit flips true so a click
can never land in the no-op window. Also fix the
action.test.ts > toggleRightPanel > should toggle chat sidebar case,
which was passing only because the old default was true; it now hydrates
the store before asserting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent): stop overwriting working-sidebar tab when reopening panel

WorkingPanelToggle unconditionally set storedTab='review' on every
click, so any Space/Files preference the user had clicked previously
got clobbered the next time they re-opened the right panel — most
visibly on hetero CC sessions where the intended default is Space.

The toggle now just toggles the panel open; the sidebar's own
resolveActiveTab handles defaulting (hetero → Space, otherwise → last
explicit click, then Review/Files based on local-system availability).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:44:14 +08:00
Arvin Xu b52ff52949 🐛 fix(hetero-agent): restore tools/model from DB at ingest refresh to fix multi-replica parent_id breaks (#14897)
* 🐛 fix(hetero-agent): restore tools/model from DB at ingest refresh to fix multi-replica parent_id breaks

In prod a topic with 11 step boundaries produced 4 assistants whose
parentId pointed at the previous assistant instead of the previous tool
message — same in-memory state.toolState gets reset at the end of every
handleStepStart, so if the next step's tools_calling lands on a different
replica, this replica stays empty and the following step boundary falls
back to currentAssistantMessageId. Two of the four also had
model=null/provider=null for the same reason: handleTurnMetadata only
cached lastModel/lastProvider in memory.

Adopt DB as authoritative at the ingest() refresh: replace
state.toolState wholesale when DB has more tools or more result_msg_ids
than memory, and restore state.lastModel/lastProvider from the refreshed
assistant row. Also extend handleTurnMetadata to persist model/provider
to DB (previously only metadata.usage was written), so the refresh path
has something to recover from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): never mark unresolved restored tools as persisted

Three sites that hydrate `state.toolState` from DB-side `assistant.tools[]`
were unconditionally pushing every id into `persistedIds`:

- `ingest()` refresh (newly added in the prior commit on this branch)
- `loadOrCreateState` (cold replica boot)
- `syncAssistantPointerForAdvancedStep`

`persistToolBatch` writes `tools[]` in Phase 1 BEFORE creating the
`role:'tool'` row in Phase 2 and backfilling `result_msg_id`. A replica
that hydrates between those two phases sees an unresolved id; marking it
as persisted then causes a follow-up retry of the same tools_calling
event to fall out of `freshForCreate`, skip Phase 2, and rewrite the
unresolved `tools[]` unchanged — leaving the tool permanently without a
tool message / result_msg_id.

Restore only ids whose `result_msg_id` is already set. Unresolved ids
stay re-createable so the BatchIngester's outer retry can complete the
write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 23:48:26 +08:00
Arvin Xu 4766bb3eb3 feat(hetero): surface project skills in working sidebar + markdown preview (#14896)
*  feat(hetero-cc): surface project skills in working sidebar + markdown preview

When the active agent is a heterogeneous Claude Code session, the Space tab
now lists skills discovered under `<cwd>/.agents/skills/` (with a fallback
to `<cwd>/.claude/skills/`). Each row shows the skill's frontmatter name,
file count, and a chevron to expand a peek at the bundle contents; clicking
the name opens `SKILL.md` in the LocalFile portal, and clicking a child
file opens that file directly.

The LocalFile portal also gets a Preview / Raw toggle for `.md` / `.mdx`
files — frontmatter is now parsed and the YAML block stripped from the
rendered markdown body (no more `name: x description: y` reading as a wall
of body text). The portal tab strip distinguishes SKILL.md tabs by showing
the skill name with the Skills icon instead of the generic filename, and
falls back to a file icon for all other open files. Markdown content gets
its own scroll container so the Preview pane scrolls correctly.

The space-tab AgentDocuments group is hidden for hetero CC sessions so the
panel focuses on skills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(hetero-cc): default to Space tab for hetero sessions

Hetero CC right-panel now defaults to the Space tab (where the Skills
module lives) when there's no prior stored tab choice. Non-hetero sessions
keep the existing review/files/resources fallback order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(hetero-cc): surface cumulative progress on Task inspector rows

TaskCreate / TaskUpdate-with-status inspector rows now lead with the
same ProgressRing (from pluginState.todos) and a `completed/total`
chip, so a mixed create/update column reads as one continuous progress
gauge instead of bare-text per-row signals. The verb in the label
still carries the per-row status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(hetero-cc): project skills in slash menu + skills panel polish

Surfaces `.agents/skills/` SKILL.md entries as a new `projectSkill`
ActionTag category in the chat input's `/` menu so users can invoke
project skills the same way CC does internally. The chip serializes to
literal `/<skill-name>` on send, leaving CC's own skill resolution
untouched (no system prompt injection).

Side-panel polish bundled in: the Space-tab Skills list expands as a
real directory tree, the LocalFile portal renders SKILL.md frontmatter
as a metadata card (reusing parseSkillMarkdownMetadata), and skill rows
use the secondary→colorText hover pattern. Also passes `data.root` (the
exact root listProjectSkills approves) to openLocalFile so previews
never hit the workspace-root mismatch 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>
2026-05-17 23:43:27 +08:00
Innei 7ab111fcc5 🐛 fix(onboarding): restore FullNameStep back button to the shared prefix (#14898)
FullNameStep is the classic branch's first step; its back button called
goToPreviousStep, which no-ops at step 1 — a dead link ever since the
telemetry/language steps were extracted into the shared prefix.

Route it back to ResponseLanguageStep, and let CommonOnboardingPage
re-enter the shared prefix when an explicit `?step` is present (a bare
`/onboarding` still resumes the branch).
2026-05-17 23:31:11 +08:00
Neko 6281ca4228 🐛 fix(agent-signal): route hinted skill documents (#14895) 2026-05-17 22:59:00 +08:00
Arvin Xu 73fa3b1689 feat: agent-documents index — hide web crawls + new table format (#14292)
*  feat: agent-documents index — hide web crawls + new table format

The default `<agent_documents_index>` was injecting every progressive
document — including hundreds of web-crawled snapshots (~73% of all
agent docs in production). The result was a low-signal list dominated
by duplicate page titles, plus zero metadata for the LLM to rank by.

This revamp:

- Hides `source_type=web` documents from the default index. Header
  surfaces the count and points the LLM at `listDocuments(sourceType=
  'web')` to enumerate them when needed.
- Renders the index as a fixed-width table with TITLE / ID / SIZE /
  UPDATED columns. Rows are sorted by recency (most-recent first).
  Empty docs render as `empty` to discourage retry reads.
- Adds `sourceType` and `updatedAt` to the `AgentContextDocument`
  contract; client mapping populates both from the DB row.
- Adds `sourceType: 'all' | 'file' | 'web'` parameter to the
  listDocuments tool/TRPC; service-layer filter applies before
  shaping the LLM response.
- Renames `target` → `scope` on listDocuments + createDocument
  (manifest, types, runtime, system role, TRPC, client service,
  call sites, tests). `target="currentTopic"` becomes
  `scope="currentTopic"` everywhere.

Coverage: inline snapshot tests in
`packages/context-engine/src/providers/__tests__/AgentDocumentInjector.test.ts`
pin the rendered output for the three load cases (mixed user docs,
web-hidden header, empty doc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(test): update listDocuments mock assertion for sourceType default

The agent-documents listDocuments runtime now forwards sourceType
(defaulting to 'all'), so the spy receives two positional args.

* 📝 docs(builtin-tool-local-system): bump documented runCommand max timeout to 800000ms

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:08:08 +08:00
Neko 04e9f7fcea ♻️ refactor(agent-signal): adapt response language for prompts (#14890) 2026-05-17 21:20:59 +08:00
Arvin Xu 1cc92db5e2 💄 style(tab-bar): add Chrome-style divider between inactive tabs (#14892) 2026-05-17 21:10:31 +08:00
Arvin Xu 2d088ca6e2 🐛 fix(local-system): forward all grepContent params + move executor to /client (#14888)
* 🐛 fix(local-system): forward all grepContent params + move executor to /client

The local-system executor was reducing the agent's full grepContent params
({pattern, glob, output_mode, -i/-n/-A/-B/-C, multiline, head_limit, type,
scope, ...}) down to {directory, pattern} before handing them to the runtime.
`directory` isn't recognized by the IPC layer (which expects path/scope), so
cwd silently fell back to process.cwd() (= apps/desktop/ in dev), and with
glob/-i/output_mode all stripped grep matched anything containing the pattern
across the whole tree — explaining LOBE-8666's dist/main/index.js +
tsconfig.tsbuildinfo leaks.

Also audited the rest of the executor layer:
- listFiles: forward `limit` (was silently dropped → manifest default of 100
  always won).
- getCommandOutput: forward `filter` (was silently dropped → no regex filter
  ever applied to streamed output).
- runCommand: mirror `run_in_background` → `background` so
  ComputerRuntime.RunCommandState.isBackground reflects reality (the IPC
  handler reads run_in_background directly, so the command itself ran in
  background — only the state field was wrong).

Structure: moved src/executor/ → src/client/executor/ to match the other
builtin-tool packages (task / lobe-agent / knowledge-base) and consolidate
renderer-only code under /client. Dropped the `./executor` package subpath;
consumers now import from `…/client`.

Defensive: also added a resolveSearchPath helper in apps/desktop's
contentSearch module that reads params.scope as a fallback for params.path,
so any non-executor caller (direct IPC, future Gateway path) that passes
`scope` still gets routed correctly instead of falling through to
process.cwd().

Regression coverage:
- grepContent full forwarding (LOBE-8666 case + all optional flags)
- listFiles.limit forwarding
- getCommandOutput.filter forwarding
- runCommand.run_in_background → background mirror
- resolveSearchPath fallback semantics (3 cases in base.test.ts)

Verified end-to-end via Electron CDP — tool.invokeBuiltinTool with the
LOBE-8666 params returns 9 clean .ts matches (no dist/, no .tsbuildinfo);
listFiles {limit:3} returns 3 files (totalCount 10); runCommand
{run_in_background:true} reports state.isBackground=true.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(desktop): readFile fails with `protocol.registerSchemesAsPrivileged should be called before app is ready`

Two-part fix for a regression where reading any text/JSON/source file via the
local-system `readFile` tool surfaced an Electron protocol error in the response
content. The error fired *after* `stat()` succeeded (so missing-file ENOENT was
unaffected), making it look like the file couldn't be parsed.

## Root cause

Stack trace (instrumented `read.ts` to capture it):

```
Error: protocol.registerSchemesAsPrivileged should be called before app is ready
    at new App (apps/desktop/dist/main/index.js:105339:21)
    at Module.<anonymous> (apps/desktop/dist/main/index.js:105615:11)
    at Module._compile (...)
```

`Module._compile` on `dist/main/index.js` means the main bundle is being freshly
evaluated as a CJS module — re-running its top-level `var app = new App(); …;
app.bootstrap();` after the real Electron-launched App was already ready.

Triggering chain: agent calls `readFile` → main runs `loadFile(path)` from
`@lobechat/file-loaders` → `getFileLoader('txt')` → `await import('./text')`.
The lazy text-loader chunk back-references the main bundle for the shared util
`detectUtf16NoBom`:

```js
// dist/main/text-Cbmlmtca.js
const require_index = require("./index.js");      // ← re-evaluates main
…
const variant = require_index.detectUtf16NoBom(buffer);
```

Electron's main entry is not in Node's CJS module cache (it's bootstrapped
separately), so this `require("./index.js")` triggers a fresh compile of the
main bundle — re-running `new App()` and `protocol.registerSchemesAsPrivileged`
*after* `app.whenReady()`, which is illegal per Electron's API contract.

Introduced by #14602 (`fix(local-system): guard readFile against binary blobs
and oversized output`): adding `isBinaryContent.ts` made `detectUtf16NoBom`
shared between the main bundle (via `sniffBinaryFile`) and the lazy text chunk,
so rolldown placed it in main and rewrote the text chunk's call as a
`require_index.detectUtf16NoBom`.

Identical class of bug previously fixed for the `debug` package in #11827.

## Fix

1. **`packages/file-loaders/src/loaders/index.ts`** — TextLoader was lazy-imported
   for no real benefit. It's a 10KB module whose only deps are `node:fs/promises`
   and a tiny utf-16 detect util — nothing like the multi-MB parsers (pdfjs-dist,
   xlsx, mammoth) that the lazy pattern was designed for. Make it a static
   import; `getFileLoader('txt')` returns it synchronously. Result: the text
   chunk disappears entirely, removing this back-reference at the source.

2. **`apps/desktop/electron.vite.config.ts`** — defensive `manualChunks` rules
   so any future shared symbol doesn't recreate the same trap:
   - `vendor-file-loaders-utils` for the three small text/binary detection
     utils (`detectUtf16` / `isBinaryContent` / `isTextReadableFile`).
     Explicitly enumerated to avoid catching `parser-utils.ts`, which pulls
     in xmldom/yauzl/concat-stream (≈900KB) and belongs in the docx/pptx
     chunks instead.
   - `vendor-jszip` for JSZip — same root cause for `.docx` reads: the docx
     chunk had `require_index.require_lib()` (JSZip) back-referencing main.
     Both ends now share the vendor chunk; no main re-eval.

Follows the project precedent set by #11827 for `debug`.

## Verification (live Electron via CDP)

Bundle inventory before/after:

| Chunk | Before | After |
| --- | --- | --- |
| `text-*.js` | 9.7KB (back-refs main) | (gone, inlined into main) |
| `vendor-file-loaders-utils-*.js` | n/a | 18KB |
| `vendor-jszip-*.js` | n/a | 899KB |
| `docx-*.js` back-refs | `require_index.require_lib` | none |

End-to-end via `tool.invokeBuiltinTool('lobe-local-system', 'readFile', …)`:

| File | Before | After |
| --- | --- | --- |
| `.md` / `.json` / `.ts` | `Error accessing or processing file: protocol.registerSchemesAsPrivileged should be called before app is ready` | real file content |

`grep -o 'require_index\\.[a-zA-Z_]*' dist/main/*-*.js | sort -u` → empty.

All 61 file-loaders tests pass; all 64 builtin-tool-local-system tests pass.
2026-05-17 20:26:15 +08:00
Arvin Xu 43b0b5e854 🐛 fix(agent-runtime): honor per-tool timeout end-to-end for client tool dispatch (#14817)
* 🐛 fix(agent-runtime): honor per-tool timeout end-to-end for client tool dispatch (LOBE-8436)

Server BLPOP was hardcoded to 60s and ignored the LLM-supplied `timeout` in
`tool_call.arguments`, so long-running shell commands consistently failed
with a server-side timeout while the desktop runner was still happily
executing. Renderer also never raced its own deadline, leaving it free to
hang past the server budget.

Plumb a per-tool timeout through the full chain:

  - New `resolveToolTimeoutMs` (server) — priority: `args.timeout` >
    `manifest.api[apiName].defaultTimeoutMs` > 120s global default,
    clamped to [1s, 800s] (cloud function ceiling).
  - `dispatchClientTool` accepts `timeoutMs` in ctx; constants moved into
    `resolveToolTimeout.ts`. Default 60→120s, max 270→800s.
  - `RuntimeExecutors` calls the resolver at both client-dispatch sites
    (single + batch) using the LLM-parsed args and the effective manifest.
  - `LobeChatPluginApi` (types + context-engine) gains
    `defaultTimeoutMs?: number` so tool authors declare per-API budgets.
  - `LocalSystemManifest` sets per-API defaults: runCommand 120s,
    read/write/edit/list 30s, grep/glob/search/move 60s, killCommand 10s.
  - `local-file-shell/runner.ts` internal kill cap raised 600→800s to
    match the server ceiling.
  - Renderer `clientToolExecution.ts` rewritten to (1) race executor
    against `executionTimeoutMs - 500ms`, abort the operation's
    AbortController, and send `client_executor_timeout` on overrun;
    (2) read `gatewayConnections[operationId]` live on every send so
    reconnects between dispatch and result are picked up; (3) wrap in
    try/finally with an exactly-once `sent` guard so every `tool_execute`
    yields exactly one `tool_result` even on logic gaps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(test): drop unused @ts-expect-error and tighten timeout assertion

CI lint failed on tsgo: an `@ts-expect-error` directive in
`resolveToolTimeout.test.ts` was unused (the field's `unknown` value
type happily accepts a string at compile time), and the
`sendToolResult.mock.calls[0][0]` access in `clientToolExecution.test.ts`
tripped TS2493/TS2532 because vitest typed `calls` as an empty tuple.

Cast the test-only string value through `unknown` for the resolver
defense check; merge the budget assertion into the `toHaveBeenCalledWith`
matcher via `expect.stringContaining('2000ms')` so we never index into
`mock.calls` by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:23:15 +08:00
Arvin Xu 0e46085176 💄 style: share branded inspector between CC MCP and built-in Linear skill (#14884)
*  feat(linear): share branded inspector between CC MCP and built-in Linear skill

The Linear-branded inspector (logomark + action chip + parentId badge) was
only registered against `mcp__claude_ai_Linear__*` tool names emitted by the
CC adapter. LobeHub's own built-in Linear skill calls land with
`identifier='linear'` and bare apiNames (`get_issue`, `save_issue`, …), so
they fell through to the generic Title + JSON inspector despite being the
exact same Linear surface.

Moves the inspector + label utilities out of `builtin-tool-claude-code` into
`packages/builtin-tools/src/linear/` (alongside `github/`) and registers
them twice in the central inspector map: once under `LinearIdentifier =
'linear'` for the built-in skill path, once merged into the CC entry for
the MCP-prefixed wire names. Same component, same look in both cases.

`formatLinearShortLabel` now matches bare apiNames against the known tool
list too, so the collapsed workflow summary reads `Linear · Get issue`
for built-in calls as well — previously only CC got the humanized label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(linear): leave CC's LinearMcp inspector inside CC, only ship the built-in skill side

Walks back the cross-package edits from the previous commit. The CC adapter
keeps its own `LinearMcp.tsx` + `linearMcpLabels.ts` exactly as #14864 left
them — `formatLinearMcpShortLabel` is still exported from
`@lobechat/builtin-tool-claude-code/client/labels` and `toolDisplayNames.ts`
still imports it from there. CC's inspector index continues to spread
`LinearMcpInspectors` into its own map.

The new shared module under `packages/builtin-tools/src/linear/` now only
covers the built-in LobeHub Linear skill path: `LinearIdentifier='linear'`
+ bare apiNames (`get_issue`, `save_issue`, …). The inspector component is
duplicated from CC on purpose — `builtin-tools` already depends on
`builtin-tool-claude-code`, so we can't import the other way without a
circular dep, and the user wants the CC code to stay put.

Drops the `LinearMcpInspectors` re-export and the CC-entry merge in
`inspectors.ts` that the previous commit had introduced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(linear): hoist shared LinearInspector + label utilities into shared-tool-ui

The Linear-branded inspector and its tool-name parsing helpers were
duplicated between `builtin-tool-claude-code/src/client/Inspector/LinearMcp`
(MCP-prefixed wire names) and `builtin-tools/src/linear/` (built-in skill
bare names). The dep graph (`builtin-tools` → `builtin-tool-claude-code` →
`shared-tool-ui`) means CC can't import from `builtin-tools`, so the
previous round kept two copies.

Moves the component + labels into `packages/shared-tool-ui/src/Inspector/
Linear/` — both CC and `builtin-tools` already depend on `shared-tool-ui`,
so they can each pull the same `LinearInspector` and register it under
whichever key shape their code path uses:

- CC's `LinearMcp.tsx` is now a 10-line wrapper that maps the shared
  inspector across every MCP-prefixed name.
- CC's `linearMcpLabels.ts` re-exports the parsing primitives + keeps the
  CC-only `formatLinearMcpShortLabel` (the prefix check stays here so the
  workflow-summary label only fires for MCP-prefixed wire names).
- `builtin-tools/src/linear/` drops its own Inspector / labels files; the
  index just registers the shared component under bare apiNames.

Exposes a labels-only subpath `@lobechat/shared-tool-ui/inspectors/
linear-labels` so the workflow-summary path can pull parsing helpers
without dragging the React inspector (and its `keyframes`-using style
modules) into `Group.test.tsx`'s mocked antd-style context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:59:27 +08:00
Neko e50e6859e7 ️ perf(agent-signal,prompts): better prompts and explicit rules (#14882) 2026-05-17 17:58:06 +08:00
LobeHub Bot 70097ad315 🌐 chore: translate non-English comments to English in agent-tasks (#14880)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:06:56 +08:00
Arvin Xu 929d23a94e feat(cc): task tools + AskUserQuestion freeform note + waitingForHuman topic status (#14870)
*  feat(cc): support TaskCreate / TaskUpdate / TaskList tools (CC 2.1.143+)

Add adapter accumulator, inspectors and Todos panel for CC's imperative
task trio that replaces TodoWrite. TaskUpdate's status flip is surfaced
as a per-call chip ("Completed: Read hosts") and the Todos panel header
mirrors that label, with subject resolved from pluginState by CC-assigned
task id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(cc): escape-toggle AskUserQuestion + waitingForHuman topic status

AskUserQuestion intervention — mode-exclusive escape hatch:
- Mirror `lobe-user-interaction`'s "Or type directly" toggle: form picks
  and the freeform reply are mutually exclusive, not stacked. Default
  view shows the multi-choice options; clicking "Or type directly"
  swaps the body to a single TextArea, and "Back to options" returns.
- Submit sends either per-question picks OR `{ __freeform__: <text> }`
  (never both). Bridge formatter (`AskUserMcpServer.formatAnswerForCC`)
  forwards the text verbatim to CC when `__freeform__` is the payload,
  bypassing the `User answers:\n- <q>: <a>` framing — keeps the model
  prompt clean when the user opts out of the structured form.
- Draft persistence resumes the user back into escape mode when
  `__freeform__` is non-empty; an empty draft starts in form mode.
  Timeout fallback respects escape mode: non-empty text submits as-is
  rather than being discarded for option-1-of-each defaults.
- Render swaps to a single "user reply" card with the typed text when
  `__freeform__` is present; otherwise renders the Q&A pairs as before.

Topic status `waitingForHuman`:
- Add new enum value to `ChatTopic` status — TS-only widening (the
  drizzle `text({enum})` is not a `pgEnum`, no migration needed) —
  wired through types + zod router schema.
- Sidebar topic row renders a warning-colored Hand icon when an
  intervention is pending so the waiting state reads from the topic list.
- `heterogeneousAgentExecutor` flips status to `waitingForHuman` when
  an AskUser intervention is raised and back to `running` once the
  bridge resolves; `conversationControl.submitHeteroIntervention` also
  flips back to `running` after the user submits / skips / cancels. The
  natural `runtime_end → writeTopicStatus('active')` takes over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(explorer-tree): drop doubled outline on selected file rows

Add `--trees-selected-focused-border-color-override: transparent` to
both ExplorerTree consumers (working-sidebar Files + AgentDocuments).
`@pierre/trees` draws an outline via `::before` on focused+selected
rows that visually fights with the filled `--trees-selected-bg`
highlight — the existing `--trees-border-color-override: transparent`
only controls structural borders, not this focus outline. Keyboard
focus ring on unselected rows stays intact (a11y).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:06:18 +08:00
Arvin Xu ad75e25443 ♻️ refactor(agent-settings): drop Meta/Documents tabs, restore inputTemplate (#14874)
* ♻️ refactor(agent-settings): drop Meta and Documents tabs

Remove the 助理信息 (Meta) and 文档 (Documents) tabs from the agent
profile/settings UI. Default chat-settings tab falls back to Opening for
non-inbox agents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(agent-chat): restore inputTemplate field in Chat Preferences

Add back the User Input Preprocessing (inputTemplate) form field that was
removed in 2.0. The pipeline (InputTemplateProcessor, i18n, types) was kept
intact when the UI was dropped — only the form entry is added back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:15:17 +08:00
YuTengjing 93492382ca 💄 style: shorten usage token details (#14849) 2026-05-16 23:21:54 +08:00
Arvin Xu 4ea80c2915 🐛 fix(gemini): sanitize enum/required from non-compliant types in tool schema (#14740)
* fix(gemini): strip enum from non-STRING types in tool schema

* fix(gemini): handle nullable types and definitions recursion in schema sanitizer

Addresses review feedback on #14740 for LOBE-8661:

1. Preserve nullable string enums (type: ['string', 'null'])
   - Replace strict type equality checks with isStringType/isObjectType
     helpers that handle both single-string and array types.
   - Apply to both sanitizeGeminiSchema and
     convertOpenAISchemaToGoogleSchema.

2. Recurse into definitions/$defs schema maps
   - When a tool schema stores non-compliant enum/required inside
     definitions/$defs and references it with $ref, the walker now
     visits these schema maps as well.

Test coverage: 6 new cases for nullable type preservation and
definitions/$defs recursion.

* 🐛 fix(test): wrap sanitizeGeminiSchema inputs in valid JSON Schema

The 3 cases were passing bare property maps directly to the sanitizer,
which only recurses through `properties`/`items`/combinators/`$defs` —
so the inner `enum`/`required` were never visited and assertions failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Arvin Xu <arvinxx@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 20:55:02 +08:00
YuTengjing f94f941fe8 💄 style(home): polish brief recommendations layout (#14871) 2026-05-16 20:20:32 +08:00
Arvin Xu fbc42b725e feat(hetero-agent): support Monitor-style signal callbacks in AssistantGroup (#14859)
*  feat(hetero-agent): emit externalSignal on Monitor-callback steps + reader-side SignalCallbacksNode

LOBE-8998 Phase 1 — data-layer work. Adapter detects repeated tool_results
on the same tool_use.id (Monitor stdout pushes etc.) and tags the next
stream_start(newStep) with an externalSignal peer field. Executor stamps
metadata.signal on the new assistant message. conversation-flow
MessageCollector / ContextTreeBuilder collect signal-tagged toolless
assistants into a SignalCallbacksNode appended inside AssistantGroup
children. UI rendering deferred to a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): keep parentId chain alive across toolless middle steps

LOBE-8993: when a CC step produced only text (e.g. Monitor stdout drove
Claude to reply without invoking a tool), the next step's parentId fell
back to the previous assistant. MessageCollector only walks the
assistant → tool → assistant zigzag, so each Monitor stdout line split
into its own bubble.

Carry the most recent tool result_msg_id across step boundaries via a
`lastToolMsgIdEver` tracker so toolless middle steps still chain back to
the originating tool result.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(chat-ui): render SignalCallbacks block inside AssistantGroup for Monitor-style callbacks

Adds the UI layer of LOBE-8998. FlatListBuilder snapshots signal-callback
groups onto the virtual AssistantGroup message via UISignalCallbacksBlock
(new typed field on UIChatMessage) and marks each callback message
processed so it does NOT render as a separate top-level bubble.
AssistantGroup reads the field and renders a collapsible
<SignalCallbacks> component under the main Group content, one block per
source tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): detect Monitor callbacks via system task lifecycle instead of repeat tool_result

The previous detection model (count repeat tool_result per tool_use.id) was
based on a wrong assumption — Monitor's stdout pushes are NOT delivered as
additional tool_result events for the same tool_use.id. Verified against a
real `claude -p` trace: Monitor emits ONE tool_result (the initial "Monitor
started" ack), then each subsequent stdout line triggers a `system init` +
new `message_start` cycle within the same CLI process. The actual lifecycle
signal is `system task_started` (long-running tool registers) followed by
`system task_notification` (terminal).

New detection: a `message_start` that opens a new turn WITHOUT a preceding
`user` event, while at least one task is active, is a signal callback.
`task_started` records `{task_id → tool_use_id}`; `task_notification` drops it.
Verified against the recorded CC trace: 5/5 reactive turns get tagged with
correct sequence and source tool, the natural confirmation turn and the
post-task summary turn are correctly excluded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(hetero-agent): keep CC post-task summary in same group + dedicated Monitor inspector (LOBE-8998)

The post-task summary turn (fired after `system task_notification` ends
a long-running tool) was spawning its own AssistantGroup because the
collector only followed the first non-signal toolless sibling under a
tool_result — it never saw the summary that came after the
SignalCallbacks. Adapter now stamps `signal.type = 'task-completion'`
on the summary turn so the collector keeps it inside the same group,
rendered AFTER the SignalCallbacks accordion (initial reply → callbacks
→ summary, in creation order).

Also adds a dedicated `MonitorInspector` (lucide `Monitor` icon, chip
shows description / command, trailing timeout label) so the Monitor
tool call line stops falling back to the generic `claude-code > Monitor`
display, and tightens the Flexbox spacing around SignalCallbacks +
taskCompletions inside the AssistantGroup so the three sections read
as one connected reply rather than disconnected blocks.

Adapter: arm `pendingTaskCompletion` on `task_notification` (last-task-
wins), consume it on the next natural `message_start`, clear on `result`
so it never leaks across LLM runs.

Tests: adapter (74) + executor (56) + conversation-flow (126) all green.
Verified end-to-end in Electron with a 5-tick Monitor run — single
AssistantGroup with the natural narrative inside.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(conversation-flow): skip signal callbacks when locating the group tail

`findLastNodeInAssistantGroup` blindly took `toolNode.children[0]` when
walking past a tool, so for the common `[signal callback, next tool-using
assistant]` order the tail landed on the callback (a leaf) and
`findNextAfterTools` returned null — truncating the AssistantGroup and
omitting follow-up messages after the real last assistant. Mirror the
signal-skip already used in `collectAssistantGroupMessages` (LOBE-8998).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:40:57 +08:00
Arvin Xu f94e4f46a4 🐛 fix(task-schedule): enforce maxExecutions cap and block sub-10min heartbeat (#14865)
* 🐛 fix(task-schedule): enforce maxExecutions cap and block sub-10min heartbeat

The "运行次数限制" input on a scheduled task was accepted by the UI and
persisted to `tasks.config.schedule.maxExecutions`, but no execution path
ever read it — scheduleDispatch/scheduleTick/runTask had no counter and
no cap check, so a "stop after N runs" schedule would loop forever.

Separately, the server-side `heartbeatInterval` zod schema was `min(0)`,
and the `setTaskSchedule` tool manifest only said "recommend ≥600s". An
LLM could pass any positive number and trigger sub-minute heartbeats.

Enforcement (no schema migration):

- `TaskService.updateStatus` stamps `context.scheduler.scheduleStartedAt`
  (ISO) when a task transitions into `scheduled` from a non-`running`
  status. The cron loop's natural `running → scheduled` flips happen via
  `taskModel.updateStatus` (taskLifecycle), bypassing the service layer,
  so they don't reset the counter. User-initiated (re)starts do.
- `TaskTopicModel.countByTaskSince(taskId, since)` counts task_topics
  rows created since a timestamp.
- `runScheduleTick` reads `config.schedule.maxExecutions`; if the count
  since `scheduleStartedAt` has reached the cap, it marks the task
  `completed` (so the next dispatch sweep filters it out) and returns a
  new `max-executions-reached` skip reason.

Heartbeat lower bound:

- `updateSchema.heartbeatInterval` on the lambda router now refines to
  `v === 0 || v >= 600`, matching `MIN_MINUTES = 10` in the UI.
- `setTaskSchedule` tool manifest description updated to "Minimum 600s
  … the server rejects positive values below 600" so the LLM sees the
  hard limit before the zod refine bounces the call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(task-topic-model): rename countByTaskSince → countByTask, use drizzle count()

- Make `since` an optional `options` argument so the helper covers total
  counts too, not only the since-window the scheduler needed.
- Swap `sql<number>\`count(*)::int\`` for drizzle's native `count()`
  aggregator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(task-schedule): cover countByTask, scheduleStartedAt stamping, and tick max-exec

- `TaskTopicModel.countByTask`: total-mode, since-window mode, task scope,
  user scope (real DB).
- `TaskService.updateStatus`: stamps `context.scheduler.scheduleStartedAt`
  on user-initiated starts/restarts of a schedule task; does NOT stamp on
  the cron loop's natural `running → scheduled` cycle, on heartbeat-mode
  tasks, or when the new status isn't `scheduled`.
- `runScheduleTick`: cap not configured / under cap → runs; cap reached
  → marks `completed` and skips with `max-executions-reached`; missing
  `scheduleStartedAt` → falls through (backwards-compat for tasks created
  before this PR).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task-schedule): complete capped schedules at the final allowed run

The pre-tick cap check in `runScheduleTick` only sees `runCount` *before*
starting the next tick. For low-frequency schedules (e.g. daily,
`maxExecutions=1`), this meant the task would consume its final allowed
run, get parked back at `scheduled` by `TaskLifecycleService.onTopicComplete`,
and then sit in `scheduled` for a full cron period before the next pre-tick
check noticed the cap was already consumed — contradicting the "stop after
N runs" promise.

Move the canonical stop to post-completion:

- New `TaskLifecycleService.scheduleCapReached(task)` helper counts
  `task_topics` rows since `context.scheduler.scheduleStartedAt` and
  compares against `config.schedule.maxExecutions`. Short-circuits when
  the task isn't in schedule mode, no cap is configured, or no
  `scheduleStartedAt` is stamped (pre-PR tasks).
- The default post-tick transition in `onTopicComplete` now routes a
  cap-reached schedule task to `completed` instead of `scheduled`, so
  the UI/API reflect the cap immediately.

The pre-tick check in `runScheduleTick` is kept as defense-in-depth:
covers crashed ticks that never reached `onTopicComplete`, users
editing `maxExecutions` downward past current count, and stale
`scheduled` rows from older code paths. Comment updated to reflect that.

Tests:
- `onTopicComplete`: schedule task under cap → still `scheduled`; at
  cap → `completed`; with no `scheduleStartedAt` (pre-PR) → still
  `scheduled` (helper short-circuits before querying).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:14:29 +08:00
Arvin Xu 6478c6012f feat(cc): render Linear MCP tool calls with branded inspector (#14864)
*  feat(cc): render Linear MCP tool calls with branded inspector

CC emits Linear MCP tools as `mcp__claude_ai_Linear__<verb>_<noun>` —
the default inspector and the collapsed summary surface those raw names,
which read as `Mcp__claude_ai_ Linear__get_issue` after title-casing.

Adds a generic Linear MCP inspector that:
- Shows the monochrome Linear logomark + "Linear" product prefix
- Renders the action as a single pill split into action / value halves
  (e.g. `Get issue | id: LOBE-8743`)
- Detects `parentId` and surfaces it with a CornerLeftUp icon, either in
  the chip's value half (when parent is the primary arg) or as a secondary
  badge after the chip (mirrors the parent visual used by AgentTask UI)
- Hard-caps chip text at 60 chars so long comment bodies / search queries
  don't push the row off-screen

Also humanizes the collapsed-workflow summary via a `formatLinearMcpShortLabel`
helper exported from `@lobechat/builtin-tool-claude-code/client`, so the
bundle row reads "Linear · Get issue" instead of the raw tool name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(cc): render WebSearch and WebFetch tool calls with custom inspector

CC's web tools were falling through to the generic tool UI because
`ClaudeCodeApiName` and the render/inspector registries hadn't been
extended. Adds dedicated inspector (query/url chip) and result card
(text for search, markdown for fetched pages) for both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(cc): isolate Linear MCP label helper to avoid antd-style mock break

`Group.test.tsx` mocks `antd-style` with only `createStaticStyles`. The
previous wiring imported `formatLinearMcpShortLabel` through the
`@lobechat/builtin-tool-claude-code/client` barrel, which transitively
loads `LinearMcp.tsx` → `@lobechat/shared-tool-ui/styles` → `keyframes`,
crashing the mock.

Splits the pure label utilities (LINEAR_MCP_PREFIX, parseToolName,
staticLabelFor, formatLinearMcpShortLabel, LINEAR_MCP_TOOL_NAMES) into
`linearMcpLabels.ts` with no React/antd-style imports, exposes it as
`@lobechat/builtin-tool-claude-code/client/labels`, and switches the
consumer in `toolDisplayNames.ts` to that subpath. The inspector
component keeps importing the same helpers locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 ui(hetero): land manual workflow expand at full level

Heterogeneous agent workflows often run 40+ tool calls. When the user
collapsed the workflow and clicked the header to re-expand, it landed
at the height-capped `semi` state and hid most of the chain. Now we
infer a "fully expanded experience" from `defaultWorkflowExpandLevel`
— any phase opting into `full` routes the manual expand straight to
`full` instead of the legacy `semi` cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:41:22 +08:00
Arvin Xu ff259bdc51 🐛 fix(agent-tracing): align DB trace_s3_key with .json.zst suffix (#14860)
🐛 fix(agent-tracing): align DB trace_s3_key with `.json.zst` suffix

PR #14807 switched the S3 object key written by `S3SnapshotStore.save()`
to `.json.zst` but the DB-persistence path in `CompletionLifecycle.ts`
still hardcoded `.json`. Result: every row inserted into
`agent_operations.trace_s3_key` points at a key that does not exist —
the actual object is the `.json.zst` sibling. Any consumer that GETs by
the DB-recorded key (dc tracing UI, agent-tracing inspect via record
lookup) hits 404.

Verified in prod: 87012/87159 populated rows still end in `.json`, 0
end in `.json.zst`, including rows inserted hours after the PR #14807
deploy.

Fix factors out a single `buildFinalSnapshotKey(agentId, topicId, opId)`
helper exported from `@/server/modules/AgentTracing` so both the S3
writer and the DB writer construct the key from the same source, making
this class of drift impossible going forward.

Existing rows need a one-off backfill (run from dc):
  UPDATE agent_operations SET trace_s3_key = trace_s3_key || '.zst'
  WHERE trace_s3_key LIKE '%.json';

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 14:56:58 +08:00
AmAzing- 7b61b9526f feat: align self-iteration builtin tool with shared runtime and inspector patterns (#14827) 2026-05-16 13:52:08 +08:00
Arvin Xu 8c4fbf4a81 🐛 fix(home): fetch agent config so knowledge toggles reflect in UI (#14834)
* 🐛 fix(home): fetch agent config so knowledge toggles reflect in UI

Home layout didn't subscribe to the agent config SWR key, so
`toggleFile` / `toggleKnowledgeBase` succeeded server-side but the
follow-up `mutate([FETCH_AGENT_CONFIG_KEY, agentId])` had no listener
and `agentMap` was never refreshed — leaving the Library submenu
checkboxes visually frozen on the home page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(home): move agent config fetch into InputArea with loading state

Move `useInitAgentConfig(agentId)` from the home layout into InputArea
so it tracks the resolved home agent id (inbox or AgentSelect override)
and refetches when the selection changes. Disable the send button while
the agent config isn't yet in `agentMap`, matching the loading shape of
the Memory/Search/History actions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:58:03 +08:00
Arvin Xu d91132c155 💄 style(thread): indent subagent rows and drop SUBAGENT badge (#14845)
Restyle subagent thread items in the Topic sidebar:
- Replace `└` TreeDownRightIcon with `↳` CornerDownRight from lucide-react
- Remove right-aligned SUBAGENT Tag badge; the indent + arrow now carry the
  nesting affordance on their own
- Apply `paddingInlineStart: 32` on the NavItem's inner Block so subagent
  rows shift right by ~one icon slot while the row background/highlight
  stays full-width
- Sync agent and group sidebar copies; drop the now-unused
  `chat:thread.subagentBadge` i18n key

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:55:45 +08:00
Tsuki b8a03bdc08 🐛 fix(task-schedule): stop SchedulerForm race + drop stale-refresh CLS (#14853)
* 🐛 fix(task-schedule): stop SchedulerForm race + drop stale-refresh CLS

Rapid edits in the schedule form (weekday toggles, frequency/time picks,
timezone changes) fired concurrent PUTs through `updateSchedule` and then
a SWR mutate refresh. The refresh was async and could land after the
user's next click, overwriting their latest input with whatever the
server happened to hold — the same race as setAutomationMode in LOBE-8893.

- Migrate `updateSchedule` to the shared `OptimisticEngine` introduced by
  LOBE-8893. Same `taskDetailMap.<id>` path, so schedule edits serialize
  against each other AND against mode toggles.
- Mirror every server-bound field (config.schedule.maxExecutions JSONB +
  flat schedulePattern/scheduleTimezone columns) into the optimistic
  patch and drop the post-PUT refresh.
- PUT failure now rolls back via inverse patches.
- Remove `#withCoalescedRefresh` + `#pendingWrites` — both unused after
  setAutomationMode and updateSchedule moved to the engine.

Fixes LOBE-8901

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-trigger-tag): ellipsis the inline primary so long patterns don't wrap to two lines

A weekly schedule with many selected days (e.g. "每周 日/四/六 09:00 运行")
overflowed the 200px properties widget width and wrapped to two lines, so
adding/removing weekdays shifted the rows above and below. Truncate with
ellipsis instead — the full text + timezone is still visible on hover via
the existing tooltip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:07:26 +08:00
Tsuki 8385a7c447 🐛 fix(editor): stop showing per-line placeholder once the editor has content (#14852)
LOBE-8924: TaskInstruction (and every other EditorCanvas consumer that doesn't
pass `lineEmptyPlaceholder` itself) was forwarding the same string into both
`placeholder` and `lineEmptyPlaceholder`. The latter renders the hint on every
empty block, so as soon as the user typed something and moved to a new line,
"Add task instruction…" reappeared inline next to the cursor. Drop the
`lineEmptyPlaceholder` pass-through so the hint only shows when the whole
editor is empty; callers that genuinely want per-line hints
(`SkillEditForm`, `agent/profile/EditorCanvas`, `CreatePlan`) already pass it
directly to `<Editor>`.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:07:12 +08:00
Tsuki c814c566d4 🐛 fix(chat): respect useCmdEnterToSend preference in thread & task inputs (#14850)
Thread feedback and task comment inputs hardcoded Cmd/Ctrl+Enter to send,
ignoring the user's "Use Cmd+Enter to send" preference and diverging from
the main chat input. Extract a shared useEnterToSend hook and apply it to
all chat-like inputs so behavior stays consistent.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:06:57 +08:00
Tsuki 5e03311d21 💄 style(agent-tasks): align Add Subtask button & card baseline (#14848)
💄 style(agent-tasks): align Add Subtask button with card content

Fixes LOBE-8904

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:06:38 +08:00
Tsuki 03f99bfeeb 💄 style(chat-input): equalize action bar padding around send button (#14846)
* 💄 style(chat-input): equalize action bar padding around send button

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-feedback): equalize commentInputCard padding around send button

The asymmetry the issue called out lives on the TopicChatDrawer
FeedbackInput card, not the main DesktopChatInput action bar. Revert
the earlier DesktopChatInput tweak and align top/bottom/right padding
on commentInputCard instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 01:27:40 +08:00
Tsuki 224079b420 🐛 fix(agent-tasks): enable send button after pasting into thread/comment input (#14816)
The Editor's `onTextChange` ignores the first content-change event after listener
registration (uses a `previousContent` baseline). Because the parent re-creates
the callback ref on every render, the listener re-registers and that gate fires
on every paste — leaving `hasContent` false and the send button disabled until
the user types something.

Switch to `onChange` (which fires unconditionally), and use `editor.isEmpty` so
each fire stays O(1) despite the higher invocation rate.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 01:27:06 +08:00
Tsuki 081a0886aa 🐛 fix: preserve TopicChatDrawer state during close animation (#14803)
Wrap title, extra and body of TopicChatDrawer in `Freeze` so the drawer
keeps its last rendered content while it animates closed, instead of
flashing to the empty/"untitled" view as `topicId` and `agentId` clear.

Fixes LOBE-8900

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 01:26:47 +08:00
Tsuki d9eba30519 🐛 fix(task-schedule): stop UI flip-flop on rapid automation-mode toggles (#14801)
Rapid Segmented clicks (schedule ↔ heartbeat) used to leave the popover trigger
row flickering and the task properties widget vertically shifting.

- TaskTriggerTag inline mode now always renders a single row; timezone moves
  to the hover tooltip so the row height is stable regardless of mode.
- setAutomationMode goes through OptimisticEngine: per-task path conflicts
  serialize concurrent toggles so PUTs land in click order, and a failure
  triggers an inverse-patch rollback instead of a manual save/restore.
- Mirror every server-bound field into the optimistic patch and drop the
  post-PUT SWR refresh — the async refresh could land after the user's next
  click and overwrite their latest state.

Fixes LOBE-8893

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 01:26:28 +08:00
Rdmclin2 a47d29b0bb 🐛 fix: bot channels (#14847)
* feat: support app home welcome messger

* feat: support welcome message in bot channels

* fix: /start commands ephemeral

* chore: fix User Block trigger style

* chore: add bot channel docs

* feat: support thread participants count

* feat: bot channel support participants count
2026-05-15 22:32:40 +07:00
Innei 3864a1eaab 🐛 fix(onboarding): gate discovery progress by phase (#14842) 2026-05-15 22:23:21 +08:00
Arvin Xu 8ca3f9a372 🐛 fix(agent-runtime): forward tools into compression budget on call_llm (#14837)
* 🐛 fix(agent-runtime): forward tools into compression budget on call_llm

Tool definition tokens were already counted by `countContextTokens`, but
`GeneralChatAgent` never passed `tools` into `compressionOptions`, so a
large tool manifest (16-22K tokens observed on openrouter `:free`
variants) could push the request past the model's context window
without ever tripping the compression threshold.

Forward `state.tools` (init/user_input) and `payload.tools` (toLLMCall)
into `shouldCompress`. Fixes LOBE-8973 Bug B.

* 🐛 fix(agent-runtime): skip tool budget on force-finish continuations

When state.forceFinish is set, RuntimeExecutors.callLlm strips every tool
via buildStepToolDelta (deactivatedToolIds: ['*']) before the model call.
The compression check must mirror that stripping — otherwise the operation's
tool schemas push the budget over threshold and the runner returns
compress_context, spending an extra summarization pass on tokens that won't
be sent.

Threads state.forceFinish through the compression budget at both the
init/user_input and the toLLMCall paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:50:58 +08:00
LiJian a2d91b205e feat(cc): show cloud credentials alert and disable input when not configured (#14822)
When a heterogeneous agent (Claude Code) is opened in the browser (cloud/web
mode) and the CLAUDE_CODE_CRED_KEY env is not yet configured, the chat input
is now disabled and a warning banner is shown with a direct link to the agent
profile page so the user can set up their token.

- Add useHeteroAgentCloudConfig hook (business slot) that checks isDesktop,
  heterogeneousProvider, and env.CLAUDE_CODE_CRED_KEY
- Guard handleSendButton in ChatInput store to respect sendButtonProps.disabled
  (blocks Enter-key send when button is externally disabled)
- Render Alert banner + pass disabled:true to sendButtonProps in
  HeterogeneousChatInput when credentials are missing
- Add i18n keys: heteroAgent.cloudNotConfigured.{title,desc,action}

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 20:45:10 +08:00
Innei a35c55c57b 🐛 fix(onboarding): remind discovery turn progress (#14833) 2026-05-15 20:28:33 +08:00
Arvin Xu 625cf80b84 🐛 fix(model-runtime): fail-fast pre-flight context check for OpenAI-compatible providers (#14824)
* 🐛 fix(model-runtime): fail-fast pre-flight context check for OpenAI-compatible providers

LOBE-8291 added `resolveSafeMaxTokens` + `MaxTokensExceededError` but only
wired them into MiniMax. NVIDIA and DeepSeek hosts continued to round-trip
doomed requests to upstream just to get a 400 back ("requested 0 output
tokens and your prompt contains at least N+1 input tokens"). LOBE-8974
captures the variants still hitting users — including 5 consecutive
failures from a single user retrying across deepseek-v4-{flash,pro}.

This change:

- Promotes the pre-flight check to `openaiCompatibleFactory` via a new
  `chatCompletion.contextPreFlight` option. When set, the factory runs
  `assertContextWithinWindow` against the provider's model list before
  invoking `handlePayload`, and surfaces a structured
  `ExceededContextWindow` error so the UI can offer fork / switch-model
  affordances instead of a raw provider 400.
- Renames `MaxTokensExceededError` to `ContextExceededPreFlightError` and
  reshapes its payload to match the LOBE-8974 spec: `{ type, promptTokens,
  ctx, model, shortBy, suggestions }`. The factory intercepts the error
  centrally so providers no longer need their own `handleError` for this.
- Wires NVIDIA and DeepSeek (OpenAI path) to opt in. MiniMax keeps using
  `resolveSafeMaxTokens` for `max_tokens` capping; its bespoke
  `handleError` is removed since the factory handles it now.

Out of scope (tracked in LOBE-8974): compression-failure metrics for the
4b "input genuinely overflows 1M" cases, repeated-ECW UX guidance to fork
the topic, and DeepSeek's Anthropic-compatible path (which lives behind a
separate factory).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): pre-flight should reject only on real context overflow

The previous `assertContextWithinWindow` reused `resolveSafeMaxTokens`'s
strict thresholds — subtracting a 1024-token buffer and then requiring
another 1024 tokens of completion headroom. That made sense for MiniMax
(which caps `max_tokens` itself and needs room left for output) but
wrong for NVIDIA / DeepSeek where the harness does not pick `max_tokens`
and the upstream chooses its own default. A 198.5k-token prompt against
a 200k-token window would be rejected pre-flight with a negative
`shortBy` even though the upstream would happily serve it.

Pre-flight-only providers now reject only when the estimated prompt
strictly exceeds the model context window. `AssertContextWithinWindowOptions`
exposes a `safetyMarginTokens` knob for callers that want to absorb
estimator drift, defaulting to 0. The error class makes `minOutputTokens`
optional and only includes it in the structured payload when the
max_tokens-capping path populated it.

Adds regression tests for the near-limit case at both the helper level
and through the factory wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:54:27 +08:00
Arvin Xu d02df7b897 🐛 fix(hetero-agent): drop ALL subagent-tagged events from main gateway handler (#14838)
The forwarding guard only filtered `stream_chunk` events. `tool_start` and
`tool_end` for subagent inner tools still reached the main handler, where
`tool_end` fired a `fetchAndReplaceMessages(main)` on every subagent inner
tool result — wasted work AND a state-drift window that surfaced as the
"orphan tool call" banner on the spawn's bubble even after DB had settled.

`tool_start(subagent)` was also leaking `dispatchOnBeforeCall` invocations
against the main context for what is actually a subagent inner tool, firing
renderer onBeforeCall hooks in the wrong scope.

Broadens the guard to drop ALL events with `event.data.subagent`. Safe
because:
- `tool_result(subagent)` is already handled inline at executor:1407 with
  an early `return`.
- `stream_chunk(subagent)` is routed through `persistSubagent*Chunk` into
  the per-spawn thread scope; the subagent's own in-thread renderer state
  is streamed via the thread-scoped dispatcher introduced in #14024.
- `tool_start` / `tool_end` are pure renderer-notification hooks; the
  subagent has no business firing them on the main bucket.

Regression test asserts:
- No forwarded event with `event.data.subagent` reaches the handler.
- Main's own `tool_start` / `tool_end` (no subagent flag) still reach
  the handler so the main bubble's animation + onAfterCall hooks fire.

Closes LOBE-8991.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:47:59 +08:00
Arvin Xu 19b11f05be 💄 i18n(chat): rename Agent mode label in zh-CN (#14835)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:48:36 +08:00
YuTengjing 59d2915bf9 🐛 fix: serialize file storage upload checks (#14829) 2026-05-15 17:28:56 +08:00
YuSaZh 17506e30ee 🐛 fix(desktop): resolve Windows npm CLI shims before spawning agents (#14772)
* 🐛 fix(desktop): resolve Windows CLI shims before spawning agents

* 🐛 fix(desktop): support Windows node-backed CLI shims

* 🐛 fix(desktop): resolve npm cmd node shims on Windows

* 🐛 fix(desktop): avoid async spawn wrapper for CLI agents
2026-05-15 17:24:43 +08:00
LiJian 1a48642a2d 🐛 fix(agent-profile): include hidden builtin tools in system prompt @-mention list (#14823)
* 🐛 fix(agent-profile): include hidden builtin tools in system prompt @-mention list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agent-profile): use discoverableMetaList for system prompt @-mention

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 17:05:43 +08:00
Arvin Xu 205b9de5c6 🐛 fix(agent-tracing): restore legacy .json fallback when fetching remote snapshots (#14826)
🐛 fix(agent-tracing): restore legacy .json fallback in RemoteSnapshotStore.fetch

After #14807, `buildRemoteUrl` always targets `.json.zst` and
`RemoteSnapshotStore.fetch` throws on any non-OK response. Because the
S3 rollout only compresses new uploads — pre-rollout final snapshots
remain at the legacy `.json` key — every pre-rollout operation ID would
404 through the CLI/viewer.

Mirror the fallback that `S3SnapshotStore.loadPartial` already uses:
try `.json.zst` first, fall back to the sibling `.json` on non-OK, and
sniff the zstd frame magic (0x28b52ffd) on the body so decoding is
content-driven rather than suffix-driven.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:51:41 +08:00
YuTengjing 20a631a637 💄 style(subscription): update credit top-up copy (#14821) 2026-05-15 16:34:47 +08:00
Arvin Xu ba6980ffe9 🐛 fix(minimax): derive max_tokens from context window to avoid ExceededContextWindow (#14814)
* 🐛 fix(minimax): derive max_tokens from context window to avoid ExceededContextWindow

MiniMax API enforces `input_tokens + max_tokens <= context_window`. The
provider was passing the model's full `maxOutput` as `max_tokens`, which
overflowed the context window as soon as a few large tool definitions or
system prompts were attached and made the very first user message fail
with "context window exceeds limit".

Add `resolveSafeMaxTokens` utility that estimates input tokens from the
payload (messages + tools), caps `max_tokens` at
`min(maxOutput, contextWindow - estimatedInput - buffer)`, and throws a
typed `MaxTokensExceededError` when no headroom remains. The MiniMax
provider now wires this into `handlePayload` and surfaces the error as
`ExceededContextWindow` via a `handleError` callback so it short-circuits
before the doomed upstream call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(minimax): estimate max_tokens against sanitized messages

handlePayload strips signed reasoning (and reasoning-without-content)
from assistant messages before sending to MiniMax, but the previous
resolveSafeMaxTokens call was still measuring the original payload.
For chats with long historical reasoning traces this overcounted the
input — capping max_tokens unnecessarily, or even raising
MaxTokensExceededError when the request would actually fit.

Pass the same processedMessages we send so the estimate matches the
wire payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:47:30 +08:00
Innei 55b4842f00 🐛 fix(chat-input): allow submenu to close on sibling-open and focus-out in ActionDropdown (#14802) 2026-05-15 13:47:26 +08:00
Arvin Xu 6e6970f1b2 🐛 fix(context-engine): account for tool_calls + reasoning + tool defs in compression budget (#14813)
🐛 fix(context-engine): account for tool_calls + reasoning + tool defs in compression budget

The pre-compression token check (`shouldCompress`) only counted `msg.content`,
which under-counted typical agent conversations by ~58% — tool_calls (~33%
of payload), reasoning traces (~17%), and top-level tool definitions (~2%)
were all silently ignored. As a result, conversations that the provider
tokenizer measured at ~656K passed the harness's 524K threshold without
firing compression, and were rejected upstream as ExceededContextWindow.

Verified empirically against 2 op snapshots in the same topic that hit
the failure mode (LOBE-8964): harness counted 267K, deepseek measured
649K — a 380K (58.8%) gap. ~92% of that gap is fixable by accounting
for the missing fields; the remaining ~8% is `tokenx` vs provider
tokenizer drift, compensated by a 1.25× multiplier on the trigger path.

Changes:

- New `@lobechat/context-engine/tokenAccounting` module exporting
  `countContextTokens({messages, tools, options})`. Returns structured
  per-source + per-message + per-tool breakdown — usable both by the
  compression trigger and by UI panels showing "context by type".
- `shouldCompress` in agent-runtime delegates to `countContextTokens`,
  applies the 1.25× drift multiplier on `adjustedTotal` for the trigger
  decision, exposes raw count via `currentTokenCount`. Signature now
  takes `UIChatMessage[]` directly.
- Removed deprecated `calculateMessageTokens` / `estimateTokens` /
  `TokenCountMessage` from agent-runtime — the new module supersedes
  them. `createAgentExecutors.ts` updated to call `countContextTokens`
  directly for post-compression telemetry.
- Added `raw-md` plugin to agent-runtime vitest config (needed once
  context-engine is imported transitively, since the import graph pulls
  in `@lobechat/agent-templates` `.md` files).

What's intentionally NOT counted (DB-only fields not sent to provider):
`plugin`, `pluginState`, `chunksList`, `extra`, `fileList`, etc.
Counting these would over-estimate and trigger compression too early.

Tests:

- 19 new unit tests for `countContextTokens` covering content / tool_calls
  / reasoning / tool_call_id / tool definitions / fast-path / aggregation
  / DB-only field exclusion.
- `tokenCounter.test.ts` updated for new drift semantics + UIChatMessage
  signature; one boundary case now triggers compression (intentional —
  the drift multiplier kicks in at the threshold).

Refs: LOBE-8964 (ECW edge boundary), LOBE-8972 (ECW umbrella),
LOBE-8973 (openrouter `:free` ctx), LOBE-8976 (compression diagnostics).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:22:19 +08:00
Arvin Xu da7e18281d feat(builtin-tool): add onBeforeCall / onAfterCall lifecycle hooks (#14719)
*  feat(builtin-tool): add onBeforeCall / onAfterCall lifecycle hooks

Tools that mutate state surfaced in the renderer (e.g. lobe-task) need a
way to invalidate UI caches after their own writes — but when the tool
runs server-side via a registered server runtime, the renderer never sees
the mutation and SWR caches go stale (e.g. delete-all-tasks succeeds on
the server but the kanban keeps showing the deleted rows).

Adds optional `onBeforeCall` / `onAfterCall` to `IBuiltinToolExecutor`,
both taking a single `ToolHookContext` object so the surface stays
non-breaking as we add fields. The gateway event handler dispatches them
on `tool_start` / `tool_end` regardless of whether the tool actually ran
client- or server-side.

`TaskExecutor` implements `onAfterCall` to refresh the task list / detail
SWR caches for write APIs. Also fills the missing `setTaskSchedule`
implementation in the server runtime so cloud-mode users can actually
configure schedules through the agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): widen empty-tasks hero to 960px

Aligns with the default `CONVERSATION_MIN_WIDTH` used elsewhere; the
720px cap was leaving the recommended-template grid feeling cramped on
wider monitors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(builtin-tool-task): refresh parent task detail after subtask mutation

Deleting a subtask through the agent left the parent's detail view
showing the stale child until a manual page reload — `onAfterCall` was
only invalidating the mutated task's own detail key, never the parent
whose `subtasks[]` array embeds it.

Adopt the same multi-target pattern that `updateTask` already uses in
the detail slice: walk `taskDetailMap` via `findSubtaskParentId` to
locate the embedding parent, and also refresh `activeTaskId`
defensively (covers e.g. `createTask` whose new identifier isn't yet in
the local map but whose parent the user is viewing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(builtin-tool): unwrap nested tool_end payload before dispatching hook

Real gateway `tool_end` events ship `data.payload` as the
`{ parentMessageId, toolCalling }` wrapper (see both publish sites in
`src/server/modules/AgentRuntime/RuntimeExecutors.ts`), but
`dispatchOnAfterCall` was passing that wrapper straight into
`readToolPayload`, which expects `identifier` / `apiName` at the top
level. Result: identity always undefined for server-runtime tool
completions, `onAfterCall` never fires, and the task cache invalidation
from the previous commit was effectively dead code.

Add `unwrapToolPayload` that prefers `payload.toolCalling` when present
and falls back to the flat shape, plus three regression tests covering
the wrapper, flat, and malformed cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool-task): colocate executor under client subpath

Aligns with the knowledge-base / lobe-agent precedent: drop the standalone
`./executor` subpath and re-export `taskExecutor` from `./client`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(builtin-tool): lazy-load executor registry to break import cycle

`gatewayEventHandler.ts` statically imported `getExecutor`, which transitively
pulled in tool client barrels (e.g. `@lobechat/builtin-tool-lobe-agent/client`
→ `PlanCard.tsx` → `@/store/chat`). Loading `gateway.ts` in isolation (as
the gateway.test.ts suite does) thus reached the chat-store module while
`gateway.ts` was still mid-evaluation, and the eager `useChatStore()` call
hit `new GatewayActionImpl(...)` before the class binding was initialized.

Dynamic-importing `getExecutor` inside the two async dispatch functions
breaks the cycle at module load; runtime behavior is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:50:00 +08:00
Arvin Xu 7083ab4ef5 🐛 fix(conversation): restore HTML preview for AssistantGroup messages (#14811)
PR #14703 wired @lobehub/ui's `enableHtmlPreview` into the Assistant
useMarkdown but missed the AssistantGroup path, so any full HTML
document the LLM emits in a grouped step rendered as a plain code
block instead of an iframe preview.

Extract the shared markdown wiring (components, plugins, animated,
HtmlPreviewDrawer) into useChatMarkdown so both paths use the same
configuration and the next markdown feature won't drift between them.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:29:21 +08:00
Arvin Xu 3dae46911b ️ perf(agent-tracing): zstd-compress S3 snapshots (#14807)
* ️ perf(agent-tracing): zstd-compress S3 snapshots

Compress operation snapshots with zstd (level 3) before uploading to S3
and write them under a `.json.zst` key. Measured on 76839 production
snapshots: 217 GB → 25.8 GB (8.4× average ratio, p99 47×). New uploads
only; old `.json` objects are left as-is.

The `.zst` suffix is the format indicator; Content-Encoding is
intentionally omitted so the object is served as opaque bytes and
readers decompress explicitly (avoids surprise behavior from HTTP
clients that negotiate zstd).

Uses Node's built-in zstd (node:zlib, available since Node 22.15) so
no new runtime dependency is added.

Reader updates:
- RemoteSnapshotStore.fetch decompresses the downloaded payload;
  local cache stays as plain `.json` for easy inspection.
- buildRemoteUrl now points at `.json.zst`.
- S3SnapshotStore.loadPartial falls back to the legacy `.json` key so
  in-flight QStash operations spanning the deploy keep working; the
  fallback dies off naturally once partials finalize.
- removePartial deletes both keys for clean transition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 chore(agent-tracing): gate zstd compression on NODE_ENV=production

Local dev (including ENABLE_AGENT_S3_TRACING=1 for S3 testing) keeps
writing plain `.json` so devs can inspect bucket payloads directly.
Only production deployments (NODE_ENV=production) compress + use the
`.json.zst` suffix.

Readers no longer assume the URL suffix matches the body format —
they sniff the zstd frame magic (0x28b52ffd) and decode accordingly.
This way prod-written `.json.zst` and dev-written `.json` round-trip
through the same code path regardless of which environment reads.

S3SnapshotStore.loadPartial tries the active suffix first then the
sibling format; removePartial cleans up both. RemoteSnapshotStore.fetch
falls back from `.json.zst` to plain `.json` on 404 so dev-uploaded
snapshots stay inspectable from another machine via the CLI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert "🔒 chore(agent-tracing): gate zstd compression on NODE_ENV=production"

This reverts commit 70d0b3d857.

*  test(agent-tracing): cover S3SnapshotStore zstd round-trip + legacy fallback

9 vitest cases mocking FileS3:
- save() → key ends in .json.zst, body starts with zstd magic, decompresses to original snapshot
- save() → falls back to "unknown" for missing agentId / topicId
- savePartial() → writes to _partial/ with zstd body
- loadPartial() → decodes .json.zst happy path
- loadPartial() → falls back to legacy .json on miss
- loadPartial() → returns null when neither key exists
- removePartial() → deletes both .json.zst and .json
- removePartial() → swallows individual delete failures (allSettled)
- get/getLatest/list/listPartials → return null/[] (OTEL owns querying)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:40:30 +08:00
Arvin Xu 36d0994ec2 🐛 fix(context-engine): attach diagnostic context to PlaceholderVariablesProcessor errors (#14741)
* fix: attach diagnostic context to ProcessorError/PipelineError

* fix: include cause summary in PipelineError message

* fix: pass structured cause to ProcessorError

* fix: enhance PlaceholderVariablesProcessor with diagnostic context

* 🐛 fix: preserve placeholderVariablesProcessed count for no-op messages

processMessagePlaceholdersWithDiagnostics always returns a spread {...message},
so the identity check `processed !== message` was always true and the count
incremented even when content was unchanged (e.g. messages with no placeholders
or only unresolved `{{missing}}` tokens). Restore the JSON-equality comparison
used by the pre-PR `processMessagePlaceholders` path.

Add regression coverage for the no-op cases and for new error paths:
- only-unresolved string content, only-unresolved array text parts, mixed batch
- per-message isolation when a generator throws
- defensive validation when variableGenerators is undefined / null

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:26:19 +08:00
Arvin Xu 516c04797d 🐛 fix(hetero-agent): defer fetch-triggering events to avoid parallel tool count rollback (#14806)
🐛 fix(hetero-agent): defer fetch-triggering events through persistQueue to avoid parallel tools[] rollback

When CC fires a large parallel tool batch, the gateway handler's
fetchAndReplaceMessages (triggered synchronously by tool_end) reads a
partial assistant.tools[] while persistToolBatch Phase 1/3 writes are
still queued, and replaceMessages clobbers the in-memory cumulative
tools[] — causing the "7 → 6 次技能调用" rollback users see in the
AssistantGroup count.

Defers tool_end / step_complete:execution_complete / stream_chunk with
toolMessageIds through persistQueue so the handler observes
DB state only after pending writes commit. Text / reasoning / regular
tools_calling forwards stay synchronous to preserve streaming UX.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:53:41 +08:00
LobeHub Bot f3cf7f4aed 🤖 style: update i18n (#14449) 2026-05-15 09:34:48 +08:00
Arvin Xu df8111aca0 🐛 fix(build): pin vite to 8.0.12 to avoid rolldown 1.0.1 preload regression (#14804)
Vite 8.0.13 bumps rolldown to 1.0.1, which ships a new
chunk-optimization dedupe pass (rolldown #9305) with an unsound
sibling-dynamic-entry handling — see rolldown #9350 (open). This
causes preload-deps entries (m.f in __vite__mapDeps) to be dropped,
leaving null slots; at runtime any dynamic import that hits the
shrunken table fires import(null) and throws "Failed to resolve
module specifier 'null'", taking down every tRPC call that flows
through src/libs/trpc/client/lambda.ts headers (await import('@/services/_auth')).

Because the repo runs with lockfile=false + resolution-mode=highest,
^8.0.9 silently floats to 8.0.13 on every fresh Vercel build. Pin
exactly to 8.0.12 (which uses rolldown 1.0.0) until rolldown 1.0.2 /
Vite 8.0.14 lands a fix.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:20:50 +08:00
Rdmclin2 566b261a12 feat: support bot watch (#14796)
* feat: add whatsAPP and iMessage comming soon

* chore: update i18n

* feat: support watch keyword instruction

* feat: add cli and messager api for bot channels

* fix: test cases

* feat: add system prompt for messenger tool

* feat: add messenger mdx
2026-05-15 00:36:40 +07:00
Innei e00c299d1c 🐛 fix(onboarding): resolve agent route loading stall and branch redirect (#14795)
* 🐛 fix(onboarding): refresh branch config before redirect

* 🐛 fix(onboarding): refresh agent route flag before branch guard

* 🐛 fix(onboarding): simplify agent branch guard

* 🐛 fix(onboarding): eliminate agent route loading stall

- Make AgentModel.getBuiltinAgent idempotent under concurrent callers.
  The web-onboarding builtin agent was inserted by both the bootstrap
  query and the standalone useInitBuiltinAgent SWR in parallel; the
  insert loser hit agents_slug_user_id_unique and SWR sat in its ~5s
  error-retry window before the row could be read.
- Prefetch /onboarding/agent and /onboarding/classic chunks while the
  shared-prefix steps are visible, so the branch redirect no longer
  pays a cold chunk load.

* 🐛 fix(onboarding): skip prefetch under test and complete fixture

- Add `__TEST__` Vite define so renderer code can branch on Vitest runs
  (set true in vitest.config.mts, false in sharedRendererDefine).
- Guard the shared-prefix chunk prefetch with `if (__TEST__) return`.
  Otherwise the fire-and-forget `import('@/routes/onboarding/agent')`
  resolves after the test asserts and tries to load builtin-agents,
  which the test's partial `vi.mock('@lobechat/const')` doesn't supply
  (`DEFAULT_MODEL` missing), surfacing as 25 unhandled rejections.
- Fix `extract.runtime.test.ts` fixture to include the new required
  `agentBenchmarkLoCoMo` field on `MemoryExtractionPrivateConfig`,
  added in 20267fc77c.
2026-05-15 01:19:37 +08:00
Arvin Xu e0d20e86fc feat: support chat mode and redesign chat input action bar (#14774)
* Refine chat parameter controls and working sidebar

* 💄 style: refine chat parameter controls

* 💄 style: refine chat input action affordances

* 💄 style: refine chat input control menus

* 💄 style: refine chat input skills menu

* 🐛 fix: replace skills policy dropdown with popover

* fix: base-ui dropdown

* fix: base-ui dropdown

* 💄 style: fix popover conflict and refine skills menu layout

- Extract PopoverLabel component with controlled open state to prevent
  conflict when skill policy menu opens
- Dispatch custom close event so detail popovers close before policy popover opens
- Add divider between pinned and auto skill groups
- Refine sticky search/footer padding via CSS attribute selectors
- Remove stray console.log from ActionDropdown

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💄 style: refine skills policy menu and chat input UI

- Skills policy menu: change active icon color to blue, add divider +
  uninstall action for Klavis/MCP/agent-skill items, suppress detail
  popover when the "..." policy menu is open
- Minor refinements across ChatInput, Conversation Error/ContentLoading,
  and HeterogeneousAgent StatusGuide components

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat: add custom MCP tag and configure action to skills menu

- Show orange "Custom" tag next to custom MCP plugin entries
- Add Configure action above Uninstall in the policy popover that
  opens the PluginDevModal drawer for editing the custom plugin

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat: default agent mode to true and gate chat mode at the tools engine

- Move `enableAgentMode` from `LobeAgentConfig` to `LobeAgentChatConfig` so it
  persists via the existing `chat_config` jsonb column and is readable on the
  server (the top-level field was silently dropped by drizzle).
- Default to agent mode for all agents — selectors treat `undefined` as `true`;
  only an explicit `false` collapses to chat mode.
- Introduce `chatModeAllowedToolIds = [knowledge-base, memory, web-browsing]`.
  Both `createServerAgentToolsEngine` and the frontend `createAgentToolsEngine`
  now switch on this whitelist in chat mode: skip user plugins, skip
  `alwaysOnToolIds`, narrow `defaultToolIds`, and turn off
  `allowExplicitActivation` so the activator can't smuggle other tools in.
- `useToggleAgentMode` is the single mode-switch entry; `plugins[]` is left
  alone — chat mode is enforced at runtime, not by mutating saved config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat: extend topic status with running/paused/failed

Widen `ChatTopicStatus` enum (DB schema, types, TRPC validation) to cover the
in-flight lifecycle that gateway and heterogeneous executor runs report. Add a
`updateTopicStatus` store action and have both runtime paths write `running`
on start and `active` on completion (or `failed` on terminal error). Sidebar
topic items render a spinner while `status === 'running'`.

Note: drizzle migration for the widened enum needs to be generated separately.

* 💄 style: polish skills menu — official tag, tooltip on settings button

Add a LobeHub "official" badge to builtin tools and agent skills surfaced in
the Skills menu. Wrap the menu's settings button in a Tooltip. Scope the
group-header padding reset to the skill-activation group only so the
Knowledge submenu keeps its native section padding.

*  feat: mark topic as paused while awaiting human tool approval

Extend the heterogeneous-agent topic status machine (c0170d032f) with a
paused state. The gateway event handler writes topic.status = 'paused' on
step_start { phase: 'human_approval' } — one hook covers both Gateway and
desktop heterogeneous paths since they share the same handler.

Resume back to 'running' is free: approve / reject_continue both spawn a
fresh op via the executor entries, which already persist 'running'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat: gate skills and agent-document injectors at the context engine in chat mode

Thread `enableAgentMode` into `MessagesEngine`. When it is explicitly `false`,
the engine forces `enabled: false` on:
- SkillContextProvider — drops the <available_skills> block
- All AgentDocument injectors (BeforeSystem / SystemAppend / SystemReplace /
  Context / Message) — drops every agent-document position

The frontend (`src/services/chat/mecha/contextEngineering.ts`) and server
(`src/server/modules/AgentRuntime/RuntimeExecutors.ts` →
`serverMessagesEngine`) read `chatConfig.enableAgentMode` from agent config
and pass it through; no caller needs to know which injectors to skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat: also gate agent-management context in chat mode

`agentManagementContext` (the `<current_agent>` + `<available_agents>` block)
was leaking into chat-mode prompts whenever the agent was in auto-skill mode,
because its caller-side guard (`isInAutoSkillMode || isAgentManagementEnabled`)
is orthogonal to `enableAgentMode`. Fold the gate into the same `isAgentMode`
switch already covering skills + agent documents in `MessagesEngine` so the
injector goes off in chat mode regardless of how the caller populates the
context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix: drop orphan rebase marker in OperationTraceRecorder

Leftover `<<<<<<< HEAD` from an earlier rebase that was only half cleaned —
the HEAD-side content is the one we want; just delete the marker line so the
file type-checks again.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style: cursor-style action bar on home input

Rework the home ChatInput footer to read like Cursor's composer while keeping
the model picker on the right:

- Replace the `agentMode` icon-only button with a pill trigger (icon + label
  + chevron) carrying a persistent fill, dropping a `bottomLeft` mode
  popover. Reuses the `RuntimeConfig/ModeSelector` design in place so any
  other action bar consumer picks it up automatically.
- Introduce a `modelLabel` action that shows the resolved model display name
  + chevron, opening `ModelSwitchPanel`. The original `model` icon stays
  untouched for callers that prefer the compact form.
- Wire the home input to use ['agentMode','plus'] on the left and
  ['modelLabel'] on the right; bump `SendArea` gap to 12 and add
  `paddingLeft={6}` to the action bar so the pill aligns with the input
  placeholder.
- Localize `chatMode.chat` to "对话" in zh-CN (default English stays "Chat").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style: surface params panel toggle and hide it for heterogeneous agents

- Drop the developer-mode gate on the conversation header params toggle so it
  ships by default; popup routes remain excluded.
- Hide both the header toggle and the right sidebar `Params` tab for
  heterogeneous agents (Claude Code / Codex etc.), since their model params
  panel doesn't apply. The active-tab resolver also falls back away from
  `params` when it isn't available.
- Strengthen the Tools popover divider to `colorFill` so the header /
  footer separators stay visible against the elevated dark-mode surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🚑 fix: address type errors surfaced on the new-input branch

- Move the `border` from the removed `overlayInnerStyle` onto `styles.content`
  so the AgentMode / ModeSelector popovers compile against the base-ui
  `PopoverProps` shape.
- Pass `paddingLeft: 6` through `style` on `ChatInputActions` since the
  underlying Flexbox only accepts `padding` / `paddingBlock` / `paddingInline`.
- Tighten skill / market menu items: drop the unsupported `closeOnClick`
  from the group item, fallback the uninstall display name to
  `identifier`, swap the antd-style `type: 'warning'` confirm option for
  `okButtonProps.danger`, and assert the conditionally-spread market
  items as `ItemType` so the inferred union no longer contains
  `undefined`.
- Annotate `resolveMark` in `LevelSlider` so the fallback branch returns
  a `ReactNode` label, fixing the `MarkObj` mismatch on `LevelOption`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:07:47 +08:00
YuTengjing b5871d327a 🐛 fix: preserve resume request trigger (#14798) 2026-05-14 23:43:09 +08:00
YuTengjing 875c9b49eb 🐛 fix: reduce task template skeleton CLS (#14788)
* 🐛 fix: reduce task template skeleton CLS

* 🐛 fix: align recommendation skeleton count

* 🐛 fix: derive recommendation skeleton count

*  test: cover recommendation count without rendering

*  test: move recommendation count coverage to const

* ♻️ refactor: simplify task template recommendation count

* ♻️ refactor: remove task template recommendation aliases

* 🐛 fix: use task template count constant in router

* ♻️ refactor: remove task template count max
2026-05-14 23:23:21 +08:00
Innei 1914ae6d43 🐛 fix(desktop): restrict local file previews (#14789)
* 🐛 fix(desktop): restrict local file previews

* 🐛 fix(desktop): close TOCTOU in localfile protocol handler

* 🐛 fix(desktop): guard approveWorkspaceRoots against undefined input

App.test.ts StoreManager mock returned undefined for unknown keys,
causing TypeError when approveWorkspaceRoots tried to call .map().
Added default parameter and updated mock to return defaultValue.

*  test: stabilize ci dependency resolution
2026-05-14 22:08:57 +08:00
YuTengjing ffd66d5465 📝 docs: simplify and refresh skill docs (#14785) 2026-05-14 15:53:05 +08:00
Arvin Xu d00770a956 💄 style: AnalyzeVisualMedia inspector, Portal HTML preview refactor & CE trace dedup (#14777)
*  feat: add AnalyzeVisualMedia inspector, Portal HTML preview refactor, and CE trace dedup

- Add AnalyzeVisualMedia inspector and state types to builtin-tool-lobe-agent
- Refactor Portal HTML renderer to use @lobehub/ui built-in HtmlPreview
- Add portal artifact type selector and portal selectors to distinguish HTML/other artifacts
- Dedup context_engine_result events in OperationTraceRecorder; add resolveCeEvent in viewer
- Update .agents/skills/builtin-tool/references/ui.md with Tool Render design principles
- Bump @lobehub/ui to 5.12.0 for HtmlPreview support

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🧪 test(trace-recorder): add deduplicateCeEvent tests for context_engine_result dedup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(agent-tracing): wire resolveCeEvent into all CE reader paths

All render functions and CLI inspect paths now call resolveCeEvent(step, allSteps)
instead of reading step.events?.find(...) directly, so deduplicated steps
correctly reconstruct their context_engine_result input/output by walking back
through previous steps.

Affected: renderSystemRole, renderEnvContext, renderPayloadTools, renderPayload,
renderMemory, renderMessageDetail, renderStepDetail, and all --system-role /
--env / --payload-tools / --payload / --memory CLI branches (both text and --json).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(conversation): pass onRegenerate through ErrorMessageExtra and fix error guard order

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(agent-tracing): lift context_engine_result out of events into typed contextEngine field

Replace ad-hoc CE event dedup (mutating input/output inside events[]) with a
dedicated `contextEngine` field on StepSnapshot that uses the same delta pattern
as messagesBaseline/messagesDelta. CE data is structural state, not a streaming
event — keeping it in events[] was a semantic mismatch.

- Add `StepSnapshot.contextEngine?: { input?, output? }` with full delta semantics
- OperationTraceRecorder: extract CE from events before building snapshotEvents,
  store in contextEngine, deduplicate via deduplicateCeSnapshot (no more mutations)
- viewer: add resolveCeSnapshot (reads contextEngine first, falls back to legacy
  events format for old snapshots); deprecate resolveCeEvent alias
- inspect CLI: update all call sites to resolveCeSnapshot
- tests: rewrite deduplicateCeEvent suite → contextEngine dedup suite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💄 style(loading): use colorTextTertiary for elapsed time display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:25:54 +08:00
Neko 20267fc77c 🔨 chore(memory-user-memory): add benchmark agent config (#14779) 2026-05-14 14:45:30 +08:00
Neko 4630785870 🔨 chore(memory-user-memory): support source ids in extraction schemas (#14778) 2026-05-14 14:45:09 +08:00
Rdmclin2 5b7611615e 🐛 fix: system bot error (#14784)
* chore: add start link short cut

* chore: update qq zh files

* fix: add messenger block message alert

* chore: update i18n files

* fix: messenger router bridge

* fix: dm thread create problem

* chore: remove lab prefer for messenger

* chore: update i18n files

* fix: e2e test
2026-05-14 13:26:10 +07:00
Arvin Xu ec547a3b57 🐛 fix(topic): restore indent for heterogeneous agent topic rows (#14783)
Remove the dead `return null` branch that skipped icon rendering entirely
for heterogeneous agents (Claude Code, Codex, …).  The early return caused
`NavItem` to omit the 28 px icon `<Center>` container, shifting the title
text leftward and breaking visual alignment with regular topic rows.

The existing `visibility: hidden` style on the HashIcon already preserves
the layout box while hiding the glyph — the null return just prevented it
from ever running.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 12:58:09 +08:00
Innei 36c4be46f0 🐛 fix(desktop): split runtime externals from native deps (#14776) 2026-05-14 01:57:46 +08:00
Neko 7b136a210f 🐛 fix(agent-signal): avoid blocking agent execution (#14775) 2026-05-14 01:53:11 +08:00
Innei 9075d5dfd3 refactor: merge agent marketplace into web onboarding
*  feat(desktop): open-in-app + agent files tab + localfile protocol

Bundle three related desktop features:
- Open-in-app: IPC contract, main-process detector/launcher/icon-extractor,
  renderer service, OpenInAppButton + hook, agent header / portal /
  files-tab integration, user preference (defaultOpenInApp).
- Agent files tab: working sidebar files tab with file tracking, store
  wiring, i18n, reveal-in-tree action in Review/FileItem.
- LocalFile protocol: serve binary images via localfile:// for inline
  preview in the review panel.

* 🐛 fix: add explicit type annotation for ref parameter in Files test

Fix TS7031: Binding element 'ref' implicitly has an 'any' type.
This error was caught by tsgo type-check in CI.

* 🐛 fix: address codex review feedback (P1 reveal retry + P2 WebStorm Windows detection)

* 🐛 fix(open-in-app): avoid process.platform reference in renderer

The Electron renderer sandbox does not expose `process`, so reading
`process.platform` in the useOpenInApp hook crashes with a ReferenceError
on app launch. Use the `window.lobeEnv.platform` value already exposed
via preload contextBridge instead.

* 🐛 fix(conversation): keep assistant runtime errors outside workflow collapse

When an assistant block carries a runtime error, render the error in the
answer segment instead of letting it fold into the workflow collapse with
the surrounding tool calls.

*  feat(portal): add file viewer tab strip and local file protocol improvements

- Add tabbed interface for local file portal viewer
- Extend LocalFileProtocolManager with audio MIME type support
- Add portal actions for file navigation and tab management
- Improve OpenInAppButton and conversation header integration
- Update working sidebar resources section
- Add comprehensive portal action tests

*  feat(agent-sidebar): redesign Review panel and refine Files explorer

- Review: drop antd Collapse, replace with a linear disclosure list
  (hairline dividers, no rounded cards, chevron-left, role=button rows).
  Add motion height/opacity expand animation. Compact row spacing.
  Move hover-revealed copy/reveal/revert into an absolute Flexbox with
  a gradient mask so they overlay the right edge without taking layout.
- Files: extract useGitWorkingTreeFiles hook + tests; surface git
  status entries in the working tree explorer.
- ExplorerTree: share folder icon style; minor type tweak.
- Locales: new chat strings for the above.

* 🐛 fix(test): add missing chatConfigByIdSelectors mock to WorkingSidebar test
2026-05-14 01:45:43 +08:00
YuTengjing 1c429f8d28 feat(chat): add Onboarding request trigger and pass via metadata (#14770)
*  feat(chat): add Onboarding request trigger and pass via metadata

- Add RequestTrigger.Onboarding for onboarding chat requests
- Replace requestTrigger option with metadata.trigger across chat service / executors
- Tag onboarding agent send-message with metadata.trigger = Onboarding
- Persist trigger on message metadata for billing & logs

* 🔨 chore(chat): share request context header constants

* 🐛 fix(chat): preserve trigger on tool resumes

* 🔧 chore(builtin-agents): expose package entry types

*  test(types): preserve request trigger metadata

* 🐛 fix(chat): scope resumed trigger metadata to message chain
2026-05-14 00:32:26 +08:00
Neko ac250b9897 ♻️ refactor(agent-signal,server,app,database,locales): self iteration exits lab (#14769) 2026-05-14 00:04:57 +08:00
Neko e8b7fe14e1 🐛 fix(server,memory-user-memory): embedding token exceeded, should limit and cut off searched memory query (#14757) 2026-05-13 22:32:28 +08:00
Innei 79cf5febed 🐛 fix(kb): preserve files on NoSuchKey and clean orphan documents/tasks (#14501)
* 🐛 fix(kb): preserve files on NoSuchKey and clean orphan documents/tasks

NoSuchKey from object storage no longer cascades into wholesale deletion
of file rows (and their chunks/embeddings). Instead the async chunking
task is marked Error with a clear message so users can re-upload or
retry. Files whose url uses the `internal://` scheme (mirror rows for
inline custom/document) skip storage fetch entirely.

fileModel.delete and deleteMany now also remove (a) mirror documents
where sourceType='file' and fileId matches, and (b) the chunk/embedding
asyncTasks rows tied to the file. Without this, deletion left orphan
documents (still indexed by BM25, still occupying KB slots) and dangling
task rows.

Closes LOBE-8607

* 🐛 fix(kb): delete document storage objects
2026-05-13 22:22:19 +08:00
Innei 4b6b341951 💄 fix(nav-panel): polish SideBarDrawer & header layout details (#14762)
* 💄 fix(nav-panel): polish SideBarDrawer & header layout details

- Use SMALL icon size for close button and settings icon
- Remove unused imports and dead code in SideBarHeaderLayout
- Fix topic item padding in AllTopicsDrawer Content

* 🐛 fix(nav-panel): update ITEM_HEIGHT to match new row height without vertical padding

Address Codex review feedback on PR #14762.
The padding change from padding='4px 8px' to paddingInline={4} removed
the 4px top/bottom padding, reducing row height from ~44px to ~36px.
Update ITEM_HEIGHT estimate from 44 to 36 to keep virtualization
fill logic accurate.
2026-05-13 20:41:03 +08:00
AmAzing- 44892960e0 feat: add Agent Signal marker to receipt descriptions (#14764)
 feat: add agent signal marker to receipt descriptions
2026-05-13 19:19:52 +08:00
Innei dc86f38dc1 🐛 fix(onboarding): hide ModeSwitch in production environment (#14760)
The ModeSwitch component was rendering in production because the cloud
repo sets AGENT_ONBOARDING_ENABLED=true, bypassing the isDev guard
inside the component. Wrap the entire ModeSwitch with isDev so neither
the segmented control nor dev actions appear in prod.
2026-05-13 19:07:39 +08:00
LiJian 3e43683132 🔨 chore(heteroContext): clarify sandbox TTL and add public-repo fork push guide (#14761)
* 🔨 chore(heteroContext): clarify sandbox TTL and add public-repo fork push guide

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(heteroContext): make fork remote setup idempotent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 17:52:35 +08:00
LobeHub Bot 2cfe9f6180 🌐 chore: translate non-English comments to English in file-loaders (#14744)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:34:53 +08:00
Neko c9bb82d09d 🐛 fix(builtin-tool-memory): clarify memory retrieval sufficiency rules (#14753) 2026-05-13 15:19:43 +08:00
Rdmclin2 6933ddc4e5 🔨 chore: Online Messager (#14755)
* feat: add line integration Banner

* chore: remove messenger lab switch

* feat: add messenger banner

* feat: add messenger promo

* chore: update i18n files
2026-05-13 14:17:07 +07:00
Arvin Xu ef8aa72af5 🐛 fix(brief): add ignore action next to retry on error briefs (#14742)
*  feat(brief): add ignore action next to retry on error briefs

Lets users dismiss error briefs without re-running the task. The button
is hardcoded in the UI alongside the retry primary action; brief.actions
stays untouched.

*  feat(agent-runtime): wire trigger field across all execAgent call sites

- Add Cli / Openapi / Notify values to RequestTrigger enum
- Pass trigger:'cli' from CLI command, trigger:'openapi' from OpenAPI service
- Pass trigger:RequestTrigger.Eval from all 4 agentEvalRun call sites
- Pass trigger:RequestTrigger.Notify from agentNotify router
- Default trigger to RequestTrigger.Chat in execAgent/execAgents tRPC handler
- execGroupAgent passes trigger:RequestTrigger.Chat explicitly
- execSubAgentTask inherits trigger from parent operation (best-effort DB lookup)
- Expose trigger as optional input on ExecAgentSchema so callers can override
- Remove dead aiAgent.createOperation tRPC mutation and its frontend counterpart
- Delete test file that only covered the removed createOperation method

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💄 style(loading): use shiny text animation for operation labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(error): broaden heterogeneous agent error guard to match any error type

The previous guard required `error.type` to be `AgentRuntimeError` or absent,
which missed cases like `ServerAgentRuntimeError`. Extract the detection into a
proper type guard (`isHeterogeneousAgentStatusGuideError`) that checks only the
body shape (agentType + code), making it resilient to wrapper error types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 15:12:24 +08:00
Neko 8618699888 🐛 fix(server/toolExecution): support server-owned memory embedding runtime (#14754) 2026-05-13 15:09:17 +08:00
Neko bfc4820a17 🐛 fix(server/userMemories): return locomo ingestion session results (#14752) 2026-05-13 15:09:10 +08:00
LiJian d8bfc58f22 🐛 fix(casc): replace new Function() template with safe string builders (#14751)
* 🐛 fix(casc): replace new Function()-based template with safe string builders and self-fetching ChangelogModal

- Remove es-toolkit/compat template (uses new Function()) from ShareModal, ShareMessageModal, and parserPlaceholder; replace with plain string building and String.replace
- ChangelogModal now self-fetches latest changelog id via lambdaClient instead of relying on async server component wrapper; setTimeout starts after data arrives
- Remove ChangelogService/gray-matter import from route component

* 🐛 fix(casc): add missing deps to changelog timer effect
2026-05-13 14:59:50 +08:00
Neko 690098dcb9 🐛 fix(agent-signal,server): both skill bundle and skill index should be considered as primary skill documents (#14748) 2026-05-13 13:11:59 +08:00
Neko a12079d338 🐛 fix(server): user id context missing in tool outcome for signal (#14749) 2026-05-13 13:11:49 +08:00
LiJian 8d1584eb78 🐛 fix(cc): preserve trailing suffix after partial deltas (#14745)
* 🐛 fix(cc): preserve trailing suffix after partial deltas

* 🐛 fix(cc): clear streamed delta buffers after reconciliation

* 🐛 fix(cc): clear streamed buffers per modality
2026-05-13 12:56:00 +08:00
LiJian c3bb289c44 🐛 fix(market-auth): add offline_access scope and guard expiresIn default (#14743)
Add `offline_access` to the OIDC authorization scope so the server
returns a refresh_token, fixing silent session expiry after ~24h.

Guard `tokenResponse.expiresIn` with `?? 3600` to prevent `NaN`
propagation into `expiresAt` when the server omits the field.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 11:30:07 +08:00
lobehubbot b125565597 🔖 chore(release): release version v2.1.58 [skip ci] 2026-05-13 02:01:19 +00:00
lobehubbot c19f87fdb2 Merge remote-tracking branch 'origin/main' into canary 2026-05-13 01:59:32 +00:00
Arvin Xu 9d03349c46 🚀 release: 20260513 (#14739)
# 🚀 LobeHub Release (20260513)

**Hotfix Scope:** Ship the canary backlog (111 PRs) onto main as a
fast-tracked patch — operator-focused, no weekly-style write-up.

> Brings the accumulated canary work into main: agent/task improvements,
hetero-agent fixes, desktop & onboarding polish, and several reliability
caps.

##  What's Included

- **Agent & tasks** — Self-review proposal-to-action automation,
sub-agent dispatch consolidated to `lobe-agent`, AskUserQuestion wiring
for Claude Code, scheduler/hotkey/TodoList polish. (#14583, #14657,
#14715, #14639, #14732, #14707, #14713)
- **Home & onboarding** — Daily brief with linkable welcome + paired
input hint, inline skill auth in recommended task templates, cleanup of
captcha-on-signin and marketplace early-exit. (#14589, #14676, #14573,
#14598)
- **Bots & integrations** — Slack MPIM support, Discord DM fix,
slash-command + connect-error fixes, gateway client-tool plugin state.
(#14733, #14591, #14596)
- **Desktop & CLI** — Windows `.cmd` shim detection for `claude` /
`codex` CLIs, auth focus & pending-login reset fixes. (#14720, #14694,
#14695)
- **Reliability** — Cap web-crawler body size and image binary at safe
limits, attach error listeners to Neon/Node pools, reject inactive OIDC
access. (#14660, #14711, #14606, #14674)
- **Database** — `agent_operations` table + persist agent operations
from the runtime; switch user memory search to `paradedb.match(...)`.
(#14416, #14736, #14590)

## ⚙️ Upgrade

- **Self-hosted:** pull the latest image and restart. Drizzle migrations
(including the new `agent_operations` table) run automatically on boot.
2026-05-13 09:58:47 +08:00
Zhijie He 1a745382b5 💄 style: add spark-x2-flash support (#14731)
* style: add spark-x2-flash support

* fix: fix deployname not send to api

fix: fix deployname not send to api

fix: fix deployname not send to api

fix: fix deployname not send to api

fix: fix deployname func

fix: fix deployname func
2026-05-13 03:08:55 +08:00
Arvin Xu a77234107e feat(agent-runtime): persist agent operations to agent_operations table (#14736)
*  feat(agent-runtime): persist agent operations to `agent_operations` table

Wire start-time INSERT and terminal UPDATE into the agent runtime so
operation history outlives the 2-hour Redis TTL. Adds
`AgentOperationModel` with `recordStart` / `recordCompletion` /
`findById` (scoped by userId so a leaked operationId can't flip another
user's row) and threads both calls through `CompletionLifecycle`, which
now owns both ends of the persistence lifecycle. Also plumbs
`parentOperationId` through `ExecAgentParams` → `OperationCreationParams`
so sub-agent invocations carry their parent lineage. Per-step aggregate
updates are intentionally out of scope.

Refs LOBE-8848

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): update CompletionLifecycle test constructor to 2 args

CompletionLifecycle now constructs MessageModel internally from
(db, userId), so the test builder passing a third messageModel arg
tripped tsgo --noEmit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:14 +08:00
Rdmclin2 729265ab5d feat: support slack mpim and fix discord dm problem (#14733)
* feat: support mpim

* chore: add errorMsg

* fix: discord commands thinking error

* fix: discord typing error

* feat: add oauth process for discord
2026-05-13 02:57:14 +08:00
Arvin Xu 5174c13ef1 🐛 fix(hetero-agent): wire AskUserBridge response events to renderer (#14732)
Close the wire-protocol gap that left CC's AskUserQuestion form stuck on
"pending" after the bridge gave up. AskUserBridge now emits an
agent_intervention_response event on every terminal path (timeout,
user resolve, cancel, cancelAll), and heterogeneousAgentExecutor handles
it by stamping pluginIntervention.status = 'rejected' for timeout /
session_ended (user-driven paths are filtered out — already optimistic).

Layered defenses so a late Submit no longer throws "Operation not found":
- cleanupCompletedOperations: find→filter so every messageOperationMap
  entry pointing to the cleaned op is removed (assistant + tool message
  pairs previously stranded one entry as a dangling reference).
- internal_getConversationContext: log + fall back to global state when
  the op has been GC'd, instead of throwing.
- submitHeteroIntervention: detect a stale opId before passing it into
  the optimistic chain.

Scoped as a short-term backstop until LOBE-8746 retires the AskUser MCP
bridge entirely.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:14 +08:00
Arvin Xu dcc9f78091 ♻️ refactor(builtin-tool): move sub-agent dispatch from lobe-gtd to lobe-agent (#14715)
* ♻️ refactor(builtin-tool): move sub-agent dispatch from lobe-gtd to lobe-agent

Move the `execTask` / `execTasks` capability out of `packages/builtin-tool-gtd/`
and into `packages/builtin-tool-lobe-agent/`, renaming the public APIs to
`callSubAgent` / `callSubAgents`. The "subtask" naming inside GTD overlapped
with the new lobe-task tool's task model and conflated planning with
sub-agent dispatch.

- API names: `execTask` → `callSubAgent`, `execTasks` → `callSubAgents`
- TS types: `ExecTaskParams` → `CallSubAgentParams`, etc.; introduce
  `SubAgentTask` to replace `ExecTaskItem`
- Client UI (Inspector / Render / Streaming) ported under
  `packages/builtin-tool-lobe-agent/src/client/`
- Central registries (`packages/builtin-tools/src/{inspectors,renders,streamings}.ts`)
  updated to register lobe-agent
- GTD `meta.description` and system role no longer mention async tasks;
  they point to lobe-agent for sub-agent dispatch
- `isSubTask` filtering in `agentConfigResolver` now excludes `lobe-agent`
  (new owner of sub-agent dispatch) instead of `lobe-gtd`
- i18n: new `builtins.lobe-agent.apiName.callSubAgent*` and
  `workflow.toolDisplayName.callSubAgent*` keys in default/zh-CN/en-US

Kept the executor's emitted `state.type` values (`execTask` / `execTasks` /
`execClientTask` / `execClientTasks`) unchanged so the agent-runtime
instruction layer (`exec_task` / `exec_tasks` / `exec_client_task*`) and all
downstream tests / heterogeneous executors (`builtin-tool-agent-management`,
server `agentManagement` runtime) continue to work without modification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(chat): rename isSubTask flag to isSubAgent

After moving sub-agent dispatch from lobe-gtd to lobe-agent, the flag name
no longer matches what it controls. Rename `isSubTask` → `isSubAgent` across
the chat / agent runtime layer and update related comments and test labels.

- `agentConfigResolver` context field + filter helper
- `streamingExecutor.internal_createAgentState` + `executeClientAgent`
  signatures and call sites
- `createAgentExecutors` (exec_task / exec_client_task handlers) and
  `GroupOrchestrationExecutors` (batch_exec_async_tasks)
- `chatService.createAssistantMessageStream` `resolvedAgentConfig` docs
- Test descriptions and assertions in `agentConfigResolver.test.ts` and
  `streamingExecutor.test.ts`

No behavior change — the flag's filter target (`lobe-agent` identifier) is
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): rename exec_task wire identifiers to exec_sub_agent

Bring the agent-runtime "wire" naming in line with the lobe-agent
callSubAgent / callSubAgents API rename. Three layers are renamed in lockstep
to keep the bridge between tool executors and the runtime consistent:

1. Tool-emitted state.type discriminators
   - 'execTask' → 'execSubAgent'
   - 'execTasks' → 'execSubAgents'
   - 'execClientTask' → 'execClientSubAgent'
   - 'execClientTasks' → 'execClientSubAgents'

2. AgentInstruction.type and matching TS interfaces
   - 'exec_task' / 'exec_tasks' / 'exec_client_task' / 'exec_client_tasks'
     → 'exec_sub_agent' / 'exec_sub_agents' / 'exec_client_sub_agent' /
       'exec_client_sub_agents'
   - AgentInstructionExecTask → AgentInstructionExecSubAgent (and the three
     siblings)
   - ExecTaskItem → SubAgentTask

3. AgentRuntimeContext.phase + matching payload types
   - 'task_result' → 'sub_agent_result'
   - 'tasks_batch_result' → 'sub_agents_batch_result'
   - TaskResultPayload → SubAgentResultPayload
   - TasksBatchResultPayload → SubAgentsBatchResultPayload

Also renames the operation-type discriminator 'execClientTask' /
'execClientTasks' to 'execClientSubAgent' / 'execClientSubAgents' and updates
its locale string in default / zh-CN / en-US.

Tests / fixtures / mocks updated in lockstep:
- packages/agent-runtime/src/agents/{GeneralChatAgent.ts,__tests__/...}
- packages/builtin-tool-{lobe-agent,agent-management}/src/...
- src/server/services/toolExecution/serverRuntimes/agentManagement.ts
- packages/agent-mock/src/cases/builtins/todo-write-stress.ts (helper renamed
  to callSubAgent)
- src/store/chat/agents/createAgentExecutors.ts + exec-task / exec-tasks tests
  + fixtures/mockInstructions.ts (createExecSubAgent[s]Instruction)
- src/store/chat/slices/aiChat/actions/streamingExecutor.ts (phase check)
- packages/conversation-flow/src/__tests__/fixtures/**/*.json (8 fixtures
  retargeted from lobe-gtd/execTask[s] to lobe-agent/callSubAgent[s] with the
  new state.type wire values)

No behavior change — the agent runtime, executors and tests all go through
the same code paths; only the strings on the wire change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool): absorb GTD tool (plan + todo) into lobe-agent

Delete `packages/builtin-tool-gtd/` and fold its full surface — plan, todo,
ExecutionRuntime, all client UI (Inspector / Render / Streaming /
Intervention / SortableTodoList) and the system role — into
`packages/builtin-tool-lobe-agent/`. Single `lobe-agent` identifier now
owns: plan + todo management, sub-agent dispatch, and visual media analysis.

Also restructures the lobe-agent package so the executor lives under
`./client/` alongside the UI it ships with, and drops the dedicated
`./executor` export — consumers go through `./client` for everything
client-side.

Package-level changes:
- DELETE `packages/builtin-tool-gtd/` entirely.
- `packages/builtin-tool-lobe-agent/`
  - Move `src/executor/` → `src/client/executor/`. Drop `./executor` from
    `package.json` exports; expose `lobeAgentExecutor` via `./client` only.
  - Rename `GTDExecutionRuntime` → `PlanExecutionRuntime` and place under
    `src/client/executor/PlanRuntime/`. Re-export from package root so the
    server runtime can consume it without pulling in client UI deps.
  - Extend `LobeAgentExecutor` with `createPlan` / `updatePlan` /
    `createTodos` / `updateTodos` / `clearTodos`, all delegated to the
    shared runtime.
  - Add Plan + Todo API entries to the manifest (with their original
    descriptions, humanIntervention, renderDisplayControl).
  - Move all GTD client UI verbatim:
    `Inspector/{ClearTodos,CreatePlan,CreateTodos,UpdatePlan,UpdateTodos}`,
    `Render/{CreatePlan,TodoList}`, `Streaming/CreatePlan`,
    `Intervention/{AddTodo,ClearTodos,CreatePlan}`,
    `components/SortableTodoList`. Register them in
    `LobeAgentInspectors / Renders / Streamings`, add new
    `LobeAgentInterventions`.
  - Merge GTD system role into lobe-agent's (`<plan_and_todos>` plus the
    existing `<sub_agents>` and `<run_in_client>` sections).
  - `package.json`: pick up `@lobechat/prompts` dep and `@lobehub/editor` +
    `antd` + `lucide-react` peer-deps inherited from GTD.

Central registries (`packages/builtin-tools/src/*`) and consumers:
- Remove every `GTDManifest / Inspectors / Renders / Streamings /
  Interventions` import + registration; existing `LobeAgent*` registrations
  now cover them.
- Replace `[GTDManifest.identifier]: GTDInterventions` with
  `[LobeAgentManifest.identifier]: LobeAgentInterventions`.
- Drop `@lobechat/builtin-tool-gtd` workspace dep from
  `packages/builtin-tools/package.json`, `packages/builtin-agents/package.json`
  and root `package.json`.
- Remove `gtdExecutor` from `src/store/tool/slices/builtin/executors/index.ts`;
  switch `lobeAgentExecutor` import to `/client`.
- Replace `serverRuntimes/gtd.ts` with a service factory
  `serverRuntimes/lobeAgentPlan.ts` (`createServerPlanRuntimeService`).
  `serverRuntimes/lobeAgent.ts` instantiates `PlanExecutionRuntime` with
  that service so the registry exposes one runtime per `lobe-agent`
  identifier covering both visual analysis and plan/todo.
- `services/chat/mecha/contextEngineering.ts`: gate plan/todo injection on
  `LobeAgentIdentifier` instead of `GTDIdentifier`.
- `agentConfigResolver.test.ts`: switch fixture plugin IDs to
  `LobeAgentIdentifier`.
- `packages/const/src/recommendedSkill.ts`: drop the standalone `lobe-gtd`
  recommendation — `lobe-agent` already covers it via `defaultToolIds`.

i18n migration (default + zh-CN + en-US; other locales regenerate on
`pnpm i18n`):
- `builtins.lobe-gtd.*` → `builtins.lobe-agent.*` in `plugin.ts/json`.
- `lobe-gtd.*` (tool namespace) → `lobe-agent.*` in `tool.ts/json`.
- Remove `tools.builtins.lobe-gtd.{description,readme,title}` from
  `setting.ts/json` (lobe-agent has its own meta now).
- Update all client component `t(...)` keys to the new namespace.

Mocks / fixtures / tests:
- `packages/agent-mock/src/cases/builtins/todo-write-stress.ts`: all
  `identifier: 'lobe-gtd'` → `'lobe-agent'`; helper comments updated.
- `packages/types/src/stepContext.ts`: comment refers to
  `builtin-tool-lobe-agent` (the only consumer of `StepContextTodoItem`).
- `packages/model-runtime/src/core/streams/google/google-ai.test.ts`:
  function-call names from `lobe-gtd____createPlan` etc. → `lobe-agent____*`.
- `src/store/chat/slices/message/selectors/dbMessage.test.ts`: same.
- `src/features/DevPanel/RenderGallery/fixtures/lobe-gtd.ts` deleted; its
  plan/todo fixtures are folded into `fixtures/lobe-agent.ts` alongside the
  existing `callSubAgent[s]` ones.
- Replace `console.log` → `console.info` in moved client components to
  satisfy lobe-agent's stricter ESLint rules (GTD package allowed
  `console.log`; lobe-agent inherits the repo-wide `no-console` rule).

No behavior change for end users: `lobe-agent` now owns all the APIs,
identifiers, and UI that previously lived in `lobe-gtd`, but as a single
consolidated package under a single tool identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(context-engine): drop residual GTD naming, rename to PlanInjector / TodoInjector

Follow-up to 9ca5c9d (which absorbed the GTD tool package into lobe-agent).
That commit moved the package surface but left the GTD vocabulary embedded
in context-engine providers, types, metadata fields, XML tags, and a pile
of comments. This change finishes the sweep so the only remaining GTD
references are user-facing docs and the legitimate Productivity & GTD Coach
methodology suggestion.

context-engine
- `GTDPlanInjector` → `PlanInjector`; types `GTDPlan`/`GTDPlanInjectorConfig`
  → `Plan`/`PlanInjectorConfig`; metadata `gtdPlanId`/`gtdPlanInjected` →
  `planId`/`planInjected`; XML tag `<gtd_plan>` → `<plan>`; debug channel
  `provider:GTDPlanInjector` → `provider:PlanInjector`.
- `GTDTodoInjector` → `TodoInjector`; types `GTDTodoItem`/`GTDTodoList`/
  `GTDTodoStatus`/`GTDTodoInjectorConfig` → `TodoItem`/`TodoList`/
  `TodoStatus`/`TodoInjectorConfig`; metadata `gtdTodo*` → `todo*`;
  XML tag `<gtd_todos>` → `<todos>`, wrapper `gtd_todo_context` →
  `todo_context`; debug channel renamed similarly.
- `MessagesEngineParams.gtd?: GTDConfig` → `planTodo?: PlanTodoConfig`;
  internal vars `isGTDPlanEnabled`/`isGTDTodoEnabled` →
  `isPlanEnabled`/`isTodoEnabled`. Re-exports updated in `providers/index.ts`
  and `engine/messages/{index,types}.ts`.

prompts
- `packages/prompts/src/prompts/gtd/` → `planTodo/` (only export was
  `formatTodoStateSummary`, which kept its name). Updated `prompts/index.ts`
  re-export.

src/services
- `contextEngineering.ts`: `GTDConfig` import → `PlanTodoConfig`;
  `isGTDEnabled`/`gtdConfig` → `isPlanTodoEnabled`/`planTodoConfig`; payload
  field `gtd` → `planTodo`; log message wording.

Tests
- `dbMessage.test.ts`: helper `createGTDToolMessage` →
  `createLobeAgentToolMessage`; `gtdMessage` → `lobeAgentMessage`; all `it`
  descriptions reworded to "lobe-agent" instead of "GTD".
- `agentConfigResolver.test.ts`: test descriptions reworded.

Comments / docs (no behavior change)
- agent-runtime (`instruction.ts`, `runtime.ts`, `generalAgent.ts`,
  `messageSelectors.ts`), `types/{stepContext,tool/builtin}.ts`,
  `builtin-agents/group-supervisor`, `builtin-tool-claude-code/types.ts`,
  `builtin-tool-lobe-agent/Render/TodoList`, `createAgentExecutors.ts:1426`,
  `AssistantGroup/{constants,Fallback.test}`, `agent-mock/todo-write-stress`,
  `.agents/skills/builtin-tool/references/architecture.md`.

Intentionally left alone
- `docs/usage/agent/gtd.{mdx,zh-CN.mdx}` and other docs — user-facing
  product brand "GTD Tools".
- `src/locales/default/suggestQuestions.ts` "Productivity & GTD Coach" —
  references the methodology, not the tool.
- `ToolSystemRoleProvider.test.ts` `'gtd-tool'` fixture — generic test
  identifier, unrelated.
- Translated locale files still carrying `lobe-gtd.*` keys — regenerated by
  `pnpm i18n` from the updated default namespace.

Verified: `bun run type-check` passes; touched test files
(dbMessage, agentConfigResolver) and full context-engine + prompts test
suites pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(builtin-tool-lobe-agent): reset TodoList auto-save status to idle

`performSave` (the debounced auto-save path) was leaving `saveStatus` stuck
on 'saved' forever — `saveNow` had the 1.5s setTimeout-to-idle but the
auto-save twin didn't, so the inline indicator never eased back to idle
after a settle. Add the same idle-reset to performSave so both paths
behave the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:14 +08:00
Arvin Xu 266d10206b 💄 style: use @lobehub/ui built-in HtmlPreview instead of custom component (#14703)
* 💄 style(home,i18n): use 已阅 for brief confirm/confirmDone in zh-CN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use 确认完成 for brief.action.confirmDone in zh-CN

confirmDone signals the terminal transition (task marked complete),
not just dismissing the brief, so 已阅 loses the semantic distinction
from `confirm`. Use 确认完成 to match the EN intent ("Confirm complete").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor: use @lobehub/ui built-in HtmlPreview instead of custom component

- Upgrade @lobehub/ui from ^5.10.1 to ^5.10.4
- Replace custom HtmlPreviewAction with lobe-ui's enableHtmlPreview
- Wire lobe-ui's onExpand callback to existing HtmlPreviewDrawer
- Remove HtmlPreviewAction.tsx (no longer needed)
- Keep HtmlPreviewDrawer for the expanded full-screen view

* 🐛 fix(task): sync useMarkdown destructuring with assistant MessageContent

* 🐛 fix(task): correct mangled search.X JSX expressions in MessageContent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(review): move revert icon to right edge of file 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>
2026-05-13 02:57:13 +08:00
LobeHub Bot 71a49b033f 🌐 chore: translate non-English comments to English in src (#14654)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:13 +08:00
Arvin Xu fc275ca4dc 🐛 fix(home): blank user bubble when sending the placeholder hint (#14678)
When the home input was empty and the user clicked send, `useSend`
correctly fell back to the daily-brief hint for `message`, but it also
forwarded `mainInputEditor.getJSONState()` as `editorData`. An empty
editor still returns a non-null JSON state (e.g. `{ type: 'doc' }`),
which makes `UserMessageContent.hasEditorData` truthy — so the renderer
took the RichTextMessage branch and drew nothing, while the agent
happily processed the hint text behind a blank user bubble.

Skip `editorData` when the hint is being used so the renderer falls
back to the markdown `content`. Adds a regression test.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:13 +08:00
Arvin Xu cb8b616546 feat(database): add agent_operations table (#14416)
 feat(database): add agent_operations table

Adds an `agent_operations` table to persist agent runtime operations
beyond the 2-hour Redis TTL. Each row captures one agent operation
(operationId) with denormalized cost/token aggregates, lifecycle
timestamps, runtime config snapshot, and a `trace_s3_key` pointer to
the full ExecutionSnapshot in S3.

- `user_id` is intentionally not a FK so operation history survives
  user deletion (auditable historical data).
- `agent_id` / `topic_id` / `thread_id` / `task_id` / `chat_group_id`
  use ON DELETE SET NULL to preserve operations when their parent
  entity is removed.
- `parent_operation_id` self-references for sub-agent (callAgent) ops.
- `human_interventions` and `human_waiting_time_ms` are nullable since
  most operations have no human interaction at all.
- Indexes optimize per-user listing and per-status / per-entity lookups;
  `metadata` has a GIN index for jsonb filters.
2026-05-13 02:57:13 +08:00
Innei 217afcf1af 🐛 fix(conversation): prevent synthetic scroll from shrinking spacer (#14584)
🐛 fix: prevent synthetic scroll from shrinking spacer
2026-05-13 02:57:13 +08:00
Arvin Xu 2f33932198 ♻️ refactor(agent-runtime): extract CompletionLifecycle, HumanInterventionHandler, stepPresentation (#14441)
* ♻️ refactor(agent-runtime): extract CompletionLifecycle

Pull terminal-state handling out of AgentRuntimeService into a dedicated
class:

- buildLifecycleEvent (was buildCompletionLifecycleEvent)
- emitSignalEvents (was emitCompletionSignalEvents)
- dispatchHooks (was dispatchCompletionHooks)
- extractErrorMessage

These four methods formed one cohesive vertical: build the lifecycle
event payload, emit completion AgentSignal source events, dispatch
onComplete/onError hooks, and write error back onto the assistant
message row. extractErrorMessage was a private helper used by all three
plus by the trace-snapshot finalize call site, so it becomes a public
method on the class.

Call sites in executeStep / executeSync change from
`this.{emit|dispatch|extract...}` to `this.completionLifecycle.{...}`.

Tests: extractErrorMessage.test.ts → CompletionLifecycle.test.ts,
instantiating CompletionLifecycle directly instead of going through
AgentRuntimeService — drops a pile of unrelated mocks.

AgentRuntimeService.ts: 2084 → 1918 (-166).

All 81 agentRuntime tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): extract HumanInterventionHandler

Pull the 165-line `handleHumanIntervention` method out of
AgentRuntimeService into its own class, splitting the three branches
(approve / rejectAndContinue / rejectAndHalt) into private methods so
each fits in one screen. Routing in `process()` now reads top-to-bottom:
detect approval, then rejection, then unsupported humanInput.

The handler depends only on `serverDB` (for the messagePlugins lookup)
and `messageModel` (for tool/plugin updates) — much narrower than
AgentRuntimeService's full surface, so the extracted unit is easier to
unit-test in isolation.

Drop the unused `runtime: AgentRuntime` parameter from the public API:
the original method threaded it through but never called it.

Tests: handleHumanIntervention.test.ts → HumanInterventionHandler.test.ts
— same 17 cases, but instantiate the handler directly instead of
constructing a full AgentRuntimeService with 11 module mocks. Tighter
arrange step, same coverage.

AgentRuntimeService.ts: 1918 → 1742 (-176).

All 81 agentRuntime tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): extract step presentation builder

Pull the ~150-line `phase`-branching block out of executeStep into a
pure `buildStepPresentation` function. The block did three things in
sequence: derive content/reasoning/toolsCalling/toolsResult from the
runtime step result, build a one-line stepSummary for logging, and
assemble the StepPresentationData DTO consumed by afterStep hooks /
snapshot recorder / callbacks.

The function takes only the stepResult and an executionTimeMs; no
service state needed. Comes with a `formatTokenCount` helper for the
log line (12345 → 12.3k, 2_500_000 → 2.5m).

executeStep keeps the log call inline (one line, references presentation
fields directly) and reads `content` / `toolsCalling` off presentation
for downstream tracking + truncation logic.

13 new unit tests: phase=tool_result (json + string + isSuccess paths),
phase=tools_batch_result, done event, llm_result with content/reasoning/
tools, empty fallback, cumulative usage zero-fallback, stepUsage
forwarding, and formatTokenCount edges.

AgentRuntimeService.ts: 1742 → 1601 (-141).

All 94 agentRuntime tests pass (was 81, +13 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:13 +08:00
Arvin Xu df0e635c45 🐛 fix(task-card): localize task card date independent of dayjs global locale (#14730)
* 🐛 fix(task-card): localize date format independent of dayjs global locale

Task card was rendering "5月 12" under English UI because t('time.formatThisYear')
returned the English "MMM D" format, but dayjs's global locale was still zh-cn,
making MMM resolve to the Chinese short month name. Thread the i18n language
into formatTaskItemDate so the date is rendered with the same locale as the
format string, decoupling it from dayjs's global state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task-card): import missing GenericItemType + type Run now onClick

Pre-existing CI regression from #14727 surfacing on every PR: the Run now
context menu satisfies-clause references GenericItemType without importing
it, and the onClick lacks a MenuInfo annotation, so tsgo widens the divider
literal's `type` to `string` and rejects the whole context menu array.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:13 +08:00
Arvin Xu 2202189ac1 🐛 fix(web-crawler): cap response body size to prevent serverless OOM (#14660)
* 🐛 fix(web-crawler): cap response body size to prevent serverless OOM

Production saw repeated SIGABRT crashes on `/trpc/tools/search.webSearch`
where Node aborted with V8 "allocation failed" — the naive crawler buffered
entire response bodies into heap before the 1 MB downstream truncation could
apply, so a single large page (or a batch of three under default
concurrency=3) could push rss past the lambda memory ceiling.

- ssrfSafeFetch: add opt-in `maxContentLength` that streams the response
  body via `for await` and stops at the cap (soft truncation — still a
  successful response). Breaking the iterator destroys the underlying
  stream and releases the connection. Default behaviour (full
  `arrayBuffer()` read) unchanged when the option is absent.
- naive crawler: pass `maxContentLength: MAX_HTML_SIZE` so any body beyond
  1 MB is dropped at the network layer instead of being materialised in heap.
- htmlToMarkdown: explicitly call `window.happyDOM.close()` in a finally
  block so the parsed DOM tree is released as soon as parsing finishes,
  rather than waiting for the function scope to drop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(ssrf-safe-fetch): add OOM regression tests for response body cap

Verify that the maxContentLength cap actually prevents the production SIGABRT
scenario, not just produces a truncated body.

- Source-pull bound: a body source with 200 MB available, capped at 1 MB,
  must not be drained beyond ~1 MB. Asserts on bytes pulled from the
  generator, which is the property that prevents OOM.
- Concurrency bound: matches production CRAWL_CONCURRENCY=3 — three
  concurrent oversized fetches should pull at most ~3 MB total, not 300 MB.
- Heap-delta bound (gated on --expose-gc): under real GC pressure,
  fetching a 50 MB body with a 1 MB cap should grow heapUsed by < 10 MB.
  Run with `NODE_OPTIONS=--expose-gc bunx vitest run` to exercise; skipped
  by default so CI doesn't false-fail on GC timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:13 +08:00
Innei 4e4294f57e 🐛 fix(desktop): focus onboarding auth success state (#14694) 2026-05-13 02:57:13 +08:00
Arvin Xu 79152fa222 feat(markdown): user_feedback card + task card polish + Run now context menu (#14727)
*  feat(markdown): render <user_feedback> task prompt blocks as a card

`buildTaskRunPrompt` wraps the user's pre-run comments in a
`<user_feedback>` block alongside `<task>`. The Task plugin captured
`<task>` into a card, but `<user_feedback>` had no plugin and leaked
into the chat as raw XML. Because CommonMark only treats tag names
matching `[a-zA-Z][a-zA-Z0-9-]*` as html, the underscore in
`user_feedback` puts the opening/closing tags inside a `paragraph` as
plain text — so the new remark plugin walks paragraph children rather
than html nodes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-card): drop standalone status row + Agent/Parent/Topics, inline semantic status badge

The status/Priority row, Agent, Parent and Topics fields aren't useful
when the task card is rendered inside the topic chat drawer (the drawer
already exposes that context). Move the task status to a compact badge
beside the identifier and reuse `taskDetail.status.*` for the label so
"scheduled" reads as "Scheduled" / "已排期".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): compact one-line header + left-border quote-style card

Slims the card down to a single 12px header line ("User feedback · N
comments") with a small 12px icon, and wraps the whole block in a
subtle fill + 2px left-border accent so it reads as a quoted aside and
visually separates from the task card that follows in the same user
message body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): drop fill + radius, render as plain left-rail blockquote

The filled card competed visually with the unstyled task block that
sits beside it in the same message body. Reducing to a 2px left-rail
quote without background or border-radius lets both blocks read as
parts of the same user message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): collapsible card with task-style head + bottom divider

Default-collapsed `<details>` whose summary mirrors the task title row
(32px icon + bold label + small count badge), with a bottom split-line
that doubles as a divider between the user feedback head and the task
card that follows in the same message body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): strip default markdown details card chrome

@lobehub/ui Markdown applies bg + padding (0.75em 1em) + box-shadow +
border-radius to every nested <details>, which made the user_feedback
head read as a wide standalone card sitting awkwardly on top of the
inline task title. Override the chrome (with !important — the lib
selector wins on specificity otherwise) so the head sits flat in the
message body, with only the bottom split line separating it from the
task that follows. The lib's right-side disclosure chevron is kept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): match task card's 12px symmetric divider spacing

Add a 12px margin-bottom so the gap below the user_feedback bottom rule
mirrors the 12px above it, matching the symmetric 12px the task card
already uses around its own internal divider. Without this, the
user_feedback rule sat flush against the T-31 row while the next rule
below T-31 had a 12px gap on both sides — visually uneven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-card): drop status badge from task title row

The task drawer header and the schedule strip on the task detail page
already convey status; surfacing it again on the task card inside the
chat body just added noise. Drop the badge along with the now-unused
KNOWN_STATUSES / isKnownStatus / TaskStatusIcon / useTranslation
plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(tasks): add "Run now" item to task card context menu

Available only for backlog and completed tasks; mirrors the inbox-agent
fallback used by the detail-page Run Now action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(topic-list): preserve `#` icon placeholder for heterogeneous agents

Returning null for the icon slot collapsed the row layout, so titles on
heterogeneous-agent topics (Claude Code, Codex, …) no longer aligned
with sibling rows. Render the same HashIcon with visibility:hidden so
the box is preserved without showing the glyph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:13 +08:00
brone1323 ece409195a 🌐 i18n: add missing task-schedule and review strings to 16 locales (#14728)
🌐 i18n: add missing translations for task-schedule and review keys across 16 locales

Adds 14 missing i18n keys to all non-zh-CN locales (ar, bg-BG, de-DE,
es-ES, fa-IR, fr-FR, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-BR, ru-RU,
tr-TR, vi-VN, zh-TW):

chat.json (11 keys):
- taskSchedule.summary.everyNHoursHalfPast
- taskSchedule.summary.hourlyHalfPast
- taskSchedule.timezoneSearchEmpty
- taskSchedule.timezoneSearchPlaceholder
- workingPanel.review.revert (and 7 sub-keys)

plugin.json (1 key):
- builtins.lobe-task.apiName.setTaskSchedule

setting.json (2 keys):
- serviceModel.modelAssignments.title
- serviceModel.optionalFeatures.title

These were added in recent commits but the automated i18n sync had not
yet propagated them to non-Chinese locales.
2026-05-13 02:57:13 +08:00
Innei e56edab711 💄 style: polish desktop header icons, sidebar density, and task menus (#14724)
* 💄 style: shrink desktop header icons and tighten sidebar/home density

Switches all desktop header action icons from DESKTOP_HEADER_ICON_SIZE to
DESKTOP_HEADER_ICON_SMALL_SIZE, and tightens vertical gaps in the home
sidebar, recents list, and nav header layout for a denser, calmer look.

* ♻️ refactor(agent-tasks): migrate task menus and scheduler select to @lobehub/ui base-ui

- TaskPriorityTag / TaskStatusTag: replace antd Dropdown with base-ui
  DropdownMenu and adopt the ContextMenuItem / MenuInfo typings.
- useTaskItemContextMenu: drop the DOM data-attribute submenu marker in
  favour of an internal activeSubmenuRef tracked via onOpenChange.
- TaskScheduleConfig / SchedulerForm: swap @lobehub/ui Select for the
  base-ui Select and replace the custom SearchBar dropdownRender with
  antd Select showSearch for timezone filtering.

* ♻️ refactor(review): migrate review dropdowns to @lobehub/ui base-ui DropdownMenu

Swap the antd Dropdown trios (mode picker, base-ref picker, more menu) in
the agent working-sidebar Review pane for the base-ui driven DropdownMenu,
matching the recent task menus / scheduler migration. Also tighten the
sidebar header paddingInline from 16 to 4 to align with the surrounding
density polish.

* 🐛 fix(tasks): replace unsupported onOpenChange with onTitleMouseEnter in context menu
2026-05-13 02:57:13 +08:00
René Wang 3a4bd4a83d fix: Docs image (#14726)
fix: image
2026-05-13 02:57:12 +08:00
René Wang 19912fe02d 📝 docs: add May 11 weekly changelog (#14651) 2026-05-13 02:57:12 +08:00
Arvin Xu a40fe91fa4 🐛 fix(desktop): detect Windows npm .cmd shims for CLI agents (claude/codex/…) (#14720) 2026-05-13 02:57:12 +08:00
LobeHub Bot ae2afe860a 🌐 chore: translate non-English comments to English in cli-migrate (#14708)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:12 +08:00
Arvin Xu d3f8f760b2 ⬆️ chore: bump @lobehub/ui to 5.10.5 2026-05-13 02:57:12 +08:00
Arvin Xu 846e648fea 💄 style(review-panel): hover revert button to discard per-file working-tree changes (#14716)
 feat(review-panel): hover revert button to discard per-file working-tree changes

Add a hover-revealed Undo icon to each file row in the Review panel's
unstaged view. Clicking opens a Popconfirm; confirming runs a new
`git.revertGitFile` IPC that restores the file from HEAD (or unstages +
deletes when the path doesn't exist at HEAD, covering staged-add and
untracked entries).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:12 +08:00
Innei 0007984637 feat(documents): add optimistic create/delete and inline rename for document tree (#14714)
- Insert pending rows immediately on create folder/document, with
  optimistic SWR mutation that rolls back on server error
- Auto-focus rename input on newly created items via onPendingInserted
  callback
- Defer rename commits for pending rows until the server create resolves,
  then rename against the real row id
- Optimistic recursive delete closes the confirm modal instantly, removes
  target + descendants from the tree, and rolls back on failure
- Fix folder path canonicalization in ExplorerTree rename lookup
  (toCanonicalTreePath ensures trailing slash for folders)
- Export getItemPathFromEventPath for composed-path–based item resolution
- Add unit tests for toCanonicalTreePath and ExplorerTree event helpers
2026-05-13 02:57:12 +08:00
Arvin Xu eea742fd5f fix: update Task page placeholder copy (#14704)
* fix: update Task page placeholder copy

* fix: update Task page placeholder copy (en-US)
2026-05-13 02:57:12 +08:00
Innei ca9a781bdd 💄 style: standardize header action icon sizes (#14717)
💄 style: standardize header action icons to DESKTOP_HEADER_ICON_SMALL_SIZE

Unify icon sizing across sidebar and header action buttons by replacing
hardcoded sizes and DESKTOP_HEADER_ICON_SIZE with
DESKTOP_HEADER_ICON_SMALL_SIZE for consistent visual density.

Affected components:
- SideBarHeaderLayout back button
- ToggleLeftPanelButton default size
- BackButton default size
- Agent sidebar header chevron
- InboxButton notification icon
2026-05-13 02:57:12 +08:00
Innei 18b1c25371 feat(devtools): add dev-only feature flag override panel (#14565)
Add a client-side feature flag override panel that lives behind a
floating button in dev builds. Overrides are persisted to localStorage
and merged into useServerConfigStore.featureFlags so existing flag
consumers see the toggled value without any callsite changes.

The panel is gated by NODE_ENV plus a localStorage opt-in
(LOBE_DEV_FEATURE_FLAG_PANEL_ENABLED = "1"); prod builds tree-shake
the entire feature.
2026-05-13 02:57:12 +08:00
Arvin Xu 5ff4590fc1 🐛 fix(builtin-tool-task): expose lobe-task and add setTaskSchedule (#14713)
*  feat(builtin-tool-task): expose lobe-task to users and add schedule config

The task tool is now generally available — flip it from a scenario-only
internal tool to a user-toggleable recommended skill, and let the LLM
configure recurring execution (cron or heartbeat) via createTask / editTask.

- Drop `discoverable: false` + `hidden: true` from TaskManifest registration
- Add `lobe-task` to RECOMMENDED_SKILLS so it stays installed by default
- Remove the USER_HIDDEN_BUILTIN_TOOL_IDS allowlist (only contained lobe-task);
  update selectors and AgentTool to stop filtering it out
- Extend createTask / createTasks / editTask with `automationMode`,
  `schedulePattern`, `scheduleTimezone`, `heartbeatInterval`; editTask also
  accepts `maxExecutions`
- Route schedule columns through taskService.update and maxExecutions through
  taskService.updateConfig (server merges into tasks.config.schedule);
  refresh detail once at the end of editTask

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool-task): split schedule config into dedicated setTaskSchedule tool

editTask was the wrong place for schedule fields — schedule needs its own
verb so the LLM (and any future human-in-the-loop review) can audit cron /
heartbeat changes separately from generic field edits, and createTask should
stay a pure "make a task" verb without automation knobs.

- Drop automationMode / schedulePattern / scheduleTimezone / heartbeatInterval
  from createTask + createTasks, and drop them plus maxExecutions from editTask
- Add new `setTaskSchedule(identifier, automationMode?, schedulePattern?,
  scheduleTimezone?, heartbeatInterval?, maxExecutions?)` API with its own
  manifest entry, executor method, types, i18n key, and inspector
- Schedule columns still route through taskService.update; maxExecutions still
  routes through taskService.updateConfig (server merges into
  tasks.config.schedule) — same wiring, just moved into the dedicated tool
- Update systemRole to advertise setTaskSchedule + keep editTask description
  clean of schedule mentions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:12 +08:00
AmAzing- eb924ec881 feat: add service model assignments settings (#14712)
*  Add default agent model setting

* 💄 Refine service model assignments UI

* 💄 Clarify optional service model features
2026-05-13 02:57:12 +08:00
Innei 51cefe0154 🐛 fix(desktop): reset pendingLoginMethod on auth failure/cancel paths (#14695)
* 🐛 fix(desktop): focus onboarding auth success state

* 🐛 fix(desktop): reset pendingLoginMethod on auth failure/cancel paths

Clear pendingLoginMethod in authorizationFailed, authorizationProgress
cancelled, and remoteServerSyncError handlers to prevent users getting
stuck without a Get Started path when a re-auth attempt fails but a
prior authorization is still valid.

* Delete src/routes/(desktop)/desktop-onboarding/features/LoginStep.test.tsx

---------

Co-authored-by: Innei <inbox@innei.in>
2026-05-13 02:57:12 +08:00
Innei cd3716d5e7 ♻️ refactor(spa): use __DEV__ define instead of process.env.NODE_ENV (#14696)
* ♻️ refactor(spa): use __DEV__ define instead of process.env.NODE_ENV

The Vite `__DEV__` define and its global type declaration are already
in place (plugins/vite/sharedRendererConfig.ts, src/types/global.d.ts).
Replace `process.env.NODE_ENV` checks across SPA-only files with the
`__DEV__` boolean so the bundler can statically eliminate dev-only
branches in production builds.

Server-side files (app/, server/, libs/next, libs/trpc, libs/better-auth,
envs, instrumentation) and modules that are also imported by Next.js
SSR pages (e.g. components/Loading/BrandTextLoading) are intentionally
left untouched to avoid runtime `__DEV__ is not defined` errors.

* fix(vitest): define __DEV__ and related constants for test environment

Vitest runs outside the Vite SPA build pipeline, so the __DEV__ define
injected by sharedRendererDefine was not available during tests. This
caused ReferenceError: __DEV__ is not defined in any test file that
transitively imports code using the __DEV__ constant.

Add a  block to vitest.config.mts that mirrors the SPA defines:
- __DEV__: true (test is not production)
- __CI__: mirrors process.env.CI
- __ELECTRON__/__MOBILE__: false (not testing platform-specific code)

* fix: replace missed isDevEnv reference with __DEV__ in AgentMockDevtools
2026-05-13 02:57:12 +08:00
Neko def9acee66 ♻️ refactor(agent-signal,prompts,database,builtin-tool-self-iteration): unified structure of service, unified tool, unified name and concepts (#14699) 2026-05-13 02:57:12 +08:00
Arvin Xu 948e48beba 🐛 fix(utils): cap image binary at 3.75MB so base64 payload stays under Anthropic 5MB limit (#14711)
* 🐛 fix(utils): cap image binary at 3.75MB so base64 payload stays under Anthropic's 5MB limit

Anthropic enforces the 5MB image cap on the base64-encoded payload, not the
binary file. Base64 inflates by ~4/3, so a 4.7MB binary file becomes 6.27MB
once encoded and trips `messages.*.content.*.image.source.base64: image
exceeds 5 MB maximum`. The previous MAX_IMAGE_BYTES of 5MB matched against
file.size, letting these images through compression untouched.

Lower the threshold to floor(5MB * 3/4) ≈ 3.75MB in both the frontend
canvas compressor and the server-side Sharp fallback so the progressive
shrink loop keeps going until the base64 payload is safely under the cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(utils): tighten image binary cap to 3MB for extra base64 headroom

Drop MAX_IMAGE_BYTES from 3.75MB (exact 5MB-base64 boundary) to a flat 3MB
so the encoded payload lands around 4MB — clear of any per-provider rounding
or jitter at the 5MB hard limit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:12 +08:00
Arvin Xu 1ae774d55e 🐛 fix(tasks): scheduler, hotkey, comment & TodoList polish (#14707)
* 🐛 fix(portal): allow TodoList to scroll when expanded content exceeds max-height

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(tasks): route 1–N hotkey to the open submenu instead of defaulting to status

The base-ui SubmenuTrigger doesn't propagate antd's `onTitleMouseEnter`, so
the hover ref in the right-click context menu never updated and every number
press fell back to the status submenu. The standalone Priority/Status tag
dropdowns also showed 1–N hints without binding any handler at all.

- Detect the currently open submenu via `data-popup-open` + a per-submenu
  `data-task-submenu` marker on the icon; numbers are ignored when no
  submenu is open.
- Install a keydown listener on TaskPriorityTag / TaskStatusTag while their
  dropdown is open so the hint numbers actually fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(scheduler): keep Continuous unchanged while editing Max runs

Clearing the Max runs input previously emitted maxExecutions=null, which the
form re-interpreted as Continuous and auto-checked the checkbox mid-edit
(disabling the input before the user could type the replacement number).

Track Continuous as its own state derived from the persisted prop. On clear
we hold the input empty locally without touching Continuous or emitting,
and unrelated emits fall back to the persisted value so they can't flip the
checkbox either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): always show comment Send button and unify action labels

- Make the Send button visible by default in CommentInput / FeedbackInput
  (greyed out when empty) so the field reads as an input instead of vanishing
  affordance.
- Align topic action menu labels to Title Case (Stop Run / Open Run /
  Copy Topic ID / Copy Operation ID / Copy Link) to match the rest of the
  Action microcopy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  perf(scheduler): seed SchedulerForm from props once and own state locally

The previous prop→state useEffects re-synced every time the parent prop
updated, which during the async updateSchedule → refreshTaskDetail roundtrip
clobbered the user's in-flight edits with stale store values — felt awful
on rapid changes.

Drop the three sync useEffects and seed local state from props only at
mount via a lazy useState initializer. The form now owns its values
optimistically; cross-task safety comes from `key={taskId}` on the
parent so the form remounts cleanly when switching tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): Notion-style timezone picker — drop underscores, offset on the right

Underscored labels like 'America/New_York (EST/EDT, UTC-5/-4)' read poorly in
the dropdown. Split each option into `label` (underscore → space) and `offset`,
and render the row with the city on the left and a subtle gray offset on the
right, in line with how Notion's timezone picker presents this.

IANA `value` keeps the underscore so cron and Drizzle stay happy. Search now
filters by the human label only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): keep zone abbreviations in the timezone offset column

Show 'EST/EDT · UTC−5/−4' instead of just 'UTC−5/−4' so users can recognize
the zone by its common abbreviation alongside the offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): drop awkward ':30' suffix from hourly summary

'Every hour:00' / 'Every 2 hours:30' read like glitched concatenations. Cron
storage always rounds to 0 or 30 minutes, so call out the non-zero case as
'at half past' and stay implicit on the top of the hour.

- Every hour
- Every hour at half past
- Every 2 hours
- Every 2 hours at half past

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): collapse advanced settings by default

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  perf(tasks): coalesce post-write refresh and add timezone search

Two follow-up fixes for the AgentTasks scheduler popover.

##### Optimistic schedule writes, single coalesced refresh

Rapid edits in the scheduler form (toggling daily/hourly/weekly, weekday
chips, time, etc.) each triggered `taskService.update` + a full
`internal_refreshTaskDetail` per call. With overlapping requests the
refreshes returned intermediate server state and bounced TaskTriggerTag /
summary text away from the user's latest choice.

- Add `#withCoalescedRefresh` on the task config slice: it tracks a per-task
  pending-writes count and only fires `internal_refreshTaskDetail` after the
  LAST in-flight write settles.
- Give `updateSchedule` an optimistic `internal_dispatchTaskDetail` so
  external readers see the new pattern/timezone/maxExecutions immediately.
- Route both `updateSchedule` and `setAutomationMode` through the coalescer.

##### Timezone picker — search input at the top

The dropdown had antd's implicit type-into-trigger search, which most users
miss. Add a `SearchBar` inside `dropdownRender`, filter the options against
label/value/offset locally, and show an empty state when nothing matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): weekday chips only show background when selected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(tasks): dispatch optimistic schedule under nested 'schedule' field

`TaskDetailData` exposes schedule as `schedule.{pattern,timezone,maxExecutions}`,
not flat columns. The previous optimistic dispatch used the DB-style flat keys,
which broke type-check and would never reach the in-memory selectors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): drop Cmd+Backspace shortcut on the Delete menu item

Header dropdown only advertised the hotkey (no handler), and the right-click
context-menu handler is gone too — keeps the visual claim honest and
removes the irreversible-by-keystroke footgun.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(agent-signal): pin `now` in proposal activity tests to fixture window

Two cases relied on the real system clock; once today crossed the
fixture's default `expiresAt` (2026-05-12), pending proposals were
classified as expired and the assertions broke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): hide '#' placeholder icon for heterogeneous agent topics

Claude Code / Codex topics aren't chat topics in the usual sense, so the
fallback HashIcon in the sidebar row reads as noise. Skip it when the
current agent has a heterogeneousProvider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🧪 test(tasks): provide agentMap in TopicItem store mock

`isCurrentAgentHeterogeneous` walks through `currentAgentConfig` which
indexes `s.agentMap[agentId]`. Extend the mocked store state to include
an empty `agentMap` so the selector resolves to `undefined` (= not
heterogeneous) instead of throwing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:12 +08:00
Arvin Xu 94e4ea6712 🐛 fix(cli): remove stale cron entry from generated man page (#14709)
* 🐛 fix(cli): remove stale cron entry from generated man page

The cron command was removed from program.ts but the generated man page
still listed it. Regenerated via bun run man:generate.

* 🔖 chore(cli): release 0.0.15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:11 +08:00
Arvin Xu bfa28506af 💄 style(tool): add word wrap toggle to tool arguments display (#14706)
 feat(tool): add word wrap toggle to tool arguments display

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:11 +08:00
Rdmclin2 fdedc9697d 🐛 fix: sidebar add agent (#14693)
* fix: sidebar add agent and group error

* feat: add billboard cta
2026-05-13 02:57:11 +08:00
Innei 877052fc1f 💄 style(nav): unify ActionIcon sizing and improve TodoList encapsulation (#14692)
- Extract SIDEBAR_HEADER_ACTION_ICON_SIZE constant for consistent sidebar header ActionIcon sizing
- Pass size prop to ToggleLeftPanelButton
- Simplify Agent selector ActionIcon to use 'small' size preset
- Move layout wrapper styles from Body into TodoList root for better component encapsulation
- Increase Nav gap from 1 to 4 for proper spacing
2026-05-13 02:57:11 +08:00
YuTengjing 4490e3ef76 feat: inline skill auth in recommended task templates (#14676)
*  feat: support refreshing recommended task templates

- Add optional `refreshSeed` through `listDailyRecommend` API, service, and
  client; SWR key includes it so a refresh actually refetches.
- Frontend stores the seed in sessionStorage (via `useSessionStorageState`)
  so a new tab or next day returns to the default daily picks.
- Home Daily Brief shows a "Refresh" affordance on the Recommendations
  subtitle row.
- Fix first-card pinning when matched candidates < RECOMMEND_COUNT: fold
  the fallback pool in so seed reorders the whole batch instead of locking
  position 0 to a single-match template.

Linear: LOBE-8689

*  feat: resolve task-template icon priority

Render the task-template card icon as self > skill provider > interest > Sparkles. Skill icons read required[0] then optional[0], skipping unresolvable providers. URL icons render via @lobehub/ui Image, component icons keep the 28x28 tile.

*  feat: inline skill auth in task template card

Single click "Add task" is now the entire flow: the button stays put, and if a required skill is missing we chain its OAuth popups and create the task automatically. Unauthorized providers (required + optional) appear as compact inline rows above the footer; the provider that already drives the card's main icon is suppressed to avoid duplicating the same logo.

*  feat: add task template detail modal

Open a detail modal when the recommended task template card is clicked,
exposing the full instruction (markdown) plus inline skill auth and the
add-task action. Rename i18n `${id}.prompt` -> `${id}.instruction` to
align with the task table column, and write both `description` and
`instruction` when creating the task. Extract shared `TemplateBriefIcon`,
`useScheduleText`, `useTaskTemplateCreate` and `useVisibleAuthSpecs` so
the card and the modal share the same creation flow and OAuth chaining.

* 🐛 fix: missing Block import in TaskTemplateCard

*  feat: render recommended templates on empty Tasks page

Replace the bare "no tasks" placeholder with a hero landing: greeting,
enlarged inline composer (hero variant), and a 2-column grid of up to
10 recommended task templates. Plumbs a new `count` option through the
service, both routers, the client service, and the recommendations hook
so the home page keeps its 3-card layout while the empty Tasks page
asks for 10.

* 🐛 fix: type cast in resolveTemplateIcon test for unknown interest

* 🌐 i18n: update translations for task template empty-state and other namespaces
2026-05-13 02:57:11 +08:00
Innei 7349ad0f53 🐛 fix: replace ScrollShadow with ScrollArea to fix React #185 infinite render loop (#14689)
Migrate all ScrollShadow usages to ScrollArea (scrollFade) to eliminate
the effect → setState → render → effect cycle that caused React error
#185 (Maximum update depth exceeded) in the scroll overflow hook.

Affected components:
- StreamingMarkdown
- AgentCouncil AutoScrollShadow
- AssistantGroup ContentBlocksScroll
- Conversation Thinking

Fixes lobehub/lobehub#14650
2026-05-13 02:57:11 +08:00
LiJian 744059c1bc 🐛 fix(heteroFinish): trigger task lifecycle on cloud sandbox agent completion (#14681)
* 🐛 fix(heteroFinish): trigger task lifecycle transition on sandbox agent completion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(heteroFinish): guard onTopicComplete against duplicate finish calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:11 +08:00
LiJian aa4533e6cb 📝 docs(cloudHeteroContext): add sandbox persistence & gh push rules (#14682)
* 📝 docs(cloudHeteroContext): add sandbox persistence & gh push rules

Inject ephemeral-sandbox warnings and mandatory GitHub push rules into
the cloud CC context block so every Claude Code run knows:
- The sandbox is wiped after inactivity — local changes will be lost
- All code changes must be committed and pushed before task is complete
- Use gh CLI (pre-authenticated) for GitHub operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(cloudHeteroContext): address review comments on sandbox persistence rules

- Remove gh push guidance (gh has no push subcommand; git push is correct)
- Gate gh-auth instructions behind githubToken availability to avoid
  auth-dependent commands failing in no-token sandbox runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 📝 docs(cloudHeteroContext): add git push auth fallback guidance

Tell CC that the sandbox has git credentials ready, but if git push
fails it can self-recover via:
1. gh auth setup-git (reconfigures git credential helper)
2. inline token URL as last resort (oauth2:$GITHUB_TOKEN@github.com)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:11 +08:00
YuTengjing ea1d926de4 📝 docs(skills): frontmatter cleanup + argument-hint (#14683)
* 🔨 chore: control skill triggering via frontmatter flags

- Rename debug skill to debug-package (avoid confusion with debugging workflows)
- Add disable-model-invocation to add-* skills so they are manual-only
- Add user-invocable: false to reference/architecture skills so they auto-load only when relevant

* 🔨 chore: rename skill reference dirs to plural references

Align with the skill-creator convention (scripts/, references/, assets/).

* 📝 docs(skills): split oversized SKILL.md files and refine triggers

- upstash-workflow: 1126L → 189L, extract implementation / best-practices / examples references
- data-fetching: 854L → 613L, move parent-keyed-map walkthrough to references
- store-data-structures: 625L → 314L, extract types and reducer references
- upstash-workflow/cloud.md, version-release/release-notes-style.md: add TOCs
- linear: rewrite ALL-CAPS MUSTs into prose explaining why; mark user-invocable: false
- version-release: mark disable-model-invocation: true (manual /version-release only)
- debug-package: expand description with concrete trigger phrases and tokens

* 📝 docs(skills): regularize microcopy structure

Move language-specific guidelines into references/zh.md and references/en.md
so SKILL.md can point to them via the standard progressive-disclosure pattern.
Previously the two files sat next to SKILL.md but were not referenced anywhere,
making them invisible to Claude Code loading.

* 📝 docs(skills): move builtin-tool refs into references subdir

Aligns builtin-tool with the references/ layout used elsewhere
(microcopy, store-data-structures). 3 md files move, SKILL.md
links updated.

* 📝 docs(skills): broaden trigger descriptions for core skills

Adds concrete API names, file paths and natural-language phrases so
auto-triggering catches more relevant prompts. Touches zustand,
drizzle, i18n, react, typescript, modal, hotkey.

* 📝 docs(skills): add argument-hint to user-only skills
2026-05-13 02:57:11 +08:00
𝑾𝒖𝒙𝒉 dfe19323b8 🐛 fix(hotkey): remove redundant onClear to prevent double updateHotkey calls (#14663)
Previously, clicking the clear button on HotkeyInput triggered both
`onClear` and `onChange` (since HotkeyInput internally calls
`setHotkeyValue('')` which fires `onChange`). This caused two
concurrent requests to `updateDesktopHotkey` and showed two toast
messages (success/error) for a single user action.

Fix: remove the redundant `onClear` prop. HotkeyInput's clear action
already fires `onChange('')`, so the single `onChange` handler is
sufficient.

Co-authored-by: Innei <i@innei.in>
2026-05-13 02:57:10 +08:00
Innei 0e58fa7126 ♻️ refactor(web-onboarding): merge agent-marketplace identifier into onboarding tool (#14672)
* ♻️ refactor(web-onboarding): merge agent-marketplace identifier into onboarding tool

Drop the standalone `lobe-agent-marketplace` builtin tool and fold its
`showAgentMarketplace` / `submitAgentPick` APIs into `lobe-web-onboarding`
so onboarding exposes a single tool identifier.

- Move marketplace API entries (with humanIntervention/renderDisplayControl)
  into WebOnboardingManifest; extend WebOnboardingApiName.
- Compose AgentMarketplaceExecutionRuntime inside WebOnboardingExecutionRuntime;
  the client WebOnboardingExecutor now owns showAgentMarketplace/submitAgentPick
  with telemetry hooks. Drop the separate client/server executor + runtime files.
- Merge marketplace Inspector / Intervention / Render maps under the
  web-onboarding identifier. Remove AgentMarketplace* entries from
  builtin-tools registries and from the builtin web-onboarding agent's
  plugins list.
- Switch customInteractionHandlers to route by (identifier, apiName) so
  the marketplace picker handler fires only on `showAgentMarketplace`.
- Drop the `lobe-agent-marketplace` fallback string in
  OnboardingActionHintInjector; match by apiName only.
- Rename plugin/setting locale keys under `lobe-web-onboarding.*`.

* 🐛 fix(onboarding): reserve scroll headroom for agent marketplace overlay

- Add a footerSlot spacer in ChatList matching the marketplace panel height so the latest message can be scrolled into view above the absolute overlay.
- Nudge the marketplace overlay inset by 2px to hide subpixel border seams.
- Document turn output order in the onboarding system role to avoid trailing filler text after tool calls.
2026-05-13 02:57:10 +08:00
YuTengjing b79c5d8e70 🐛 fix: reject inactive OIDC access (#14674)
* 🐛 fix: reject inactive OIDC access

* 🐛 fix: honor expired OIDC bans

* 🐛 fix: decouple OIDC inactive error from tRPC

*  test: fix OIDC auth type checks
2026-05-13 02:57:10 +08:00
Arvin Xu f591f7ac34 💄 style(web-onboarding): add Render for saveUserQuestion & showAgentMarketplace (#14667)
 feat(builtin-tool-web-onboarding): add Render for saveUserQuestion + showAgentMarketplace

Tool messages for `saveUserQuestion` and `showAgentMarketplace` previously
fell back to the raw Arguments/Response table once the call resolved
because neither API had a Render registered. Wire both up:

- `saveUserQuestion`: new Render mirroring the Intervention's detail-card
  style — agent identity (emoji + name), full name, and interests chips —
  rendered conditionally per the fields actually saved.
- `showAgentMarketplace`: reuse the existing `SubmitAgentPick` Render.
  After the picker submits, `customInteractionHandlers` rewrites the
  `showAgentMarketplace` tool message's `pluginState` to the same
  `{ summaries, installedAgentIds, ... }` shape, so the card grid
  renders without a new component.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:10 +08:00
Arvin Xu 3f43e69fa6 ♻️ refactor(knowledge-base): share RAG runtime across client/server via KnowledgeBaseSearchService (#14673)
* ♻️ refactor(knowledge-base): share runtime across client/server via KnowledgeBaseSearchService

Extract a server-side `KnowledgeBaseSearchService` (semanticSearchForChat
fan-out + getFileContents branching + groupAndRankFiles) so both the lambda
chunk router and the builtin tool server runtime orchestrate RAG through one
implementation. Wire the builtin knowledge-base tool to the shared
ExecutionRuntime in the package by moving the client executor to
`src/client/executor/` and registering a thin server runtime factory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(knowledge-base): move PG 23505 handling into adapters, restore executor path

ExecutionRuntime is dual-end so it cannot detect PG error codes — only the
server adapter can. Move the unique-constraint check there and translate the
lambda router's `FILE_ALREADY_IN_KNOWLEDGE_BASE` sentinel in the client
adapter, so the runtime's generic catch surfaces the human-readable message
on both code paths. Restore `src/executor/` as a top-level sibling of
`src/client/` to match the convention of every other builtin tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(knowledge-base): collapse executor into /client, drop ./executor export

The executor is just another client-only adapter (alongside Inspector and
Render) — no reason for it to sit at the package root with a dedicated
subpath. Move it under `src/client/executor/`, re-export from
`src/client/index.ts`, drop the `./executor` entry from package.json, and
update the consumer to import from `@lobechat/builtin-tool-knowledge-base/client`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(knowledge-base): cover KnowledgeBaseSearchService

13 unit tests across both methods:
- getFileContents: docs_* direct read, missing doc, file_* via findByFileId,
  parseFile fallback, parse failure surfaces as error entry, missing file,
  mixed batch.
- semanticSearchForChat: chunk grouping + relevance ranking, BM25 skip when
  no knowledgeIds, knowledgeIds → fileIds expansion, vector/BM25 isolated
  failure capture (preserves the other path's results + structured
  rejections), full failure 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>
2026-05-13 02:57:10 +08:00
Arvin Xu 314619d798 ♻️ refactor(bot): close activator bypass + converge device-access checks (#14664)
* ♻️ refactor(aiAgent): introduce deviceToolRegistry as single source of truth

Centralise "what counts as a device tool" into one module so the next
device-tool addition only touches one file. Removes the hardcoded
`new Set(['local-system', 'remote-device'])` from `deviceToolAudit.ts`,
which had drifted from `LocalSystemManifest.identifier` /
`RemoteDeviceManifest.identifier` imports elsewhere.

Foundation for the LOBE-8768 activator-bypass fix landing next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(aiAgent): block activator from bypassing canUseDevice gate

External bot senders could still reach the owner's machine by having the
LLM call `lobe-activator.activateTools(["lobe-remote-device"])`, because
`enableCheckerFactory.allowExplicitActivation` short-circuits before the
canUseDevice rule, and the engine's `manifestSchemas` always contained
the full builtin list (LOBE-8768 B1).

Fix by filtering builtin manifests **physically** through
`buildAllowedBuiltinTools` at both feed-points (ToolsEngine input and
the activator-discovery `toolManifestMap`). When `canUseDevice=false`,
the device manifests no longer exist in either map, so explicit
activation cannot resolve them — the rule-layer gate becomes
defense-in-depth instead of the sole barrier.

Validates with the prod incident's repro path: an external sender's
`<available_tools>` no longer advertises `lobe-remote-device`, and an
activator call to enable it returns "not found".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(bot,messenger): centralise isOwner derivation in buildBotContext

The same fail-closed expression
`!!operatorUserId && senderExternalUserId === operatorUserId` was
duplicated across `BotMessageRouter.onNewMention`, `.onSubscribedMessage`,
the DM catch-all, and `MessengerRouter.dispatchToAgent` — four sites,
one rule, one place to silently regress.

Route all four through `buildBotContext`. The helper now owns the
fail-closed contract referenced by `ChatTopicBotContext.isOwner`'s
docstring, so adding the next platform/router can't accidentally
default to "trusted when in doubt".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(aiAgent): apply device filter post-merge across all manifest sources

The previous fix only filtered the `builtinTools` source. An installed
plugin or a Skill/Klavis manifest declaring
`identifier: 'lobe-remote-device'` would still survive in
`manifestSchemas` and reach `toolManifestMap` via either
`getEnabledPluginManifests` or the direct ingest loops in
`aiAgent/index.ts` — letting an external bot sender activate the device
identifier through the activator.

Two changes close the gap:

  1. `ServerAgentToolsEngineConfig.excludeIdentifiers` — applied **after**
     combining plugin + builtin + additional manifests in
     `createServerToolsEngine`. `createServerAgentToolsEngine` passes
     `DEVICE_TOOL_IDENTIFIERS` whenever `canUseDevice` is false.

  2. `isManifestIngestAllowed` in `aiAgent.execAgent` — a single
     identifier guard reused at every `toolManifestMap` / `toolSourceMap`
     write (engine-returned plugin manifests, lobehub-skill loop,
     klavis loop). New ingest points inherit the wall automatically.

New test pins the regression: a plugin + an additional manifest
spoofing the device identifiers are dropped from `availablePlugins`
when `excludeIdentifiers` is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:10 +08:00
Arvin Xu d9fe275a4c ♻️ refactor(task): snapshot agent model into task.config at create time (#14670)
*  feat(task): snapshot agent model into task.config at create time

Pin the assignee agent's current model/provider into task.config when a
task is created so later changes to the agent's default model don't
silently affect already-created tasks. On first run, backfill the
snapshot for tasks created before this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task-runner): fall back to inbox agent when task has no assignee

`TaskRunnerService.runTask` previously threw `BAD_REQUEST` for any task
without `assigneeAgentId`, which broke runs created without `--agent`.
Resolve and persist the user's built-in inbox agent instead, surfacing
an `INTERNAL_SERVER_ERROR` only if that resolution itself fails.

Picked from #14671 (closes once landed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(task): collapse router orchestration into TaskService

Move multi-step task verbs out of the TRPC router into `TaskService`:
`createTask`, `cancelTopic`, `deleteTopic`, `runReview`, `updateStatus`,
`previewSubtaskLayers`, `runReadySubtasks`. The router keeps only input
validation + error wrapping; the tool runtime now shares the same
`createTask` path (was duplicating the model snapshot + parent
resolution).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🚨 ci: fix tsgo errors from TaskService extraction

`runReadySubtasks` router was rebuilding the `data` payload via a
conditional spread, which forced TS to infer a discriminated union that
broke `result.data.skipped` access in the integration test. Pass the
service result straight through so `skipped` stays a single optional
field. Also cast the stubbed `taskService` in the tool runtime unit
tests to bypass strict structural typing — same pattern the other
dep stubs already use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:10 +08:00
YuTengjing 03b3e2fc12 🔥 chore: drop task template tracking (#14666)
* 🔥 chore: drop task template tracking

The recommendation surface is about to be redesigned, so the analytics
funnel added in #14517 is being removed up front. A fresh tracking
schema will land alongside the redesigned UI.

- Delete `analytics.ts` plus its test and the tracking-focused
  `TaskTemplateCard.test.tsx`.
- Drop `RecommendedTaskTemplate` / `TaskTemplateRecommendationSource` /
  `TaskTemplateFallbackPool` and revert the service to plain
  `TaskTemplate[]`.
- Strip impression, dismiss, create-clicked/result and
  skill-connect-clicked/result calls from `TaskTemplateCard.tsx`, while
  keeping the createTask + navigate-to-task flow from #14540.
- Remove `recommendationBatchId` / `userInterestCount` / `onCreated`
  plumbing from `useDailyBriefRecommendationsUI`,
  `DailyBriefRecommendationsView`, and the card props.
- Revert `useSkillConnection` to the pre-tracking variant (no
  onConnectResult / SkillConnectionResult).

* 🐛 fix: remove created template from recommendation cache

After #14540 changed the create-task flow to auto-navigate to
`/task/{id}`, removing the `onCreated` plumbing from #14517 in the same
sweep meant the SWR recommendation cache was never mutated on success.
Combined with the server-side `recordCreated` being a no-op and
`listDailyRecommend` not excluding created IDs, returning to Home
showed the same recommendation as actionable again — letting users
trigger duplicate scheduled tasks from the same template.

Re-add the minimal cache-eviction plumbing (no analytics):

- TaskTemplateCard exposes `onCreated` and calls it on success
- useDailyBriefRecommendationsUI shares `removeTemplateFromList` for
  both dismiss and created flows
- DailyBriefRecommendationsView passes `onCreated` through
2026-05-13 02:57:10 +08:00
YuTengjing b0ee35dd35 🐛 fix: drop unreachable aihubmix empty-apiKey test (#14669)
* 🐛 fix: drop unreachable aihubmix empty-apiKey test

The `should return empty array when API key is missing` test asserts a
contract that doesn't hold: RouterRuntime.models() constructs the
underlying runtime via the OpenAI-compatible factory before calling
modelsOption, and the factory throws InvalidProviderAPIKey on empty
apiKey at construction time — so aihubmix's own `if (!apiKey) return []`
short-circuit can never actually fire.

Just delete the dead test. The defensive guard in aihubmix's modelsOption
stays as intent documentation. Also tighten an implicit-any in the
adjacent `should normalize model_id field to id` test.

* 🔥 chore: drop dead empty-apiKey guard in aihubmix modelsOption

* 💄 style: tighten aihubmix apiKey assertion to string
2026-05-13 02:57:10 +08:00
Zhijie He a1fac45b3a 💄 style: add reasoning_effort support for Grok 4.3 (#14642)
* style: add reasoning_effort for Grok 4.3

* style: remove grok 4.1 series & grok-imagine-image-pro (Model retirement)

style: remove grok 4.1 series & grok-imagine-image-pro (Model retirement)

style: remove grok 4.1 series & grok-imagine-image-pro (Model retirement)
2026-05-13 02:57:10 +08:00
Arvin Xu e0ead0c47a 💄 style: increase chat topic title length (#14659)
* 💄 style: increase chat topic title length

- bump initial topic title slice from 20 to 40 chars
- bump dev fallback slice from 30 to 40 chars
- bump thread title slice from 20 to 40 chars
- raise LLM summary title prompt limit from 50/10w to 80/15w

* 💄 style: bump topic/thread title slice from 40 to 80 chars

Align slice limits with the LLM summary prompt cap (80 chars) so the
initial visible title is no shorter than what the summarizer can return.
2026-05-13 02:57:10 +08:00
Bianzinan f4de472e82 fix(aihubmix): use full models endpoint to return complete model list (#14511)
* fix(aihubmix): use full models endpoint to return complete model list

The /v1/models endpoint at api.aihubmix.com returns only per-user-group
models (~256). The new endpoint at aihubmix.com/api/v1/models returns
the complete catalog (800+). Fetch from the full endpoint directly.

* fix(aihubmix): normalize model_id to id from full models endpoint

The https://aihubmix.com/api/v1/models endpoint uses `model_id` instead
of `id`. Map it to `id` before passing to processMultiProviderModelList
to prevent toLowerCase() errors and empty model list.

* fix(aihubmix): add apiKey guard, AbortController timeout, and better error messages

- Extract apiKey with runtime guard to fail fast when key is missing
- Add AbortController with 10s timeout to prevent indefinite hanging
- Include response body in error message for easier debugging
- Add APP-Code header comment pointing to docs
- Expand tests: mock global fetch, cover missing key / HTTP error / network error / AbortError cases

* fix(aihubmix): add field mapping adapter and fix timeout scope

Address review feedback from #14511:

- Update AiHubMixModelCard interface to reflect the new endpoint schema
  with full JSDoc (model_id, desc, types, features, input_modalities,
  context_length, max_output, pricing.cache_read/cache_write)
- Add mapAiHubMixModel() to adapt API response fields to LobeHub model
  card fields before passing to processMultiProviderModelList:
    desc             -> description
    model_name       -> displayName
    context_length   -> contextWindowTokens
    max_output       -> maxOutput
    types            -> type  (llm/t2t->chat, image_generation/t2i->image,
                               video/t2v->video, tts, stt, embedding,
                               rerank/reranking->rerank)
    pricing.cache_read  -> pricing.cachedInput
    pricing.cache_write -> pricing.writeCacheInput
    features(tools/function_calling) -> functionCall
    features(thinking)               -> reasoning
    features(web)                    -> search
    input_modalities(image)          -> vision
- Fix timeout scope: move clearTimeout into the finally block so the
  AbortController stays active during response.json() body read, not
  just during the initial fetch() call
- Update baseURL from https://api.aihubmix.com to https://aihubmix.com
  to match official integration docs (https://docs.aihubmix.com/cn/api/Aihubmix-Integration)
- Strengthen normalize test: assert list.some(m => m.id === 'some-model')
  instead of just Array.isArray to detect normalization failures
- Add field-mapping test using vi.spyOn on processMultiProviderModelList
  to assert that all adapted fields are passed correctly

* fix(aihubmix): filter out unsupported rerank types to prevent chat fallback

- Remove rerank/reranking from TYPE_MAP; they have no LobeHub AiModelType
  equivalent and would silently fall back to 'chat' in processModelCard
- Add UNSUPPORTED_AIHUBMIX_TYPES set and filter before mapAiHubMixModel()
- Add regression test asserting rerank/reranking models are excluded and
  llm models still pass through

---------

Co-authored-by: Bianzinan <bianzinan@users.noreply.github.com>
2026-05-13 02:57:10 +08:00
Innei 5f14b7e463 feat(activator): require activation reason (#14597) 2026-05-13 02:57:09 +08:00
Innei a9eb904cf4 🐛 fix(onboarding): skip marketplace on early exit, drop CJK in prompts (#14598)
* 🐛 fix(onboarding): skip marketplace on early exit, drop CJK examples in prompts

Honor the user's wish to leave: when the onboarding agent detects a true
early-exit signal in any phase, persist what is known, send a brief
farewell, and call finishOnboarding directly. The marketplace handoff is
mandatory only on normal Phase 4 / Summary completion. Previously the
spec forced the agent to invent categoryHints from environment cues
when discovery was thin, producing noisy recommendations for users who
explicitly asked to stop.

- Replace systemRole §Early Exit with a 4-step flow (no marketplace, no
  summary), and remove the trailing "respect their time" rationale that
  contradicted the new policy.
- Update toolSystemRole turn-protocol exception accordingly; mark
  persistence as best-effort (do not retry on failure) since the
  Pre-Finish Checklist is overridden on early exit.
- Update OnboardingActionHintInjector L101/L127 hints to match the new
  flow, and append an EXCEPTION clause to the Summary not-opened hint
  so a true exit signal in Summary skips the marketplace too.
- Strip CJK example phrases from prompt text; rely on the LLM's
  multilingual recognition with "equivalents in any language" hints.

* 🔨 refactor(FollowUpChips): remove unused consume function and reset editor state on chip click
🔨 style(InterventionBar): remove overflow hidden from container style

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(ci): align FollowUpChips test with removed consume and increase timeout for PGlite cold-start

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-13 02:57:09 +08:00
Neko 1374fd29e8 feat(agent-signal,server,prompts): consolidate in self-review implemented (#14657) 2026-05-13 02:57:09 +08:00
Arvin Xu 31e9130cf0 💄 style(hetero-agent): read-only SubAgent threads with breadcrumb header and thread switcher (#14658)
*  feat(hetero-agent): read-only SubAgent threads with breadcrumb header and thread switcher

- Hide chat input on SubAgent threads (execution is driven by the parent agent) and replace it with an inline read-only hint
- Render the hint as the last item inside the virtual list so it scrolls with messages instead of being pinned to the viewport bottom
- ChatList exposes a new `footerSlot` prop that VirtualizedList injects as a synthetic trailing data item
- Header now shows `topic / thread` breadcrumb; thread title is a popover trigger that lists sibling threads in the same topic for one-click switching
- Hide the working-directory tag while inside a thread — directory switching doesn't belong in this read-only view
- Unify user-facing strings to "SubAgent" (badge, hint, open/close labels)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(chat-input): soften queue tray preview borders

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(conversation): scrollToBottom lands on the true last VList item

scrollToBottom targeted displayMessages.length - 1, which leaves any
trailing synthetic items (spacer, SubAgent footer hint) below the
viewport. In SubAgent threads this kept atBottom = false after the
BackBottom click or auto-scroll, so the button appeared stuck.

VirtuaScrollMethods now exposes getTotalCount, which VirtualizedList
fills from the live data length (messages + spacer + optional
footerSlot) via a ref. scrollToBottom uses that to scroll to the real
last index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:09 +08:00
Arvin Xu 84b802cf96 💄 style(chat-input): show skeleton in action bar while config is loading (#14656)
* 💄 style(chat-input): show skeleton in action bar while config is loading

Before agent / group config hydrates, action buttons read DEFAULT_*
fallbacks and the send button would dispatch against a not-yet-ready
target. Add an `isConfigLoading` prop on DesktopChatInput that swaps the
action bar + send area for skeleton placeholders. The chat page passes
`agentSelectors.isAgentConfigLoading`, group chat passes
`agentGroupSelectors.isGroupsInit`. The editor itself stays usable so
users can start typing immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(home,i18n): use 已阅 for brief confirm/confirmDone in zh-CN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use 确认完成 for brief.action.confirmDone in zh-CN

confirmDone signals the terminal transition (task marked complete),
not just dismissing the brief, so 已阅 loses the semantic distinction
from `confirm`. Use 确认完成 to match the EN intent ("Confirm complete").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use "Confirm complete" for brief.action.confirmDone in en-US

Match the semantic distinction the call site relies on:
`confirm` is dismiss-only for recurring scheduled runs, while
`confirmDone` marks the terminal completion transition. The test
mock already used "Confirm complete" — align the source defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:09 +08:00
Arvin Xu e261a6ff98 💄 style(home): add Recommendations module with hetero agent action library (#14645)
*  feat(home): add Recommendations module with hetero agent action library

Introduce a `Recommendations` section that renders above the existing daily-brief
task templates. The module is driven by an extensible action registry with per-action
eligibility checks; the first registered actions surface "Add Claude Code agent" and
"Add Codex agent" cards on desktop when the matching local CLI is detected and the
user hasn't added that hetero agent yet.

- New `src/features/Recommendations/` with action types, registry, hetero-agent
  factory, eligibility hook, parallel CLI detection (SWR-cached) and card UI.
- Extract `createHeterogeneousAgent` from `useCreateMenuItems` into a shared
  `useCreateHeteroAgent` hook so the sidebar menu and Recommendations card share
  one creation path (create + refresh sidebar + navigate to chat).
- `DailyBrief` now renders `<Recommendations />` in place of the standalone
  template-only section; visibility is driven by the new
  `useRecommendationsVisible` hook.
- Add `recommendations.*` i18n keys to the `home` namespace (default + zh-CN +
  en-US dev preview).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(home): polish Recommendations card with brand avatar and tighter copy

Use brand Avatar icons with rounded square shape, drop the duplicate title, and tighten copy (Coding Agent tag, Add Agent CTA).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:09 +08:00
Rdmclin2 3fb8daaa08 🔨 chore: optimize system bot (#14649)
* feat: add already consumed alert

* feat: support slack send slack commends  emphemeral in channel

* chore: handle parse commands imperial

* fix: slack messenger callback ok

* feat: add messager connectionId per user

* fix: add userId to webhookbody

* fix: test case
2026-05-13 02:57:09 +08:00
Arvin Xu 49c3d7e367 feat(hetero-agent): support AskUserQuestion tools for claude code (#14639)
*  feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2)

Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's
built-in tool short-circuits in `-p` mode, so we host an in-process MCP
server that exposes an equivalent `ask_user_question` tool. The handler
blocks until the consumer submits an answer (or the 5min deadline / op
shutdown fires), surfacing a structured `agent_intervention_request` /
`agent_intervention_response` round-trip on the existing event stream.

Added in this commit:

- `packages/heterogeneous-agents/src/askUser/`
  - `AskUserBridge` — per-op pending map with timeout / cancel / progress
    keepalive support; emits an async-iterable of outbound events
  - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server,
    `?op=<id>` query routes via `AsyncLocalStorage` →
    `onsessioninitialized` → sessionId↔opId map; tool handler hands off
    to the matching bridge and pumps `notifications/progress` back to CC
    every 30s as wire-level keepalive (required for >5min waits, see
    spike notes)
  - `constants.ts` — shared tool/server names + the stable `apiName`
    the adapter rewrites to
  - Unit tests cover bridge lifecycle (resolve / cancel / timeout /
    progress / event stream) and an end-to-end MCP probe via
    `StreamableHTTPClientTransport`

- `packages/agent-gateway-client/src/types.ts` — wire-level
  `agent_intervention_request` / `agent_intervention_response` event
  variants + payload interfaces. Re-exported through the package barrel.

- `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's
  `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter
  rewrites `apiName` to `askUserQuestion` so the renderer routes on a
  clean domain key. Identifier stays `claude-code`. Applied to both the
  main-agent and subagent paths for symmetry (subagent ask isn't
  expected today, but doesn't hurt).

- `src/server/routers/lambda/aiAgent.ts` — Zod input schema for
  `aiAgent.heteroIngest` extended with the two new event types so the
  CLI sandbox can forward them through the server.

No producer wiring yet — Steps 3-5 plug this into Electron main, the
renderer executor, and the new UI.

*  feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3)

Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the
desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now
goes live during real prompts; renderer-submitted answers route back via
new IPC.

Changes
- `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add
  optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so
  controller-managed temp configs flow into the driver.
- `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts`
  — append `--mcp-config <path>` when provided. Disallowed-tools pin
  stays so CC's built-in AskUserQuestion remains off (avoids double-
  registration of the same tool name).
- `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts`
  - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt
    (de-duped concurrent first-callers via in-flight promise).
  - Per-op `setupInterventionForOp(opId, sessionId)`: registers an
    `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with
    `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no
    ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()`
    into the existing `heteroAgentEvent` broadcast.
  - Cleanup paths: exit handler `await intervention.cleanup()` settles
    pending MCP handlers + unlinks the temp config; pre-spawn errors
    short-circuit the same cleanup so we don't leak bridges on
    `buildSpawnPlan` / trace-session failures.
  - `before-quit` stops the MCP server (in addition to killing CC
    processes).
  - New `@IpcMethod() submitIntervention({ operationId, toolCallId,
    result?, cancelled?, cancelReason? })` — renderer side will dispatch
    answers / cancellations through this in Step 4/5.
  - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`.
- `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy
  for `submitIntervention`.
- New `claudeCode.test.ts` covers the four driver-arg paths
  (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay
  disallowed). Existing 28 controller tests still pass.

What still doesn't run end-to-end
- The renderer `heteroExecutor` doesn't consume `agent_intervention_request`
  yet — events go through the broadcast but the chat store ignores them.
- No UI to render the intervention card or to call `submitIntervention`.
Both lands in Steps 4/5 next.

*  feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4)

Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId`
from MCP `_meta`) instead of a random UUID, so the
`agent_intervention_request` event references the same id as the existing
tool message on the renderer side.

Renderer-side `heteroExecutor` learns the new event:

- Added `persistInterventionRequest(...)` next to `persistToolResult` —
  stamps `pluginState.askUserQuestion` (apiName + identifier + questions
  parsed from `arguments` + deadline + status='pending' + toolCallId)
  onto the matching tool message via `messageService.updateToolMessage`.
- New branch in `handleStreamEvent` for `'agent_intervention_request'`:
  defers behind `persistQueue` (so it lands AFTER `persistToolBatch`
  populates `toolMsgIdByCallId`), then mirrors the same pluginState onto
  the in-memory message via `internal_dispatchMessage` so the UI lights
  up immediately — no fetchAndReplaceMessages round-trip needed.
- The eventual `tool_result` for the same toolCallId hits the existing
  `tool_result` branch unchanged: it overwrites `pluginState` with
  whatever the result carries (typically undefined for our MCP tool, so
  `pluginState.askUserQuestion` clears and the intervention UI yields to
  the regular Render).

Bridge tests cover the new contract:
- caller-supplied toolCallId becomes the wire correlation key
- duplicate-toolCallId pendings reject loudly so two-handler clobbers
  surface immediately

153 package tests + 1167 desktop main tests + 51 hetero executor tests
still green; type-check clean.

*  feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5)

Dedicated Render for the synthetic `askUserQuestion` apiName the adapter
rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives
under CC's render registry so the existing chat tool-detail flow picks
it up automatically — no changes to the conversation framework.

- New `AskUserQuestionItem` / `AskUserQuestionArgs` /
  `AskUserQuestionPluginState` types (mirrors CC's own
  AskUserQuestion schema verbatim).
- `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'`
  member so the renders / inspectors / streamings registries can key
  off the same enum value.
- `client/Render/AskUserQuestion/index.tsx` is the component:
  - `pluginState.askUserQuestion?.status === 'pending'` → renders the
    questions form (Select for single-select, CheckboxGroup for
    multi-select), a 5-min countdown ticking once a second, Submit /
    Skip buttons. Reads `operationId` via `messageOperationMap` so we
    can route through `heterogeneousAgentService.submitIntervention`.
  - Otherwise → renders the questions as muted captions plus the
    final answer text from `content`. Surfaces a warning when the
    tool_result was an error (timeout / cancelled / session ended).
  - Submit button stays disabled until every question has a
    selection; Skip always enabled (sends `cancelled: true`).
- `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers
  the new component.

What this does NOT do
- Doesn't touch `BuiltinToolInterventions` — the form is rendered
  inside the regular tool body (Render slot), not the canonical
  intervention slot. Cleanest for now: the framework intervention
  flow assumes `submitToolInteraction` store actions, which would
  fight our IPC path. We can refactor onto that surface later if
  CC grows additional interactions (approval, file picker).
- Doesn't translate strings — i18n in a follow-up.

Type-check clean. Step 6 (real desktop e2e via CC) is next.

*  feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up)

Step 5 registered the Render component but stopped at the registry — the
chat tool-detail still returned the loading placeholder while
`isToolCalling` was true, so users only ever saw a spinner during the 5
min intervention window.

Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on
CC + apiName=askUserQuestion tool messages) and route to the registered
builtin Render inline before the placeholder branch. Once the
intervention resolves, the eventual `tool_result` clears
`pluginState.askUserQuestion` and the regular Render takes over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up)

LOBE-8519 left two TODOs in `generationSlice` where hetero runtime
silently fell through to client mode — regenerate would secretly hit the
agent's underlying LLM, and continue would synthesize a fake "please
continue" turn that confuses CC / Codex.

- regenerateMessage: re-create the assistant row branched off the same
  user message, resolve resume sessionId (drop on cwd mismatch), then
  spawn a child `execHeterogeneousAgent` op so Stop only kills the
  executor, not the parent regenerate op. Mirrors sendMessage's hetero
  branch.
- continueGenerationMessage: hetero CLIs have no continue primitive —
  each prompt is a fresh user turn — so bail out instead of polluting
  the session.
- continueGenerationMessage: gateway mode now branches a server-side
  resume run instead of falling through to client.

Surfaced while testing CC AskUserQuestion end-to-end on the
LOBE-8725 branch (regenerating after an answered question went through
the wrong runtime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2

Two bugs surfaced when invoking the local-testing helper from a fresh
session on macOS:

- `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit
  code propagates through `pipefail`. With `set -e`, an empty pid set
  silently kills the whole script — `do_start` reported success, no
  Electron, no error. Trail with `|| true`.
- `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`;
  process-tree teardown still works because `expand_descendants` walks
  the tree directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725)

`AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across
every CC subprocess. The SDK transport latches `_initialized=true`
after the first `initialize`, so the second op's CC subprocess sees
`Invalid Request: Server already initialized` (400) and reports the
`lobe_cc` server as `failed`. From the model's POV the MCP tool is
absent — it falls back to ToolSearch, can't find anything, and
verbalizes the question instead.

Refactor to the canonical multi-tenant pattern: one transport + one
`McpServer` per session, looked up by the SDK-managed `mcp-session-id`
header. New transports are minted on the first POST without a session
id (must be an `initialize` request); subsequent requests route via
the stored map; `onsessionclosed` cleans up.

The first run of any process still works as before — this only matters
once a second op spins up. Added a 3-op sequential regression test
that fails on the old single-transport implementation and passes now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725)

Step 5's first cut shoehorned the pending form into the Render slot and
drove submit/skip with a custom `pluginState.askUserQuestion.status`
field, which forced three layers of glue:

- `Tool/Detail` had to bypass the loading placeholder via an
  identifier+apiName hardcode so the form would surface during
  `isToolCalling`
- The executor had to `messageService.getMessages → replaceMessages`
  after `agent_intervention_request` to drag the freshly-created tool
  row into in-memory state (the framework's own `tool_end →
  fetchAndReplaceMessages` only fires after the user answers)
- The executor also had to `associateMessageWithOperation` for the tool
  row so the form could look up the running CC op for IPC

All three were patches around skipping the canonical surface. This
commit moves AskUserQuestion onto `pluginIntervention.status='pending'`
and the `BuiltinToolInterventions` registry, which the framework
already drives end-to-end:

- `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx`
  — pure form, no IPC, no store reads. Resolves through the standard
  `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback.
- `Render/AskUserQuestion` shrinks to the answered/aborted view only;
  the framework hides Render while pending, so no status switching.
- New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}"
  chip in the inline tool body, matching the rest of CC's tools.
- Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new
  `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`;
  `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry.

Hetero needs a different action handler than `submitToolInteraction`
(which spawns `executeClientAgent` — wrong for a CC subprocess that's
already blocked on an MCP call). Two thin pieces wire that:

- `submitHeteroIntervention` (chat store) — sets
  `pluginIntervention` via `optimisticUpdateMessagePlugin` (which
  already syncs DB + in-memory + parent-assistant `tools[].intervention`
  in one shot), then forwards the answer through
  `heterogeneousAgentService.submitIntervention` IPC. Operation lookup
  walks the tool message's `parentId` to hit the assistant's
  `messageOperationMap` entry — drops the explicit
  `associateMessageWithOperation` call from the executor.
- `customInteractionHandlers.isHeteroInteractionIdentifier` flags
  `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits
  there before reaching the existing `submitToolInteraction` path.

Executor change collapses to one line:
`optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`.
The post-intervention refresh, the associate call, and the
`persistInterventionRequest` helper all go away.

Removed:
- `AskUserQuestionPluginState` type (custom field is gone)
- `Tool/Detail` `askUserPending` inline-render branch
- Executor `messageService.getMessages + replaceMessages` round-trip
- Executor `associateMessageWithOperation` for tool rows
- `persistInterventionRequest` helper

Verified end-to-end against a real CC subprocess on desktop:
- Inline body shows the new Inspector chip; pending form lives in the
  bottom InterventionBar (canonical surface)
- Submit ships answer through MCP, CC continues with structured result
- Skip flips status to `rejected`, framework's RejectedResponse
  shows "User skipped"; CC receives isError and falls back to text
- `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op
  (the per-session transport fix from the previous commit)
- `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725)

Select dropdown was the wrong primitive — it hides options behind an extra
click and doesn't read like a question to answer. CC's underlying tool is
1-4 questions × 2-4 options, so the whole option set always fits inline.

- Each option renders as a clickable card: numbered chip (1/2/3/4) +
  bold label + secondary description on a single row. Hover tints the
  background; selected state lights up `colorPrimary` on both the chip
  and the card outline so the pick is unmistakable at a glance.
- Multi-select (`q.multiSelect`) toggles instead of replacing, with a
  "(multi-select)" hint in the question header.
- Multi-question support gets a proper visual hierarchy: each question
  past the first sits below a dashed divider, headed by a `Q1/N` tag
  + the original `q.header` chip. The `Q*/N` lets the user track
  progress without counting.
- Inspector picks up the question count too: now shows
  "askUserQuestion · {first header} +N" when multiple are queued.

Verified end-to-end on desktop with a CC-driven 2-question prompt
(4-option + 3-option). Both selections feed back to CC as a single
"User answers" payload, CC echoes both picks in its continuation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725)

- Multi-question forms now use a top tab strip; single question renders inline.
- Picking a single-select option auto-advances to the next unanswered question.
- Drafts persist to tool message `pluginState.askUserDraft` so picks survive
  remount / HMR; new `setInterventionDraft` action on the chat store dispatches
  the pluginState patch.
- Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for
  every unanswered question instead of letting the bridge time out into a
  cancelled isError — model gets a structured answer it can act on.
- Visual: selected option now uses filled `colorPrimaryBg` + right-aligned
  check icon; index chip stays neutral.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725)

The async exit-handler cleanup raced Electron's main-process teardown and
left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync
unlink in the quit hook is the only reliable guarantee.

Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q
or `app.quit()`, not on external kills (test harness, OS shutdown).

Verified by manual test: pending askUserQuestion forms now leave zero
residue after both Cmd+Q and SIGTERM paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725)

Submit now writes the structured `{ questionText: pickedLabel(s) }` payload
to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so
Render no longer has to scrape the bridge's prose `User answers:` content.

Render shows one Q&A block per question — header + question + a checkmark
card per picked option (multi-select fans out into multiple rows). Falls
back to a `—` placeholder when answers are missing (older messages or
skipped flows), and keeps the existing `pluginError` warning for cancel /
no-answer paths.

Also surfaces the answers in the Skill state inspector tab, which was
previously empty for completed askUserQuestion messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725)

Locks down the regression fixed in c0de0cdb7c — async exit-handler cleanup
losing to Electron's main-process teardown. Four cases: `before-quit`
(Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown),
`SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not
throw on the second pass).

`process.on` and `process.exit` are stubbed in the signal-path tests so the
controller's listener attaches to a spy, not the test runner's process —
otherwise we'd leak a real SIGTERM listener every test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:09 +08:00
Neko 71ddedaa83 ️ perf(agent-signal,prompts,types,database,server): fixed many minor self-review issues, harden the structure, verified with eval (#14647) 2026-05-13 02:57:09 +08:00
Arvin Xu 60a127b1e5 💄 style(copyable-label): wrap long tool-call params instead of truncating (#14640)
* 💄 style(copyable-label): wrap long values instead of truncating

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(copyable-label): make wrap an opt-in via Descriptions prop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(descriptions): omit GridProps wrap to avoid type collision

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:09 +08:00
Arvin Xu b85a1ad851 💄 style: format tool execution time as Xmin Ys instead of X.Y min (#14641)
🐛 fix: format tool execution time as `Xmin Ys` instead of `X.Y min`

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
Arvin Xu 7daed90d0e 🐛 fix(model-runtime): enrich stream parse errors with provider/model context (#14636)
*  feat(model-runtime): enrich stream parse errors with provider/model context

When the OpenAI / Anthropic SDK iterator throws (most often a JSON
SyntaxError on a malformed SSE chunk — e.g. an upstream response with an
illegal backslash escape), `convertIterableToStream` previously only
surfaced `message`/`name`/`stack`. Downstream error logs (agent-gateway
errors table) end up with just "Bad escaped character in JSON at
position 160050" and no way to correlate which provider/model produced
it or whether the same offset keeps recurring.

This change threads optional `{ provider, model }` context through
`convertIterableToStream` / `readableFromAsyncIterable` and enriches the
FIRST_CHUNK_ERROR payload with:

- `provider` / `model` so triage can group identical upstream failures
- `parsePosition` extracted from V8 JSON SyntaxError messages
- `causeName` / `causeMessage` when `error.cause` is set (many wrapped
  errors carry the actionable detail in `cause` and the bare triplet
  drops it)

Threaded through OpenAI/Responses/Anthropic stream handlers, which all
already receive `payload` containing provider/model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): walk error.cause for parsePosition + JSON-safe payload

Two review findings on #14636:

1. Wrapped SyntaxErrors lost their parsePosition. Provider SDKs commonly
   rethrow `JSON.parse` failures wrapped in their own error class
   (e.g. `APIError(cause: SyntaxError)`), so the outer `error.name` is
   no longer `'SyntaxError'` and the previous check skipped extraction
   for the exact case this enrichment was meant to diagnose. Now
   `extractParsePosition` walks both the outer error and any `Error`
   cause, and accepts any error whose message still carries the
   `"JSON at position N"` signature even if the SyntaxError name was
   lost in wrapping.

2. Cause cloning could blow up the entire diagnostic path.
   `structuredClone` succeeds on values that `JSON.stringify` later
   throws on (BigInt, circular refs), so a non-Error cause carrying
   either would surface as `payload.cause = clonedObject`, then the
   outer `JSON.stringify(payload)` would throw inside the catch handler,
   and the FIRST_CHUNK_ERROR chunk never gets emitted. Replaced with
   `safeJsonStringify` (BigInt → string, cycles → `[Circular]`) and
   route the cause object through `toJsonSafe` so the returned shape is
   always plain JSON.

Added tests for both: a wrapped APIError(cause: SyntaxError) yields
parsePosition, and a cause containing both BigInt and a circular ref
still emits a parseable error chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
Arvin Xu 0babdcfc00 🐛 fix(home): strip markdown links from daily-brief input placeholder (#14635)
The daily-brief hint will start carrying `[name](url)` markdown links so
the AI can resolve referenced entities when the user submits via the
hint. The placeholder layer is the only consumer that wants the visible
label without the link syntax — extract a small `stripMarkdownLinks`
util and apply it at `InputArea/index.tsx` only. `useSend` continues to
forward the raw hint, so the agent still receives the link in the
outgoing message.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
YuTengjing d445a89c85 🐛 fix: consume visual content parts in server runtime (#14637) 2026-05-13 02:57:08 +08:00
Arvin Xu 3c8101128e feat(bot): gate device tools by sender identity (#14634)
*  feat(bot): gate device tools by sender identity (LOBE-8715)

External users who @-mentioned a bot ran the agent as the bot owner and
could call LocalSystem / RemoteDevice tools — a confused-deputy hole that
let any group member indirectly read/write the owner's machine.

- `ChatTopicBotContext` carries `senderExternalUserId` + `isOwner`
- `BotMessageRouter` / `MessengerRouter` compute `isOwner` at the entry
  point (fail-closed when `settings.userId` is missing)
- `resolveDeviceAccessPolicy` maps sender identity to
  `{ canUseDevice, reason }`; trusted-list branch is reserved for future
  work without engine changes
- `AgentToolsEngine` gates `LocalSystem` + `RemoteDevice` on `canUseDevice`
- `RemoteDeviceManifest.systemRole` is no longer injected on
  external-sender turns — closes the device-list information leak
- Per-call audit log (`lobe-server:agent-device-tool-audit`) at the
  dispatch site records sender, isOwner, reason, identifier, apiName

Fixes LOBE-8715

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🚨 chore(bot): replace `any` on botContext / botPlatformContext with concrete types

Picks up the existing `BotPlatformContext` (`@lobechat/context-engine`)
and `ChatTopicBotContext` (`@lobechat/types`) — both already exported —
instead of the inherited `any` placeholders on:

- `OperationCreationParams.{botContext, botPlatformContext, deviceAccessPolicy}`
- `InternalExecAgentParams.botPlatformContext`
- `RuntimeExecutorContext.botPlatformContext`

`deviceAccessPolicy.reason` is now `DeviceAccessReason` instead of `string`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 fix(bot): clear activeDeviceId when canUseDevice=false (LOBE-8715)

The previous patch gated `LocalSystemManifest` in the engine's enabledToolIds,
but `buildStepToolDelta` re-injects local-system from `state.metadata.activeDeviceId`
on every step regardless of whether the engine excluded it. Auto-activation
in `aiAgent.execAgent` populated `activeDeviceId` whenever
`(discordContext || botContext) && onlineDevices.length === 1`, so an
external bot sender with one device online could still get local-system
tools against the owner's device.

- `aiAgent/index.ts`: skip `activeDeviceId` derivation entirely when
  `canUseDevice` is false. `deviceSystemInfo` short-circuits naturally on
  `if (activeDeviceId) {...}`, so no extra change needed there.
- `RuntimeExecutors.ts`: belt-and-suspenders — if
  `state.metadata.deviceAccessPolicy.canUseDevice` is false, swallow
  `activeDeviceId` before passing to `buildStepToolDelta`, so a future
  plumbing bug at the source can't reopen the bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 feat(bot): allow device tools on personal-scope platforms (WeChat) (LOBE-8715)

Not every bot platform can identify an owner. WeChat's LobeHub integration
encodes every inbound thread as 1:1 (`packages/chat-adapter-wechat/src/adapter.ts:465`)
and its settings schema has no `userId` field, so `isOwner` is structurally
false on every WeChat turn. The previous policy denied every WeChat call
with `bot-owner-not-configured` — fail-closed but unusable.

This commit treats platforms whose integration is structurally personal-
scope as trusted. WeChat is the only member today; LINE is intentionally
excluded because its adapter handles group/room threads even though its
schema also lacks `userId` — those must be fixed at the schema layer
before being whitelisted.

- New `bot-personal-platform` reason in `DeviceAccessReason`
- `PERSONAL_SCOPE_BOT_PLATFORMS = new Set(['wechat'])`
- Personal-scope check sits AFTER `isOwner` so a future WeChat schema
  with a `userId` field still resolves as the more specific `bot-owner`
- Tests: WeChat without isOwner → allow; WeChat with isOwner=true → still
  `bot-owner` (more specific wins); regression guard ensuring Discord /
  Slack / Telegram / Feishu / Lark / QQ / LINE keep going through the
  standard isOwner gate

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(engine): opt existing device gate tests into canUseDevice=true (LOBE-8715)

The `LocalSystem` / `RemoteDevice` enable rules now short-circuit on
`canUseDevice` (default `false`), so tests that exercise the
engine-internal gates (`runtimeMode`, `deviceContext`, `clientRuntime`)
must explicitly pass `canUseDevice: true` — otherwise they assert the
right behavior for the wrong reason or fail outright (e.g. the desktop
RemoteDevice-suppression case the reviewer flagged).

- All `LocalSystem` / `RemoteDevice` / `LocalSystem + RemoteDevice` /
  `clientRuntime === "desktop" (Phase 6.4)` blocks now set
  `canUseDevice: true`.
- The "disable RemoteDevice in bot conversations" test was repurposed:
  the dropped `!isBotConversation` clause is now subsumed by `canUseDevice`,
  so for a trusted bot caller (canUseDevice=true) RemoteDevice DOES surface.
  The original intent — block when caller is untrusted — is captured in
  the new `canUseDevice gate` block.
- New `canUseDevice gate` describe block asserts:
    1. `canUseDevice=false` blocks LocalSystem even on a desktop caller
    2. `canUseDevice=false` blocks RemoteDevice with proxy configured
    3. Omitting `canUseDevice` → fail-closed default (deny)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(execAgent): set isOwner=true on device auto-activation tests (LOBE-8715)

These pre-existing tests model an owner using the bot through Discord and
assert that `activeDeviceId` auto-populates when one device is online.
After LOBE-8715, `activeDeviceId` is gated on `canUseDevice` from
`resolveDeviceAccessPolicy`, so a `botContext` without `isOwner: true`
resolves to `bot-external-sender` → `canUseDevice=false` →
`activeDeviceId=undefined`.

Filling out the `botContext` mocks with `isOwner: true` (plus the other
required fields the type now demands) preserves the tests' original
intent while exercising the new gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
YuTengjing 9982de3a5c 🐛 fix: store onboarding interests as keys (#14624) 2026-05-13 02:57:08 +08:00
Arvin Xu 7f6fdd7c14 🔥 chore(web-crawler): remove WeChat URL rules (#14633)
Drop the `weixin.sogou.com` and `mp.weixin.qq.com` rules from the crawler
URL ruleset since they are no longer needed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
LobeHub Bot d13f2e3ad8 🌐 chore: translate non-English strings to English in apps/cli, apps/device-gateway, and apps/desktop scripts (#14626)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
LiJian 7675bd9fb5 🐛 fix(hetero-agent): sync new-step assistant across replicas (#14631)
* 🐛 fix(hetero-agent): sync new-step assistant across replicas

* 🐛 fix(hetero-agent): tighten new-step assistant fallback

* fix: slove the test
2026-05-13 02:57:08 +08:00
LiJian 457d112a74 🐛 fix: remove the old cron job from lobehub (#14630)
* fix: remove the old cron job from lobehub

* fix: add some ts back
2026-05-13 02:57:08 +08:00
LiJian 6595961e5a 🐛 fix: refresh content baseline from DB on every ingest call (#14603)
* 🐛 fix: refresh content baseline from DB on every ingest call

Vercel serverless routes consecutive batches to different Lambda
instances. A warm replica's in-memory `accumulatedContent` only
reflects batches it processed; it has no visibility into batches
handled by other replicas.

The failure pattern (worst when a repo is selected, since CC makes
tool calls early):

1. Lambda A — batch 1 (text "你好!...") → flushBatchContent writes
2. Lambda B — batch 2 (text "...任务。") → restores from DB, appends,
   writes longer text to DB
3. Lambda A — batch 3 (tools_calling only, warm state) → its stale
   `accumulatedContent` = batch-1 text → persistMainToolBatch Phase 1
   writes `{ tools, content: stale-short-text }` → OVERWRITES the
   correct longer DB value → content truncated at "你"

Fix: re-read the current assistant message from DB at the start of
every `ingest()` call. Since `flushBatchContent` writes at the end of
every batch, DB is authoritative. The refresh gives each Lambda the
latest flushed baseline, so new text in the current batch extends
the correct full string.

Cost: one extra `findById` round-trip per warm ingest call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat: auto-inject GitHub OAuth token into CC sandbox

Previously the GitHub token was only resolved when repos were selected
AND GITHUB_CRED_KEY was explicitly configured in the agent config —
so CC running without pre-selected repos had no GitHub access and had
to ask the user for a PAT manually.

Changes:
- aiAgent/index.ts: always try to resolve the token using key 'github'
  (standard LobeHub OAuth connector default); GITHUB_CRED_KEY still
  overrides. No longer guarded behind topicRepos.length > 0.
- sandboxRunner.ts: new buildCredsSetupScript() runs before CC starts:
    mkdir -p ~/.creds
    printf 'GITHUB_ACCESS_TOKEN=%s\n' <token> > ~/.creds/env
    gh auth login --hostname github.com --with-token
  Writes ~/.creds/env in the same format as injectCredsToSandbox(["github"])
  so CC can source it in sub-shells. Creds step runs before repo clone step.
- cloudHeteroContext.ts: system prompt now tells CC that GITHUB_TOKEN is
  set, gh CLI is pre-authenticated, and ~/.creds/env has GITHUB_ACCESS_TOKEN
  with the source/auth recipe for sub-shell usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: adopt max-length content on DB refresh to guard flushBatch retry

The unconditional DB overwrite in ingest() broke the retry contract:
if flushBatchContent threw after events were already marked in
processedKeys, a retry on the same warm instance would read the stale
(shorter) DB value and wipe the in-memory chunks — which processedKeys
would then skip, losing them permanently.

Fix: only adopt the DB value when it is LONGER than in-memory.
This preserves both behaviours:
- Multi-replica stale (the original fix): DB has more content from
  another replica → dbContent.length > in-memory → adopt DB. ✓
- flushBatchContent retry on same Lambda: DB still has the old shorter
  value, in-memory has the correct accumulation → keep in-memory. ✓

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
Arvin Xu ae8f9cfb27 🐛 fix(hetero-agent): disable Claude Code AskUserQuestion to avoid auto-decline (#14629)
* 🐛 fix(hetero-agent): disable Claude Code AskUserQuestion to avoid auto-decline

CC's built-in AskUserQuestion self-injects an `is_error: "Answer questions?"`
tool_result inside the CLI in `-p` non-interactive mode before the host can
surface the questions, so the model falls back to plain-text prompting after
a wasted round-trip. Add `--disallowedTools AskUserQuestion` to both spawn
sites (desktop driver + lh hetero exec) so the model goes straight to text.

To be revisited once a local MCP-backed replacement is wired to LobeHub's
intervention UI.

* ♻️ refactor(hetero-agent): share CC base args, opt-in partial deltas

- Promote CLAUDE_CODE_BASE_ARGS in `@lobechat/heterogeneous-agents/spawn` to
  the canonical source of truth for invariant CC CLI flags (`-p`, stream-json
  IO, `--verbose`, `--disallowedTools AskUserQuestion`); export it so the
  desktop driver can compose on top instead of duplicating.
- Pull `--include-partial-messages` out of the base. It's now a
  `SpawnAgentOptions.includePartialMessages` flag, off by default so
  `lh hetero exec` standalone/sandbox runs don't pay for delta noise they
  don't render. The desktop driver opts in (chat bubble streams live).
- Permission mode stays caller-specific: desktop hardcodes bypassPermissions
  (always user-mode), the package keeps its root-vs-user branch for cloud
  sandbox.

* 🎨 style(hetero-agent): pass spawn-args builders an options object

Positional list grew to four args with mixed types — switch to a single
`BuildSpawnArgsParams` object so call sites read by field name and adding
future per-agent flags doesn't push every other caller around.
2026-05-13 02:57:08 +08:00
Arvin Xu 96165e453a 🐛 fix(local-system): guard readFile against binary blobs and oversized output (#14602)
* 🐛 fix(local-system): guard readFile against binary blobs and oversized output

Previously `lobe-local-system.readFile` would happily decode any extension
as UTF-8 and return the entire content. Reading a 27KB base64-encoded git
bundle blew up the next LLM call to 3.28M tokens / 416s and triggered a
DB rollback. The default 200-line cap was bypassed because base64 was a
single very long line.

Add four layers of protection in `readLocalFile`:
- Hard-reject extensions outside the text-readable + special-parser
  whitelist with a structured error pointing the agent at runCommand.
- Sniff the first 8KB and refuse files that look binary (null bytes or
  >30% non-printable chars).
- 10MB hard size cap before the file is read into memory.
- Cap each returned line at 8K chars and total output at 500K chars,
  with `truncated` / `linesTruncated` flags surfaced in the result.

Refs LOBE-8703.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(file-loaders): preserve UTF-16 text files without a BOM in binary sniffer

The binary sniffer rejected UTF-16LE/BE files that lacked a BOM because
their alternating 0x00 bytes tripped the null-byte heuristic. `TextLoader`
already has a `detectUtf16NoBom` heuristic for these Windows-style exports;
extract it to a shared `detectUtf16` util and run it in the sniffer before
the null-byte check, decoding with the matching variant for the printable
ratio test instead of declaring the file binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(local-system): render WriteFile new files as a unified diff

Switch the WriteFile render from a syntax-highlighted preview to a
synthesized "new file" unified diff via PatchDiff, matching the
EditLocalFile visual. Markdown files keep their rendered preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(local-system): exercise readFile / readFiles end-to-end

The previous LocalFileCtr.readFile / readFiles tests deep-mocked
node:fs/promises and @lobechat/file-loaders. Since the controller is a
thin pass-through to readLocalFile, the assertions ended up testing
shell internals (already covered in packages/local-file-shell), and
broke as soon as readLocalFile gained new pre-flight checks.

Move them into a sibling LocalFileCtr.readFile.test.ts that runs
against a real tmpdir + real file-loaders, so adding more upstream
guards no longer requires touching this suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:08 +08:00
YuTengjing 521566bdb7 feat: add user activity business hook (#14601) 2026-05-13 02:57:08 +08:00
Hardy ab7b9e3e69 ♻️ refactor(siliconcloud): sync models with API, fix duplicates, adjust reasoning params (#14464)
* ♻️ refactor(siliconcloud): sync models with API, fix duplicates, adjust reasoning params

* 🐛 fix(siliconcloud): fix GLM-4.7 checkModel casing to match model ID
2026-05-13 02:57:08 +08:00
AmAzing- fa55b3fb25 🌐 i18n: update banner copy translations (#14623) 2026-05-13 02:57:08 +08:00
AmAzing- e300766046 💬 i18n: remove trailing punctuation from banner titles (#14622) 2026-05-13 02:57:08 +08:00
YuTengjing 9b032f0773 feat: add Gemini 3.1 Flash-Lite provider cards (#14604) 2026-05-13 02:57:08 +08:00
YuTengjing 629213189b ♻️ refactor: remove model extend param options (#14607) 2026-05-13 02:57:08 +08:00
René Wang f38f0c258b 📝 docs: add intro and screenshot to task scheduler changelog (#14585) 2026-05-13 02:57:07 +08:00
Neko 38b793f41b 🐛 fix(database,utils,userMemories): should perfer to use paradedb.match(...) instead of hardcoded normalizer (#14590) 2026-05-13 02:57:07 +08:00
Arvin Xu 11ec59b8c8 🐛 fix(database): attach error listeners to Neon/Node pools to prevent Lambda crash (#14606)
* 🐛 fix(database): attach error listeners to Neon/Node pools to prevent Lambda crash

NeonPool (and NodePool) inherit pg.Pool semantics: when a backend connection
drops on an idle client the pool emits 'error'. With no listener Node
escalates that into uncaughtException — on Vercel this killed the entire
Lambda process (exit 129) and produced a 1805-crash avalanche in 5 minutes,
spiking Neon connection count from 30 to 330+ as half-closed sockets
accumulated (LOBE-8704).

Primary fix: attach `.on('error', ...)` to both pool variants in
`packages/database/src/core/web-server.ts` so the error is logged but
swallowed; the pool recovers on its own per pg docs.

Defense in depth: register `uncaughtException` / `unhandledRejection`
handlers in `instrumentation.ts` (gated to nodejs runtime) so any future
unhandled error doesn't take down the process either.

Refs: https://node-postgres.com/apis/pool#error

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔧 chore: drop process-wide uncaughtException handler

Per review on #14606: the catch-all listener in instrumentation.ts swallowed
every uncaughtException / unhandledRejection — not just NeonPool errors —
leaving the process in an undefined state instead of letting the platform
restart it, and would mask future production bugs.

LOBE-8704 is fully addressed by the targeted pool listeners in
packages/database/src/core/web-server.ts; the broad backstop is unnecessary
and unsafe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
sxjeru 867e22a90e 💄 style: Add new DeepSeek-V4 models (#14110)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-13 02:57:07 +08:00
Arvin Xu 4bfd434552 🐛 fix: gateway client-tool pluginState + drop redundant Exit code: 0 tail (#14596)
* 🐛 fix(agent-runtime): forward pluginState through gateway client tool result

Gateway-mode client tool results lost the `state` field at three points:
the toolResult Zod schema didn't declare it (silently stripped by safeParse),
the ToolResultPayload interface didn't carry it, and projectToExecutionResult
didn't return it. As a result the "技能状态" tab was always empty for tools
dispatched via Agent Gateway, even though clients send `state` correctly and
non-gateway paths persist it as `pluginState`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(prompts): suppress redundant `Exit code: 0` tail in command result

For successful runs, "Command completed successfully." already conveys
the same signal — appending "Exit code: 0" was just noise the LLM had
to skim past. Non-zero exit codes (130 SIGINT, 137 OOM, etc.) keep the
line so the diagnostic information remains available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(prompts): treat non-zero exit code as command failure in result header

`success` is the envelope ("the service responded") and `exitCode` is the
command's own status — they're independent. With `success: true` +
`exitCode: 137` the prior format rendered "Command completed successfully."
on top of a SIGKILL/OOM, lying to the LLM.

Now the header is derived from both: any non-zero exit folds the message
into the failure branch as "Command failed with exit code N[: error]".
The trailing "Exit code: N" line is gone — the same info now lives in the
header, so success rendering is also free of the redundant zero tail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
sxjeru 307cd8e523 🐛 fix(gemini): handle zero cachedContentTokenCount in usage conversion (#14567)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-13 02:57:07 +08:00
Arvin Xu a2750098f4 💄 style(topic): add copy session ID to topic dropdown menu (#14595)
 feat(topic): add copy session ID to topic dropdown menu

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
Arvin Xu 12e37f1e46 feat: home daily brief with linkable welcome + paired input hint (#14589)
*  feat: home daily brief with linkable welcome + paired input hint

Add a per-user "daily brief" surface to the home page. A cron-driven
backend (in the cloud repo) writes paired { welcome, hint } entries
into Redis under `aiGeneration:home_brief:{userId}`. This change exposes
that data through:

- `RedisKeys.aiGeneration.homeBrief` key builder
- `home.getDailyBrief` lambda router query that reads the cached payload
- `homeService.getDailyBrief` client and `useHomeDailyBrief` hook with
  shared rotating index via `useSyncExternalStore`
- `WelcomeText` runs a custom typewriter (supports real `\n` line breaks
  and parses inline `[label](url)` markdown links so cached entity
  references become clickable; falls back to the i18n welcome list)
- `InputArea` shows the matching hint as the chat input placeholder

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract daily-brief Redis read into HomeService

Mirrors the AgentService pattern: the lambda home router was reaching
into Redis directly, which mixed I/O concerns with the routing layer.
Move the read into a dedicated `HomeService` so future home-page reads
have a clear home and the router stays thin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix: keep WelcomeText typewriter index in sync with shared store

Before: DailyTypewriter held its own `sentenceIndex` state, separate
from the module-level `currentIndex` in `useHomeDailyBrief`. After
the home page rotated past the first pair, navigating away and back
remounted the typewriter and reset its local index to 0 — but the
external index stayed where it was. InputArea read the hint at the
stale external index while WelcomeText restarted at pair 0, breaking
the welcome / hint pairing.

Make the typewriter fully controlled: drop the local `sentenceIndex`,
expose `currentIndex` from `useHomeDailyBrief`, and pass it as a prop.
On `pause`, the typewriter just calls `onSentenceComplete` — the
parent flips the shared index, the new prop flows back, the reset
effect re-arms typing for the new sentence. Single source of truth,
remount-safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(redis): factor JSON cache reads into getJSONFromRedis util

Three call sites were inlining the same "fetch + null-check + JSON.parse
+ try/catch" recipe against a scoped Redis client:

- AgentService.getAgentWelcomeFromRedis
- HomeService.readDailyBriefFromRedis (new)

Move the recipe into a small `getJSONFromRedis<T>` helper next to the
other Redis utilities and have both services delegate to it. Caller
keeps responsibility for resolving the right scoped client (we don't
want to hide the prefix selection inside the helper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use live editor content for Enter-to-send guard

When typing into the home input and pressing Enter immediately, the
empty-message guard sometimes wrongly bailed out. The cause: the guard
read the cached `inputMessage` in `useChatStore`, which is populated by
the editor's async `onMarkdownContentChange`. Lexical commits its
update on a microtask after each keystroke, so a fast type-then-Enter
fires the send path before the cache catches up.

`SendButtonHandler` already passes `getMarkdownContent` through — read
it instead, falling back to the cached value if the handler is invoked
without it. Also propagate the live message into all `inputActiveMode`
branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(home): accept daily-brief hint as the message on empty Enter

Press Enter on the empty home input → send the currently displayed
daily-brief hint as the message (smart-compose / Tab-to-accept style).
Trims the cosmetic trailing ellipsis and rotates the carousel so the
next press picks up a different pair.

Falls through to the previous "no content, skip" path when there's
neither a typed message nor a hint to use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): scope daily-brief SWR key + rotation index by userId

The SWR key was a constant string, so an account switch within the same
SPA session — sign out + sign in as another user, or a multi-account
swap that keeps `isSignedIn` true — could surface the previous user's
cached pairs from the same slot. The keyspace in Redis is per-user,
so the served data leaks personalization.

Include the resolved userId in the SWR key, and reset the module-level
rotation index on user change so the new account starts from pair 0
rather than inheriting a stale offset (which could also point past the
end of a smaller pairs list).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
LiJian 09c66ffb4c 🐛 fix: first inject the cloudecc runtime session should use the existingStatus (#14592)
* 🐛 fix: skip reconnect when gateway action already established a connection

Race condition on new-topic first message:
1. switchTopic loads runningOperation → useGatewayReconnect fires
2. executeGatewayAgent calls connectToGateway (status: connecting)
3. reconnectToGatewayOperation overwrites with resumeOnConnect:true
4. Gateway sees resume on a brand-new session → no events → stuck

Second message works because the client store's runningOperation is
stale (from the first op), so SWR deduplications and no reconnect fires.

Fix: bail out of reconnectToGatewayOperation if gatewayConnections
already shows connecting/connected for that operationId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: always pass --cwd /workspace for cloud CC to ensure session resume

CC stores session files at ~/.claude/projects/<encoded-cwd>/.
Without an explicit --cwd the actual working directory can differ
between sandbox invocations, so --resume <heteroSessionId> fails
to locate the previous session files even though the container is
persistent and the ID is correctly stored in topic.metadata.

Default cwd to /workspace for cloud runs (desktop keeps its own
explicit path), guaranteeing a stable session-file location across
page reloads within the same sandbox lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: extend reconnect guard to cover all in-flight connection statuses

The previous guard only skipped reconnect for 'connecting'/'connected'
but the connection can already be in 'authenticating' or 'reconnecting'
by the time useGatewayReconnect fires, leaving the race window open.

Flip the condition: skip for any status that is not 'disconnected'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: restore cold replica state in HeterogeneousPersistenceHandler

Vercel serverless functions are stateless per-request, so `operationStates`
is empty on every `heteroIngest` call. loadOrCreateState always cold-creates.

#14539 fixed `toolMsgIdByCallId` restoration but left `accumulatedContent`,
`toolState.payloads`, and `toolState.persistedIds` empty on cold load,
causing two bugs:

- Content truncation: cold instance starts with `accumulatedContent=''`,
  accumulates only the current batch's text, then writes that shorter string
  on the next step boundary or terminal — overwriting the longer content the
  previous write had already stored in DB.

- Tool duplication / tools[] overwrite: `persistedIds={}` on cold load
  means every `tools_calling` event re-creates already-persisted tool
  messages, and `payloads=[]` means phase 1/3 writes only the current
  batch's tools, wiping previous tools from `assistant.tools[]`.

Fix: in `loadOrCreateState`, fetch the current assistant message and restore
`accumulatedContent`, `accumulatedReasoning`, `toolState.payloads`, and
`toolState.persistedIds` from it. Cold load is now equivalent to warm load.

Also adds two regression tests covering the cold-replica scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
Arvin Xu 909b1ec461 💄 style: use visible divider between queued messages (#14593)
💄 style(QueueTray): use visible divider color between queued messages

The previous `colorBorderSecondary` rendered the divider effectively
invisible on the elevated dark surface. Switch to `colorFillTertiary`
so stacked queued messages have a perceptible separator.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
Rdmclin2 8274be0d1d 🐛 fix: slack connect error & slash commands (#14591)
* feat: displayToolCalls default undefined

* chore: restrict billboard to home page

* fix: add slack bot scope

* fix: show billboard in home nav
2026-05-13 02:57:07 +08:00
Neko b7a50206bf feat(agent-signal,prompts,database): self-review now proposal actions to briefs, and automatically execute actions (#14583) 2026-05-13 02:57:07 +08:00
Innei 5c1113031d 💄 style(intervention): polish confirmation bar layout (#14587) 2026-05-13 02:57:07 +08:00
AmAzing- fa17c75f90 chore: Refine homepage banner copy for channels and skills (#14588) 2026-05-13 02:57:07 +08:00
AmAzing- 0c659dbe22 🛠️ fix: unify SKILL.md frontmatter parsing and edit validation in agent documents (#14566) 2026-05-13 02:57:07 +08:00
LiJian d2c379c78d feat: add signOperationJwt with 4h expiry for hetero-agent operations (#14586)
*  feat: add signOperationJwt with 4h expiry for hetero-agent operations

- Add `signOperationJwt(userId)` to internalJwt.ts with 4h expiry and
  `purpose: 'hetero-operation'`, so Claude Code / Codex tasks running
  beyond 5 minutes no longer hit 401 on heteroIngest / heteroFinish
- Update `execAgent` hetero path to use `signOperationJwt` instead of
  `signUserJWT`; gatewayToken continues to use 5m `signUserJWT`
- Add unit tests in `__tests__/internalJwt.test.ts` with correct mocks
  for `jose` (SignJWT class + importJWK) and `authEnv`, covering all
  three signing functions and the expiry difference assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔒 security: restrict hetero-operation JWT scope to heteroIngest/heteroFinish

A leaked 4-hour sandbox LOBEHUB_JWT must not be replayable against any
other authenticated lambda route.

- Forward `purpose` claim from JWT payload through validateOIDCJWT →
  tokenData → oidcAuth context so middlewares can inspect it
- oidcAuth: reject tokens with purpose 'hetero-operation' — they cannot
  reach any normal authedProcedure route
- New heteroOperationAuth middleware: exclusively accepts
  purpose 'hetero-operation' tokens, rejects all others
- Export heteroAuthedProcedure (baseProcedure + heteroOperationAuth +
  userAuth) from trpc/lambda/index.ts
- heteroIngest / heteroFinish now use heteroAgentProcedure built on
  heteroAuthedProcedure + serverDatabase + HeterogeneousAgentService
- Tests: heteroOperationAuth (4), oidcAuth (4), update heteroIngest
  test caller to supply purpose:'hetero-operation' context (23 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:07 +08:00
Innei d73de25623 💄 style(settings): remove image avatar from lab input markdown rendering item (#14582) 2026-05-13 02:57:07 +08:00
YuTengjing a02ecbc40d 🐛 fix: polish task agent manager (#14569) 2026-05-13 02:57:07 +08:00
AmAzing- f1f2e58e01 feat: migrate Notion to LobeHub Market (#14578)
Migrate Notion to LobeHub Market
2026-05-13 02:57:06 +08:00
Arvin Xu 5f8ec8bbfb 🐛 fix(agent-runtime): recover malformed tool_call names instead of finishing silently (#14577)
* 🐛 fix(agent-runtime): recover malformed tool_call names instead of finishing silently

When an LLM emits tool_call names without the `____` separator (e.g. `activateTools`
instead of `lobe-activator____activateTools`), the resolver dropped them silently and
the harness finished with "completed without tool calls" — empty assistant bubble,
no error in dashboards.

Three layers of defense:

- Resolver fallback: when the bare name uniquely matches an API across known
  manifests, recover the identifier; ambiguous matches still drop to avoid
  false binding.
- StreamingHandler logs unresolved tool_call names so the silent-drop path is
  observable in debug output.
- GeneralChatAgent surfaces the unresolvable count and names in reasonDetail
  so dashboards can distinguish this from a genuine no-tool completion.

Fixes LOBE-8696

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): restrict bare-name fallback to tools offered this turn

Address review feedback on the LOBE-8696 resolver fallback. The
manifests map passed to ToolNameResolver.resolve is broader than the
tools actually sent to the LLM (the client builds it from every
installed plugin and every builtin; the server can preserve manifests
even after a step deactivates a tool). Without a turn-scope
restriction:

- A model returning a malformed bare name could resolve to a tool that
  was not enabled for this turn.
- A disabled duplicate API name could shadow the enabled call and make
  it look ambiguous, dropping a valid call.

Pipe an `offeredToolNames` list (the names actually sent in this LLM
payload) into resolve(): when set, the missing-prefix fallback only
considers manifests whose generated tool name appears in the list.

- ToolNameResolver.resolve gains an optional `offeredToolNames` param.
- internal_transformToolCalls forwards the list through.
- createAgentExecutors builds resolvedAgentConfig before the
  StreamingHandler so the closure can bind the offered names — same
  list that gets sent to the model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:06 +08:00
LiJian 7792f63453 feat: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context (#14568)
*  feat: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context

- Add CloudRepoSwitcher component (web-only multi-select repo picker)
  - Pre-topic selections buffered in module singleton (pendingTopicRepos)
  - Consumed by gateway.ts at topic creation time via appContext.initialTopicMetadata
  - Eliminates race condition where updateTopicMetadata dropped silently
- Extend ChatTopicMetadata with repos[] field for multi-repo binding
- Add initialTopicMetadata to ExecAgentAppContext so repos are written to
  topic metadata at creation time (server-side, zero race condition)
- Extend ExecAgentSchema Zod schema with initialTopicMetadata
- Inject GITHUB_TOKEN env var into sandbox so CC can use git/gh CLI
- Build cloudHeteroContext with GitHub auth section when token is available
- Add workingDirectory selector for web (repos[0] fallback)
- Add refreshTopic call in gateway path after new topic creation
- Add CloudHeterogeneousConfig profile editor for GITHUB_REPOS / GITHUB_CRED_KEY
- Extend sandboxRunner with repo clone setup script and systemContext support

* 🐛 fix: add open-source stub for pendingTopicRepos to fix Vite build

* ♻️ refactor: move pendingTopicRepos real impl into submodule, remove cloud override

* 🐛 fix: consume pendingTopicRepos only after topic creation succeeds

* 🐛 fix: add missing getPendingTopicRepos import in gateway

* 🔒 fix: address security and dead-code issues from PR review

- sandboxRunner: sanitize repo dir name to prevent shell injection
- sandboxRunner: use git insteadOf (-c flag) so token is never stored in .git/config
- cloudHeteroContext: fix return type from string|undefined to string (dead branch)
- CloudRepoSwitcher: remove unreachable empty-list branch in popover content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💬 i18n: add claude setup-token hint to token description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: remove incorrect web hetero→gateway forced routing in agentDispatcher

On web, heterogeneousProvider is ignored — routing falls through to isGatewayMode.
Cloud CC only runs when gateway mode is enabled; gateway.ts handles sandbox
spawning when it detects a hetero provider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: restore web hetero→gateway routing; update stale test

On web, a configured heterogeneousProvider always routes to gateway —
the cloud sandbox is the only execution environment regardless of
isGatewayMode. The test assumed the pre-cloud-CC world where web
ignored hetero providers entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 02:57:06 +08:00
Innei 2959ec3883 📝 docs(version-release): enforce git-derived PR refs and metrics (#14575)
* 📝 docs(version-release): enforce git-derived PR refs and metrics

Add the skill's first-class hard rules for computing release-note inputs
from git instead of memory: latest-tag base via `git describe`, PR refs
from commit subjects, metric counts from `wc -l`, handle resolution via
`gh pr view`, and a pre-publish `comm -23` diff that must be empty.
Also adds @cy948 to the team roster and notes Tsuki / René Wang's
commit-author aliases so contributor classification stops drifting.

* ♻️ refactor(version-release): split skill into router + per-flow references

SKILL.md was 426 lines covering three distinct flows. Split it so each
flow lives next to its own checklist:

- reference/minor-release.md — minor workflow (lifted from SKILL.md)
- reference/patch-release-scenarios.md — patch flows (existing)
- reference/release-notes-style.md — long-form changelog standard,
  template, and Computing Inputs hard rules (lifted from SKILL.md)

SKILL.md now reads as a router (~100 lines) with shared CI trigger
rules, post-release automation, precheck, and hard rules. Cross-links
between references replace the previous in-file jumps. Also fixes a
prettier-mangled redirect (`< some-pr-by-them >`) by using a `$PR`
variable instead of an angle-bracket placeholder.

* 📝 docs(version-release): add Hotfix and DB Migration variants to release-notes-style

The Canonical Structure was implicitly long-form (Minor / Weekly), and
hotfix authors had to read `changelog-example/hotfix.md` to learn it
existed. Make the divergence explicit:

- New § Variants for Shorter Releases describes Hotfix structure
  (Scope / What's Fixed / Upgrade / Owner) and DB Migration structure
  (Migration overview / Operator impact / Rollback) as overrides of the
  canonical long-form layout.
- Renamed the canonical section to "Canonical Structure (Long-Form:
  Minor / Weekly)" so the boundary is visible.
- Added Hotfix entry to Release Size Heuristics.
- Added a Hotfix subsection to Quick Checklist so the verification
  gates differ from long-form (no metric line / no Contributors / Owner
  resolved via gh).
2026-05-13 02:57:06 +08:00
YuTengjing 181b7eb117 🐛 fix: remove signin captcha flow (#14573) 2026-05-13 02:57:06 +08:00
YuTengjing 2bdd901ce2 🐛 fix: add temporary email auth error locale (#14564) 2026-05-13 02:57:06 +08:00
Rdmclin2 e4b5e52aff 🐛 fix: add bot callback service (#14570)
fix: add bot callback service
2026-05-13 02:57:06 +08:00
LiJian 1a6e07b5ef 🐛 fix: sanitize sensitive comments and examples from production JS bundle (#14557)
* 🐛 fix: sanitize sensitive comments and examples from production JS bundle

- Replace app.example.com with RFC 2606 example.com in agent-browser skill content
- Replace password-stdin examples with interactive auth prompts
- Remove hardcoded password-like strings from code examples
- Reword flagged code comments in page-agent system role

Addresses TAC Security CASA Tier 2 DAST Info findings:
Information Disclosure - Suspicious Comments (CWE-615)

The flagged strings appeared in SPA production bundles:
- /_spa/assets/chat-*.js
- /_spa/assets/index-*.js

* 🐛 fix: revert --interactive to --password-stdin in auth vault examples

The --interactive flag does not exist in agent-browser CLI (only --password
and --password-stdin are supported). Using --interactive would cause auth
save to fail and block login workflows.

Reverted both auth vault examples to use echo | --password-stdin pattern,
which pipes the password via stdin — the recommended secure approach.
2026-05-13 02:57:06 +08:00
Arvin Xu a7cc553212 💄 style(task): activity card stop run + register /tasks in SPA proxy (#14559)
*  feat(task): add stop run action to activity card menu

Surface the existing cancelTopic flow in the task detail activity card so
users can interrupt a running topic without opening the chat drawer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(task): confirm before stopping a running topic

Wrap the new Stop run action in a confirmModal so an accidental click can't
silently abort an in-flight run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(spa): register /tasks and /task in SPA proxy matcher

Without these matcher entries, the Next.js middleware never rewrote /tasks
and /task/:taskId to the SPA catch-all, so the activity feed entries 404'd
in production builds even though the routes were wired in the SPA router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:57:06 +08:00
YuTengjing c208723904 💄 style: update auth captcha retry copy (#14561) 2026-05-13 02:57:06 +08:00
Rdmclin2 760a342557 🐛 fix: multiple account link (#14562)
* feat: avoid rebind link same account

* chore: update i18n locales

* feat: avoid discord account misslink

* feat: support slack account mis match

* fix: avoid claim conflict
2026-05-13 02:57:06 +08:00
Arvin Xu ce08b9b116 feat(agent-runtime): persist agent operations to agent_operations table (#14736)
*  feat(agent-runtime): persist agent operations to `agent_operations` table

Wire start-time INSERT and terminal UPDATE into the agent runtime so
operation history outlives the 2-hour Redis TTL. Adds
`AgentOperationModel` with `recordStart` / `recordCompletion` /
`findById` (scoped by userId so a leaked operationId can't flip another
user's row) and threads both calls through `CompletionLifecycle`, which
now owns both ends of the persistence lifecycle. Also plumbs
`parentOperationId` through `ExecAgentParams` → `OperationCreationParams`
so sub-agent invocations carry their parent lineage. Per-step aggregate
updates are intentionally out of scope.

Refs LOBE-8848

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): update CompletionLifecycle test constructor to 2 args

CompletionLifecycle now constructs MessageModel internally from
(db, userId), so the test builder passing a third messageModel arg
tripped tsgo --noEmit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:53:35 +08:00
Rdmclin2 efa57ad4ab feat: support slack mpim and fix discord dm problem (#14733)
* feat: support mpim

* chore: add errorMsg

* fix: discord commands thinking error

* fix: discord typing error

* feat: add oauth process for discord
2026-05-13 00:55:25 +07:00
Arvin Xu 844f885b60 🐛 fix(hetero-agent): wire AskUserBridge response events to renderer (#14732)
Close the wire-protocol gap that left CC's AskUserQuestion form stuck on
"pending" after the bridge gave up. AskUserBridge now emits an
agent_intervention_response event on every terminal path (timeout,
user resolve, cancel, cancelAll), and heterogeneousAgentExecutor handles
it by stamping pluginIntervention.status = 'rejected' for timeout /
session_ended (user-driven paths are filtered out — already optimistic).

Layered defenses so a late Submit no longer throws "Operation not found":
- cleanupCompletedOperations: find→filter so every messageOperationMap
  entry pointing to the cleaned op is removed (assistant + tool message
  pairs previously stranded one entry as a dangling reference).
- internal_getConversationContext: log + fall back to global state when
  the op has been GC'd, instead of throwing.
- submitHeteroIntervention: detect a stale opId before passing it into
  the optimistic chain.

Scoped as a short-term backstop until LOBE-8746 retires the AskUser MCP
bridge entirely.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:46:32 +08:00
Arvin Xu ccddbaa25d ♻️ refactor(builtin-tool): move sub-agent dispatch from lobe-gtd to lobe-agent (#14715)
* ♻️ refactor(builtin-tool): move sub-agent dispatch from lobe-gtd to lobe-agent

Move the `execTask` / `execTasks` capability out of `packages/builtin-tool-gtd/`
and into `packages/builtin-tool-lobe-agent/`, renaming the public APIs to
`callSubAgent` / `callSubAgents`. The "subtask" naming inside GTD overlapped
with the new lobe-task tool's task model and conflated planning with
sub-agent dispatch.

- API names: `execTask` → `callSubAgent`, `execTasks` → `callSubAgents`
- TS types: `ExecTaskParams` → `CallSubAgentParams`, etc.; introduce
  `SubAgentTask` to replace `ExecTaskItem`
- Client UI (Inspector / Render / Streaming) ported under
  `packages/builtin-tool-lobe-agent/src/client/`
- Central registries (`packages/builtin-tools/src/{inspectors,renders,streamings}.ts`)
  updated to register lobe-agent
- GTD `meta.description` and system role no longer mention async tasks;
  they point to lobe-agent for sub-agent dispatch
- `isSubTask` filtering in `agentConfigResolver` now excludes `lobe-agent`
  (new owner of sub-agent dispatch) instead of `lobe-gtd`
- i18n: new `builtins.lobe-agent.apiName.callSubAgent*` and
  `workflow.toolDisplayName.callSubAgent*` keys in default/zh-CN/en-US

Kept the executor's emitted `state.type` values (`execTask` / `execTasks` /
`execClientTask` / `execClientTasks`) unchanged so the agent-runtime
instruction layer (`exec_task` / `exec_tasks` / `exec_client_task*`) and all
downstream tests / heterogeneous executors (`builtin-tool-agent-management`,
server `agentManagement` runtime) continue to work without modification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(chat): rename isSubTask flag to isSubAgent

After moving sub-agent dispatch from lobe-gtd to lobe-agent, the flag name
no longer matches what it controls. Rename `isSubTask` → `isSubAgent` across
the chat / agent runtime layer and update related comments and test labels.

- `agentConfigResolver` context field + filter helper
- `streamingExecutor.internal_createAgentState` + `executeClientAgent`
  signatures and call sites
- `createAgentExecutors` (exec_task / exec_client_task handlers) and
  `GroupOrchestrationExecutors` (batch_exec_async_tasks)
- `chatService.createAssistantMessageStream` `resolvedAgentConfig` docs
- Test descriptions and assertions in `agentConfigResolver.test.ts` and
  `streamingExecutor.test.ts`

No behavior change — the flag's filter target (`lobe-agent` identifier) is
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): rename exec_task wire identifiers to exec_sub_agent

Bring the agent-runtime "wire" naming in line with the lobe-agent
callSubAgent / callSubAgents API rename. Three layers are renamed in lockstep
to keep the bridge between tool executors and the runtime consistent:

1. Tool-emitted state.type discriminators
   - 'execTask' → 'execSubAgent'
   - 'execTasks' → 'execSubAgents'
   - 'execClientTask' → 'execClientSubAgent'
   - 'execClientTasks' → 'execClientSubAgents'

2. AgentInstruction.type and matching TS interfaces
   - 'exec_task' / 'exec_tasks' / 'exec_client_task' / 'exec_client_tasks'
     → 'exec_sub_agent' / 'exec_sub_agents' / 'exec_client_sub_agent' /
       'exec_client_sub_agents'
   - AgentInstructionExecTask → AgentInstructionExecSubAgent (and the three
     siblings)
   - ExecTaskItem → SubAgentTask

3. AgentRuntimeContext.phase + matching payload types
   - 'task_result' → 'sub_agent_result'
   - 'tasks_batch_result' → 'sub_agents_batch_result'
   - TaskResultPayload → SubAgentResultPayload
   - TasksBatchResultPayload → SubAgentsBatchResultPayload

Also renames the operation-type discriminator 'execClientTask' /
'execClientTasks' to 'execClientSubAgent' / 'execClientSubAgents' and updates
its locale string in default / zh-CN / en-US.

Tests / fixtures / mocks updated in lockstep:
- packages/agent-runtime/src/agents/{GeneralChatAgent.ts,__tests__/...}
- packages/builtin-tool-{lobe-agent,agent-management}/src/...
- src/server/services/toolExecution/serverRuntimes/agentManagement.ts
- packages/agent-mock/src/cases/builtins/todo-write-stress.ts (helper renamed
  to callSubAgent)
- src/store/chat/agents/createAgentExecutors.ts + exec-task / exec-tasks tests
  + fixtures/mockInstructions.ts (createExecSubAgent[s]Instruction)
- src/store/chat/slices/aiChat/actions/streamingExecutor.ts (phase check)
- packages/conversation-flow/src/__tests__/fixtures/**/*.json (8 fixtures
  retargeted from lobe-gtd/execTask[s] to lobe-agent/callSubAgent[s] with the
  new state.type wire values)

No behavior change — the agent runtime, executors and tests all go through
the same code paths; only the strings on the wire change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool): absorb GTD tool (plan + todo) into lobe-agent

Delete `packages/builtin-tool-gtd/` and fold its full surface — plan, todo,
ExecutionRuntime, all client UI (Inspector / Render / Streaming /
Intervention / SortableTodoList) and the system role — into
`packages/builtin-tool-lobe-agent/`. Single `lobe-agent` identifier now
owns: plan + todo management, sub-agent dispatch, and visual media analysis.

Also restructures the lobe-agent package so the executor lives under
`./client/` alongside the UI it ships with, and drops the dedicated
`./executor` export — consumers go through `./client` for everything
client-side.

Package-level changes:
- DELETE `packages/builtin-tool-gtd/` entirely.
- `packages/builtin-tool-lobe-agent/`
  - Move `src/executor/` → `src/client/executor/`. Drop `./executor` from
    `package.json` exports; expose `lobeAgentExecutor` via `./client` only.
  - Rename `GTDExecutionRuntime` → `PlanExecutionRuntime` and place under
    `src/client/executor/PlanRuntime/`. Re-export from package root so the
    server runtime can consume it without pulling in client UI deps.
  - Extend `LobeAgentExecutor` with `createPlan` / `updatePlan` /
    `createTodos` / `updateTodos` / `clearTodos`, all delegated to the
    shared runtime.
  - Add Plan + Todo API entries to the manifest (with their original
    descriptions, humanIntervention, renderDisplayControl).
  - Move all GTD client UI verbatim:
    `Inspector/{ClearTodos,CreatePlan,CreateTodos,UpdatePlan,UpdateTodos}`,
    `Render/{CreatePlan,TodoList}`, `Streaming/CreatePlan`,
    `Intervention/{AddTodo,ClearTodos,CreatePlan}`,
    `components/SortableTodoList`. Register them in
    `LobeAgentInspectors / Renders / Streamings`, add new
    `LobeAgentInterventions`.
  - Merge GTD system role into lobe-agent's (`<plan_and_todos>` plus the
    existing `<sub_agents>` and `<run_in_client>` sections).
  - `package.json`: pick up `@lobechat/prompts` dep and `@lobehub/editor` +
    `antd` + `lucide-react` peer-deps inherited from GTD.

Central registries (`packages/builtin-tools/src/*`) and consumers:
- Remove every `GTDManifest / Inspectors / Renders / Streamings /
  Interventions` import + registration; existing `LobeAgent*` registrations
  now cover them.
- Replace `[GTDManifest.identifier]: GTDInterventions` with
  `[LobeAgentManifest.identifier]: LobeAgentInterventions`.
- Drop `@lobechat/builtin-tool-gtd` workspace dep from
  `packages/builtin-tools/package.json`, `packages/builtin-agents/package.json`
  and root `package.json`.
- Remove `gtdExecutor` from `src/store/tool/slices/builtin/executors/index.ts`;
  switch `lobeAgentExecutor` import to `/client`.
- Replace `serverRuntimes/gtd.ts` with a service factory
  `serverRuntimes/lobeAgentPlan.ts` (`createServerPlanRuntimeService`).
  `serverRuntimes/lobeAgent.ts` instantiates `PlanExecutionRuntime` with
  that service so the registry exposes one runtime per `lobe-agent`
  identifier covering both visual analysis and plan/todo.
- `services/chat/mecha/contextEngineering.ts`: gate plan/todo injection on
  `LobeAgentIdentifier` instead of `GTDIdentifier`.
- `agentConfigResolver.test.ts`: switch fixture plugin IDs to
  `LobeAgentIdentifier`.
- `packages/const/src/recommendedSkill.ts`: drop the standalone `lobe-gtd`
  recommendation — `lobe-agent` already covers it via `defaultToolIds`.

i18n migration (default + zh-CN + en-US; other locales regenerate on
`pnpm i18n`):
- `builtins.lobe-gtd.*` → `builtins.lobe-agent.*` in `plugin.ts/json`.
- `lobe-gtd.*` (tool namespace) → `lobe-agent.*` in `tool.ts/json`.
- Remove `tools.builtins.lobe-gtd.{description,readme,title}` from
  `setting.ts/json` (lobe-agent has its own meta now).
- Update all client component `t(...)` keys to the new namespace.

Mocks / fixtures / tests:
- `packages/agent-mock/src/cases/builtins/todo-write-stress.ts`: all
  `identifier: 'lobe-gtd'` → `'lobe-agent'`; helper comments updated.
- `packages/types/src/stepContext.ts`: comment refers to
  `builtin-tool-lobe-agent` (the only consumer of `StepContextTodoItem`).
- `packages/model-runtime/src/core/streams/google/google-ai.test.ts`:
  function-call names from `lobe-gtd____createPlan` etc. → `lobe-agent____*`.
- `src/store/chat/slices/message/selectors/dbMessage.test.ts`: same.
- `src/features/DevPanel/RenderGallery/fixtures/lobe-gtd.ts` deleted; its
  plan/todo fixtures are folded into `fixtures/lobe-agent.ts` alongside the
  existing `callSubAgent[s]` ones.
- Replace `console.log` → `console.info` in moved client components to
  satisfy lobe-agent's stricter ESLint rules (GTD package allowed
  `console.log`; lobe-agent inherits the repo-wide `no-console` rule).

No behavior change for end users: `lobe-agent` now owns all the APIs,
identifiers, and UI that previously lived in `lobe-gtd`, but as a single
consolidated package under a single tool identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(context-engine): drop residual GTD naming, rename to PlanInjector / TodoInjector

Follow-up to 9ca5c9d (which absorbed the GTD tool package into lobe-agent).
That commit moved the package surface but left the GTD vocabulary embedded
in context-engine providers, types, metadata fields, XML tags, and a pile
of comments. This change finishes the sweep so the only remaining GTD
references are user-facing docs and the legitimate Productivity & GTD Coach
methodology suggestion.

context-engine
- `GTDPlanInjector` → `PlanInjector`; types `GTDPlan`/`GTDPlanInjectorConfig`
  → `Plan`/`PlanInjectorConfig`; metadata `gtdPlanId`/`gtdPlanInjected` →
  `planId`/`planInjected`; XML tag `<gtd_plan>` → `<plan>`; debug channel
  `provider:GTDPlanInjector` → `provider:PlanInjector`.
- `GTDTodoInjector` → `TodoInjector`; types `GTDTodoItem`/`GTDTodoList`/
  `GTDTodoStatus`/`GTDTodoInjectorConfig` → `TodoItem`/`TodoList`/
  `TodoStatus`/`TodoInjectorConfig`; metadata `gtdTodo*` → `todo*`;
  XML tag `<gtd_todos>` → `<todos>`, wrapper `gtd_todo_context` →
  `todo_context`; debug channel renamed similarly.
- `MessagesEngineParams.gtd?: GTDConfig` → `planTodo?: PlanTodoConfig`;
  internal vars `isGTDPlanEnabled`/`isGTDTodoEnabled` →
  `isPlanEnabled`/`isTodoEnabled`. Re-exports updated in `providers/index.ts`
  and `engine/messages/{index,types}.ts`.

prompts
- `packages/prompts/src/prompts/gtd/` → `planTodo/` (only export was
  `formatTodoStateSummary`, which kept its name). Updated `prompts/index.ts`
  re-export.

src/services
- `contextEngineering.ts`: `GTDConfig` import → `PlanTodoConfig`;
  `isGTDEnabled`/`gtdConfig` → `isPlanTodoEnabled`/`planTodoConfig`; payload
  field `gtd` → `planTodo`; log message wording.

Tests
- `dbMessage.test.ts`: helper `createGTDToolMessage` →
  `createLobeAgentToolMessage`; `gtdMessage` → `lobeAgentMessage`; all `it`
  descriptions reworded to "lobe-agent" instead of "GTD".
- `agentConfigResolver.test.ts`: test descriptions reworded.

Comments / docs (no behavior change)
- agent-runtime (`instruction.ts`, `runtime.ts`, `generalAgent.ts`,
  `messageSelectors.ts`), `types/{stepContext,tool/builtin}.ts`,
  `builtin-agents/group-supervisor`, `builtin-tool-claude-code/types.ts`,
  `builtin-tool-lobe-agent/Render/TodoList`, `createAgentExecutors.ts:1426`,
  `AssistantGroup/{constants,Fallback.test}`, `agent-mock/todo-write-stress`,
  `.agents/skills/builtin-tool/references/architecture.md`.

Intentionally left alone
- `docs/usage/agent/gtd.{mdx,zh-CN.mdx}` and other docs — user-facing
  product brand "GTD Tools".
- `src/locales/default/suggestQuestions.ts` "Productivity & GTD Coach" —
  references the methodology, not the tool.
- `ToolSystemRoleProvider.test.ts` `'gtd-tool'` fixture — generic test
  identifier, unrelated.
- Translated locale files still carrying `lobe-gtd.*` keys — regenerated by
  `pnpm i18n` from the updated default namespace.

Verified: `bun run type-check` passes; touched test files
(dbMessage, agentConfigResolver) and full context-engine + prompts test
suites pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(builtin-tool-lobe-agent): reset TodoList auto-save status to idle

`performSave` (the debounced auto-save path) was leaving `saveStatus` stuck
on 'saved' forever — `saveNow` had the 1.5s setTimeout-to-idle but the
auto-save twin didn't, so the inline indicator never eased back to idle
after a settle. Add the same idle-reset to performSave so both paths
behave the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:13:04 +08:00
Arvin Xu 4ffce4fbbf 💄 style: use @lobehub/ui built-in HtmlPreview instead of custom component (#14703)
* 💄 style(home,i18n): use 已阅 for brief confirm/confirmDone in zh-CN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use 确认完成 for brief.action.confirmDone in zh-CN

confirmDone signals the terminal transition (task marked complete),
not just dismissing the brief, so 已阅 loses the semantic distinction
from `confirm`. Use 确认完成 to match the EN intent ("Confirm complete").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor: use @lobehub/ui built-in HtmlPreview instead of custom component

- Upgrade @lobehub/ui from ^5.10.1 to ^5.10.4
- Replace custom HtmlPreviewAction with lobe-ui's enableHtmlPreview
- Wire lobe-ui's onExpand callback to existing HtmlPreviewDrawer
- Remove HtmlPreviewAction.tsx (no longer needed)
- Keep HtmlPreviewDrawer for the expanded full-screen view

* 🐛 fix(task): sync useMarkdown destructuring with assistant MessageContent

* 🐛 fix(task): correct mangled search.X JSX expressions in MessageContent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(review): move revert icon to right edge of file 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>
2026-05-13 01:08:18 +08:00
LobeHub Bot 9da8ed0a6c 🌐 chore: translate non-English comments to English in src (#14654)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 00:54:54 +08:00
Arvin Xu e8ab37e5d4 🐛 fix(home): blank user bubble when sending the placeholder hint (#14678)
When the home input was empty and the user clicked send, `useSend`
correctly fell back to the daily-brief hint for `message`, but it also
forwarded `mainInputEditor.getJSONState()` as `editorData`. An empty
editor still returns a non-null JSON state (e.g. `{ type: 'doc' }`),
which makes `UserMessageContent.hasEditorData` truthy — so the renderer
took the RichTextMessage branch and drew nothing, while the agent
happily processed the hint text behind a blank user bubble.

Skip `editorData` when the hint is being used so the renderer falls
back to the markdown `content`. Adds a regression test.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:51:41 +08:00
Arvin Xu 9dff0acd36 feat(database): add agent_operations table (#14416)
 feat(database): add agent_operations table

Adds an `agent_operations` table to persist agent runtime operations
beyond the 2-hour Redis TTL. Each row captures one agent operation
(operationId) with denormalized cost/token aggregates, lifecycle
timestamps, runtime config snapshot, and a `trace_s3_key` pointer to
the full ExecutionSnapshot in S3.

- `user_id` is intentionally not a FK so operation history survives
  user deletion (auditable historical data).
- `agent_id` / `topic_id` / `thread_id` / `task_id` / `chat_group_id`
  use ON DELETE SET NULL to preserve operations when their parent
  entity is removed.
- `parent_operation_id` self-references for sub-agent (callAgent) ops.
- `human_interventions` and `human_waiting_time_ms` are nullable since
  most operations have no human interaction at all.
- Indexes optimize per-user listing and per-status / per-entity lookups;
  `metadata` has a GIN index for jsonb filters.
2026-05-13 00:51:03 +08:00
Innei 84c89f9c03 🐛 fix(conversation): prevent synthetic scroll from shrinking spacer (#14584)
🐛 fix: prevent synthetic scroll from shrinking spacer
2026-05-13 00:18:10 +08:00
Arvin Xu a5ea379079 ♻️ refactor(agent-runtime): extract CompletionLifecycle, HumanInterventionHandler, stepPresentation (#14441)
* ♻️ refactor(agent-runtime): extract CompletionLifecycle

Pull terminal-state handling out of AgentRuntimeService into a dedicated
class:

- buildLifecycleEvent (was buildCompletionLifecycleEvent)
- emitSignalEvents (was emitCompletionSignalEvents)
- dispatchHooks (was dispatchCompletionHooks)
- extractErrorMessage

These four methods formed one cohesive vertical: build the lifecycle
event payload, emit completion AgentSignal source events, dispatch
onComplete/onError hooks, and write error back onto the assistant
message row. extractErrorMessage was a private helper used by all three
plus by the trace-snapshot finalize call site, so it becomes a public
method on the class.

Call sites in executeStep / executeSync change from
`this.{emit|dispatch|extract...}` to `this.completionLifecycle.{...}`.

Tests: extractErrorMessage.test.ts → CompletionLifecycle.test.ts,
instantiating CompletionLifecycle directly instead of going through
AgentRuntimeService — drops a pile of unrelated mocks.

AgentRuntimeService.ts: 2084 → 1918 (-166).

All 81 agentRuntime tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): extract HumanInterventionHandler

Pull the 165-line `handleHumanIntervention` method out of
AgentRuntimeService into its own class, splitting the three branches
(approve / rejectAndContinue / rejectAndHalt) into private methods so
each fits in one screen. Routing in `process()` now reads top-to-bottom:
detect approval, then rejection, then unsupported humanInput.

The handler depends only on `serverDB` (for the messagePlugins lookup)
and `messageModel` (for tool/plugin updates) — much narrower than
AgentRuntimeService's full surface, so the extracted unit is easier to
unit-test in isolation.

Drop the unused `runtime: AgentRuntime` parameter from the public API:
the original method threaded it through but never called it.

Tests: handleHumanIntervention.test.ts → HumanInterventionHandler.test.ts
— same 17 cases, but instantiate the handler directly instead of
constructing a full AgentRuntimeService with 11 module mocks. Tighter
arrange step, same coverage.

AgentRuntimeService.ts: 1918 → 1742 (-176).

All 81 agentRuntime tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(agent-runtime): extract step presentation builder

Pull the ~150-line `phase`-branching block out of executeStep into a
pure `buildStepPresentation` function. The block did three things in
sequence: derive content/reasoning/toolsCalling/toolsResult from the
runtime step result, build a one-line stepSummary for logging, and
assemble the StepPresentationData DTO consumed by afterStep hooks /
snapshot recorder / callbacks.

The function takes only the stepResult and an executionTimeMs; no
service state needed. Comes with a `formatTokenCount` helper for the
log line (12345 → 12.3k, 2_500_000 → 2.5m).

executeStep keeps the log call inline (one line, references presentation
fields directly) and reads `content` / `toolsCalling` off presentation
for downstream tracking + truncation logic.

13 new unit tests: phase=tool_result (json + string + isSuccess paths),
phase=tools_batch_result, done event, llm_result with content/reasoning/
tools, empty fallback, cumulative usage zero-fallback, stepUsage
forwarding, and formatTokenCount edges.

AgentRuntimeService.ts: 1742 → 1601 (-141).

All 94 agentRuntime tests pass (was 81, +13 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:12:15 +08:00
Arvin Xu b9fb68464d 🐛 fix(task-card): localize task card date independent of dayjs global locale (#14730)
* 🐛 fix(task-card): localize date format independent of dayjs global locale

Task card was rendering "5月 12" under English UI because t('time.formatThisYear')
returned the English "MMM D" format, but dayjs's global locale was still zh-cn,
making MMM resolve to the Chinese short month name. Thread the i18n language
into formatTaskItemDate so the date is rendered with the same locale as the
format string, decoupling it from dayjs's global state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task-card): import missing GenericItemType + type Run now onClick

Pre-existing CI regression from #14727 surfacing on every PR: the Run now
context menu satisfies-clause references GenericItemType without importing
it, and the onClick lacks a MenuInfo annotation, so tsgo widens the divider
literal's `type` to `string` and rejects the whole context menu array.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:31:51 +08:00
Arvin Xu ca873e3c34 🐛 fix(web-crawler): cap response body size to prevent serverless OOM (#14660)
* 🐛 fix(web-crawler): cap response body size to prevent serverless OOM

Production saw repeated SIGABRT crashes on `/trpc/tools/search.webSearch`
where Node aborted with V8 "allocation failed" — the naive crawler buffered
entire response bodies into heap before the 1 MB downstream truncation could
apply, so a single large page (or a batch of three under default
concurrency=3) could push rss past the lambda memory ceiling.

- ssrfSafeFetch: add opt-in `maxContentLength` that streams the response
  body via `for await` and stops at the cap (soft truncation — still a
  successful response). Breaking the iterator destroys the underlying
  stream and releases the connection. Default behaviour (full
  `arrayBuffer()` read) unchanged when the option is absent.
- naive crawler: pass `maxContentLength: MAX_HTML_SIZE` so any body beyond
  1 MB is dropped at the network layer instead of being materialised in heap.
- htmlToMarkdown: explicitly call `window.happyDOM.close()` in a finally
  block so the parsed DOM tree is released as soon as parsing finishes,
  rather than waiting for the function scope to drop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(ssrf-safe-fetch): add OOM regression tests for response body cap

Verify that the maxContentLength cap actually prevents the production SIGABRT
scenario, not just produces a truncated body.

- Source-pull bound: a body source with 200 MB available, capped at 1 MB,
  must not be drained beyond ~1 MB. Asserts on bytes pulled from the
  generator, which is the property that prevents OOM.
- Concurrency bound: matches production CRAWL_CONCURRENCY=3 — three
  concurrent oversized fetches should pull at most ~3 MB total, not 300 MB.
- Heap-delta bound (gated on --expose-gc): under real GC pressure,
  fetching a 50 MB body with a 1 MB cap should grow heapUsed by < 10 MB.
  Run with `NODE_OPTIONS=--expose-gc bunx vitest run` to exercise; skipped
  by default so CI doesn't false-fail on GC timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 23:21:08 +08:00
Innei ddc67bc3db 🐛 fix(desktop): focus onboarding auth success state (#14694) 2026-05-12 22:57:34 +08:00
Arvin Xu dfb5e0176e feat(markdown): user_feedback card + task card polish + Run now context menu (#14727)
*  feat(markdown): render <user_feedback> task prompt blocks as a card

`buildTaskRunPrompt` wraps the user's pre-run comments in a
`<user_feedback>` block alongside `<task>`. The Task plugin captured
`<task>` into a card, but `<user_feedback>` had no plugin and leaked
into the chat as raw XML. Because CommonMark only treats tag names
matching `[a-zA-Z][a-zA-Z0-9-]*` as html, the underscore in
`user_feedback` puts the opening/closing tags inside a `paragraph` as
plain text — so the new remark plugin walks paragraph children rather
than html nodes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-card): drop standalone status row + Agent/Parent/Topics, inline semantic status badge

The status/Priority row, Agent, Parent and Topics fields aren't useful
when the task card is rendered inside the topic chat drawer (the drawer
already exposes that context). Move the task status to a compact badge
beside the identifier and reuse `taskDetail.status.*` for the label so
"scheduled" reads as "Scheduled" / "已排期".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): compact one-line header + left-border quote-style card

Slims the card down to a single 12px header line ("User feedback · N
comments") with a small 12px icon, and wraps the whole block in a
subtle fill + 2px left-border accent so it reads as a quoted aside and
visually separates from the task card that follows in the same user
message body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): drop fill + radius, render as plain left-rail blockquote

The filled card competed visually with the unstyled task block that
sits beside it in the same message body. Reducing to a 2px left-rail
quote without background or border-radius lets both blocks read as
parts of the same user message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): collapsible card with task-style head + bottom divider

Default-collapsed `<details>` whose summary mirrors the task title row
(32px icon + bold label + small count badge), with a bottom split-line
that doubles as a divider between the user feedback head and the task
card that follows in the same message body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): strip default markdown details card chrome

@lobehub/ui Markdown applies bg + padding (0.75em 1em) + box-shadow +
border-radius to every nested <details>, which made the user_feedback
head read as a wide standalone card sitting awkwardly on top of the
inline task title. Override the chrome (with !important — the lib
selector wins on specificity otherwise) so the head sits flat in the
message body, with only the bottom split line separating it from the
task that follows. The lib's right-side disclosure chevron is kept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(user-feedback): match task card's 12px symmetric divider spacing

Add a 12px margin-bottom so the gap below the user_feedback bottom rule
mirrors the 12px above it, matching the symmetric 12px the task card
already uses around its own internal divider. Without this, the
user_feedback rule sat flush against the T-31 row while the next rule
below T-31 had a 12px gap on both sides — visually uneven.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-card): drop status badge from task title row

The task drawer header and the schedule strip on the task detail page
already convey status; surfacing it again on the task card inside the
chat body just added noise. Drop the badge along with the now-unused
KNOWN_STATUSES / isKnownStatus / TaskStatusIcon / useTranslation
plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(tasks): add "Run now" item to task card context menu

Available only for backlog and completed tasks; mirrors the inbox-agent
fallback used by the detail-page Run Now action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(topic-list): preserve `#` icon placeholder for heterogeneous agents

Returning null for the icon slot collapsed the row layout, so titles on
heterogeneous-agent topics (Claude Code, Codex, …) no longer aligned
with sibling rows. Render the same HashIcon with visibility:hidden so
the box is preserved without showing the glyph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:39:11 +08:00
brone1323 a109d22c8d 🌐 i18n: add missing task-schedule and review strings to 16 locales (#14728)
🌐 i18n: add missing translations for task-schedule and review keys across 16 locales

Adds 14 missing i18n keys to all non-zh-CN locales (ar, bg-BG, de-DE,
es-ES, fa-IR, fr-FR, it-IT, ja-JP, ko-KR, nl-NL, pl-PL, pt-BR, ru-RU,
tr-TR, vi-VN, zh-TW):

chat.json (11 keys):
- taskSchedule.summary.everyNHoursHalfPast
- taskSchedule.summary.hourlyHalfPast
- taskSchedule.timezoneSearchEmpty
- taskSchedule.timezoneSearchPlaceholder
- workingPanel.review.revert (and 7 sub-keys)

plugin.json (1 key):
- builtins.lobe-task.apiName.setTaskSchedule

setting.json (2 keys):
- serviceModel.modelAssignments.title
- serviceModel.optionalFeatures.title

These were added in recent commits but the automated i18n sync had not
yet propagated them to non-Chinese locales.
2026-05-12 22:13:31 +08:00
Innei b8587cef73 💄 style: polish desktop header icons, sidebar density, and task menus (#14724)
* 💄 style: shrink desktop header icons and tighten sidebar/home density

Switches all desktop header action icons from DESKTOP_HEADER_ICON_SIZE to
DESKTOP_HEADER_ICON_SMALL_SIZE, and tightens vertical gaps in the home
sidebar, recents list, and nav header layout for a denser, calmer look.

* ♻️ refactor(agent-tasks): migrate task menus and scheduler select to @lobehub/ui base-ui

- TaskPriorityTag / TaskStatusTag: replace antd Dropdown with base-ui
  DropdownMenu and adopt the ContextMenuItem / MenuInfo typings.
- useTaskItemContextMenu: drop the DOM data-attribute submenu marker in
  favour of an internal activeSubmenuRef tracked via onOpenChange.
- TaskScheduleConfig / SchedulerForm: swap @lobehub/ui Select for the
  base-ui Select and replace the custom SearchBar dropdownRender with
  antd Select showSearch for timezone filtering.

* ♻️ refactor(review): migrate review dropdowns to @lobehub/ui base-ui DropdownMenu

Swap the antd Dropdown trios (mode picker, base-ref picker, more menu) in
the agent working-sidebar Review pane for the base-ui driven DropdownMenu,
matching the recent task menus / scheduler migration. Also tighten the
sidebar header paddingInline from 16 to 4 to align with the surrounding
density polish.

* 🐛 fix(tasks): replace unsupported onOpenChange with onTitleMouseEnter in context menu
2026-05-12 21:42:28 +08:00
René Wang ba750161ca fix: Docs image (#14726)
fix: image
2026-05-12 20:19:55 +08:00
René Wang 60c55b731c 📝 docs: add May 11 weekly changelog (#14651) 2026-05-12 20:06:45 +08:00
Arvin Xu 09230e7af5 🐛 fix(desktop): detect Windows npm .cmd shims for CLI agents (claude/codex/…) (#14720) 2026-05-12 17:46:48 +08:00
LobeHub Bot fac91067ce 🌐 chore: translate non-English comments to English in cli-migrate (#14708)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 17:36:22 +08:00
Arvin Xu 0b5c1fb53f ⬆️ chore: bump @lobehub/ui to 5.10.5 2026-05-12 17:17:02 +08:00
Arvin Xu 5d21b9e149 💄 style(review-panel): hover revert button to discard per-file working-tree changes (#14716)
 feat(review-panel): hover revert button to discard per-file working-tree changes

Add a hover-revealed Undo icon to each file row in the Review panel's
unstaged view. Clicking opens a Popconfirm; confirming runs a new
`git.revertGitFile` IPC that restores the file from HEAD (or unstages +
deletes when the path doesn't exist at HEAD, covering staged-add and
untracked entries).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:03:31 +08:00
Innei 9e0e76fda2 feat(documents): add optimistic create/delete and inline rename for document tree (#14714)
- Insert pending rows immediately on create folder/document, with
  optimistic SWR mutation that rolls back on server error
- Auto-focus rename input on newly created items via onPendingInserted
  callback
- Defer rename commits for pending rows until the server create resolves,
  then rename against the real row id
- Optimistic recursive delete closes the confirm modal instantly, removes
  target + descendants from the tree, and rolls back on failure
- Fix folder path canonicalization in ExplorerTree rename lookup
  (toCanonicalTreePath ensures trailing slash for folders)
- Export getItemPathFromEventPath for composed-path–based item resolution
- Add unit tests for toCanonicalTreePath and ExplorerTree event helpers
2026-05-12 16:40:17 +08:00
Arvin Xu 66b9c67494 fix: update Task page placeholder copy (#14704)
* fix: update Task page placeholder copy

* fix: update Task page placeholder copy (en-US)
2026-05-12 16:25:23 +08:00
Innei 2d4822ad7b 💄 style: standardize header action icon sizes (#14717)
💄 style: standardize header action icons to DESKTOP_HEADER_ICON_SMALL_SIZE

Unify icon sizing across sidebar and header action buttons by replacing
hardcoded sizes and DESKTOP_HEADER_ICON_SIZE with
DESKTOP_HEADER_ICON_SMALL_SIZE for consistent visual density.

Affected components:
- SideBarHeaderLayout back button
- ToggleLeftPanelButton default size
- BackButton default size
- Agent sidebar header chevron
- InboxButton notification icon
2026-05-12 15:48:56 +08:00
Innei a50b230fae feat(devtools): add dev-only feature flag override panel (#14565)
Add a client-side feature flag override panel that lives behind a
floating button in dev builds. Overrides are persisted to localStorage
and merged into useServerConfigStore.featureFlags so existing flag
consumers see the toggled value without any callsite changes.

The panel is gated by NODE_ENV plus a localStorage opt-in
(LOBE_DEV_FEATURE_FLAG_PANEL_ENABLED = "1"); prod builds tree-shake
the entire feature.
2026-05-12 15:33:51 +08:00
Arvin Xu 5d6d01601d 🐛 fix(builtin-tool-task): expose lobe-task and add setTaskSchedule (#14713)
*  feat(builtin-tool-task): expose lobe-task to users and add schedule config

The task tool is now generally available — flip it from a scenario-only
internal tool to a user-toggleable recommended skill, and let the LLM
configure recurring execution (cron or heartbeat) via createTask / editTask.

- Drop `discoverable: false` + `hidden: true` from TaskManifest registration
- Add `lobe-task` to RECOMMENDED_SKILLS so it stays installed by default
- Remove the USER_HIDDEN_BUILTIN_TOOL_IDS allowlist (only contained lobe-task);
  update selectors and AgentTool to stop filtering it out
- Extend createTask / createTasks / editTask with `automationMode`,
  `schedulePattern`, `scheduleTimezone`, `heartbeatInterval`; editTask also
  accepts `maxExecutions`
- Route schedule columns through taskService.update and maxExecutions through
  taskService.updateConfig (server merges into tasks.config.schedule);
  refresh detail once at the end of editTask

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool-task): split schedule config into dedicated setTaskSchedule tool

editTask was the wrong place for schedule fields — schedule needs its own
verb so the LLM (and any future human-in-the-loop review) can audit cron /
heartbeat changes separately from generic field edits, and createTask should
stay a pure "make a task" verb without automation knobs.

- Drop automationMode / schedulePattern / scheduleTimezone / heartbeatInterval
  from createTask + createTasks, and drop them plus maxExecutions from editTask
- Add new `setTaskSchedule(identifier, automationMode?, schedulePattern?,
  scheduleTimezone?, heartbeatInterval?, maxExecutions?)` API with its own
  manifest entry, executor method, types, i18n key, and inspector
- Schedule columns still route through taskService.update; maxExecutions still
  routes through taskService.updateConfig (server merges into
  tasks.config.schedule) — same wiring, just moved into the dedicated tool
- Update systemRole to advertise setTaskSchedule + keep editTask description
  clean of schedule mentions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:25:53 +08:00
AmAzing- b49340742b feat: add service model assignments settings (#14712)
*  Add default agent model setting

* 💄 Refine service model assignments UI

* 💄 Clarify optional service model features
2026-05-12 14:59:09 +08:00
Innei b29816e927 🐛 fix(desktop): reset pendingLoginMethod on auth failure/cancel paths (#14695)
* 🐛 fix(desktop): focus onboarding auth success state

* 🐛 fix(desktop): reset pendingLoginMethod on auth failure/cancel paths

Clear pendingLoginMethod in authorizationFailed, authorizationProgress
cancelled, and remoteServerSyncError handlers to prevent users getting
stuck without a Get Started path when a re-auth attempt fails but a
prior authorization is still valid.

* Delete src/routes/(desktop)/desktop-onboarding/features/LoginStep.test.tsx

---------

Co-authored-by: Innei <inbox@innei.in>
2026-05-12 14:30:06 +08:00
Innei f03a1f0022 ♻️ refactor(spa): use __DEV__ define instead of process.env.NODE_ENV (#14696)
* ♻️ refactor(spa): use __DEV__ define instead of process.env.NODE_ENV

The Vite `__DEV__` define and its global type declaration are already
in place (plugins/vite/sharedRendererConfig.ts, src/types/global.d.ts).
Replace `process.env.NODE_ENV` checks across SPA-only files with the
`__DEV__` boolean so the bundler can statically eliminate dev-only
branches in production builds.

Server-side files (app/, server/, libs/next, libs/trpc, libs/better-auth,
envs, instrumentation) and modules that are also imported by Next.js
SSR pages (e.g. components/Loading/BrandTextLoading) are intentionally
left untouched to avoid runtime `__DEV__ is not defined` errors.

* fix(vitest): define __DEV__ and related constants for test environment

Vitest runs outside the Vite SPA build pipeline, so the __DEV__ define
injected by sharedRendererDefine was not available during tests. This
caused ReferenceError: __DEV__ is not defined in any test file that
transitively imports code using the __DEV__ constant.

Add a  block to vitest.config.mts that mirrors the SPA defines:
- __DEV__: true (test is not production)
- __CI__: mirrors process.env.CI
- __ELECTRON__/__MOBILE__: false (not testing platform-specific code)

* fix: replace missed isDevEnv reference with __DEV__ in AgentMockDevtools
2026-05-12 14:29:58 +08:00
Neko 29db177524 ♻️ refactor(agent-signal,prompts,database,builtin-tool-self-iteration): unified structure of service, unified tool, unified name and concepts (#14699) 2026-05-12 14:08:23 +08:00
Arvin Xu 5d8d2abe4c 🐛 fix(utils): cap image binary at 3.75MB so base64 payload stays under Anthropic 5MB limit (#14711)
* 🐛 fix(utils): cap image binary at 3.75MB so base64 payload stays under Anthropic's 5MB limit

Anthropic enforces the 5MB image cap on the base64-encoded payload, not the
binary file. Base64 inflates by ~4/3, so a 4.7MB binary file becomes 6.27MB
once encoded and trips `messages.*.content.*.image.source.base64: image
exceeds 5 MB maximum`. The previous MAX_IMAGE_BYTES of 5MB matched against
file.size, letting these images through compression untouched.

Lower the threshold to floor(5MB * 3/4) ≈ 3.75MB in both the frontend
canvas compressor and the server-side Sharp fallback so the progressive
shrink loop keeps going until the base64 payload is safely under the cap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(utils): tighten image binary cap to 3MB for extra base64 headroom

Drop MAX_IMAGE_BYTES from 3.75MB (exact 5MB-base64 boundary) to a flat 3MB
so the encoded payload lands around 4MB — clear of any per-provider rounding
or jitter at the 5MB hard limit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:04:12 +08:00
Arvin Xu 49c8d17e2c 🐛 fix(tasks): scheduler, hotkey, comment & TodoList polish (#14707)
* 🐛 fix(portal): allow TodoList to scroll when expanded content exceeds max-height

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(tasks): route 1–N hotkey to the open submenu instead of defaulting to status

The base-ui SubmenuTrigger doesn't propagate antd's `onTitleMouseEnter`, so
the hover ref in the right-click context menu never updated and every number
press fell back to the status submenu. The standalone Priority/Status tag
dropdowns also showed 1–N hints without binding any handler at all.

- Detect the currently open submenu via `data-popup-open` + a per-submenu
  `data-task-submenu` marker on the icon; numbers are ignored when no
  submenu is open.
- Install a keydown listener on TaskPriorityTag / TaskStatusTag while their
  dropdown is open so the hint numbers actually fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(scheduler): keep Continuous unchanged while editing Max runs

Clearing the Max runs input previously emitted maxExecutions=null, which the
form re-interpreted as Continuous and auto-checked the checkbox mid-edit
(disabling the input before the user could type the replacement number).

Track Continuous as its own state derived from the persisted prop. On clear
we hold the input empty locally without touching Continuous or emitting,
and unrelated emits fall back to the persisted value so they can't flip the
checkbox either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): always show comment Send button and unify action labels

- Make the Send button visible by default in CommentInput / FeedbackInput
  (greyed out when empty) so the field reads as an input instead of vanishing
  affordance.
- Align topic action menu labels to Title Case (Stop Run / Open Run /
  Copy Topic ID / Copy Operation ID / Copy Link) to match the rest of the
  Action microcopy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  perf(scheduler): seed SchedulerForm from props once and own state locally

The previous prop→state useEffects re-synced every time the parent prop
updated, which during the async updateSchedule → refreshTaskDetail roundtrip
clobbered the user's in-flight edits with stale store values — felt awful
on rapid changes.

Drop the three sync useEffects and seed local state from props only at
mount via a lazy useState initializer. The form now owns its values
optimistically; cross-task safety comes from `key={taskId}` on the
parent so the form remounts cleanly when switching tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): Notion-style timezone picker — drop underscores, offset on the right

Underscored labels like 'America/New_York (EST/EDT, UTC-5/-4)' read poorly in
the dropdown. Split each option into `label` (underscore → space) and `offset`,
and render the row with the city on the left and a subtle gray offset on the
right, in line with how Notion's timezone picker presents this.

IANA `value` keeps the underscore so cron and Drizzle stay happy. Search now
filters by the human label only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): keep zone abbreviations in the timezone offset column

Show 'EST/EDT · UTC−5/−4' instead of just 'UTC−5/−4' so users can recognize
the zone by its common abbreviation alongside the offset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): drop awkward ':30' suffix from hourly summary

'Every hour:00' / 'Every 2 hours:30' read like glitched concatenations. Cron
storage always rounds to 0 or 30 minutes, so call out the non-zero case as
'at half past' and stay implicit on the top of the hour.

- Every hour
- Every hour at half past
- Every 2 hours
- Every 2 hours at half past

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): collapse advanced settings by default

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  perf(tasks): coalesce post-write refresh and add timezone search

Two follow-up fixes for the AgentTasks scheduler popover.

##### Optimistic schedule writes, single coalesced refresh

Rapid edits in the scheduler form (toggling daily/hourly/weekly, weekday
chips, time, etc.) each triggered `taskService.update` + a full
`internal_refreshTaskDetail` per call. With overlapping requests the
refreshes returned intermediate server state and bounced TaskTriggerTag /
summary text away from the user's latest choice.

- Add `#withCoalescedRefresh` on the task config slice: it tracks a per-task
  pending-writes count and only fires `internal_refreshTaskDetail` after the
  LAST in-flight write settles.
- Give `updateSchedule` an optimistic `internal_dispatchTaskDetail` so
  external readers see the new pattern/timezone/maxExecutions immediately.
- Route both `updateSchedule` and `setAutomationMode` through the coalescer.

##### Timezone picker — search input at the top

The dropdown had antd's implicit type-into-trigger search, which most users
miss. Add a `SearchBar` inside `dropdownRender`, filter the options against
label/value/offset locally, and show an empty state when nothing matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(scheduler): weekday chips only show background when selected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(tasks): dispatch optimistic schedule under nested 'schedule' field

`TaskDetailData` exposes schedule as `schedule.{pattern,timezone,maxExecutions}`,
not flat columns. The previous optimistic dispatch used the DB-style flat keys,
which broke type-check and would never reach the in-memory selectors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): drop Cmd+Backspace shortcut on the Delete menu item

Header dropdown only advertised the hotkey (no handler), and the right-click
context-menu handler is gone too — keeps the visual claim honest and
removes the irreversible-by-keystroke footgun.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(agent-signal): pin `now` in proposal activity tests to fixture window

Two cases relied on the real system clock; once today crossed the
fixture's default `expiresAt` (2026-05-12), pending proposals were
classified as expired and the assertions broke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tasks): hide '#' placeholder icon for heterogeneous agent topics

Claude Code / Codex topics aren't chat topics in the usual sense, so the
fallback HashIcon in the sidebar row reads as noise. Skip it when the
current agent has a heterogeneousProvider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🧪 test(tasks): provide agentMap in TopicItem store mock

`isCurrentAgentHeterogeneous` walks through `currentAgentConfig` which
indexes `s.agentMap[agentId]`. Extend the mocked store state to include
an empty `agentMap` so the selector resolves to `undefined` (= not
heterogeneous) instead of throwing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:01:59 +08:00
Arvin Xu c62af095f5 🐛 fix(cli): remove stale cron entry from generated man page (#14709)
* 🐛 fix(cli): remove stale cron entry from generated man page

The cron command was removed from program.ts but the generated man page
still listed it. Regenerated via bun run man:generate.

* 🔖 chore(cli): release 0.0.15

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:40:55 +08:00
Arvin Xu 9c746d5784 💄 style(tool): add word wrap toggle to tool arguments display (#14706)
 feat(tool): add word wrap toggle to tool arguments display

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 13:31:59 +08:00
Rdmclin2 a74cd2bf9f 🐛 fix: sidebar add agent (#14693)
* fix: sidebar add agent and group error

* feat: add billboard cta
2026-05-12 10:27:38 +07:00
Innei 1a368ea823 💄 style(nav): unify ActionIcon sizing and improve TodoList encapsulation (#14692)
- Extract SIDEBAR_HEADER_ACTION_ICON_SIZE constant for consistent sidebar header ActionIcon sizing
- Pass size prop to ToggleLeftPanelButton
- Simplify Agent selector ActionIcon to use 'small' size preset
- Move layout wrapper styles from Body into TodoList root for better component encapsulation
- Increase Nav gap from 1 to 4 for proper spacing
2026-05-12 00:59:13 +08:00
YuTengjing 98156dba8d feat: inline skill auth in recommended task templates (#14676)
*  feat: support refreshing recommended task templates

- Add optional `refreshSeed` through `listDailyRecommend` API, service, and
  client; SWR key includes it so a refresh actually refetches.
- Frontend stores the seed in sessionStorage (via `useSessionStorageState`)
  so a new tab or next day returns to the default daily picks.
- Home Daily Brief shows a "Refresh" affordance on the Recommendations
  subtitle row.
- Fix first-card pinning when matched candidates < RECOMMEND_COUNT: fold
  the fallback pool in so seed reorders the whole batch instead of locking
  position 0 to a single-match template.

Linear: LOBE-8689

*  feat: resolve task-template icon priority

Render the task-template card icon as self > skill provider > interest > Sparkles. Skill icons read required[0] then optional[0], skipping unresolvable providers. URL icons render via @lobehub/ui Image, component icons keep the 28x28 tile.

*  feat: inline skill auth in task template card

Single click "Add task" is now the entire flow: the button stays put, and if a required skill is missing we chain its OAuth popups and create the task automatically. Unauthorized providers (required + optional) appear as compact inline rows above the footer; the provider that already drives the card's main icon is suppressed to avoid duplicating the same logo.

*  feat: add task template detail modal

Open a detail modal when the recommended task template card is clicked,
exposing the full instruction (markdown) plus inline skill auth and the
add-task action. Rename i18n `${id}.prompt` -> `${id}.instruction` to
align with the task table column, and write both `description` and
`instruction` when creating the task. Extract shared `TemplateBriefIcon`,
`useScheduleText`, `useTaskTemplateCreate` and `useVisibleAuthSpecs` so
the card and the modal share the same creation flow and OAuth chaining.

* 🐛 fix: missing Block import in TaskTemplateCard

*  feat: render recommended templates on empty Tasks page

Replace the bare "no tasks" placeholder with a hero landing: greeting,
enlarged inline composer (hero variant), and a 2-column grid of up to
10 recommended task templates. Plumbs a new `count` option through the
service, both routers, the client service, and the recommendations hook
so the home page keeps its 3-card layout while the empty Tasks page
asks for 10.

* 🐛 fix: type cast in resolveTemplateIcon test for unknown interest

* 🌐 i18n: update translations for task template empty-state and other namespaces
2026-05-12 00:28:24 +08:00
Innei 3ef4083dfb 🐛 fix: replace ScrollShadow with ScrollArea to fix React #185 infinite render loop (#14689)
Migrate all ScrollShadow usages to ScrollArea (scrollFade) to eliminate
the effect → setState → render → effect cycle that caused React error
#185 (Maximum update depth exceeded) in the scroll overflow hook.

Affected components:
- StreamingMarkdown
- AgentCouncil AutoScrollShadow
- AssistantGroup ContentBlocksScroll
- Conversation Thinking

Fixes lobehub/lobehub#14650
2026-05-12 00:15:12 +08:00
LiJian a5299696de 🐛 fix(heteroFinish): trigger task lifecycle on cloud sandbox agent completion (#14681)
* 🐛 fix(heteroFinish): trigger task lifecycle transition on sandbox agent completion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(heteroFinish): guard onTopicComplete against duplicate finish calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 23:31:26 +08:00
LiJian f64c74db90 📝 docs(cloudHeteroContext): add sandbox persistence & gh push rules (#14682)
* 📝 docs(cloudHeteroContext): add sandbox persistence & gh push rules

Inject ephemeral-sandbox warnings and mandatory GitHub push rules into
the cloud CC context block so every Claude Code run knows:
- The sandbox is wiped after inactivity — local changes will be lost
- All code changes must be committed and pushed before task is complete
- Use gh CLI (pre-authenticated) for GitHub operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(cloudHeteroContext): address review comments on sandbox persistence rules

- Remove gh push guidance (gh has no push subcommand; git push is correct)
- Gate gh-auth instructions behind githubToken availability to avoid
  auth-dependent commands failing in no-token sandbox runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 📝 docs(cloudHeteroContext): add git push auth fallback guidance

Tell CC that the sandbox has git credentials ready, but if git push
fails it can self-recover via:
1. gh auth setup-git (reconfigures git credential helper)
2. inline token URL as last resort (oauth2:$GITHUB_TOKEN@github.com)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 23:21:15 +08:00
YuTengjing 83b2a00314 📝 docs(skills): frontmatter cleanup + argument-hint (#14683)
* 🔨 chore: control skill triggering via frontmatter flags

- Rename debug skill to debug-package (avoid confusion with debugging workflows)
- Add disable-model-invocation to add-* skills so they are manual-only
- Add user-invocable: false to reference/architecture skills so they auto-load only when relevant

* 🔨 chore: rename skill reference dirs to plural references

Align with the skill-creator convention (scripts/, references/, assets/).

* 📝 docs(skills): split oversized SKILL.md files and refine triggers

- upstash-workflow: 1126L → 189L, extract implementation / best-practices / examples references
- data-fetching: 854L → 613L, move parent-keyed-map walkthrough to references
- store-data-structures: 625L → 314L, extract types and reducer references
- upstash-workflow/cloud.md, version-release/release-notes-style.md: add TOCs
- linear: rewrite ALL-CAPS MUSTs into prose explaining why; mark user-invocable: false
- version-release: mark disable-model-invocation: true (manual /version-release only)
- debug-package: expand description with concrete trigger phrases and tokens

* 📝 docs(skills): regularize microcopy structure

Move language-specific guidelines into references/zh.md and references/en.md
so SKILL.md can point to them via the standard progressive-disclosure pattern.
Previously the two files sat next to SKILL.md but were not referenced anywhere,
making them invisible to Claude Code loading.

* 📝 docs(skills): move builtin-tool refs into references subdir

Aligns builtin-tool with the references/ layout used elsewhere
(microcopy, store-data-structures). 3 md files move, SKILL.md
links updated.

* 📝 docs(skills): broaden trigger descriptions for core skills

Adds concrete API names, file paths and natural-language phrases so
auto-triggering catches more relevant prompts. Touches zustand,
drizzle, i18n, react, typescript, modal, hotkey.

* 📝 docs(skills): add argument-hint to user-only skills
2026-05-11 22:48:38 +08:00
𝑾𝒖𝒙𝒉 c0b9124956 🐛 fix(hotkey): remove redundant onClear to prevent double updateHotkey calls (#14663)
Previously, clicking the clear button on HotkeyInput triggered both
`onClear` and `onChange` (since HotkeyInput internally calls
`setHotkeyValue('')` which fires `onChange`). This caused two
concurrent requests to `updateDesktopHotkey` and showed two toast
messages (success/error) for a single user action.

Fix: remove the redundant `onClear` prop. HotkeyInput's clear action
already fires `onChange('')`, so the single `onChange` handler is
sufficient.

Co-authored-by: Innei <i@innei.in>
2026-05-11 22:47:58 +08:00
Innei b794eb1fb9 ♻️ refactor(web-onboarding): merge agent-marketplace identifier into onboarding tool (#14672)
* ♻️ refactor(web-onboarding): merge agent-marketplace identifier into onboarding tool

Drop the standalone `lobe-agent-marketplace` builtin tool and fold its
`showAgentMarketplace` / `submitAgentPick` APIs into `lobe-web-onboarding`
so onboarding exposes a single tool identifier.

- Move marketplace API entries (with humanIntervention/renderDisplayControl)
  into WebOnboardingManifest; extend WebOnboardingApiName.
- Compose AgentMarketplaceExecutionRuntime inside WebOnboardingExecutionRuntime;
  the client WebOnboardingExecutor now owns showAgentMarketplace/submitAgentPick
  with telemetry hooks. Drop the separate client/server executor + runtime files.
- Merge marketplace Inspector / Intervention / Render maps under the
  web-onboarding identifier. Remove AgentMarketplace* entries from
  builtin-tools registries and from the builtin web-onboarding agent's
  plugins list.
- Switch customInteractionHandlers to route by (identifier, apiName) so
  the marketplace picker handler fires only on `showAgentMarketplace`.
- Drop the `lobe-agent-marketplace` fallback string in
  OnboardingActionHintInjector; match by apiName only.
- Rename plugin/setting locale keys under `lobe-web-onboarding.*`.

* 🐛 fix(onboarding): reserve scroll headroom for agent marketplace overlay

- Add a footerSlot spacer in ChatList matching the marketplace panel height so the latest message can be scrolled into view above the absolute overlay.
- Nudge the marketplace overlay inset by 2px to hide subpixel border seams.
- Document turn output order in the onboarding system role to avoid trailing filler text after tool calls.
2026-05-11 21:29:41 +08:00
YuTengjing 5ef0238b22 🐛 fix: reject inactive OIDC access (#14674)
* 🐛 fix: reject inactive OIDC access

* 🐛 fix: honor expired OIDC bans

* 🐛 fix: decouple OIDC inactive error from tRPC

*  test: fix OIDC auth type checks
2026-05-11 21:20:04 +08:00
Arvin Xu dd02ac7062 💄 style(web-onboarding): add Render for saveUserQuestion & showAgentMarketplace (#14667)
 feat(builtin-tool-web-onboarding): add Render for saveUserQuestion + showAgentMarketplace

Tool messages for `saveUserQuestion` and `showAgentMarketplace` previously
fell back to the raw Arguments/Response table once the call resolved
because neither API had a Render registered. Wire both up:

- `saveUserQuestion`: new Render mirroring the Intervention's detail-card
  style — agent identity (emoji + name), full name, and interests chips —
  rendered conditionally per the fields actually saved.
- `showAgentMarketplace`: reuse the existing `SubmitAgentPick` Render.
  After the picker submits, `customInteractionHandlers` rewrites the
  `showAgentMarketplace` tool message's `pluginState` to the same
  `{ summaries, installedAgentIds, ... }` shape, so the card grid
  renders without a new component.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:50:16 +08:00
Arvin Xu ae3dc902e3 ♻️ refactor(knowledge-base): share RAG runtime across client/server via KnowledgeBaseSearchService (#14673)
* ♻️ refactor(knowledge-base): share runtime across client/server via KnowledgeBaseSearchService

Extract a server-side `KnowledgeBaseSearchService` (semanticSearchForChat
fan-out + getFileContents branching + groupAndRankFiles) so both the lambda
chunk router and the builtin tool server runtime orchestrate RAG through one
implementation. Wire the builtin knowledge-base tool to the shared
ExecutionRuntime in the package by moving the client executor to
`src/client/executor/` and registering a thin server runtime factory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(knowledge-base): move PG 23505 handling into adapters, restore executor path

ExecutionRuntime is dual-end so it cannot detect PG error codes — only the
server adapter can. Move the unique-constraint check there and translate the
lambda router's `FILE_ALREADY_IN_KNOWLEDGE_BASE` sentinel in the client
adapter, so the runtime's generic catch surfaces the human-readable message
on both code paths. Restore `src/executor/` as a top-level sibling of
`src/client/` to match the convention of every other builtin tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(knowledge-base): collapse executor into /client, drop ./executor export

The executor is just another client-only adapter (alongside Inspector and
Render) — no reason for it to sit at the package root with a dedicated
subpath. Move it under `src/client/executor/`, re-export from
`src/client/index.ts`, drop the `./executor` entry from package.json, and
update the consumer to import from `@lobechat/builtin-tool-knowledge-base/client`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(knowledge-base): cover KnowledgeBaseSearchService

13 unit tests across both methods:
- getFileContents: docs_* direct read, missing doc, file_* via findByFileId,
  parseFile fallback, parse failure surfaces as error entry, missing file,
  mixed batch.
- semanticSearchForChat: chunk grouping + relevance ranking, BM25 skip when
  no knowledgeIds, knowledgeIds → fileIds expansion, vector/BM25 isolated
  failure capture (preserves the other path's results + structured
  rejections), full failure 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>
2026-05-11 20:49:17 +08:00
Arvin Xu 853998b560 ♻️ refactor(bot): close activator bypass + converge device-access checks (#14664)
* ♻️ refactor(aiAgent): introduce deviceToolRegistry as single source of truth

Centralise "what counts as a device tool" into one module so the next
device-tool addition only touches one file. Removes the hardcoded
`new Set(['local-system', 'remote-device'])` from `deviceToolAudit.ts`,
which had drifted from `LocalSystemManifest.identifier` /
`RemoteDeviceManifest.identifier` imports elsewhere.

Foundation for the LOBE-8768 activator-bypass fix landing next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(aiAgent): block activator from bypassing canUseDevice gate

External bot senders could still reach the owner's machine by having the
LLM call `lobe-activator.activateTools(["lobe-remote-device"])`, because
`enableCheckerFactory.allowExplicitActivation` short-circuits before the
canUseDevice rule, and the engine's `manifestSchemas` always contained
the full builtin list (LOBE-8768 B1).

Fix by filtering builtin manifests **physically** through
`buildAllowedBuiltinTools` at both feed-points (ToolsEngine input and
the activator-discovery `toolManifestMap`). When `canUseDevice=false`,
the device manifests no longer exist in either map, so explicit
activation cannot resolve them — the rule-layer gate becomes
defense-in-depth instead of the sole barrier.

Validates with the prod incident's repro path: an external sender's
`<available_tools>` no longer advertises `lobe-remote-device`, and an
activator call to enable it returns "not found".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(bot,messenger): centralise isOwner derivation in buildBotContext

The same fail-closed expression
`!!operatorUserId && senderExternalUserId === operatorUserId` was
duplicated across `BotMessageRouter.onNewMention`, `.onSubscribedMessage`,
the DM catch-all, and `MessengerRouter.dispatchToAgent` — four sites,
one rule, one place to silently regress.

Route all four through `buildBotContext`. The helper now owns the
fail-closed contract referenced by `ChatTopicBotContext.isOwner`'s
docstring, so adding the next platform/router can't accidentally
default to "trusted when in doubt".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(aiAgent): apply device filter post-merge across all manifest sources

The previous fix only filtered the `builtinTools` source. An installed
plugin or a Skill/Klavis manifest declaring
`identifier: 'lobe-remote-device'` would still survive in
`manifestSchemas` and reach `toolManifestMap` via either
`getEnabledPluginManifests` or the direct ingest loops in
`aiAgent/index.ts` — letting an external bot sender activate the device
identifier through the activator.

Two changes close the gap:

  1. `ServerAgentToolsEngineConfig.excludeIdentifiers` — applied **after**
     combining plugin + builtin + additional manifests in
     `createServerToolsEngine`. `createServerAgentToolsEngine` passes
     `DEVICE_TOOL_IDENTIFIERS` whenever `canUseDevice` is false.

  2. `isManifestIngestAllowed` in `aiAgent.execAgent` — a single
     identifier guard reused at every `toolManifestMap` / `toolSourceMap`
     write (engine-returned plugin manifests, lobehub-skill loop,
     klavis loop). New ingest points inherit the wall automatically.

New test pins the regression: a plugin + an additional manifest
spoofing the device identifiers are dropped from `availablePlugins`
when `excludeIdentifiers` is set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:45:52 +08:00
Arvin Xu e51c38c182 ♻️ refactor(task): snapshot agent model into task.config at create time (#14670)
*  feat(task): snapshot agent model into task.config at create time

Pin the assignee agent's current model/provider into task.config when a
task is created so later changes to the agent's default model don't
silently affect already-created tasks. On first run, backfill the
snapshot for tasks created before this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(task-runner): fall back to inbox agent when task has no assignee

`TaskRunnerService.runTask` previously threw `BAD_REQUEST` for any task
without `assigneeAgentId`, which broke runs created without `--agent`.
Resolve and persist the user's built-in inbox agent instead, surfacing
an `INTERNAL_SERVER_ERROR` only if that resolution itself fails.

Picked from #14671 (closes once landed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(task): collapse router orchestration into TaskService

Move multi-step task verbs out of the TRPC router into `TaskService`:
`createTask`, `cancelTopic`, `deleteTopic`, `runReview`, `updateStatus`,
`previewSubtaskLayers`, `runReadySubtasks`. The router keeps only input
validation + error wrapping; the tool runtime now shares the same
`createTask` path (was duplicating the model snapshot + parent
resolution).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🚨 ci: fix tsgo errors from TaskService extraction

`runReadySubtasks` router was rebuilding the `data` payload via a
conditional spread, which forced TS to infer a discriminated union that
broke `result.data.skipped` access in the integration test. Pass the
service result straight through so `skipped` stays a single optional
field. Also cast the stubbed `taskService` in the tool runtime unit
tests to bypass strict structural typing — same pattern the other
dep stubs already use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 20:21:40 +08:00
YuTengjing 6a66901b12 🔥 chore: drop task template tracking (#14666)
* 🔥 chore: drop task template tracking

The recommendation surface is about to be redesigned, so the analytics
funnel added in #14517 is being removed up front. A fresh tracking
schema will land alongside the redesigned UI.

- Delete `analytics.ts` plus its test and the tracking-focused
  `TaskTemplateCard.test.tsx`.
- Drop `RecommendedTaskTemplate` / `TaskTemplateRecommendationSource` /
  `TaskTemplateFallbackPool` and revert the service to plain
  `TaskTemplate[]`.
- Strip impression, dismiss, create-clicked/result and
  skill-connect-clicked/result calls from `TaskTemplateCard.tsx`, while
  keeping the createTask + navigate-to-task flow from #14540.
- Remove `recommendationBatchId` / `userInterestCount` / `onCreated`
  plumbing from `useDailyBriefRecommendationsUI`,
  `DailyBriefRecommendationsView`, and the card props.
- Revert `useSkillConnection` to the pre-tracking variant (no
  onConnectResult / SkillConnectionResult).

* 🐛 fix: remove created template from recommendation cache

After #14540 changed the create-task flow to auto-navigate to
`/task/{id}`, removing the `onCreated` plumbing from #14517 in the same
sweep meant the SWR recommendation cache was never mutated on success.
Combined with the server-side `recordCreated` being a no-op and
`listDailyRecommend` not excluding created IDs, returning to Home
showed the same recommendation as actionable again — letting users
trigger duplicate scheduled tasks from the same template.

Re-add the minimal cache-eviction plumbing (no analytics):

- TaskTemplateCard exposes `onCreated` and calls it on success
- useDailyBriefRecommendationsUI shares `removeTemplateFromList` for
  both dismiss and created flows
- DailyBriefRecommendationsView passes `onCreated` through
2026-05-11 18:47:45 +08:00
YuTengjing 63c2e251ce 🐛 fix: drop unreachable aihubmix empty-apiKey test (#14669)
* 🐛 fix: drop unreachable aihubmix empty-apiKey test

The `should return empty array when API key is missing` test asserts a
contract that doesn't hold: RouterRuntime.models() constructs the
underlying runtime via the OpenAI-compatible factory before calling
modelsOption, and the factory throws InvalidProviderAPIKey on empty
apiKey at construction time — so aihubmix's own `if (!apiKey) return []`
short-circuit can never actually fire.

Just delete the dead test. The defensive guard in aihubmix's modelsOption
stays as intent documentation. Also tighten an implicit-any in the
adjacent `should normalize model_id field to id` test.

* 🔥 chore: drop dead empty-apiKey guard in aihubmix modelsOption

* 💄 style: tighten aihubmix apiKey assertion to string
2026-05-11 18:44:07 +08:00
Zhijie He dee254c197 💄 style: add reasoning_effort support for Grok 4.3 (#14642)
* style: add reasoning_effort for Grok 4.3

* style: remove grok 4.1 series & grok-imagine-image-pro (Model retirement)

style: remove grok 4.1 series & grok-imagine-image-pro (Model retirement)

style: remove grok 4.1 series & grok-imagine-image-pro (Model retirement)
2026-05-11 17:20:35 +08:00
Arvin Xu 28bf990c88 💄 style: increase chat topic title length (#14659)
* 💄 style: increase chat topic title length

- bump initial topic title slice from 20 to 40 chars
- bump dev fallback slice from 30 to 40 chars
- bump thread title slice from 20 to 40 chars
- raise LLM summary title prompt limit from 50/10w to 80/15w

* 💄 style: bump topic/thread title slice from 40 to 80 chars

Align slice limits with the LLM summary prompt cap (80 chars) so the
initial visible title is no shorter than what the summarizer can return.
2026-05-11 16:32:22 +08:00
Bianzinan f3a785970e fix(aihubmix): use full models endpoint to return complete model list (#14511)
* fix(aihubmix): use full models endpoint to return complete model list

The /v1/models endpoint at api.aihubmix.com returns only per-user-group
models (~256). The new endpoint at aihubmix.com/api/v1/models returns
the complete catalog (800+). Fetch from the full endpoint directly.

* fix(aihubmix): normalize model_id to id from full models endpoint

The https://aihubmix.com/api/v1/models endpoint uses `model_id` instead
of `id`. Map it to `id` before passing to processMultiProviderModelList
to prevent toLowerCase() errors and empty model list.

* fix(aihubmix): add apiKey guard, AbortController timeout, and better error messages

- Extract apiKey with runtime guard to fail fast when key is missing
- Add AbortController with 10s timeout to prevent indefinite hanging
- Include response body in error message for easier debugging
- Add APP-Code header comment pointing to docs
- Expand tests: mock global fetch, cover missing key / HTTP error / network error / AbortError cases

* fix(aihubmix): add field mapping adapter and fix timeout scope

Address review feedback from #14511:

- Update AiHubMixModelCard interface to reflect the new endpoint schema
  with full JSDoc (model_id, desc, types, features, input_modalities,
  context_length, max_output, pricing.cache_read/cache_write)
- Add mapAiHubMixModel() to adapt API response fields to LobeHub model
  card fields before passing to processMultiProviderModelList:
    desc             -> description
    model_name       -> displayName
    context_length   -> contextWindowTokens
    max_output       -> maxOutput
    types            -> type  (llm/t2t->chat, image_generation/t2i->image,
                               video/t2v->video, tts, stt, embedding,
                               rerank/reranking->rerank)
    pricing.cache_read  -> pricing.cachedInput
    pricing.cache_write -> pricing.writeCacheInput
    features(tools/function_calling) -> functionCall
    features(thinking)               -> reasoning
    features(web)                    -> search
    input_modalities(image)          -> vision
- Fix timeout scope: move clearTimeout into the finally block so the
  AbortController stays active during response.json() body read, not
  just during the initial fetch() call
- Update baseURL from https://api.aihubmix.com to https://aihubmix.com
  to match official integration docs (https://docs.aihubmix.com/cn/api/Aihubmix-Integration)
- Strengthen normalize test: assert list.some(m => m.id === 'some-model')
  instead of just Array.isArray to detect normalization failures
- Add field-mapping test using vi.spyOn on processMultiProviderModelList
  to assert that all adapted fields are passed correctly

* fix(aihubmix): filter out unsupported rerank types to prevent chat fallback

- Remove rerank/reranking from TYPE_MAP; they have no LobeHub AiModelType
  equivalent and would silently fall back to 'chat' in processModelCard
- Add UNSUPPORTED_AIHUBMIX_TYPES set and filter before mapAiHubMixModel()
- Add regression test asserting rerank/reranking models are excluded and
  llm models still pass through

---------

Co-authored-by: Bianzinan <bianzinan@users.noreply.github.com>
2026-05-11 16:24:54 +08:00
Innei a238838fea feat(activator): require activation reason (#14597) 2026-05-11 16:23:56 +08:00
Innei 831c2585f1 🐛 fix(onboarding): skip marketplace on early exit, drop CJK in prompts (#14598)
* 🐛 fix(onboarding): skip marketplace on early exit, drop CJK examples in prompts

Honor the user's wish to leave: when the onboarding agent detects a true
early-exit signal in any phase, persist what is known, send a brief
farewell, and call finishOnboarding directly. The marketplace handoff is
mandatory only on normal Phase 4 / Summary completion. Previously the
spec forced the agent to invent categoryHints from environment cues
when discovery was thin, producing noisy recommendations for users who
explicitly asked to stop.

- Replace systemRole §Early Exit with a 4-step flow (no marketplace, no
  summary), and remove the trailing "respect their time" rationale that
  contradicted the new policy.
- Update toolSystemRole turn-protocol exception accordingly; mark
  persistence as best-effort (do not retry on failure) since the
  Pre-Finish Checklist is overridden on early exit.
- Update OnboardingActionHintInjector L101/L127 hints to match the new
  flow, and append an EXCEPTION clause to the Summary not-opened hint
  so a true exit signal in Summary skips the marketplace too.
- Strip CJK example phrases from prompt text; rely on the LLM's
  multilingual recognition with "equivalents in any language" hints.

* 🔨 refactor(FollowUpChips): remove unused consume function and reset editor state on chip click
🔨 style(InterventionBar): remove overflow hidden from container style

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(ci): align FollowUpChips test with removed consume and increase timeout for PGlite cold-start

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-11 15:45:54 +08:00
Neko 79ed4b5faf feat(agent-signal,server,prompts): consolidate in self-review implemented (#14657) 2026-05-11 15:14:02 +08:00
Arvin Xu d4a33d4434 💄 style(hetero-agent): read-only SubAgent threads with breadcrumb header and thread switcher (#14658)
*  feat(hetero-agent): read-only SubAgent threads with breadcrumb header and thread switcher

- Hide chat input on SubAgent threads (execution is driven by the parent agent) and replace it with an inline read-only hint
- Render the hint as the last item inside the virtual list so it scrolls with messages instead of being pinned to the viewport bottom
- ChatList exposes a new `footerSlot` prop that VirtualizedList injects as a synthetic trailing data item
- Header now shows `topic / thread` breadcrumb; thread title is a popover trigger that lists sibling threads in the same topic for one-click switching
- Hide the working-directory tag while inside a thread — directory switching doesn't belong in this read-only view
- Unify user-facing strings to "SubAgent" (badge, hint, open/close labels)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(chat-input): soften queue tray preview borders

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(conversation): scrollToBottom lands on the true last VList item

scrollToBottom targeted displayMessages.length - 1, which leaves any
trailing synthetic items (spacer, SubAgent footer hint) below the
viewport. In SubAgent threads this kept atBottom = false after the
BackBottom click or auto-scroll, so the button appeared stuck.

VirtuaScrollMethods now exposes getTotalCount, which VirtualizedList
fills from the live data length (messages + spacer + optional
footerSlot) via a ref. scrollToBottom uses that to scroll to the real
last index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:42:31 +08:00
Arvin Xu db22573a88 💄 style(chat-input): show skeleton in action bar while config is loading (#14656)
* 💄 style(chat-input): show skeleton in action bar while config is loading

Before agent / group config hydrates, action buttons read DEFAULT_*
fallbacks and the send button would dispatch against a not-yet-ready
target. Add an `isConfigLoading` prop on DesktopChatInput that swaps the
action bar + send area for skeleton placeholders. The chat page passes
`agentSelectors.isAgentConfigLoading`, group chat passes
`agentGroupSelectors.isGroupsInit`. The editor itself stays usable so
users can start typing immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(home,i18n): use 已阅 for brief confirm/confirmDone in zh-CN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use 确认完成 for brief.action.confirmDone in zh-CN

confirmDone signals the terminal transition (task marked complete),
not just dismissing the brief, so 已阅 loses the semantic distinction
from `confirm`. Use 确认完成 to match the EN intent ("Confirm complete").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use "Confirm complete" for brief.action.confirmDone in en-US

Match the semantic distinction the call site relies on:
`confirm` is dismiss-only for recurring scheduled runs, while
`confirmDone` marks the terminal completion transition. The test
mock already used "Confirm complete" — align the source defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:56:45 +08:00
Arvin Xu 399db9963a 💄 style(home): add Recommendations module with hetero agent action library (#14645)
*  feat(home): add Recommendations module with hetero agent action library

Introduce a `Recommendations` section that renders above the existing daily-brief
task templates. The module is driven by an extensible action registry with per-action
eligibility checks; the first registered actions surface "Add Claude Code agent" and
"Add Codex agent" cards on desktop when the matching local CLI is detected and the
user hasn't added that hetero agent yet.

- New `src/features/Recommendations/` with action types, registry, hetero-agent
  factory, eligibility hook, parallel CLI detection (SWR-cached) and card UI.
- Extract `createHeterogeneousAgent` from `useCreateMenuItems` into a shared
  `useCreateHeteroAgent` hook so the sidebar menu and Recommendations card share
  one creation path (create + refresh sidebar + navigate to chat).
- `DailyBrief` now renders `<Recommendations />` in place of the standalone
  template-only section; visibility is driven by the new
  `useRecommendationsVisible` hook.
- Add `recommendations.*` i18n keys to the `home` namespace (default + zh-CN +
  en-US dev preview).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(home): polish Recommendations card with brand avatar and tighter copy

Use brand Avatar icons with rounded square shape, drop the duplicate title, and tighten copy (Coding Agent tag, Add Agent CTA).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:18:55 +08:00
Rdmclin2 d5562f9933 🔨 chore: optimize system bot (#14649)
* feat: add already consumed alert

* feat: support slack send slack commends  emphemeral in channel

* chore: handle parse commands imperial

* fix: slack messenger callback ok

* feat: add messager connectionId per user

* fix: add userId to webhookbody

* fix: test case
2026-05-11 02:02:33 +07:00
Arvin Xu 5f24d179d4 feat(hetero-agent): support AskUserQuestion tools for claude code (#14639)
*  feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2)

Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's
built-in tool short-circuits in `-p` mode, so we host an in-process MCP
server that exposes an equivalent `ask_user_question` tool. The handler
blocks until the consumer submits an answer (or the 5min deadline / op
shutdown fires), surfacing a structured `agent_intervention_request` /
`agent_intervention_response` round-trip on the existing event stream.

Added in this commit:

- `packages/heterogeneous-agents/src/askUser/`
  - `AskUserBridge` — per-op pending map with timeout / cancel / progress
    keepalive support; emits an async-iterable of outbound events
  - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server,
    `?op=<id>` query routes via `AsyncLocalStorage` →
    `onsessioninitialized` → sessionId↔opId map; tool handler hands off
    to the matching bridge and pumps `notifications/progress` back to CC
    every 30s as wire-level keepalive (required for >5min waits, see
    spike notes)
  - `constants.ts` — shared tool/server names + the stable `apiName`
    the adapter rewrites to
  - Unit tests cover bridge lifecycle (resolve / cancel / timeout /
    progress / event stream) and an end-to-end MCP probe via
    `StreamableHTTPClientTransport`

- `packages/agent-gateway-client/src/types.ts` — wire-level
  `agent_intervention_request` / `agent_intervention_response` event
  variants + payload interfaces. Re-exported through the package barrel.

- `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's
  `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter
  rewrites `apiName` to `askUserQuestion` so the renderer routes on a
  clean domain key. Identifier stays `claude-code`. Applied to both the
  main-agent and subagent paths for symmetry (subagent ask isn't
  expected today, but doesn't hurt).

- `src/server/routers/lambda/aiAgent.ts` — Zod input schema for
  `aiAgent.heteroIngest` extended with the two new event types so the
  CLI sandbox can forward them through the server.

No producer wiring yet — Steps 3-5 plug this into Electron main, the
renderer executor, and the new UI.

*  feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3)

Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the
desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now
goes live during real prompts; renderer-submitted answers route back via
new IPC.

Changes
- `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add
  optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so
  controller-managed temp configs flow into the driver.
- `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts`
  — append `--mcp-config <path>` when provided. Disallowed-tools pin
  stays so CC's built-in AskUserQuestion remains off (avoids double-
  registration of the same tool name).
- `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts`
  - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt
    (de-duped concurrent first-callers via in-flight promise).
  - Per-op `setupInterventionForOp(opId, sessionId)`: registers an
    `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with
    `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no
    ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()`
    into the existing `heteroAgentEvent` broadcast.
  - Cleanup paths: exit handler `await intervention.cleanup()` settles
    pending MCP handlers + unlinks the temp config; pre-spawn errors
    short-circuit the same cleanup so we don't leak bridges on
    `buildSpawnPlan` / trace-session failures.
  - `before-quit` stops the MCP server (in addition to killing CC
    processes).
  - New `@IpcMethod() submitIntervention({ operationId, toolCallId,
    result?, cancelled?, cancelReason? })` — renderer side will dispatch
    answers / cancellations through this in Step 4/5.
  - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`.
- `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy
  for `submitIntervention`.
- New `claudeCode.test.ts` covers the four driver-arg paths
  (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay
  disallowed). Existing 28 controller tests still pass.

What still doesn't run end-to-end
- The renderer `heteroExecutor` doesn't consume `agent_intervention_request`
  yet — events go through the broadcast but the chat store ignores them.
- No UI to render the intervention card or to call `submitIntervention`.
Both lands in Steps 4/5 next.

*  feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4)

Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId`
from MCP `_meta`) instead of a random UUID, so the
`agent_intervention_request` event references the same id as the existing
tool message on the renderer side.

Renderer-side `heteroExecutor` learns the new event:

- Added `persistInterventionRequest(...)` next to `persistToolResult` —
  stamps `pluginState.askUserQuestion` (apiName + identifier + questions
  parsed from `arguments` + deadline + status='pending' + toolCallId)
  onto the matching tool message via `messageService.updateToolMessage`.
- New branch in `handleStreamEvent` for `'agent_intervention_request'`:
  defers behind `persistQueue` (so it lands AFTER `persistToolBatch`
  populates `toolMsgIdByCallId`), then mirrors the same pluginState onto
  the in-memory message via `internal_dispatchMessage` so the UI lights
  up immediately — no fetchAndReplaceMessages round-trip needed.
- The eventual `tool_result` for the same toolCallId hits the existing
  `tool_result` branch unchanged: it overwrites `pluginState` with
  whatever the result carries (typically undefined for our MCP tool, so
  `pluginState.askUserQuestion` clears and the intervention UI yields to
  the regular Render).

Bridge tests cover the new contract:
- caller-supplied toolCallId becomes the wire correlation key
- duplicate-toolCallId pendings reject loudly so two-handler clobbers
  surface immediately

153 package tests + 1167 desktop main tests + 51 hetero executor tests
still green; type-check clean.

*  feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5)

Dedicated Render for the synthetic `askUserQuestion` apiName the adapter
rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives
under CC's render registry so the existing chat tool-detail flow picks
it up automatically — no changes to the conversation framework.

- New `AskUserQuestionItem` / `AskUserQuestionArgs` /
  `AskUserQuestionPluginState` types (mirrors CC's own
  AskUserQuestion schema verbatim).
- `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'`
  member so the renders / inspectors / streamings registries can key
  off the same enum value.
- `client/Render/AskUserQuestion/index.tsx` is the component:
  - `pluginState.askUserQuestion?.status === 'pending'` → renders the
    questions form (Select for single-select, CheckboxGroup for
    multi-select), a 5-min countdown ticking once a second, Submit /
    Skip buttons. Reads `operationId` via `messageOperationMap` so we
    can route through `heterogeneousAgentService.submitIntervention`.
  - Otherwise → renders the questions as muted captions plus the
    final answer text from `content`. Surfaces a warning when the
    tool_result was an error (timeout / cancelled / session ended).
  - Submit button stays disabled until every question has a
    selection; Skip always enabled (sends `cancelled: true`).
- `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers
  the new component.

What this does NOT do
- Doesn't touch `BuiltinToolInterventions` — the form is rendered
  inside the regular tool body (Render slot), not the canonical
  intervention slot. Cleanest for now: the framework intervention
  flow assumes `submitToolInteraction` store actions, which would
  fight our IPC path. We can refactor onto that surface later if
  CC grows additional interactions (approval, file picker).
- Doesn't translate strings — i18n in a follow-up.

Type-check clean. Step 6 (real desktop e2e via CC) is next.

*  feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up)

Step 5 registered the Render component but stopped at the registry — the
chat tool-detail still returned the loading placeholder while
`isToolCalling` was true, so users only ever saw a spinner during the 5
min intervention window.

Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on
CC + apiName=askUserQuestion tool messages) and route to the registered
builtin Render inline before the placeholder branch. Once the
intervention resolves, the eventual `tool_result` clears
`pluginState.askUserQuestion` and the regular Render takes over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up)

LOBE-8519 left two TODOs in `generationSlice` where hetero runtime
silently fell through to client mode — regenerate would secretly hit the
agent's underlying LLM, and continue would synthesize a fake "please
continue" turn that confuses CC / Codex.

- regenerateMessage: re-create the assistant row branched off the same
  user message, resolve resume sessionId (drop on cwd mismatch), then
  spawn a child `execHeterogeneousAgent` op so Stop only kills the
  executor, not the parent regenerate op. Mirrors sendMessage's hetero
  branch.
- continueGenerationMessage: hetero CLIs have no continue primitive —
  each prompt is a fresh user turn — so bail out instead of polluting
  the session.
- continueGenerationMessage: gateway mode now branches a server-side
  resume run instead of falling through to client.

Surfaced while testing CC AskUserQuestion end-to-end on the
LOBE-8725 branch (regenerating after an answered question went through
the wrong runtime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2

Two bugs surfaced when invoking the local-testing helper from a fresh
session on macOS:

- `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit
  code propagates through `pipefail`. With `set -e`, an empty pid set
  silently kills the whole script — `do_start` reported success, no
  Electron, no error. Trail with `|| true`.
- `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`;
  process-tree teardown still works because `expand_descendants` walks
  the tree directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725)

`AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across
every CC subprocess. The SDK transport latches `_initialized=true`
after the first `initialize`, so the second op's CC subprocess sees
`Invalid Request: Server already initialized` (400) and reports the
`lobe_cc` server as `failed`. From the model's POV the MCP tool is
absent — it falls back to ToolSearch, can't find anything, and
verbalizes the question instead.

Refactor to the canonical multi-tenant pattern: one transport + one
`McpServer` per session, looked up by the SDK-managed `mcp-session-id`
header. New transports are minted on the first POST without a session
id (must be an `initialize` request); subsequent requests route via
the stored map; `onsessionclosed` cleans up.

The first run of any process still works as before — this only matters
once a second op spins up. Added a 3-op sequential regression test
that fails on the old single-transport implementation and passes now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725)

Step 5's first cut shoehorned the pending form into the Render slot and
drove submit/skip with a custom `pluginState.askUserQuestion.status`
field, which forced three layers of glue:

- `Tool/Detail` had to bypass the loading placeholder via an
  identifier+apiName hardcode so the form would surface during
  `isToolCalling`
- The executor had to `messageService.getMessages → replaceMessages`
  after `agent_intervention_request` to drag the freshly-created tool
  row into in-memory state (the framework's own `tool_end →
  fetchAndReplaceMessages` only fires after the user answers)
- The executor also had to `associateMessageWithOperation` for the tool
  row so the form could look up the running CC op for IPC

All three were patches around skipping the canonical surface. This
commit moves AskUserQuestion onto `pluginIntervention.status='pending'`
and the `BuiltinToolInterventions` registry, which the framework
already drives end-to-end:

- `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx`
  — pure form, no IPC, no store reads. Resolves through the standard
  `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback.
- `Render/AskUserQuestion` shrinks to the answered/aborted view only;
  the framework hides Render while pending, so no status switching.
- New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}"
  chip in the inline tool body, matching the rest of CC's tools.
- Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new
  `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`;
  `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry.

Hetero needs a different action handler than `submitToolInteraction`
(which spawns `executeClientAgent` — wrong for a CC subprocess that's
already blocked on an MCP call). Two thin pieces wire that:

- `submitHeteroIntervention` (chat store) — sets
  `pluginIntervention` via `optimisticUpdateMessagePlugin` (which
  already syncs DB + in-memory + parent-assistant `tools[].intervention`
  in one shot), then forwards the answer through
  `heterogeneousAgentService.submitIntervention` IPC. Operation lookup
  walks the tool message's `parentId` to hit the assistant's
  `messageOperationMap` entry — drops the explicit
  `associateMessageWithOperation` call from the executor.
- `customInteractionHandlers.isHeteroInteractionIdentifier` flags
  `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits
  there before reaching the existing `submitToolInteraction` path.

Executor change collapses to one line:
`optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`.
The post-intervention refresh, the associate call, and the
`persistInterventionRequest` helper all go away.

Removed:
- `AskUserQuestionPluginState` type (custom field is gone)
- `Tool/Detail` `askUserPending` inline-render branch
- Executor `messageService.getMessages + replaceMessages` round-trip
- Executor `associateMessageWithOperation` for tool rows
- `persistInterventionRequest` helper

Verified end-to-end against a real CC subprocess on desktop:
- Inline body shows the new Inspector chip; pending form lives in the
  bottom InterventionBar (canonical surface)
- Submit ships answer through MCP, CC continues with structured result
- Skip flips status to `rejected`, framework's RejectedResponse
  shows "User skipped"; CC receives isError and falls back to text
- `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op
  (the per-session transport fix from the previous commit)
- `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725)

Select dropdown was the wrong primitive — it hides options behind an extra
click and doesn't read like a question to answer. CC's underlying tool is
1-4 questions × 2-4 options, so the whole option set always fits inline.

- Each option renders as a clickable card: numbered chip (1/2/3/4) +
  bold label + secondary description on a single row. Hover tints the
  background; selected state lights up `colorPrimary` on both the chip
  and the card outline so the pick is unmistakable at a glance.
- Multi-select (`q.multiSelect`) toggles instead of replacing, with a
  "(multi-select)" hint in the question header.
- Multi-question support gets a proper visual hierarchy: each question
  past the first sits below a dashed divider, headed by a `Q1/N` tag
  + the original `q.header` chip. The `Q*/N` lets the user track
  progress without counting.
- Inspector picks up the question count too: now shows
  "askUserQuestion · {first header} +N" when multiple are queued.

Verified end-to-end on desktop with a CC-driven 2-question prompt
(4-option + 3-option). Both selections feed back to CC as a single
"User answers" payload, CC echoes both picks in its continuation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725)

- Multi-question forms now use a top tab strip; single question renders inline.
- Picking a single-select option auto-advances to the next unanswered question.
- Drafts persist to tool message `pluginState.askUserDraft` so picks survive
  remount / HMR; new `setInterventionDraft` action on the chat store dispatches
  the pluginState patch.
- Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for
  every unanswered question instead of letting the bridge time out into a
  cancelled isError — model gets a structured answer it can act on.
- Visual: selected option now uses filled `colorPrimaryBg` + right-aligned
  check icon; index chip stays neutral.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725)

The async exit-handler cleanup raced Electron's main-process teardown and
left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync
unlink in the quit hook is the only reliable guarantee.

Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q
or `app.quit()`, not on external kills (test harness, OS shutdown).

Verified by manual test: pending askUserQuestion forms now leave zero
residue after both Cmd+Q and SIGTERM paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725)

Submit now writes the structured `{ questionText: pickedLabel(s) }` payload
to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so
Render no longer has to scrape the bridge's prose `User answers:` content.

Render shows one Q&A block per question — header + question + a checkmark
card per picked option (multi-select fans out into multiple rows). Falls
back to a `—` placeholder when answers are missing (older messages or
skipped flows), and keeps the existing `pluginError` warning for cancel /
no-answer paths.

Also surfaces the answers in the Skill state inspector tab, which was
previously empty for completed askUserQuestion messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725)

Locks down the regression fixed in c0de0cdb7c — async exit-handler cleanup
losing to Electron's main-process teardown. Four cases: `before-quit`
(Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown),
`SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not
throw on the second pass).

`process.on` and `process.exit` are stubbed in the signal-path tests so the
controller's listener attaches to a spy, not the test runner's process —
otherwise we'd leak a real SIGTERM listener every test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 02:16:24 +08:00
Neko ccc8ee1315 ️ perf(agent-signal,prompts,types,database,server): fixed many minor self-review issues, harden the structure, verified with eval (#14647) 2026-05-11 00:46:30 +08:00
Arvin Xu 07eef8e7d9 💄 style(copyable-label): wrap long tool-call params instead of truncating (#14640)
* 💄 style(copyable-label): wrap long values instead of truncating

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(copyable-label): make wrap an opt-in via Descriptions prop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(descriptions): omit GridProps wrap to avoid type collision

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:29:47 +08:00
Arvin Xu ca59baa814 💄 style: format tool execution time as Xmin Ys instead of X.Y min (#14641)
🐛 fix: format tool execution time as `Xmin Ys` instead of `X.Y min`

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:28:44 +08:00
Arvin Xu 0f9b6904fd 🐛 fix(model-runtime): enrich stream parse errors with provider/model context (#14636)
*  feat(model-runtime): enrich stream parse errors with provider/model context

When the OpenAI / Anthropic SDK iterator throws (most often a JSON
SyntaxError on a malformed SSE chunk — e.g. an upstream response with an
illegal backslash escape), `convertIterableToStream` previously only
surfaced `message`/`name`/`stack`. Downstream error logs (agent-gateway
errors table) end up with just "Bad escaped character in JSON at
position 160050" and no way to correlate which provider/model produced
it or whether the same offset keeps recurring.

This change threads optional `{ provider, model }` context through
`convertIterableToStream` / `readableFromAsyncIterable` and enriches the
FIRST_CHUNK_ERROR payload with:

- `provider` / `model` so triage can group identical upstream failures
- `parsePosition` extracted from V8 JSON SyntaxError messages
- `causeName` / `causeMessage` when `error.cause` is set (many wrapped
  errors carry the actionable detail in `cause` and the bare triplet
  drops it)

Threaded through OpenAI/Responses/Anthropic stream handlers, which all
already receive `payload` containing provider/model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): walk error.cause for parsePosition + JSON-safe payload

Two review findings on #14636:

1. Wrapped SyntaxErrors lost their parsePosition. Provider SDKs commonly
   rethrow `JSON.parse` failures wrapped in their own error class
   (e.g. `APIError(cause: SyntaxError)`), so the outer `error.name` is
   no longer `'SyntaxError'` and the previous check skipped extraction
   for the exact case this enrichment was meant to diagnose. Now
   `extractParsePosition` walks both the outer error and any `Error`
   cause, and accepts any error whose message still carries the
   `"JSON at position N"` signature even if the SyntaxError name was
   lost in wrapping.

2. Cause cloning could blow up the entire diagnostic path.
   `structuredClone` succeeds on values that `JSON.stringify` later
   throws on (BigInt, circular refs), so a non-Error cause carrying
   either would surface as `payload.cause = clonedObject`, then the
   outer `JSON.stringify(payload)` would throw inside the catch handler,
   and the FIRST_CHUNK_ERROR chunk never gets emitted. Replaced with
   `safeJsonStringify` (BigInt → string, cycles → `[Circular]`) and
   route the cause object through `toJsonSafe` so the returned shape is
   always plain JSON.

Added tests for both: a wrapped APIError(cause: SyntaxError) yields
parsePosition, and a cause containing both BigInt and a circular ref
still emits a parseable error chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:09:23 +08:00
Arvin Xu a9f41c2217 🐛 fix(home): strip markdown links from daily-brief input placeholder (#14635)
The daily-brief hint will start carrying `[name](url)` markdown links so
the AI can resolve referenced entities when the user submits via the
hint. The placeholder layer is the only consumer that wants the visible
label without the link syntax — extract a small `stripMarkdownLinks`
util and apply it at `InputArea/index.tsx` only. `useSend` continues to
forward the raw hint, so the agent still receives the link in the
outgoing message.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:28:10 +08:00
YuTengjing 80916c05d9 🐛 fix: consume visual content parts in server runtime (#14637) 2026-05-10 18:33:30 +08:00
Arvin Xu 2615c00480 feat(bot): gate device tools by sender identity (#14634)
*  feat(bot): gate device tools by sender identity (LOBE-8715)

External users who @-mentioned a bot ran the agent as the bot owner and
could call LocalSystem / RemoteDevice tools — a confused-deputy hole that
let any group member indirectly read/write the owner's machine.

- `ChatTopicBotContext` carries `senderExternalUserId` + `isOwner`
- `BotMessageRouter` / `MessengerRouter` compute `isOwner` at the entry
  point (fail-closed when `settings.userId` is missing)
- `resolveDeviceAccessPolicy` maps sender identity to
  `{ canUseDevice, reason }`; trusted-list branch is reserved for future
  work without engine changes
- `AgentToolsEngine` gates `LocalSystem` + `RemoteDevice` on `canUseDevice`
- `RemoteDeviceManifest.systemRole` is no longer injected on
  external-sender turns — closes the device-list information leak
- Per-call audit log (`lobe-server:agent-device-tool-audit`) at the
  dispatch site records sender, isOwner, reason, identifier, apiName

Fixes LOBE-8715

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🚨 chore(bot): replace `any` on botContext / botPlatformContext with concrete types

Picks up the existing `BotPlatformContext` (`@lobechat/context-engine`)
and `ChatTopicBotContext` (`@lobechat/types`) — both already exported —
instead of the inherited `any` placeholders on:

- `OperationCreationParams.{botContext, botPlatformContext, deviceAccessPolicy}`
- `InternalExecAgentParams.botPlatformContext`
- `RuntimeExecutorContext.botPlatformContext`

`deviceAccessPolicy.reason` is now `DeviceAccessReason` instead of `string`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 fix(bot): clear activeDeviceId when canUseDevice=false (LOBE-8715)

The previous patch gated `LocalSystemManifest` in the engine's enabledToolIds,
but `buildStepToolDelta` re-injects local-system from `state.metadata.activeDeviceId`
on every step regardless of whether the engine excluded it. Auto-activation
in `aiAgent.execAgent` populated `activeDeviceId` whenever
`(discordContext || botContext) && onlineDevices.length === 1`, so an
external bot sender with one device online could still get local-system
tools against the owner's device.

- `aiAgent/index.ts`: skip `activeDeviceId` derivation entirely when
  `canUseDevice` is false. `deviceSystemInfo` short-circuits naturally on
  `if (activeDeviceId) {...}`, so no extra change needed there.
- `RuntimeExecutors.ts`: belt-and-suspenders — if
  `state.metadata.deviceAccessPolicy.canUseDevice` is false, swallow
  `activeDeviceId` before passing to `buildStepToolDelta`, so a future
  plumbing bug at the source can't reopen the bypass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 feat(bot): allow device tools on personal-scope platforms (WeChat) (LOBE-8715)

Not every bot platform can identify an owner. WeChat's LobeHub integration
encodes every inbound thread as 1:1 (`packages/chat-adapter-wechat/src/adapter.ts:465`)
and its settings schema has no `userId` field, so `isOwner` is structurally
false on every WeChat turn. The previous policy denied every WeChat call
with `bot-owner-not-configured` — fail-closed but unusable.

This commit treats platforms whose integration is structurally personal-
scope as trusted. WeChat is the only member today; LINE is intentionally
excluded because its adapter handles group/room threads even though its
schema also lacks `userId` — those must be fixed at the schema layer
before being whitelisted.

- New `bot-personal-platform` reason in `DeviceAccessReason`
- `PERSONAL_SCOPE_BOT_PLATFORMS = new Set(['wechat'])`
- Personal-scope check sits AFTER `isOwner` so a future WeChat schema
  with a `userId` field still resolves as the more specific `bot-owner`
- Tests: WeChat without isOwner → allow; WeChat with isOwner=true → still
  `bot-owner` (more specific wins); regression guard ensuring Discord /
  Slack / Telegram / Feishu / Lark / QQ / LINE keep going through the
  standard isOwner gate

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(engine): opt existing device gate tests into canUseDevice=true (LOBE-8715)

The `LocalSystem` / `RemoteDevice` enable rules now short-circuit on
`canUseDevice` (default `false`), so tests that exercise the
engine-internal gates (`runtimeMode`, `deviceContext`, `clientRuntime`)
must explicitly pass `canUseDevice: true` — otherwise they assert the
right behavior for the wrong reason or fail outright (e.g. the desktop
RemoteDevice-suppression case the reviewer flagged).

- All `LocalSystem` / `RemoteDevice` / `LocalSystem + RemoteDevice` /
  `clientRuntime === "desktop" (Phase 6.4)` blocks now set
  `canUseDevice: true`.
- The "disable RemoteDevice in bot conversations" test was repurposed:
  the dropped `!isBotConversation` clause is now subsumed by `canUseDevice`,
  so for a trusted bot caller (canUseDevice=true) RemoteDevice DOES surface.
  The original intent — block when caller is untrusted — is captured in
  the new `canUseDevice gate` block.
- New `canUseDevice gate` describe block asserts:
    1. `canUseDevice=false` blocks LocalSystem even on a desktop caller
    2. `canUseDevice=false` blocks RemoteDevice with proxy configured
    3. Omitting `canUseDevice` → fail-closed default (deny)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(execAgent): set isOwner=true on device auto-activation tests (LOBE-8715)

These pre-existing tests model an owner using the bot through Discord and
assert that `activeDeviceId` auto-populates when one device is online.
After LOBE-8715, `activeDeviceId` is gated on `canUseDevice` from
`resolveDeviceAccessPolicy`, so a `botContext` without `isOwner: true`
resolves to `bot-external-sender` → `canUseDevice=false` →
`activeDeviceId=undefined`.

Filling out the `botContext` mocks with `isOwner: true` (plus the other
required fields the type now demands) preserves the tests' original
intent while exercising the new gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:44:56 +08:00
YuTengjing 58318e97df 🐛 fix: store onboarding interests as keys (#14624) 2026-05-10 16:44:22 +08:00
Arvin Xu 4b8105b8b2 🔥 chore(web-crawler): remove WeChat URL rules (#14633)
Drop the `weixin.sogou.com` and `mp.weixin.qq.com` rules from the crawler
URL ruleset since they are no longer needed.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:28:53 +08:00
LobeHub Bot 2a65f81f0d 🌐 chore: translate non-English strings to English in apps/cli, apps/device-gateway, and apps/desktop scripts (#14626)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:04:17 +08:00
LiJian 1d2f0dcdb9 🐛 fix(hetero-agent): sync new-step assistant across replicas (#14631)
* 🐛 fix(hetero-agent): sync new-step assistant across replicas

* 🐛 fix(hetero-agent): tighten new-step assistant fallback

* fix: slove the test
2026-05-10 14:05:20 +08:00
LiJian 2098ac8374 🐛 fix: remove the old cron job from lobehub (#14630)
* fix: remove the old cron job from lobehub

* fix: add some ts back
2026-05-10 13:49:32 +08:00
LiJian cfe618fb50 🐛 fix: refresh content baseline from DB on every ingest call (#14603)
* 🐛 fix: refresh content baseline from DB on every ingest call

Vercel serverless routes consecutive batches to different Lambda
instances. A warm replica's in-memory `accumulatedContent` only
reflects batches it processed; it has no visibility into batches
handled by other replicas.

The failure pattern (worst when a repo is selected, since CC makes
tool calls early):

1. Lambda A — batch 1 (text "你好!...") → flushBatchContent writes
2. Lambda B — batch 2 (text "...任务。") → restores from DB, appends,
   writes longer text to DB
3. Lambda A — batch 3 (tools_calling only, warm state) → its stale
   `accumulatedContent` = batch-1 text → persistMainToolBatch Phase 1
   writes `{ tools, content: stale-short-text }` → OVERWRITES the
   correct longer DB value → content truncated at "你"

Fix: re-read the current assistant message from DB at the start of
every `ingest()` call. Since `flushBatchContent` writes at the end of
every batch, DB is authoritative. The refresh gives each Lambda the
latest flushed baseline, so new text in the current batch extends
the correct full string.

Cost: one extra `findById` round-trip per warm ingest call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

*  feat: auto-inject GitHub OAuth token into CC sandbox

Previously the GitHub token was only resolved when repos were selected
AND GITHUB_CRED_KEY was explicitly configured in the agent config —
so CC running without pre-selected repos had no GitHub access and had
to ask the user for a PAT manually.

Changes:
- aiAgent/index.ts: always try to resolve the token using key 'github'
  (standard LobeHub OAuth connector default); GITHUB_CRED_KEY still
  overrides. No longer guarded behind topicRepos.length > 0.
- sandboxRunner.ts: new buildCredsSetupScript() runs before CC starts:
    mkdir -p ~/.creds
    printf 'GITHUB_ACCESS_TOKEN=%s\n' <token> > ~/.creds/env
    gh auth login --hostname github.com --with-token
  Writes ~/.creds/env in the same format as injectCredsToSandbox(["github"])
  so CC can source it in sub-shells. Creds step runs before repo clone step.
- cloudHeteroContext.ts: system prompt now tells CC that GITHUB_TOKEN is
  set, gh CLI is pre-authenticated, and ~/.creds/env has GITHUB_ACCESS_TOKEN
  with the source/auth recipe for sub-shell usage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: adopt max-length content on DB refresh to guard flushBatch retry

The unconditional DB overwrite in ingest() broke the retry contract:
if flushBatchContent threw after events were already marked in
processedKeys, a retry on the same warm instance would read the stale
(shorter) DB value and wipe the in-memory chunks — which processedKeys
would then skip, losing them permanently.

Fix: only adopt the DB value when it is LONGER than in-memory.
This preserves both behaviours:
- Multi-replica stale (the original fix): DB has more content from
  another replica → dbContent.length > in-memory → adopt DB. ✓
- flushBatchContent retry on same Lambda: DB still has the old shorter
  value, in-memory has the correct accumulation → keep in-memory. ✓

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 12:22:37 +08:00
Arvin Xu e3cace359b 🐛 fix(hetero-agent): disable Claude Code AskUserQuestion to avoid auto-decline (#14629)
* 🐛 fix(hetero-agent): disable Claude Code AskUserQuestion to avoid auto-decline

CC's built-in AskUserQuestion self-injects an `is_error: "Answer questions?"`
tool_result inside the CLI in `-p` non-interactive mode before the host can
surface the questions, so the model falls back to plain-text prompting after
a wasted round-trip. Add `--disallowedTools AskUserQuestion` to both spawn
sites (desktop driver + lh hetero exec) so the model goes straight to text.

To be revisited once a local MCP-backed replacement is wired to LobeHub's
intervention UI.

* ♻️ refactor(hetero-agent): share CC base args, opt-in partial deltas

- Promote CLAUDE_CODE_BASE_ARGS in `@lobechat/heterogeneous-agents/spawn` to
  the canonical source of truth for invariant CC CLI flags (`-p`, stream-json
  IO, `--verbose`, `--disallowedTools AskUserQuestion`); export it so the
  desktop driver can compose on top instead of duplicating.
- Pull `--include-partial-messages` out of the base. It's now a
  `SpawnAgentOptions.includePartialMessages` flag, off by default so
  `lh hetero exec` standalone/sandbox runs don't pay for delta noise they
  don't render. The desktop driver opts in (chat bubble streams live).
- Permission mode stays caller-specific: desktop hardcodes bypassPermissions
  (always user-mode), the package keeps its root-vs-user branch for cloud
  sandbox.

* 🎨 style(hetero-agent): pass spawn-args builders an options object

Positional list grew to four args with mixed types — switch to a single
`BuildSpawnArgsParams` object so call sites read by field name and adding
future per-agent flags doesn't push every other caller around.
2026-05-10 12:15:04 +08:00
Arvin Xu ca6c9ad7a2 🐛 fix(local-system): guard readFile against binary blobs and oversized output (#14602)
* 🐛 fix(local-system): guard readFile against binary blobs and oversized output

Previously `lobe-local-system.readFile` would happily decode any extension
as UTF-8 and return the entire content. Reading a 27KB base64-encoded git
bundle blew up the next LLM call to 3.28M tokens / 416s and triggered a
DB rollback. The default 200-line cap was bypassed because base64 was a
single very long line.

Add four layers of protection in `readLocalFile`:
- Hard-reject extensions outside the text-readable + special-parser
  whitelist with a structured error pointing the agent at runCommand.
- Sniff the first 8KB and refuse files that look binary (null bytes or
  >30% non-printable chars).
- 10MB hard size cap before the file is read into memory.
- Cap each returned line at 8K chars and total output at 500K chars,
  with `truncated` / `linesTruncated` flags surfaced in the result.

Refs LOBE-8703.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(file-loaders): preserve UTF-16 text files without a BOM in binary sniffer

The binary sniffer rejected UTF-16LE/BE files that lacked a BOM because
their alternating 0x00 bytes tripped the null-byte heuristic. `TextLoader`
already has a `detectUtf16NoBom` heuristic for these Windows-style exports;
extract it to a shared `detectUtf16` util and run it in the sniffer before
the null-byte check, decoding with the matching variant for the printable
ratio test instead of declaring the file binary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(local-system): render WriteFile new files as a unified diff

Switch the WriteFile render from a syntax-highlighted preview to a
synthesized "new file" unified diff via PatchDiff, matching the
EditLocalFile visual. Markdown files keep their rendered preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(local-system): exercise readFile / readFiles end-to-end

The previous LocalFileCtr.readFile / readFiles tests deep-mocked
node:fs/promises and @lobechat/file-loaders. Since the controller is a
thin pass-through to readLocalFile, the assertions ended up testing
shell internals (already covered in packages/local-file-shell), and
broke as soon as readLocalFile gained new pre-flight checks.

Move them into a sibling LocalFileCtr.readFile.test.ts that runs
against a real tmpdir + real file-loaders, so adding more upstream
guards no longer requires touching this suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:01:24 +08:00
YuTengjing ecaec1bf9d feat: add user activity business hook (#14601) 2026-05-10 11:18:39 +08:00
Hardy 23dced5de9 ♻️ refactor(siliconcloud): sync models with API, fix duplicates, adjust reasoning params (#14464)
* ♻️ refactor(siliconcloud): sync models with API, fix duplicates, adjust reasoning params

* 🐛 fix(siliconcloud): fix GLM-4.7 checkModel casing to match model ID
2026-05-10 10:40:52 +08:00
AmAzing- b5c4abcaef 🌐 i18n: update banner copy translations (#14623) 2026-05-10 10:28:50 +08:00
AmAzing- e72f30e53e 💬 i18n: remove trailing punctuation from banner titles (#14622) 2026-05-10 10:23:55 +08:00
YuTengjing 7bd7baf6b6 feat: add Gemini 3.1 Flash-Lite provider cards (#14604) 2026-05-10 10:04:27 +08:00
YuTengjing 78fc0931b0 ♻️ refactor: remove model extend param options (#14607) 2026-05-10 10:02:35 +08:00
René Wang b15c9e43d4 📝 docs: add intro and screenshot to task scheduler changelog (#14585) 2026-05-10 09:53:02 +08:00
Neko 25ee8221a7 🐛 fix(database,utils,userMemories): should perfer to use paradedb.match(...) instead of hardcoded normalizer (#14590) 2026-05-10 01:39:16 +08:00
Arvin Xu 8fa7607747 🐛 fix(database): attach error listeners to Neon/Node pools to prevent Lambda crash (#14606)
* 🐛 fix(database): attach error listeners to Neon/Node pools to prevent Lambda crash

NeonPool (and NodePool) inherit pg.Pool semantics: when a backend connection
drops on an idle client the pool emits 'error'. With no listener Node
escalates that into uncaughtException — on Vercel this killed the entire
Lambda process (exit 129) and produced a 1805-crash avalanche in 5 minutes,
spiking Neon connection count from 30 to 330+ as half-closed sockets
accumulated (LOBE-8704).

Primary fix: attach `.on('error', ...)` to both pool variants in
`packages/database/src/core/web-server.ts` so the error is logged but
swallowed; the pool recovers on its own per pg docs.

Defense in depth: register `uncaughtException` / `unhandledRejection`
handlers in `instrumentation.ts` (gated to nodejs runtime) so any future
unhandled error doesn't take down the process either.

Refs: https://node-postgres.com/apis/pool#error

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔧 chore: drop process-wide uncaughtException handler

Per review on #14606: the catch-all listener in instrumentation.ts swallowed
every uncaughtException / unhandledRejection — not just NeonPool errors —
leaving the process in an undefined state instead of letting the platform
restart it, and would mask future production bugs.

LOBE-8704 is fully addressed by the targeted pool listeners in
packages/database/src/core/web-server.ts; the broad backstop is unnecessary
and unsafe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 01:30:16 +08:00
sxjeru d3159436e8 💄 style: Add new DeepSeek-V4 models (#14110)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-10 01:05:24 +08:00
Arvin Xu ca3879a23c 🐛 fix: gateway client-tool pluginState + drop redundant Exit code: 0 tail (#14596)
* 🐛 fix(agent-runtime): forward pluginState through gateway client tool result

Gateway-mode client tool results lost the `state` field at three points:
the toolResult Zod schema didn't declare it (silently stripped by safeParse),
the ToolResultPayload interface didn't carry it, and projectToExecutionResult
didn't return it. As a result the "技能状态" tab was always empty for tools
dispatched via Agent Gateway, even though clients send `state` correctly and
non-gateway paths persist it as `pluginState`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(prompts): suppress redundant `Exit code: 0` tail in command result

For successful runs, "Command completed successfully." already conveys
the same signal — appending "Exit code: 0" was just noise the LLM had
to skim past. Non-zero exit codes (130 SIGINT, 137 OOM, etc.) keep the
line so the diagnostic information remains available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(prompts): treat non-zero exit code as command failure in result header

`success` is the envelope ("the service responded") and `exitCode` is the
command's own status — they're independent. With `success: true` +
`exitCode: 137` the prior format rendered "Command completed successfully."
on top of a SIGKILL/OOM, lying to the LLM.

Now the header is derived from both: any non-zero exit folds the message
into the failure branch as "Command failed with exit code N[: error]".
The trailing "Exit code: N" line is gone — the same info now lives in the
header, so success rendering is also free of the redundant zero tail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:53:31 +08:00
sxjeru 7a3de98348 🐛 fix(gemini): handle zero cachedContentTokenCount in usage conversion (#14567)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-10 00:36:26 +08:00
Arvin Xu 56ddccdc1c 💄 style(topic): add copy session ID to topic dropdown menu (#14595)
 feat(topic): add copy session ID to topic dropdown menu

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:26:39 +08:00
Arvin Xu cd2c074843 feat: home daily brief with linkable welcome + paired input hint (#14589)
*  feat: home daily brief with linkable welcome + paired input hint

Add a per-user "daily brief" surface to the home page. A cron-driven
backend (in the cloud repo) writes paired { welcome, hint } entries
into Redis under `aiGeneration:home_brief:{userId}`. This change exposes
that data through:

- `RedisKeys.aiGeneration.homeBrief` key builder
- `home.getDailyBrief` lambda router query that reads the cached payload
- `homeService.getDailyBrief` client and `useHomeDailyBrief` hook with
  shared rotating index via `useSyncExternalStore`
- `WelcomeText` runs a custom typewriter (supports real `\n` line breaks
  and parses inline `[label](url)` markdown links so cached entity
  references become clickable; falls back to the i18n welcome list)
- `InputArea` shows the matching hint as the chat input placeholder

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract daily-brief Redis read into HomeService

Mirrors the AgentService pattern: the lambda home router was reaching
into Redis directly, which mixed I/O concerns with the routing layer.
Move the read into a dedicated `HomeService` so future home-page reads
have a clear home and the router stays thin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix: keep WelcomeText typewriter index in sync with shared store

Before: DailyTypewriter held its own `sentenceIndex` state, separate
from the module-level `currentIndex` in `useHomeDailyBrief`. After
the home page rotated past the first pair, navigating away and back
remounted the typewriter and reset its local index to 0 — but the
external index stayed where it was. InputArea read the hint at the
stale external index while WelcomeText restarted at pair 0, breaking
the welcome / hint pairing.

Make the typewriter fully controlled: drop the local `sentenceIndex`,
expose `currentIndex` from `useHomeDailyBrief`, and pass it as a prop.
On `pause`, the typewriter just calls `onSentenceComplete` — the
parent flips the shared index, the new prop flows back, the reset
effect re-arms typing for the new sentence. Single source of truth,
remount-safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(redis): factor JSON cache reads into getJSONFromRedis util

Three call sites were inlining the same "fetch + null-check + JSON.parse
+ try/catch" recipe against a scoped Redis client:

- AgentService.getAgentWelcomeFromRedis
- HomeService.readDailyBriefFromRedis (new)

Move the recipe into a small `getJSONFromRedis<T>` helper next to the
other Redis utilities and have both services delegate to it. Caller
keeps responsibility for resolving the right scoped client (we don't
want to hide the prefix selection inside the helper).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): use live editor content for Enter-to-send guard

When typing into the home input and pressing Enter immediately, the
empty-message guard sometimes wrongly bailed out. The cause: the guard
read the cached `inputMessage` in `useChatStore`, which is populated by
the editor's async `onMarkdownContentChange`. Lexical commits its
update on a microtask after each keystroke, so a fast type-then-Enter
fires the send path before the cache catches up.

`SendButtonHandler` already passes `getMarkdownContent` through — read
it instead, falling back to the cached value if the handler is invoked
without it. Also propagate the live message into all `inputActiveMode`
branches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(home): accept daily-brief hint as the message on empty Enter

Press Enter on the empty home input → send the currently displayed
daily-brief hint as the message (smart-compose / Tab-to-accept style).
Trims the cosmetic trailing ellipsis and rotates the carousel so the
next press picks up a different pair.

Falls through to the previous "no content, skip" path when there's
neither a typed message nor a hint to use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(home): scope daily-brief SWR key + rotation index by userId

The SWR key was a constant string, so an account switch within the same
SPA session — sign out + sign in as another user, or a multi-account
swap that keeps `isSignedIn` true — could surface the previous user's
cached pairs from the same slot. The keyspace in Redis is per-user,
so the served data leaks personalization.

Include the resolved userId in the SWR key, and reset the module-level
rotation index on user change so the new account starts from pair 0
rather than inheriting a stale offset (which could also point past the
end of a smaller pairs list).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:52:13 +08:00
LiJian f35e2d843a 🐛 fix: first inject the cloudecc runtime session should use the existingStatus (#14592)
* 🐛 fix: skip reconnect when gateway action already established a connection

Race condition on new-topic first message:
1. switchTopic loads runningOperation → useGatewayReconnect fires
2. executeGatewayAgent calls connectToGateway (status: connecting)
3. reconnectToGatewayOperation overwrites with resumeOnConnect:true
4. Gateway sees resume on a brand-new session → no events → stuck

Second message works because the client store's runningOperation is
stale (from the first op), so SWR deduplications and no reconnect fires.

Fix: bail out of reconnectToGatewayOperation if gatewayConnections
already shows connecting/connected for that operationId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: always pass --cwd /workspace for cloud CC to ensure session resume

CC stores session files at ~/.claude/projects/<encoded-cwd>/.
Without an explicit --cwd the actual working directory can differ
between sandbox invocations, so --resume <heteroSessionId> fails
to locate the previous session files even though the container is
persistent and the ID is correctly stored in topic.metadata.

Default cwd to /workspace for cloud runs (desktop keeps its own
explicit path), guaranteeing a stable session-file location across
page reloads within the same sandbox lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: extend reconnect guard to cover all in-flight connection statuses

The previous guard only skipped reconnect for 'connecting'/'connected'
but the connection can already be in 'authenticating' or 'reconnecting'
by the time useGatewayReconnect fires, leaving the race window open.

Flip the condition: skip for any status that is not 'disconnected'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: restore cold replica state in HeterogeneousPersistenceHandler

Vercel serverless functions are stateless per-request, so `operationStates`
is empty on every `heteroIngest` call. loadOrCreateState always cold-creates.

#14539 fixed `toolMsgIdByCallId` restoration but left `accumulatedContent`,
`toolState.payloads`, and `toolState.persistedIds` empty on cold load,
causing two bugs:

- Content truncation: cold instance starts with `accumulatedContent=''`,
  accumulates only the current batch's text, then writes that shorter string
  on the next step boundary or terminal — overwriting the longer content the
  previous write had already stored in DB.

- Tool duplication / tools[] overwrite: `persistedIds={}` on cold load
  means every `tools_calling` event re-creates already-persisted tool
  messages, and `payloads=[]` means phase 1/3 writes only the current
  batch's tools, wiping previous tools from `assistant.tools[]`.

Fix: in `loadOrCreateState`, fetch the current assistant message and restore
`accumulatedContent`, `accumulatedReasoning`, `toolState.payloads`, and
`toolState.persistedIds` from it. Cold load is now equivalent to warm load.

Also adds two regression tests covering the cold-replica scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 23:44:09 +08:00
Arvin Xu 53f6fe43b4 💄 style: use visible divider between queued messages (#14593)
💄 style(QueueTray): use visible divider color between queued messages

The previous `colorBorderSecondary` rendered the divider effectively
invisible on the elevated dark surface. Switch to `colorFillTertiary`
so stacked queued messages have a perceptible separator.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:06:24 +08:00
Rdmclin2 69b1d9503e 🐛 fix: slack connect error & slash commands (#14591)
* feat: displayToolCalls default undefined

* chore: restrict billboard to home page

* fix: add slack bot scope

* fix: show billboard in home nav
2026-05-09 21:43:13 +07:00
Neko 395eb8598c feat(agent-signal,prompts,database): self-review now proposal actions to briefs, and automatically execute actions (#14583) 2026-05-09 22:34:19 +08:00
Innei 746bf4f316 💄 style(intervention): polish confirmation bar layout (#14587) 2026-05-09 22:21:39 +08:00
AmAzing- 58dd297141 chore: Refine homepage banner copy for channels and skills (#14588) 2026-05-09 22:09:18 +08:00
AmAzing- a4e5a20b4d 🛠️ fix: unify SKILL.md frontmatter parsing and edit validation in agent documents (#14566) 2026-05-09 22:04:05 +08:00
LiJian 95f41f8cec feat: add signOperationJwt with 4h expiry for hetero-agent operations (#14586)
*  feat: add signOperationJwt with 4h expiry for hetero-agent operations

- Add `signOperationJwt(userId)` to internalJwt.ts with 4h expiry and
  `purpose: 'hetero-operation'`, so Claude Code / Codex tasks running
  beyond 5 minutes no longer hit 401 on heteroIngest / heteroFinish
- Update `execAgent` hetero path to use `signOperationJwt` instead of
  `signUserJWT`; gatewayToken continues to use 5m `signUserJWT`
- Add unit tests in `__tests__/internalJwt.test.ts` with correct mocks
  for `jose` (SignJWT class + importJWK) and `authEnv`, covering all
  three signing functions and the expiry difference assertion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔒 security: restrict hetero-operation JWT scope to heteroIngest/heteroFinish

A leaked 4-hour sandbox LOBEHUB_JWT must not be replayable against any
other authenticated lambda route.

- Forward `purpose` claim from JWT payload through validateOIDCJWT →
  tokenData → oidcAuth context so middlewares can inspect it
- oidcAuth: reject tokens with purpose 'hetero-operation' — they cannot
  reach any normal authedProcedure route
- New heteroOperationAuth middleware: exclusively accepts
  purpose 'hetero-operation' tokens, rejects all others
- Export heteroAuthedProcedure (baseProcedure + heteroOperationAuth +
  userAuth) from trpc/lambda/index.ts
- heteroIngest / heteroFinish now use heteroAgentProcedure built on
  heteroAuthedProcedure + serverDatabase + HeterogeneousAgentService
- Tests: heteroOperationAuth (4), oidcAuth (4), update heteroIngest
  test caller to supply purpose:'hetero-operation' context (23 total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 21:57:50 +08:00
lobehubbot 0516184b45 🔖 chore(release): release version v2.1.57 [skip ci] 2026-05-09 13:36:15 +00:00
lobehubbot f7fbc1c833 Merge remote-tracking branch 'origin/main' into canary 2026-05-09 13:33:21 +00:00
Innei 0f5fb54cb6 🚀 release: 20260509 (#14563)
# 🚀 LobeHub Release (20260509)

**Release Date:** May 9, 2026  
**Since v2.1.56:** 236 merged PRs · 19 contributors

> Agent Task System reaches general availability, the Agent Signal
pipeline runs nightly self-review with skill-aware policies, the
heterogeneous-agent runtime crosses replica boundaries, inline documents
become a first-class context source, and bot platforms expand across
Messager, Line, and Telegram.

---

##  Highlights

- **Agent Task System (GA)** — End-to-end task execution platform:
templates, tracking, comment tools, parent reassignment, scheduled cron,
and dependency-ordered batch runs. (#14540, #14515, #14517, #14272,
#14246, #14418, #14403, #14488)
- **Agent Signal nightly self-review** — Wired self-review loop with
prompt + DB support, exponential-backoff retry on receipt listing,
skill-aware policy, and improved skill-intent detection. (#14543,
#14542, #14281, #14409, #14526, #14437)
- **Inline documents in KB tool** — BM25 search and `docs_*` read for
inline document grounding; agent documents usable as VFS. (#14494,
#14222)
- **Inline agent cards in chat** — `lobeAgents` markdown tag renders
agent profile cards inline; clickable card after `createAgent`. (#14495,
#14493)
- **Heterogeneous agent runtime** — Cloud hetero exec pipeline steps 3+4
land, persistence recovers across Vercel replicas, server-side
ingest/finish handler, and `lh hetero exec` CLI. (#14486, #14539,
#14444, #14431)
- **Bot platforms expand** — Messager, Line, DM pair policy, and
messenger DB tables; Telegram API path restored. (#14442, #14207,
#14211, #14496, #14519)
- **Visual analysis tool** — New visual understanding tool, with trigger
tracking and flattened schema. (#14378, #14399, #14550)
- **DeepSeek V4 Pro as OSS default** — OSS deployments ship with
DeepSeek V4 Pro by default; DeepSeek Anthropic runtime supported.
(#14555, #14312)

---

## 🏗️ Core Agent & Architecture

### Agent Task System

- **Task System GA** — End-to-end execution platform now available.
(#14540)
- **Templates, comments, reparenting** — Template tracking, comment
tools, and parent reassignment. (#14515, #14517, #14488)
- **Cron + dependency-ordered runs** — Scheduled status with cron editor
and dependency-ordered subtask batches. (#14246, #14418, #14272)
- **Inspector + chip UI + batch tasks** — Task Inspector/Render
registry, batch `createTasks`/`runTasks`, and chip-based agent-documents
inspector. (#14403, #14404)
- **Recommend templates regardless of brief count** — Recommendations no
longer suppressed when briefs are sparse. (#14508)
- **Scheduling resilience** — Manual run no longer eats next scheduled
tick; recurring tasks survive brief resolution. (#14304, #14348)
- **Brief synthesis** — Auto-synthesize topic briefs; brief actions
revamp; mute resolved-brief icon on home. (#14324, #14228, #14452)
- **Task list & detail polish** — Topic operation ID exposed; task
drawer Gateway reconnect. (#14282)

### Agent Signal pipeline

- **Nightly self-review wired** — Prompt + DB support for the
self-review loop. (#14543)
- **Self-review activities push to briefs** — Activities during nightly
self-reflection now create briefs. (#14437)
- **Skill management policy** — New policy for Skill management running
inside Agent Signal. (#14281)
- **Skill intent detection & routing** — Improved detection plus direct
intent handling when `hintIsSkill`. (#14409, #14526)
- **Document tool outcome rendering** — Decision view restores missing
document tool outcomes. (#14534)
- **Exponential backoff retry** — Listing signal receipts retries with
jittered backoff. (#14542)
- **Easier-to-use signals** — Structural simplification +
recent-activities surface for receipts. (#14290, #14326, #14407)

### Heterogeneous agent runtime

- **Cloud hetero exec pipeline (steps 3 + 4)** — Refactor lands the next
two stages of the cloud hetero agent execution pipeline. (#14486)
- **Persistence recovery on Vercel** — Hetero state recovered across
replica boundaries. (#14539)
- **Server-side ingest/finish + persistence** — `aiAgent.heteroIngest` /
`heteroFinish` handlers. (#14444)
- **`lh hetero exec` CLI** — Standalone heterogeneous agent runs from
CLI. (#14431)
- **Gateway round-trip loading** — `execAgentTask` keeps the input box
in loading state through the full round-trip. (#14503)
- **Provider SDK type routing** — Provider routing now respects SDK
type. (#14520)
- **DeepSeek reasoning preserved** — `reasoning_content` preserved in
OpenAI-compatible runtime for DeepSeek models. (#14546)

### Knowledge & inline docs

- **KB tool BM25 + docs read** — BM25 search and `docs_*` read
integrated for inline documents. (#14494)
- **Agent documents as VFS** — FS-compatible output for agent documents.
(#14222)
- **`lobeAgents` markdown tag** — Inline agent cards rendered from a
markdown tag. (#14495)
- **Clickable agent card after `createAgent`** — Mentions and
recommendations become clickable. (#14493)
- **ExplorerTree** — Generic tree component built on `@pierre/trees` for
reusable explorer surfaces. (#14094)
- **Local file mention snapshots** — Mentions can now snapshot local
files. (#14278)

### Architecture

- **Agent Hono routes** — New agent routes added on Hono. (#14535)
- **`/api/agent` migrated to Hono** — Remaining `/api/agent` routes
finish their migration. (#14478)
- **Agent marketplace merged into web-onboarding** — Reduces package
fragmentation. (#14514)
- **Producer pipeline extracted** — Shared package for the producer
pipeline. (#14425)
- **`agentDispatcher.selectRuntimeType`** — New runtime selection
abstraction. (#14428)
- **pnpm v11 migration** — Workspace consolidated. (#14316)
- **Browser-compatible frontmatter parser** — Replaces `gray-matter`.
(#14435)

---

## 📱 Platforms & Integrations

- **Messager support** — New messager package wired into the chat
surface. (#14442)
- **Messenger DB tables** — IM bot integration gains its persistence
layer. (#14496)
- **Line bot** — Initial Line support and downstream optimization.
(#14207, #14448)
- **DM pair policy** — Group/DM pair-based delivery. (#14211)
- **Telegram API restored** — Missing Telegram API path reconnected.
(#14519)
- **xAI Responses tools stabilized** — Plus unsupported parameter
handling. (#14462, #14445)
- **Volcengine websearch via ResponseAPI** — Built-in websearch for
Volcengine. (#14216)

---

## 🤖 Models & Providers

- **DeepSeek V4 Pro default for OSS** — OSS distribution defaults to
DeepSeek V4 Pro. (#14555)
- **DeepSeek Anthropic runtime** — Anthropic-shape runtime support for
DeepSeek. (#14312)
- **GPT-5.5 / GPT-5.5 Pro** — New OpenAI tier. (#14142)
- **Grok 4.20 / Grok 4.3 / LobeHub-hosted Grok 4.3** — (#14253, #14382,
#14446)
- **Gemma 4 + provider settings normalization** — (#13313)
- **gpt-image-2 + step-image-edit-2** — (#14253, #14329)
- **Model bank refresh + original-pricing display** — Batch model
updates and pricing surfaces. (#14070, #14391)
- **Hunyuan migrated to TokenHub for Hy3 Preview** — (#14108)
- **Reject lobehub model ids no longer in the bank** — (#14261)
- **Hide runtime-only aliases** — Runtime-only model aliases no longer
leak into the model picker. (#14552)

---

## 🖥️ User Experience

### Onboarding

- **Shared prefix steps** — Language and privacy extracted as shared
prefix steps. (#14538)
- **Identity intervention card simplified** — Plus tool result renders
cleanup. (#14505, #14506)
- **Welcome polish + web-onboarding tool UI** — (#14475)
- **Templates fetched from market API** — (#14286)
- **Virtual model id for default onboarding model** — (#14311)
- **Skip / mode-switch footer behind feature flag** — Footer guarded for
desktop and web initialization. (#14560)

### Home & navigation

- **Home recents performance** — Recents refresh periodically and inline
task status; brief and task-template fetch overhead trimmed. (#14518,
#14516)
- **Home refactor + skill-connect recommendations** — Restructured home
with skill-connect recommendation system. (#14266, #14214)
- **Tasks in agent sidebar** — Tasks moved from welcome card into the
sidebar list. (#14500)
- **Sidebar collapse persists** — Home sidebar collapse state stored.
(#14473)
- **Agent-specific topic grouping** — Plus improved empty state and
agent identity in topic search. (#14225)
- **MentionMenu scroll fix** — Mention menu no longer clips inside chat
input. (#14533)

### Conversation & chat

- **Follow-up chips fill input** — Clicking a follow-up chip now fills
the input instead of sending immediately. (#14536)
- **Quick-reply chips below assistant messages** — (#14350)
- **Inline single-tool assistant group + leading sentence promotion** —
(#14244)
- **Assistant-group rendering** — Per-segment content overrides flow
into MessageContent. (#14504)
- **Tool call timer fix** — Timer no longer resets when tool calls
collapse or expand. (#14513)
- **Streaming re-render reduction** — Reference stabilization and
self-subscribing components. (#14470)
- **Topic chat drawer feedback input** — (#14392)

### Skills, agents, devtools

- **Managed skill folders** — Agent view displays managed skill folders
and aligns delete confirmations. (#14553)
- **Review tab + bulk git diffs** — New Review tab with bulk diffs;
gating uses effective working directory. (#14334, #14512)
- **Devtools gallery rebuild** — Plus Review polish, queue-tray images.
(#14423)
- **Agent mock devtools** — Playback & fixture viewer. (#14436)

### Desktop & CLI

- **App tray visibility setting** — (#14463)
- **Notification settings in desktop** — (#14491)
- **Multimodal input across CLI / shared spawn / desktop** — (#14433)
- **CLI bot + userId guide** — (#14258)

---

## 🔧 Tooling

- **Visual analysis tool** — New visual understanding tool with
flattened schema. (#14378, #14550)
- **GitHub marketplace tool UI** — (#14420)
- **Drop "Local" prefix and `____builtin` suffix from tool names** —
(#14364, #14289)
- **Sanitize provider tool names** — Avoids invalid characters from
external providers. (#14510)
- **Generation moderation context** — Moderation context passed through
the generation pipeline. (#14541)
- **Visual analysis trigger tracking** — (#14399)
- **Claude thinking signature sanitization** — History signatures
sanitized when replaying Claude conversations. (#14499)
- **Responses input media sanitization** — Assistant media sanitized in
Responses input. (#14497)

---

## 🔒 Security & Reliability

- **Security:** Removed the `/webapi/proxy` route and dead URL-manifest
plugin code to shrink the SSRF surface. (#14549)
- **Security:** Sessions revoked after password reset. (#14424)
- **Reliability:** Added `prompt_cache_key` to OpenAI chat requests for
stable cache hits. (#14349)
- **Reliability:** `onFinish` now fires even when the browser tab is
backgrounded mid-SSE stream. (#14461)
- **Reliability:** Better-auth session refetch preserves user fields
rather than overwriting them. (#14531)
- **Reliability:** User-memory queries sanitize backticks; user-memory
errors now explicitly injected so failures stay visible. (#14524,
#14525)
- **Reliability:** Auth captcha retries handled; input loading unsticks
on `auth_failed` and recoverable `auth_expired`. (#14346, #14419)
- **Reliability:** Trace snapshot finalized on error path. (#14440)
- **Reliability:** Drop `switchTopic` race under rapid sidebar clicks.
(#14115)
- **Reliability:** PDF chunking logic fixed to prevent vectorization
failure. (#14327)
- **Performance:** Marketplace fork uses a batched API for parallel
installs. (#14537)
- **Performance:** Review tab open latency cut ~9× on large dirty trees.
(#14338)

---

## 👥 Contributors

Huge thanks to **18 contributors** who shipped **236 merged PRs** this
cycle.

@hezhijie0327 · @sxjeru · @yueyinqiu · @octo-patch · @hardy-one ·
@Coooolfan · @CanYuanA · @BillionClaw · @arvinxx · @tjx666 · @Innei ·
@Neko · @AmAzing129 · @Rdmclin2 · @LiJian · @sudongyuer · @rivertwilight
· @cy948

Plus @lobehubbot for i18n and translation maintenance.

---

**Full Changelog**:
https://github.com/lobehub/lobe-chat/compare/v2.1.56...release/weekly-20260509
2026-05-09 21:30:37 +08:00
Innei feaaaba2a9 💄 style(settings): remove image avatar from lab input markdown rendering item (#14582) 2026-05-09 21:15:02 +08:00
YuTengjing 21f6f94bed 🐛 fix: polish task agent manager (#14569) 2026-05-09 20:58:29 +08:00
AmAzing- b180c03e04 feat: migrate Notion to LobeHub Market (#14578)
Migrate Notion to LobeHub Market
2026-05-09 20:55:26 +08:00
Arvin Xu 0d39dff2d5 🐛 fix(agent-runtime): recover malformed tool_call names instead of finishing silently (#14577)
* 🐛 fix(agent-runtime): recover malformed tool_call names instead of finishing silently

When an LLM emits tool_call names without the `____` separator (e.g. `activateTools`
instead of `lobe-activator____activateTools`), the resolver dropped them silently and
the harness finished with "completed without tool calls" — empty assistant bubble,
no error in dashboards.

Three layers of defense:

- Resolver fallback: when the bare name uniquely matches an API across known
  manifests, recover the identifier; ambiguous matches still drop to avoid
  false binding.
- StreamingHandler logs unresolved tool_call names so the silent-drop path is
  observable in debug output.
- GeneralChatAgent surfaces the unresolvable count and names in reasonDetail
  so dashboards can distinguish this from a genuine no-tool completion.

Fixes LOBE-8696

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): restrict bare-name fallback to tools offered this turn

Address review feedback on the LOBE-8696 resolver fallback. The
manifests map passed to ToolNameResolver.resolve is broader than the
tools actually sent to the LLM (the client builds it from every
installed plugin and every builtin; the server can preserve manifests
even after a step deactivates a tool). Without a turn-scope
restriction:

- A model returning a malformed bare name could resolve to a tool that
  was not enabled for this turn.
- A disabled duplicate API name could shadow the enabled call and make
  it look ambiguous, dropping a valid call.

Pipe an `offeredToolNames` list (the names actually sent in this LLM
payload) into resolve(): when set, the missing-prefix fallback only
considers manifests whose generated tool name appears in the list.

- ToolNameResolver.resolve gains an optional `offeredToolNames` param.
- internal_transformToolCalls forwards the list through.
- createAgentExecutors builds resolvedAgentConfig before the
  StreamingHandler so the closure can bind the offered names — same
  list that gets sent to the model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 20:47:21 +08:00
LiJian 6fb24adbd2 feat: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context (#14568)
*  feat: Cloud Claude Code V3 — repo picker, GitHub token, sandbox context

- Add CloudRepoSwitcher component (web-only multi-select repo picker)
  - Pre-topic selections buffered in module singleton (pendingTopicRepos)
  - Consumed by gateway.ts at topic creation time via appContext.initialTopicMetadata
  - Eliminates race condition where updateTopicMetadata dropped silently
- Extend ChatTopicMetadata with repos[] field for multi-repo binding
- Add initialTopicMetadata to ExecAgentAppContext so repos are written to
  topic metadata at creation time (server-side, zero race condition)
- Extend ExecAgentSchema Zod schema with initialTopicMetadata
- Inject GITHUB_TOKEN env var into sandbox so CC can use git/gh CLI
- Build cloudHeteroContext with GitHub auth section when token is available
- Add workingDirectory selector for web (repos[0] fallback)
- Add refreshTopic call in gateway path after new topic creation
- Add CloudHeterogeneousConfig profile editor for GITHUB_REPOS / GITHUB_CRED_KEY
- Extend sandboxRunner with repo clone setup script and systemContext support

* 🐛 fix: add open-source stub for pendingTopicRepos to fix Vite build

* ♻️ refactor: move pendingTopicRepos real impl into submodule, remove cloud override

* 🐛 fix: consume pendingTopicRepos only after topic creation succeeds

* 🐛 fix: add missing getPendingTopicRepos import in gateway

* 🔒 fix: address security and dead-code issues from PR review

- sandboxRunner: sanitize repo dir name to prevent shell injection
- sandboxRunner: use git insteadOf (-c flag) so token is never stored in .git/config
- cloudHeteroContext: fix return type from string|undefined to string (dead branch)
- CloudRepoSwitcher: remove unreachable empty-list branch in popover content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 💬 i18n: add claude setup-token hint to token description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: remove incorrect web hetero→gateway forced routing in agentDispatcher

On web, heterogeneousProvider is ignored — routing falls through to isGatewayMode.
Cloud CC only runs when gateway mode is enabled; gateway.ts handles sandbox
spawning when it detects a hetero provider.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix: restore web hetero→gateway routing; update stale test

On web, a configured heterogeneousProvider always routes to gateway —
the cloud sandbox is the only execution environment regardless of
isGatewayMode. The test assumed the pre-cloud-CC world where web
ignored hetero providers entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 20:39:14 +08:00
Innei a09991af8c 📝 docs(version-release): enforce git-derived PR refs and metrics (#14575)
* 📝 docs(version-release): enforce git-derived PR refs and metrics

Add the skill's first-class hard rules for computing release-note inputs
from git instead of memory: latest-tag base via `git describe`, PR refs
from commit subjects, metric counts from `wc -l`, handle resolution via
`gh pr view`, and a pre-publish `comm -23` diff that must be empty.
Also adds @cy948 to the team roster and notes Tsuki / René Wang's
commit-author aliases so contributor classification stops drifting.

* ♻️ refactor(version-release): split skill into router + per-flow references

SKILL.md was 426 lines covering three distinct flows. Split it so each
flow lives next to its own checklist:

- reference/minor-release.md — minor workflow (lifted from SKILL.md)
- reference/patch-release-scenarios.md — patch flows (existing)
- reference/release-notes-style.md — long-form changelog standard,
  template, and Computing Inputs hard rules (lifted from SKILL.md)

SKILL.md now reads as a router (~100 lines) with shared CI trigger
rules, post-release automation, precheck, and hard rules. Cross-links
between references replace the previous in-file jumps. Also fixes a
prettier-mangled redirect (`< some-pr-by-them >`) by using a `$PR`
variable instead of an angle-bracket placeholder.

* 📝 docs(version-release): add Hotfix and DB Migration variants to release-notes-style

The Canonical Structure was implicitly long-form (Minor / Weekly), and
hotfix authors had to read `changelog-example/hotfix.md` to learn it
existed. Make the divergence explicit:

- New § Variants for Shorter Releases describes Hotfix structure
  (Scope / What's Fixed / Upgrade / Owner) and DB Migration structure
  (Migration overview / Operator impact / Rollback) as overrides of the
  canonical long-form layout.
- Renamed the canonical section to "Canonical Structure (Long-Form:
  Minor / Weekly)" so the boundary is visible.
- Added Hotfix entry to Release Size Heuristics.
- Added a Hotfix subsection to Quick Checklist so the verification
  gates differ from long-form (no metric line / no Contributors / Owner
  resolved via gh).
2026-05-09 20:32:44 +08:00
YuTengjing 4c76d2430f 🐛 fix: remove signin captcha flow (#14573) 2026-05-09 19:49:04 +08:00
Innei 8ed31dfca4 🐛 fix(docker): replace pnpm init with static package.json in /deps (#14576)
`pnpm init` writes `devEngines.packageManager: { version: "^11.0.9" }`
into the generated package.json. corepack@latest rejects ranges in this
field with "Invalid package manager specification ... expected a semver
version", causing the subsequent `pnpm add pg drizzle-orm` to exit 1.

Skip init and write a minimal package.json directly so corepack has
nothing to validate.
2026-05-09 19:36:09 +08:00
YuTengjing c374892fea 🐛 fix: add temporary email auth error locale (#14564) 2026-05-09 18:50:32 +08:00
Rdmclin2 4617468e87 🐛 fix: add bot callback service (#14570)
fix: add bot callback service
2026-05-09 17:45:34 +07:00
LiJian 4c3a71a2c3 🐛 fix: sanitize sensitive comments and examples from production JS bundle (#14557)
* 🐛 fix: sanitize sensitive comments and examples from production JS bundle

- Replace app.example.com with RFC 2606 example.com in agent-browser skill content
- Replace password-stdin examples with interactive auth prompts
- Remove hardcoded password-like strings from code examples
- Reword flagged code comments in page-agent system role

Addresses TAC Security CASA Tier 2 DAST Info findings:
Information Disclosure - Suspicious Comments (CWE-615)

The flagged strings appeared in SPA production bundles:
- /_spa/assets/chat-*.js
- /_spa/assets/index-*.js

* 🐛 fix: revert --interactive to --password-stdin in auth vault examples

The --interactive flag does not exist in agent-browser CLI (only --password
and --password-stdin are supported). Using --interactive would cause auth
save to fail and block login workflows.

Reverted both auth vault examples to use echo | --password-stdin pattern,
which pipes the password via stdin — the recommended secure approach.
2026-05-09 18:19:31 +08:00
Arvin Xu 7892e553ea 💄 style(task): activity card stop run + register /tasks in SPA proxy (#14559)
*  feat(task): add stop run action to activity card menu

Surface the existing cancelTopic flow in the task detail activity card so
users can interrupt a running topic without opening the chat drawer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(task): confirm before stopping a running topic

Wrap the new Stop run action in a confirmModal so an accidental click can't
silently abort an in-flight run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(spa): register /tasks and /task in SPA proxy matcher

Without these matcher entries, the Next.js middleware never rewrote /tasks
and /task/:taskId to the SPA catch-all, so the activity feed entries 404'd
in production builds even though the routes were wired in the SPA router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:13:24 +08:00
YuTengjing 793a8deb43 💄 style: update auth captcha retry copy (#14561) 2026-05-09 17:35:03 +08:00
Rdmclin2 e56ccf6a5c 🐛 fix: multiple account link (#14562)
* feat: avoid rebind link same account

* chore: update i18n locales

* feat: avoid discord account misslink

* feat: support slack account mis match

* fix: avoid claim conflict
2026-05-09 16:31:21 +07:00
Innei 9756daba2d 🐛 fix(onboarding): guard skip/mode-switch footer with feature flag, desktop & init checks (#14560)
- Only show the skip-and-switch footer when all conditions are met:
  AGENT_ONBOARDING_ENABLED, not desktop, server config initialized,
  and runtime enableAgentOnboarding flag is on
- Fix typo: swichMode → switchMode
- Expand tests with hoisted mocks covering each visibility condition
2026-05-09 17:09:12 +08:00
AmAzing- 2b165ec722 🎨 Refine Agent Signal receipt cards (#14558)
*  Refine agent document skill trees and delete confirms

* 🐛 fix: improve receipt card accessibility
2026-05-09 16:41:57 +08:00
YuTengjing 8105fc0b16 feat: set OSS default model to DeepSeek V4 Pro (#14555) 2026-05-09 16:36:02 +08:00
YuTengjing 2d3332200a 🐛 fix: hide runtime-only model aliases (#14552) 2026-05-09 15:53:15 +08:00
Arvin Xu cb8645f65a 🐛 fix(security): remove /webapi/proxy and dead URL-manifest plugin code (#14549)
* 🐛 fix(security): remove /webapi/proxy and dead URL-manifest plugin code

Closes #14530. The /webapi/proxy endpoint was an unauthenticated open
HTTP proxy. All client callers were dead except NewAPI provider's
browser-side pricing fetch, which now silently falls back to no-pricing
since `parsePricingResponse` already handles non-OK responses.

Removes:
- /webapi/proxy route + API_ENDPOINTS.proxy
- toolService.getToolManifest (+ packages/utils/src/toolManifest.ts)
- src/features/PluginDevModal/UrlManifestForm.tsx
- uploadService.getImageFileByUrlWithCORS
- non-MCP branch in customPlugin reinstall (silently returns for
  legacy URL-manifest plugin data)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔥 chore(model-runtime): drop /webapi/proxy hop in NewAPI pricing fetch

The browser branch routed pricing requests through /webapi/proxy to bypass
CORS. Now that the proxy is removed, fetch the upstream pricing endpoint
directly — if CORS or any other error blocks it, fall through to the
existing null fallback (NewAPI just renders without enriched pricing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(model-runtime): drop console.debug assertion in NewAPI pricing fetch

The pricing-network-error case used to assert that console.debug was
called; with the log removed, just assert the graceful fallback (no
pricing on the resulting model). Also tightens an adjacent
branch-coverage test that ESLint flagged for a useless assignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:50:31 +08:00
YuTengjing cef69e9b72 🐛 fix: flatten visual analysis tool schema (#14550) 2026-05-09 14:42:53 +08:00
LiJian d0b938a0cb 🐛 fix: recover hetero persistence state across Vercel replicas (#14539)
* 🐛 fix: recover hetero persistence state across Vercel replicas

Three-part fix for multi-replica split-brain on Vercel serverless:

1. Flush accumulated content to DB after every ingest batch so a
   replica switch mid-accumulation doesn't lose text chunks.
2. Persist `heteroCurrentMsgId` to topic.metadata on every step
   boundary so new replicas restore the correct currentAssistantMessageId.
3. Restore toolMsgIdByCallId from DB on state creation so tool_results
   landing on a different replica than their tool_use are still matched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add the test fixed

* fix: slove the some topic problem

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:36:48 +08:00
AmAzing- af319af936 🐛 fix(agent): display managed skill folders and align delete confirms (#14553)
* 🐛 fix: display managed skill folders and align delete confirms

* 🐛 fix: allow recovery for orphan managed skill bundles

*  test: cover agent document group recovery paths

* 🐛 fix: render empty state for hidden skill indexes

*  test: relax agent signal hydration timeout
2026-05-09 14:32:46 +08:00
Innei 4ebd8f7f7c ♻️ refactor(onboarding): extract language and privacy as shared prefix steps (#14538)
* ♻️ refactor(onboarding): extract language and privacy as shared prefix steps

Move the language-selection and privacy/telemetry consent out of the classic
flow into a shared prefix that runs at /onboarding before branching into either
the agent or classic experience. Welcome decoration is merged with language
selection on a single screen, dropping the total step count by one.

Shared-prefix completion is derived from raw stored settings
(s.settings.general.responseLanguage and telemetry), so no new schema fields
are introduced and existing consumers that rely on the merged-default
telemetry value are unaffected.

Branch routing remains automatic (feature flag + isDesktop check) and is now
encapsulated in deriveOnboardingBranchPath. Both branch routes guard against
entering before the shared prefix is complete.

MAX_ONBOARDING_STEPS drops from 5 to 3 (FullName, Interests, ProSettings).

* ♻️ refactor(onboarding): use original Telemetry + ResponseLanguage as shared steps

Revert the merged welcome+language design. The shared prefix now reuses the
original two classic steps as-is:
- Step 1: TelemetryStep (welcome decoration + privacy/telemetry consent)
- Step 2: ResponseLanguageStep (language selection)

Also suppress the mode-switch + skip footer on the bare /onboarding path so
it only appears once the user has entered the agent or classic branch.

* 🐛 fix(onboarding): persist shared-prefix step in URL to survive locale-triggered remounts

Use react-router's useSearchParams to keep the active shared step in the URL
(?step=2). Local useState was lost when switching language for the first time
because i18next's first-time resource load triggers a remount up the tree;
the URL param survives any remount.

* 🐛 fix(onboarding): unblock branch redirect when user accepts default telemetry

Derive commonStepsCompleted from responseLanguage alone. setSettings strips
fields whose value matches DEFAULT_COMMON_SETTINGS, so accepting the default
telemetry: true left s.settings.general.telemetry undefined and the derive
selector never flipped to true — the redirect to the branch never fired.

Step 2 (language) implies step 1 was completed because the flow is sequential,
so checking responseLanguage alone is sufficient and robust against the
default-strip behavior.

* 🐛 fix(onboarding): redirect after step 2 by deriving completion from responseLanguage only

setSettings strips fields that match defaultSettings, so writing
telemetry=true (the default) never persists to s.settings.general.
That made commonStepsCompleted permanently false even after the user
finished both steps, blocking the redirect to the branch flow.

Drop telemetry from the derive check. Step 1 completion is already
tracked via the URL ?step=2 marker; step 2 completion is the only
event that needs to flip commonStepsCompleted, signalled by writing
responseLanguage (which always differs from the default since
DEFAULT_COMMON_SETTINGS has no responseLanguage entry).

* 🔨 chore(scripts): add reset-onboarding script for redoing the flow

Takes an email, clears users.onboarding, agent_onboarding, full_name,
interests and removes responseLanguage + telemetry from
user_settings.general so the user re-enters the shared-prefix
onboarding from step 1.

Usage:
  pnpm workflow:reset-onboarding <email>
  bunx tsx scripts/resetOnboarding/index.ts <email>

* 🐛 fix(signup): add refs for email and password inputs to improve focus handling

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(onboarding): skip responseLanguage auto-fill while onboarding is in progress

useInitUserState's onSuccess callback auto-fills general.responseLanguage
from navigator.language whenever the field is missing. For new users
this fired immediately after signup, which made commonStepsCompleted
(which derives from responseLanguage being set) flip to true on first
load, and CommonOnboardingPage's early-redirect skipped past the shared
prefix straight into /onboarding/agent.

Gate the auto-fill on onboarding.finishedAt or agentOnboarding.finishedAt
being set, so legacy users who finished onboarding without
responseLanguage still get the safety-net detection, but in-progress
users keep the field undefined until they explicitly choose it on the
language step.

* 🐛 fix(onboarding): refresh welcome message locale until conversation starts

ensureWelcomeMessage previously only created the welcome on first call
and skipped on subsequent ones, leaving stale welcomes locked to the
locale that was active when the topic was first created. After the
shared-prefix refactor users pick their language earlier than they
used to, so the welcome that was generated during the auto-detect
phase never gets re-translated.

Now the welcome content is rewritten in-place to match the current
responseLanguage as long as no user reply has been recorded yet
(message count <= 1). Once the conversation has started, the welcome
is left as part of the chat history.

* 🐛 fix(onboarding): update welcome message handling to render client-side and avoid persisting during onboarding

Signed-off-by: Innei <tukon479@gmail.com>

* Refactor onboarding user profile handling: remove responseLanguage field

- Removed responseLanguage from SaveUserQuestionInput and related schemas.
- Updated onboarding logic to no longer save or request responseLanguage.
- Adjusted related components and services to reflect the removal of responseLanguage.
- Enhanced user info handling to include displayName and fullName from OAuth.
- Updated tests to align with the new onboarding structure.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor(onboarding): update locale handling to use i18n's resolved language

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(onboarding): remap legacy 5-step classic currentStep on shared-prefix mount

Mid-flow legacy users with persisted currentStep authored under the old
5-step classic flow (Telemetry, FullName, Interests, Language, ProSettings)
would silently skip required profile steps after the renumbering: old
step 2 (FullName) rendered Interests, old step 3 (Interests) rendered
ProSettings. Apply a one-time remap (2->1, 3->2, >=4->MAX) when Common
mounts, gated by isUserStateInit and onboarding.finishedAt absence so it
fires only for in-flight legacy users. Idempotent for new-schema values.

* refactor(onboarding): implement AGENT_ONBOARDING_ENABLED master switch for onboarding flow

Signed-off-by: Innei <tukon479@gmail.com>

* refactor(onboarding): standardize AGENT_ONBOARDING_ENABLED naming in tests

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-09 14:31:50 +08:00
Arvin Xu de698eef92 feat: Agent Task System available (#14540)
* 🔥 chore: remove agent_task feature flag and graduate task feature

Drop the agent_task / enableAgentTask gate that was guarding the agent
task rollout. The feature is now permanently enabled, so all flag
checks, disabled-state redirects, and disabled-only fallback UI
(SuggestQuestions, CommunityAgents) are removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(brief): create regular task instead of cron job from template card

The "Add task" button on DailyBrief recommendation cards was creating an
agentCronJob (scheduled recurring job). Switch to taskService.create via
the createTask store action so it creates a one-off inbox task and
refreshes the task list, matching user expectation that the click adds
a task rather than a schedule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(task): support schedule fields on task.create

The brief recommendation card needs to create a recurring scheduled
task in one shot (template carries `cronPattern`). Extend `task.create`
to accept `automationMode`, `schedulePattern`, `scheduleTimezone`, and
thread them through the service + store action. The model already
accepts these via NewTask, and the central schedule-dispatch sweep
picks the task up once status is dispatchable.

TaskTemplateCard now creates a schedule-mode task with the template's
cron pattern and the user's local timezone, restoring the recurring
behavior previously provided by AgentCronJob.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 i18n(home): shorten brief.title from "Daily brief" to "Brief"

Daily-frequency tasks are no longer the only source feeding the section
(scheduled, manual, and on-demand briefs all flow through it now), so
the more general label fits better.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(task-list): show skeleton instead of blank while task list loads

Both the list view (TaskList) and kanban view (KanbanBoard / KanbanColumn)
returned null until isInit, leaving the page empty during the first SWR
fetch. Render a TaskItemSkeleton (default + compact variants) to keep the
layout stable and signal that data is loading.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(git-status): toggle review panel on diff-stat click

Clicking the diff-stat chip used to always open the review panel — if
the panel was already showing review, the click was a no-op. Switch to
a toggle: clicking again with the review tab active closes the panel,
matching the implicit expectation that the chip is the entry/exit
control for that view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(brief): update TaskTemplateCard test for createTask flow

Card now calls useTaskStore.createTask with schedule fields instead of
agentCronJobService.create. Replace the agentCronJob service mock with
a useTaskStore mock exposing createTask, and assert the schedule-mode
payload (automationMode + schedulePattern + scheduleTimezone) on the
success path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(brief): jump to task detail after creating from template

The success toast asked users to look in the inbox agent for the new
scheduled task; navigating directly to the task detail is a clearer
landing for what they just confirmed. Drop the toast and route to
`/task/<identifier>` once createTask resolves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:07:15 +08:00
YuTengjing c03e79c118 🐛 fix: pass generation moderation context (#14541) 2026-05-09 11:27:11 +08:00
Arvin Xu aef7158f4a 🐛 fix(model-runtime): preserve reasoning_content for deepseek models in OpenAI-compat layer (#14546)
DeepSeek thinking-mode (deepseek-reasoner / deepseek-v4-*) rejects follow-up
turns when assistant history messages omit reasoning_content. Until now this
was only enforced in the dedicated DeepSeek runtime's handlePayload; users
routing deepseek model ids through any other OpenAI-compatible runtime hit a
400 with "The reasoning_content in the thinking mode must be passed back to
the API."

Move the safety net into convertOpenAIMessages so any OpenAI-compatible call
with a deepseek-named model derives reasoning_content from reasoning.content
and forces an empty placeholder for thinking-eligible models.

Fixes LOBE-8290

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:53:18 +08:00
Neko be42e056e6 feat(agent-signal,prompts,database): nightly self-review wired, improved (#14543) 2026-05-09 07:16:54 +08:00
Neko b47e32436e ️ perf(agent-signal,app): exp backoff retry of listing signal receipts (#14542) 2026-05-09 04:25:17 +08:00
Neko 85b412270b 🐛 fix(agent-signal,server): missing document tool outcome rendering into decision agent (#14534)
Emit agent document tool outcome events from client-triggered agent document tools with tool attribution so hinted skill documents can be observed by Agent Signal.

Hydrate client runtime completion back to the completed assistant message for pre-created assistant turns, allowing same-turn hinted document receipts to match the originating user message.

Harden agent document snapshot reads by falling back to markdown content when stale editor data cannot be projected for decision evidence.
2026-05-09 04:08:06 +08:00
Arvin Xu 0e216dec8e 💄 style: fill input on follow-up chip click instead of sending (#14536)
* 💄 style: fill input on follow-up chip click instead of sending

Mirrors the NameSuggestions pattern so users can edit a suggested
follow-up before sending, matching onboarding interaction conventions.

*  test: update FollowUpChips click test for input-fill behavior

Mock updateInputMessage + editor (setDocument/focus) instead of
sendMessage and assert the new fill-input flow.

* 💄 style: move branching action into the message "..." menu

Surface "branching" inside the dropdown menu (right after copy) for
assistant, assistantGroup, and user messages, instead of as an inline
toolbar icon gated behind dev mode. Drops the dev-mode bar override and
renames the now-only ACP-related selector binding to isHeteroAgent.
2026-05-09 01:33:52 +08:00
sxjeru 1d2db96a38 🐛 fix: add prompt_cache_key for OpenAI chat requests (#14349) 2026-05-09 01:15:34 +08:00
Innei 4dade3196f ️ perf(market): batch fork API for parallel marketplace install (#14537)
Rewrite the onboarding marketplace install pipeline from a serial per-agent
loop to a parallel pipeline anchored on a batched fork call. Multi-select
in the picker now finishes in roughly four parallel rounds instead of
~5N sequential round-trips.

- forkAgent tRPC now takes { items: AgentForkBatchInput[] } and returns
  per-item AgentForkBatchResult (discriminated union, best-effort: a single
  failure does not abort the batch). The upstream market endpoint stays
  per-id, fanned out via Promise.all on the server.
- installMarketplaceAgents fans out dedupe, detail fetch, and createAgent
  steps via Promise.all/allSettled and consolidates into one batched fork.
- ForkAndChat (community single-fork action) wraps its call as a 1-item
  batch and unwraps the per-item result.
2026-05-09 01:02:49 +08:00
LiJian f934e2ff46 ♻️ refactor: implement cloud hetero agent exec pipeline (step 3 + step 4) (#14486)
* refactor: add the cloud hetero execAgent Runtime way

*  feat: support session resume for heterogeneous agents (Claude Code / Codex)

- Expose `sessionId` getter on `SpawnAgentHandle` (read from `AgentStreamPipeline`)
- Pass `sessionId` to `IngestSink.finish()` so CLI reports it via `heteroFinish`
- Server stores `heteroSessionId` in topic metadata after each turn
- Server reads and passes `resumeSessionId` as `--resume` on subsequent turns
- Remove debug `console.log` statements from aiAgent service and sandboxRunner

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: slove some bugs

* fix: add the is dev back

* 🐛 fix: add async to handleAgentRunRequest in gatewayConnectionSrv

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 00:28:27 +08:00
Innei 1bc8d59922 💄 fix(chat-input): fix MentionMenu scroll area clipping caused by container padding (#14533)
💄 fix(chat-input): fix MentionMenu scroll area clipping with negative margin padding trick
2026-05-09 00:10:05 +08:00
Arvin Xu 8fab0b014e 💄 style: polish onboarding interventions and add tool result renders (#14506)
*  feat: add collapse toggle to onboarding mode switch toolbar

The dev-mode actions pill at the bottom-right of the onboarding page
covered the operation area below it. Add a chevron toggle so users can
collapse the pill down to a single icon button. Collapsed state is
persisted in localStorage so it survives reloads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat: make name and avatar editable in onboarding intervention card

Lets the user override the agent's proposed identity in-place before
approving — pick a different emoji from the avatar picker, type into
the name field, and the edits flow through registerBeforeApprove ->
onArgsChange so the actual save uses the user's values.

Other changes:
- Title is now derived from the live edit state, so adding a missing
  field flips the wording from "I'll update my name" to "I'll update my
  name and avatar" without staleness
- Subtitle hint ("如果不满意,可以直接修改名字或头像") tells the user
  the card is interactive
- Test covers the edit-flush path: edits to name + emoji are observed
  via onArgsChange when the framework triggers the beforeApprove flush

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style: redesign intervention approval card as codex-style options

Drops the inline approve / reject button row in favor of a numbered
two-option layout with a single Submit at the bottom-right, mirroring
Codex's approval picker. The reject row's content is the reason input
itself (placeholder doubles as the row label) so users can type a
follow-up instruction in place; reason flows through to the existing
rejectAndContinueToolCall(messageId, reason) action.

Behavior:
- Default selection is approve; arrow keys (↑/↓) and 1/2 switch options
- Enter submits when no input is focused; reject input has its own
  Enter / ↑ handlers so typing doesn't hijack the picker
- Window-level shortcuts skip while any input/textarea/contenteditable
  is focused, so the main chat composer is never affected
- approvalMode='allow-list' adds a "Don't ask again for similar actions"
  checkbox under option 1, replacing the old split-button dropdown

Also tighten the onboarding intervention editHint copy from
"如果不满意,可以直接修改名字或头像" to "你可以直接在下方修改名字或头像"
(positive framing instead of conditional).

i18n changes (default + en-US + zh-CN):
- Add optionApprove, rememberSimilar, submit
- Repurpose rejectReasonPlaceholder as the inline reject row's placeholder
- Drop now-unused approveAndRemember, approveOnce, rejectAndContinue,
  rejectTitle keys

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style: tighten PickAgents card layout

- Move avatar and title into a single row (cardHeader) so the agent
  template title sits next to the avatar instead of below it; description
  stays as a multi-line block beneath
- Switch card border from colorBorderSecondary to colorFillSecondary so
  the card outline is visible when sitting on the elevated picker panel
- Mirror the row layout in the loading Skeleton so the shimmer matches

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(agent-marketplace): add Inspector for showAgentMarketplace and submitAgentPick

The marketplace tool was previously falling back to the generic raw-args
"等 N 个参数" header. Add per-API Inspectors:

- showAgentMarketplace: title + up to 3 localized category chips
  (sourced from existing CATEGORY_LABEL_I18N_KEYS in tool namespace);
  overflow shown as +N
- submitAgentPick: title + selected agent count

Wire AgentMarketplaceInspectors into builtin-tools/src/inspectors.ts
under AgentMarketplaceManifest.identifier and export from the package's
agentMarketplace/client surface.

i18n adds (default + en-US + zh-CN tool namespace):
- agentMarketplace.inspector.pickCount plurals
- agentMarketplace.inspector.moreCategories plurals

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style: rename showAgentMarketplace label to "Assemble agent team"

The agent narrates intent ("组建 Agent 团队" / "Assemble agent team")
rather than describing a UI surface ("打开助手市场" / "Open agent
marketplace"), which reads more naturally in the inspector header
during onboarding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style: hide chat/page view switcher in agent conversation header

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(agent-marketplace): render picked agent cards from pluginState

Adds a SubmitAgentPick Render that shows a grid of agent cards (avatar +
title + description + "already in library" tag) instead of the raw text
content the LLM consumes. Also wires the framework so custom-interaction
handlers can return structured pluginState alongside toolResultContent.

Framework changes:
- submitToolInteraction(options) now accepts a pluginState field. After
  writing toolResultContent, the chat store calls
  optimisticUpdatePluginState so the message's structured state is
  available to render components (matching how server-executed builtin
  tools persist state)
- Cloud-side wrapper in Conversation/store/slices/tool/action.ts
  forwards the new field
- customInteractionHandlers.ts SubmitToolInteractionOptions adds
  pluginState; handleAgentMarketplaceSubmit returns the install
  summaries via pluginState (same shape that built the LLM-facing text)

Marketplace changes:
- InstallMarketplaceAgentSummary gains an avatar field; the install
  helper threads marketAgent.avatar through
- New Render/SubmitAgentPick reads pluginState.summaries to draw a
  responsive card grid (already-in-library entries dimmed + tagged)
- Wire AgentMarketplaceRenders through the package's
  agentMarketplace/client surface and register under
  AgentMarketplaceManifest.identifier in builtin-tools/src/renders.ts

Workflow display labels (collapsed grouped tool row):
- Add showAgentMarketplace ("Assembled agent team" / "组建了 Agent 团队")
  and submitAgentPick ("Picked agents" / "选好了助手") to
  TOOL_API_DISPLAY_NAMES so the collapsed group no longer falls back to
  "Show Agent Marketplace" / "Submit Agent Pick" via toTitleCase

i18n adds (default + en-US + zh-CN):
- tool.agentMarketplace.render.alreadyInLibrary plurals + alreadyInLibraryTag
- chat.workflow.toolDisplayName.{showAgentMarketplace,submitAgentPick}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(web-onboarding): add UpdateDocument render with hunk diff

Replace the raw "Updated persona document (id). Applied N hunk(s)."
text with a structured per-hunk diff view rendered from args.hunks
(no executor state changes — args already carry the patches).

For each hunk render a mode label + line range chip and paint the
affected text:
- replace: removed (red border) → added (green border)
- delete: removed only
- insertAt: green block + L<line> chip
- replaceLines: green block + line range chip
- deleteLines: line range chip only (no body)

The total hunk count piggy-backs on the first hunk's label row instead
of getting its own header (the inspector header chip already shows
total + doc type, so a separate render-side header would be redundant).

i18n adds builtins.lobe-web-onboarding.updateDocument.hunkMode.{replace,
delete,deleteLines,insertAt,replaceLines} across default + en-US +
zh-CN.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:08:24 +08:00
Rdmclin2 507909dc2c feat: add agent hono routes (#14535)
feat: add agent hono routes
2026-05-08 22:31:47 +07:00
YuTengjing 4721d14a81 🐛 fix: trim brief / task-template fetch overhead on home (#14516) 2026-05-08 23:06:22 +08:00
YuTengjing e1a5b27db0 feat(task): add comment tools and reparent support (#14515) 2026-05-08 22:42:10 +08:00
Innei 03621d0664 feat(explorer-tree): add generic ExplorerTree component built on @pierre/trees (#14094)
*  feat(explorer-tree): introduce generic ExplorerTree component

Scaffold a reusable tree component at `src/features/ExplorerTree/`
built on top of `@pierre/trees`. The component exposes a typed
`ExplorerTreeNode<TData>[]` input (tree or flat+parentId),
path-driven identity hidden behind an adapter, and a minimal
imperative handle (startRenaming, focus, select, setExpanded,
getSelectedIds).

Wired v1 capabilities:
- multi-select (default* + onChange), uncontrolled + ref
- DnD abstracted as `onMove(MoveEvent)` with canDrag/canDrop gates
- declarative right-click menu via `getContextMenuItems` rendered
  through the library's `renderContextMenu` slot
- inline rename via `canRename`/`onCommitRename`/`onRenameError`
- trailing row decorations via `getRowDecoration`
- built-in icon set driven by file extensions

Old `src/features/FileTree/` is tagged `@deprecated` so consumers
can migrate gradually (SkillStore, LibraryHierarchy, WorkingSidebar).
No consumers migrated in this PR — that is tracked as a follow-up.

Design spec: docs/superpowers/specs/2026-04-23-explorer-tree-design.md

* 📝 docs: add ResourceManager ExplorerTree refactor design

* ♻️ refactor(explorer-tree): use id-based tree contracts

* ♻️ refactor(explorer-tree): narrow transitional tree types

* ♻️ refactor(explorer-tree): align transitional prop contracts

* ♻️ refactor(explorer-tree): remove future-only transitional types

* ♻️ refactor(explorer-tree): support controlled id state

* 🐛 fix(explorer-tree): suppress controlled sync feedback

* 🐛 fix(explorer-tree): reconcile controlled ids on stable paths

*  feat(resource): add tree snapshot derivation

*  feat(resource): add tree mutation helpers

* 🐛 fix(resource): harden tree mutation rollback boundaries

*  feat(resource): add tree controller

* 🐛 fix(resource): guard tree controller request ordering

*  feat(resource): add tree route and bridge modules

* 🐛 fix(resource): harden tree route bridge boundaries

* ♻️ refactor(explorer-tree): expose row host events

* ♻️ refactor(resource): wire hierarchy to ExplorerTree

* ♻️ refactor(resource): remove global tree store

* 🐛 fix(resource): revalidate tree mutations by source parent

* 🐛 fix(spa): prebundle explorer tree dependency

* ♻️ refactor(sharedRendererConfig): remove unused dependencies '@pierre/trees' and '@pierre/trees/react'

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ revert(resource): remove business integration, keep ExplorerTree component only

Revert all ResourceManager business integration while preserving the
generic ExplorerTree component implementation:

- Restore ResourceManager component files to canary state
- Restore src/store/tree/ (deleted by integration commit)
- Remove src/features/ResourceManager/tree/ (controller, mutations, bridge)
- Keep src/features/ExplorerTree/ (generic component)
- Keep @pierre/trees dependency in package.json

*  feat(agent): integrate ExplorerTree into agent documents section

- Replace flat document list with ExplorerTree for 'documents' filter tab
- Convert flat AgentDocument[] to tree nodes via parentId/fileType
- Add tree node click handler (navigate/open) and context menu (delete)
- Fix height chain: ResourcesSection flex:1 -> AgentDocumentsGroup -> ExplorerTree
- Style ExplorerTree via --trees-*-override CSS vars (transparent bg, relaxed density, theme tokens)

* ♻️ refactor(resource-manager): remove outdated ExplorerTree design document

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(agent-documents): wire context menu and DnD via base-ui imperative API

- Replace nested antd Menu surface with @lobehub/ui showContextMenu, capturing right-click on the tree host directly so menu actions (rename, create, delete) survive base-ui focus restoration
- Fix DnD root drop by routing canDrop through directoryPath instead of hoveredPath, so dragging a nested file onto empty root no longer treats the hovered file row as the target zone

* ♻️ refactor(DocumentExplorerToolbar): adjust padding styles for better layout

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(useDocumentTreeOps): integrate confirmModal for delete confirmation

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(ExplorerTree): cast through unknown to satisfy antd MenuItem types

*  feat(AgentDocumentsGroup.test): add mock for DocumentExplorerTree and update tests for document count

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-08 22:34:20 +08:00
YuTengjing fcc5aa181a 🐛 fix: preserve user fields on better-auth session refetch (#14531) 2026-05-08 22:14:05 +08:00
Rdmclin2 4d934f8275 🐛 fix: telegram api lost (#14519)
* fix: bot message callback

* fix: add telegram timeout error

* Potential fix for pull request finding 'CodeQL / Incomplete multi-character sanitization'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'CodeQL / Double escaping or unescaping'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-08 20:40:32 +07:00
Neko c760171f49 🐛 fix(agent-signal,types,prompts,server): should handle skill intent directly when hintIsSkill on, and reroute the source signal, or otherwise it will be hard to have skill triggers (#14526) 2026-05-08 20:14:07 +08:00
YuTengjing c7b7717faa 🐛 fix: support provider sdk type routing (#14520) 2026-05-08 20:03:08 +08:00
YuTengjing 385afbcc57 ️ perf: refresh home recents periodically and inline task status (#14518) 2026-05-08 19:32:42 +08:00
Neko d051ac008c 🐛 fix(database,userMemories): should sanitize for ` or otherwise memory search can easily fail (#14524) 2026-05-08 19:30:31 +08:00
Neko 9b2832bba9 🐛 fix(server,userMemories): should have user memory errors explicitly injected (#14525) 2026-05-08 19:30:17 +08:00
Innei 9b5cea7391 ♻️ refactor: merge agent-marketplace into web-onboarding package (#14514)
* ♻️ refactor: merge agent-marketplace into web-onboarding package

Move the standalone `builtin-tool-agent-marketplace` package into
`builtin-tool-web-onboarding/src/agentMarketplace/` as a sub-module
to reduce package sprawl and consolidate related onboarding tooling.

Also adds locale-aware fetching for onboarding agent templates:
- Accept optional `locale` param in `getOnboardingFull` TRPC endpoint
- Pass normalized i18next locale from the client fetcher
- Add unit test for locale resolution

* ♻️ refactor: integrate FollowUpChips into ChatItem and update GroupMessage components

Signed-off-by: Innei <tukon479@gmail.com>

* fix: address Codex review feedback for PR #14514

- Make getOnboardingFull input schema optional with default to preserve
  backward compatibility for callers that invoke .query() without arguments
- Parameterize SWR cache key by resolved locale to prevent cross-locale
  cache pollution in the PickAgents marketplace component

* chore: remove accidentally pushed .kagura directory and add to .gitignore

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-08 19:08:39 +08:00
Rdmclin2 f7f8bc625f 🐛 fix: tsc error (#14521)
fix: tsc error
2026-05-08 17:34:03 +07:00
YuTengjing 83bc73c2ae feat: add task template tracking (#14517) 2026-05-08 17:48:46 +08:00
Rdmclin2 75fd477bff feat: support messager (#14442)
* feat: support messagers

* chore: refactor lobeai to messager prefix

* feat: reigister messager platforms

* feat: support slack messager

* fix: verify im route redirect

* fix: link page style

* chore: optimize agent select and /agents commands

* feat:support lab switch

* feat: use same  agent select

* chore: add runtime error info

* chore: optimize error text

* feat: add slack messagger installation implementation

* chore: add more scope

* feat: add slack messager account link

* fix: open slack in a new link

* feat: optimze messager link page

* feat: optimize messager locales and bot options

* chore: optimize messager

* fix: slack integration detail

* fix: avoid taking over and fix slash commands

* chore: optimize slack app setup

* chore: update slack manifest and setup

* feat: support discrod platform

* feat: discord messger slash commands and agent picker

* chore: update discord messager

* feat: support db bot provider credentials

* chore: remove message router ensure  connected

* chore: remove notes field

* chore: add applicationId and credentails

* chore: squash db migations

* chore: remove installedAt and linkedAt field

* chore: remove messager releated env variables

* chore: remove old skill bot skill

* feat: add operationId when throwing error

* chore: abstract platform clients and registery

* chore: fix link modal message i18n and add platform definition name field

* feat: add integration detail

* feat: add platfom definition i18n files

* chore: abstract messenger router platform branches

Collapse parallel Slack/Discord slash & action paths in MessengerRouter
into a single command registry + binder hooks (replyPrivately,
extractActionFromEvent, acknowledgeCallback). Wire Discord /start by
resolving DM via openDM(authorUserId) so a public-channel slash invocation
posts the link privately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: update installation and oauth process for discord and slack

* fix: telegram local button

* chore: remove messager docs

* feat: add discord installation process

* chore: remove discord bot username

* chore: adjust discord integration detail

* feat: extract platfom specific implementation

* chore: handle connection flow and redirect

* feat: add platform router for messager

* chore: move messager to agents group

* chore: update i18n files

* chore: update messager table sql

* chore: update messager sql

* fix: link with tenantId

* chore: move messger verify page to features/Messager

* chore: refactor messager verify page

* Potential fix for pull request finding 'Property access on null or undefined'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: Rebind by platform user when confirming messenger link

* chore: remove unnecessary journals

* chore: update i18n files

* fix: lint error and i18n

* fix: test cases

* chore: add lost test cases

* chore: try cpus 2

* chore: try remove optimize package import

* chore: fallback define config

* chore: try to reduce OOM

* chore: fallback

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-05-08 16:27:16 +07:00
AmAzing- 26da6b9ad4 Fix tool call timer reset on collapse and expand (#14513)
* 🐛 Preserve tool call timer across collapse and expand

* 🧪 Add coverage for execution timer reset cleanup

* 🐛 fix: clear execution timer cache after unmount
2026-05-08 15:01:53 +08:00
AmAzing- 1d4fb21885 🐛 fix: Review panel gating to use effective working directory (#14512)
* 🐛 Align working sidebar review with agent cwd

* 🐛 Align review cwd selector with GitStatus
2026-05-08 13:16:26 +08:00
YuTengjing 38c92fa04a 🐛 fix: sanitize provider tool names (#14510) 2026-05-08 11:47:07 +08:00
YuTengjing 555a375e67 🐛 fix: always recommend task templates regardless of brief count (#14508) 2026-05-08 11:17:26 +08:00
YuTengjing 6989e8f9e6 🐛 fix: sanitize Claude thinking history signatures (#14499) 2026-05-08 09:56:00 +08:00
Rdmclin2 e4d1d1fc17 👷 build(database): add messenger tables for IM bot integration (#14496)
* 👷 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.
2026-05-08 01:10:34 +07:00
Arvin Xu 026c79a4c2 💄 style: simplify onboarding agent identity intervention card (#14505)
* 💄 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>
2026-05-08 02:02:12 +08:00
Arvin Xu 1e2782ece4 🐛 fix(gateway): keep input loading on through execAgentTask round-trip (#14503)
* 🐛 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>
2026-05-08 02:01:26 +08:00
Innei b5ddac56dc 🐛 fix(assistant-group): pass per-segment content overrides to MessageContent (#14504)
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.
2026-05-08 01:51:11 +08:00
Innei ad0da3753e feat(kb-tool): integrate BM25 search and docs_* read for inline documents (#14494)
*  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>
2026-05-08 01:32:42 +08:00
Arvin Xu e6905fe0fd feat(agent-sidebar): move tasks from welcome card to sidebar list (#14500)
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>
2026-05-08 00:32:01 +08:00
Hardy a9d2110565 🐛 fix: onFinish never called when browser tab is backgrounded during SSE streaming (#14461)
🐛 fix: remove rAF animation blocking onFinish when tab is backgrounded

Replace await-on-animation with synchronous flushQueue() to prevent
background tab throttling from stalling chat completions, MCP tool
calls, and agent loop continuation.
2026-05-08 00:10:31 +08:00
Arvin Xu e4d5f69b27 ♻️ refactor(agent): migrate remaining /api/agent routes to Hono (#14478)
* ♻️ 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>
2026-05-07 23:37:23 +08:00
LiJian a372acd50d feat: add lobeAgents markdown tag for inline agent card rendering (#14495)
*  feat: add lobeAgents markdown tag for inline agent card rendering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 📝 docs(agent-management): instruct LLM to render lobeAgents card after agent operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🐛 fix(lobe-agents): support single-quoted attrs and preserve trailing paragraph siblings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 23:37:20 +08:00
YuTengjing 0af5e51477 🐛 fix: sanitize assistant media in Responses input (#14497) 2026-05-07 23:26:22 +08:00
LiJian 40f0557158 feat(agent-management): render clickable agent card after createAgent (#14493)
 feat(agent-management): render clickable agent card after createAgent tool execution

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:59:07 +08:00
YuTengjing 62f06540ba 🐛 fix: show notification settings in desktop (#14491) 2026-05-07 19:58:53 +08:00
YuTengjing 43b064f803 feat: add RecommendTaskTemplates UI and default noop router (#14488) 2026-05-07 19:14:08 +08:00
YuTengjing 8e8a463a05 🐛 fix: use runtime config to hide LobeHub provider toggle (#14487) 2026-05-07 19:07:05 +08:00
Neko decc25554e 🐛 fix(memory-user-memory): should have nullable when parsing activities (#14489) 2026-05-07 19:04:12 +08:00
CanisMinor 1c8ec2681c 💄 style: update brief template style (#14483)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-07 17:34:47 +08:00
Innei 0a32fbc737 🐛 fix(desktop-overlay): hide model picker and stabilize panel enter (#14484) 2026-05-07 16:39:32 +08:00
sxjeru 7fc41a9677 🐛 fix: add provider settings normalization & add Gemma 4 models (#13313)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-07 12:46:19 +08:00
AmAzing- 22c880763d ️ revert: remove e2e topic group expansion workaround (#14480) 2026-05-07 12:16:24 +08:00
Arvin Xu d324736edf 💄 style: polish onboarding agent welcome and add web-onboarding tool UI (#14475)
* 💄 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>
2026-05-07 10:32:32 +08:00
Arvin Xu 608498a950 feat(agent): inactivity watchdog finalize endpoint + agent-hono migration (#14476) 2026-05-07 09:54:47 +08:00
Innei 5e1a35f259 🐛 fix(conversation): reduce streaming re-renders with reference stabilization and self-subscribing components (#14470)
* 🐛 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>
2026-05-07 02:29:31 +08:00
Innei 6b010c8380 🐛 fix(editor-runtime): add mutation handlers for post-save synchronization (#14469)
* 🐛 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>
2026-05-07 02:21:35 +08:00
YuTengjing ead5631bab 🐛 fix: preserve nested router runtime id (#14474) 2026-05-07 01:45:12 +08:00
YuTengjing ddd5c20836 💄 style: add grok-4.3 to LobeHub-hosted xAI models (#14446) 2026-05-07 00:49:54 +08:00
YuTengjing c51835193f 🐛 fix: stabilize xAI Responses API tools (#14462) 2026-05-07 00:11:44 +08:00
Arvin Xu 0c375e4428 💄 style: update heterogeneous agent ChatInput placeholder text (#14454)
* 💄 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
2026-05-06 23:38:53 +08:00
YuTengjing 58cda8a645 🐛 fix: persist home sidebar collapse state (#14473) 2026-05-06 23:32:14 +08:00
AmAzing- 65ba4ad435 🐛 fix(e2e): expand visible topic groups in E2E steps (#14472)
🧪 Expand visible topic groups in E2E steps
2026-05-06 22:27:03 +08:00
AmAzing- 41ffd1e0d3 🧪 Fix streaming executor agent spy tests (#14471) 2026-05-06 21:32:51 +08:00
LiJian 02767bac55 🐛 fix: resolve template variables in server-side (execAgent) context engine (#14468)
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>
2026-05-06 21:09:22 +08:00
Innei be5d61d40a feat(desktop): add app tray visibility setting (#14463)
*  feat(desktop): add app tray visibility setting

* ♻️ refactor(desktop): move tray setting to appearance
2026-05-06 18:13:23 +08:00
Rylan Cai 282b20c454 🐛 fix context compression threshold config (#14439) 2026-05-06 17:08:10 +08:00
AmAzing- cc506c036d 🐛 fix: task breadcrumb title truncation (#14460)
 Fix task breadcrumb title truncation
2026-05-06 16:46:23 +08:00
LiJian 5fca91a488 🐛 fix: inject user response language into task summary chains (#14459)
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>
2026-05-06 16:41:20 +08:00
Neko c3530ad221 🐛 fix(app,agent-signal): for skills, target to skill document, and auto refresh (#14457) 2026-05-06 16:19:36 +08:00
Zhijie He 8b8b0f0579 💄 style: add step-image-edit-2 support (#14329)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-05-06 15:51:54 +08:00
YuTengjing 958bf52978 🐛 fix: preserve visual refs for bot uploads (#14456) 2026-05-06 15:38:39 +08:00
René Wang 480d4b2b4e 📝 docs: add May 4 weekly changelog (#14434) 2026-05-06 14:48:01 +08:00
YuTengjing 4d00c22e7f 🐛 fix: handle unsupported xAI parameters (#14445) 2026-05-06 14:45:09 +08:00
Innei f30d9da5a9 feat(agent-mock): add agent mock devtools with playback & fixture viewer (#14436)
* 📦 feat(agent-mock): scaffold package skeleton

* 🔧 chore(agent-mock): align deps + add vitest config

*  feat(agent-mock): add core types

*  feat(agent-mock): add chunkSplitter with code-point safety

*  feat(agent-mock): map ExecutionSnapshot → MockEvent[]

*  feat(agent-mock): add defineCase / llmStep / toolStep / errorStep DSL

*  feat(agent-mock): add snapshotToMockCase helper

*  feat(agent-mock): add todo-write-stress builtin case + registry

*  feat(agent-mock): add generator registry + tool-stress generator

*  feat(agent-mock): add 4 more builtin cases (long-reasoning, mixed, error, subagent)

*  feat(agent-mock): add subagent-tree + long-reasoning generators

*  feat(agent-mock): add MockPlayer state machine + step navigation

*  feat(agent-mock): add __agentMockSilent flag + signal bridge guard

*  feat(agent-mock): add executeMockStream with side-effect gating

*  feat(agent-mock): add dev-only devClearMockTopics TRPC procedure

*  feat(agent-mock): add dev API to list/read .agent-tracing snapshots

*  feat(agent-mock): add agentMockStore zustand

*  feat(agent-mock): add useMockCases hook

*  feat(agent-mock): add useAgentMockPlayer hook

*  feat(agent-mock): add useMockTopicCleanup hook

*  feat(agent-mock): add Fab entry component

*  feat(agent-mock): add Modal shell with tab bar

*  feat(agent-mock): add CaseList sidebar with search + groups

*  feat(agent-mock): add MiniBar floating playback controls

*  feat(agent-mock): add StatusGrid component

*  feat(agent-mock): add Controls (play/pause/step/speed)

*  feat(agent-mock): add ProgressBar

*  feat(agent-mock): add TargetPicker

*  feat(agent-mock): compose PlayerPanel

*  feat(agent-mock): add TimelinePanel + virtualized EventRow

*  feat(agent-mock): add read-only FixtureViewer with copy button

*  feat(agent-mock): add SettingsPanel with toggles + clear topics

* ♻️ refactor(agent-mock): address quality review (stable itemContent, type-safe error handling, clipboard catch)

*  feat(agent-mock): wire entry component (FAB + Modal + MiniBar)

*  feat(agent-mock): mount AgentMockDevtools in SPAGlobalProvider

* ♻️ refactor(agent-mock): switch Modal to imperative createModal API

* 🐛 fix(agent-mock): use close() + onOpenChangeComplete to preserve motion exit animation

* work

Signed-off-by: Innei <tukon479@gmail.com>

* minify

Signed-off-by: Innei <tukon479@gmail.com>

* 💄 refactor(agent-mock): rebuild devtools UI/UX with mono palette and IA reorg

Replace the in-modal sidebar + tab strip + MiniBar with a Fab-anchored
draggable Popover (case picker, transport, replay/loop, scrubbable progress,
stop, Open DevTools) and a token-driven Modal layout (two-row header,
Segmented view tabs, StatsStrip, sticky TransportBar). Wire EventRow and the
progress bars to seekToEventIndex (resolves the prior TODO), swap alert() for
toast.warning, persist loop and popover position to localStorage.

* work

Signed-off-by: Innei <tukon479@gmail.com>

* 🧹 chore(agent-mock): remove replay debug logs

* 👷 build: add @google/genai to pnpm allowBuilds

Fixes ERR_PNPM_IGNORED_BUILDS in CI — pnpm v11 blocks install
when a dependency with install scripts is not in the allowBuilds list.

* 🐛 fix: resolve TS type errors in useAgentMockPlayer

- parentMessageId: coerce `undefined` to `null` to match `string | null`
- threadId: coerce `null` to `undefined` for cancelOperations param

* ♻️ refactor: revert ConversationArea & sync-import AgentMockDevtools

- ConversationArea: restore messageMapKey(context), avoid needless field spread
- SPAGlobalProvider: switch AgentMockDevtools to sync import (dev-only, no need to lazy)

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-05-06 14:32:59 +08:00
LiJian 831b4ee5ca 🐛 fix: execAgent runtime should have agent management tools (#14371)
* 🐛 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>
2026-05-06 13:56:19 +08:00
Neko c744eab116 feat(agent-signal,database,app,server): agent signal activities during nightly self-reflection will now push to briefs (#14437) 2026-05-06 13:33:54 +08:00
Rdmclin2 7697399da8 feat: optimize line bot (#14448)
* chore: optimize line config schema

* chore: optimize form render order

* chore: update i18n files
2026-05-06 11:50:31 +07:00
LobeHub Bot 05a9eae504 🌐 chore: translate non-English comments to English in edge-config (#14453) 2026-05-06 11:56:09 +08:00
Arvin Xu cc1e0d29d3 💄 style(brief-card): mute icon for resolved briefs on home page (#14452)
* 💄 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>
2026-05-06 11:20:11 +08:00
Arvin Xu 0e6eba61a9 feat(hetero-agent): server-side aiAgent.heteroIngest / heteroFinish + persistence handler (#14444)
*  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>
2026-05-06 10:30:19 +08:00
Arvin Xu 3e8016b502 🔨 chore(cli): update cli version to 0.0.11 (#14451)
🔨 chore: update cli version to 0.0.11

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:26:28 +08:00
Arvin Xu 970733aaeb ♻️ refactor(time): extract useActivityTime hook and move time keys to common (#14443)
♻️ 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>
2026-05-06 10:26:10 +08:00
Arvin Xu c72b1ee698 🐛 fix(changelog): replace gray-matter with browser-compatible frontmatter parser (#14435) 2026-05-06 10:13:46 +08:00
Arvin Xu 7bf923d762 🐛 fix(agent-runtime): finalize trace snapshot on error path (#14440)
* 🐛 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>
2026-05-06 00:26:27 +08:00
Arvin Xu 10300ba0e1 feat(hetero-agent): support multimodal input across CLI / shared spawn / desktop (#14433)
*  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>
2026-05-05 23:06:52 +08:00
Tsuki 431abf36d6 feat(mobile): add homeRouter to mobile tRPC router (#14438)
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>
2026-05-05 19:12:32 +08:00
AmAzing- ce516fff9d 🐛 fix(space): show document update time (#14366) 2026-05-05 14:32:32 +08:00
Zhijie He 9e231835b2 💄 style: add grok-4.3 for xAI (#14382) 2026-05-05 12:24:43 +08:00
LobeHub Bot 79b84a68ec 🌐 chore: translate non-English comments to English in brief-types and conversation (#14410) 2026-05-05 12:23:26 +08:00
LobeHub Bot 56e811f5bd 🌐 chore: translate non-English comments to English in agentSignal and builtin-tool-claude-code (#14432) 2026-05-05 11:53:02 +08:00
Arvin Xu 5fb795b092 feat(cli): add lh hetero exec for standalone heterogeneous agent runs (#14431)
* 🌐 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>
2026-05-05 10:41:58 +08:00
Neko fbe71e76db test(workflows,workflows-hono): mixed export of agent signal types for workflow (#14429) 2026-05-05 04:57:52 +08:00
Arvin Xu d83f0a0f2f ♻️ refactor(chat): introduce agentDispatcher.selectRuntimeType (#14428)
* 🔥 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>
2026-05-05 01:09:01 +08:00
Arvin Xu fe65741a32 ♻️ refactor(hetero-agent): extract producer pipeline into shared package (#14425)
* 💄 style(todo-progress): use colorFillSecondary so left/right borders are visible against QueueTray

The colorBorderSecondary stroke nearly vanished against the dark elevated bg, so the TODO card looked open on the sides when stacked under QueueTray. Match QueueTray's outer border token (colorFillSecondary) for a consistent visible seam; inner dividers keep colorBorderSecondary as a softer secondary level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(hetero-agent): extract producer pipeline into shared package

LOBE-8516 phase 0. Move the JSONL framing + adapter conversion + toStreamEvent
chain out of the renderer into a new `@lobechat/heterogeneous-agents/spawn`
entry, then have desktop main run it before broadcasting. Renderer now
consumes ready-made `AgentStreamEvent`s on `heteroAgentEvent`, dropping ~50
lines of in-renderer adapter wiring.

This unifies the wire shape across desktop main, the upcoming `lh hetero exec`
CLI, and the server `heteroIngest` handler — every consumer gets the same
stamped `AgentStreamEvent` with no per-consumer adapter step.

The desktop CC flow is unchanged behavior-wise: same adapter, same persistence
ordering, same step-boundary semantics; only the seam between main and
renderer moved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(hetero-agent): pull codex tracker into shared spawn, drop desktop's gateway-client dep

Two cleanups on top of the phase 0 refactor:

1. Move `CodexFileChangeTracker` (+ its test) out of `apps/desktop/src/main/modules/heterogeneousAgent/` into `packages/heterogeneous-agents/src/spawn/`. `AgentStreamPipeline` now auto-instantiates it when `agentType === 'codex'`, so the desktop controller (and the future `lh hetero exec` CLI) stays agent-agnostic — no more "if codex { wire tracker via transformPayload }" branching at the call site. The public `transformPayload` hook is removed since it had no other consumer.

2. Re-export `AgentStreamEvent` / `AgentStreamEventType` from `@lobechat/heterogeneous-agents/spawn` and drop `@lobechat/agent-gateway-client` from `apps/desktop/package.json`. The gateway-client package is a browser-side WebSocket client; producer-side callers (desktop main, sandbox CLI) shouldn't carry it as a direct dep — they only need the type, which now flows through the producer-side entry.

Type predicate on Codex payloads tightened to a non-`Required<>` shape so the moved file passes the root tsconfig's `strict: true` (apps/desktop's tsconfig was lax).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🧑‍💻 chore(local-testing): harden electron-dev.sh process management

Lifecycle improvements for the local-testing helper so smoke runs against the desktop dev session are reliable:

- `find_project_pids` now also catches user-started `bun run dev` Electron sessions (matches by project electron path, not just `--remote-debugging-port`), the launcher subshell saved to PIDFILE, and any process bound to the CDP port. Vite match tightened to `electron-vite[/.].*\bdev\b` so unrelated Vite invocations aren't swept up.
- `do_stop` expands seed PIDs into their descendant trees (DFS via `pgrep -P`), SIGTERMs the whole tree, waits 5s, then SIGKILLs survivors. Belt-and-suspenders sweep for stragglers + anything still bound to the CDP port. Closes the long-standing "Helper processes survive the kill" gotcha.
- `do_start` detects existing project Electron/vite before tearing it down so the user sees what's being killed; waits for port + user-data-dir locks to release before relaunching to avoid the "user data directory in use" race.
- `wait_for_cdp` uses an explicit deadline + early bail-out if the launcher PID dies, instead of the previous fixed-step loop. `wait_for_renderer` no longer pre-sleeps 10s.

`setsid` use is intentional; it puts the launched Electron in its own session so the whole tree shares a PGID we can signal in one shot. Note: `setsid` is GNU coreutils — on macOS without `brew install util-linux` the script will fail at the launch step. Documented as a known limitation; no fallback added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): gate session-complete on stdout fully drained

Node may emit `proc.on('exit')` BEFORE child stdio fully closes (documented
in child_process: "stdio streams might still be open"). Phase 0 of LOBE-8516
moved adapter ownership to main, so renderer no longer flushes its own
adapter on session-complete — meaning trailing events synthesized by
`pipeline.flush()` (e.g. Codex's `tool_end` for unfinished tool calls) would
race against, and lose to, the `heteroAgentSessionComplete` broadcast,
leaving renderer-side persistence to finalize on incomplete state.

Fix: in `proc.on('exit')`, await `streamFinished(stdout)` (covers `'end'`,
`'close'`, and `'error'`) BEFORE awaiting the broadcast queue. The first
await ensures the `stdout.on('end')` handler has had a chance to schedule
`pipeline.flush()` onto the queue; the second drains it. Only then do we
broadcast complete / error.

Regression test repros the documented Node race by emitting `exit` before
`stdout.end()` and asserts every `heteroAgentEvent` (including the
synthesized `tool_end` from `pipeline.flush()`) lands before
`heteroAgentSessionComplete`. Bisected: test fails without the gate, passes
with it.

Also: add `packages/heterogeneous-agents` to `apps/desktop/pnpm-workspace.yaml`
to mirror the new workspace dep added in the phase 0 refactor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): drop builtin-tool-claude-code dep, inline the 3 CC wire shapes the adapter needs

Phase 0 added `@lobechat/heterogeneous-agents` as a runtime dep of the desktop
main process. That transitively pulled in `@lobechat/builtin-tool-claude-code`
(declared in the shared package's deps), which the desktop pnpm workspace
doesn't list — CI install on the desktop project fails:

    ERR_PNPM_WORKSPACE_PKG_NOT_FOUND  In ../../packages/heterogeneous-agents:
    "@lobechat/builtin-tool-claude-code@workspace:*" is in the dependencies but
    no package named "@lobechat/builtin-tool-claude-code" is present in the
    workspace

The dep is also a layer-violation: `heterogeneous-agents` is the producer
side (CLI stream → AgentStreamEvent), `builtin-tool-claude-code` is the UI
tool definition (renderers / inspectors / agent template). Producer
shouldn't depend on UI-tool packages, even if today the import is just
types/constants — the dep cascade still drags `shared-tool-ui` etc. into
every workspace that wants the adapter.

Fix: inline the three things the adapter actually uses (`'TodoWrite'` tool
name string, `TodoWriteArgs` interface, `ClaudeCodeTodoItem` interface).
They reflect upstream Claude Code's wire schema — if `claude` ever renames
`TodoWrite`, the adapter and the downstream renderers must both update
regardless of whether they share a constant. Renderer-side packages
(`builtin-tools/codex/TodoListRender`, etc.) keep importing the canonical
`ClaudeCodeApiName` from `@lobechat/builtin-tool-claude-code`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:04:09 +08:00
YuTengjing b5e4cd0805 🐛 fix: revoke sessions after password reset (#14424) 2026-05-05 00:05:05 +08:00
YuTengjing f565ca9450 🐛 fix: revoke sessions after password reset (#14424) 2026-05-04 23:55:48 +08:00
YuTengjing e6d49fdb76 🐛 fix: track visual analysis trigger (#14399) 2026-05-04 23:52:49 +08:00
YuTengjing 47c524a388 🐛 fix: handle Claude assistant prefill errors (#14398) 2026-05-04 23:28:26 +08:00
Arvin Xu cb4412421f ♻️ refactor(local-system,cloud-sandbox): drop "Local" prefix from tool names (#14364)
* ♻️ 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>
2026-05-04 23:19:29 +08:00
Arvin Xu 78b3dbed03 feat: devtools gallery rebuild, Review polish, queue-tray images (#14423) 2026-05-04 23:12:59 +08:00
Arvin Xu 95375cec79 ♻️ refactor(builtin-tools): retire lobe-tools alias and slim lobe-notebook to render-only (#14422)
* ♻️ 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>
2026-05-04 20:58:00 +08:00
Arvin Xu aa3c7e585b 💄 style(builtin-tools): add UI render for github marketplace tool (#14420)
*  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>
2026-05-04 20:28:47 +08:00
Arvin Xu 11e6619a3c feat(server,task): batch run subtasks in dependency order (#14418)
*  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>
2026-05-04 19:44:40 +08:00
Arvin Xu 41719dfd29 🐛 fix(gateway): unstick input loading on auth_failed + recoverable auth_expired (#14419)
* 🐛 fix(gateway): complete local op on auth_failed to unstick input loading

When the gateway client receives `auth_failed` (server has GC'd the op or
the refreshed JWT no longer matches), the local op stayed `running`
forever — input kept the stop button, and `topic.metadata.runningOperation`
never cleared, so every revisit re-fired the same broken reconnect.

Treat `auth_failed` as session-terminal alongside `session_complete` so
`onSessionComplete` fires and `completeOperation` runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(gateway): support recoverable auth_expired with token refresh

When the JWT expires while the operation is still alive on the server,
sending `auth_failed` is wrong — the op is fine, only the credential
went stale. Treat that as a separate, recoverable signal instead.

Server (agent-gateway repo) emits a new `auth_expired` message and
keeps the WebSocket open. The client refreshes its JWT (via the
existing `aiAgentService.refreshGatewayToken`), updates the in-flight
client, and reconnects. `auth_failed` stays terminal for cases where
the op truly no longer exists.

Mirrors the device-gateway-client pattern (`auth_expired` event +
`updateToken` + `reconnect`). If no `tokenRefresher` is wired in (or
the refresh itself fails), we fall back to terminal so the input
doesn't stay stuck on the loading state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(gateway): disconnect ws on auth_expired without tokenRefresher

The server keeps the WebSocket open after `auth_expired` (so the client
can refresh and re-auth on the same connection). When no `tokenRefresher`
is wired in, we mark the local op complete but were leaving the socket —
heartbeat and autoReconnect kept running indefinitely after the op was
gone, leaking background connections.

Mirror the refresh-failure branch and call `client.disconnect()` before
firing onSessionComplete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(gateway): make tokenRefresher required on connectToGateway

Both real callers (executeGatewayAgent + reconnectToGatewayOperation)
already supply a refresher built from `aiAgentService.refreshGatewayToken`,
and there's no scenario where a Gateway op runs without a topic to refresh
against. The optional path was carrying its own foot-gun (socket leak if
forgotten) and a defensive ternary on `result.topicId` that the type
already rules out.

Required-only collapses both into the existing refresh-failure branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(gateway): inline token refresh, take topicId instead of refresher

Both callers of connectToGateway built identical refresher closures over
`aiAgentService.refreshGatewayToken(topicId)`. Pass `topicId` directly and
let connectToGateway call the service inline — gateway.ts already imports
aiAgentService for the cancel-handler path, so no new coupling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 chore(gateway): rewrite stale auth_expired comment

The "no refresher provided" branch is gone — fold that case out of the
comment and explain why the catch branch needs explicit disconnect().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:39:31 +08:00
Arvin Xu b66e83a57c 🐛 fix(security): add max pagination limits to tRPC endpoints (#14415)
* 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>
2026-05-04 18:53:28 +08:00
Arvin Xu bc103b2e11 ♻️ refactor(web-crawler): remove zhihu-specific crawl rules (#14414) 2026-05-04 14:54:46 +08:00
AmAzing- d28b401aaf refactor: agent list reuse to isolate drawer state (#14411)
 Refactor agent list reuse to isolate drawer state
2026-05-04 12:01:06 +08:00
Neko a79cdd19f8 ️ perf(server,agent-signal): improved skill intent detection (#14409) 2026-05-04 06:33:58 +08:00
Neko 222f525bf4 ♻️ refactor(types,agent-signal): request trigger will use agent-signal enum (#14408) 2026-05-04 04:56:47 +08:00
Neko 317fdcec13 feat(app,agent-signal): new agent recent activities to display for signal receipts triggered (#14407) 2026-05-04 04:14:54 +08:00
Neko 162d6cfa67 🐛 fix(userMemories): should parse and validate date string for time intent (#14406) 2026-05-04 04:14:13 +08:00
Arvin Xu 2870cc73c2 feat(builtin-tool-task): add Inspector + Render, batch createTasks/runTasks (#14403)
*  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>
2026-05-04 01:15:41 +08:00
Arvin Xu d5097c7964 💄 fix(builtin-tool-agent-documents): wire Inspectors into registry, switch to chip UI (#14404)
* 💄 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>
2026-05-04 00:56:06 +08:00
Neko aa3d245cfd ♻️ refactor(server,prompts,builtin-tool-skill-maintainer): correct context passing, skill format, chained (#14397) 2026-05-03 23:30:44 +08:00
Arvin Xu 61c3f42f10 🐛 fix: sync DEFAULT_MODEL into desktop business-const stub (#14402)
🐛 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>
2026-05-03 23:29:04 +08:00
YuTengjing 2dd52c6813 feat: show original pricing and prioritize DeepSeek (#14391) 2026-05-03 01:27:52 +08:00
Arvin Xu 3f82249ed1 💄 style: add feedback input at bottom of TopicChatDrawer (#14392)
*  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>
2026-05-03 01:07:43 +08:00
LobeHub Bot b49c1c15b7 🤖 style: update i18n (#14383)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2026-05-03 01:07:03 +08:00
YuTengjing df32dd4966 feat: support model defaults and DeepSeek pricing (#14379) 2026-05-02 23:21:09 +08:00
YuTengjing b5d7696dbd feat: add visual understanding tool (#14378) 2026-05-02 22:18:50 +08:00
Arvin Xu d2d81ba64a 💄 style(document-modal): show skeleton for title while loading (#14377)
* 💄 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>
2026-05-01 18:51:09 +08:00
YuTengjing b2130f7612 🐛 fix: handle auth captcha retries (#14346) 2026-05-01 18:27:04 +08:00
Arvin Xu 626d274859 🔨 chore(release-template): clean up changelog templates (#14375)
* 🔨 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>
2026-05-01 16:46:16 +08:00
Arvin Xu 9c509680b9 🚀 release: sync main branch to canary (#14374)
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.
2026-05-01 16:33:03 +08:00
Arvin Xu 70f81ad1a1 🚑 fix: resolve unresolved merge conflict markers in main→canary sync
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>
2026-05-01 16:18:01 +08:00
Arvin Xu c401d1b97f Merge remote-tracking branch 'origin/main' into sync/main-to-canary-20260501-25207007930 2026-05-01 15:57:49 +08:00
lobehubbot eddb0c991b 🔖 chore(release): release version v2.1.56 [skip ci] 2026-05-01 07:49:26 +00:00
lobehubbot 6340ab55e9 chore: merge main into canary (has conflicts to resolve) 2026-05-01 07:47:44 +00:00
Neko 86a23b5555 👷 build(database): add metadata and trigger to briefs table (#14354)
* 👷 build(database): add metadata and trigger to briefs table

*  test(server): should not use adhoc Date.now() (#14280)
2026-05-01 15:47:02 +08:00
Arvin Xu 3cb06e07e3 💄 style(taskDetail): force daily briefs for scheduled tasks; switch activity timestamps to absolute date (#14367)
*  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>
2026-05-01 15:37:15 +08:00
Innei c9b44935ed revert: revert pnpm v11 migration (#14372)
* Revert "👷 build: disable pnpm gvs for desktop ci (#14357)"

This reverts commit 948ba5ec68.

* Revert "👷 build(repo): migrate to pnpm v11 and consolidate workspace config (#14316)"

This reverts commit 1d9b6099bd.
2026-05-01 14:45:28 +08:00
Innei 948ba5ec68 👷 build: disable pnpm gvs for desktop ci (#14357)
* 👷 build: disable pnpm gvs for desktop ci

* 👷 build: increase desktop install heap

* 👷 build: raise linux desktop file limit

* 👷 build: skip desktop package rebuild

* 👷 build: hoist desktop isolated install

* 👷 build: skip desktop dependency collector

* 👷 build: mark desktop modules externally handled

* 👷 build: limit desktop native runtime deps

* 👷 build: include get-windows runtime resolver deps
2026-05-01 13:17:21 +08:00
LiJian d0091901dc 🐛 fix(skill): skip OAuth redirectUri on desktop to prevent broken app (#14345)
🐛 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>
2026-05-01 13:00:55 +08:00
Arvin Xu 8c3b83f8b3 🐛 fix(local-system): tokenize mdfind keywords, scope glob to home, align tool prompts (#14358)
* 🐛 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>
2026-05-01 02:19:31 +08:00
Arvin Xu b031513321 🐛 fix(brief): keep recurring tasks active when resolving their result briefs (#14348)
* 🐛 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>
2026-05-01 01:26:03 +08:00
Innei c2b379139d feat(followUpAction): add quick-reply chips below assistant messages (#14350)
*  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>
2026-05-01 01:20:45 +08:00
Arvin Xu 6d1d8a0d16 💄 style(brief): use Footprints icon and hide view-run until card hover (#14347)
* 💄 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>
2026-05-01 00:34:01 +08:00
Arvin Xu dc3c48e469 🐛 fix(local-system): forward all search params and guard empty mdfind (#14355)
* 🐛 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>
2026-05-01 00:22:14 +08:00
AmAzing- 79dc61ac50 🐛 fix: subtask assignee refresh and rollback handling (#14353)
* Handle subtask assignee rollback refresh

* Ignore cache refresh failures after successful task update
2026-04-30 22:45:11 +08:00
AmAzing- 506bb7b29f Fix task subtitle and assignee trigger layout (#14351) 2026-04-30 19:05:51 +08:00
Innei 807af0688f 🐛 fix: type tag cloud pointer event (#14352) 2026-04-30 19:00:54 +08:00
Innei 1d9b6099bd 👷 build(repo): migrate to pnpm v11 and consolidate workspace config (#14316)
* 👷 build(repo): migrate to pnpm v11 and consolidate workspace config

Made-with: Cursor

* 👷 fix pnpm v11 install config
2026-04-30 17:56:22 +08:00
LiJian 5fc7eea754 🐛 fix: inject skill instruction into tool system role (#14342)
*  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>
2026-04-30 17:06:28 +08:00
YuTengjing a9716975a7 💄 style: unify notification setting item copy (#14343) 2026-04-30 16:56:45 +08:00
Arvin Xu c77d201c49 💄 style(brief): open run topic drawer from daily brief card (#14340)
*  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>
2026-04-30 15:47:24 +08:00
Arvin Xu 39107ba107 ️ perf(agent,working-sidebar): cut Review tab open latency ~9× on large dirty trees (#14338)
* ️ 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>
2026-04-30 15:46:09 +08:00
YuTengjing d0e99aada4 🐛 fix: stop router fallback for invalid requests (#14285) 2026-04-30 15:15:25 +08:00
Arvin Xu 13e8ef9c7b 💄 style(brief): show artifacts in card and extract DocumentModal (#14339)
*  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>
2026-04-30 14:34:19 +08:00
CanYuanA 8387067807 🐛 fix: fix PDF chunking logic to prevent vectorization failure (#14327) 2026-04-30 13:55:36 +08:00
Tsuki 375e6381ce feat(mobile-router): add task and brief routers (#14337)
 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>
2026-04-30 13:09:38 +08:00
Arvin Xu f7c1ebf652 🚀 release: sync main branch to canary (#14317)
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.
2026-04-30 13:08:39 +08:00
Arvin Xu 156a870cf3 🐛 fix(model-runtime): preserve LLM finishReason through callbacks transformer (#14336)
* 🐛 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>
2026-04-30 12:46:30 +08:00
Innei f017dcd0ea test: fix home cold route CI coverage 2026-04-30 12:40:31 +08:00
lobehubbot 719a554456 🔖 chore(release): release version v2.1.55 [skip ci] 2026-04-30 12:37:58 +08:00
Innei 3b1eef72d8 🐛 fix(chat): preserve topics across cold route sends (#14284)
**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
2026-04-30 12:37:58 +08:00
lobehubbot 9e20cd6b3a 🔖 chore(release): release version v2.1.54 [skip ci] 2026-04-30 12:37:18 +08:00
LobeHub Bot a5f4b4b569 🌐 chore: translate non-English comments to English in agent-runtime examples and siliconcloud provider (#14332)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 12:14:03 +08:00
LiJian 5a15f759d6 refactor(creds): add local/desktop credential injection guidance (#14306)
*  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>
2026-04-30 12:07:29 +08:00
Arvin Xu b7ecf2fd4d feat(agent,working-sidebar): add Review tab with bulk git diffs (#14334)
 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>
2026-04-30 11:53:49 +08:00
Tsuki 24062bb412 💄 style(daily-brief): add skeleton loading state (#14333)
💄 style(daily-brief): add skeleton loading state for DailyBrief component

LOBE-8400

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 11:40:14 +08:00
LobeHub Bot 61d432a991 🤖 style: update i18n (#14330)
💄 style: update i18n

Co-authored-by: canisminor1990 <17870709+canisminor1990@users.noreply.github.com>
2026-04-30 10:17:41 +08:00
Arvin Xu f59954137a 💄 style(task): add start-scheduling button in automation popover (#14323)
*  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>
2026-04-30 10:03:52 +08:00
Neko 1324b67590 ♻️ refactor(server): agent signal now is easier to use (#14326) 2026-04-30 05:36:26 +08:00
Neko f390d04ef2 🐛 fix(server): prefer to use tool call id first (#14322) 2026-04-30 05:07:51 +08:00
Arvin Xu 84df8a9994 ♻️ refactor(task-brief): auto-synthesize topic briefs (#14324)
*  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>
2026-04-30 00:58:39 +08:00
YuTengjing 9aea74659f 🐛 fix: restore task agent panel toggle (#14321) 2026-04-30 00:46:28 +08:00
Arvin Xu 105321bfe1 🐛 fix(file-loaders): support UTF-16 encoded text files in TextLoader (#13615)
* 🐛 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>
2026-04-29 23:59:50 +08:00
YuTengjing b0b6e67d5f feat: support DeepSeek Anthropic runtime (#14312) 2026-04-29 23:57:18 +08:00
YuTengjing d2aa3cd1b4 🐛 fix(model-bank): reject lobehub model ids no longer in the bank (#14261) 2026-04-29 23:52:11 +08:00
AmAzing- babdc6ade5 Fix task drawer agent metadata hydration (#14315) 2026-04-29 22:55:55 +08:00
YuTengjing 7e6255096a ♻️ refactor: use virtual model id for default onboarding model (#14311) 2026-04-29 22:44:20 +08:00
Neko 0e7eda4b47 feat(agent-signal,server,prompts,builtin-tool-memory): score based orchestration, consolidate better (#14314) 2026-04-29 22:42:58 +08:00
lobehubbot 71cfba9906 🔖 chore(release): release version v2.1.55 [skip ci] 2026-04-29 14:09:35 +00:00
Innei b8fe675508 🐛 fix(chat): preserve topics across cold route sends (#14284)
**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
2026-04-29 22:06:01 +08:00
Innei 990942fb45 feat(agent-marketplace): fetch onboarding templates from market API (#14286)
*  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>
2026-04-29 21:25:16 +08:00
Innei ecec2e87e3 🐛 fix: use fileId for proxy URL in knowledge queries (#14051)
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
2026-04-29 20:02:23 +08:00
Innei 7b6978271a feat(chat): support local file mention snapshots (#14278)
*  support local file mention snapshots

*  feat(local-file-mention): implement useLocalFileMention hook for local file search functionality

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix desktop project file index fallback

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-29 19:39:31 +08:00
YuTengjing 28c2e9002a 🔥 chore: drop useSkillConnection hook (moved into cloud feature) (#14308) 2026-04-29 19:32:15 +08:00
Rdmclin2 b9034ce9c1 🐛 fix: e2e page related tests (#14309)
* chore: add default home locales

* fix: e2e tests

* fix: LobeAI locales

* fix: Lobe AI locales

* fix: test case errors

* chore: update i18n files
2026-04-29 17:49:56 +07:00
Rdmclin2 2eb7ee824f feat: support Line (#14207)
* feat: support Line

* chore: update Line docs

* feat: support line platform

* chore: update markdown files

* fix: lint error

* fix: home padding block
2026-04-29 15:37:27 +07:00
YuTengjing e78949cd23 🐛 fix: reset task agent transient state (#14303) 2026-04-29 16:37:13 +08:00
Arvin Xu afae236628 🐛 fix(task): manual run no longer eats the next scheduled tick (#14304)
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>
2026-04-29 16:21:09 +08:00
Neko 8830c6d560 ♻️ refactor(server): prevent writing useless metadata into documents for agent signal managed skills (#14291) 2026-04-29 15:57:03 +08:00
Arvin Xu f42fc7d65d 🐛 fix: include all properties in task_topic_handoff response_format required (#14297)
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>
2026-04-29 15:54:29 +08:00
Neko e5e154afcb ♻️ refactor(agent-signal): simplify structure of agent-signal (#14290) 2026-04-29 15:45:21 +08:00
Rdmclin2 346812ab88 🔨 chore: update i18n files & fix title skeleton (#14301)
* chore: update i18n files & fix title skeleton

* chore: update taskTemplate json

* chore: update i18n files
2026-04-29 13:23:26 +07:00
YuTengjing a099749b41 ♻️ refactor(taskTemplate): use string icon identifiers (#14302) 2026-04-29 13:54:41 +08:00
Arvin Xu fbe8ab3891 ♻️ refactor(context-engine): drop ____builtin suffix from tool names (#14289)
♻️ 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>
2026-04-29 11:25:24 +08:00
Arvin Xu 2965cbc83a docs(lobehub-skill): add video/image model lookup guide to generate & model references (#14264)
* 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>
2026-04-29 11:04:04 +08:00
AmAzing- fc44aaef38 Persist model detail panel expansion state (#14294) 2026-04-29 10:35:13 +08:00
Arvin Xu a2b8f4c81a 🐛 fix: consolidate agent-documents tools and fix empty readDocument (#14288) 2026-04-29 09:33:48 +08:00
Arvin Xu 6f9f5643d1 feat: polish task list & detail, expose topic operation ID (#14282)
* ♻️ 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>
2026-04-29 02:40:38 +08:00
yueyinqiu e4877436fe uncomment image / video / text2music in modelTypeOptions (Form.tsx) (#14275) 2026-04-29 02:32:16 +08:00
Zhijie He 04775f66ff 💄 style: migrate Hunyuan to TokenHub for Hy3 Preview (#14108)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-04-29 02:31:21 +08:00
Neko 9fff5fccf0 feat(app,server,agent-signal,cli): new policy for Skill management running inside of Agent Signal (#14281) 2026-04-29 02:18:57 +08:00
Rdmclin2 5a46c5a971 feat: refactor home (#14266)
* 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>
2026-04-29 00:57:22 +07:00
YuTengjing 5722b7159b feat: add task manager copilot (#14272) 2026-04-29 01:21:40 +08:00
Zhijie He 49a71bed6e 🐛 fix: expose CRAWLER_TIMEOUT env for crawler (#14274)
chore: expose CRAWLER_TIMEOUT env for crawler
2026-04-29 00:06:53 +08:00
Neko d5511a6af2 feat(cli,server,database): now agent document can be used as vfs, offer fs compatible output (#14222) 2026-04-28 23:50:50 +08:00
Neko e46e81a08a test(server): should not use adhoc Date.now() (#14280) 2026-04-28 23:43:15 +08:00
Arvin Xu 9555e4fda3 feat: task card, agent profile nav, CC streaming, view switcher polish (#14277)
* 💄 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>
2026-04-28 23:21:45 +08:00
Neko 729fbc72d5 🔨 chore(agent-signal,app): added tracing to agent signal, ensure traceparent propagate to handler (#14212) 2026-04-28 22:54:03 +08:00
Neko 0e1a55f2f8 🔨 chore(.agents): added skill for agent-signal (#14206) 2026-04-28 22:53:16 +08:00
Arvin Xu c1e2d134ed 🐛 fix(conversation): stop topic scroll restore from corrupting its own snapshot (#14247)
* 🐛 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>
2026-04-28 22:51:36 +08:00
Innei 8663991c7c feat: auto-dismiss upload dock after completion (#14055)
*  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>
2026-04-28 21:59:19 +08:00
Coooolfan 35edca5531 🐛 fix: render intervention fallback avatar as image (#14271) 2026-04-28 21:45:36 +08:00
Arvin Xu 101b9f9973 💄 style(task): task detail polish (#14269)
* 💄 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>
2026-04-28 19:50:28 +08:00
Innei c6a013a1a1 🐛 fix(home): restore welcome typewriter stability (#14270) 2026-04-28 19:12:41 +08:00
YuTengjing 19643ba662 feat(task-template): add home recommendation system with skill connect (#14214) 2026-04-28 18:11:00 +08:00
Arvin Xu 2654c4d31e 💄 style(task): polish schedule, artifacts, and task list UI (#14248)
* 💄 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>
2026-04-28 17:31:16 +08:00
Innei b94aa1da90 feat(chat): route leading agent mentions directly (#14237)
*  feat(chat): route leading agent mentions directly

* 🐛 fix(chat): propagate thread flag for direct mention runtime
2026-04-28 17:14:16 +08:00
Rdmclin2 e896024b68 feat: optimize bot cli & userId guide (#14258)
* 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>
2026-04-28 15:14:51 +07:00
Innei 2835b99d1a 🐛 fix(conversation): inline single-tool assistant group and promote leading sentence (#14244)
Made-with: Cursor
2026-04-28 15:16:02 +08:00
YuTengjing 47812b2be9 🐛 fix(user-state): include notification settings in getUserState (#14257) 2026-04-28 11:33:07 +08:00
René Wang 798644414a 📝 docs: add April 27 weekly changelog (#14249) 2026-04-28 11:04:51 +08:00
YuTengjing 54bb83f229 feat(aihubmix): add gpt-image-2 and Grok 4.20 models (#14253) 2026-04-28 10:57:49 +08:00
Octopus 65da232c64 fix(utils): preserve JPEG format when compressing uploaded images (#13585)
* 🐛 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>
2026-04-28 10:33:22 +08:00
BillionToken dacc7798ab fix(image): preserve resolution when changing aspect ratio (#13324)
Co-authored-by: BillionClaw <267901332+BillionClaw@users.noreply.github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-04-28 10:27:57 +08:00
Zhijie He 9508807da7 💄 style: add build-in websearch for Volcengine via ResponseAPI (#14216) 2026-04-28 10:18:39 +08:00
Zhijie He 6a7eb17cd2 💄 style: update batch of models (#14070) 2026-04-28 10:17:17 +08:00
YuTengjing c5da34b680 🔨 chore: refresh team assignment guide (#14243) 2026-04-28 10:15:18 +08:00
Arvin Xu 2a37b77482 ♻️ refactor(recent): rewrite queryRecent in Drizzle, exclude web-tool scrapes (#14239)
* ♻️ 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>
2026-04-28 01:24:16 +08:00
Arvin Xu b814cf2611 feat(task): scheduled status + cron schedule editor (#14246)
*  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>
2026-04-28 01:17:43 +08:00
LiJian c37817e2d8 🐛 fix: add the lobehub cli oidc expreis should refresh aksk (#13925)
* fix: add the lobehub cli oidc expreis should refresh aksk

* fix: add the buffer seconds
2026-04-28 00:47:25 +08:00
Arvin Xu bbf239705c 🐛 fix(send-message): forward topic-list filter to server response (#14160)
* 🐛 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>
2026-04-28 00:31:38 +08:00
Innei 8a9f42596d 📝 docs(version-release): add hotfix changelog example and patch scenario [skip ci] (#14242)
📝 docs(version-release): add hotfix example and patch scenario note

Made-with: Cursor
2026-04-27 23:43:35 +08:00
lobehubbot 682657ba50 🔖 chore(release): release version v2.1.54 [skip ci] 2026-04-27 15:41:37 +00:00
sxjeru 29235dc1ed 💄 style: interactive improvement of model search (#14192)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-04-27 23:41:28 +08:00
lobehubbot e326400dbe Merge remote-tracking branch 'origin/main' into canary 2026-04-27 15:39:18 +00:00
Innei deeb97ab5b 🐛 hotfix: clear stale topic when switching agents from a topic route (#14231)
* 🐛 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>
2026-04-27 23:35:51 +08:00
sxjeru d73858ef42 💄 style: add GPT-5.5 and GPT-5.5 Pro models (#14142)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-04-27 23:27:28 +08:00
sxjeru 6b9584714d 🐛 fix(Action): model params panel closes unexpectedly during auto-save (#14198)
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 23:18:49 +08:00
Arvin Xu b9a4a9093c 🐛 fix(topic): drop switchTopic race under rapid sidebar clicks (#14115)
* 🐛 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>
2026-04-27 23:17:21 +08:00
Arvin Xu ef5be7e17c fix(cli): clarify asyncTaskId vs generationId in gen status/download + better error message (#14230)
* 🔖 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>
2026-04-27 23:16:05 +08:00
Innei a4235d3f68 ⬆️ chore: upgrade desktop Electron to 41.3.0 (#14223)
* ⬆️ chore: upgrade desktop electron to 41.3.0

* 🐛 fix: patch ant design icons vitest resolution

* 🐛 fix: require fixed ant design icons version
2026-04-27 21:54:23 +08:00
AmAzing- fa508f4259 feat: add agent-specific topic grouping mode and improve empty state… (#14225) 2026-04-27 21:52:10 +08:00
YuTengjing 94767fddcb 🐛 fix(utils): keep tiny prices visible in formatPrice (#14235) 2026-04-27 20:20:53 +08:00
Arvin Xu 685b17e59e 💄 style(tasks): detail polish round + heartbeat webhook fix + notif deep-link (#14228)
*  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>
2026-04-27 19:50:18 +08:00
YuTengjing 9acb128943 📝 docs(skills): rename code-review to review-checklist (#14229) 2026-04-27 18:17:16 +08:00
Arvin Xu ee55d74dd4 💄 style(tasks): drop custom actions on result briefs & show trigger tag in subtasks (#14226)
 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>
2026-04-27 17:44:03 +08:00
YuTengjing cca1050e82 🐛 fix: localize provider moderation generation errors (#14220) 2026-04-27 15:22:56 +08:00
Arvin Xu 92a848c69c feat(tasks/brief): subtask avatar polish, brief actions revamp & task drawer Gateway reconnect (#14208)
* 💄 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>
2026-04-27 13:54:19 +08:00
Arvin Xu f32fff19dd 📝 docs(skills): record contributor roster in version-release (#14219)
📝 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>
2026-04-27 13:23:04 +08:00
lobehubbot 376976849b 🔖 chore(release): release version v2.1.53 [skip ci] 2026-04-27 05:20:52 +00:00
lobehubbot 38d7bdbd96 Merge remote-tracking branch 'origin/main' into canary 2026-04-27 05:19:09 +00:00
Arvin Xu a52104552a 🚀 release: 20260427 (#14217)
# 🚀 LobeHub v2.1.53 (20260427)

**Release Date:** April 27, 2026
**Since v2.1.52:** 194 merged PRs · 17 contributors

> Introduce Heterogeneous Agent — Claude Code and Codex run as
first-class desktop runtimes, paired with a new Agent Signal package,
sharper desktop UX, and a wave of flagship model additions.

---

##  Highlights

- **Introduce Heterogeneous Agent** — Claude Code and Codex run as
first-class desktop agents: subagent rendering, partial-message
streaming, multi-turn resume, terminal error surfacing, rich tool
inspectors, and runtime polish. (#14162, #13754, #14067, #14001, #13970,
#13942)
- **Screen capture & Quick Chat tray** — New desktop screen capture
overlay (macOS permission-gated) with Quick Chat tray and upload
pipeline improvements; chat input auto-focuses on overlay mount.
(#13818, #14097, #14105)
- **Desktop topic & tab UX** — Dedicated topic popup window with
cross-window sync, Cmd+W/Cmd+T tab shortcuts, TabBar polish, recent
working directories expanded to 20, and human approval notifications.
(#13957, #13983, #13972, #14036, #14092)
- **Git workflow built-in** — One-click pull/push from the branch chip,
ahead/behind badge, and submodule/worktree repo detection. (#14041,
#13980, #13978)
- **Agent Signal package** — New `@lobechat/agent-signal` runtime for
dynamic memory feedback signals, with OTel metrics and self-iteration in
Lab. (#14157, #14170, #14159, #14169, #14187)
- **New models** — Claude Opus 4.7 with `xhigh` effort tier, GPT-5.5,
DeepSeek V4 Flash/Pro with reasoning slider, Kimi K2.6, MiMo-V2.5/Pro,
gpt-image-2, Qwen3.6 Flash/Plus, and Pixverse-c1. (#13903, #14147,
#14114, #14004, #14089, #14039, #13923)
- **New providers** — OpenCode Zen, OpenCode Go, and Azure OpenAI Router
runtime. (#13943, #14064, #13823)
- **Mobile settings overhaul** — Full settings menu and responsive
profile layout for mobile. (#14019)

---

## 🏗️ Heterogeneous Agent

- Claude Code runtime, working-directory awareness, and sidebar polish.
(#13970)
- CC subagent rendering with persistent streamed text; parallel-tool
orphan fix. (#14001, #13968, #14024)
- Per-step usage persisted to each step assistant message. (#13964)
- Per-phase workflow expand defaults; full-expand toggle with
three-level expansion. (#14171, #13906)
- Hetero-mode actions bar; tool inspector polish. (#13963, #14034,
#14030)
- Codex desktop integration with rich tool rendering and devtools
preview. (#14067, #14100)
- Codex terminal error surfacing and CLI output tracing. (#14166)
- Tighten `isCanUseVision` default and add aggregator fallback. (#14172)
- Persist `ccSessionId` in topic metadata for CC multi-turn resume.
(#13902)
- CC account card, topic filter, and integration polish. (#13955,
#13942, #13950)
- Token-level deltas streamed via `--include-partial-messages`. (#13929)

---

## 🧠 Agent Signal & Self-Iteration

- New `@lobechat/agent-signal` package with dynamic feedback signals.
(#14157)
- AgentSignalRuntime wired through agent-tracing and observability-otel
metrics. (#14170, #14159)
- Self-iteration feature flag added to Lab; front-side flag check.
(#14169, #14186)
- Signal policy for receiving memory feedback dynamically. (#14187)

---

## 💬 Conversation

- Queue follow-up sends during running CC turns. (#14179)
- Persist per-topic chat scroll position; pin user message + fold long
messages. (#14191, #14056)
- Inline resend when editing last user message. (#14080)
- Disable first-block markdown streaming to prevent flicker. (#14193,
#13904)
- Prevent Markdown stream replay when vlist remounts streaming items.
(#14086)
- Stop repinning after manual scroll; unify scroll-to-user + spacer
hooks. (#14099, #14132)

---

## 📱 Platforms & Integrations

### Desktop / Electron

- Screen capture overlay, Quick Chat tray, and upload pipeline
improvements. (#13818)
- macOS permission gate for screen capture; auto-focus chat panel input.
(#14097, #14105)
- Dedicated topic popup window with cross-window sync. (#13957)
- TabBar polish: `+` button for new topic, dark theme blend, close icon
by default. (#13972, #14203, #13973)
- Recent working directories expanded from 5 to 20; submodule/worktree
repo detection. (#14036, #13978)
- Cmd+W / Cmd+T tab shortcuts and global shortcut consolidation.
(#13983, #13880)
- Linux icon configuration; human approval desktop notifications.
(#14042, #14092)

### Git Workflow

- One-click pull/push from branch chip; ahead/behind badge with
refactored GitCtr. (#14041, #13980)

### Mobile

- Full settings menu and responsive profile layout. (#14019)
- Agent route added to mobile router; mobile agent topic route
registered. (#14103, #14158)
- Session list skeleton row layout corrected. (#14040)

### Bot / Messaging

- DM strategy support; bot emoji and markdown render optimization.
(#14201, #14091, #14140)
- Slack webhook fix; bot platform setup guide reference. (#14052,
#14121)

---

## 🤖 Models & Providers

### New models

- **Claude Opus 4.7** with `xhigh` effort tier; strip temperature/top_p.
(#13903, #13909)
- **GPT-5.5**. (#14147)
- **DeepSeek V4** Flash/Pro cards with reasoning slider; cache-hit and
Pro discount pricing. (#14114, #14209, #14196, #14131)
- **Kimi K2.6** model with LobeHub-hosted card. (#14004, #14006)
- **MiMo-V2.5 / V2.5-Pro**. (#14089)
- **gpt-image-2**, **Qwen3.6 Flash/Plus**, **Pixverse-c1**. (#14039,
#13923)

### New providers

- **OpenCode Zen** and **OpenCode Go** with env-var support. (#13943,
#14064)
- **Azure OpenAI Router** runtime support. (#13823)
- Model alias mapping for image and video runtimes. (#13896)
- Seedance video models migrated to Dreamina. (#14144)

### Runtime reliability

- Sanitize invalid tool_call arguments to unbreak strict providers.
(#14033)
- Tolerate null `function.name` in streaming tool_call deltas. (#14139)
- Preserve Gemini 3 `thoughtSignature` in `call_tools_batch`
normalization. (#14032)
- Downgrade `image_url` parts when target model lacks vision. (#14029)
- Preserve Cloudflare provider error context. (#14136)
- Use `safety_identifier` for OpenAI Responses API. (#14148)
- Unwrap underlying PG error in `formatErrorEventData`. (#14038)

---

## 🖥️ User Experience

- **Onboarding** — Preset agent naming suggestions, structured hunk ops
for `updateDocument`, persona analytics snapshot, footer promotion
pipeline, wrap-up button. (#13931, #13989, #13930, #13853, #13934)
- **Document workflow** — Agent documents promoted as primary workspace
panel; history management and compare workflow; web-crawl docs
associated with agent documents. (#13924, #13725, #13893)
- **cmdk** — Agent identity surfaced on topic search results;
topic/message search scoped to current agent. (#14204, #13960)
- **Floating chat panel** and workspace improvements. (#13887)
- **Topic completion status** with dropdown action and filter. (#14005)

---

## 🔧 Tooling

- Redis-backed feature flag provider for runtime config. (#14098)
- Vite upgraded to 8.0.0 with Rolldown strict execution order. (#12720,
#14058)
- `@lobechat/model-bank` automated npm release with provenance. (#14015,
#14017, #14018)
- Skill activation fallback when `activateTools` cannot find identifier.
(#14010)
- Cron tool: timezone and existing jobs injected into system prompt;
clarified `lobe-gtd` and `lobe-cron` descriptions. (#14012, #14013)

---

## 🔒 Security & Reliability

- **Security:** uuid bumped to v14 (advisory). (#14083)
- **Security:** validate avatar URL and scope old-avatar deletion to
owner. (#13982)
- **Security:** clear OIDC sessions on better-auth signout; return 401
(not 500) for expired OIDC JWT. (#13916, #14014)
- **Reliability:** scope pending-approval check to current assistant
turn. (#14182)
- **Reliability:** sanitize heterogeneous-agent attachment cache
filenames. (#13937)
- **Reliability:** reduce subagent task status error noise. (#14026)

---

## 👥 Contributors

Huge thanks to **17 contributors** who shipped **194 merged PRs** this
week.

@Hardy · @shaun0927 · @hezhijie0327 · @sxjeru · @arvinxx · @Innei ·
@tjx666 · @LiJian · @Neko · @Rdmclin2 · @AmAzing129 · @sudongyuer ·
@CanisMinor · @rivertwilight

Plus @lobehubbot and renovate[bot] for maintenance.

---

**Full Changelog**:
https://github.com/lobehub/lobehub/compare/v2.1.52...v2.1.53
2026-04-27 13:18:26 +08:00
Rdmclin2 3e236ec36f feat: support dm pair policy (#14211)
* feat: support pair dm policy

* feat: add enum descriptions

* chore: optimize labels and copy

* chore: update i18n

* fix: lint error

* chore: update bot docs

* fix: peek paring request and so on issues
2026-04-27 11:31:07 +07:00
YuTengjing 57781850ce feat(notification): add i18n keys for scheduled task failure (#14088) 2026-04-27 10:26:55 +08:00
LiJian a101957715 fix(activator): add Klavis service triggers to lobe-creds activation rules (#14134)
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
2026-04-27 10:26:28 +08:00
YuTengjing 4e309e6f26 🐛 fix: update DeepSeek cache hit pricing (#14209) 2026-04-27 01:21:53 +08:00
Neko fd9b0531ec feat(agent-signal,agent-signal/policies): added signal policy for receiving feedback dynamically, for memory (#14187) 2026-04-26 22:49:54 +08:00
Arvin Xu 91db61b74f feat(cmdk): show agent identity on topic search results (#14204)
*  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>
2026-04-26 22:48:58 +08:00
Innei 1d7b81233a 💄 style(electron): refine desktop tab bar dark theme surface (#14203)
Made-with: Cursor
2026-04-26 22:12:11 +08:00
Arvin Xu 35c3d5e08d feat(task): wire QStash-driven heartbeat self-rescheduling (#14199)
* 💄 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>
2026-04-26 20:53:18 +08:00
Arvin Xu a176288670 💄 style(conversation): persist per-topic chat scroll position (#14191)
*  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>
2026-04-26 18:14:32 +08:00
Rdmclin2 f0ba92776b feat: support dm strategy (#14201)
* feat: support dm policy

* feat: update channels docs about dm strategy

* feat: add dm reject policy docs and default to open

* feat: add platform reply locale

* feat: discord extract locale

* feat: optimize locale ack messages

* fix: qq platform respond twice

* feat: support dm policy and group policy

* feat: add userID to allowList

* feat: support white list form

* fix: group policy

* fix: bot slash commands passby
2026-04-26 17:09:27 +07:00
Innei d12e050157 🐛 fix(agent-runtime): scope pending-approval check to current assistant turn (#14182)
* 🐛 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
2026-04-26 17:05:47 +08:00
YuTengjing cc48e9ff8e feat: add generation error business hook (#14195) 2026-04-26 16:53:12 +08:00
Innei 939f20e783 🐛 fix(conversation): disable first assistant block markdown streaming (#14193)
* 🐛 fix(conversation): disable first assistant block markdown streaming

* Add assistant group generating selector

* 🐛 fix(conversation): preserve workflow block markdown streaming

*  test(conversation): mock assistant group generating selector
2026-04-26 15:52:18 +08:00
YuTengjing 8f6848fba2 🐛 fix: update DeepSeek V4 Pro discount pricing (#14196) 2026-04-26 13:43:37 +08:00
YuTengjing 8b22e55271 🐛 fix: omit DeepSeek reasoning effort when disabled (#14194) 2026-04-26 13:24:56 +08:00
YuTengjing 196c0a7650 🔨 chore: sync tsgo version (#14181) 2026-04-26 11:31:12 +08:00
Neko ec7e696587 ️ perf(app): check if self iteration feature flag on from front side (#14186) 2026-04-26 06:02:19 +08:00
Arvin Xu 9b48e24ded feat(conversation): queue follow-up sends during running CC turns (#14179)
*  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>
2026-04-26 01:12:58 +08:00
YuTengjing 79d5d2286a 📝 docs: make AGENTS the source of truth (#14180) 2026-04-26 00:25:00 +08:00
Arvin Xu 998c22890d 🐛 fix(context-engine): normalize tool parameters required to [] (#14178)
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>
2026-04-25 23:43:04 +08:00
Neko d5315fe745 feat(agent-signal): added AgentSignalRuntime (#14170) 2026-04-25 23:28:52 +08:00
Innei 5c75b0865f ♻️ refactor(agent): remove working sidebar from desktop chat page (#14174)
Drop AgentWorkingSidebar from the desktop agent route layout.

Made-with: Cursor
2026-04-25 21:57:24 +08:00
Innei 7f6f77ec9d ♻️ refactor(agent): reuse task flag for page agent (#14161) 2026-04-25 21:57:14 +08:00
Arvin Xu 7c0203a9c7 🐛 fix(agent-runtime): tighten isCanUseVision default and add aggregator fallback (#14172)
🐛 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>
2026-04-25 21:22:27 +08:00
Arvin Xu 84fd8da4a3 🐛 fix(tasks): scope task completion to terminal result briefs only (#14168)
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>
2026-04-25 21:00:36 +08:00
Arvin Xu f98a314cf5 💄 style(conversation): per-phase workflow expand defaults for heterogeneous agents (#14171)
 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>
2026-04-25 20:54:41 +08:00
YuTengjing 35c43fb580 🐛 fix: migrate Seedance video models to Dreamina (#14144) 2026-04-25 20:09:37 +08:00
Neko 56bc216c5e feat(agent-signal,app,const,types): added self interation into lab, and feature flag (#14169) 2026-04-25 19:41:01 +08:00
Arvin Xu 66c25cce4b 🐛 fix(heterogeneous-agent): surface Codex terminal errors and trace CLI output (#14166)
* 🐛 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>
2026-04-25 19:16:36 +08:00
Arvin Xu 774e29e400 ♻️ refactor(tasks): default to await-review on topic done, drive completion via accept signals (#14167)
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>
2026-04-25 19:04:58 +08:00
YuTengjing eec89338da feat: add GPT-5.5 model support (#14147) 2026-04-25 19:04:02 +08:00
Arvin Xu 91cb2a8e65 🐛 fix(builtin-tool-memory): flatten searchUserMemory schema for strict tool validation (#14165)
🐛 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>
2026-04-25 18:47:36 +08:00
Arvin Xu 61d27b46a0 😂 refactor(tasks): drop per-agent /agent/:aid/tasks routes again (#14164)
* 🔥 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>
2026-04-25 18:19:39 +08:00
Arvin Xu 01f6858cc1 🔥 feat(heterogeneous-agent): remove lab flag for GA rollout (#14162)
* 🧹 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>
2026-04-25 17:24:24 +08:00
YuTengjing b3e993f7b1 🐛 fix(agent-tracing): annotate agent signal event union (#14163) 2026-04-25 17:23:58 +08:00
Arvin Xu 22e6e1dbcc 🐛 fix(model-runtime): guard tool_use.input against non-object parsed arguments (#14150)
* 🐛 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>
2026-04-25 17:03:57 +08:00
Arvin Xu f7205552e8 ♻️ refactor(devtools): split RenderGallery into routed sub-pages (#14151)
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>
2026-04-25 16:41:45 +08:00
Innei 0077a7286a 🐛 fix: register mobile agent topic route (#14158) 2026-04-25 16:24:59 +08:00
YuTengjing 697ac3bf6e 🔨 chore(model-runtime): support azure openai router runtime (#13823) 2026-04-25 16:08:09 +08:00
Neko fc12fac53b feat(agent-signal,agent-tracing,observability-otel): added o11y metrics, agent-tracing for rendering -S for signals (#14159) 2026-04-25 15:48:17 +08:00
Innei ba59d85ae6 🐛 fix(agent): refine page switcher and empty topic docs (#14155) 2026-04-25 15:36:30 +08:00
Neko a6cb200d5b feat(agent-signal): created new package agent-signal (#14157) 2026-04-25 15:28:40 +08:00
AmAzing- 87d7b41186 docs: update Discord bot authorization image in English and Chinese d… (#14154) 2026-04-25 14:53:06 +08:00
AmAzing- 8e807c6b10 📝 docs: update Discord bot permission requirements for channel(#14143) 2026-04-25 14:05:28 +08:00
Arvin Xu 53c5a014ba 🐛 fix(model-runtime): use safety_identifier for OpenAI Responses API (#14148)
🐛 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>
2026-04-25 13:10:09 +08:00
Arvin Xu ba05c32489 🐛 fix(model-runtime): tolerate null function.name in streaming tool_call deltas (#14139)
* 🐛 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>
2026-04-25 12:17:09 +08:00
Arvin Xu d4a12c0ebb 🐛 fix(tasks): preserve topic title when handoff is missing (#14137)
* 💄 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>
2026-04-25 12:15:58 +08:00
Rdmclin2 7f025b9c5a feat: optimize bot markdown render (#14140)
* chore: optimize strip markdown & preview

* feat: remove strip markdown for wechat

* chore: remove preview script
2026-04-25 01:33:47 +07:00
Innei 35c9e1b224 🧹 chore(remove-docs-superpowers): remove docs/superpowers and ignore path (#14138) 2026-04-25 00:03:42 +08:00
Innei 043d2a81fb feat(agent): add floating chat panel and workspace improvements (#13887)
*  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>
2026-04-24 23:56:25 +08:00
Arvin Xu f39392749a 🐛 fix(model-runtime): preserve cloudflare provider error context (#14136) 2026-04-24 22:41:53 +08:00
Arvin Xu b3dc59f77a feat(tasks): unified Tasks routes, detail polish & CC Agent streaming (#14109)
*  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>
2026-04-24 21:32:50 +08:00
YuTengjing 9b6a60339f 🐛 fix: default DeepSeek V4 reasoning control (#14131) 2026-04-24 20:46:25 +08:00
Innei b55cf6b936 ♻️ refactor(conversation): unify scroll-to-user + spacer hooks (#14132)
* ♻️ 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.
2026-04-24 20:29:18 +08:00
YuTengjing 933cfbf789 🐛 fix: keep artifact script content in card (#14135) 2026-04-24 20:26:42 +08:00
LiJian 0e11d3d9c0 🔨 chore: add the agent runtime tools call hooks (#13874)
* 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>
2026-04-24 19:09:11 +08:00
LiJian 600f10fcea 🐛 fix(lh): fix cron create parameter mapping for cronPattern and content (#14113) 2026-04-24 18:19:17 +08:00
LiJian 421427f3a2 ♻️ refactor: add bot platform setup guide reference (#14121)
*  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.
2026-04-24 16:28:40 +08:00
YuTengjing 5dc7c2592c 🐛 fix: stabilize artifact html scripts (#14120) 2026-04-24 16:24:24 +08:00
Neko a19b6b50e0 🐛 fix(userMemories,app): should collect exact error when processing, normalize more parsing issues (#14123) 2026-04-24 15:41:18 +08:00
YuTengjing fd2112cbcd 👷 build(vitest): pin @lobechat/business-model-runtime to local stub (#14118) 2026-04-24 14:13:56 +08:00
YuTengjing 0b57c9d3da feat(deepseek): add V4 Flash/Pro cards + reasoning_effort slider (#14114) 2026-04-24 13:37:40 +08:00
YuTengjing 1958a59f4e feat: add MiMo-V2.5 and MiMo-V2.5-Pro model cards (#14089) 2026-04-24 11:51:52 +08:00
Arvin Xu f7ed6df35b feat(codex): improve rich tool rendering and add devtools preview (#14100)
*  feat: improve codex rich tool rendering

*  feat: add desktop tool render gallery

* 🐛 fix(codex): address rich render review feedback

* 🐛 fix(devtools): sort render gallery fixture imports
2026-04-24 10:36:27 +08:00
Innei a18569c690 🐛 fix(desktop): auto-focus ChatPanel input on screen capture overlay mount (#14105)
🐛 fix(desktop): auto-focus ChatPanel input on overlay mount
2026-04-24 02:06:00 +08:00
Tsuki 4ff4dead20 💄 style: compact kanban card layout with variant prop (#14102)
* 💄 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>
2026-04-24 01:31:53 +08:00
Tsuki 5a7d46e900 feat(mobile-router): add aiAgentRouter to mobileRouter (#14103)
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>
2026-04-24 00:46:32 +08:00
Arvin Xu 92f34bcc0d feat(runtime-config): add redis-backed feature flag provider (#14098)
*  feat(runtime-config): add redis-backed feature flag provider with env fallback

* 🐛 fix(runtime-config): cache null snapshots in redis provider

* 🐛 fix(ci): sort runtime-config imports

* 🐛 fix(runtime-config): evict expired selector cache entries
2026-04-24 00:18:27 +08:00
Innei 7955a43a9e feat(desktop): gate screen capture on macOS permission and add overlay hint (#14097)
*  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.
2026-04-24 00:06:27 +08:00
Innei fa0ec62d71 🐛 fix(conversation): stop repinning after manual scroll (#14099)
* 🐛 fix(conversation): stop repinning after manual scroll

* 🐛 fix(conversation): avoid stale pin cancellation
2026-04-23 23:45:06 +08:00
Arvin Xu 3b94f86303 🐛 fix(desktop): surface human approval notifications (#14092)
🐛 fix(desktop): notify when human approval is required
2026-04-23 23:29:51 +08:00
Rdmclin2 05b2aca92b 🐛 fix: remote device disabled in bot converation (#14096)
fix: remote device disabled in bot converation
2026-04-23 22:57:42 +08:00
Rdmclin2 e4b15caf74 feat: support bot emoji (#14091)
* feat: support bot emoji

* chore: add local bot error message

* feat: add emoji  replace action

* feat: add emoji reaction

* fix: test case
2026-04-23 19:25:45 +08:00
Arvin Xu 82096dcd89 feat(heterogeneous-agent): add Codex desktop integration (#14067)
*  feat(heterogeneous-agent): integrate Codex desktop MVP

*  feat(heterogeneous-agent): polish Codex profile and install guidance

* 🐛 fix(heterogeneous-agent): stabilize Codex desktop error handling

*  improve codex desktop integration

*  feat(desktop): support custom heterogeneous CLI commands

* 💄 style(profile): refine heterogeneous CLI status card

* 🐛 fix(chat): persist heterogeneous CLI auth errors

* 💄 style(profile): align CLI card radius with container

*  feat(chat): add heterogeneous CLI rate-limit guide

* 🐛 fix(heterogeneous-agent): split Codex multi-turn steps

* 📝 docs(skill): add heterogeneous-agent debugging guide

* ♻️ refactor: split heterogeneous agent status guide and fix i18n fallback

* 🐛 fix(heterogeneous-agent): align Codex step and tool-call boundaries

* 💄 style(skills): use capsule chip in activate inspector

* 🐛 fix(chat): resolve status guide type errors
2026-04-23 19:18:51 +08:00
LiJian 66d096e963 🐛 fix(creds): integrate Klavis authorization status into lobe-creds system (#14090)
*  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>
2026-04-23 17:47:10 +08:00
Innei 50ffa5b100 🐛 fix: prevent Markdown stream replay when vlist remounts streaming items (#14086)
* 🐛 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`.
2026-04-23 17:24:40 +08:00
renovate[bot] 8e20bd182f Update dependency uuid to v14 [SECURITY] (#14083)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 15:51:42 +08:00
AmAzing- 53b4b4d4d3 feat(chat): inline resend when editing last user message (#14080) 2026-04-23 15:47:56 +08:00
Innei decbc4ce7f ♻️ refactor: alias buffer package as buffer.js for cleaner imports (#14081)
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'`.
2026-04-23 15:10:29 +08:00
Innei 4e31a33599 🐛 fix: strip manifest link in Vite dev to silence 404 warning (#14079)
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.
2026-04-23 15:10:00 +08:00
YuTengjing cad10007ef 📝 docs(skills): add sub-issue tree guide to linear skill (#14076) 2026-04-23 11:33:30 +08:00
René Wang 73860a9ffd 📝 docs: add April 20 weekly changelog (#14072) 2026-04-23 10:38:46 +08:00
Hardy 4696968edb 🐛 fix: add env var support for Coding Plan and OpenCode providers (#14064)
* 🐛 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.
2026-04-23 10:31:14 +08:00
Hardy 48760e353a feat: add OpenCode Zen and OpenCode Go providers (#13943)
*  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
2026-04-23 02:13:09 +08:00
Tsuki 70e7e441b2 🔨 chore: premerge Task detail page UI (#13653)
*  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>
2026-04-23 02:10:45 +08:00
Innei 5196203414 ♻️ refactor: replace antd Modal with base-ui Modal in FileEditor (#14054)
♻️ 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
2026-04-23 01:36:11 +08:00
Innei 5c2fe6c579 🐛 fix(onboarding): unify footer visibility behind AGENT_ONBOARDING_ENABLED (#14065)
🐛 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.
2026-04-23 01:17:43 +08:00
Arvin Xu 042987fe34 🐛 fix(agent-runtime): unwrap underlying PG error in formatErrorEventData (#14038)
* 🐛 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.
2026-04-23 00:46:01 +08:00
Innei f00d95f4a6 🐛 fix(desktop): add Linux icon configuration to electron-builder (#14042)
The Linux target was missing the icon field, causing the .deb package
to show no application icon on Ubuntu and other Linux distributions.

Closes #9785
2026-04-23 00:34:20 +08:00
Innei ed6330362c 🐛 fix(conversation): pin user message to viewport top & fold long user messages (#14056)
* 🐛 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.
2026-04-22 23:59:43 +08:00
YuTengjing 17834d41c3 🐛 fix(route-log): record image/video generation triggers (#14048) 2026-04-22 23:48:59 +08:00
Innei 5e9546c537 🐛 fix(page-editor): use remoteServerUrl for copy link on desktop (#14057)
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.
2026-04-22 23:40:25 +08:00
Innei 25e4b3e33b 🐛 fix(build): enable Rolldown strictExecutionOrder for production builds (#14058)
Made-with: Cursor
2026-04-22 23:14:11 +08:00
Innei 82ba3706a7 feat(desktop): screen capture overlay, Quick Chat tray, and upload pipeline improvements (#13818)
* 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>
2026-04-22 22:00:30 +08:00
Rdmclin2 993f3f29ea 🐛 fix: slack webhook error (#14052)
* chore: add slack error docs

* chore: universal merge config and default schema settings

* fix: setting save lost

* chore: remove legacy webhook
2026-04-22 21:19:14 +08:00
Arvin Xu 2a3667493f feat(git-status): one-click pull/push from branch chip (#14041)
*  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>
2026-04-22 20:53:35 +08:00
Innei 9c5831ac54 🐛 fix(vite): exclude .html from code-inspector to fix Vite 8 bundledDev (#14053)
* 🐛 fix(vite): exclude .html from code-inspector to fix bundledDev

* 🔥 chore: remove @code-inspector/vite patch, fixed via exclude config
2026-04-22 20:43:24 +08:00
Innei 31d76ccb90 ⬆️ chore: upgrade Vite to 8.0.0 (#12720)
* ⬆️ 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
2026-04-22 19:59:38 +08:00
Innei 9a03c182da ♻️ refactor(desktop): increase recent working directories from 5 to 20 with scroll container (#14036)
* ♻️ 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)
2026-04-22 17:14:06 +08:00
YuTengjing 9d41c8b71c 🐛 fix(mobile): correct session list skeleton row layout (#14040) 2026-04-22 17:04:51 +08:00
YuTengjing 16f2b97de2 feat: add gpt-image-2 to LobeHub-hosted card (#14039) 2026-04-22 16:57:31 +08:00
Arvin Xu 6d339d6a64 🐛 fix(agent-runtime): sanitize invalid tool_call arguments to unbreak strict providers (#14033)
* 🐛 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>
2026-04-22 16:09:26 +08:00
LiJian 9e4bcf88c6 🐛 fix: add the inbox agentid Sync to resume the /agnet/inbox/message=xxx link (#14035)
* fix: add the inbox agentid Sync

* fix: should have the fallback

* fix: update the test
2026-04-22 15:20:08 +08:00
AmAzing- b8cd21a257 chore: add Twitter to recommended skills (#14037) 2026-04-22 15:08:38 +08:00
YuTengjing b4de72b032 feat(mobile): full settings menu and responsive profile layout (#14019) 2026-04-22 15:08:34 +08:00
Arvin Xu e963c640b9 🎨 style(claude-code): tool inspector polish + unstick Read-on-image spinner (#14034)
* 💄 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>
2026-04-22 14:33:54 +08:00
Arvin Xu 1f61e965a6 🎨 style(claude-code): tool inspectors + heterogeneous-agent follow-ups (#14030)
*  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>
2026-04-22 13:23:37 +08:00
Arvin Xu 3b306a8aed 🐛 fix(agent-runtime): preserve Gemini 3 thoughtSignature in call_tools_batch normalization (#14032)
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>
2026-04-22 13:14:28 +08:00
Arvin Xu 4af6fddd7a 🐛 fix(context-engine): downgrade image_url parts when target model lacks vision (#14029)
* 🐛 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>
2026-04-22 13:07:42 +08:00
YuTengjing e9600407ff 🐛 fix: reduce subagent task status error noise (#14026) 2026-04-22 12:58:30 +08:00
Arvin Xu f3fca500e4 🐛 fix(heterogeneous-agents): stream subagent Thread + fix parallel-tool orphan (#14024)
*  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>
2026-04-21 22:18:30 +08:00
AmAzing- 6ddef95249 chore: fix follow-up chat input state during message queueing (#14020)
* 💄 style(chat-input): improve agent assignment placeholder

*  improve follow-up queue input ux

* 💄 sync runtime placeholder locale keys

* Update SKILL.md

* 💄 style(chat-input): hide send menu while generating

Co-Authored-By: Oz <oz-agent@warp.dev>

* fix: ensure sendMenu is correctly cleared in store when prop becomes undefined and add test coverage

---------

Co-authored-by: Oz <oz-agent@warp.dev>
2026-04-21 18:56:52 +08:00
Arvin Xu b02b727261 feat(heterogeneous-agent): support CC subagent rendering (#14001)
*  feat(heterogeneous-agents): preserve CC subagent lineage in adapter

Restores the CC subagent-lineage adapter work that was held back from
#LOBE-7392 until the thread-router backend changes ship. This PR targets
the LOBE-7392 branch so the adapter diff stays isolated from the
thread/UI foundation — GitHub will auto-retarget to canary once
LOBE-7392 merges.

Original scope (unchanged from the held-back commits):
- ToolCallPayload.parentToolCallId carries parent tool_use id downstream
  so consumers can group subagent inner tools under their spawning
  parent.
- claudeCode.ts routes raw.parent_tool_use_id events through
  handleSubagentAssistant so the main-agent step tracker is not advanced
  on subagent message.id changes, usage is not double-counted, and
  subagent text / reasoning are dropped (their final answer flows back
  via the outer tool_result).
- emitToolChunk helper shared by main-agent and subagent paths so new
  suppress-rules live in one place.
- 6 subagent-lineage tests: lineage propagation, no newStep on
  subagent message.id change, no turn_metadata emission, text/reasoning
  drop, main-agent step boundary resumes after subagent, subagent
  tool_result passthrough.

Refs LOBE-7319, LOBE-7260

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(workflow-collapse): move expand toggle to action slot

Pass the fullscreen toggle as AccordionItem action so the built-in
chevron indicator (same as TopicList) sits inline with the title on
the left, with Maximize2/Minimize2 on the right.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(heterogeneous-agents): route CC Task tool_use to subagent Thread

When a main-agent tool_use spawns a subagent, the executor now sync-
allocates a threadId and creates a Thread, routing subsequent subagent
inner tool_uses (tagged with `parentToolCallId` by the adapter) into
that thread instead of the main assistant's tools[].

The "this tool_use spawns a subagent" decision lives entirely in the
adapter layer via a new `ToolCallPayload.subagentSpawn` descriptor
(`description`, `subagentType`). The CC adapter populates it on every
`Task` tool_use; when Codex (or any other CLI) grows a subtask concept,
its adapter populates the same field and the executor needs zero
changes. The executor never checks `identifier === 'claude-code'` or
`apiName === 'Task'` — it just reacts to the presence of
`subagentSpawn`.

- `ToolCallPayload.subagentSpawn?: { description?, subagentType? }`
  in `packages/heterogeneous-agents/src/types.ts` — adapter-agnostic
  spawn signal, paired with the existing `parentToolCallId` (which
  marks tool_uses BELONGING to a subagent). Together they cover both
  directions of the lineage.
- `claudeCode.ts` stamps `subagentSpawn` on main-agent `Task` tool_uses
  using the already-parsed `block.input` — no redundant JSON.parse.
- `ThreadService.createThread` helper wraps the sync-id TRPC mutation
  shipped in #14000. `generateThreadId()` mirrors the server's
  `idGenerator('threads', 16)` shape (`thd_<16 chars>`) so caller-
  provided ids match the schema pattern.
- `persistNewToolCalls` splits fresh tools into main/subagent groups:
  Phase 1 (pre-register assistant.tools[]) and Phase 3 (backfill
  result_msg_id) run for main tools only. A new Phase 1b creates the
  Thread per `subagentSpawn` — guarded on `context.topicId` (required
  for Thread creation; missing falls back to normal tool rendering).
  Phase 2 writes tool messages for both groups, attaching `threadId`
  to subagent writes. Orphaned subagent events (parent spawn never
  registered) warn + drop instead of leaking into the main timeline.
- `taskThreadMap` lives at executor scope (not on ToolPersistenceState
  which resets per step) so pathological orderings that straddle the
  main-agent step boundary can't lose the parent→thread mapping.

7 new tests: 2 adapter-level (subagentSpawn stamped on Task,
NOT stamped on Read) + 5 executor-level (Thread creation, threadId
propagation onto subagent tool messages, main assistant.tools[]
isolation, orphan drop + warn, topicId-missing fallback).

Refs LOBE-7319, LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(types): persist subagent lineage fields on ChatToolPayload schema

Add `parentToolCallId` and `subagentSpawn` as first-class optional
fields on `ChatToolPayload` + `ChatToolPayloadSchema`, so the adapter-
emitted lineage metadata survives the TRPC `update-message` gate
instead of being silently stripped by zod's default strip behavior.

Reviewer-flagged bug: `UpdateMessageParamsSchema.tools` runs each
payload through `ChatToolPayloadSchema`, which previously only
whitelisted `apiName / arguments / id / identifier / intervention /
result_msg_id / thoughtSignature / type`. Any adapter-level
extension (subagent spawn marker, parent-child pointer) was dropped
before it ever reached the `messages.tools` JSONB column, so lineage
only lived in transient stream events and vanished on the first
`tool_end → fetchAndReplaceMessages`. Downstream consumers that
wanted to key off `tool.subagentSpawn` to render a TaskBlock, or
follow `tool.parentToolCallId` to reconstruct the spawning parent,
had nothing to work with.

- `SubagentSpawnInfo` + `SubagentSpawnInfoSchema` defined in
  `packages/types/src/message/common/tools.ts` as the canonical
  shape. Structurally identical to the same-named type in
  `@lobechat/heterogeneous-agents` (which stays self-contained by
  design) — TypeScript structural typing handles the bridge.
- Both new fields are optional on the interface and the zod schema,
  so existing callers continue to parse unchanged.
- Jsonb column accepts any shape, so no DB migration — the only
  missing piece was the schema gate.

3 new regression tests next to the executor's subagent-thread-routing
suite, asserting `ChatToolPayloadSchema.parse()` preserves both
fields and the same fields survive through `UpdateMessageParamsSchema`
(the actual TRPC gate that was stripping them before).

Refs LOBE-7319

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Revert " feat(types): persist subagent lineage fields on ChatToolPayload schema"

This reverts commit 042e48c7338aa8b502bcd6298a2871c758f348af.

* ♻️ refactor(heterogeneous-agents): lift subagent context to event-peer fields

`ToolCallPayload` is "one tool call" — it shouldn't carry stream-level
lineage (parent spawn id, subagent turn id). That info describes the
containing event/chunk and should live as a peer field on the event
`data`, not nested inside each payload.

Event model changes:
- New `SubagentEventContext` + `SubagentSpawnMetadata` types. Events
  originating from a subagent stream (CC Task, future Codex subtask,
  etc.) carry `data.subagent` as a peer field next to `toolsCalling`
  / `toolCallId`. Covers `stream_chunk` (tools_calling), `tool_start`,
  `tool_end`, and `tool_result`.
- `SubagentEventContext.spawnMetadata` appears ONLY on the first event
  for each new parent — lets the executor lazy-create the subagent
  Thread on first sight without needing to know CC-specific argument
  shapes or to re-parse `tool_use.input`. Subsequent events for the
  same parent carry just the lineage ids.
- `ToolCallPayload` is back to its minimal form (`apiName / arguments
  / id / identifier / type`). No `parentToolCallId`, no `subagentSpawn`
  — those were the wrong abstraction level; removing them also sidesteps
  the `ChatToolPayloadSchema` strip-on-persist issue (the fields never
  need to survive DB roundtrip because Thread container persistence
  expresses the lineage).

CC adapter (`claudeCode.ts`):
- `handleSubagentAssistant` emits tools through a shared `emitToolChunk`
  that stamps the `subagent` peer field on the chunk + each tool_start.
  The FIRST subagent chunk for a new parent gets `spawnMetadata` pulled
  from a new adapter-internal `taskArgsById` cache — description /
  prompt / subagentType — announced exactly once via `announcedSpawns`.
- `handleUser` stamps `subagent.parentToolCallId` on `tool_result` +
  `tool_end` when the user event carries `parent_tool_use_id`
  (CC's shape for subagent inner tool_results).
- Main-agent tool_use handling no longer stamps lineage on payloads.

Adapter tests updated — 4 rewrites in the subagent suite:
- assert chunk-level peer fields (not payload-nested lineage)
- assert `spawnMetadata` on first subagent event, absent on subsequent
- assert main-agent tool_uses don't get `subagent` context
- assert subagent `tool_result` + `tool_end` carry the peer

59 adapter tests pass (52 existing + 7 covering the new peer contract).

Refs LOBE-7319, LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(heterogeneous-agents): persist subagent runs as Thread containers

Subagents now materialize as a nested conversation inside a Thread,
shaped identically to the main topic:

    Thread
    ├─ user          (content = Task prompt, threadId=thread.id)
    ├─ assistant#1   (tools[] = subagent turn 1 tool_uses, threadId)
    ├─ tool          (parentId=assistant#1, threadId)
    ├─ assistant#2   (tools[] = subagent turn 2 tool_uses, threadId)
    └─ tool          (parentId=assistant#2, threadId)

Same schema as a main topic, just rooted at a Thread instead of a
Topic. No new persistence shape, no new renderer — the existing
`query({ threadId })` read path reconstructs the subagent's full
conversation when the UI expands the TaskBlock.

Executor changes:
- `ToolPersistenceState` shrinks to `{ payloads, persistedIds }` — the
  `tool_use.id → tool message DB id` map moves to executor scope as
  one global `toolMsgIdByCallId` shared across main + every subagent
  run. `tool_result` lookups don't care which scope created the row.
- `persistNewToolCalls` → renamed `persistToolBatch` and made scope-
  agnostic (takes an optional `threadId` + the global id map). Runs
  the same 3-phase flow (pre-register → create → backfill) whether
  target is main assistant or in-thread subagent assistant.
- New `persistSubagentToolChunk` handles the subagent path: reads the
  adapter's `SubagentEventContext` peer field off the chunk, lazy-
  creates the Thread + user message on the FIRST chunk for each
  parent (using `spawnMetadata`), opens a new in-thread assistant on
  `subagentMessageId` change (same shape as main-agent step
  boundary), then delegates to `persistToolBatch`.
- `SubagentRunState` tracks per-parent Thread id, current in-thread
  assistant, `currentSubagentMessageId`, chain parent, and its own
  `ToolPersistenceState`. Lives at executor scope so subagent events
  straddling a main-agent step boundary keep their mapping.
- Step-boundary parent lookup reads from `toolState.payloads` (not
  the global id map) so main-agent chain doesn't accidentally pick
  up a subagent tool's msg id as the step parent.
- Executor has NO CC-specific knowledge — it never checks
  `identifier`, `apiName`, or parses `tool_use.arguments`. All CC
  quirks live in the adapter; new CLIs (Codex subtask, ...) plug in
  by emitting the same `SubagentEventContext` peer.

Test rewrite — 6 tests under "CC subagent thread-container":
- Task tool_use alone does NOT create a Thread (lazy)
- First subagent event creates Thread + `role:'user'` seeded with
  the Task prompt + first in-thread `role:'assistant'`
- Subagent inner tools persist as `role:'tool'` messages with
  threadId set and parentId chained to the in-thread assistant
- `subagentMessageId` change opens a new in-thread assistant
- Main `assistant.tools[]` carries Task only; subagent inner tools
  appear on the in-thread assistant's `tools[]`
- Missing topicId gracefully skips Thread creation

25 executor tests pass (19 existing + 6 rewritten for new shape).

Refs LOBE-7319, LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(heterogeneous-agents): subagent prompt + closing summary in Thread view

Electron E2E surfaced two gaps in the Thread-container model shipped in
the previous commit:

1. **Subagent user-message content empty.** Real CC emits `Agent` as
   the spawn-tool name for general-purpose subagents (not only `Task`
   as the spec documents). My earlier `taskArgsById` cache keyed off
   `ClaudeCodeApiName.Task` only, so `spawnMetadata.prompt` was
   undefined when the user watched the actual app — the Thread's
   `role:'user'` message landed with empty content and the thread
   view looked like a tool call floating alone.

2. **No closing summary in the Thread.** The adapter dropped subagent
   text/reasoning per an earlier comment claiming the subagent's
   final answer arrives via the outer tool_result. That's true for
   the MAIN timeline (the outer spawn tool's result content = the
   subagent's summary), but the THREAD view is a standalone
   conversation — dropping the subagent's final text left it ending
   on a bare tool call with no assistant conclusion.

Adapter changes (`claudeCode.ts`):
- Rename `taskArgsById` → `mainToolInputsById` and cache EVERY
  main-agent tool_use input (not just `Task`). `emitToolChunk` looks
  up the parent's input by `parent_tool_use_id` on the first subagent
  event and extracts `description` / `prompt` / `subagent_type`
  defensively — any CC spawn-tool variant that shares this input
  shape (`Task`, `Agent`, future ones) gets spawn metadata for free.
- `handleSubagentAssistant` stops filtering `tool_use` only. Text
  and `thinking` blocks now emit as `stream_chunk` events with the
  `subagent` peer field attached — routed to the in-thread assistant,
  NOT the main assistant's accumulators.

Executor changes (`heterogeneousAgentExecutor.ts`):
- `SubagentRunState` gains `accumulatedContent` + `accumulatedReasoning`,
  mirroring main-agent content tracking.
- Extract `ensureSubagentRun` helper so text chunks and tool chunks
  share the Thread / user / assistant lifecycle logic. On turn
  boundary (`subagentMessageId` change), flush the prior turn's
  accumulated content before creating the next in-thread assistant —
  covers text-only turns that never hit `persistToolBatch`.
- New `persistSubagentTextChunk` accumulates text/reasoning onto the
  run; `persistToolBatch` writes content alongside tools[] so DB
  sees both in one update (same pattern as main agent).
- New `finalizeSubagentRun` flushes pending content when the main-
  agent receives the spawn tool's `tool_result` — ensures the
  closing summary lands before `fetchAndReplaceMessages` refreshes
  from stale DB state.
- `onComplete` iterates `subagentRuns.keys()` and flushes any
  un-finalized runs, covering the CLI-crashed-mid-subagent edge case.

Tests:
- Adapter: replaced the "drops subagent text" test with two tests
  asserting text/reasoning ARE emitted with correct `subagent` peer
  context. New test covers the `Agent` spawn-tool variant.
- Executor: 4 new tests cover the Thread user message content
  population, subagent text accumulation into the in-thread assistant,
  non-leakage into main assistant content, and tool_result-triggered
  finalization. Total 29 executor tests pass.

E2E verified via Electron + CDP: fresh CC session → `Agent`-based
subagent → Thread created with `title="Run pwd command"`,
`metadata.subagentType="general-purpose"`, `role:'user'` seeded with
the Task prompt, Bash tool_use + result inside the thread.

Refs LOBE-7319, LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(heterogeneous-agents): refresh thread list when subagent Thread is lazy-created

Earlier Electron E2E repro: a subagent Thread born mid-stream landed
in DB correctly, but the topic sidebar only picked it up after the
user manually navigated topics / called `refreshThreads()` — the
SWR cache for the thread list (`SWR_USE_FETCH_THREADS`) wasn't
invalidated, so the new Thread stayed invisible until the next
cold fetch.

- `ensureSubagentRun` now accepts an optional `onThreadCreated`
  callback fired once per lazy Thread create. Kept as a callback
  (not a direct `store.refreshThreads` call) so the executor
  persistence logic stays decoupled from the Zustand store shape.
- `persistSubagentToolChunk` + `persistSubagentTextChunk` thread
  the callback through to `ensureSubagentRun`.
- Executor defines `onSubagentThreadCreated` once at run scope and
  passes it into all three subagent persist call sites. Calls
  `get().refreshThreads()` fire-and-forget — it's a no-op when the
  user has navigated away from the topic, so no need to block
  persist on cache refresh.

Two regression tests:
- Subagent-spawning run → `refreshThreads` called exactly once
- Non-subagent run (plain tool only) → `refreshThreads` NOT called

Refs LOBE-7319, LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(builtin-tool-claude-code): specialize Agent subagent Inspector + Render

CC's subagent-spawn tool arrives as `tool_use.name: 'Agent'`, not `Task` —
rename the apiName so the Inspector/Render registry actually matches the
stream. Inspector switches icon/label by `subagent_type` (Explore / Plan /
general-purpose / statusline-setup), with `description` surfaced in a chip;
new Render shows `prompt` and tool_result as labelled Markdown blocks that
can't fit in the folded header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(workflow-collapse): unify expand toggle with ActionIcon

Replace the hand-rolled motion span + role="button" / keyboard-handler
expand toggle with a single @lobehub/ui ActionIcon — fewer a11y edge
cases to maintain and the icon/title/blockSize layout matches other
toolbar buttons in the group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(builtin-tool-claude-code): inline-pad Edit diff container

Give the Edit render a small inline padding so the CodeDiff lines up
with the rest of the tool renders; zero-width flush-left was awkward
against the surrounding labelled blocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(heterogeneous-agents): interpolate agent name in running indicator

ContentLoading now renders "{name} is running" / "{name} 运行中" for
heterogeneous agent execution — previously it collapsed to the generic
"External agent running" so a user watching a long CC run couldn't tell
which external CLI was working (mattered once Codex landed as a sibling
adapter).

- Share `HETEROGENEOUS_TYPE_LABELS` (claude-code / codex) out of the
  heterogeneous-agents package so all consumers read one map; home
  Sidebar AgentItem switches to it and drops its inline copy.
- `conversationLifecycle.startOperation` passes
  `metadata.heterogeneousType` on the heterogeneous-exec operation so
  ContentLoading can resolve the label from the running op without
  re-deriving the adapter type from session state.
- New `operation.heterogeneousAgentFallback` key covers the (rare) case
  where the metadata is absent — keeps the dot loader labelled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(claude-code): CC subagent Thread rendering pipeline

Closes the viewing loop for CC subagent runs: the main-topic Agent tool
row now links into the spawned Thread, the Thread's Portal view renders
with provenance + read-only affordances, and the sidebar surfaces which
entries are subagent-produced.

UX:
- Agent render gains a trailing "View / Collapse full subagent
  conversation" toggle. It looks up the Thread by
  `metadata.sourceToolCallId === toolCallId` and calls
  openThreadInPortal / closeThreadPortal — hidden until the executor
  lazy-creates the Thread on the first subagent event, so it never
  renders as a no-op.
- Portal Thread Header shows a `[icon] subagentType` Tag next to the
  title ("Explore" / "General purpose" / ...). Inspector's folded row
  already exposes the same detail, so the icon + label stays
  consistent across the two surfaces.
- Portal Thread Chat flips into read-only mode when
  `metadata.sourceToolCallId` is set: ChatInput is hidden (the
  external CLI owns the session — new turns have nowhere to go),
  `disableEditing` propagates to every message (no double-click to
  edit, no user action bar), and `useThreadActionsBarConfig` wipes
  `bar` + `menu` across assistant / assistantGroup / user roles.
- Sidebar ThreadItem on both /agent and /group routes renders a plain
  "Subagent" badge next to the title when
  `metadata.subagentType` is present. The type detail deliberately
  lives on the Thread Header, not here — sidebar space is tight.

Shared resolver:
- `CC_SUBAGENT_TYPES` + `resolveCCSubagentType` move out of the
  Inspector into `packages/builtin-tool-claude-code/src/client/
  subagentTypes.ts` and re-export from the `/client` entry. Inspector
  + Portal Thread Header both consume it, so the icon/label stay in
  sync. Kept UI-level (LucideIcon | FC) rather than pushed into
  heterogeneous-agents, which is a pure-data package.
- Root package.json adds a direct dep on
  `@lobechat/builtin-tool-claude-code` so Portal Thread Header can
  import from `/client` (previously only transitive via builtin-tools).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(workflow-collapse): mock @lobehub/ui ActionIcon + AccordionItem action slot

After the expand-toggle refactor to ActionIcon + the `action` prop on
AccordionItem, the test's module mocks were missing both: ActionIcon
wasn't exported from the @lobehub/ui mock, and AccordionItem dropped
`action` on the floor so the toggle never made it into the rendered
DOM. Restore both — ActionIcon renders as a real \`button\` with
aria-label so \`getByRole('button', { name })\` can still target it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:48:16 +08:00
Arvin Xu c0db58e622 feat(topic): add completed status with dropdown action and filter (#14005)
*  feat(topic): add completed status with dropdown action and filter

- Surface ChatTopicStatus (active/completed/archived) on topic list items and pass to dropdown menu
- Add markTopicCompleted / unmarkTopicCompleted store actions wired into the topic item dropdown
- Show CheckCircle2 icon on completed topics in the sidebar list
- Add topicIncludeCompleted user preference (default false) and an "Include Completed" toggle in the topic filter menu (agent + group routes)
- Wire excludeStatuses and triggers filters through TopicModel, TRPC router, service, and store SWR keys so completed topics are excluded by default

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🌐 i18n(topic): add zh-CN/en-US for completed status keys

Translate actions.markCompleted / actions.unmarkCompleted and filter.filter / filter.showCompleted for dev preview. CI's pnpm i18n will fill in remaining locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(topic): scope completed exclusion to routes with the toggle

Move the topicIncludeCompleted preference read out of the chat-store useFetchTopics action and into the (main) agent/group sidebars where the "Include Completed" filter actually lives. Popup and mobile topic views call useFetchTopics without excludeStatuses, so completed topics remain reachable on surfaces that don't expose the toggle (e.g. the popup window for a deep-linked completed topic, the mobile TopicModal).

Also switch ChatTopicStatus imports in the topic item / dropdown files to @lobechat/types to match the rest of the topic-feature imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(topic-model): cover excludeStatuses + triggers filters

Add cases to the TopicModel.query suite for the new params introduced alongside the topic.status column:
- triggers (positive trigger filter) on the container branch
- excludeStatuses on the container, agent, and groupId branches (verifies null status rows are still returned)
- status / completedAt are populated on returned items

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(topic): move "Mark Completed" to top of agent topic dropdown

Promote the completed-status toggle to the first menu item, with a divider before favorite, so the most-used status action sits at the top of the dropdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:37:09 +08:00
YuTengjing 61224fe76c 🐛 fix(auth): return 401 for expired OIDC JWT instead of 500 (#14014) 2026-04-21 16:43:57 +08:00
Innei 8119789849 🐛 fix(model-bank): add repository metadata for provenance (#14018) 2026-04-21 15:59:55 +08:00
Innei 1ffd01a9eb 🐛 fix(model-bank): publish initial npm package publicly (#14017) 2026-04-21 15:50:28 +08:00
Innei 9d3696ceef 👷 build(model-bank): automate npm release (#14015) 2026-04-21 15:38:04 +08:00
LiJian 595193ce62 🐛 fix: clarify lobe-gtd and lobe-cron tool descriptions to prevent routing confusion (#14013)
When users say "daily task" or "routine", the model confused lobe-gtd (one-time todos) with lobe-cron (recurring automation), often falling back to user-memory or GTD instead of cron.

Fixes LOBE-7486

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 15:30:45 +08:00
LiJian 665b482390 🐛 fix: inject timezone and cron jobs list into cron tool system prompt (#14012)
* 🐛 fix: inject timezone and cron jobs list into cron tool system prompt

Add {{timezone}} to cron systemRole session_context so the model knows
the user's local timezone when creating scheduled tasks. Wire up the
{{CRON_JOBS_LIST}} placeholder that was already referenced in the
systemRole but never populated — now fetches the agent's existing cron
jobs via tRPC and injects them, following the same pattern as CREDS_LIST.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: limit cron jobs context to 4 items to save context window

Only inject a preview of up to 4 cron jobs into the system prompt.
When there are more, append a hint directing the model to call
listCronJobs API for the full list. This avoids bloating the context
window for agents with many scheduled tasks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 15:25:55 +08:00
LiJian ca47d972a4 🐛 fix: fallback to skill activation when activateTools cannot find identifier (#14010)
* 🐛 fix: fallback to skill activation when activateTools cannot find identifier

When an LLM calls activateTools with a skill identifier (e.g. "lobehub"),
the tool lookup fails with "Not found" because skills and tools are separate
registries. Now activateTools falls back to activateSkill for identifiers
not found as tools, so skills can be activated regardless of which API the
LLM chooses to call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: fallback to skill activation when activateTools cannot find identifier

When an LLM calls activateTools with a skill identifier (e.g. "lobehub"),
the tool lookup fails because skills and tools are separate registries.

Two changes:
1. ActivatorExecutionRuntime.activateTools() now falls back to activateSkill
   for identifiers not found as tools
2. selectActivatedSkillsFromMessages() now also extracts skills from
   activateTools messages (pluginState.activatedSkills[]), so downstream
   stepContext and execScript zip resolution work correctly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 14:04:58 +08:00
YuTengjing c5db823a69 💄 style: add Kimi K2.6 to LobeHub-hosted card (#14006) 2026-04-21 11:40:15 +08:00
Arvin Xu 518358b95e 💄 style(todo-progress): vertically center collapsed header row (#13996)
Clear residual list-container margin/border when collapsed and slightly
increase bottom padding so the header sits on the bar's visual center.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:02:37 +08:00
sxjeru a15d962ae8 💄 style: add new Kimi K2.6 model (#14004)
*  feat(models): update AI models with new capabilities and pricing adjustments

*  feat(aiModels): add new AI models Kimi K2.6 and GLM-5.1 to ollamaCloud; enhance siliconCloud with Qwen3.6 35B A3B and update pricing and settings
2026-04-21 10:02:26 +08:00
Arvin Xu 569dcc8765 💄 style(thread): sync id allocation + ChatMiniMap polish (#14000)
*  feat(heterogeneous-agents): preserve CC subagent lineage in adapter

Claude Code tags subagent events (Agent / Task tool spawns) with
parent_tool_use_id pointing back at the outer tool_use. The adapter
used to flatten these, breaking the main-agent step tracker — each
subagent turn introduces a NEW message.id, which the adapter read as
"new main-agent step" and forced stream_end + stream_start(newStep),
producing orphan assistant bubbles and double-counted usage.

- ToolCallPayload.parentToolCallId carries the pointer to downstream
  consumers so they can group subagent inner tools under their parent.
- claudeCode.ts reads raw.parent_tool_use_id and:
  * skips main-agent step boundary on subagent message.id changes
  * skips model tracking for subagent events (the result event has
    the authoritative usage, would double-count otherwise)
  * drops subagent text / reasoning in this adapter pass — the
    subagent's final answer is delivered via the outer tool_result;
    verified against a real CC trace where 76 subagent assistant
    events carried only tool_use, zero text / thinking
  * stamps parentToolCallId onto subagent tool_use payloads
- 6 new unit tests cover lineage propagation, no newStep for subagent
  message.id changes, no turn_metadata emission, text/reasoning drop,
  main-agent resuming step boundary, and subagent tool_result
  passthrough.

Refs LOBE-7319, LOBE-7260

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(types): foundation types for CC Task block (LOBE-7392)

Sets up the data shape for rendering CC subagent spawns as inline
`task` blocks inside the parent assistantGroup, replacing the
role:'task' message intermediary that was previously proposed in
PR #13928. Pure data layer — no DB schema migration, no new
columns.

- TaskBlock + AssistantContentBlock.tasks?: derived view that the
  MessageTransformer will populate by joining Threads onto the
  parent message's tool_use entries (follow-up commit). Carries
  threadId, subagentType, description, status — enough for the
  folded inline header without re-fetching the thread on every
  render pass.
- ThreadMetadata gains sourceToolCallId, subagentType, description.
  sourceToolCallId disambiguates parallel subagents that share a
  sourceMessageId (one assistant turn can spawn multiple Task
  tool_uses in one batch).
- CreateThreadParams.id + zod schema field + thread router
  passthrough lets clients allocate the threadId synchronously
  before the create mutation resolves. The CC adapter emits
  Task tool_use synchronously while the create call is async, so
  having the id up-front lets us persist subagent inner messages
  with the right threadId without a queue or blocking the stream.
- ClaudeCodeApiName.Task + TaskArgs match the CC tool_use shape
  (description, prompt, subagent_type) so executor / renderer can
  type the input safely.

Refs LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract subagent assistant handler + drop ThreadMetadata.description

Two review-feedback cleanups on the LOBE-7392 foundation:

1. **Adapter — early-return + shared helper.** The main-agent path no
   longer carries `if (!isSubagentEvent)` guards; subagent events short-
   circuit into a dedicated `handleSubagentAssistant` that only extracts
   `tool_use` blocks, and both paths share a new `emitToolChunk` helper
   for the `tools_calling` + `tool_start` emission. Adding a new
   subagent suppress-rule (no model / no text / no step) now lives in
   one method instead of sprinkling guards across the main handler.

2. **ThreadMetadata — drop `description`, use `Thread.title`.** Thread
   already has a `title` column; storing the CC Task `description`
   input there is the canonical spot and removes the redundant metadata
   field. `TaskBlock.description` is collapsed into `TaskBlock.title`
   (single source), and the MessageTransformer will populate it from
   `thread.title` at read time. Also adds `status?: ThreadStatus` on
   `TaskBlock` so the renderer gets the processing / completed / failed
   state without a separate lookup.

Behavior unchanged — all 56 adapter tests still pass.

Refs LOBE-7392, LOBE-7319

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(thread-router): translate id-collision into CONFLICT error

ThreadModel.create uses onConflictDoNothing() and returns undefined
when a caller-provided id collides with an existing row. With the
new client-side id passthrough (introduced in 16d73261f9 to let the
CC subagent executor allocate threadId synchronously), the original
router would silently insert a follow-up message with
threadId: undefined and return { threadId: undefined } — a data-
integrity regression flagged in PR review.

Translates the model's undefined return into TRPCError(CONFLICT) at
the router boundary so callers see an explicit error and can
regenerate their id and retry. The model layer is untouched —
onConflictDoNothing remains the right primitive for server-generated
ids where collisions are unreachable; the new validation only
applies when the router is the entry point.

- ensureThreadCreated helper extracted; both createThread and
  createThreadWithMessage routes funnel through it
- New thread model tests document the conflict behavior and
  caller-provided id passthrough that the router relies on (16/16
  pass)

Refs LOBE-7392

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 feat(chat-minimap): user-message peek with in-place hover preview

- Filter ticks to user messages; fall back to last user when viewport is on assistant reply
- Replace per-tick popovers with one in-place panel that crossfades from rail center
- Drop arrow nav buttons (hover panel makes them redundant)
- Smooth sqrt width curve (5–16px) so short messages cluster naturally

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(claude-code-todo): chip-style detail in inspector, plain header in render

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  revert(heterogeneous-agents): pull CC adapter subagent-lineage changes

The CC subagent-lineage adapter work (parent_tool_use_id routing,
parentToolCallId on ToolCallPayload, dedicated handleSubagentAssistant /
emitToolChunk helpers, 6 subagent tests) would ship before the thread
backend changes in this PR are deployed — online flows would see the new
payload field with no server to receive it.

Holding this PR to thread-router + foundation types only. The adapter
work is preserved on feat/lobe-7392-cc-adapter-followup and will ship
as a separate PR after this one is deployed.

Refs LOBE-7392, LOBE-7319

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 01:27:01 +08:00
Arvin Xu b4aa51baaa 🐛 fix: hetero-agent ToolSearch content + bot IM reply + titlebar polish (#13998)
* 💄 style(electron): use colorBgElevated for active title-bar tab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 fix(bot): show operation id instead of raw error in IM failure reply

Replace the error message content in bot-facing failure replies with the
operation id so end users don't see raw runtime errors; errors are still
logged server-side for debugging and correlation via operation id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): extract tool_name from ToolSearch tool_reference blocks

CC CLI returns ToolSearch results as `tool_reference` content blocks with
only a `tool_name` field — no `text`/`content` — so the generic array
mapper collapsed every entry to '' and persisted empty content, keeping
the UI tool StatusIndicator stuck on the spinner (LOBE-7369).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:11:34 +08:00
Arvin Xu 16df8350fe 🐛 fix(user-panel): remove consecutive dividers in user panel menu (#13990)
When businessMenuItems (from cloud deployment) returns items that
include a trailing divider, and getDesktopApp prepends its own divider,
two dividers appear back-to-back between Credits and Get Desktop App.

Add a post-filter on mainItems that strips any consecutive divider,
regardless of which module injected them.
2026-04-20 22:29:24 +08:00
Innei a59a9c4943 feat(onboarding): structured hunk ops for updateDocument (#13989)
*  feat(onboarding): structured hunk ops for updateDocument

Extend `updateDocument` (and the underlying `@lobechat/markdown-patch`) with
explicit hunk modes so agents can unambiguously express deletes and inserts
instead of encoding them as clever search/replace pairs.

Modes: `replace` (default, backward-compatible), `delete`, `deleteLines`,
`insertAt`, `replaceLines`. Line-based modes use 1-based inclusive ranges
and are applied after content-based hunks, sorted by anchor line descending
so earlier lines stay stable. New error codes: `LINE_OUT_OF_RANGE`,
`INVALID_LINE_RANGE`, `LINE_OVERLAP`.

Onboarding document injection now prefixes each line with its 1-based number
(cat -n style) so the agent can cite line numbers when issuing line-based
hunks. Tool description, system role, and per-phase action hints updated to
teach the new shape.

* 🐛 fix(onboarding): align patchOnboardingDocument zod schema with structured hunks

The tRPC input schema still accepted only the legacy `{search, replace}` shape,
so agent calls using the new `insertAt`/`delete`/`deleteLines`/`replaceLines`
hunk modes were rejected before reaching `applyMarkdownPatch`. Switch to a
z.union matching MarkdownPatchHunk.

* 🐛 fix(markdown-patch): validate line ranges before overlap detection

Previously the overlap loop ran before per-hunk range validation, so an
invalid range (e.g. startLine=0 or endLine<startLine) combined with another
line hunk would be misreported as LINE_OVERLAP instead of the real
LINE_OUT_OF_RANGE / INVALID_LINE_RANGE. Validate each line hunk against the
baseline line count first, then run overlap detection on valid ranges only.
2026-04-20 21:17:28 +08:00
Innei a939962fa1 feat(env): add Kimi Coding Plan API environment variables (#13997)
*  feat(env): add Kimi Coding Plan API environment variables

Made-with: Cursor

* 📝 docs(env): document Kimi Coding Plan env vars in .env.example
2026-04-20 21:06:40 +08:00
Arvin Xu bb59b7391e 🚀 release: sync main branch to canary (#13995)
Automatic sync from main to canary. Merge conflicts detected.

**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260420-24659236264
git merge origin/main
# Resolve conflicts
git add -A && git commit
git push
```

> Do NOT merge canary into a main-based branch — always merge main INTO
the canary-based branch to keep a clean commit graph.
2026-04-20 20:03:28 +08:00
Arvin Xu 038070285a resolve merge conflicts 2026-04-20 17:41:43 +08:00
lobehubbot 57e3940bc6 🔖 chore(release): release version v2.1.52 [skip ci] 2026-04-20 09:36:46 +00:00
lobehubbot a0303b7c18 chore: merge main into canary (has conflicts to resolve) 2026-04-20 09:34:54 +00:00
Arvin Xu 3bcd581e7c 👷 build(database): add topic status and tasks automation mode (#13994) 2026-04-20 17:34:13 +08:00
Tsuki bacf422890 🐛 fix: remove desktop tracker legacy imports (#13993) 2026-04-20 15:39:12 +08:00
YuTengjing eb99190f9f feat(chat-input): gate prompt optimize by image output capability (#13992) 2026-04-20 15:04:12 +08:00
LiJian 18042b7d31 🐛 fix: remove systemRole truncation in getAgentDetail (#13988)
The 200-char truncation is no longer needed as the caller
already handles length limits.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 14:26:16 +08:00
Tsuki 5dd7cd7408 feat: add x ads tracking entry points (#13986)
*  feat: add x ads tracking entry points

* 🔨 chore: bump analytics to v1.6.2

* 🐛 fix: add auth analytics provider entry
2026-04-20 14:12:14 +08:00
Arvin Xu ed64e2b8af feat(electron): add Cmd+W/Cmd+T tab shortcuts with misc desktop polish (#13983)
* 💄 style(topic): darken project group folder label in sidebar

Previous `type='secondary'` on the group title was too faint against the
sidebar background; promote the text to default color for better
legibility and keep the folder icon at tertiary so it stays subtle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(topic): use colorTextSecondary for project group title

Text's `type='secondary'` resolves to a lighter token than
`colorTextSecondary`; apply `colorTextSecondary` directly so the title
lands at the intended shade (darker than before, lighter than default).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(electron): show blue unread dot on tab when agent has unread badge

Mirror the sidebar agent unread badge on the corresponding browser-like tab as a subtle blue dot, so unread completions are visible even when the sidebar is out of view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(electron): forward proxy env vars to spawned agent CLI

The main-process undici dispatcher set by ProxyDispatcherManager only
covers in-process requests — child processes like claude-code CLI never
saw the user's proxy config. Extract a shared `buildProxyEnv` so any CLI
spawn can merge HTTP(S)_PROXY / ALL_PROXY / NO_PROXY into its env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(electron): close active tab on Cmd+W when multiple tabs are open

Cmd/Ctrl+W now closes the focused tab first and only closes the window when
a single tab (or none) remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(electron): add Cmd+T shortcut to open a new tab

Reuses the active tab's plugin context to create a same-type tab, mirroring
the TabBar + button behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(electron): use container color for active tab background

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(electron): update Close menu item expectations for smart Cmd+W

Tests now assert the CmdOrCtrl+W accelerator and click handler instead of
the legacy role: 'close'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(electron): drop const/store import from HeterogeneousAgentCtr

The controller previously pulled defaultProxySettings from @/const/store,
which chain-loads @/modules/updater/configs and electron-is — that breaks
any unit test that mocks `electron` without a full app shim. Make
buildProxyEnv accept undefined and read the store value directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:38:54 +08:00
Arvin Xu e7236c0169 🐛 fix(user): validate avatar URL and scope old-avatar deletion to owner (#13982)
Reject avatar values that aren't a base64 data URL, an absolute http(s) URL,
or an internal /webapi/user/avatar/<userId>/ path for the caller. Also
require the old avatar URL to live under the caller's own prefix (and
contain no '..') before removing it from S3.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:58:14 +08:00
YuTengjing fb471123fc feat: support model alias mapping for image and video runtimes (#13896) 2026-04-20 09:38:56 +08:00
Arvin Xu a0471d5906 feat(chat-input): branch ahead/behind badge + GitCtr refactor (#13980)
* 💄 style(todo-progress): replace green bar with inline progress ring

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(chat-input): split branch and diff blocks, add changed-files popover

Branch now has its own hover tooltip for the full name; the diff stat is a
sibling block that opens a lazy-loaded popover listing changed files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(chat-input): show ahead/behind commit count vs upstream

Adds a badge next to the branch chip showing commits pending push (↑, blue)
and pull (↓, red) against the branch's upstream tracking ref. Hidden when
no upstream is configured or both counts are zero. Refreshed on focus,
after checkout, and on manual refresh from the branch switcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(desktop): extract git IPC methods into dedicated GitController

Moves detectRepoType, getGitBranch, getLinkedPullRequest, listGitBranches,
getGitWorkingTree{Status,Files}, getGitAheadBehind, and checkoutGitBranch out
of SystemCtr into a new GitCtr (groupName = 'git'). Shared helpers (resolveGitDir
/ resolveCommonGitDir / detectRepoType) become pure functions under utils/git.ts
so SystemCtr's selectFolder can still probe the picked folder without crossing
controller boundaries. Renderer side: new electronGitService wraps ipc.git.*,
and all six chat-input hooks plus BranchSwitcher are switched over.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(chat-input): inline ahead/behind arrows into branch chip

Moves the ↑/↓ counts out of a separate status block and inside the branch
trigger next to the label, so they sit with the branch they describe instead
of after the file-change badge. Tooltip folds into the branch tooltip (full
name · N to push · M to pull) so a single hover covers both pieces of info.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(desktop): parse git status with -z to avoid filename misparse

The previous getGitWorkingTreeFiles split every line on ' -> ' to detect
renames, but only R/C status codes emit that delimiter. Legitimate filenames
containing ' -> ' (or spaces, or embedded newlines) were misparsed — the
popover would report a truncated path or lose the entry entirely.

Switch both getGitWorkingTreeStatus and getGitWorkingTreeFiles to
`git status --porcelain -z`: NUL-terminated records, no C-style quoting,
no \n splitting hazards. Rename/copy entries emit two NUL-separated tokens
(DEST\0SRC) which we consume as a pair so counts and paths stay correct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(todo-progress): hide stale todos when a new user turn starts

Add `selectCurrentTurnTodosFromMessages` that scopes the todos lookup
to messages after the last user message. The inline TodoProgress
component now uses it, so a completed 8/8 progress bar from a previous
operation no longer lingers across the next user turn.

The original `selectTodosFromMessages` is unchanged because the agent
runtime step context still needs cross-turn visibility of the plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔒 fix(desktop): tighten GitHub remote detection to host position

Replace substring check `config.includes('github.com')` with a regex
anchored to URL host position so look-alikes like `evilgithub.com` and
`github.com.attacker.com` no longer classify as GitHub. Closes CodeQL
"Incomplete URL substring sanitization" on PR #13980.

Not a real security issue (the config file is local and the
classification only drives a UI icon), but the tightened check is
strictly more correct and silences the scanner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 02:11:43 +08:00
Innei 3bd7f1f146 🐛 fix(electron): align TabBar left padding with NavPanel width on initial load (#13981)
🐛 fix(electron): align TabBar left padding with NavPanel width on initial load

Defer DraggablePanel mount in NavPanelDraggable until `isStatusInit` flips true
so defaultSize captures the hydrated `leftPanelWidth` instead of the pre-hydration
default. Before hydration, render a placeholder div matching the store's current
width so NavigationBar's live-read width stays aligned with the DOM. Also adds
a small paddingRight to NavigationBar for visual balance.

Without this, the TabBar's left edge drifted away from the NavPanel's right edge
whenever the user's persisted panel width differed from the 320px default.
2026-04-20 01:46:05 +08:00
Innei 730169e6b6 feat(electron): add + button to TabBar for new topic in active context (#13972)
*  feat(electron): add + button to TabBar to open new topic in active context

Introduce a pluggable `createNewTabAction` extension on RecentlyViewed
plugins so each page type can decide whether (and how) to spawn a new
tab from the active tab. Implemented for agent / agent-topic /
group / group-topic — clicking `+` creates a fresh topic under the
current agent/group and opens it as a new tab; other page types hide
the button by default.

*  feat(electron): support new tab from page context

Page plugin now implements `createNewTabAction`, creating a fresh
untitled document via `usePageStore().createPage` and opening it as
a new `page` tab.

* 🐛 fix(electron): refresh page list after creating a new page via TabBar +

`createPage` only hits the service; without refreshing the documents
list, the sidebar / PageExplorer wouldn't show the freshly-created
page until the next full reload.

* 🐛 fix(electron): highlight new page in sidebar when opened via TabBar +

Switch to `createNewPage`, which runs the full optimistic flow —
dispatches the new document into the sidebar list and sets
`selectedPageId` — so the nav item active state stays in sync with
the freshly-opened page tab.

* 🐛 fix(electron): dispatch real page doc into sidebar list for TabBar +

The earlier `createNewPage` approach relied on an optimistic temp
document that SWR revalidation can clobber before the real doc
replaces it, leaving the new page absent from the sidebar. Create
the page via `createPage` first, then synthesize a `LobeDocument`
from the server response and dispatch it into the list alongside
setting `selectedPageId` — the nav item now appears and highlights
in sync with the new tab.
2026-04-20 01:04:51 +08:00
Innei 6b6915d147 feat(onboarding): add preset agent naming suggestions (#13931)
*  feat(onboarding): add preset agent naming suggestions

* 🐛 fix(test): align AgentDocumentsGroup test assertions with title-first rendering

#13940 changed DocumentItem to prefer title over filename, but the
AgentDocumentsGroup tests from #13924 were still asserting on filename
strings. Update all text matchers to use titles (Brief / Example).
2026-04-20 00:54:11 +08:00
Rdmclin2 0213656565 🐛 fix: message gateway (#13979)
* fix: local webhook typing

* feat: add dormant status

* feat: add bot status tag

* feat: add bot connection status and refresh status

* feat: support bot status list refresh

* fix: bot status

* chore: add test timeout
2026-04-20 00:17:57 +08:00
Arvin Xu 8240e8685d 🐛 fix(desktop): repo-type detection for submodule/worktree + chat & sidebar polish (#13978)
* 🐛 fix(desktop): detect repo type for submodule and worktree directories

Route detectRepoType through resolveGitDir so directories where `.git`
is a pointer file (submodules, worktrees) are correctly identified as
git/github repos instead of falling back to the plain folder icon.

Fixes LOBE-7373

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(desktop): reprobe repo type for stale recent-dir entries

The recents picker rendered `entry.repoType` directly from localStorage,
so any submodule/worktree entry cached while `detectRepoType` still
returned `undefined` stayed stuck on the folder icon even after the
main-process fix. Wrap each row icon in a component that calls
`useRepoType`, which re-probes missing entries and backfills the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-input): clear autocomplete hint on IME start to prevent freeze

Dispatch KEY_ESCAPE_COMMAND on compositionstart so the autocomplete
plugin removes PlaceholderInline/PlaceholderBlock nodes before the IME
begins composing. Composing next to those placeholder nodes caused the
editor to freeze during pinyin input with a visible hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(topic-sidebar): split project grouping into ByProjectMode

Extracts project-specific group rendering from ByTimeMode into its own ByProjectMode folder, with a shared GroupedAccordion container. Project groups get a folder-icon column aligned with the topic item layout and a "new topic in {directory}" action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(desktop): read config via commondir for linked worktrees

`resolveGitDir` returns `.git/worktrees/<name>/` for linked worktrees —
that dir has its own `HEAD` but no `config`, so `detectRepoType` still
returned `undefined` and worktrees missed the repo icon. Resolve the
`commondir` pointer first so `config` is read from the shared gitdir.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:56:39 +08:00
Arvin Xu 46df77ac3f 💄 style(tab-bar): blend inactive tabs with titlebar, show close icon by default (#13973)
* 💄 style(tab-bar): blend inactive tabs with titlebar, show close icon by default

Inactive tabs now use a transparent background and gain a subtle hover fill,
matching Chrome's tab chrome so the titlebar feels visually unified. The close
icon is always visible instead of fading in on hover, so users don't have to
hunt for it on narrow tabs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(desktop): CMD+N now actually clears active topic on agent page

Previously the File → 新建话题 (CMD+N) handler only `navigate()`d to the
agent base path. When the user was on `/agent/:aid?topic=xxx`, this stripped
the URL param but `ChatHydration`'s URL→store updater skips `undefined`
values, so `activeTopicId` in the chat store was never cleared and the
subscriber would push the stale topic right back into the URL.

Call `switchTopic(null)` on the store directly when an agent is active so
the change propagates store→URL via the existing subscriber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): don't surface self-cancelled exits as runtime errors

User-initiated cancel/stop and Electron before-quit kill the agent process
with SIGINT/SIGTERM, producing non-zero exit codes (130/143/137). Mark
these via session.cancelledByUs so the exit handler routes them through
the complete broadcast — otherwise a user cancel or app shutdown would
look like an agent failure (e.g. "Agent exited with code 143" leaking
into other live CC sessions' topics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(tab-bar): show running indicator dot on tab when agent is generating

Adds a useTabRunning hook that reads agent runtime state from the chat
store for agent / agent-topic tabs, and renders a small gold dot over
the tab avatar/icon while the conversation is generating. Other tab
types stay unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(claude-code): render ToolSearch select: queries as inline tags

Parses select:A,B,C into individual tag chips (monospace, subtle pill
background) instead of a comma-joined string, so the names of tools
being loaded read more clearly. Keyword queries keep the existing
single-highlight rendering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(git-status): show +N ±M -K diff badge next to branch name

Surface uncommitted-file count directly in the runtime-config status bar
so the dirty state is visible at a glance without opening the branch
dropdown. Each segment is color-coded (added / modified / deleted) and
hidden when zero; a tooltip shows the verbose breakdown.

Implementation:
- Backend buckets `git status --porcelain` lines into added / modified /
  deleted / total via X+Y status pair
- New always-on useWorkingTreeStatus SWR hook (focus revalidation, 5s
  throttle) shared by GitStatus and BranchSwitcher — single fetch path
- BranchSwitcher's "uncommitted changes: N files" now reads `total`

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(assistant-group): show only delete button while tool call is in progress

When the last child of an assistantGroup is a running tool call, `contentId`
is undefined and the action bar fell through to a branch that dropped the
`menu` and `ReactionPicker`, leaving a single copy icon with no overflow.
Replace the legacy `continueGeneration / delAndRegenerate / del` bar with a
del-only bar in this state — delete is the only action that makes sense
before any text block is finalized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(conversation-flow): aggregate per-step nested metadata.usage in assistantGroup

After hetero-agent moved to per-step usage writes (`metadata: { usage: {...} }`),
the assistantGroup virtual message stopped showing the cumulative token total
across steps and instead surfaced only the last step's numbers.

Root cause: splitMetadata only recognised the legacy flat shape
(`metadata.totalTokens`, etc.) and didn't read the new nested shape, so each
child block went into aggregateMetadata with `usage: undefined`. The sum was
empty, and the final group inherited a single child's metadata.usage purely
because Object.assign collapsed groupMetadata down to the last child.

- splitMetadata now reads both nested (`metadata.usage` / `metadata.performance`)
  and flat (legacy) shapes; nested takes priority
- Add `'usage'` / `'performance'` to the usage/performance field sets in parse
  and FlatListBuilder so the nested objects don't leak into "other metadata"
- Regression test: multi-step assistantGroup chain sums child usages

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(hetero-agent): tone down full-access badge to match left bar items

The badge was shouting in colorWarning + 500 weight; reduce to
colorTextSecondary at normal weight so it sits at the same visual rank
as the working-dir / git buttons on the left. The CircleAlert icon
still carries the warning semantics. Also force cursor:default so the
non-interactive label doesn't pick up an I-beam over its text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 21:53:22 +08:00
Arvin Xu 6ca5fc4bdc feat(hetero-agent): Claude Code runtime, cwd, and sidebar polish (#13970)
*  feat(hetero-agent): synthesize pluginState.todos from CC TodoWrite

Adapter now translates Claude Code's declarative TodoWrite tool_use input into the shared StepContextTodos shape and attaches it to tool_result. Selector drops the GTD identifier filter so any producer honoring pluginState.todos lights up the TodoProgress card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): skip TodoWrite pluginState synthesis on error results

A failed TodoWrite (is_error=true) means the snapshot was never applied on CC's side. Since selectTodosFromMessages now picks the latest pluginState.todos from any producer, leaking a failed-write snapshot could overwrite the live todo UI with changes that never actually happened. Drain the cache either way so a retry with a fresh tool_use id doesn't inherit stale args.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): prefer topic-level cwd on send; route UI changes to active topic

Topic-level workingDirectory now takes priority over agent-level on the
send path, matching what the topic is actually pinned to. The UI picker
writes to the active topic's metadata (not the agent default), and warns
before switching when doing so would invalidate an existing CC session.

*  feat(tab): reset tab cache when page type changes to stop stale metadata bleed

Switching a tab from one page type to another (e.g. agent → home) kept
the previous page's cached title/avatar, so the new page rendered with
the wrong header. Reset the cache on type change; preserve the merge
only when the type stays the same.

* 🐛 fix(hetero-agent): kill CC process tree on cancel so tool children exit

SIGINT to just the claude binary was leaving bash/grep/etc. tool
subprocesses running, which kept the CLI hung waiting on them. Spawn
the child detached (Unix) so we can signal the whole group via
process.kill(-pid, sig); use taskkill /T /F on Windows. Escalate
SIGINT → SIGKILL after 2s for tool calls that swallow SIGINT, and do
the same tree kill on disposeSession's SIGTERM path.

*  feat(hetero-agent): show "Full access" badge in CC working-directory bar

Claude Code runs locally with full read/write on the working directory
and permission mode switching isn't wired up yet — the badge sets that
expectation up-front instead of leaving users guessing. Tooltip spells
out the constraint for anyone who wants detail.

* ♻️ refactor(agent-list): show runtime name (Claude Code/Codex) instead of generic "External" tag

The "External" tag on heterogeneous agents didn't tell users which
runtime backs the agent — multiple CLI runtimes (Claude Code, Codex, …)
looked identical in the sidebar. Map the heterogeneous type to its
display name so the tag identifies the actual runtime, with the raw
type as a fallback for any future provider we haven't mapped yet.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:33:11 +08:00
Arvin Xu 77fd0f13f0 🐛 fix(hetero-agent): persist streamed text alongside tool writes; collapse workflow summary (#13968)
* 🐛 fix(hetero-agent): persist accumulated text alongside tools[] writes

Carry the latest streamed content/reasoning into the same UPDATE that
writes tools[], so the DB row stays in sync with the in-memory stream.
Without this, gateway `tool_end → fetchAndReplaceMessages` reads a
tools-only row and clobbers the UI's streamed text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(workflow-summary): collapse summary when many tool kinds

When a turn calls >4 distinct tool kinds, list only the top 3 by count
and append "+N more · X calls total[ · Y failed]". Keeps the inline
summary scannable on long tool-heavy turns instead of running off the
line. Short turns keep the existing full list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(claude-code): use chip style for Skill inspector name

Replace the colon+highlight text with a pill-shaped chip containing the
SkillsIcon and skill name. Gives the Skill activation readout visual
parity with other tool chips and prevents long skill names from
overflowing the inspector line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(agent-documents): assert on rendered title, not filename

#13940 changed DocumentItem to prefer document.title over filename, but
the sidebar test still expected 'brief.md' / 'example.com'. Align the
assertions with the current behavior so the suite is green on canary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(tab-bar): show agent avatar on agent/topic tabs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:13:46 +08:00
Arvin Xu ccbb75da06 ♻️ refactor(hetero-agent): persist per-step usage to each step assistant message (#13964)
* ♻️ refactor(hetero-agent): persist per-step usage to each step assistant message

Previously, usage tokens from a multi-step Claude Code run were accumulated
across all turns and written only to the final assistant message, leaving
intermediate step messages with no usage metadata.

Each Claude Code `turn_metadata` event carries per-turn token usage
(deduped by adapter per message.id), so write it straight through to the
current step's assistant message via persistQueue (runs after any in-flight
stream_start(newStep) that swaps currentAssistantMessageId). The `result_usage`
grand-total event is intentionally dropped — applying it would overwrite the
last step with the sum of all prior steps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(hetero-agent): normalize usage inside CC adapter (UsageData)

Follows the same principle as LOBE-7363: provider-native shape knowledge
stays in the adapter, executor only sees normalized events. The previous
commit left Anthropic-shape fields (input_tokens, cache_creation_input_tokens,
cache_read_input_tokens) leaking into the executor via `buildUsageMetadata`.

Introduce `UsageData` in `@lobechat/heterogeneous-agents` types with LobeHub's
MessageMetadata.usage field names. The Claude Code adapter now normalizes
Anthropic usage into `UsageData` before emitting step_complete, for both
turn_metadata (per-turn) and result_usage (grand total). Executor drops
`buildUsageMetadata` and writes `{ metadata: { usage: event.data.usage } }`
directly. Future adapters (Codex, Kimi-CLI) normalize their native usage into
the same shape; executor stays provider-agnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(hetero-agent): persist per-step provider alongside model

CC / hetero-agent assistant messages were writing `model` per step but
leaving `message.provider` NULL, so pricing/usage lookups could not key on
the adapter (e.g. `claude-code`, billed via CLI subscription rather than
raw Anthropic API rates).

CC adapter now emits `provider: 'claude-code'` on every turn_metadata event
(same collection point as model + normalized usage). Executor tracks
`lastProvider` alongside `lastModel` and writes it into:

- the step-boundary update for the previous step
- `createMessage` for each new step's assistant
- the onComplete write for the final step

Provider choice is the CLI flavor (what the adapter knows), not the wrapped
model's native vendor — CC runs under its own subscription billing, so
downstream pricing must treat `claude-code` as its own provider rather than
conflating with `anthropic`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(hetero-agent): read authoritative usage from message_delta, not assistant

Under `--include-partial-messages` (enabled by the CC adapter preset), Claude
Code echoes a STALE usage snapshot from `message_start` on every content-block
`assistant` event — e.g. `output_tokens: 8` or `1` — and never updates that
snapshot as more output tokens are generated. The authoritative per-turn
total arrives on a separate `stream_event: message_delta` with the final
`input_tokens` + cache counts + cumulative `output_tokens` (e.g. 265).

The adapter previously grabbed usage from the first `assistant` event per
message.id and deduped, so DB rows ended up with `totalOutputTokens: 1` on
every CC turn.

Move turn_metadata emission from `handleAssistant` to a new `message_delta`
case in `handleStreamEvent`. `handleAssistant` still tracks the latest model
so turn_metadata (emitted later on message_delta) carries the correct model
even if `message_start` doesn't.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(extras-usage): fall back to metadata.usage when top-level is absent

The assistant Extras bar passes `message.usage` to the Usage component,
which conditionally renders a token-count badge on `!!usage.totalTokens`.
Nothing in the read path aggregates `message.metadata.usage` up to
`message.usage`, so the top-level field is always undefined for DB-read
messages — the badge never shows for CC/hetero turns (and in practice also
skips the gateway path where usage only lands in `metadata.usage`).

Prefer `usage` when the top-level field is populated, fall back to
`metadata.usage` otherwise. Both fields are the same `ModelUsage` shape, so
the Usage/TokenDetail components don't need any other change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(extras-usage): promote metadata.usage inside conversation-flow parse

The previous fix spread a `usage ?? metadata?.usage` fallback across each
renderer site that passed usage to the Extras bar. Consolidate: `parse`
(src/store → packages/conversation-flow) is the single renderer-side
transform every consumer flows through, so promote `metadata.usage` onto the
top-level `usage` field there and revert the per-site fallbacks.

UIChatMessage exposes a canonical `usage` field, but no server-side or
client-side transform populated it — executors write to `metadata.usage`
(canonical storage, JSONB-friendly). Doing the promotion in parse keeps the
rule in one place, close to where display shapes are built, and covers both
desktop (local PGlite) and web (remote Postgres) without a backend deploy.

Top-level `usage` is preserved when already present (e.g. group-level
aggregates) — `metadata.usage` is strictly a fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:19:18 +08:00
Innei 2711aa9191 feat(desktop): add dedicated topic popup window with cross-window sync (#13957)
*  feat(desktop): add dedicated topic popup window with cross-window sync

Introduce a standalone Vite entry for the desktop "open topic in new window"
action. The popup is a lightweight SPA (no sidebar, no portal) hosting only
the Conversation, and stays in sync with the main window through a
BroadcastChannel bus.

- Add popup.html + entry.popup.tsx + popupRouter.config.tsx
- Add /popup/agent/:aid/:tid and /popup/group/:gid/:tid routes
- Reuse main Conversation/ChatInput; wrap in MarketAuth + Hotkeys providers
- Pin-on-top button in the popup titlebar (new windows IPC: set/isAlwaysOnTop)
- Group topic "open in new window" now uses groupId (previously misused agentId)
- Cross-window sync: refreshMessages/refreshTopic emit via BroadcastChannel;
  subscriber revalidates local SWR caches with echo-loop suppression
- Hide WorkingPanel toggle inside /popup (no WorkingSidebar present)
- RendererUrlManager dispatches /popup/* to popup.html in prod; dev middleware
  rewrites SPA deep links while skipping asset/module requests

* 💄 style(desktop): restore loading splash in popup window

* ♻️ refactor(desktop): replace cross-window sync with popup-ownership guard

The BroadcastChannel-based bidirectional sync between the main SPA and the
topic popup window had edge cases during streaming. Drop it in favour of a
simpler ownership model: when a topic is already open in a popup, the main
window shows a "focus popup" redirect instead of rendering a second
conversation.

- Remove src/libs/crossWindowBus.ts and src/features/CrossWindowSync
- Remove postMessagesMutation/postTopicsMutation calls from refresh actions
- Add windows.listTopicPopups + windows.focusTopicPopup IPC
- Main process broadcasts topicPopupsChanged on popup open/close; parses
  (scope, id, topicId) from the popup window's /popup/... path
- Renderer useTopicPopupsRegistry subscribes to broadcasts and fetches the
  initial snapshot; useTopicInPopup selects by scope
- New TopicInPopupGuard component with "Focus popup window" button
- Desktop-only index.desktop.tsx variants for (main)/agent and (main)/group
  render the guard when the current topic is owned by a popup
- i18n: topic.inPopup.title / description / focus in default + en/zh

* 🐛 fix(desktop): re-evaluate popup guard when topic changes

Subscribe to the popups array and derive findPopup via useMemo so scope changes (e.g. switching topic in the sidebar while a popup is open) correctly re-compute the guard and let the main window render the newly active topic.

* 🐛 fix(desktop): focus detached topic popup from main window

*  feat(desktop): add open in popup window action to menu for active topic

Signed-off-by: Innei <tukon479@gmail.com>

* 🎨 style: sort imports to satisfy simple-import-sort rule

*  feat(error): add resetPath prop to ErrorCapture and ErrorBoundary for customizable navigation

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor: restore ChatHydration in ConversationArea for web/mobile routes

Reintroduce ChatHydration component to agent and group ConversationArea
so that URL query sync (topic/thread) works on web and mobile routes,
not only on desktop entry files.

*  feat(electron): enforce absolute base URL in renderer config to fix asset resolution in popup windows

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-19 02:15:29 +08:00
Innei c213483a7a feat(workflow): tri-state completion status icon for WorkflowCollapse (#13952)
*  feat: add full-expand toggle to WorkflowCollapse with three-level expansion

- Replace boolean expanded with expandLevel: 'collapsed' | 'semi' | 'full'
- Add cyclic toggle button in header (ChevronDown / Maximize2 / Minimize2)
- Keep max-height scroll constraint in semi mode, remove it in full mode
- Update tests for three-level states and toggle behavior

*  feat: enhance WorkflowCollapse with animated expand toggle and refined icon behavior

- Introduced animated transitions for the expand toggle button using `motion` from `framer-motion`.
- Updated expand toggle logic to improve user experience with clearer icon states.
- Removed unused `ChevronDown` icon and adjusted expand toggle label conditions.
- Added constants for toggle icon size and transition settings for better maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* test: fix WorkflowCollapse tests for animated toggle behavior

* feat(workflow): tri-state completion status icon for WorkflowCollapse

Replace binary errorPresent with getWorkflowCompletionStatus:
- success → green Check
- partial failure → yellow AlertTriangle
- all failed → red X

Adds unit tests for all three states.

* fix(workflow): address Codex review feedback

- Add workflow.collapse / workflow.expandFull locale keys
- Make expand toggle keyboard-accessible (tabIndex + Enter/Space)

* refactor(workflow): replace nested ternary with switch for statusIcon

* 🌐 fix(workflow): remove hardcoded defaultValue from i18n keys

Addresses Codex review: per AGENTS.md i18n rule, user-facing strings
should live in locale files, not as defaultValue fallbacks.

- Remove defaultValue from t('workflow.expandFull') and t('workflow.collapse')
- Update test mock to include the new keys so tests remain green

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-19 01:23:51 +08:00
Arvin Xu 4e5db98ffc ♻️ refactor(agent-documents): fix title/documentId flow + split Inspector per action (#13940)
- extract H1 from markdown content as document title (stripped from content)
- use title verbatim as filename (no extension); simplify dedup to `-2`, `-3`
- AgentDocumentModel.create accepts optional title; falls back to filename
- ExecutionRuntime createDocument returns documents.id (not agentDocuments.id)
  as state.documentId so the portal can resolve the row for openDocument
- sidebar DocumentItem prefers title over filename
- split AgentDocumentsInspector into 11 per-apiName components (Notebook pattern)
- tests: filename util (13), ExecutionRuntime wiring (5), updated model + service

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:06:06 +08:00
Arvin Xu b909e4ae20 💄 style(hetero-agent): add hetero-mode actions bar (#13963)
*  feat(hetero-agent): add hetero-mode actions bar with copy/delete only

Hide edit, regenerate, branching, translate, tts, share and delAndRegenerate
for heterogeneous-agent sessions where these actions don't apply. Introduce
`mode: 'hetero'` on MessageActionsConfig and dispatch to dedicated Hetero
action bars for user, assistant, and assistant-group messages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(conversation): replace per-role action hooks with declarative action registry

Replace the 4 duplicate per-role action hooks (useUserActions / useAssistantActions
/ useGroupActions / Task.useAssistantActions) and the 4 copies of
stripHandleClick / buildActionsMap / dispatch logic with a single registry +
universal MessageActionBar renderer.

Each action (copy / del / edit / regenerate / delAndRegenerate /
continueGeneration / translate / tts / share / collapse / branching) is now a
standalone module under components/MessageActionBar/actions/. Config is
declarative — string slot keys (e.g. ['copy', 'divider', 'del']) resolved
against the registry at render time.

Hetero-agent sessions drop the special mode flag; they just declare copy-only
slot lists via config. Dev-mode branching becomes a registry key instead of a
factory.

Deletes ErrorActionsBar (handled in-place via slot lists), the dead
Supervisor/Actions folder, and the HeteroActionsBar scaffold introduced in
the previous commit.

Net: -1900 lines, one place to add a new action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:16:48 +08:00
Rdmclin2 7fe751eaec feat: billboard in sidebar (#13962)
* feat: support billboard

* feat: support BillBoard display

* fix: carousel dot style

* chore: adjust Anouncements copy

* feat: add annoucements animations

* feat: support  i18n and show less and more

* fix: notification copy

* chore: remove show less and show more

* feat:support Billboard title i18n

* fix: show billboard in time window

* feat: add  schema validation

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* Potential fix for pull request finding 'Unused variable, import, function or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: test case

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-19 00:00:34 +08:00
Arvin Xu f38dcc4cfc 🐛 fix(cc): persist workingDirectory when CC topic is created (#13956)
Hetero-agent topic creation went through `aiChat.sendMessageInServer`'s
`newTopic` payload, which had no metadata field, so the topic row was
inserted with `metadata.workingDirectory = NULL`. Today the only writer
is the post-execution `updateTopicMetadata` in `heterogeneousAgentExecutor`
— that never lands when CC is cancelled or errors before completion, and
in the meantime the topic is missed by By-Project grouping and `--resume`
cwd verification has nothing to compare against.

Source the cwd at the start of the hetero branch and thread it through
`newTopic.metadata`, so the binding is set at insert time. The post-exec
update still runs to record `ccSessionId` (and is now a no-op for cwd).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 23:08:15 +08:00
Arvin Xu 30e93ada67 ♻️ refactor(hetero-agent): rename ccSessionId to heteroSessionId (#13961)
CC-specific naming leaked into a field/module that's meant to be shared
across heterogeneous agent adapters. Rename to a provider-neutral id so
new adapters can reuse the topic-level session binding without inheriting
CC terminology.

- ChatTopicMetadata.ccSessionId -> heteroSessionId
- resolveCcResume / CcResumeDecision -> resolveHeteroResume / HeteroResumeDecision
- ccResume.{ts,test.ts} -> heteroResume.{ts,test.ts}
- updateTopicMetadata zod schema + executor + conversationLifecycle callsites

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:52:08 +08:00
Arvin Xu bc9164ae4a 🐛 fix(cmdk): scope topic/message search to current agent (#13960)
Previously `agentId` was only used to boost relevance in SearchRepo,
so results from other agents still leaked into CMD+K when scoped to
an agent. Strictly filter topics/messages by `agentId` when provided,
and surface the active agent (avatar + title) as the scope chip so
users can see what the search is limited to.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:41:32 +08:00
Innei e990b08cc6 ♻️ refactor(types): break circular dep between types and const packages (#13948)
* ♻️ refactor(types): break circular dep between types and const packages

Types package should only carry types, not values. Moved hotkey type
definitions to be owned by @lobechat/types and removed the @lobechat/const
runtime dependency from @lobechat/types. @lobechat/const now imports its
hotkey types from @lobechat/types via import type and uses satisfies to
keep enum values aligned.

*  feat(types): add desktop hotkey types and configuration

Introduced new types for desktop hotkeys, including `DesktopHotkeyId`, `DesktopHotkeyItem`, and `DesktopHotkeyConfig`. These types facilitate the management of hotkeys in the desktop application, ensuring better type safety and clarity in the codebase. Updated documentation to reflect the relationship with `@lobechat/const` entrypoints.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-18 22:36:13 +08:00
Innei 5c82da7515 feat(onboarding): persist topic onboarding analytics snapshot (#13930)
*  feat(onboarding): persist topic onboarding analytics snapshot

* fix(onboarding): allow null in syncTopicOnboardingSession metadata option

Resolves TS2322 where topic?.metadata (ChatTopicMetadata | null | undefined)
was not assignable to metadata?: ChatTopicMetadata (undefined only).
The function already safely handles null via the ?? fallback, so widening
the parameter type is the minimal correct fix.

* fix(test): add ShikiLobeTheme to @lobehub/ui mock in WorkflowCollapse test

Resolves vitest error where @lobehub/editor tries to load
ShikiLobeTheme from the mocked module.
2026-04-18 22:08:56 +08:00
Arvin Xu 9218fbfcf3 💄 style(shared-tool-ui): wrap Bash inspector in a rounded chip (#13959)
💄 style(shared-tool-ui): wrap RunCommand inspector in a rounded chip

Put the terminal-prompt icon and the mono command text inside a single
pill-shaped chip (colorFillTertiary background) so the command reads as
one unit instead of two loose elements next to the "Bash:" label. Row
goes back to center-aligned since the chip has its own vertical padding.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:01:23 +08:00
Arvin Xu d581937196 feat(cc): account card, topic filter, and CC integration polish (#13955)
* 💄 style(error): refine error page layout and stack panel

Replace Collapse with Accordion for a clickable full-row header, move
stack below action buttons as a secondary branch, and wrap in a Block
that softens to filled when collapsed and outlined when expanded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(cc): boost topic loading ring contrast in light mode

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(error): reload page on retry instead of no-op navigate

The retry button called navigate(resetPath) which often landed on the
same path and re-triggered the same error, feeling broken. Switch to
window.location.reload() so the error page actually recovers, and drop
the now-unused resetPath prop across route configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(cc-agent): send prompt via stdin stream-json to avoid CLI arg parsing

Previously the Claude Code prompt was appended as a positional CLI arg,
so any prompt starting with `-` / `--` (dashes, 破折号) got
misinterpreted as a flag by the CC CLI's argparser.

Switch the claude-code preset to `--input-format stream-json` and write
the prompt as a newline-delimited JSON user message on stdin for all
messages (not just image-attached ones). Unifies the image and text
paths and paves the way for LOBE-7346 Phase 2 (persistent process +
native queue/interrupt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(cc): extract per-tool inspectors into Inspector/ folder

Mirrors the Inspector/<Tool>/index.tsx convention used by builtin-tool-skills,
builtin-tool-skill-store, and builtin-tool-activator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(cc): flatten Inspector/ to per-tool tsx files

Drop the per-tool subfolder wrapper (Inspector/Edit/index.tsx → Inspector/Edit.tsx)
since each tool is a single file — no co-located assets to justify the folder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(topic): add filter with By project grouping and sort-by option

Split the legacy topicDisplayMode enum into independent topicGroupMode
(byTime / byProject / flat) and topicSortBy (createdAt / updatedAt), and
surface them from a new sidebar Filter dropdown. Adds groupTopicsByProject
so topics can be grouped by their workingDirectory, with favorites pinned
and the "no project" bucket placed last.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(cc): show Claude Code account and subscription on profile

Add a getClaudeAuthStatus IPC that shells out to claude auth status --json,
and render the returned email + subscription tag on the CC Status Card.
The auth fetch runs independently of tool detection so a failure can't
flip the CLI card to unavailable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(home): show running spinner badge on agent/inbox avatars

Replace NavItem's generic loading state with a bottom-right spinner badge
on the avatar, so a running agent stays clearly labelled without hiding
the avatar. Inbox entries switch to per-agent isAgentRunning so only the
actively running inbox shows the badge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(cc): default-expand Edit and Write tool renderers

Add ClaudeCodeApiName.Edit and Write to ClaudeCodeRenderDisplayControls
so their inspectors render expanded by default, matching TodoWrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🔧 chore(cc): drop default system prompt when creating Claude Code agent

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Update avatar URL for Claude Code

*  test(workflow-collapse): stub ShikiLobeTheme on @lobehub/ui mock

@lobehub/editor's init code reads ShikiLobeTheme from @lobehub/ui, which
some transitive import pulls in during the test. Add the stub to match
the pattern used in WorkingSidebar/index.test.tsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(cc): fall back to Desktop path instead of `/` when no cwd is set

- Selector prefers desktopPath over homePath before it resolves nothing,
  so the renderer always forwards a sensible cwd.
- Main-process spawn mirrors the same fallback with app.getPath('desktop'),
  covering cases where Electron is launched from Finder (parent cwd is `/`).

Fixes LOBE-7354

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(topic): use remote app origin for topic copy link

Desktop 下 window.location.origin 是 app://renderer,复制出来的链接无法分享。
改用 useAppOrigin(),与分享链接保持一致(web 用 window.location.origin,
desktop 用 electron store 的 remoteServerUrl)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 21:58:50 +08:00
Innei 568389d43f ♻️ refactor(web-onboarding): rename doc tools and drive incremental persona writes (#13933)
* ♻️ refactor(web-onboarding): rename doc tools and drive incremental persona writes

- Rename writeDocument (full rewrite) and updateDocument (SEARCH/REPLACE patch) so tool
  names match model intuition; the old updateDocument (full) is now writeDocument and the
  old patchDocument (patch) is now updateDocument.
- Rework systemRole, toolSystemRole, and OnboardingActionHintInjector to require per-turn
  persistence: seed persona on user_identity, patch on every discovery turn where a new
  fact is learned, and stop the one-shot full-write pattern.
- Add a Pre-Finish Checklist so agents verify soul/persona reflect the session before
  calling finishOnboarding.

Eval (deepseek-chat, web-onboarding-v3):
- fe-intj-crud-v1: write=2, updateDocument=6/6 success
- extreme-minimal-response-v1: write=2, updateDocument=4/4 success
- Previously 0 patch usage; now patch dominates incremental edits.

* 🐛 fix(web-onboarding): decouple fullName persistence from role discovery

Persona seeding and saveUserQuestion(fullName) were gated on learning both
name AND role in the same turn, which regressed the prior behavior of saving
the name the moment it was provided. If the user shared only a name (or left
early before role was clarified), the agent could skip the save and end
onboarding with missing identity data.

Split the hint:
1. saveUserQuestion(fullName) fires as soon as the name is known, regardless
   of role.
2. Persona seeding fires on ANY useful fact (name alone, role alone, or both).

Thanks to codex review for catching this.
2026-04-18 20:02:39 +08:00
Arvin Xu 7d5889a7ed feat(heterogeneous-agent): git-aware runtime config + topic rename modal + inspectors (#13951)
*  feat(cc-desktop): git-aware runtime config + topic rename modal + inspectors

Cluster of desktop UX improvements around the Claude Code integration:

- CC chat input runtime bar: branch switcher, git status, and a richer
  working-directory bar powered by a new SystemCtr git API
  (branch list / current status) and `useGitInfo` hook.
- Topic rename: switch to a dedicated RenameModal component; add an
  auto-rename action in the conversation header menu.
- ToolSearch inspector for the CC tool client.
- Shared DotsLoading indicator.
- Operation slice tidy-ups for CC flows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(types): rename heterogeneous provider type `claudecode` → `claude-code`

Align the type literal with the npm/CLI naming convention used elsewhere
(@lobechat/builtin-tool-claude-code, claude-code provider id) so the union
matches the rest of the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(cc-desktop): polish TodoWrite labels, branch switcher refresh, and chat input affordances

- TodoWrite render + inspector: i18n the header label (Todos / Current step
  / All tasks completed), surface the active step inline as highlighted text,
  and switch the in-progress accent from primary to info for better contrast.
- BranchSwitcher: move the refresh button into the dropdown's section header,
  switch the search and create-branch inputs to the filled variant, and
  reuse DropdownMenuItem for the create-branch entry instead of a custom
  footer chip.
- GitStatus: drop the inline refresh affordance (now lives in the switcher),
  collapse trigger styles, and split the PR badge with its own separator.
- WorkingDirectory / WorkingDirectoryBar: tighten paddings and gaps so the
  runtime config row reads at a consistent height.
- InputEditor: skip inline placeholder completion when the cursor is not at
  end of paragraph — inserting a placeholder mid-text triggered nested
  editor updates that froze the input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(cc-desktop): probe repoType for working dirs not cached in recents

GitStatus was gated on the `repoType` stored in `recentDirs`, but legacy
string entries and agent-config-driven paths that never went through the
folder picker have no cached `repoType`. As a result, branch / PR status
silently disappeared for valid git repos until users re-selected the
folder.

Promote `detectRepoType` to a public IPC method and add a `useRepoType`
hook that uses the cached value as a fast path, otherwise probes the
filesystem via SWR and backfills the recents entry so subsequent reads
hit cache. Both runtime config bars (CC mode + heterogeneous chat input)
now resolve `repoType` through the hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(shared-tool-ui): rework Bash/Grep/Glob inspector rows

- RunCommand: terminal-prompt icon + mono command text instead of underline highlight
- Grep: split pattern by `|` into mono tag chips
- Glob: single mono tag chip matching Grep
- Switch rows to baseline alignment so the smaller mono text lines up with the label

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 🐛 fix(DotsLoading): allow optional color in styles params

The Required<StyleArgs> generic forced color to string, but it's only
defaulted at the CSS level via fallback to token.colorTextSecondary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 18:40:39 +08:00
Arvin Xu 5dc94cbc45 feat(cc-agent): improve for CC integration mode (#13950)
*  feat(cc-agent-profile): swap model/skills pickers for CC CLI status in CC mode

When an agent runs under the Claude Code heterogeneous runtime, its model and tools are
owned by the external CLI, so the profile page's model selector and integration-skills
block are misleading. Replace them with a card that re-detects `claude --version` on
mount and shows the resolved binary path — useful when CLAUDE_CODE_BIN or similar
points at a non-default CLI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(cc-agent-profile): hide cron for CC agent and polish render previews

- Hide cron sidebar entry when current agent is heterogeneous (CC)
- Allow model avatar in agent header emoji picker
- Add padding to Glob/Grep/Read/Write preview boxes for consistent spacing
- Simplify NavPanelDraggable by removing slide animation layer

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ♻️ refactor(shared-tool-ui): extract ToolResultCard for Read/Write/Glob/Grep renders

Hoist the shared card shell (icon + header + preview box) into
@lobechat/shared-tool-ui/components so the four Claude Code Render
files no longer duplicate container/header/previewBox styles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(agent-header): restyle title and expand actions menu

Bold the topic title, render the working directory as plain text (no chip/icon), move the "..." menu to the left, and expand it with pin/rename/copy working directory/copy session ID/delete. Fall back to "New Topic" when no topic is active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(topic-list): replace spinning loader with ring-and-arc loading icon

Adds a reusable RingLoadingIcon (static track + rotating arc, mirroring the send-button style) and swaps the topic-item loader over to it so the loading state reads as a polished ring rather than a thin spinning dash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(topic-list): switch unread indicator to a radar ping effect

Replaces the glowing neon-dot pulse with a smaller 6px core dot plus a CSS-keyframe ripple ring that scales out and fades, giving the unread marker a subtler, more refined cadence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 💄 style(cc-chat-input): drop file upload in CC mode, surface typo toggle

Claude Code brings its own file handling and knowledge context, so the
paperclip dropdown only showed "Upload Image" + a useless "View More"
link — confusing and not clean. Replace fileUpload with typo in the
heterogeneous chat input, and fold ServerMode back into a single
Upload/index.tsx now that the ClientMode/ServerMode split is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:53:58 +08:00
Arvin Xu 13fe968480 feat: claude code intergration polish (#13942)
* 🐛 fix(cc-resume): guard resume against cwd mismatch (LOBE-7336)

Claude Code CLI stores sessions per-cwd under `~/.claude/projects/<encoded-cwd>/`,
so resuming a session from a different working directory fails with
"No conversation found with session ID". Persist the cwd alongside the session
id on each turn and skip `--resume` when the current cwd can't be verified
against the stored one, falling back to a fresh session plus a toast explaining
the reset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(cc-desktop): Claude Code desktop polish + completion notifications

Bundles the follow-on UX improvements for Claude Code on desktop:

- Completion notifications: CC / Codex / ACP runs now fire a desktop
  notification (when the window is hidden) plus dock badge when the turn
  finishes, matching the Gateway client-mode behavior.
- Inspector + renders: add Skill and TodoWrite inspectors, wire them
  through Render/index + renders registry, expose shared displayControls.
- Adapter: extend claude-code adapter with additional event coverage and
  regression tests.
- Sidebar / home menu: clean up Topic list item and dropdown menu, rename
  "Claude Code Agent" entry point to "Add Claude Code" across EN/ZH.
- Assorted: NotificationCtr, Browser, WorkflowCollapse, ServerMode upload,
  agent/tool selectors — small follow-ups surfaced while building the
  above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  test(browser): mock electron.app for badge-clear on focus

Browser.focus handler now calls app.setBadgeCount / app.dock.setBadge to
clear the completion badge when the user returns. Tests imported the
Browser module without exposing app on the electron mock, causing a
module-load failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

*  feat(cc-topic): folder chip + unify cwd into workingDirectory (#13949)

 feat(cc-topic): show bound folder chip and unify cwd into workingDirectory

Replace the separate `ccSessionCwd` metadata field with the existing
`workingDirectory` so a CC topic's bound cwd has one source of truth:
persisted on first CC execution, read back by resume validation, and
surfaced in a clickable folder chip next to the topic title on desktop.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:42:00 +08:00
Innei a98d113a80 feat: add full-expand toggle to WorkflowCollapse with three-level expansion (#13906)
*  feat: add full-expand toggle to WorkflowCollapse with three-level expansion

- Replace boolean expanded with expandLevel: 'collapsed' | 'semi' | 'full'
- Add cyclic toggle button in header (ChevronDown / Maximize2 / Minimize2)
- Keep max-height scroll constraint in semi mode, remove it in full mode
- Update tests for three-level states and toggle behavior

*  feat: enhance WorkflowCollapse with animated expand toggle and refined icon behavior

- Introduced animated transitions for the expand toggle button using `motion` from `framer-motion`.
- Updated expand toggle logic to improve user experience with clearer icon states.
- Removed unused `ChevronDown` icon and adjusted expand toggle label conditions.
- Added constants for toggle icon size and transition settings for better maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* test: fix WorkflowCollapse tests for animated toggle behavior

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-18 12:25:00 +08:00
Innei 9a2ee8a58f feat(onboarding): add wrap-up button for agent onboarding (#13934)
Let users finish agent onboarding explicitly once they've engaged
enough, instead of waiting for the agent to trigger finishOnboarding.

- New WrapUpHint component above ChatInput; shows in summary phase or
  discovery phase after ≥3 user messages
- Confirm modal before finish; reuses existing finishOnboarding service
- Tightened Phase 2 (user_identity) system prompt: MUST save fullName
  before leaving phase, handle ambiguous name responses explicitly
2026-04-18 11:58:49 +08:00
LobeHub Bot 326ca352b1 🌐 chore: translate non-English comments to English in oidc-provider (#13945)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 11:41:26 +08:00
Junghwan 2c43f409d9 🐛 fix(desktop): sanitize heterogeneous-agent attachment cache filenames (#13937)
* Keep heterogeneous-agent attachment cache writes inside the cache root

The desktop heterogeneous-agent controller used raw image ids as path
segments for cache payload and metadata files. Path-like ids could
escape the intended cache directory, and pre-seeded traversal targets
could be treated as cache hits. Hashing the cache key removes any path
semantics from user-controlled ids while preserving stable cache reuse.
A regression test covers both out-of-root write prevention and ignoring
pre-seeded traversal cache files.

Constraint: The fix must preserve deterministic cache hits without trusting user-controlled path segments
Rejected: path.basename(image.id) | collapses distinct ids onto the same filename and leaves edge-case normalization concerns
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Any future cache layout change must keep user-controlled identifiers out of direct filesystem path composition
Tested: Custom local reproduction against current controller source; custom local validation against patched source; regression test added for desktop controller path handling
Not-tested: Upstream vitest/CI run in this workspace (desktop dependencies unavailable locally)

* Keep heterogeneous-agent cache regression aligned with runtime MIME behavior

The traversal regression test uses a data:text/plain URL under the desktop
node test environment, so the controller returns text/plain from the fetch
response headers. The expectation now matches the actual runtime behavior
instead of assuming the image/png fallback path.

Constraint: The regression should validate cache isolation rather than rely on an incorrect MIME fallback assumption
Rejected: Mock fetch in the regression test | adds extra indirection without improving the path traversal coverage
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep this test focused on path safety and cache-hit behavior; avoid coupling it to unrelated transport mocks unless the controller logic changes
Tested: Local patched-controller validation harness; static review against desktop vitest node environment behavior
Not-tested: Upstream vitest/CI run in this workspace (desktop dependencies unavailable locally)

* Keep heterogeneous-agent cache regression isolated to the temp test namespace

The first regression test used a fixed traversal target name under the shared
system temp directory. Switching that escape target to a unique name derived
from the test's temporary appStoragePath preserves the same out-of-root check
while avoiding accidental interaction with unrelated files under /tmp.

Constraint: The regression must still verify escape prevention beyond appStoragePath without touching shared fixed temp paths
Rejected: Remove the out-of-root assertion entirely | weakens coverage for the exact traversal behavior this PR is meant to guard
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep filesystem regressions hermetic; if a test needs to reason about escaped paths, derive them from per-test temp namespaces whenever possible
Tested: Static review of resolved path behavior before/after the change
Not-tested: Upstream vitest/CI run in this workspace (desktop dependencies unavailable locally)

---------

Co-authored-by: OpenAI Codex <codex@example.com>
2026-04-18 00:54:32 +08:00
YuTengjing 4d7ca56c21 🔨 chore: split test-app shards and deprecate isOnboarded (#13938) 2026-04-18 00:23:01 +08:00
Arvin Xu 80ae553f0f 🔨 chore: stream token-level deltas via --include-partial-messages (#13929)
 feat(cc-partial-messages): stream token-level deltas via --include-partial-messages

Enables Claude Code's --include-partial-messages flag so the CLI emits
token-level deltas wrapped in stream_event events. The adapter surfaces
these deltas as incremental stream_chunk events and suppresses the
trailing full-block emission from handleAssistant for any message.id
whose text/thinking has already been streamed.

Message-boundary handling is refactored into an idempotent
openMainMessage() helper so stepIndex advances on the first signal of a
new turn (delta or assistant), keeping deltas attached to the correct
step.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 23:12:47 +08:00
Arvin Xu 75b55edca1 feat: promote agent documents as primary workspace panel (#13924)
* ♻️ refactor: adopt Notebook list + EditorCanvas for agent documents

The agent working sidebar previously used a FileTree directory view and
a hand-rolled Markdown+TextArea editor with manual save. Agent documents
already back onto the canonical `documents` table via an FK, so they can
reuse the exact same rendering surface as Notebook.

- AgentDocumentsGroup: replace FileTree with a flat card list styled
  after Portal/Notebook/DocumentItem (icon + title + description + delete).
- AgentDocumentEditorPanel: drop the bespoke draft/save/segmented view
  logic; mount the shared <EditorCanvas documentId={doc.documentId}
  sourceType="notebook" /> inside an EditorProvider so auto-save and
  rich editing are handled by useDocumentStore.

*  feat: promote agent documents as the primary workspace panel

- Replace the agent-document sidebar with a Notebook-style list: pill
  filter (All/Docs/Web), per-item createdAt, globe icon for sourceType=web.
- Add a stable panel header "Resources" with a close button (small size,
  consistent with other chat header actions); no border divider.
- Wire clicks to the shared Portal Document view via openDocument(),
  retiring the inline AgentDocumentEditorPanel.
- Portal/Document/Header now resolves title directly from documentId
  via documentService.getDocumentById + a skeleton loading state.
- Portal top-right close icon switched to `X`.
- Layout: move AgentWorkingSidebar to the rightmost position; auto-collapse
  the left navigation sidebar while Portal is open (PortalAutoCollapse).
- Header: remove dead NotebookButton, drop the Notebook menu item; add a
  WorkingPanelToggle visible only when the working panel is collapsed.
- ProgressSection hides itself when the topic has no GTD todos.
- Builtin tool list removes Notebook; migrate CreateDocument Render and
  Streaming renderers to builtin-tool-agent-documents (notebook package
  kept for legacy rendering of historical tool calls).
- agent_documents list UI now reads from a separate SWR key
  (documentsList) so the agent-store context mapping doesn't strip
  documentId/sourceType/createdAt from the UI payload.
- i18n: add workingPanel.resources.filter.{all,documents,web},
  viewMode.{list,tree}, and the expanded empty-state copy; zh-CN
  translations seeded for preview.
- New local-testing reference: agent-browser-login (inject better-auth
  cookie for authenticated agent-browser sessions).

* update

* 🐛 fix: satisfy tsc strict i18next keys, remove duplicate getDocumentById, coerce showLeftPanel

* ♻️ refactor: graduate agent working panel out of labs
2026-04-17 23:04:59 +08:00
Arvin Xu 7981bab5bd 🐛 fix(auth): clear OIDC sessions when user signs out via better-auth (#13916)
🐛 fix(auth): clear current-browser OIDC session on sign-out

When a user signs out and signs back in as a different account,
the oidc-provider session cookie (_session) still references the
old accountId. The next /authorize silently reuses it, issuing
tokens for the wrong user.

Fix: add a POST /oidc/clear-session endpoint that:
1. Reads the _session cookie from the current request
2. Deletes the matching row in oidc_sessions (by primary key)
3. Expires the _session cookies in the response

The frontend logout action calls this endpoint *before* signOut()
while the better-auth session is still valid.

Only the current browser's OIDC session is affected — other
devices (desktop, CLI, mobile) keep their sessions intact.
2026-04-17 22:32:29 +08:00
Innei 03d2068a5d feat(onboarding): add feature flags and footer promotion pipeline (#13853)
*  feat(onboarding): enhance agent onboarding experience and add feature flags

- Added new promotional messages for agent onboarding in both Chinese and default locales.
- Updated HighlightNotification component to support action handling and target attributes.
- Introduced feature flags for agent onboarding in the configuration schema and tests.
- Implemented logic to conditionally display onboarding options based on feature flags and user state.
- Added tests for the onboarding flow and promotional notifications in the footer.

This update aims to improve the user experience during the onboarding process and ensure proper feature management through flags.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(home): add footer promotion pipeline with feature-flag gating

Extract resolveFooterPromotionState for agent onboarding vs Product Hunt promos.
Normalize isMobile boolean, refine HighlightNotification CTA layout, extend tests.

Made-with: Cursor

*  feat(locales): add agent onboarding promotional messages in multiple languages

Added new promotional messages for agent onboarding across various locales, enhancing the user experience with localized action labels, descriptions, and titles. This update supports a more engaging onboarding process for users globally.

Signed-off-by: Innei <tukon479@gmail.com>

* 💄 chore: refresh quick wizard onboarding promo

* 🐛 fix(chat): keep long mixed assistant content outside workflow fold

*  feat(onboarding): add agent onboarding feedback panel and service

LOBE-7210

Made-with: Cursor

*  feat(markdown-patch): add shared markdown patch tool with SEARCH/REPLACE hunks

Introduce @lobechat/markdown-patch util and expose patchDocument API on the
web-onboarding and agent-documents builtin tools so agents can apply
byte-exact SEARCH/REPLACE hunks instead of resending full document content.

*  feat(onboarding): prefer patchDocument for non-empty documents

Teach the onboarding agent (systemRole) and context engine
(OnboardingActionHintInjector) to prefer patchDocument over updateDocument
when SOUL.md or User Persona already has content, keeping updateDocument
reserved for the initial seed write or full rewrites.

* 🐛 fix(conversation): add rightActions to ChatInput component

Updated the AgentOnboardingConversation component to include rightActions in the ChatInput, enhancing the functionality of the onboarding conversation interface.

Signed-off-by: Innei <tukon479@gmail.com>

* Add specialized onboarding approval UI

* 🐛 fix(serverConfig): handle fetch errors in server config actions

Updated the server configuration action to include error handling for fetch failures, ensuring that the server config is marked as initialized when an error occurs. Additionally, modified the SWR mock to simulate error scenarios in tests.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(tests): update Group component tests with new data-testid attributes

Added data-testid attributes for workflow and answer segments in the Group component tests to improve test targeting. Adjusted the isFirstBlock property for consistency and ensured the component renders correctly with the provided props.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-17 21:14:27 +08:00
Zhijie He d6a47531c6 💄 style: add qwen3.6-flash/plus & pixverse-c1 support (#13923)
style: add qwen3.6-flash/plus & pixverse-c1 support
2026-04-17 19:46:49 +08:00
Arvin Xu 2298ad8ce1 chore(heterogeneous-agent): integrate heterogeneous agents with claude code (#13754)
* ♻️ refactor(acp): move agent provider to agencyConfig + restore creation entry

- Move AgentProviderConfig from chatConfig to agencyConfig.heterogeneousProvider
- Rename type from 'acp' to 'claudecode' for clarity
- Restore Claude Code agent creation entry in sidebar + menu
- Prioritize heterogeneousProvider check over gateway mode in execution flow
- Remove ACP settings from AgentChat form (provider is set at creation time)
- Add getAgencyConfigById selector for cleaner access
- Use existing agent workingDirectory instead of duplicating in provider config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat(acp): defer terminal events + extract model/usage per turn

Three improvements to ACP stream handling:

1. Defer agent_runtime_end/error: Previously the adapter emitted terminal
   events from result.type directly into the Gateway handler. The handler
   immediately fires fetchAndReplaceMessages which reads stale DB state
   (before we persist final content/tools). Fix: intercept terminal events
   in the executor's event loop and forward them only AFTER content +
   metadata has been written to DB.

2. Extract model/usage per assistant event: Claude Code sets model name
   and token usage on every assistant event. Adapter now emits a
   'step_complete' event with phase='turn_metadata' carrying these.
   Executor accumulates input/output/cache tokens across turns and
   persists them onto the assistant message (model + metadata.totalTokens).

3. Missing final text fix: The accumulated assistant text was being
   written AFTER agent_runtime_end triggered fetchAndReplaceMessages,
   so the UI rendered stale (empty) content. Deferred terminals solve this.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

🐛 fix(acp): eliminate orphan-tool warning flicker during streaming

Root cause:
LobeHub's conversation-flow parser (collectToolMessages) filters tool
messages by matching `tool_call_id` against `assistant.tools[].id`. The
previous flow created tool messages FIRST, then updated assistant.tools[],
which opened a brief window where the UI saw tool messages that had no
matching entry in the parent's tools array — rendering them as "orphan"
with a scary "请删除" warning to the user.

Fix:
Reorder persistNewToolCalls into three phases:
  1. Pre-register tool entries in assistant.tools[] (id only, no result_msg_id)
  2. Create the tool messages in DB (tool_call_id matches pre-registered ids)
  3. Back-fill result_msg_id and re-write assistant.tools[]

Between phase 1 and phase 3 the UI always sees consistent state: every
tool message in DB has a matching entry in the parent's tools array.

Verified: orphan count stays at 0 across all sampled timepoints during
streaming (vs 1+ before fix).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

🐛 fix(acp): dedupe tool_use + capture tool_result + persist result_msg_id

Three critical fixes to ACP tool-call handling, discovered via live testing:

1. **tool_use dedupe** — Claude Code stream-json previously produced 15+
   duplicate tool messages per tool_call_id. The adapter now tracks emitted
   ids so each tool_use → exactly one tool message.

2. **tool_result content capture** — tool_result blocks live in
   `type: 'user'` events in Claude Code's stream-json, not in assistant
   events. The adapter now handles the 'user' event type and emits a new
   `tool_result` HeterogeneousAgentEvent which the executor consumes to
   call messageService.updateToolMessage() with the actual result content.
   Previously all tool messages had empty content.

3. **result_msg_id on assistant.tools[]** — LobeHub's parse() step links
   tool messages to their parent assistant turn via tools[].result_msg_id.
   Without it, the UI renders orphan-message warnings. The executor now
   captures the tool message id returned by messageService.createMessage
   and writes it back into the assistant.tools[] JSONB.

Also adds vitest config + 9 unit tests for the adapter covering lifecycle,
content mapping, and tool_result handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat(acp): integrate external AI agents via ACP protocol

Adds support for connecting external AI agents (Claude Code and future
agents like Codex, Kimi CLI) into LobeHub Desktop via a new heterogeneous
agent layer that adapts agent-specific protocols to the unified Gateway
event stream.

Architecture:
- New @lobechat/heterogeneous-agents package: pluggable adapters that
  convert agent-specific outputs to AgentStreamEvent
- AcpCtr (Electron main): agent-agnostic process manager with CLI
  presets registry, broadcasts raw stdout lines to renderer
- acpExecutor (renderer): subscribes to broadcasts, runs events through
  adapter, feeds into existing createGatewayEventHandler
- Tool call persistence: creates role='tool' messages via messageService
  before emitting tool_start/tool_end to the handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: rename acpExecutor to heterogeneousAgentExecutor

- Rename file acpExecutor.ts → heterogeneousAgentExecutor.ts
- Rename ACPExecutorParams → HeterogeneousAgentExecutorParams
- Rename executeACPAgent → executeHeterogeneousAgent
- Change operation type from execAgentRuntime to execHeterogeneousAgent
- Change operation label to "Heterogeneous Agent Execution"
- Change error type from ACPError to HeterogeneousAgentError
- Rename acpData/acpContext variables to heteroData/heteroContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: rename AcpCtr and acp service to heterogeneousAgent

Desktop side:
- AcpCtr.ts → HeterogeneousAgentCtr.ts
- groupName 'acp' → 'heterogeneousAgent'
- IPC channels: acpRawLine → heteroAgentRawLine, etc.

Renderer side:
- services/electron/acp.ts → heterogeneousAgent.ts
- ACPService → HeterogeneousAgentService
- acpService → heterogeneousAgentService
- Update all IPC channel references in executor

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: switch CC permission mode to bypassPermissions

Use bypassPermissions to allow Bash and other tool execution.
Previously acceptEdits only allowed file edits, causing Bash tool
calls to fail during CC execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: don't fallback activeAgentId to empty string in AgentIdSync

Empty string '' causes chat store to have a truthy but invalid
activeAgentId, breaking message routing. Pass undefined instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use AI_RUNTIME_OPERATION_TYPES for loading and cancel states

stopGenerateMessage and cancelOperation were hardcoding
['execAgentRuntime', 'execServerAgentRuntime'], missing
execHeterogeneousAgent. This caused:
- CC execution couldn't be cancelled via stop button
- isAborting flag wasn't set for heterogeneous agent operations

Now uses AI_RUNTIME_OPERATION_TYPES constant everywhere to ensure
all AI runtime operation types are handled consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: split multi-step CC execution into separate assistant messages

Claude Code's multi-turn execution (thinking → tool → final text) was
accumulating everything onto a single assistant message, causing the
final text response to appear inside the tool call message.

Changes:
- ClaudeCodeAdapter: detect message.id changes and emit stream_end +
  stream_start with newStep flag at step boundaries
- heterogeneousAgentExecutor: on newStep stream_start, persist previous
  step's content, create a new assistant message, reset accumulators,
  and forward the new message ID to the gateway handler

This ensures each LLM turn gets its own assistant message, matching
how Gateway mode handles multi-step agent execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: fix multi-step CC execution and add DB persistence tests

Adapter fixes:
- Fix false step boundary on first assistant after init (ghost empty message)

Executor fixes:
- Fix parentId chain: new-step assistant points to last tool message
- Fix content contamination: sync snapshot of content accumulators on step boundary
- Fix type errors (import path, ChatToolPayload casts, sessionId guard)

Tests:
- Add ClaudeCodeAdapter unit tests (multi-step, usage, flush, edge cases)
- Add ClaudeCodeAdapter E2E test (full multi-step session simulation)
- Add registry tests
- Add executor DB persistence tests covering:
  - Tool 3-phase write (pre-register → create → backfill)
  - Tool result content + error persistence
  - Multi-step parentId chain (assistant → tool → assistant)
  - Final content/reasoning/model/usage writes
  - Sync snapshot preventing cross-step contamination
  - Error handling with partial content persistence
  - Full multi-step E2E (Read → Write → text)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: add orphan tool regression tests and debug trace

- Add orphan tool regression tests for multi-turn tool execution
- Add __HETERO_AGENT_TRACE debug instrumentation for event flow capture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: support image attachments in CC via stream-json stdin

- Main process downloads files by ID from cloud (GET {domain}/f/{fileId})
- Local disk cache at lobehub-storage/heteroAgent/files/ (by fileId)
- When fileIds present, switches to --input-format stream-json + stdin pipe
- Constructs user message with text + image content blocks (base64)
- Pass fileIds through executor → service → IPC → controller

Closes LOBE-7254

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: pass imageList instead of fileIds for CC vision support

- Use imageList (with url) instead of fileIds — Main downloads from URL directly
- Cache by image id at lobehub-storage/heteroAgent/files/
- Only images (not arbitrary files) are sent to CC via stream-json stdin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: read imageList from persisted DB message instead of chatUploadFileList

chatUploadFileList is cleared after sendMessageInServer, so tempImages
was empty by the time the executor ran. Now reads imageList from the
persisted user message in heteroData.messages instead.

Also removes debug console.log/console.error statements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update i18n

* 🐛 fix: prevent orphan tool UI by deferring handler events during step transition

Root cause: when a CC step boundary occurs, the adapter produces
[stream_end, stream_start(newStep), stream_chunk(tools_calling)] in one batch.
The executor deferred stream_start via persistQueue but forwarded stream_chunk
synchronously — handler received tools_calling BEFORE stream_start, dispatching
tools to the OLD assistant message → UI showed orphan tool warning.

Fix: add pendingStepTransition flag that defers ALL handler-bound events through
persistQueue until stream_start is forwarded, guaranteeing correct event ordering.

Also adds:
- Minimal regression test in gatewayEventHandler confirming correct ordering
- Multi-tool per turn regression test from real LOBE-7240 trace
- Data-driven regression replaying 133 real CC events from regression.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add lab toggle for heterogeneous agent (Claude Code)

- Add enableHeterogeneousAgent to UserLabSchema + defaults (off by default)
- Add selector + settings UI toggle (desktop only)
- Gate "Claude Code Agent" sidebar menu item behind the lab setting
- Remove regression.json (no longer needed)
- Add i18n keys for the lab feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: gate heterogeneous agent execution behind isDesktop check

Without this, web users with an agent that has heterogeneousProvider
config would hit the CC execution path and fail (no Electron IPC).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: rename tool identifier from acp-agent to claude-code

Also update operation label to "External agent running".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add CLI agent detectors for system tools settings

Detect agentic coding CLIs installed on the system:
- Claude Code, Codex, Gemini CLI, Qwen Code, Kimi CLI, Aider
- Uses validated detection (which + --version keyword matching)
- New "CLI Agents" category in System Tools settings
- i18n for en-US and zh-CN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: fix token usage over-counting in CC execution

Two bugs fixed:

1. Adapter: same message.id emitted duplicate step_complete(turn_metadata)
   for each content block (thinking/text/tool_use) — all carry identical
   usage. Now deduped by message.id, only emits once per turn.

2. Executor: CC result event contains authoritative session-wide usage
   totals but was ignored. Now adapter emits step_complete(result_usage)
   from the result event, executor uses it to override accumulated values.

Fixes LOBE-7261

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: gitignore cc-stream.json and .heterogeneous-tracing/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: untrack .heerogeneous-tracing/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: wire CC session resume for multi-turn conversations

Reads `ccSessionId` from topic metadata and passes it as `resumeSessionId`
into the heterogeneous-agent executor, which forwards it into the Electron
main-process controller. `sendPrompt` then appends `--resume <id>` so the
next turn continues the same Claude Code session instead of starting fresh.
After each run, the CC init-event session_id (captured by the adapter) is
persisted back onto the topic so the chain survives page reloads.

Also stops killing the session in `finally` — it needs to stay alive for
subsequent turns; cleanup happens on topic deletion or app quit.

* 🐛 fix: record cache token breakdown in CC execution metadata

The prior token-usage fix only wrote totals — `inputCachedTokens`,
`inputWriteCacheTokens` and `inputCacheMissTokens` were dropped, so the
pricing card rendered zero cached/write-cache tokens even though CC had
reported them. Map the accumulated Anthropic-shape usage to the same
breakdown the anthropic usage converter emits, so CC turns display
consistently with Gateway turns.

Refs LOBE-7261

* ♻️ refactor: write CC usage under metadata.usage instead of flat fields

Flat `inputCachedTokens / totalInputTokens / ...` on `MessageMetadata` are
the legacy shape; new code should put usage under `metadata.usage`. Move
the CC executor to the nested shape so it matches the convention the rest
of the runtime is migrating to.

Refs LOBE-7261

* ♻️ refactor(types): mark flat usage fields on MessageMetadata as deprecated

Stop extending `ModelUsage` and redeclare each token field inline with a
`@deprecated` JSDoc pointing to `metadata.usage` (nested). Existing readers
still type-check, but IDEs now surface the deprecation so writers migrate
to the nested shape.

* ♻️ refactor(types): mark flat performance fields on MessageMetadata as deprecated

Stop extending `ModelPerformance` and redeclare `duration` / `latency` /
`tps` / `ttft` inline with `@deprecated`, pointing at `metadata.performance`.
Mirrors the same treatment just done for the token usage fields.

*  feat: CC agent gets claude avatar + lands on chat page directly

Skip the shared createAgent hook's /profile redirect for the Claude Code
variant — its config is fixed so the profile editor would be noise — and
preseed the Claude avatar from @lobehub/icons-static-avatar so new CC
agents aren't blank.

* 🐛 fix(conversation-flow): read usage/performance from nested metadata

`splitMetadata` only scraped the legacy flat token/perf fields, so messages
written under the new canonical shape (`metadata.usage`, `metadata.performance`)
never populated `UIChatMessage.usage` and the Extras panel rendered blank.

- Prefer nested `metadata.usage` / `metadata.performance` when present; keep
  flat scraping as fallback for pre-migration rows.
- Add `usage` / `performance` to FlatListBuilder's filter sets so the nested
  blobs don't leak into `otherMetadata`.
- Drop the stale `usage! || metadata` fallback in the Assistant / CouncilMember
  Extra renders — with splitMetadata fixed, `item.usage` is always populated
  when usage data exists, and passing raw metadata as ModelUsage is wrong now
  that the flat fields are gone.

* 🐛 fix: skip stores.reset on initial dataSyncConfig hydration

`useDataSyncConfig`'s SWR onSuccess called `refreshUserData` (which runs
`stores.reset()`) whenever the freshly-fetched config didn't deep-equal the
hard-coded initial `{ storageMode: 'cloud' }` — which happens on every
first load. The reset would wipe `chat.activeAgentId` just after
`AgentIdSync` set it from the URL, and because `AgentIdSync`'s sync
effects are keyed on `params.aid` (which hasn't changed), they never re-fire
to restore it. Result: topic SWR saw `activeAgentId === ''`, treated the
container as invalid, and left the sidebar stuck on the loading skeleton.

Gate the reset on `isInitRemoteServerConfig` so it only runs when the user
actually switches sync modes, not on the first hydration.

*  feat(claude-code): wire Inspector layer for CC tool calls

Mirrors local-system: each CC tool now has an inspector rendered above the
tool-call output instead of an opaque default row.

- `Inspector.tsx` — registry that passes the CC tool name itself as the
  shared factories' `translationKey`. react-i18next's missing-key fallback
  surfaces the literal name (Bash / Edit / Glob / Grep / Read / Write), so
  we don't add CC-specific entries to the plugin locale.
- `ReadInspector.tsx` / `WriteInspector.tsx` — thin adapters that map
  Anthropic-native args (`file_path` / `offset` / `limit`) onto the shared
  inspectors' shape (`path` / `startLine` / `endLine`), so shared stays
  pure. Bash / Edit / Glob / Grep reuse shared factories directly.
- Register `ClaudeCodeInspectors` under `claude-code` in the builtin-tools
  inspector dispatch.

Also drops the redundant `Render/Bash/index.tsx` wrapper and pipes the
shared `RunCommandRender` straight into the registry.

* ♻️ refactor: use agentSelectors.isCurrentAgentHeterogeneous

Two callsites (ConversationArea / useActionsBarConfig) were reaching into
`currentAgentConfig(...)?.agencyConfig?.heterogeneousProvider` inline.
Switch them to the existing `isCurrentAgentHeterogeneous` selector so the
predicate lives in one place.

* update

* ♻️ refactor: drop no-op useCallback wrapper in AgentChat form

`handleFinish` just called `updateConfig(values)` with no extra logic; the
zustand action is already a stable reference so the wrapper added no
memoization value. Leftover from the ACP refactor (930ba41fe3) where the
handler once did more work — hand the action straight to `onFinish`.

* update

*  revert: roll back conversation-flow nested-shape reads

Unwind the `splitMetadata` nested-preference + `FlatListBuilder` filter
additions from 306fd6561f. The nested `metadata.usage` / `metadata.performance`
promotion now happens in `parse.ts` (and a `?? metadata?.usage` fallback at
the UI callsites), so conversation-flow's transformer layer goes back to
its original flat-field-only behavior.

* update

* 🐛 fix(cc): wire Stop to cancel the external Claude Code process

Previously hitting Stop only flipped the `execHeterogeneousAgent` operation
to `cancelled` in the store — the spawned `claude -p` process kept
running and kept streaming/persisting output for the user. The op's abort
signal had no listeners and no `onCancelHandler` was registered.

- On session start, register an `onCancelHandler` that calls
  `heterogeneousAgentService.cancelSession(sessionId)` (SIGINT to the CLI).
- Read the op's `abortController.signal` and short-circuit `onRawLine` so
  late events the CLI emits between SIGINT and exit don't leak into DB
  writes.
- Skip the error-event forward in `onError` / the outer catch when the
  abort came from the user, so the UI doesn't surface a misleading error
  toast on top of the already-cancelled operation.

Verified end-to-end: prompt that runs a long sequence of Reads → click
Stop → `claude -p` process is gone within 2s, op status = cancelled, no
error message written to the conversation.

*  feat(sidebar): mark heterogeneous agents with an "External" tag

Pipes the agent's `agencyConfig.heterogeneousProvider.type` through the
sidebar data flow and renders a `<Tag>` next to the title for any agent
driven by an external CLI runtime (Claude Code today, more later). Mirrors
the group-member External pattern so future provider types just need a
label swap — the field is a string, not a boolean.

- `SidebarAgentItem.heterogeneousType?: string | null` on the shared type
- `HomeRepository.getSidebarAgentList` selects `agents.agencyConfig` and
  derives the field via `cleanObject`
- `AgentItem` shows `<Tag>{t('group.profile.external')}</Tag>` when the
  field is present

Verified client-side by injecting `heterogeneousType: 'claudecode'` into
a sidebar item at runtime — the "外部" tag renders next to the title in
the zh-CN locale.

* ♻️ refactor(i18n): dedicated key for the sidebar external-agent tag

Instead of reusing `group.profile.external` (which is about group members
that are user-linked rather than virtual), add `agentSidebar.externalTag`
specifically for the heterogeneous-runtime tag. Keeps the two concepts
separate so we can swap this one to "Claude Code" / provider-specific
labels later without touching the group UI copy.

Remember to run `pnpm i18n` before the PR so the remaining locales pick
up the new key.

* 🐛 fix: clear remaining CI type errors

Three small fixes so `tsgo --noEmit` exits clean:

- `AgentIdSync`: `useChatStoreUpdater` is typed off the chat-store key, whose
  `activeAgentId` is `string` (initial ''). Coerce the optional URL param to
  `''` so the store key type matches; `createStoreUpdater` still skips the
  setState when the value is undefined-ish.
- `heterogeneousAgentExecutor.test.ts`: `scope: 'session'` isn't a valid
  `MessageMapScope` (the union dropped that variant); switch the fixture to
  `'main'`, which is the correct scope for agent main conversations.
- Same test file: `Array.at(-1)` is `T | undefined`; non-null assert since
  the preceding calls guarantee the slot is populated.

* 🐛 fix: loosen createStoreUpdater signature to accept nullable values

Upstream `createStoreUpdater` types `value` as exactly `T[Key]`, so any
call site feeding an optional source (URL param, selector that may return
undefined) fails type-check — even though the runtime already guards
`typeof value !== 'undefined'` and no-ops in that case.

Wrap it once in `store/utils/createStoreUpdater.ts` with a `T[Key] | null
| undefined` value type so callers can pass `params.aid` directly, instead
of the lossy `?? ''` fallback the previous commit used (which would have
written an empty-string sentinel into the chat store).

Swap the import in `AgentIdSync.tsx`.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-17 19:33:39 +08:00
Neko 3fb6b0d8e1 🐛 fix(app): right panel should use stableLayout, bump @lobehub/ui to 5.9.0 (#13920)
🐛 fix(app): right panel should use stableLayout, bump @lobehub/ui to 5.9.0
2026-04-17 19:11:45 +08:00
Arvin Xu 34b60e1842 🔨 chore: return full brief data in task activities (#13914)
*  feat: return full brief data in task activities (LOBE-7266)

The activity feed for tasks previously emitted a stripped `brief` row that
concatenated `resolvedAction` and `resolvedComment` and omitted everything
BriefCard needs (taskId, topicId, agentId, cronJobId, agents, actions,
artifacts, readAt, resolvedAt, etc.). Map the full `BriefItem` into each
activity row and reuse `BriefService.enrichBriefsWithAgents` to populate
the participant avatars. The CLI and prompt formatter now compose the
action + comment display string themselves.

* 🐛 fix: degrade gracefully when brief agent enrichment fails

getTaskDetail was calling BriefService.enrichBriefsWithAgents inside
Promise.all without a fallback, so a failure in the agent-tree lookup
would reject the whole request — a regression vs. the existing
.catch(() => []) pattern used by other activity reads in this method.
Fall back to agentless briefs on error so the task detail keeps
rendering.
2026-04-17 19:10:48 +08:00
LiJian 828175f8f0 🐛 fix: add the lost tools into manual agent runtime mode (#13918)
* fix: slove the manual mode cant use some builtin tools

* refactor: change the active skill tools from lobe-activtor to  lobe-skill tools

* fix: only inject the avaiable skill when use the auto mode

* fix: update the desktop tools skill

* fix: add the some test to ensure the builin tools will use in manual mode
2026-04-17 17:02:53 +08:00
Arvin Xu 316349ea06 💄 style: remove 'Management' from API Key tab title (#13919)
fix: remove 'Management' from API Key tab title
2026-04-17 16:30:35 +08:00
Innei 2f4fbd35d4 🐛 fix: show success status for tool calls with no return value (#13905)
* 🐛 fix: show success status for tool calls with no return value

When a tool call completes without returning content, the status indicator
was incorrectly showing a loading spinner instead of a success checkmark.
This fix passes the isToolCalling operation state to StatusIndicator to
correctly determine when a tool has finished executing.

https://claude.ai/code/session_01EBaKqzVTeEmrUXgFdNk7WH

* 🐛 fix(conversation): improve tool execution status handling

Updated the logic for determining tool execution states in both the Tool and Inspector components. The changes ensure that the status indicator accurately reflects when a tool is actively processing, even if no result is returned. This prevents misleading loading indicators and enhances user experience during tool interactions.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(DocumentHistoryDiff): correct JSX syntax for CircleLoading component

Removed unnecessary semicolon from CircleLoading component in DocumentHistoryDiff to ensure proper rendering. This minor fix enhances code clarity and maintains JSX standards.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(ModeSwitch.test): refactor tests to improve readability and performance

Updated the ModeSwitch test suite by removing unnecessary async/await patterns, simplifying the mock configuration, and ensuring consistent cleanup after each test. These changes enhance the clarity and efficiency of the test cases for the onboarding mode switch functionality.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 16:12:35 +08:00
Innei 669cb98c3d 🐛 fix(conversation): restore markdown animation for first assistant group block (#13904)
Made-with: Cursor
2026-04-17 14:46:58 +08:00
LiJian 2824c826bd 🐛 fix: should inject the user Locals Language into systemRole (#13911)
* fix: should inject the user Locals Language into systemRole

* fix: slove the ts

* fix: update the snapshot test

* fix: update the test.ts

* fix: test fixed
2026-04-17 14:12:37 +08:00
YuTengjing d658daa95d 🐛 fix: strip temperature/top_p for Claude Opus 4.7 (#13909) 2026-04-17 11:47:22 +08:00
YuTengjing d707f60365 feat: add Claude Opus 4.7 with xhigh effort tier (#13903) 2026-04-17 02:55:02 +08:00
Arvin Xu 91428ea0d2 🔨 chore: persist ccSessionId in topic metadata for CC multi-turn resume (#13902)
🐛 fix: persist ccSessionId in topic metadata for CC multi-turn resume

The renderer writes `ccSessionId` to topic metadata after each Claude Code
execution so the next turn can spawn `claude --resume <id>`, but the server
zod schema on `updateTopicMetadata` didn't list `ccSessionId`, so zod silently
stripped it — every turn started a fresh CC session and lost prior context.
2026-04-17 01:50:23 +08:00
LobeHub Bot 3471d2bf74 🚀 release: sync main branch to canary (#13900)
* 🔖 chore(release): release version v2.1.50 [skip ci]

* 📝 docs: Update changelog docs and release skills (#13897) 

* 🔨 chore: update .vscode/settings.json (#13894)

* 🐛 fix(builtin-tool-local-system): honor glob scope in local system tool (#13875)

Made-with: Cursor

* 📝 docs: Update changelog docs and release skills (#13897)

- Update changelog documentation format across all historical changelog files
- Merge release-changelog-style skill into version-release skill
- Update changelog examples with improved formatting and structure

Made-with: Cursor

---------

Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>

* 🐛 fix: resolve merge conflicts in sync main to canary

Restore canary versions of skill docs that were overwritten during
main-to-canary sync, keeping #13899 improvements intact.

---------

Co-authored-by: CanisMinor <i@canisminor.cc>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>
Co-authored-by: Innei <tukon479@gmail.com>
2026-04-17 00:35:29 +08:00
Innei d2197f4c30 ♻️ refactor(desktop): consolidate global shortcuts (LOBE-7181) (#13880)
* ♻️ refactor(desktop): consolidate global shortcuts and remove default showApp hotkey

- Add desktopGlobalShortcuts.ts as single source for Electron + renderer defaults
- Wire ShortcutManager and store to DEFAULT_ELECTRON_DESKTOP_SHORTCUTS
- Use DesktopHotkeyId for @shortcut; drop local shortcuts barrel
- Stop re-exporting DESKTOP_HOTKEYS_REGISTRATION from hotkeys

Fixes LOBE-7181

Made-with: Cursor

*  feat(desktop): introduce new stubs for business constants and types

- Added `@lobechat/business-const` and `@lobechat/types` packages to support workspace dependency resolution.
- Updated `package.json` and `pnpm-workspace.yaml` to include new stubs.
- Refactored imports in `index.ts` to utilize the new constants structure.
- Enhanced `desktopGlobalShortcuts.ts` with improved type definitions for hotkeys.

This change streamlines the management of constants and types across the desktop application.

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor(hotkeys): consolidate desktop global shortcut definitions (LOBE-7181)

Made-with: Cursor

*  feat(session, user): replace direct type imports with constants

- Updated session.ts to use constants for session types instead of direct imports from @lobechat/types.
- Updated user.ts to use a constant for the default topic display mode, enhancing consistency and maintainability.

This change improves code clarity and reduces dependencies on external type definitions.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-17 00:32:05 +08:00
Innei 35558cbea1 🐛 fix(desktop): prevent invalid proxy toggle saves (#13850)
* 🐛 fix(desktop): prevent invalid proxy toggle saves

* 🩹 fix: close proxy form ci gaps

*  style: enhance SaveBar component with updated styles and improved color variables

Signed-off-by: Innei <tukon479@gmail.com>

* 🩹 fix(test): increase ProxyForm test timeout and add explicit delay: null

CI runs with coverage instrumentation cause these form-interaction
tests to take ~4–6s each, exceeding the default 5000ms timeout.
Increase describe timeout to 10000ms and add { delay: null } to
all user.type() calls to keep them stable under coverage.

* 🩹 fix(test): resolve ProxyForm test type errors with user-event v14

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-17 00:30:50 +08:00
Neko fef6ed122a 🐛 fix(app): collapse button of agent working panel should be clickable (#13884) 2026-04-17 00:29:22 +08:00
lobehubbot 93603ae83b 🔖 chore(release): release version v2.1.51 [skip ci] 2026-04-16 15:42:58 +00:00
CanisMinor d87094236a 🚀 release: 20260416 (#13895)
# 🚀 LobeHub v2.1.50 (20260416)

**Release Date:** April 16, 2026\
**Since v2.1.49:** 107 commits · 101 merged PRs · 13 contributors

> This weekly release focuses on improving runtime stability and gateway
execution consistency, while making Home/Recents workflows faster to
navigate and easier to manage in daily use.

---

##  Highlights

- **Server-side Human Approval Flow** — Agent runtime now supports more
reliable approve/reject/reject-continue handling in gateway mode,
reducing stalled execution paths in long-running tasks. (#13829, #13863,
#13873)

- **Message Gateway End-to-End Hardening** — Gateway message flow, queue
handling, tool callback routing, and stop interruption behavior were
strengthened for better execution continuity. (#13761, #13816, #13820,
#13815)

- **Client Tool Execution in Gateway Mode** — Client-executor tools now
run more predictably across gateway and desktop callers, with improved
executor dispatch behavior. (#13792, #13790)

- **Home / Recents / Sidebar Upgrade** — Sidebar layout, custom sort,
recents operations, and profile actions were improved to reduce
navigation friction in active sessions. (#13719, #13812, #13723, #13739,
#13878, #13734)

- **Agent Workspace and Documents Expansion** — Working panel and agent
document workflows were expanded and polished for better day-to-day
agent operations. (#13766, #13857)

- **Provider and Model Compatibility Improvements** — Added GLM-5.1
support and refined model/provider edge-case handling, including schema
and error-path fixes. (#13757, #13806, #13736, #13740)

---

## 🏗️ Core Agent & Architecture

### Agent runtime and intervention lifecycle

- Added server-side human approval and improved runtime coordination
across approve/reject decision paths. (#13829, #13863)
- Improved interrupted-task handling and operation lifecycle consistency
to reduce half-finished runtime states. (#13714)
- Refined error classification and payload propagation so downstream
surfaces receive clearer actionable errors. (#13736, #13740)

### Execution model and dispatch behavior

- Introduced executor-aware runtime behavior to better separate
client/server tool execution semantics. (#13758)
- Improved tool/plugin resolution and manifest handling to avoid runtime
failures on malformed inputs. (#13856, #13840, #13807)

---

## 📱 Gateway & Platform Integrations

- Added message gateway support and strengthened queue/error behavior
for more stable cross-channel execution. (#13761, #13816, #13820)
- Improved gateway callback pipeline with protocol and API additions for
`tool_execute` / `tool_result`. (#13762, #13764, #13765)
- Improved bot/channel reliability and DM/slash handling in
Discord-related paths. (#13805, #13724)

---

## 🖥️ CLI & User Experience

- Improved CLI reliability across message/topic operations and
build/minify-related paths. (#13731, #13888)
- Added image-to-video options and improved command behavior for
generation workflows. (#13788)
- Improved desktop runtime behavior for remote fetch and Linux
notification urgency handling. (#13789, #13782)

---

## 🔧 Tooling

- Extracted gateway stream client into `@lobechat/agent-gateway-client`
to centralize protocol usage and reduce duplication. (#13866)
- Improved built-in tool coverage and runtime support, including GTD
server runtime and missing lobe-kb tools. (#13854, #13876)
- Updated skill and frontmatter consistency in workflow tooling.
(#13730)

---

## 🔒 Security & Reliability

- **Security:** Strengthened API key WS auth behavior and safer
serverUrl forwarding in gateway-related auth paths. (#13824)
- **Reliability:** Reduced runtime stalls by improving gateway
stop/interrupt and approval-state routing behavior. (#13815, #13863,
#13873)
- **Reliability:** Added defensive guards for malformed tool manifests
and non-string content edge cases. (#13856, #13753)

---

## 👥 Contributors

**101 merged PRs** from **13 contributors** across **107 commits**.

### Community Contributors

- @arvinxx - Runtime, gateway, and execution reliability improvements
- @Innei - Navigation, workflow UX, and desktop/CLI refinements
- @rdmclin2 - Sidebar, recents, and channel behavior updates
- @ONLY-yours - Tooling/runtime fixes and model execution compatibility
- @tjx666 - Model support and release/tooling maintenance
- @nekomeowww - Memory and search-path stability fixes
- @cy948 - CLI indexing and command flow fixes
- @octo-patch - Local system runtime edge-case fixes
- @djthread - Desktop runtime request reliability improvements
- @rivertwilight - Documentation and changelog updates
- @sudongyuer - Subscription/mobile support improvements
- @Zhouguanyang - Provider/model configuration correctness fixes
- @lobehubbot - Translation and maintenance automation support

---

**Full Changelog**: v2.1.49...v2.1.50
2026-04-16 23:40:02 +08:00
Innei f1d615fa9f feat(document): add history management and compare workflow (#13725)
* Add document history versioning and TRPC APIs

* 🩹 Improve document history patching for rekeyed editor nodes

* Refine PageEditor history timeline UI

* Enhance modal API documentation and update modal implementation guidelines. Introduce new modal components and migration notes for transitioning from legacy `@lobehub/ui` to `@lobehub/ui/base-ui`. Update version history localization for improved clarity in UI. Add new CompareModal components for document history comparison.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔥 chore(docs): remove document history tech spec

Made-with: Cursor

* Enhance document history management by introducing a 30-day limit for history queries and updating related APIs. Refactor history service methods to support new options for filtering history based on the saved date. Improve UI elements in the PageEditor history timeline for better user experience.

Signed-off-by: Innei <tukon479@gmail.com>

* Add document history management features and improve API integration

- Introduced constants for document history retention and limits.
- Updated document history service to compact history based on new retention limits.
- Refactored PageEditor to utilize constants for document history limits.
- Added new TRPC router for document history management.
- Enhanced JSON diffing capabilities for better patching of document history.

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor: sync document history schema and simplify history service

- Sync simplified document_history table from feat/document-history-db

- Remove version/storage_kind/payload/base_version, use editor_data + saved_at

- Rewrite pagination with composite (savedAt, id) cursor

- Update TRPC APIs from version-based to historyId-based

- Replace DocumentVersionControl with AutoSaveHint

- Add integration tests for history service

*  feat: add per-source document history retention limits

- autosave / manual: retain 20 entries each

- restore / system: retain 5 entries each

- trimHistoryBySource now deletes in batches of 100 to avoid unbounded overflow

- removed obsolete constants: PATCH_THRESHOLD, RETENTION_LIMIT, SNAPSHOT_INTERVAL

- added integration tests for large overflow trimming

*  add llm_call history source and queue-based snapshot for page agent

* 💄 restyle document history list to Notion timeline

* 💄 fix history timeline alignment, unify fonts and highlight current

*  feat(PageEditor): refine document history compare UI and date formatting

Made-with: Cursor

*  feat(editor): add validation for editor data and update related interfaces

- Introduced `isValidEditorData` function to validate editor data structure.
- Updated `GetHistoryItemOutput` and `DocumentHistoryItemResult` interfaces to allow `editorData` to be `null`.
- Modified `getDocumentEditorData` to return `null` for invalid editor data.
- Added integration tests to ensure proper handling of invalid editor data in document history service.
- Enhanced editor actions to prevent saving of invalid editor data.

Signed-off-by: Innei <tukon479@gmail.com>

* 💾 chore(database): split document history indexes

* Fix manual saves and optimize history item rendering

* 🌐 locale: add missing llm_call translation key in en-US file.json

Add pageEditor.history.saveSource.llm_call = \"AI Edit\" to match
the default locale and prevent raw i18n key from showing in the
history panel.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-16 23:24:28 +08:00
CanisMinor 29734eec23 📝 docs: update release changelog skills (#13899)
docs: add release changelog skills
2026-04-16 23:14:00 +08:00
Arvin Xu c046d042f5 feat: associate web crawl documents with agent documents (#13893)
*  feat: associate web crawl documents with agent documents

- Add `associate` method to AgentDocumentModel for linking existing documents
- Add `associateDocument` to AgentDocumentsService, TRPC router, and client service
- Update web browsing executor to associate crawled pages with agent after notebook save
- Add server-side crawl-to-agent-document persistence in webBrowsing runtime
- Add `findOrCreateFolder` to DocumentModel for folder hierarchy support
- Extract `DOCUMENT_FOLDER_TYPE` constant from hardcoded 'custom/folder' strings
- Add tests for associate, findOrCreateFolder, and service layer

Fixes LOBE-7242

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: log errors in web crawl agent document association

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: add onCrawlComplete callback to WebBrowsingExecutionRuntime

Replace monkey-patching of crawlMultiPages with a proper onCrawlComplete
callback in the runtime constructor options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: move document save logic into WebBrowsingExecutionRuntime

Replace onCrawlComplete callback with documentService dependency injection.
The runtime now directly handles createDocument + associateDocument internally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: pass per-call context to documentService via crawlMultiPages

Add WebBrowsingDocumentContext (topicId, agentId) as a parameter to
crawlMultiPages, which flows through to documentService methods. This
allows a singleton runtime with per-call context on the client side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: enforce document ownership in associate and match root folders by null parentId

- associate: verify documentId belongs to current user before creating link
- findOrCreateFolder: add parentId IS NULL condition for root-level lookup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 23:11:21 +08:00
Neko 13d1b011b7 🐛 fix(app): include working panel into Lab feature, minor fixes (#13889)
* 🐛 fix(app): include working panel into Lab feature, minor fixes

* 🐛 fix(app): conditional disabled.
2026-04-16 23:05:33 +08:00
CanisMinor 549735be7f 📝 docs: Update changelog docs and release skills (#13897)
* 🔨 chore: update .vscode/settings.json (#13894)

* 🐛 fix(builtin-tool-local-system): honor glob scope in local system tool (#13875)

Made-with: Cursor

* 📝 docs: Update changelog docs and release skills (#13897)

- Update changelog documentation format across all historical changelog files
- Merge release-changelog-style skill into version-release skill
- Update changelog examples with improved formatting and structure

Made-with: Cursor

---------

Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>
2026-04-16 22:24:48 +08:00
CanisMinor df524103e4 📝 docs: Update changelog docs and release skills (#13897)
- Update changelog documentation format across all historical changelog files
- Merge release-changelog-style skill into version-release skill
- Update changelog examples with improved formatting and structure

Made-with: Cursor
2026-04-16 22:22:35 +08:00
Innei e487bcd8a1 🐛 fix(builtin-tool-local-system): honor glob scope in local system tool (#13875)
Made-with: Cursor
2026-04-16 22:09:38 +08:00
YuTengjing dfc6000ecd 🔨 chore: update .vscode/settings.json (#13894) 2026-04-16 21:07:05 +08:00
lobehubbot 282415b886 🔖 chore(release): release version v2.1.50 [skip ci] 2026-04-16 11:29:10 +00:00
lobehubbot 94b6827580 Merge remote-tracking branch 'origin/main' into canary 2026-04-16 11:27:19 +00:00
Innei c1174d3eb8 👷 build(database): add document history schema (#13789)
#### 💻 Change Type

- [ ]  feat
- [ ] 🐛 fix
- [ ] ♻️ refactor
- [ ] 💄 style
- [x] 👷 build
- [ ] ️ perf
- [ ]  test
- [ ] 📝 docs
- [ ] 🔨 chore

#### 🔗 Related Issue

- None

#### 🔀 Description of Change

- Extract the document history database changes from the feature branch
onto a branch based on main.
- Add the document history migration, schema, relations, model, and
database tests only.
- Exclude UI, router, and service-layer changes so the PR stays focused
on the database layer.

#### 🧪 How to Test

- Run: cd packages/database && bunx vitest run --silent=passed-only
src/models/__tests__/document.test.ts
src/models/__tests__/documentHistory.test.ts
- [x] Tested locally
- [x] Added or updated tests
- [ ] No tests needed

#### 📸 Screenshots / Videos

| Before | After |
| ------ | ----- |
| N/A | N/A |

#### 📝 Additional Information

- This PR intentionally targets main because the database migration
needs to land on the release branch first.
2026-04-16 19:26:37 +08:00
Arvin Xu cb4ad01135 🐛 fix: fix minify cli (#13888)
* update

* update

* 🔧 chore: update CLI build command in electron-builder and ensure proper newline in package.json

* Changed the CLI build command from 'npm run build' to 'npm run build:cli' in electron-builder.mjs.
* Added a newline at the end of package.json for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Innei <tukon479@gmail.com>
2026-04-16 18:39:18 +08:00
Rdmclin2 2cfcd4a75f 🔨 chore: add ENABLE_BOT_IN_DEV swtich (#13883)
* chore: add  ENABLE_BOT_IN_DEV swtich

* chore: add explicit judge
2026-04-16 17:47:31 +08:00
LiJian 843248fb77 🐛 fix: add some lost lobe-kb builtin tools (#13876)
* feat: add some lost lobe-kb builtin tools

* feat: add the list files and get file detail

* feat: add the list files and get file detail

* fix: update the search limit
2026-04-16 17:08:22 +08:00
Arvin Xu 1476cd86ee ♻️ refactor: add backgroundColor to TaskParticipant and rename name to title (#13877)
* ♻️ refactor: add backgroundColor to TaskParticipant and rename name to title

Add backgroundColor field and rename name→title in TaskParticipant interface
to match agent avatar data. Add LobeAI fallback for inbox agent in
getAgentAvatarsByIds when avatar/title are missing.
2026-04-16 17:06:51 +08:00
Innei 7c8f721d6d 💾 chore(database): sync document history schema indexes 2026-04-16 16:48:15 +08:00
Rdmclin2 85227cf467 🐛 fix: recent delete (#13878)
* chore: update skills dir

* chore: remove unused recent fetch actions and components

* fix: recent delete functions

* chore: update comments
2026-04-16 16:42:50 +08:00
Innei d526b40b78 🐛 fix(deps): pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg
Picked from canary commit 9f61b58a29.
- Bump @react-pdf/renderer from ^4.3.2 to 4.4.1
- Pin @react-pdf/image to 3.0.4 via pnpm.overrides
2026-04-16 15:01:49 +08:00
Innei a7339bea13 🌐 chore(locale): update page editor placeholder to new AI command prompt (#13872)
Update `pageEditor.editorPlaceholder` from `Start writing your page. Press / to open the command menu` to `Press "/" for AI and commands.` across all supported locales and the default locale source.
2026-04-16 14:41:07 +08:00
Arvin Xu ab05020f62 🐛 fix: default execAgent approval mode to headless (#13873)
* 🐛 fix: default execAgent approval mode to headless

Backend execAgent calls should run headlessly by default since only
frontend scenarios require manual human approval. This prevents cron
jobs and other server-side triggers from unexpectedly waiting for
human intervention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add regression test for headless approval default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:05:53 +08:00
Arvin Xu 4203e32dc7 ♻️ refactor: createAgent uses agentModel.create directly (#13871)
* ♻️ refactor: createAgent uses agentModel.create directly

The createAgent router was still going through sessionModel.create,
which is a legacy path that doesn't pass all agent fields (like
agencyConfig) to the agents table. Switch to agentModel.create
which directly inserts into the agents table with full field support.

- Add CreateAgentSchema in types package for proper input validation
- Remove dependency on insertAgentSchema from database package
- Remove sessionId from CreateAgentResult

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🏷️ chore: mark session-based agent creation as deprecated

Add @deprecated JSDoc tags to the legacy session-based agent creation
path (session router, SessionService, SessionModel.create, session store,
insertAgentSchema). New code should use agent.createAgent / agentModel.create
directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: honor groupId when creating agents

Pass input.groupId as sessionGroupId to agentModel.create so that
agents created from a sidebar folder are correctly assigned to that group.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve type errors from createAgent refactor

- Remove sessionId fallback in AddAgent.tsx and ForkAndChat.tsx
- Use z.custom<T>() for agencyConfig and tts in CreateAgentSchema
  to match agentModel.create parameter types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:10:13 +08:00
LobeHub Bot 9583de88e3 🌐 chore: translate non-English comments to English in desktop-controller-tests (#13867)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 12:02:18 +08:00
LiJian 0699a0b5ce 🐛 fix: improve the skill execution error body back (#13868)
fix: improve the skill execution error body back
2026-04-16 11:43:01 +08:00
Arvin Xu dd81642d83 ♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package (#13866)
* ♻️ refactor: extract agent-stream into @lobechat/agent-gateway-client package

Move the Agent Gateway WebSocket client from src/libs/agent-stream/ into
a standalone workspace package at packages/agent-gateway-client/. This
eliminates the duplicate AgentStreamEvent type in apps/cli and provides
a single source of truth for the Gateway WS protocol types shared by
SPA, server, and CLI consumers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* add agent-gateway-client

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 11:25:32 +08:00
Arvin Xu f6c70210f2 ♻️ refactor(chat): remove reject-only button, unify to rejected_continue (#13865)
* ♻️ refactor(chat): remove reject-only button, unify to rejected_continue

Server-side `decision='rejected'` and `decision='rejected_continue'`
share the exact same code path — both surface the rejection to the
LLM as user feedback. Having a separate "reject only" button added UI
complexity without behavioural difference.

- Remove the "仅拒绝" button from InterventionBar popover; the single
  "拒绝" button now calls `rejectAndContinueToolCall` directly
- `rejectToolCalling` Gateway branch sends `rejected_continue` instead
  of `rejected` so all rejection paths use one decision value

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update ApprovalActions.tsx

*  feat(tool): add executors field to BuiltinToolManifest and dispatch page-agent to client

Add `executors?: ('client' | 'server')[]` to `BuiltinToolManifest` so
each builtin tool declares where it can run. The server-side dispatch
logic in `aiAgent/index.ts` now reads this field instead of hardcoding
per-identifier checks.

- `lobe-local-system`: `executors: ['client', 'server']` — runs on
  client via Electron IPC or server via Remote Device proxy
- `lobe-page-agent`: `executors: ['client']` — requires EditorRuntime,
  client-only
- Stdio MCP plugins still use the `customParams.mcp.type` heuristic
  (not manifest-driven)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:45:17 +08:00
Arvin Xu 8109bbbbc3 🐛 fix(gateway): route approve/reject via lab flag (#13863)
🐛 fix(gateway): route approve/reject via lab flag, not transient server op state

After the coordinator fix for `waiting_for_human` (#13860) the paused
`execServerAgentRuntime` op is marked `completed` client-side as soon
as the server emits `agent_runtime_end`. `startOperation` then runs
`cleanupCompletedOperations(30_000)`, which deletes any op completed
more than 30 seconds ago — so by the time the user sees the
InterventionBar and clicks approve/reject, the running (or recently
completed) server op is gone.

The previous `#hasRunningServerOp` check therefore kept returning
false against a live Gateway backend, flipping approve/reject into
the client-mode `internal_execAgentRuntime` branch and stranding the
server-side paused conversation.

Switch the helper to `#shouldUseGatewayResume`, which checks the same
`isGatewayModeEnabled()` lab flag used to route the initial send. The
signal now mirrors how the conversation was dispatched and survives
the op-cleanup window.

New regression test exercises the post-coordinator-fix state: the
paused `execServerAgentRuntime` op is explicitly `completed` before
the approve call runs, and we still expect the Gateway branch to
fire with `decision='approved'`.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:26:22 +08:00
Arvin Xu 1005f442d6 🐛 fix(gateway): clean up paused server op after human approve/reject (#13860)
* 🐛 fix(gateway): clean up paused server op after human approve/reject

In Gateway mode with userInterventionConfig.approvalMode='ask', the
paused execServerAgentRuntime op was never released — the loading
spinner kept spinning after the user approved, rejected, or
reject-and-continued, and reject-only silently did nothing on the
server.

- ToolAction.rejectToolCall now delegates to chatStore.rejectToolCalling
  so the Gateway resume op actually fires with decision='rejected';
  previously it only mutated local intervention state and the server's
  paused op waited forever.
- AgentRuntimeCoordinator treats waiting_for_human as end-of-stream so
  the coordinator emits agent_runtime_end when request_human_approve
  flips state, letting the client close the paused op via the normal
  terminal-event path.
- conversationControl adds #completeRunningServerOps as a fallback
  guard in the approve/reject/reject-continue Gateway branches — if
  the server-side signal is delayed or missing, the client still clears
  the orphan op before starting the resume op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(gateway): defer paused-op cleanup until resume starts successfully

If `executeGatewayAgent` failed (transient network/auth/server error),
the paused `execServerAgentRuntime` op was already marked completed
locally by the pre-call `#completeRunningServerOps`. Retries would
then see no running server op, miss `#hasRunningServerOp`, and fall
through to the non-Gateway client-mode path — while the backend was
still paused awaiting human input.

Snapshot the paused op IDs before the resume call and retire them
only inside the try block after `executeGatewayAgent` resolves. On
failure the running marker stays intact so a retry still lands on
the Gateway branch and can re-issue the resume.

The helper was renamed from `#completeRunningServerOps(context)` to
`#completeOpsById(ids)` to reflect the new contract: callers must
snapshot beforehand, not re-query at completion time (which would
incorrectly match the new resume op too).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(gateway): avoid double reject dispatch in reject-and-continue

Now that `rejectToolCall` delegates to `chatStore.rejectToolCalling`,
the chained `await get().rejectToolCall(...)` inside
`rejectAndContinueToolCall` fired a full halting reject before the
continue call. In Gateway mode that meant two resume ops on the same
tool_call_id (`decision='rejected'` followed by
`decision='rejected_continue'`) racing server-side; in client mode it
duplicated reject bookkeeping that `chatStore.rejectAndContinueToolCalling`
already handles internally.

Drop the chained call and fire `onToolRejected` inline so hook
semantics are preserved. `chatStore.rejectAndContinueToolCalling` is
now the single entry point for both the rejection persist and the
continue dispatch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:43:00 +08:00
Arvin Xu 2cf65e9fb3 💄 style: agent documents (#13857)
* improve style

* improve style
2026-04-16 01:05:27 +08:00
Arvin Xu 6636b35188 🐛 fix: drop manifests missing api before feeding ToolsEngine (#13856)
🐛 fix(toolEngineering): drop manifests missing `api` before feeding ToolsEngine

`ToolsEngine.convertManifestsToTools` calls `manifest.api.map(...)`
without a null check, so any manifest that is truthy but lacks a valid
`api` array crashes the entire tools build with "Cannot read properties
of undefined (reading 'map')". This takes down anything that touches
the tools pipeline on that agent — including TokenTag in ChatInput,
which is why users see the crash on the chat page load path.

Manifests are merged from 5 sources (installed plugins, builtin tools,
Klavis, LobeHub skills, caller-supplied extras), only some of which
filter falsy entries, and none validate `api`. Guard defensively at
the merge point and log the offending source + identifier so the
underlying bad data can be traced.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:04:55 +08:00
Arvin Xu 8475bc11fc 🐛 fix(builtin-tool-gtd): add server runtime for GTD tool (#13854)
*  feat(builtin-tool-gtd): add server runtime for GTD tool

Implement server-side execution runtime so the GTD tool works when
agents run in a pure server context (bot platforms, async task workers,
QStash workflows). Previously only the client executor existed, which
relied on `useNotebookStore` and `notebookService` and would break on
the server.

- `packages/builtin-tool-gtd/src/ExecutionRuntime/index.ts`: pure
  `GTDExecutionRuntime` class with an injected service interface,
  covering createPlan/updatePlan/createTodos/updateTodos/clearTodos
  and execTask/execTasks. Since server runtime has no stepContext,
  todo state is read from / written back to the Plan document's
  `metadata.todos` field.
- `src/server/services/toolExecution/serverRuntimes/gtd.ts`: factory
  wiring `DocumentModel` + `TopicDocumentModel` into the runtime and
  registering under `GTDIdentifier`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor(builtin-tool-gtd): share runtime logic between executor and server

Make the client executor a thin adapter over `GTDExecutionRuntime` so
all processing logic (todo reducer, plan CRUD flow, execTask state
builder, output formatting) lives in one place. Previously the server
runtime was a near-duplicate of the client executor.

- Expand `GTDRuntimeContext` with `currentTodos`, `messageId`, `signal`
  so both callers can thread their environment through:
  - client supplies `currentTodos` from stepContext / pluginState via
    `getTodosFromContext`, and `messageId` for execTask parentMessageId
  - server lets the runtime resolve todos from the plan document's
    metadata when `currentTodos` is not supplied
- Split service surface into `updatePlan` (user-facing: goal / desc /
  context — client routes through `useNotebookStore` to refresh SWR)
  vs `updatePlanMetadata` (silent todos sync — client stays on the
  raw `notebookService`)
- Runtime methods now return `BuiltinToolResult` (superset of
  `BuiltinServerRuntimeOutput`), so `stop: true` on execTask /
  execTasks is typed cleanly without `@ts-expect-error`

Net effect: `executor/index.ts` shrinks from 510 → 134 lines; the
server factory just maps models to the service interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:04:48 +08:00
LobeHub Bot 3bb4fd6046 🌐 chore: translate non-English comments to English in lambda-router-tests (#13838)
🌐 chore: translate non-English comments to English in lambda router tests

Translated all Chinese/CJK comments to English in 6 test files under
src/server/routers/lambda/__tests__/. Code logic and string literals
are unchanged; only explanatory comments were translated.

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 23:59:08 +08:00
Arvin Xu 9608494b0a 💄 style(chat): tighten execServerAgentRuntime loading copy (#13855)
💄 style(chat): tighten `execServerAgentRuntime` loading copy

Current text was trying to do too much in one line — status + two
separate user affordances — and read as an explanation, not a status.
Replaces it with a status-first line that mentions where the work is
happening and the single reassurance users actually need.

- EN: "Task is running in the server. You are safe to leave this page."
- zh-CN: "任务正在服务器运行,您可以放心离开此页面。"

Only en-US and zh-CN are edited; CI translates the rest from the
default file.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:42:37 +08:00
Innei bc7b798dbb 🐛 fix(conversation): improve workflow display when user intervention is pending (#13847)
* 🐛 fix(conversation): improve workflow display when user intervention is pending

Made-with: Cursor

* 🐛 fix(builtin-tool-activator): add ActivatedToolInfo typing to requestedTools for tsgo compat

requestedTools was inferred as `{ identifier, name }[]` which lacks the
`avatar` property required by `ActivatedToolInfo`, causing tsgo errors.
2026-04-15 23:30:34 +08:00
Arvin Xu 986bd2f7ec 🐛 fix(agent-runtime): fetch tool plugin from message_plugins for resumeApproval (#13852)
`messageModel.findById(parentMessageId)` only returns the row from the
`messages` table — the tool-call metadata (identifier / apiName /
arguments / type / toolCallId) lives in the separate `message_plugins`
table. The resumeApproval path was reading `(resumeParentMessage as any).plugin`
and `(resumeParentMessage as any).tool_call_id`, both always undefined,
which meant:

- Approved tool calls were dispatched with `identifier: undefined`,
  causing the server-side tool executor to throw
  `Builtin tool "undefined" is not implemented`. The follow-up LLM
  step could still describe success (it sees the user prompt + picks
  plausible output) but the tool message content is permanently the
  error string.
- The toolCallId mismatch guard was silently disabled because the
  stored value was always null → validation always passed regardless
  of what the client sent.

Fix: query `messagePlugins.findFirst` by message id, use the fetched
row for both the toolCallId equality check and the approvedToolCall
payload that the runtime dispatches.

Tests:
- Mock `db.query.messagePlugins.findFirst` with the plugin fields so
  existing asserts on `approvedToolCall.identifier`/`apiName` pass
  against real values.
- Move `tool_call_id` / identifier / apiName / arguments / type out of
  the mock `messages` row fixture into a separate `pendingToolPlugin`
  fixture that mirrors the actual DB layout.
- Flip the "toolCallId mismatch" guard test to mutate the plugin mock
  (not the message mock) — this is exactly the class of bug the fetch
  guards against, so the test would have masked it before.
- New guard test: throw when `messagePlugins.findFirst` returns
  undefined (stale message id, wrong user, etc.).

Discovered during E2E verification of LOBE-7152 approve flow — the
approve decision was flipping to the new op correctly but every tool
execution was failing with the "undefined" error.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:53:50 +08:00
Arvin Xu 843cb8f30b 🔨 chore: wire server-mode human approval through conversationControl (#13830)
 feat(chat): server-mode human approval via new Gateway op + resumeApproval

When the current agent runtime is Gateway-mode (execServerAgentRuntime),
approve / reject / reject_continue now start a **new** Gateway op carrying
a `resumeApproval` decision instead of resuming the paused op in place
over tRPC — mirroring the "interrupt + new op" pattern from LOBE-7142
(stop/interrupt). This sidesteps the stepIndex / executeStep early-exit
race that was blocking the in-place resume path and matches the Linear
spec for LOBE-7152. Client mode is unchanged.

### Client

- `conversationControl.ts`
  - `approveToolCalling` / `rejectToolCalling` / `rejectAndContinueToolCalling`:
    server-mode branch calls `executeGatewayAgent({ message: '',
    parentMessageId: toolMessageId, resumeApproval: { decision, ... } })`.
    The local runtime never spins up; the new op's `agent_runtime_end`
    clears loading.
  - `#hasRunningServerOp` replaces the old `#getServerOperationId` helper
    (we no longer need the paused op's id). Forwards scope/groupId/
    subAgentId from `ConversationContext` into the operation lookup so
    group/thread conversations correctly resolve their running server op
    — `operationsByContext` is keyed on the full `messageMapKey`.
- `gateway.ts` — `executeGatewayAgent` takes an optional `resumeApproval`
  and forwards it to `aiAgentService.execAgentTask`.
- `services/aiAgent.ts` — `ExecAgentTaskParams.resumeApproval` with new
  `ResumeApprovalParam` shape (decision + parentMessageId + toolCallId
  + optional rejectionReason).
- `gatewayEventHandler.ts` — kept the `toolMessageIds` branch that fetches
  pending tool messages on `tools_calling`.
- `services/agentRuntime/{type,index}.ts` — removed the short-lived
  `toolMessageId` / `reject_continue` additions; this flow no longer
  routes through `processHumanIntervention`.
- `store/chat/slices/operation/selectors.ts` — `getOperationsByContext` /
  `hasRunningOperationByContext` now take `MessageMapKeyInput` so scope/
  group/subAgent fields are honoured end-to-end.

### Server

- `ExecAgentSchema` / `InternalExecAgentParams.resumeApproval` — optional
  `{ decision, parentMessageId, rejectionReason?, toolCallId }`.
- `AiAgentService.execAgent`
  - `resumeApproval` implies resume semantics (skip user-message creation,
    reuse `parentMessageId` as the target tool message). Folded into a
    single `effectiveResume` flag so the existing resume branches apply.
  - Validates parent is a `role='tool'` message whose `tool_call_id`
    matches the request — guards stale / double-clicks.
  - Writes the decision to DB before `historyMessages` is fetched so the
    runtime sees the updated tool message on the first step:
    * `approved` → `intervention: { status: 'approved' }`
    * `rejected` / `rejected_continue` → tool content =
      "User reject this tool calling [with reason: X]",
      `intervention: { status: 'rejected', rejectedReason }`.
  - Branches initial runtime context:
    * `approved` → `phase: 'human_approved_tool'` + `approvedToolCall`
      payload rebuilt from the tool message plugin → runtime executes
      the tool.
    * `rejected` / `rejected_continue` → `phase: 'user_input'` with
      empty content → LLM re-reads history (now including the rejected
      tool) and responds. Both decisions share this path: the client
      split is only about optimistic writes and button UX; once the
      rejection is persisted there's nothing meaningful to differentiate
      server-side.

### Tests

- `conversationControl.test.ts` — rewrote the three server-mode blocks
  to spy `executeGatewayAgent` and assert the `resumeApproval` payload
  shape. Added a regression test covering group-scope lookup so dropping
  scope/groupId from `#hasRunningServerOp` breaks the suite.
- `execAgent.resumeApproval.test.ts` (new) — covers approved and the
  unified rejected branches (parameterized), the no-reason fallback, and
  the role/tool_call_id validation guards.

Relates to LOBE-7152.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:17:22 +08:00
Arvin Xu 75626de0b3 🐛 fix: forward serverUrl in WS auth for apiKey verification (#13824)
* 🐛 fix: forward serverUrl in WS auth for apiKey verification

The agent gateway verifies an apiKey by calling
\`\${serverUrl}/api/v1/users/me\` with the token, so \`serverUrl\` has to be
part of the WebSocket auth handshake. The device-gateway-client already
does this; \`lh agent run\` was missing it, producing
"Gateway auth failed: Missing serverUrl for apiKey auth".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔨 chore: bump cli to 0.0.7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:16:11 +08:00
Innei ad634daf32 🧹 chore(onboarding): remove builtin-agent-onboarding and consolidate prompts (#13825)
* 🧹 chore: remove builtin-agent-onboarding and consolidate web onboarding

- Merge agent system role into builtin-agents; colocate toolSystemPrompt in builtin-tool-web-onboarding
- Drop unused QuestionRenderer client bundle
- Gate onboarding footer switch/skip on AGENT_ONBOARDING_ENABLED for agent route

Made-with: Cursor

* 🧪 test: fix onboarding layout translation mock

* 🧪 test: align onboarding layout test with feature flag

* 🧪 test: type onboarding business const mock
2026-04-15 20:40:37 +08:00
Arvin Xu f99935e992 🐛 fix(agent-runtime): carry persisted assistant id into state.messages (#13841)
When `call_llm` pushed the assistant turn into `state.messages`, it
dropped the DB id even though the row was already persisted. The
downstream `request_human_approve` executor filters parent lookup on
`m.role === 'assistant' && m.id`, and the DB fallback query is not
reliably finding the just-written row on every topology — so when
human-approve fires on the fresh LLM turn the op errors out with
"No assistant message found as parent for pending tool messages".

Attach `assistantMessageItem.id` to the pushed message so the existing
in-memory lookup hits, and nextContext's `parentMessageId` and
`state.messages` agree on a single source of truth.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:02:40 +08:00
Neko 632a6383f0 feat(app): working panel, and many agent document feat (#13766) 2026-04-15 19:18:24 +08:00
LiJian 15fcce97c9 ♻️ refactor: add more tools in lobe-agent-manangerment(modify、update、delete) (#13842)
* feat: add more tools in lobe-agent-manangerment

* feat: add the ensureAgentLoaded to modify it

* feat: add the update prompt tools
2026-04-15 17:57:05 +08:00
Neko e5be1801a1 🐛 fix(userMemories,database): bm25 should escape more characters like < and > (#13843) 2026-04-15 17:30:32 +08:00
Innei 64fc6d4bbd feat(database): add document history table and update related models
- Introduced a new `document_histories` table to track changes made to documents, including fields for `editor_data`, `save_source`, and `saved_at`.
- Updated foreign key relationships to link `document_histories` with `documents` and `users`.
- Modified existing models and tests to accommodate the new document history functionality, including changes to pagination and retrieval methods.
- Removed the versioning system from documents in favor of a more flexible history tracking approach.

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-15 14:39:37 +08:00
LiJian 524e07540c 🐛 fix: update the builtin lobehub skill manifest (#13840)
* fix: update the lobehub skill manifest

* fix: remove the no use code

* fix: update the test
2026-04-15 13:24:57 +08:00
Arvin Xu 9f61b58a29 feat(agent-runtime): server-side human approval flow (#13829)
*  feat(agent-runtime): implement server-side human approval flow

Port the client-mode human approval executors (request_human_approve,
call_tool resumption, handleHumanIntervention) to the server agent
runtime so that execServerAgentRuntime can correctly pause on
waiting_for_human and resume on approve / reject / reject_continue.

- request_human_approve now creates one `role='tool'` message per pending
  tool call with `pluginIntervention: { status: 'pending' }` and ships
  the `{ toolCallId → toolMessageId }` mapping on the `tools_calling`
  stream chunk.
- call_tool gains a `skipCreateToolMessage` branch that updates the
  pre-existing tool message in-place (prevents duplicate rows / parent_id
  FK violations that show up as LOBE-7154 errors).
- AgentRuntimeService.handleHumanIntervention implements all three
  paths: approve → `phase: 'human_approved_tool'`; reject → interrupted
  with `reason: 'human_rejected'`; reject_continue → `phase: 'user_input'`.
- ProcessHumanIntervention schema carries `toolMessageId` and a new
  `reject_continue` action; schema remains permissive (handler no-ops on
  missing toolMessageId) to keep legacy callers working.

Fixes LOBE-7151

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): address LOBE-7151 review (P1 reject_continue, P2 duplicate tool msg)

P1 — reject_continue with remaining pending tools must NOT resume the LLM.
Previously `handleHumanIntervention` kept `status='waiting_for_human'` but
returned `nextContext: { phase: 'user_input' }`, which `executeStep` would
hand to `runtime.step` immediately, breaking batch semantics. Now when
other tools are still pending, the rejection is persisted but no context
is returned; the `user_input` continuation only fires when this is the
last pending tool.

P2 — request_human_approve was pushing an empty placeholder
`{ role: 'tool', tool_call_id, content: '' }` into `newState.messages`
to "reflect" the newly-created pending DB row. On resume, the `call_tool`
skip-create path appends the real tool result, leaving two entries for
the same `tool_call_id` in runtime state. The downstream short-circuit
(`phase=human_approved_tool` → `call_tool`) doesn't consult
state.messages, so the placeholder was unused cost. Removed.

Also fixes a TS 2339 in the skipCreateToolMessage test where
`nextContext.payload` is typed `{}` and needed an explicit cast.

Tests: 99 pass (82 RuntimeExecutors + 17 handleHumanIntervention), type-check clean.
Verified end-to-end via the human-approval eval — it now exercises a
multi-turn retry path (LLM calls the gated tool twice) and both
approvals resolve cleanly through to `completionReason=done`.

Relates to LOBE-7151

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* pin @react-pdf/renderer

* 🐛 fix(deps): pin @react-pdf/image to 3.0.4 to avoid privatized @react-pdf/svg

@react-pdf/image@3.1.0 (auto-resolved via layout@4.6.0 ← renderer@4.4.1)
declares `@react-pdf/svg@^1.1.0` as a dependency, but the svg package was
unpublished/made private on npm (returns 404). CI installs blow up with
ERR_PNPM_FETCH_404.

Upstream issue: https://github.com/diegomura/react-pdf/issues/3377

Pin image to 3.0.4 (the last release before the broken svg dep was
introduced) via pnpm.overrides until react-pdf publishes a fix.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:07:06 +08:00
Arvin Xu f12cf8f2ea 🐛 fix: fail fast when tool/assistant message persist hits a missing parent (#13828)
* 🐛 fix: fail fast when tool/assistant message persist hits a missing parent

When a conversation parent was deleted mid-operation (LOBE-7154), the
runtime was silently swallowing the parent_id FK violation in three tool
persist paths and continuing with a stale parentMessageId. The next LLM
call hit the same FK without context, surfacing as a raw SQL error to
the user after burning several LLM + tool call round trips.

Changes

- packages/types: add AgentRuntimeErrorType.ConversationParentMissing
- new messagePersistErrors.ts helper: FK detection + structured error
  constructor + persist-fatal marker (keeps RuntimeExecutors smaller)
- RuntimeExecutors:
  - call_tool: publish error event + re-throw on persist failure;
    outer catch propagates when persist-fatal
  - call_tools_batch: same, mark so the per-tool outer catch doesn't
    swallow and fall back to the already-deleted parent
  - resolve_aborted_tools: same pattern
  - call_llm: preflight parent existence via findById so we fail before
    the LLM call instead of after
- tests: replace old swallow-on-fail expectations, add LOBE-7158 cases
  for each executor plus focused unit tests for the helper module

Fixes LOBE-7158

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 chore: publish normalized ConversationParentMissing on persist failure

Review feedback on LOBE-7158: the three persist catches were emitting
the raw DB exception as a stream `error` event before normalizing it.
Clients treat `error` events as terminal and surface `event.data.error`
directly, so the raw SQL text leaked to users and ended the stream
before the typed `ConversationParentMissing` throw could propagate.

Move normalization ahead of the publish in call_tool, call_tools_batch,
and resolve_aborted_tools so the stream event always carries the
intended business error. Add a regression assertion on the
call_tool FK test that the error event's `errorType` is
`ConversationParentMissing` and no `Failed query` text leaks through.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:27:01 +08:00
Arvin Xu 1a98e1b5aa 💄 style(nav-panel): remove nav panel content switch animation (#13814)
Drop the `motion/react` slide + fade transition on NavPanel content
switches (e.g. navigating from `/` to `/agent`). The new content now
renders directly without the 0.28s x-translate animation.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 01:10:37 +08:00
Innei b4fc85b57b 💄 style(proxy-settings): sticky pill SaveBar + instant enable toggle (#13821)
* 🔖 chore(release): release version v2.1.49 [skip ci]

* 💄 style(proxy-settings): sticky pill SaveBar + instant enable toggle

- Split enableProxy into instant-apply (no save required)
- Floating pill SaveBar fixed bottom-center, visible only when dirty
- Test connection feedback moved to toast (@lobehub/ui)
- Refresh style guidance: prefer createStaticStyles + cssVar

Fixes LOBE-7071

* 🐛 fix(proxy-settings): rollback enable toggle on save failure, preserve in-progress edits

---------

Co-authored-by: lobehubbot <i@lobehub.com>
2026-04-15 00:05:00 +08:00
Rdmclin2 fd0d846975 feat: support layout custom sort and fix copy (#13812)
* fix: menu locale keys

* feat: support resort sidebar

* feat: add lock to middle messages

* feat: add memory menu and default hidden

* fix: lint error

* fix: legacy secion order

* chore: add test cases

* chore: remove top zone

* feat: custom sidebar reorder

* chore: fix sidebar items
2026-04-14 23:49:47 +08:00
Arvin Xu 41efd16bba 🔨 chore: update cli version (#13822)
update cli version
2026-04-14 23:37:28 +08:00
Arvin Xu f6081c9914 🔨 chore: add headless approval and apiKey WS auth to lh agent run (#13819)
 feat: add headless approval and apiKey ws auth to `lh agent run`

Two fixes so `lh agent run` works end-to-end against the WebSocket agent
gateway when the user is authenticated via LOBEHUB_CLI_API_KEY.

- Default to `userInterventionConfig: { approvalMode: 'headless' }` when
  running the agent from the CLI. Without this flag the runtime waits
  for human tool-call approval and local-device commands hang forever.
  Users who want interactive approval can pass `--no-headless`.
- Pass `tokenType` (`jwt` | `apiKey`) in the WebSocket auth handshake so
  the gateway knows how to verify the token. Previously the CLI sent
  only the raw token value and the gateway assumed JWT, rejecting valid
  API keys.

Fixes LOBE-6939

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:28:01 +08:00
Arvin Xu d6f11f80b6 🐛 fix(agent-runtime): harden classifyLLMError so it never masks the original provider error (#13774)
* 🐛 fix(agent-runtime): harden classifyLLMError so it never masks the original provider error

Production traces across multiple providers (openrouter, openai, google)
surface a single opaque error — `e.trim is not a function` with
`errorType: 'unknown'` — hiding whatever the upstream actually returned.

Root cause: `normalizeCode` / `normalizeErrorType` assumed their input is
always `string | undefined` (matching the TypeScript signature), but real
provider error objects frequently carry a numeric `code` (HTTP status) or
a structured object in `errorType`. `value?.trim()` short-circuits only
on null/undefined, so a truthy non-string turns into a TypeError that
the outer catch records as the "final" error, erasing the upstream one.

Fixes:
- Guard `normalizeCode` / `normalizeErrorType` on `typeof value ===
  'string'`, widen parameter type to `unknown`.
- Wrap the whole `classifyLLMError` in a try/catch that falls back to a
  conservative `stop` decision and preserves the best-effort message of
  the ORIGINAL error. A classifier that throws is worse than a
  classifier that's wrong — it must never shadow the real failure.
- `bestEffortMessage` swallows property-access errors (hostile Proxy
  etc.) to guarantee the fallback itself can't throw.

Regression tests cover: numeric `code`, structured `errorType`, nested
OpenAI-SDK-shaped `error.error.code`, and a hostile Proxy that throws on
every property access.

This is a forcing function for root-cause diagnosis: after this lands,
the real upstream errors behind the 'e.trim' mask will finally surface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove fallback warning in classifyLLMError

Removed console warning for classification failure.

* 🐛 fix(agent-runtime): treat numeric provider code as status fallback

Bare HTTP proxies sometimes surface the HTTP status ONLY as a numeric `code`
on the error object (no `status`/`statusCode`, no digits in the message).
After widening `normalizeCode` to require `typeof === 'string'`, those numeric
codes were dropped entirely and auth/permission failures fell through to
retry — wasting the full retry budget on permanent errors.

Forward numeric `raw.code` / `nested?.code` / `nestedError?.code` into the
status chain (after the real status/statusCode lookups, before the
message-digit extractor) so classifyKind still maps 401/403 → stop and
429/5xx → retry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:23:21 +08:00
Rdmclin2 1c75686b70 🐛 fix: gateway typing error (#13820)
fix: gateway typing error
2026-04-14 23:15:41 +08:00
Arvin Xu 7e89fa782d 🐛 fix: detect truncated tool_calls arguments in builtin tools (#13817)
* 🐛 fix: detect truncated tool_call arguments in builtin tools

When an LLM hits max_tokens mid tool_call, the arguments JSON is
truncated. The previous flow passed `{}` to the tool, which returned a
generic "required field missing" error; the model re-tried with the same
payload and the truncation repeated — one observed trace burned 17 min
and $2.46 on 5 blind retries.

Detect structural truncation (unclosed braces/brackets/strings) in
BuiltinToolsExecutor before schema validation, and return a dedicated
TRUNCATED_ARGUMENTS error telling the model to reduce payload size or
raise max_tokens instead of retrying.

Fixes LOBE-7148

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 chore: echo raw arguments string and reject all unparseable JSON

Two improvements based on review:

- Append the received arguments string to the error content so the model
  can verify the payload is exactly what it produced (stops it from
  blaming upstream or guessing what went wrong).
- Treat ANY unparseable non-empty argsStr as an error (new code
  INVALID_JSON_ARGUMENTS), not just truncation. The previous fallback
  of passing `{}` to the tool produced generic "missing field" errors
  that hid the real cause. Empty argsStr still falls through to `{}`
  for tools that take no parameters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:50:49 +08:00
Arvin Xu 18bc2716b2 🔨 fix: wire Gateway-mode stop via direct tRPC interrupt (#13815)
*  feat: wire Gateway-mode stop button to WS interrupt

Frontend half of [LOBE-7142](https://linear.app/lobehub/issue/LOBE-7142)
— the stop button previously silently failed in Gateway mode because:

1. `stopGenerateMessage` only filtered `execAgentRuntime`, so
   `execServerAgentRuntime` ops (Gateway) were skipped.
2. Even if the local op got cancelled, nothing bridged the cancel to
   the server-side agent loop running behind the Agent Gateway WS.

## Changes

**`conversationControl.ts::stopGenerateMessage`** — extend the type
filter to include both op types so both client-side and Gateway-mode
runs are cancelled from the same entry point.

**`gateway.ts::executeGatewayAgent` + `reconnectToGatewayOperation`** —
register an `onOperationCancel` handler on the local `gatewayOpId` that
forwards the server-side operation id to `interruptGatewayAgent(...)`,
which sends `{ type: 'interrupt' }` over the Agent Gateway WS. The
closure cleanly resolves the "local op id vs server op id" mapping —
no metadata lookup needed.

**`operation/actions.ts::cancelOperation`** — `isAborting` flag was
gated on `execAgentRuntime`. Extend to `execServerAgentRuntime` too so
the UI loading state transitions out immediately on Gateway-mode stop,
without waiting for the round-trip `session_complete` from the server.

## What this doesn't do (follow-ups)

- **Backend**: new `POST /api/agent/interrupt` route + Redis LPUSH
  (LOBE-7145). Without it, the WS interrupt reaches Agent Gateway but
  never gets forwarded to cloud.
- **Agent loop**: `AgentRuntimeService.executeStep` LPOP polling of the
  interrupt key (LOBE-7146). Without it, the state never flips to
  `interrupted` server-side.
- **Agent Gateway DO** (external repo): `_forwardInterrupt` HTTP POST
  from the WS interrupt handler (LOBE-7147).

With only this PR merged, clicking stop will clear the local UI state
and send the WS frame correctly — the server-side loop keeps running
until those three are merged too.

## Tests

- `conversationControl.test.ts`: +1 — stopGenerateMessage cancels
  `execServerAgentRuntime`, invokes the onCancel handler, sets
  `isAborting: true`.
- `gateway.test.ts`: +1 — `executeGatewayAgent` registers a handler
  against the local opId, handler invokes `interruptGatewayAgent`
  with the server opId.

All 123 touched-slice tests pass; type-check clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔨 chore: switch Gateway stop to direct tRPC instead of WS roundtrip

Rewiring only — no new behaviour on top of the previous commit. See
the discussion in PR #13815 for the full reasoning.

TL;DR the WS-based path (client → Agent Gateway WS → DO forwards
HTTP → cloud route → Redis LPUSH → loop LPOP) has the same end-effect
as the tRPC-direct path (client → tRPC → AgentRuntimeService
.interruptOperation → DB state flip), except:

- the tRPC path is one hop instead of three
- the tRPC path reuses infrastructure that's *already on canary* —
  `aiAgentService.interruptTask` → `AiAgentService.interruptTask` →
  `AgentRuntimeService.interruptOperation` → `coordinator.saveAgentState`
  with status='interrupted' — and the existing step-boundary polling
  in `executeStep` (AgentRuntimeService.ts:474, 565) already picks it up
- zero new server code required; zero Agent Gateway (external repo)
  coordination required

The only reason the WS path was in the original spec (LOBE-7142) was
symmetry with the Phase 6.4 tool_execute/tool_result path, but
`interrupt` is a one-shot control signal, not stream data — there's
no actual benefit to routing it through the same channel. Mid-step
abort would require threading an AbortSignal into `runtime.step(...)`,
which WS doesn't help with either.

Closes out the need for LOBE-7145 / LOBE-7146 / LOBE-7147.

Changes:
- `gateway.ts`: both `executeGatewayAgent` and
  `reconnectToGatewayOperation` register the cancel handler against
  the local op id, but the handler body now calls
  `aiAgentService.interruptTask({ operationId: serverOpId })` via
  tRPC instead of `this.interruptGatewayAgent(serverOpId)` (which sent
  the WS interrupt frame).
- `gateway.test.ts`: adjust the one new test case to verify the
  tRPC call rather than the WS-path spy; add `interruptTask` to the
  `aiAgentService` mock.

`AgentStreamClient.sendInterrupt()` and `interruptGatewayAgent()` are
kept as-is — public API, might be useful elsewhere. Just not called
from the cancel handler anymore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:41:45 +08:00
Rdmclin2 636a3b77c3 🐛 fix: message gateway queue error (#13816)
* fix: gateway sync

* fix: skip  error connection

* feat: add disconnect all &  MESSAGE_GATEWAY_ENABLED env vairable

* chore: add gateway test case

* chore: clean lobehub connnections when switch to message gateway

* chore: optimize disconnect all

* chore: disconnect gateway connnections when using lobehub gateway

* chore: clean up exsiting gateway connections after reconnect and avoid gateway callback when not enabled
2026-04-14 22:10:17 +08:00
Arvin Xu c70ac84da7 feat: support run client tools in agent gateway mode (#13792)
*  feat: receive and execute executor=client tools on desktop Electron

Frontend half of LOBE-7076 (Phase 6.4). Pairs with server PR #13790,
which adds the `clientRuntime` signal + `hasClientExecutor` gate so
`local-system` and stdio MCP can enter the manifest for desktop callers.

Data flow, client side:

  Agent Gateway WS
     └─ tool_execute event ──► AgentStreamClient
            └─ 'agent_event' ──► gatewayEventHandler (case 'tool_execute')
                    └─ internal_executeClientTool (fire-and-forget)
                          ├─ parse args → params
                          ├─ mark pendingClientToolExecutions[toolCallId]
                          ├─ dispatch: builtin → invokeExecutor,
                          │            else   → mcpService.invokeMcpToolCall
                          ├─ clear pending
                          └─ AgentStreamClient.sendToolResult(...)
                                └─ WS → /api/agent/tool-result → LPUSH
                                       → server BLPOP unblocks → loop continues

Key guarantees:

- `internal_executeClientTool` never throws; ALL error paths (parse
  failure, no executor match, thrown executor, missing connection, MCP
  error) still call `sendToolResult({ success: false, error })`. The
  server's BLPOP must never hang on a silent client.
- `case 'tool_execute'` uses `void`, not `await`. A long-running tool
  must not block subsequent `stream_chunk` / `tool_end` events on the
  same WebSocket.
- UI loading state is kept separate from `toolCallingStreamIds` (the
  LLM-streaming animation) via a dedicated
  `pendingClientToolExecutions: Record<toolCallId, true>` map, so a
  renderer can show a distinct "running on device" indicator without
  entangling existing selectors.

Client → server signal:

`executeGatewayAgent` now passes `clientRuntime: isDesktop ? 'desktop' : 'web'`
so the server knows this Electron caller can receive `tool_execute`.

Tests: 39 new cases across AgentStreamClient / internal_executeClientTool
/ gatewayEventHandler covering success, error, MCP fallback, pending
state lifecycle, and fire-and-forget semantics. 148 total in affected
suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: pass server operationId to tool_result dispatch (operationId mismatch)

The gateway event handler received `tool_execute` events but the resulting
`internal_executeClientTool` call looked up `gatewayConnections` by the
*local* operation id (e.g. `op_8chrnd`) instead of the *server-side*
operation id (e.g. `op_1776171452938_...`) the WS connection is actually
keyed on. `conn` was therefore always `undefined`, the early-return in
`send(...)` swallowed the response, and the server's BLPOP waiter timed
out after 60 s.

This was reproducible on canary E2E: server logs showed
`dispatching client tool lobe-local-system/readLocalFile` followed by
`client tool ... timed out after 60027ms`, with no outbound `tool_result`
frame ever reaching the Agent Gateway.

Fix: thread a distinct `gatewayOperationId` through
`createGatewayEventHandler` and use it for the `case 'tool_execute'`
dispatch. The existing `operationId` (used for `dispatchContext` →
`internal_dispatchMessage` keying) is untouched. Both `executeGatewayAgent`
and `reconnectToGatewayOperation` now pass the server id explicitly; when
a caller omits it, it falls back to the local `operationId` for backwards
compatibility.

Verified live on canary: WS now shows
`[in] tool_execute` → `[out] tool_result success=true content=...` and
the agent returns the real local-file contents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:30:13 +08:00
LiJian 116495bd1e 🐛 fix: slove the execAgents tools exec types not correct (#13807)
* fix: slove the execAgents tools exec types not correct

* fix: should inject source:discovery when tools type is lost

* fix: delete the source inject test
2026-04-14 17:51:08 +08:00
LiJian 922f7ace41 🐛 fix: fixed the when call saveCreds the bad request problem (#13809)
* fix: fixed the when call saveCreds the bad request problem

* fix: add the empty kv checked
2026-04-14 17:51:00 +08:00
YuTengjing b369c53bda 🐛 fix(model-bank): disable GLM-5.1 built-in search in LobeHub (#13806) 2026-04-14 17:05:42 +08:00
René Wang 5ecccf4b9e 📝 docs: add April 13 weekly changelog (#13808) 2026-04-14 17:02:10 +08:00
Rdmclin2 f9fbd45fee feat: discord support slash commands and DM (#13805)
* fix: slack not respond to text commands

* feat: add slack slash commands instructions

* chore: add slack validate in test connections

* chore: update slack docs

* chore: remove text commands for slack
2026-04-14 16:48:16 +08:00
LiJian 0b490a7268 🐛 fix: execAgent should get builtin discoverable tools into manifests (#13804)
* fix: execAgent should get all tools manifests

* fix: should add the tools source into payload source

* fix: add the discoverable tools into tools enginer

* fix: update the test, should include the discoverable tools
2026-04-14 16:07:49 +08:00
Innei a9c5badb80 ♻️ refactor(navigation): stable navigate hook and imperative routing (#13795)
*  fix: implement stable navigation hook and refactor navigation handling

- Introduced `useStableNavigate` hook to provide a stable `navigate` function that can be used across the application.
- Refactored components to utilize the new stable navigation approach, replacing direct access to the navigation function from the global store.
- Updated `NavigatorRegistrar` to sync the `navigate` function into a ref for consistent access.
- Removed deprecated navigation handling from various components and actions, ensuring a cleaner and more maintainable codebase.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: refactor navigation handling to prevent state mutation

- Updated navigation reference handling in the global store to use a dedicated function for creating navigation refs, ensuring that the initial state is not mutated by nested writes.
- Adjusted tests and components to utilize the new navigation ref creation method, enhancing stability and maintainability of navigation logic.

Signed-off-by: Innei <tukon479@gmail.com>

*  test: mock Electron's net.fetch in unit tests

- Added a mock for Electron's net.fetch in the AuthCtr and BackendProxyProtocolManager tests to ensure proper handling of remote server requests.
- This change allows tests to simulate network interactions without relying on the actual fetch implementation, improving test reliability.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-14 13:28:12 +08:00
LiJian cd0f65210c ♻️ refactor: update the codesandbox systemRole(preinstalled_software) (#13799)
refactor: update the codesandbox systemRole(preinstalled_software)
2026-04-14 12:11:44 +08:00
Arvin Xu 24be35fd84 🐛 fix(agent-runtime): resolve S3 image keys when refreshing messages (#13794)
messageModel.query() calls inside RuntimeExecutors were missing a
postProcessUrl callback, so imageList/videoList/fileList entries retained
raw S3 keys (e.g. `files/user_xxx/icon.png`). After the first tool batch,
the refreshed state fed those raw keys straight into the next LLM call,
and providers like Anthropic reject anything that isn't an absolute URL or
data URI ("Invalid image URL"). Wire a lazy FileService-backed
postProcessUrl into all three query sites (topic reference resolution,
compression, and post-batch refresh) so imageLists stay resolved across
multi-step operations.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:29:49 +08:00
Arvin Xu 46adf43453 🐛 fix: dispatch executor=client tools to desktop callers when DEVICE_GATEWAY is configured (#13793)
🐛 fix: dispatch executor=client tools to desktop caller even with DEVICE_GATEWAY configured

Two fixes to make Phase 6.4 (LOBE-7076) actually reach a desktop caller on
canary, where DEVICE_GATEWAY is configured and a separate remote device
may be registered.

### 1. AgentToolsEngine: suppress RemoteDevice for desktop callers

The `lobe-remote-device` tool is meant for the legacy "tunnel commands to
a separately registered desktop" flow. When the caller itself is a
desktop Electron client, that's redundant — and worse, the LLM was
picking `listOnlineDevices` + `activateDevice` *first*, then routing the
subsequent `readLocalFile` to a different registered host (a remote
Linux VM in our E2E trace, returning ENOENT for a path that only exists
on the caller).

Adds `&& !hasClientExecutor` to the RemoteDevice enable rule. Desktop
callers now see only `local-system` in their manifest.

### 2. aiAgent.execAgent: mark executor='client' for desktop callers

The existing gate was `if (!gatewayConfigured) { executorMap[...] = 'client' }`.
On canary, `gatewayConfigured === true` (DEVICE_GATEWAY set), so
`local-system` / stdio MCP stayed server-executed and were dispatched to
the Remote Device proxy instead of back to the caller's Agent Gateway WS.

Extends the gate to:
  `if (clientRuntime === 'desktop' || !gatewayConfigured)`

So a caller that explicitly signals it can receive `tool_execute` bypasses
the DEVICE_GATEWAY heuristic. Legacy behaviour unchanged for web callers
and for callers that don't send `clientRuntime`.

### Tests

- AgentToolsEngine: +1 case verifying RemoteDevice is suppressed when
  `clientRuntime === 'desktop'` even with `gatewayConfigured: true`
- execAgent.deviceToolPipeline: +3 cases
  - local-system gets executor='client' for desktop + DEVICE_GATEWAY
  - stdio MCP gets executor='client' for desktop + DEVICE_GATEWAY
  - web caller preserves legacy routing (executor unset)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:22:18 +08:00
Arvin Xu f0a811ef83 🐛 fix: enable executor=client tools for desktop Electron callers (#13790)
*  feat: enable executor=client tools for desktop Electron callers

Adds a `clientRuntime` signal to execAgent so the server knows the caller
itself can execute `executor: 'client'` tools (local-system, stdio MCP) over
its Agent Gateway WebSocket. This is the missing server piece for Phase 6.4
(LOBE-7076): previously `local-system` only entered the manifest when a
*separately registered* remote device was online & auto-activated, so a
desktop Electron caller sitting on the other end of the Gateway WS could
never actually be dispatched to via `tool_execute`.

The new signal is orthogonal to the legacy device-proxy `deviceContext` —
it describes the caller itself, not a third-party device. The enable rule
for LocalSystemManifest simply gets one extra OR branch:

  local && gatewayConfigured && (hasClientExecutor || legacy-device-online-activated)

`toolExecutorMap[LocalSystemManifest.identifier] = 'client'` (LOBE-7067)
then kicks in as soon as the manifest entry is present, so
`RuntimeExecutors.call_tool` (LOBE-7068) will push `tool_execute` over the
Agent Gateway WS to this caller.

Plumbing:
- packages/types: `ExecAgentParams.clientRuntime?: 'desktop' | 'web'`
- lambda router: accepts + forwards `clientRuntime`
- aiAgent service: forwards to `createServerAgentToolsEngine`
- AgentToolsEngine: +1 field, +1 OR branch in LocalSystem enable rule.
  Zero changes to `runtimeMode` / `platform` / `RemoteDeviceManifest` /
  `deviceContext` semantics.

Tests: 3 new cases in AgentToolsEngine covering desktop / web / gateway-off
branches; 3 new cases in execAgent.deviceToolPipeline verifying the
`clientRuntime` param is forwarded verbatim.

Follow-up (separate PR): frontend receives `tool_execute`, runs the tool
via Electron IPC, and sends `tool_result` back over the same WS.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: untangle runtime / platform / device-proxy flags in AgentToolsEngine

Renames and separates two orthogonal concerns that used to share the
misleading `isDesktopClient` name:

- `hasClientExecutor` — caller itself can receive `tool_execute` over
  the Agent Gateway WS (Phase 6.4). Property of the caller.
- `hasDeviceProxy` — server has a device-proxy configured that tunnels
  to a separately registered device (legacy Remote Device). Property of
  the server.

`platform` is now derived from the caller (`clientRuntime`) first,
falling back to the device-proxy signal for backwards compat — it was
previously derived purely from the server's proxy config, which
conflated "server can reach a desktop" with "caller is a desktop".

LocalSystem enable rule restructured to read in natural order:
  runtimeMode === 'local'         // user opted in
  && hasDeviceProxy               // server has a Gateway path
  && (hasClientExecutor || ...)   // an execution target exists

Behavior is identical to the previous commit; this is a pure rename /
regrouping refactor. 38 existing tests still pass without changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: decouple hasClientExecutor from hasDeviceProxy in local-system gate

The previous rule required `hasDeviceProxy` as a shared prerequisite for
BOTH enable paths, which is wrong: `hasDeviceProxy` reflects the legacy
device-proxy (`deviceProxy.isConfigured`), while Phase 6.4's
`tool_execute` rides the Agent Gateway WebSocket that this request is
already on. The two systems are orthogonal — a desktop caller on the
Gateway WS can receive `tool_execute` without any device-proxy being
configured server-side.

Correct enable rule:

  runtimeMode === 'local'
  && (hasClientExecutor                              // Phase 6.4, self
      || (hasDeviceProxy && deviceOnline && autoActivated))  // legacy

Updated the `still requires gateway to be configured` test, which was
asserting the incorrect coupling, to instead verify that agent-level
`runtimeMode.desktop === 'none'` opt-out is respected for desktop
callers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:12:45 +08:00
Arvin Xu 10914ff015 🐛 fix: add image-to-video options to CLI generate video command (#13788)
*  feat: add image-to-video options to CLI generate video command

Why: CLI only supported text-to-video. Backend already accepts imageUrl/endImageUrl
for image-to-video, but the CLI had no way to pass them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update cli version

* update cli version

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 01:12:10 +08:00
Innei b9c4b87a90 🩹 fix(database): enforce document history ownership and pagination 2026-04-14 00:51:13 +08:00
Adam Bellinson b857ae6c57 🐛 fix(desktop): use Electron net.fetch for remote server requests (#13400)
* use Electron's net.fetch() so system trusted certs are honored

* 🐛 fix(tests): mock netFetch in unit tests broken by net.fetch migration

Both LocalFileCtr and RemoteServerConfigCtr tests were patching
global.fetch / stubGlobal, which no longer intercepts calls now that
the controllers route through Electron's net.fetch via @/utils/net-fetch.
Hoist the fetch mock and point vi.mock('@/utils/net-fetch') at it directly.
2026-04-14 00:45:54 +08:00
Arvin Xu e11c89fc48 🐛 fix(agent-runtime): skip client-executor marking when gateway is configured (#13787)
Tools flagged as `executor: 'client'` are dispatched via `dispatchClientTool`
through the Agent Gateway WS path. In cloud deployments where the gateway is
configured but no desktop device is connected, this path 404s on
`/api/operations/tool-execute` and the tool fails with `dispatch_failed`.

Only mark local-system and stdio MCP plugins as `'client'` when the gateway
is NOT configured (standalone Electron). When deviceContext is available,
tool routing goes through the RemoteDevice proxy instead.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 00:25:19 +08:00
Innei e3eef044ca 👷 build(database): add document history schema 2026-04-14 00:05:06 +08:00
LiJian b9a136f9f1 🐛 fix: slove the execAgent not have lobehub skills & builtin tools (#13781)
fix: slove the execAgent not have lobehub skills & builtin tools
2026-04-13 18:15:54 +08:00
Rdmclin2 809e1e0716 🐛 fix: message gateway ensure running (#13780)
fix: message gateway ensure running
2026-04-13 17:43:18 +08:00
Octopus 7953cf5b5a fix(desktop): use low urgency for Linux notifications to prevent GNOME Shell freeze (#13767)
🐛 fix(desktop): use low urgency for Linux notifications to prevent GNOME Shell freeze

On Linux/GNOME Shell, desktop notifications with urgency 'normal' appear
as banner pop-ups. Clicking the dismiss (X) button on these banners can
cause the system to freeze for 30-45 seconds due to heavy gnome-shell
CPU and memory usage.

Setting urgency to 'low' on Linux routes notifications to the message
tray instead of displaying them as banners, which avoids the problematic
X button interaction. The urgency option is ignored on macOS and Windows.

Fixes #13538

Co-authored-by: octo-patch <octo-patch@github.com>
2026-04-13 16:19:44 +08:00
LobeHub Bot 012214205e 🌐 chore: translate non-English comments to English in database-tests (#13771)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 16:14:09 +08:00
Arvin Xu f0f2feb015 🔨 chore(task): add participants to task.list response (#13778)
*  feat(task): add participants array to task.list response

Return a participants array per task (id / type / avatar / name) so
clients can show avatar groups on task cards. For now participants
only contains the assignee agent; future iterations can aggregate
comment authors and topic executors.

Also extract TaskItem into @lobechat/types as an explicit type
definition so it no longer relies on drizzle schema inference.

* ♻️ refactor(task): extract NewTask to @lobechat/types

Remove the drizzle $inferInsert NewTask from schemas and define it
explicitly in @lobechat/types alongside TaskItem.

*  test(task): cover participants in task.list response
2026-04-13 16:09:53 +08:00
Innei f439fb913a 🐛 fix(editor): bump @lobehub/editor to 4.8.1 (#13756)
🐛 fix: bump @lobehub/editor to 4.8.1
2026-04-13 14:17:39 +08:00
Neko 6966d366d1 🐛 fix(userMemories): should trim way too long bm25 (#13744) 2026-04-13 13:45:37 +08:00
LiJian f89adb36b3 🐛 fix: slove the agent details pages not get the agent config always lo… (#13772)
fix: slove the agent details pages not get the agent config always loading problem
2026-04-13 12:46:10 +08:00
Arvin Xu 3c2fc7f368 🔨 chore(agent-runtime): dispatch client-executor tools via Agent Gateway WS (#13769)
 feat(agent-runtime): dispatch client-executor tools via Agent Gateway WS

Wire the block-await dispatch path for tools marked as `executor: 'client'`:

- `aiAgent/index.ts` (6.3a) — derive `toolExecutorMap` from manifests:
  * `local-system` builtin → `'client'` (requires Electron IPC)
  * MCP plugins with `customParams.mcp.type === 'stdio'` → `'client'`
    (subprocess runs on the user's machine)
  Purely manifest-driven; no new context / capability fields needed.

- `dispatchClientTool` (6.3b) — helper that:
  * Pushes a `tool_execute` event via `streamManager.sendToolExecute`
  * Block-awaits on Redis BLPOP via `ToolResultWaiter`
  * Returns a `ToolExecutionResultResponse`-shaped object (drop-in with
    the existing server path)
  * Never throws — timeouts / gateway errors / missing infra all
    produce a failed-but-structured result so the agent loop continues

- `RuntimeExecutors.call_tool` / `call_tools_batch` — route to
  `dispatchClientTool` when `payload.executor === 'client'` AND the
  stream manager exposes `sendToolExecute`. Otherwise fall through to
  the existing server path unchanged. Response API (`source: 'client'`)
  interrupt branch is untouched.

Capped at 270s per tool to match Vercel's streaming function window;
longer tools will be handled by the resumable path in Phase 6.3c.

Covered by:
- 5 unit tests on `dispatchClientTool` (gateway missing, redis missing,
  happy path, timeout, dispatch error)
- 286 existing tests still pass in adjacent suites

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:25:43 +08:00
LobeHub Bot a72ae190a3 🌐 chore: translate non-English comments to English in integration-test-utilities (#13749)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 11:01:34 +08:00
Arvin Xu 4feafb3fcb ♻️ refactor: migrate memory-user-memory workflows to Hono (#13768)
Replace 6 per-path Next.js `route.ts` handlers (using `@upstash/workflow/nextjs` serve) with a single Hono app mounted at `[[...route]]`. Workflow logic moves to `src/server/workflows-hono/memory-user-memory/`; all public URLs remain unchanged so existing `MemoryExtractionWorkflowService.triggerXxx` callers need no update.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:50:10 +08:00
Arvin Xu eff527de65 🔨 chore(agent-runtime): add ToolResultWaiter for BLPOP-based tool result await (#13763)
*  feat(agent-runtime): add ToolResultWaiter for Redis BLPOP-based tool result await

Introduce ToolResultWaiter — a Promise-based wrapper around Redis BLPOP
that server-side agent loops will use to block-await client-side tool
execution results delivered via the callback API (LPUSH on another
connection).

Design highlights:
- Takes two ioredis clients: a dedicated blocking connection for BLPOP
  (must not be shared with business traffic) and a normal producing
  connection for side effects (cancel sentinel).
- `waitForResult(id, timeoutMs)` returns the parsed payload or null on
  timeout / cancel, never throws for timeout (caller decides fallback).
- `waitForResults(ids[], timeoutMs)` fans out via Promise.all, aligning
  results with input order.
- `cancel(id)` LPUSHes a poison-pill sentinel to wake a pending waiter,
  used when the agent loop is terminated mid-tool.

Covered by unit tests (6 cases: push-before / push-after / timeout /
batch / cancel / malformed payload).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): use multi-key BLPOP in waitForResults to avoid N×timeout latency

Promise.all-ing waitForResult over a shared blocking Redis connection
actually serializes: BLPOP holds the socket, so calls run back-to-back
rather than concurrently. A batch of N where some results never arrive
would take up to N × timeoutMs to resolve, stalling tool-call loops
and delaying cancellation.

Rewrite waitForResults to use Redis's multi-key BLPOP in a loop with a
shared deadline: each iteration blocks on all remaining keys with the
remaining budget, wakes when any one arrives, drops that key, and
re-enters with the rest. Total latency is bounded by one timeoutMs
regardless of N. Single-key waitForResult now delegates to this path.

Covered by a new regression test asserting that an N=3 batch of
never-arriving keys completes in ~1 timeout window, not N×.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:47:49 +08:00
Arvin Xu c60563fffc 🔨 chore(api): add POST /api/agent/tool-result callback endpoint (#13764)
 feat(api): add POST /api/agent/tool-result callback endpoint

Agent Gateway forwards client tool execution results to this endpoint;
the handler LPUSHes into a per-toolCallId Redis list with a 120s TTL so
the server-side agent loop's BLPOP can wake and continue.

- Auth via AGENT_GATEWAY_SERVICE_TOKEN bearer header
- Zod-validated body: { toolCallId, content, success, error? }
- Key: tool_result:{toolCallId}
- Idempotency not required; duplicates sit under TTL until expired

No runtime caller yet — wiring lands with the BLPOP waiter in LOBE-7068.

Covered by unit tests (6 cases: missing/wrong token, missing token env,
invalid body, Redis unavailable, happy path, Redis write error).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:35:03 +08:00
Arvin Xu b36c5a2f1b 🔨 chore: add GatewayStreamNotifier.sendToolExecute (#13765)
 feat(agent-runtime): add GatewayStreamNotifier.sendToolExecute

Expose a request-response-style push for tool_execute on top of the
existing Gateway HTTP pipe. Callers use this to delegate tool execution
to the client; failures surface back to the caller so the agent loop
can decide whether to fall back to the interrupt-resume path.

- `IStreamEventManager.sendToolExecute?` — optional interface method,
  only the Gateway-backed notifier implements it (InMemory/Redis-only
  managers intentionally leave it undefined)
- `GatewayStreamNotifier.sendToolExecute(operationId, ToolExecuteData)`
  POSTs to Gateway `/api/operations/tool-execute`
- New private `httpPostAwait` helper preserves the 5s timeout but,
  unlike the fire-and-forget `httpPost`, rejects on non-ok / network
  failure so callers can react

No runtime caller yet; the dispatch branch lands with LOBE-7068.

Covered by unit tests (3 new cases: happy path payload, non-ok
response, network error).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:29:43 +08:00
Arvin Xu 12bbc56db3 🔨 chore: add tool_execute / tool_result protocol types (#13762)
*  feat(agent-stream): add tool_execute / tool_result protocol types

Introduce the type-level scaffold for the Gateway-mediated client tool
execution flow:

- `tool_execute` server→client event with `ToolExecuteData` payload
  (toolCallId, identifier, apiName, arguments, executionTimeoutMs)
- `tool_result` client→server message with success/error and content,
  added to the `ClientMessage` union

No runtime wiring yet; this PR is pure type scaffolding so subsequent
server (Redis BLPOP waiter, Gateway notifier, RuntimeExecutors branch)
and client (gateway handler) work can land independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update types.ts

* 💄 style(agent-stream): reorder ToolResultMessage fields for perfectionist

Move `error?` before `state?` to satisfy `perfectionist/sort-interfaces`
after the `state?: any` field was added to align with ChatToolResult.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:09:53 +08:00
Rdmclin2 73be58ba12 feat: support message gateway (#13761)
* feat: support message gateway

* feat: sync message gateway connections

* chore: add cloudflare http v2

* fix: typing interval

* feat: add connnectionMode to gateway

* chore: add applicationId when connect

* fix: judge typing supoort by  triggerTyping implementation

* fix: skip message gateway & start connnections

* fix: qq platform hint

* chore: skip webhook mode in gateway connection

* fix: test case

* fix:  message gateway check

* chore: add failaure case post

* fix: test case

* feat: add GatewayManager for webhook-mode platforms
2026-04-13 01:27:54 +08:00
Arvin Xu 3ad124ac4f 🔨 chore: support multimodal input for server-side agent execution (#13759)
*  feat(agent): support multimodal input for server-side agent execution

Wires already-uploaded file IDs through the Gateway-mode execAgent path so
SPA-attached images / documents / videos reach the LLM when the agent runs
server-side. Resolves attachments via FileModel.findByIds, classifies by
MIME, parses documents idempotently, and persists the messages_files link
for history replay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent): dedupe repeated fileIds before writing messages_files

messages_files has a composite PK on (file_id, message_id); a fileIds array
containing the same id twice would fail the insert and abort execAgent. Dedupe
the input while preserving caller-provided order so rendering stays stable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:52:03 +08:00
Arvin Xu e569c8dee0 ♻️ refactor: introduce ToolExecutor field orthogonal to ToolSource (#13760)
Add ToolExecutor ('client' | 'server') as a new orthogonal dimension
alongside ToolSource to describe where a tool invocation is dispatched.
Thread executorMap through OperationToolSet / ResolvedToolSet / AgentState
and attach executor to the ChatToolPayload emitted in onToolsCalling.

Defaults remain empty (all server-side), so behavior is unchanged. This
is pure scaffolding to unblock subsequent work on client-side dispatch.

Also remove the unused 'plugin' value from ToolSource (no downstream
consumers branched on it; installed plugins now labeled 'mcp').

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 00:28:30 +08:00
YuTengjing 406cb5554b feat: add GLM-5.1 model support for Zhipu provider (#13757) 2026-04-12 22:14:52 +08:00
Arvin Xu 0486be4773 🐛 fix: guard non-string content in context-engine to prevent e.trim errors (#13753)
🐛 fix: guard non-string content in context-engine to prevent `e.trim is not a function`

Two unguarded `.trim()` / string-concatenation paths in the context-engine
could throw or produce garbage text when a message's `content` is not a
plain string (multimodal parts array, null tool turns). Both are reached
in normal chat and trigger `e.trim is not a function` in production.

- `resolveTopicReferences`: filter out non-string content in the fallback
  `lookupMessages` path before calling `.trim()`. Without this guard, the
  outer try/catch swallows the TypeError and drops the whole fallback.
- `MessageContent` processor: normalize `message.content` (string or
  parts array) before concatenating file context, instead of relying on
  implicit `toString()` coercion which emitted `[object Object]` into
  the LLM prompt.

Adds regression tests for both paths.
2026-04-12 19:27:52 +08:00
Innei f2ee67c3c5 🐛 fix(inbox): restore inbox avatar fallback after deletion (#13752) 2026-04-12 17:40:40 +08:00
Rdmclin2 16ed80701c 🐛 fix: revert anthropic base64 image (#13751)
chore: revert anthropic base64 image
2026-04-12 16:06:19 +08:00
Octopus 37bf1bd191 fix(local-system): restore loc param when calling readLocalFile IPC (#13748)
🐛 fix(local-system): restore loc param when calling readLocalFile IPC

The `denormalizeParams` method in `LocalSystemExecutionRuntime` was
missing a case for `readLocalFile`. It fell through to `default`, which
passed `{startLine, endLine, path}` as-is to the IPC layer. However,
the IPC handler (`LocalFileCtr.readFile`) expects `LocalReadFileParams`
with `loc?: [number, number]`, not `startLine`/`endLine`. As a result,
`loc` was always `undefined` on the IPC side, causing `readLocalFile`
to default to `[0, 200]` and always return content from line 0.

Fix: add an explicit `readLocalFile` case that reconstructs the `loc`
tuple from `startLine` and `endLine` before forwarding to the IPC layer.

Fixes #13735

Co-authored-by: octo-patch <octo-patch@github.com>
2026-04-12 14:34:42 +08:00
Neko e0f97c4920 🐛 fix(userMemories): missing cancel webhook api for cascading cancellation (#13742) 2026-04-12 04:35:17 +08:00
Arvin Xu 93698f76f8 🔨 chore: update cli version (#13741)
update cli
2026-04-12 02:20:08 +08:00
Arvin Xu 2c79b5ab78 🐛 fix: refine ProviderBizError classification for insufficient balance and quota limit (#13740)
* 🐛 fix: refine ProviderBizError classification for insufficient balance and quota limit errors

Extract inline "Insufficient Balance" check into a dedicated `isInsufficientQuotaError` utility with case-insensitive matching and broader patterns. Add "too many tokens" pattern to `isQuotaLimitError` for Moonshot rate-limit messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update

* 🐛 fix: remove "account has been deactivated" from InsufficientQuota patterns

Account deactivation can be triggered by policy, security, or account review — not just billing. Classifying it as InsufficientQuota misleads users into topping up balance when the fix is usually permission or support escalation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add AccountDeactivated error type for deactivated/suspended accounts

Separate account deactivation from InsufficientQuota so users get actionable guidance (contact support) instead of misleading billing advice.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 02:14:30 +08:00
Rylan Cai 5613935b73 🐛 fix: fix cli message/topic list page indexing (#13731)
* 🐛 fix cli message/topic list page indexing

* ♻️ inline page parsing in message command
2026-04-12 00:46:31 +08:00
Arvin Xu fb7f0c3e92 🐛 fix: preserve error message in ChatCompletionErrorPayload (#13736)
* 🐛 fix: preserve error message in ChatCompletionErrorPayload for ProviderBizError

Add `message` field to `ChatCompletionErrorPayload` and extract SDK error messages in `handleOpenAIError` and `handleAnthropicError`, so downstream consumers (agent tracing, error state) receive human-readable error details instead of generic "ProviderBizError".

Closes LOBE-7019

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: guard nullish error in handleAnthropicError

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 23:42:03 +08:00
Rdmclin2 08769e5bf1 🐛 fix: layout recent locale and support dismiss banner (#13739)
* fix: CN locale for rencents

* fix: community profile setup modal

* feat: support skill banner dismiss
2026-04-11 23:27:21 +08:00
Arvin Xu 732a3ae54a ♻️ refactor: clean up unused sessionStore selectors and slices (#13738)
Remove dead code from the legacy sessionStore:
- Delete `recent` slice (migrated to homeStore)
- Delete `homeInput` slice (migrated to homeStore)
- Remove unused selectors: currentSessionSafe, hasCustomAgents,
  defaultSessionsCount, defaultSessionsLimited, getSessionMetaById,
  currentGroupMeta, getDescription
- Update store type, initialState, and tests accordingly

Closes LOBE-7018

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 22:37:22 +08:00
Arvin Xu a8fee05c3e 🔨 chore: resolve author info for task activity list (#13732)
*  feat: resolve author info (avatar + name) for task activity list

Add `author` field to `TaskDetailActivity` with `{id, type, name, avatar}`.
Backend resolves agent/user info via batch queries in `getTaskDetail`:
- Topics: author is the task's assignee agent
- Briefs: author is the brief's agentId
- Comments: author is authorAgentId or authorUserId

Fixes LOBE-7013

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: move author resolution queries to model layer

Replace direct db.select() calls in TaskService with:
- AgentModel.getAgentAvatarsByIds() for agent info
- UserModel.findByIds() for user info

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:43:00 +08:00
Arvin Xu c255bfe97d 💄 style: show loading state for assistant message during optimistic update (#13733)
🐛 fix: show loading state for assistant message during sendMessage phase

During optimistic update, the assistant message content is "..." but the
loading indicator was not shown because isGenerating only checks
AI_RUNTIME_OPERATION_TYPES (execAgentRuntime), not sendMessage. Include
isCreating state so the loading dots appear immediately when message is sent.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:37:16 +08:00
Arvin Xu f7f2e063d1 💄 style: add delete action to agent profile dropdown menu (#13734)
*  feat: add delete action to agent profile dropdown menu

Add a "Delete" option to the three-dot menu in Agent Profile header,
with confirmation modal. Uses existing `removeAgent` from homeStore.

Fixes LOBE-6582

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: navigate to home after deleting agent from profile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 20:31:55 +08:00
Sun13138 39bca4bc1a 🐛 fix(gemini): align thinkingLevel config resolution across the stack (#13457)
* 🐛 fix(gemini): omit empty thinkingConfig and align thinkingLevel keys

- Google runtime: omit empty thinkingConfig to avoid sending thinkingConfig: {} upstream.\n- UI: ThinkingLevel2/3/4/5 sliders read/write only their own config key.\n- Resolver: map model extend params thinkingLevel* to matching chatConfig key (no fallback/priority logic).\n- Tests: add regression coverage for empty thinkingConfig omission.

* 🐛 fix(gemini): restore default thinking levels by model param

* 🐛 fix(gemini): prefer configured thinkingLevel params
2026-04-11 19:10:26 +08:00
Arvin Xu 9b765eb360 update og 2026-04-11 18:43:28 +08:00
Arvin Xu f68c45cab6 Merge remote-tracking branch 'origin/canary' into fix/task-topic-status-cascade 2026-04-11 18:42:57 +08:00
LobeHub Bot 44c569c5db 🌐 chore: translate non-English comments to English in chat store (#13728)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 18:37:00 +08:00
Arvin Xu 390d82d730 🐛 fix: complete operation and show error on gateway error event (#13714)
* 🐛 fix: complete operation and show error on gateway error event

- Error event handler writes inline error immediately via
  internal_dispatchMessage, then fetches from DB for richer detail.
  This ensures the UI always shows an error even when the server
  hasn't persisted the error into the message table.
- disconnected listener only fires onSessionComplete after a terminal
  agent event (agent_runtime_end / error), not on auth failures or
  explicit disconnect calls.
- Track terminal events via agent_event listener with dedup guard to
  prevent double-firing onSessionComplete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: persist error into assistant message on agent runtime failure

When an agent runtime step fails, the error was written to error_logs
and Redis state but not to the assistant message in the DB. This caused
the frontend to show an empty message after fetchAndReplaceMessages,
since the message had no error field set.

Now dispatchCompletionHooks writes the error to the assistant message
via messageModel.update when reason is 'error', matching the pattern
used by updateAbortedAssistantMessage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:22:18 +08:00
Arvin Xu 2b44cdd298 🐛 fix: add null guard for topicId to fix type error
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:05:41 +08:00
Arvin Xu 345f144b1e 🐛 fix: use conditional cancel and fail-fast on interrupt errors
- Add `cancelIfRunning` to TaskTopicModel: atomically cancel only if topic
  is still running, preventing overwrite of concurrent completed/timeout transitions
- Skip topic cancellation when `interruptTask` fails, keeping DB state
  consistent with the still-running remote operation
- Add test for interrupt failure scenario

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 17:52:29 +08:00
YuTengjing f780f43863 🔨 chore: fix skill frontmatter key to use kebab-case (#13730) 2026-04-11 15:53:02 +08:00
Rdmclin2 ac1abbaf8b 🐛 fix: bot error lobe 6925 (#13724)
* chore: remove unused variables

* fix: add  catch error

* chore: use url for anthropic image

* feat: add bot  process warnings to context

* feat: add thread context

* fix: rename thread name when already has one

* chore: update test cases

* fix: warning sanitize

* fix: threadName safe review
2026-04-11 02:11:33 +08:00
Innei b5f98bd745 feat(chat-input): improve mention menu skill and tool icons (#13722)
*  feat(chat-input): improve mention menu skill and tool icons

- Add MentionItemIcon with SkillAvatar, McpIcon, and Avatar fallbacks
- Strip placeholder avatars ending with _AVATAR
- Tweak mention item icon frame (overflow, border-radius)

Made-with: Cursor

* 💄 fix(chat-input): use theme-aware mention skill fallback
2026-04-11 01:35:02 +08:00
Innei 48d0a759a8 🐛 fix(chat): refine workflow collapse headline (#13717)
* 🐛 fix(chat): refine workflow collapse headline

* 🐛 fix(chat): use state machine for workflow headline

* 🐛 fix(chat): backtrack workflow headline state

* ♻️ refactor(chat): simplify workflow headline selector

* 💄 style(chat): use lucide workflow collapse arrow

* ♻️ refactor(chat): use accordion indicator layout

* Move workflow duration text beside the title

* Localize workflow tool display labels

* Update Page workflow localization labels

* fix: sort imports in toolDisplayNames.test.ts
2026-04-11 00:49:25 +08:00
Rdmclin2 5d135b3ae1 🔨 fix: layout sidebar en More (#13723)
* fix: layout sidebar en More

* chore: update i18n files
2026-04-11 00:46:53 +08:00
Tsuki 17b3acead6 feat(subscription): add cross-platform subscription support for mobile IAP (#13413)
feat(subscription): add cross-platform subscription i18n and mobile subscription router

- Add crossPlatform.title/desc/manageOnMobile translations for 18 languages
- Register mobileSubscriptionRouter in mobile tRPC router
- Add mobileSubscription business router placeholder

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:27:51 +08:00
Rdmclin2 2c397390b4 feat: layout sidebar impl (#13719)
* ♻️ Restructure sidebar layout: extract Lobe AI entry, move New Agent button

- Extract Lobe AI (InboxItem) from agent list to standalone top entry in sidebar body
- Move "New Agent" button from header to below Lobe AI entry
- Add "Create" to bottom menu items alongside Community and Resources
- Filter hidden items in BottomMenu component

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

*  Add unified Recents section to home page

- New TRPC router `recent.getAll` aggregating topics, documents, files, and tasks
- New client service and SWR-based store integration for recents data
- Unified Recents component on home page with type-based icons
- Items sorted by updatedAt, limited to 10, mixed across all types

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

*  Prefetch agent config on hover for faster page loads

- Add usePrefetchAgent hook using SWR mutate to warm cache
- Trigger prefetch on mouseEnter for sidebar agent items
- Reduces or eliminates loading screen when navigating to agent pages

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

*  Redesign agent homepage with info, recent topics, and tasks

- New AgentHome feature replacing the old AgentWelcome component
- Agent info section: avatar, name, description, opening questions
- Recent Topics: horizontal scrollable cards for agent-specific topics
- Tasks section: list with status labels for agent-assigned tasks
- Preserve ToolAuthAlert for tool authorization flows

Fixes LOBE-6938

https://claude.ai/code/session_01RtfXck3GUngoLAgP2yHArz

* fix: common misstakes in layout

* chore: add fetch Recents cache

* chore: add back createagents

* chore: add back lobe ai

* feat: add display count

* feat: add create agent button

* feat: add sidebar section order

* chore: move divider

*  feat: show current page size in display items submenu

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add sidebar display management with customize sidebar modal

- Add "Hide section" and "Customize sidebar" to Recents/Agents dropdown menus
- Create CustomizeSidebarModal with eye toggle for section visibility
- BottomMenu (Community/Resources) also manageable via modal
- Show customize sidebar button in footer when all sections hidden
- Add hiddenSidebarSections to store with localStorage persistence
- Rename "Display Items" to "Show" in dropdown menus
- Add 12px margin between accordion sections and bottom menu
- Add i18n keys for en-US and zh-CN

Fixes LOBE-6938

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: use SlidersHorizontal icon for customize sidebar

Replace Settings2/PanelLeft icon with SlidersHorizontal to avoid
confusion with the settings gear icon.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: refine sidebar customization UX

- Move Settings entry from Footer to BottomMenu alongside Community/Resources
- Add Settings to Customize sidebar modal with eye toggle
- Allow hiding all sections (remove disabled constraint)
- Move Customize sidebar button next to help button in Footer
- Merge Agent dropdown: group Create items with Category items
- Use SlidersHorizontal icon for Customize sidebar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add recents item actions and "more" drawer

- Add inline rename (same as Agent Topic) and delete to Recents items
- Topic/document/file support rename + delete, task supports delete only
- Add "more" button when items exceed pageSize, opens AllRecentsDrawer
- AllRecentsDrawer shows all cached recents from store (up to 50)
- Fetch max(pageSize, 50) items to support drawer without extra request

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add create agent/group modal with ChatInput and examples

- Add CreateAgentModal using base-ui Modal with ChatInputProvider
- Show suggestion examples (agent/group mode) in 2-column grid
- Submit triggers sendAsAgent/sendAsGroup to auto-generate via Agent Builder
- "Create Blank" button for skipping the prompt
- Integrate modal into AgentModalProvider for shared state across sidebar
- Wire up AddButton, NewAgentButton, and dropdown menus to open modal

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: optimitic update rename

* chore: prefetch agent detail

* feat: add recent topic meta data

* feat: add recents search

*  perf: optimize recents API with single UNION query and prefetch

- Replace 3 separate DB queries with single UNION ALL query (RecentModel)
- Add optimistic updates for rename and delete actions
- Add hover prefetch for resources (usePrefetchResource)
- Add hover prefetch for agent config on topic/task items
- Change default pageSize to 5 for both Agents and Recents
- Unify delete confirmation messages per item type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: adjust settings page

* chore: optimize side bar

* feat: recents support right click

* chore: add pin icon to Agents

* chore: add custom side bar modal

* chore: reserve rencent drawer status

* feat: add prefetch route

* feat: add LobeAI prefetch

* fix: document and task rename and delete operation lost

* fix: group route id

* fix: lint error

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-11 00:13:19 +08:00
Innei cd49e98936 chore: bump lucide-react to v1.8.0 (#13703)
* chore: bump lucide-react from ^0.577.0 to ^1.8.0

 Breaking change: Github icon was removed from lucide-react v1.x (brand icons removed).
 Replaced with Github from @lobehub/icons in 5 affected files.

* fix: use GithubIcon from @lobehub/ui/icons instead of @lobehub/icons
2026-04-10 20:17:23 +08:00
Arvin Xu 7894a0a28e 🐛 fix: cascade cancel running topics when task status transitions out of running
When a task's status changes from `running` to another state (backlog/paused/completed/canceled),
automatically cancel all associated running topics and interrupt their operations.
This prevents 409 CONFLICT errors when users try to re-run a task after manually changing its status.

Fixes LOBE-6719

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:37:03 +08:00
lobehubbot d47f4fec76 🔖 chore(release): release version v2.1.49 [skip ci] 2026-04-10 09:51:03 +00:00
Tsuki 9088a074e2 🚀 release: 20260410 (#13716)
## 📦 Weekly Release 20260410

This release includes **67 commits**. Key user-facing updates below.

### New Features and Enhancements

- Introduced **Prompt Rewrite & Translate** feature for assisted input
editing.
- Added **Skill Panel** with dedicated skills tab in the skill store and
fixed skill icon rendering.
- Introduced `lh notify` CLI command for external agent callbacks.
- Added `migrate openclaw` CLI command.
- Added **GraphAgent** and `agentFactory` for graph-driven agent
execution (experimental).
- New topic auto-creation every 4 hours for long-running sessions.

### Models and Provider Expansion

- Added a new provider: **StreamLake (快手万擎)**.
- Added **GLM-5.1** model support with Kimi CodingPlan fixes.
- Added **Seedance 2.0** & **Seedance 2.0 Fast** video generation models
(pricing adjusted with 20% service fee).
- Expanded AIGC parameter support for image and video generation.
- Improved model type normalization for better provider compatibility.
- Multi-media and multiple connection mode support for ComfyUI
integration.

### Desktop Improvements

- **Embedded CLI** in the desktop app with PATH installation support.
- Added Electron version display in system tools settings.
- Fixed RuntimeConfig instant-apply working directory with recent list.
- Fixed desktop locale restore — now uses stored URL parameter instead
of system locale.
- Improved remote re-auth for batched tRPC and clean OIDC on gateway
disconnect.

### Stability, Security, and UX Fixes

- **Security**: prevented path traversal in
`TempFileManager.writeTempFile`; patched IDOR in
`addFilesToKnowledgeBase`; upgraded `better-auth` with hardened
`humanIntervention` requirement in builtin-tool-activator.
- **Context engine**: added `typeof` guard before `.trim()` calls to
prevent runtime crashes.
- **Agent runtime**: preserved reasoning state across OpenAI providers;
fixed service error serialization producing `[object Object]`; surfaced
error `reasonDetail` in `agent_runtime_end` events.
- **Knowledge Base**: cleaned up vector storage when deleting knowledge
bases.
- **Templates**: allow templates to specify `policyLoad` so default docs
are fully injected.
- **Skills**: inject current agents information when `lobehub_skill` is
activated; filter current agent out of available agents list; fix
`agents_documents` overriding `systemRole`.
- **Google Tools**: use `parametersJsonSchema` for Google tool schemas.
- **Web Crawler**: prevent happy-dom CSS parsing crash in
`htmlToMarkdown`.
- **Mobile/UI**: fixed video page icon collision, missing locale keys,
model query param; hidden LocalFile actions on topic share page; allow
manual close of hidden builtin tools.
- **Auth**: `ENABLE_MOCK_DEV_USER` now supported in `checkAuth` and
openapi auth middleware.
- **Sandbox**: stopped using `sanitizeHTMLContent` to block scripts &
sandbox styles.

### Refactors

- Library/resource tree store for hierarchy and move sync.
- Removed legacy `messageLoadingIds` from chat store.
- Removed promptfoo configs and dependencies.
- `OnboardingContextInjector` wired into context engine.

### Credits

Huge thanks to these contributors (alphabetical):

@arvinxx @canisminor1990 @cy948 @hardy-one @hezhijie0327 @Innei
@MarcellGu @ONLY-yours @rdmclin2 @rivertwilight @sxjeru @tjx666
2026-04-10 17:48:33 +08:00
Arvin Xu b95720d210 🐛 fix: add typeof guard before .trim() calls in context engine (#13715)
Add `typeof !== 'string'` checks before `.trim()` calls in BaseSystemRoleProvider,
SystemRoleInjector, and BaseProcessor to prevent TypeError when a non-string truthy
value (e.g. object, array, number) is passed at runtime.
2026-04-10 14:21:18 +08:00
Marcell Gu 560ec57f75 🐛 fix: changed builtin-tool-activator's humanIntervention to require & upgraded better auth (#13682)
* fix(builtin-tool-activator): add humanIntervention required field to activateTools manifest

- Add humanIntervention: "required" to the activateTools API manifest
- Update better-auth dependency from 1.4.6 to 1.4.9 (GHSA-xg6x-h9c9-2m83, 分数: 7.4)

* Downgrade better-auth version to 1.4.6

Thanks for your correction.
2026-04-10 14:20:51 +08:00
Arvin Xu dbca232e35 feat: support regenerateUserMessage in gateway mode (#13711)
*  feat: add gateway mode branch to regenerateUserMessage

When gateway mode is enabled, regenerateUserMessage now calls
executeGatewayAgent with parentMessageId instead of running
internal_execAgentRuntime locally. The server handles branching
and agent execution.

Fixes LOBE-6934

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: switch branch before gateway regeneration and keep operation open

- Move switchMessageBranch before the gateway/client branch so
  activeBranchIndex is advanced and the UI shows the new response
  immediately (fixes regression from client path)
- Add onComplete callback to executeGatewayAgent so callers can
  run cleanup when the gateway session finishes
- Keep regenerate operation running until onComplete fires,
  preventing duplicate concurrent regenerations via isMessageRegenerating

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:23:19 +08:00
Zhijie He c879629439 feat: add prompt rewrite & translate feat (#13523) 2026-04-10 12:33:50 +08:00
Zhijie He 1ecf7d2be8 💄 style(image,video): extend more AIGC params support (#13597)
* 🐛 fix(image,video): preserve prompt and image when switching model

*  feat(image): smart imageUrl ↔ imageUrls conversion on model switch

- When switching from multi-image to single-image model: use imageUrls[0] as imageUrl
- When switching from single-image to multi-image model: wrap imageUrl into [imageUrl] as imageUrls
- Preserves prompt and other compatible parameters
- Add test cases for bidirectional conversion

♻️ refactor(image): simplify preserveImageInputParams logic

- Remove intermediate variables for cleaner code readability
- Condense 9 intermediate variables to 3 core ones
- Inline condition checks for simpler if statements
- Improve code clarity without changing functionality

* 🐛 fix(image): preserve imageUrl when target imageUrls default is empty array

* chore: format imageUrl & imageUrls

* feat: support imageUrls for videoGen

fix: fix ci error

fix: fix ci error

fix: fix + button

fix: fix batch images display

fix: fix muti images upload display

fix: fix ci error

style: add Seedance 2.0 support

style: add Seedance 2.0 support

fix: fix veo imageUrls logic

* style: add watermark & prompt_extend & web_search support

style: update minimax & seedream price

style: fix fix ui error

style: update z-image

style: fix video ui

style: fix seedance & seedream params

style: fix seedance & seedream params

style: fix seedance & seedream params

fix ci error

Update createImage.ts

fix ci error

fix ci error

fix ci error

fix ci error

fix ci error

fix ci error

fix: fix optimize_prompt_options

* fix rebase issue

* fix: seedance 2.0 price missing

* fix: apply some suggestions
2026-04-10 11:50:22 +08:00
LobeHub Bot 8b5aaeebdf 🌐 chore: translate non-English comments to English in comfyui (#13712)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 11:10:11 +08:00
CanisMinor 4787bed380 💄 style: Update agent onboarding style (#13678)
* 💄 style: Update onboarding

* style: update

* 💄 style: Update i18n

* fix: test
2026-04-10 10:44:09 +08:00
Hardy 5f25efd54c feat: add GLM-5.1 model and fix KimiCodingPlan issues (#13700)
* 🐛 fix: fix Kimi K2.5 model icon display by using deploymentName

- Change model id from 'k2p5' to 'kimi-k2.5' to match Moonshot icon keywords
- Add deploymentName 'k2p5' for API calls to use original model name
- Add KimiCodingPlan to providersWithDeploymentName list

This allows the model icon to display correctly while maintaining
backward compatibility with the API using the original 'k2p5' name.

* 🐛 fix: normalize messages for KimiCodingPlan thinking models

Add message normalization for Kimi K2.5 and K2 Thinking models to ensure
every assistant message has a thinking block when thinking is enabled.

This fixes the issue where regenerating with KimiCodingPlan after using
other providers would fail with "thinking is enabled but reasoning_content
is missing" error, because historical messages from other providers don't
have reasoning fields.

The normalization adds a placeholder thinking block when:
1. Thinking is enabled for Kimi K2.5/K2 Thinking models
2. Assistant message doesn't have reasoning content

*  feat(siliconcloud): add GLM-5.1 model support

Add GLM-5.1 (Pro) model configuration with:
- 198K context window
- Function call and reasoning capabilities
- Tiered pricing (0-32k / 32k+)
- reasoningBudgetToken32k extension parameter

* 🐛 fix: use hardcoded maxOutput mapping for KimiCodingPlan models

Replace getModelPropertyWithFallback with a simple hardcoded mapping to fix
the issue where max_tokens lookup fails when using deploymentName (k2p5).

The model id is converted to deploymentName in ChatService layer before
reaching the provider, causing getModelPropertyWithFallback('k2p5', ...) to
fail since the model card uses id 'kimi-k2.5'.

By using a hardcoded mapping that supports both model id and deploymentName,
we avoid the lookup issue while keeping the code simple (KimiCodingPlan only
has a few models).

*  test(kimiCodingPlan): add tests for thinking and max_tokens handling

Add comprehensive tests for KimiCodingPlan provider covering:
- Hardcoded maxOutput mapping for k2p5, kimi-k2.5, kimi-k2-thinking
- Thinking parameter handling for kimi-k2.5 and kimi-k2-thinking models
- Message normalization with forceThinking for assistant messages
- Tool calls with reasoning content to prevent API error

*  test(kimiCodingPlan): add tests for thinking and max_tokens handling

Add comprehensive tests for KimiCodingPlan provider covering:
- Hardcoded maxOutput mapping for k2p5, kimi-k2.5, kimi-k2-thinking
- Thinking parameter handling for kimi-k2.5 and kimi-k2-thinking models
- Message normalization with forceThinking for assistant messages
- Tool calls with reasoning content to prevent API error
2026-04-10 10:41:06 +08:00
Rylan Cai c85be1265f 🐛 fix:(agent-runtime): keep reasoning state in openai providers (#13701)
* 🐛 fix: preserve assistant reasoning in runtime state

* 🐛 fix: preserve agent reasoning and cached usage conversion

* 💬 docs: move usage retention comment to helper

* ♻️ refactor: remove redundant any cast in runtime executor

* 🐛 filter non-finite OpenAI usage values
2026-04-10 10:19:08 +08:00
Innei 4f1d2d494f feat(conversation): assistant group workflow collapse and activate-tools inspector (#13696)
* refactor(workflow): rewrite WorkflowSummary with status dot and minimal flat style

* refactor(workflow): rewrite WorkflowCollapse with unified borderless container

*  feat(workflow): add WorkflowExpandedList component and fix type errors

* ♻️ refactor(workflow): add missing Workflow components with Minimal Flat design

- WorkflowReasoningLine: cssVar tokens, aligned padding
- WorkflowToolDetail: new expandable result panel with motion animation
- WorkflowToolLine: expand chevron, getToolColor, detail panel integration
- WorkflowExpandedList: flat rendering with reasoning + tool lines

* Add tool call collapse support

Made-with: Cursor

* 💄 style(workflow): align WorkflowCollapse UI with @lobehub/ui design system

- Align border-radius, gap, padding tokens across all Workflow components
- Replace chevron expand/collapse with status icons (CheckCircle2, CircleX, Loader2)
- Use @lobehub/ui Highlighter for tool detail panel with JSON auto-formatting
- Use @lobehub/ui Flexbox for WorkflowExpandedList with proper gap and padding
- Fix delete action to use removeToolFromMessage instead of deleteAssistantMessage
- Wire debug button to existing Tool/Debug panel with full tabs
- Fix auto-collapse to only trigger on incomplete→complete transition
- Single ChevronDown with rotation for WorkflowSummary (match @lobehub/ui pattern)

* 💄 style(workflow): use AccordionItem and inspectorTextStyles for WorkflowCollapse

- Replace custom WorkflowSummary with @lobehub/ui AccordionItem
- Use StatusIndicator pattern (Block outlined 24x24) for status icon
- Apply inspectorTextStyles.root for title text (colorTextSecondary)
- Remove WorkflowSummary.tsx (dead code)
- Match Tool component AccordionItem usage (paddingBlock/Inline=4, borderless)

* 💄 style(workflow): remove divider and gap from WorkflowExpandedList

* 💄 style(workflow): align WorkflowCollapse title bar with Thinking component

* 💄 style(workflow): unify inner item spacing, font size, and colors

*  feat(workflow): add streaming scroll behavior with max-height and auto-scroll

* 💄 refactor(assistant-group): refine workflow collapse UI and duration

- Use Accordion for collapse; align tool/reasoning lines with generation state
- Show workflow header duration from summed block performance, not reasoning only

Made-with: Cursor

*  feat(inspector): enhance ActivateToolsInspector to display not found tools count

- Added localization for not found tools message in English, Chinese, and default locales.
- Updated ActivateToolsInspector to show a tooltip with the count of tools not found.
- Modified StatusIndicator to support a warning state for scenarios where no tools are activated but some are not found.

Signed-off-by: Innei <tukon479@gmail.com>

* 💄 style(workflow): simplify padding in WorkflowExpandedList component

- Removed unnecessary paddingInline from Flexbox elements in WorkflowExpandedList for cleaner layout.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(assistant-group): introduce constants and utility functions for workflow management

- Added constants for workflow timing, limits, and tool display names to enhance the assistant group's functionality.
- Implemented utility functions for processing and scoring post-tool answers, improving the workflow's response handling.
- Created new components for rendering content blocks and managing scroll behavior in the assistant group.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(assistant-group): enhance ContentBlock and Group components with content handling logic

- Added logic to conditionally render message content based on content availability and tool presence in ContentBlock.
- Introduced utility functions to determine substantive content and reasoning in Group, improving block partitioning for workflow management.
- Updated partitioning logic to handle trailing reasoning candidates and streamline answer and working block separation.

Signed-off-by: Innei <tukon479@gmail.com>

* 🙈 chore(gitignore): clarify superpowers local paths

Document that `.superpowers/` and `docs/superpowers/` are plugin/local outputs
and must not be committed.

Made-with: Cursor

* 👷 chore(ci): restore auto-tag-release workflow from canary

Revert unintended workflow edits so release tagging stays on main with
sync-main-to-canary dispatch.

Made-with: Cursor

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-10 02:00:38 +08:00
Innei 3b81a94d76 🐛 fix(kb): clean up vector storage when deleting knowledge bases (#13254)
* 🐛 feat(db): add findExclusiveFileIds, deleteWithFiles, deleteAllWithFiles to KnowledgeBaseModel

Add methods to safely clean up vector storage when deleting knowledge bases:
- findExclusiveFileIds: identifies files belonging only to a specific KB
- deleteWithFiles: deletes KB and its exclusive files with chunks/embeddings
- deleteAllWithFiles: bulk version for deleting all user KBs

* 🐛 fix(kb): wire vector cleanup in TRPC router, OpenAPI service, and client

- TRPC removeKnowledgeBase: use deleteWithFiles when removeFiles=true + S3 cleanup
- TRPC removeAllKnowledgeBases: use deleteAllWithFiles + S3 cleanup
- OpenAPI deleteKnowledgeBase: use deleteWithFiles + S3 cleanup
- Client service: default removeFiles=true when deleting knowledge base

* 🐛 fix(knowledgeBase): change default behavior of deleteKnowledgeBase to not remove files and update related tests

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(knowledgeBase): add optional query parameter to deleteKnowledgeBase for file removal

- Introduced `removeFiles` query parameter to control the deletion of exclusive files and derived data when deleting a knowledge base.
- Updated `KnowledgeBaseController`, `KnowledgeBaseService`, and related schemas to support this new functionality.

This change enhances the flexibility of the delete operation, allowing users to choose whether to remove associated files.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: cascade knowledge base deletion and add orphan cleanup runbook

*  feat(knowledgeRepo): implement cascading deletion for file-backed documents

- Enhanced the `KnowledgeRepo` to ensure that when a document with an associated file is deleted, all related data (files, chunks, embeddings) are also removed.
- Introduced a new method `deleteDocumentWithRelations` to handle the cascading deletion logic.
- Updated tests to verify that all related entities are deleted when a file-backed document is removed.

This change improves data integrity by ensuring that no orphaned records remain after deletions.

Signed-off-by: Innei <tukon479@gmail.com>

* Defer DocumentService file initialization

* Fix flaky database tests and knowledge repo fixtures

* Add deletion regression tests for folders and external files

*  chore: remove kb orphan cleanup files from pr

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-10 01:56:05 +08:00
Arvin Xu a4d9967e60 🐛 fix: gateway not receiving error reasonDetail in agent_runtime_end event (#13707)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 01:51:19 +08:00
Arvin Xu 6a40eb8a3b 🐛 fix: resolve agent runtime service error serialization producing [object Object] (#13704)
 feat: add remote snapshot fetch for agent-tracing CLI and fix error serialization
2026-04-10 00:01:01 +08:00
Arvin Xu a23e159ef3 🔨 chore: extend execAgent with parentMessageId for Gateway regeneration/continue (#13699)
* 🌐 chore: update execServerAgentRuntime i18n copy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: extend execAgent with parentMessageId for regeneration/continue via Gateway

Add parentMessageId support to the execAgent API, enabling regeneration and continue-generation flows through the Gateway WebSocket path. When parentMessageId is provided, user message creation is skipped (resume mode) and the new assistant message branches from the specified parent.

Fixes LOBE-6933

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: propagate parentMessageId through execAgents batch and fix test types

- Forward parentMessageId in execAgents executeTask to maintain batch parity with execAgent
- Fix ExecAgentResult mock types in gateway tests
- Fix messages table insert type cast in server router test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 21:51:59 +08:00
Arvin Xu 1eb1fca7f2 🌐 chore: update execServerAgentRuntime i18n copy (#13698)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 18:52:07 +08:00
sxjeru 4100f2f700 🐛 fix: enhance model type normalization (#13548)
* feat(modelParse): enhance model type normalization and add tests for invalid types

* feat(modelParse): optimize imports and improve model type handling
2026-04-09 18:46:14 +08:00
Arvin Xu 23f91d044c 🐛 fix: buffer and deduplicate events during gateway resume (#13689)
* 🐛 fix: buffer and deduplicate events during resume to prevent out-of-order display

When reconnecting with empty lastEventId (page reload), live broadcast
events can arrive before resume replay completes, causing content to
appear out of order. Now AgentStreamClient enters resume mode: buffers
all events, waits for a 500ms gap (resume replay is dense, live events
are sparse), then deduplicates by event ID and emits in order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: clear runningOperation on agent finish + resume timeout for completed sessions

- RuntimeExecutors.finish clears topic metadata.runningOperation when
  agent reaches terminal state, so stale entries don't trigger reconnect
- AgentStreamClient resume mode: add 3s timeout for empty buffer —
  if no events arrive after resume request, session has already completed,
  emit session_complete and disconnect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: eagerly fetch messages after topic switch to avoid skeleton flash

After switchTopic in Gateway mode, immediately fetch messages from DB
and replace in store, so the UI renders content right away instead of
showing a skeleton loading state while SWR re-fetches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: eliminate skeleton flash on gateway topic switch

Match the client-mode pattern: fetch messages from DB and replaceMessages
BEFORE calling switchTopic with skipRefreshMessage: true. This ensures
messages are already in the store when the topic switches, preventing
a skeleton loading flash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: flush resume buffer on session_complete before disconnect

session_complete is a top-level ServerMessage (not an agent_event), so
it bypassed the resume buffer. When it arrived during resume mode,
disconnect() cleared the buffer and all replayed events were lost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: limit resume buffering to explicit reconnect scenarios only

Resume mode was triggered for ALL new connections (lastEventId always
empty on first connect), delaying live streaming for normal operations.

Now resume buffering requires explicit opt-in via resumeOnConnect option,
which is only set by reconnectToGatewayOperation (page-reload reconnect).
Normal executeGatewayAgent connections stream events immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:26:21 +08:00
LiJian 06ac87dc45 🐛 fix: should inject current agnets information when actived the lobehub_skill (#13661)
* fix: should inject current agnets information when actived the lobehub skill

* fix: not inject the agent systemRole in lobehub skill inject

* fix: should use the isLobeHubSkillActive hook to judge

* fix: change the tools inject to vars replace function

* fix: add the lost topic id & agent title

* fix: later the PlaceholderVariablesProcessor

* fix: update the description
2026-04-09 16:11:18 +08:00
Zhijie He 6d731dd116 feat: add StreamLake Provider support (#13651)
*  feat: add StreamLake (快手) support

* style: add thinking support

style: add thinking support

style: add thinking support

style: add thinking support

style: add thinking support
2026-04-09 15:00:50 +08:00
LobeHub Bot f804d0fc7c 🌐 chore: translate non-English comments to English in scripts (#13690)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 14:38:29 +08:00
Arvin Xu b268f44f06 🐛 fix(server): prevent path traversal in TempFileManager.writeTempFile (#13684)
🐛 fix(server): prevent path traversal in TempFileManager.writeTempFile

Use path.basename() to strip directory components from user-supplied
filenames before writing temp files, preventing arbitrary file write
via crafted filenames like "../../app/startServer.js".

Fixes LOBE-6904

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 14:35:20 +08:00
Rdmclin2 475622a4b9 feat: support multi media and multiple connection mode (#13624)
* test: add feishu and qq test cases

* feat: support qq websocket mode

* feat: support slack websocket mode

* feat: feishu/lark support websocket connectMode

* chore: add default connection mode

* fix: discord 401 sign error

* fix: feishu websocket need verification token

* fix: heartbeate interval

* fix: get effective connnection mode

* chore: extract  getEffectiveConnectionMode utils

* chore: merge with default settings

* chore: add connectionMode fallback

* fix: file extract error

* fix: list platforms by connectionMode

* fix: qq bot gateway

* fix: support fileList

* feat: support video list

* chore: migrate local testing to references

* chore: add bot skill

* fix: telegram file serialize error

* feat: extract file extract logic to platform client

* fix: wechat file read

* feat: skip no metion in thread and set default message mode to queue

* chore: refact download resources

* fix: feishu adapter mention and dm error

* fix: feishu thread id

* fix: slack remove action

* fix: bot resovle files
2026-04-09 14:16:03 +08:00
René Wang 7b40538486 feat: add iamge (#13688) 2026-04-09 10:33:08 +08:00
Arvin Xu 5531ff7907 🔨 chore: Gateway reconnect after page reload (#13685)
*  feat: persist runningOperation to topic metadata for gateway reconnect

- Add runningOperation field to ChatTopicMetadata type
- execAgent writes { operationId, assistantMessageId } to topic metadata
  after creating the operation
- onSessionComplete clears runningOperation from metadata (best-effort)
- Extend updateTopicMetadata tRPC schema + service to support the field

Fixes LOBE-6905

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add refreshGatewayToken tRPC endpoint

Signs a fresh JWT for Gateway WebSocket reconnection after page reload.
The token is scoped to the authenticated user via signUserJWT.

Fixes LOBE-6906

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: auto-reconnect to running Gateway operation on topic load

- Add reconnectToGatewayOperation to GatewayActionImpl — refreshes JWT,
  creates local operation, and connects WebSocket with event replay
- Add useGatewayReconnect hook — checks topic metadata.runningOperation
  when entering a topic and triggers reconnection
- Wire hook into ConversationArea

Fixes LOBE-6907

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: preserve thread scope in reconnect context and subscribe to topic metadata

- Store scope + threadId in topic metadata.runningOperation
- reconnectToGatewayOperation uses stored scope/threadId instead of
  hardcoded main/null
- useGatewayReconnect subscribes to runningOperation via useChatStore
  selector so it triggers when topic data arrives from SWR (not just
  on mount when data may be empty)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: update device tests to allow runningOperation metadata writes

The tests asserted updateMetadata was never called, but now execAgent
persists runningOperation. Changed to assert no device-binding metadata
was written (boundDeviceId), which is the actual intent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: use SWR for gateway reconnect lifecycle

Replace useEffect + ref with useSWR keyed by operationId. SWR
naturally deduplicates (same key = no re-fetch), handles the async
reconnect, and doesn't fire when key is null (no runningOperation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: validate topic has running operation before issuing gateway token

refreshGatewayToken now requires topicId, verifies the topic belongs to
the user and has a runningOperation in metadata before signing a JWT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: break signin title into two lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix signin.title formatting in auth.json

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:23:57 +08:00
Arvin Xu 4f56868545 🐛 fix: allow templates to specify policyLoad so default docs are fully injected (#13672)
* 🐛 fix: allow templates to specify policyLoad so default docs are fully injected

All documents were hardcoded to PolicyLoad.PROGRESSIVE on creation,
causing CLAW template docs (IDENTITY, SOUL, BOOTSTRAP, AGENTS) to be
progressively disclosed instead of fully injected into context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: forward policyLoad through upsertDocument and persist on update

- Add policyLoad to UpsertDocumentParams and pass it through to model
- Add policyLoad param to update() so upsert's existing-document path
  writes the value instead of silently discarding it
- Ensures re-running template init migrates pre-existing docs to ALWAYS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: change update() to use named params object instead of positional args

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: change create() and upsert() to use named params object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: improve agentDocuments test coverage to 99%

Add tests for uncovered branches:
- normalizeLoadRule default branch (unknown rule)
- explicit 'always' rule match
- by-time-range with NaN dates
- resolveDocumentLoadPosition fallback paths
- composeToolPolicyUpdate with existing context values
- upsert create path for new filenames
- getAgentContext empty docs path

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: preserve policyLoad when copying documents

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  fix: align test assertion with refactored create() params object signature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:09:05 +08:00
Arvin Xu dc1b43d86c 🐛 fix(database): prevent IDOR in addFilesToKnowledgeBase (#13683)
🐛 fix(database): add ownership check in addFilesToKnowledgeBase to prevent IDOR

Verify that the target knowledge base belongs to the authenticated user
before inserting files, preventing unauthorized file injection into
other users' knowledge bases.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:36:51 +08:00
Arvin Xu 4d7cbfea8e 🐛 fix: skip sendMessageInServer in Gateway mode + NavItem loading fix + i18n (#13681)
* 🐛 fix: reuse existing messages in execAgent when existingMessageIds provided

When existingMessageIds contains [userMsgId, assistantMsgId], skip
creating new messages and reuse the existing ones. This fixes duplicate
messages in Gateway mode where sendMessageInServer already created
the messages before execAgentTask is called.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: allow clicking NavItem while loading

Loading state should only show a visual indicator, not block onClick.
This fixes topic sidebar items being unclickable during agent execution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "🐛 fix: reuse existing messages in execAgent when existingMessageIds provided"

This reverts commit 43b808024d5c4a0074b692a85083a72046ab47e0.

* 🐛 fix: skip sendMessageInServer in Gateway mode to avoid duplicate messages

Gateway mode now calls execAgentTask directly instead of going through
sendMessageInServer first. The backend creates user + assistant messages
and topic in one call. executeGatewayAgent handles topic switching
internally after receiving the server response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🌐 chore: add i18n for execServerAgentRuntime operation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: move temp message cleanup after executeGatewayAgent succeeds

Keep temp messages visible during the gateway call so the UI isn't
blank. On failure, mark the operation as failed instead of silently
returning — temp messages remain so the user sees something went wrong.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: remove manual temp message cleanup in gateway mode

switchTopic handles new topic navigation, and fetchAndReplaceMessages
replaces the message list from DB — no need to manually delete temp
messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: clear _new key temp messages when gateway creates new topic

Pass clearNewKey: true to switchTopic so temp messages from the
optimistic create don't persist in the _new key after switching
to the server-created topic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: import ExecAgentResult from @lobechat/types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 01:33:54 +08:00
Innei e65e2c3628 feat(desktop): embed CLI in app and PATH install (#13669)
*  feat(desktop): embed CLI in app and PATH install

Made-with: Cursor

*  feat(desktop): add CLI command execution feature and UI integration

- Implemented `runCliCommand` method in `ElectronSystemService` to execute CLI commands.
- Added `CliTestSection` component for testing CLI commands within the app.
- Updated `SystemCtr` to include CLI command execution functionality.
- Enhanced `generateCliWrapper` to create short aliases for CLI commands.
- Integrated CLI testing UI in the system tools settings page.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance working directory handling for desktop

- Updated working directory logic to prioritize topic-level settings over agent-level.
- Introduced local storage management for agent working directories.
- Modified tests to reflect changes in working directory behavior.
- Added checks to ensure working directory retrieval is only performed on desktop environments.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(desktop): implement CLI command routing and cleanup

- Introduced `CliCtr` for executing CLI commands, enhancing the desktop application with CLI capabilities.
- Updated `ShellCommandCtr` to route specific commands to `CliCtr`, improving command handling.
- Removed legacy CLI path installation methods from `SystemCtr` and related services.
- Cleaned up localization files by removing obsolete entries related to CLI path installation.

Signed-off-by: Innei <tukon479@gmail.com>

* 🚸 settings(system-tools): show CLI embedded test only in dev mode

Made-with: Cursor

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-09 00:53:49 +08:00
Arvin Xu eebf9cb056 chore: add gatewayMode translations for labs (#13680)
* 🌐 i18n: add gatewayMode translations for labs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update labs.json

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:49:32 +08:00
Arvin Xu 3e7ee1fbfc 🔨 chore: integrate Gateway connection management into chat store (#13636)
*  feat: integrate Gateway connection management into chat store

Add GatewayActionImpl to aiChat slice for managing Agent Gateway
WebSocket connections per operationId. Includes connect, disconnect,
interrupt, and status tracking. Also type the execAgentTask return value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add Gateway mode branch in sendMessage for server-side agent execution

When agentGatewayUrl is set in server config (enableQueueAgentRuntime),
sendMessage now triggers server-side agent execution via execAgentTask
and receives events through the Agent Gateway WebSocket, instead of
running the agent loop client-side.

Includes:
- Expose agentGatewayUrl in GlobalServerConfig when queue mode is enabled
- Gateway event handler mapping stream events to UI message updates
- Fallback to client-side agent loop when Gateway is not configured

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: emit disconnected event on intentional disconnect

disconnect() was only calling setStatus('disconnected') but not emitting
the 'disconnected' event. This caused the store's cleanup listener to
never fire after terminal events (agent_runtime_end), leaving stale
connections in gatewayConnections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: enhance Gateway event handler for multi-step agent streaming

Support multi-step agent execution display (LLM → tool calls → next LLM)
using hybrid approach: real-time streaming for current step, DB refresh at
step transitions.

Fixes LOBE-6874

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: wire up Gateway JWT token from execAgent to connectToGateway

Pass the RS256 JWT token returned by execAgentTask to connectToGateway
for WebSocket authentication. Also use ExecAgentResult from @lobechat/types
instead of local duplicate definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: handle wss:// protocol in AgentStreamClient buildWsUrl

When gatewayUrl already uses ws:// or wss:// protocol, use it directly
instead of stripping and re-adding the protocol prefix. Previously,
wss://host would become ws://wss://host (double protocol).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: queue gateway events to ensure stream_chunk waits for refreshMessages

Use a sequential Promise chain to process gateway events, so that
stream_chunk dispatches only run after stream_start's refreshMessages
resolves. Previously, chunks arrived before the new assistant message
existed in dbMessagesMap, causing updates to be silently dropped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: pass operationId context to internal_dispatchMessage in gateway handler

Without operationId, internal_dispatchMessage falls back to global state
to compute the messageMapKey, which may differ from the key where
refreshMessages stored the server-created messages. Passing operationId
ensures the correct conversation context is resolved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve gateway streaming display issues

- Use fetchAndReplaceMessages (direct DB fetch + replaceMessages) instead
  of refreshMessages which mutates an orphaned SWR key
- Create dedicated execServerAgentRuntime operation with correct topicId
  context for internal_dispatchMessage to resolve the right messageMapKey
- Complete operation on agent_runtime_end instead of relying on
  onSessionComplete callback
- Keep loading state active between steps (only clear on agent_runtime_end)
  so users don't think the session ended during tool execution gaps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: maintain loading state across gateway step transitions

- Create dedicated execServerAgentRuntime operation with correct topicId
- Use fetchAndReplaceMessages instead of orphaned refreshMessages SWR key
- Re-apply loading after tool_end refresh so UI stays active between steps
- Complete operation on agent_runtime_end
- Add record-app-screen.sh for automated screen recording
- Output recordings to .records/ (gitignored)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: show loading on assistant message immediately in stream_start

Set loading on the current assistant message BEFORE awaiting
fetchAndReplaceMessages, so the UI shows a loading indicator while
waiting for the DB response instead of appearing frozen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: drive gateway loading state via operation system instead of messageLoadingIds

Associate the assistant message with the gateway operation via
associateMessageWithOperation so the Conversation store's operation-based
loading detection (isGenerating) works correctly. This shows the proper
loading skeleton on the assistant message while waiting for gateway events.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: remove unused internal_toggleMessageLoading from gateway handler

Loading state is now fully driven by the operation system via
associateMessageWithOperation + completeOperation. The old
messageLoadingIds-based approach is no longer needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: rewrite record-app-screen.sh to use CDP screenshot assembly

Replace broken ffmpeg avfoundation live recording (corrupts on kill) with
agent-browser CDP screenshot capture + ffmpeg assembly on stop. This works
reliably on any screen including external monitors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add Gateway Mode lab toggle and fix CI type error

- Add enableGatewayMode to UserLabSchema as experimental feature
- Add lab selector and settings UI toggle in Advanced > Labs
- Gateway mode now requires both server config (agentGatewayUrl) AND
  user opt-in via Labs toggle
- Fix TS2322: result.token (string | undefined) → fallback to ''
- Add i18n keys for gateway mode feature

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: hide Gateway Mode toggle when agentGatewayUrl is not configured

Only show the lab toggle when the server has AGENT_GATEWAY_URL set,
so users without gateway infrastructure don't see the option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: move Gateway Mode toggle below Input Markdown in labs section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: remove default AGENT_GATEWAY_URL value and make schema optional

Without an explicit env var, the gateway URL should be undefined so the
lab toggle and gateway mode are not available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs: update SKILL.md to reference record-app-screen.sh

Replace outdated record-gateway-demo.sh references with the renamed
record-app-screen.sh and its start/stop lifecycle documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs: add record-app-screen reference doc and slim down SKILL.md

Move detailed recording documentation to references/record-app-screen.md
and keep SKILL.md concise with a link to the full reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: guard GatewayStreamNotifier with AGENT_GATEWAY_URL check

AGENT_GATEWAY_URL is now optional, so check both URL and service token
before wrapping with GatewayStreamNotifier to avoid TS2345.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract gateway execution logic to GatewayActionImpl

Move server-side gateway execution logic from conversationLifecycle.ts
into GatewayActionImpl.startGatewayExecution(). The sendMessage flow
now does a simple early return when gateway mode is active, keeping
the existing client-mode code path untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: split gateway into isGatewayModeEnabled check + executeGatewayAgent

Replace fire-and-forget startGatewayExecution with explicit check/execute
pattern. Caller does: if (check) { await execute(); return; } — giving
proper error handling and clearer control flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:31:26 +08:00
renovate[bot] 84eff30be1 Update dependency lucide-react to ^0.577.0 (#13580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 23:31:20 +08:00
Innei 50a1cc1ec2 ♻️ refactor(resource): tree store for library hierarchy and move sync (#13640)
*  feat(ResourceManager): integrate tree store for folder management and enhance file operations

- Added `useTreeStore` to manage folder structure and state, replacing previous file store dependencies.
- Updated `EmptyPlaceholder` to utilize `currentFolderId` for file uploads.
- Refactored `MoveToFolderModal` to use tree store for moving items, improving folder navigation.
- Enhanced drag-and-drop functionality in `DndContextWrapper` to support moving items between folders.
- Removed obsolete `LibraryHierarchy` state management, streamlining folder operations.
- Improved file renaming and deletion processes to ensure tree state consistency.

This update enhances the overall file management experience by leveraging a dedicated tree store for better performance and maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(TreeAction): enhance resource movement and update handling

- Updated mutation logic for moving resources to differentiate between items visible in the Explorer and those not visible, improving performance and user experience.
- Added refresh functionality for the file list after resource updates (move, update, delete) to ensure the Explorer reflects the latest state.
- Refactored mutation methods to use async/await for better readability and error handling.

This update streamlines resource management within the tree structure, ensuring a more responsive and consistent user interface.

Signed-off-by: Innei <tukon479@gmail.com>

* Fix file updates and tree move fallback regressions

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-08 23:26:58 +08:00
Arvin Xu d49aba748e 🐛 fix: hide LocalFile actions in topic share page (#12254)
🐛 fix: hide LocalFile actions (Open/Show in Folder) in share page

In topic share pages, the LocalFile component was showing 'Open' and
'Show in Folder' action buttons on hover, which are desktop-only
operations not available to share page viewers.

- Add 'readonly' prop to LocalFile component to disable interactive actions
- Detect share page context via topicShareId in LocalFile Render plugin
- Skip Popover rendering when readonly is true
2026-04-08 22:45:08 +08:00
Arvin Xu 8a0c3cb36a ♻️ refactor: remove legacy messageLoadingIds from chat store (#13662)
* ♻️ refactor: remove legacy messageLoadingIds from chat store

The messageLoadingIds state and internal_toggleMessageLoading action in the
chat store have been fully superseded by the operation system. The state was
being written to but never read by any consumer — all UI components and
selectors already use operation-based selectors (isMessageGenerating,
isMessageProcessing, etc.).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 chore: update skill docs to remove messageLoadingIds references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: replace messageLoadingIds with operationSelectors in generation action

The Conversation store's regenerateUserMessage was reading messageLoadingIds
from the chat store to check if a message is already being processed. Replace
with operationSelectors.isMessageProcessing which is the correct way to check
operation state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add operationsByMessage to test mocks for operation selector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:54:11 +08:00
LiJian 26d1d6bbfb 🐛 fix: slove the agents_documents will coverd the systemRole (#13667)
fix: slove the agents_documents will coverd the systemRole
2026-04-08 20:54:20 +08:00
YuTengjing c5ec0ef2a1 💰 chore: adjust Seedance 2.0 pricing with 20% service fee (#13676) 2026-04-08 20:50:18 +08:00
YuTengjing 6d0c8d710a 🐛 fix: video page icon collision, missing locale keys, and model query param (#13671) 2026-04-08 19:44:35 +08:00
Rdmclin2 e10265fadd feat: add skill panel and fix skill icon (#13666)
* fix: custom agent skill icon

* feat: support skill detail

* chore: remove unnecessary custom tag
2026-04-08 18:51:01 +08:00
Arvin Xu c68dfa00df feat(cli): add lh notify command for external agent callbacks (#13664)
*  feat(cli): add `lh notify` command for external agent callbacks

Add a new `lh notify` CLI command and server-side TRPC endpoint that allows
external agents (e.g. Claude Code) to send callback messages to a topic and
trigger the agent loop to process them.

Fixes LOBE-6888

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore(cli): replace sessionId with agentId and threadId in notify command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:03:55 +08:00
Arvin Xu b6a47debfd ♻️ refactor: remove promptfoo configs and dependencies (#13665)
♻️ refactor: remove promptfoo configs and dependencies from packages

Migrate all prompt evaluation tests to the cloud repo's agent-evals framework.
Remove promptfoo directories, configs, dependencies, and generator scripts
from @lobechat/prompts, @lobechat/memory-user-memory, and @lobechat/builtin-tool-memory.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 17:50:55 +08:00
YuTengjing 147ff3976f feat: add Seedance 2.0 & 2.0 Fast video generation models (#13663) 2026-04-08 17:39:50 +08:00
René Wang 034c7c203b feat: changelog (#13634)
* feat: changelog

* feat: edito content
2026-04-08 15:30:33 +08:00
Rdmclin2 b0b6684294 🔨 chore: optimize model and skills (#13659)
* chore: model detail default close

* fix: model detail show info in normal mode
2026-04-08 15:20:35 +08:00
Arvin Xu 36d2427947 🐛 fix: use parametersJsonSchema for Google tool schemas (#13656)
* 🐛 fix: use parametersJsonSchema for Google tool schemas to support full JSON Schema

Replace Google's restrictive Schema subset with parametersJsonSchema, which accepts
standard JSON Schema directly. This eliminates the need for resolveRefs and
sanitizeSchemaForGoogle, fixing nullable enum (LOBE-6607) and $ref (LOBE-6680) issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: update remaining tests to use parametersJsonSchema

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 15:08:59 +08:00
Innei 4d15979fab 💄 fix(RuntimeConfig): instant-apply working directory with recent list (#13641)
* 💄 fix(RuntimeConfig): instant-apply working directory with recent list

Remove Save/Cancel buttons from working directory selector.
Directories now apply immediately on click. Show recent directories
list with checkmark for active selection and "Choose a different folder"
entry at bottom.

*  feat(SystemCtr): enhance folder selection to return repository type

Updated the `selectFolder` method to return an object containing the selected folder path and its repository type (either 'git' or 'github'). Added a new private method `detectRepoType` to determine the repository type based on the presence of a `.git/config` file. Introduced a new utility for managing recent directories, allowing the application to display appropriate icons based on the repository type in the UI.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-08 14:56:18 +08:00
Arvin Xu 53786e30b6 🔨 chore: remove redundant update-status call from GatewayStreamNotifier (#13655)
* ♻️ refactor: remove redundant update-status call from GatewayStreamNotifier

Gateway now handles session completion directly in pushEvent when it
receives agent_runtime_end, so the separate update-status HTTP call
is no longer needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: update GatewayStreamNotifier tests for removed update-status call

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:03:46 +08:00
LiJian 7300b53e99 🐛 fix: not use sanitizeHTMLContent to block the scripts & sandbox styles (#13649)
* fix: not use sanitizeHTMLContent to block the scripts & sandbox styles

* fix: clean the code & remove the allows-popups
2026-04-08 13:34:40 +08:00
Arvin Xu 6f3897a6e8 🔨 chore: generate JWT token for Gateway WebSocket auth in execAgent (#13654)
 feat: generate JWT token for Gateway WebSocket auth in execAgent

Sign a short-lived RS256 JWT via signUserJWT(userId) when creating an agent
operation, and return it in ExecAgentResult.token so the client can
authenticate with the Agent Gateway WebSocket.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:28:37 +08:00
Arvin Xu a6e330cfa9 🐛 fix(web-crawler): prevent happy-dom CSS parsing crash in htmlToMarkdown (#13652)
- Disable CSS file loading and JS evaluation in happy-dom Window (root cause)
- Add try-catch around Readability.parse() for defense in depth
- Add regression tests for invalid CSS selectors and external stylesheet links

Closes LOBE-6869

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:59:49 +08:00
LobeHub Bot accc173068 🌐 chore: translate non-English comments to English in openapi routes (#13647)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:50:17 +08:00
Arvin Xu 81ab8aa07b 🔨 chore: support nested subtask tree in task.detail (#13625)
*  feat: support nested subtask tree in task.detail

Replace flat subtask list with recursive nested tree structure.
Backend builds the complete subtask tree in one response,
eliminating the need for separate getTaskTree API calls.

Fixes LOBE-6814

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: return empty array for root subtasks instead of undefined

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs: add cli-backend-testing skill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:49:26 +08:00
YuTengjing 12ee7c9e9a 🐛 fix: support ENABLE_MOCK_DEV_USER in checkAuth and openapi auth middleware (#13648) 2026-04-08 12:37:27 +08:00
LiJian 8d8b60e4f9 🐛 fix: should filiter the current agents in avaiable agents list (#13644)
* fix: should inject the current agents & remove current agent from avaiable agents list

* fix: delete the current agents blocks
2026-04-08 11:24:53 +08:00
YuTengjing 19aedcdf56 fix: skip @mention for team members in PR assign and issue triage (#13633) 2026-04-08 11:00:19 +08:00
YuTengjing 3bb09e0ef9 feat: enhance linear skill with image extraction and in-progress status (#13629) 2026-04-08 10:58:07 +08:00
Arvin Xu 13fc65faa2 update 2026-04-08 10:53:00 +08:00
Arvin Xu de8761cf29 🐛 fix: import hook types before re-exporting for tsgo compatibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:52:11 +08:00
Arvin Xu 4f2f0055e1 ♻️ refactor(agent-runtime): improve AgentInstruction types and extract hook event types
- Each instruction interface now extends AgentInstructionBase directly instead of intersection
- Group instructions by category: LLM, Tool, Task, Human Interaction, Control
- Extract AgentHookType and AgentHookEvent into agent-runtime package
- Keep AgentHook, AgentHookWebhook, SerializedHook in server layer (webhook is server-specific)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:45:00 +08:00
Arvin Xu 2290929255 🔨 chore: add GraphAgent and agentFactory for graph-driven agent execution (#13643)
*  feat: add GraphAgent and agentFactory for graph-driven agent execution

- Add GraphAgent: a decorator around GeneralChatAgent that drives execution via declarative ReasoningGraph
  - Agent nodes: delegate to GeneralChatAgent for tool-calling loops, then extract structured output
  - LLM nodes: single structured LLM call
  - Programmatic transition evaluation (not LLM-driven)
  - Backtracking with configurable limits
- Add AgentInstruction.stepLabel: allows any Agent to label steps for display in stream events and hooks
- Add agentFactory to AgentRuntimeServiceOptions: external injection of custom Agent implementations
- Add stepLabel propagation: stream_start/stream_end events and afterStep hooks carry the label
- Fix: sanitize null bytes in MessageModel.create content (consistent with existing plugin argument sanitization)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(agent-runtime): validate graph node existence and preserve transitions at backtrack limit

- Add node existence check in startNode to prevent runtime crash on invalid entry/transition targets
- Evaluate all transitions even when backtrack limit is reached; only suppress actual backtrack targets
2026-04-08 10:28:15 +08:00
Innei a2eab24536 🐛 fix(device-gateway-client): prevent uncaught WebSocket error on disconnect (#13635)
* 🐛(device-gateway-client): prevent uncaught error when closing connecting WebSocket

Detach ws event listeners safely, temporarily handle close-phase errors, and guard ws.close() so logout/token clear does not surface a main-process uncaught exception.

Made-with: Cursor

* 🧹 refactor(tests): remove unused mockProps from ComfyUIForm test

Cleaned up the ComfyUIForm test by removing the unused mockProps object, streamlining the test setup for better clarity and maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* Hide onboarding finish tool call and preserve close error listener

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-07 23:59:03 +08:00
Innei b279c108b6 🐛 fix(desktop): use stored locale from URL parameter instead of syste… (#13620)
🐛 fix(desktop): use stored locale from URL parameter instead of system language

When the desktop app restarts, the UI language was reverting to the system
language instead of respecting the user's saved language preference.

Root cause: The inline script in index.html was setting document.documentElement.lang
from navigator.language (system language) before i18n initialization could read
the stored locale from Electron store.

Fix: Check the URL's `lng` query parameter first (which is set by Electron main
process from stored settings in Browser.ts:buildUrlWithLocale()), then fall back
to navigator.language.

Fixes #13616

https://claude.ai/code/session_0128LZAbJL1a5vkGboH4U5FP

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-07 22:58:09 +08:00
Innei 7a6fd8e865 🐛 fix(desktop): remote re-auth for batched tRPC and clean OIDC on disconnect (#13614)
* 🐛 fix(desktop): remote re-auth for batched tRPC and clean OIDC on disconnect

- Notify authorization required when X-Auth-Required is set, not only on HTTP 401 (207 batch)
- Show AuthRequiredModal after remote config init; do not gate on dataSyncConfig.active
- Desktop: market 401 only silent refresh; avoid community sign-in UI (AuthRequiredModal handles cloud)
- Disconnect: clearRemoteServerConfig to wipe encrypted OIDC tokens

Made-with: Cursor

* 🐛 Reset user-data Zustand stores on remote disconnect and sync refresh

- Add ResetableStoreAction helper and batched reset via userDataStores
- Wire reset into Electron remote disconnect and refreshUserData
- Handle refreshUserData failures in data sync SWR onSuccess

Made-with: Cursor

* 🐛 fix(useUserAvatar): refactor desktop environment checks to use mockConstEnv

- Replace direct manipulation of mockIsDesktop with mockConstEnv.isDesktop for better encapsulation.
- Update all relevant test cases to utilize the new mock structure, ensuring consistent behavior across tests.

This change improves the clarity and maintainability of the test code.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 test: update mocks for ShikiLobeTheme and refactor session/agent mocks

- Added ShikiLobeTheme mock to ComfyUIForm and AddFilesToKnowledgeBase tests for consistent theming.
- Refactored session and agent mocks to use async imports, improving test isolation and performance.

This enhances the clarity and maintainability of the test suite.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-07 22:57:49 +08:00
lobehubbot 7d599a980f 🔖 chore(release): release version v2.1.48 [skip ci] 2026-04-07 14:50:49 +00:00
lobehubbot 1206db7c12 Merge remote-tracking branch 'origin/main' into canary 2026-04-07 14:48:16 +00:00
Arvin Xu bd61b61843 🚀 release: 20260407 (#13626)
# 🚀 release: 20260407

This release includes **148 commits**. Key updates are below.

- **Response API tool execution is more capable and reliable** — Added
hosted builtin tools + client-side function tools and improved tool-call
streaming/completion behavior.
[#13406](https://github.com/lobehub/lobehub/pull/13406)
[#13414](https://github.com/lobehub/lobehub/pull/13414)
[#13506](https://github.com/lobehub/lobehub/pull/13506)
[#13555](https://github.com/lobehub/lobehub/pull/13555)
- **Input and composition UX upgraded** — Added AI input auto-completion
and multiple chat-input stability fixes.
[#13458](https://github.com/lobehub/lobehub/pull/13458)
[#13551](https://github.com/lobehub/lobehub/pull/13551)
[#13481](https://github.com/lobehub/lobehub/pull/13481)
- **Model/provider compatibility improved** — Better Gemini/Google tool
schema handling and additional model updates.
[#13429](https://github.com/lobehub/lobehub/pull/13429)
[#13465](https://github.com/lobehub/lobehub/pull/13465)
[#13613](https://github.com/lobehub/lobehub/pull/13613)
- **Desktop and CLI reliability improved** — Gateway WebSocket support
and desktop runtime upgrades.
[#13608](https://github.com/lobehub/lobehub/pull/13608)
[#13550](https://github.com/lobehub/lobehub/pull/13550)
[#13557](https://github.com/lobehub/lobehub/pull/13557)
- **Security hardening continued** — Fixed auth and sanitization risks
and upgraded vulnerable dependencies.
[#13535](https://github.com/lobehub/lobehub/pull/13535)
[#13529](https://github.com/lobehub/lobehub/pull/13529)
[#13479](https://github.com/lobehub/lobehub/pull/13479)

### Models & Providers

- Added/updated support for `glm-5v-turbo`, GLM-5.1 updates, and
qwen3.5-omni series.
[#13487](https://github.com/lobehub/lobehub/pull/13487)
[#13405](https://github.com/lobehub/lobehub/pull/13405)
[#13422](https://github.com/lobehub/lobehub/pull/13422)
- Added additional ImageGen providers/models (Wanxiang 2.7 and Keling
from Qwen). [#13478](https://github.com/lobehub/lobehub/pull/13478)
- Improved Gemini/Google tool schema and compatibility handling across
runtime paths. [#13429](https://github.com/lobehub/lobehub/pull/13429)
[#13465](https://github.com/lobehub/lobehub/pull/13465)
[#13613](https://github.com/lobehub/lobehub/pull/13613)

### Response API & Runtime

- Added hosted builtin tools in Response API and client-side function
tool execution support.
[#13406](https://github.com/lobehub/lobehub/pull/13406)
[#13414](https://github.com/lobehub/lobehub/pull/13414)
- Improved stream tool-call argument handling and `response.completed`
output correctness.
[#13506](https://github.com/lobehub/lobehub/pull/13506)
[#13555](https://github.com/lobehub/lobehub/pull/13555)
- Improved runtime error/context handling for intervention and provider
edge cases. [#13420](https://github.com/lobehub/lobehub/pull/13420)
[#13607](https://github.com/lobehub/lobehub/pull/13607)

### Desktop App

- Bumped desktop dependencies and runtime integrations (`agent-browser`,
`electron`). [#13550](https://github.com/lobehub/lobehub/pull/13550)
[#13557](https://github.com/lobehub/lobehub/pull/13557)
- Simplified desktop release channel setup by removing nightly release
flow. [#13480](https://github.com/lobehub/lobehub/pull/13480)

### CLI

- Added OpenClaw migration command.
[#13566](https://github.com/lobehub/lobehub/pull/13566)
- Added local device binding support for `lh agent run`.
[#13277](https://github.com/lobehub/lobehub/pull/13277)
- Added WebSocket gateway support and reconnect reliability
improvements. [#13608](https://github.com/lobehub/lobehub/pull/13608)
[#13418](https://github.com/lobehub/lobehub/pull/13418)

### Security

- Removed risky `apiKey` fallback behavior in webapi auth path to
prevent bypass risk.
[#13535](https://github.com/lobehub/lobehub/pull/13535)
- Sanitized HTML artifact rendering and iframe sandboxing to reduce
XSS-to-RCE risk. [#13529](https://github.com/lobehub/lobehub/pull/13529)
- Upgraded nodemailer to v8 to address SMTP command injection advisory.
[#13479](https://github.com/lobehub/lobehub/pull/13479)

### Bug Fixes

- Fixed image generation model default switch issues.
[#13587](https://github.com/lobehub/lobehub/pull/13587)
- Fixed subtopic re-fork message scope behavior and agent panel reset
edge cases. [#13606](https://github.com/lobehub/lobehub/pull/13606)
[#13556](https://github.com/lobehub/lobehub/pull/13556)
- Fixed chat-input freeze on paste and mention plugin behavior.
[#13551](https://github.com/lobehub/lobehub/pull/13551)
[#13415](https://github.com/lobehub/lobehub/pull/13415)
- Fixed auth/social sign-in and settings UX edge cases.
[#13368](https://github.com/lobehub/lobehub/pull/13368)
[#13392](https://github.com/lobehub/lobehub/pull/13392)
[#13338](https://github.com/lobehub/lobehub/pull/13338)

### Credits

Huge thanks to these contributors:

@chriszf @hardy-one @Innei @LiJian @Neko @octopusnote @rdmclin2
@rivertwilight @RylanCai @suyua9 @sxjeru @Tsuki @WangYK @WindSpiritSR
@Yizhuo @YuTengjing @hezhijie0327 @arvinxx
2026-04-07 22:45:54 +08:00
Arvin Xu 0c49b0a039 🔨 chore: add AgentStreamClient for Agent Gateway WebSocket (#13628)
* 🤖 chore(skills): add electron-dev.sh script and update local-testing skill

Add reusable electron-dev.sh script with start/stop/status/restart commands
that reliably manages all Electron processes (main + helpers + vite).
Update SKILL.md to reference the script instead of inline bash commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add AgentStreamClient for Agent Gateway WebSocket communication

Browser-compatible WebSocket client for receiving agent execution events
from the Agent Gateway. Supports auto-reconnect with exponential backoff,
heartbeat keep-alive, and event replay via lastEventId resume.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 22:42:54 +08:00
Innei 1beb9d4eb6 feat(desktop): add Electron version display in system tools settings (#13630)
*  feat(desktop): add Electron version display in system tools settings

Display Electron, Chrome, and Node.js versions in the desktop app's Settings > System Tools page under a new "App Environment" section.

https://claude.ai/code/session_01C6nUdBci6A29CZCvQSUuDt

* 🐛 fix(desktop): update preload test for new version properties

https://claude.ai/code/session_01C6nUdBci6A29CZCvQSUuDt

* ♻️ refactor: remove unused i18n name keys for app environment section

Tool names (Electron, Chrome, Node.js) are proper nouns that don't need
localization, matching the existing pattern in ToolDetectorSection.

https://claude.ai/code/session_01C6nUdBci6A29CZCvQSUuDt

* 🐛 fix(desktop): handle undefined electron/chrome versions in test env

process.versions.electron and process.versions.chrome are only available
in Electron runtime, not in the Node.js test environment.

https://claude.ai/code/session_01C6nUdBci6A29CZCvQSUuDt

* 🐛 fix: use const assertion for i18n key type safety

https://claude.ai/code/session_01C6nUdBci6A29CZCvQSUuDt

* 🌐 Add app environment strings to setting locales and refine copy

Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-07 21:53:27 +08:00
LiJian 021fd07deb 🐛 fix: can manual close the hidden builtin tools (#13631)
* fix: can manual close the hidden builtin tools

* fix: should change it into chatConfigByIdSelectors

* fix: add the always not close tools
2026-04-07 21:37:32 +08:00
LiJian 33f729cd1a 🐛 fix: add the availableAgents into the prompt inject (#13621)
* fix: add the availableAgents into the prompt inject

* fix: should auto inject the avaiable agents into context when use the auto model

* fix: update the prompt

* fix: test fixed
2026-04-07 19:45:29 +08:00
Innei 8b3c871d08 ♻️ refactor(onboarding): add OnboardingContextInjector and wire context engine (#13518)
* ♻️ refactor(onboarding): add OnboardingContextInjector and wire context engine

Made-with: Cursor

* 🔧 refactor(onboarding): update tool call references to use `lobe-user-interaction________builtin`

Modified onboarding documentation and utility functions to standardize the use of the `lobe-user-interaction________builtin` tool call for structured input collection, enhancing clarity and consistency across the codebase.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor(onboarding): standardize tool call references to `lobe-user-interaction____askUserQuestion____builtin`

Updated documentation and utility functions to replace instances of the `lobe-user-interaction________builtin` tool call with `lobe-user-interaction____askUserQuestion____builtin`, ensuring consistency in structured input collection across the onboarding process.

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor(onboarding): move onboarding context before first user

* ♻️ refactor(context-engine): add virtual last user provider

* update v3

* 🐛 fix(onboarding): add early exit escape hatch for boundary cases

The `<next_actions>` directive only prompted finishOnboarding in the
summary phase, but phase transition required all fields + 5 discovery
exchanges — a condition extreme cases rarely meet. This left the model
stuck in discovery, never calling finishOnboarding.

- Add EARLY EXIT hint in discovery phase next_actions
- Add universal completion-signal REMINDER across all phases
- Add minimum-viable discovery fallback in systemRole
- Add explicit completion signal list in Early Exit section
- Add off-topic redirect limit in Boundaries
- Add CRITICAL persistence rule in toolSystemRole

*  test(context-engine): fix OnboardingContextInjector tests to match BaseFirstUserContentProvider

Remove brittle MessagesEngine onboarding test that hardcoded XML content.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-07 19:25:16 +08:00
Arvin Xu bd8143c464 🐛 fix(prompts): enforce user perspective in input completion (#13619)
🐛 fix(prompts): enforce user perspective in input completion prompt

The autocomplete prompt was generating completions from the AI assistant's
perspective (e.g., "How can I help you?") instead of the user's perspective.
Added explicit perspective constraints with good/bad examples.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 17:31:14 +08:00
LobeHub Bot 68762fc4ae 🌐 chore: translate non-English comments to English in desktop i18nWorkflow (#13604)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 16:51:56 +08:00
Arvin Xu 1a58d530fb ♻️ refactor: add WebSocket gateway support to CLI agent run (#13608)
*  feat: add WebSocket gateway support to CLI agent run

CLI `agent run` now connects to Agent Gateway via WebSocket by default,
falling back to SSE when `--sse` is passed. After auth, sends `resume`
to fetch buffered events (covers race between exec and WS connect).

- Add `streamAgentEventsViaWebSocket` in agentStream.ts
- Add `resolveAgentGatewayUrl` in settings
- Add `OFFICIAL_AGENT_GATEWAY_URL` constant
- Support `AGENT_GATEWAY_SERVICE_TOKEN` env for gateway auth
- Add `--sse` flag for forced SSE fallback

Fixes LOBE-6800

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add WebSocket gateway stream tests for CLI

Cover auth flow, resume, event rendering, JSON mode, auth failure,
heartbeat_ack, URL construction, and a multi-step tool-call scenario.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: persist agentGatewayUrl in saveSettings/loadSettings

saveSettings and loadSettings now handle agentGatewayUrl so custom
gateway configuration survives across CLI runs. Default URL is
stripped like serverUrl to keep the settings file minimal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: remove AGENT_GATEWAY_SERVICE_TOKEN and fix JSON double-print in WS stream

1. Remove AGENT_GATEWAY_SERVICE_TOKEN env var — gateway auth should
   only use Oidc-Auth / X-API-Key from the existing auth flow.

2. Fix --json mode printing duplicate JSON arrays: agent_runtime_end,
   session_complete, and onclose all called console.log independently.
   Add jsonPrinted guard so only the first path outputs JSON.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:49:25 +08:00
Arvin Xu ca01385666 🐛 fix(model-runtime): strip additionalProperties and leftover $ref in Google tool schemas (#13613)
Google Gemini / Vertex AI rejects `additionalProperties` and `$ref` in
function declaration schemas. The previous fix (PR #13524) resolved most
`$ref` via `resolveRefs()` but missed two cases:

1. `additionalProperties` was never stripped
2. `$ref` survived when `resolveRefs` hit its depth limit (>10) on
   recursive schemas

Add both keys to UNSUPPORTED_SCHEMA_KEYS so `sanitizeSchemaForGoogle()`
strips them after ref resolution.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 16:08:15 +08:00
dependabot[bot] 5231bbbcac build(deps-dev): bump electron from 41.0.3 to 41.1.0 in /apps/desktop (#13557)
Bumps [electron](https://github.com/electron/electron) from 41.0.3 to 41.1.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v41.0.3...v41.1.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 41.1.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 14:19:46 +08:00
Octopus 496b10f5c0 fix(github-copilot): surface quota exhaustion 429 instead of retrying (#13584)
🐛 fix(github-copilot): surface quota exhaustion 429 instead of retrying

When the GitHub Copilot API returns a 429 with a Retry-After header
exceeding 5 minutes (indicating quota exhaustion rather than transient
rate limiting), throw the error immediately instead of retrying up to
MAX_RATE_LIMIT_RETRIES times with a silently capped 10s delay.

Fixes #13572
2026-04-07 14:06:52 +08:00
Arvin Xu 1800110748 🐛 fix: use main scope messages for subtopic re-fork (#13606)
* 🐛 fix: use main scope messages for thread fork to fix subtopic re-fork failure

When inside a subtopic (activeThreadId set), openThreadCreator and portalAIChats
used activeDisplayMessages which included activeThreadId in the key, returning
thread-scoped messages instead of main conversation messages. This caused
genParentMessages to fail finding the target message, resulting in empty parent
messages and a broken/loading fork UI.

Fix: use messageMapKey with only agentId/topicId to always get main scope messages.

Closes LOBE-5023

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: include groupId in main scope key for group session support

Address Codex review: pass activeGroupId to messageMapKey so that
fork and thread selectors work correctly in group conversations
where messages are keyed by group scope instead of main scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:40:59 +08:00
YuTengjing b068c427d4 🐛 fix: preserve backend traceId in error handler (#13607) 2026-04-07 12:58:26 +08:00
Arvin Xu d5eec83a72 🔧 chore: disable input completion by default (#13605)
* 🔧 chore: disable input completion by default

The input auto-completion experience is not polished enough yet,
so disable it by default. Users can still enable it manually in
Settings > Agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: update snapshot for disabled input completion default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:29:50 +08:00
Arvin Xu 6c9cbb07ee 🔨 chore: add GatewayStreamNotifier for Agent Gateway WebSocket push (#13603)
*  feat: add GatewayStreamNotifier for Agent Gateway WebSocket push

Add a decorator that wraps IStreamEventManager to additionally push
events to the Agent Gateway via HTTP (fire-and-forget). When
AGENT_GATEWAY_SERVICE_TOKEN is configured, the factory automatically
wraps the base stream manager with the gateway notifier. Redis SSE
remains the primary event channel; the gateway is an additive push
layer for WebSocket delivery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add GatewayStreamNotifier and factory gateway wrapping tests

Ensure the decorator always delegates to the inner stream event manager
first, gateway failure never drops Redis events, and the factory
correctly wraps/skips based on AGENT_GATEWAY_SERVICE_TOKEN.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add timeout, bounded concurrency and url-join to gateway notifier

- 5s AbortSignal timeout on every gateway POST to prevent hanging sockets
- Max 20 inflight requests; excess silently dropped with a debug log
- Use url-join for URL construction instead of string concatenation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve TS18048 possibly undefined in test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: move gateway env vars to appEnv

Read AGENT_GATEWAY_SERVICE_TOKEN and AGENT_GATEWAY_URL from the
validated appEnv config instead of raw process.env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: move gateway URL default into appEnv

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:16:49 +08:00
LobeHub Bot b92ee0ade5 🌐 chore: translate non-English comments to English in store/task (#13561)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:16:24 +08:00
Arvin Xu 3327b293d6 🔒 fix: remove apiKey fallback in webapi auth to prevent auth bypass (#13535)
* 🔒 fix: remove XOR auth header and legacy apiKey bypass (GHSA-5mwj-v5jw-5c97)

Completely remove the forgeable X-lobe-chat-auth XOR obfuscation mechanism:

- Remove apiKey fallback in checkAuthMethod (auth bypass vector)
- Rewrite checkAuth to use session/OIDC userId only, never trust client header
- Delete XOR encoding/decoding utilities and tests
- Delete dead keyVaults TRPC middleware (no consumers)
- Simplify createHeaderWithAuth (no longer sends XOR payload)
- Remove SECRET_XOR_KEY constant
- Remove authorizationHeader from TRPC lambda context
- Clean up CLI to only send Oidc-Auth header
- Update all affected tests

The LOBE_CHAT_AUTH_HEADER constant is retained for the async caller
(server-to-server) path which uses AES encryption via KeyVaultsGateKeeper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: restore createPayloadWithKeyVaults for fetchOnClient path

The client-side model runtime (fetchOnClient) needs getProviderAuthPayload
and createPayloadWithKeyVaults to build provider SDK init params directly
in the browser. These functions are unrelated to XOR encoding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: guard against null session before accessing user id

Add explicit null check before accessing session.user.id to prevent
TypeError when session is null (e.g. unauthenticated requests).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add missing AgentRuntimeError import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: remove dead createRuntime code path causing type error

The createRuntime property was removed from checkAuth's RequestHandler
type but still referenced in the route handler, causing TS2339.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 11:53:07 +08:00
Innei d7e5d4645d ⬆️ chore(desktop): bump agent-browser to v0.24.0 (#13550)
* ⬆️ chore(desktop): bump agent-browser to v0.24.0

https://claude.ai/code/session_01XnRtpGn54turwVXf4MziLM

* 📝 chore: update agent-browser skill to match upstream v0.24.0

Sync the local-testing skill's agent-browser section with the upstream
SKILL.md from vercel-labs/agent-browser. Adds new commands: batch, auth
vault, semantic locators, annotated screenshots, clipboard, dialog
handling, diff, streaming, iOS simulator, dashboard, cloud providers,
and engine selection.

https://claude.ai/code/session_01XnRtpGn54turwVXf4MziLM

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-07 02:28:50 +08:00
lobehubbot 918e4a8fa1 Merge remote-tracking branch 'origin/main' into canary 2026-04-06 14:54:26 +00:00
Rdmclin2 f58015bb23 docs: clarify WeChat channel availability (#13540)
## Summary
- clarify in the channels overview that WeChat currently requires an
active subscription
- note that community edition users may not see the WeChat option in
channel settings yet
- keep the English and Chinese overview pages aligned

## Testing
- `git diff --check`

Related to #13461.
2026-04-06 22:53:44 +08:00
Zhijie He e6244aaea6 🐛 fix: fix imageGen button always switch to Nano Banaba (#13587) 2026-04-06 10:20:51 +08:00
Arvin Xu e9d43cb43f ♻️ refactor(bot): migrate Bot service to Agent Runtime Hooks framework (#13546)
* ♻️ refactor(bot): migrate Bot service to Agent Runtime Hooks framework

Migrate the last consumer (Bot/AgentBridgeService) from legacy
completionWebhook/stepWebhook/stepCallbacks dual-track pattern
to the unified hooks API. This completes LOBE-6208 Step 4.

- Enrich AgentHookEvent with step presentation + tracking data
- Enrich afterStep hook dispatch with full step context
- Merge executeWithWebhooks + executeWithInMemoryCallbacks into unified hooks
- Remove legacy triggerCompletionWebhook, triggerStepWebhook, stepCallbacks
- Remove completionWebhook/stepWebhook/webhookDelivery from params

LOBE-6675

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(hooks): dispatch completion hooks on early-terminal return and fix totalToolCalls lag

- Add dispatchCompletionHooks in early-terminal branch of executeStep
  so onComplete hooks fire when operation is already interrupted/done/error
  between queued steps (e.g., via /stop)
- Include current step's toolsCalling in afterStep totalToolCalls so
  consumers get an accurate cumulative count instead of lagging by one step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: update tests to match hooks-based architecture

- Rewrite executeStep tests to use hookDispatcher spies instead of
  removed registerStepCallbacks/getStepCallbacks API
- Rewrite completionWebhook tests to use hooks param and _hooks metadata
  instead of removed completionWebhook param
- Delete stepLifecycleCallbacks.test.ts (tests removed API, coverage
  now provided by HookDispatcher.test.ts + executeStep.test.ts)
- Update AgentRuntimeService.test.ts abort test to remove stepCallbacks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve remaining CI failures from hooks migration

- Fix TS18048 errors: guard metadata access with null check in
  _stepTracking block
- Migrate remaining registerStepCallbacks usage in
  AgentRuntimeService.test.ts to hookDispatcher.dispatch spies:
  onComplete error tests and onAfterStep tool result extraction tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(bot): update AgentBridgeService tests for hooks-based execution

Old tests expected execAgent to NOT be called (because APP_URL check
would throw in queue mode). With hooks migration, the APP_URL check
is gone (hooks use relative URLs resolved by HookDispatcher), so
execAgent is now called. Update tests to verify hooks are passed
correctly instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(bot): add hook payload compatibility tests for BotCallbackService

Add tests verifying that webhook payloads from HookDispatcher (containing
hookId/hookType fields) are correctly handled by BotCallbackService.
This validates the critical contract between the hooks framework and
the bot callback endpoint for step progress, completion, and error paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add hooks integration tests for e2e payload verification

Add integration tests that verify the full executeStep → hookDispatcher
chain produces events with all fields bot consumers depend on:

- afterStep event includes content, stepType, totalTokens, executionTimeMs
- afterStep event includes cross-step tracking (lastLLMContent, totalToolCalls)
- afterStep event includes toolsResult for tool_result phases
- onComplete fires on early-terminal states (interrupted) with lastAssistantContent
- All RenderStepParams-required fields are present and correctly typed

These tests catch payload format regressions without needing production
infrastructure (Redis, QStash, real bot platforms).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 22:14:56 +08:00
Arvin Xu 5b03f009ee 🐛 fix(agentDocuments): add progressive disclosure PolicyLoad mode (#13571)
---------

Co-authored-by: Arvin Xu <arvinxx@ArvindeMacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by-agent: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 01:23:32 +08:00
Neko 25cf3bfafd 🐛 fix(userMemories): i18n for purge button (#13569) 2026-04-05 00:28:10 +08:00
Rdmclin2 3cb7206d90 feat: create new topic every 4 hours (#13570)
* feat: create new topic every  4 hours

* fix: bot topic try catch

* fix: test case
2026-04-04 23:40:04 +08:00
Rdmclin2 e364b9a516 feat: skill store add skills tab (#13568)
* feat: add skill list and mcp list

* feat: support market skill detail

* fix: market skill detail render

* feat: add task emoji

* chore: lost  setting locales

* fix: build market download url
2026-04-04 22:11:17 +08:00
Arvin Xu a7e3d198df 🐛 fix(chat-input): memoize mentionOption/slashOption to prevent freeze on paste (#13551)
* 🐛 fix(chat-input): memoize mentionOption and slashOption to prevent page freeze on paste

Stabilize mentionOption and slashOption references with useMemo/useCallback to break the
infinite re-render loop that occurs when pasting text triggers autocomplete.

Fixes LOBE-6684

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-input): trim trailing newlines from autocomplete result to prevent empty lines

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(chat-input): skip autocomplete during IME composition to prevent interrupting Chinese input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 20:09:17 +08:00
Arvin Xu 14cd81b624 feat(cli): add migrate openclaw command (#13566)
*  feat(cli): add `migrate openclaw` command for importing OpenClaw workspace

Add a new CLI command `lh migrate openclaw` that imports all files from the
OpenClaw workspace (~/.openclaw/workspace) as agent documents into the LobeHub
inbox agent. Supports --source, --agent-id, --slug, --dry-run, and --yes options.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor(cli): restructure migrate as directory for future providers

Refactor `migrate` command from a single file to a directory structure
(`migrate/index.ts` + `migrate/openclaw.ts`) to support future migration
sources like ChatGPT, Claude, Codex, etc.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(cli): remove unnecessary `as any` casts in migrate openclaw

Use proper TrpcClient type instead of casting to any. Extract
resolveInboxAgentId helper with correct typing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor(cli): migrate openclaw creates a new "OpenClaw" agent by default

Instead of importing into the inbox, the default behavior now creates a
dedicated "OpenClaw" agent and imports workspace files as its documents.
Use --agent-id to import into an existing agent instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(cli): restore --agent-id and --slug options for migrate openclaw

Support three modes: --agent-id (by ID), --slug (by slug, e.g. "inbox"),
or default (create a new "OpenClaw" agent).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(cli): print agent URL after migrate openclaw completes

Show a clickable link (e.g. https://app.lobehub.com/agent/<id>) at the
end of the import so users can open the agent directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(cli): check login state early in migrate openclaw

Verify authentication before scanning files so users get a clear
"Run 'lh login' first" message upfront instead of after confirmation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(cli): read agent name, description, avatar from OpenClaw workspace

Parse IDENTITY.md (or SOUL.md) for Name, Creature/Vibe/Description, and
Emoji fields to populate the new agent's title, description, and avatar
instead of hardcoding "OpenClaw".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style(cli): show emoji + name instead of agent ID in migrate output

Display the agent avatar emoji and title throughout the migrate flow
(confirmation, creation, importing). The agent ID only appears in the
final URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(cli): exclude .venv from openclaw workspace scan

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore(cli): expand excluded dirs/files for openclaw workspace scan

Filter out IDE configs, VCS dirs, OS artifacts, dependency dirs, Python
caches, build outputs, env files, and other common non-content items.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update version

*  feat(cli): use `ignore` package for gitignore-based file filtering & improve output

- Replace hardcoded EXCLUDED_NAMES set with `ignore` package (gitignore syntax)
- Respect workspace .gitignore if present, plus comprehensive default rules
- Cover all common languages/tools: Python, Ruby, Rust, Go, Java, .NET, etc.
- Improve final output: friendlier completion message with agent name + URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(cli): add tests for migrate openclaw command

Cover profile parsing, file filtering (gitignore + default rules),
dry-run, agent resolution (--agent-id, --slug, default create),
confirmation flow, error handling, and output formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(cli): filter placeholder emoji and binary/database files

- Skip avatar values like (待定), _(待定)_, TBD, N/A, etc.
- Add ignore rules for database files (*.sqlite, *.db, *.mdb, etc.),
  images, media, fonts, lock files, and compiled binaries
- Runtime binary detection: check first 8KB for null bytes and skip
  binary files that slip through the extension filter
- Add tests for placeholder emoji filtering, binary skip, and db exclusion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(api,cli): support optional createdAt for agent document upsert

Thread an optional `createdAt` parameter through all layers:
- Model: AgentDocumentModel.create/upsert accept optional createdAt,
  set both createdAt and updatedAt on documents + agent_documents rows
- Service: UpsertDocumentParams includes createdAt
- Router: agentDocument.upsertDocument accepts optional z.date()
- CLI: migrate openclaw passes file mtime as createdAt to preserve
  original file timestamps

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style(cli): add npx usage hint to auth error message

Show 'npx -y @lobehub/cli login' alongside 'lh login' so users who
haven't installed the CLI globally know how to authenticate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update version

*  feat(api,cli): support optional updatedAt for agent document upsert

Add updatedAt alongside createdAt through all layers. When both are
provided, updatedAt is used independently; when only createdAt is
given, updatedAt falls back to createdAt.

CLI now passes file birthtime as createdAt and mtime as updatedAt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(cli): use os.homedir() for default source & wrap file reads in try

- Replace process.env.HOME || '~' with os.homedir() so the default
  --source path resolves correctly on Windows and when HOME is unset
- Move fs.readFileSync/statSync inside the try block so a single
  unreadable file doesn't abort the entire migration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:45:04 +08:00
Arvin Xu bd345d35a8 🐛 fix(openapi): fix response.completed output missing message, wrong tool name & id (#13555)
* 🐛 fix(openapi): fix response.completed output missing message, wrong tool name & id

Fix three bugs in extractOutputItems for the Response API:
1. Assistant message with text+tool_calls was dropped from output
2. Function call names kept internal ____-separated format instead of identifier/apiName
3. Function call IDs were off by one due to missing message item

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(openapi): exercise real ResponsesService in regression tests

Replace local reimplementation with vi.mock stubs + real class import
so the tests fail if the production extractOutputItems regresses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 15:08:58 +08:00
Neko 40d0825d79 🐛 fix(agent,topic): should reset agent side panel if agent state changes (#13556) 2026-04-04 03:46:45 +08:00
Neko ea725aca9e test(agentDocuments): incorrect assertion against agent document (#13552) 2026-04-03 23:32:39 +08:00
Innei dbdbe16da9 ♻️ refactor: move skills/tools to @ mention with direct context injection (#13419)
* ♻️ refactor: move skills/tools from slash menu to @ mention with direct context injection

Separates slash menu (/) and @ mention responsibilities:
- Slash menu: only commands (compact, newTopic)
- @ mention: agents, topics, skills, tools

Replaces fake activateSkill tool-call preload messages with direct
content injection via SelectedSkillInjector/SelectedToolInjector,
preventing models from mimicking activateSkill calls.

Fixes LOBE-6048

* ♻️ refactor: skip activateSkill when skill content already injected via selected_skill_context

Fixes LOBE-6048

* ♻️ refactor: align @-mention skills/tools with context injectors and preload

Made-with: Cursor

* 🐛 fix(chat): preserve editorData across queue and home input sends

* Update home send APIs and align related tests
2026-04-03 22:09:48 +08:00
Innei 5cd4e390e3 👷 build(model-bank): align pnpm setup with packageManager (#13545)
Remove hardcoded pnpm versions in the model-bank release workflow so CI uses the repository packageManager setting and avoids pnpm version mismatch failures. Also align release commit identity with the lobehub bot account used by other release workflows.

Made-with: Cursor
2026-04-03 21:13:43 +08:00
Rdmclin2 5c17a0d652 feat: bot related common features (#13483)
* chore: remove default platform header

* fix: clean speaker tag when copy

* fix: discord client memory leak

* feat: support tool display config

* fix: test case

* fix: lint error
2026-04-03 19:58:32 +08:00
Innei ec3dd471b1 👷 build(model-bank): add release workflow (#13384)
* 👷 build(model-bank): add release workflow

* 🐛 fix(model-bank): bundle business const for publish

* Remove ModelBank CI package rewrite before publish
2026-04-03 19:35:26 +08:00
Innei 1d7a0d6bd8 👷 build(desktop): remove nightly release channel (#13480)
* 👷 build(desktop): remove nightly release channel

* 🐛 fix(database): remove invalid tool_call_id from messages inserts in tests

* 🧪 test(desktop): fix updater channel migration mocks

* ♻️ refactor(desktop): migrate update channel in bootstrap

* ♻️ refactor(desktop): extract store migrations

* 🐛 fix(desktop): use custom store migration runner

* ♻️ refactor(desktop): split store migrations into files

* update

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: codex-514 <codex514@users.noreply.github.com>
2026-04-03 19:13:25 +08:00
Neko 71df4aa473 🐛 fix(agentDocuments): should fetch passively for agent documents (#13508) 2026-04-03 18:29:15 +08:00
renovate[bot] 48d14bfb7e chore(deps): update dependency electron to v39 [security] (#13527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-03 17:53:21 +08:00
dependabot[bot] 74bcf41fe8 build(deps-dev): bump electron from 41.0.2 to 41.0.3 in /apps/desktop (#13525)
Bumps [electron](https://github.com/electron/electron) from 41.0.2 to 41.0.3.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v41.0.2...v41.0.3)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 41.0.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 17:20:15 +08:00
Zhijie He 210f020092 💄 style: add wanxiang2.7 & keling ImageGen from Qwen (#13478) 2026-04-03 17:17:21 +08:00
suyua9 306691b4d7 docs: clarify WeChat channel availability 2026-04-03 17:03:11 +08:00
WangYK f531c65fbb 🐛 fix: align message sharing modal with topic sharing (#13003) 2026-04-03 16:15:07 +08:00
YuTengjing 6d742388fa 🐛 fix: hide copy link button when share visibility is private (#13537) 2026-04-03 15:42:46 +08:00
LiJian aec2d30506 ♻️ refactor: add the cronjob tools executiors (#13536)
* fix: add the cronjob tools executiors

* fix: should use mutate to refresh the cronjob when it changes && add it into backend runtime

* fix: add the lost deps

* fix: should await the delete servers
2026-04-03 15:21:32 +08:00
Rylan Cai eb086b8456 feat: support local device binding in lh agent run (#13277)
*  support device binding in lh agent run

*  align device binding tests with current behavior
2026-04-03 13:44:12 +08:00
LiJian 3dd91a04fa 🐛 fix: slove the lobehub skill cant use activator to active (#13534)
fix: slove the lobehub skill cant use activator to active
2026-04-03 12:26:38 +08:00
Rylan Cai 9264a9c66d ♻️ refactor(eval): + resume agent run (#13412)
* ♻️ refactor: support minimal execAgent resume flow

* ♻️ refactor execAgent resume to caller-owned continuation context

* 📝 fix execAgent topic metadata comment

* 🚚 revert non-essential public execAgent resume changes

* 🚚 narrow execAgent resume changes to internal service scope

* 🚚 keep execAgent resume scoped to internal service

* 📝 restore taskId in execAgent runtime appContext

*  add agent eval trajectory resume

* ♻️ route agent eval resume through workflow

* 🩹 tighten eval resume workflow semantics

* ♻️ refine agent eval resume semantics

* ♻️ simplify resume workflow dispatch

* wip: rm redundancy interfaces

* wip: trim code

* wip: remove unuse

* wip: add eval detail resume btn

* 🐛 fix: message chains

* 🐛 fix: incorrect steps & cost count

* 🐛 fix: should allow start from non-zero step

* 🐛 fix: batch resume

* 🐛 fix: import

* ♻️ restore retry visibility guard in eval case table

* 🐛 fix: should not check run status

* 🐛 fix agent eval resume test regressions

* 🐛 fix: allow retry pass@k trajectory

* 🐛 fix eval case thread messages during run

* 🐛 fix pass@k batch resume target resolution

* 🐛 fix eval resume thread state handling

* ♻️ simplify eval resume validation

* 🚑 fix lint:ts interface order

* wip: fix lint

* 🐛 enforce max steps per resumed eval thread

* 🐛 avoid topic-level max steps check for pass@k resumes
2026-04-03 12:17:57 +08:00
Arvin Xu f9f7283fec 🐛 fix(model-runtime): resolve Vertex AI $ref schema error and toolConfig incompatibility (#13524)
* 🐛 fix(model-runtime): resolve Vertex AI $ref schema error and toolConfig incompatibility

1. Dereference $ref in JSON Schema before sending to Google/Vertex AI — the memory
   tool manifest (from neko's recent refactor) uses $ref which Vertex AI rejects.
2. Skip includeServerSideToolInvocations for Vertex AI — only Google AI supports it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): preserve sibling schema fields when resolving $ref

When a schema node has $ref plus sibling keys (e.g. description from
allOf unwrapping), the resolved definition now merges with those siblings
instead of dropping them. This preserves argument-level descriptions for
fields like timeIntent, improving tool-call quality.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:06:27 +08:00
Arvin Xu 25e851b359 🔒 fix: sanitize HTML artifact content and sandbox iframe to prevent XSS-to-RCE (#13529)
* 🔒 fix: sanitize HTML artifact content and sandbox iframe to prevent XSS-to-RCE

- Add sanitizeHTMLContent() using DOMPurify to strip dangerous tags (script, embed, object, etc.) and all on* event handler attributes
- Add sandbox="" attribute to HTML artifact iframe to block all script execution and parent frame access
- Replace doc.write() with srcDoc for cleaner rendering
- Extract shared FORBID_EVENT_HANDLERS list to DRY up SVG and HTML sanitization

Ref: GHSA-xq4x-622m-q8fq

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: correct import path from @lobehub/utils to @lobechat/utils

The package name is @lobechat/utils, not @lobehub/utils. This caused a build failure in Electron desktop app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:05:54 +08:00
Tsuki f2a95f9ae6 🔨 chore: add Task store — service layer, selectors, and 4 slices (#13500)
 feat: add Task store with service layer, selectors, and 4 slices (LOBE-6597)

Implement frontend Task system state management:
- Service layer wrapping all TRPC task/brief endpoints
- List slice: SWR fetch by agent, list/kanban view mode
- Detail slice: CRUD with optimistic updates, immer reducer
- Lifecycle slice: run/pause/cancel/complete/resume, heartbeat ping
- Config slice: checkpoint, review, brief ops (model config deferred to LOBE-6634)
- Selectors: list (kanban columns, display status), detail (field accessors, operation guards), activity (sorted/filtered)
- Types derived from TRPC inference (TaskListItem, TaskStatus)
- 118 tests across 9 test files

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 11:57:46 +08:00
Chris Z 4e0bcf1c4d 📝 docs: sync contributing guide branding (#13533) 2026-04-03 11:57:06 +08:00
Arvin Xu bbcb3304dc 📝 docs: add SECURITY.md with vulnerability reporting policy (#13528)
📝 docs: add SECURITY.md with vulnerability reporting policy

Define supported versions, reporting guidelines, response timeline, scope (in/out), and disclosure policy for security vulnerabilities.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:48:19 +08:00
Tsuki 3b316e3a4e 🐛 fix(task): include config in getTaskDetail response (#13521) 2026-04-03 09:49:35 +08:00
Arvin Xu 251e12c7d1 feat(editor): implement AI input auto-completion (#13458)
*  feat: implement AI input auto-completion with ReactAutoCompletePlugin

Adds GitHub Copilot-style ghost text completion to the chat input,
powered by a configurable system agent (disabled by default).

Key changes:
- Add `inputCompletion` system agent config (type, default, selector, i18n)
- Create `chainInputCompletion` prompt chain (V2 few-shot, benchmarked)
- Mount `ReactAutoCompletePlugin` in InputEditor when enabled
- Wire `getMessages` through ChatInput store for conversation context
- Add settings UI in Service Model page with enable toggle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: update systemAgent snapshot for inputCompletion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: restrict auto-complete context to visible user/assistant turns

Filter getMessages to use displayMessages (active visible thread)
instead of dbMessages (raw DB records including tool messages and
inactive branches). Also limit to last 10 user/assistant turns to
keep payload small and relevant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: enable input completion by default

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ️ perf: use non-streaming for input completion requests

Autocomplete needs the full result before displaying ghost text,
so streaming adds unnecessary overhead. Setting stream: false
reduces latency by avoiding SSE chunking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: revert stream:false for input completion

fetchPresetTaskResult uses fetchSSE internally which cannot handle
non-streaming JSON responses, causing the editor to freeze after
receiving the completion result.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ️ perf: use non-streaming for input completion requests

Autocomplete waits for the full result before displaying ghost text.
fetchSSE handles non-streaming responses via its fallback path
(response.clone().text()), avoiding SSE chunking overhead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ️ perf: skip contextEngineering for input completion

Call getChatCompletion directly instead of fetchPresetTaskResult
to avoid triggering agentDocument.getDocuments on every autocomplete
request. Input completion only needs a simple LLM call with the
prompt chain, not the full context engineering pipeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: revert to fetchPresetTaskResult for input completion

Use the standard contextEngineering pipeline. The agentDocument
overhead will be addressed separately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 02:00:18 +08:00
Arvin Xu 3b13a1b6d4 🐛 fix: expose tool discovery config to context engine and inject available tools (#13417)
♻️ refactor: extract server tool discovery config builder
2026-04-03 01:54:22 +08:00
Arvin Xu 126db9612f 🐛 fix: stream tool call arguments incrementally in Response API (#13506)
* 🐛 fix: stream tool call arguments incrementally in Response API

The tool_calling stream chunks contain accumulated arguments (not
deltas), but the Response API was treating each chunk as a complete
independent output_item — creating a new lifecycle (added → delta →
done) per token and incrementing output_index to 90+.

Fix: track active tool calls by call_id and compute true incremental
deltas by slicing off previously-seen content. Each tool call now
gets a single stable output_item with proper streaming deltas,
finalized only when the stream ends or tool execution begins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: clear stale tool-call state on LLM stream retry

When call_llm retries after a failed attempt, activeToolCalls may
contain entries from the failed stream that never received a
tool_end. Without clearing, finishActiveToolCalls would emit
phantom function_call done events and misalign output_index for
the successful attempt. Reset the map on stream_retry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 01:46:14 +08:00
Arvin Xu dd7819b1be 🔨 chore(cli): register task command and add kanban board view (#13511)
*  feat(cli): register task command and add kanban board view

Register the missing `registerTaskCommand` in program.ts so `lh task` commands are accessible. Add `--board` flag to `task list` that renders a kanban-style view grouping tasks by status columns (backlog, running, paused, completed, etc.) with color-coded borders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:54:12 +08:00
Arvin Xu 3415df3715 ♻️ refactor: remove chat-plugin-sdk (#13512)
* ♻️ refactor: remove @lobehub/chat-plugin-sdk dependency

Plugins have been deprecated. This removes the SDK entirely:

- Define built-in ToolManifest, ToolManifestSettings, ToolErrorType types
- Delete src/features/PluginsUI/ (plugin iframe rendering)
- Delete src/store/tool/slices/oldStore/ (deprecated plugin store)
- Delete src/server/services/pluginGateway/ (plugin gateway)
- Delete src/app/(backend)/webapi/plugin/gateway/ (plugin API route)
- Migrate all ~50 files from SDK imports to @lobechat/types
- Remove @lobehub/chat-plugin-sdk, @lobehub/chat-plugins-gateway deps
- Remove @swagger-api/apidom-reference override and patch

Fixes LOBE-6655

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add missing getInstalledPlugins mock in customPlugin test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore: increase Vercel build memory limit to 8192MB

The 6144MB limit was causing OOM during Vite SPA chunk rendering.
Aligned with other build commands that already use 8192MB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: unify default tool type to builtin and fix CustomRender

- Remove `invokeDefaultTypePlugin` — default type now falls through to builtin in both server and client execution paths
- Fix `CustomRender` to actually render builtin tool components via `getBuiltinRender` instead of always returning null
- Increase SPA build memory limit from 7168MB to 8192MB to fix OOM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: remove legacy plugin gateway and type-specific invocations

- Delete `runPluginApi`, `internal_callPluginApi`, `invokeMarkdownTypePlugin`, `invokeStandaloneTypePlugin`
- Remove plugin gateway endpoint (`/webapi/plugin/gateway`) from URL config
- Remove special `builtin → default` runtimeType mapping in plugin model
- Clean up unused imports and related tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add 'builtin' to runtimeType union to fix type error

Use ToolManifestType instead of inline union for runtimeType fields
so that 'builtin' is included as a valid type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 00:46:19 +08:00
YuTengjing 0dc8930750 🔨 chore: update team assignment and fix prompt formatting (#13520) 2026-04-03 00:40:45 +08:00
Zhijie He 9f2d7daa17 💄 style: add more videoGen provider support (#13428)
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-04-03 00:37:15 +08:00
YuTengjing 249483c3e1 🔨 chore: skip PR welcome comment for maintainer (#13519) 2026-04-02 23:42:28 +08:00
YuTengjing eb2731183f 🔨 chore(i18n): remove unused suspectedReason locale key (#13517) 2026-04-02 22:21:00 +08:00
YuTengjing d9c50b97f8 🐛 fix(database): apply injectSearchSettings consistently for unmodified builtin models (#13514) 2026-04-02 21:48:02 +08:00
Innei 8b445a1dc3 refactor: consolidate imports and add electron styling to theme/lang buttons (#13495)
🐛 fix(electron): add nodrag to userinfo dropdown menus

Add `-webkit-app-region: no-drag` to ThemeButton and LangButton
dropdown popups to prevent Electron from capturing click events
when the dropdown appears in the titlebar drag region.

https://claude.ai/code/session_01K6FLLJ4PMhKWqbRmrGEZkS

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-02 20:27:21 +08:00
Arvin Xu be99aaebd0 ♻️ refactor: unify tool content formatting with ComputerRuntime and shared UI (#13470)
* ♻️ refactor: unify tool content formatting with ComputerRuntime and shared UI components

Introduce `@lobechat/tool-runtime` with `ComputerRuntime` abstract class to ensure consistent
content formatting (via `formatCommandResult`, `formatFileContent`, etc.) across local-system,
cloud-sandbox, and skills packages. Create `@lobechat/shared-tool-ui` to share Render and
Inspector components, eliminating duplicated UI code across tool packages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: address review issues — state mapping for renders and IPC param denormalization

- Add legacy state field mappings in local-system executor (listResults, fileContent,
  searchResults) for backward compatibility with existing render components
- Add denormalizeParams in LocalSystemExecutionRuntime to map ComputerRuntime params
  back to IPC-expected field names (file_path, items, shell_id, etc.)
- Fix i18n type casting for dynamic translation keys in shared-tool-ui inspectors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: inject render capabilities via context, unify state shape for cross-package render reuse

- Add ToolRenderContext with injectable capabilities (openFile, openFolder,
  isLoading, displayRelativePath) to shared-tool-ui
- Update local-system render components (ReadLocalFile, ListFiles, SearchFiles,
  MoveLocalFiles, FileItem) to use context instead of direct Electron imports
- Enrich ReadFileState with render-compatible fields (filename, fileType,
  charCount, loc, totalCharCount)
- Cloud-sandbox now fully reuses local-system renders — renders degrade
  gracefully when capabilities are not provided (no open file buttons in sandbox)
- Remove executor-level state mapping hacks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: fix sandbox render bugs — SearchFiles, GrepContent, MoveFiles, GlobFiles

- SearchFiles: ensure results is always an array (not object passthrough)
- GrepContent: update formatGrepResults to support object matches
  `{path, content, lineNumber}` alongside string matches
- MoveFiles: render now handles both IPC format (items/oldPath/newPath) and
  ComputerRuntime format (operations/source/destination)
- GlobFiles: fallback totalCount to files.length when API returns 0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: unify SearchLocalFiles inspector with shared factory

SearchLocalFiles inspector now supports all keyword field variants
(keyword, keywords, query) and reads from unified state (results/totalCount).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: handle missing path in grep matches to avoid undefined display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: improve render field compatibility for sandbox

- EditLocalFile render: support both file_path (IPC) and path (sandbox) args
- SearchFiles render: support keyword/keywords/query arg variants
- FileItem: derive name from path when not provided

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add missing cloud-sandbox i18n key for noResults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:42:45 +08:00
Arvin Xu f96edd56fb 🔨 chore(task): add task.groupList API for kanban view (#13507)
*  feat(task): add task.groupList API for kanban board view

Support querying tasks grouped by status in a single request, with per-group independent pagination. Returns array structure with hasMore/limit/offset for each group.

LOBE-6589

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(task): bound groupList groups and statuses array size

Prevent query storms from oversized requests by capping groups to 20
and statuses per group to 10.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔧 chore(task): reduce groupList max groups from 20 to 10

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:38:12 +08:00
Arvin Xu 074de037cd 🔨 chore(task): add generic updateTaskConfig for safe config merging (#13502)
*  feat(task): add generic updateTaskConfig method for safe config merging

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add updateTaskConfig tests and use deep merge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 18:41:23 +08:00
YuTengjing 297c884b88 🐛 fix(model-runtime): ensure before* hook errors trigger on*Error handlers (#13496) 2026-04-02 16:12:15 +08:00
Arvin Xu 04b32e3152 🔨 chore: add agent avatar data to brief list API (#13489)
*  feat: add agent avatar data to brief list API

Enrich brief list and listUnresolved endpoints with agent avatars
from the task tree. For each brief's associated task, walks up to
find the root task, then collects all agents (assignee + creator)
across the full tree.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add BriefService and unit tests for brief agent enrichment

Extract enrichBriefsWithAgents logic into BriefService for reuse.
Add unit tests for TaskModel.getTreeAgentIdsForTaskIds,
AgentModel.getAgentAvatarsByIds, and BriefService.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔒 fix: scope recursive CTE to current user in getTreeAgentIdsForTaskIds

Add created_by_user_id filter to both the ancestor walk-up and
descendant walk-down recursive legs to prevent cross-tenant tree
traversal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 15:41:08 +08:00
Zhijie He bbd09d6785 💄 style: add glm-5v-turbo support (#13487) 2026-04-02 14:39:07 +08:00
Neko 6a2ca59592 ♻️ refacor(database,userMemories): rework of user memory search (#13453) 2026-04-02 14:13:06 +08:00
LiJian 8aeb47eda3 🐛 fix: should clean up tools when the old tools is deprecated (#13492)
* fix: should clean up tools when the old tools is deprecated

* fixshould try delete incetent first
2026-04-02 12:39:51 +08:00
LiJian da1bccfd20 🐛 fix: slove the creds detail page cant modify the kv creds (#13473)
fix: slove the creds detail page cant modify the kv creds
2026-04-02 12:36:58 +08:00
LobeHub Bot 03c7a3fd42 🌐 chore: translate non-English comments to English in database messages tests (#13491)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 11:57:33 +08:00
Innei be8903e707 refactor: Extract web onboarding runtime to builtin package (#13446)
*  feat: add server runtime for lobe-web-onboarding tool

Implement server-side execution runtime for onboarding agent tools, enabling them to run in server environment without client-side dependencies.

https://claude.ai/code/session_01Das8jWLe5ibm6vJUFGu6Xb

* ♻️ refactor: deduplicate web onboarding utils by exporting from package

Move formatWebOnboardingStateMessage, createDocumentReadResult, createWebOnboardingToolResult, and EMPTY_DOCUMENT_MESSAGES into @lobechat/builtin-tool-web-onboarding/utils and update all consumers to import from there.

https://claude.ai/code/session_01Das8jWLe5ibm6vJUFGu6Xb

* 🔧 fix: sort imports in webOnboardingToolResult test

https://claude.ai/code/session_01Das8jWLe5ibm6vJUFGu6Xb

* 🔧 fix: sort imports with eslint --fix

https://claude.ai/code/session_01Das8jWLe5ibm6vJUFGu6Xb

* 🐛 fix: add missing properties to OnboardingStateContext interface

https://claude.ai/code/session_01Das8jWLe5ibm6vJUFGu6Xb

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-02 02:19:26 +08:00
Innei d8534c2966 🐛 fix(chat-input): preserve fullscreen editor state and send behavior (#13481)
* 🐛 fix(chat-input): preserve editor state and behavior in fullscreen

Keep chat input content and interaction consistent when toggling fullscreen by restoring editor JSON state, adjusting Enter/Cmd+Enter semantics, and rendering fullscreen input in the desktop layout container.

Made-with: Cursor

* 🐛 fix(chat-input): auto-collapse fullscreen after send

Automatically exit fullscreen after sending from chat input so users do not need a second manual collapse action, and clear saved editor snapshot to avoid stale restore.

Made-with: Cursor
2026-04-02 02:13:15 +08:00
Innei d25db6e6f8 🐛 fix(conversation): hide loading placeholder when AI generation is stopped (#13482)
🐛 fix: hide loading placeholder when AI generation is stopped

Only render ContentLoading for LOADING_FLAT messages when actively generating.
Previously, stopping AI mid-generation left the "..." placeholder visible
with a loading animation even though nothing was being generated.
2026-04-02 01:44:45 +08:00
YuTengjing df6d8f19f8 🔒 fix: upgrade nodemailer to v8 to fix SMTP command injection (#13479) 2026-04-01 21:51:32 +08:00
Arvin Xu 8af28a778b 🐛 fix(fetch-sse): stop injecting contextBody into structured provider errors (#13477)
* 🐛 fix(fetch-sse): stop injecting contextBody into structured provider errors

Structured errors (ProviderBizError etc.) already contain complete context.
Spreading contextBody into their body overwrites fields like `provider` and
pollutes the error structure that downstream renderers depend on.

Fixes #13476

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(fetch-sse): add regression test for structured error body pollution

Ensures structured provider errors (e.g. ProviderBizError) are passed through
unchanged without contextBody injection, and that contextBody is only applied
to unknown/unstructured errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 21:24:01 +08:00
Innei 6ecae1bbd1 ♻️ refactor: gate agent onboarding with dedicated business flag (#13472)
* ♻️ refactor: gate agent onboarding with dedicated business flag

Made-with: Cursor

* 🗑️ chore(migrations): remove agent onboarding column from users table

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): enable agent onboarding based on environment and add redirect to classic onboarding

- Updated AGENT_ONBOARDING_ENABLED to be true in development mode.
- Introduced RedirectToClassicOnboarding component to handle navigation to classic onboarding.
- Simplified ClassicOnboardingPage by removing the mode switch button for non-development environments.
- Adjusted OnBoardingContainer to conditionally render the skip onboarding button based on the current route.

This change enhances the onboarding experience by ensuring that the agent onboarding feature is only available in development, while also improving navigation for users.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(test): inline emoji-mart and @lobehub/* deps in Vitest to fix ESM JSON import error

Widen server.deps.inline to include `emoji-mart` and all `@lobehub/*`
packages so their transitive `@emoji-mart/data` import (a .json main
entry) goes through Vite's transform pipeline instead of Node's native
ESM loader, which requires `with { type: "json" }`.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-04-01 19:38:14 +08:00
Arvin Xu 60a59e89f6 🛠 chore(fetch-sse): preserve legacy body.message and body.name keys for compatibility (#13469)
Restores the original body.message / body.name fields that downstream error
handlers rely on. The previous PR renamed them to errorMessage / errorName
which broke existing error renderers.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:04:37 +08:00
Arvin Xu 7fd6d67fe3 🐛 fix(model-runtime): add toolConfig for Gemini 3+ combined tools (#13465)
* 🐛 fix(model-runtime): add toolConfig for Gemini 3+ combined tools

When Gemini 3+ models combine built-in tools (googleSearch/urlContext)
with functionDeclarations, the API requires
toolConfig.includeServerSideToolInvocations to be set to true.

Without this flag, requests return 400: "Please enable
tool_config.include_server_side_tool_invocations to use Built-in tools
with Function calling."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test(google): fix incomplete grounding metadata test

The test defined grounding response data but never used it as mock
input and had no assertions. Rewrote to properly feed grounding chunks
through the stream and verify the output contains grounding events
with citations and search queries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(test): use type assertion for grounding test data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 15:03:23 +08:00
Arvin Xu 453db9f165 🔨 chore(fetch-sse): enrich error context with provider, model, and network diagnostics (#13468)
 feat(fetch-sse): enrich error context with provider, model, and network diagnostics

When a fetch error occurs (e.g. TypeError: Failed to fetch), the error body now
includes provider, model, apiMode, fetchOnClient, elapsedMs, networkStatus, and
traceId to help diagnose issues instead of only showing a useless minified stack.

Fixes LOBE-6594

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:58:14 +08:00
LiJian 19f90e3d9a ♻️ refactor: change the klavis github tools into lobehub skill & add vercel skills (#13442)
* refactor: change the klavis github tools into lobehub skill & add the vercel skill

* fix: slove the test & topicid parse
2026-04-01 14:48:16 +08:00
Arvin Xu fee0fe5699 🔨 chore: add disableTools option to execAgent (#13454)
*  feat: add disableTools option to execAgent for eval/benchmark scenarios

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: short-circuit tool discovery when disableTools is set

Move all tool-related fetches (plugin DB query, LobeHub/Klavis manifest
fetches, device list probing, model-bank import) inside the disableTools
guard so they are fully skipped in eval/benchmark runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add unit tests for disableTools short-circuit behavior

Verify that when disableTools=true, all expensive tool discovery
(plugin query, manifest fetches, ToolsEngine creation) is skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: hoist variables referenced outside disableTools guard

Move lobehubSkillManifests, klavisManifests, agentPlugins, and
LOBE_DEFAULT_MODEL_LIST declarations outside the else block since
they are also used by agent management context and skill engine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:28:59 +08:00
Arvin Xu 88246e5719 🔨 chore: support per-task model/provider override via task.config (#13466)
*  feat: support per-task model/provider override via task.config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract agent execution types into dedicated agentExecution module

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add unit tests for execAgent model/provider override

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 14:25:24 +08:00
Innei aaefe6c0d2 👷 chore(ci): unify CI package manager from bun to pnpm (#13464)
* 👷 chore(ci): unify CI package manager from bun to pnpm

Replace bun with pnpm across all GitHub Actions workflows to ensure
lockfile consistency with pnpm-lock.yaml as single source of truth.

* 👷 chore(ci): replace bun run with pnpm run in package.json scripts

Fix build failure in CI where bun is not installed. Replace bun run
references in root and e2e package.json scripts with pnpm run.

* 👷 chore(e2e): replace bunx with npx in e2e server startup

* 👷 chore(ci): create unified setup-env action, use pnpm install + bun run

- Add .github/actions/setup-env composite action (pnpm + bun + node)
- Refactor desktop-build-setup to use setup-env internally
- All workflows: pnpm install for deps, bun run for scripts
- Revert package.json/e2e scripts back to bun run
- Remove all direct pnpm/action-setup and oven-sh/setup-bun from workflows

* 🐛 fix(test): inline lexical ESM deps for vitest under pnpm

pnpm's strict node_modules layout causes vitest ESM resolution to fail
for lexical's named exports. Add lexical and @lexical/* to inline deps.
2026-04-01 14:08:37 +08:00
Arvin Xu cbc9bfccaa 💄 style: show live elapsed timer during tool execution (#13437)
*  feat: show live elapsed timer during tool execution

Display a real-time elapsed timer on tool call inspector while the tool is executing.
The timer automatically hides once execution completes.

Fixes LOBE-6331

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: stop execution timer for rejected tool calls and reset elapsed on restart

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:34:54 +08:00
Rylan Cai 3e056ad37a 🔧 chore:(web-browsing): OOM debug logs (#13452)
* 🔧 add oom debug logging for web browsing search

* wip: trim

* 🔧 minimize web browsing oom debug logs

* ♻️ revert incidental crawl error check change

* 🔧 refine web browsing oom tracing

* 🔧 polish oom memory logs

* ♻️ inline crawler impl fallback in caller

* 🐛 guard memory snapshot behind debug enablement
2026-04-01 00:22:48 +08:00
Innei 46bac5b540 🐛 fix(utils): auto-reload on chunk load error (#13450)
🐛 fix: auto-reload on chunk load error instead of showing toast

When a chunk fails to load the old version is already unusable,
so reload the page automatically. Uses sessionStorage guard to
prevent infinite reload loops.

Fixes LOBE-6572
2026-03-31 23:56:20 +08:00
Innei 57ed8f8541 ♻️ refactor(tool): decouple topic-reference executor from app TRPC client (#13451)
♻️ refactor(tool): inject topic reference runtime in app layer

Move topic-reference executor to runtime injection so package code no longer imports app-level TRPC client aliases. Keep the TRPC call in store executor wiring for clear package/app boundaries.

Made-with: Cursor
2026-03-31 23:27:42 +08:00
LobeHub Bot 132893549a test: add unit tests for TaskService (#13432)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 22:14:50 +08:00
sxjeru d717d5da20 🐛 fix: update payload handling for OpenRouter image models (#10622)
*  feat: add imageAspectRatio support and update payload handling for image models

*  feat: enhance image model handling and support imageAspectRatio configuration

*  feat: add support for new image model "Nano Banana 2" and enhance image configuration handling

*  feat: add 'thinkingLevel4' to extendParams and improve image configuration handling

*  feat: add new AI models including DeepSeek V3.2 and Ministral 3 series, enhancing model capabilities and configurations

*  feat: update context window tokens and add new models in AIChatModelCard

*  feat: update Mistral model IDs and add new models; change AiHubMix base URL to API endpoint
2026-03-31 22:12:52 +08:00
Innei 58fa4f869d feat(chat): intervention fallback UI, GTD default tools, intervention guard order (#13447)
*  Improve intervention fallback UI; add GTD to default tools; defer unknown-tool guard

- Fallback intervention: show tool/action titles, collapsible parameters with i18n
- Register GTD manifest in defaultToolIds for shared tool list
- Run unknown-tool intervention only after per-tool resolver (auto-run skips early)
- TodoProgress: horizontal margin and top corner radii

Made-with: Cursor

* 🌐 chore(i18n): sync default keys and locale JSON across namespaces

Align knowledgeBase, labs, memory, notification, portal, thread, models, and chat bundles with default sources.

Made-with: Cursor
2026-03-31 22:07:58 +08:00
Rdmclin2 32e36e330a 🔨 chore: optimize message tool (#13444)
* chore: adjust electron testing to local testing

* chore: comprehence discord docs

* chore: add common capture window

* chore: default enable message tool in bot conversation

* fix: discord readMessages error

* chore: optimize readMessages prompt

* chore: optimize limit description

* chore: optimize limit size

* chore: remove limit parameter for discord

* chore: add threadRecover  Patch

* chore: optimize system role and bot context

* fix: avoid overide user config message tool

* chore: add default timeout
2026-03-31 21:28:18 +08:00
Innei ee8cab8305 🐛 fix: set context before replaceMessages in StoreUpdater layout effect (#13421)
🐛 fix: set context before replaceMessages in layout effect

replaceMessages calls onMessagesChange(messages, get().context) internally.
Without updating context first, it writes new topic's messages to the old
topic's key in ChatStore, corrupting cached data.
2026-03-31 20:47:33 +08:00
Innei 393653e20c ⬆️ chore: bump Lexical to 0.42 and align editor imports (#13440)
* ⬆️ chore: bump Lexical to 0.42 and align editor imports

- Bump lexical and @lexical/utils; pin lexical in pnpm overrides
- Return serialized nodes from ActionTag/ReferTopic XML readers (no INodeHelper require)
- Drop IEditorPlugin implements; import MenuRenderProps and IEditor from @lobehub/editor barrel

Made-with: Cursor

*  chore: add lexical dependency version 0.42.0 to package.json

Signed-off-by: Innei <tukon479@gmail.com>

*  test: enhance MCPClient Stdio Transport tests with local stdio entry

- Updated the test configuration to use a local stdio entry instead of `npx`, improving test reliability in CI environments.
- Added necessary imports for path resolution to support the new configuration.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-31 20:45:32 +08:00
Hardy 560f598789 🐛 fix(glmCodingPlan): update default URL and add GLM-5.1 model (#13405)
*  feat(glmCodingPlan): update default URL and add GLM-5.1 model

- Change default URL to open.bigmodel.cn/api/coding/paas/v4
- Add GLM-5.1 model with 200K context window, 128K max output, reasoning support

* 🐛 fix: update test baseline URL for GLM Coding Plan provider
2026-03-31 18:42:40 +08:00
LobeHub Bot 993dfe1bb0 🌐 chore: translate non-English comments to English in packages (#13427)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 18:11:39 +08:00
Arvin Xu 967302269e 🐛 fix: support multiple artifacts rendering in the same message (#13436)
* 🐛 fix: support multiple artifacts rendering in the same message

When a message contains multiple `<lobeArtifact>` tags, only the first one
rendered correctly. The rest stayed in loading state or showed incorrect content.

Root causes:
- processWithArtifact used non-global regex, only removing newlines from first artifact
- artifactCode selector only extracted first artifact's content
- isArtifactTagClosed returned true if ANY artifact was closed
- Render onClick compared only messageId, closing portal instead of switching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add comprehensive tests for multiple artifacts rendering

- rehypePlugin: test multiple artifact tags in same tree (both p-wrapped and raw)
- action: test openArtifact switching between artifacts (same message, different messages)
- selectors: test artifactCode/isArtifactTagClosed with identifier edge cases
  (non-existent identifier, unclosed artifact, both closed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve type error in rehypePlugin test

Cast tree.children elements to any when accessing tagName property
to fix TS2339 error in the raw node test case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: escape regex special characters in artifact identifier

Artifact identifiers interpolated directly into `new RegExp()` could cause
SyntaxError or incorrect matching when containing regex metacharacters
like (, [, +, etc. Now escapes identifiers before building regex patterns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:21:15 +08:00
Arvin Xu 674c849254 feat: support client-side function tool execution in Response API (#13414)
*  feat: support client-side function tool execution in Response API

Implement LOBE-6543: when the Response API receives tools with type='function',
inject them into the LLM and pause execution when the LLM calls them, allowing
the client to provide results via function_call_output input items.

Key changes:
- Add 'client' to ToolSource type
- Inject function tools into LLM via execAgent with source='client' in sourceMap
- Pause agent loop (interrupt) when LLM calls a client function tool
- Handle function_call_output resume flow via previous_response_id
- Add response.function_call_arguments.done streaming event
- Emit response.incomplete when interrupted for client tool execution
- Use original function name for client tools instead of identifier/apiName
- Simplify response ID to use topicId directly (includes LOBE-6536 fix)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: remove MessageModel import, use prompt-based resume flow

MessageModel is not exported from @lobechat/database package.
Replace direct DB writes with prompt-based approach for tool result resume.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use separator-safe client function identifier and add client to ToolSource

CLIENT_FN_IDENTIFIER `__fn__` caused ambiguous splits with PLUGIN_SCHEMA_SEPARATOR `____`,
breaking tool name resolution. Renamed to `lobe-client-fn` and added `client` to the
ToolSource union in @lobechat/types to match context-engine's definition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:24:39 +08:00
René Wang f327e377a6 feat: update changelog (#13430)
* feat: Update changelog

* fix: changelog images missing
2026-03-31 14:58:04 +08:00
Rylan Cai e7be5b1928 🔧 chore: adjust eval qstash runtime retries (#13364)
* 🔧 tune eval qstash runtime retries

* 🔧 smooth eval qstash retry delay

* 🔧 persist eval qstash retry telemetry

* ♻️ trim hook types formatting noise

* 🗑️ remove eval retry telemetry passthrough

* 🚚 restore hook event spacing
2026-03-31 14:12:11 +08:00
Arvin Xu b54a41968d 🐛 fix(model-runtime): allow Gemini 3+ to combine search tools with function declarations (#13429)
* 🐛 fix(model-runtime): allow Gemini 3+ models to combine search tools with function declarations

Gemini 3+ models support urlContext, googleSearch, and functionDeclarations coexisting in the tools array. Previously, enabling search/urlContext would exclude function declarations (MCP tools/skills), causing them to silently fail.

Fixes LOBE-6450

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(model-runtime): restore hasToolCalls guard for pre-Gemini 3 multi-turn tool sessions

Restores the hasToolCalls check for pre-Gemini 3 models so that when
tool_calls exist in message history, functionDeclarations are prioritized
over search tools to maintain multi-turn tool-calling sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 12:34:48 +08:00
Zhijie He f39f5e9fd6 🐛 fix: fix lmstudio api key field has been hidden (#12678)
fix: fix lmstudio api key field has been hidden

env: support api key env
2026-03-31 12:17:16 +08:00
Rylan Cai 7be18092d3 ♻️ refactor: Agent Runtime stability improvement (#13257)
*  feat: add tool error kind classification and runtime retry dispatch

*  feat: add llm retry loop and stream reset event

* 🚑 stop retrying unknown tool execution errors

* ♻️ reduce runtime executor diff noise

* ♻️ restore runtime executor context comments

* ♻️ compress runtime executor retry diff

*  add llm retry backoff

* ♻️ tighten llm error classification kinds

* ♻️ tighten retry test assertions

* 🐛 stop llm retry after operation interruption

*  fix runtime executor retry stream test

* 🐛 stop retries after operation interruption

* 🐛 stop retrying provider invalid_request llm errors

* wip: reset

*  sync runAgent test with canary expectations
2026-03-31 11:18:30 +08:00
sxjeru c60c02bcfe 🐛 fix: correct extend params reasoning payloads and persist cleared model settings & add MiniMax M2.7 (#12760)
* fix: 允许单独传递 thinking.budget_tokens 参数

* fix: 添加 normalizeExtendParamsValue 函数并更新 ExtendParamsSelect 组件逻辑

* add new GPT-5.4 mini and nano models to AIChatModelCard array

* 🐛 fix: update DEFAULT_MINI_MODEL to gpt-5.4-mini

* 🐛 fix: update model references to gpt-5.4-mini in tests and snapshots

* 🐛 fix: 移除 MiniMax-M2.1 模型的定义

* feat: 添加 MiniMax M2.7 和高速度模型,更新现有模型定价和描述

* typo

* feat: 添加 MiniMax M2.7 和 MiMo V2 系列模型,更新模型能力和定价

* fix test

*  feat: update NVIDIA chat models with new entries and enhanced descriptions

* feat: 添加 Qwen3.5 Omni Plus 和 Qwen3.5 Omni Flash 模型,更新模型能力和定价
feat: 更新响应 API 模型,添加 gpt-5.4-mini 和 gpt-5.4-nano
2026-03-31 11:01:32 +08:00
Zhijie He ec3443d1db 💄 style: add qwen3.5-omni series (#13422)
style: add qwen3.5-omni series
2026-03-31 10:05:29 +08:00
Arvin Xu e76ab1f990 💄 style: mount DynamicFavicon for agent operation favicon switching (#13416)
*  feat: mount DynamicFavicon to enable favicon state switching during agent operations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add favicon link tags to SPA HTML templates and handle missing links in updateFaviconDOM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:25:18 +08:00
Innei c59c066330 🐛 fix(intervention): resolve InterventionBar context errors, rendering, and topic transition issues (#13420)
* 🐛 fix: resolve InterventionBar context errors and rendering issues

- Replace useMessageAggregationContext with prop drilling for assistantGroupId,
  fixing crash when ApprovalActions renders outside MessageAggregationContext
- Filter out tmp_ message IDs from pending interventions to prevent
  disabled buttons during message creation
- Portal ApprovalActions outside scroll container in InterventionBar
  so buttons are always accessible for long content
- Clear stale displayMessages synchronously on topic change to prevent
  old interventions from persisting during transitions

* 🐛 fix: use useLayoutEffect to clear stale interventions on topic switch

Replace render-phase side effect with useLayoutEffect to properly clear
displayMessages before browser paint when context changes, preventing
old topic interventions from flashing during transitions.

* 🐛 fix: synchronously reset store on context change to prevent stale data flash

Use React's "setState during render" pattern instead of useLayoutEffect.
When contextKey changes, React bails out and re-renders StoreUpdater
before rendering sibling components (ChatList/ChatInput), ensuring they
read fresh store state with no visible flash of old topic data.

* 🐛 fix: remount store on context change to eliminate stale data flash

Add key={contextKey} to zustand Provider so the store is recreated on
topic switch. Seed the new store with initialMessages in createStore to
render correct data on first mount — no intermediate skeleton or stale
flash. Remove render-phase reset hack from StoreUpdater as it's no
longer needed.

* 🐛 fix: revert Provider key approach, use useLayoutEffect for context reset

Provider key={contextKey} caused ChatHydration to remount and reset
activeTopicId from URL query, preventing topic switches entirely.

Reverted to stable Provider. Instead, use useLayoutEffect in StoreUpdater
to atomically reset displayMessages + messagesInit when contextKey changes.
This fires after commit but before paint, and React processes store updates
from layout effects synchronously, ensuring subscribers re-render with
correct state before the browser paints.
2026-03-31 02:57:56 +08:00
Innei 7097167613 🐛 fix(editor): add ReactMentionPlugin to ChatInput for mention node rendering (#13415)
🐛 fix: add ReactMentionPlugin to ChatInput so mention nodes render

The ChatInput editor plugins did not include ReactMentionPlugin, causing
mention nodes inserted via @ to be invisible. Move the plugin into
CHAT_INPUT_EMBED_PLUGINS so all ChatInput instances (including Home)
render mention nodes, and remove the now-duplicate entry from EditorCanvas.

Fixes LOBE-6270
2026-03-31 01:53:29 +08:00
Arvin Xu 2c2795e73a 🐛 fix: cli gateway auto reconnect (#13418)
* ♻️ refactor: move Marketplace below Resources in sidebar

Move the Marketplace (Community) nav item from topNavItems to bottomMenuItems,
positioning it below Resources in the sidebar navigation.

Closes LOBE-6320

* 🐛 fix(cli): auto-reconnect on auth expiry instead of exit

- Add `updateToken()` and `reconnect()` methods to GatewayClient
- On `auth_expired`, refresh JWT then reconnect automatically (no more process.exit)
- Add heartbeat ack timeout detection: force reconnect after 3 missed acks
- Reset missed heartbeat counter on `heartbeat_ack` receipt
- Add comprehensive tests for updateToken, reconnect, and missed heartbeat scenarios

Closes connection drop issue when JWT expires after long-running sessions.
2026-03-31 01:16:17 +08:00
Rdmclin2 965fc929e1 feat: add unified messaging tool for cross-platform communication (#13296)
*  feat: add cross-platform message tool for AI bot channel operations

Implement a unified message tool (`lobe-message`) that provides AI with
messaging capabilities across Discord, Telegram, Slack, Google Chat,
and IRC through a single interface with platform-specific extensions.

Core APIs: sendMessage, readMessages, editMessage, deleteMessage,
searchMessages, reactToMessage, getReactions, pin/unpin management,
channel/member info, thread operations, and polls.

Architecture follows the established builtin-tool pattern:
- Package: @lobechat/builtin-tool-message (manifest, types, executor,
  ExecutionRuntime, client components)
- Registry: registered in builtin-tools (renders, inspectors,
  interventions, streamings)
- Server runtime: stub service ready for platform adapter integration

https://claude.ai/code/session_011sHc6R7V4cSYKere9RY1QM

* feat: implement platform specific message service

* chore: add wechat platform

* chore: update wechat api service

* chore: update protocol implementation

* chore: optimize  platform api test

* fix: lark domain error

* feat: support bot message cli

* chore: refactor adapter to service

* chore: optimize bot status fetch

* fix: bot status

* fix: channel nav ignore

* feat: message tool support bot manage

* feat: add lobe-message runtime

* feat: support direct message

* feat: add history limit

* chore: update const limit

* feat: optimize  server id message history limit

* chore: optimize system role & inject platform environment info

* chore: update  readMessages vibe

* fix: form body width 50%

* chore: optimize tool prompt

* chore: update i18n files

* chore: optimize read message system role and update bot message lh

* updage readMessage api rate limit

* chore: comatible for readMessages

* fix: feishu readMessage implementation error

* fix: test case

* chore: update i18n files

* fix: lint error

* chore: add timeout for conversaction case

* fix: message test case

* fix: vite gzip error

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 00:26:32 +08:00
Innei 491aba4dbd ♻️ refactor(store): class-based Zustand actions with flattenActions (#13383)
♻️ refactor(store): migrate slices to class actions with flattenActions

- Video store: generationConfig/Topic/Batch/createVideo as *ActionImpl; aggregate with flattenActions
- Eval store: benchmark/dataset/run/testCase as classes; top-level flattenActions
- Tool agentSkills: AgentSkillsActionImpl + Pick typing
- groupProfile: flattenActions around ActionImpl instead of spreading instance
- agentGroup: wrap chatGroupAction with flattenActions for consistent aggregation

Made-with: Cursor
2026-03-30 23:46:35 +08:00
Innei 6402656ec7 feat: use skill-specific icons in slash menu instead of generic wrench icon (#13401)
*  feat: use skill-specific icons in slash menu instead of generic wrench icon

Each skill/tool in the slash menu now displays its own avatar (emoji or image URL)
instead of the generic 🔧 wrench icon for all items.

https://claude.ai/code/session_01KbUecMiAUDHvFtEULkSDvr

* ♻️ refactor: use SkillsIcon as default slash menu skill icon

https://claude.ai/code/session_01KbUecMiAUDHvFtEULkSDvr

*  feat: enhance slash action item rendering and mention menu styles

- Updated `useSlashActionItems.ts` to improve icon rendering for URLs, now supporting blob and data-URI images.
- Modified `MenuItem.tsx` to conditionally apply additional styles for items with extra categories.
- Added new style for `itemWithCategoryExtra` in `style.ts` to enhance layout consistency.

These changes aim to improve the visual presentation and functionality of the chat input components.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(mention-menu): satisfy cx ClassNamesArg types in MenuItem

Made-with: Cursor

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 22:36:12 +08:00
Innei f6314cc673 ♻️ refactor: serve Vite SPA static assets under /_spa (#13409)
Made-with: Cursor
2026-03-30 21:54:20 +08:00
Yizhuo cded932f1a 📝 docs: update telegram channel guide & remove wip description (#13226)
*  docs: add screenshots to Telegram channel guide

* 📝 docs: Remove "feature in development" callout and developer mode requirement from channels documentation.

* docs: Migrate Telegram channel images to local assets and update CDN cache.

* docs: Add screenshots to channel setup guides for various platforms.

* chore: Update documentation image paths from GitHub user attachments to local blog assets.

---------

Co-authored-by: Rdmclin2 <rdmclin2@gmail.com>
2026-03-30 21:42:29 +08:00
Innei e7c496352f 🐛 fix: defer scroll-to-user-message until spacer is mounted (#13378)
* 🐛 fix: defer scroll-to-user-message until spacer is mounted

The scroll that pins a user message to the top of the viewport was
racing with the conversation spacer mount. When the spacer hadn't
rendered yet, there wasn't enough scrollable height, so the scroll
had no effect.

Now `useScrollToUserMessage` accepts a `spacerActive` flag and
defers the scroll until the spacer is mounted, guaranteeing the
fill height is available before scrolling.

https://claude.ai/code/session_016GDASpf7Rh5yN7BJTdXYwT

* 🐛 fix: always scroll immediately, re-scroll when spacer mounts

The previous fix deferred scrolling entirely until spacerActive was
true. This regressed the no-spacer case (content fills viewport,
spacer height = 0, mounted stays false) — the scroll never fired.

Now the hook always scrolls immediately on message send (preserving
original behavior), and additionally fires a follow-up scroll when
spacerActive transitions to true. This covers both cases:
- Content fills viewport: immediate scroll works, no spacer needed
- Content is short: immediate scroll may under-scroll, but the
  follow-up scroll after spacer mounts corrects the position

https://claude.ai/code/session_016GDASpf7Rh5yN7BJTdXYwT

* 🐛 fix(conversation): shrink bottom spacer on scroll-up when idle

- Track scroll delta to reduce spacer height while not streaming
- Disable height transition during scroll-shrink for immediate feedback
- Reset reduction on new user/assistant pair and generation state changes

Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 21:31:35 +08:00
Arvin Xu 296c6f3cb3 🔧 refactor: simplify response ID to use topicId directly (#13410)
Remove resp_ prefix and random suffix encoding from response IDs.
Response ID now equals topicId directly, simplifying multi-turn
conversation support via previous_response_id.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:31:09 +08:00
LiJian 53d0ee9ca5 🐛 fix: should use env.APP_URL to replace online url (#13408)
* fix: should use env.APP_URL to replace online url

* fix: fixed the double / path problem
2026-03-30 20:37:44 +08:00
Arvin Xu 689d5a51e8 feat(openapi): support hosted builtin tools in Response API (#13406)
*  feat(openapi): support hosted builtin tools in Response API

Allow declaring builtin tools via { type: 'lobe-xxx' } syntax in the
tools array of POST /api/v1/responses. Hosted tool identifiers are
extracted and passed as additionalPluginIds to execAgent, where the
existing ToolsEngine handles manifest resolution automatically.

LOBE-6535

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat(openapi): stream tool calls and results in Response API

Add full streaming support for tool execution events in the Response
API. Previously only text deltas were streamed; tool calls and results
were only visible in the final response.completed event.

Now emits:
- response.output_item.added (function_call) when LLM invokes a tool
- response.function_call_arguments.delta for tool arguments
- response.output_item.done (function_call) when tool call is complete
- response.output_item.added/done (function_call_output) when tool
  execution finishes with results
- Proper text message lifecycle (added/delta/done) across multi-step
  agent loops

LOBE-6535

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(openapi): handle nullable tools param in extractHostedToolIds

The tools field from CreateResponseRequest uses .nullish() in zod,
so it can be null. Accept null in the parameter type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:37:07 +08:00
Arvin Xu 23eab8769b 🐛 fix: add unread completion notification for group topic orchestration (#13407)
* 🐛 fix: add unread completion notification for group topic orchestration

Group orchestration was missing markUnreadCompleted() call after completion,
and group topic NavItem lacked the unread completion indicator UI.

Fixes LOBE-4878

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: extract neon dot inline styles to createStaticStyles

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add test screenshot 01.jpg for LOBE-4878

* docs: add test screenshot 02.jpg for LOBE-4878

* docs: add test screenshot 03.jpg for LOBE-4878

* 🔥 chore: remove temporary test screenshots

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: change unread neon dot color from green to blue (colorInfo)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: replace remaining successColor references with infoColor in group topic item

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:33:31 +08:00
Innei 0e57fd9955 feat(onboarding): agent web onboarding, feature toggle, and lifecycle sync (#13139)
*  feat(onboarding): add agent-guided web onboarding flow

Made-with: Cursor

* Update onboarding prompts

Co-authored-by: Codex <noreply@openai.com>

* 🐛 fix web onboarding builtin tool flow

*  feat(onboarding): enhance agent onboarding flow with new dimensions and refined rules

- Updated onboarding structure to include new nodes: agentIdentity, userIdentity, workStyle, workContext, and painPoints.
- Revised system role instructions to emphasize a conversational approach and concise interactions.
- Adjusted manifest and type definitions to reflect the new onboarding schema.
- Implemented tests to ensure proper functionality of the onboarding context and flow.

This update aims to improve user experience during onboarding by making it more engaging and structured.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): enhance onboarding experience with localized welcome messages and interaction hints

- Added localized welcome messages for onboarding in English and Chinese.
- Refactored system role handling to support dynamic interaction hints based on user locale.
- Updated onboarding context to include interaction hints for improved user engagement.
- Implemented tests to validate the new interaction hint functionality.

This update aims to create a more personalized and engaging onboarding experience for users across different languages.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): overhaul onboarding flow with new question structure and refined interaction rules

- Replaced existing interaction hints with a focused question structure to enhance user engagement.
- Updated system role instructions to clarify onboarding protocols and improve conversational flow.
- Refactored type definitions and manifest to align with the new onboarding schema.
- Removed deprecated interaction hint components and tests to streamline the codebase.

This update aims to create a more structured and engaging onboarding experience for users, ensuring clarity and efficiency in interactions.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): introduce builtin agent onboarding package with structured roles and prompts

- Added a new package for agent onboarding, including a package.json configuration and initial TypeScript files.
- Implemented system role templates and tool prompts to guide the onboarding process.
- Established a client interface for rendering questions and handling user interactions.
- Updated dependencies in related packages to integrate the new onboarding functionality.

This update aims to enhance the onboarding experience by providing a structured approach for agents, ensuring clarity and efficiency in user interactions.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): enhance agent onboarding with new question renderer and refined interaction logic

- Introduced a new `QuestionRendererView` component to streamline the rendering of onboarding questions.
- Refactored the `QuestionRenderer` to utilize a runtime hook for improved state management and separation of concerns.
- Updated the onboarding context to fallback to stored questions when the current question is empty, enhancing user experience.
- Simplified the onboarding API by removing unnecessary read token requirements from various endpoints.
- Added tests to validate the new question rendering logic and ensure proper functionality.

This update aims to create a more efficient and user-friendly onboarding experience by improving the question handling and rendering process.

Signed-off-by: Innei <tukon479@gmail.com>

* Add dev history view for onboarding

* remove: prosetting

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): inline response language step in agent conversation

- Add ResponseLanguageInlineStep and wire into Conversation flow
- Extend agent onboarding context and update ResponseLanguageStep route
- Add tests and onboarding agent document design spec

Made-with: Cursor

*  feat(onboarding): enhance onboarding flow with inbox integration and schema refactor

- Updated onboarding process to migrate conversation topics to the inbox upon completion, ensuring users can revisit their onboarding discussions.
- Introduced a new schema-driven normalizer and node handler registry to streamline onboarding data handling, reducing code duplication and improving maintainability.
- Added comprehensive tests for new document builders and onboarding service methods to ensure functionality and reliability.
- Refactored existing components to support the new onboarding structure and improve user experience.

This update aims to create a more cohesive onboarding experience by integrating user identity data into the inbox and simplifying the underlying code structure.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(agent-documents): add listDocuments, readDocumentByFilename, upsertDocumentByFilename APIs

*  feat(onboarding): add generic user interaction builtin tool

*  feat(onboarding): wire generic tool interaction semantics

Register user-interaction tool in builtin-tools registry with manifest,
intervention components, client executor, and server runtime. Extend
BuiltinInterventionProps with interactionMode and onInteractionAction
to support custom (non-approval) interaction UIs. Add submit/skip/cancel
actions to conversation store with full operation lifecycle management.

* 🔧 fix: add builtin-tool-user-interaction to root workspace dependencies

* ♻️ refactor(onboarding): remove onboarding-owned question persistence

Drop askUserQuestion from the web-onboarding tool and remove
questionSurface from persisted state. Question presentation is now
delegated to the generic lobe-user-interaction tool.

* ♻️ refactor(onboarding): switch UI to generic interaction tool

Enable UserInteraction and AgentDocuments tools in web-onboarding and
inbox agent configs. Remove obsolete inline question renderers
(QuestionRenderer, QuestionRendererView, questionRendererRuntime,
questionRendererSchema, ResponseLanguageInlineStep) and simplify
Conversation component to only render summary CTA.

* 🔥 refactor(onboarding): remove identity doc and rewrite soul sync

* 🐛 fix(user-interaction): add humanIntervention to manifest and implement form UI

* 🐛 fix(onboarding): create user message on interaction submit instead of re-executing tool

* ♻️ refactor(onboarding): rebuild generic interaction flow

Align agent/tool roles and onboarding UI/runtime around the generic interaction rebuild.

Made-with: Cursor

*  feat(onboarding): implement onboarding document and persona management

Introduce a new onboarding document structure that separates agent identity and user persona data. Replace existing `readSoulDocument` and `updateSoulDocument` APIs with `readDocument` and `updateDocument` to handle both SOUL.md and user persona documents. Update related services, client executors, and localization keys to reflect these changes. Ensure document updates are driven by the agent, allowing for incremental updates and improved content management.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(workflow): introduce unified tool call collapse UI and supporting components

Add a new workflow collapse feature that groups tool calls and reasoning into a single collapsible unit, enhancing the user interface for tool call progress. This includes the creation of several components: `WorkflowCollapse`, `WorkflowSummary`, `WorkflowExpandedList`, `WorkflowToolLine`, and `WorkflowReasoningLine`. Update the design specifications and implementation plans to reflect this new structure, aiming for a more cohesive and user-friendly experience.

Signed-off-by: Innei <tukon479@gmail.com>

* feat(types): add discovery pacing types and constant

* feat(onboarding): add countTopicUserMessages and pacing gate to derivePhase

* feat(onboarding): capture discovery baseline and return pacing data in getState

*  feat(onboarding): add pacing hints to discovery phase tool result

* test(onboarding): add discovery pacing gate tests

* ♻️ refactor(onboarding): soften discovery pacing gate and add early exit exception

- MIN_DISCOVERY_USER_MESSAGES lowered from 4 to 2 (hard floor)
- RECOMMENDED_DISCOVERY_USER_MESSAGES = 4 (advisory hint)
- Tool protocol rule 2 now has explicit early exit exception
- Pacing hint text changed from imperative to advisory

*  feat(onboarding): update .gitignore and remove outdated onboarding plans

- Added `docs/superpowers` to .gitignore to exclude documentation files from version control.
- Deleted several outdated onboarding implementation plans, including those for onboarding inbox integration, generic interaction rebuild, and user question simplification, to streamline project documentation.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): refine agent onboarding, streaming, and AskUserQuestion

Made-with: Cursor

*  feat(store): add pending interventions selector

* 🐛 fix(store): handle standalone tool messages and structural children traversal in pending interventions selector

*  feat(conversation): create InterventionBar component

Add InterventionBar UI component with tab bar for multiple pending
interventions, reusing the existing Intervention detail component.

* 🐛 fix(conversation): use stable toolCallId for active tab state and add min-height: 0

Track active intervention by toolCallId instead of array index to prevent
stale selection when interventions are resolved. Add min-height: 0 to
scrollable content for correct overflow in flex column layout.

* feat(chatinput): show InterventionBar when pending interventions exist

* feat(tool): collapse inline intervention to one-line summary with scroll-to-bottom

* feat(i18n): add intervention bar translation keys

* 🐛 fix(chatinput): prevent infinite render loop from pendingInterventions selector

* 🐛 fix(chatinput): use equality function for pendingInterventions to break render loop

* refactor(tool): remove CollapsedIntervention, return null for pending inline

* feat(i18n): add form.other translation key

* feat(tool): add styles for select field with Other option

* feat(tool): add SelectFieldInput with Other option row

* feat(tool): wire SelectFieldInput and update validation in AskUserQuestion

* fix(tool): add keyboard handler to Other row, fix label flex

* refactor(tool): restore Select dropdown, add Other toggle row below

* refactor(tool): change Other to form-level escape hatch, restore antd Select

* refactor(tool): replace checkbox toggle with minimal text link escape hatch

* feat(tool): use lucide icons, auto-focus on escape toggle, createStaticStyles

* refactor(onboarding): update onboarding model references and improve styling in ModeSwitch component

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): add greeting entry animation keyframes and card styles

*  feat(onboarding): add LogoThree and entry animations to greeting card

*  feat(onboarding): add View Transition morph from greeting to conversation

* refactor(onboarding): simplify ModeSwitch component by removing segmentedGlass styling

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(onboarding): increase maximum onboarding steps to 5 and add ProSettingsStep component

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance user interaction question handling with validation schema

- Introduced Zod validation for askUserQuestion arguments to ensure correct structure.
- Updated test to reflect new question format with fields.
- Added error handling in AskUserQuestion component to log submission errors.

This improves the robustness of user interactions by enforcing schema validation and enhancing error reporting.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance agent metadata handling and onboarding synchronization

- Updated `useAgentMeta` to prioritize custom titles from the database, falling back to the default Lobe AI title if none exists.
- Integrated `refreshBuiltinAgent` into the onboarding process to ensure the latest agent data is reflected during user interactions.
- Adjusted the `InboxItem` component to display the correct agent title and avatar based on the updated metadata.
- Refactored optimistic update actions to improve message handling and synchronization across components.

This improves the user experience by ensuring that the most relevant agent information is displayed and updated in real-time during onboarding and conversation flows.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance conversation lifecycle and onboarding agent synchronization

- Updated `ConversationLifecycleActionImpl` to include additional context parameters (agentId, groupId, threadId, topicId) when updating message plugins for aborted interactions.
- Integrated `refreshBuiltinAgent` for the inbox during the onboarding process to ensure the latest agent data is synchronized.

These changes improve the handling of conversation lifecycle events and ensure that onboarding reflects the most current agent information, enhancing user experience during interactions.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: implement agent onboarding feature toggle and enhance ModeSwitch component

- Introduced `AGENT_ONBOARDING_ENABLED` configuration to control the visibility of the agent onboarding options.
- Updated `ModeSwitch` component to conditionally render onboarding options based on the feature toggle.
- Enhanced tests for `ModeSwitch` to cover scenarios for both enabled and disabled states of agent onboarding.
- Refactored `AgentOnboardingRoute` to navigate to the classic onboarding if the agent onboarding feature is disabled.

These changes improve the onboarding experience by allowing dynamic control over the agent onboarding feature, ensuring that users only see relevant options based on the configuration.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: update agent onboarding feature toggle to include development mode

- Modified `AGENT_ONBOARDING_ENABLED` to also activate in development mode using `isDev`.
- This change allows for easier testing and development of the agent onboarding feature without needing to alter production configurations.

Signed-off-by: Innei <tukon479@gmail.com>

* Prevent welcome message when onboard

* 🐛 fix: satisfy ToolExecutionContext and updateMessageTools typings

Made-with: Cursor

* 🐛 fix: update tests for custom builtin agent title and discovery phase constants

* 🐛 fix: use custom inbox agent title and avatar in InboxWelcome

* 🧹 chore(onboarding): remove HistoryPanel unit test

Made-with: Cursor

* 🐛 fix: add missing onboarding/agent and onboarding/classic routes to desktop config

*  test: fix failing tests for onboarding container, document helpers, and executor

*  test: mock LogoThree to prevent Spline runtime fetch errors in CI

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
2026-03-30 20:28:54 +08:00
LobeHub Bot 2f5a31fc99 test: add unit tests for LocalTaskScheduler (#13398)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 18:53:03 +08:00
Arvin Xu 143a15fdb9 💄 style: show interrupted hint when AI generation is stopped (#13397)
*  feat: show interrupted hint when AI generation is stopped

Display "Interrupted · What should I do instead?" text below the message
when user stops AI generation, replacing the infinite dotting animation.

Fixes LOBE-4462
Fixes LOBE-5726

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add edit button to queued messages tray

Allow users to edit queued messages by clicking the pencil icon,
which removes the message from the queue and restores its content
to the input editor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 chore: move record-electron-demo.sh to electron-testing skill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: derive isInterrupted from latest runtime operation only

Previously isInterrupted used .some() to check if any cancelled AI
runtime operation existed for a message. In stop-then-retry flows,
the old cancelled op persisted alongside the new completed one,
causing the interrupted hint to reappear after the retry finished.

Now only the latest AI runtime operation is checked, so completed
retries correctly clear the interrupted state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: read group interruption from active block ID

For assistant groups, continuation runs attach cancelled operations to
lastBlockId (contentId) rather than the group root. Check isInterrupted
on both the group root and the active block so the interrupted hint
is shown correctly for stopped group continuations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: update test to expect cancelled status after user stop

The test for resolving aborted tools after cancellation now correctly
expects 'cancelled' status, since completeOperation preserves the
user's intentional cancellation rather than overwriting it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:46:46 +08:00
LiJian 9c08fa5cdf 🐛 fix: add the creds tools into execAgentRuntime (#13399)
fix: add the creds tools into execAgentRuntime
2026-03-30 17:20:37 +08:00
Hardy 59d8d878a2 🐛 fix: use Anthropic SDK for Kimi Coding Plan provider (#13345)
🐛 fix: use Anthropic SDK for Kimi Coding Plan provider

- Switch from OpenAI SDK to Anthropic SDK for Kimi Coding Plan
- Update baseURL from `/coding/v1` to `/coding` (Anthropic-compatible endpoint)
- Update model IDs: `kimi-k2.5` → `k2p5`, remove `kimi-k2`
- Fix max_tokens resolution to use KimiCodingPlan model list
- Rewrite tests for Anthropic SDK compatibility
2026-03-30 16:53:28 +08:00
WindSpiritSR 0439a29189 🔨 chore(docker): replace dev/prod pgsql docker image with paradedb (#13373)
🐛 fix(docker): replace dev/prod pgsql docker image with paradedb

Signed-off-by: WindSpiritSR <simon343riley@gmail.com>
2026-03-30 16:52:00 +08:00
LobeHub Bot 4a63ea3dcc 🌐 chore: translate non-English comments to English in src/routes (#13395)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 16:50:45 +08:00
YuTengjing 91b2653c71 🐛 fix: check error value in social sign-in result (#13392) 2026-03-30 11:38:48 +08:00
René Wang 8c8e7dd992 Update team assignments and feature responsibilities (#13393) 2026-03-30 10:44:25 +08:00
Arvin Xu a9cd2f7301 ♻️ refactor: remove DefaultAgentForm UI from settings pages (#13342)
🔥 refactor: remove DefaultAgentForm UI from settings pages

Remove the user-facing Default Agent configuration form from both
the agent settings page and the service-model settings page.
The underlying store action and selectors are preserved for
programmatic use (e.g. onboarding).

Fixes LOBE-1125

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:18:31 +08:00
YuTengjing b6c66dbdd7 feat: add businessElement and getFetchOptions plumbing to signin page (#13382)
*  feat: add businessElement and getFetchOptions plumbing to signin page

Add extension points to the signin flow so cloud overrides can inject
custom UI elements and modify fetch options for social sign-in requests.

- Add businessElement slot to SignInEmailStep component
- Pass getFetchOptions to signIn.social() and signIn.oauth2() calls
- Add businessElement and getFetchOptions defaults to useBusinessSignin

* 🐛 fix: resolve TS error on signIn.social result type with fetchOptions
2026-03-29 23:15:16 +08:00
Rylan Cai 5e1738ad4b ♻️ refactor(context engine): tool message normalization (#13359)
* ♻️ normalize tool call messages in context engine

* ♻️ prune tool message normalization implementation

* ♻️ prune tool message normalization diff

* ♻️ simplify tool message normalization diff

* ♻️ restore tool message reorder logging

* ♻️ restore reorder tool message shape

* ♻️ restore tool message reorder comment

* ♻️ prune tool message normalization diff

* ♻️ restore tool message reorder shape

* 🐛 fix(context-engine): keep empty tool content in reorder
2026-03-29 23:04:02 +08:00
Arvin Xu 4dc3c4ea1d 💄 style: move Marketplace below Resources in sidebar (#13381)
♻️ refactor: move Marketplace below Resources in sidebar

Move the Marketplace (Community) nav item from topNavItems to bottomMenuItems,
positioning it below Resources in the sidebar navigation.

Closes LOBE-6320
2026-03-29 22:32:30 +08:00
Arvin Xu bc9ae6b4e5 feat: support message queue (#13343)
*  feat: add message queue for agent runtime (soft interrupt)

Implement per-context message queue that allows users to send messages
while the agent is executing. Messages are queued and consumed via two
paths: injected at step boundaries during execution (Path A), or
triggering a new sendMessage after completion (Path B).

- Add QueuedMessage type and queuedMessages state in operation store
- Add enqueue/drain/remove/clear actions and selectors
- Modify sendMessage to enqueue when execAgentRuntime is running
- Add queue checkpoint in step loop (streamingExecutor)
- Add Path B: drain remaining queue after completion → new sendMessage
- Keep input enabled during agent execution (remove isInputLoading guard)
- Add QueueTray component showing "N Queued" above ChatInput
- Add electron-testing skill for agent-browser CLI automation

Fixes LOBE-6001

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: Path B deferred execution to avoid recursive internal_execAgentRuntime

Use setTimeout(0) to break out of the current execution context when
triggering a new agent runtime for queued messages after completion.
Direct recursive calls caused issues with zustand state batching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: remove premature Path A drain, fix Path B with fresh store ref

Path A (step checkpoint injection) was draining the queue before the
last LLM step, leaving nothing for Path B. For agents without tool
calls, this meant queued messages were consumed but never acted upon.

Fix: remove Path A for now (will be re-added for tool-call scenarios),
and use useChatStore.getState() in Path B setTimeout to get a fresh
store reference instead of a stale closure capture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs: update electron-testing skill with real-world patterns

Based on lessons learned during message queue testing:
- Must cd to apps/desktop before npx electron-vite dev
- Use polling loop for startup detection
- snapshot -i -C required for contenteditable (chat input)
- Use sleep + screenshot instead of agent-browser wait for long ops
- Access store via window.__LOBE_STORES.chat()
- Add error interceptor and store inspection patterns
- Document all gotchas (HMR, daemon blocking, fill vs type)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add Path A - early handoff to Path B at tool completion

When tools finish and queue has messages, break the step loop early
and let Path B create user message + start new operation. The new
LLM call sees full context including tool results + new user message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: Path B use sendMessage for proper message creation

Use sendMessage instead of optimisticCreateMessage + internal_execAgentRuntime.
sendMessage handles the full lifecycle correctly: creates user message
on server, creates assistant message placeholder, and triggers
internal_execAgentRuntime — ensuring both messages are visible in UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: redesign QueueTray to Codex-style card layout

Each queued message shows as a card with icon, text preview,
and delete button. Uses antd CSS variables for consistent theming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: connect QueueTray with ChatInput as unified container

QueueTray and ChatInput now share a connected border:
- QueueTray has top-rounded corners, no bottom border
- ChatInput gets bottom-only rounded corners when queue has items
- Uses cssVar for proper theme token styling
- Zero gap between tray and input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: move queue check logic into GeneralChatAgent

Move the "finish early when queue has messages" decision from
streamingExecutor into GeneralChatAgent.runner(). The agent now
checks stepContext.hasQueuedMessages at tools_batch_result phase
and returns finish instruction, which is architecturally cleaner.

- Add hasQueuedMessages to RuntimeStepContext and computeStepContext
- GeneralChatAgent returns finish when tools complete + queue non-empty
- Remove Path A/B labels from comments
- streamingExecutor just passes hasQueuedMessages via stepContext

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: forward queued files in sendMessage and drain only on success

- Forward merged file attachments when replaying queued messages
  (sendMessage now receives files from merged queue)
- Move drainQueuedMessages inside the status==='done' branch so
  queued messages are preserved on error/interrupted states
- Add queued_message_interrupt to FinishReason enum
- Add hasQueuedMessages check to tool_result and tasks_batch_result
  phases in GeneralChatAgent (not just tools_batch_result)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: use full operationContext for context key indexing

- operationsByContext index now uses messageMapKey(context) with full
  context (including threadId, scope, etc.) instead of stripped key
- Fixes key mismatch where thread/scoped contexts couldn't find
  running operations, causing overlapping generations
- Move mergeQueuedMessages from services/messageQueue.ts into
  operation/types.ts alongside QueuedMessage type
- Delete services/messageQueue.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:08:15 +08:00
YuTengjing 70091935ba 🔥 refactor(auth): remove NextAuth dead code from auth middleware (#13370)
* 🔥 refactor(auth): remove NextAuth dead code from auth middleware

* chore: shorter cookie cache duration
2026-03-29 21:17:45 +08:00
YuTengjing 50e373ad1c 🐛 fix(i18n): add missing credits.packages.charged key (#13369) 2026-03-29 02:09:57 +08:00
YuTengjing 966f943175 🐛 fix(auth): throw Unauthorized when no valid auth method found (#13368) 2026-03-29 01:56:40 +08:00
Rdmclin2 c7c2b56f3b feat: support bot manage (#13365)
* feat: support platform manage

* feat: auto connect when import config

* fix: lint error
2026-03-29 01:52:59 +08:00
Innei 841c1d2ef2 ♻️ refactor(styles): migrate remaining createStyles to createStaticStyles (#13358)
- Replace antd-style createStyles hooks with createStaticStyles and cssVar tokens
- Update MentionMenu, reactions, eval bench UI, OAuth device flow, DeviceGateway, GTD plan UI
- ModelSelect: use popupMatchSelectWidth for numeric popupWidth; narrow prop to number

Made-with: Cursor
2026-03-28 21:57:15 +08:00
Innei 26449e522a feat(resource): add select all hint and improve resource explorer selection (#13134)
*  feat(resource): add select all hint and improve resource explorer selection

Made-with: Cursor

* ♻️ refactor(resource): flatten store actions and improve type imports

Made-with: Cursor

* ♻️ refactor resource explorer list view

* refactor: engine

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: checkpoint current workspace updates

* ♻️ refine resource explorer fetch ownership

* 🐛 fix: resolve resource manager ci regressions

* 🐛 fix(lambda): delete page-backed knowledge items by document id

* 🐛 fix(lambda): include knowledge-base files in remove-all

* 🐛 fix(resource): preserve cross-page select-all exclusions

* 🐛 fix(resource): retain off-screen optimistic resources

* 🐛 fix(resource): hide moved root items from current query

* 🐛 fix(resource): reset explorer selection on query change

* 🐛 fix(resource): fix select-all batchChunking and optimistic replace visibility

- batchChunking: pass through server-resolved IDs not in local resourceMap
  when selectAllState is 'all', letting server filter unsupported types
- replaceLocalResource: keep replacement visible if the optimistic item was
  already in the list, avoiding slug-vs-UUID mismatch in visibility check

* 🐛 fix(resource): reset selectAllState after batch operations and preserve off-screen optimistic items

- Reset selectAllState to 'none' after delete, removeFromKnowledgeBase,
  and batchChunking to prevent stale 'all' state causing unintended
  re-selection of remaining items
- Preserve off-screen optimistic resources in clearCurrentQueryResources
  so background uploads from other folders survive delete-all-by-query

* 🐛 fix: satisfy import-x/first in resource action test

Made-with: Cursor

* 🎨 lint: sort imports in ResourceExplorer

Made-with: Cursor

* 🐛 fix: widen searchQuery type in useResetSelectionOnQueryChange test

Made-with: Cursor

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-28 11:51:23 +08:00
Rdmclin2 f4c4ba7db5 🐛 fix: bot callback error (#13349)
* fix: not edit message id

* fix: error edit message

* chore: merge config & default

* chore: remove typing var

* fix: agent setting problem

* fix: test case error
2026-03-28 00:53:53 +08:00
LiJian 83f8f0319c 🐛 fix: slove the list connection always use require auth & should have trust client auth (#13344)
fix: slove the list connection always use require auth & should have trust client check
2026-03-27 21:14:03 +08:00
YuTengjing 197a0cc8f1 🌐 chore: sync i18n locale translations (#13340) 2026-03-27 18:59:54 +08:00
LiJian 6b4046eb17 🐛 fix: add the user github oauth in community home page profiles (#13222)
* fix: add the user github oauth in community home page profiles

* fix: change the oauth from social Profiles into skill connector way

* feat: add the claims user mcp and skills in community profiles

* fix: improve some claim model and skills/mcp
2026-03-27 18:04:17 +08:00
Innei 9e27bef8fa 🐛 fix(settings): remove system tools full-page loading (#13338) 2026-03-27 17:28:23 +08:00
lobehubbot 11318f8ab9 🔖 chore(release): release version v2.1.47 [skip ci] 2026-03-27 08:07:22 +00:00
lobehubbot aaff9af3b7 Merge remote-tracking branch 'origin/main' into canary 2026-03-27 08:05:35 +00:00
LiJian feb50e7007 🚀 release: 20260327 (#13330)
# 🚀 release: 20260326

This release includes **91 commits**. Key updates are below.


- **Agent can now execute background tasks** — Agents can perform
long-running operations without blocking your conversation.
[#13289](https://github.com/lobehub/lobe-chat/pull/13289)
- **Better error messages** — Redesigned error UI across chat and image
generation with clearer explanations and recovery options.
[#13302](https://github.com/lobehub/lobe-chat/pull/13302)
- **Smoother topic switching** — No more full page reloads when
switching topics while an agent is responding.
[#13309](https://github.com/lobehub/lobe-chat/pull/13309)
- **Faster image uploads** — Large images are now automatically
compressed to 1920px before upload, reducing wait times.
[#13224](https://github.com/lobehub/lobe-chat/pull/13224)
- **Improved knowledge base** — Documents are now properly parsed before
chunking, improving retrieval accuracy.
[#13221](https://github.com/lobehub/lobe-chat/pull/13221)

### Bot Platform

- **WeChat Bot support** — You can now connect LobeChat to WeChat, in
addition to Discord.
[#13191](https://github.com/lobehub/lobe-chat/pull/13191)
- **Richer bot responses** — Bots now support custom markdown rendering
and context injection.
[#13294](https://github.com/lobehub/lobe-chat/pull/13294)
- **New bot commands** — Added `/new` to start fresh conversations and
`/stop` to halt generation.
[#13194](https://github.com/lobehub/lobe-chat/pull/13194)
- **Discord stability fixes** — Fixed thread creation issues and Redis
connection drops.
[#13228](https://github.com/lobehub/lobe-chat/pull/13228)
[#13205](https://github.com/lobehub/lobe-chat/pull/13205)

### Models & Providers

- **GLM-5** is now available in the LobeHub model list.
[#13189](https://github.com/lobehub/lobe-chat/pull/13189)
- **Coding Plan providers** — Added support for code planning assistant
providers. [#13203](https://github.com/lobehub/lobe-chat/pull/13203)
- **Tencent Hunyuan 3.0 ImageGen** — New image generation model from
Tencent. [#13166](https://github.com/lobehub/lobe-chat/pull/13166)
- **Gemini content handling** — Better handling when Gemini blocks
content due to safety filters.
[#13270](https://github.com/lobehub/lobe-chat/pull/13270)
- **Claude token limits fixed** — Corrected max window tokens for
Anthropic Claude models.
[#13206](https://github.com/lobehub/lobe-chat/pull/13206)

### Skills & Tools

- **Auto credential injection** — Skills can now automatically request
and use required credentials.
[#13124](https://github.com/lobehub/lobe-chat/pull/13124)
- **Smarter tool permissions** — Built-in tools skip confirmation for
safe paths like `/tmp`.
[#13232](https://github.com/lobehub/lobe-chat/pull/13232)
- **Model switcher improvements** — Quick access to provider settings
and visual highlight for default model.
[#13220](https://github.com/lobehub/lobe-chat/pull/13220)

### Memory

- **Bulk delete memories** — You can now delete all memory entries at
once. [#13161](https://github.com/lobehub/lobe-chat/pull/13161)
- **Per-agent memory control** — Memory injection now respects
individual agent settings.
[#13265](https://github.com/lobehub/lobe-chat/pull/13265)

### Desktop App

- **Gateway connection** — Desktop app can now connect to LobeHub
Gateway for enhanced features.
[#13234](https://github.com/lobehub/lobe-chat/pull/13234)
- **Connection status indicator** — See gateway connection status in the
titlebar. [#13260](https://github.com/lobehub/lobe-chat/pull/13260)
- **Settings persistence** — Gateway toggle state now persists across
app restarts. [#13300](https://github.com/lobehub/lobe-chat/pull/13300)

### CLI

- **API key authentication** — CLI now supports API key auth for
programmatic access.
[#13190](https://github.com/lobehub/lobe-chat/pull/13190)
- **Shell completion** — Tab completion for bash/zsh/fish shells.
[#13164](https://github.com/lobehub/lobe-chat/pull/13164)
- **Man pages** — Built-in manual pages for CLI commands.
[#13200](https://github.com/lobehub/lobe-chat/pull/13200)

### Security

- **XSS protection** — Sanitized search result image titles to prevent
script injection.
[#13303](https://github.com/lobehub/lobe-chat/pull/13303)
- **Workflow hardening** — Fixed potential shell injection in release
automation. [#13319](https://github.com/lobehub/lobe-chat/pull/13319)
- **Dependency update** — Updated nodemailer to address security
advisory. [#13326](https://github.com/lobehub/lobe-chat/pull/13326)

### Bug Fixes

- Fixed skill page not redirecting correctly after import.
[#13255](https://github.com/lobehub/lobe-chat/pull/13255)
[#13261](https://github.com/lobehub/lobe-chat/pull/13261)
- Fixed token counting in group chats.
[#13247](https://github.com/lobehub/lobe-chat/pull/13247)
- Fixed editor not resetting when switching to empty pages.
[#13229](https://github.com/lobehub/lobe-chat/pull/13229)
- Fixed manual tool toggle not working.
[#13218](https://github.com/lobehub/lobe-chat/pull/13218)
- Fixed Search1API response parsing.
[#13207](https://github.com/lobehub/lobe-chat/pull/13207)
[#13208](https://github.com/lobehub/lobe-chat/pull/13208)
- Fixed mobile topic menus rendering issues.
[#12477](https://github.com/lobehub/lobe-chat/pull/12477)
- Fixed history count calculation for accurate context.
[#13051](https://github.com/lobehub/lobe-chat/pull/13051)
- Added missing Turkish translations.
[#13196](https://github.com/lobehub/lobe-chat/pull/13196)

### Credits

Huge thanks to these contributors:

@bakiburakogun @hardy-one @Zhouguanyang @sxjeru @hezhijie0327 @arvinxx
@cy948 @CanisMinor @Innei @LiJian @lobehubbot @Neko @rdmclin2
@rivertwilight @tjx666
2026-03-27 16:04:56 +08:00
Zhijie He dc9adf8f10 🐛 fix: fix some features for Github Copilot (ResponseAPI / Vision, etc) (#13279)
* 🐛 fix(github-copilot): switch codex models to responses api

* ♻️ refactor(github-copilot): simplify responses api routing

style: update model list

style: update model list

🐛 fix: align github copilot payload mapping and tests

style: update model list

style: update model list

* chore: add debug stream support

* refactor: use anthropic sdk for claude

* fix: fix ci error

* fix: fix github copilot reasoning_text chunk

* style: update Raptor mini base config, same as gpt-5-mini

style: update Raptor mini base config, same as gpt-5-mini

style: update Raptor mini base config, same as gpt-5-mini

* style: update model contextWindowTokens

* style: set default reasoning.summary to detailed, default as vscode
2026-03-27 15:13:28 +08:00
Innei 3d592ca70d ♻️ refactor: add generic SafeBoundary error boundary with tiered fallback (#13321)
Introduce a unified SafeBoundary component (silent/alert variants) to
replace scattered custom ErrorBoundary class components. Automatically
wraps Inspector, ContentBlock sub-components, MessageItem, and
EditorCanvas to prevent individual component crashes from propagating
to the entire app.
2026-03-27 15:10:00 +08:00
LobeHub Bot 8d0ac45476 🌐 chore: translate non-English comments to English in packages/openapi (#13329)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 14:15:46 +08:00
Rdmclin2 953033355b 🔨 chore: optimize bot platform ux (#13262)
* chore: remove typing interval

* chore: optimize wechat media problem

* chore: add webhook helpers

* chore: update telegram docs

* chore: extract wechat credentials to custom render form

* feat: support wechat file upload

* feat: support concurrency mode and debounceMs

* chore: add locales

* chore: support visible then

* chore: support auto disapear save result info

* chore: default debounce mode

* chore: optimize doc position

* chore: adjust ack message logic

* fix: aes throw
2026-03-27 13:28:52 +08:00
sxjeru 48b5927024 💄 style: enhance handling of blocked content on Gemini (#13270)
*  feat: improve error messages for Google AI block reasons and enhance handling of blocked content

*  feat: add error localization for Google provider in createAgentExecutors
2026-03-27 10:51:01 +08:00
renovate[bot] 6e86912e7f Update dependency nodemailer to ^7.0.13 [SECURITY] (#13326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-27 10:34:22 +08:00
Arvin Xu 4576059f4f ♻️ refactor: implement SkillResolver, BaseSystemRoleProvider, and agent document injection pipeline (#13315)
* ♻️ refactor: implement SkillResolver to replace ad-hoc skill assembly

Introduces a two-layer skill resolution architecture mirroring ToolsEngine + ToolResolver:

- SkillEngine (assembly layer): accepts raw skills + enableChecker, outputs OperationSkillSet
- SkillResolver (resolution layer): merges operation + step delta + accumulated activations

Key changes:
- Add SkillResolver, OperationSkillSet, StepSkillDelta, ActivatedStepSkill types
- Enhance SkillEngine with enableChecker and generate() method
- Wire SkillResolver into RuntimeExecutors call_llm
- Replace manual skillMetas assembly in aiAgent with SkillEngine.generate()
- Update client-side skillEngineering to use SkillEngine + enableChecker
- Add activatedStepSkills to AgentState for step-level skill accumulation

Fixes: agent-browser content injected into non-desktop scenarios (Discord bot)
due to missing filterBuiltinSkills call in aiAgent

LOBE-6410

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract agent-templates to standalone package and inject documents server-side

- Create @lobechat/agent-templates package with types, templates, and registry
- Move DocumentLoadPosition, DocumentLoadFormat, DocumentLoadRule, etc. to new package
- Move claw templates (AGENTS, BOOTSTRAP, IDENTITY, SOUL) with .md file imports
- Add BOOTSTRAP.md as new onboarding template (priority 1, system-append)
- Fix template positions: AGENTS→before-system, IDENTITY/SOUL→system-append
- Update database package to re-export from @lobechat/agent-templates
- Migrate all consumers to import directly from @lobechat/agent-templates
- Add agent documents injection in server-side RuntimeExecutors (was missing)
- Support -p CLI flag in devStartupSequence for port configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: correct import statement for non-type exports from agent-templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📦 build: add @lobechat/agent-templates to root dependencies

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: remove template proxy files from database package

Stop re-exporting template/templates from database — consumers import
directly from @lobechat/agent-templates. Keep types.ts re-exports for
internal database code only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: create BaseSystemRoleProvider to unify system message append pattern

All providers that append to the system message now inherit from
BaseSystemRoleProvider and only implement buildSystemRoleContent().
The base class handles find-or-create and join logic.

Migrated providers:
- EvalContextSystemInjector
- BotPlatformContextInjector
- SystemDateProvider
- ToolSystemRoleProvider
- HistorySummaryProvider
- SkillContextProvider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: restore metadata tracking in BaseSystemRoleProvider via onInjected hook

Add onInjected() callback to BaseSystemRoleProvider so subclasses can
update pipeline metadata after successful injection. Also add raw-md
plugin to context-engine vitest config for .md imports.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: add enabled field to AgentDocumentInjector config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: add enabled field to all providers, remove spread conditionals in MessagesEngine

All providers now accept an `enabled` config field. MessagesEngine
pipeline is a flat array with no spread conditionals — each provider
is always instantiated and uses `enabled` to skip internally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: clean up MessagesEngine pipeline comments

Remove numbered prefixes, keep descriptive comments for each provider.
Only phase headers use separator blocks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: reorganize MessagesEngine pipeline phases by injection target

Phase 1: History Truncation
Phase 2: System Message Assembly (all BaseSystemRoleProvider)
Phase 3: Context Injection (before first user message, BaseFirstUserContentProvider)
Phase 4: User Message Augmentation (last user message injections)
Phase 5: Message Transformation (flatten, template, variables)
Phase 6: Content Processing & Cleanup (multimodal, tool calls, cleanup)

Moved SkillContext, ToolSystemRole, HistorySummary from Phase 3 to
Phase 2 since they append to system message, not user context.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: split Phase 6 into Content Processing (6) and Cleanup (7)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: split AgentDocumentInjector into three position-based injectors

- AgentDocumentSystemInjector (Phase 2): before-system, system-append, system-replace
- AgentDocumentContextInjector (Phase 3): before-first-user
- AgentDocumentMessageInjector (Phase 4): after-first-user, context-end

Shared utilities (filterByRules, formatDocument, sortByPriority) extracted
to AgentDocumentInjector/shared.ts. Old monolithic injector removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: split AgentDocumentSystemInjector into three separate injectors

- AgentDocumentBeforeSystemInjector: prepends as separate system message (before-system)
- AgentDocumentSystemAppendInjector: appends to system message (system-append)
- AgentDocumentSystemReplaceInjector: replaces entire system message (system-replace)

Each has distinct semantics and correct pipeline placement:
- BeforeSystem → before SystemRoleInjector
- SystemAppend → after HistorySummary (end of Phase 2)
- SystemReplace → last in Phase 2 (destructive)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: auto-enable agent-documents tool when agent has documents

- Add AgentDocumentsManifest to defaultToolIds
- Add hasAgentDocuments rule in server createServerAgentToolsEngine
- Query agent documents in AiAgentService.execAgent to determine flag
- Pattern matches KnowledgeBase auto-enable via enableChecker rules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔨 chore: add agent documents status to execAgent operation log

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update content

* fix tests

* 🐛 fix: add raw-md plugin to database vitest configs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:10:06 +08:00
Arvin Xu 9e9ba3e6c3 🐛 fix: prevent first assistant message re-animation on assistantGroup transition (#13320)
* 🐛 fix: prevent first assistant message re-animation on assistantGroup transition

When tool calls arrive during streaming, the message transitions from
assistant to assistantGroup, causing a full React remount. The first
content block's text was re-animating because isGenerating was still
true. Pass isFirstBlock prop through the render chain to disable
animation for the first block, since its text is guaranteed complete
by the time the group forms.

Fixes LOBE-6414

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: remove redundant isToolSingleLine animation check

isFirstBlock already covers the first block case, and subsequent blocks
should not have animation disabled just because they are single-line
with tools — they may still be streaming.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 01:41:17 +08:00
Innei 46602be0b3 🐛 fix(workflow): prevent shell injection in auto-tag release (#13319) 2026-03-27 01:18:35 +08:00
YuTengjing 14b278fba8 💄 style: add payment upgrade i18n keys and update microcopy (#13317) 2026-03-27 00:51:28 +08:00
Arvin Xu 53c5708c9f 🔨 chore: improve start up scripts (#13318)
update scripts
2026-03-27 00:49:23 +08:00
YuTengjing edc8920703 🔨 chore: temporarily disable notification triggers (#13314) 2026-03-26 23:35:04 +08:00
Arvin Xu 926de076d9 🐛 fix: sanitize search grounding image titles to prevent XSS (#13303)
* 🐛 fix: sanitize search grounding image titles to prevent XSS

Replace dangerouslySetInnerHTML with stripHtml() for image result titles
in SearchGrounding and ImageSearchRef components to prevent stored XSS
attacks via malicious search result data.

Ref: GHSA-m5qx-g8hx-5f2p

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🔒 fix: remove SystemJS plugin renderer to eliminate arbitrary JS execution risk

The old plugin render system (ui.mode === 'module') that used SystemJS
to dynamically load and execute JS from untrusted URLs has been fully
retired. Remove SystemJsRender and systemjs dependency entirely.

Ref: GHSA-46v7-wvmj-6vf7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "🔒 fix: remove SystemJS plugin renderer to eliminate arbitrary JS execution risk"

This reverts commit 99a7603a72.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:38:49 +08:00
Innei 9b7beca85e 💄 style(conversation): align user rich text line height with LexicalRenderer (#13312)
💄 style(conversation): set LexicalRenderer line height in user rich text

Made-with: Cursor
2026-03-26 21:58:24 +08:00
Arvin Xu 0724d8ca60 🐛 fix: prevent full page reload when switching topics during agent execution (#13309)
Move `e.preventDefault()` before the `disabled || loading` early return
in NavItem's onClick handler. Previously, when a NavItem was in disabled
or loading state, the early return skipped `preventDefault()`, allowing
the underlying `<a>` tag's default navigation to trigger a full browser
page load instead of SPA routing.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:30:08 +08:00
YuTengjing 9f36fe95ac feat: add notification system (temporarily disabled) (#13301) 2026-03-26 21:16:38 +08:00
Arvin Xu 3f148005e4 ♻️ refactor: remove langchain dependency, use direct document loaders (#13304)
* ♻️ refactor: remove langchain dependency, use direct document loaders

Replace langchain and @langchain/community with self-implemented text
splitters and direct usage of underlying libraries (pdf-parse, d3-dsv,
mammoth, officeparser, epub2). This eliminates unnecessary dependency
bloat and addresses CVE-2026-26019 in @langchain/community.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add missing @types/html-to-text and @types/pdf-parse

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:13:55 +08:00
Arvin Xu 4e60d87514 🔒 refactor: remove deprecated SystemJS plugin renderer (#13305)
🔒 fix: remove SystemJS plugin renderer to eliminate arbitrary JS execution risk

The old plugin render system (ui.mode === 'module') that used SystemJS
to dynamically load and execute JS from untrusted URLs has been fully
retired. Remove SystemJsRender and systemjs dependency entirely.

Ref: GHSA-46v7-wvmj-6vf7

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 20:41:06 +08:00
YuTengjing d2a16d0714 feat: improve error UI and error handling across chat and image generation (#13302) 2026-03-26 20:09:06 +08:00
Arvin Xu 0f04463708 🐛 fix(desktop): persist gateway toggle state across app restarts (#13300)
🐛 fix: persist gateway toggle state across app restarts

The gateway auto-connect logic only checked if the user was logged in,
ignoring whether they had manually disabled the toggle. Added a
`gatewayEnabled` flag to the Electron store that is set on
connect/disconnect and checked before auto-connecting on startup.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:31:42 +08:00
Arvin Xu 093fa7bcae feat: support agent tasks system (#13289)
*  feat: agent task system — CLI, review rubrics, workspace, comments, brief tool split

support import md

Major changes:
- Split task CLI into modular files (task/, lifecycle, topic, doc, review, checkpoint, dep)
- Split builtin-tool-task into task + brief tools (conditional injection)
- Task review uses EvalBenchmarkRubric from @lobechat/eval-rubric
- Task workspace: documents auto-pin via Notebook, tree view with folders
- Task comments system (task_comments table)
- Task topics: dedicated TaskTopicModel with userId, handoff fields, review results
- Heartbeat timeout auto-detection in detail API
- Run idempotency (reject duplicate runs) + error rollback
- Topic cancel/delete by topicId only (no taskId needed)
- Integration tests for task router (13 tests)
- interruptOperation fix (string param, not object)
- Global TRPC error handler in CLI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

task document workflow

task handoff loop

🗃️ chore: consolidate task system migrations into single 0095

Merged 7 separate migrations (0095-0101) into one:
- tasks, briefs, task_comments, task_dependencies, task_documents, task_topics tables
- All fields including sort_order, resolved_action/comment, review fields
- Idempotent CREATE TABLE IF NOT EXISTS, DROP/ADD CONSTRAINT, CREATE INDEX IF NOT EXISTS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

fix interruptOperation

topic auto review workflow

topic handoff workflow

finish run topic and brief workflow

support task tool

improve task schema

update

 feat: add onComplete hook to task.run for completion callbacks

When agent execution completes, the hook:
- Updates task heartbeat
- Creates a result Brief (on success) with assistant content summary
- Creates an error Brief (on failure) with error message
- Supports both local (handler) and production (webhook) modes

Uses the new Agent Runtime Hooks system instead of raw stepCallbacks.

LOBE-6160 LOBE-6208

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add Review system — LLM-as-Judge automated review

Task review uses an independent LLM call to evaluate topic output
quality against configurable criteria with pass/fail thresholds.

- TaskReviewService: structured LLM review via generateObject,
  auto-resolves model/provider from user's system agent defaults
- Model: getReviewConfig, updateReviewConfig on TaskModel
- Router: getReview, updateReview, runReview procedures
- CLI: `task review set/view/run` commands
- Auto-creates Brief with review results

LOBE-6165

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add TaskScheduler, multi-topic execution, and handoff context

- TaskScheduler: interface + Local implementation (setTimeout-based),
  following QueueService dual-mode pattern
- Multi-topic execution: `task run --topics N --delay S` runs N topics
  in sequence with optional delay between them
- Handoff context: buildTaskPrompt() queries previous topics by
  metadata.taskId and injects handoff summaries into the next topic's
  prompt (sliding window: latest full, older summaries only)
- Heartbeat auto-update between topics

LOBE-6161

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add Heartbeat watchdog + heartbeat CLI

Watchdog scans running tasks with expired heartbeats, marks them as
failed, and creates urgent error Briefs. Heartbeat CLI allows manual
heartbeat reporting for testing.

- Model: refactored to use Drizzle operators (isNull, isNotNull, ne)
  instead of raw SQL where possible; fixed findStuckTasks to skip
  tasks without heartbeat data
- Router: heartbeat (manual report), watchdog (scan + fail + brief)
- Router: updateSchema now includes heartbeatInterval, heartbeatTimeout
- CLI: `task heartbeat <id>`, `task watchdog`, `task edit` with
  --heartbeat-timeout, --heartbeat-interval, --description

LOBE-6161

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

♻️ refactor: move CheckpointConfig to @lobechat/types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add task run — trigger agent execution for tasks

Task.run creates a topic, triggers AiAgentService.execAgent with task
context, and streams results via SSE. Supports both agentId and slug.

- Service: added taskId to ExecAgentParams, included in topic metadata
- Router: task.run procedure — resolves agent, builds prompt, calls execAgent,
  updates topic count and heartbeat
- CLI: `task run <id>` command with SSE streaming, --prompt, --verbose

LOBE-6160

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add Checkpoint system for task review gates

Checkpoint allows configuring pause points in task execution flow.
Supports beforeIds (pause before subtask starts) and afterIds (pause
after subtask completes) on parent tasks.

- Model: CheckpointConfig type, getCheckpointConfig, updateCheckpointConfig,
  shouldPauseBeforeStart, shouldPauseAfterComplete
- Router: getCheckpoint, updateCheckpoint procedures; integrated with
  updateStatus for automatic checkpoint triggering
- CLI: `task checkpoint view/set` commands with --before, --after,
  --topic-before, --topic-after, --on-agent-request options
- Tests: 3 new checkpoint tests (37 total)

LOBE-6162

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add dependency unlocking on task completion

When a task completes, automatically check and unlock blocked tasks
whose dependencies are all satisfied (backlog → running). Also notify
when all subtasks of a parent are completed.

- Model: getUnlockedTasks, areAllSubtasksCompleted (Drizzle, no raw SQL)
- Router: updateStatus hook triggers unlocking on completion
- CLI: shows unlocked tasks and parent completion notification
- Tests: 3 new tests (34 total)

LOBE-6164

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add Brief system — schema, model, router, CLI

Brief is a universal Agent-to-User reporting mechanism, not limited to
Tasks. CronJobs, Agents, and future systems can all produce Briefs.

- Schema: briefs table with polymorphic source (taskId, cronJobId, agentId)
- Model: BriefModel with CRUD, listUnresolved (Daily Brief), markRead, resolve
- Router: TRPC brief router with taskId identifier resolution
- CLI: `lh brief` command (list/view/read/resolve)
- Tests: 11 model tests
- Migration: 0096_add_briefs_table.sql

LOBE-6163

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

 feat: add Task system — schema, model, router, CLI

Implement the foundational Task system for managing long-running,
multi-topic agent tasks with subtask trees and dependency chains.

- Schema: tasks, task_dependencies, task_documents tables
- Model: TaskModel with CRUD, tree queries, heartbeat, dependencies, document pinning
- Router: TRPC task router with identifier/id resolution
- CLI: `lh task` command (list/view/create/edit/delete/start/pause/resume/complete/cancel/tree/dep)
- Tests: 31 model tests
- Migration: 0095_add_task_tables.sql

LOBE-6036 LOBE-6054

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update

* 🐛 fix: update brief model import path and add raw-md vitest plugin

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: eslint import sort in vitest config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: brief ID validation, auto-review retry, and continueTopicId operationId

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: task integration tests — create test agent for FK, fix children spread

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: task integration tests — correct identifier prefix and agent ID

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: remove unused toolsActivatorRuntime import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: create real topic in task integration tests to satisfy FK constraint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: type errors in task prompt tests, handoff schema, and activity mapping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: create real agent/topic/brief records in database model tests for FK constraints

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:43:51 +08:00
lobehubbot ac8a9ec0f8 🔖 chore(release): release version v2.1.46 [skip ci] 2026-03-26 09:07:05 +00:00
lobehubbot aa48b856fb Merge remote-tracking branch 'origin/main' into canary 2026-03-26 09:05:30 +00:00
YuTengjing b4d27c7232 🗃️ db: add notification tables (#13295)
🗃️ db: add notification tables migration (UUID, with indexes)
2026-03-26 17:04:47 +08:00
Rdmclin2 dd192eda3e feat: bot support custom markdown render and context injection (#13294)
* feat: support  bot mardown format

* feat: support custom markdownRender and bot context inject

* feat: support custom PORT

* feat: telegram support html render

* feat: slack support markdown render

* chore: feishu and lark don't handle markdown for now
2026-03-26 16:52:35 +08:00
huangkairan c6b0f868ef 🐛 fix: skill page redirect & activeTab handling in Details component (#13255) 2026-03-26 15:39:43 +08:00
Arvin Xu 3bea920193 🔁 chore: sync main branch to canary (#13286)
## Summary
- Sync main branch (v2.1.44 + v2.1.45 releases, agent task system DB
schema) into canary
- Resolved Body.tsx merge conflict by keeping canary version
2026-03-26 15:03:02 +08:00
arvinxx ca16a40a44 Merge remote-tracking branch 'origin/main' into sync/main-to-canary-20260326-v2
# Conflicts:
#	src/routes/(main)/agent/channel/detail/Body.tsx
2026-03-26 15:01:04 +08:00
lobehubbot 59e19310fe 🔖 chore(release): release version v2.1.45 [skip ci] 2026-03-26 05:58:23 +00:00
Arvin Xu b005a9c73b 👷 build: add agent task system database schema (#13280)
* 🗃️ chore: add agent task system database schema

Add 6 new tables for the Agent Task System:
- tasks: core task with tree structure, heartbeat, scheduling
- task_dependencies: inter-task dependency graph (blocks/relates)
- task_documents: MVP workspace document pinning
- task_topics: topic tracking with handoff (jsonb) and review results
- task_comments: user/agent comments with author tracking (text id: cmt_)
- briefs: unresolved notification system (text id: brf_)

All sub-tables include userId FK for row-level user isolation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add self-referential FK on tasks.parentTaskId (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use foreignKey() for self-referential parentTaskId to avoid TS circular inference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on task_topics.topic_id → topics.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve pre-existing TS type-check errors

- Fix i18next defaultValue type (string | null → string)
- Fix i18next options type mismatches
- Fix fieldTags.webhook possibly undefined

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on tasks.currentTopicId → topics.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK constraints for assignee, author, topic, and parent fields

- tasks.assigneeUserId → users.id (ON DELETE SET NULL)
- tasks.assigneeAgentId → agents.id (ON DELETE SET NULL)
- tasks.parentTaskId → tasks.id (ON DELETE SET NULL)
- tasks.currentTopicId → topics.id (ON DELETE SET NULL)
- task_comments.authorUserId → users.id (ON DELETE SET NULL)
- task_comments.authorAgentId → agents.id (ON DELETE SET NULL)
- task_topics.topicId → topics.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: change task_topics.topicId FK to ON DELETE CASCADE

Topic deleted → task_topic mapping row removed (not just nulled).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: use inline .references() for currentTopicId FK

No circular inference issue — only parentTaskId (self-ref) needs foreignKey().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on task_comments.briefId and topicId (ON DELETE SET NULL)

- task_comments.briefId → briefs.id (SET NULL)
- task_comments.topicId → topics.id (SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: merge briefs table into task.ts to fix circular dependency

brief.ts imported task.ts (briefs.taskId FK) and task.ts imported
brief.ts (taskComments.briefId FK), causing circular dependency error.
Merged briefs into task.ts since briefs are part of the task system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🗃️ chore: add FK on tasks.createdByAgentId → agents.id (ON DELETE SET NULL)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:56:01 +08:00
Rdmclin2 2c657670fe 🐛 fix: skill import url and github address problem (#13261)
* chore: optimize github import placeholder and hint

* fix: support import a github hosted skill.md url

* fix:  reimport skill problem

* fix: github zip url file correctly resovled

* fix:  empty content

* fix: test case

* fix: regex lint
2026-03-26 11:28:31 +08:00
Rylan Cai 4dd271c968 feat(cli): support api key auth in cli (#13190)
*  support cli api key auth

* 🔒 reject invalid x-api-key without fallback auth

* ♻️ clean up cli api key auth diff

* ♻️ clean up cli auth command diff

* ♻️ clean up remaining cli auth diff

* ♻️ split stored auth token fields

* ♻️ trim connect auth surface

* ♻️ drop redundant jwt user id carry-over

* ♻️ trim auth test wording diff

* 🐛 fix api key model imports

* 🐛 fix api key util subpath import

* 🔐 chore(cli): use env-only api key auth

* ♻️ refactor(cli): simplify auth credential flow

*  feat: simplify cli api key login flow

* 🐛 fix(cli): prefer jwt for webapi auth

* ♻️ refactor(cli): trim auth http diff

* 🐛 fix(cli): skip api key auth expiry handling

* 🐛 fix(cli): restore non-jwt expiry handling

* ♻️ refactor(cli): trim connect auth expired diff

* ♻️ refactor(cli): trim login comment diff

* ♻️ refactor(cli): trim resolve token comment diff

* ♻️ refactor(cli): restore connect expiry flow

* ♻️ refactor(cli): trim login api key message

* 🐛 fix(cli): support api key gateway auth

* ♻️ refactor(cli): restore resolve token comment

* ♻️ refactor(cli): trim test-only auth diffs

* ♻️ refactor(cli): restore resolve token comments

*  test(cli): add api key expiry coverage

* 🐛 fix cli auth server resolution and gateway auth

* ♻️ prune auth fix diff noise

* ♻️ unify cli server url precedence

* ♻️ simplify device gateway auth tests

*  add gateway auth edge case coverage

*  remove low-value gateway auth test

* 🐛 fix api key context test mock typing
2026-03-26 10:11:38 +08:00
Arvin Xu b76db6bcbd 🐛 fix(memory): respect agent-level memory toggle when injecting memories (#13265)
* 🐛 fix(memory): respect agent-level memory toggle when injecting memories

When the user disables the memory toggle in ChatInput (which writes to
agent-level chatConfig.memory.enabled), the actual message-sending path
in chat/index.ts was only checking the user-level memoryEnabled setting,
completely ignoring the agent-level override.

This aligns the injection logic with useMemoryEnabled hook:
agent-level config takes priority, falls back to user-level setting.

Also fix pre-commit hook to use bunx instead of npx to ensure the
correct ESLint version (v10) is used in monorepo context.

Adds regression tests verifying all three priority scenarios.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update pre-commit

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 01:51:56 +08:00
Innei 84674b1e10 feat(builtin-tool-local-system): skip intervention for safe paths like /tmp (#13232)
*  feat(builtin-tool-local-system): skip intervention for safe paths like /tmp

Add SAFE_PATH_PREFIXES whitelist to bypass user confirmation for
file operations targeting ephemeral directories (/tmp, /var/tmp).

* Fix intervention audit tests

* Move fs checks into Electron
2026-03-26 01:38:36 +08:00
LobeHub Bot 1cb13d9f93 test: add unit tests for mcpStore selectors (#13240)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 01:19:27 +08:00
Arvin Xu 169f11b63b feat(desktop): add device gateway status indicator in titlebar (#13260)
* support desktop gateway

* support device mode

*  feat(desktop): add device gateway status indicator in titlebar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  test(desktop): update getDeviceInfo test to include name and description fields

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ✏️ chore(i18n): update gateway status copy to reference Gateway instead of cloud

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ✏️ chore(i18n): translate Gateway to 网关 in zh-CN

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ✏️ chore(i18n): simplify description placeholder to Optional

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ✏️ chore(desktop): use fixed title 'Connect to Gateway' in device popover

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 01:14:08 +08:00
Arvin Xu 2c7a3f934d 🐛 fix: use display messages for token counting in group chats (#13247)
* 🐛 fix: use partial-json fallback in ToolArgumentsRepairer to recover incomplete args

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use display messages for token counting in group chats

The TokenTag component used dbMessageSelectors.activeDbMessages which
generates a key without groupId, causing empty results in group chats.
This made the Context Details token tag invisible for group agents.

Switch to using the messageString prop (from mainAIChatsMessageString)
which correctly includes groupId in its key generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 00:59:45 +08:00
YuTengjing a1e91ab30d test: add tests for topic updatedTime grouping (#13249) 2026-03-25 19:46:40 +08:00
Rdmclin2 4a7c89ec25 fix: discord not create thread & wechat media and connect optimize (#13228)
* fix: avoid subscribe whole channel

* chore: add start message whatever

* chore: remove typing interval

* feat: support typing keep alive

* fix: wechat redis client

* feat: add common gateway

* chore: use persistent to replace websocket

* chore: add wechat tip

* fix: add queue Handoff Succeeded stop typing

* feat: optimize connect status display and wechat connect infomation

* chore: wechat maximum 2048

* feat: support wechat files type

* feat: support wechat image upload

* feat: support wechat image resolve

* fix: lint error

* fix: lint error

* fix: postProcessUrl test case

* chore: moke file service

* chore: add page test case timeout
2026-03-25 18:43:45 +08:00
Neko 684a186e3b 🐛 fix(agent-runtime): missing agentId in context (#13250)
Authored-by-agent: Codex <267193182+codex@users.noreply.github.com>
2026-03-25 18:41:14 +08:00
Rdmclin2 e8a948cfaf style: replace plugin icon with skill icon (#13252)
chore: replace plugin icon  with skill icon
2026-03-25 18:21:36 +08:00
YuTengjing 11daf645e9 💄 style: unlock downgrade restrictions i18n and copy improvements (#13241)
* 💬 chore: add i18n keys for unlocking downgrade restrictions

Add subscription i18n keys:
- plans.downgradeWillCancel: warning shown when action cancels pending downgrade
- plans.pendingDowngrade: button text for pending downgrade target
- Update plans.downgradeTip to reflect cancellation context

LOBE-6155

* 🐛 fix: close model switch panel on clicking multi-provider item in generation mode

* 🌐 i18n: add cancel downgrade schedule translations

* 💄 style: simplify menu and tab labels for billing, credits, and usage

* 💄 style: rename switch success to downgrade and update copy

* 🌐 i18n: add switchDowngradeTarget translation key

* 🌐 i18n: sync translations for downgrade schedule keys
2026-03-25 16:44:49 +08:00
Rdmclin2 a4a03eadc4 chore: remove like github star footer (#13246) 2026-03-25 16:29:04 +08:00
Innei 04ddb992d1 🐛 fix(desktop): add missing Stats and Creds tabs to Electron componentMap (#13243) 2026-03-25 16:27:37 +08:00
LobeHub Bot 991de25b97 🌐 chore: translate non-English comments to English in packages/openapi (#13184)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 15:42:28 +08:00
Arvin Xu 056f390abc 🐛 fix: use partial-json fallback in ToolArgumentsRepairer to recover incomplete args (#13239)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:50:34 +08:00
Rdmclin2 9b9949befa chore: remove runtime config in agent builder and doc writer (#13238) 2026-03-25 12:54:35 +08:00
LobeHub Bot 366b02bb46 test: add unit tests for topicReference serverRuntime (#13055)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 12:31:45 +08:00
Hardy ad2087cf65 feat: add Coding Plan providers support (#13203)
*  feat: add Aliyun Bailian Coding Plan provider

- Add new AI provider for Bailian Coding Plan (coding.dashscope.aliyuncs.com/v1)
- Support 8 coding-optimized models: Qwen3.5 Plus, Qwen3 Coder Plus/Next, Qwen3 Max, GLM-5/4.7, Kimi K2.5, MiniMax M2.5
- Reuse QwenAIStream for stream processing
- Static model list (Coding Plan does not support API model fetching)
- Add i18n translations for provider description

*  feat: add MiniMax Coding Plan provider

- Add new AI provider for MiniMax Token Plan (api.minimax.io/v1)
- Support 6 models: MiniMax-M2.7, M2.7-highspeed, M2.5, M2.5-highspeed, M2.1, M2
- Static model list (Coding Plan does not support API model fetching)
- Add i18n translations for provider description

*  feat: add GLM Coding Plan provider

- Add new AI provider for GLM Coding Plan (api.z.ai/api/paas/v4)
- Support 6 models: GLM-5, GLM-5-Turbo, GLM-4.7, GLM-4.6, GLM-4.5, GLM-4.5-Air
- Static model list (Coding Plan does not support API model fetching)
- Add i18n translations for provider description

*  feat: add Kimi Code Plan provider

- Add new AI provider for Kimi Code Plan (api.moonshot.ai/v1)
- Support 3 models: Kimi K2.5, Kimi K2, Kimi K2 Thinking
- Static model list (Coding Plan does not support API model fetching)
- Add i18n translations for provider description

*  feat: add Volcengine Coding Plan provider

- Add new AI provider for Volcengine Coding Plan (ark.cn-beijing.volces.com/api/coding/v3)
- Support 5 models: Doubao-Seed-Code, Doubao-Seed-Code-2.0, GLM-4.7, DeepSeek-V3.2, Kimi-K2.5
- Static model list (Coding Plan does not support API model fetching)
- Add i18n translations for provider description

*  feat: update coding plan providers default enabled models and configurations

*  feat: add reasoningBudgetToken32k and reasoningBudgetToken80k slider variants

- Add ReasoningTokenSlider32k component (max 32*1024)
- Add ReasoningTokenSlider80k component (max 80*1024)
- Add reasoningBudgetToken32k and reasoningBudgetToken80k to ExtendParamsType
- Update ControlsForm to render appropriate slider based on extendParams
- Update ExtendParamsSelect with new options and previews
- Fix ReasoningTokenSlider max value to use 64*Kibi (65536) instead of 64000

* 🔧 fix: support reasoningBudgetToken32k/80k in ControlsForm and modelParamsResolver

- Add reasoningBudgetToken32k and reasoningBudgetToken80k fields to chatConfig type and schema
- Update ControlsForm to use correct name matching for 32k/80k sliders
- Add processing logic for 32k/80k params in modelParamsResolver
- Add i18n translations for extendParams hints

* 🎨 style: use linear marks for reasoning token sliders (32k/80k)

- Switch from log2 scale to linear scale for equal mark spacing
- Add minWidth/maxWidth constraints to limit slider length
- Fix 64k and 80k marks being too close together

* 🎨 fix: use equal-spaced index for reasoning token sliders (32k/80k)

- Slider uses index [0,1,2,3,...] for equal mark spacing
- Map index to token values via MARK_TOKENS array
- Add minWidth/maxWidth to limit slider length when marks increase

*  feat: add reasoningBudgetToken32k for GLM-5 and GLM-4.7 in Bailian Coding Plan

* 🔧 fix: update coding plan API endpoints and model configurations

- minimaxCodingPlan: change API URL to api.minimaxi.com (China site)
- kimiCodingPlan: change API URL to api.kimi.com/coding/v1
- volcengineCodingPlan: update doubao-seed models with correct deploymentName, pricing
- volcengineCodingPlan: add minimax-m2.5 model
- bailianCodingPlan & volcengineCodingPlan: remove unsupported extendParams from minimax-m2.5

*  feat: add Coding Plan tag to provider cards with i18n support

* ♻️ refactor: set showModelFetcher to false for Bailian Coding Plan

- Coding Plan does not support fetching model list via API
- Set both modelList.showModelFetcher and settings.showModelFetcher to false

* 🔧 fix: correct Coding Plan exports case in package.json

*  feat: update coding plan models with releasedAt and remove pricing

* 🔧 fix: remove unsupported reasoning abilities from MiniMax Coding Plan models

* 🐛 fix(modelParamsResolver): fix reasoningBudgetToken32k/80k not being read when enableReasoning is present

- Add nested logic to check which budget field (32k/80k/generic) the model supports when enableReasoning is true
- Move reasoningBudgetToken32k/80k else-if branches before reasoningBudgetToken to ensure correct field is read
- Fix GLM-5/GLM-4.7 models sending wrong budget_tokens value to API
2026-03-25 11:53:16 +08:00
LobeHub Bot 0689dd68a3 🌐 chore: translate non-English comments to English in routes and layout (#13210)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:52:28 +08:00
LobeHub Bot 75ea33153f 🌐 chore: translate non-English comments to English in packages/agent-runtime (#13236)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:51:28 +08:00
YuTengjing dbff1e0668 🐛 fix: default topic display mode to byUpdatedTime and fix nanoBanana2 resolution enum (#13235) 2026-03-25 11:17:41 +08:00
LobeHub Bot afefe217db test: add unit tests for eval-dataset-parser (#13197)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 10:55:58 +08:00
Arvin Xu fed8b39957 feat: desktop support connect to gateway (#13234)
* support desktop gateway

* support device mode

* support desktop

* fix tests

* improve

* fix tests

* fix tests

* fix case
2026-03-25 10:43:15 +08:00
Rdmclin2 f853537695 Add /new and /stop slash commands for bot message management (#13194)
*  feat(bot): implement /new and /stop slash commands

Add Chat SDK slash command handlers for bot integrations:
- /new: resets conversation state so the next message starts a fresh topic
- /stop: cancels any active agent execution on the current thread

https://claude.ai/code/session_01MDofskrz64tRjh2T6xzGBL

* feat: support telegram text type  commands

* fix: stop commands

* feat: register discord slash commands

* feat: add chat adapter patch

* feat: add interuption action

* chore: add agent thread interuption signal

* chore: optimize interruption result

* fix: /stop command message edit

* chore: create a message when interrupted

* chore: add bot test case

* chore: fix test case

* chore: fix test case and remove duplicate completion

* fix: lint error

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-25 00:31:01 +08:00
Baki Burak Öğün 0cdaf117cb 🌐 fix(locale): translate missing Turkish (tr-TR) strings (#13196)
fix(locale): translate missing Turkish (tr-TR) strings in setting.json

- Translate agentCronJobs.clearTopics, clearTopicsFailed, confirmClearTopics
- Translate agentCronJobs.confirmDeleteCronJob, deleteCronJob, deleteFailed

Co-authored-by: bakiburakogun <bakiburakogun@users.noreply.github.com>
2026-03-25 00:11:55 +08:00
Innei ada555789d 🐛 fix(editor): reset editor state when switching to empty page (#13229)
Fixes LOBE-6321
2026-03-24 21:37:08 +08:00
Arvin Xu 007d2dc554 🐛 fix: compress uploaded images to max 1920px before sending to API (#13224)
* 🐛 fix: compress uploaded images to max 1920px before sending to API

Anthropic API rejects images exceeding 2000px in multi-image requests.
Compress images during upload to stay within limits while preserving
original aspect ratio and format (no webp conversion).

Fixes LOBE-6315

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: skip canvas compression for GIF and SVG images

Canvas serialization flattens animated GIFs and rasterizes SVGs.
Restrict compression to safe raster formats: JPEG, PNG, WebP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: always compress images to PNG to avoid MIME mismatch

canvas.toDataURL with original file type can produce content that
doesn't match the declared MIME type, causing Anthropic API errors.
Always output PNG which is universally supported and consistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: progressively shrink images to stay under 5MB API limit

If compressed PNG still exceeds 5MB, progressively reduce dimensions
by 20% until it fits. Also triggers compression for small-dimension
images that exceed 5MB file size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract compressImageFile to utils and add comprehensive tests

Move compressImageFile, COMPRESSIBLE_IMAGE_TYPES, and constants to
@lobechat/utils/compressImage for reusability and testability.
Add tests for: dimension compression, file size limit, format filtering,
error handling, and progressive shrinking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:23:58 +08:00
Innei 995d5ea354 🐛 fix(conversation): preserve mention runtime context (#13223)
* 🐛 fix(conversation): preserve mention context on retry

* 🐛 fix(runtime): preserve initial payload for mention context

*  feat(store): expose Zustand stores on window.__LOBE_STORES in dev

Made-with: Cursor
2026-03-24 19:50:26 +08:00
Arvin Xu 72ba8c8923 🐛 fix: add document parsing to knowledge base chunking pipeline (#13221)
* 🐛 fix: add document parsing to knowledge base chunking pipeline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix plugin title

* update

* 🐛 fix: add missing findByFileId mock in document service tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:49:26 +08:00
YuTengjing 6f65b1e65e feat: improve model switch panel with provider settings shortcut and default highlight (#13220) 2026-03-24 16:30:38 +08:00
YuTengjing 383caceb77 ♻️ refactor: rename getBusinessMenuItems to useBusinessMenuItems hook (#13219) 2026-03-24 15:58:29 +08:00
Rdmclin2 b4862f2942 🐛 fix: manual tool disabled (#13218)
fix: manual tool disabled
2026-03-24 15:24:18 +08:00
YuTengjing d1affa8e44 🌐 feat(i18n): add userPanel.upgradePlan i18n key (#13213) 2026-03-24 15:20:34 +08:00
Innei 6e3053fcb3 feat(cli): add generated man pages (#13200) 2026-03-24 14:46:56 +08:00
Innei b845ba4476 🔨 chore(vite): support direct markdown imports (#13216)
 feat(vite): support markdown imports
2026-03-24 14:33:57 +08:00
LiJian 7c00650be5 ♻️ refactor: add the user creds modules & skill should auto inject the need creds (#13124)
* feat: add the user creds modules & skill should auto inject the need creds

* feat: add the builtin creds tools

* fix: add some prompt in creds & codesandbox

* fix: open this settings/creds in community plan

* fix: refacoter the settings/creds the ui

* feat: improve the tools inject system Role

* feat: change the settings/creds mananger ui

* fix: add the creds upload Files api

* feat: should call back the files creds url
2026-03-24 14:28:23 +08:00
Innei 5bc015a746 🐛 fix: move nodrag from TabBar container to individual TabItems (#13211) 2026-03-24 11:33:00 +08:00
Arvin Xu 6757e10ec2 🐛 fix: map unsupported time_range values for Search1API (#13208)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 09:22:04 +08:00
Arvin Xu 48428594c3 🐛 fix: correct Search1API response parsing to match actual API format (#13207)
* 🐛 fix: correct Search1API response parsing to match actual API format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix tests

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 02:18:28 +08:00
Innei 6a45414b46 🐛 fix(electron): reserve titlebar control space (#13204)
* 🐛 fix(electron): reserve titlebar control space

* 🐛 fix(electron): update titlebar padding for Windows control space
2026-03-23 23:29:55 +08:00
Arvin Xu 0f53490633 🐛 fix: fix anthropic claude model max window tokens (#13206)
* fix anthropic max tokens

* fix anthropic max tokens

* clean

* fix tests
2026-03-23 23:01:31 +08:00
Rdmclin2 66fba60194 fix: add discord redisClient lost problem (#13205) 2026-03-23 21:13:03 +08:00
YuTengjing fadaeef8d3 feat: add GLM-5 model support to LobeHub provider (#13189) 2026-03-23 17:46:32 +08:00
CanisMinor 3c5249eae7 📝 docs: fix agent usage typo (#13198)
docs: fix agent usage
2026-03-23 14:14:58 +08:00
Innei 9eca3d2ec0 ♻️ refactor(store): replace dynamic imports with static imports in actions (#13159)
Made-with: Cursor
2026-03-23 14:11:04 +08:00
Innei 4e89a00d2a feat(cli): add shell completion and migrate to tsdown (#13164)
* 👷 build(cli): migrate bundler from tsup to tsdown

Made-with: Cursor

* 🔧 chore(cli): update package.json and tsdown.config.ts dependencies

- Moved several dependencies from "dependencies" to "devDependencies" in package.json.
- Updated the bundling configuration in tsdown.config.ts to simplify the bundling process.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore(cli): reorganize package.json and tsdown.config.ts

- Moved "fast-glob" from "dependencies" to "devDependencies" in package.json for better clarity.
- Removed the "onlyBundle" option from tsdown.config.ts to streamline the configuration.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(cli): add shell completion support

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-23 14:10:39 +08:00
LobeHub Bot 89a0211adf 🌐 chore: translate non-English comments to English in plugindevmodal and image-config (#13169)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 13:29:46 +08:00
Rdmclin2 ecde45b4ce feat: support wechat bot (#13191)
* feat: support weixin channel

* chore: rename to wechat

* chore: refact wechat adapter with ilink spec

* feat: add qrcode generate and refresh

* chore: update wechat docs

* fix: qrcode

* chore: remove developer mode restrict

* fix: wechat link error

* chore: add thread typing

* chore: support skip progressMessageId

* fix: discord eye reaction

* chore: resolve CodeQL regex rule

* test: add chat adapter wechat test case

* chore: wechat refresh like discord

* fix: perist token and add typing action

* chore: bot cli support weixin

* fix: database test case
2026-03-23 12:52:11 +08:00
LiJian 1df02300bc 🐛 fix: add the lost desktop community skill page (#13170)
fix: add the lost desktop community skill page
2026-03-23 10:48:47 +08:00
Rdmclin2 637ef4a84e 🔨 chore: remove default calculator (#13162)
* chore: remove calculator from RECOMMENDED_SKILLS

* chore: add default uninstalled builtin list

* fix: ensure uninstall tool loaded

* fix: lint error
2026-03-22 23:15:59 +08:00
Zhijie He 7af4562a60 💄 style: add Tencent Hunyuan 3.0 ImageGen support (#13166) 2026-03-22 12:54:27 +08:00
Sun13138 f9166133a7 🐛 fix(mobile): render topic menus and rename popovers inside active overlay container (#12477) 2026-03-22 01:15:28 +08:00
René Wang 81bd6dc732 📝 docs: add changelog entries for Jan–Mar 2026 (#13163)
* 📝 docs: add changelog entries for Jan–Mar 2026

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: Changelog content

* feat: Changelog content

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 17:53:48 +08:00
Arvin Xu b97c33a29a 🔧 chore: grant write permissions to Claude Code Action workflow (#13173)
Allow Claude Code to push branches and create PRs by upgrading
contents/pull-requests/issues permissions from read to write,
and adding git/gh to allowed tools.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:39:28 +08:00
Rylan Cai b0253d05dd 🔧 chore: adjust jina timeout to 15s (#13171)
🔧 adjust jina timeout setting
2026-03-21 14:39:15 +08:00
Neko 48c3f0c23b feat(memory): support to delete all memory entries (#13161) 2026-03-20 23:32:28 +08:00
LobeHub Bot f812d05ca6 🌐 chore: translate non-English comments to English in openapi services (#13092)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:31:02 +08:00
Neko 88935d84bf 🔧 chore(memory): analysis action icon not aligned (#13160) 2026-03-20 21:39:50 +08:00
Rdmclin2 c39ba410f2 📝 docs: spilit feishu with lark and update overview (#13165)
chore: spilit feishu with lark and update overview
2026-03-20 21:31:33 +08:00
sxjeru 12280badbd 🐛 fix: adjust historyCount calculation to include accurate user messages (#13051) 2026-03-20 21:26:25 +08:00
Rdmclin2 e18855aa25 🔨 chore: bot architecture upgrade (#13096)
* chore: bot architecture upgrade

* chore: unify schema definition

* chore: adjust channel schema

* feat: add setting render page

* chore: add i18n files

* chore: tag use field.key

* chore: add i18n files

* chore: add dev mode

* chore: refactor body to header and footer with body

* chore: add dev portal dev

* chore: add showWebhookUrl config

* chore: optimize form render

* feat: add slack channel

* chore: add new bot platform docs

* chore: unify applicationId to replace appId

* chore: add instrumentation file logger

* fix: gateway client error

* feat: support usageStats

* fix: bot settings pass and add  invalidate

* chore: update delete modal title and description

* chore: adjust save and connect button

* chore: support canEdit function

* fix: platform specific config

* fix: enable logic reconnect

* feat: add connection mode

* chore: start  gateway service in local dev env

* chore: default add a thread in channel when on mention at discord

* chore: add necessary permissions for slack

* feat: support charLimt and debounceMS

* chore: add schema maximum and minimum

* chore: adjust debounceMs and charLimit default value

* feat: support reset to default settings

* chore: hide reset when collapse

* fix: create discord bot lost app url

* fix: registry test case

* fix: lint error
2026-03-20 20:34:48 +08:00
Innei a64f4bf7ab 🔨 chore(desktop): bust stable release manifest cache (#13157)
🐛 fix(desktop): bust stable release manifest cache
2026-03-20 20:12:45 +08:00
Rylan Cai e577c95fa8 🐛 fix: should record unique case id in eval dataset (#13129)
* fix: should capture id if dataset has

* fix: should use unique case id
2026-03-20 19:07:36 +08:00
LobeHub Bot 15cda726a0 🌐 chore: translate non-English comments to English in chat-input-features (#13119)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 18:58:12 +08:00
lobehubbot b53abaa3b2 🔖 chore(release): release version v2.1.44 [skip ci] 2026-03-20 10:39:27 +00:00
lobehubbot 12c325494d Merge remote-tracking branch 'origin/main' into canary 2026-03-20 10:37:53 +00:00
YuTengjing 0edc57319e 🚀 release: 20260320 (#13155)
fix (#13110).
Fixed empty editor state structure and wide screen layout (#13131).
Fixed missing `BusinessAuthProvider` slot in auth layout (#13130).
Fixed artifacts code scroll preservation while streaming (#13114).
Fixed SSRF block error distinction from network errors (#13103).
Fixed Responses API tool pairing and context limit errors (#13078).
Fixed missing `userId` in embeddings API calls (#13077) and
Fixed unsupported xAI reasoning penalties pruning (#13066).
Fixed market OIDC lost call tools error (#13025).
Fixed `jsonb ?` operator usage to avoid Neon `rt_fetch` bug (#13040).
Fixed model provider popup problems (#13012).
Fixed agent-level memory config priority over user settings (#13018).
Fixed multi-provider model item selection (#12968).
Fixed agent stream error in local dev (#13054).
Fixed skill crash (#13011).
Fixed desktop agent-browser upgrade to v0.20.1 (#12985).
Fixed topic share modal inside router (#12951).
Fixed Enter key submission during IME composition (#12963).
Fixed error collapse default active key (#12967).
2026-03-20 18:37:09 +08:00
Rylan Cai 4d360714ad 🐛 fix: fix compression UI (#13113)
* 🐛 fix: restore eval pass@1 display after compression

* ♻️ refactor: narrow eval compression pass@1 fix scope

* ♻️ refactor: reduce eval compression fix to parser core

* 🐛 fix compressed group indexing type narrowing

*  add conversation-flow compression tests

*  fix orphan structuring test expectation
2026-03-20 17:23:02 +08:00
LobeHub Bot 9d441c5ab3 🌐 chore: translate non-English comments to English in packages/openapi/src/controllers (#13146)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:13:41 +08:00
YuTengjing abd152b805 🐛 fix: misc UI/UX improvements and bug fixes (#13153) 2026-03-20 16:42:16 +08:00
LobeHub Bot c0834fb59d test: add unit tests for rbac utils (#13150)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:33:41 +08:00
CanisMinor 2067cb2300 💄 style: add image/video switch (#13152)
* style: add image/video switch

* style: update i18n
2026-03-20 15:55:53 +08:00
Innei cada9a06fc 🔧 chore(vercel): add SPA asset cache headers and no-store for dev proxy (#13151)
Made-with: Cursor
2026-03-20 14:45:19 +08:00
Innei cd75228933 👷 build(ci): add dedicated docker canary tag (#13148) 2026-03-20 14:38:58 +08:00
CanisMinor 57469f860e 💄 style: redesign image / video (#13126)
* ♻️ refactor: Refactor image and video

* chore: rabase canary

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

* style: update

* chore: update i18n

* style: update

* fix: fix config

* fix: fix proxy

* fix: fix type

* chore: fix test
2026-03-20 14:10:01 +08:00
Arvin Xu d3ea4a4894 ♻️ refactor: refactor agent-runtime hooks mode (#13145)
*  feat: add Agent Runtime Hooks — external lifecycle hook system

Hooks are registered once and automatically adapt to runtime mode:
- Local: handler functions called directly (in-process)
- Production: webhook configs persisted to Redis, delivered via HTTP/QStash

- HookDispatcher: register, dispatch, serialize hooks per operationId
- AgentHook type: id, type (beforeStep/afterStep/onComplete/onError),
  handler function, optional webhook config
- Integrated into AgentRuntimeService.createOperation + executeStep
- Hooks persisted in AgentState.metadata._hooks for cross-request survival
- Dispatched at both normal completion and error paths
- Non-fatal: hook errors never affect main execution flow

LOBE-6208

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add HookDispatcher unit tests (19 tests)

Tests cover:
- register/unregister/hasHooks
- Local mode dispatch: matching types, multiple handlers, error isolation
- Production mode dispatch: webhook delivery, body merging, mode isolation
- Serialization: getSerializedHooks filters webhook-only hooks
- All hook types: beforeStep, afterStep, onComplete, onError

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: migrate SubAgent to hooks + add afterStep dispatch + finalState

- AgentHookEvent: added finalState field (local-mode only, stripped from webhooks)
- AgentRuntimeService: dispatch afterStep hooks alongside legacy callbacks
- AiAgentService: createThreadHooks() replaces createThreadMetadataCallbacks()
  for SubAgent Thread execution — same behavior, using hooks API
- HookDispatcher: strip finalState from webhook payloads (too large)

LOBE-6208

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: add Vercel bypass header to QStash hook webhooks

Preserves x-vercel-protection-bypass header when delivering hook
webhooks via QStash, matching existing behavior in
AgentRuntimeService.deliverWebhook and libs/qstash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  feat: migrate Eval Run to hooks + add finalState to AgentHookEvent

Eval Run now uses hooks API instead of raw completionWebhook:
- executeTrajectory: hook with local handler + webhook fallback
- executeThreadTrajectory: hook with local handler + webhook fallback
- Local mode now works for eval runs (previously production-only)

Also:
- AgentHookEvent: added finalState field (local-only, stripped from webhooks)
  for consumers that need deep state access

LOBE-6208

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: dispatch beforeStep hooks + fix completion event payload fields

P1: Add hookDispatcher.dispatch('beforeStep') alongside legacy
onBeforeStep callback. All 4 hook types now dispatch correctly:
beforeStep, afterStep, onComplete, onError.

P2: Fix completion event payload to use actual AgentState fields
(state.cost.total, state.usage.llm.*, state.messages) instead of
non-existent state.session.* properties. Matches the field access
pattern in triggerCompletionWebhook.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: update eval test assertions for hooks migration + fix status type

- Test: update executeTrajectory assertion to expect hooks array
  instead of completionWebhook object
- Fix: add fallback for event.status (string | undefined) when passing
  to recordTrajectoryCompletion/recordThreadCompletion (status: string)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: update SubAgent test assertions for hooks migration

Update execGroupSubAgentTask tests to expect hooks array instead of
stepCallbacks object, matching the SubAgent → hooks migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 12:05:25 +08:00
Rylan Cai 6ce9d9a814 🐛 fix: agent stream error in local dev (#13054)
* 🐛 fix: close local agent streams on terminal errors

* ♻️ refactor: revert redundant cli stream error handling

* 🧪 test: remove redundant cli stream error test

* wip: prune tests

* 🐛 fix: guard terminal agent runtime end step index
2026-03-20 11:39:54 +08:00
Neko f51da14f07 🔧 chore(locales): use "created" for "sent" in "sent x messages" (#13140) 2026-03-20 11:08:13 +08:00
Protocol Zero bc8debe836 🐛 fix(chat): strip forkedFromIdentifier before LLM API request (#13142)
fix(chat): strip forkedFromIdentifier before LLM API request

Fork & Chat stores forkedFromIdentifier in agent.params for DB lookup.
Spreading params into the chat payload forwarded it to Responses API,
causing strict providers (e.g. AiHubMix) to reject the request.

Remove the field in getChatCompletion alongside existing non-API keys.

Fixes lobehub/lobehub#13071

Made-with: Cursor
2026-03-20 11:07:29 +08:00
Neko 1b909a74d7 🔧 chore(locales): missing category locale for productivity (#13141) 2026-03-20 03:46:23 +08:00
Arvin Xu 04f963d1da ♻️ refactor: use incremental diff for snapshot messages to prevent OOM (#13136)
* ♻️ refactor: use incremental diff for snapshot messages to prevent OOM

Replace full messages/messagesAfter duplication per step with baseline + delta approach:
- Step 0 and compression resets store full messagesBaseline
- Other steps store only messagesDelta (new messages added)
- Strip llm_stream events from snapshot (not useful for post-analysis)
- Strip messages from done.finalState (reconstructible from delta chain)
- Strip duplicate toolResults from context.payload
- Reduce context_engine_result event size by removing messages and toolsConfig
- Add reconstructMessages() utility for rebuilding full state from delta chain
- AiAgentService constructor now accepts runtimeOptions for DI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: add incremental toolset delta for snapshot

- Store operationToolSet as toolsetBaseline in step 0 only (immutable)
- Track activatedStepTools changes via per-step activatedStepToolsDelta
- Strip operationToolSet/toolManifestMap/tools/toolSourceMap from done.finalState
- Add reconstructToolsetBaseline() and reconstructActivatedStepTools() utilities

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: correct snapshot delta recording and restore context-engine output

- P1: messagesDelta now always stores only appended messages (afterMessages.slice),
  fixing duplication when isBaseline was true (step 0 / compression reset)
- P2: Restore context_engine_result.output (processedMessages) — needed by
  inspect CLI for --env, --system-role, and -m commands
- Add P1 regression test for message deduplication

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 01:01:35 +08:00
YuTengjing d6f75f3282 feat(model-runtime): add xiaomimimo to RouterRuntime base runtime map (#13137) 2026-03-20 01:01:06 +08:00
YuTengjing 563f4a25f1 💄 style: add XiaomiMiMo LobeHub-hosted model cards and fix pricing (#13133) 2026-03-19 23:51:23 +08:00
Zhijie He e2d25be729 💄 style: add mimo-v2-pro & mimo-v2-omni support (#13123) 2026-03-19 22:14:20 +08:00
Innei 80cb6c9d11 feat(chat-input): add category-based mention menu (#13109)
*  feat(chat-input): add category-based mention menu with keyboard navigation

Replace flat mention list with a structured category menu (Agents, Members, Topics).
Supports home/category/search views, Fuse.js fuzzy search, floating-ui positioning,
and full keyboard navigation.

* 🔧 chore: update @lobehub/editor to version 4.3.0 and refactor type definition in useMentionCategories

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(MentionMenu): enhance icon rendering logic in MenuItem component

Updated the MenuItem component to improve how icons are rendered. Now, it checks if the icon is a valid React element or a function, ensuring better flexibility in icon usage. This change enhances the overall user experience in the mention menu.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/editor to version 4.3.1 in package.json

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-19 21:48:10 +08:00
YuTengjing 57ec43cd00 🐛 fix(database): add drizzle-zod and zod as peer dependencies to fix type-check errors (#13132) 2026-03-19 21:24:41 +08:00
Innei 0f67a5b8d7 💄 style(desktop): improve WelcomeStep layout centering in onboarding (#13125)
* 💄 style(desktop): improve WelcomeStep layout centering in onboarding

Made-with: Cursor

* 🐛 fix(desktop): validate remote server URL in isRemoteServerConfigured

Made-with: Cursor
2026-03-19 21:18:41 +08:00
Innei 8d387a98a0 🐛 fix(editor): correct empty editor state structure and wide screen layout (#13131)
- Fix EMPTY_EDITOR_STATE with proper Lexical node structure (root id, paragraph id)
- Add flex-grow to WideScreenContainer for proper editor canvas expansion

Made-with: Cursor
2026-03-19 21:07:18 +08:00
YuTengjing 3931aa9f76 🐛 fix(auth): add BusinessAuthProvider slot to auth layout (#13130) 2026-03-19 18:56:45 +08:00
YuTengjing 73d46bb4c4 feat(ci): add Claude PR auto-assign reviewer workflow (#13120) 2026-03-19 16:13:01 +08:00
Innei f827b870c3 feat(version): display actual desktop app version with canary suffix (#13110)
*  feat(version): display actual desktop app version with canary suffix

Add support for fetching and displaying the desktop application's actual version number in the About section. When running on desktop, the version now displays the desktop app's version (including canary suffix if applicable), falling back to the web version if unavailable.

- Add getAppVersion IPC method in SystemController
- Create versionDisplay utility module with comprehensive tests
- Integrate desktop version fetching in Version component

* ♻️ refactor(desktop): inject about version at build time
2026-03-19 14:24:03 +08:00
Neko efd99850df feat(agentDocuments): added agent documents impl, and tools (#13093) 2026-03-19 14:05:02 +08:00
Neko 87c770cda7 🔨 chore: use percentage value for Codecov (#13121)
build: use percentage value
2026-03-19 13:28:48 +08:00
YuTengjing 715481c471 🐛 fix(portal): preserve artifacts code scroll while streaming (#13114) 2026-03-19 00:35:20 +08:00
YuTengjing 25e1a64c1b 💄 style: update Grok 4.20 to 0309 and add MiniMax M2.7 models (#13112) 2026-03-19 00:05:07 +08:00
Innei 465c9699e7 feat(context-engine): inject referenced topic context into last user message (#13104)
*  feat: inject referenced topic context into last user message

When users @refer_topic in chat, inject the referenced topic's summary
or recent messages directly into the context, reducing unnecessary tool calls.

* 🐛 fix: include agentId and groupId in message retrieval for context engineering

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: skip topic reference resolution for messages with existing topic_reference_context

Added logic to prevent double injection of topic references when messages already contain the topic_reference_context. Updated tests to verify the behavior for both cases: when topic references should be resolved and when they should be skipped.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-18 21:58:41 +08:00
Innei ac29897d72 ♻️ refactor(perf): user message renderer (#13108)
refactor(perf): user message renderer
2026-03-18 21:58:29 +08:00
YuTengjing 1df5ae32f1 🐛 fix: distinguish SSRF block errors from network errors (#13103) 2026-03-18 18:16:19 +08:00
Innei 8a90f79c11 ♻️ refactor(nav): remove devOnly mode from nav layout and stabilize Footer (#13101)
* ♻️ refactor(nav): remove devOnly mode from nav layout and stabilize Footer during panel transitions

- Remove devOnly filtering from useNavLayout, treat all items as non-dev mode
- Move Pages to top nav position, remove video/image/settings/memory nav items
- Extract Footer from SideBarLayout into NavPanelDraggable outside animation layer
- Show settings ActionIcon in Footer when dev mode is enabled (hidden on settings page)

* 🔧 fix(footer): update settings icon in Footer component

- Replace Settings2 icon with Settings icon in the Footer when dev mode is enabled, ensuring consistency in the user interface.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-18 15:44:51 +08:00
LobeHub Bot 91ec7b412b 🌐 chore: translate non-English comments to English in ProfileEditor and related features (#13048)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 15:40:07 +08:00
YuTengjing e9766be3f3 🐛 fix: pass userId in initModelRuntimeFromDB (#13100) 2026-03-18 15:11:29 +08:00
Rylan Cai 52652866e0 feat: support server context compression (#12976)
* ♻️ refactor: add eval-only server context compression

* ♻️ refactor: align eval compression with runtime step flow

* ♻️ refactor: trim redundant call_llm diff

*  add mid-run context compression step

* 📝 document post compression helper

* 🐛 revert unnecessary agent runtime service diff

* ♻️ refactor: clean up context compression follow-up logic

* ♻️ refactor: move compression gate before call llm

* ♻️ refactor: make call llm compression gate explicit

* ♻️ refactor: restore agent-side compression checks

* ♻️ refactor: rename agent llm continuation helper

* ♻️ refactor: inline agent compression helper

* ♻️ refactor: preserve trailing user message during compression

* 📝 docs: clarify toLLMCall refactor direction

*  test: add coverage for context compression flow

*  reset: unstash
2026-03-18 12:48:34 +08:00
YuTengjing 95ef230354 💄 style: add GPT-5.4 mini and nano models (#13094) 2026-03-18 12:34:31 +08:00
lobehubbot b894622dfe Merge remote-tracking branch 'origin/main' into canary 2026-03-18 04:29:39 +00:00
Arvin Xu ae77fee1b8 👷 build: add settings column to agent_bot_providers (#13081) 2026-03-18 12:28:58 +08:00
YuTengjing 7cd4b1942f 💄 style: use credit terminology in auto top-up tooltips (#13091) 2026-03-18 11:12:39 +08:00
Rylan Cai 69c24c714e 🔧 chore(eval): improve trajectory workflow controls and execution metadata (#13049)
* 🔧 chore(search): reduce Exa default result count

* 🐛 fix(eval): relax run input schema limits

*  feat(agent): persist tool execution time in message metadata

* 🔧 chore(eval): add flow control to trajectory workflows

* 🧪 test: adjust Exa numResults expectation
2026-03-18 10:29:49 +08:00
Sirui He 3a789dc612 🐛 fix: SPA HTML entry returns stale content after server upgrade (#12998)
fix: add no-cache header to SPA HTML entry point

Prevent stale SPA HTML from being served after server upgrades.
JS/CSS assets still cache normally via hashed filenames.
2026-03-18 01:27:52 +08:00
Xial 46455cb6c3 🐛 fix: load PDF.js worker from local assets via Vite ?url import (#13006) 2026-03-18 01:26:10 +08:00
YuTengjing 81becc3583 🐛 fix(model-runtime): handle Responses API tool pairing and context limit errors (#13078) 2026-03-18 00:07:10 +08:00
YuTengjing cb0037ce1e 🐛 fix: pass userId to all embeddings API calls (#13077) 2026-03-17 23:44:34 +08:00
Innei 03f3a2438c 🐛 fix(skills): repair db-migrations frontmatter (#13073) 2026-03-17 23:32:14 +08:00
Innei 4994d19a9c 🐛 fix(desktop): remove electron-liquid-glass to fix click event blocking (#13070)
* 🐛 fix(desktop): remove electron-liquid-glass to fix click event blocking

The electron-liquid-glass native addon was blocking all click events in the
Electron desktop app window. Remove the dependency and restore vibrancy-based
transparency with semi-transparent body background via `.desktop` CSS class.

* 🔨 chore(desktop): remove electron-liquid-glass from native modules config
2026-03-17 22:56:29 +08:00
YuTengjing f8d51bbf4f 🐛 fix(model-runtime): filter internal thinking content in openai-compatible payloads (#13067) 2026-03-17 22:28:08 +08:00
YuTengjing 189e5d5a20 🐛 fix(model-runtime): prune unsupported xAI reasoning penalties (#13066) 2026-03-17 22:27:59 +08:00
Innei b2122a5224 ♻️ refactor: replace per-message useNewScreen with centralized useConversationSpacer (#13042)
* ♻️ refactor: replace per-message useNewScreen with centralized useConversationSpacer

Replace the old per-message min-height approach with a single spacer element appended to the virtual list, simplifying scroll-to-top UX when user sends a new message.

* 🔧 refactor: streamline handleSendButton logic and enhance editor focus behavior

Removed redundant editor null check and added double requestAnimationFrame calls to ensure the editor is focused after sending a message.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-17 21:19:58 +08:00
Innei d2d9e6034e 🐛 fix(chat): clear input immediately on send to preserve drafts during streaming (#13038)
* 🐛 fix: clear input immediately on send to preserve drafts typed during streaming

Move inputMessage reset before the async streaming lifecycle so text
entered while the assistant is responding is not overwritten on completion.

Also normalize null/undefined in operation context matching so that
cancelOperations works correctly in null-topic sessions.

Fixes LOBE-2647

* 🐛 fix: resolve TS2322 null-vs-undefined type error in useOperationState test
2026-03-17 21:14:40 +08:00
YuTengjing 97f4a370ab feat: add request trigger tracking, embeddings billing hooks, and memory extraction userId fix (#13061) 2026-03-17 20:54:28 +08:00
YuTengjing 62a6c3da1d 🌐 i18n: add pending_reward status translation for referral table (#13065) 2026-03-17 20:08:18 +08:00
YuTengjing 10b7906071 🔨 chore: add device fingerprint utility and pending_reward status (#13062) 2026-03-17 18:49:35 +08:00
YuTengjing 3207d14403 🔨 chore: add batch query methods for UserModel and MessageModel (#13060) 2026-03-17 18:20:03 +08:00
Innei 8f7527b7e2 feat(desktop): Linux window specialization (#13059)
*  feat(desktop): Linux window specialization

- Add minimize/maximize/close buttons for Linux (WinControl)
- Linux: no tray, close main window quits app
- Linux: native window shadow and opaque background
- i18n for window control tooltips

Made-with: Cursor

* 🌐 i18n: add window control translations for all locales

Made-with: Cursor

* 🐛 fix(desktop): show WinControl in SimpleTitleBar only on Linux

Made-with: Cursor

* 🐛 fix(desktop): limit custom titlebar controls to Linux

Avoid rendering duplicate window controls on Windows and keep the Linux maximize button in sync with the current window state.

Made-with: Cursor

---------

Co-authored-by: LiJian <onlyyoulove3@gmail.com>
2026-03-17 16:59:33 +08:00
LiJian 26269eacbb 🐛 fix: slove the market oidc lost the call tools error (#13025)
* fix: slove the market oidc lost the call tools error

* fix: add the beta-version & add some log

* fix: fixed the oidc error ts
2026-03-17 11:27:19 +08:00
Zhijie He 78cfb087b4 💄 style: update claude 4.6 series 1M contextWindow (#12994)
* style: update claude 4.6 series 1M contextWindow

* chore: cleanup bedrock search tag

chore: cleanup bedrock retired model

chore: cleanup bedrock retired model

chore: cleanup bedrock retired model

* fix: fix ci test
2026-03-17 10:58:20 +08:00
Zhijie He 2717f8a86c 💄 style: add Seedance 1.5 Pro support for OSS (#13035)
* style: add seedance 1.5 support for OSS

* style: update volcengine videoGen models
2026-03-17 10:43:25 +08:00
Arvin Xu 44e4f6e4b0 ️ perf: optimize tool system prompt — remove duplicate APIs, simplify XML tags (#13041)
* 💄 style: remove platform-specific Spotlight reference from searchLocalFiles

Replace "using Spotlight (macOS) or native search" with "using native search"
since the actual search implementation is platform-dependent and the LLM
doesn't need to know the specific backend.

Fixes LOBE-5778

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ️ perf: remove duplicate API descriptions from tool system prompt

API identifiers and descriptions are already in the tools schema passed
via the API tools parameter. Repeating them in the system prompt wastes
tokens. Now only tools with systemRole (usage instructions) are injected.

Also rename XML tags: plugins→tools, collection→tool,
collection.instructions→tool.instructions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: inject tool description when no systemRole instead of skipping

Tools without systemRole now show their description as <tool> children.
Tools with systemRole use <tool.instructions> wrapper as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: always emit <tool> tag, fallback to "no description"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update tools

* fix

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:01:05 +08:00
Arvin Xu 9bdc3b0474 feat: improve agent context injection (skills discovery, device optimization, prompt cleanup) (#13021)
*  feat: inject all installed skills into <available_skills> for AI discovery

Previously, only skills explicitly added to the agent's plugins list appeared
in <available_skills>. Now all installed skills are exposed so the AI can
discover and activate them via activateSkill.

Changes:
- Frontend: use getAllSkills() instead of getEnabledSkills(plugins)
- Backend: pass skillMetas through createOperation → RuntimeExecutors → serverMessagesEngine
- Add skillsConfig support to serverMessagesEngine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use DB + builtin skills for available_skills instead of provider manifests

lobehubSkillManifests are tool provider manifests (per-provider, containing
tool APIs), not skill metadata. Using them for <available_skills> incorrectly
showed provider names (e.g. "Arvin Xu") as skills.

Now fetches actual skills from AgentSkillModel (DB) + builtinSkills for correct
<available_skills> injection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 💄 style: use XML structure for online-devices in system prompt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ♻️ refactor: extract online-devices prompt to @lobechat/prompts package

Move device XML prompt generation from builtin-tool-remote-device into
the shared prompts package for reusability and consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: add failing tests for Remote Device suppression when auto-activated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ️ perf: suppress Remote Device tool when device is auto-activated

When a device is auto-activated (single device in IM/Bot or bound device),
the Remote Device management tool (listOnlineDevices, activateDevice) is
unnecessary — saves ~500 tokens of system prompt + 2 tool functions.

- Add autoActivated flag to deviceContext
- Move activeDeviceId computation before tool engine creation
- Disable Remote Device in enableChecker when autoActivated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update system role

* update system role

* ♻️ refactor: use agentId instead of slug for OpenAPI responses model field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: use JSON round-trip instead of structuredClone in InMemoryAgentStateManager

structuredClone fails with DataCloneError when state contains non-cloneable
objects like DOM ErrorEvent (from Neon DB WebSocket errors).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: only inject available_skills when tools are enabled

Restore plugins guard to prevent skills injection when tool use is
disabled (plugins is undefined), fixing 28 test failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

*  test: update system message assertions for skills injection

Use stringContaining instead of exact match for system message content,
since available_skills may now be appended after the date.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 00:35:18 +08:00
Arvin Xu 41c1b1ee85 🐛 fix: use jsonb ? operator to avoid Neon rt_fetch bug (#13040)
🐛 fix: use jsonb ? operator instead of ->> to avoid Neon rt_fetch bug

The ->> operator in WHERE clauses triggers a Neon-specific
`rt_fetch used out-of-bounds` error. Switch to the ? operator
which is semantically equivalent for checking key existence.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 23:33:35 +08:00
Innei 23385abaea ♻️ refactor: centralize NavBar dev mode logic into useNavLayout hook (#13037)
* ♻️ refactor: centralize NavBar dev mode logic into useNavLayout hook

Extract scattered isDevMode checks from Nav, BottomMenu, Footer, and
UserPanel into a single useNavLayout hook with declarative devOnly
metadata. Also restore dev-mode-gated home page modules and fix
LangButton visual alignment in UserPanel.

*  test: update PanelContent test to match LangButton Menu removal
2026-03-16 23:16:13 +08:00
YuTengjing fc5b462892 ️ perf: optimize search with BM25 indexes and ICU tokenizer (#12914) 2026-03-16 21:37:57 +08:00
LobeHub Bot 935304dbd2 🌐 chore: translate non-English comments to English in features/MCPPluginDetail (#13008)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 21:21:45 +08:00
YuTengjing d2666b735b feat: add ModelRuntime hooks for billing lifecycle interception (#13013) 2026-03-16 20:59:40 +08:00
Arvin Xu 69accd11df 🐛 fix: return structured error from invokeBuiltinTool instead of undefined (#13020)
When a builtin tool executor is not found, invokeBuiltinTool now returns
a structured error object instead of silently returning undefined. Also
adds a fallback in call_tool executor for undefined results to prevent
agent loop from terminating abnormally.

Fixes LOBE-5318

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 20:12:48 +08:00
lobehubbot 9fa060f01e 🔖 chore(release): release version v2.1.43 [skip ci] 2026-03-16 11:53:29 +00:00
lobehubbot 7a8f682879 Merge remote-tracking branch 'origin/main' into canary 2026-03-16 11:51:39 +00:00
YuTengjing 70a74f485a 👷 build: add BM25 indexes with ICU tokenizer for search optimization (#13032) 2026-03-16 19:50:57 +08:00
YuTengjing cec079d34b 🗃️ db: add BM25 indexes with ICU tokenizer for 14 tables 2026-03-16 19:41:19 +08:00
Innei ee8eade485 🔨 chore: add trpc mock.vite stub to stop Vite SPA warmup from traversing server router (#13022)
Made-with: Cursor
2026-03-16 18:10:35 +08:00
Rdmclin2 d9388f2c31 🐛 fix: add skill crash (#13011)
* fix: Error Page style lost

* fix: add skill button error

* chore: add add skill e2e tests

* chore: remove unnecessary skill
2026-03-16 16:46:49 +08:00
Innei bffdbf8ad4 🐛 fix: upgrade desktop agent-browser to v0.20.1 and default native mode (#12985)
* 🐛 fix(desktop): update bundled agent-browser to v0.20.1 and align native-mode docs

Upgrade desktop bundled agent-browser to 0.20.1 and remove obsolete AGENT_BROWSER_NATIVE runtime override since native mode is now default. Update builtin agent-browser skill descriptions to reflect the new default behavior.

Made-with: Cursor

*  feat: enable agent-browser skill on Windows desktop

Made-with: Cursor

* 🔧 refactor: remove isWindows from ToolAvailabilityContext interface

Updated the ToolAvailabilityContext interface to remove the isWindows property, simplifying the context checks in the isBuiltinSkillAvailableInCurrentEnv function.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-16 16:41:17 +08:00
Rdmclin2 51d6fa7579 🐛 fix: model provider pop up problems (#13012)
* fix: model provider pop up problems

* chore: optimize list scroll
2026-03-16 16:27:45 +08:00
Arvin Xu 517a67ced7 🐛 fix: respect agent-level memory config priority over user settings (#13018)
* update skills

* 🐛 fix: respect agent-level memory config priority over user settings

Agent chatConfig.memory.enabled now takes priority. Falls back to user-level
memory setting when agent config is absent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix: resolve tsgo type error in memory integration test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 15:48:14 +08:00
YuTengjing 1d1e48d1b5 ♻️ refactor: split Stats into separate settings tab and update i18n (#13016)
* 🌐 i18n: add auto top-up payment method hint translations

* ♻️ refactor: split Stats into separate settings tab and rename Subscription group to Plans

* 🌐 i18n: update auto top-up payment method hint copy

* 🌐 i18n: add auto top-up payment method hint translations for all locales

* 🌐 i18n: rename Subscription Plans tab to Plans

* 🌐 i18n: add high usage FAQ, rename Text Generation to Chat Message, rename tab.plans
2026-03-16 14:45:31 +08:00
René Wang 70ef815692 🐛 fix: select first provider on click for multi-provider model items (#12968) 2026-03-16 14:08:10 +08:00
lobehubbot a2c22f705d Merge remote-tracking branch 'origin/main' into canary 2026-03-16 03:49:09 +00:00
Neko 93ee1e30af 👷 build: add agent_documents table (#12944) 2026-03-16 11:48:30 +08:00
LobeHub Bot a1fdd56565 🌐 chore: translate non-English comments to English in packages/database (#12975)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 11:01:39 +08:00
LobeHub Bot 4bfec4191e test: add unit tests for error utility functions (#12996)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 10:58:43 +08:00
LobeHub Bot cb955048f3 🌐 chore: translate non-English comments to English in openapi-services (#12993)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:57:00 +08:00
Arvin Xu 6a4d6c6a86 🛠 chore: support injectable snapshot store in AgentRuntimeService (#12984) 2026-03-15 01:00:56 +08:00
Arvin Xu adbf11dc11 📝 docs: update documents (#12982)
update document
2026-03-14 22:06:09 +08:00
Arvin Xu a96cac59d7 🛠 chore: add subscribeStreamEvents to InMemoryStreamEventManager (#12964)
*  feat: add subscribeStreamEvents to InMemoryStreamEventManager and use factory for stream route

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix: remove duplicate agentExecution types and fix stream route test mock

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 13:07:46 +08:00
lobehubbot ae9e51ec12 🔖 chore(release): release version v2.1.42 [skip ci] 2026-03-14 04:03:41 +00:00
lobehubbot 6052b67953 Merge remote-tracking branch 'origin/main' into canary 2026-03-14 04:01:59 +00:00
Innei 9bb9222c3d 🐛 fix(ci): create stable update manifests for S3 publish (#12974) 2026-03-14 12:01:21 +08:00
YuTengjing 46eb28dff4 feat: add i18n keys for auto top-up feature (#12972) 2026-03-14 02:16:53 +08:00
YuTengjing 4aadfd608b 🐛 fix: require valid action for referral backfill and add anti-abuse rule (#12958) 2026-03-14 01:48:07 +08:00
Rdmclin2 942412155e feat: support skill activite switch back (#12970)
* feat: support skill activate mode

* feat: support skill panel search

* chore: update i18n files

* chore: update i18n files
2026-03-13 23:15:31 +08:00
Coooolfan 8373135253 🐛 fix: prevent Enter key submission during IME composition in LoginStep (#12963)
* 🐛 fix: prevent Enter key submission during IME composition in LoginStep

* ♻️ refactor: extract useIMECompositionEvent hook for IME composition tracking

Made-with: Cursor

---------

Co-authored-by: Innei <tukon479@gmail.com>
2026-03-13 22:41:26 +08:00
Innei 4438b559e6 feat: add slash action tags, topic reference tool, and command bus system (#12860)
*  feat: add slash action tags in chat input

Made-with: Cursor

*  feat: enhance editor with new slash actions and localization updates

- Added new slash actions: change tone, condense, expand, polish, rewrite, summarize, and translate.
- Updated localization files for English and Chinese to include new action tags and slash commands.
- Removed deprecated useSlashItems component and integrated its functionality directly into InputEditor.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: add slash placement configuration to chat input components

- Introduced `slashPlacement` prop to `ChatInputProvider`, `StoreUpdater`, and `InputEditor` for customizable slash menu positioning.
- Updated initial state to include `slashPlacement` with default value 'top'.
- Adjusted `ChatInput` and `InputArea` components to utilize the new `slashPlacement` prop.

This enhancement allows for better control over the user interface in chat input interactions.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: implement command bus for slash action tags processing

Add command bus system to parse and execute slash commands (compact context,
new topic). Refactor action tag categories from ai/prompt to command/skill.
Add useEnabledSkills hook for dynamic skill registration.

* feat: compress command

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: compress

Signed-off-by: Innei <tukon479@gmail.com>

* fix: skill inject

*  feat: slash action tags with context engine integration

Made-with: Cursor

*  feat: add topic reference builtin tool and server runtime

Made-with: Cursor

*  feat: add topic mention items and update ReferTopic integration

Made-with: Cursor

* 🐛 fix: preserve editorData through assistant-group edit flow and update RichTextMessage reactively

- EditState now forwards editorData from EditorModal to modifyMessageContent
- modifyMessageContent accepts and passes editorData to updateMessageContent
- RichTextMessage uses useEditor + effect to update document on content change instead of key-based remount
- Refactored RichTextMessage plugins to use shared createChatInputRichPlugins()

*  feat(context-engine): add metadata types and update processors/providers

Made-with: Cursor

*  feat(chat-input): add slash action tags and restore failed input state

* 🔧 chore: update package dependencies and enhance Vite configuration

- Changed @lobehub/ui dependency to a specific package URL.
- Added multiple SPA entry points and layout files to the Vite warmup configuration.
- Removed unused monorepo packages from sharedOptimizeDeps and added various dayjs locales for better localization support.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui dependency to version 5.4.0 in package.json

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: correct SkillsApiName.runSkill to activateSkill and update trimmed content assertions

* 🐛 fix: resolve type errors in context-engine tests and InputEditor slashPlacement

* 🐛 fix: update runSkill to activateSkill in conversationLifecycle test

* 🐛 fix: avoid regex backtracking in placeholder parser

*  feat(localization): add action tags and tooltips for slash commands across multiple languages

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: preserve file attachments when /newTopic has no text content

* cleanup

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-13 22:17:36 +08:00
Innei d7bfd1b6c8 🐛 fix: fix error collapse default active key (#12967) 2026-03-13 21:35:35 +08:00
Innei 110f27f2ac ♻️ refactor: merge beta settings into advanced tab (#12962)
* ♻️ refactor: merge beta settings into advanced tab

- Remove dedicated beta settings tab (desktop only)
- Integrate update channel selection into advanced settings
- Rename i18n keys from tab.beta.* to tab.advanced.updateChannel.*
- Mark SettingsTabs.Beta as deprecated
- Clean up unused FlaskConical icon import
- Update all 18 locale files with migrated keys

* 🔥 chore: remove deprecated SettingsTabs.Beta enum value

* 🔀 refactor: redirect deprecated /settings/beta to /settings/advanced

* 🔥 chore: remove unnecessary beta redirect from REDIRECT_MAP

* 🐛 fix: resolve lint errors and update outdated User panel tests

---------

Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2026-03-13 20:29:07 +08:00
LobeHub Bot e4d960376c test: add unit tests for search impls (brave, exa, tavily) (#12960)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-13 19:43:31 +08:00
lobehubbot 7bcde61e5d 🔖 chore(release): release version v2.1.41 [skip ci] 2026-03-13 10:47:25 +00:00
lobehubbot 7d2f88f384 Merge remote-tracking branch 'origin/main' into canary 2026-03-13 10:45:42 +00:00
Rdmclin2 3712d75bf8 🚀 release: 20260313 (#12956)
This release includes **~400 commits**. Key updates are below.

### New Features and Enhancements

- **Bot Platform Integration**: Added abstract bot platform layer with
**QQ Bot**, **Telegram Bot**, **Lark/Feishu Bot**, and **Discord Bot**
integrations, including remote device support for IM integration.
- **LobeHub CLI**: Full CLI implementation across 5 phases — agent
run/status, generate (text/image/video/TTS/ASR), doc, search, device,
bot integration, cron, topic share, agent KB/file/pin, thread, and eval
commands.
- **Agent Skills**: Added built-in skills management, skill store, agent
browser automation skill, and tool detection.
- **Video Generation**: End-to-end video generation feature with free
quota, webhook handling, and skeleton loading.
- **Agent Benchmark**: Added benchmark support with external scoring
mode and dedicated DB schema.
- **Memory Settings**: Support for memory effort/tool permission
configuration, user persona injection, and improved memory analysis.
- **Batch Topic Deletion** from file support.
- **Runtime Config** support for flexible deployment configuration.
- **V1 API** and **Response API** support (including OpenAI Responses
API).
- **Device Code Auth Flow** for CLI authentication.
- **Emoji Reactions** for messages.
- **Starter Suggested Questions** and recommend agents.
- **Page Tabs** for Electron desktop.
- **Sort Topics by Updated Time** option.
- **Change Email Address** in profile settings.
- **Model Detail Dropdown** in model switch panel.
- Added **unread completion indicator** for agents and topics.

### Models and Provider Expansion

- New providers: **Straico**, **LongCat (美团)**.
- Added/updated model support:
  - **GPT-5.4** series
  - **Claude Sonnet 4.6** and **Claude Opus 4.6** (including Bedrock)
  - **Gemini 3.1 Pro Preview** and **Gemini 3.1 Flash Lite Preview**
  - **Qwen3.5** series (including Flash, OSS, and SiliconCloud models)
  - **Grok 4.20** series and **Grok Imagine** image generation
  - **Kimi K2.5** thinking models
  - **MiniMax 2.5** / **MiniMax M2.5**
  - **Nano Banana 2**
  - **Seedream 5 Lite** / **Seedance 2.0**
  - **NVIDIA** new models
  - **GLM-5**, **GLM-4.6V**, **GLM-Image** for Zhipu
  - Additional Qwen image-to-image and text-to-image models
- Added video input support for SiliconCloud provider.
- Use Response API for Grok as default.

### Desktop Improvements

- Integrated `electron-liquid-glass` for macOS Tahoe.
- Unified canary with stable app name/icon, added channel tag in About.
- Support clearing hotkey bindings in ShortcutManager.
- Subscription pages embedding with webview.
- Enhanced desktop menu and navigation system.
- Proactive token refresh on app startup and activation.
- DMG background image configuration.
- S3 publish for canary/nightly with cleanup.
- Unified update channel switching with S3 distribution.

### Architecture and Infrastructure

- **Vite SPA Migration**: Migrated frontend from Next.js App Router to
Vite SPA, restructured SPA routes to `src/routes` and `src/router`.
- **Response API Support** across agent runtime.
- Refactored client agent runtime and centralized tool availability
checks.
- Added Redis pipeline support and Lua script execution.
- Database migrations: `pg_search` extension, video generation schema,
agent skills schema, benchmark schema, topics description column, API
key hash column, ID migration to nanoid.
- Preload bundled i18n resources with lazy-load for target language.
- Simplified build config, removed webpack customization, and resolved
Vercel OOM.
- Class-based Zustand actions with `flattenActions` migration.
- Extracted `@lobechat/local-file-shell` shared package.
- Resolved all ESLint suppressions and enabled `consistent-type-imports`
rule.

### Stability, Security, and UX Fixes

- Fixed model provider popup problems and ModelSelect crash.
- Fixed tool engine, input-loading, and MCP tool install loading issues.
- Hardened Anthropic message building and sampling parameter handling.
- Fixed Vertex AI 400 error from duplicate tool function declarations.
- Fixed context window exceeded error detection from message text.
- Added rate limit custom rules for password reset and email
verification.
- Fixed `sanitizeFileName` path traversal risks.
- Fixed multiple Docker build issues (`@napi-rs/canvas`, `librt.so.1`,
`ffmpeg-static`).
- Fixed desktop advanced mode, onboarding redirect, and auth modal
during onboarding.
- Added unsaved changes guard to prevent data loss on navigation.
- Fixed SiliconCloud thinking mode toggle issue.
- Improved Moonshot interleaved thinking and circular dependency.
- Fixed multimodal `content_part` images rendered as base64 text.
- Security: upgraded `next-mdx-remote` to v6 for CVE-2026-0969.

### Credits

Huge thanks to these contributors (alphabetical):

@AmAzing- @AntoineRoux @BrandonStudio @CanisMinor @Coooolfan @eronez
@Hardy @huangkairan @Innei @Kingsword @LiJian @LuisSambrano @MarcellGu
@MikeLambert @Neko @rdmclin2 @Rdmclin2 @RenéWang @RuxiaoYin @RylanCai
@Shinji-Li @Sun13138 @sxjeru @VarunChawla @WangYK @YuTengjing @Zephyr
@ZhijieHe
2026-03-13 18:45:02 +08:00
Innei 7729adcfd4 🐛 fix: support topic share modal inside router (#12951)
🐛 fix(share-modal): support topic share modal
2026-03-13 17:27:46 +08:00
René Wang a09316a474 feat: Simplify UI (#12961)
* style: Simplify the sidebar

* style: Simplify the sidebar

* style: Simplify the sidebar

* style: Simpliofy the model selct

* style: Simpliofy the model selct

* style: Simpliofy the model selct

* style: Simpliofy the agent profile

* style: Simplify the input bar

* style: Re-organize the settings

* style: Simplify the mode linfo pane

* style: Simplify agent profile

* style: Advanced settings

* style: Advanced settings

* feat: Update translation

* fix: type error

* fix: Add missing translation

* fix: Add missing translation

* fix: Remove Lite mode

* fix: Add model paramters

* style: Remove token tag

* fix: model order

* fix: model order

* fix: Add missing translation

* fix: Add missing translation

* fix: Hide the subtopic button

* fix: User plan badge

* feat: Add settings

* feat: Add cover to the lab

* style: Make the switch vertically centered

* style: Add divider

* feat: Add group by provider

* feat: Move Usage stats

* fix: Subscription badge

* fix: Rebase onto canary

* fix: Rebase onto canary

* fix: Drag to adjust width

* feat: Rebase onto canary

* feat: Regroup settings tab

* feat: Regroup settings tab

* feat: Regroup settings tab

* feat: Regroup settings tab
2026-03-13 16:48:14 +08:00
Arvin Xu a5cc75c1ed 🐛 fix: lh command issue (#12949)
* fix command issue

* add run command UI

* fix API key

* add apikey page

* add apikey

* 🐛 fix: update apiKey model tests to use new sk-lh- prefix format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:16:11 +08:00
Rdmclin2 11ce1b2f9f 🐛 fix: model provider pop up problems (#12950)
fix: model provider pop up problems
2026-03-12 22:29:04 +08:00
Rdmclin2 afb6d8d3ca feat: bot platform abstract & QQ bot intergration (#12941)
* chore: add bot platform abstract

* chore: refactor platform abstract

* feat: support QQ platform

* docs : add qq channel

* fix: crypto algorithm

* fix: discord metion thread

* fix: discord threadId bypass

* fix: edit messsage throw error

* chore: update memory tool icon

* chore: use lobe channel icon

* chore: update platfom icon color

* fix: lint error
2026-03-12 21:25:15 +08:00
Arvin Xu 04a064aaf3 feat: support batch topic deletion from file (#12931)
Add `--file` option to `lh topic delete` command, allowing users to
pass topic IDs via a file (one per line or JSON array) for bulk deletion.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 20:56:17 +08:00
Innei 46f9135308 ♻️ refactor(tool): centralize availability checks (#12938)
* ♻️ refactor(tool): centralize availability checks

* 🐛 fix(tool): preserve windows skill fallback

* 🐛 fix(tool): restore stdio engine filtering
2026-03-12 20:17:02 +08:00
lobehubbot 425dd81bcf 🔖 chore(release): release version v2.1.40 [skip ci] 2026-03-12 11:42:06 +00:00
lobehubbot fd90f83f0f Merge remote-tracking branch 'origin/main' into canary 2026-03-12 11:40:28 +00:00
YuTengjing 3091489695 👷 build: add description column to topics table (#12939) 2026-03-12 19:39:47 +08:00
LiJian 4065dc0565 🐛 fix: improve skill exec script way (#12926)
* fix: add the activatedSkills to improve the execScripte tools

* feat: change the activePath into call market endpoint

* fix: clean the code

* feat: fixed the execScript in desktop ts error
2026-03-12 17:29:59 +08:00
Rdmclin2 3529b46f2c 💄 style: restore foot gap (#12936)
chore: add back padding
2026-03-12 17:17:06 +08:00
Innei 8b29bb7fc9 feat: preload bundled i18n resources and lazy-load target language (#12929)
 feat: preload bundled i18n resources synchronously and reload actual language in background

For non-default languages, preload bundled en-US resources synchronously to avoid
Suspense on first render, then reload the user's actual language from backend
in the background. This ensures instant rendering with fallback text while the
correct translations load asynchronously.
2026-03-12 16:42:03 +08:00
Rdmclin2 804eb57dd8 💄 style: fix skill banner gap and apporve mode icon style (#12930)
* fix: skill banner style and footer runtime config

* fix:  approval mode icon style fix
2026-03-12 15:33:08 +08:00
Arvin Xu 2399f672e2 feat: add lobehub skill (#12922)
* add builtin lobehub skills

* refactor cloud sandbox

* refactor cloud sandbox

* improve styles
2026-03-12 14:00:35 +08:00
Arvin Xu 9c9e8e8ece 🐛 fix: tool engine and input-loading (#12908)
* 🐛 fix: ensure always-on builtin tools and user-selected plugins are enabled in tool engine

- Add alwaysOnToolIds (lobe-tools, lobe-skills) that are always enabled regardless of user selection
- Include user-selected plugins in enableChecker rules for both frontend and server-side tool engines
- Change enableCheckerFactory default from enabled to disabled (tools must be explicitly enabled via rules)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix: improve input loading state to cover sendMessage through AI generation

- Add isInputLoading state that includes sendMessage operation type, so input stays
  in loading state from the moment user sends until AI finishes generating
- Add INPUT_LOADING_OPERATION_TYPES constant (superset of AI_RUNTIME_OPERATION_TYPES + sendMessage)
- Update ChatInput to use isInputLoading instead of isAIGenerating for disable/loading state
- Update stopGenerating to cancel all input-loading operations and restore editor on cancel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  test: fix stopGenerating tests to match updated action implementation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix agent

* 🐛 fix: add missing selector mocks in toolEngineering tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:35:48 +08:00
Zhijie He 2e45e24df3 💄 style: use Response API for Grok as default (#12843)
* sytle: use Response API for Grok

* chore: add unit test for response api only, cleanup xai unit test
2026-03-12 11:22:20 +08:00
Zhijie He fded8dbb4e 🔨 chore: extend video_url support for OpenAI SDK (#12885)
* style: update moonshot models

* 🔨 chore: extend `video_url` support for OpenAI SDK

* fix: fix ci error

* hotfix: fix sensenova baseUrl error

* fix: fix kimi-k2.5 video tag from LobeHub

* fix: wenxin flag

* chore: cleanup utils

* style: add video tag for `glm-4.1/4.5v`

remove video tag for sensenova due to not support in OpenAI mode
2026-03-12 11:20:48 +08:00
LobeHub Bot 709c9749d0 🌐 chore: translate non-English comments to English in packages/openapi/src (#12873)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2026-03-12 11:19:33 +08:00
sxjeru c07574af12 🔧 chore: refactor build scripts to prevent Vercel OOM (#12912)
* ♻️ refactor: update build scripts for improved performance and consistency

* 🐛 fix: update build:spa script to use pnpm for improved consistency
2026-03-12 10:39:29 +08:00
Arvin Xu b4624e6515 🔨 chore: add Response API support (#12918)
* add response api framework

* finish response api structure

* finish response api structure

*  feat: implement basic text generation for Response API (LOBE-5858)

- Add instructions extraction from system/developer input messages
- Add instructions param to ExecAgentParams, append to agent systemRole
- Implement extractPrompt, extractAssistantContent, extractUsage in ResponsesService
- Wire up execAgent + executeSync flow for non-streaming and streaming
- Add logprobs field to output_text content parts for schema compliance
- Fix truncation field to output string enum instead of object

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat: implement real token-level streaming for Response API (LOBE-5859)

- Replace fake streaming (executeSync → emit events) with real streaming
- Subscribe to InMemoryStreamEventManager for live stream_chunk events
- Run executeSync in background, convert text chunks to output_text.delta SSE events
- Add missing schema fields: item_id on content_part/text events, logprobs on delta/done events
- Fix content_part.added/done to include item_id per OpenResponses spec

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat: implement tool calling output extraction for Response API (LOBE-5860)

- Add extractOutputItems to convert AgentState messages to OpenResponses output items
- Extract assistant tool_calls → function_call output items
- Extract tool result messages → function_call_output output items
- Skip message items for assistant messages that have tool_calls (avoid duplicates)
- Add status field to function_call_output items per OpenResponses spec
- Update FunctionCallOutputItemSchema with optional status field
- Output array reflects execution order: function_call → function_call_output → message

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat: implement multi-turn conversations via previous_response_id (LOBE-5861)

Encode topicId in response.id to enable stateless multi-turn conversation
chaining. When previous_response_id is provided, extract topicId and pass
to execAgent via appContext, which automatically loads history messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix: add missing type fields for OpenResponses compliance (logprobs, item_id, input_tokens_details)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 10:39:08 +08:00
Arvin Xu f94f1ae08a feat(cli): CLI Phase 5 - agent KB/file/pin, thread, eval and miscellaneous command enhancements (#12920)
*  feat(cli): CLI Phase 5 - agent KB/file/pin, thread management, eval expansion

- Add agent subcommands: pin/unpin, kb-files, add-file/remove-file/toggle-file, add-kb/remove-kb/toggle-kb
- Create thread command with list/list-all/delete subcommands
- Expand eval with internal benchmark/dataset/testcase/irun management
- Move existing external eval commands under `eval ext` namespace
- Add comprehensive unit tests for all new functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 💄 style(cli): rename eval `irun` to `run` since external moved to `ext` namespace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ♻️ refactor(cli): merge external eval commands into unified tree with --external flag

Remove separate `eval ext` namespace; use `--external` flag on overlapping commands
(dataset get, run get) and integrate external-only commands directly into the tree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat(cli): CLI Phase 6 - miscellaneous command enhancements

- file: add upload (hash check + create), edit (move to folder), kb-items
- user: new command with info, settings, preferences, update-avatar, update-name
- model: add batch-update, sort order
- plugin: add create (without settings, distinct from install)
- generation: add delete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 09:47:16 +08:00
Arvin Xu 165697ce47 feat(cli): CLI Phase 4 - cron, message, topic share, agent-group, session-group (#12915)
*  feat(cli): CLI Phase 4 - cron, message enhance, topic share, agent-group, session-group

Add core commands to complete CLI coverage of TRPC routers:

- `lh cron` — Agent cron job management (list/view/create/edit/delete/toggle/reset/stats)
- `lh message` — Enhanced with create/edit/add-files/word-count/rank-models/delete-by-assistant/delete-by-group
- `lh topic` — Enhanced with clone/share/unshare/share-info/import
- `lh agent-group` — Agent group management (list/view/create/edit/delete/duplicate/add-agents/remove-agents)
- `lh session-group` — Session group management (list/create/edit/delete/sort)

Closes LOBE-5920

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update version

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:32:00 +08:00
Rdmclin2 14dd5d09dd feat: support runtime config (#12902)
* feat: support runtime config

* fix: cloud sandbox default tool ids
2026-03-11 23:43:33 +08:00
Innei 21d1f0e472 feat(settings): improve tool detector display layout (#12906)
*  feat(settings): improve tool detector display layout

- Move version to left side with Name, display as Tag
- Right side: two lines (Available status + path), right-aligned
- Unavailable: single line centered
- Add runtime environment detectors (Node, Python, npm)
- Add i18n for system tools settings

Made-with: Cursor

* 🔧 fix(toolDetectors): ensure successful version check for Python runtime

- Update pythonDetector to enforce successful invocation of `--version` for confirming usable runtime.
- Removed redundant version handling logic to streamline the detection process.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-11 19:55:36 +08:00
Rdmclin2 bc50db6a8b 🐛 fix: desktop advanced mode (#12911)
* fix: advanced mode empty

* fix: desktop channel router lost
2026-03-11 19:02:37 +08:00
LobeHub Bot 8db8dff7b0 test: add unit tests for MarketService (#12905)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 15:51:25 +08:00
LiJian 1a3c561e21 💄 style: add the history count limit back in agents params settings (#12199)
* fix: add the history count limit back in agents params settings

* fix: fixed the test

* fix: change the default settings snap the enableHistoryCount as false

* fix: change the history process to the first into MessageEngine

* fix: fixed some count limited

* fix: fixed the enableHistoryCount check test

* fix: change the getEnableHistoryCountById logic
2026-03-11 15:46:56 +08:00
Arvin Xu 8e60b9f620 feat(cli): CLI Phase 3 - bot integration, search & device (#12904)
* fix cli alias

* 🐛 fix(cli): fix gen text non-streaming mode and streaming SSE parsing

- Add `responseMode: 'json'` for non-streaming requests to get plain JSON instead of SSE
- Fix streaming SSE parser to handle LobeHub's JSON string format (e.g. `"Hello"`)
- Support both OpenAI and Anthropic response formats in non-streaming mode
- Add E2E tests for all generate commands (text, list, tts, asr, alias)
- Update skills knowledge.md docs with new kb commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat(cli): unify skill install command and add e2e tests

Merge import-github/import-url/import-market into a single `skill install <source>` command with auto-detection (GitHub URL/shorthand, ZIP URL, or marketplace identifier). Add alias `skill i`. Add comprehensive e2e and unit tests for skill commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🔨 chore: fix linter formatting in memory e2e test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix: add vitest-environment node declaration to aiProvider test

Fix server-side env variable access error by declaring node environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix cli review

* fix test

*  feat(cli): add web search and crawl support to search command

Add --web flag for web search via tools TRPC client, and search view
subcommand for viewing results (URLs via crawl, local resources by type:id).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat(cli): add device management command with TRPC endpoints

Add `lh device` command for managing connected devices via server-side
TRPC API, complementing the existing `lh connect` (device-as-client).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat(cli): add bot integration management command

Add `lh bot` top-level command for managing agent bot integrations
(Discord, Slack, Telegram, Lark/Feishu). Includes list, view, add,
update, remove, enable/disable, and connect subcommands.

Also adds `list` procedure to agentBotProvider TRPC router for
querying all bots with optional agent/platform filters.

Closes LOBE-5900

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:29:15 +08:00
Innei 874c2dd706 🐛 fix(i18n): preload default language from JSON to avoid Suspense on first render (#12895)
* 🐛 fix(i18n): preload default language from JSON to avoid Suspense on first render

- Sync load en-US common/error/chat from locales/en-US/*.json
- Use JSON (not locales/default/*.ts) as runtime values - TS source is type-only
- Prevents useTranslation from suspending, avoids CLS from 44px skeleton fallback

Made-with: Cursor

*  feat(i18n): enable partial loading of languages and add tests for dynamic namespace loading

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-11 14:00:39 +08:00
LobeHub Bot 4988413d58 🌐 chore: translate non-English comments to English in src/features/Electron (#12901)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 13:43:39 +08:00
YuTengjing f1dd2fc458 📝 docs: add catch error logging rule to TypeScript skill (#12903) 2026-03-11 12:10:36 +08:00
Arvin Xu aa8082d6b2 feat: lobehub cli for better agency agent (#12897)
* fix cli alias

* 🐛 fix(cli): fix gen text non-streaming mode and streaming SSE parsing

- Add `responseMode: 'json'` for non-streaming requests to get plain JSON instead of SSE
- Fix streaming SSE parser to handle LobeHub's JSON string format (e.g. `"Hello"`)
- Support both OpenAI and Anthropic response formats in non-streaming mode
- Add E2E tests for all generate commands (text, list, tts, asr, alias)
- Update skills knowledge.md docs with new kb commands

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat(cli): unify skill install command and add e2e tests

Merge import-github/import-url/import-market into a single `skill install <source>` command with auto-detection (GitHub URL/shorthand, ZIP URL, or marketplace identifier). Add alias `skill i`. Add comprehensive e2e and unit tests for skill commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🔨 chore: fix linter formatting in memory e2e test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix: add vitest-environment node declaration to aiProvider test

Fix server-side env variable access error by declaring node environment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix cli review

* fix test

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 11:06:52 +08:00
YuTengjing 37cb4983de 🐛 fix: filter out delisted lobehub provider models from DB residuals (#12896) 2026-03-11 10:22:51 +08:00
Innei 9098d0074a ♻️ refactor(desktop): move onboarding state to main process (#12890)
* refactor: desktop onboarding

* ♻️ refactor(desktop): reinstate onboarding guard before auto OIDC

- Add getDesktopOnboardingCompleted/setDesktopOnboardingCompleted back to localStorage
- These functions persist across sign-out, preventing unexpected OIDC popups
- Fix for Codex review feedback on PR #12890

* ♻️ refactor(desktop): use sessionStorage for onboarding completed flag

*  test(desktop): fix BrowserManager test for async initializeBrowsers
2026-03-11 00:36:05 +08:00
Arvin Xu 860e11ab3a ♻️ refactor(cli): extract shared @lobechat/local-file-shell package (#12865)
* ♻️ refactor(cli): extract shared @lobechat/local-file-shell package

Extract common file and shell operations from Desktop and CLI into a
shared package to eliminate ~1500 lines of duplicated code. CLI now
uses @lobechat/file-loaders for rich format support (PDF, DOCX, etc.).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update

* update commands

* update version

* update deps

* refactor version issue

*  feat(local-file-shell): add cwd support, move/rename ops, improve logging

- Add missing `cwd` parameter to `runCommand` (align with Desktop)
- Add `moveLocalFiles` with batch support and detailed error handling
- Add `renameLocalFile` with path validation and traversal prevention
- Add error logging in shell runner's error/completion handlers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* support update model and provider in cli

* fix desktop build

* fix

* 🐛 fix: pin fast-xml-parser to 5.4.2 in bun overrides

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 00:04:22 +08:00
YuTengjing c2e9b45d4c feat: add InsufficientBudget error type and Pro badge i18n (#12886) 2026-03-10 23:43:24 +08:00
YuTengjing 8063378a1d 🐛 fix: resolve ModelSelect crash and update default model (#12892) 2026-03-10 21:10:11 +08:00
Innei 93aed84399 🔨 chore(i18n): sync locale files across desktop and web (#12887)
Made-with: Cursor
2026-03-10 19:23:47 +08:00
LiJian eec8e113fc ♻️ refactor: add the skills in community pages (#12761)
* feat: add the skills in community pages

* feat: add some skills & import the import routes

* feat: add detail used pages & prompt

* feat: add the skill sort way

* fix: ts fixed

* fix: ts fixed

* fix: test fixed

* fix: test fixed
2026-03-10 18:00:15 +08:00
Sun13138 826a099f8d 🐛 fix: harden market auth popup handoff and storage fallback (#12863)
* 🐛 fix: make market auth popup handoff COOP-safe

* 🐛 fix: harden market auth popup handoff flow

* 🐛 fix: guard market auth handoff storage access
2026-03-10 17:19:22 +08:00
Innei c087134953 feat(desktop): unify canary with stable app name/icon, add channel tag in About (#12881)
- Use same app name (LobeHub) and icon as stable for canary builds
- Add build channel tag in Settings > About for non-stable channels (Canary, Nightly, Beta)
- Add getBuildChannel IPC to expose build-time channel for display

Made-with: Cursor
2026-03-10 16:41:56 +08:00
Innei 5e468cd850 feat(agent-browser): add browser automation skill and tool detection (#12858)
*  feat(tool-detectors): add browser automation support and refactor tool detector categories

- Introduced browser automation detectors to the tool detector manager.
- Updated tool categories to include 'browser-automation'.
- Refactored imports to use type imports where applicable for better clarity.
- Cleaned up unnecessary comments in tool filters.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: add browser automation tool detection UI

* 🔧 chore: update react-scan version and enhance agent-browser documentation

- Updated `react-scan` dependency from version 0.4.3 to 0.5.3 in package.json.
- Improved documentation in `content.ts` for the agent-browser, clarifying command usage and workflows.
- Added development mode flag `__DEV__` in sharedRendererConfig for better environment handling.
- Integrated `scan` functionality in `initialize.ts` to enable scanning in development mode.
- Updated global type definitions to include `__DEV__` constant for clarity.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore(builtin-skills): add dependency and refactor skill filtering logic

- Added `@lobechat/const` as a dependency in package.json.
- Introduced a new function `shouldEnableBuiltinSkill` to determine if a skill should be enabled based on the environment.
- Refactored the `builtinSkills` export to filter skills using the new logic.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore(builtin-skills): refactor skill management and add filtering logic

- Removed unnecessary dependency from package.json.
- Simplified skill filtering logic by introducing `filterBuiltinSkills` and `shouldEnableBuiltinSkill` functions.
- Updated various components to utilize the new filtering logic for managing builtin skills based on the environment.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(builtin-skills): introduce new skill APIs and refactor manifest structure

- Added new APIs for skill management: `runSkillApi`, `readReferenceApi`, and `exportFileApi` to enhance functionality.
- Created a base manifest file (`manifest.base.ts`) to centralize API definitions.
- Updated the desktop manifest (`manifest.desktop.ts`) to utilize the new base APIs.
- Refactored existing manifest to streamline API integration and improve maintainability.
- Introduced a detailed system prompt for better user guidance on skill usage.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: desktop skill runtime, skill store inspectors, and tool UI updates

Made-with: Cursor

*  feat: enhance skill import functionality and testing

- Updated `importFromUrl` method in `SkillImporter` to accept additional options for identifier and source.
- Modified `importFromMarket` in `agentSkillsRouter` to utilize the new options for better tracking of skill imports.
- Added integration tests to ensure stable behavior when re-importing skills from the market, verifying that identifiers remain consistent across imports.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update .gitignore and package.json dependencies

- Added 'bin' to .gitignore to exclude binary files from version control.
- Included 'fflate' as a new dependency in package.json to support file compression in the application.
- Updated writeFile method in LocalFileCtr to handle file content as Uint8Array for improved type safety.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update package.json dependencies

- Removed 'fflate' from dependencies and added it to devDependencies for better organization.
- Ensured proper formatting by adding a newline at the end of the file.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: add agent-browser download script and integrate binary handling

- Introduced a new script to download the `agent-browser` binary, ensuring it is available for the application.
- Updated `electron-builder.mjs` to include the binary in the build process.
- Modified `dir.ts` to define the binary directory path based on the packaging state.
- Enhanced the `App` class to set environment variables for the agent-browser integration.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: add DevTools toggle to Linux and Windows menus

- Introduced a new menu item for toggling DevTools with the F12 accelerator key in both Linux and Windows menu implementations.
- Added a separator for better organization of the view submenu items.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: integrate agent-browser binary download into build process

- Added functionality to download the `agent-browser` binary during the build process in `electron-builder.mjs`.
- Enhanced the download script with detailed logging for better visibility of the download status and errors.
- Updated the `App` class to log the binary directory path for improved debugging.
- Reintroduced the `AuthRequiredModal` in the layout for desktop users.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: mock binary directory path in tests

- Added a mock for the binary directory path in the App tests to facilitate testing of the agent-browser integration.
- This change enhances the test environment by providing a consistent path for the binary during test execution.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: improve authorization notification handling

- Updated the `notifyAuthorizationRequired` method to implement trailing-edge debounce, ensuring that rapid 401 responses are coalesced and the IPC event is sent after the burst settles.
- Refactored the notification logic to enhance clarity and maintainability.

 feat: add desktop onboarding redirect

- Introduced a `useEffect` hook in `StoreInitialization` to redirect users to the `/desktop-onboarding` page if onboarding is not completed, ensuring a smoother user experience on fresh installs.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(desktop): hide Agent Browser skill on Windows

Made-with: Cursor

* 🔧 chore: update memory limits for build processes

- Increased the `NODE_OPTIONS` memory limit for both `build:next` and `build:spa` scripts from 6144 to 7168, optimizing build performance and resource management.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-10 16:13:33 +08:00
Arvin Xu eb7cf10ff9 test: fix GatewayManager tests to include platform parameter (#12876)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 14:39:00 +08:00
lobehubbot 7d88b8cda5 Merge remote-tracking branch 'origin/main' into canary 2026-03-10 06:35:39 +00:00
YuTengjing 258e9cb982 👷 build: add migration to enable pg_search extension (#12874)
*  feat: add migration to enable pg_search extension

* 🐛 fix: skip pg_search migration for PGlite test compatibility
2026-03-10 14:34:42 +08:00
sxjeru a7d896843f 💄 style: Add new GPT-5.4 model (#12654)
*  feat(openai): add GPT-5.3 Chat model with enhanced features and pricing details

*  feat: add Codex Max Reasoning Effort parameter and slider component for enhanced model configuration

*  feat: update Qwen model configurations and add new Qwen3.5 models with detailed descriptions and pricing

*  feat: add GPT-5.4 and GPT-5.4 pro models with pricing and capabilities to the model bank

*  feat: add GPT-5.4, GPT-5.4 pro, and GPT-5.3 Chat models with detailed capabilities and pricing to the model bank

*  feat: 更新 zhipu 聊天模型的定价参数,移除不必要的 textOutput 参数

*  feat: 移除 Gemini 3 Pro 模型的详细信息,标记为已弃用
2026-03-10 09:59:14 +08:00
Hardy 7de2a68d20 feat(siliconcloud): add Qwen3.5 series models (#12785) 2026-03-10 09:58:37 +08:00
LobeHub Bot e753856abf test: add unit tests for gateway service (#12784)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 09:58:13 +08:00
René Wang b94503db8b 📝 docs: upgrade usage docs with improved structure and content (#12704)
Adopt Mintlify-quality writing patterns across 11 existing docs and add 3 new docs.
Adds Steps, Tabs, AccordionGroup, and mermaid diagrams for better readability.

Priority 1 (major expansion): agent-market, resource, scheduled-task, mcp-market
Priority 2 (structural): memory, web-search, tts-stt, vision, chain-of-thought
Priority 3 (minor): artifacts, agent
New docs: chat, file-upload, skills-and-tools

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 09:56:39 +08:00
Marcell Gu 023e3ef11a 📝 docs: Simplify docker compose network architecture & Remove broken links from docker compose docs (#12749)
* ♻️ refactor(docker): simplify network architecture and add admin port
- Remove unnecessary network-service (alpine) container
- Use dedicated lobe-network bridge for all services
- Add RUSTFS_ADMIN_PORT environment variable for admin console
- Update container-to-container communication to use Docker service names
- Use relative path volumes for better data persistence

* 📝 docs: update Docker Compose deployment guide
- Add single-domain deployment documentation
- Update INTERNAL_APP_URL guidance
- Clarify Port Mode vs Domain Mode behavior
- Add S3_ENDPOINT configuration tips
- Remove broken link to non-existent server-database documentation

* fix(docker): keep backward-compatible volume paths for existing deployments

- PostgreSQL: Keep ./data (not ./postgres_data)
- Redis: Keep redis_data named volume (not ./redis_data)
- RustFS: Keep rustfs-data named volume (not ./rustfs_data)

This ensures existing users can upgrade without data migration.

* fix(docker): correct Port Mode vs Domain Mode description

- Fix reversed explanation in comments
- Port Mode: Uses default ports (3210/9000/9001)
- Domain Mode: Custom ports via reverse proxy

This aligns with the actual deployment script behavior.
2026-03-10 09:55:56 +08:00
Rylan Cai ea329113be feat(eval): add external scoring mode (#12729)
* wip: add llm relevant & BrowseComp

* wip: add widesearch desc

* wip: dsqa, hle, widesearch

* wip: add dsqa

* wip: add awaiting eval status for runs

* wip: add awaiting status for run

* wip: adjust hle-verified

* 🐛 fix: browsecomp topics

* 📝 docs: add annotations

* wip: add awaiting status for pass@k

* wip: add complete status

* wip: update theard dots

* wip: update run status page

* wip: remove useless impl

* wip: update prompt

*  feat: add external eval routes

* wip: add eval cli

* 🐛 fix: support authoritize in no browser environment

* wip: pass tests

* ♻️ refactor: remove tests

* ♻️ refactor: mo camel case
2026-03-10 09:53:26 +08:00
Innei 255a1c21a8 🐛 fix: redirect to desktop onboarding when not completed (#12866)
* 🐛 fix: redirect to desktop onboarding when not completed

Desktop app was missing the redirect to `/desktop-onboarding` when
onboarding hadn't been completed. The `useDesktopUserStateRedirect`
callback silently returned instead of navigating, causing:
- Users never see the onboarding flow on fresh install
- `AuthRequiredModal` suppressed because onboarding guard fails

* 🐛 fix: remove desktop onboarding routes from proxy configuration

The `/desktop-onboarding` and its regex route have been removed from the proxy configuration. This change simplifies the routing logic as the onboarding flow is now handled directly in the user state redirect logic.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-10 02:15:27 +08:00
Zephyr 81d25bf124 feat: add v1 api (#12758)
*  feat(openapi): add API key hash support for secure storage

*  feat(openapi): enhance message translation and knowledge base functionality

- Added MessageTranslationController and associated routes for managing message translations, including fetching, creating, updating, and deleting translations.
- Introduced KnowledgeBaseController with routes for CRUD operations on knowledge bases, including file management and access control.
- Updated existing message and translation routes to improve structure and naming consistency.
- Refactored related services and types to support new features and ensure type safety.

This update enhances the API's capabilities for handling message translations and knowledge base management, improving overall functionality and user experience.

* fix: allow OWNER scope to list agents in agents route

- Add OWNER scope to AGENT_READ permission check
- Aligns list behavior with AgentService.queryAgents ownership filter
- Allows owner-scoped users to list their own agents

* 🔧 refactor(rbac): improve import structure in rbac.ts

- Changed import statements to separate type imports from regular imports for better clarity and organization.
- This refactor enhances code readability and maintains consistency in the import structure.

* fix: 修复 chunk 服务与 async router 的循环依赖

- 将 createAsyncCaller 的静态导入改为动态导入 (await import)
- 打破 file.ts -> chunk/index.ts -> async/index.ts 的循环依赖链
- 使用 --skip-dynamic-imports 参数的 dpdm 验证循环依赖已解决

* 🐛 fix: resolve CI failures

* test: 补充 apiKey、KeyVaultsEncrypt、ChunkService 单测至 100% 覆盖率

- test(database): 补充 apiKey.ts query() 解密失败分支测试
- test(server): 补充 KeyVaultsEncrypt 非法密钥/密文格式 getUserKeyVaults 测试
- test(server): 新增 ChunkService 完整测试覆盖异步任务创建/触发/失败回写

所有新增测试通过 (46/46),目标文件覆盖率均达 100%
2026-03-10 01:00:36 +08:00
Rylan Cai 3894facf5f 🐛 fix(cli): require gateway for custom server (#12856)
* 🐛 fix(cli): require --gateway for custom server logins

* 🐛 fix(cli): persist custom server gateway settings

* ♻️ refactor(cli): centralize official endpoint urls
2026-03-10 00:02:51 +08:00
WangYK 473bc4e005 💄 style: support video input for SiliconCloud provider (#9988)
*  feat: support video input for SiliconCloud models

* 🐛 fix: resolve SSRF issue in video fetching; move message transformation to `context-builders`

* 🐛 fix: update MiniMax M2 context size

* 🐛 fix: use ssrf-safe-fetch in `videoUrlToBase64` and `imageUrlToBase64`

* 🐛 fix: fix tests

* 🐛 fix: dynamically import ssrf-safe-fetch to prevent build failures

* Revert "🐛 fix: dynamically import ssrf-safe-fetch to prevent build failures"

This reverts commit 5de0829527ae6dbdc78d694ccc9dca86f46e3168.

* chore: move `videoToBase64` to the `util` package

* fix: fix tests

* chore: update siliconcloud models

* fix: deduplicate siliconcloud models

* fix: videoUrlToBase64 should determine runtime when fetching

* fix: fix tests

* chore: update siliconcloud models

* chore: remove deprecated models

* chore: update model info

* fix: fix tests
2026-03-10 00:02:29 +08:00
lobehubbot 3cf4f28af0 🔖 chore(release): release version v2.1.39 [skip ci] 2026-03-09 15:07:18 +00:00
lobehubbot d54b30750a Merge remote-tracking branch 'origin/main' into canary 2026-03-09 15:05:28 +00:00
Arvin Xu 4e6790e3d7 👷 build: add api key hash column migration (#12862)
*  feat(database): extract openapi database changes

* 📝 docs: update db-migrations and version-release skills

---------

Co-authored-by: MarioJames <mocha.wyh@msn.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
2026-03-09 23:04:45 +08:00
LobeHub Bot 8a679aa772 🌐 chore: translate non-English comments to English in src/app/(backend) (#12836)
🌐 chore: translate non-English comments to English in src/app/(backend) and src/app/[variants]/(auth)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 20:03:13 +08:00
Arvin Xu 1329490306 feat(cli): add agent run and status commands (#12839)
*  feat(cli): add agent run and status commands

Implement `lh agent run` for executing agents with SSE streaming
and `lh agent status` for checking operation status. Includes
`--replay` option for offline replay from saved JSON fixtures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix(cli): preserve SSE frame state across read boundaries and enable verbose logging

- Move eventType/eventData outside the read loop so partial SSE frames
  split across chunks are not silently dropped
- Call setVerbose(true) when --verbose is passed so logger helpers
  actually print detailed tool arguments and results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:02:41 +08:00
YuTengjing 228044e649 🐛 fix: add ffmpeg-static to default serverExternalPackages (#12846) 2026-03-09 18:13:17 +08:00
YuTengjing 857f469323 🐛 fix: remove ffmpeg-static from outputFileTracingExcludes (#12844) 2026-03-09 17:10:57 +08:00
Zhijie He 8d4d657a5d feat: add LongCat(美团) provider support (#12603)
* feat: add LongCat(美团) provider support

* chore: remove enable_thinking, due to not in doc anymore
2026-03-09 16:59:29 +08:00
Innei 50dbc653fa 🐛 fix: filter v-prefixed Docker tags in manifest creation (#12842) 2026-03-09 16:07:06 +08:00
YuTengjing 5af5b80b83 🐛 fix: include pnpm store path for ffmpeg-static in Vercel tracing (#12838) 2026-03-09 14:37:59 +08:00
Arvin Xu c6de80931e 🐛 fix: fix agent runtime error handle (#12834)
* improve inspect partial ability

* fix error

* fix runtime error
2026-03-09 12:24:13 +08:00
YuTengjing 6e26135978 🐛 fix: harden Anthropic message building and sampling parameter handling (#12827) 2026-03-09 11:05:02 +08:00
LobeHub Bot 10dfc6eec6 test: add unit tests for InMemoryAgentStateManager (#12377)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 10:40:34 +08:00
Hardy 8855ac3b8a feat: add new NVIDIA models and tweak the behavior of the enable thinking (#12533)
*  feat: add new NVIDIA models with thinking budget support

- Add 7 new models: MiniMax-M2.1, DeepSeek V3.2, GLM-4.7, GLM-5, Kimi K2.5, MiniMax-M2.5, Qwen3.5-397B-A17B
- Add thinkingBudget support for qwen3.5-397b-a17b model
- Update test case description

* 🐛 fix: remove thinking budget and add video support for Qwen3.5-397B-A17B
2026-03-09 10:34:00 +08:00
Zhijie He e4f8ed78ba 💄 style: add grok-4.20 series early support (#12743)
* style: add grok-4.20 series early support

* chore: disable browser request due to CORS

* style: update ability tag
2026-03-09 10:23:16 +08:00
Arvin Xu 4363994945 feat: support use remote device in IM integration (#12798)
* support timezone in system prompt

refactor to improve user prompts

refactor tool engine

refactor tools map mode

add bot callback service

clean

improve cli

update agentic tracing

refactor cli login

refactor cli

add device auth

improve device gateway implement

implement gateway pipeline

support device Gateway connect

support gateway

* revert electron device

* inject builtins agent prompts

* update tracing

* add testing

* refactor the activeDeviceId

* refactor BotCallbackService

* fix test and lint

* fix test and lint

* add tests

* fix tests

* fix lint
2026-03-09 01:17:56 +08:00
LobeHub Bot c1757e2e19 🌐 chore: translate non-English comments to English in GenerationItem (#12745)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 00:52:07 +08:00
LobeHub Bot 39e36320b2 🌐 chore: translate non-English comments to English in AgentSetting (#12807)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 23:59:44 +08:00
Arvin Xu ccd7f4e22b 🐛 fix(cli): fix type errors in generate image/video commands (#12828)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 23:36:08 +08:00
Rdmclin2 3f9c23e7b4 feat: support lark and feishu bot (#12712)
* feat: support lark and feishu

* chore: change integration to channel

* chore: rename from integration to channel

* fix: channel router

* feat: add topic list channel provider icon

* chore: update webhook url

* chore:  channel form refact

* chore: update i18n  keys to channel

* chore: update form item description

* style: hide required mark

* feat: add lark chat adapter

* chore: clean speaker tag  & add username api adapter

* chore: adjust topic channel icon

* chore: move developer mode to advanced setting

* chore: add lark icon

* fix: detail style

* fix: token check logic

* fix: encrpted risk

* fix: vercel function appId

* chore: remove webhook mode for discord

* chore: add doc link

* chore: add channel docs

* chore: remove unused import

* fix: create bot with wrong platform

* chore: update intergration to channel

* fix: udpate variable import

* fix: tsgo error

* chore: optimize webhook url trim

* chore: update copy text

* fix: telegram webhook not set

* chore: add persist logic

* docs: update feishu doc

* chore: update feishu and lark tenant

* chore: update docs

* chore: make verfication code required

* chore: update feishu docs

* chore: update verfication comment

* chore: update docs permission  list

* chore: verificationToken optional

* chore: update feishu and lark color

* chore: use test id
2026-03-08 19:18:06 +08:00
YuTengjing 15a95156f3 💄 style: update i18n locales (#12809) 2026-03-08 13:25:46 +08:00
YuTengjing f25edcc027 🔒 fix: add rate limit custom rules for password reset and email verification (#12808) 2026-03-08 12:40:14 +08:00
Arvin Xu e67bcb2571 feat(cli): add generate command for text/image/video/tts/asr (#12799)
*  feat(cli): add generate command for text/image/video/tts/asr

LOBE-5711

- `lh generate text <prompt>` — LLM text completion with SSE streaming
  - Supports --model (provider/model format), --system, --temperature, --pipe
- `lh generate image <prompt>` — Image generation via async task
- `lh generate video <prompt>` — Video generation via async task
- `lh generate tts <text>` — Text-to-speech (openai/microsoft/edge backends)
- `lh generate asr <file>` — Speech-to-text via OpenAI Whisper
- `lh generate status` — Check async generation task status
- `lh generate list` — List generation topics
- Add shared HTTP auth helper (api/http.ts) for webapi endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update info

* ♻️ refactor(cli): split generate command into submodules, text defaults non-streaming

- Split monolithic generate.ts into generate/{index,text,image,video,tts,asr}.ts
- Text subcommand now defaults to non-streaming (use --stream to opt in)
- Text subcommand supports --json for full JSON response output
- Video subcommand uses requiredOption for --model and --provider

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix(cli): read generation data from result.data and add required X-lobe-chat-auth header

Image/video mutations return { success, data: { ... } }, read IDs from data.
WebAPI endpoints require X-lobe-chat-auth (XOR-encrypted) alongside Oidc-Auth.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 11:19:01 +08:00
Zhijie He 2cce103137 💄 style: add qwen-image-2.0 series support (#12771) 2026-03-08 10:33:48 +08:00
Arvin Xu 6acba612fc feat(cli): add full API integration commands In cli (#12795)
*  feat(cli): add full API integration commands

Add comprehensive CLI commands for managing LobeHub resources:

P0 - Search, Knowledge Base, Memory:
- `lh search` - Global unified search across all resource types
- `lh kb` - Knowledge base CRUD, file management
- `lh memory` - User memory CRUD (identity/activity/context/experience/preference), persona, extraction

P1 - Agent, Session, Topic, Message:
- `lh agent` - Agent CRUD (list/view/create/edit/delete/duplicate)
- `lh session` - Session management with search
- `lh topic` - Topic CRUD with search and recent
- `lh message` - Message listing, search, delete, count, heatmap

P2 - Model, Provider:
- `lh model` - Model listing, toggle, delete per provider
- `lh provider` - Provider listing, toggle, delete

P3 - Plugin, Config:
- `lh plugin` - Plugin install/uninstall/update
- `lh whoami` - User info display
- `lh usage` - Usage statistics (monthly/daily)

Also refactors shared formatting utilities into utils/format.ts.
All commands support `--json` output for scripting.

Closes LOBE-5706, LOBE-5770

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

*  feat(cli): add file/skill commands, remove session, split kb

- Add standalone `file` command (list, view, delete, recent)
- Add `skill` command (list, view, create, edit, delete, search, import, resources)
- Remove `session` command (no longer needed)
- Remove `files` subcommand from `kb` (now separate `file` command)
- Add tests for file and skill commands
- Register new commands in index.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 🐛 fix(cli): fix ESM require in confirm, login unhandled rejections, memory create

- Replace CommonJS require('node:readline') with ESM import in confirm helper
- Add return after process.exit(1) in login.ts to prevent unhandled rejections
- Simplify memory create to only support identity (other categories lack create procedures)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:18:01 +08:00
Rylan Cai e48fd47d4e 🐛 fix: cli login and run browser in Windows (#12787)
* 🐛 fix: support authoritize in no browser environment

* wip: remove tests

* 📝 docs: remove redundant alerts

* 🐛 fix: could not invoke brower in windows

* wip: add link and unlink cli to global
2026-03-07 23:33:05 +08:00
YuTengjing b91fa68b31 🐛 fix: detect exceeded context window errors from message text (#12788) 2026-03-07 23:26:57 +08:00
LobeHub Bot ac1376ede5 🌐 chore: translate non-English comments to English in ProtocolUrlHandler (#12781) 2026-03-07 17:45:18 +08:00
YuTengjing 32b83b8c0a feat(topic): add sort by updated time option for topic sidebar (#12774) 2026-03-07 17:16:50 +08:00
Arvin Xu 2822b984f4 feat: add doc command in cli (#12752)
* add doc cli

* add doc cli

* add document command
2026-03-07 13:48:02 +08:00
lobehubbot 169d5afa93 🔖 chore(release): release version v2.1.38 [skip ci] 2026-03-06 13:43:22 +00:00
lobehubbot 42ed155944 Merge remote-tracking branch 'origin/main' into canary 2026-03-06 13:41:48 +00:00
Innei 2dc7b15c31 🚀 release: 20260306 (#12757)
This release includes **31 commits**. Key updates are below.

### New Features and Enhancements

- Added **Telegram bot access** support.
- Added **electron page tabs** functionality for desktop.
- Added **device code auth flow** for authentication.
- Added **GPT-5.4** model support.
- Show **last used auth provider** on sign-in page for better UX.
- Support **clearing hotkey bindings** in desktop ShortcutManager.
- Added **Gemini 3.1 Flash Lite Preview** model and thinkingLevel5
extend param.
- Added **auto aspect ratio and image search** support for Nano Banana
2.
- User memories now default to inject user persona instead of
identities.

### Desktop Improvements

- Unified **update channel switching** with S3 distribution.
- Added **S3 publish for canary/nightly** and S3 cleanup (keep latest
15).
- Added electron page tabs functionality.

### Stability and Fixes

- Fixed agents fork not working in community deploy.
- Fixed animation for single-line messages between reasoning and tool
calls.
- Fixed Discord bot conflict with keyPrefix.
- Fixed skew plugin issue.
- Fixed `userMemories` database failure on extra structure mismatch.
- Fixed old LobeHub plugins update issue.
- Fixed context-engine tool type recovery from manifest when models
strip suffixes.
- Added `await` to `handleResponseAPIMode` for proper error handling.
- Fixed M2M token for community agents/MCP/skill list.
- Fixed scripts to support Win32.
- Improved gateway and device gateway CI.

### Credits

Huge thanks to these contributors (alphabetical):

@arvinxx @huangkairan @Innei @LiJian @Luis-Sambrano @nekomeowww
@rdmclin2 @ReneWang @sxjeru @tjx666
2026-03-06 21:41:07 +08:00
Innei 5391ceda7d 🐛 fix(ci): add version prefix to S3 update manifest URLs (#12772)
🐛 fix(ci): target channel yml files instead of latest*.yml for version prefix

The merge-mac-files step already renames latest*.yml to {channel}*.yml
(e.g., canary-mac.yml). The previous fix targeted release/latest*.yml
which matched nothing, so the sed was a no-op.

Now targets release/${CHANNEL}*.yml directly, with latest*.yml as fallback.
2026-03-06 19:34:32 +08:00
Innei a2bf627531 🐛 fix(ci): add version prefix to latest*.yml URLs in S3 upload (#12770)
The latest*.yml files uploaded to S3 channel root lacked the $VERSION/
prefix in their URLs, causing electron-updater to request files at
the wrong path (e.g., /canary/LobeHub-Canary-xxx.zip instead of
/canary/2.1.38-canary.1/LobeHub-Canary-xxx.zip), resulting in 404.

Now sed -i modifies latest*.yml in-place before uploading, and
channel-specific yml files are copied from the already-modified ones.
2026-03-06 18:41:26 +08:00
Innei 0b7c917745 👷 build(ci): fix changelog auto-generation in release workflow (#12765)
After auto-tag-release.yml was introduced, semantic-release in release.yml
stopped working because the tag already exists when it runs. This caused
CHANGELOG.md to never be updated.

Fix: move changelog generation into auto-tag-release.yml with a custom
script that parses git log and generates gitmoji-formatted entries,
matching the existing CHANGELOG.md format. Remove the broken
semantic-release step from release.yml.
2026-03-06 17:25:44 +08:00
YuTengjing 716c27df12 🐛 fix: resolve message reordering in Responses API input conversion (#12764) 2026-03-06 17:14:26 +08:00
Innei 0dd0d11731 👷 build(ci): fix changelog auto-generation in release workflow (#12763)
After auto-tag-release.yml was introduced, semantic-release in release.yml
stopped working because the tag already exists when it runs. This caused
CHANGELOG.md to never be updated.

Fix: move changelog generation into auto-tag-release.yml with a custom
script that parses git log and generates gitmoji-formatted entries,
matching the existing CHANGELOG.md format. Remove the broken
semantic-release step from release.yml.
2026-03-06 17:08:47 +08:00
LiJian 400a0205a3 🐛 fix: when use trustclient not register market m2m token (#12762)
fix: when use trust client not take inject token
2026-03-06 17:03:34 +08:00
lobehubbot 86889b81bd 🔖 chore(release): release version v2.1.37 [skip ci] 2026-03-06 06:25:38 +00:00
Innei d3550afe05 🐛 hotfix(ci): correct stable renderer tar source path (#12755)
🐛 fix(ci): correct stable renderer tar source path

Use the current Electron renderer output directory when creating the stable renderer archive so Linux desktop release builds stop failing after packaging succeeds.

Made-with: Cursor
2026-03-06 14:24:06 +08:00
LiJian 4d240cf7fa 🐛 fix: slove the agnets fork not work in communtiy deploy (#12750)
* fix: slove the agnets fork not work in communtiy deploy

* fix: slove the secure token set & registerM2MToken not batch

* Revert "fix: slove the secure token set & registerM2MToken not batch"

This reverts commit 4485e57165.
2026-03-06 14:12:48 +08:00
YuTengjing db45907ab8 feat: add GPT-5.4 model support (#12744)
*  feat: add GPT-5.4 model support and fix reasoning payload pruning

- Add GPT-5.4 model card to model-bank
- Update planCardModels to use gpt-5.4
- Add gpt-5.4 to responsesAPIModels
- Fix pruneReasoningPayload to strip logprobs/top_logprobs for reasoning models
- Add logprobs, top_logprobs to ChatStreamPayload type
- Extend reasoning_effort to include none and xhigh
- Add success log for non-fallback requests in RouterRuntime
- Fix log parameter mismatch in RouterRuntime

Fixes LOBE-5735

* 🐛 fix: match gpt-5.4 to gpt5_2ReasoningEffort in openrouter and vercelaigateway

* 🐛 fix: update OpenRouterReasoning effort type to include none and xhigh

* 🐛 fix: use tiered pricing for gpt-5.4 based on 272K token threshold

* 🌐 chore: update i18n translations

* 🐛 fix: update claude-sonnet model version to 4-6 in planCardModels

*  feat: add GPT-5.4 Pro model support

* 🐛 fix: remove dated snapshot for gpt-5.4-pro in responsesAPIModels

* 🐛 fix: add tierBy support for cross-unit tiered pricing threshold

OpenAI charges output at 1.5x when INPUT exceeds 272K tokens.
The tiered strategy previously only checked the unit's own quantity
to select a tier. Added optional tierBy field to TieredPricingUnit
so output/cacheRead tiers can reference input quantity for selection.

* 🐛 fix: use totalInputTokens for tiered pricing tier selection

Tiered pricing tiers should be determined by total prompt size
(totalInputTokens), not each unit's own quantity. This fixes output
and cacheRead being charged at the wrong tier rate when the prompt
exceeds the threshold but the individual unit quantity does not.
2026-03-06 13:47:31 +08:00
Arvin Xu 76a07d811b feat: init lobehub-cli (#12735)
* init cli project

* Potential fix for code scanning alert no. 184: Uncontrolled command line

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* update

* Potential fix for code scanning alert no. 185: Uncontrolled command line

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-06 11:42:29 +08:00
LobeHub Bot 616d53e2ec 🌐 chore: translate non-English comments to English in ChatInput/ActionBar/Tools (#12663)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 11:27:27 +08:00
YuTengjing 92cb759c37 feat(auth): show last used auth provider on sign-in page (#12737) 2026-03-06 03:29:31 +08:00
Neko 07f44e2ba2 feat(userMemories,memory-user-memory): now default to inject user persona instead of identities (#12731) 2026-03-05 21:37:27 +08:00
Innei 5920500371 feat(desktop): support clearing hotkey bindings in ShortcutManager (#12727)
* 🔧 chore: update @lobehub/ui dependency to a specific version URL and enhance ShortcutManager functionality

- Updated @lobehub/ui dependency in package.json to a specific version URL.
- Improved ShortcutManager to handle empty accelerator bindings, allowing users to clear shortcuts.
- Updated tests to reflect changes in shortcut handling and added localization for clear binding messages in both Chinese and English.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui dependency to version 5.4.0 in package.json

- Changed the @lobehub/ui dependency from a specific version URL to version 5.4.0 for improved stability and consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui dependency to use caret versioning in package.json

- Changed the @lobehub/ui dependency from a fixed version to caret versioning (^5.4.0) to allow for minor updates and improvements.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-05 21:23:58 +08:00
Innei 92c70d2485 👷 ci(desktop): add S3 cleanup for canary/nightly (keep latest 15) (#12722)
* 👷 ci(desktop): add S3 cleanup for canary/nightly (keep latest 15)

- Create `.github/actions/desktop-cleanup-s3/` reusable composite action
- Add S3 version cleanup step to canary and nightly cleanup jobs
- Cleanup runs after both publish-release and publish-s3 complete

* 👷 ci(desktop): fix S3 yml upload and add debug output

- Restore latest*.yml → {channel}*.yml logic (electron-builder always generates latest-*.yml)
- Upload both {channel}*.yml and latest*.yml to S3
- Change upload glob from latest* to *.yml for robustness
- Add yml file listing debug output in both upload and publish steps
2026-03-05 21:06:34 +08:00
lobehubbot 6c1c60ee27 🔖 chore(release): release version v2.1.36 [skip ci] 2026-03-05 12:45:01 +00:00
lobehubbot a4a3e024a6 Merge remote-tracking branch 'origin/main' into canary 2026-03-05 12:44:49 +00:00
LiJian e13e0a4db6 🐛 hotfix: add the market m2m request into market api (#12714)
🐛 fix: should add m2m token to use community agents/mcp/skill list (#12708)

fix: should add m2m token to use community agents/mcp/skill list
2026-03-05 20:44:10 +08:00
Innei eb009866cc 🐛 fix(ci): improve release workflows for prereleases (#12634)
- Use GH_TOKEN for desktop canary release upload
- Fix Docker tagging: latest only for stable, tag prerelease versions
- Skip release job when ref contains '-' (prerelease tags)

Made-with: Cursor
2026-03-05 17:32:13 +08:00
Innei 2ebac4679c 👷 ci(desktop): add S3 publish for canary/nightly and extract reusable action (#12721)
👷 ci(desktop): extract S3 publish logic into reusable composite action

- Create `.github/actions/desktop-publish-s3/` composite action for S3 upload
- Add `publish-s3` job to canary and nightly workflows (previously missing)
- Refactor stable workflow to use the shared action
- Fix canary/nightly builds not uploading to S3 despite UPDATE_SERVER_URL being set
2026-03-05 17:23:44 +08:00
Innei 51c857e4a5 feat: add electron page tabs functionality (#12310)
*  feat: add electron page tabs functionality

Implement browser-style page tabs in the Electron titlebar:

- Add TabBar component with explicit tab creation via context menu (desktop only)
- Tab creation triggers: TopicItem/PageItem context menu "Open in New Tab" or double-click
- TabBar only visible when tab count >= 2
- Update active tab's reference when navigating within it (tab follows user navigation)
- Reuse existing plugin system (pluginRegistry, 11 page plugins, PageReference types)
- Persist tabs to localStorage with automatic recovery on restart
- Apply logic to TopicItem (agent & group) and PageItem

Changes:
- src/store/electron/actions/tabPages.ts: New store slice with tab state + actions
- src/features/Electron/titlebar/TabBar/: New UI component + storage + hooks
- src/features/Electron/navigation/: New useTabNavigation hook + extracted cachedData helper
- src/app/.../Topic/List/Item/: Double-click creates tab, context menu "Open in New Tab"
- src/app/.../page/.../Item/: Double-click creates tab, context menu "Open in New Tab"
- i18n: New keys in topic.ts and file.ts namespaces

*  feat: enhance agent topic plugin and tab resolution logic

- Added a new line to ensure the cached title is included when resolving tabs in the useResolvedTabs hook.
- Minor adjustment in the agentTopicPlugin to improve code clarity.

These changes improve the handling of cached titles in the tab resolution process and enhance the overall functionality of the agent topic plugin.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance agent and conversation handling in PageEditor

- Refactored PageAgentProvider to eliminate direct pageAgentId prop, improving context management.
- Updated Conversation and Copilot components to utilize conversation state for agent selection, ensuring better handling of chat-group session IDs.
- Adjusted FileCopilot to synchronize active agent ID with conversation context, enhancing file interaction capabilities.

These changes streamline agent management and improve the overall user experience in the PageEditor feature.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update ESLint suppressions for chat service [skip ci]

- Removed the suppression for `object-shorthand` in `src/services/chat/index.ts` to improve code quality.
- Adjusted the ESLint suppressions in `eslint-suppressions.json` for better linting consistency.

These changes enhance the linting process by ensuring adherence to coding standards in the chat service files.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: optimize NavigationBar panel width handling

*  feat: add tab context menu with close actions

Add right-click context menu on tab items:
- Close current tab
- Close other tabs
- Close tabs to the left (disabled on first tab)
- Close tabs to the right (disabled on last tab)

* 🐛 fix: defer single-click navigation on desktop to prevent double-click addTab race

*  feat: implement onActivate method for RecentlyViewed plugins to manage store state transitions

- Added onActivate method to RecentlyViewedPlugin interface for handling tab activations.
- Updated agentPlugin and agentTopicPlugin to switch topics based on tab activation.
- Enhanced PluginRegistry to notify plugins on tab activation.
- Modified TabBar to trigger onActivate when a tab is activated.
- Improved AgentIdSync to preserve topic state during agent switches.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update test for BackendProxyProtocolManager to throw on upstream fetch failure

- Changed test description to reflect behavior change from returning a 502 status to throwing an error.
- Updated test implementation to use expect().rejects.toThrow for handling fetch errors.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: use optional chaining for agent configuration properties

- Updated agent configuration properties to use optional chaining for safer access.
- This change prevents potential runtime errors when properties are undefined.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: optimize navigation handling in TabBar with startTransition

- Introduced startTransition for navigation updates to improve performance and user experience.
- Updated handleActivate, handleCloseOthers, handleCloseLeft, and handleCloseRight methods to use startTransition for routing.
- Enhanced code readability by grouping navigation logic within startTransition.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-05 16:50:14 +08:00
Innei 9cb0560ebf feat(desktop): unified update channel switching with S3 distribution (#12644)
*  feat(desktop): add update channel settings for desktop app

* 🔧 chore(desktop): update test scripts for multi-channel update flow

- Support stable/nightly/canary channel structure in generate-manifest.sh
- Add --all-channels flag for generating manifests across all channels
- Dual-mode run-test.sh: packaged (full updater) and --dev (UI only)
- Fix package:mac:local to skip signing for local builds
- Document Squirrel.Mac signature validation limitation

* 🔧 chore(desktop): update local app update configuration

- Change provider from GitHub to Generic for local testing.
- Update local server URL and cache directory settings.
- Revise comments for clarity on usage and configuration.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(desktop): fix update channel switch race condition and downgrade flag

- P1: Use generation counter to discard stale check results when channel
  is switched mid-flight. Pending recheck is scheduled after current check
  completes instead of forcing concurrent checks.
- P2: Explicitly reset allowDowngrade=false on non-downgrade transitions
  to prevent stale downgrade permission from persisting.
- Fix GitHub fallback repo name (lobe-chat -> lobehub).

* 🔧 chore(settings): remove dynamic import for Beta component from componentMap

- Eliminated the dynamic import for the Beta settings tab, streamlining the component map.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore(settings): simplify UpdateChannel component structure

- Refactored the UpdateChannel component to streamline the Select component usage by removing unnecessary nested children.

Signed-off-by: Innei <tukon479@gmail.com>

* update

* 🐛 fix(desktop): strip channel suffix from UPDATE_SERVER_URL before appending channel

The UPDATE_SERVER_URL secret may already contain a channel path (e.g., /stable).
Previously, the code unconditionally appended /{channel}, resulting in double
paths like /stable/stable/stable-mac.yml.

Now both electron-builder.mjs and UpdaterManager strip any trailing channel
suffix before re-appending the correct channel, supporting both legacy URLs
(with channel) and clean base URLs.

* update

* update

* redesign ui

- Added `getUpdaterState` method to `UpdaterManager` for retrieving current update status.
- Introduced `UpdaterState` type to encapsulate update progress, stage, and error messages.
- Updated UI components to reflect update states, including checking, downloading, and latest version notifications.
- Enhanced menu items for macOS and Windows to display appropriate update statuses.
- Localized new update messages in English and Chinese.

This improves user experience by providing real-time feedback during the update process.

Signed-off-by: Innei <tukon479@gmail.com>

* Enhance UpdaterManager tests and mock implementations

- Updated tests for UpdaterManager to reflect changes in broadcasting update states, including 'checking', 'downloading', and 'error' stages.
- Modified mock implementations in macOS and Windows test files to include `getUpdaterState` and `installNow` methods for better state management.
- Improved test coverage for update availability and download processes.

These changes ensure more accurate testing of the update flow and enhance the overall reliability of the UpdaterManager functionality.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-05 15:15:03 +08:00
LiJian 15a50e999a 🐛 fix: should add m2m token to use community agents/mcp/skill list (#12708)
fix: should add m2m token to use community agents/mcp/skill list
2026-03-05 13:44:56 +08:00
Rdmclin2 1be9c000ec feat: support telegram bot access (#12671)
* support keyPrefix

* feat: add telegram platform implementation

* feat: add telegram front end ui

* feat: support webhookProxyUrl

* chore: add more log info

* fix: header align

* chore: add onNewMessage to platform specific

* fix: test connnectitvity and add application id

* feat: support local tunnel

* chore: optimize telegram message format

* fix: webhook secrect

* feat: keep typing interval

---------

Co-authored-by: arvinxx <arvinx@foxmail.com>
2026-03-05 10:52:32 +08:00
Innei 522dcf789c 🐛 fix(conversation): disable animation for single-line messages between reasoning and tool calls (#12675)
When a short message is sandwiched between reasoning and tool call blocks,
component remount during streaming causes the fadeIn animation to replay.
Disable animation for these tool-adjacent single-line messages to prevent
the visual flicker.
2026-03-05 01:10:11 +08:00
Arvin Xu a0c6c9765c 🐛 fix: use keyPrefix to fix discord bot conflict (#12638)
support keyPrefix
2026-03-05 00:01:40 +08:00
Arvin Xu ab376d9185 feat: add device code auth flow (#12697)
* add i18n

* fix types
2026-03-04 23:58:41 +08:00
YuTengjing 08b23a9732 feat(google): add gemini-3.1-flash-lite-preview model and thinkingLevel5 extend param (#12652) 2026-03-04 23:39:49 +08:00
LiJian 1fece1f8d9 🔨 chore: update the market sdk to 0.31.3 (#12693)
chore: update the market sdk to 0.31.3
2026-03-04 21:50:58 +08:00
Innei 07997b44a5 🐛 fix: skew plugin (#12669)
* fix: skew plugin

Signed-off-by: Innei <tukon479@gmail.com>

* refactor(vite): enhance vercelSkewProtection to handle static imports and improve coverage

- Added handling for static import/export declarations to ensure correct deployment links.
- Updated coverage documentation to reflect new handling for static imports and additional cases.
- Adjusted comment numbering for clarity in the processing steps.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: dev proxy

Signed-off-by: Innei <tukon479@gmail.com>

* refactor(AssistantGroup): streamline contentId handling in GroupMessage component

- Simplified the logic for determining contentId by directly using lastAssistantMsg?.id.
- Moved the creation and generation state checks to follow the contentId assignment for better clarity.

Signed-off-by: Innei <tukon479@gmail.com>

* ♻️ refactor: remove chunk error reload retry, keep notification only

Made-with: Cursor

* fix: inject

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-04 21:47:31 +08:00
René Wang 5d19dbf430 fix: Move email contact (#12323)
* fix: Move email contact

* style: profile

* fix: urk

* fix: urk

* feat: loading indicator

* fix: build error

* fix: sort

* fix: sort

* fix: sort

* fix: sort

* fix: sort
2026-03-04 19:37:31 +08:00
Neko 3f1473d65f fix(userMemories,database): should not fail if extra structure mismatch (#12686) 2026-03-04 19:09:13 +08:00
LiJian bf5d6ce2f8 🐛 fix: updown the old lobehub plugins (#12674)
* fix: updown the old lobehub plugins

* fix: update test.ts
2026-03-04 18:05:43 +08:00
Arvin Xu 6ba657e6d0 🔨 chore: fix to improve gateway (#12647)
improve gateway
2026-03-04 00:10:53 +08:00
Arvin Xu c2a49342f0 🔨 chore: update device gateway ci (#12645)
improve gateway
2026-03-03 23:41:51 +08:00
Luis Sambrano d92bb7a8e8 🐛 fix(context-engine): allow tool type recovery from manifest when models strip suffixes (#12636)
🐛 fix(context-engine): allow tool type recovery from manifest when models like GLM strip suffixes
2026-03-03 23:35:07 +08:00
Arvin Xu f223aeb7f4 🔨 chore: improve gateway (#12643)
improve gateway
2026-03-03 23:26:46 +08:00
YuTengjing 90714af0dc 🐛 fix: add await to handleResponseAPIMode to ensure proper error handling (#12640) 2026-03-03 21:56:57 +08:00
lobehubbot 8263359cc2 🔖 chore(release): release version v2.1.35 [skip ci] 2026-03-03 12:07:58 +00:00
lobehubbot 936379bd21 Merge remote-tracking branch 'origin/main' into canary 2026-03-03 12:07:53 +00:00
Arvin Xu a2c2a0ae76 🚀 release: 20260303 (#12631)
This release includes **90 commits**. Key updates are below.

###  New Features and Enhancements

- 🤖 Added **Discord IM bot integration** for receiving and responding to
messages within Discord channels.
([#12517](https://github.com/lobehub/lobehub/pull/12517))
- 🧩 Introduced **Agent Skills** support with progressive disclosure via
`lobe-tools`, allowing agents to expose task-specific capabilities.
([#12424](https://github.com/lobehub/lobehub/pull/12424),
[#12489](https://github.com/lobehub/lobehub/pull/12489))
- 🧠 Added **Memory Settings** for configuring memory effort and tool
permissions. ([#12514](https://github.com/lobehub/lobehub/pull/12514))
- 📧 Support for **changing email address** in profile settings.
([#12549](https://github.com/lobehub/lobehub/pull/12549))
- 🛡️ Added **unsaved changes guard** to prevent data loss on navigation.
([#12332](https://github.com/lobehub/lobehub/pull/12332))
- 🖱️ Support **Cmd+Click to open sidebar nav in new tab**.
([#12574](https://github.com/lobehub/lobehub/pull/12574))
- 🧮 Added **calculator builtin tool** for agents.
([#11715](https://github.com/lobehub/lobehub/pull/11715))
- 🎬 Added **video tab** to provider ModelList settings page with image
dimension/aspect ratio constraints for uploads.
([#12534](https://github.com/lobehub/lobehub/pull/12534),
[#12607](https://github.com/lobehub/lobehub/pull/12607))
- 🎯 Center active model on open in **model switch panel**.
([#12215](https://github.com/lobehub/lobehub/pull/12215))
- 🕹️ Support **agent management**.
([#12061](https://github.com/lobehub/lobehub/pull/12061))

### 🤖 Models and Provider Expansion

- 🌟 Added **Kimi K2 thinking models** (Moonshot).
([#12630](https://github.com/lobehub/lobehub/pull/12630))
- 🍌 Added **Nano Banana 2** support.
([#12493](https://github.com/lobehub/lobehub/pull/12493),
[#12496](https://github.com/lobehub/lobehub/pull/12496))
- 🎨 Added **Seedream 5 Lite** image generation model.
([#12459](https://github.com/lobehub/lobehub/pull/12459))
- 💨 Added **Qwen3.5 Flash** and Qwen3.5 OSS models.
([#12465](https://github.com/lobehub/lobehub/pull/12465))
- 🔮 Added **GLM-5**, **GLM-4.6V**, and **GLM-Image** for Zhipu.
([#12272](https://github.com/lobehub/lobehub/pull/12272))
- 📦 Batch updated model lists for AI360, Hunyuan, InternLM, Spark,
StepFun, Wenxin, and Seedream.
([#12371](https://github.com/lobehub/lobehub/pull/12371))
- 🗑️ Removed deprecated `chatgpt-4o-latest`.
([#12486](https://github.com/lobehub/lobehub/pull/12486))
-  Supplemented models from NewAPI pricing endpoint.
([#10628](https://github.com/lobehub/lobehub/pull/10628))

### 🏗️ Architecture

-  **Migrated frontend from Next.js App Router to Vite SPA** — a major
architectural change improving dev experience and build performance.
([#12404](https://github.com/lobehub/lobehub/pull/12404))
- 📂 Restructured SPA routes to `src/routes` and `src/router`.
([#12542](https://github.com/lobehub/lobehub/pull/12542))
- ♻️ Refactored client agent runtime.
([#12482](https://github.com/lobehub/lobehub/pull/12482))
- 🔥 Removed invite code requirement feature.
([#12474](https://github.com/lobehub/lobehub/pull/12474))

### 🖥️ Desktop Improvements

- 🔧 Fixed better-auth client stub for Electron renderer.
([#12563](https://github.com/lobehub/lobehub/pull/12563))

### Stability, Security, and UX Fixes

- Fixed topic/thread title summarization to respect `responseLanguage`
setting. ([#12627](https://github.com/lobehub/lobehub/pull/12627))
- Fixed MCP tool install loading state.
([#12629](https://github.com/lobehub/lobehub/pull/12629))
- Fixed mermaid rendering in notebook documents.
([#12624](https://github.com/lobehub/lobehub/pull/12624))
- Fixed global memory setting and tool enabled logic.
([#12610](https://github.com/lobehub/lobehub/pull/12610))
- Fixed Vertex AI 400 error caused by duplicate tool function
declarations. ([#12604](https://github.com/lobehub/lobehub/pull/12604))
- Fixed multiple Vertex AI and Moonshot runtime issues.
([#12595](https://github.com/lobehub/lobehub/pull/12595))
- Fixed SiliconCloud model thinking mode toggle.
([#10011](https://github.com/lobehub/lobehub/pull/10011))
- Fixed DeepSeek-Reasoner `reasoning_content` for tool calls.
([#12564](https://github.com/lobehub/lobehub/pull/12564))
- Fixed Google API key header passing (`x-goog-api-key`).
([#12506](https://github.com/lobehub/lobehub/pull/12506))
- Fixed `@napi-rs/canvas` hoisting for PDF parsing in Docker.
([#12475](https://github.com/lobehub/lobehub/pull/12475))
- Fixed model select panel flickering and improved list implementation.
([#12485](https://github.com/lobehub/lobehub/pull/12485))
- Fixed memory tools to run in server correctly with correct cron
schedule. ([#12471](https://github.com/lobehub/lobehub/pull/12471),
[#12568](https://github.com/lobehub/lobehub/pull/12568))
- Fixed group agent rename, skill search, and editor focus issues in
agent settings.
([#12511](https://github.com/lobehub/lobehub/pull/12511),
[#12432](https://github.com/lobehub/lobehub/pull/12432),
[#12512](https://github.com/lobehub/lobehub/pull/12512))
- Fixed NewAPI proxy gzip handling.
([#10628](https://github.com/lobehub/lobehub/pull/10628))
- Fixed provider request filtering for disabling browser requests.
([#12002](https://github.com/lobehub/lobehub/pull/12002))
- Fixed `input_image` incorrectly passed when no `image_url` present.
([#12017](https://github.com/lobehub/lobehub/pull/12017))
- Fixed crawler error handling and timeout cancellation.
([#12487](https://github.com/lobehub/lobehub/pull/12487))
- Added username and fullName length validation.
([#12614](https://github.com/lobehub/lobehub/pull/12614))
- Added database migration to Vercel build command.
([#12551](https://github.com/lobehub/lobehub/pull/12551))
- Improved auth db fallback for secondary-storage sessions.
([#12548](https://github.com/lobehub/lobehub/pull/12548))
- Fixed type not preserved when model batch processing.
([#10015](https://github.com/lobehub/lobehub/pull/10015))
- Fixed search issue.
([#12457](https://github.com/lobehub/lobehub/pull/12457))

### 🙏 Credits

Huge thanks to these contributors (alphabetical):

@Innei @arvinxx @canisminor1990 @cy948 @eaten-cake @eronez @hezhijie0327
@mikelambert @nekomeowww @rdmclin2 @sxjeru @tjx666
2026-03-03 20:07:11 +08:00
sxjeru 1c1af17716 feat: add auto aspect ratio and image search support for Nano Banana 2 (#12537)
* Update sync.yml

*  feat: update aspect ratio defaults to 'auto' for image generation models

*  feat: enhance grounding metadata handling with image search results support

*  feat: filter empty strings from searchQueries in groundingMetadata and update favicon handling in SearchGrounding component

*  feat: add inputToolTokens tracking and update related components for tool usage

*  feat: enhance search grounding with image results and update related components

*  feat: add ImageSearchRef component and related tests for image reference handling

* fix test: rename VertexAIStream to GoogleGenerativeAIStream for consistency in test cases

* Update sync.yml
2026-03-03 19:31:29 +08:00
huangkairan 1cf0257326 fix: scripts support win32 (#12613)
Co-authored-by: Innei <tukon479@gmail.com>
2026-03-03 19:19:56 +08:00
YuTengjing 138788b1d4 feat(moonshot): add kimi-k2 thinking models and update model bank (#12630) 2026-03-03 19:15:10 +08:00
YuTengjing b44f79857b 🐛 fix(topic): use responseLanguage for topic/thread title summarization (#12627) 2026-03-03 18:52:29 +08:00
Innei 58fb45d251 🐛 fix: add unsaved changes guard to prevent data loss on navigation (#12332)
* 🐛 fix: add unsaved changes guard to prevent data loss on navigation

Migrate from BrowserRouter to createBrowserRouter (data router API) to enable
route-level navigation blocking. Add UnsavedChangesGuard component that uses
useBlocker to prevent leaving editor pages with unsaved changes, auto-saving
before navigation. Remove legacy renderRoutes/RouteConfig dead code.

Fixes LOBE-4973

* 🔧 chore: remove unused ESLint suppressions for welcome.ts

Cleaned up eslint-suppressions.json by removing suppressions related to sort-keys-fix and typescript-sort-keys for welcome.ts, as they are no longer needed.

Signed-off-by: Innei <tukon479@gmail.com>

*  perf: skip JSON snapshot on selection-only Lexical updates

Reintroduce dirtyElements/dirtyLeaves guard before editor.getDocument('json')
and deep-equality check, avoiding O(document-size) work on caret/selection
updates that do not mutate content.

* 🔧 test: update UnsavedChangesGuard tests to use message.destroy instead of message.success

Signed-off-by: Innei <tukon479@gmail.com>

* fix: dayjs init

- Moved dayjs plugin extensions (relativeTime, utc, isToday, isYesterday) to src/initialize.ts for centralized initialization.
- Removed redundant extensions from individual components to prevent duplicate calls.
- Updated locale loading logic in Locale.tsx to ensure correct dayjs locale handling.

This change improves performance by ensuring dayjs plugins are only extended once during application initialization.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: update router configuration to use RouteObject type

- Changed the type of desktopRoutes from RouteConfig[] to RouteObject[] for better compatibility with react-router-dom.
- Removed the RouteConfig interface from the router utility file to streamline the codebase.

This refactor enhances the router's integration with the latest routing library standards.

Signed-off-by: Innei <tukon479@gmail.com>

* feat: enhance Vite configuration and chunk management

- Added a function to suppress Vite's default URL print in the server configuration.
- Updated chunk file naming strategy in sharedRollupOutput to organize output files into specific directories based on chunk type (i18n, vendor, assets).
- Removed redundant dayjs chunk handling logic to streamline the manualChunks function.

These changes improve the clarity of the build output and enhance the server's configuration options.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: add collapsible error stack with __CI__ default expand

- Add Collapse + Highlighter for error stack in Error component
- Define __CI__ in Vite (sharedRendererDefine) based on process.env.CI
- Add __CI__ to global.d.ts
- Add error.stack i18n to all 18 locales

Made-with: Cursor

* chore: update build:spa:copy script to handle multiple asset directories

- Modified the build:spa:copy script in package.json to iterate over multiple directories (assets, i18n, vendor) for both desktop and mobile builds, improving the asset copying process.

This change enhances the build process by ensuring all relevant directories are copied correctly.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: mark initialize.ts as sideEffects to prevent tree-shaking

sideEffects: false caused Rollup to drop the side-effect-only import
of initialize.ts, removing dayjs.extend(relativeTime) and enableMapSet()
from the production bundle.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-03 18:50:57 +08:00
Rdmclin2 026af3f6bc 🐛 fix: mcp tool install loading (#12629)
fix: mcp tool install loading
2026-03-03 18:47:43 +08:00
Arvin Xu bcae49ff65 🔨 chore: exclude apps/device-gateway in type check (#12628)
exclude apps/device-gateway route
2026-03-03 18:41:26 +08:00
Innei 89857847bf 🐛 fix(markdown): render mermaid in notebook document (#12624)
* 🐛 fix(markdown): render mermaid blocks in notebook documents

Render `mermaid` code fences with the Mermaid component in MDX code blocks so notebook documents display diagrams consistently with chat flow.

Made-with: Cursor

* fix: search event

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-03 18:03:58 +08:00
Arvin Xu a1a89b3531 💄 style: improve server agent harness (#12611)
* add device gateway

* improve persona memory

* support auto renaming

* support memory

* fix memory captureAt

* add more db testing

* add more db testing

* add agent tracing tool

* add agent tracing tool

* fix lint

* fix lint

* update skills

* Potential fix for code scanning alert no. 178: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-03 17:35:18 +08:00
LobeHub Bot f234397bf8 🌐 chore: translate non-English comments to English in bot/ackPhrases (#12606)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 17:34:20 +08:00
Rdmclin2 ceeb9c6613 🐛 fix: global memory setting & tool enabled logic (#12610)
* fix: global enable user memories

* fix: memory tool enabled logic
2026-03-03 16:50:24 +08:00
YuTengjing 9ab2f219e4 🌐 locale: add usernameTooLong translations with CJK spacing fix (#12615) 2026-03-03 16:13:18 +08:00
CanisMinor 43578a9bcc 📝 docs: Polishing and improving product documentation (#12612)
* 🔖 chore(release): release version v2.1.34 [skip ci]

* 📝 docs: Polish documents

* 📝 docs: Fix typo

* 📝 docs: Update start

* 📝 docs: Fix style

* 📝 docs: Update start

* 📝 docs: Update layout

* 📝 docs: Fix typo

* 📝 docs: Fix typo

---------

Co-authored-by: lobehubbot <i@lobehub.com>
2026-03-03 16:01:41 +08:00
YuTengjing 4926b20271 🐛 fix(user): add length validation for username and fullName (#12614) 2026-03-03 15:48:04 +08:00
YuTengjing 521a0a077e feat(video): add image dimension and aspect ratio constraints for uploads (#12607)
* ♻️ refactor: replace minImageSize with width/height min/max constraints

Refactor image dimension validation from a single `minImageSize` value
to flexible `width`/`height` objects with `min`/`max`, consistent with
the `duration` field's min/max pattern.

* ♻️ refactor: handle single-axis constraints in dimension error messages

Build dimension constraint text dynamically (e.g. "width ≥ 300px" or
"width ≥ 300px, height ≥ 300px") instead of interpolating raw
minWidth/minHeight values, preventing "300xundefinedpx" when only one
axis is constrained.

* ♻️ refactor(video): add max dimension constraint per official docs

Seedance image dimensions: 300-6000px per official documentation.

*  feat(video): add aspect ratio validation for image uploads

Support aspectRatio constraint (width/height) with min/max in schema.
Seedance config: aspectRatio { min: 0.4, max: 2.5 } per official docs.

* ♻️ refactor(locales): add image dimension validation messages for multiple languages
2026-03-03 14:12:34 +08:00
YuTengjing e733397f5d 🐛 fix: deduplicate tool function declarations to fix Vertex AI 400 error (#12604) 2026-03-03 11:44:58 +08:00
YuTengjing c1521d2aeb 💄 style: batch fix eslint violations across packages (#12601) 2026-03-03 02:19:50 +08:00
Arvin Xu 466f713ca6 💄 style: improve discord intergration (#12598)
* add tests

* fix reference messages issue

* support file upload

* support reference file content

* fix eye issue
2026-03-03 01:44:45 +08:00
YuTengjing 8ced872e53 🐛 fix(model-runtime): fix multiple Vertex AI and Moonshot runtime issues (#12595) 2026-03-03 01:34:53 +08:00
YuTengjing 6ecba929b7 🔨 chore: remove dead eslint disable comments for deleted rules (#12597) 2026-03-02 23:18:01 +08:00
Rdmclin2 607dfdec96 feat: support memory setting (#12514)
* feat: add memory actionbar and setting config

* chore: hide memory tool in skill popcontent

* test: add memory effort test case

* chore: update i18n files

* chore: update i18n files
2026-03-02 23:14:02 +08:00
Innei c4d85d100c ⬆️ chore(deps): migrate @lobehub/ui to base-ui exports (#12587)
* ⬆️ chore(deps): migrate @lobehub/ui to base-ui exports

- Migrate LobeSelect → Select from @lobehub/ui/base-ui
- Migrate LobeSwitch → Switch from @lobehub/ui/base-ui
- Fix DropdownItem import (use main package instead of internal path)
- Add initialWidth, popupWidth support to ModelSelect

Made-with: Cursor

* ⬆️ chore(deps): update @lobehub packages to latest versions

- Upgrade @lobehub/charts to ^5.0.0
- Upgrade @lobehub/editor to ^4.0.0
- Upgrade @lobehub/icons to ^5.0.0
- Upgrade @lobehub/market-sdk to ^0.31.1
- Upgrade @lobehub/tts to ^5.0.0
- Upgrade @lobehub/ui to ^5.0.0 across multiple packages
- Update peer dependencies for various packages to align with new @lobehub/ui version

Made-with: Cursor
Signed-off-by: Innei <tukon479@gmail.com>

* ⬆️ chore(deps): update @lobehub/tts to version 5.1.2

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-02 22:40:46 +08:00
YuTengjing a9511344f9 feat(model-runtime): add optionIndex to RouteAttemptResult (#12588) 2026-03-02 20:10:22 +08:00
LobeHub Bot eb1da3c297 🌐 chore: translate non-English comments to English in market-auth module (#12572)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:45:04 +08:00
YuTengjing c8d2f28bf5 💄 style: fix cursor pointer on dropdown tags and rename downloadClient to getDesktopApp (#12582) 2026-03-02 17:04:52 +08:00
Arvin Xu 46c9cb3b03 💄 style: improve discord interaction (#12573)
* improve discord interaction

* improve discord interaction

* update

* update Message engine

* add test

* update vercel route
2026-03-02 16:13:21 +08:00
Innei dc6d5cf489 📝 docs(desktop): update Development.md to reflect current project structure [skip ci] (#12581)
📝 docs(desktop): update Development.md to reflect current project structure

Made-with: Cursor
2026-03-02 15:36:04 +08:00
YuTengjing c068363fac 💄 style: support Cmd+Click to open sidebar nav in new tab (#12574) 2026-03-02 12:50:05 +08:00
arvinxx dd0d4d8890 improve discord interaction 2026-03-02 11:38:47 +08:00
Neko 01606208c5 fix(userMemories): incorrect hourly cron schedule for memory analysis (#12568) 2026-03-02 02:06:56 +08:00
Rylan Cai 5fe0ac228e 🐛 fix: should not pass input_image in message content when has no image_url (#12017)
* 🐛 fix: should not pass `input_image` when has no url

* 🐛 fix: model output should use output text

* 📝 docs: re run ci

* 📝 docs: improve codes
2026-03-02 01:35:04 +08:00
Arvin Xu 16946a4d5b 💄 style: get user timezone when open (#12567)
* fix timezone issue

* fix tests
2026-03-02 01:30:10 +08:00
Innei 37e90cebfa 🐛 fix(desktop): stub better-auth client for Electron renderer (#12563)
* 🐛 fix(desktop): stub better-auth client for Electron and improve drag regions

Add auth-client.desktop.ts noop stub so the Electron renderer build
skips the real better-auth dependency that was crashing module evaluation
and preventing React from mounting.

Also fix drag-bar regions in splash.html and error.html, and add
dev:desktop convenience script.

* ♻️ refactor(desktop): lazy-init better-auth client with remote server URL

Replace noop stub with Proxy-based lazy initialization that creates the
real better-auth client on first use, using the configured remote server
URL from the electron store as baseURL.

* 🔧 fix(desktop): update Proxy target in lazyProp for better-auth client initialization

Change the Proxy target in the lazyProp function from a noop stub to a function, ensuring the apply trap works correctly for lazy initialization of the better-auth client.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix(profile): restrict SSO providers display to non-desktop view

Update the condition for rendering the SSO Providers Row in the Profile Settings to only show when the user is logged in and not on a desktop device. This change improves the user interface by preventing unnecessary display on desktop screens.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-03-01 22:20:10 +08:00
YuTengjing ee85ea728a 🐛 fix(model-runtime): ensure reasoning_content for deepseek-reasoner tool calls (#12564) 2026-03-01 22:16:12 +08:00
YuTengjing 9b5b4d2579 feat(model-runtime): pass userId in RouteAttemptResult callback (#12562) 2026-03-01 21:45:03 +08:00
Innei 5f2f49a26e feat(vite): add env restart keys plugin for selective .env restart (#12561)
Add a Vite plugin that prevents server restart on every .env file change,
only restarting when whitelisted env keys actually change their values.
2026-03-01 20:58:05 +08:00
sxjeru 8ca9c0100e 🐛 fix(vercel): add database migration to build command (#12551)
* 🐛 fix(vercel): add database migration to build command

* 🐛 fix(build): update Vercel build command to use new build script

---------

Co-authored-by: Arvin Xu <arvinx@foxmail.com>
2026-03-01 20:56:32 +08:00
LobeHub Bot 1c11921a32 🌐 chore: translate non-English comments to English in MCP module (#12520)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 19:58:59 +08:00
Arvin Xu d68acec58e feat: support Discord IM bot intergration (#12517)
* clean

fix tools calling results

improve display

support discord bot

finish bot integration

* improve next config

* support queue callback mode

* support queue callback mode

* improve error

* fix build

* support serverless gateway

* support serverless gateway

* support serverless enable

* improve ui

* improve ui

* add credentials config

* improve and refactor data working

* update config

* fix integration

* fix types

* fix types

* fix types

* fix types

* move files

* fix update

* fix update

* fix update
2026-03-01 19:54:38 +08:00
sxjeru 902a265aed 🐛 fix: type not preserved when model batch processing (#10015)
*  feat(aiModel): preserve type information when creating and updating models
🔤 fix(clerk): update translation for password to通行密钥

*  feat(qwen): 添加 Qwen3 Max Preview 模型,支持上下文缓存和复杂任务

*  feat(qwen): 更新 qwenChatModels,添加推理和搜索能力

*  feat(aiModel): 优化批量插入和更新模型

*  feat(cerebras, google): 移除 Qwen 3 Coder 480B 模型并更新 Nano Banana 模型的上下文窗口和最大输出

*  feat(moonshot): 添加 Kimi K2 Thinking 和 Kimi K2 Thinking Turbo 模型,更新模型参数处理

*  feat(minimax, ollamacloud): 添加缓存读取和写入定价,更新 Kimi K2 Thinking 模型信息

* ♻️ refactor: 更新通行密钥相关文本及优化数据库模型代码

*  feat(moonshot): 处理模型列表以包含上下文窗口令牌和模型 ID

*  feat(moonshot): 添加支持图像输入的模型属性

*  feat: 更新模型参数,调整上下文窗口令牌,移除冗余代码
2026-03-01 19:30:07 +08:00
LobeHub Bot 9cd63765b0 test: add unit tests for mimeType utility (#12555)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 19:25:28 +08:00
Innei 794fe5f60b ♻️ refactor: restructure SPA routes to src/routes and src/router (#12542)
* 📝 docs: add SPA routes restructure design and implementation plan

* ♻️ refactor: restructure SPA routes to src/routes and src/router

- Move SPA page components from src/app/[variants] to src/routes/
  - (main) -> Desktop pages
  - (mobile) -> Mobile pages
  - (desktop) -> Desktop-specific pages
  - onboarding -> Onboarding pages
  - share -> Share pages
- Move router configurations from src/app/[variants]/router to src/router/
  - desktopRouter.config.tsx
  - desktopRouter.config.desktop.tsx
  - mobileRouter.config.tsx
- Keep auth pages in src/app/[variants]/(auth) for SSR
- Update all import paths:
  - @/app/[variants]/ -> @/routes/
  - Relative paths adjusted for new directory structure
- Update CLAUDE.md and project-overview skill documentation

* 🔧 chore: restore imports for RouteConfig and ErrorBoundary in desktopRouter.config.desktop.tsx

- Reintroduced the imports for RouteConfig, ErrorBoundary, and redirectElement in the desktop router configuration file.
- Ensured proper organization and functionality of the desktop routing setup.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: update import paths after routes restructure

- Fix imports from old `src/app/[variants]/` to new `src/routes/` paths
- Update Title, Sidebar, MakedownRender, McpList imports
- Fix desktop-onboarding/storage import path
- Run lint --fix to sort imports

* 📝 docs: SPA routes convention and spa-routes skill

- Add roots vs features rules to CLAUDE.md and AGENTS.md
- Add .agents/skills/spa-routes for route/feature file division
- Phase 1: move page route logic to src/features/Pages, thin route files

Made-with: Cursor

* 🌐 chore: translate non-English comments to English in memory module (#12547)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor: move router and entries to src/spa, platform-based warmup

- Move src/router and entry.*.tsx to src/spa/
- Update HTML, vite.config, and entry imports
- Warmup only the entry matching current platform (web/mobile)
- Update CLAUDE.md, AGENTS.md, and spa-routes skill

Made-with: Cursor

* 🗂️ chore: restructure SPA routes and configurations

- Deleted outdated SPA routes and implementation plan documents.
- Migrated SPA page components to new `src/routes/` directory.
- Moved route configurations to `src/router/`.
- Updated import paths across the project to reflect new structure.
- Revised AI documentation to align with the updated directory layout.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: LobeHub Bot <i@lobehub.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 18:35:38 +08:00
LobeHub Bot 5e3a8146d1 🌐 chore: translate non-English comments to English in memory module (#12547)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 14:58:23 +08:00
YuTengjing c1d2e761fe 💄 style: unify zh-CN wording from 点数 to 积分 (#12553) 2026-03-01 12:38:19 +08:00
Arvin Xu 8bc3a8a886 🔨 chore: fix oidc-provider issue in turbopack (#12550)
fix oidc-provider issue
2026-03-01 12:19:10 +08:00
Zhijie He 2b2892aa2b 💄 style: add qwen3.5-flash & Qwen3.5 OSS models (#12465)
* style: add `qwen3.5-flash` & Qwen3.5 OSS models

* fix: fix vision tag missing

* update list

* fix: restore imageGen models default enabled status
2026-03-01 12:02:46 +08:00
YuTengjing 984884ba2b feat: support changing email address in profile settings (#12549) 2026-03-01 12:01:53 +08:00
YuTengjing dc26f23ea0 🐛 fix(auth): enable db fallback for secondary-storage sessions (#12548) 2026-03-01 11:25:22 +08:00
Innei 18ec113bba 🔧 chore: simplify build config and remove webpack customization (#12539)
- Remove desktop-related build steps from Dockerfile
- Simplify next.config.ts, only apply Vercel-specific config on Vercel
- Remove webpack customization from define-config.ts
- Fix String() type conversion in video.ts
2026-03-01 00:22:21 +08:00
YuTengjing d9b4ab01ce feat(redis): add pipeline support to Redis abstraction layer (#12538) 2026-03-01 00:07:35 +08:00
sxjeru 4279f0e57c 🐛 fix: unable to toggle SiliconCloud model thinking mode (#10011)
*  fix: 修复 siliconcloud 思考模型开关

*  feat: 更新思考模型参数设置,优化 enable_thinking 和 thinking_budget 逻辑

*  feat: 增强 SiliconCloud API 错误处理,支持提取错误代码和消息

* feat: 使用 TextEncoder 计算响应内容的字节长度,替代 Buffer

* 🐛 fix: handle undefined thinking.type in enable_thinking assignment

* 🐛 fix: 修复 enable_thinking 赋值时处理 undefined thinking.type 的情况;更新测试以确保错误消息有效

*  feat: 更新 aiModels 文件中的描述为英文,确保符合英语描述规范;移除不必要的字段
2026-02-28 23:23:25 +08:00
YuTengjing ac0be5ed5c feat: add video tab to provider ModelList settings page (#12534) 2026-02-28 22:29:51 +08:00
lobehubbot 9f22867f3c 🔖 chore(release): release version v2.1.34 [skip ci] 2026-02-28 13:06:21 +00:00
lobehubbot ed4eb874b2 Merge remote-tracking branch 'origin/main' into canary 2026-02-28 13:06:11 +00:00
Arvin Xu 49a8f6b497 🐛 fix: fix benchmarks table schema not correctly (#12532)
* fix benchmark table issue

* add new db migration

* fix types
2026-02-28 21:05:32 +08:00
Innei 3112036b38 🔧 chore: resolve all ESLint suppressions and remove suppression file (#12518)
* 🔧 chore: upgrade ESLint deps and resolve all suppressions

- Upgrade eslint 10.0.0→10.0.2, @lobehub/lint 2.1.3→2.1.5, eslint-plugin-mdx ^3.6.2→^3.7.0
- Remove eslint-suppressions.json and all suppression-related scripts/configs
- Fix 197 ESLint errors: no-console, no-unused-private-class-members, no-useless-assignment, preserve-caught-error, prefer-const, regex issues, etc.
- Remove dead rule references (sort-keys-fix, typescript-sort-keys, ban-types)
- Disable project-convention-conflicting rules globally in eslint.config.mjs
- Update test spies from console.log to console.info

* 🔧 fix: update regex for unresolved model error handling

- Modified the UNRESOLVED_MODEL_REGEXP to allow for additional valid characters in model names, enhancing error detection for missing models.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-28 20:23:04 +08:00
sxjeru 7d7af6b8ca 💄 style: add support for Nano Banana 2 (#12496)
*  feat: add support for new image resolution and thinking level parameters in model configurations

*  feat: add Gemini 3.1 Flash Image model with enhanced capabilities and update model configurations

* 🐛 fix: adjust temperature setting based on model modalities in LobeGoogleAI

*  feat: add DeepSeek V3.2 model and update existing model configurations

*  feat: add ImageResolution2Slider and ThinkingLevel4Slider components; remove obsolete imports

*  feat: add imageAspectRatio2 parameter for Nano Banana 2 model; update related components and configurations

* 🐛 fix: refactor outputTextTokens calculation for clarity and consistency
2026-02-28 20:03:50 +08:00
LobeHub Bot 27c3a831f2 test: add unit tests for DocumentService (#12525)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:50:06 +08:00
Innei a5c454589d ️ perf(spa): lazy import spaHtmlTemplates to reduce initial bundle (#12526)
* ️ perf(spa): lazy import spaHtmlTemplates to reduce initial bundle

* 🔧 chore: update package dependencies and Vite configuration

- Bump @lobehub/icons version from ^4.1.0 to ^4.9.0 in package.json for improved features and fixes.
- Set Vite server host to '0.0.0.0' for better accessibility during development.
- Refactor asset URL rewriting in route.ts to simplify the return statement in the development mode.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(analytics): enhance Google and Vercel analytics integration

- Refactored GoogleAnalytics component to accept a `gaId` prop for improved flexibility.
- Updated VercelAnalytics component to accept a `debug` prop, allowing for dynamic debugging.
- Modified Analytics index to pass the appropriate props to Google and Vercel components based on environment settings.
- Removed the obsolete LobeAnalyticsProviderWrapper.vite.tsx file to streamline the codebase.

This update improves the configurability of analytics components and cleans up unused files.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-28 17:15:59 +08:00
Arvin Xu 4d3c759d25 🔨 chore: fix dev start scripts (#12528)
fix dev start
2026-02-28 15:00:17 +08:00
sxjeru c8b23c6819 🐛 fix(newapi): supplement models from NewAPI pricing endpoint & fix proxy gzip handling (#10628)
*  feat: enhance model pricing handling and prevent duplication from pricing list

*  feat: add description field to NewAPIPricing and update fetch headers for JSON response

*  feat: 添加混元2.0模型及其定价信息,增强模型能力描述

*  feat: 添加 DeepSeek V3.2 模型及其定价信息,移除过时的实验性模型

*  feat: 启用 DeepSeek V3.2 和 V3.1 Terminus 模型,移除不必要的 enabled 属性

*  feat: 添加 GLM-4.6V 和 GLM-4.6V-Flash 模型,更新模型能力和定价信息

*  feat: 移除 Mistral Saba 24B 模型,更新 Zenmux 模型的能力描述

*  feat: 移除 LearnLM 实验性模型,更新 Mistral 模型的上下文窗口和描述信息

*  feat: 添加 GLM-4.6V 模型,更新 siliconcloud 模型的上下文窗口,移除过时的 Gemini 模型

*  feat: update model descriptions and add new model processing test

* update model descriptions to English for better clarity

* Update siliconcloud.ts

* 🔧 refactor: simplify pricing list check and remove unused input reference in Editing component

* translated

*  feat(models): add Z.ai GLM 4.7 model and update Qwen deployment date

* Delete src/app/[variants]/(main)/chat/_layout/Sidebar/Topic/List/Item/Editing.tsx

* 🔧 refactor(groq): remove unused 'Llama 4 Maverick' model and its properties

* 🔧 refactor(models): 移除多个重复模型模型及其属性

* 🔧 chore(package): 调整构建脚本中的内存限制
2026-02-28 14:49:05 +08:00
Arvin Xu eef04c499f 💄 style: suppot agent management (#12061)
* feat: improve the inject model context plugins decriptions

fix: change the conversation-flow to change the subAgent message show place

fix: eslint fixed

fix: slove the inject not work problem

feat: add the lost agent management inject open

feat: add the AgentManagementInjector

fix: add the exec task mode & improve the Pre-load agents

fix: improve the executor import way & update the getEffectiveAgentId function

fix: slove the test problem

🐛 fix: support agnet manager ments (#12171)

feat: add the sub agents in context scope to support call subagent

refactor agent management implement

update

add builtin agent management

* fix types

* fix import

* fix test

* fix tests

* fix tests
2026-02-28 13:52:35 +08:00
sxjeru 4f3055e0c5 🐛 fix: update provider request filtering to include settings for disabling browser requests (#12002)
* 🐛 fix: update provider request filtering to include settings for disabling browser requests

* 🐛 test: add unit tests for isProviderDisableBrowserRequest function

* 🐛 fix(models): remove deprecated AI models and add Step 3.5 Flash model

*  feat(model-bank): add Qwen3 Coder Next and GLM-4.7 models; remove Qianfan Lightning 128B A19B

* feat: add new Qwen3.5 and MiniMax-M2.5 models with updated pricing and capabilities

* feat: update cerebras and ollamacloud models with new Qwen3.5 capabilities and adjust pricing in qwen model
2026-02-28 12:48:21 +08:00
YuTengjing d9d1d071b7 💄 style: correct translation for Nano Banana2 in home.json (#12515) 2026-02-28 01:07:16 +08:00
YuTengjing c22cd67b5f 💄 style: replace Nano Banana emoji with icon component (#12513) 2026-02-28 01:05:18 +08:00
Innei 687b36c81c ♻️ refactor: migrate frontend from Next.js App Router to Vite SPA (#12404)
* init plan

* 📝 docs: update SPA plan for dev mode Worker cross-origin handling

- Clarified the handling of Worker cross-origin issues in dev mode, emphasizing the need for `workerPatch` to wrap cross-origin URLs as blob URLs.
- Enhanced the explanation of the dev mode's resource URL rewriting process for better understanding.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: Phase 1 - 环境变量整治

- Fix Pyodide env var mismatch (NEXT_PUBLIC_PYPI_INDEX_URL → pythonEnv.NEXT_PUBLIC_PYODIDE_PIP_INDEX_URL)
- Consolidate python.ts to use pythonEnv instead of direct process.env
- Remove NEXT_PUBLIC_ prefix from server-side MARKET_BASE_URL (5 files)

* 🏗️ chore: Phase 2 - Vite 工程搭建

- Add vite.config.ts with dual build (desktop/mobile via MOBILE env)
- Add index.html SPA template with __SERVER_CONFIG__ placeholder
- Add entry.desktop.tsx and entry.mobile.tsx SPA entry points
- Add dev:spa, dev:spa:mobile, build:spa, build:spa:copy scripts
- Install @vitejs/plugin-react and linkedom

* ♻️ refactor: Phase 3 - 第一方包 Next.js 解耦

- Replace next/link with <a> in builtin-tool-web-browsing (4 files, external links)
- Replace next/image with <img> in builtin-tool-agent-builder/InstallPlugin.tsx
- Add Vite import.meta.env compat for isDesktop in const/version.ts, builtin-tool-gtd, builtin-tool-group-management

* ♻️ refactor: Phase 4a - Auth 页面改用直接 next/navigation 和 next/link

- 9 auth files: @/libs/next/navigation → next/navigation
- 5 auth files: @/libs/next/Link → next/link
- Auth pages remain in Next.js App Router, need direct Next.js imports

* ♻️ refactor: Phase 4b - Next.js 抽象层替换为 react-router-dom/vanilla React

- navigation.ts: useRouter/usePathname/useSearchParams/useParams → react-router-dom
- navigation.ts: redirect/notFound → custom error throws
- navigation.ts: useServerInsertedHTML → no-op for SPA
- Link.tsx: next/link → react-router-dom Link adapter (href→to, external→<a>)
- Image.tsx: next/image → <img> wrapper with fill/style support
- dynamic.tsx: next/dynamic → React.lazy + Suspense wrapper

*  feat: Phase 5 - 新建 SPAGlobalProvider

- Create SPAServerConfig type (analyticsConfig, clientEnv, theme, featureFlags, locale)
- Add window.__SERVER_CONFIG__ and __MOBILE__ to global.d.ts
- Create SPAGlobalProvider (client-only Provider tree mirroring GlobalProvider)
- Includes AuthProvider for user session support
- Update entry.desktop.tsx and entry.mobile.tsx to wrap with SPAGlobalProvider

* ♻️ refactor: add SPA catch-all route handler with Vite dev proxy

- Create (spa)/[[...path]]/route.ts for serving SPA HTML
- Dev mode: proxy Vite dev server, rewrite asset URLs, inject Worker patch
- Prod mode: read pre-built HTML templates
- Build SPAServerConfig with analytics, theme, clientEnv, featureFlags
- Update middleware to pass SPA routes through to catch-all

* ♻️ refactor: skip auth checks for SPA routes in middleware

SPA pages are all public (no sensitive data in HTML).
Auth is handled client-side by SPAGlobalProvider's AuthProvider.
Only Next.js auth routes and API endpoints go through session checks.

* ♻️ refactor: replace Next.js-specific analytics with vanilla JS

- Google.tsx: replace @next/third-parties/google with direct gtag script
- ReactScan.tsx: replace react-scan/monitoring/next with generic script
- Desktop.tsx: replace next/script with native script injection

* ♻️ refactor: migrate @t3-oss/env-nextjs to @t3-oss/env-core

Replace framework-specific env validation with framework-agnostic version.
Add clientPrefix where client schemas exist.

* ♻️ refactor: replace next-mdx-remote/rsc with react-markdown

Use client-side react-markdown for MDX rendering instead of
Next.js RSC-dependent next-mdx-remote.

* 🔧 chore: update build scripts and Dockerfile for SPA integration

- build:docker now includes SPA build + copy steps
- dev defaults to Vite SPA, dev:next for Next.js backend
- Dockerfile copies public/spa/ assets for production
- Add public/spa/ to .gitignore (build artifact)

* 🗑️ chore: remove old Next.js route segment files and serwist PWA

- Delete [variants] page.tsx, error.tsx, not-found.tsx, loading.tsx
- Delete root loading.tsx and empty [[...path]] directory
- Delete unused loaders directory
- Remove @serwist/next PWA wrapper from Next.js config

* plan2

*  feat: add locale detection script to index.html for SPA dev mode

* ♻️ refactor: remove locale and theme from SPAServerConfig

*  feat: add [locale] segment with force-static and SEO meta generation

* ♻️ refactor: remove theme/locale reads from SPAGlobalProvider

*  feat: set vite base to /spa/ for production builds

*  feat: auto-generate spaHtmlTemplates from vite build output

* 🔧 chore: register dev:next task in turbo.json for parallel dev startup

* ♻️ refactor: rename (spa) route group to spa segment, rewrite SPA routes via middleware

*  feat: add Vite-compatible i18n/locale modules with import.meta.glob and resolve aliases

* 🔧 fix: use custom Vite plugin for module redirects instead of resolve.alias

* very important

* build

* 🔧 chore: update build scripts and clean up Vite configuration by removing unused plugin and code

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ refactor: remove all electron modifier scripts

Modifiers are no longer needed with Vite SPA renderer build.

*  feat: add Vite renderer entry to electron-vite config

Add renderer build configuration to electron-vite, replacing the old
Next.js shadow workspace build flow. Delete buildNextApp.mts and
moveNextExports.ts, update package.json scripts accordingly.

*  feat: add .desktop suffix files for eager i18n loading

Create 4 .desktop files that use import.meta.glob({ eager: true })
for synchronous locale access in Electron desktop builds, replacing
the async lazy-loading used in web SPA builds.

* 🔧 refactor: adapt Electron main process for Vite renderer

Replace nextExportDir with rendererDir, update protocol from
app://next to app://renderer, simplify file resolution to SPA
fallback pattern, update _next/ asset paths to /assets/.

* 🔧 chore: update electron-builder files config for Vite renderer

Replace dist/next references with dist/renderer, remove Next.js
specific exclusion rules no longer applicable to Vite output.

* 🗑️ chore: remove @ast-grep/napi dependency

No longer needed after removing electron modifier scripts.

* 🔧 refactor: unify isDesktop to __ELECTRON__ compile-time constant

Remove NEXT_PUBLIC_IS_DESKTOP_APP and VITE_IS_DESKTOP_APP env vars.
Unify isDesktop in @lobechat/const using __ELECTRON__ defined by Vite.
Re-export from builtin-tool packages. Scripts use DESKTOP_BUILD.

* update

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: use electron-vite ELECTRON_RENDERER_URL instead of hardcoded port 3015

Replace hardcoded http://localhost:3015 with process.env.ELECTRON_RENDERER_URL
injected by electron-vite dev server. Clean up stale Next.js references.

* 🐛 fix: use local renderer-entry shim to resolve Vite root path issue

HTML entry ../../src/entry.desktop.tsx resolves to /src/entry.desktop.tsx
in URL space, which Vite cannot find within apps/desktop/ root. Add a
local shim that imports across root via module resolver instead.

* 🔧 refactor: extract shared renderer Vite config into sharedRendererConfig

Deduplicate plugins (nodeModuleStub, platformResolve, tsconfigPaths) and
define (__MOBILE__, __ELECTRON__, process.env) between root vite.config.ts
and electron.vite.config.ts renderer section.

* 🔧 refactor: move all renderer plugins and optimizeDeps into shared config

sharedRendererPlugins now includes react, codeInspectorPlugin alongside
nodeModuleStub, platformResolve, tsconfigPaths. Add sharedOptimizeDeps
for pre-bundling list. Both root and electron configs consume shared only.

* 🐛 fix: set electron renderer root to monorepo root for correct glob resolution

import.meta.glob with absolute paths (e.g. /node_modules/antd/...) resolved
within apps/desktop/ instead of monorepo root. Change renderer root to ROOT_DIR,
add electronDesktopHtmlPlugin middleware to rewrite / to /apps/desktop/index.html,
and remove the now-unnecessary renderer-entry.ts shim.

* desktop vite !!

Signed-off-by: Innei <tukon479@gmail.com>

* sync import !!

Signed-off-by: Innei <tukon479@gmail.com>

* clean ci!!

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: update SPA path structure and clean up dependencies

- Changed the path in .gitignore and related files from [locale] to [variants] for SPA templates.
- Updated index.html to set body height to 100%.
- Cleaned up package.json by removing unused dependencies and reorganizing devDependencies.
- Refactored RendererUrlManager to use a constant for SPA entry HTML path.
- Removed obsolete route.ts file from the SPA structure.
- Adjusted proxy configuration to reflect the new SPA path structure.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update build script to include mobile SPA build

- Modified the build script in package.json to add the mobile SPA build step.
- Ensured the build process accommodates both desktop and mobile SPA versions.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update build scripts and improve file encoding consistency

- Modified the build script in package.json to ensure the SPA copy step runs after the build.
- Updated file encoding in generateSpaTemplates.mts from 'utf-8' to 'utf8' for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 fix: correct Blob import syntax and update global server config type

- Fixed the Blob import syntax in route.ts to ensure proper module loading.
- Updated the global server configuration type in global.d.ts for improved type safety.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 test: update RendererUrlManager test to reflect new file path

- Modified the mock implementation in RendererUrlManager.test.ts to check for the updated file path '/mock/export/out/apps/desktop/index.html'.
- Adjusted the expected resolved path in the test to match the new structure.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: remove catch-all example file and update imports

- Deleted the catch-all example file `catch-all.eg.ts` to streamline the codebase.
- Updated import paths in `ClientResponsiveLayout.tsx` and `ClientResponsiveContent/index.tsx` to use the new dynamic import location.
- Added type declarations for HTML templates in `spaHtmlTemplates.d.ts`.
- Adjusted `tsconfig.json` to include the updated file structure.
- Enhanced type definitions in `global.d.ts` and fixed locale loading in `locale.vite.ts`.

Signed-off-by: Innei <tukon479@gmail.com>

* e2e

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: remove unused build script for Vercel deployment

- Deleted the `build:vercel` script from package.json to streamline the build process.
- Ensured the remaining build scripts are organized and relevant.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 config: update Vite build input for mobile support

- Changed the build input path in vite.config.ts to conditionally use 'index.mobile.html' for mobile builds, enhancing support for mobile SPA versions.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 feat: add compatibility checks for import maps and cascade layers

- Implemented functions to check for browser support of import maps and CSS cascade layers.
- Redirected users to a compatibility page if their browser does not support the required features.
- Updated the build script in package.json to use the experimental analyze command for better performance.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: rename

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 feat: refactor authentication layout and introduce global providers

- Created a new `RootLayout` component to streamline the layout structure.
- Removed the old layout file for variants and integrated necessary features into the new layout.
- Added `AuthGlobalProvider` to manage authentication context and server configurations.
- Introduced language and theme selection components for enhanced user experience.
- Updated various components to utilize the new context and improve modularity.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 config: exclude build artifacts from serverless functions

- Updated the `next.config.ts` to exclude SPA, desktop, and mobile build artifacts from serverless functions.
- Added paths for `public/spa/**`, `dist/**`, `apps/desktop/build/**`, and `packages/database/migrations/**` to the exclusion list.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 config: refine exclusion of build artifacts from serverless functions

- Updated `next.config.ts` to specify exclusion paths for desktop and mobile build artifacts.
- Changed exclusions from `dist/**` and `apps/desktop/build/**` to `dist/desktop/**`, `dist/mobile/**`, and `apps/desktop/**` for better clarity and organization.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 fix: update BrowserRouter basename for local development

- Modified the `ClientRouter` component to conditionally set the `basename` of `BrowserRouter` based on the `__DEBUG_PROXY__` variable, improving local development experience.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 feat: implement mobile SPA workflow and S3 asset management

- Added a new workflow for building and uploading mobile SPA assets to S3, including environment variable configurations in `.env.example`.
- Updated `package.json` to include a new script for the mobile SPA workflow.
- Enhanced the Vite configuration to support dynamic CDN base paths.
- Refactored the template generation script to handle mobile HTML templates more effectively.
- Introduced new modules for uploading assets to S3 and generating mobile HTML templates.

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: extract origin from MOBILE_S3_PUBLIC_DOMAIN to prevent double key prefix

* 🔧 fix: update mobile HTML template to use the latest asset versions

- Modified the mobile HTML template to reference the updated JavaScript asset version for improved functionality.
- Ensured consistency in the template structure while maintaining existing styles and scripts.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update dependencies and refine service worker integration

- Removed outdated dependencies related to Serwist from package.json and tsconfig.json.
- Added vite-plugin-pwa to enhance PWA capabilities in the Vite configuration.
- Updated service worker registration logic in the PWA installation component.
- Introduced a new local development proxy route for debugging purposes.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: refactor development scripts and remove Turbo configuration

- Updated the `dev` script in `package.json` to use a new startup sequence script for improved development workflow.
- Removed the outdated `turbo.json` configuration file as it is no longer needed.
- Introduced `devStartupSequence.mts` to manage the startup of Next.js and Vite processes concurrently.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 feat: update entry points and introduce debug proxy for local development

- Changed the main entry point in `index.html` from `entry.desktop.tsx` to `entry.web.tsx` for improved web compatibility.
- Added an `initialize.ts` file to enable `immer`'s `enableMapSet` functionality.
- Introduced a new `__DEBUG_PROXY__` variable in global types to support local development proxy features.
- Implemented a debug proxy route to facilitate local development with dynamic HTML injection and script handling.
- Removed outdated mobile routing components to streamline the codebase.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: replace BrowserRouter with RouterProvider for improved routing

- Updated entry points for desktop, mobile, and web to utilize RouterProvider and createAppRouter for better routing management.
- Removed the deprecated renderRoutes function in favor of a more streamlined router configuration.
- Enhanced router setup to support error boundaries and dynamic routing.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: remove direct access handling for SPA routes in proxy configuration

- Eliminated the handling of direct access to pre-rendered SPA pages in the proxy configuration.
- Simplified the request processing logic by removing checks for SPA routes, streamlining the middleware response flow.

Signed-off-by: Innei <tukon479@gmail.com>

* update

* 🔧 refactor: enhance Worker instantiation logic in mobile HTML template

* 🐛 fix: remove duplicate waitForPageWorkspaceReady calls in page CRUD e2e steps

* 🔧 refactor: simplify createTracePayload function by using btoa for base64 encoding

* 🔧 refactor: specify locales in import.meta.glob for dayjs and antd

* 🔧 refactor: replace Node.js Buffer with web-compatible btoa for base64 encoding in file upload

* 🐛 fix: disable consistent-type-imports rule for mdx files to prevent eslint crash

* 🔧 refactor: add height style to root div for consistent layout

* 🔧 refactor: replace btoa with Buffer for base64 encoding in trace and file upload handling

* 🔧 refactor: extract nextjsOnlyRoutes to a separate file for better organization

* 🔧 refactor: enable Immer MapSet plugin in tests for better state management

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: integrate sharedRollupOutput configuration and increase cache size for better performance

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove obsolete desktop.routes.test.ts file as it is no longer needed

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: use cross-env for env vars in npm scripts (Windows CI)

Co-authored-by: Cursor <cursoragent@cursor.com>

* 🔧 chore: update Dockerfile for web-only build and adjust npm scripts to use pnpm

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: enhance Dockerfile prebuild process with environment checks and add new dependencies

- Updated Dockerfile to include environment checks before removing desktop-only code.
- Added new dependencies in package.json: @aws-sdk/client-bedrock-runtime, @opentelemetry/auto-instrumentations-node, @opentelemetry/resources, @opentelemetry/sdk-metrics, and ajv.
- Configured Rollup to exclude @aws-sdk/client-bedrock-runtime from the SPA bundle.
- Introduced dockerPrebuild.mts script for environment variable validation and information logging.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: enhance Vite and Electron configurations with environment loading and trace encoding improvements

- Updated Vite and Electron configurations to load environment variables using loadEnv.
- Modified trace encoding in utils to use TextEncoder for better compatibility.
- Adjusted sharedRendererConfig to expose only necessary public environment variables.

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove plans directory (migrated to discussion)

* ♻️ refactor: inject NEXT_PUBLIC_* env per key in Vite define

Co-authored-by: Cursor <cursoragent@cursor.com>

*  feat: add loading screen with animation to enhance user experience

- Introduced a loading screen with a brand logo and animations for better visual feedback during loading times.
- Implemented CSS styles for the loading screen and animations in index.html.
- Removed the loading screen from the DOM once the layout is ready using useLayoutEffect in SPAGlobalProvider.

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove unnecessary external dependency from Vite configuration

- Eliminated the external dependency '@aws-sdk/client-bedrock-runtime' from the Vite configuration to streamline the build process for the SPA bundle.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: add web app manifest link in index.html and enable PWA support in Vite configuration

- Added a link to the web app manifest in index.html to enhance PWA capabilities.
- Enabled manifest support in Vite configuration for improved service worker functionality.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update link rel attributes for improved SEO and consistency

- Modified link rel attributes in multiple components to remove 'noreferrer' and standardize to 'nofollow'.
- Adjusted imports in PageContent components for better organization.

Signed-off-by: Innei <tukon479@gmail.com>

* update provider

*  feat: enhance loading experience and update package dependencies

- Added a loading screen with animations and a brand logo in index.html for improved user feedback during loading times.
- Introduced CSS styles for the loading screen and animations.
- Updated package.json files across multiple packages to include "@lobechat/const" as a dependency.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: update proxy

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove GlobalLayout and Locale components

- Deleted GlobalLayout and Locale components from the GlobalProvider directory to streamline the codebase.
- This removal is part of a refactor to simplify the layout structure and improve maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: clean up console logs and improve component structure

- Removed unnecessary console log statements from AgentForkTag components in both agent and community directories to enhance code cleanliness.
- Refactored UserAgentList component for better readability by restructuring the useUserDetailContext hook and adjusting the layout of Flexbox components.

Signed-off-by: Innei <tukon479@gmail.com>

* chore: remove console log from MemoryAnalysis component

* chore: update mobile HTML template with new asset links

- Replaced the previous asset links in the mobile HTML template with updated versions to ensure the latest resources are utilized.
- Adjusted the link rel attributes for module preloading to enhance performance and loading efficiency.

Signed-off-by: Innei <tukon479@gmail.com>

* fix: correct variable assignment in createClientTaskThread integration test

- Updated the assignment of the second parent message in the createClientTaskThread integration test to improve clarity and ensure proper data handling.
- Changed the variable name from 'secondParentMsg' to 'inserted' for better context before extracting the first message from the inserted results.

Signed-off-by: Innei <tukon479@gmail.com>

* refactor: simplify authentication check in define-config

- Removed the dependency on the isDesktop variable in the authentication check to streamline the logic.
- Enhanced the clarity of the redirection process for protected routes by focusing solely on the isLoggedIn status.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(dev): enhance local development setup with debug proxy instructions

- Added detailed instructions for starting the development environment in CLAUDE.md, including commands for SPA and full-stack modes.
- Updated README.md and README.zh-CN.md to reflect new commands and the debug proxy URL for local development.
- Introduced a Vite plugin to print the debug proxy URL upon server start, facilitating easier local development against the production backend.
- Corrected the debug proxy route in entry.web.tsx and define-config.ts for consistency.

This improves the developer experience by providing clear guidance and tools for local development.

Signed-off-by: Innei <tukon479@gmail.com>

* optimize perf

* optimize perf

* optimize perf

* remove speedy plugin

* add dayjs vendor

* Revert "remove speedy plugin"

This reverts commit bf986afeb1.

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-28 00:01:01 +08:00
YuTengjing 6c3e75634f 🐛 fix: prevent editor from stealing focus in agent settings modal (#12512) 2026-02-27 23:20:49 +08:00
Rdmclin2 9b8dabc072 🐛 fix: group agent rename problem (#12511)
* chore: align agent rename usage with agent profile editor

* fix: agent content emoji picker popupProps

* chore: agent and agent group use emoji background color in session list

* chore: remove fixed popupProps
2026-02-27 23:02:05 +08:00
eronez d286c1a9ad 🐛 fix: pass Google API key via x-goog-api-key header (#12506)
Fixes #12462
2026-02-27 21:32:28 +08:00
Zhijie He c2483f97a0 💄 style: add calculator builtin tool (#11715)
 feat: add calculator builtin tool
2026-02-27 21:10:39 +08:00
Rdmclin2 3152568c7d 🔨 chore: add back model select config (#12509)
* fix: user select problem

* feat: add back model config button

* chore: refact model detail props

* chore: add model detail loading

* chore: adjust dropdown side offset
2026-02-27 20:56:55 +08:00
sxjeru 02f2498140 💄 style: center active model on open in model switch panel (#12215)
* 🐛 fix: correct eslint command syntax in lint-ts.sh script

*  feat: add scroll functionality for active model in List component

*  feat: improve scroll behavior for active model in List component
2026-02-27 20:12:18 +08:00
Ruxiao Yin c9b243ca31 🐛 fix(docker): hoist @napi-rs/canvas for PDF parsing (#12475)
🐛 fix(docker): hoist @napi-rs/canvas for PDF parsing in Docker

Add `@napi-rs/canvas-*` to public-hoist-pattern in .npmrc to fix
`DOMMatrix is not defined` error when parsing PDFs in Docker.
2026-02-27 14:32:22 +08:00
YuTengjing 0ec6c2f38e feat(cloud): add Nano Banana 2 support (#12493) 2026-02-27 10:26:07 +08:00
Zhijie He c960705177 💄 style: add glm-5 & glm-4.6v & glm-image for zhipu (#12272)
* sytle: add glm-5 & glm-4.6v for zhipu

sytle: add glm-5 & glm-4.6v for zhipu

sytle: add glm-5 & glm-4.6v for zhipu

sytle: add glm-5 & glm-4.6v for zhipu

* fix: truncated response issue; implicitly set the default limit to 65536.
2026-02-27 09:19:31 +08:00
Arvin Xu c5d41fd2be feat: use lobe-tools to support progressive disclosure (#12489)
* activatedTool type

* support active tools

* fix explicitActivation tools issue

* improve search ux

* improve search result

* improve search result

* improve system prompts

* fix types

* fix tests

* refactor a skills store tools

* refactor a skills store tools

* improve issue

* fix some tests

* fix tests

* enable skills by default
2026-02-27 01:59:12 +08:00
Mike Lambert 22072789b6 🔨 chore(model-runtime): add User-Agent header for Anthropic API calls (#12433)
 feat(model-runtime): add User-Agent header for Anthropic API calls

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 00:49:54 +08:00
YuTengjing 306c50704e 🐛 fix: improve crawler error handling and timeout cancellation (#12487) 2026-02-26 22:59:10 +08:00
YuTengjing 0365a14e16 💄 style(cloud): remove deprecated chatgpt-4o-latest (#12486) 2026-02-26 18:39:22 +08:00
Rdmclin2 5fbf4b3cd4 🐛 fix: model select panel shiny problem and use normal list implementation (#12485)
fix: model select panel shiny problem and use normal list implementation
2026-02-26 16:52:04 +08:00
Arvin Xu b29a533285 ♻️ refactor: refactor client agent runtime (#12482)
* refactor to remove internal_fetchAIChatMessage

* improve

* fix tests
2026-02-26 15:17:39 +08:00
LobeHub Bot 7f7eeb8fa8 🌐 chore: translate non-English comments to English in src/app/[variants]/(main)/settings (#12430)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:09:26 +08:00
LobeHub Bot 059d0cc0fc 🌐 chore: translate non-English comments to English in model-runtime (#12480)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 12:30:20 +08:00
Arvin Xu 5a30c9a14f 🐛 fix: support memory tools run in server (#12471)
* support server memory run

* refactor builtin tools list

* refactor builtin tools list

* add lobe-tools

* fix lint
2026-02-25 22:13:02 +08:00
YuTengjing 5371507b22 🔥 refactor: remove invite code requirement feature (#12474) 2026-02-25 20:55:39 +08:00
YuTengjing f84a363b75 feat(cloud): add Seedream 5 Lite model (#12459) 2026-02-24 20:18:13 +08:00
Arvin Xu bbfbc45925 🔨 chore: update drizzle orm (#12458)
update drizzle orm
2026-02-24 19:14:41 +08:00
Arvin Xu 616ac9438c 🐛 fix: fix search issue (#12457)
* fix search issue

* fix tests
2026-02-24 17:21:26 +08:00
Arvin Xu ddce51eaba 🐛 fix: fix skill search not found (#12432)
* fix skill issue

* fix skills

* fix skills search query
2026-02-23 00:28:10 +08:00
YuTengjing bdc901d1dc 💄 style: video loading circular progress indicator (#12418) 2026-02-22 15:03:12 +08:00
LobeHub Bot 5ab874e877 🌐 chore: translate non-English comments to English in src/store (#12393)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 10:49:16 +08:00
Zhijie He 28b1455f2f 💄 style: update batch of model lists (ai360, hunyuan, intern, spark, stepfun, wenxin, seedream) (#12371) 2026-02-22 09:50:48 +08:00
Arvin Xu e95f7419b9 feat: support agent skills (#12424) 2026-02-22 09:48:11 +08:00
Arvin Xu 93bb83db5d 🔨 chore: improve version sync (#12422)
* update workflow

* update skills

* update skills
2026-02-22 01:09:23 +08:00
LobeHub Bot 01e13959d1 🌐 chore: translate non-English comments to English in src/features/Conversation (#12410)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:13:27 +08:00
lobehubbot ef0e4a6743 🐛 chore(hotfix): bump version to v2.1.33 [skip ci] 2026-02-21 16:01:41 +00:00
Arvin Xu 4d1508ee9b 👷 build: update auto tag release (#12421)
update
2026-02-22 00:00:53 +08:00
Arvin Xu 84ecc1e9f6 🚀 release 20260221 (#12420)
This release includes **82 commits** and **854 changed files**. Key updates are below.

### 🚀 New Features and Enhancements

- Added **Agent Benchmark** support for more systematic agent
performance evaluation.
- Introduced the **video generation** feature end-to-end, including
entry points, sidebar “new” badge support, and skeleton loading for
topic switching.
- Expanded memory capabilities: support for memory effort/tool
permission configuration and improved timeout calculation for memory
analysis tasks.
- Added desktop editor support for image upload via file picker.

### 🤖 Models and Provider Expansion

- Added a new provider: **Straico**.
- Added/updated support for:
  - Claude Sonnet 4.6
  - Gemini 3.1 Pro Preview
  - Qwen3.5 series
  - Grok Imagine (`grok-imagine-image`)
  - MiniMax 2.5
- Added related i18n copy and model parameter adaptations.

### 🖥️ Desktop Improvements

- Integrated `electron-liquid-glass` (macOS Tahoe).
- Improved DMG background assets and desktop release workflow.

### 🛠️ Stability, Security, and UX Fixes

- Fixed multiple video generation pipeline issues: precharge refund
handling, webhook token verification, pricing parameter usage, asset
cleanup, and type safety.
- Fixed `sanitizeFileName` path traversal risks and added unit tests.
- Fixed MCP media URL generation with duplicated `APP_URL` prefix.
- Fixed Qwen3 embedding failures caused by batch-size limits.
- Fixed multiple UI/interaction issues, including mobile header agent
selector/topic count, ChatInput scrolling behavior, and tooltip stacking
context.
- Fixed missing `@napi-rs/canvas` native bindings in Docker standalone
builds.
- Improved GitHub Copilot authentication retry behavior and response
error handling in edge cases.

### 🙏 Thanks to Committers

Huge thanks to these contributors (alphabetical):

@AmAzing129 @Coooolfan @Innei @ONLY-yours @Zhouguanyang @arvinxx
@eaten-cake @hezhijie0327 @nekomeowww @rdmclin2 @rivertwilight @sxjeru
@tjx666
2026-02-21 23:37:57 +08:00
Arvin Xu 093af9889d chore: merge main into canary (#12419)
## Summary

Merge latest changes from main into canary.

## Changes

- Merge branch main into canary via
codex/merge-main-into-canary-20260221
- Resolve one merge conflict in packages/model-bank/src/types/aiModel.ts
by keeping both ModelParamsSchema and VideoModelParamsSchema imports

## Summary by Sourcery

Update NVIDIA provider to support preserved thinking and
reasoning_content for new reasoning-capable chat models and bump package
version.

New Features:
- Add NVIDIA chat model entries for MiniMax-M2.1, DeepSeek V3.2,
GLM-4.7, GLM-5, and Kimi K2.5 with reasoning support.

Enhancements:
- Extend NVIDIA runtime payload handling to map reasoning to
reasoning_content and to translate thinking into either thinking or
enable_thinking/clear_thinking depending on model capabilities.
- Refine NVIDIA provider tests to cover GLM preserved thinking behavior
and reasoning_content conversion across models.

Build:
- Bump package version from 2.1.31 to 2.1.32.
2026-02-21 23:11:28 +08:00
arvinxx 508c3ae20c Merge branch 'main' into codex/merge-main-into-canary-20260221 2026-02-21 23:08:54 +08:00
Arvin Xu e7598fe90b feat: support agent benchmark (#12355)
* improve total

fix page size issue

fix error message handler

fix eval home page

try to fix batch run agent step issue

fix run list

fix dataset loading

fix abort issue

improve jump and table column

fix error streaming

try to fix error output in vercel

refactor qstash workflow client

improve passK

add evals to proxy

refactor metrics

try to fix build

refactor tests

improve detail page

fix passK issue

improve eval-rubric

fix types

support passK

fix type

update

fix db insert issue

improve dataset ui

improve run config

finish step limit now

add step limited

100% coverage to models

add failed tests todo

support interruptOperation

fix lint

improve report detail

improve pass rate

improve sort order issue

fix timeout issue

Update db schema

完整 case 跑通

update database

improve error handling

refactor to improve database

优化 test case 的处理流程

优化部分细节体验和实现

基本完成 Benchmark 全流程功能

优化 run case 展示

优化 run case 序号问题

优化 eval test case 页面

新增 eval test 模式

新增 dataset 页面

update schema

support

finish create test run

fix

update

improve import exp

refactor data flow

improve import workflow

rubric Benchmark detail 页面

improve import ux

update schema

finish eval home page

add eval workflow endpoint

implement benchmark run model

refactor RAG eval

implement backend

update db schema

update db migration

init benchmark

* support rerun error test case

* fix tests

* fix tests
2026-02-21 20:36:40 +08:00
Zhijie He c2280561f5 💄 style: add grok-imagine-image series support via Grok Imagine API (#12365) 2026-02-21 20:21:24 +08:00
Innei 9b692c239a ♻️ chore(ci): remove unnecessary fetch-depth: 0 from workflows (#12403)
Only 4 checkouts truly need full git history (tag operations, branch sync).
The remaining 18 occurrences were used in build/lint/test jobs that only
need the current commit. Also removed redundant fetch-tags: true where
fetch-depth: 0 already implies full tag fetch.
2026-02-21 19:20:28 +08:00
LobeHub Bot e32a2fbad4 🌐 chore: translate non-English comments to English in src/libs/oidc-provider (#12383)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 10:27:45 +08:00
lobehubbot cb688b6cfa 🐛 chore(hotfix): bump version to v2.1.32 [skip ci] 2026-02-21 02:23:26 +00:00
Hardy ef474afe84 feat(nvidia): add new models and simplify payload handling for NVIDIA NIM (#12333)
*  feat(nvidia): add interleaved thinking support with new reasoning models

- Add MiniMax-M2.1, DeepSeek V3.2, GLM-4.7, GLM-5, Kimi K2.5 to NVIDIA model bank
- Add reasoning conversion for interleaved thinking mode
- Add tests for GLM-5 and DeepSeek V3.2 thinking

* ♻️ refactor(nvidia): simplify payload handling logic

- Remove redundant THINKING_MODELS and INTERLEAVED_THINKING_MODELS sets
- Apply reasoning -> reasoning_content conversion for all NVIDIA models
- Apply thinking -> chat_template_kwargs conversion based on user input only
- Let API decide if model supports the parameters instead of client-side filtering

* ♻️ refactor(nvidia): add preserved thinking support with model-specific params
2026-02-21 10:20:04 +08:00
YuTengjing 7a1e2b6a48 👷 chore: enable consistent-type-imports ESLint rule (#12399)
👷 chore: enable consistent-type-imports ESLint rule and fix violations
2026-02-21 10:11:38 +08:00
lobehubbot cc926f252a Merge remote-tracking branch 'origin/main' into canary 2026-02-21 02:01:38 +00:00
Arvin Xu 4260474f4e 👷 build: add build prefix to auto-tag release trigger (#12406) 2026-02-21 09:54:48 +08:00
LobeHub Bot ed4c5d125e test: add unit tests for QueueService.calculateDelay (#12356)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 09:49:54 +08:00
sxjeru 086dd15add 💄 style: add Claude Sonnet 4.6 model and enhance adaptive thinking logic (#12375)
 feat: add Claude Sonnet 4.6 model and enhance adaptive thinking logic
2026-02-21 09:49:24 +08:00
Zhijie He 1e506c5ebb 💄 style: add qwen3.5 series support (#12364)
style: add `qwen3.5` series support
2026-02-21 09:48:58 +08:00
Coooolfan b799c98487 🐛 fix(mcp): fix double APP_URL prefix in image/audio content URLs (#12400)
contentBlocksToString() was prepending APP_URL via urlJoin() to item.data,
but item.data already contained the full URL after processContentBlocks()
uploaded to S3. This caused URLs like:
https://example.com/https://example.com/f/uuid

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 09:37:04 +08:00
sxjeru 5475188fa0 💄 style: add Gemini 3.1 Pro Preview model (#12392)
*  feat: 添加 Gemini 3.1 Pro Preview 模型及其相关参数

*  feat: 更新 Anthropic 模型

*  feat: 移除过时的 Gemini 2.5 Flash 和 Imagen 4 预览模型

*  feat: 添加 Qwen3 Coder Next 模型并更新 Anthropic 测试用例中的模型版本
2026-02-21 09:35:10 +08:00
Zhijie He 0674eee0d4 🐛 fix: fix qwen3 embedding error due to batch size limitation (#12382)
fix: fix embdding chunk_size limit for qwen

apply suggestion

Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

fix: fix embdding chunk_size limit for qwen

fix: fix embdding chunk_size limit for qwen
2026-02-21 01:15:14 +08:00
lobehubbot 870638ea8a Merge remote-tracking branch 'origin/main' into canary 2026-02-20 16:44:44 +00:00
Arvin Xu b155656a7c 👷 build: add benchmark db schema (#12402)
* add eval benchmark database

* fix types

* remove regions
2026-02-21 00:44:06 +08:00
lobehubbot d8b947828c 🐛 chore(hotfix): bump version to v2.1.31 [skip ci] 2026-02-20 13:04:35 +00:00
lobehubbot 8310c5755c Merge remote-tracking branch 'origin/main' into canary 2026-02-20 13:04:23 +00:00
YuTengjing bb2d760b4b 💄 style: support more Qwen i2i & t2i models (#11708)
#### 💻 Change Type

<!-- For change type, change [ ] to [x]. -->

- [ ]  feat
- [ ] 🐛 fix
- [ ] ♻️ refactor
- [X] 💄 style
- [ ] 👷 build
- [ ] ️ perf
- [ ]  test
- [ ] 📝 docs
- [ ] 🔨 chore

#### 🔗 Related Issue

Closes #10346

<!-- Link to the issue that is fixed by this PR -->

<!-- Example: Fixes #xxx, Closes #xxx, Related to #xxx -->

#### 🔀 Description of Change

1. 更新的文生图,图生图模型列表,`z-image` `wan2.5` `wan2.6` `qwen-image-plus/max`
`qwen-image-edit-plus/max`
2. 新增 `image2image` endpoint,为老版本图生图模型进行兼容
3. 默认使用 `multimodal-generation` endpoint(新模型目前调研下来都是用这个了,同时支持图生图和文生图)
4. 支持多区域 Dashscope URL,跟随 baseUrl 参数,自动切分 `/compatible-mode/v1` 默认北京区域
    北京 https://dashscope.aliyuncs.com
    新加坡 https://dashscope-intl.aliyuncs.com
    弗吉尼亚 https://dashscope-us.aliyuncs.com

|Endpoint||
|-|-|
|`multimodal-generation`|<img width="826" height="547" alt="image"
src="https://github.com/user-attachments/assets/38206851-94bc-48cc-8a57-24ed7155782f"
/><img width="521" height="383" alt="image"
src="https://github.com/user-attachments/assets/40fe0ed0-35fd-443d-868f-3ae2c27352f9"
/><img width="681" height="557" alt="image"
src="https://github.com/user-attachments/assets/8101b0f1-81c8-4892-a6e2-b51b5b0e0235"
/>|
|`text2image`|<img width="600" height="564" alt="image"
src="https://github.com/user-attachments/assets/39e82a4f-5305-4f30-ae4d-e5339f401e6d"
/>|

<!-- Thank you for your Pull Request. Please provide a description
above. -->

#### 🧪 How to Test

<!-- Please describe how you tested your changes -->

<!-- For AI features, please include test prompts or scenarios -->

- [ ] Tested locally
- [ ] Added/updated tests
- [ ] No tests needed

#### 📸 Screenshots / Videos

<!-- If this PR includes UI changes, please provide screenshots or
videos -->

| Before | After |
| ------ | ----- |
| ...    | ...   |

#### 📝 Additional Information

ref: https://help.aliyun.com/zh/model-studio/newly-released-models
ref:
https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=2987148

<!-- Add any other context about the Pull Request here. -->

<!-- Breaking changes? Migration guide? Performance impact? -->

## Summary by Sourcery

Extend Qwen image generation support to cover new text-to-image and
image-to-image models while routing legacy models via dedicated
text2image/image2image endpoints and defaulting other models to the
multimodal-generation API.

New Features:
- Add model metadata and configuration for new Qwen image models
including Z-Image Turbo, Qwen Image Edit Max/Plus, Qwen Image Max/Plus,
and Wanxiang 2.5/2.6 variants.
- Introduce explicit handling of legacy text-to-image and image-to-image
Qwen models via separate async text2image and image2image endpoints.

Enhancements:
- Update the Qwen image creation flow to prefer the
multimodal-generation endpoint for newer models and improve error
messaging and logging across image workflows.
- Reformat select Qwen chat model descriptions for consistency without
changing behavior.

Tests:
- Adjust Qwen image creation tests to align with the new
multimodal-generation behavior and removed strict input validation on
qwen-image-edit-specific image URL requirements.
2026-02-20 21:03:45 +08:00
Sun13138 a9d9e7adf0 🐛 fix: correct mobile header agent selector and topic count (#12204) 2026-02-20 21:02:53 +08:00
YuTengjing e28593cc38 feat: add Gemini 3.1 Pro Preview model support (#12391)
- Add gemini-3.1-pro-preview to Google, Vertex AI, and LobeHub providers
- Add thinkingLevel3 extend param type (low/medium/high)
- Create ThinkingLevel3Slider component for 3-level thinking control
- Fix thinkingLevel2/3 not passing values to API due to form field name mismatch
- Add medium to GoogleThinkingLevel and ChatStreamPayload thinkingLevel types
- Update planCardModels to use gemini-3.1-pro-preview
2026-02-20 09:57:20 +08:00
sxjeru e51234443a feat: Add new provider Straico (#12219)
 feat: add Straico model provider integration and environment variable support
2026-02-19 20:02:07 +08:00
LobeHub Bot a0c4baf1aa 🌐 chore: translate non-English comments to English in src/libs (#12353)
* 🌐 chore: translate non-English comments to English in src/libs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* 🐛 fix: update test to match translated error message

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: arvinxx <arvinx@foxmail.com>
2026-02-18 23:47:49 +08:00
LobeHub Bot 1e20edef3d 🌐 chore: translate non-English comments to English in plugin slice (#12367)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 22:28:30 +08:00
Ruxiao Yin 949873adbc 🐛 fix(docker): Fix the issue of missing @napi-rs/canvas platform native binding package in standalone build (#12370)
🐛 fix(docker): include platform-specific @napi-rs/canvas bindings in standalone output
2026-02-18 22:27:58 +08:00
LobeHub Bot 5030177f3d 🌐 chore: translate non-English comments to English in apps/desktop/src/main (#12376)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-18 22:15:03 +08:00
Zhijie He a96a5576a7 fix: force fix style 2026-02-18 20:54:53 +08:00
Zhijie He 9766d20979 fix: fix some bugs 2026-02-18 20:51:34 +08:00
Zhijie He 6a4118c628 feat: support switch dashscope URL based on baseURL
feat: support switch dashscope URL based on baseURL

feat: support switch dashscope URL based on baseURL

feat: support switch dashscope URL based on baseURL
2026-02-18 20:36:23 +08:00
Zhijie He 4fba491793 fix: fix multimodal-generation I2I calling
fix: fix wan2.6-image imageUrls num limit

fix: fix wan2.6-image imageUrls format
2026-02-18 20:36:23 +08:00
Zhijie He ea951b72d3 style: fix height & weight range (apply suggestion 5) 2026-02-18 20:36:23 +08:00
Zhijie He 4a29904516 chore: add imageUrl & imageUrls input validation (apply suggestion 3) 2026-02-18 20:36:23 +08:00
Zhijie He 5d7ee01bce fix: fix edit & i2i models input muti-images 2026-02-18 20:36:23 +08:00
Zhijie He 763352c56a chore: apply some suggestion (1,4,6)
chore: apply some suggestion (1,4,6)
2026-02-18 20:36:23 +08:00
Zhijie He 86624e8808 feat: improve error messages with model names for better debugging 2026-02-18 20:36:23 +08:00
Zhijie He f018a7b790 sytle: update default models 2026-02-18 20:36:23 +08:00
Zhijie He 5f10ede77a sytle: add i2i models 2026-02-18 20:36:23 +08:00
Zhijie He 9312298564 feat: support t2i mode in multimodal-generation endpoint 2026-02-18 20:36:23 +08:00
Zhijie He 6f08c302ac chore: minor code 2026-02-18 20:36:23 +08:00
Zhijie He 0aea1b0a3d feat: update Qwen image models info and code formatting 2026-02-18 20:36:23 +08:00
Zhijie He bf9df0424d feat: add model routing logic with multimodal-generation as default 2026-02-18 20:36:23 +08:00
Zhijie He 989223b355 feat: add Qwen image2image endpoint support and refactor image generation functions 2026-02-18 20:36:23 +08:00
Zhijie He d9f91fc23a Update qwen.ts 2026-02-18 20:36:23 +08:00
YuTengjing 38e1adba2f 🌐 chore: add i18n translations for Claude Sonnet 4.6 (#12374) 2026-02-18 10:13:44 +08:00
YuTengjing f91acfca95 feat(model-bank): lobehub provider add Claude Sonnet 4.6 support (#12373)
Add Claude Sonnet 4.6 model card to lobehub provider and extend
assistant turn prefill restriction to all 4.6 models.
2026-02-18 10:07:43 +08:00
Innei 1f1c49fc52 🐛 fix(tool-ui): fix icon margin and text overflow in FilePathDisplay (#12331)
* 🐛 fix(tool-ui): fix icon margin and text overflow in FilePathDisplay

Fixes LOBE-2541

* 🐛 fix(DragUploadZone): remove border-radius from upload overlay
2026-02-16 21:02:20 +08:00
AmAzing- 8db783b5b8 🔨 chore: add MiniMax 2.5 (#12345)
chore: add mini max 2.5
2026-02-16 19:40:39 +08:00
Innei b3e87f6cd4 ♻️ refactor: replace per-item Editing components with singleton EditingPopover (#12327)
* ♻️ refactor: replace per-item Editing components with singleton EditingPopover

Eliminate 3 duplicate Editing components (AgentItem, AgentGroupItem, Group)
in favor of a single imperative EditingPopover using @lobehub/ui Popover atoms.
Anchor elements are passed via React state (useState + callback ref) instead
of DOM queries. Removes agentRenamingId/groupRenamingId from homeStore.

* fix: edit group agent avaar

Signed-off-by: Innei <tukon479@gmail.com>

*  test(e2e): update rename popover selectors and allow console in tests

Support both antd Popover and @lobehub/ui Popover atoms selectors.
Use save button click instead of click-outside for non-Enter rename flow.
Disable no-console rule for e2e and test files.

*  test(e2e): fix rename popover input detection with data-testid

Add data-testid="editing-popover" to PopoverPopup. Simplify inputNewName
to use single combined selector instead of sequential try-catch loop that
caused 8s+ timeout. Support both @lobehub/ui and antd Popover.

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-16 18:17:59 +08:00
lobehubbot abbf53feda Merge remote-tracking branch 'origin/main' into canary 2026-02-16 06:44:35 +00:00
Arvin Xu 3f432a43d8 🐛 fix(ci): use pull_request_target to support fork PR secrets (#12350)
🐛 fix(ci): use pull_request_target to support fork PR secrets
2026-02-16 14:43:58 +08:00
Innei 8b0d1ec9e3 👷 ci: improve canary versioning to patch+1 sequential numbering (#12347)
Change canary version format from X.(Y+1).0-canary.TIMESTAMP to
X.Y.(Z+1)-canary.N with auto-incrementing sequence. Also add
release prefix as build trigger and update sync PR titles.
2026-02-16 00:49:33 +08:00
Innei b43cbae2e1 🐛 fix: remove isDesktop guard from client fetch switch visibility (#12336)
Allow desktop app users to see and toggle the "Fetch on Client" option
in provider settings. The server-side default values for desktop remain
unchanged, so this only affects UI visibility.
2026-02-16 00:48:02 +08:00
Arvin Xu d2a042cd95 🐛 fix: scroll ChatInput into view when starter mode activates (#12334)
* 🐛 fix: scroll ChatInput into view when starter mode activates

When clicking Create Agent/Group/Write, the SuggestQuestions panel
renders below the ChatInput and pushes total content beyond the
viewport, causing the ChatInput to scroll out of view. This adds
scrollIntoView + focus on mode change so the editor stays visible
and ready for input. Also improves E2E test to target contenteditable
inside ChatInput directly and wait for animation to settle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:06:14 +08:00
lobehubbot c1916a1996 Merge remote-tracking branch 'origin/main' into canary 2026-02-15 13:28:56 +00:00
sxjeru 502d94bd4c 💄 style: add new MiniMax-M2.5 model (#12289)
* feat: 添加多个新模型及其定价信息,更新模型解析配置

* fix: 更新多个模型的导入语法,添加新模型GLM-5及其属性

* feat: 添加多个Doubao模型及其定价信息,优化payload处理逻辑
2026-02-15 21:28:16 +08:00
YuTengjing f4bd332d11 🐛 fix: use batch config for computePriceParams and pass latency (#12348)
## Summary
- Pass latency (task submission → webhook callback) to
`chargeAfterGenerate` for video generation metrics
- Use `batch.config` instead of webhook `result` for
`computePriceParams` (generateAudio), ensuring pricing uses
user-submitted config
- Rename `generateAudio` top-level param to `computePriceParams: {
generateAudio }` for better structure

## Test plan
- [ ] Submit a video generation task with generateAudio enabled, verify
charge uses correct pricing
- [ ] Submit a video generation task with generateAudio disabled, verify
charge reflects the difference
- [ ] Verify latency is recorded in the charge metrics

## Summary by Sourcery

Adjust video generation charging and metadata to rely on batch config
and record end-to-end latency.

Bug Fixes:
- Ensure generateAudio pricing uses the original batch configuration
instead of the webhook result payload.

Enhancements:
- Pass task submission-to-webhook latency into video charging for
improved metrics.
- Rename chargeAfterGenerate pricing input to computePriceParams for
clearer structure and future extensibility.
- Name generated video files using the batch prompt prefix when
available to improve asset identification.

Tests:
- Add unit tests for the Volcengine video provider request/response
handling, including payload mapping, client config, and error cases.
- Add initial test scaffolding for video standard parameters, cost
computation, single-price resolution, and Volcengine video webhook
handling.
2026-02-15 20:46:29 +08:00
YuTengjing 7df81ffaa1 🐛 fix: add sanitizeFileName alias to vitest config for test resolution 2026-02-15 20:32:24 +08:00
YuTengjing ed076b3cf5 test: add unit tests for sanitizeFileName utility 2026-02-15 20:13:22 +08:00
YuTengjing 0abde1623d 🐛 fix: extract sanitizeFileName util to prevent path traversal in generated file names 2026-02-15 20:03:04 +08:00
YuTengjing 481e5c0066 🐛 fix: sanitize prompt for video file name to prevent path traversal 2026-02-15 20:00:41 +08:00
YuTengjing 8719354282 🐛 fix: use batch config for computePriceParams instead of webhook result 2026-02-15 19:47:10 +08:00
YuTengjing 5957bd4578 🐛 fix: use prompt as video file name instead of generation id 2026-02-15 19:34:14 +08:00
YuTengjing 335e246ac5 test: add unit tests for video generation feature
Cover resolveVideoSinglePrice, computeVideoCost, handleCreateVideoWebhook,
createVolcengineVideo, and video standard-parameters (63 cases total).
2026-02-15 19:34:14 +08:00
YuTengjing 918d048b3d feat: pass latency to chargeAfterGenerate for video generation metrics 2026-02-15 19:32:19 +08:00
Arvin Xu 398d8b7f3c Sync main branch to canary branch (#12339)
Automatic sync from main to canary. Merge conflicts detected.

**Resolution steps:**
```bash
git fetch origin
git checkout sync/main-to-canary-20260214-22020422229
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.
2026-02-15 18:52:05 +08:00
Innei 1529f31dff ci: sync workflow for main to canary branch
- Added concurrency control to prevent overlapping sync jobs.
- Improved logic for detecting changes between main and canary branches.
- Streamlined handling of fast-forward merges and conflict resolution.
- Updated PR creation process for manual conflict resolution with detailed instructions.

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-15 16:36:16 +08:00
Innei b4bd5b288c Merge remote-tracking branch 'origin/main' into sync/main-to-canary-20260214-22020422229 2026-02-15 16:03:47 +08:00
YuTengjing 448cfb2cfd 🐛 fix: prevent stale topic ID from persisting in URL on remount (#12341)
## Summary
- Fix bidirectional sync race condition in `TopicUrlSync` where stale
`activeGenerationTopicId` from zustand store overwrites the URL when
navigating back to image/video pages without a `?topic=` param
- Replace `createStoreUpdater` (useEffect-based) with `useLayoutEffect`
for URL → store sync, ensuring it runs before the store → URL
subscription
- Generate latest i18n locales

## Test plan
- [ ] Visit `/video?topic=<id>`, navigate to home, click `/video` —
topic should not persist
- [ ] Visit `/image?topic=<id>`, navigate to home, click `/image` —
topic should not persist
- [ ] Click a topic in the sidebar — URL should update with
`?topic=<id>`
- [ ] Refresh page with `?topic=<id>` — correct topic should be selected

## Summary by Sourcery

Fix URL and store synchronization for generation topics and refresh
localized video page translations.

Bug Fixes:
- Prevent stale activeGenerationTopicId values in the store from
overwriting the URL when remounting image or video pages without a topic
query parameter.

Documentation:
- Regenerate and update i18n locale JSON files across all supported
languages, including new video translation files.
2026-02-15 11:11:47 +08:00
YuTengjing d8c3ef3232 🌐 chore: generate latest i18n locales 2026-02-15 11:10:47 +08:00
YuTengjing 2a23fb9a10 🐛 fix: prevent stale topic ID from persisting in URL on remount 2026-02-15 11:03:06 +08:00
YuTengjing 82f9cb4486 🔨 chore: add video generation feature (#12312)
## Summary
- Add complete video generation feature including UI pages, store
management, server routes, and webhook handling
- Support Volcengine video generation provider with text-to-video and
image-to-video capabilities
- Add video generation topic management, config panel, generation feed,
and prompt input components
- Include database migration for video generation schema
- Refactor GenerationTopicList/TopicPanel as shared components for both
image and video generation

## Test plan
- [ ] Verify video generation page renders correctly
- [ ] Test text-to-video generation flow end-to-end
- [ ] Test image-to-video generation with reference frames
- [ ] Verify video generation topic CRUD operations
- [ ] Confirm webhook handling for async video generation results
- [ ] Check video generation config panel model selection and parameter
controls
2026-02-15 10:13:43 +08:00
YuTengjing 6419fd32b1 🔧 chore: revert some lint config 2026-02-15 09:46:02 +08:00
Arvin Xu 927fe3fd22 ci: fix sync workflow by using PAT for checkout (#12338)
The GITHUB_TOKEN cannot push changes to .github/workflows/ files due to
GitHub's security restrictions. The 'workflows' permission key added in
the previous commit is not a valid workflow permission scope.

Fix: Use secrets.GH_TOKEN (PAT with workflow scope) in the checkout step
so that git push has the necessary credentials to push branches that
contain workflow file changes (e.g. from merge conflicts).

Also reverts the invalid 'workflows: write' permission.
2026-02-15 00:13:13 +08:00
Arvin Xu 03bda41c07 chore(ci): add workflows permission to sync-main-to-canary (#12337)
When merge conflicts involve .github/workflows/ files, GitHub requires
the `workflows: write` permission to push branches containing workflow
file changes. Without this permission, the sync branch push is rejected
with 'refusing to allow a GitHub App to create or update workflow without
workflows permission'.
2026-02-14 23:56:07 +08:00
LobeHub Bot e804773a7e 🌐 chore: translate non-English comments to English in lobehub-skill-store (#12316)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 22:22:40 +08:00
YuTengjing 67875bd60a feat: add skeleton loading screen for video topic switching 2026-02-14 21:07:01 +08:00
YuTengjing 916d4841f4 🔒 chore: hide video nav entry behind enableBusinessFeatures flag 2026-02-14 20:52:03 +08:00
YuTengjing d5b1ff20e0 🔧 chore: revert locales/ changes and remove unused locale key
- Revert locales/ directory to canary state (CI auto-generates translations)
- Remove unused `video.topic.empty` key from default locale
2026-02-14 20:16:26 +08:00
YuTengjing 212348eafe 🐛 fix: refund precharge when video task submission fails 2026-02-14 20:16:26 +08:00
YuTengjing 43820eeb2e 🐛 fix: improve video generation security and type safety
- Reject webhook requests when token is missing instead of skipping verification
- Use proper union type for generation assets instead of blanket VideoGenerationAsset cast
- Replace sync js-sha256 with Node.js crypto.createHash for video hashing
- Add 500MB size limit and 5-minute timeout for video downloads
- Add displayName to VideoLoading component
2026-02-14 20:16:26 +08:00
YuTengjing 7397d6f8c1 🐛 fix: add type assertion for prechargeResult in video webhook route 2026-02-14 20:16:26 +08:00
YuTengjing 85d5bc9e08 ♻️ refactor: add model parameter to video free quota query 2026-02-14 20:16:26 +08:00
YuTengjing 3fbc46b23a 🔒 feat: add webhook token verification for video generation callbacks
Generate a one-time crypto-random token per video task, store it in
asyncTask metadata, and append it to the callback URL. The webhook
endpoint verifies the token using timing-safe comparison before
processing, returning 401 on mismatch. Old tasks without a token
are allowed through for backward compatibility.
2026-02-14 20:16:26 +08:00
YuTengjing 6e2ef05270 feat: support new badge on sidebar nav items and enable for video 2026-02-14 20:16:26 +08:00
YuTengjing 06d65e9ce5 🙈 chore: hide Seedance 2.0 entry from home starter list
Temporarily comment out the Seedance 2.0 video button until it's ready for launch. Seedance 1.5 will be released first.
2026-02-14 20:16:26 +08:00
YuTengjing 6002863c17 feat: add video free quota query endpoint and UI stub
Add getVideoFreeQuota TRPC query and business stub for displaying
daily free video generation quota in PromptInput.
2026-02-14 20:16:26 +08:00
YuTengjing 8f2e72d1b8 🐛 fix: update generationBatch tests to match filesToDelete refactor 2026-02-14 20:16:26 +08:00
YuTengjing fcf2444fa8 feat: add eval method to RedisClient for Lua script execution 2026-02-14 20:16:25 +08:00
YuTengjing 661f1a80b4 ♻️ refactor: improve video generation webhook and type safety
- Make AsyncTaskModel.findByInferenceId static to avoid empty userId
- Extract GenerationTopicType for type-safe topic type narrowing
- Hoist batch query to eliminate duplicate DB call in webhook handler
- Add missing i18n keys for video error actions and status
- Fix comment accuracy in generationBatch deletion
2026-02-14 20:16:25 +08:00
YuTengjing 57772d1f3b 🐛 fix: add videoGeneration pricing unit and clean up all asset files on deletion
- Add videoGeneration to PricingGroup maps to fix TS2741 type errors
- Collect url and coverUrl in addition to thumbnailUrl when deleting topics/batches
- Fix createTopic test assertion to match new optional type parameter
2026-02-14 20:16:25 +08:00
YuTengjing abe4c969a5 feat: add video generation feature 2026-02-14 20:16:25 +08:00
lobehubbot b767a66d38 🐛 chore(hotfix): bump version to v2.1.30 [skip ci] 2026-02-14 11:03:50 +00:00
Innei 53e4228ea7 🐛 fix: hotfix v2.1.30 (#12321)
*  chore: enhance release workflow to include conditional release body handling

- Added environment variable `RELEASE_BODY` to capture release notes from the GitHub event.
- Updated the workflow to use this variable, ensuring proper handling of release body content during manual dispatch events.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: simplify GitHub release workflow by removing hotfix-specific logic

- Consolidated the GitHub release creation step to handle both regular and hotfix releases under a single condition.
- Removed the separate hotfix release creation step to streamline the workflow.

Signed-off-by: Innei <tukon479@gmail.com>

*  fix: replace UserPanel popover

- Introduced `PanelContentSkeleton` for better user experience during loading states in the UserPanel.
- Updated `UserPanel` to use the new skeleton and adjusted popover content handling.
- Refactored `PanelContent` to use `FC` type for better type safety.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: bump @lobehub/ui dependency to version 4.38.1

- Updated the @lobehub/ui package to the latest version for improved features and bug fixes.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 refactor: remove inset prop from Popover in UserPanel

- Cleaned up the Popover component in UserPanel by removing the unnecessary inset prop for improved clarity and maintainability.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-14 19:03:00 +08:00
Innei 7efcdd2f7c 🐛 fix: resolve tooltip z-index stacking context in ModelSwitchPanel (#12324)
🐛 fix: move TooltipGroup to panel root to fix z-index stacking context
2026-02-14 18:49:23 +08:00
Neko bde1503309 🔨 chore(memory-user-memory): support effort & tool permission for configuring memory (#12311) 2026-02-14 18:17:15 +08:00
Neko 487713361a 🔨 chore(userMemories): support to auto calculate the timeout of the memory analysis task (#12325) 2026-02-14 17:36:58 +08:00
LiJian 7bad876259 🐛 fix: slove the execAgent task run error & parse crash problem (#12318)
fix: slove the execAgent task run error & parse crash problem
2026-02-14 14:22:33 +08:00
lobehubbot 229200853a 🐛 chore(hotfix): bump version to v2.1.29 [skip ci] 2026-02-14 02:09:17 +00:00
Innei 5ec89941f3 🐛 fix: bump lobehub/ui and fix workflow (#12313)
## 🩹 Hotfix v2.1.29

This PR starts a hotfix release from `main`.

### Release Process
1.  Hotfix branch created from main
2.  Pushed to remote
3. 🔄 Waiting for PR review and merge
4.  Auto tag + GitHub Release will be created after merge

---
Created by hotfix script

## Summary by Sourcery

Improve main-to-canary sync workflow robustness and tighten hotfix
auto-tagging criteria for release automation.

Enhancements:
- Make the main-to-canary sync workflow attempt direct merges to canary,
falling back to PR creation only when necessary or when conflicts occur,
and handle existing sync PRs more gracefully.
- Refine hotfix detection in the auto-tag workflow by requiring both a
hotfix branch prefix and a valid conventional commit-style PR title
prefix before tagging.
- Update the @lobehub/ui dependency to the latest patch version.

Build:
- Adjust release auto-tag workflow logic to gate hotfix tagging by both
branch naming and PR title format.

CI:
- Enhance GitHub Actions workflow for syncing main to canary with
conflict handling, direct-push optimization, and automated PR
management.
2026-02-14 10:08:31 +08:00
Innei f46916a74d feat(desktop): integrate electron-liquid-glass for macOS Tahoe (#12277)
*  feat(desktop): integrate electron-liquid-glass for macOS Tahoe

Add native liquid glass visual effect on macOS 26+ (Tahoe), replacing
vibrancy with Apple's NSGlassEffectView API via electron-liquid-glass.

- Centralize all platform visual effects in WindowThemeManager
- Strip platform props from BrowserWindow options to prevent config leaking
- Remove vibrancy from appBrowsers/WindowTemplate (managed by ThemeManager)
- Add isMacTahoe detection in env.ts and preload
- Fix applyVisualEffects to handle macOS platform symmetrically

* fix(tests): add isMacTahoe detection in Browser test environment

Introduce isMacTahoe flag in the test environment to support macOS Tahoe-specific features. This change enhances the test suite's ability to simulate and validate platform-specific behavior.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(theme): update liquid glass variant and adjust background color mix for desktop themes

- Changed liquid glass variant from 2 to 15 for improved visual effects.
- Adjusted background color mix percentages for dark and light themes on desktop to enhance visual consistency.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat(theme): adjust background color mix for dark theme on desktop

- Updated the background color mix percentage for the dark theme on desktop from 70% to 90% for improved visual effect consistency.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-14 00:31:16 +08:00
Arvin Xu 2ee46b8693 Sync main branch to canary branch (#12308)
Automatic sync

## Summary by Sourcery

Extend database schemas and migrations to support async task inference
tracking and typed generation topics.

New Features:
- Add an inferenceId field to async tasks with a dedicated index for
lookup by inference ID.
- Add a typed generation topic field to distinguish between image and
video topics with a default of image.

Enhancements:
- Update database schema metadata and documentation snapshots to reflect
the new async task and generation topic fields.

Tests:
- Adjust async task and generation topic tests to cover the new
inferenceId and topic type fields.
2026-02-13 23:44:28 +08:00
Innei baf0b56f64 🔧 ci: optimize sync-main-to-canary to merge directly when no conflicts (#12306)
## Summary
- Optimize the main-to-canary sync workflow to directly merge and push
when there are no conflicts, avoiding unnecessary PR creation
- When merge conflicts exist, fall back to creating a PR for manual
resolution
- Add duplicate PR detection to prevent multiple PRs on the same day

## Test plan
- [ ] Push to main with no conflicts on canary → should auto-merge
without PR
- [ ] Push to main with conflicts on canary → should create PR
- [ ] Trigger workflow twice on same day with conflicts → should not
create duplicate PRs

## Summary by Sourcery

CI:
- Update the sync-main-to-canary workflow to merge main into canary
directly on no-conflict updates, only creating a PR when merge conflicts
occur and avoiding duplicate PRs for the same sync date.
2026-02-13 17:39:19 +08:00
YuTengjing 12dc7f90be 👷 build: add video generation schema changes (#12293) 2026-02-13 17:13:16 +08:00
Innei 1b905ede31 Sync main branch to canary branch (#12297)
Automatic sync
2026-02-13 15:50:44 +08:00
Innei cfaa911153 🔧 ci: add commit prefix gate for hotfix auto-tag (#12304)
* 🔧 ci: add commit prefix gate for hotfix auto-tag

* 🔧 chore: update ESLint suppressions and dependencies

- Added new ESLint suppressions for various files to address linting issues, including `no-console` and `object-shorthand`.
- Updated ESLint version to 10.0.0 in both root and desktop package.json files.
- Adjusted linting scripts for improved performance and consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: add ESLint support for YAML files in package.json

- Included ESLint fix command for YAML files (*.yml, *.yaml) in the linting scripts section.
- Ensured consistent formatting by adding a newline at the end of the file.

This update enhances linting capabilities for YAML configuration files.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: remove ESLint configuration file

- Deleted the .eslintrc.js file, which contained custom ESLint rules and overrides.
- This change simplifies the project by relying on default ESLint configurations.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-13 15:49:33 +08:00
Innei e17bf4b0cc Merge remote-tracking branch 'origin/main' into canary
# Conflicts:
#	locales/ja-JP/suggestQuestions.json
#	locales/vi-VN/models.json
2026-02-13 15:43:49 +08:00
LiJian 58cf27dcf8 🐛 fix: add the Agent Meta info back into agent advance model (#12302)
fix: add the Agent Meta info back into agent advance model
2026-02-13 15:01:50 +08:00
YuTengjing f12d9fbd22 🔒 fix: upgrade next-mdx-remote to v6 for CVE-2026-0969 (#12296)
Upgrade next-mdx-remote from v5.0.0 to v6.0.0 to fix CVE-2026-0969,
an arbitrary code execution vulnerability in MDX content processing.
2026-02-13 13:36:41 +08:00
YuTengjing d4f72eb752 🐛 fix: add missing inferenceId and type fields in test mocks 2026-02-13 12:34:19 +08:00
YuTengjing e112cd6f7f 🗃️ db: add video generation schema changes
- async_tasks: add inference_id column with index
- generation_topics: add type column (default 'image')
2026-02-13 12:09:18 +08:00
Innei 9a9147ca7e feat: support image upload in editor with desktop file picker (#12285)
- Add handleShowOpenDialog and handlePickFile IPC methods for Electron
- Create useImageUpload hook for editor image upload with progress
- Refactor ReactImagePlugin config to support handleUpload and onPickFile
- Simplify slash command image insertion by delegating upload to plugin
- Upgrade @lobehub/editor to ^3.16.1
2026-02-13 01:27:22 +08:00
Innei 2d1eec4482 feat(desktop): configure DMG background image (#12284)
*  feat: configure DMG background image for macOS installer

*  feat(desktop): set DMG window size, icon positions, and retina DPI

* 🔧 chore(desktop): resize DMG background to 600x400 and adjust window/icon positions

* chore: update remote
2026-02-13 01:06:43 +08:00
Innei c11d6de7db 🔧 build: add canary desktop release workflow (#12286)
🔧 build: add canary desktop release workflow and channel support

Add automated canary build pipeline triggered by build/fix/style commits
on canary branch, with concurrency control to cancel stale builds.
2026-02-13 00:37:03 +08:00
René Wang bbbe3a8d09 feat: add banner (#12258)
* feat: add banner

* fix: type error
2026-02-12 21:22:21 +08:00
Innei e51fbba881 feat: redesign Copilot ChatInput with compact action bar layout (#12279)
* add

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance ChatInput with customizable action bar properties

- Added `actionSize` to `ActionBarContextValue` for flexible action sizing.
- Updated `Action` component to utilize `actionSize` for dynamic sizing.
- Introduced `actionBarStyle` and `leftContent` props in `DesktopChatInput` for custom styling and content.
- Enhanced `SendButton` to accept size from `sendButtonProps`.
- Updated `ChatInput` to support new props for improved layout customization.
- Refactored `Conversation` component to implement compact action bar style and size.

This update improves the flexibility and customization of the ChatInput feature, allowing for better user experience.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: enhance NavHeader and Copilot Toolbar with layout improvements

- Added `allowShrink` property to Flexbox components in NavHeader for better responsiveness.
- Updated Text component in Copilot Toolbar to include tooltip support for ellipsis overflow, improving user experience.

These changes enhance the layout flexibility and visual consistency across the application.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: update Conversation and AgentSelectorAction components for improved styling

- Adjusted padding in the compact action bar style for better alignment.
- Enhanced AgentSelectorAction styles with additional border radius for a refined look.
- Simplified title translation in AgentSelectorAction for consistency.

These changes enhance the visual appeal and maintainability of the components within the Copilot feature.

Signed-off-by: Innei <tukon479@gmail.com>

*  feat: update ModelSwitchPanel to improve DropdownMenuPositioner prop usage

- Changed the `hoverTrigger` prop in `DropdownMenuPositioner` from a boolean to a destructured assignment for better clarity and consistency in the component's API.

This update enhances the readability and maintainability of the ModelSwitchPanel component.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-12 20:09:39 +08:00
Arvin Xu 1bfeeea6f4 🔧 chore: always run E2E tests on main and canary branches (#12268)
Skip duplicate check only applies to development branches now.
Main and canary branches will always execute E2E tests on every commit.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:42:56 +08:00
Rdmclin2 12d0ec21d0 fix: setting response error and other bugs (#12265)
* fix: setting response error and other bugs

* chore: remove  popover arrow
2026-02-12 18:20:28 +08:00
Rdmclin2 2bf0a08919 feat: support model detail dropdown (#12275)
* feat: support model detail dropdown

# Conflicts:
#	src/features/ChatInput/ActionBar/Model/index.tsx

* chore: fix test cases

* fix: type error

* fix: e2e tests
2026-02-12 17:51:52 +08:00
Innei 79e146f1a3 🐛 fix: improve RunCommand copy button visibility and ActionBar border radius (#12280)
Closes LOBE-4402
2026-02-12 16:01:44 +08:00
Innei 0e42ca5ca2 🐛 fix: improve GitHub Copilot auth retry logic (#12250)
* 🐛 fix: improve GitHub Copilot auth retry logic

Simplify auth refresh tracking from counter to boolean flag and clear
cached bearer token on 401 to ensure fresh token exchange.

* 🔧 fix: update package.json formatting and import statements in GitHub Copilot provider

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 fix: refine GitHub Copilot auth refresh logic to check for exchange credential

Update the 401 error handling to refresh the token only if an exchange credential is available, improving the authentication flow.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-12 13:09:38 +08:00
LobeHub Bot 823aa29c67 Sync main branch to canary branch (#12267)
* 🔧 chore(release): bump version to v2.1.27 [skip ci]

* chore: update sync main to canary workflow

* 🐛 fix: update @lobehub/ui version and refactor dynamic import handling (#12260)

*  feat: add hotfix workflow and script for automated hotfix management

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 fix: refactor PR creation command to use execFileSync for improved reliability

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui version and refactor dynamic import handling

- Bump @lobehub/ui dependency from ^4.35.0 to ^4.36.2 in package.json.
- Refactor settingsContentToStatic.mts to simplify dynamic import processing by removing business feature checks.
- Add initialize.ts to enable immer's map set functionality.
- Correct import path in layout.tsx from 'initiallize' to 'initialize'.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @types/react version in package.json

- Bump @types/react dependency from ^19.2.9 to 19.2.14.
- Add @types/react version to overrides section for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: enhance auto-tag-release workflow for strict semver validation

- Updated regex to match strict semantic versioning format, allowing for optional prerelease and build metadata.
- Added validation step to ensure the version is a valid semver before proceeding with the release process.

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove defaultSecurityBlacklist test file

- Deleted the test file for DEFAULT_SECURITY_BLACKLIST as it is no longer needed.
- This cleanup helps maintain a more streamlined test suite.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update localization files for multiple languages

- Improved translations in Arabic, Bulgarian, German, English, and Spanish for chat and tool-related strings.
- Enhanced descriptions for various parameters and added new keys for file handling and security warnings.
- Adjusted phrasing for clarity and consistency across languages.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update PR comment script to include Actions Artifacts link

- Modified the PR comment generation script to accept an additional artifactsUrl parameter.
- Updated the comment format to include both Release download and Actions Artifacts links for better accessibility.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>

* 🐛 chore(hotfix): bump version to v2.1.28 [skip ci]

* chore: update secrets token

---------

Signed-off-by: Innei <tukon479@gmail.com>
Co-authored-by: rdmclin2 <rdmclin2@gmail.com>
Co-authored-by: Arvin Xu <arvinx@foxmail.com>
Co-authored-by: Innei <i@innei.in>
2026-02-11 23:51:35 +08:00
rdmclin2 d225da96df chore: update secrets token 2026-02-11 23:45:07 +08:00
lobehubbot 0acaf01f9a 🐛 chore(hotfix): bump version to v2.1.28 [skip ci] 2026-02-11 15:34:52 +00:00
Innei 5a8911b72d 🐛 fix: update @lobehub/ui version and refactor dynamic import handling (#12260)
*  feat: add hotfix workflow and script for automated hotfix management

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 fix: refactor PR creation command to use execFileSync for improved reliability

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @lobehub/ui version and refactor dynamic import handling

- Bump @lobehub/ui dependency from ^4.35.0 to ^4.36.2 in package.json.
- Refactor settingsContentToStatic.mts to simplify dynamic import processing by removing business feature checks.
- Add initialize.ts to enable immer's map set functionality.
- Correct import path in layout.tsx from 'initiallize' to 'initialize'.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update @types/react version in package.json

- Bump @types/react dependency from ^19.2.9 to 19.2.14.
- Add @types/react version to overrides section for consistency.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: enhance auto-tag-release workflow for strict semver validation

- Updated regex to match strict semantic versioning format, allowing for optional prerelease and build metadata.
- Added validation step to ensure the version is a valid semver before proceeding with the release process.

Signed-off-by: Innei <tukon479@gmail.com>

* 🗑️ chore: remove defaultSecurityBlacklist test file

- Deleted the test file for DEFAULT_SECURITY_BLACKLIST as it is no longer needed.
- This cleanup helps maintain a more streamlined test suite.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update localization files for multiple languages

- Improved translations in Arabic, Bulgarian, German, English, and Spanish for chat and tool-related strings.
- Enhanced descriptions for various parameters and added new keys for file handling and security warnings.
- Adjusted phrasing for clarity and consistency across languages.

Signed-off-by: Innei <tukon479@gmail.com>

* 🔧 chore: update PR comment script to include Actions Artifacts link

- Modified the PR comment generation script to accept an additional artifactsUrl parameter.
- Updated the comment format to include both Release download and Actions Artifacts links for better accessibility.

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
2026-02-11 23:33:44 +08:00
Arvin Xu e6596e94a5 🔨 chore: update sync main to canary script (#12264)
#### 💻 Change Type

<!-- For change type, change [ ] to [x]. -->

- [ ]  feat
- [ ] 🐛 fix
- [ ] ♻️ refactor
- [ ] 💄 style
- [ ] 👷 build
- [ ] ️ perf
- [ ]  test
- [ ] 📝 docs
- [x] 🔨 chore

#### 🔗 Related Issue

<!-- Link to the issue that is fixed by this PR -->

<!-- Example: Fixes #xxx, Closes #xxx, Related to #xxx -->

#### 🔀 Description of Change

<!-- Thank you for your Pull Request. Please provide a description
above. -->

#### 🧪 How to Test

<!-- Please describe how you tested your changes -->

<!-- For AI features, please include test prompts or scenarios -->

- [ ] Tested locally
- [ ] Added/updated tests
- [ ] No tests needed

#### 📸 Screenshots / Videos

<!-- If this PR includes UI changes, please provide screenshots or
videos -->

| Before | After |
| ------ | ----- |
| ...    | ...   |

#### 📝 Additional Information

<!-- Add any other context about the Pull Request here. -->

<!-- Breaking changes? Migration guide? Performance impact? -->

## Summary by Sourcery

CI:
- Introduce a new sync-main-to-canary GitHub Actions workflow that
creates an automatic PR from main to canary on pushes to main and remove
the previous sync-main-to-dev workflow.
2026-02-11 23:18:27 +08:00
rdmclin2 9c09160154 chore: update sync main to canary workflow 2026-02-11 23:09:00 +08:00
LiJian 6eee83ab4c ♻️ refactor: imporve agent builder prompt (#12259) 2026-02-11 21:16:37 +08:00
lobehubbot d7d186df1a 🔧 chore(release): bump version to v2.1.27 [skip ci] 2026-02-11 09:33:15 +00:00
10359 changed files with 1393948 additions and 203858 deletions
+4 -2
View File
@@ -1,6 +1,8 @@
---
name: add-provider-doc
description: Guide for adding new AI provider documentation. Use when adding documentation for a new AI provider (like OpenAI, Anthropic, etc.), including usage docs, environment variables, Docker config, and image resources. Triggers on provider documentation tasks.
description: Add documentation for a new AI provider — usage docs, env vars, Docker config, image resources.
disable-model-invocation: true
argument-hint: '[provider-name]'
---
# Adding New AI Provider Documentation
@@ -79,7 +81,7 @@ Update all Dockerfiles at the **end** of ENV section:
- Cover image
- 3-4 API dashboard screenshots
- 2-3 LobeChat configuration screenshots
- 2-3 LobeHub configuration screenshots
- Host on LobeHub CDN: `hub-apac-1.lobeobjects.space`
## Checklist
+5 -3
View File
@@ -1,6 +1,8 @@
---
name: add-setting-env
description: Guide for adding environment variables to configure user settings. Use when implementing server-side environment variables that control default values for user settings. Triggers on env var configuration or setting default value tasks.
description: Add server-side environment variables that control default values for user settings.
disable-model-invocation: true
argument-hint: '[setting-name]'
---
# Adding Environment Variable for User Settings
@@ -49,7 +51,7 @@ export interface GlobalServerConfig {
### 3. Assemble Server Config (if new domain)
In `src/server/globalConfig/index.ts`:
In `apps/server/src/globalConfig/index.ts`:
```typescript
import { <domain>Env } from '@/envs/<domain>';
@@ -95,7 +97,7 @@ AI_IMAGE_DEFAULT_IMAGE_NUM: z.coerce.number().min(1).max(20).optional(),
// packages/types/src/serverConfig.ts
image?: PartialDeep<UserImageConfig>;
// src/server/globalConfig/index.ts
// apps/server/src/globalConfig/index.ts
image: cleanObject({ defaultImageNum: imageEnv.AI_IMAGE_DEFAULT_IMAGE_NUM }),
// src/store/user/slices/common/action.ts
+209
View File
@@ -0,0 +1,209 @@
---
name: agent-runtime-hooks
description: 'Agent runtime lifecycle hooks. Use for before/after tool or step hooks, tool mocks, human intervention, sub-agent calls, context compression, evals, tracing, callAgent, or lifecycle events.'
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
```
## Key Files
| File | Role |
| --------------------------------------------------------------- | ------------------------------------------------------ |
| `packages/agent-runtime/src/types/hooks.ts` | Type definitions (AgentHookType, all event interfaces) |
| `apps/server/src/services/agentRuntime/hooks/types.ts` | Server-side types (AgentHook, re-exports) |
| `apps/server/src/services/agentRuntime/hooks/HookDispatcher.ts` | Registration, dispatch, dispatchBeforeToolCall |
| `apps/server/src/modules/AgentRuntime/RuntimeExecutors.ts` | Tool/Compact/HumanIntervention hook dispatch |
| `apps/server/src/services/agentRuntime/AgentRuntimeService.ts` | Step hooks + HumanIntervention resume/reject |
| `apps/server/src/services/aiAgent/index.ts` | CallAgent hook dispatch |
## Registration Flow
```ts
const hooks: AgentHook[] = [
{ id: 'my-hook', type: 'afterStep', handler: async (event) => { ... } },
];
await aiAgentService.execAgent({ agentId, prompt, hooks });
// Internally: hookDispatcher.register(operationId, hooks)
// Cleanup: hookDispatcher.unregister(operationId)
```
## Hook Reference
### Step Level
**`beforeStep`** — Before each step. `event: AgentHookEvent`
**`afterStep`** — After each step. `event: AgentHookEvent` (content, toolsCalling, totalCost, etc.)
**`onComplete`** — Terminal state. `event: AgentHookEvent` (reason: done/error/interrupted/max_steps/cost_limit)
**`onError`** — Error occurred. `event: AgentHookEvent` (errorMessage, errorDetail)
### Tool Call Level
**`beforeToolCall`** — Before tool executes. **Supports mocking** via `event.mock()`.
```ts
// event: ToolCallHookEvent
{
(identifier, apiName, args, callIndex, stepIndex, operationId, mock);
}
// Mock example:
event.mock({ content: '{"error":"rate limited"}' });
```
Dispatch method: `hookDispatcher.dispatchBeforeToolCall()` (returns mock result or null).
**`afterToolCall`** — After tool completes. Observation only.
```ts
// event: AfterToolCallHookEvent
{
(identifier, apiName, args, callIndex, content, success, mocked, executionTimeMs, stepIndex);
}
```
**`onToolCallError`** — Tool threw an exception (catch block, not just `success=false`).
```ts
// event: ToolCallErrorHookEvent
{
(identifier, apiName, args, callIndex, error, stepIndex);
}
```
### Human Intervention
**`beforeHumanIntervention`** — Before agent pauses for approval.
```ts
// event: BeforeHumanInterventionHookEvent
{ operationId, stepIndex, pendingTools: [{ identifier, apiName }] }
```
**`afterHumanIntervention`** — After approve/reject, agent resumes.
```ts
// event: AfterHumanInterventionHookEvent
{ operationId, action: 'approve' | 'reject' | 'rejectAndContinue', toolCallId?, rejectionReason? }
```
**`onStopByHumanIntervention`** — User rejected, agent halted.
```ts
// event: StopByHumanInterventionHookEvent
{ operationId, toolCallId?, rejectionReason? }
```
### Context Compression
**`beforeCompact`** — Before compression starts.
```ts
// event: BeforeCompactHookEvent
{
(operationId, stepIndex, messageCount, tokenCount);
}
```
**`afterCompact`** — After compression completes.
```ts
// event: AfterCompactHookEvent
{
(operationId, stepIndex, groupId, messagesBefore, messagesAfter, summary);
}
```
**`onCompactError`** — Compression failed.
```ts
// event: CompactErrorHookEvent
{
(operationId, stepIndex, tokenCount, error);
}
```
### Sub-Agent (CallAgent)
**`beforeCallAgent`** — Before calling sub-agent. Dispatched on **parent** operation.
```ts
// event: BeforeCallAgentHookEvent
{
(operationId, agentId, instruction);
}
```
**`afterCallAgent`** — Sub-agent completed. Dispatched on **parent** operation.
```ts
// event: AfterCallAgentHookEvent
{
(operationId, agentId, subOperationId, threadId, success);
}
```
**`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).
+95
View File
@@ -0,0 +1,95 @@
---
name: agent-signal
description: 'Build or extend LobeHub Agent Signal pipelines. Use for signal sources, signal/action types, policies, middleware, workflow handoff, dedupe, scope behavior, or observability.'
---
# Agent Signal
Use this skill to implement event-driven background work for agents without coupling the work to the foreground chat request.
Agent Signal has one consistent shape:
`source event` -> `signal interpretation` -> `action execution` -> built-in result signals
## Start Here
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:
- `apps/server/src/services/agentSignal/index.ts`
- `apps/server/src/workflows/agentSignal/index.ts`
- `apps/server/src/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 `apps/server/src/services/agentSignal/sourceTypes.ts`.
3. Define or reuse signal and action types in `apps/server/src/services/agentSignal/policies/types.ts`.
4. Implement handlers with `defineSourceHandler`, `defineSignalHandler`, or `defineActionHandler`.
5. Bundle handlers with `defineAgentSignalHandlers(...)`.
6. Register the policy in `apps/server/src/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.
## Default Reading Set
- Shared semantic core:
`packages/agent-signal/src/index.ts`
`packages/agent-signal/src/base/builders.ts`
`packages/agent-signal/src/base/types.ts`
- Server-owned runtime and middleware:
`apps/server/src/services/agentSignal/runtime/AgentSignalRuntime.ts`
`apps/server/src/services/agentSignal/runtime/AgentSignalScheduler.ts`
`apps/server/src/services/agentSignal/runtime/middleware.ts`
`apps/server/src/services/agentSignal/runtime/context.ts`
- Existing policy example:
`apps/server/src/services/agentSignal/policies/analyzeIntent/index.ts`
`apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackSatisfaction.ts`
`apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackDomain.ts`
`apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackAction.ts`
`apps/server/src/services/agentSignal/policies/analyzeIntent/actions/userMemory.ts`
- Observability:
`apps/server/src/services/agentSignal/observability/projector.ts`
`apps/server/src/services/agentSignal/observability/traceEvents.ts`
`packages/observability-otel/src/modules/agent-signal/index.ts`
## Implementation Rules
- 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 `apps/server/src/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`
@@ -0,0 +1,4 @@
interface:
display_name: 'Agent Signal'
short_description: 'Build AgentSignal sources, signals, actions, and policies.'
default_prompt: 'Use $agent-signal to add a new Agent Signal source, policy, handler, or observability flow.'
@@ -0,0 +1,199 @@
# Agent Signal Architecture
## Pipeline
Use this mental model first:
```text
producer
-> emitAgentSignalSourceEvent(...) or enqueueAgentSignalSourceEvent(...)
-> emitSourceEvent(...)
-> dedupe + scope lock + source normalization
-> runtime.emitNormalized(source)
-> source handlers
-> signal handlers
-> action handlers
-> built-in result signals
-> observability projection + persistence
```
The scheduler is queue-driven, not hard-coded for one policy:
```text
source node
-> matching source handlers
-> dispatch signals/actions
-> matching signal handlers
-> dispatch more signals/actions
-> matching action handlers
-> ExecutorResult
-> signal.action.applied | signal.action.skipped | signal.action.failed
```
Read:
- `apps/server/src/services/agentSignal/index.ts`
- `apps/server/src/services/agentSignal/sources/index.ts`
- `apps/server/src/services/agentSignal/runtime/AgentSignalScheduler.ts`
## Package Boundaries
### `packages/agent-signal`
Treat this as the shared semantic core.
It provides:
- base node types: source, signal, action
- builders: `createSource`, `createSignal`, `createAction`
- built-in result signal types
- runtime result contracts such as `RuntimeProcessorResult` and `ExecutorResult`
Read:
- `packages/agent-signal/src/base/types.ts`
- `packages/agent-signal/src/base/builders.ts`
- `packages/agent-signal/src/types/events.ts`
- `packages/agent-signal/src/types/builtin.ts`
### `apps/server/src/services/agentSignal`
Treat this as the server-owned implementation layer.
It owns:
- source catalogs and payload maps
- policy-specific signal and action catalogs
- middleware registration
- runtime scheduling and guard backends
- Redis-backed dedupe, waypoint, and policy state
- service entrypoints for synchronous and async execution
### `packages/observability-otel/src/modules/agent-signal`
Treat this as shared OTEL ownership for Agent Signal metrics and tracer instances.
## Core Vocabulary
### Source
A source is the normalized external fact that started the chain.
Examples:
- `agent.user.message`
- `runtime.before_step`
- `runtime.after_step`
- `client.runtime.start`
- `bot.message.merged`
Define source payloads in:
- `apps/server/src/services/agentSignal/sourceTypes.ts`
Build normalized sources in:
- `apps/server/src/services/agentSignal/sources/buildSource.ts`
- `packages/agent-signal/src/base/builders.ts`
### Signal
A signal is a semantic interpretation. Signals should be reusable and meaning-oriented.
Examples from `analyzeIntent`:
- `signal.feedback.satisfaction`
- `signal.feedback.domain.memory`
- `signal.feedback.domain.prompt`
- `signal.feedback.domain.skill`
Define server-owned signal types in:
- `apps/server/src/services/agentSignal/policies/types.ts`
### Action
An action is a concrete side effect the runtime should execute.
Example:
- `action.user-memory.handle`
Action handlers usually:
- check idempotency
- call tools, models, or services
- return `ExecutorResult`
### Policy
A policy is an installable bundle of handlers. It is the composition unit that turns the generic runtime into a feature.
Example:
- `createAnalyzeIntentPolicy(...)`
### Procedure
"Procedure" is not a first-class type in this runtime. Use the word to describe one end-to-end use case:
1. define ingress source
2. emit or enqueue the source
3. interpret source into signals
4. plan actions from signals
5. execute actions
6. persist trace and metrics
When a user asks for "the procedure", document the flow above and point to the exact producer, handlers, and execution entrypoint.
## Scope, Deduping, And Quiet Background Work
`scopeKey` is the serialization boundary for related work. It is used for:
- source dedupe windows
- scope locks during source generation
- runtime guard state
- waypoint persistence for queued processing
Read:
- `apps/server/src/services/agentSignal/sources/index.ts`
- `apps/server/src/services/agentSignal/runtime/context.ts`
- `apps/server/src/services/agentSignal/constants.ts`
Use `enqueueAgentSignalSourceEvent(...)` when the work should stay quiet and out-of-band. That path:
1. normalizes the source envelope
2. derives or reuses `scopeKey`
3. triggers `AgentSignalWorkflow`
4. executes later in `runAgentSignalWorkflow`
This is the preferred path when the UI request should finish immediately and the policy can run in the background.
Read:
- `apps/server/src/workflows/agentSignal/index.ts`
- `apps/server/src/workflows/agentSignal/run.ts`
## Existing Example: `analyzeIntent`
Use `analyzeIntent` as the reference chain:
```text
agent.user.message
-> feedback satisfaction source handler
-> signal.feedback.satisfaction
-> feedback domain signal handler
-> signal.feedback.domain.*
-> feedback action planner
-> action.user-memory.handle
-> signal.action.applied | skipped | failed
```
Read:
- `apps/server/src/services/agentSignal/policies/analyzeIntent/index.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackSatisfaction.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackDomain.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackAction.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/actions/userMemory.ts`
@@ -0,0 +1,228 @@
# Writing Handlers And Policies
## Fluent Registration API
Use the middleware helpers in `apps/server/src/services/agentSignal/runtime/middleware.ts`.
They provide:
- `defineSourceHandler(...)`
- `defineSignalHandler(...)`
- `defineActionHandler(...)`
- `defineAgentSignalHandlers(...)`
These helpers do two jobs:
1. keep handler registration terse
2. preserve strong typing when `listen` points at concrete source, signal, or action types
## Handler Shape
Each handler receives:
- the current runtime node
- `RuntimeProcessorContext`
The context gives you:
- `scopeKey`
- `now()`
- `runtimeState.getGuardState(lane)`
- `runtimeState.touchGuardState(lane, now?)`
Read:
- `apps/server/src/services/agentSignal/runtime/context.ts`
## Return Contracts
Return one of these shapes:
- `void`: no fan-out, stop at this handler
- `{ status: 'dispatch', signals?, actions? }`: continue the chain
- `{ status: 'wait', pending? }`: pause for later host coordination
- `{ status: 'schedule', nextHop }`: schedule another hop
- `{ status: 'conclude', concluded? }`: stop with a terminal runtime result
- `ExecutorResult`: only for action handlers that performed a concrete side effect
Read:
- `packages/agent-signal/src/base/types.ts`
- `apps/server/src/services/agentSignal/runtime/AgentSignalScheduler.ts`
## Policy Composition Pattern
Use `defineAgentSignalHandlers([...])` to bundle related handlers into one policy.
Example from `analyzeIntent`:
```ts
return defineAgentSignalHandlers([
createFeedbackSatisfactionJudgeProcessor(...),
createFeedbackDomainJudgeSignalHandler(...),
createFeedbackActionPlannerSignalHandler(),
defineUserMemoryActionHandler(...),
]);
```
That bundle is later passed into the runtime via:
- `createDefaultAgentSignalPolicies(...)`
- `createAgentSignalRuntime({ policies })`
Read:
- `apps/server/src/services/agentSignal/policies/index.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/index.ts`
## Source Handler Pattern
Use a source handler when you are interpreting a producer event into semantic signals.
Reference:
- `apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackSatisfaction.ts`
Pattern:
```ts
return defineSourceHandler(
AGENT_SIGNAL_SOURCE_TYPES.agentUserMessage,
'agent.user.message:my-handler',
async (source, ctx): Promise<RuntimeProcessorResult | void> => {
// interpret source payload
// optionally use ctx.runtimeState
return {
signals: [
/* one or more semantic signals */
],
status: 'dispatch',
};
},
);
```
Write source handlers when:
- a raw message, lifecycle event, or bot ingress needs interpretation
- the work is still semantic, not side-effectful
## Signal Handler Pattern
Use a signal handler when one semantic state should branch into more semantic states or planned actions.
References:
- `apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackDomain.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/feedbackAction.ts`
Pattern:
```ts
return defineSignalHandler(
MY_SIGNAL_TYPE,
'signal.my-policy-router',
async (signal): Promise<RuntimeProcessorResult | void> => {
return {
actions: [
/* planned work */
],
status: 'dispatch',
};
},
);
```
Use signal handlers for:
- routing
- fan-out
- filtering
- conflict resolution
- converting interpretation into planned actions
## Action Handler Pattern
Use an action handler when the runtime should do actual work.
Reference:
- `apps/server/src/services/agentSignal/policies/analyzeIntent/actions/userMemory.ts`
Pattern:
```ts
return defineActionHandler(
MY_ACTION_TYPE,
'action.my-policy-executor',
async (action, ctx): Promise<ExecutorResult> => {
// run service/tool/model side effect
// check idempotency if needed
return {
actionId: action.actionId,
attempt: {
completedAt: ctx.now(),
current: 1,
startedAt,
status: 'succeeded',
},
status: 'applied',
};
},
);
```
Keep these rules:
- perform idempotency checks here or immediately before side effects
- return stable `actionId`
- include failure detail in `error`
- let the scheduler turn the `ExecutorResult` into built-in result signals
## Source, Signal, And Action Type Placement
Use this split:
- external event payloads:
`apps/server/src/services/agentSignal/sourceTypes.ts`
- policy-owned signal and action payloads:
`apps/server/src/services/agentSignal/policies/types.ts`
- normalized shared node contracts:
`packages/agent-signal/src/base/types.ts`
Do not put app-specific signal catalogs into `packages/agent-signal`. That package should stay generic and reusable.
## Choosing The Right Node
Choose `source` when:
- the outside world emitted a new fact
Choose `signal` when:
- the system needs semantic meaning that downstream handlers can reuse
Choose `action` when:
- the runtime is ready for a concrete side effect
If a handler both interprets meaning and performs side effects, split it. That keeps chains inspectable and testable.
## Testing Strategy
Prefer focused tests near the touched code.
Useful references:
- `apps/server/src/services/agentSignal/runtime/__tests__/AgentSignalRuntime.test.ts`
- `apps/server/src/services/agentSignal/__tests__/index.integration.test.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/__tests__/*`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/actions/__tests__/*`
Test at the smallest level that proves the behavior:
- handler unit test for one routing rule
- runtime test for queue fan-out
- integration test for service ingress and observability persistence
@@ -0,0 +1,118 @@
# Observability And Debugging
## OTEL Ownership
Use `packages/observability-otel/src/modules/agent-signal/index.ts` for the shared tracer and metrics.
Available instruments:
- `tracer`
- `sourceCounter`
- `signalCounter`
- `actionCounter`
- `actionResultCounter`
- `chainCounter`
- `signalActionTransitionCounter`
- `chainDurationHistogram`
- `actionDurationHistogram`
Use this module when you need shared telemetry ownership instead of creating feature-local meters or tracers.
## Projection Pipeline
After runtime execution, the service projects one compact observability model from the full chain.
Read:
- `apps/server/src/services/agentSignal/observability/projector.ts`
- `apps/server/src/services/agentSignal/observability/traceEvents.ts`
- `apps/server/src/services/agentSignal/observability/store.ts`
Projection outputs:
- 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:
- `apps/server/src/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
Read:
- `apps/server/src/services/agentSignal/index.ts`
- `apps/server/src/services/agentSignal/sources/index.ts`
### The signal exists but no action runs
Check:
- the signal type has a registered signal handler
- the signal handler returns `status: 'dispatch'`
- the handler actually returned actions
### The action runs twice
Check:
- source dedupe key stability
- action idempotency strategy
- scope key stability across retries and workflow handoff
Reference:
- `apps/server/src/services/agentSignal/policies/actionIdempotency.ts`
- `apps/server/src/services/agentSignal/policies/analyzeIntent/actions/userMemory.ts`
### Background runs are hard to discover
Check:
- workflow snapshot bridge in development
- projected telemetry record contents
- OTEL counters and histograms in the shared module
## Minimal Completion Checklist
- source ingress is testable
- handler registration is discoverable from the policy factory
- action executor returns structured results
- projection includes the new path cleanly
- tests cover at least one happy path and one no-op or failure path
+212
View File
@@ -0,0 +1,212 @@
---
name: agent-testing
description: >
Agentic end-to-end testing for LobeHub: backend verification via the CLI,
frontend verification via agent-browser (Electron), full-stack verification in
the browser, and bot-channel verification via osascript. Local-first today,
designed to extend to cloud automation. Triggers on 'cli test', 'test with cli',
'verify with cli', 'backend test with cli', 'local test', 'test in electron',
'test desktop', 'test bot', 'bot test', 'test in discord', 'test in telegram',
'test in slack', 'test in wechat', 'test in weixin', 'test in lark', 'test in feishu',
'test in qq', 'manual test', 'osascript', 'test report', or any local
end-to-end verification task.
---
# Agent Testing (Agentic End-to-End Verification)
One skill for all agentic end-to-end testing — local-first today, designed to
also run as full cloud automation. Every test session follows the same
four-step contract:
```
Step 0: Env + Auth → Step 1: Pick surface → Step 2: Run → Step 3: Structured report
```
## Step 0 — Environment setup + auth check (mandatory)
Step 0 is about getting the environment ready: **dependencies are healthy**
and **auth is green**. A test run that dies halfway on a missing dependency or
a login wall wastes the whole session — clear both gates BEFORE writing a
single test step.
### 0.1 Dependencies are installed — root AND standalone apps
The root pnpm workspace does **NOT** cover every app: `pnpm-workspace.yaml`
lists `packages/**`, `e2e`, `apps/server`, and only `apps/desktop/src/main`
**`apps/desktop` and `apps/cli` are standalone**, each keeping its own
`node_modules` with its own links into `packages/`. A root install does not
refresh them, so install in every app the test will touch:
```bash
pnpm install # root workspace
cd apps/desktop && pnpm install # Electron surface
cd apps/cli && pnpm install # CLI surface
```
Symptom of a stale standalone install: the build/launch fails to resolve a
recently added workspace package — `Rolldown failed to resolve import
"@lobechat/<pkg>"` (Electron) or `Cannot find module '@lobechat/<pkg>'` (CLI).
### 0.2 Run scripts from the repo root
All paths in this skill (`./.agents/skills/agent-testing/...`) are
repo-root-relative, and background commands inherit the current working
directory — a script launched while `cwd` is `apps/desktop` fails with
`No such file or directory`. Verify `pwd` is the repo root before launching
long-running scripts.
### 0.3 Auth is green
**Auth is the gate for all automated testing.**
```bash
./.agents/skills/agent-testing/scripts/setup-auth.sh status
```
| Surface | Mechanism | One-key path | Standard check |
| -------- | ------------------------------------------------- | ------------------------------ | ------------------------------ |
| CLI | OIDC Device Code Flow (`apps/cli/.lobehub-dev`) | `setup-auth.sh cli` | `setup-auth.sh status` |
| Web | better-auth cookie injection into `agent-browser` | `pbpaste \| setup-auth.sh web` | `setup-auth.sh web-verify` |
| Electron | App's own persistent login state | Log in once in the app | `app-probe.sh auth` |
| Bot | Native apps already logged in | — | per-platform screenshot |
Login-state checks are standardized — do NOT hand-roll `window.__LOBE_STORES`
eval snippets; use `scripts/app-probe.sh auth` (returns `{ isSignedIn, userId }`,
works for Electron CDP and web sessions via `AB_TARGET`).
If `status` is not all green, fix auth first (the steps that need a human must be
requested from the user explicitly). Full background and failure modes:
[references/auth.md](./references/auth.md).
## Step 1 — Pick the surface by change scope
| Change scope | Default surface | Why | Guide |
| ------------------------------------------------------- | ------------------------------------ | ----------------------------------------------------------------- | ---------------------------------- |
| **Backend** (TRPC router / service / model / migration) | **CLI** | Fastest loop, text-assertable output, zero UI flakiness | [cli/index.md](./cli/index.md) |
| **Pure frontend** (components, store, styles, UX) | **Electron** (agent-browser + CDP) | Primary product shape; `__LOBE_STORES` state introspection | [ui/electron.md](./ui/electron.md) |
| **Full-stack** (new API + UI consuming it) | **Web** (browser + local dev server) | One surface where network requests and UI are observable together | [ui/web.md](./ui/web.md) |
| **Bot channels** (Discord / WeChat / Lark / …) | Native app via osascript / bridge | Only way to exercise the real channel end-to-end | `bot/<platform>/index.md` |
Escalate, don't duplicate: verify a backend change with the CLI first; only add
a UI pass when the change actually affects the UI.
### Environment support (local macOS vs cloud Linux)
The decisive constraint per surface is **how evidence (screenshots) is
captured**: CDP-based capture (`agent-browser screenshot`) renders from the
browser engine and needs no real display; OS-level capture (`screencapture`,
osascript) is macOS-only.
| Surface | macOS (local) | Linux / cloud (headless) | Screenshot mechanism |
| -------- | ------------- | --------------------------------------------------------- | ------------------------------------------------------ |
| CLI | ✅ | ✅ | n/a — text output |
| Web | ✅ | ✅ headless Chromium works natively | CDP — no display needed |
| Electron | ✅ | ⚠️ runs, but needs a display server: wrap with `xvfb-run` | CDP works under Xvfb; `capture-app-window.sh` does NOT |
| Bot | ✅ | ❌ osascript + native apps are macOS-only | macOS `screencapture` only |
When a test must stay cloud-portable, prefer CDP-based evidence over
OS-level capture wherever both exist.
### Bot platforms
| Platform | Guide | Quick switcher |
| ------------- | ------------------------------------------------ | --------------------- |
| Discord | [bot/discord/index.md](./bot/discord/index.md) | `Cmd+K` |
| Slack | [bot/slack/index.md](./bot/slack/index.md) | `Cmd+K` |
| Telegram | [bot/telegram/index.md](./bot/telegram/index.md) | `Cmd+F` |
| WeChat / 微信 | [bot/wechat/index.md](./bot/wechat/index.md) | `Cmd+F` |
| Lark / 飞书 | [bot/lark/index.md](./bot/lark/index.md) | `Cmd+K` |
| QQ | [bot/qq/index.md](./bot/qq/index.md) | `Cmd+F` |
| iMessage | [bot/imessage/index.md](./bot/imessage/index.md) | bridge (no osascript) |
Each platform folder contains an `index.md` (activation, navigation,
send-message, verification snippets) and a `test-<platform>-bot.sh` script
sharing the interface:
```bash
./.agents/skills/agent-testing/bot/<platform>/test-<platform>-bot.sh <channel_or_contact> <message> [wait_seconds] [screenshot_path]
```
New to osascript automation? Read
[references/osascript.md](./references/osascript.md) first — it is a general
macOS-automation asset (activate, type, paste, screenshot, accessibility reads,
gotchas), not bot-specific.
## Step 2 — Run
Surface guides above carry the detailed workflows. Shared infrastructure:
| Need | Where |
| ------------------------------------ | -------------------------------------------------------------------- |
| Start / restart the local dev server | [references/dev-server.md](./references/dev-server.md) |
| `agent-browser` command reference | [references/agent-browser.md](./references/agent-browser.md) |
| osascript patterns (general macOS) | [references/osascript.md](./references/osascript.md) |
| Agent gateway probing | [references/agent-gateway.md](./references/agent-gateway.md) |
| Screen recording | [references/record-app-screen.md](./references/record-app-screen.md) |
### Scripts
All under `.agents/skills/agent-testing/scripts/`:
| Script | Usage |
| ------------------------- | ------------------------------------------------------------------------------ |
| `setup-auth.sh` | One-stop auth setup & status check (`status` / `cli` / `web`) |
| `app-probe.sh` | LobeHub app probes: `auth` / `route` / `ops` / `goto <path>` / `errors` |
| `record-gif.sh` | Frame-sequence → GIF for time-based behavior (streaming, timers, animations) |
| `report-init.sh` | Scaffold a structured test report (Step 3) |
| `electron-dev.sh` | Manage Electron dev env (start/stop/status/restart, CDP 9222) |
| `capture-app-window.sh` | Screenshot a specific app window (general; used by bot tests) |
| `record-app-screen.sh` | Record app screen (video + periodic screenshots) |
| `record-electron-demo.sh` | Record Electron app demo with ffmpeg |
| `agent-gateway/` | Gateway probe / dump / analyze tools |
`app-probe.sh` is the LobeHub-specific fast path into app state — auth check,
current route, running operations, and `goto <path>` quick navigation
(`/agent/<agentId>/<topicId>`, `/task/<taskId>`, `/settings`, …) so a test can
jump straight to the state under test instead of clicking through the UI. See
[ui/electron.md](./ui/electron.md#lobehub-probes--quick-navigation) for usage.
## Step 3 — Structured report (mandatory deliverable)
Every automated test session ends with a structured, evidence-backed report —
not a chat-only summary. Scaffold it up front and fill it as you test:
```bash
DIR=$(./.agents/skills/agent-testing/scripts/report-init.sh my-feature "Verify my feature")
# ... test, saving screenshots / CLI transcripts into $DIR/assets/ ...
# fill $DIR/report.md (case table, embedded evidence, verdict) and $DIR/result.json
```
Reports live in `.records/reports/<timestamp>-<slug>/` (gitignored): `report.md`
(human-readable, with embedded screenshots), `result.json` (machine-readable
pass/fail + score), `assets/` (evidence). Format spec and evidence rules:
[references/report.md](./references/report.md).
Two hard rules worth front-loading:
- **Report language = the user's conversation language.** Write the ENTIRE
`report.md` (headings included) in the language the user is conversing in —
no mixed English. `result.json` keys/status values stay English.
- **Time-based behavior needs a GIF, not a screenshot.** If a case asserts
change over time (streaming output, a ticking timer, loading states,
animations), record it with `scripts/record-gif.sh` and embed the GIF —
a static screenshot cannot prove the behavior.
## Directory map
```
agent-testing/
├── SKILL.md # this router
├── cli/index.md # backend verification via the LobeHub CLI
├── ui/electron.md # pure-frontend verification in the desktop app
├── ui/web.md # full-stack verification in the browser
├── bot/<platform>/ # bot-channel verification (osascript / bridge)
├── references/ # shared knowledge: auth, dev-server, agent-browser, osascript, report
└── scripts/ # setup-auth, report-init, electron-dev, capture, recording, gateway
```
## Gotchas
- agent-browser: see [references/agent-browser.md](./references/agent-browser.md#gotchas)
- Electron: see [ui/electron.md](./ui/electron.md#electron-gotchas)
- osascript: see [references/osascript.md](./references/osascript.md#gotchas)
@@ -0,0 +1,97 @@
# Discord Bot Testing
**App name:** `Discord` | **Process name:** `Discord`
See [references/osascript.md](../../references/osascript.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Discord
osascript -e 'tell application "Discord" to activate'
sleep 1
# Open Quick Switcher (Cmd+K) to navigate to a channel
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
sleep 1
osascript -e 'tell application "System Events" to key code 36' # Enter
sleep 2
```
## Send Message to Bot
```bash
# The message input is focused after navigating to a channel
# Type a message
osascript -e 'tell application "System Events" to keystroke "/hello"'
sleep 0.5
osascript -e 'tell application "System Events" to key code 36' # Enter
```
## Send Long Message (via clipboard)
```bash
osascript -e '
tell application "Discord" to activate
delay 0.5
set the clipboard to "Write a 3000 word essay about space exploration"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
## Verify Bot Response
```bash
# Wait for bot to respond, then screenshot
sleep 10
screencapture /tmp/discord-bot-response.png
# Read with the Read tool for visual verification
```
## Full Bot Test Example
```bash
#!/usr/bin/env bash
# test-discord-bot.sh — Send message and verify bot response
# 1. Activate Discord and navigate to channel
osascript -e '
tell application "Discord" to activate
delay 1
-- Quick Switcher
tell application "System Events" to keystroke "k" using command down
delay 0.5
tell application "System Events" to keystroke "bot-testing"
delay 1
tell application "System Events" to key code 36
delay 2
'
# 2. Send test message
osascript -e '
set the clipboard to "!ping"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
# 3. Wait for response and capture
sleep 5
screencapture /tmp/discord-test-result.png
echo "Screenshot saved to /tmp/discord-test-result.png"
```
## Script
```bash
./.agents/skills/agent-testing/bot/discord/test-discord-bot.sh "bot-testing" "!ping"
./.agents/skills/agent-testing/bot/discord/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
```
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
#
# test-discord-bot.sh — Send a message to a Discord bot and capture the response
#
# Usage:
# ./scripts/test-discord-bot.sh <channel> <message> [wait_seconds] [screenshot_path]
#
# channel — Channel name to navigate to via Quick Switcher (Cmd+K)
# message — Message to send to the bot
# wait_seconds — Seconds to wait for bot response (default: 10)
# screenshot_path — Output screenshot path (default: /tmp/discord-bot-test.png)
#
# Prerequisites:
# - Discord desktop app installed and logged in
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
#
# Examples:
# ./scripts/test-discord-bot.sh "bot-testing" "!ping"
# ./scripts/test-discord-bot.sh "bot-testing" "/ask Tell me a joke" 30
# ./scripts/test-discord-bot.sh "general" "Hello bot" 15 /tmp/my-test.png
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHANNEL="${1:?Usage: test-discord-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
MESSAGE="${2:?Usage: test-discord-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
WAIT="${3:-10}"
SCREENSHOT="${4:-/tmp/discord-bot-test.png}"
APP="Discord"
echo "[$APP] Activating..."
osascript -e "tell application \"$APP\" to activate"
sleep 1
echo "[$APP] Navigating to channel: $CHANNEL"
osascript -e '
tell application "System Events"
-- Quick Switcher
keystroke "k" using command down
delay 0.8
keystroke "'"$CHANNEL"'"
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
echo "[$APP] Sending message: $MESSAGE"
osascript -e '
set the clipboard to "'"$MESSAGE"'"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
echo "[$APP] Waiting ${WAIT}s for bot response..."
sleep "$WAIT"
echo "[$APP] Capturing screenshot..."
"$SCRIPT_DIR/../../scripts/capture-app-window.sh" "$APP" "$SCREENSHOT"
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
@@ -0,0 +1,232 @@
# iMessage Desktop bridge regression test
The iMessage channel is different from the other bot platforms: there is **no
native app to drive with osascript**. Instead the Desktop app runs a local
**BlueBubbles bridge** — a small HTTP server in the Electron main process that
registers a webhook on a local [BlueBubbles](https://bluebubbles.app/) server,
receives iMessage events, and forwards them to LobeHub Cloud.
So the test surface is three layers:
1. **Electron main IPC**`imessageBridge.*` handlers (`getStatus`,
`testConfig`, `upsertConfig`, `removeConfig`, `start`, `stop`)
2. **Local bridge HTTP server**`http://127.0.0.1:<port>/webhooks/bluebubbles/<appId>?secret=<secret>`
3. **BlueBubbles REST API**`http://127.0.0.1:1234/api/v1/*` (webhook + server/info)
## Prerequisites
- A running **BlueBubbles server** (macOS, default `http://127.0.0.1:1234`) with
a known password. Sanity check:
```bash
curl -sS -m4 -o /dev/null -w '%{http_code}\n' \
"http://127.0.0.1:1234/api/v1/server/info?password=<PW>" # expect 200
```
- **Electron dev running with CDP**: `./.agents/skills/agent-testing/scripts/electron-dev.sh start`
- The **iMessage Desktop branch** checked out (the `imessageBridge` IPC group
and `@lobechat/chat-adapter-imessage` must be compiled into the main bundle).
Run `pnpm install --ignore-scripts` at the repo root **and** in `apps/desktop/`
after switching branches — the new workspace package must be linked or the
main build fails to resolve `@lobechat/chat-adapter-imessage`.
## Fast path: automated script
```bash
./.agents/skills/agent-testing/bot/imessage/test-imessage-bridge.sh '<bluebubbles_password>' [bb_url] [cdp_port]
```
Asserts the whole flow and self-cleans (unique `applicationId` per run, removes
its bridge config + BlueBubbles webhook on exit). Exit 0 = all green. It covers:
- BlueBubbles reachable + password valid; Electron CDP reachable; IPC available
- `testConfig` happy path → success
- `testConfig` wrong password → rejected; unreachable URL → rejected
- `upsertConfig` **first-time save → success** (Bug #1 regression guard, below)
- `getStatus` → `running:true`, config persisted, password redacted (`blueBubblesPasswordSet`)
- BlueBubbles webhook actually registered for the appId
- Local bridge HTTP server: wrong secret → 401; valid secret → past auth
The password is passed as argv (visible in `ps`) — local dev only, don't use a
real secret on a shared machine.
## Layer 1 — IPC probes (no UI)
The renderer exposes the main-process handlers via `window.electronAPI.invoke`.
This is the quickest way to exercise the bridge without clicking:
```bash
# baseline
agent-browser --cdp 9222 eval \
"(async()=>JSON.stringify(await window.electronAPI.invoke('imessageBridge.getStatus',{})))()"
# test a connection (note: password as a JS string)
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(async function () {
try {
var r = await window.electronAPI.invoke('imessageBridge.testConfig', {
applicationId: 'probe',
blueBubblesServerUrl: 'http://127.0.0.1:1234',
blueBubblesPassword: 'PASTE_PW',
enabled: true,
webhookSecret: 'probe-secret',
});
return JSON.stringify(r); // { success: true }
} catch (e) { return 'ERR: ' + (e.message || e); }
})()
EVALEOF
```
`upsertConfig` persists to the Electron store, starts the local HTTP server, and
registers the BlueBubbles webhook. `removeConfig` + `stop` reverse it.
## Layer 2 — full UI flow (agent-browser)
The bridge settings only render in Desktop (`isDesktop` guard) under the agent's
**Channel → iMessage** screen. The platform tile only appears as a real (non
"Coming Soon") entry once the server registers `imessage` **and** the frontend
drops it from `COMING_SOON_PLATFORMS` (`src/routes/(main)/agent/channel/const.ts`).
```bash
agent-browser --cdp 9222 open "http://localhost:5173/agent/<aid>/channel"
agent-browser --cdp 9222 wait --load networkidle && agent-browser --cdp 9222 wait 1500
# confirm the remote backend lists imessage (it must be registered + deployed)
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(async function(){
var url='lobe-backend://lobe/trpc/lambda/agentBotProvider.listPlatforms?input='+encodeURIComponent('{"json":null,"meta":{"values":["undefined"],"v":1}}');
var d=await (await fetch(url,{credentials:'include'})).json();
var p=d.result?.data?.json||d;
return JSON.stringify(p.map(function(x){return x.id;}));
})()
EVALEOF
# click the iMessage tile, then fill the form by ref
agent-browser --cdp 9222 eval "(()=>{var b=[...document.querySelectorAll('aside button')].find(x=>/imessage/i.test(x.textContent));b&&b.click();})()"
agent-browser --cdp 9222 wait 1500
agent-browser --cdp 9222 snapshot -i | grep -iE "127.0.0.1:1234|Application ID|Webhook Secret|Test BlueBubbles|Save Bridge"
```
Field refs (from the snapshot): Application ID, Webhook Secret, BlueBubbles
Server URL (`placeholder="http://127.0.0.1:1234"`), and a **nested** textbox right
under the URL one is the BlueBubbles Password. Fill with `fill` (real input
events — `eval`-setting React inputs won't fire onChange), click **Test
BlueBubbles**, then **Save Bridge**. Read the antd toast immediately (it
auto-dismisses):
```bash
agent-browser --cdp 9222 eval \
"JSON.stringify([...new Set([...document.querySelectorAll('.ant-message-custom-content')].map(n=>n.textContent.trim()))])"
# Test → "BlueBubbles connection passed"
# Save → "iMessage Desktop bridge saved"
```
Verify the end state via BlueBubbles + IPC:
```bash
curl -sS "http://127.0.0.1:1234/api/v1/webhook?password=<PW>" # webhook for the appId present
agent-browser --cdp 9222 eval "(async()=>JSON.stringify(await window.electronAPI.invoke('imessageBridge.getStatus',{})))()"
# running:true, serverUrl: http://127.0.0.1:33270, configs[].blueBubblesPasswordSet:true
```
Cleanup: `removeConfig` + `stop` via IPC, then `DELETE /api/v1/webhook/<id>` on
BlueBubbles.
## Outbound send test (desktop → BlueBubbles → iMessage)
Verifies the leg the bridge uses to _reply_: `BlueBubblesApiClient.sendText`
→ `POST /api/v1/message/text`. Run the helper against your own number:
```bash
./.agents/skills/agent-testing/bot/imessage/send-imessage-test.sh '<bb_password>' '+<E164>' # e.g. +15551234567
```
**Gotcha that bites everyone:** with `method=apple-script` and a _new_
conversation, the HTTP POST often **times out** even though the message is
sent. Never judge success by the HTTP response. Instead poll
`POST /api/v1/message/query` and read the matching `isFromMe:true` row's
`error` field:
- `error: 0` (or null) → sent OK
- non-zero `error` → real send failure
The script does exactly this: fires the send, ignores the timeout, then matches
its marker text in the message store and asserts `error == 0`.
Two more notes:
- Use a full E.164 handle (`iMessage;-;+<countrycode><number>`) or an Apple ID
email. Looking the chat up by guid afterwards may 404 if BB filed the message
under a differently-formatted guid — that's a lookup quirk, not a send failure.
- Sending to _your own_ number round-trips: BB records both the outgoing
(`fromMe:true`) and an incoming copy (`fromMe:false`).
## Inbound e2e test (iMessage → cloud agent → reply)
Full inbound chain: a message arrives → BlueBubbles fires its `new-message`
webhook → local bridge (`:33270`) → `forwardWebhook` POSTs to
`<remote>/api/agent/webhooks/imessage/<appId>?secret=…` → cloud agent → reply
flows back via Device Gateway → BB `sendText`.
Prerequisites:
- A cloud bot provider for the same `applicationId` exists and is **connected**
(Save Configuration + the device gateway connected — a _disconnected_ gateway
yields `DEVICE_NOT_FOUND` on connect and blocks the reply leg).
- The `imessage` Labs toggle is on (otherwise the channel is gated to "Coming
Soon"), and `webhookSecret` matches on both ends (auto-generated on save).
Two ways to drive it:
1. **Second device / Apple ID (recommended).** Have _another_ Apple ID message
the BB-hosted number (e.g. "please reply pong"). The bot replies; you see it
on the other device. **No loop risk** — the reply goes to the other party,
not back to itself.
2. **Send to your own number (quick, loop-aware).** `sendText` to the hosted
number; the loopback _incoming_ copy (`isFromMe:false`) triggers the bot.
Watch the reply land in `message/query` as a `fromMe:true` row.
**Loop guard — why a self-send doesn't spin forever:** the Chat SDK adapter
drops any `isFromMe` message before dispatch
(`packages/chat-adapter-imessage/src/adapter.ts`: `if (message.isFromMe) return`).
The bot's own reply (`isFromMe:true`) is never re-processed, so in the normal
case (someone else → bot → reply to them) there is no loop. The self-send case
is a **test-only edge**: the bot's reply also round-trips to your number, and
only the adapter's `isFromMe` check stops a second pass. Keep the prompt
conversational (so the bot doesn't keep finding something to answer), and
**turn the `imessage` lab off / remove the config when done** — never leave a
self-send bot running unattended.
Watch the chain live:
```bash
tail -f /tmp/electron-dev.log | grep -iE "imessage|bridge|forward|Message API"
# the agent reply shows up as a fromMe:true row with the bot's text:
curl -sS -X POST "http://127.0.0.1:1234/api/v1/message/query?password=<PW>" \
-H 'Content-Type: application/json' -d '{"limit":5,"sort":"DESC"}'
```
`startTyping` will log a Private-API error unless BlueBubbles has the Private
API helper set up (needs a jailbroken / SIP-disabled Mac) — it's logged and
ignored; text replies still work.
## Known bugs / gotchas
- **Bug #1 — first-time save (fixed; guarded by the script).** BlueBubbles'
`GET /api/v1/webhook?url=<unregistered>` returns **HTTP 500**
(`Cannot read properties of null (reading 'events')`). The bridge must list
**all** webhooks and match client-side, never pass the `?url=` filter. If you
see `upsertConfig` fail with "An unhandled error has occurred!" originating in
`listWebhooks`, this regressed.
- **Save leaves a half-state on webhook failure.** `upsertConfig` writes the
config + starts the HTTP server _before_ registering the webhook, so a webhook
failure still reports `running:true` with the config persisted but no
BlueBubbles webhook. Always assert the BlueBubbles webhook list, not just IPC
status.
- **Unknown appId / forward failure → 500.** Posting to the local bridge for an
unknown appId, or when no cloud bot is bound, returns 500 (BlueBubbles retries
on 5xx). Auth (wrong secret → 401) is enforced before that.
- **Backend deploy lag.** Desktop dev proxies tRPC through `lobe-backend://` to
the _remote_ server. iMessage only appears in `listPlatforms` once the server
registration is deployed there, regardless of local branch.
- **Restart to load main-process fixes.** Editing `imessageBridgeSrv.ts` /
`@lobechat/chat-adapter-imessage` needs `electron-dev.sh restart` — main isn't
hot-replaced. On restart, enabled configs auto-register their webhook again.
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
#
# send-imessage-test.sh — Verify the outbound leg: desktop → BlueBubbles → iMessage
#
# Sends one real iMessage via the same REST call the Desktop bridge uses
# (`POST /api/v1/message/text`, which BlueBubblesApiClient.sendText wraps) and
# confirms it actually went out.
#
# KEY GOTCHA: with method=apple-script and a NEW conversation, the HTTP request
# often TIMES OUT even though the message is sent. Do NOT treat the timeout as a
# failure — instead poll `POST /api/v1/message/query` and check the message's
# `error` field (0 = sent OK). This script does that for you.
#
# This sends a REAL message, so it has side effects. Target your own number.
#
# Usage:
# ./send-imessage-test.sh <bb_password> <target_e164> [message] [bb_url]
#
# Example (send to your own phone, E.164 with country code):
# ./send-imessage-test.sh 'my-bb-pass' '+15551234567'
#
set -euo pipefail
BB_PASS="${1:?Usage: $0 <bb_password> <target_e164(+countrycode)> [message] [bb_url]}"
TARGET="${2:?Need a target handle in E.164, e.g. +15551234567 (or an Apple ID email)}"
MARKER="lobe-imsg-test-$(date +%s)"
MESSAGE="${3:-[${MARKER}] desktop bridge → BlueBubbles → iMessage outbound check}"
BB_URL="${4:-http://127.0.0.1:1234}"
CHAT_GUID="iMessage;-;${TARGET}"
echo "[send-test] target=${TARGET} marker=${MARKER}"
# 1) Fire the send. apple-script on a new chat may hang the HTTP response, so we
# cap it short and ignore a timeout — step 2 is the source of truth.
python3 - "$BB_PASS" "$BB_URL" "$CHAT_GUID" "$MESSAGE" <<'PY' || true
import json,sys,urllib.request,urllib.parse,uuid
pw,base,guid,msg=sys.argv[1:5]
url=base+"/api/v1/message/text?password="+urllib.parse.quote(pw)
body={"chatGuid":guid,"message":msg,"method":"apple-script","tempGuid":str(uuid.uuid4())}
req=urllib.request.Request(url,data=json.dumps(body).encode("utf-8"),
headers={"Content-Type":"application/json"},method="POST")
try:
r=urllib.request.urlopen(req,timeout=8)
print("[send-test] HTTP",r.status,"(immediate response)")
except urllib.error.HTTPError as e:
print("[send-test] HTTP",e.code,e.read().decode()[:200])
except Exception as e:
print("[send-test] HTTP request returned no body (likely apple-script delay):",type(e).__name__)
PY
# 2) Source of truth: find our marker in the message store and read its error.
echo "[send-test] verifying via message/query (the HTTP timeout above is expected)…"
sleep 3
python3 - "$BB_PASS" "$BB_URL" "$MARKER" <<'PY'
import json,sys,time,urllib.request,urllib.parse
pw,base,marker=sys.argv[1:4]
url=base+"/api/v1/message/query?password="+urllib.parse.quote(pw)
def query():
body={"limit":15,"offset":0,"with":["chats"],"sort":"DESC"}
req=urllib.request.Request(url,data=json.dumps(body).encode(),
headers={"Content-Type":"application/json"},method="POST")
return json.load(urllib.request.urlopen(req,timeout=12)).get("data") or []
hit=None
for _ in range(5):
for m in query():
if marker in (m.get("text") or "") and m.get("isFromMe"):
hit=m; break
if hit: break
time.sleep(2)
if not hit:
print("[send-test] ✗ outbound message not found in BB store — send likely failed")
sys.exit(1)
err=hit.get("error")
if err in (0,None):
print("[send-test] ✓ outbound message sent (fromMe=True, error=%s)"%err)
print("[send-test] → confirm it arrived in the Messages app on the target device")
else:
print("[send-test] ✗ BlueBubbles reported send error=%s"%err)
sys.exit(1)
PY
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
#
# test-imessage-bridge.sh — Regression test for the iMessage Desktop bridge
#
# Drives the Electron main-process `imessageBridge.*` IPC handlers plus the
# local bridge HTTP server and the BlueBubbles server, asserting the full
# connect/configure flow. Use it to regression-test PR work on the iMessage
# channel (BlueBubbles bridge) without clicking through the UI every time.
#
# Prerequisites:
# 1. BlueBubbles server running and reachable (default http://127.0.0.1:1234)
# 2. Electron dev running with CDP — `electron-dev.sh start`
# 3. `agent-browser` on PATH, connected to the same CDP port
#
# Usage:
# ./test-imessage-bridge.sh <bluebubbles_password> [bb_url] [cdp_port]
#
# Example:
# ./test-imessage-bridge.sh 'my-bb-password'
# ./test-imessage-bridge.sh 'my-bb-password' http://127.0.0.1:1234 9222
#
# Notes:
# - The password is passed as an argv, so it is visible in `ps`. This is a
# local dev tool; do not run it on shared machines with a real secret.
# - It uses a unique applicationId per run (imsg-regression-$$) and cleans up
# its own bridge config + BlueBubbles webhook on exit, so it is safe to
# re-run and does not disturb real configs.
set -euo pipefail
BB_PASS="${1:?Usage: $0 <bluebubbles_password> [bb_url] [cdp_port]}"
BB_URL="${2:-http://127.0.0.1:1234}"
CDP_PORT="${3:-9222}"
APP_ID="imsg-regression-$$"
SECRET="regression-secret-$$"
PASS=0
FAIL=0
# ── Output helpers ───────────────────────────────────────────────────
ok() { echo "$1"; PASS=$((PASS + 1)); }
bad() { echo "$1$2"; FAIL=$((FAIL + 1)); }
note() { echo "[imsg-test] $1"; }
# ── BlueBubbles REST helpers ─────────────────────────────────────────
bb_get_webhooks() {
curl -sS -m 8 "${BB_URL}/api/v1/webhook?password=${BB_PASS}"
}
# Delete every webhook whose URL mentions our APP_ID (cleanup is idempotent).
bb_cleanup_webhooks() {
local ids
ids=$(bb_get_webhooks | python3 -c '
import json,sys
try: d=json.load(sys.stdin)
except Exception: sys.exit(0)
for w in (d.get("data") or []):
if "'"$APP_ID"'" in (w.get("url") or ""): print(w["id"])
' 2>/dev/null || true)
for id in $ids; do
curl -sS -m 8 -X DELETE "${BB_URL}/api/v1/webhook/${id}?password=${BB_PASS}" >/dev/null 2>&1 || true
done
}
# ── IPC helper (drives the Electron renderer's electronAPI bridge) ───
# Runs a JS snippet that returns a string token; prints the raw token.
# The BlueBubbles password is base64-injected (atob) so special chars in the
# secret never need shell/JS quoting.
ipc_eval() {
local js="$1"
agent-browser --cdp "$CDP_PORT" eval -b "$(printf '%s' "$js" | base64)" 2>/dev/null
}
PASS_B64=$(printf '%s' "$BB_PASS" | base64)
# Emit an inline JS object literal for the bridge config. $1 overrides the
# password expression (defaults to atob of the real password); pass a JS string
# literal like "'wrong'" to test the rejection path.
ipc_config_js() {
local pwexpr="${1:-atob('${PASS_B64}')}"
printf "{applicationId:'%s',blueBubblesServerUrl:'%s',blueBubblesPassword:%s,enabled:true,webhookSecret:'%s'}" \
"$APP_ID" "$BB_URL" "$pwexpr" "$SECRET"
}
# ── Preflight ────────────────────────────────────────────────────────
note "BlueBubbles: ${BB_URL} CDP: ${CDP_PORT} appId: ${APP_ID}"
code=$(curl -sS -m 6 -o /dev/null -w '%{http_code}' \
"${BB_URL}/api/v1/server/info?password=${BB_PASS}" || echo 000)
if [ "$code" = "200" ]; then ok "BlueBubbles reachable + password valid"; else
bad "BlueBubbles preflight" "HTTP $code (is BlueBubbles running on ${BB_URL}?)"
echo "Aborting — fix BlueBubbles first."; exit 1
fi
if ! curl -sf --max-time 3 "http://localhost:${CDP_PORT}/json/version" >/dev/null 2>&1; then
bad "Electron CDP preflight" "CDP ${CDP_PORT} unreachable — run electron-dev.sh start"
echo "Aborting."; exit 1
fi
ok "Electron CDP reachable"
# Bridge must expose the IPC group (built from this branch's code).
probe=$(ipc_eval "(async()=>{try{var s=await window.electronAPI.invoke('imessageBridge.getStatus',{});return 'OK:'+JSON.stringify(s);}catch(e){return 'ERR:'+(e.message||e);}})()")
case "$probe" in
*OK:*) ok "imessageBridge IPC available" ;;
*) bad "imessageBridge IPC" "got: $probe (is the iMessage Desktop branch checked out?)"; echo "Aborting."; exit 1 ;;
esac
# Start clean: remove any leftover config for this appId + BB webhooks.
ipc_eval "(async()=>{try{await window.electronAPI.invoke('imessageBridge.removeConfig',{applicationId:'${APP_ID}'});}catch(e){}return 'done';})()" >/dev/null
bb_cleanup_webhooks
# ── testConfig: happy path ───────────────────────────────────────────
r=$(ipc_eval "(async()=>{try{var c=$(ipc_config_js);var x=await window.electronAPI.invoke('imessageBridge.testConfig',c);return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
case "$r" in
*OK:*success*true*) ok "testConfig with valid password → success" ;;
*) bad "testConfig (valid)" "got: $r" ;;
esac
# ── testConfig: wrong password rejects ───────────────────────────────
r=$(ipc_eval "(async()=>{try{var c=$(ipc_config_js "'definitely-wrong-password'");var x=await window.electronAPI.invoke('imessageBridge.testConfig',c);return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
case "$r" in
*ERR:*) ok "testConfig with wrong password → rejected" ;;
*) bad "testConfig (wrong password)" "expected rejection, got: $r" ;;
esac
# ── testConfig: unreachable URL rejects ──────────────────────────────
r=$(ipc_eval "(async()=>{try{var x=await window.electronAPI.invoke('imessageBridge.testConfig',{applicationId:'${APP_ID}',blueBubblesServerUrl:'http://127.0.0.1:65530',blueBubblesPassword:atob('${PASS_B64}'),enabled:true,webhookSecret:'${SECRET}'});return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
case "$r" in
*ERR:*) ok "testConfig with unreachable URL → rejected" ;;
*) bad "testConfig (unreachable)" "expected rejection, got: $r" ;;
esac
# ── upsertConfig: FIRST-TIME registration (Bug #1 regression guard) ──
# BlueBubbles' GET /webhook?url=<unregistered> returns HTTP 500. The bridge
# must list ALL webhooks and match client-side, otherwise this first save
# fails. This assertion guards that fix.
r=$(ipc_eval "(async()=>{try{var c=$(ipc_config_js);var x=await window.electronAPI.invoke('imessageBridge.upsertConfig',c);return 'OK:'+JSON.stringify(x);}catch(e){return 'ERR:'+(e.message||e);}})()")
case "$r" in
*OK:*success*true*) ok "upsertConfig first-time save → success (Bug #1 guard)" ;;
*) bad "upsertConfig (first-time)" "got: $r" ;;
esac
# ── getStatus: bridge running + config persisted ─────────────────────
# Return a quote-free token so grep isn't tripped up by agent-browser's
# JSON-string escaping of the eval result.
r=$(ipc_eval "(async()=>{var s=await window.electronAPI.invoke('imessageBridge.getStatus',{});var c=(s.configs||[]).find(function(x){return x.applicationId==='${APP_ID}';});return 'RUN='+(s.running?'Y':'N')+' CFG='+(c?'Y':'N')+' PW='+((c&&c.blueBubblesPasswordSet)?'Y':'N');})()")
echo "$r" | grep -q 'RUN=Y' && ok "bridge running" || bad "bridge running" "got: $r"
echo "$r" | grep -q 'CFG=Y' && ok "config persisted" || bad "config persisted" "got: $r"
echo "$r" | grep -q 'PW=Y' && ok "password stored (redacted in status)" || bad "password stored" "got: $r"
# ── BlueBubbles webhook actually registered ──────────────────────────
if bb_get_webhooks | grep -q "${APP_ID}"; then
ok "BlueBubbles webhook registered for appId"
else
bad "BlueBubbles webhook" "no webhook URL containing ${APP_ID}"
fi
# ── Local bridge HTTP server: secret enforcement ─────────────────────
BRIDGE_URL=$(ipc_eval "(async()=>{var s=await window.electronAPI.invoke('imessageBridge.getStatus',{});return s.serverUrl||'';})()" | tr -d '"')
if [ -n "$BRIDGE_URL" ]; then
# wrong secret → 401
code=$(curl -sS -m 6 -o /dev/null -w '%{http_code}' -X POST \
-H 'Content-Type: application/json' \
"${BRIDGE_URL}/webhooks/bluebubbles/${APP_ID}?secret=WRONG" \
-d '{"type":"new-message","data":{"guid":"x"}}' || echo 000)
[ "$code" = "401" ] && ok "local bridge rejects wrong secret (401)" || bad "local bridge wrong secret" "expected 401, got $code"
# right secret → passes auth (reaches forward; without a bound cloud bot it
# returns 5xx — that's fine, we're only asserting auth + routing here)
code=$(curl -sS -m 6 -o /dev/null -w '%{http_code}' -X POST \
-H 'Content-Type: application/json' \
"${BRIDGE_URL}/webhooks/bluebubbles/${APP_ID}?secret=${SECRET}" \
-d '{"type":"new-message","data":{"guid":"x","text":"hi"}}' || echo 000)
[ "$code" != "401" ] && ok "local bridge accepts valid secret (HTTP $code, past auth)" || bad "local bridge valid secret" "got 401 with correct secret"
else
bad "local bridge URL" "getStatus returned no serverUrl"
fi
# ── Cleanup ──────────────────────────────────────────────────────────
ipc_eval "(async()=>{try{await window.electronAPI.invoke('imessageBridge.removeConfig',{applicationId:'${APP_ID}'});await window.electronAPI.invoke('imessageBridge.stop',{});}catch(e){}return 'cleaned';})()" >/dev/null
bb_cleanup_webhooks
note "cleaned up config + BlueBubbles webhook for ${APP_ID}"
# ── Summary ──────────────────────────────────────────────────────────
echo ""
echo "[imsg-test] PASS=${PASS} FAIL=${FAIL}"
[ "$FAIL" -eq 0 ] || exit 1
@@ -0,0 +1,61 @@
# Lark / 飞书 Bot Testing
**App name:** `Lark` or `飞书` | **Process name:** `Lark` or `飞书`
See [references/osascript.md](../../references/osascript.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Lark (auto-detects Lark or 飞书)
osascript -e 'tell application "Lark" to activate' 2> /dev/null \
|| osascript -e 'tell application "飞书" to activate'
sleep 1
# Quick Switcher / Search (Cmd+K)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e '
set the clipboard to "bot-testing"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
```
## Send Message to Bot
```bash
osascript -e '
set the clipboard to "@MyBot help me with this task"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/lark-bot-response.png
```
## Lark-Specific Notes
- App name varies: `Lark` (international) vs `飞书` (China mainland) — the script auto-detects
- Uses `Cmd+K` for quick search (same as Discord/Slack)
- Enter sends message by default
- Always use clipboard paste for CJK characters
## Script
```bash
./.agents/skills/agent-testing/bot/lark/test-lark-bot.sh "bot-testing" "@MyBot hello"
./.agents/skills/agent-testing/bot/lark/test-lark-bot.sh "bot-testing" "Help me with this" 30
```
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
#
# test-lark-bot.sh — Send a message to a Lark/Feishu bot and capture the response
#
# Usage:
# ./scripts/test-lark-bot.sh <chat> <message> [wait_seconds] [screenshot_path]
#
# chat — Chat or contact name to search for
# message — Message to send to the bot
# wait_seconds — Seconds to wait for bot response (default: 10)
# screenshot_path — Output screenshot path (default: /tmp/lark-bot-test.png)
#
# Prerequisites:
# - Lark (飞书) desktop app installed and logged in
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
#
# Notes:
# - The app name may be "Lark" or "飞书" depending on version/locale
# - Uses Cmd+K to open search/quick switcher
# - Enter sends message by default
#
# Examples:
# ./scripts/test-lark-bot.sh "TestBot" "Hello"
# ./scripts/test-lark-bot.sh "bot-testing" "/ask Tell me a joke" 30
# ./scripts/test-lark-bot.sh "MyBot" "Help me summarize this" 60 /tmp/my-test.png
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHAT="${1:?Usage: test-lark-bot.sh <chat> <message> [wait_seconds] [screenshot_path]}"
MESSAGE="${2:?Usage: test-lark-bot.sh <chat> <message> [wait_seconds] [screenshot_path]}"
WAIT="${3:-10}"
SCREENSHOT="${4:-/tmp/lark-bot-test.png}"
# Detect app name — "Lark" or "飞书"
APP=""
if osascript -e 'tell application "Lark" to name' &>/dev/null; then
APP="Lark"
elif osascript -e 'tell application "飞书" to name' &>/dev/null; then
APP="飞书"
else
echo "[error] Lark/飞书 app not found. Install Lark or 飞书."
exit 1
fi
echo "[$APP] Activating..."
osascript -e "tell application \"$APP\" to activate"
sleep 1
echo "[$APP] Searching for chat: $CHAT"
osascript -e '
tell application "System Events"
-- Quick Switcher / Search (Cmd+K)
keystroke "k" using command down
delay 0.8
end tell
'
# Use clipboard for chat name (supports CJK characters)
osascript -e '
set the clipboard to "'"$CHAT"'"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter to select first result
end tell
'
sleep 2
echo "[$APP] Sending message: $MESSAGE"
osascript -e '
set the clipboard to "'"$MESSAGE"'"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter to send
end tell
'
echo "[$APP] Waiting ${WAIT}s for bot response..."
sleep "$WAIT"
echo "[$APP] Capturing screenshot..."
"$SCRIPT_DIR/../../scripts/capture-app-window.sh" "$APP" "$SCREENSHOT"
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
@@ -0,0 +1,62 @@
# QQ Bot Testing
**App name:** `QQ` | **Process name:** `QQ`
See [references/osascript.md](../../references/osascript.md) for shared patterns.
## Activate & Navigate
```bash
osascript -e 'tell application "QQ" to activate'
sleep 1
# Search for contact/group (Cmd+F)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.8
end tell
'
osascript -e '
set the clipboard to "bot-testing"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
```
## Send Message to Bot
```bash
osascript -e '
set the clipboard to "Hello bot!"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/qq-bot-response.png
```
## QQ-Specific Notes
- Enter sends message by default; Shift+Enter for newlines
- Uses `Cmd+F` for search (not `Cmd+K` like Discord/Slack/Lark)
- Always use clipboard paste for CJK characters
## Script
```bash
./.agents/skills/agent-testing/bot/qq/test-qq-bot.sh "bot-testing" "Hello bot" 15
./.agents/skills/agent-testing/bot/qq/test-qq-bot.sh "MyBot" "/help" 10
```
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# test-qq-bot.sh — Send a message to a QQ bot and capture the response
#
# Usage:
# ./scripts/test-qq-bot.sh <contact> <message> [wait_seconds] [screenshot_path]
#
# contact — Contact, group, or bot name to search for
# message — Message to send
# wait_seconds — Seconds to wait for bot response (default: 10)
# screenshot_path — Output screenshot path (default: /tmp/qq-bot-test.png)
#
# Prerequisites:
# - QQ desktop app installed and logged in
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
#
# Notes:
# - The app name is "QQ"
# - Uses Cmd+F to open search
# - Enter sends message by default; Shift+Enter for newlines
# - Uses clipboard paste for CJK character support
#
# Examples:
# ./scripts/test-qq-bot.sh "TestBot" "Hello"
# ./scripts/test-qq-bot.sh "bot-testing" "Hello bot" 30
# ./scripts/test-qq-bot.sh "MyBot" "/help" 15 /tmp/my-test.png
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTACT="${1:?Usage: test-qq-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
MESSAGE="${2:?Usage: test-qq-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
WAIT="${3:-10}"
SCREENSHOT="${4:-/tmp/qq-bot-test.png}"
APP="QQ"
echo "[$APP] Activating..."
osascript -e "tell application \"$APP\" to activate"
sleep 1
echo "[$APP] Searching for contact: $CONTACT"
osascript -e '
tell application "System Events"
-- Search (Cmd+F)
keystroke "f" using command down
delay 0.8
end tell
'
# Use clipboard for contact name (supports CJK characters)
osascript -e '
set the clipboard to "'"$CONTACT"'"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter to select first result
end tell
'
sleep 2
echo "[$APP] Sending message: $MESSAGE"
osascript -e '
set the clipboard to "'"$MESSAGE"'"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter to send
end tell
'
echo "[$APP] Waiting ${WAIT}s for bot response..."
sleep "$WAIT"
echo "[$APP] Capturing screenshot..."
"$SCRIPT_DIR/../../scripts/capture-app-window.sh" "$APP" "$SCREENSHOT"
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
@@ -0,0 +1,73 @@
# Slack Bot Testing
**App name:** `Slack` | **Process name:** `Slack`
See [references/osascript.md](../../references/osascript.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Slack
osascript -e 'tell application "Slack" to activate'
sleep 1
# Quick Switcher (Cmd+K)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e 'tell application "System Events" to keystroke "bot-testing"'
sleep 1
osascript -e 'tell application "System Events" to key code 36' # Enter
sleep 2
```
## Send Message to Bot
```bash
# Direct message input (focused after channel nav)
osascript -e 'tell application "System Events" to keystroke "@mybot hello"'
sleep 0.3
osascript -e 'tell application "System Events" to key code 36'
```
## Send Long Message
```bash
osascript -e '
tell application "Slack" to activate
delay 0.5
set the clipboard to "A long test message for the bot..."
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
## Slash Command Test
```bash
osascript -e '
tell application "Slack" to activate
delay 0.5
tell application "System Events"
keystroke "/ask What is the meaning of life?"
delay 0.5
key code 36
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/slack-bot-response.png
```
## Script
```bash
./.agents/skills/agent-testing/bot/slack/test-slack-bot.sh "bot-testing" "@mybot hello"
./.agents/skills/agent-testing/bot/slack/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
```
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
#
# test-slack-bot.sh — Send a message to a Slack bot and capture the response
#
# Usage:
# ./scripts/test-slack-bot.sh <channel> <message> [wait_seconds] [screenshot_path]
#
# channel — Channel name to navigate to via Quick Switcher (Cmd+K)
# message — Message to send (e.g., "@mybot hello" or "/ask question")
# wait_seconds — Seconds to wait for bot response (default: 10)
# screenshot_path — Output screenshot path (default: /tmp/slack-bot-test.png)
#
# Prerequisites:
# - Slack desktop app installed and logged in
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
#
# Examples:
# ./scripts/test-slack-bot.sh "bot-testing" "@mybot hello"
# ./scripts/test-slack-bot.sh "bot-testing" "/ask What is 2+2?" 20
# ./scripts/test-slack-bot.sh "general" "Hey bot" 15 /tmp/my-test.png
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CHANNEL="${1:?Usage: test-slack-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
MESSAGE="${2:?Usage: test-slack-bot.sh <channel> <message> [wait_seconds] [screenshot_path]}"
WAIT="${3:-10}"
SCREENSHOT="${4:-/tmp/slack-bot-test.png}"
APP="Slack"
echo "[$APP] Activating..."
osascript -e "tell application \"$APP\" to activate"
sleep 1
echo "[$APP] Navigating to channel: $CHANNEL"
osascript -e '
tell application "System Events"
-- Quick Switcher
keystroke "k" using command down
delay 0.8
keystroke "'"$CHANNEL"'"
delay 1.5
key code 36 -- Enter
end tell
'
sleep 2
echo "[$APP] Sending message: $MESSAGE"
osascript -e '
set the clipboard to "'"$MESSAGE"'"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
echo "[$APP] Waiting ${WAIT}s for bot response..."
sleep "$WAIT"
echo "[$APP] Capturing screenshot..."
"$SCRIPT_DIR/../../scripts/capture-app-window.sh" "$APP" "$SCREENSHOT"
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
@@ -0,0 +1,80 @@
# Telegram Bot Testing
**App name:** `Telegram` | **Process name:** `Telegram`
See [references/osascript.md](../../references/osascript.md) for shared patterns.
## Activate & Navigate
```bash
# Activate Telegram
osascript -e 'tell application "Telegram" to activate'
sleep 1
# Search for a bot (Cmd+F or click search)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.5
keystroke "MyTestBot"
delay 1
key code 36 -- Enter to select
end tell
'
sleep 2
```
## Send Message to Bot
```bash
# After navigating to bot chat, input is focused
osascript -e '
tell application "System Events"
keystroke "/start"
delay 0.3
key code 36
end tell
'
```
## Send Long Message
```bash
osascript -e '
tell application "Telegram" to activate
delay 0.5
set the clipboard to "Tell me about quantum computing in detail"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/telegram-bot-response.png
```
## Telegram Bot API (programmatic alternative)
For sending messages directly to the bot's chat without UI:
```bash
# Send message as the bot (for testing webhooks/responses)
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID&text=test message"
# Get recent updates
curl -s "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/getUpdates?limit=5" | jq .
```
## Script
```bash
./.agents/skills/agent-testing/bot/telegram/test-telegram-bot.sh "MyTestBot" "/start"
./.agents/skills/agent-testing/bot/telegram/test-telegram-bot.sh "GPTBot" "Hello" 60
```
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
#
# test-telegram-bot.sh — Send a message to a Telegram bot and capture the response
#
# Usage:
# ./scripts/test-telegram-bot.sh <bot_or_chat> <message> [wait_seconds] [screenshot_path]
#
# bot_or_chat — Bot username or chat name to search for
# message — Message to send to the bot
# wait_seconds — Seconds to wait for bot response (default: 10)
# screenshot_path — Output screenshot path (default: /tmp/telegram-bot-test.png)
#
# Prerequisites:
# - Telegram desktop app installed and logged in
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
#
# Notes:
# - The app name may be "Telegram" or "Telegram Desktop" depending on installation
# - Uses Cmd+F to search for the bot, then Enter to open the chat
#
# Examples:
# ./scripts/test-telegram-bot.sh "MyTestBot" "/start"
# ./scripts/test-telegram-bot.sh "MyTestBot" "Hello bot" 30
# ./scripts/test-telegram-bot.sh "GPTBot" "/ask What is AI?" 60 /tmp/my-test.png
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BOT="${1:?Usage: test-telegram-bot.sh <bot_or_chat> <message> [wait_seconds] [screenshot_path]}"
MESSAGE="${2:?Usage: test-telegram-bot.sh <bot_or_chat> <message> [wait_seconds] [screenshot_path]}"
WAIT="${3:-10}"
SCREENSHOT="${4:-/tmp/telegram-bot-test.png}"
# Detect app name — "Telegram" or "Telegram Desktop"
APP=""
if osascript -e 'tell application "Telegram" to name' &>/dev/null; then
APP="Telegram"
elif osascript -e 'tell application "Telegram Desktop" to name' &>/dev/null; then
APP="Telegram Desktop"
else
echo "[error] Telegram app not found. Install Telegram or Telegram Desktop."
exit 1
fi
echo "[$APP] Activating..."
osascript -e "tell application \"$APP\" to activate"
sleep 1
echo "[$APP] Searching for: $BOT"
osascript -e '
tell application "System Events"
-- Search (Escape first to clear any existing state)
key code 53 -- Escape
delay 0.3
keystroke "f" using command down
delay 0.8
keystroke "'"$BOT"'"
delay 2
key code 36 -- Enter to select first result
end tell
'
sleep 2
echo "[$APP] Sending message: $MESSAGE"
osascript -e '
set the clipboard to "'"$MESSAGE"'"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter
end tell
'
echo "[$APP] Waiting ${WAIT}s for bot response..."
sleep "$WAIT"
echo "[$APP] Capturing screenshot..."
"$SCRIPT_DIR/../../scripts/capture-app-window.sh" "$APP" "$SCREENSHOT"
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
@@ -0,0 +1,81 @@
# WeChat / 微信 Bot Testing
**App name:** `微信` or `WeChat` | **Process name:** `WeChat`
See [references/osascript.md](../../references/osascript.md) for shared patterns.
## Activate & Navigate
```bash
# Activate WeChat
osascript -e 'tell application "微信" to activate'
sleep 1
# Search for a contact/bot (Cmd+F)
osascript -e '
tell application "System Events"
keystroke "f" using command down
delay 0.5
keystroke "TestBot"
delay 1
key code 36 -- Enter to select
end tell
'
sleep 2
```
## Send Message
```bash
# After navigating to a chat, the input is focused
osascript -e '
tell application "System Events"
keystroke "Hello bot!"
delay 0.3
key code 36
end tell
'
```
## Send Long Message (clipboard)
```bash
osascript -e '
tell application "微信" to activate
delay 0.5
set the clipboard to "Please help me with this task..."
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
```
## Verify Response
```bash
sleep 10
screencapture /tmp/wechat-bot-response.png
```
## WeChat-Specific Notes
- WeChat macOS app name can be `微信` or `WeChat` depending on system language. Try both:
```bash
osascript -e 'tell application "微信" to activate' 2> /dev/null \
|| osascript -e 'tell application "WeChat" to activate'
```
- WeChat uses **Enter** to send (not Cmd+Enter by default, but configurable)
- For multi-line messages without sending, use **Shift+Enter**:
```bash
osascript -e 'tell application "System Events" to key code 36 using shift down'
```
- Always use clipboard paste for CJK characters — `keystroke` mangles non-ASCII
## Script
```bash
./.agents/skills/agent-testing/bot/wechat/test-wechat-bot.sh "文件传输助手" "test message" 5
./.agents/skills/agent-testing/bot/wechat/test-wechat-bot.sh "MyBot" "Tell me a joke" 30
```
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
#
# test-wechat-bot.sh — Send a message to a WeChat bot and capture the response
#
# Usage:
# ./scripts/test-wechat-bot.sh <contact> <message> [wait_seconds] [screenshot_path]
#
# contact — Contact or bot name to search for
# message — Message to send
# wait_seconds — Seconds to wait for bot response (default: 10)
# screenshot_path — Output screenshot path (default: /tmp/wechat-bot-test.png)
#
# Prerequisites:
# - WeChat (微信) desktop app installed and logged in
# - Accessibility permission granted (System Preferences > Privacy > Accessibility)
#
# Notes:
# - The app name may be "微信" or "WeChat" depending on system language
# - WeChat sends on Enter by default; use Shift+Enter for newlines
# - For Chinese text, always uses clipboard paste (keystroke can't handle CJK)
#
# Examples:
# ./scripts/test-wechat-bot.sh "TestBot" "Hello"
# ./scripts/test-wechat-bot.sh "文件传输助手" "test message" 5
# ./scripts/test-wechat-bot.sh "MyBot" "Tell me a joke" 30 /tmp/my-test.png
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONTACT="${1:?Usage: test-wechat-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
MESSAGE="${2:?Usage: test-wechat-bot.sh <contact> <message> [wait_seconds] [screenshot_path]}"
WAIT="${3:-10}"
SCREENSHOT="${4:-/tmp/wechat-bot-test.png}"
# Detect app name — "微信" or "WeChat"
APP=""
if osascript -e 'tell application "微信" to name' &>/dev/null; then
APP="微信"
elif osascript -e 'tell application "WeChat" to name' &>/dev/null; then
APP="WeChat"
else
echo "[error] WeChat app not found. Install 微信 (WeChat)."
exit 1
fi
echo "[$APP] Activating..."
osascript -e "tell application \"$APP\" to activate"
sleep 1
echo "[$APP] Searching for contact: $CONTACT"
osascript -e '
tell application "System Events"
-- Search (Cmd+F)
keystroke "f" using command down
delay 0.8
end tell
'
# Use clipboard for contact name (supports CJK characters)
osascript -e '
set the clipboard to "'"$CONTACT"'"
tell application "System Events"
keystroke "v" using command down
delay 1.5
key code 36 -- Enter to select first result
end tell
'
sleep 2
echo "[$APP] Sending message: $MESSAGE"
# Always use clipboard paste — keystroke can't handle CJK or special characters
osascript -e '
set the clipboard to "'"$MESSAGE"'"
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36 -- Enter to send
end tell
'
echo "[$APP] Waiting ${WAIT}s for bot response..."
sleep "$WAIT"
echo "[$APP] Capturing screenshot..."
"$SCRIPT_DIR/../../scripts/capture-app-window.sh" "$APP" "$SCREENSHOT"
echo "[$APP] Done! Screenshot saved to $SCREENSHOT"
+142
View File
@@ -0,0 +1,142 @@
# CLI Backend Verification
Default surface for verifying **backend changes** (TRPC routers, services,
models, migrations) end-to-end: fastest loop, text-assertable output, zero UI
flakiness.
## When to use
- Verifying TRPC router / service / model changes end-to-end
- Testing new API fields or response structure changes
- Validating CLI command output after backend modifications
- Debugging data flow issues between server and CLI
## Prerequisites
| Requirement | Details |
| ------------ | --------------------------------------------------------------------------------- |
| Dev server | `localhost:3010` — see [../references/dev-server.md](../references/dev-server.md) |
| CLI source | `apps/cli/` — runs from source, no rebuild; standalone `node_modules` — run `pnpm install` inside `apps/cli/` (root install does not cover it) |
| CLI dev mode | `LOBEHUB_CLI_HOME=.lobehub-dev` for isolated credentials |
| Auth | Device Code Flow login — see [../references/auth.md](../references/auth.md) |
All CLI dev commands run from `apps/cli/`. Subsequent examples use `$CLI`:
```bash
CLI="LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts"
```
## Workflow
### Step 1 — Server up?
See [../references/dev-server.md](../references/dev-server.md) for the health
check, start, and restart commands. Server-side code changes require a restart.
### Step 2 — Auth ready?
```bash
./.agents/skills/agent-testing/scripts/setup-auth.sh status
```
If the CLI is not logged in, **the user must run the login themselves**
(interactive browser authorization):
```bash
cd apps/cli && LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts login --server http://localhost:3010
```
Credentials persist in `apps/cli/.lobehub-dev/`. Details:
[../references/auth.md](../references/auth.md).
### Step 3 — Test with CLI commands
CLI runs from source, so CLI-side code changes take effect immediately without
rebuilding:
```bash
cd apps/cli
$CLI <command>
```
Capture output for the report as you go (e.g. `$CLI task list | tee "$DIR/assets/task-list.txt"`).
### Step 4 — Clean up test data
```bash
$CLI task delete < id > -y
$CLI agent delete < id > -y
```
### Step 5 — Report
Finish with a structured report —
[../references/report.md](../references/report.md). CLI evidence = exact
command + trimmed output.
## Common testing patterns
### Task system
```bash
$CLI task list
$CLI task create -n "Root Task" -i "Test instruction"
$CLI task create -n "Child Task" -i "Sub instruction" --parent T-1
$CLI task view T-1
$CLI task tree T-1
$CLI task edit T-1 --status running
$CLI task comment T-1 -m "Test comment"
$CLI task delete T-1 -y
```
### Agent system
```bash
$CLI agent list
$CLI agent view <agent-id>
$CLI agent run <agent-id> -m "Test prompt"
```
### Document & knowledge base
```bash
$CLI doc list
$CLI doc create -t "Test Doc" -c "Content here"
$CLI doc view <doc-id>
$CLI kb list
$CLI kb tree <kb-id>
```
### Model & provider
```bash
$CLI model list
$CLI provider list
$CLI provider test <provider-id>
```
## Dev-test cycle
```
1. Make code changes (service/model/router/type)
|
2. Run unit tests (fast feedback)
bunx vitest run --silent='passed-only' '<test-file>'
|
3. Restart dev server (if server-side changes — see dev-server.md)
|
4. CLI verification (end-to-end)
$CLI <command>
|
5. Clean up test data + write the report
```
## Troubleshooting
| Issue | Solution |
| --------------------------- | ----------------------------------------------- |
| `No authentication found` | Run `login --server http://localhost:3010` |
| `UNAUTHORIZED` on API calls | Token expired; re-run login |
| `ECONNREFUSED` | Dev server not running — see dev-server.md |
| CLI shows old data/behavior | Server needs restart to pick up code changes |
| Login opens wrong server | Must use `--server` flag (env var doesn't work) |
@@ -0,0 +1,257 @@
# agent-browser CLI Reference
Generic reference for the `agent-browser` CLI — automate Chromium-based apps (Electron, Chrome, web) via Chrome DevTools Protocol. LobeHub-specific patterns live in [../ui/electron.md](../ui/electron.md) and [../ui/web.md](../ui/web.md); authentication recipes live in [auth.md](./auth.md).
Use `agent-browser` to automate Chromium-based apps via Chrome DevTools Protocol.
Install via `npm i -g agent-browser`, `brew install agent-browser`, or `cargo install agent-browser`. Run `agent-browser install` to download Chrome. Run `agent-browser upgrade` to update.
## Core Workflow
Every browser automation follows this pattern:
1. **Navigate**: `agent-browser open <url>`
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
3. **Interact**: Use refs to click, fill, select
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
```bash
agent-browser open https://example.com/form
agent-browser snapshot -i
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
agent-browser fill @e1 "user@example.com"
agent-browser fill @e2 "password123"
agent-browser click @e3
agent-browser wait --load networkidle
agent-browser snapshot -i # Check result
```
## Command Chaining
```bash
# Chain open + wait + snapshot in one call
agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i
```
Use `&&` when you don't need to read intermediate output. Run commands separately when you need to parse output first (e.g., snapshot to discover refs, then interact).
## Essential Commands
```bash
# Navigation
agent-browser open <url> # Navigate (aliases: goto, navigate)
agent-browser close # Close browser
agent-browser close --all # Close all active sessions
# Snapshot
agent-browser snapshot -i # Interactive elements with refs (recommended)
agent-browser snapshot -s "#selector" # Scope to CSS selector
# Interaction (use @refs from snapshot)
agent-browser click @e1 # Click element
agent-browser click @e1 --new-tab # Click and open in new tab
agent-browser fill @e2 "text" # Clear and type text
agent-browser type @e2 "text" # Type without clearing
agent-browser select @e1 "option" # Select dropdown option
agent-browser check @e1 # Check checkbox
agent-browser press Enter # Press key
agent-browser keyboard type "text" # Type at current focus (no selector)
agent-browser keyboard inserttext "text" # Insert without key events
agent-browser scroll down 500 # Scroll page
agent-browser scroll down 500 --selector "div.content" # Scroll within container
# Get information
agent-browser get text @e1 # Get element text
agent-browser get url # Get current URL
agent-browser get title # Get page title
agent-browser get cdp-url # Get CDP WebSocket URL
# Wait
agent-browser wait @e1 # Wait for element
agent-browser wait --load networkidle # Wait for network idle
agent-browser wait --url "**/page" # Wait for URL pattern
agent-browser wait 2000 # Wait milliseconds
agent-browser wait --text "Welcome" # Wait for text to appear
agent-browser wait --fn "!document.body.innerText.includes('Loading...')" # Wait for text to disappear
agent-browser wait "#spinner" --state hidden # Wait for element to disappear
# Downloads
agent-browser download @e1 ./file.pdf # Click element to trigger download
agent-browser wait --download ./output.zip # Wait for any download to complete
# Network
agent-browser network requests # Inspect tracked requests
agent-browser network requests --type xhr,fetch # Filter by resource type
agent-browser network requests --method POST # Filter by HTTP method
agent-browser network route "**/api/*" --abort # Block matching requests
agent-browser network har start # Start HAR recording
agent-browser network har stop ./capture.har # Stop and save HAR file
# Viewport & Device Emulation
agent-browser set viewport 1920 1080 # Set viewport size (default: 1280x720)
agent-browser set viewport 1920 1080 2 # 2x retina
agent-browser set device "iPhone 14" # Emulate device (viewport + user agent)
# Capture
agent-browser screenshot # Screenshot to temp dir
agent-browser screenshot --full # Full page screenshot
agent-browser screenshot --annotate # Annotated screenshot with numbered element labels
agent-browser pdf output.pdf # Save as PDF
# Clipboard
agent-browser clipboard read # Read text from clipboard
agent-browser clipboard write "text" # Write text to clipboard
agent-browser clipboard copy # Copy current selection
agent-browser clipboard paste # Paste from clipboard
# Dialogs (alert, confirm, prompt, beforeunload)
agent-browser dialog accept # Accept dialog
agent-browser dialog accept "input" # Accept prompt dialog with text
agent-browser dialog dismiss # Dismiss/cancel dialog
agent-browser dialog status # Check if dialog is open
# Diff (compare page states)
agent-browser diff snapshot # Compare current vs last snapshot
agent-browser diff screenshot --baseline before.png # Visual pixel diff
agent-browser diff url <url1> <url2> # Compare two pages
# Streaming
agent-browser stream enable # Start WebSocket streaming
agent-browser stream status # Inspect streaming state
agent-browser stream disable # Stop streaming
```
## Batch Execution
```bash
echo '[
["open", "https://example.com"],
["snapshot", "-i"],
["click", "@e1"],
["screenshot", "result.png"]
]' | agent-browser batch --json
```
## Authentication
```bash
# Option 1: Auth vault (credentials stored encrypted)
echo "$PASSWORD" | agent-browser auth save myapp --url https://app.example.com/login --username user --password-stdin
agent-browser auth login myapp
# Option 2: Session name (auto-save/restore cookies + localStorage)
agent-browser --session-name myapp open https://app.example.com/login
agent-browser close # State auto-saved
agent-browser --session-name myapp open https://app.example.com/dashboard # Auto-restored
# Option 3: Persistent profile
agent-browser --profile ~/.myapp open https://app.example.com/login
# Option 4: State file
agent-browser state save auth.json
agent-browser state load auth.json
```
### LobeHub dev server — inject better-auth cookie
`agent-browser --headed` on macOS can create an off-screen Chromium window, blocking manual login. For a local LobeHub dev server (e.g. `localhost:3010`), copy the `better-auth.session_token` cookie out of a **Network request** in the user's own Chrome DevTools and load it via `state load`. See [auth.md](./auth.md) for the full recipe.
## Semantic Locators (Alternative to Refs)
```bash
agent-browser find text "Sign In" click
agent-browser find label "Email" fill "user@test.com"
agent-browser find role button click --name "Submit"
agent-browser find placeholder "Search" type "query"
agent-browser find testid "submit-btn" click
```
## JavaScript Evaluation (eval)
```bash
# Simple expressions
agent-browser eval 'document.title'
# Complex JS: use --stdin with heredoc (RECOMMENDED)
agent-browser eval --stdin << 'EVALEOF'
JSON.stringify(
Array.from(document.querySelectorAll("img"))
.filter(i => !i.alt)
.map(i => ({ src: i.src.split("/").pop(), width: i.width }))
)
EVALEOF
# Base64 encoding (avoids all shell escaping issues)
agent-browser eval -b "$(echo -n 'document.title' | base64)"
```
## Ref Lifecycle
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after clicking links/buttons that navigate, form submissions, or dynamic content loading.
## Annotated Screenshots (Vision Mode)
```bash
agent-browser screenshot --annotate
# Output includes the image path and a legend:
# [1] @e1 button "Submit"
# [2] @e2 link "Home"
agent-browser click @e2 # Click using ref from annotated screenshot
```
## Parallel Sessions
```bash
agent-browser --session site1 open https://site-a.com
agent-browser --session site2 open https://site-b.com
agent-browser session list
```
## Connect to Existing Chrome
```bash
agent-browser --auto-connect snapshot # Auto-discover running Chrome
agent-browser --cdp 9222 snapshot # Explicit CDP port
```
## iOS Simulator (Mobile Safari)
```bash
agent-browser device list
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
agent-browser -p ios snapshot -i
agent-browser -p ios tap @e1
agent-browser -p ios swipe up
agent-browser -p ios screenshot mobile.png
agent-browser -p ios close
```
## Observability Dashboard
```bash
agent-browser dashboard install
agent-browser dashboard start # Background server on port 4848
agent-browser dashboard stop
```
## Cloud Providers
Use `-p <provider>` to run against cloud browsers: `agentcore`, `browserbase`, `browserless`, `browseruse`, `kernel`.
## Browser Engine Selection
```bash
agent-browser --engine lightpanda open example.com # 10x faster, 10x less memory
```
## Gotchas
- **Daemon can get stuck** — if commands hang, `agent-browser close --all` or `pkill -f agent-browser` to reset
- **HMR invalidates everything** — after code changes, refs break. Re-snapshot or restart
- **`snapshot -i` doesn't find contenteditable** — use `snapshot -i -C` for rich text editors
- **`fill` doesn't work on contenteditable** — use `type` for chat inputs
- **Screenshots go to `~/.agent-browser/tmp/screenshots/`** — read them with the `Read` tool
- **Dialogs block all commands** — if commands time out, check `agent-browser dialog status`
- **Default timeout is 25s** — override with `AGENT_BROWSER_DEFAULT_TIMEOUT` (ms) or use explicit waits
- **Shell quoting corrupts eval** — use `eval --stdin <<'EVALEOF'` for complex JS
@@ -0,0 +1,93 @@
# LobeHub gateway streaming + tab-switch test harness
Captures store + DOM state at 200ms intervals so we can prove or disprove
claims like "切回 tab 后消息回到了很早以前". Built for gateway-mode chat but
works for any LobeHub streaming session.
## Files
`scripts/agent-gateway/`
| File | Role |
| --------------- | ---------------------------------------------------------------- |
| `probe.js` | Injects a 200ms sampler + `__PROBE_EVENT` marker + `__switchTab` |
| `probe-dump.js` | Stops the sampler and returns `{events, samples}` as JSON string |
| `tab-switch.js` | Runs N round-trip switches between two tabs, marks each step |
| `analyze.mjs` | Node post-processor: timeline + regression detection |
## Standard workflow
```bash
# 1. Start Electron with CDP
./.agents/skills/agent-testing/scripts/electron-dev.sh start
# 2. Navigate to a chat, switch runtime to Cloud Sandbox (gateway mode)
# 3. Install the probe + helpers
agent-browser --cdp 9222 eval --stdin \
< .agents/skills/agent-testing/scripts/agent-gateway/probe.js
# 4. Send a tool-call message — manually or via type+press
agent-browser --cdp 9222 eval "window.__PROBE_EVENT('SENT')"
# 5. Run the multi-switch driver (auto-picks active tab as BACK and the
# rightmost inactive tab as AWAY — edit ROUND_TRIPS / DWELL_MS in the
# file if you want different timing)
agent-browser --cdp 9222 eval --stdin \
< .agents/skills/agent-testing/scripts/agent-gateway/tab-switch.js
# 6. Wait for streaming to finish, then dump
agent-browser --cdp 9222 eval --stdin \
< .agents/skills/agent-testing/scripts/agent-gateway/probe-dump.js \
> /tmp/probe.json
# 7. Analyze
node .agents/skills/agent-testing/scripts/agent-gateway/analyze.mjs /tmp/probe.json
```
The analyzer prints three sections: EVENTS, TIMELINE, REGRESSIONS. If
REGRESSIONS is non-empty it means content/reasoning/childN dropped on the
same topic — the symptom users describe.
## What the probe tracks (and why)
`chat.messagesMap` only stores the top-level `assistantGroup` shell. The
actual streamed content, reasoning, and tool calls live in
`assistantGroup.children: AssistantContentBlock[]`. Any probe that only
reads `m.content` / `m.reasoning` will see zeros throughout streaming and
miss everything that matters. probe.js walks both levels and sums:
- `cT` total content length
- `rT` total reasoning length
- `toolT` total tool-call count
- `childN` number of content blocks
Plus DOM-side signals (`domLen`, search/crawl indicator counts) so you can
tell store-side regressions apart from render-side regressions.
## Gotchas
- **Optimistic new-topic state.** Before the first chunk lands, messages
live under the `<scope>_new` key with `tmp_*` ids and no `topicId` field.
probe.js falls back to those when `activeTopicId` is null.
- **Reasoning resets to 0 are not bugs.** When the assistant finishes
thinking and starts tool-use or text, the streaming reasoning buffer
empties and the finalised reasoning gets sealed into a completed block.
Filter these out manually if needed.
- **DOM length jitters by a handful of chars** because counters like "(10)"
in tool-call labels change as results arrive. analyze.mjs only flags
`domLen` drops greater than 100 chars to ignore that noise.
- **Never identify tabs by innerText.** The active tab's text embeds a
` · <agent name>` suffix, so a search like `'LobeHub Growth'` matches the
active tab when the active agent happens to be LobeHub Growth — and you
end up clicking the tab you're already on. probe.js uses the stable
`data-contextmenu-trigger` attribute (a React `useId()` value that's set
per-tab and survives focus changes) plus `data-active="true"` to mark
the active one. Helpers exposed:
`__listTabs()` / `__clickTabByKey(key)` / `__clickTabByIndex(i)` /
`__activeTabKey()`.
- **`tab-switch.js` fires-and-forgets.** The IIFE kicks off an async loop
and returns immediately so the agent-browser CLI eval doesn't blow past
its default 25 s timeout. Wait on the `SWITCH_LOOP_DONE` event marker
before dumping. Re-running while a loop is in flight is refused — the
chaotic data from overlapping runs is not worth debugging.
@@ -0,0 +1,123 @@
# Auth Setup for Local Agent Testing
**Auth is the gate for all automated testing.** Prepare and verify it before
writing any test step. The one-stop entry point is:
```bash
SCRIPT=".agents/skills/agent-testing/scripts/setup-auth.sh"
$SCRIPT status # check server + CLI + web auth readiness
$SCRIPT cli # interactive CLI device-code login (must be run by the user)
pbpaste | $SCRIPT web # inject a copied Cookie header into the agent-browser session
$SCRIPT web-verify # live-check that the agent-browser session is authenticated
```
`SERVER_URL` defaults to `http://localhost:3010` (this repo's `dev:next` port).
Override it when testing against another server (e.g. `SERVER_URL=http://localhost:3011`
in the cloud repo).
## Per-surface overview
| Surface | Mechanism | Persistence | Human interaction |
| -------- | ---------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------- |
| CLI | OIDC Device Code Flow | `apps/cli/.lobehub-dev/settings.json` | Yes — browser authorization, every token expiry |
| Web | better-auth cookie injection | `~/.lobehub-agent-testing/web-state.json` + agent-browser session | Copy the Cookie header once per token rotation |
| Electron | App's own login state | Electron user-data dir | Log in once manually in the app |
| Bot | Native apps (Discord/WeChat/…) logged in | Each app's own session | Once per app |
## CLI — Device Code Flow
Credentials are isolated from the user's real CLI config via
`LOBEHUB_CLI_HOME=.lobehub-dev` (kept inside `apps/cli/`, gitignored).
Login requires interactive browser authorization, so **the user must run it
themselves** (e.g. via the `!` prefix in Claude Code):
```bash
cd apps/cli && LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts login --server http://localhost:3010
```
- The `--server` flag is required — an env var does NOT work and login will hit
the wrong server without it.
- Check state without logging in: `setup-auth.sh status` (verifies
`settings.json` exists and `serverUrl` matches).
- `UNAUTHORIZED` on API calls means the token expired — re-run login.
## Web — better-auth cookie injection (agent-browser)
`agent-browser --headed` on macOS often creates the Chromium window off-screen —
the user can't see or interact with it, so manual login inside the agent-browser
session fails. Instead, copy the **better-auth session cookie** out of the
user's own logged-in Chrome and inject it as a Playwright-style state file.
Do **not** use this on production URLs — only local dev. Treat the cookie as a
secret: don't paste it into shared logs, PRs, or commit it anywhere.
### One-key path
1. Ask the user to copy the Cookie header **from a Network request, NOT
`document.cookie`** (`document.cookie` cannot see HttpOnly cookies, which is
exactly where better-auth puts its session):
- Open the logged-in tab (`http://localhost:<port>/…`) in Chrome.
- `Cmd+Option+I`**Network** tab → refresh → click any same-origin request.
- Under **Request Headers**, right-click the `Cookie:` line → **Copy value**.
2. Inject and verify in one shot:
```bash
pbpaste | ./.agents/skills/agent-testing/scripts/setup-auth.sh web
```
The script filters the header down to the better-auth cookies
(`better-auth.session_token`, `better-auth.state`), builds the Playwright
`storageState` JSON, loads it into the `agent-browser` session (default name
`lobehub-dev`), opens `SERVER_URL`, and asserts the URL is not `/signin`.
### Using the authenticated session
```bash
agent-browser --session lobehub-dev open "http://localhost:3010/"
agent-browser --session lobehub-dev snapshot -i | head -20
# Look for the user's avatar/name in the sidebar, or absence of the signin form.
```
### Notes
- `storageState` doesn't enforce the HttpOnly flag on load — the script stores
cookies with `httpOnly: false`, which is fine for local dev and sidesteps a
CDP-context quirk where HttpOnly cookies sometimes fail to attach.
- The state file is kept at `~/.lobehub-agent-testing/web-state.json` so
`setup-auth.sh status` can report web-auth readiness across sessions.
### Common failure modes
| Symptom | Cause | Fix |
| --------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- |
| Still redirects to `/signin` after injection | User pasted from `document.cookie` → missed HttpOnly session | Re-pull from Network request Headers, not console |
| Script reports `no better-auth cookies found` | Separator wrong, or user pasted URL-decoded value | Keep the raw `Cookie:` header as-is |
| Login works briefly then expires | `better-auth.session_token` rotated (user logged out / signed in again) | Re-copy and re-inject |
| Domain mismatch | Cookie domain must be `localhost` literally, no leading dot for local dev | — |
## Electron
The desktop app keeps its own persistent login state in its user-data
directory — log in once manually inside the app and it survives restarts of
`electron-dev.sh`. No injection needed. The standard check (do NOT hand-roll a
store eval) once Electron is up with CDP:
```bash
./.agents/skills/agent-testing/scripts/app-probe.sh auth
# → {"ok":true,"isSignedIn":true,"userId":"user_xxx"}
```
`setup-auth.sh status` runs this probe automatically when CDP 9222 is
reachable.
## Scope
These recipes only cover **local dev** authentication. They do not:
- Work for production — production cookies are `Secure; HttpOnly; Domain=.lobehub.com`
and must be delivered over HTTPS.
- Replace real OAuth flows — tests that must exercise the login UI itself need a
real Chromium with `--remote-debugging-port` or a bot account.
- Flow cookies back to the user's Chrome — injection is one-way.
@@ -0,0 +1,55 @@
# Local Dev Server
Single source of truth for starting / restarting the backend that all test
surfaces (CLI, Electron, Web) hit.
## Ports & modes
| Command | What it runs | Port |
| ------------------- | --------------------------------------------------------- | --------------------------------- |
| `pnpm run dev:next` | Next.js backend (API + auth) | `3010` |
| `bun run dev` | Full-stack (Next.js + Vite SPA, via `devStartupSequence`) | `3010` (API) + SPA |
| `bun run dev:spa` | Vite SPA only, proxies API to `3010` | `9876` (prints a Debug Proxy URL) |
In the **cloud repo** (where this repo is the `lobehub/` submodule) the dev
server conventionally runs on `3011` — set `SERVER_URL=http://localhost:3011`
for the scripts in this skill when testing there.
## Health check
```bash
curl -s -o /dev/null -w '%{http_code}' http://localhost:3010/
```
## Start / restart
```bash
# Start (from repo root)
pnpm run dev:next
# Restart — required to pick up server-side code changes
lsof -ti:3010 | xargs kill
pnpm run dev:next
```
## When a server restart is needed
Next.js hot-reload may not pick up changes in workspace packages — restart when
in doubt.
| Change location | Restart? |
| ----------------------------------------------- | -------- |
| `apps/server/src/` (routers, services, modules) | Yes |
| `src/server/` (agent-hono, workflows-hono) | Yes |
| `packages/database/` (models) | Yes |
| `packages/types/` | Yes |
| `packages/prompts/` | Yes |
| `apps/cli/` (CLI runs from source) | No |
## Troubleshooting
| Issue | Solution |
| ------------------------- | ------------------------------------------------------- |
| `ECONNREFUSED` | Server not running — start it |
| `EADDRINUSE` on the port | Already running — `lsof -ti:<port> \| xargs kill` first |
| Stale data / old behavior | Server needs a restart to pick up code changes |
@@ -0,0 +1,217 @@
# osascript Common Patterns
Shared AppleScript / `osascript` patterns used by all platform bot tests. Read this first, then refer to the per-platform file for app-specific quirks.
## Core Patterns
### Activate an App
```bash
osascript -e 'tell application "Discord" to activate'
```
### Type Text
```bash
# Type character by character (reliable, but slow for long text)
osascript -e 'tell application "System Events" to keystroke "Hello world"'
# Press Enter
osascript -e 'tell application "System Events" to key code 36'
# Press Tab
osascript -e 'tell application "System Events" to key code 48'
# Press Escape
osascript -e 'tell application "System Events" to key code 53'
```
### Paste from Clipboard (fast, for long text)
```bash
# Set clipboard and paste — much faster than keystroke for long messages
osascript -e 'set the clipboard to "Your long message here"'
osascript -e 'tell application "System Events" to keystroke "v" using command down'
```
Or in one shot:
```bash
osascript -e '
set the clipboard to "Your long message here"
tell application "System Events" to keystroke "v" using command down
'
```
### Keyboard Shortcuts
```bash
# Cmd+K (quick switcher in Discord/Slack)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
# Cmd+F (search)
osascript -e 'tell application "System Events" to keystroke "f" using command down'
# Cmd+N (new message/chat)
osascript -e 'tell application "System Events" to keystroke "n" using command down'
# Cmd+Shift+K (example: multi-modifier)
osascript -e 'tell application "System Events" to keystroke "k" using {command down, shift down}'
```
### Click at Position
```bash
# Click at absolute screen coordinates
osascript -e '
tell application "System Events"
click at {500, 300}
end tell
'
```
### Get Window Info
```bash
# Get window position and size
osascript -e '
tell application "System Events"
tell process "Discord"
get {position, size} of window 1
end tell
end tell
'
```
### Screenshot
```bash
# Full screen
screencapture /tmp/screenshot.png
# Interactive region select
screencapture -i /tmp/screenshot.png
# Specific window (by window ID from CGWindowList)
screencapture -l < WINDOW_ID > /tmp/screenshot.png
```
To get window ID for a specific app:
```bash
osascript -e '
tell application "System Events"
tell process "Discord"
get id of window 1
end tell
end tell
'
```
### Read Accessibility Elements
```bash
# Get all UI elements of the frontmost window (can be slow/large)
osascript -e '
tell application "System Events"
tell process "Discord"
entire contents of window 1
end tell
end tell
'
# Get a specific element's value
osascript -e '
tell application "System Events"
tell process "Discord"
get value of text field 1 of window 1
end tell
end tell
'
```
> **Warning:** `entire contents` can be extremely slow on complex UIs. Prefer screenshots + `Read` tool for visual verification.
### Read Screen Text via Clipboard
For reading the latest message or response from an app:
```bash
# Select all text in the focused area and copy
osascript -e '
tell application "System Events"
keystroke "a" using command down
keystroke "c" using command down
end tell
'
sleep 0.5
# Read clipboard
pbpaste
```
---
## Common Bot Testing Workflow
Regardless of platform, the pattern is:
```bash
APP_NAME="Discord" # or "Slack", "Telegram", "微信"
CHANNEL="bot-testing"
MESSAGE="Hello bot!"
WAIT_SECONDS=10
# 1. Activate
osascript -e "tell application \"$APP_NAME\" to activate"
sleep 1
# 2. Navigate to channel/chat (via Quick Switcher or Search)
osascript -e 'tell application "System Events" to keystroke "k" using command down'
sleep 0.5
osascript -e "tell application \"System Events\" to keystroke \"$CHANNEL\""
sleep 1
osascript -e 'tell application "System Events" to key code 36'
sleep 2
# 3. Send message
osascript -e "set the clipboard to \"$MESSAGE\""
osascript -e '
tell application "System Events"
keystroke "v" using command down
delay 0.3
key code 36
end tell
'
# 4. Wait for bot response
sleep "$WAIT_SECONDS"
# 5. Screenshot for verification
screencapture /tmp/"${APP_NAME,,}"-bot-test.png
echo "Result saved to /tmp/${APP_NAME,,}-bot-test.png"
```
### Tips
- **Use clipboard paste** (`Cmd+V`) for messages containing special characters or long text — `keystroke` can mangle non-ASCII
- **Add `delay`** between actions — apps need time to process UI events
- **Screenshot for verification** — use `screencapture` + `Read` tool for visual checks
- **Use a dedicated test channel/chat** — avoid polluting real conversations
- **Check app name** — some apps have different names in different locales (e.g., `微信` vs `WeChat`)
- **Accessibility permissions required** — System Events automation requires granting Accessibility access in System Preferences > Privacy & Security > Accessibility
---
## Gotchas
- **Accessibility permission required** — first run will prompt for access; grant it in System Preferences > Privacy & Security > Accessibility for Terminal / iTerm / Claude Code
- **`keystroke` is slow for long text** — always use clipboard paste (`Cmd+V`) for messages over \~20 characters
- **`keystroke` can mangle non-ASCII** — use clipboard paste for Chinese, emoji, or special characters
- **`key code 36` is Enter** — this is the hardware key code, works regardless of keyboard layout
- **`entire contents` is extremely slow** — avoid for complex UIs; use screenshots instead
- **App name varies by locale** — `微信` vs `WeChat`, `企业微信` vs `WeCom`; handle both
- **WeChat Enter sends immediately** — use `Shift+Enter` for newlines within a message
- **Rate limiting** — don't send messages too fast; platforms may throttle or flag automated input
- **Lark / 飞书 app name varies** — `Lark` (international) vs `飞书` (China mainland); scripts auto-detect
- **QQ uses `Cmd+F` for search** — not `Cmd+K` like Discord/Slack/Lark
- **Bot response times vary** — AI-powered bots may take 10-60s; use generous sleep values
@@ -0,0 +1,142 @@
# record-app-screen.sh
General-purpose screen recording tool for the Electron app. Captures CDP screenshots as video frames and gallery snapshots, then assembles into an MP4 on stop.
## Why CDP Screenshots Instead of ffmpeg Screen Capture
- **Works on any screen** — CDP screenshots capture the browser viewport directly, so external monitors, Retina scaling, and window positioning are all handled automatically
- **No signal handling issues** — ffmpeg-static (npm) produces corrupt MP4 files when killed (missing moov atom). CDP screenshots avoid this entirely
- **Consistent output** — Screenshots are resolution-independent and don't require crop coordinate calculations
## Commands
```bash
# Start recording (Electron must be running with CDP)
.agents/skills/agent-testing/scripts/record-app-screen.sh start [output_name]
# Stop recording and assemble video
.agents/skills/agent-testing/scripts/record-app-screen.sh stop
# Check if recording is active
.agents/skills/agent-testing/scripts/record-app-screen.sh status
```
### Arguments
| Argument | Default | Description |
| ------------- | --------------------------- | -------------------------- |
| `output_name` | `recording-YYYYMMDD-HHMMSS` | Base name for output files |
### Environment Variables
| Variable | Default | Description |
| ---------------------- | ------- | -------------------------------------- |
| `CDP_PORT` | `9222` | Chrome DevTools Protocol port |
| `SCREENSHOT_INTERVAL` | `3` | Seconds between gallery screenshots |
| `VIDEO_FRAME_INTERVAL` | `0.5` | Seconds between video frames (\~2 fps) |
## Output Structure
```
.records/
<name>.mp4 # Video assembled from frames (~2 fps)
<name>/ # Gallery screenshots (every 3s)
0000.png
0001.png
0002.png
...
```
The `.records/` directory is at the project root and is gitignored.
## How It Works
### Start
1. Creates two background loops:
- **Video frames** — `agent-browser screenshot` every `VIDEO_FRAME_INTERVAL` seconds into a temp directory (`/tmp/record-frames-XXXXXX/`)
- **Gallery screenshots** — `agent-browser screenshot` every `SCREENSHOT_INTERVAL` seconds into `.records/<name>/`
2. Saves PIDs and paths to `/tmp/record-app-screen.pids` and `/tmp/record-app-screen.state`
### Stop
1. Kills both background loops
2. Assembles video frames into MP4 using ffmpeg:
```
ffmpeg -framerate 2 -i frame_%06d.png -c:v libx264 -crf 23 -pix_fmt yuv420p <output>.mp4
```
3. Cleans up temp frame directory
4. Reports file sizes and paths
## Usage Examples
### Basic Test Recording
```bash
# Start Electron
.agents/skills/agent-testing/scripts/electron-dev.sh start
# Start recording
.agents/skills/agent-testing/scripts/record-app-screen.sh start my-test
# Run automation
agent-browser --cdp 9222 click @e61
agent-browser --cdp 9222 type @e42 "hello"
agent-browser --cdp 9222 press Enter
sleep 10
# Stop and get results
.agents/skills/agent-testing/scripts/record-app-screen.sh stop
# → .records/my-test.mp4 + .records/my-test/*.png
```
### Gateway Streaming Demo
```bash
.agents/skills/agent-testing/scripts/electron-dev.sh start
# Inject gateway URL
agent-browser --cdp 9222 eval --stdin << 'EOF'
(function() {
var store = window.global_serverConfigStore;
store.setState({ serverConfig: { ...store.getState().serverConfig,
agentGatewayUrl: 'https://agent-gateway.lobehub.com' } });
return 'ready';
})()
EOF
# Record
.agents/skills/agent-testing/scripts/record-app-screen.sh start gateway-demo
# Navigate to agent, send message, wait for completion...
# (automation commands here)
.agents/skills/agent-testing/scripts/record-app-screen.sh stop
open .records/gateway-demo.mp4
```
### Check Active Recording
```bash
.agents/skills/agent-testing/scripts/record-app-screen.sh status
# [record] Active recording
# Frames: 42 captured (running: yes)
# Screenshots: 14 captured (running: yes)
# Output: .records/my-test.mp4
```
## Prerequisites
- **ffmpeg** — For video assembly. Install via `bun add -g ffmpeg-static` or `brew install ffmpeg`
- **agent-browser** — For CDP screenshots. Install via `npm i -g agent-browser`
- **Electron app running** — With CDP enabled (use `electron-dev.sh start`)
## Troubleshooting
| Problem | Solution |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| "No active recording found" on stop | PID file was cleaned up. Check if background processes are still running with `ps aux \| grep agent-browser` |
| "A recording is already active" | Run `stop` first, or manually clean: `rm /tmp/record-app-screen.pids /tmp/record-app-screen.state` |
| Video is 0 bytes | No frames were captured. Ensure Electron is running and CDP port is correct |
| Screenshots are blank/white | SPA may not have loaded yet. Wait for `electron-dev.sh` to report "Renderer ready" |
| ffmpeg assembly fails | Check `/tmp/ffmpeg-assemble.log`. Ensure ffmpeg is installed and frames exist |
@@ -0,0 +1,124 @@
# Structured Test Reports
Every automated test session ends with a structured, evidence-backed report.
A chat-only summary is not an acceptable deliverable: the report is what the
user (or a reviewer, or a later agent) audits without replaying the session.
## Location & layout
Reports live under `.records/reports/` (gitignored, like all `.records/`
output):
```
.records/reports/<YYYYMMDD-HHMMSS>-<slug>/
├── report.md # human-readable report (embedded screenshots, case table, verdict)
├── result.json # machine-readable results (pass/fail counts, score)
└── assets/ # evidence: screenshots, HAR files, CLI transcripts
```
## Workflow
1. **Scaffold up front** — before running the first test step:
```bash
DIR=$(./.agents/skills/agent-testing/scripts/report-init.sh < slug > "<title>")
```
The script creates the directory, pre-fills branch / commit / date in both
files, and prints the directory path.
2. **Collect evidence as you test** — every asserted behavior gets one evidence
item in `$DIR/assets/`:
- UI (static state): `agent-browser screenshot` or `capture-app-window.sh`,
then **verify the screenshot with the Read tool before citing it** —
never cite an image you haven't looked at.
- UI (time-based behavior): **screenshot vs GIF is a judgment you must
make per case.** If the assertion is about change over time — streaming
output, a ticking timer, loading/progress states, animations,
appear/disappear transitions — a static screenshot cannot prove it.
Record a frame sequence and synthesize a GIF:
```bash
# start recording (background), trigger the behavior, wait for it to finish
../scripts/record-gif.sh "$DIR/assets/case2-streaming.gif" 12 2 &
GIF_PID=$!
# ... drive the scenario ...
wait $GIF_PID
```
Embed it like an image: `![case 2](assets/case2-streaming.gif)`. Verify
at least the first/last frames visually (Read the GIF) before citing.
- CLI: exact command + trimmed output (`$CLI task list | tee "$DIR/assets/task-list.txt"`).
- Network: `agent-browser network requests` dumps or HAR files.
3. **Fill `report.md` as you go** — don't reconstruct from memory at the end.
4. **Set the verdict** in both `report.md` and `result.json`, then link the
report directory in your final answer to the user.
## Report language (hard rule)
**`report.md` MUST be written in the language the user is conversing in** —
the whole file, headings included. If the conversation is in Chinese, the
report is in Chinese; do not mix English prose into it. The scaffold's English
headings are placeholders — translate them when filling. Exceptions that stay
as-is: code/commands, identifiers, log excerpts, and `result.json` (its keys
and status values are machine-read and stay English; the `title` and case
`name` fields follow the user's language).
## report.md sections
| Section | Content |
| --------------- | ---------------------------------------------------------------------------------- |
| **Scope** | What changed / what is being verified; branch + commit |
| **Environment** | Server URL, surfaces used (cli / electron / web / bot), relevant versions |
| **Cases** | Table: `# \| case \| surface \| steps \| expected \| actual \| status \| evidence` |
| **Evidence** | Embedded screenshots/GIFs (`![case 1](assets/case1.png)`), fenced CLI transcripts |
| **Verdict** | Pass/fail/blocked counts, optional 0100 score, open issues / follow-ups |
Status values: `pass` / `fail` / `blocked` (couldn't run — e.g. auth or env
missing; a blocked case is not a pass).
## result.json schema
```json
{
"branch": "feat/task-tree",
"cases": [
{
"id": "1",
"name": "task tree returns nested children",
"surface": "cli",
"status": "pass",
"evidence": ["assets/task-tree.txt"]
}
],
"commit": "abc1234",
"createdAt": "2026-06-11T15:30:00+08:00",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"blocked": 0,
"score": 100,
"verdict": "pass"
},
"surfaces": ["cli"],
"title": "Verify task tree API"
}
```
`score` is optional — use it when the verdict has a subjective component (UI
polish, copy quality); omit it for purely binary runs. `verdict` is the single
word the user reads first: `pass`, `fail`, or `partial`.
## Rules
- **No evidence, no claim** — every `pass`/`fail` in the case table must link
at least one asset.
- **Screenshots must be visually verified** with the Read tool before being
cited.
- **Report failures faithfully** — a failing case with clear evidence is a good
report; a vague green one is not.
- If coverage was cut (cases skipped, surfaces not exercised), say so in the
Verdict section — silent truncation reads as "covered everything".
@@ -0,0 +1,243 @@
// Analyzer for probe-events dumps. Reads a JSON file produced by `run.ts dump`
// and prints a layered breakdown:
//
// 1. STREAM EVENTS — every non-chunk WS/SSE event in receipt order
// 2. CHUNKS SUMMARY — collapsed per-step chunk counts (otherwise floods)
// 3. ACTION CALLS — replaceMessages / refreshMessages / MARK:* with stack
// 4. CORRELATION — calls ↔ nearest stream event within ±300ms
// 5. PER-KEY ASSISTANT GROWTH — for each messagesMap key, when the leading
// assistant message's cLen / rLen actually moves (this is what reveals
// "chunks arrived but the message never grew" regressions)
// 6. ROLLBACKS — msgN / childN / role drops in the active-topic timeline
//
// Usage:
// bun run .agents/skills/agent-testing/scripts/agent-gateway/analyze-events.ts <dump.json>
import { readFileSync } from 'node:fs';
import type {
ProbeActionCall,
ProbeDump,
ProbeMessageSummary,
ProbeStreamEvent,
ProbeTimelineSample,
} from './types';
const file = process.argv[2];
if (!file) {
console.error('usage: bun run analyze-events.ts <dump.json>');
process.exit(1);
}
const raw = readFileSync(file, 'utf8');
// agent-browser eval --stdin wraps return values in quotes when the value is
// a string — so the JSON file may be double-encoded depending on how it was
// captured. Handle both.
const parsedOnce = JSON.parse(raw) as ProbeDump | string;
const dump: ProbeDump = typeof parsedOnce === 'string' ? JSON.parse(parsedOnce) : parsedOnce;
const { streamEvents = [], actionCalls = [], timeline = [] } = dump;
const pad = (v: unknown, n: number) => String(v).padStart(n);
// ── META ───────────────────────────────────────────────────────────
console.log('=== META ===');
console.log(` events: ${streamEvents.length}`);
console.log(` calls: ${actionCalls.length}`);
console.log(` timeline: ${timeline.length}`);
// ── 1. STREAM EVENTS (non-chunk) ───────────────────────────────────
const nonChunkEvents = streamEvents.filter((e) => e.type !== 'stream_chunk');
const chunkEvents = streamEvents.filter((e) => e.type === 'stream_chunk');
console.log(
`\n=== STREAM EVENTS (${nonChunkEvents.length} non-chunk + ${chunkEvents.length} chunks elided) ===`,
);
for (const e of nonChunkEvents) {
const dataStr = e.dataKeys?.length ? ` [${e.dataKeys.join(',')}]` : '';
const data = e.data as Record<string, unknown> | undefined;
const uiHint = data?.uiMessagesPreview
? ` uiPreview=${JSON.stringify(data.uiMessagesPreview)}`
: data?.uiMessagesTotal
? ` uiTotal=${data.uiMessagesTotal}`
: '';
const phaseHint = data?.phase ? ` phase=${data.phase}` : '';
const extra = e.serverType ? ` serverType=${e.serverType}` : '';
console.log(
` t=${pad(e.t, 7)} [${(e.transport ?? '?').padEnd(3)}] step=${pad(e.stepIndex ?? '-', 2)} ` +
`type=${(e.type ?? '').padEnd(22)} op=${e.opIdTail ?? '-'}${phaseHint}${uiHint}${extra}${dataStr}`,
);
}
// ── 2. CHUNK SUMMARY ───────────────────────────────────────────────
console.log('\n=== CHUNKS SUMMARY (per step / chunkType) ===');
const chunkBuckets = new Map<string, { count: number; firstT: number; lastT: number }>();
for (const c of chunkEvents) {
const data = c.data as Record<string, unknown> | undefined;
const ct = (data?.chunkType as string | undefined) ?? '?';
const key = `step=${c.stepIndex ?? '-'} chunkType=${ct.padEnd(8)} op=${c.opIdTail}`;
const slot = chunkBuckets.get(key);
if (slot) {
slot.count += 1;
slot.lastT = c.t;
} else {
chunkBuckets.set(key, { count: 1, firstT: c.t, lastT: c.t });
}
}
for (const [k, v] of chunkBuckets) {
console.log(` ${k} count=${pad(v.count, 4)} t=${pad(v.firstT, 7)}..${pad(v.lastT, 7)}`);
}
// ── 3. ACTION CALLS ───────────────────────────────────────────────
console.log('\n=== ACTION CALLS (replace/refresh/MARK) ===');
for (const c of actionCalls) {
if (c.name?.startsWith('MARK:')) {
console.log(` t=${pad(c.t, 7)} ${c.name}`);
continue;
}
const snapshot = (c.args as any)?.snapshot as
| Array<{ id: string; role: string; cLen: number; rLen: number }>
| undefined;
const snapStr = snapshot?.length
? ' snapshot=' + snapshot.map((m) => `${m.id}:${m.role}/c${m.cLen}/r${m.rLen}`).join(' | ')
: '';
const summary =
c.name === 'replaceMessages'
? `count=${c.args?.count} action=${(c.args?.params as any)?.action ?? '-'}${snapStr}`
: c.name === 'refreshMessages'
? `ctx=${JSON.stringify(c.args?.context)}`
: c.error
? `error=${c.error}`
: '';
console.log(` t=${pad(c.t, 7)} ${c.name.padEnd(20)} ${summary}`);
if (c.stack) {
const frames = c.stack
.split(' ← ')
.filter((f) => !!f && !f.includes('Object.<anonymous>'))
.slice(0, 3);
for (const f of frames) console.log(`${f}`);
}
}
// ── 4. CORRELATION ────────────────────────────────────────────────
function nearestEventForCall(
call: ProbeActionCall,
windowMs = 300,
): { event: ProbeStreamEvent; delta: number } | null {
let best: ProbeStreamEvent | null = null;
let bestDelta = Infinity;
for (const e of streamEvents) {
const d = Math.abs(e.t - call.t);
if (d < bestDelta && d <= windowMs) {
bestDelta = d;
best = e;
}
}
return best ? { event: best, delta: bestDelta } : null;
}
console.log('\n=== CORRELATION (replace/refresh ↔ nearest event within ±300ms) ===');
for (const c of actionCalls) {
if (c.name !== 'refreshMessages' && c.name !== 'replaceMessages') continue;
const hit = nearestEventForCall(c);
if (hit) {
const phase = (hit.event.data as Record<string, unknown> | undefined)?.phase;
console.log(
` t=${pad(c.t, 7)} ${c.name.padEnd(16)} ← Δ${pad(hit.delta, 4)}ms ${hit.event.type}` +
(phase ? ` phase=${phase}` : ''),
);
} else {
console.log(` t=${pad(c.t, 7)} ${c.name.padEnd(16)} ← (no event nearby — external trigger)`);
}
}
// ── 5. PER-KEY ASSISTANT GROWTH ───────────────────────────────────
// For each messagesMap key, find the trailing assistant message and report
// the points in time where its cLen / rLen actually changed. If the timeline
// shows chunks arriving but the assistant cLen never moves, that's the
// signature of "dispatch queue blocked / messageId mismatch".
console.log('\n=== PER-KEY ASSISTANT GROWTH ===');
const keysEverSeen = new Set<string>();
for (const s of timeline) for (const k of Object.keys(s.byKey ?? {})) keysEverSeen.add(k);
for (const key of keysEverSeen) {
console.log(`\n key=${key}`);
let lastSig: string | null = null;
for (const s of timeline) {
const slot = s.byKey?.[key];
if (!slot) continue;
const last = slot.msgs.at(-1) as ProbeMessageSummary | undefined;
if (!last) continue;
const sig = `${last.id}|c${last.cLen}|r${last.rLen}|n${slot.n}`;
if (sig === lastSig) continue;
lastSig = sig;
console.log(
` t=${pad(s.t, 7)} msgN=${pad(slot.n, 3)} ` +
`lastAssistant=${last.id} cLen=${pad(last.cLen, 5)} rLen=${pad(last.rLen, 5)}` +
` runOps=${s.runOps}`,
);
}
}
// ── 6. ROLLBACKS (active-topic msgN / childN / role drops) ─────────
console.log('\n=== ROLLBACKS (active-topic msgN / childN / role drops) ===');
let prev: ProbeTimelineSample | null = null;
const rollbacks: Array<{ t: number; topic: string | null; drops: string[] }> = [];
const flatten = (s: ProbeTimelineSample) => {
if (!s.activeTopic) return [];
return Object.entries(s.byKey ?? {})
.filter(([k]) => k.includes(s.activeTopic!))
.flatMap(([, v]) => v.msgs);
};
for (const s of timeline) {
if (s.err) {
prev = null;
continue;
}
if (!prev || prev.activeTopic !== s.activeTopic) {
prev = s;
continue;
}
const prevMsgs = flatten(prev);
const curMsgs = flatten(s);
const drops: string[] = [];
if (curMsgs.length < prevMsgs.length) drops.push(`msgN ${prevMsgs.length}${curMsgs.length}`);
let prevChild = 0;
let curChild = 0;
for (const m of prevMsgs) prevChild += m.chN ?? 0;
for (const m of curMsgs) curChild += m.chN ?? 0;
if (curChild < prevChild) drops.push(`childN ${prevChild}${curChild}`);
const prevById = new Map(prevMsgs.map((m) => [m.id, m]));
for (const m of curMsgs) {
const pr = prevById.get(m.id);
if (!pr) continue;
if (m.cLen < pr.cLen) drops.push(`cLen[${m.id}] ${pr.cLen}${m.cLen}`);
if (m.rLen < pr.rLen) drops.push(`rLen[${m.id}] ${pr.rLen}${m.rLen}`);
}
if (drops.length) rollbacks.push({ t: s.t, topic: s.activeTopic, drops });
prev = s;
}
if (rollbacks.length === 0) {
console.log(' (none)');
} else {
for (const r of rollbacks) {
const nearEvent = streamEvents
.filter((e) => Math.abs(e.t - r.t) <= 300)
.map((e) => `${e.type}${(e.data as any)?.phase ? ':' + (e.data as any).phase : ''}`);
const nearCall = actionCalls
.filter((c) => Math.abs(c.t - r.t) <= 300 && !c.name?.startsWith('MARK:'))
.map((c) => c.name);
console.log(
` t=${pad(r.t, 7)} topic=${r.topic} ${r.drops.join(' | ')}` +
(nearEvent.length ? ` near-event:[${nearEvent.join(',')}]` : '') +
(nearCall.length ? ` near-call:[${nearCall.join(',')}]` : ''),
);
}
}
@@ -0,0 +1,119 @@
#!/usr/bin/env node
// Analyze a probe dump captured by probe.js + probe-dump.js.
//
// node analyze.mjs /tmp/probe.json
//
// Prints:
// 1. EVENTS — user-action markers with their relative timestamps
// 2. TIMELINE — periodic samples (~1 per second + event-adjacent samples)
// showing every interesting field; columns:
// t(ms) | runOps | msgN | childN | content | reasoning | tools | domLen | search | crawl | topic | event
// 3. REGRESSIONS — every place a tracked counter *dropped* on the same
// topic between adjacent samples. A "true" UI rollback shows up as a
// drop in content/reasoning/tools/childN/domLen without a topic change.
//
// Whitelisted transitions (not flagged):
// - topic change → all drops expected (focus moved away)
// - reasoning length 0 after content starts → reasoning gets sealed into a
// completed sub-block; the parent's running reasoning resets to ''.
// - msgN drop when topic transitions from `_new` placeholder to a real id.
import fs from 'node:fs';
const file = process.argv[2];
if (!file) {
console.error('usage: node analyze.mjs <probe.json>');
process.exit(1);
}
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
// probe-dump.js wraps the payload in JSON.stringify so agent-browser returns
// it as a single quoted string. Unwrap.
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
const { events, samples } = data;
const fmt = {
pad(v, n) {
return String(v).padStart(n);
},
};
console.log('=== EVENTS ===');
for (const e of events) console.log(` t=${fmt.pad(e.t, 7)} ${e.name}`);
console.log(
'\n=== TIMELINE (~1s cadence, plus event-adjacent samples) ===\n' +
' t(ms) runOps msgN childN content reasoning tools domLen search crawl topic event',
);
let lastSampledAt = -1e9;
const eventBuckets = events.map((e) => e.t);
for (let i = 0; i < samples.length; i++) {
const s = samples[i];
const nearEvent = eventBuckets.some((et) => Math.abs(et - s.t) < 110);
if (!nearEvent && s.t - lastSampledAt < 1000) continue;
lastSampledAt = s.t;
const ev = events.find((e) => Math.abs(e.t - s.t) < 110);
const evMarker = ev ? `${ev.name}` : '';
const topicSuffix = s.topicId ? s.topicId.slice(-6) : '(none)';
const search = s.ind?.search ?? 0;
const crawl = s.ind?.crawl ?? 0;
console.log(
` ${fmt.pad(s.t, 6)} ` +
`${fmt.pad(s.runOps, 6)} ` +
`${fmt.pad(s.msgN, 4)} ` +
`${fmt.pad(s.childN ?? 0, 5)} ` +
`${fmt.pad(s.cT ?? 0, 8)} ` +
`${fmt.pad(s.rT ?? 0, 9)} ` +
`${fmt.pad(s.toolT ?? 0, 5)} ` +
`${fmt.pad(s.domLen ?? 0, 7)} ` +
`${fmt.pad(search, 6)} ` +
`${fmt.pad(crawl, 5)} ` +
`${topicSuffix.padEnd(8)}${evMarker}`,
);
}
console.log('\n=== REGRESSIONS (same topic, value dropped) ===');
const regressions = [];
for (let i = 1; i < samples.length; i++) {
const prev = samples[i - 1];
const cur = samples[i];
if (!cur.topicId || prev.topicId !== cur.topicId) continue;
const drops = [];
if (cur.msgN < prev.msgN) drops.push(`msgN: ${prev.msgN}${cur.msgN}`);
if ((cur.childN ?? 0) < (prev.childN ?? 0)) drops.push(`childN: ${prev.childN}${cur.childN}`);
if ((cur.cT ?? 0) < (prev.cT ?? 0)) drops.push(`content: ${prev.cT}${cur.cT}`);
if ((cur.rT ?? 0) < (prev.rT ?? 0)) drops.push(`reasoning: ${prev.rT}${cur.rT}`);
if ((cur.toolT ?? 0) < (prev.toolT ?? 0)) drops.push(`tools: ${prev.toolT}${cur.toolT}`);
// domLen jitters by a few chars from counter labels — only flag big drops.
if ((cur.domLen ?? 0) < (prev.domLen ?? 0) - 100) {
drops.push(`domLen: ${prev.domLen}${cur.domLen}`);
}
if (drops.length === 0) continue;
const nearbyEv = events.filter((e) => Math.abs(e.t - cur.t) < 600).map((e) => e.name);
regressions.push({ t: cur.t, topic: cur.topicId.slice(-6), drops, nearbyEv });
}
if (regressions.length === 0) {
console.log(' (none)');
} else {
for (const r of regressions) {
const evStr = r.nearbyEv.length ? ` near:[${r.nearbyEv.join(',')}]` : '';
console.log(` t=${fmt.pad(r.t, 7)} topic=${r.topic} ${r.drops.join(' | ')}${evStr}`);
}
}
console.log(`\n=== SUMMARY ===`);
console.log(` samples: ${samples.length}`);
console.log(` events: ${events.length}`);
console.log(` regressions: ${regressions.length}`);
if (samples.length) {
const last = samples.at(-1);
console.log(
` final: msgN=${last.msgN} childN=${last.childN ?? 0} content=${last.cT ?? 0} ` +
`reasoning=${last.rT ?? 0} tools=${last.toolT ?? 0} runOps=${last.runOps}`,
);
}
@@ -0,0 +1,17 @@
// Stop the probe and serialize collected data.
//
// agent-browser --cdp 9222 eval --stdin < probe-dump.js > /tmp/probe.json
//
// The whole thing is wrapped in a JSON.stringify so agent-browser returns it
// as a single quoted string — the analyzer double-parses to handle that.
(function () {
if (window.__PROBE_TIMER) {
clearInterval(window.__PROBE_TIMER);
window.__PROBE_TIMER = null;
}
return JSON.stringify({
events: window.__PROBE_EVENTS || [],
samples: window.__PROBE_SAMPLES || [],
});
})();
@@ -0,0 +1,37 @@
// Stops the events-probe timeline timer and stashes the full capture as a
// JSON string on `window.__PROBE_LAST_DUMP_JSON`. `run.ts` wraps the bundle
// in an IIFE that returns that global, which `agent-browser eval` prints to
// stdout — the runner then persists it under `.agent-gateway/`.
import type { ProbeDump } from './types';
declare global {
interface Window {
__PROBE_LAST_DUMP_JSON?: string;
}
}
const w = window;
if (w.__PROBE_TIMELINE_TIMER) {
clearInterval(w.__PROBE_TIMELINE_TIMER);
w.__PROBE_TIMELINE_TIMER = null;
}
const mutations = w.__PROBE_MUTATIONS ?? [];
const dump: ProbeDump & { mutations: typeof mutations } = {
meta: {
t0: w.__PROBE_T0 ?? 0,
collectedAt: Date.now(),
sampleCount: (w.__PROBE_MSG_TIMELINE ?? []).length,
eventCount: (w.__PROBE_STREAM_EVENTS ?? []).length,
callCount: (w.__PROBE_ACTION_CALLS ?? []).length,
},
streamEvents: w.__PROBE_STREAM_EVENTS ?? [],
actionCalls: w.__PROBE_ACTION_CALLS ?? [],
timeline: w.__PROBE_MSG_TIMELINE ?? [],
mutations,
};
w.__PROBE_LAST_DUMP_JSON = JSON.stringify(dump);
@@ -0,0 +1,637 @@
// LobeHub gateway raw-event-stream probe.
//
// Gateway-mode chats subscribe via WebSocket — NOT via the `/api/agent/stream`
// SSE endpoint (that one belongs to the direct/client durable-agent runtime).
// `AgentStreamClient` (`packages/agent-gateway-client/src/client.ts`) opens
// `new WebSocket('wss://.../ws?operationId=...')`, then parses JSON frames in
// its `onmessage` handler and re-emits `agent_event.event` objects to the
// chat store.
//
// To capture the RAW gateway events before the store touches them, we wrap
// `window.WebSocket` so that for any socket whose URL contains `operationId=`
// we intercept the `onmessage` handler / `addEventListener('message')` and
// log every `agent_event` frame.
//
// We *also* keep the `window.fetch` hook for `/api/agent/stream` so this
// probe still works for direct-mode runs — but gateway-mode events come
// through the WebSocket path.
//
// Buffers (read via `dump`):
// __PROBE_STREAM_EVENTS — raw events parsed off the wire
// __PROBE_ACTION_CALLS — replaceMessages / refreshMessages calls (best-effort)
// __PROBE_MSG_TIMELINE — 200ms snapshots of every messagesMap key
import type {
ProbeActionCall,
ProbeMessageSummary,
ProbeStreamEvent,
ProbeTimelineSample,
} from './types';
// Bundled by esbuild as an IIFE. Top-level code runs once on injection.
const w = window;
// ── Buffers ─────────────────────────────────────────────────────────
declare global {
interface Window {
__PROBE_MUTATIONS?: Array<{
t: number;
key: string;
n: number;
last?: { id: string; role: string; cLen: number; rLen: number; updatedAt?: unknown };
prevLast?: { id: string; role: string; cLen: number; rLen: number };
delta?: string;
}>;
__PROBE_STORE_UNSUB?: () => void;
}
}
const events: ProbeStreamEvent[] = (w.__PROBE_STREAM_EVENTS ??= []);
const calls: ProbeActionCall[] = (w.__PROBE_ACTION_CALLS ??= []);
const timeline: ProbeTimelineSample[] = (w.__PROBE_MSG_TIMELINE ??= []);
const mutations = (w.__PROBE_MUTATIONS ??= []);
events.length = 0;
calls.length = 0;
timeline.length = 0;
mutations.length = 0;
const t0 = Date.now();
w.__PROBE_T0 = t0;
const now = (): number => Date.now() - t0;
// ── Helpers ─────────────────────────────────────────────────────────
function summarizeData(data: unknown): Record<string, unknown> | unknown {
if (!data || typeof data !== 'object') return data;
const src = data as Record<string, unknown>;
const out: Record<string, unknown> = {};
for (const k of Object.keys(src)) {
const v = src[k];
if (v == null) {
out[k] = v;
} else if (Array.isArray(v)) {
out[k] = `Array(${v.length})`;
if (k === 'uiMessages') {
out.uiMessagesPreview = v.slice(0, 5).map((m: any) => ({
id: (m.id ?? '').slice(-8),
role: m.role,
cLen: (m.content ?? '').length,
children: (m.children ?? []).length,
tools: (m.tools ?? []).length,
reasoning: (m.reasoning?.content ?? '').length,
}));
out.uiMessagesTotal = v.length;
}
} else if (typeof v === 'object') {
const obj = v as Record<string, unknown>;
out[k] =
'Object{' +
Object.keys(obj)
.slice(0, 6)
.map((kk) => kk + (typeof obj[kk] === 'string' ? `=${(obj[kk] as string).length}ch` : ''))
.join(',') +
'}';
} else if (typeof v === 'string') {
out[k] = v.length > 100 ? v.slice(0, 100) + `…(${v.length})` : v;
} else {
out[k] = v;
}
}
return out;
}
function summarizeMessages(msgs: any[]): ProbeMessageSummary[] {
return (msgs ?? []).slice(0, 80).map((m) => ({
id: (m.id ?? '').slice(-8),
role: m.role,
cLen: (m.content ?? '').length,
rLen: (m.reasoning?.content ?? '').length,
tools: (m.tools ?? []).length,
chN: (m.children ?? []).length,
}));
}
function shortStack(): string {
const raw = new Error('probe-stack').stack ?? '';
return raw
.split('\n')
.slice(3)
.filter((l) => !l.includes('probe-events') && !l.includes('node_modules'))
.map((l) => l.trim().replace(/^at\s+/, ''))
.slice(0, 6)
.join(' ← ');
}
function recordAgentEvent(args: {
transport: 'ws' | 'sse';
opId: string | null;
agentEvent: any;
eventId?: string | null;
rawLen?: number;
}): void {
const { transport, opId, agentEvent, eventId, rawLen } = args;
if (!agentEvent || typeof agentEvent !== 'object') return;
events.push({
t: now(),
transport,
opIdTail: (opId ?? '').slice(-10),
eventId: eventId ?? null,
type: agentEvent.type,
stepIndex: agentEvent.stepIndex,
dataKeys: agentEvent.data ? Object.keys(agentEvent.data) : [],
data: summarizeData(agentEvent.data) as Record<string, unknown>,
rawLen,
});
}
// ── 1. Patch window.WebSocket for gateway WS events ────────────────
if (!w.__PROBE_ORIG_WEBSOCKET) w.__PROBE_ORIG_WEBSOCKET = w.WebSocket;
const OrigWS = w.__PROBE_ORIG_WEBSOCKET;
function extractOpIdFromWsUrl(url: string | URL): string | null {
const m = String(url ?? '').match(/operationId=([^&]+)/);
return m ? decodeURIComponent(m[1]) : null;
}
function isGatewayWs(url: string | URL): boolean {
return String(url ?? '').includes('operationId=');
}
function handleWsFrame(rawData: unknown, opId: string | null): void {
const rawLen = typeof rawData === 'string' ? rawData.length : -1;
let parsed: any;
try {
parsed = typeof rawData === 'string' ? JSON.parse(rawData) : null;
} catch {
events.push({
t: now(),
transport: 'ws',
opIdTail: (opId ?? '').slice(-10),
type: '_PARSE_ERROR_',
raw: typeof rawData === 'string' && rawData.length < 400 ? rawData : '(non-string or large)',
});
return;
}
if (!parsed) return;
if (parsed.type === 'agent_event') {
recordAgentEvent({
transport: 'ws',
opId,
agentEvent: parsed.event,
eventId: parsed.id,
rawLen,
});
} else {
events.push({
t: now(),
transport: 'ws',
opIdTail: (opId ?? '').slice(-10),
type: '_SERVER_MSG_',
serverType: parsed.type,
rawLen,
});
}
}
// Wrap the constructor. Instance `constructor` will still reflect OrigWS
// (we share prototypes), so use the `_WS_OPEN_` sentinel events to confirm
// the patch is firing.
function PatchedWebSocket(this: WebSocket, url: string | URL, protocols?: string | string[]) {
const ws: WebSocket = protocols == null ? new OrigWS(url) : new OrigWS(url, protocols);
const opId = extractOpIdFromWsUrl(url);
if (!isGatewayWs(url)) return ws;
events.push({
t: now(),
transport: 'ws',
opIdTail: (opId ?? '').slice(-10),
type: '_WS_OPEN_',
url: String(url),
});
// One observer listener that always fires, regardless of how the consumer
// (AgentStreamClient uses `ws.onmessage = …`) subscribes.
ws.addEventListener('message', (e) => {
try {
handleWsFrame((e as MessageEvent).data, opId);
} catch {
/* swallow */
}
});
ws.addEventListener('close', () => {
events.push({
t: now(),
transport: 'ws',
opIdTail: (opId ?? '').slice(-10),
type: '_WS_CLOSE_',
});
});
return ws;
}
// Preserve prototype + static fields so `instanceof WebSocket` and
// `WebSocket.OPEN` constants still work.
(PatchedWebSocket as unknown as { prototype: WebSocket }).prototype = OrigWS.prototype;
for (const k of Object.keys(OrigWS) as Array<keyof typeof OrigWS>) {
try {
(PatchedWebSocket as any)[k] = (OrigWS as any)[k];
} catch {
/* readonly */
}
}
(['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'] as const).forEach((k) => {
(PatchedWebSocket as any)[k] = (OrigWS as any)[k];
});
w.WebSocket = PatchedWebSocket as unknown as typeof WebSocket;
// ── 2. Patch window.fetch for `/api/agent/stream` (direct-mode SSE) ─
if (!w.__PROBE_ORIG_FETCH) w.__PROBE_ORIG_FETCH = w.fetch.bind(w);
const origFetch = w.__PROBE_ORIG_FETCH;
function isAgentStreamUrl(input: RequestInfo | URL): boolean {
let url = '';
if (typeof input === 'string') url = input;
else if (input instanceof URL) url = input.toString();
else if (input && typeof (input as Request).url === 'string') url = (input as Request).url;
return url.includes('/api/agent/stream');
}
function extractOpIdFromHttpUrl(input: RequestInfo | URL): string | null {
const url = typeof input === 'string' ? input : (input as Request | URL).toString();
const m = url.match(/operationId=([^&]+)/);
return m ? decodeURIComponent(m[1]) : null;
}
function pushFromSSEFrame(rawFrame: string, opId: string | null): void {
const lines = rawFrame.split('\n');
let dataJson = '';
let evtName = 'message';
for (const line of lines) {
if (line.startsWith('event:')) evtName = line.slice(6).trim();
else if (line.startsWith('data:')) dataJson += line.slice(5).trim();
}
if (!dataJson) return;
let parsed: any;
try {
parsed = JSON.parse(dataJson);
} catch {
events.push({
t: now(),
transport: 'sse',
opIdTail: (opId ?? '').slice(-10),
type: '_PARSE_ERROR_',
sseEvent: evtName,
raw: dataJson.length > 400 ? dataJson.slice(0, 400) + '…' : dataJson,
});
return;
}
recordAgentEvent({
transport: 'sse',
opId,
agentEvent: parsed,
eventId: null,
rawLen: dataJson.length,
});
}
async function teeAndDrain(response: Response, opId: string | null): Promise<Response> {
if (!response.body) return response;
const [a, b] = response.body.tee();
void (async () => {
const reader = b.getReader();
const decoder = new TextDecoder();
let buf = '';
try {
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
let idx: number;
while ((idx = buf.indexOf('\n\n')) !== -1) {
const frame = buf.slice(0, idx);
buf = buf.slice(idx + 2);
if (frame.trim()) pushFromSSEFrame(frame, opId);
}
}
if (buf.trim()) pushFromSSEFrame(buf, opId);
} catch (e: any) {
events.push({
t: now(),
transport: 'sse',
opIdTail: (opId ?? '').slice(-10),
type: '_TEE_ERROR_',
message: String(e?.message ?? e),
});
}
})();
return new Response(a, {
headers: response.headers,
status: response.status,
statusText: response.statusText,
});
}
w.fetch = async function patchedFetch(input: RequestInfo | URL, init?: RequestInit) {
const response = await origFetch(input as any, init);
if (!isAgentStreamUrl(input)) return response;
const opId = extractOpIdFromHttpUrl(input);
const url =
typeof input === 'string'
? input.split('?')[0]
: (input as Request | URL).toString().split('?')[0];
events.push({
t: now(),
transport: 'sse',
opIdTail: (opId ?? '').slice(-10),
type: '_CONNECTED_',
url,
status: response.status,
});
return teeAndDrain(response, opId);
} as typeof fetch;
// ── 3. Wrap store actions (best-effort for "who called replace") ────
// Side-global stash for the original chat-store actions. Re-installs ALWAYS
// rewrap from the originals so updates to the probe body take effect
// without a page reload — using only a `__probeWrapped` flag on the chat
// state object would freeze the first-installed wrapper across re-installs.
declare global {
interface Window {
__PROBE_ORIG_REFRESH_MESSAGES?: any;
__PROBE_ORIG_REPLACE_MESSAGES?: any;
}
}
try {
const chat = w.__LOBE_STORES?.chat?.();
if (chat) {
// First-time install: cache the originals. Re-install: restore from
// the cached originals before wrapping again.
if (!w.__PROBE_ORIG_REFRESH_MESSAGES) w.__PROBE_ORIG_REFRESH_MESSAGES = chat.refreshMessages;
if (!w.__PROBE_ORIG_REPLACE_MESSAGES) w.__PROBE_ORIG_REPLACE_MESSAGES = chat.replaceMessages;
const origRefresh = w.__PROBE_ORIG_REFRESH_MESSAGES;
const origReplace = w.__PROBE_ORIG_REPLACE_MESSAGES;
chat.refreshMessages = origRefresh;
chat.replaceMessages = origReplace;
chat.refreshMessages = async function probeRefresh(this: unknown, ...args: any[]) {
calls.push({
t: now(),
name: 'refreshMessages',
args: { context: args[0] ?? null },
stack: shortStack(),
});
return origRefresh.apply(this, args);
};
chat.replaceMessages = function probeReplace(this: unknown, ...args: any[]) {
const msgs = (args[0] as any[]) ?? [];
const snapshot = msgs.slice(-2).map((m) => ({
id: (m.id ?? '').slice(-8),
role: m.role,
cLen: (m.content ?? '').length,
rLen: (m.reasoning?.content ?? '').length,
updatedAt: m.updatedAt,
}));
calls.push({
t: now(),
name: 'replaceMessages',
args: { count: msgs.length, params: args[1] ?? null, snapshot } as any,
stack: shortStack(),
});
// Pair the call with a mutation row so the analyzer can build a
// single ordered timeline across replaceMessages + dispatchMessage.
const stackTop = shortStack().split(' ← ')[0]?.slice(0, 80);
const last = msgs.at(-1);
const lastSum = last
? {
id: (last.id ?? '').slice(-8),
role: last.role,
cLen: (last.content ?? '').length,
rLen: (last.reasoning?.content ?? '').length,
updatedAt: last.updatedAt,
}
: undefined;
const params: any = args[1] ?? {};
const ctxKey = params.context
? `main_${params.context.agentId ?? '?'}_${
params.context.topicId ? 'tpc_' + params.context.topicId : 'new'
}`.replace('main_tpc_', 'main_') // crude key inference
: '(no-ctx)';
mutations.push({
t: now(),
key: ctxKey,
n: msgs.length,
last: lastSum,
delta: `replaceMessages(action=${params.action ?? '-'}) src=${stackTop ?? '-'}`,
});
return origReplace.apply(this, args);
};
}
} catch (e: any) {
calls.push({ t: now(), name: '_WRAP_ERROR_', error: String(e?.message ?? e) });
}
// ── 3.5. Mutation log — wrap the TWO ChatStore writers (replaceMessages,
// internal_dispatchMessage) to record EVERY dbMessagesMap[key] reference
// change with a one-line "before/after last assistant message" delta. This
// reveals dispatchMessage-driven collapses that the replaceMessages wrap
// alone cannot see.
declare global {
interface Window {
__PROBE_ORIG_DISPATCH_MESSAGE?: any;
}
}
try {
const chat = w.__LOBE_STORES?.chat?.();
if (chat?.internal_dispatchMessage) {
if (!w.__PROBE_ORIG_DISPATCH_MESSAGE)
w.__PROBE_ORIG_DISPATCH_MESSAGE = chat.internal_dispatchMessage;
const origDispatch = w.__PROBE_ORIG_DISPATCH_MESSAGE;
chat.internal_dispatchMessage = origDispatch;
chat.internal_dispatchMessage = function probeDispatch(this: unknown, payload: any, ctx?: any) {
// Snapshot BEFORE — read the would-be target key + last message.
const before = (() => {
try {
const state = w.__LOBE_STORES?.chat?.();
if (!state) return null;
// Replicate state.internal_getConversationContext logic enough to
// resolve a key — but most callers pass operationId on ctx, and
// operationId-keyed lookup needs store internals. Easiest: snapshot
// ALL keys' last-assistant cLen and compare BEFORE vs AFTER below.
const map = state.dbMessagesMap ?? {};
const out: Record<string, any> = {};
for (const k of Object.keys(map)) {
const last = (map[k] ?? []).at(-1);
out[k] = last
? {
id: (last.id ?? '').slice(-8),
cLen: (last.content ?? '').length,
rLen: (last.reasoning?.content ?? '').length,
n: map[k].length,
}
: { n: 0 };
}
return out;
} catch {
return null;
}
})();
const result = origDispatch.apply(this, [payload, ctx]);
// Snapshot AFTER — find which key(s) actually changed.
try {
const state = w.__LOBE_STORES?.chat?.();
if (state && before) {
const map = state.dbMessagesMap ?? {};
for (const k of Object.keys(map)) {
const last = (map[k] ?? []).at(-1);
const beforeSnap = before[k];
const afterSnap = last
? {
id: (last.id ?? '').slice(-8),
cLen: (last.content ?? '').length,
rLen: (last.reasoning?.content ?? '').length,
n: map[k].length,
}
: { n: 0 };
const changed =
!beforeSnap ||
beforeSnap.n !== afterSnap.n ||
beforeSnap.id !== (afterSnap as any).id ||
beforeSnap.cLen !== (afterSnap as any).cLen ||
beforeSnap.rLen !== (afterSnap as any).rLen;
if (!changed) continue;
let delta = '';
if (beforeSnap?.id !== undefined && beforeSnap.id !== (afterSnap as any).id)
delta += `id:${beforeSnap.id}${(afterSnap as any).id};`;
if (
beforeSnap?.cLen !== undefined &&
(afterSnap as any).cLen !== undefined &&
(afterSnap as any).cLen < beforeSnap.cLen
)
delta += `cLen↓${beforeSnap.cLen}${(afterSnap as any).cLen};`;
if (
beforeSnap?.rLen !== undefined &&
(afterSnap as any).rLen !== undefined &&
(afterSnap as any).rLen < beforeSnap.rLen
)
delta += `rLen↓${beforeSnap.rLen}${(afterSnap as any).rLen};`;
if (beforeSnap?.n !== undefined && afterSnap.n < beforeSnap.n)
delta += `n↓${beforeSnap.n}${afterSnap.n};`;
mutations.push({
t: now(),
key: k,
n: afterSnap.n,
last: (afterSnap as any).id ? (afterSnap as any) : undefined,
prevLast: beforeSnap?.id ? beforeSnap : undefined,
delta: delta || `dispatch:${payload?.type}`,
});
}
}
} catch (e: any) {
mutations.push({
t: now(),
key: '_DISPATCH_PROBE_ERROR_',
n: -1,
delta: String(e?.message ?? e),
});
}
return result;
};
}
} catch (e: any) {
calls.push({ t: now(), name: '_DISPATCH_WRAP_ERROR_', error: String(e?.message ?? e) });
}
// ── 4. Periodic per-key timeline snapshots ─────────────────────────
function captureTimeline(): void {
try {
const c = w.__LOBE_STORES?.chat?.();
if (!c) return;
const msgsMap = (c.messagesMap ?? {}) as Record<string, any[]>;
const dbMap = (c.dbMessagesMap ?? {}) as Record<string, any[]>;
const byKey: ProbeTimelineSample['byKey'] = {};
for (const k of Object.keys(msgsMap)) {
const display = msgsMap[k] ?? [];
const db = dbMap[k] ?? [];
if (display.length === 0 && db.length === 0) continue;
byKey[k] = {
n: display.length,
dbN: db.length,
msgs: summarizeMessages(display),
};
}
const ops = Object.values((c.operations ?? {}) as Record<string, any>);
timeline.push({
t: now(),
activeTopic: ((c.activeTopicId as string | null) ?? '').slice(-10) || null,
keys: Object.keys(byKey),
byKey,
runOps: ops.filter((o: any) => o.status === 'running').length,
});
} catch (e: any) {
timeline.push({
t: now(),
activeTopic: null,
keys: [],
byKey: {},
runOps: 0,
err: e?.message ?? String(e),
});
}
}
captureTimeline();
if (w.__PROBE_TIMELINE_TIMER) clearInterval(w.__PROBE_TIMELINE_TIMER);
w.__PROBE_TIMELINE_TIMER = setInterval(captureTimeline, 200);
// ── 5. Tab-switch helpers ──────────────────────────────────────────
function listTopBarTabs(): HTMLElement[] {
return Array.from(
document.querySelectorAll<HTMLElement>(
'[data-insp-path*="TabItem.tsx"][data-contextmenu-trigger]',
),
).filter((t) => t.getBoundingClientRect().top < 30);
}
w.__listTabs = () =>
listTopBarTabs().map((t, i) => ({
i,
key: t.getAttribute('data-contextmenu-trigger'),
active: t.getAttribute('data-active') === 'true',
title: (t.innerText ?? '').slice(0, 60),
}));
w.__clickTabByKey = (key: string) => {
const tab = listTopBarTabs().find((t) => t.getAttribute('data-contextmenu-trigger') === key);
if (!tab) return 'not found: ' + key;
if (tab.getAttribute('data-active') === 'true') return 'already active: ' + key;
tab.click();
return 'clicked key=' + key;
};
w.__PROBE_EVENT = (name: string) => {
calls.push({ t: now(), name: 'MARK:' + name });
};
// `run.ts` wraps the bundle in an IIFE and appends a `return <confirmation>`
// after the bundle body — agent-browser then prints the confirmation back to
// the operator. Nothing to do here at the end of the module body.
@@ -0,0 +1,204 @@
// LobeHub chat streaming time-series probe.
//
// Inject into the renderer (via agent-browser eval) to record store + DOM
// snapshots every 200ms during a streaming session. Designed to surface
// "UI rolled back to an earlier state" symptoms — especially around
// gateway-mode tab switches that happen while the assistant is still writing.
//
// Usage:
// agent-browser --cdp 9222 eval --stdin < probe.js
// # ...do test interactions, call window.__PROBE_EVENT('LABEL') to mark moments...
// agent-browser --cdp 9222 eval --stdin < probe-dump.js > /tmp/probe.json
// node analyze.mjs /tmp/probe.json
//
// What it captures per sample:
// - activeTopicId
// - msgN: top-level messages in chat.messagesMap for this topic
// - childN: total assistantGroup.children blocks across all msgs (THIS is
// where streaming content actually lives — top-level assistantGroup stays empty)
// - cT / rT / toolT: totals across messages AND their children
// (content, reasoning, tool-call count)
// - perMsg: per-message breakdown so regressions can be located precisely
// - runOps: number of running operations (execServerAgentRuntime etc.)
// - domLen: total innerText length of the rendered chat list area
// - ind: visible UI indicators (Search pages, Crawled pages, Deeply Thought, Sending)
//
// Event markers: window.__PROBE_EVENT('NAME') records {t, name} into
// __PROBE_EVENTS, used by the analyzer to align state changes with
// user-driven actions (SENT, AWAY_1, BACK_1, ...).
(function () {
if (window.__PROBE_TIMER) clearInterval(window.__PROBE_TIMER);
window.__PROBE_SAMPLES = [];
window.__PROBE_EVENTS = [];
const t0 = Date.now();
function snapshot() {
try {
const chat = window.__LOBE_STORES.chat();
const topicId = chat.activeTopicId;
const idTail = topicId ? topicId.replace('tpc_', '') : null;
const keys = Object.keys(chat.messagesMap || {});
// Collect messages for the active topic. Before a topic is committed,
// optimistic messages live under the `<agentScope>_new` key — fall
// back to those when no topic is active yet.
let msgs = [];
if (idTail) {
keys.forEach((k) => {
if (k.includes(idTail)) msgs = msgs.concat(chat.messagesMap[k] || []);
});
} else {
keys
.filter((k) => k.endsWith('_new'))
.forEach((k) => {
msgs = msgs.concat(chat.messagesMap[k] || []);
});
}
// Walk top-level + assistantGroup.children. children carry the actual
// streamed content / reasoning / tool calls; the parent assistantGroup
// remains a placeholder (cLen=0, rLen=0) for its whole lifetime.
let totalContent = 0;
let totalReason = 0;
let totalTools = 0;
let childCount = 0;
const perMsg = msgs.map((m) => {
const cLen = (m.content || '').length;
const rLen = ((m.reasoning && m.reasoning.content) || '').length;
const tools = (m.tools || []).length;
totalContent += cLen;
totalReason += rLen;
totalTools += tools;
const children = m.children || [];
let chC = 0;
let chR = 0;
let chT = 0;
children.forEach((c) => {
chC += (c.content || '').length;
chR += ((c.reasoning && c.reasoning.content) || '').length;
chT += (c.tools || []).length;
});
totalContent += chC;
totalReason += chR;
totalTools += chT;
childCount += children.length;
return {
id: (m.id || '').slice(-8),
role: m.role,
cLen,
rLen,
tools,
chCount: children.length,
chC,
chR,
chT,
};
});
const ops = Object.values(chat.operations || {});
const runningOps = ops.filter((o) => o.status === 'running');
// DOM probe: total rendered text in the chat scroll area (proxy for
// "how much is actually visible to the user").
const convScroll =
document.querySelector(
'[data-chat-list], [class*="ChatList"], [class*="ConversationList"]',
) ||
document.querySelector('main [class*="scroll"]') ||
document.querySelector('main');
const domTxt = convScroll ? convScroll.innerText || '' : '';
const bodyTxt = document.body.innerText || '';
const searchMatches = (bodyTxt.match(/Search pages?:|Searched the web/g) || []).length;
const crawlMatches = (bodyTxt.match(/Crawl(ed|ing) pages?/g) || []).length;
window.__PROBE_SAMPLES.push({
t: Date.now() - t0,
topicId,
msgN: msgs.length,
childN: childCount,
cT: totalContent,
rT: totalReason,
toolT: totalTools,
perMsg,
runOps: runningOps.length,
runOpTypes: runningOps.map((o) => o.type),
domLen: domTxt.length,
ind: {
search: searchMatches,
crawl: crawlMatches,
sending: bodyTxt.includes('Sending message'),
deeplyThinking: bodyTxt.includes('Deeply Thinking'),
deeplyThought: bodyTxt.includes('Deeply Thought'),
},
});
} catch (e) {
window.__PROBE_SAMPLES.push({ t: Date.now() - t0, err: e.message });
}
}
snapshot();
window.__PROBE_TIMER = setInterval(snapshot, 200);
window.__PROBE_EVENT = function (name) {
window.__PROBE_EVENTS.push({ t: Date.now() - t0, name });
};
// Tab-switch helpers installed alongside the probe.
//
// The Electron tab bar mounts each tab as a div with data-insp-path
// ending in `TabItem.tsx:...`. The active tab is marked with
// data-active="true". DO NOT search by innerText — the active tab's text
// includes a ` · <agent name>` suffix that produces false matches when
// your search string happens to overlap with the agent name.
function listTabs() {
return Array.from(
document.querySelectorAll('[data-insp-path*="TabItem.tsx"][data-contextmenu-trigger]'),
).filter((t) => t.getBoundingClientRect().top < 30);
}
function tabKey(el) {
// Stable for the tab's lifetime; survives focus changes.
return el.getAttribute('data-contextmenu-trigger');
}
function findActiveTab() {
return listTabs().find((t) => t.getAttribute('data-active') === 'true') || null;
}
// Click by stable key captured earlier (preferred for round-trips).
window.__clickTabByKey = function (key) {
const tab = listTabs().find((t) => tabKey(t) === key);
if (!tab) return 'not found: key=' + key;
if (tab.getAttribute('data-active') === 'true') return 'already active: ' + key;
tab.click();
return 'clicked key=' + key;
};
// Click by index in the tab strip (0-based, left-to-right).
window.__clickTabByIndex = function (i) {
const tabs = listTabs();
if (i < 0 || i >= tabs.length) return 'index out of range: ' + i + '/' + tabs.length;
const t = tabs[i];
if (t.getAttribute('data-active') === 'true') return 'already active: i=' + i;
t.click();
return 'clicked i=' + i + ' key=' + tabKey(t);
};
// Snapshot all tabs in order: [{key, active, title (first 60 chars of innerText)}]
window.__listTabs = function () {
return listTabs().map((t, i) => ({
i,
key: tabKey(t),
active: t.getAttribute('data-active') === 'true',
title: (t.innerText || '').slice(0, 60),
}));
};
window.__activeTabKey = function () {
const a = findActiveTab();
return a ? tabKey(a) : null;
};
return 'probe installed';
})();
@@ -0,0 +1,211 @@
// CLI for the agent-gateway probe.
//
// Bundles the TS probes with esbuild, pipes them into `agent-browser eval`,
// and persists dumps under `.agent-gateway/` (gitignored) for later use as
// streaming-replay test fixtures.
//
// Commands:
// bun run .agents/skills/agent-testing/scripts/agent-gateway/run.ts install
// Bundle probe-events.ts and inject into the CDP-attached browser.
// Re-installing clears all buffers and re-patches WebSocket / fetch.
//
// bun run .agents/skills/agent-testing/scripts/agent-gateway/run.ts dump [name]
// Stop the timeline timer, fetch the capture as JSON, write it to
// `.agent-gateway/<name>-<YYYYMMDD-HHmmss>.json`. `name` defaults to
// `dump`. Prints the absolute path written.
//
// bun run .agents/skills/agent-testing/scripts/agent-gateway/run.ts analyze [path]
// Run analyze-events.ts on the dump. `path` defaults to the most
// recently modified file in `.agent-gateway/`.
//
// Optional flags:
// --cdp <port> CDP port (default 9222)
// --browser <bin> agent-browser binary (default 'agent-browser')
import { spawn } from 'node:child_process';
import { mkdirSync, readdirSync, statSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
// .agents/skills/agent-testing/scripts/agent-gateway/ → 5 levels up
const PROJECT_ROOT = path.resolve(SCRIPT_DIR, '../../../../..');
const DUMP_DIR = path.join(PROJECT_ROOT, '.agent-gateway');
interface Flags {
browser: string;
cdp: string;
positional: string[];
}
function parseFlags(argv: string[]): Flags {
const out: Flags = { cdp: '9222', browser: 'agent-browser', positional: [] };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--cdp') out.cdp = argv[++i] ?? out.cdp;
else if (a === '--browser') out.browser = argv[++i] ?? out.browser;
else out.positional.push(a);
}
return out;
}
async function bundle(entry: string): Promise<string> {
// Bun.build is built into the Bun runtime — no external dep needed.
const r = await Bun.build({
entrypoints: [path.join(SCRIPT_DIR, entry)],
target: 'browser',
format: 'esm',
minify: false,
});
if (!r.success) {
const msgs = r.logs.map((l) => `${l.level}: ${l.message}`).join('\n');
throw new Error(`bundle failed for ${entry}:\n${msgs}`);
}
return await r.outputs[0].text();
}
function wrapIife(body: string, returnExpr: string): string {
// Wrap as an IIFE that swallows the bundled top-level (top-level `const`
// declarations get scoped to the IIFE, so re-injection doesn't conflict)
// and returns the configured expression — which `agent-browser eval`
// captures and prints to stdout.
return `(() => {\n${body}\n;return ${returnExpr};\n})()`;
}
function runAgentBrowserEval(flags: Flags, script: string): Promise<string> {
return new Promise((resolveP, rejectP) => {
const child = spawn(flags.browser, ['--cdp', flags.cdp, 'eval', '--stdin'], {
stdio: ['pipe', 'pipe', 'inherit'],
});
let stdout = '';
child.stdout.on('data', (chunk: Buffer) => {
stdout += chunk.toString('utf8');
});
child.on('error', rejectP);
child.on('close', (code) => {
if (code === 0) resolveP(stdout);
else rejectP(new Error(`agent-browser exited ${code}`));
});
child.stdin.write(script);
child.stdin.end();
});
}
// agent-browser prints eval results as JSON (string values are quoted).
function unquoteAgentBrowserResult(raw: string): string {
const trimmed = raw.trim();
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
try {
return JSON.parse(trimmed) as string;
} catch {
/* fall through */
}
}
return trimmed;
}
function isoStamp(): string {
const d = new Date();
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const hh = String(d.getHours()).padStart(2, '0');
const mi = String(d.getMinutes()).padStart(2, '0');
const ss = String(d.getSeconds()).padStart(2, '0');
return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;
}
function ensureDumpDir(): void {
mkdirSync(DUMP_DIR, { recursive: true });
}
function latestDump(): string | null {
ensureDumpDir();
const entries = readdirSync(DUMP_DIR)
.filter((f) => f.endsWith('.json'))
.map((f) => ({ f, mtime: statSync(path.join(DUMP_DIR, f)).mtimeMs }))
.sort((a, b) => b.mtime - a.mtime);
return entries[0] ? path.join(DUMP_DIR, entries[0].f) : null;
}
// ── Commands ────────────────────────────────────────────────────────
async function cmdInstall(flags: Flags): Promise<void> {
const body = await bundle('probe-events.ts');
const installMsg = JSON.stringify(
'events probe installed: WebSocket+fetch interception. ' +
'WS captures operationId= sockets (gateway), fetch captures /api/agent/stream (direct).',
);
const script = wrapIife(body, installMsg);
const out = await runAgentBrowserEval(flags, script);
console.log(unquoteAgentBrowserResult(out));
}
async function cmdDump(flags: Flags): Promise<void> {
const name = flags.positional[1] ?? 'dump';
const body = await bundle('probe-dump.ts');
const script = wrapIife(body, 'window.__PROBE_LAST_DUMP_JSON');
const raw = await runAgentBrowserEval(flags, script);
const json = unquoteAgentBrowserResult(raw);
ensureDumpDir();
const filename = `${name}-${isoStamp()}.json`;
const dumpPath = path.join(DUMP_DIR, filename);
writeFileSync(dumpPath, json, 'utf8');
// Validate by parsing the meta header so we error early on bad capture
try {
const parsed = JSON.parse(json) as {
meta?: { eventCount?: number; callCount?: number; sampleCount?: number };
};
const meta = parsed.meta ?? {};
console.log(
`wrote ${dumpPath} (${json.length} bytes events=${meta.eventCount ?? '?'} ` +
`calls=${meta.callCount ?? '?'} samples=${meta.sampleCount ?? '?'})`,
);
} catch {
console.log(`wrote ${dumpPath} (${json.length} bytes — JSON.parse failed; see file)`);
}
}
async function cmdAnalyze(flags: Flags): Promise<void> {
const target = flags.positional[1] ?? latestDump();
if (!target) {
console.error('no dump file found. run `dump` first or pass a path.');
process.exit(1);
}
const child = spawn('bun', ['run', path.join(SCRIPT_DIR, 'analyze-events.ts'), target], {
stdio: 'inherit',
});
await new Promise<void>((resolveP, rejectP) => {
child.on('error', rejectP);
child.on('close', (code) => (code === 0 ? resolveP() : rejectP(new Error(`exit ${code}`))));
});
}
// ── Entry point ─────────────────────────────────────────────────────
const flags = parseFlags(process.argv.slice(2));
const cmd = flags.positional[0];
const usage = `usage:
bun run run.ts install [--cdp 9222]
bun run run.ts dump [name] [--cdp 9222]
bun run run.ts analyze [path]
`;
if (!cmd) {
console.error(usage);
process.exit(1);
}
try {
if (cmd === 'install') await cmdInstall(flags);
else if (cmd === 'dump') await cmdDump(flags);
else if (cmd === 'analyze') await cmdAnalyze(flags);
else {
console.error(`unknown command: ${cmd}\n\n${usage}`);
process.exit(1);
}
} catch (e: any) {
console.error(e?.stack ?? e);
process.exit(1);
}
@@ -0,0 +1,72 @@
// Run N round-trip tab switches with event markers timed against the probe.
//
// agent-browser --cdp 9222 eval --stdin < tab-switch.js
//
// Captures the currently-active tab as the BACK target and the rightmost
// inactive tab as the AWAY target. Both are addressed by their stable
// data-contextmenu-trigger key (NOT by visible title — the active tab's
// innerText embeds a ` · <agent name>` suffix that breaks text matching).
//
// Fires the loop in the background and returns immediately so the
// agent-browser eval doesn't have to await the full ROUND_TRIPS × DWELL_MS
// duration. Wait on the `SWITCH_LOOP_DONE` event before dumping.
//
// Refuses to launch if a previous loop is still in flight.
//
// Requires probe.js to have been installed first (provides
// window.__PROBE_EVENT / __listTabs / __clickTabByKey / __activeTabKey).
(function () {
const ROUND_TRIPS = 4;
const DWELL_MS = 10_000;
if (!window.__PROBE_EVENT || !window.__listTabs || !window.__clickTabByKey) {
return 'probe not installed — eval probe.js first';
}
if (window.__SWITCH_LOOP_RUNNING) {
return 'switch loop already running — wait for SWITCH_LOOP_DONE first';
}
const tabs = window.__listTabs();
const activeTab = tabs.find((t) => t.active);
if (!activeTab) return 'no active tab — abort';
// Pick the first inactive tab as AWAY target. With multiple inactive tabs
// you'll usually want the one that's stable across the test — feel free
// to swap to tabs[tabs.length-1] if you want the rightmost.
const inactives = tabs.filter((t) => !t.active);
if (inactives.length === 0) return 'no inactive tab to switch to — abort';
const awayTab = inactives.at(-1); // rightmost inactive
const BACK_KEY = activeTab.key;
const AWAY_KEY = awayTab.key;
window.__SWITCH_LOOP_RUNNING = true;
window.__PROBE_EVENT('SWITCH_LOOP_CONFIG:back=' + BACK_KEY + ',away=' + AWAY_KEY);
(async function () {
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
try {
window.__PROBE_EVENT('SWITCH_LOOP_START');
for (let i = 1; i <= ROUND_TRIPS; i++) {
window.__PROBE_EVENT('AWAY_' + i);
const awayResult = window.__clickTabByKey(AWAY_KEY);
window.__PROBE_EVENT('AWAY_' + i + '_RES:' + awayResult.slice(0, 50));
await sleep(DWELL_MS);
window.__PROBE_EVENT('BACK_' + i);
const backResult = window.__clickTabByKey(BACK_KEY);
window.__PROBE_EVENT('BACK_' + i + '_RES:' + backResult.slice(0, 50));
await sleep(DWELL_MS);
}
window.__PROBE_EVENT('SWITCH_LOOP_DONE');
} finally {
window.__SWITCH_LOOP_RUNNING = false;
}
})();
return 'switch loop kicked off (BACK=' + BACK_KEY + ', AWAY=' + AWAY_KEY + ')';
})();
@@ -0,0 +1,113 @@
// Shared types between the in-browser probe and the Node-side analyzer.
// Kept tiny on purpose — anything the analyzer can re-derive is left off.
export interface ProbeStreamEvent {
/** Summarized payload — long strings truncated, arrays printed as Array(N) */
data?: Record<string, unknown>;
/** Keys present on the event's `data` payload — useful at a glance */
dataKeys?: string[];
/** ServerMessage.id — gateway WS frames carry an event-id we may resume from */
eventId?: string | null;
message?: string;
/** Last 10 chars of the operationId (full id is excessively long) */
opIdTail: string;
raw?: string;
/** Raw frame byte length, when applicable */
rawLen?: number;
/** For non-agent_event server frames (auth_success, heartbeat_ack, …) */
serverType?: string;
sseEvent?: string;
status?: number;
stepIndex?: number;
/** Milliseconds since the probe's t0 (install time). */
t: number;
/** 'ws' for gateway WebSocket frames, 'sse' for direct /api/agent/stream */
transport: 'ws' | 'sse';
/** Either the AgentStreamEvent.type, or a probe sentinel like `_WS_OPEN_` */
type: string;
url?: string;
}
export interface ProbeActionCall {
args?: {
count?: number;
context?: unknown;
params?: unknown;
};
error?: string;
/** `replaceMessages` / `refreshMessages` / `MARK:<label>` / `_WRAP_ERROR_` */
name: string;
stack?: string;
t: number;
}
export interface ProbeMessageSummary {
/** children.length */
chN: number;
/** content.length */
cLen: number;
/** Last 8 chars of the message id */
id: string;
/** reasoning.content.length */
rLen: number;
role: string;
/** tools.length */
tools: number;
}
export interface ProbeTimelineSample {
/** Last 10 chars of activeTopicId, or null */
activeTopic: string | null;
/** Per-key breakdown: display count, db count, message summaries */
byKey: Record<
string,
{
n: number;
dbN: number;
msgs: ProbeMessageSummary[];
}
>;
err?: string;
/** All messagesMap keys that have content at this moment */
keys: string[];
/** Number of operations in 'running' status */
runOps: number;
t: number;
}
export interface ProbeDumpMeta {
callCount: number;
/** Date.now() at dump call */
collectedAt: number;
eventCount: number;
sampleCount: number;
/** Date.now() at probe install */
t0: number;
}
export interface ProbeDump {
actionCalls: ProbeActionCall[];
meta: ProbeDumpMeta;
streamEvents: ProbeStreamEvent[];
timeline: ProbeTimelineSample[];
}
/**
* Globals the probe attaches to `window`. Keeps `as any` casts at the boundary
* instead of sprinkling them through the probe body.
*/
declare global {
interface Window {
__clickTabByKey?: (key: string) => string;
__listTabs?: () => Array<{ i: number; key: string | null; active: boolean; title: string }>;
__LOBE_STORES?: Record<string, () => any>;
__PROBE_ACTION_CALLS?: ProbeActionCall[];
__PROBE_EVENT?: (label: string) => void;
__PROBE_MSG_TIMELINE?: ProbeTimelineSample[];
__PROBE_ORIG_FETCH?: typeof fetch;
__PROBE_ORIG_WEBSOCKET?: typeof WebSocket;
__PROBE_STREAM_EVENTS?: ProbeStreamEvent[];
__PROBE_T0?: number;
__PROBE_TIMELINE_TIMER?: ReturnType<typeof setInterval> | null;
}
}
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# app-probe.sh — standardized probes for a running LobeHub app (Electron via
# CDP, or a web agent-browser session). Use these instead of hand-rolling
# `window.__LOBE_STORES` eval snippets — especially the auth check.
#
# Usage:
# app-probe.sh auth # { isSignedIn, userId } from the user store
# app-probe.sh route # current SPA route
# app-probe.sh ops # running chat operations (type / status / startTime)
# app-probe.sh goto <path> # navigate the SPA to a route (full reload), e.g. goto /agent/agt_xxx
# app-probe.sh errors-install # install a console.error interceptor
# app-probe.sh errors # dump errors captured since errors-install
#
# Target selection (default: Electron over CDP 9222):
# AB_TARGET="--cdp 9222" # Electron (default; CDP_PORT also honored)
# AB_TARGET="--session lobehub-dev" # web agent-browser session
#
# Common routes (desktop SPA): / /agent/<agentId> /agent/<agentId>/<topicId>
# /task /task/<taskId> /page /settings /community
set -euo pipefail
AB_TARGET="${AB_TARGET:---cdp ${CDP_PORT:-9222}}"
run_eval() {
# shellcheck disable=SC2086
agent-browser $AB_TARGET eval --stdin
}
case "${1:-}" in
auth)
run_eval << 'EVALEOF'
(function () {
var stores = window.__LOBE_STORES;
if (!stores || !stores.user) return JSON.stringify({ ok: false, reason: 'no user store — app not loaded yet?' });
var u = stores.user();
return JSON.stringify({ ok: !!u.isSignedIn, isSignedIn: !!u.isSignedIn, userId: (u.user && u.user.id) || null });
})()
EVALEOF
;;
route)
run_eval << 'EVALEOF'
location.pathname + location.search + location.hash
EVALEOF
;;
ops)
run_eval << 'EVALEOF'
(function () {
var stores = window.__LOBE_STORES;
if (!stores || !stores.chat) return JSON.stringify({ ok: false, reason: 'no chat store — open a conversation first' });
var ops = Object.values(stores.chat().operations || {});
var running = ops.filter(function (o) { return o.status === 'running'; });
return JSON.stringify({
ok: true,
running: running.map(function (o) { return { startTime: o.metadata && o.metadata.startTime, type: o.type }; }),
runningCount: running.length,
total: ops.length,
});
})()
EVALEOF
;;
goto)
TARGET_PATH="${2:?Usage: app-probe.sh goto <path>}"
# shellcheck disable=SC2086
agent-browser $AB_TARGET eval "location.href = '$TARGET_PATH'" > /dev/null
sleep 2
bash "${BASH_SOURCE[0]}" route
;;
errors-install)
run_eval << 'EVALEOF'
(function () {
window.__CAPTURED_ERRORS = [];
var orig = console.error;
console.error = function () {
var msg = Array.from(arguments).map(function (a) {
if (a instanceof Error) return a.message;
return typeof a === 'object' ? JSON.stringify(a) : String(a);
}).join(' ');
window.__CAPTURED_ERRORS.push(msg);
orig.apply(console, arguments);
};
return 'installed';
})()
EVALEOF
;;
errors)
run_eval << 'EVALEOF'
JSON.stringify(window.__CAPTURED_ERRORS || 'interceptor not installed — run errors-install first')
EVALEOF
;;
*)
echo "Usage: $0 {auth|route|ops|goto <path>|errors-install|errors}" >&2
exit 2
;;
esac
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
#
# capture-app-window.sh — Capture a screenshot of a specific app window
#
# Uses CGWindowList via Swift to find the window by process name, then
# screencapture -l <windowID> to capture only that window.
# Falls back to full-screen capture if the window is not found.
#
# Usage:
# ./capture-app-window.sh <process_name> <output_path>
#
# Arguments:
# process_name — The process/owner name as shown in Activity Monitor
# (e.g., "Discord", "Slack", "Telegram", "WeChat", "QQ", "Lark")
# output_path — Path to save the screenshot (e.g., /tmp/screenshot.png)
#
# Examples:
# ./capture-app-window.sh "Discord" /tmp/discord.png
# ./capture-app-window.sh "Slack" /tmp/slack.png
# ./capture-app-window.sh "微信" /tmp/wechat.png
#
set -euo pipefail
PROCESS="${1:?Usage: capture-app-window.sh <process_name> <output_path>}"
OUTPUT="${2:?Usage: capture-app-window.sh <process_name> <output_path>}"
# Find the CGWindowID for the target process using Swift + CGWindowList
# Pass process name via environment variable (swift -e doesn't support -- args)
WINDOW_ID=$(TARGET_PROCESS="$PROCESS" swift -e '
import Cocoa
import Foundation
let target = ProcessInfo.processInfo.environment["TARGET_PROCESS"] ?? ""
let windowList = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as! [[String: Any]]
for w in windowList {
let owner = w["kCGWindowOwnerName"] as? String ?? ""
let layer = w["kCGWindowLayer"] as? Int ?? -1
let bounds = w["kCGWindowBounds"] as? [String: Any] ?? [:]
let ww = bounds["Width"] as? Double ?? 0
let wh = bounds["Height"] as? Double ?? 0
let wid = w["kCGWindowNumber"] as? Int ?? 0
// Match process name, normal window layer (0), and reasonable size
if owner == target && layer == 0 && ww > 200 && wh > 200 {
print(wid)
break
}
}
' 2>/dev/null || true)
if [ -n "$WINDOW_ID" ]; then
screencapture -l "$WINDOW_ID" -x "$OUTPUT"
else
echo "[capture] Warning: Could not find window for '$PROCESS', falling back to full screen"
screencapture -x "$OUTPUT"
fi
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env bash
#
# electron-dev.sh — Manage Electron dev environment for testing
#
# Usage:
# ./electron-dev.sh start # Kill existing, start fresh, wait until ready
# ./electron-dev.sh stop # Kill all Electron-related processes
# ./electron-dev.sh status # Check if Electron is running and CDP is reachable
# ./electron-dev.sh restart # Stop then start
#
# Environment variables:
# CDP_PORT — Chrome DevTools Protocol port (default: 9222)
# ELECTRON_LOG — Log file path (default: /tmp/electron-dev.log)
# ELECTRON_WAIT_S — Max seconds to wait for CDP to become reachable (default: 90)
# RENDERER_WAIT_S — Max seconds to wait for SPA after CDP is up (default: 60)
# FORCE_KILL_USER — When set to 1, silently kill the user's `bun run dev`
# Electron without confirmation (default: always confirm-by-action)
#
set -euo pipefail
CDP_PORT="${CDP_PORT:-9222}"
ELECTRON_LOG="${ELECTRON_LOG:-/tmp/electron-dev.log}"
ELECTRON_WAIT_S="${ELECTRON_WAIT_S:-90}"
RENDERER_WAIT_S="${RENDERER_WAIT_S:-60}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
PIDFILE="/tmp/electron-dev-cdp-${CDP_PORT}.pid"
# Project-scoped electron path prefix used for pgrep matching. Any Electron
# binary from this project (main + helpers, with or without --remote-debugging-port)
# starts with this string in its argv[0], so a single substring match catches all.
PROJECT_ELECTRON_PATH="${PROJECT_ROOT}/apps/desktop/node_modules/.pnpm/electron@"
# ── Helpers ──────────────────────────────────────────────────────────
# Print pid + every descendant pid (DFS via pgrep -P).
expand_descendants() {
local pid="$1"
echo "$pid"
local children
children=$(pgrep -P "$pid" 2>/dev/null || true)
for c in $children; do
expand_descendants "$c"
done
}
# Find seed PIDs related to this project's Electron dev session.
# Matches REGARDLESS of whether --remote-debugging-port was passed, so it also
# catches a plain `bun run dev` session the user started outside this script.
find_project_pids() {
local pids=""
# 1. Any process whose command line mentions this project's electron path
# (covers the main Electron binary AND every Helper subprocess)
local electron_pids
electron_pids=$(pgrep -f "$PROJECT_ELECTRON_PATH" 2>/dev/null || true)
pids="$pids $electron_pids"
# 2. electron-vite dev server (narrow match to avoid catching unrelated Vite invocations)
local vite_pids
vite_pids=$(pgrep -f "electron-vite[/.].*\\bdev\\b" 2>/dev/null || true)
pids="$pids $vite_pids"
# 3. The launcher subshell from a previous `start` (saved to pidfile)
if [ -f "$PIDFILE" ]; then
local saved_pid
saved_pid=$(cat "$PIDFILE" 2>/dev/null || true)
if [ -n "$saved_pid" ] && kill -0 "$saved_pid" 2>/dev/null; then
pids="$pids $saved_pid"
fi
fi
# 4. Whatever is currently bound to the CDP port — catches strays whose
# binary path doesn't match (e.g. orphaned from a crashed restart)
local port_pid
port_pid=$(lsof -ti tcp:"$CDP_PORT" -sTCP:LISTEN 2>/dev/null || true)
pids="$pids $port_pid"
# `|| true` because `grep -v '^$'` exits 1 when input has no non-empty
# lines, which (with pipefail + set -e) silently kills the caller.
echo "$pids" | tr ' ' '\n' | sort -u | grep -v '^$' | tr '\n' ' ' || true
}
# Wait for the CDP HTTP endpoint to respond, with a deadline + early bail-out
# if the launcher process died (no point waiting if Electron crashed).
wait_for_cdp() {
local deadline=$(( $(date +%s) + ELECTRON_WAIT_S ))
echo "[electron-dev] Waiting for CDP on port ${CDP_PORT} (up to ${ELECTRON_WAIT_S}s)..."
while [ "$(date +%s)" -lt "$deadline" ]; do
if curl -sf --max-time 2 "http://localhost:${CDP_PORT}/json/version" >/dev/null 2>&1; then
echo "[electron-dev] CDP is reachable."
return 0
fi
# If our launcher subshell died, abort early so we don't hang the full timeout
if [ -f "$PIDFILE" ]; then
local saved_pid
saved_pid=$(cat "$PIDFILE" 2>/dev/null || true)
if [ -n "$saved_pid" ] && ! kill -0 "$saved_pid" 2>/dev/null; then
echo "[electron-dev] Launcher PID $saved_pid is gone before CDP came up."
echo "[electron-dev] Last 30 lines of $ELECTRON_LOG:"
tail -30 "$ELECTRON_LOG" 2>/dev/null || true
return 1
fi
fi
sleep 2
done
echo "[electron-dev] ERROR: CDP did not respond within ${ELECTRON_WAIT_S}s"
echo "[electron-dev] Last 30 lines of $ELECTRON_LOG:"
tail -30 "$ELECTRON_LOG" 2>/dev/null || true
return 1
}
# After CDP is up, wait until the SPA renders interactive elements.
wait_for_renderer() {
local deadline=$(( $(date +%s) + RENDERER_WAIT_S ))
echo "[electron-dev] Waiting for SPA to load (up to ${RENDERER_WAIT_S}s)..."
while [ "$(date +%s)" -lt "$deadline" ]; do
local snap
snap=$(agent-browser --cdp "$CDP_PORT" snapshot -i 2>&1 || true)
if echo "$snap" | grep -qE '\b(link|button)\b'; then
echo "[electron-dev] Renderer ready."
return 0
fi
sleep 2
done
echo "[electron-dev] WARNING: Renderer not interactive within ${RENDERER_WAIT_S}s — proceeding anyway."
return 0
}
# ── Commands ─────────────────────────────────────────────────────────
do_stop() {
echo "[electron-dev] Stopping Electron dev environment..."
local seed_pids
seed_pids=$(find_project_pids)
# Expand to include all descendants — catches helpers spawned by the main
# process AFTER our pgrep snapshot, and the launcher's child node/electron-vite
# process tree.
local all_pids=""
for pid in $seed_pids; do
all_pids="$all_pids $(expand_descendants "$pid")"
done
all_pids=$(echo "$all_pids" | tr ' ' '\n' | sort -u | grep -v '^$' | tr '\n' ' ' || true)
if [ -z "$all_pids" ]; then
echo "[electron-dev] No project Electron/vite processes found."
else
local count
count=$(echo "$all_pids" | tr ' ' '\n' | grep -c .)
echo "[electron-dev] Sending SIGTERM to $count process(es): $all_pids"
for pid in $all_pids; do
kill "$pid" 2>/dev/null || true
done
# Wait up to 5s for graceful exit
local waited=0
while [ $waited -lt 5 ]; do
local any_alive=0
for pid in $all_pids; do
if kill -0 "$pid" 2>/dev/null; then any_alive=1; break; fi
done
[ "$any_alive" = "0" ] && break
sleep 1
waited=$((waited + 1))
done
# SIGKILL anyone still alive
for pid in $all_pids; do
if kill -0 "$pid" 2>/dev/null; then
echo "[electron-dev] Force-killing PID $pid"
kill -9 "$pid" 2>/dev/null || true
fi
done
fi
# Belt-and-suspenders: anything still bound to the CDP port goes away
local port_pid
port_pid=$(lsof -ti tcp:"$CDP_PORT" -sTCP:LISTEN 2>/dev/null || true)
if [ -n "$port_pid" ]; then
echo "[electron-dev] Port $CDP_PORT still bound by PID $port_pid; force-killing"
# shellcheck disable=SC2086
kill -9 $port_pid 2>/dev/null || true
fi
# Also re-sweep the project's electron processes — sometimes the OS spawns
# new helpers during shutdown that didn't exist when we first enumerated.
local stragglers
stragglers=$(pgrep -f "$PROJECT_ELECTRON_PATH" 2>/dev/null || true)
if [ -n "$stragglers" ]; then
echo "[electron-dev] Cleaning up stragglers: $stragglers"
for pid in $stragglers; do
kill -9 "$pid" 2>/dev/null || true
done
fi
# Close any agent-browser sessions connected to this port
agent-browser --cdp "$CDP_PORT" close --all 2>/dev/null || true
rm -f "$PIDFILE"
echo "[electron-dev] Stopped."
}
do_status() {
local pids
pids=$(find_project_pids)
if [ -z "$pids" ]; then
echo "[electron-dev] No project Electron processes found."
return 1
fi
echo "[electron-dev] Project processes: $pids"
if curl -sf --max-time 2 "http://localhost:${CDP_PORT}/json/version" >/dev/null 2>&1; then
local url
url=$(agent-browser --cdp "$CDP_PORT" get url 2>&1 | tail -1 || echo "?")
echo "[electron-dev] CDP port ${CDP_PORT} is reachable. URL: $url"
return 0
else
echo "[electron-dev] CDP port ${CDP_PORT} is NOT reachable (no --remote-debugging-port, or still loading)."
return 2
fi
}
do_start() {
# Already up and CDP is reachable → nothing to do
if curl -sf --max-time 2 "http://localhost:${CDP_PORT}/json/version" >/dev/null 2>&1; then
echo "[electron-dev] CDP already reachable on port $CDP_PORT. Skipping start."
echo "[electron-dev] Use 'restart' to force a fresh session."
return 0
fi
# Detect the user's existing dev session (or stale processes) BEFORE killing
local existing
existing=$(find_project_pids)
if [ -n "$existing" ]; then
echo "[electron-dev] Existing project Electron/vite processes detected:"
echo "$existing" | tr ' ' '\n' | sed 's/^/[electron-dev] PID /'
echo "[electron-dev] Tearing them down so we can start a CDP-enabled session..."
fi
do_stop
# Wait for port + user-data-dir locks to release. Without this, the new
# Electron may fail with "user data directory in use" or fail to bind CDP.
local waited=0
while [ $waited -lt 10 ]; do
if ! lsof -i tcp:"$CDP_PORT" >/dev/null 2>&1 \
&& ! pgrep -f "$PROJECT_ELECTRON_PATH" >/dev/null 2>&1; then
break
fi
[ $waited -eq 0 ] && echo "[electron-dev] Waiting for port + Electron locks to release..."
sleep 1
waited=$((waited + 1))
done
echo "[electron-dev] Starting Electron dev server..."
echo "[electron-dev] Project: $PROJECT_ROOT"
echo "[electron-dev] CDP port: $CDP_PORT"
echo "[electron-dev] Log: $ELECTRON_LOG"
: > "$ELECTRON_LOG" # Truncate log
# Launch in a new session (setsid) so the whole process tree shares a PGID
# we can later signal in one shot. `setsid bash -c '... exec ...' &` keeps
# the bash shell as the session leader; its PID is what we save.
# macOS doesn't ship setsid by default — fall back to plain bash; cleanup
# still works via `expand_descendants` walking the process tree.
local launch_cmd="
cd '$PROJECT_ROOT/apps/desktop'
exec npx electron-vite dev -- --remote-debugging-port=$CDP_PORT
"
if command -v setsid >/dev/null 2>&1; then
setsid bash -c "$launch_cmd" >> "$ELECTRON_LOG" 2>&1 < /dev/null &
else
bash -c "$launch_cmd" >> "$ELECTRON_LOG" 2>&1 < /dev/null &
fi
local launcher_pid=$!
echo "$launcher_pid" > "$PIDFILE"
echo "[electron-dev] Launcher PID (session leader): $launcher_pid"
if ! wait_for_cdp; then
echo "[electron-dev] Failed to bring up CDP. Cleaning up..."
do_stop
return 1
fi
if ! wait_for_renderer; then
echo "[electron-dev] Renderer not interactive — you may need to wait more."
fi
echo "[electron-dev] Ready! Use: agent-browser --cdp $CDP_PORT snapshot -i"
}
do_restart() {
do_stop
sleep 1
do_start
}
# ── Main ─────────────────────────────────────────────────────────────
case "${1:-help}" in
start) do_start ;;
stop) do_stop ;;
status) do_status ;;
restart) do_restart ;;
*)
echo "Usage: $0 {start|stop|status|restart}"
echo ""
echo " start — Start Electron dev with CDP. Detects + tears down any"
echo " existing project Electron (e.g. \`bun run dev\`) first."
echo " stop — Kill all project Electron/vite processes (main + helpers"
echo " + descendants), with SIGTERM → 5s wait → SIGKILL fallback."
echo " status — Check if Electron is running and CDP is reachable."
echo " restart — Stop then start."
exit 1
;;
esac
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
#
# record-app-screen.sh — Record the Electron app window (video + screenshots)
#
# Captures screenshots via agent-browser (CDP), then assembles into video on stop.
# Works on any screen (including external monitors) since it uses CDP, not screen capture.
#
# Usage:
# ./record-app-screen.sh start [output_name] # Begin recording
# ./record-app-screen.sh stop # Stop and save
# ./record-app-screen.sh status # Check recording state
#
# Outputs to .records/ directory:
# .records/<name>.mp4 — Video assembled from screenshots (~2 fps)
# .records/<name>/ — Screenshots every SCREENSHOT_INTERVAL seconds
#
# Prerequisites:
# - ffmpeg installed (bun add -g ffmpeg-static, or brew install ffmpeg)
# - agent-browser CLI installed
# - Electron app already running with CDP enabled
#
# Environment variables:
# CDP_PORT — Chrome DevTools Protocol port (default: 9222)
# SCREENSHOT_INTERVAL — Seconds between gallery screenshots (default: 3)
# VIDEO_FRAME_INTERVAL — Seconds between video frames (default: 0.5)
#
# Examples:
# ./electron-dev.sh start
# ./record-app-screen.sh start gateway-demo
# # ... run automation via agent-browser ...
# ./record-app-screen.sh stop
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
RECORDS_DIR="$PROJECT_DIR/.records"
PID_FILE="/tmp/record-app-screen.pids"
STATE_FILE="/tmp/record-app-screen.state"
CDP_PORT="${CDP_PORT:-9222}"
SCREENSHOT_INTERVAL="${SCREENSHOT_INTERVAL:-3}"
VIDEO_FRAME_INTERVAL="${VIDEO_FRAME_INTERVAL:-0.5}"
AB="agent-browser --cdp $CDP_PORT"
# ─── Commands ───
cmd_start() {
local output_name="${1:-recording-$(date +%Y%m%d-%H%M%S)}"
local output_video="$RECORDS_DIR/${output_name}.mp4"
local screenshot_dir="$RECORDS_DIR/${output_name}"
local frames_dir
frames_dir=$(mktemp -d /tmp/record-frames-XXXXXX)
if [ -f "$PID_FILE" ]; then
echo "[record] A recording is already active. Run '$0 stop' first."
exit 1
fi
mkdir -p "$RECORDS_DIR" "$screenshot_dir"
# Video frames loop (~2 fps via agent-browser CDP screenshots)
(
local idx=0
while true; do
local fname
fname=$(printf "%s/frame_%06d.png" "$frames_dir" "$idx")
$AB screenshot "$fname" 2>/dev/null || true
idx=$((idx + 1))
sleep "$VIDEO_FRAME_INTERVAL"
done
) &
local frames_pid=$!
# Gallery screenshots loop (every N seconds for human review)
(
local idx=0
while true; do
local fname
fname=$(printf "%s/%04d.png" "$screenshot_dir" "$idx")
$AB screenshot "$fname" 2>/dev/null || true
idx=$((idx + 1))
sleep "$SCREENSHOT_INTERVAL"
done
) &
local screenshot_pid=$!
# Save state
echo "$frames_pid $screenshot_pid" > "$PID_FILE"
echo "$output_video $frames_dir $screenshot_dir" > "$STATE_FILE"
echo "[record] Started!"
echo " Video frames: every ${VIDEO_FRAME_INTERVAL}s (PID $frames_pid)"
echo " Screenshots: every ${SCREENSHOT_INTERVAL}s → $screenshot_dir/"
echo " Stop with: $0 stop"
}
cmd_stop() {
if [ ! -f "$PID_FILE" ] || [ ! -f "$STATE_FILE" ]; then
echo "[record] No active recording found."
return 0
fi
local frames_pid screenshot_pid
read -r frames_pid screenshot_pid < "$PID_FILE"
local output_video frames_dir screenshot_dir
read -r output_video frames_dir screenshot_dir < "$STATE_FILE"
# Stop both capture loops
kill "$frames_pid" 2>/dev/null || true
kill "$screenshot_pid" 2>/dev/null || true
wait "$frames_pid" 2>/dev/null || true
wait "$screenshot_pid" 2>/dev/null || true
# Assemble frames into video
local frame_count
frame_count=$(ls -1 "$frames_dir"/frame_*.png 2>/dev/null | wc -l | tr -d ' ')
if [ "$frame_count" -gt 0 ]; then
echo "[record] Assembling $frame_count frames into video..."
ffmpeg -y -framerate 2 -i "$frames_dir/frame_%06d.png" \
-c:v libx264 -crf 23 -pix_fmt yuv420p -an \
"$output_video" > /tmp/ffmpeg-assemble.log 2>&1
if [ ! -s "$output_video" ]; then
echo " [warn] Video assembly failed. Check /tmp/ffmpeg-assemble.log"
echo " Frames preserved in: $frames_dir/"
fi
else
echo " [warn] No frames captured."
fi
rm -rf "$frames_dir" 2>/dev/null
rm -f "$PID_FILE" "$STATE_FILE"
local video_size screenshot_count
video_size=$(ls -lh "$output_video" 2>/dev/null | awk '{print $5}' || echo "?")
screenshot_count=$(ls -1 "$screenshot_dir"/*.png 2>/dev/null | wc -l | tr -d ' ' || echo "0")
echo "[record] Stopped!"
echo " Video: $output_video ($video_size)"
echo " Screenshots: ${screenshot_count} files in $screenshot_dir/"
echo " Play: open $output_video"
}
cmd_status() {
if [ ! -f "$PID_FILE" ]; then
echo "[record] No active recording."
return 0
fi
local frames_pid screenshot_pid
read -r frames_pid screenshot_pid < "$PID_FILE"
local frames_ok="no" screenshot_ok="no"
kill -0 "$frames_pid" 2>/dev/null && frames_ok="yes"
kill -0 "$screenshot_pid" 2>/dev/null && screenshot_ok="yes"
if [ -f "$STATE_FILE" ]; then
local output_video frames_dir screenshot_dir
read -r output_video frames_dir screenshot_dir < "$STATE_FILE"
local frame_count ss_count
frame_count=$(ls -1 "$frames_dir"/frame_*.png 2>/dev/null | wc -l | tr -d ' ' || echo "0")
ss_count=$(ls -1 "$screenshot_dir"/*.png 2>/dev/null | wc -l | tr -d ' ' || echo "0")
echo "[record] Active recording"
echo " Frames: $frame_count captured (running: $frames_ok)"
echo " Screenshots: $ss_count captured (running: $screenshot_ok)"
echo " Output: $output_video"
fi
}
# ─── Main ───
case "${1:-}" in
start) shift; cmd_start "$@" ;;
stop) cmd_stop ;;
status) cmd_status ;;
*)
echo "Usage: $0 {start [name] | stop | status}"
echo ""
echo " start [name] Start recording (default: recording-YYYYMMDD-HHMMSS)"
echo " stop Stop recording and save outputs"
echo " status Check if recording is active"
exit 1
;;
esac
@@ -0,0 +1,353 @@
#!/usr/bin/env bash
#
# record-electron-demo.sh — Record an automated demo of the Electron app
#
# Usage:
# ./scripts/record-electron-demo.sh [script.sh] [output.mp4]
#
# script.sh — A shell script containing agent-browser commands to automate.
# It receives the CDP port as $1. Defaults to a built-in queue-edit demo.
# output.mp4 — Output file path. Defaults to /tmp/electron-demo.mp4
#
# Prerequisites:
# - agent-browser CLI installed globally
# - ffmpeg installed (brew install ffmpeg)
# - Electron app NOT already running (script manages lifecycle)
#
# Examples:
# # Run built-in demo
# ./scripts/record-electron-demo.sh
#
# # Run custom automation script
# ./scripts/record-electron-demo.sh ./my-demo.sh /tmp/my-demo.mp4
#
set -euo pipefail
CDP_PORT=9222
DEMO_SCRIPT="${1:-}"
OUTPUT="${2:-/tmp/electron-demo.mp4}"
ELECTRON_LOG="/tmp/electron-dev.log"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
RECORD_PID=""
# ── Helpers ──────────────────────────────────────────────────────────
cleanup() {
echo "[cleanup] Stopping all processes..."
[ -n "$RECORD_PID" ] && kill -INT "$RECORD_PID" 2>/dev/null && sleep 2
pkill -f "electron-vite" 2>/dev/null || true
pkill -f "Electron" 2>/dev/null || true
pkill -f "agent-browser" 2>/dev/null || true
echo "[cleanup] Done."
}
trap cleanup EXIT
wait_for_electron() {
echo "[wait] Waiting for Electron to start..."
for i in $(seq 1 24); do
sleep 5
if strings "$ELECTRON_LOG" 2>/dev/null | grep -q "starting electron"; then
echo "[wait] Electron process ready."
return 0
fi
echo "[wait] Still waiting... (${i}/24)"
done
echo "[error] Electron failed to start within 120s"
exit 1
}
wait_for_renderer() {
echo "[wait] Waiting for renderer to load..."
sleep 15
agent-browser --cdp "$CDP_PORT" wait 3000
# Poll until interactive elements appear (SPA may take extra time)
for i in $(seq 1 12); do
local snap
snap=$(agent-browser --cdp "$CDP_PORT" snapshot -i 2>&1)
if echo "$snap" | grep -q 'link "'; then
echo "[wait] Renderer ready (interactive elements found)."
return 0
fi
echo "[wait] SPA still loading... (${i}/12)"
sleep 5
done
echo "[warn] Timed out waiting for interactive elements, proceeding anyway."
}
get_window_and_screen_info() {
# Returns: window_x window_y window_w window_h screen_index
# Uses Swift to find the Electron window bounds and which screen it's on
swift -e '
import Cocoa
let windowList = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID) as! [[String: Any]]
for w in windowList {
let owner = w["kCGWindowOwnerName"] as? String ?? ""
let name = w["kCGWindowName"] as? String ?? ""
let layer = w["kCGWindowLayer"] as? Int ?? -1
let bounds = w["kCGWindowBounds"] as? [String: Any] ?? [:]
let wx = bounds["X"] as? Double ?? 0
let wy = bounds["Y"] as? Double ?? 0
let ww = bounds["Width"] as? Double ?? 0
let wh = bounds["Height"] as? Double ?? 0
if (owner == "Electron" || owner == "LobeHub") && layer == 0 && name == "LobeHub" && ww > 200 && wh > 200 {
// Find which screen this window is on
let screens = NSScreen.screens
var screenIdx = 0
let windowCenter = NSPoint(x: wx + ww / 2, y: wy + wh / 2)
for (i, screen) in screens.enumerated() {
let frame = screen.frame
// Convert CG coords (top-left origin) to NSScreen coords (bottom-left origin)
let mainHeight = screens[0].frame.height
let screenTop = mainHeight - frame.origin.y - frame.height
let screenBottom = screenTop + frame.height
let screenLeft = frame.origin.x
let screenRight = screenLeft + frame.width
if windowCenter.x >= screenLeft && windowCenter.x <= screenRight &&
windowCenter.y >= screenTop && windowCenter.y <= screenBottom {
screenIdx = i
break
}
}
// Compute window position relative to the screen it is on
let screen = screens[screenIdx]
let mainHeight = screens[0].frame.height
let screenTop = mainHeight - screen.frame.origin.y - screen.frame.height
let relX = wx - screen.frame.origin.x
let relY = wy - screenTop
let scale = Int(screen.backingScaleFactor)
print("\(Int(relX)) \(Int(relY)) \(Int(ww)) \(Int(wh)) \(screenIdx) \(scale)")
break
}
}
'
}
start_recording() {
local rel_x=$1 rel_y=$2 w=$3 h=$4 screen_idx=$5 scale=$6
# ffmpeg avfoundation device index for screens
# List devices and find the one matching our screen index
local device_idx
device_idx=$(ffmpeg -f avfoundation -list_devices true -i "" 2>&1 \
| grep "Capture screen ${screen_idx}" \
| grep -oE '\[[0-9]+\]' | tr -d '[]' || true)
if [ -z "$device_idx" ]; then
echo "[warn] Could not find capture device for screen $screen_idx, trying default (3)"
device_idx=3
fi
# Scale coordinates to native resolution
local cx=$((rel_x * scale))
local cy=$((rel_y * scale))
local cw=$((w * scale))
local ch=$((h * scale))
echo "[record] Window: ${rel_x},${rel_y} ${w}x${h} on screen ${screen_idx} (scale=${scale})"
echo "[record] Crop: ${cx},${cy} ${cw}x${ch}, device: ${device_idx}"
echo "[record] Output: $OUTPUT"
ffmpeg -y \
-f avfoundation -framerate 30 -capture_cursor 1 -i "${device_idx}:" \
-vf "crop=${cw}:${ch}:${cx}:${cy},scale=${w}:${h}" \
-c:v libx264 -crf 23 -preset fast -an \
"$OUTPUT" \
> /tmp/ffmpeg-record.log 2>&1 &
RECORD_PID=$!
sleep 2
if ! kill -0 "$RECORD_PID" 2>/dev/null; then
echo "[error] ffmpeg failed to start. Log:"
cat /tmp/ffmpeg-record.log
RECORD_PID=""
return 1
fi
echo "[record] Recording started (PID=$RECORD_PID)"
}
stop_recording() {
if [ -n "$RECORD_PID" ]; then
echo "[record] Stopping recording..."
kill -INT "$RECORD_PID" 2>/dev/null || true
wait "$RECORD_PID" 2>/dev/null || true
RECORD_PID=""
echo "[record] Saved to $OUTPUT"
ls -lh "$OUTPUT"
fi
}
# ── Built-in demo: Queue Edit ────────────────────────────────────────
find_input_ref() {
local port=$1
agent-browser --cdp "$port" snapshot -i -C 2>&1 \
| grep "editable" \
| grep -oE 'ref=e[0-9]+' \
| head -1 \
| sed 's/ref=//'
}
builtin_demo() {
local port=$1
echo "[demo] Step 1: Navigate to first available agent"
local snapshot agent_ref
snapshot=$(agent-browser --cdp "$port" snapshot -i 2>&1)
# Try Lobe AI first, then fall back to any agent link in the sidebar
agent_ref=$(echo "$snapshot" | grep -oE 'link "Lobe AI" \[ref=e[0-9]+\]' | grep -oE 'e[0-9]+' || true)
if [ -z "$agent_ref" ]; then
# Pick the first agent-like link (skip nav links)
agent_ref=$(echo "$snapshot" | grep 'link "' | grep -vE '"Home"|"Pages"|"Settings"|"Search"|"Resources"|"Marketplace"' | head -1 | grep -oE 'ref=e[0-9]+' | sed 's/ref=//' || true)
fi
if [ -z "$agent_ref" ]; then
echo "[error] No agent link found in snapshot"
echo "$snapshot" | head -30
return 1
fi
echo "[demo] Clicking agent ref: @$agent_ref"
agent-browser --cdp "$port" click "@$agent_ref"
sleep 3
echo "[demo] Step 2: Send first message (triggers AI generation)"
local input_ref
input_ref=$(find_input_ref "$port")
agent-browser --cdp "$port" click "@$input_ref"
agent-browser --cdp "$port" type "@$input_ref" "Write a 3000 word essay about the complete history of space exploration from Sputnik to the James Webb Space Telescope"
sleep 1
agent-browser --cdp "$port" press Enter
sleep 3
echo "[demo] Step 3: Queue message 1"
input_ref=$(find_input_ref "$port")
agent-browser --cdp "$port" click "@$input_ref"
agent-browser --cdp "$port" type "@$input_ref" "This message should be edited"
sleep 1
agent-browser --cdp "$port" press Enter
sleep 1
echo "[demo] Step 4: Queue message 2"
input_ref=$(find_input_ref "$port")
agent-browser --cdp "$port" click "@$input_ref"
agent-browser --cdp "$port" type "@$input_ref" "Another queued message"
sleep 1
agent-browser --cdp "$port" press Enter
sleep 1
echo "[demo] Step 5: Verify queue has messages"
local queue_count
queue_count=$(agent-browser --cdp "$port" eval --stdin << 'EVALEOF'
(function() {
var chat = window.__LOBE_STORES.chat();
var total = 0;
Object.keys(chat.queuedMessages).forEach(function(k) {
total += chat.queuedMessages[k].length;
});
return String(total);
})()
EVALEOF
)
echo "[demo] Queue count: $queue_count"
if [ "$queue_count" = "0" ] || [ "$queue_count" = '"0"' ]; then
echo "[demo] Queue was already drained. Retrying..."
input_ref=$(find_input_ref "$port")
agent-browser --cdp "$port" click "@$input_ref"
agent-browser --cdp "$port" type "@$input_ref" "Now write another 3000 word essay about artificial intelligence from Turing to transformers covering every major breakthrough"
sleep 1
agent-browser --cdp "$port" press Enter
sleep 2
input_ref=$(find_input_ref "$port")
agent-browser --cdp "$port" click "@$input_ref"
agent-browser --cdp "$port" type "@$input_ref" "This message should be edited"
sleep 1
agent-browser --cdp "$port" press Enter
sleep 1
input_ref=$(find_input_ref "$port")
agent-browser --cdp "$port" click "@$input_ref"
agent-browser --cdp "$port" type "@$input_ref" "Another queued message"
sleep 1
agent-browser --cdp "$port" press Enter
sleep 1
fi
echo "[demo] Step 6: Scroll to show queue tray"
agent-browser --cdp "$port" scroll down 5000
sleep 2
echo "[demo] Step 7: Click edit button on first queued message"
agent-browser --cdp "$port" eval --stdin << 'EVALEOF'
(function() {
var chat = window.__LOBE_STORES.chat();
var keys = Object.keys(chat.queuedMessages);
for (var k = 0; k < keys.length; k++) {
var queue = chat.queuedMessages[keys[k]];
if (queue.length > 0) {
var targetText = queue[0].content;
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
while (walker.nextNode()) {
var node = walker.currentNode;
if (node.textContent.trim() === targetText) {
var row = node.parentElement.parentElement;
var buttons = row.querySelectorAll('[role="button"]');
if (buttons.length >= 1) {
buttons[0].click();
return 'clicked edit on: ' + targetText;
}
}
}
}
}
return 'edit button not found';
})()
EVALEOF
sleep 3
echo "[demo] Step 8: Show result — content restored to input"
sleep 3
echo "[demo] Complete!"
}
# ── Main ─────────────────────────────────────────────────────────────
echo "=== Electron Demo Recorder ==="
# 1. Kill existing instances
echo "[setup] Cleaning up existing processes..."
pkill -f "Electron" 2>/dev/null || true
pkill -f "electron-vite" 2>/dev/null || true
pkill -f "agent-browser" 2>/dev/null || true
sleep 3
# 2. Start Electron
echo "[setup] Starting Electron..."
cd "$PROJECT_ROOT/apps/desktop"
ELECTRON_ENABLE_LOGGING=1 npx electron-vite dev -- --remote-debugging-port="$CDP_PORT" > "$ELECTRON_LOG" 2>&1 &
wait_for_electron
wait_for_renderer
# 3. Get window position and start recording
WIN_INFO=$(get_window_and_screen_info)
if [ -z "$WIN_INFO" ]; then
echo "[error] Could not find Electron window"
exit 1
fi
read -r WIN_X WIN_Y WIN_W WIN_H SCREEN_IDX SCALE <<< "$WIN_INFO"
start_recording "$WIN_X" "$WIN_Y" "$WIN_W" "$WIN_H" "$SCREEN_IDX" "$SCALE"
# 4. Run demo script
if [ -n "$DEMO_SCRIPT" ] && [ -f "$DEMO_SCRIPT" ]; then
echo "[demo] Running custom script: $DEMO_SCRIPT"
bash "$DEMO_SCRIPT" "$CDP_PORT"
else
echo "[demo] Running built-in queue-edit demo"
builtin_demo "$CDP_PORT"
fi
# 5. Stop recording
stop_recording
echo "=== Done! Output: $OUTPUT ==="
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# record-gif.sh — capture a frame sequence via agent-browser (CDP) and
# synthesize a GIF for embedding in a test report.
#
# Use this whenever the asserted behavior is about CHANGE OVER TIME —
# streaming output, a ticking timer, loading states, animations. A static
# screenshot cannot prove those; a GIF can. Cloud-portable: frames come from
# CDP rendering, no OS-level screen capture.
#
# Usage:
# record-gif.sh <output.gif> <duration_seconds> [fps]
#
# AB_TARGET="--cdp 9222" # Electron (default; CDP_PORT honored)
# AB_TARGET="--session lobehub-dev" # web agent-browser session
# GIF_WIDTH=960 # output width (px), default 960
#
# Requires ffmpeg (`brew install ffmpeg`). Effective fps is capped by
# screenshot latency (~0.3-0.5s per frame); 1-2 fps is the realistic range.
#
# Example — record a 12s run and embed it in the report:
# ./record-gif.sh "$DIR/assets/case2-tray-running.gif" 12 2 &
# GIF_PID=$!
# # ... trigger the streaming behavior ...
# wait $GIF_PID
set -euo pipefail
OUT="${1:?Usage: record-gif.sh <output.gif> <duration_seconds> [fps]}"
DUR="${2:?Usage: record-gif.sh <output.gif> <duration_seconds> [fps]}"
FPS="${3:-2}"
AB_TARGET="${AB_TARGET:---cdp ${CDP_PORT:-9222}}"
GIF_WIDTH="${GIF_WIDTH:-960}"
command -v ffmpeg > /dev/null || {
echo "ffmpeg not found — install with: brew install ffmpeg" >&2
exit 1
}
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
FRAMES=$((DUR * FPS))
INTERVAL=$(python3 -c "print(1 / $FPS)")
for i in $(seq -f '%04g' 1 "$FRAMES"); do
# shellcheck disable=SC2086
agent-browser $AB_TARGET screenshot "$TMP/frame-$i.png" > /dev/null 2>&1 || true
sleep "$INTERVAL"
done
CAPTURED=$(find "$TMP" -name 'frame-*.png' | wc -l | tr -d ' ')
[ "$CAPTURED" -gt 0 ] || {
echo "no frames captured — is the app reachable via $AB_TARGET?" >&2
exit 1
}
ffmpeg -y -loglevel error -framerate "$FPS" -pattern_type glob -i "$TMP/frame-*.png" \
-vf "fps=$FPS,scale=$GIF_WIDTH:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
"$OUT"
echo "$OUT ($CAPTURED frames @ ${FPS}fps)"
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# report-init.sh — scaffold a structured test report under .records/reports/.
#
# Format spec and evidence rules: ../references/report.md
#
# Usage:
# report-init.sh <slug> [title]
#
# Prints the report directory path (capture it: DIR=$(report-init.sh my-test)).
set -euo pipefail
SLUG="${1:?Usage: report-init.sh <slug> [title]}"
TITLE="${2:-$SLUG}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
TS="$(date +%Y%m%d-%H%M%S)"
DIR="$REPO_ROOT/.records/reports/$TS-$SLUG"
mkdir -p "$DIR/assets"
BRANCH=$(git -C "$REPO_ROOT" branch --show-current 2> /dev/null || echo "unknown")
COMMIT=$(git -C "$REPO_ROOT" rev-parse --short HEAD 2> /dev/null || echo "unknown")
DATE_HUMAN=$(date '+%Y-%m-%d %H:%M')
DATE_ISO=$(date '+%Y-%m-%dT%H:%M:%S%z')
cat > "$DIR/report.md" << EOF
# Test Report: $TITLE
## Scope
<!-- What changed / what is being verified -->
- Branch: \`$BRANCH\`
- Commit: \`$COMMIT\`
- Date: $DATE_HUMAN
## Environment
- Server: <!-- e.g. http://localhost:3010 -->
- Surfaces: <!-- cli / electron / web / bot:<platform> -->
## Cases
| # | Case | Surface | Steps | Expected | Actual | Status | Evidence |
| - | ---- | ------- | ----- | -------- | ------ | ------ | -------- |
| 1 | | | | | | | |
## Evidence
<!-- Embed screenshots: ![case 1](assets/case1.png) -->
<!-- CLI transcripts in fenced blocks, with the exact command -->
## Verdict
- Passed: 0 / 0
- Failed: 0
- Blocked: 0
- Score (optional): —
- Open issues / follow-ups:
EOF
cat > "$DIR/result.json" << EOF
{
"title": "$TITLE",
"createdAt": "$DATE_ISO",
"branch": "$BRANCH",
"commit": "$COMMIT",
"surfaces": [],
"cases": [],
"summary": { "total": 0, "passed": 0, "failed": 0, "blocked": 0, "verdict": "pending" }
}
EOF
echo "$DIR"
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env bash
# setup-auth.sh — one-stop auth setup & check for local agent testing.
#
# Auth is the gate for all automated testing: prepare it BEFORE writing any
# test step. Background and failure modes: ../references/auth.md
#
# Usage:
# setup-auth.sh status # check server + CLI + web auth readiness
# setup-auth.sh cli # interactive CLI device-code login (run by a human)
# setup-auth.sh web # stdin = Cookie header -> inject into agent-browser session
# setup-auth.sh web-verify # live-check the agent-browser session is authenticated
#
# Env:
# SERVER_URL (default http://localhost:3010) dev server under test
# SESSION (default lobehub-dev) agent-browser session name
# AUTH_DIR (default ~/.lobehub-agent-testing) where web state is persisted
set -euo pipefail
SERVER_URL="${SERVER_URL:-http://localhost:3010}"
SESSION="${SESSION:-lobehub-dev}"
AUTH_DIR="${AUTH_DIR:-$HOME/.lobehub-agent-testing}"
STATE_FILE="$AUTH_DIR/web-state.json"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
CLI_HOME="$REPO_ROOT/apps/cli/.lobehub-dev"
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; }
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; }
note() { printf ' %s\n' "$1"; }
check_server() {
local code
code=$(curl -s -o /dev/null -w '%{http_code}' "$SERVER_URL/" 2> /dev/null || true)
if [[ "$code" =~ ^[23] ]]; then
ok "dev server reachable at $SERVER_URL"
else
bad "dev server NOT reachable at $SERVER_URL (http_code='$code')"
note "start it: pnpm run dev:next (see references/dev-server.md)"
return 1
fi
}
check_cli() {
if [[ -f "$CLI_HOME/settings.json" ]] && grep -q "$SERVER_URL" "$CLI_HOME/settings.json"; then
ok "CLI logged in to $SERVER_URL (creds: apps/cli/.lobehub-dev)"
else
bad "CLI not logged in to $SERVER_URL"
note "ask the user to run:"
note "cd apps/cli && LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts login --server $SERVER_URL"
return 1
fi
}
check_web() {
if [[ -f "$STATE_FILE" ]]; then
ok "web auth state saved ($STATE_FILE)"
note "live-verify: $0 web-verify"
else
bad "no web auth state for agent-browser"
note "copy the Cookie header from Chrome DevTools (Network tab), then:"
note "pbpaste | $0 web (see references/auth.md)"
return 1
fi
}
check_electron() {
local cdp_port="${CDP_PORT:-9222}"
if ! curl -s -o /dev/null --max-time 2 "http://localhost:$cdp_port/json/version" 2> /dev/null; then
note "electron: not running (CDP $cdp_port unreachable) — start with electron-dev.sh; check skipped"
return 0
fi
local probe result
probe="$(dirname "${BASH_SOURCE[0]}")/app-probe.sh"
result=$(bash "$probe" auth 2> /dev/null || true)
# agent-browser eval returns the JSON string with escaped quotes — normalize.
result="${result//\\/}"
if [[ "$result" == *'"isSignedIn":true'* ]]; then
ok "electron app signed in ($result)"
else
bad "electron app NOT signed in ($result)"
note "log in once manually inside the app (state persists across restarts)"
return 1
fi
}
cmd_status() {
echo "agent-testing auth status (SERVER_URL=$SERVER_URL):"
local rc=0
check_server || rc=1
check_cli || rc=1
check_web || rc=1
check_electron || rc=1
if [[ $rc -eq 0 ]]; then
echo "all green — safe to start automated testing."
else
echo "auth NOT ready — fix the ✘ items before writing any test step."
fi
return $rc
}
cmd_cli() {
echo "Starting CLI device-code login against $SERVER_URL ..."
echo "(opens a browser authorization — must be run by a human in a terminal)"
cd "$REPO_ROOT/apps/cli"
LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts login --server "$SERVER_URL"
}
# Build a Playwright storageState file from a raw Cookie header on stdin,
# keeping only the better-auth cookies. See references/auth.md for why the
# header must come from a Network request (HttpOnly) and why httpOnly=false.
cmd_web() {
mkdir -p "$AUTH_DIR"
python3 - "$STATE_FILE" << 'PY'
import json, sys, time
raw = sys.stdin.read().strip()
if raw.lower().startswith("cookie:"):
raw = raw.split(":", 1)[1].strip()
WANTED = {"better-auth.session_token", "better-auth.state"}
exp = int(time.time()) + 30 * 24 * 3600 # 30 days
cookies = []
for pair in raw.split("; "):
if "=" not in pair:
continue
name, _, value = pair.partition("=")
if name not in WANTED:
continue
cookies.append({
"name": name,
"value": value,
"domain": "localhost",
"path": "/",
"expires": exp,
"httpOnly": False,
"secure": False,
"sameSite": "Lax",
})
if not cookies:
sys.stderr.write("no better-auth cookies found in input — paste the raw Cookie header from a Network request\n")
sys.exit(1)
with open(sys.argv[1], "w") as f:
json.dump({"cookies": cookies, "origins": []}, f, indent=2)
print(f"wrote {len(cookies)} cookie(s) to {sys.argv[1]}")
PY
agent-browser --session "$SESSION" state load "$STATE_FILE"
cmd_web_verify
}
cmd_web_verify() {
agent-browser --session "$SESSION" open "$SERVER_URL/" > /dev/null
local url
url=$(agent-browser --session "$SESSION" get url)
if [[ "$url" == *"/signin"* || "$url" == *"/login"* ]]; then
bad "agent-browser session '$SESSION' NOT authenticated (landed on $url)"
note "re-copy the Cookie header and re-run: pbpaste | $0 web"
return 1
fi
ok "agent-browser session '$SESSION' authenticated (at $url)"
}
case "${1:-status}" in
status) cmd_status ;;
cli) cmd_cli ;;
web) cmd_web ;;
web-verify) cmd_web_verify ;;
*)
echo "Usage: $0 {status|cli|web|web-verify}" >&2
exit 2
;;
esac
+154
View File
@@ -0,0 +1,154 @@
# Electron (LobeHub Desktop) UI Testing
Default surface for verifying **pure frontend changes** (components, store logic, styles, interactions) in the primary product shape. Drives the Electron renderer over CDP with `agent-browser` — see [../references/agent-browser.md](../references/agent-browser.md) for the full command reference.
**Auth**: the Electron app keeps its own persistent login state — log in once manually in the app; sessions survive restarts. Run `../scripts/setup-auth.sh status` before testing (see [../references/auth.md](../references/auth.md)).
**Linux / headless (cloud)**: Electron itself runs on Linux, but it has no true headless mode — it needs a display server. In a headless environment wrap the launch with `xvfb-run` (virtual framebuffer). Everything CDP-based keeps working under Xvfb: the `agent-browser --cdp 9222` connection, snapshots, eval, and `agent-browser screenshot` (captured from the renderer via CDP, not the OS screen). What does NOT work on Linux: `capture-app-window.sh` (macOS `screencapture`), osascript, and the ffmpeg recording scripts in their current form.
### Setup / Teardown
Use the `electron-dev.sh` script to manage the Electron dev environment. It handles process lifecycle, waits for SPA readiness, and reliably kills all child processes (main + helpers + vite).
```bash
SCRIPT=".agents/skills/agent-testing/scripts/electron-dev.sh"
# Start Electron dev with CDP (idempotent — skips if already running)
$SCRIPT start
# Check if Electron is running and CDP is reachable
$SCRIPT status
# Kill all Electron-related processes (main + helper + vite)
$SCRIPT stop
# Force fresh restart
$SCRIPT restart
```
After `start` succeeds, connect with: `agent-browser --cdp 9222 snapshot -i`
**Always run `$SCRIPT stop` when done testing**`pkill -f "Electron"` alone won't catch all helper processes.
#### Environment Variables
| Variable | Default | Description |
| ----------------- | ----------------------- | ---------------------------------------- |
| `CDP_PORT` | `9222` | Chrome DevTools Protocol port |
| `ELECTRON_LOG` | `/tmp/electron-dev.log` | Electron process log |
| `ELECTRON_WAIT_S` | `60` | Max seconds to wait for Electron process |
| `RENDERER_WAIT_S` | `60` | Max seconds to wait for SPA to load |
### LobeHub Probes & Quick Navigation
`scripts/app-probe.sh` is the standard fast path into app state — **use it
instead of hand-rolling `__LOBE_STORES` eval snippets** for these common needs:
```bash
PROBE=".agents/skills/agent-testing/scripts/app-probe.sh"
$PROBE auth # login check (Step 0.3) → { isSignedIn, userId }
$PROBE route # current SPA route
$PROBE ops # running chat operations (type / startTime)
$PROBE goto /settings # jump the SPA straight to a route (full reload)
$PROBE errors-install # install console.error interceptor
$PROBE errors # dump captured errors
```
`goto` lets a test enter the state under test directly instead of clicking
through the UI. Common desktop routes:
| Route | Where it lands |
| ----------------------------- | ------------------------------------ |
| `/` | Home (has a chat input) |
| `/agent/<agentId>` | Agent conversation (latest topic) |
| `/agent/<agentId>/<topicId>` | Specific topic in a conversation |
| `/task` · `/task/<taskId>` | Task list / task detail |
| `/page` | Documents (文稿) |
| `/settings` | Settings |
| `/community` | Discover / community |
Targets default to Electron (`--cdp 9222`); set `AB_TARGET="--session <name>"`
for web sessions. For deeper or one-off state inspection, fall back to raw
eval below.
### LobeHub-Specific Patterns
#### Access Zustand Store State
```bash
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(function() {
var chat = window.__LOBE_STORES.chat();
var ops = Object.values(chat.operations);
return JSON.stringify({
ops: ops.map(function(o) { return { type: o.type, status: o.status }; }),
activeAgent: chat.activeAgentId,
activeTopic: chat.activeTopicId,
});
})()
EVALEOF
```
#### Find and Use the Chat Input
```bash
# The chat input is contenteditable — must use -C flag
agent-browser --cdp 9222 snapshot -i -C 2>&1 | grep "editable"
agent-browser --cdp 9222 click @e48
agent-browser --cdp 9222 type @e48 "Hello world"
agent-browser --cdp 9222 press Enter
```
#### Wait for Agent to Complete
```bash
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(function() {
var chat = window.__LOBE_STORES.chat();
var ops = Object.values(chat.operations);
var running = ops.filter(function(o) { return o.status === 'running'; });
return running.length === 0 ? 'done' : 'running: ' + running.length;
})()
EVALEOF
```
#### Install Error Interceptor
```bash
agent-browser --cdp 9222 eval --stdin << 'EVALEOF'
(function() {
window.__CAPTURED_ERRORS = [];
var orig = console.error;
console.error = function() {
var msg = Array.from(arguments).map(function(a) {
if (a instanceof Error) return a.message;
return typeof a === 'object' ? JSON.stringify(a) : String(a);
}).join(' ');
window.__CAPTURED_ERRORS.push(msg);
orig.apply(console, arguments);
};
return 'installed';
})()
EVALEOF
# Later, check captured errors:
agent-browser --cdp 9222 eval "JSON.stringify(window.__CAPTURED_ERRORS)"
```
## Electron Gotchas
- **Always use `electron-dev.sh stop` to clean up** — `pkill -f "Electron"` only kills the main process; helper processes (GPU, renderer, network) survive. The script finds and kills all of them via PID matching against the project's electron binary path.
- **`npx electron-vite dev` must run from `apps/desktop/`** — running from project root fails silently. The `electron-dev.sh` script handles this automatically.
- **Dev build auto-opens DevTools, which hijacks the CDP target** — `agent-browser --cdp 9222` may attach to the DevTools page (`devtools://…`) instead of the app (`app://renderer/`). Symptom: `get url` returns a `devtools://` URL. Fix: close the DevTools target and reconnect:
```bash
DT_ID=$(curl -s http://localhost:9222/json/list | python3 -c "import json,sys; ts=json.load(sys.stdin); print(next(t['id'] for t in ts if t['type']=='page' and t['url'].startswith('devtools://')))")
curl -s "http://localhost:9222/json/close/$DT_ID" > /dev/null
agent-browser close --all && agent-browser --cdp 9222 get url # expect app://renderer/
```
- **Don't resize the Electron window after load** — resizing triggers full SPA reload
- **Store is at `window.__LOBE_STORES`** not `window.__ZUSTAND_STORES__`
- **Streaming / ticking UI needs GIF evidence** — see `scripts/record-gif.sh`; a static screenshot cannot prove time-based behavior.
+69
View File
@@ -0,0 +1,69 @@
# Web (Full-Stack) Testing
Default surface for **full-stack changes** — a new/changed API plus the UI that
consumes it. The browser is the one surface where network requests and UI state
are observable together, so you can assert both sides of the contract in a
single run.
For pure-frontend changes prefer [electron.md](./electron.md); for
backend-only changes prefer [../cli/index.md](../cli/index.md).
## Prerequisites
- Local dev server running — [../references/dev-server.md](../references/dev-server.md)
- Web auth injected into agent-browser — [../references/auth.md](../references/auth.md):
```bash
pbpaste | ./.agents/skills/agent-testing/scripts/setup-auth.sh web # after copying the Cookie header
```
## Option A — agent-browser with injected auth (recommended)
```bash
SESSION=lobehub-dev
agent-browser --session $SESSION open "http://localhost:3010/"
agent-browser --session $SESSION snapshot -i
# interact via refs — full command reference: ../references/agent-browser.md
```
### Watch the API while driving the UI
```bash
# After triggering the UI action under test:
agent-browser --session $SESSION network requests --type xhr,fetch
agent-browser --session $SESSION network requests --method POST
# Record a full HAR for the report
agent-browser --session $SESSION network har start
# ... drive the scenario ...
agent-browser --session $SESSION network har stop ./capture.har
```
Assert both layers: the request/response shape (network) and the rendered
result (snapshot/screenshot). Both belong in the report as evidence.
## Option B — real Chrome with remote debugging
For flows that need a real, visible browser (e.g. exercising the login UI
itself):
```bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-test-profile \
"<URL>" &
sleep 5
agent-browser --cdp 9222 snapshot -i
# Or auto-discover running Chrome with remote debugging
agent-browser --auto-connect snapshot -i
```
## Option C — Debug Proxy (local frontend, production backend)
`bun run dev:spa` prints a **Debug Proxy** URL
(`https://app.lobehub.com/_dangerous_local_dev_proxy?debug-host=…`) that loads
your local Vite SPA inside the online environment — HMR against real server
config. Useful for verifying frontend behavior against production data, **not**
for testing backend changes (the backend is production, not your branch).
+221
View File
@@ -0,0 +1,221 @@
---
name: agent-tracing
description: 'Agent tracing CLI for execution snapshots. Use for agent-tracing, traces, snapshots, LLM call inspection, context engine data, agent step analysis, or execution debugging.'
user-invocable: false
---
# Agent Tracing CLI Guide
`@lobechat/agent-tracing` is a zero-config local dev tool that records agent execution snapshots to disk and provides a CLI to inspect them.
## How It Works
In `NODE_ENV=development`, `AgentRuntimeService.executeStep()` automatically records each step to `.agent-tracing/` as partial snapshots. When the operation completes, the partial is finalized into a complete `ExecutionSnapshot` JSON file.
**Data flow**: executeStep loop -> build `StepPresentationData` -> write partial snapshot to disk -> on completion, finalize to `.agent-tracing/{timestamp}_{traceId}.json`
**Context engine capture**: In `RuntimeExecutors.ts`, the `call_llm` executor calls `ctx.tracingContextEngine(input, output)` after `serverMessagesEngine()` processes messages. `AgentRuntimeService.executeStep` buffers the call per step and forwards it to `OperationTraceRecorder.appendStep` as the typed `contextEngine` field. CE flows through this side channel rather than the `events` array so its heavy payload (agentDocuments, systemRole, …) never enters the Redis state pipeline (LOBE-9110).
## Package Location
```
packages/agent-tracing/
src/
types.ts # ExecutionSnapshot, StepSnapshot, SnapshotSummary
store/
types.ts # ISnapshotStore interface
file-store.ts # FileSnapshotStore (.agent-tracing/*.json)
recorder/
index.ts # appendStepToPartial(), finalizeSnapshot()
viewer/
index.ts # Terminal rendering: renderSnapshot, renderStepDetail, renderMessageDetail, renderSummaryTable, renderPayload, renderPayloadTools, renderMemory
cli/
index.ts # CLI entry point (#!/usr/bin/env bun)
inspect.ts # Inspect command (default)
partial.ts # Partial snapshot commands (list, inspect, clean)
index.ts # Barrel exports
```
## Data Storage
- Completed snapshots: `.agent-tracing/{ISO-timestamp}_{traceId-short}.json`
- Latest symlink: `.agent-tracing/latest.json`
- In-progress partials: `.agent-tracing/_partial/{operationId}.json`
- `FileSnapshotStore` resolves from `process.cwd()`**run CLI from the repo root**
## CLI Commands
All commands run from the **repo root**:
```bash
# View latest trace (tree overview, `inspect` is the default command)
agent-tracing
agent-tracing inspect
agent-tracing inspect <traceId>
agent-tracing inspect latest
# List recent snapshots
agent-tracing list
agent-tracing list -l 20
# Inspect specific step (-s is short for --step)
agent-tracing inspect <traceId> -s 0
# View messages (-m is short for --messages)
agent-tracing inspect <traceId> -s 0 -m
# View full content of a specific message (by index shown in -m output)
agent-tracing inspect <traceId> -s 0 --msg 2
agent-tracing inspect <traceId> -s 0 --msg-input 1
# View tool call/result details (-t is short for --tools)
agent-tracing inspect <traceId> -s 1 -t
# View raw events (-e is short for --events)
agent-tracing inspect <traceId> -s 0 -e
# View runtime context (-c is short for --context)
agent-tracing inspect <traceId> -s 0 -c
# View context engine input overview (-p is short for --payload)
agent-tracing inspect <traceId> -p
agent-tracing inspect <traceId> -s 0 -p
# View available tools in payload (-T is short for --payload-tools)
agent-tracing inspect <traceId> -T
agent-tracing inspect <traceId> -s 0 -T
# View user memory (-M is short for --memory)
agent-tracing inspect <traceId> -M
agent-tracing inspect <traceId> -s 0 -M
# Raw JSON output (-j is short for --json)
agent-tracing inspect <traceId> -j
agent-tracing inspect <traceId> -s 0 -j
# List in-progress partial snapshots
agent-tracing partial list
# Inspect a partial (use `inspect` directly — all flags work with partial IDs)
agent-tracing inspect <partialOperationId>
agent-tracing inspect <partialOperationId> -T
agent-tracing inspect <partialOperationId> -p
# Clean up stale partial snapshots
agent-tracing partial clean
```
## Inspect Flag Reference
| Flag | Short | Description | Default Step |
| ----------------- | ----- | ------------------------------------------------------------------------------------------------- | ------------ |
| `--step <n>` | `-s` | Target a specific step | — |
| `--messages` | `-m` | Messages context (CE input → params → LLM payload) | — |
| `--tools` | `-t` | Tool calls & results (what agent invoked) | — |
| `--events` | `-e` | Raw events (llm_start, llm_result, etc.) | — |
| `--context` | `-c` | Runtime context & payload (raw) | — |
| `--system-role` | `-r` | Full system role content | 0 |
| `--env` | | Environment context | 0 |
| `--payload` | `-p` | Context engine input overview (model, knowledge, tools summary, memory summary, platform context) | 0 |
| `--payload-tools` | `-T` | Available tools detail (plugin manifests + LLM function definitions) | 0 |
| `--memory` | `-M` | Full user memory (persona, identity, contexts, preferences, experiences) | 0 |
| `--diff <n>` | `-d` | Diff against step N (use with `-r` or `--env`) | — |
| `--msg <n>` | | Full content of message N from Final LLM Payload | — |
| `--msg-input <n>` | | Full content of message N from Context Engine Input | — |
| `--json` | `-j` | Output as JSON (combinable with any flag above) | — |
Flags marked "Default Step: 0" auto-select step 0 if `--step` is not provided. All flags support `latest` or omitted traceId.
## Typical Debug Workflow
```bash
# 1. Trigger an agent operation in the dev UI
# 2. See the overview
agent-tracing inspect
# 3. List all traces, get traceId
agent-tracing list
# 4. Quick overview of what was fed into context engine
agent-tracing inspect -p
# 5. Inspect a specific step's messages to see what was sent to the LLM
agent-tracing inspect TRACE_ID -s 0 -m
# 6. Drill into a truncated message for full content
agent-tracing inspect TRACE_ID -s 0 --msg 2
# 7. Check available tools vs actual tool calls
agent-tracing inspect -T # available tools
agent-tracing inspect -s 1 -t # actual tool calls & results
# 8. Inspect user memory injected into the conversation
agent-tracing inspect -M
# 9. Diff system role between steps (multi-step agents)
agent-tracing inspect TRACE_ID -r -d 2
```
## Key Types
```typescript
interface ExecutionSnapshot {
traceId: string;
operationId: string;
model?: string;
provider?: string;
startedAt: number;
completedAt?: number;
completionReason?:
| 'done'
| 'error'
| 'interrupted'
| 'max_steps'
| 'cost_limit'
| 'waiting_for_human';
totalSteps: number;
totalTokens: number;
totalCost: number;
error?: { type: string; message: string };
steps: StepSnapshot[];
}
interface StepSnapshot {
stepIndex: number;
stepType: 'call_llm' | 'call_tool';
executionTimeMs: number;
content?: string; // LLM output
reasoning?: string; // Reasoning/thinking
inputTokens?: number;
outputTokens?: number;
toolsCalling?: Array<{ apiName: string; identifier: string; arguments?: string }>;
toolsResult?: Array<{
apiName: string;
identifier: string;
isSuccess?: boolean;
output?: string;
}>;
messages?: any[]; // DB messages before step
context?: { phase: string; payload?: unknown; stepContext?: unknown };
events?: Array<{ type: string; [key: string]: unknown }>;
contextEngine?: {
input?: unknown; // contextEngineInput minus messages + toolsConfig (reconstructible from baseline)
output?: unknown; // processed messages array (final LLM payload)
};
}
```
## --messages Output Structure
When using `--messages`, the output shows three sections (if context engine data is available):
1. **Context Engine Input** — DB messages passed to the engine, with `[0]`, `[1]`, ... indices. Use `--msg-input N` to view full content.
2. **Context Engine Params** — systemRole, model, provider, knowledge, tools, userMemory, etc.
3. **Final LLM Payload** — Processed messages after context engine (system date injection, user memory, history truncation, etc.), with `[0]`, `[1]`, ... indices. Use `--msg N` to view full content.
## Integration Points
- **Recording**: `apps/server/src/services/agentRuntime/AgentRuntimeService.ts` — in the `executeStep()` method, after building `stepPresentationData`, writes partial snapshot in dev mode
- **Context engine capture**: `apps/server/src/modules/AgentRuntime/RuntimeExecutors.ts` — in `call_llm` executor, after `serverMessagesEngine()` returns, calls `ctx.tracingContextEngine(input, output)`. `AgentRuntimeService.executeStep` buffers it per step and passes it to `traceRecorder.appendStep` as the typed `contextEngine` field (kept off the `events` array to stay out of Redis state).
- **Store**: `FileSnapshotStore` reads/writes to `.agent-tracing/` relative to `process.cwd()`
+130
View File
@@ -0,0 +1,130 @@
---
name: builtin-tool
description: 'Build LobeHub builtin tool packages. Use when adding agent-callable tools, manifests, executors, runtimes, inspectors, renders, placeholders, streaming, interventions, portals, or tool registries.'
---
# Builtin Tool Authoring Guide
A builtin tool is a package the agent runtime can call. It ships **five faces**:
| Face | Lives in | Audience |
| -------------------- | -------------------------------------------------------------------------------------- | ------------------------------------- |
| **Manifest + types** | `src/{manifest,types,systemRole}.ts` | The LLM (tool spec + system prompt) |
| **ExecutionRuntime** | `src/ExecutionRuntime/` | Server / desktop / any runtime caller |
| **Executor** | `src/client/executor/` | Frontend (wraps stores/services) |
| **Client UI** | `src/client/{Inspector,Render,…}/` | Chat UI |
| **Registry wiring** | `packages/builtin-tools/src/*.ts` + `src/store/tool/slices/builtin/executors/index.ts` | Framework |
---
## Read These First
| Question | Doc |
| ------------------------------------------------------------------------------------ | --------------------------------------------- |
| Where do files live? What does each face do? Wiring? | [architecture.md](references/architecture.md) |
| How do I name the tool, design APIs, write the manifest, executor, ExecutionRuntime? | [tool-design.md](references/tool-design.md) |
| How do I build Inspector / Render / Placeholder / Streaming / Intervention / Portal? | [ui/](references/ui/README.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.
---
## Package Layout (preferred, post-2026 convention)
```
packages/builtin-tool-<name>/
├── package.json
└── src/
├── index.ts # exports manifest + types + systemRole + Identifier (no React, no stores)
├── manifest.ts # BuiltinToolManifest with JSON Schema for every API
├── types.ts # ApiName const + Params/State interfaces per API
├── systemRole.ts # System prompt teaching the model when/how to use the APIs
├── ExecutionRuntime/ # ✅ Default home for runtime logic (server- or anywhere-callable)
│ └── index.ts
└── client/
├── index.ts # Re-exports for the registries
├── executor/ # ✅ Frontend executor — extends BaseExecutor, often delegates to ExecutionRuntime
│ └── index.ts
├── Inspector/ # required — header chip per API
├── Render/ # optional — rich result card
├── Placeholder/ # optional — skeleton during streaming/execution
├── Streaming/ # optional — live output renderer (e.g. RunCommand, WriteFile)
├── Intervention/ # optional — approval / edit-before-run UI
├── Portal/ # optional — full-screen detail view
└── 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.
`package.json` exports map:
```json
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/client/executor/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
}
```
---
## Authoring Checklist
Before opening the PR:
- [ ] Identifier follows `lobe-<domain>` and is **stable** (lives in message history).
- [ ] Every `<Name>ApiName` value has: a manifest `api[]` entry, an executor method, an Inspector, an i18n `apiName.*` key.
- [ ] `Params` interfaces match the JSON Schema; `State` interfaces match what the executor returns and what the UI surfaces read.
- [ ] System prompt disambiguates confusable APIs and points to batch variants.
- [ ] Runtime logic lives in `ExecutionRuntime/`; the `client/executor/` only wires stores/services and delegates.
- [ ] Executor returns `{ success, content, state, error? }` via a single `toResult()` funnel — `content` always non-empty (default to `error.message`).
- [ ] Inspector handles `isArgumentsStreaming`, `isLoading`, `partialArgs`, missing `pluginState`.
- [ ] Render returns `null` until it has data; only created for APIs with rich results.
- [ ] Placeholder added if the API has a perceivable execution lag (search, list, crawl).
- [ ] Streaming added for APIs that emit incremental output (run command, write file, code execution).
- [ ] Intervention added if `humanIntervention` is set in the manifest.
- [ ] All registry files updated (see [architecture.md → Registry wiring](references/architecture.md#registry-wiring)).
- [ ] i18n keys in `src/locales/default/plugin.ts` plus dev seeds in `en-US`/`zh-CN`.
- [ ] `bunx vitest run --silent='passed-only' 'packages/builtin-tool-<name>'` passes.
- [ ] `bun run type-check` passes.
---
## Reference Tools
Pick the closest neighbor and copy:
| If your tool is… | Read first |
| ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| 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 |
@@ -0,0 +1,315 @@
# Builtin Tool Architecture
## The Five Faces
A builtin tool ships five distinct faces, each compiled into a different bundle:
```
┌─────────────────────────────────────────────────────────────────┐
│ ./ │
│ Manifest + Types + systemRole │
│ ─ Pure data, no React, no Node-only deps. │
│ ─ Imported by: server (LLM tool spec), client (registries), │
│ anyone who needs to know "what tools exist". │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ./executionRuntime │
│ src/ExecutionRuntime/index.ts │
│ ─ Pure runtime logic. Accepts services via constructor — │
│ never imports concrete services or stores directly. │
│ ─ Imported by: server (BuiltinServerRuntimeOutput), tests, │
│ and the client executor as a delegate. │
│ ─ Returns: BuiltinServerRuntimeOutput { content, state, … } │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ./executor │
│ src/client/executor/index.ts │
│ ─ BaseExecutor subclass. Wires Zustand stores and frontend │
│ services into ExecutionRuntime, then funnels through │
│ toResult() into BuiltinToolResult { content, state, error, │
│ success }. │
│ ─ Imported by: src/store/tool/slices/builtin/executors/ │
│ index.ts (registered as a singleton). │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ ./client │
│ src/client/{Inspector,Render,Placeholder,Streaming, │
│ Intervention,Portal,components}/ │
│ ─ React 'use client' surfaces. Read args + pluginState. │
│ ─ Imported by: packages/builtin-tools/src/{inspectors, │
│ renders,placeholders,streamings,interventions,portals}.ts. │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Registry wiring │
│ packages/builtin-tools/src/*.ts │
│ src/store/tool/slices/builtin/executors/index.ts │
│ ─ Aggregator maps: identifier → { apiName → component }. │
└─────────────────────────────────────────────────────────────────┘
```
The split exists so:
- 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`:
```ts
export interface ILocalSystemService {
readLocalFile: (params: any) => Promise<any>;
writeFile: (params: any) => Promise<any>;
/* … */
}
export class LocalSystemExecutionRuntime extends ComputerRuntime {
constructor(private service: ILocalSystemService) {
super();
}
/* methods delegate to this.service.* */
}
```
The `client/executor` instantiates it once with the real service:
```ts
import { localFileService } from '@/services/electron/localFileService';
import { LocalSystemExecutionRuntime } from '../../ExecutionRuntime';
class LocalSystemExecutor extends BaseExecutor<typeof LocalSystemApiEnum> {
private runtime = new LocalSystemExecutionRuntime(localFileService);
/* … */
}
```
### 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.
---
## The Result Contract
### `BuiltinServerRuntimeOutput` (what ExecutionRuntime returns)
```ts
{
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`.
```ts
private toResult(output: BuiltinServerRuntimeOutput): BuiltinToolResult {
const errorMessage = typeof output.error?.message === 'string' ? output.error.message : undefined;
const safeContent = output.content || errorMessage || 'Tool execution failed';
if (!output.success) {
return {
success: false,
content: safeContent,
state: output.state,
error: output.error
? { type: 'PluginServerError', message: errorMessage ?? safeContent, body: output.error }
: undefined,
};
}
return { success: true, content: safeContent, state: output.state };
}
```
---
## `BaseExecutor` — How Method Dispatch Works
`BaseExecutor.invoke(apiName, params, ctx)` does:
```ts
if (!this.hasApi(apiName)) return { error: { type: 'ApiNotFound', }, success: false };
return (this as any)[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:
| Field | Use |
| ----------------------------- | -------------------------------------------------------------- |
| `agentId` | Default agent for "current agent" semantics (e.g. `listTasks`) |
| `groupId` | Group chat scope |
| `topicId` | Current topic — needed when creating messages/operations |
| `taskId` | Current task identifier — fallback for "implicit" param |
| `documentId` | Current page/document scope |
| `messageId` | The tool message being created (for state attachments) |
| `sourceMessageId` | The user message that triggered this tool turn |
| `operationId` | Operation lineage (use for cancellation, tracing) |
| `scope` | `'task' \| 'agent' \| …` — toggles default behaviors |
| `signal: AbortSignal` | Honor for long-running ops |
| `stepContext` | Cross-message runtime state (lobe-agent todos, etc.) |
| `registerAfterCompletion(cb)` | Defer side-effects past message-update race |
| `groupOrchestration` | Group orchestration callbacks |
**Use rule:** read with `?.`, fall back to explicit params, **never silently override** an explicit param with a context value.
---
## i18n Integration
Source of truth: `src/locales/default/plugin.ts`. Keys follow `builtins.<identifier>.<topic>.<…>`:
| Key | Use |
| ------------------------------------- | ------------------------------------------------------------ |
| `builtins.<identifier>.title` | Display title (overrides `manifest.meta.title` when present) |
| `builtins.<identifier>.apiName.<api>` | Inspector header label (one per ApiName) |
| `builtins.<identifier>.inspector.<…>` | Extra Inspector strings ("no results", chips, counters) |
| `builtins.<identifier>.<feature>.<…>` | Render / Intervention strings, free-form per tool |
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.
| File | Add what |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| **Required** | |
| `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 `apps/server/src/modules/Mecha/AgentToolsEngine/index.ts` and `src/helpers/toolEngineering/index.ts`
---
## File-Map at a Glance
```
packages/builtin-tool-<name>/
├── package.json # exports: ., ./client, ./executor, ./executionRuntime
└── src/
├── index.ts # export Manifest, Identifier, types, systemPrompt
├── manifest.ts # BuiltinToolManifest + Identifier const
├── types.ts # ApiName + Params/State per API
├── systemRole.ts # System prompt (multiple variants OK: systemRole.desktop.ts)
├── ExecutionRuntime/
│ └── index.ts # <Name>ExecutionRuntime — pure runtime, service injection
└── client/
├── index.ts # exports for the registries
├── executor/
│ └── index.ts # <Name>Executor extends BaseExecutor; export <name>Executor
├── Inspector/
│ ├── index.ts # <Name>Inspectors record
│ └── <ApiName>/index.tsx # one folder per API (or .tsx file when trivial)
├── Render/
│ ├── index.ts # <Name>Renders record
│ └── <ApiName>/ # rich renders → folder with subcomponents
├── Placeholder/
│ ├── index.ts
│ └── <ApiName>.tsx # usually a single skeleton file
├── Streaming/
│ ├── index.ts
│ └── <ApiName>/ # live-output renderer
├── Intervention/
│ ├── index.ts
│ └── <ApiName>/ # approval / edit-before-run UI
├── Portal/
│ ├── index.tsx # routing component (switch on apiName)
│ └── <ApiName>/ # full-screen detail view
└── components/ # FileItem, EngineAvatar, etc. — shared subcomponents
```
Skip every `client/<surface>/` directory you don't need — empty registries are fine.
@@ -0,0 +1,478 @@
# Tool Design (Naming, Manifest, Executor, Runtime)
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/](ui/README.md).
For where files live and how registries work, see [architecture.md](architecture.md).
---
## 1. Naming
| Thing | Convention | Example |
| ----------------------- | -------------------------------------------------------------- | ------------------------------------------------------------ |
| Package directory | `packages/builtin-tool-<kebab>/` | `builtin-tool-task` |
| npm name | `@lobechat/builtin-tool-<kebab>` | `@lobechat/builtin-tool-task` |
| Tool `identifier` | `lobe-<kebab-domain>`**persisted in message history** | `lobe-task`, `lobe-calculator`, `lobe-knowledge-base` |
| Identifier const | `<Name>Identifier` exported from `manifest.ts` (or `types.ts`) | `export const TaskIdentifier = 'lobe-task'` |
| API name const | `<Name>ApiName``as const` object, **camelCase verbs** | `createTask`, `listTasks`, `runTask` |
| Executor class | `<Name>Executor extends BaseExecutor<typeof <Name>ApiName>` | `TaskExecutor` |
| Executor singleton | `<name>Executor` (camelCase) | `export const taskExecutor = new TaskExecutor()` |
| ExecutionRuntime class | `<Name>ExecutionRuntime` | `LocalSystemExecutionRuntime`, `WebBrowsingExecutionRuntime` |
| Inspector / Render etc. | `<ApiName>Inspector` / `<ApiName>Render` | `CreateTaskInspector`, `SearchInspector` |
### Identifier rules
- **`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.
### ApiName rules
- Verb + noun, camelCase: `createTask`, `viewTask`, `runTasks`.
- **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.
- Read-only verbs: `list*`, `view*`, `get*`, `search*`. Mutating: `create*`, `edit*`, `update*`, `delete*`. Triggers/effects: `run*`, `execute*`, `submit*`.
---
## 2. `types.ts` — ApiName + Params/State
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
export const TaskIdentifier = 'lobe-task';
export const TaskApiName = {
createTask: 'createTask',
createTasks: 'createTasks',
listTasks: 'listTasks',
/* …one entry per API, group logically (CRUD then run-style) */
} as const;
export type TaskApiNameType = (typeof TaskApiName)[keyof typeof TaskApiName];
// One block per API
export interface CreateTaskParams {
name: string;
instruction: string; /* … */
}
export interface CreateTaskState {
identifier?: string;
success: boolean;
}
export interface CreateTasksParams {
tasks: CreateTaskParams[];
}
export interface CreateTasksItemResult {
error?: string;
identifier?: string;
name: string;
success: boolean;
}
export interface CreateTasksState {
failed: number;
results: CreateTasksItemResult[];
succeeded: number;
}
```
**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.
---
## 3. `manifest.ts` — JSON Schema for the LLM
```ts
import type { BuiltinToolManifest } from '@lobechat/types';
import { systemPrompt } from './systemRole';
import { TaskApiName, TaskIdentifier } from './types';
export const TaskManifest: BuiltinToolManifest = {
identifier: TaskIdentifier,
type: 'builtin',
systemRole: systemPrompt,
meta: {
avatar: '📋',
title: 'Task Tools',
description: 'Create, list, edit, delete tasks with dependencies',
readme: 'Optional long description shown in tool detail pages',
},
api: [
{
name: TaskApiName.createTask,
description:
'Create a new task. Optionally attach as a subtask via parentIdentifier. ' +
'Prefer createTasks when planning a batch.',
parameters: {
type: 'object',
required: ['name', 'instruction'],
properties: {
name: { type: 'string', description: 'Short, descriptive name.' },
instruction: {
type: 'string',
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/intervention.md). */
humanIntervention: 'never' | 'always' | { /* extended config */ },
}
```
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
export const systemPrompt = `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
export const systemPrompt = (today: string) => `Today is ${today}. You have web search tools…`;
// manifest.ts
import dayjs from 'dayjs';
systemRole: systemPrompt(dayjs(new Date()).format('YYYY-MM-DD')),
```
---
## 5. `ExecutionRuntime/index.ts` — Pure Runtime
This is **the default home for new tool logic** going forward. The runtime is a class that:
- Has no React, no Zustand, no `@/services/...` direct imports.
- Receives services as **constructor injection** (or as method args).
- Returns `BuiltinServerRuntimeOutput` from each method.
- Is unit-testable by passing in mocks.
### Pattern A: Inject a service interface
Use when the runtime calls out to IPC, network, or DB.
```ts
// ExecutionRuntime/index.ts
import type { BuiltinServerRuntimeOutput } from '@lobechat/types';
export interface IWebBrowsingService {
search: (q: SearchQuery) => Promise<UniformSearchResponse>;
crawlPages: (urls: string[]) => Promise<CrawlResults>;
}
export interface WebBrowsingRuntimeOptions {
searchService: IWebBrowsingService;
documentService?: WebBrowsingDocumentService;
agentId?: string;
topicId?: string;
}
export class WebBrowsingExecutionRuntime {
constructor(private opts: WebBrowsingRuntimeOptions) {}
async search(
args: SearchQuery,
options?: { signal?: AbortSignal },
): Promise<BuiltinServerRuntimeOutput> {
try {
const data = await this.opts.searchService.search(args, options);
if (data.errorDetail) {
return {
success: false,
content: data.errorDetail,
error: { message: data.errorDetail },
state: data,
};
}
return {
success: true,
content: searchResultsPrompt(data.results.slice(0, 10)),
state: data,
};
} catch (e) {
return { success: false, content: (e as Error).message, error: e };
}
}
}
```
### Pattern B: Reuse the executor
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.
---
## 6. `client/executor/index.ts` — Frontend Wiring
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.
```ts
import { BaseExecutor, type BuiltinToolContext, type BuiltinToolResult } from '@lobechat/types';
import debug from 'debug';
import { taskService } from '@/services/task';
import { getTaskStoreState } from '@/store/task';
import { TaskIdentifier } from '../../manifest';
import { TaskApiName, type CreateTaskParams } from '../../types';
const log = debug('lobe-task:executor');
class TaskExecutor extends BaseExecutor<typeof TaskApiName> {
readonly identifier = TaskIdentifier;
protected readonly apiEnum = TaskApiName;
// ⚠ class FIELD, not a method — preserves `this` when invoked via registry
createTask = async (
params: CreateTaskParams,
ctx?: BuiltinToolContext,
): Promise<BuiltinToolResult> => {
try {
log('createTask params=%o', params);
const task = await getTaskStoreState().createTask({
name: params.name,
instruction: params.instruction,
// Default assignee from context — never silently override an explicit value
assigneeAgentId:
params.assigneeAgentId ?? (ctx?.scope === 'task' ? undefined : ctx?.agentId),
parentTaskId: params.parentIdentifier?.trim() || undefined,
priority: params.priority,
});
if (!task) return this.errorResult('Failed to create task', 'CreateFailed');
return {
success: true,
content: formatTaskCreated({ identifier: task.identifier, name: task.name /* … */ }),
state: { identifier: task.identifier, success: true },
};
} catch (error) {
return this.errorResult(error, 'CreateTaskFailed');
}
};
private errorResult(err: unknown, type: string): BuiltinToolResult {
const message = err instanceof Error ? err.message : String(err) || 'Unknown error';
return { success: false, content: `Failed: ${message}`, error: { type, message } };
}
}
export const taskExecutor = new TaskExecutor();
```
### Hard rules
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
```ts
class LocalSystemExecutor extends BaseExecutor<typeof LocalSystemApiEnum> {
readonly identifier = LocalSystemIdentifier;
protected readonly apiEnum = LocalSystemApiEnum;
private runtime = new LocalSystemExecutionRuntime(localFileService);
readLocalFile = async (params: LocalReadFileParams): Promise<BuiltinToolResult> => {
try {
const result = await this.runtime.readFile({
path: params.path,
startLine: params.loc?.[0],
endLine: params.loc?.[1],
});
return this.toResult(result);
} catch (error) {
return this.errorResult(error);
}
};
private toResult(out: BuiltinServerRuntimeOutput): BuiltinToolResult {
const errMsg = typeof out.error?.message === 'string' ? out.error.message : undefined;
const safe = out.content || errMsg || 'Tool execution failed';
if (!out.success) {
return {
success: false,
content: safe,
state: out.state, // ← preserve partial state on failure
error: out.error
? { type: 'PluginServerError', message: errMsg ?? safe, body: out.error }
: undefined,
};
}
return { success: true, content: safe, state: out.state };
}
}
```
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.**
```ts
export { TaskIdentifier, TaskManifest } from './manifest';
export { systemPrompt } from './systemRole';
export {
TaskApiName,
type TaskApiNameType,
type CreateTaskParams,
type CreateTaskState,
/* …all Params/State types */
} from './types';
// Optional helpers used by both the runtime and the UI
export { TASK_STATUSES, UNFINISHED_TASK_STATUSES } from './constants';
```
This entry is what `packages/builtin-tools/src/index.ts` and `identifiers.ts` import — it must be importable from server bundles.
---
## 8. `package.json`
```json
{
"dependencies": {
"@lobechat/prompts": "workspace:*"
},
"devDependencies": {
"@lobechat/types": "workspace:*"
},
"exports": {
".": "./src/index.ts",
"./client": "./src/client/index.ts",
"./executor": "./src/client/executor/index.ts",
"./executionRuntime": "./src/ExecutionRuntime/index.ts"
},
"main": "./src/index.ts",
"name": "@lobechat/builtin-tool-<name>",
"peerDependencies": {
"@lobehub/ui": "^5",
"antd": "^6",
"antd-style": "*",
"lucide-react": "*",
"react": "*",
"react-i18next": "*"
},
"private": true,
"version": "1.0.0"
}
```
**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`).
---
## 9. Common Pitfalls
| Symptom | Likely cause |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| "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 |
@@ -0,0 +1,36 @@
# Tool UI Surfaces
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 |
| ------------ | --------- | --------------------------------------------------------------------- | --------------------------------------------- |
| Inspector | ✅ Always | Header strip of every tool call (one-line chip) | `packages/builtin-tools/src/inspectors.ts` |
| Render | Optional | Rich result card below the header, after the call returns | `packages/builtin-tools/src/renders.ts` |
| Placeholder | Optional | Skeleton between "args streaming complete" and "result arrives" | `packages/builtin-tools/src/placeholders.ts` |
| Streaming | Optional | Live output during execution (e.g. command stdout) | `packages/builtin-tools/src/streamings.ts` |
| Intervention | Optional | Approval / edit-before-run dialog (when `humanIntervention` triggers) | `packages/builtin-tools/src/interventions.ts` |
| Portal | Optional | Full-screen detail view (right-side or modal) | `packages/builtin-tools/src/portals.ts` |
The two reference tools to read end-to-end:
- **`builtin-tool-web-browsing/src/client/`** — Inspector + Render + Placeholder + Portal (no Intervention/Streaming).
- **`builtin-tool-local-system/src/client/`** — all six surfaces, including `components/` for shared building blocks.
---
## Files in this folder
Read **principles** and **shared-rules** first — they apply to every surface. Then jump to the surface you're building.
| File | What it covers |
| ---------------------------------- | ----------------------------------------------------------------------- |
| [principles.md](principles.md) | Design principles — when each surface exists and how far to take it |
| [shared-rules.md](shared-rules.md) | Cross-surface rules: component skeleton, styling, single-layer surfaces |
| [inspector.md](inspector.md) | Inspector — header chip (required) |
| [render.md](render.md) | Render — rich result card |
| [placeholder.md](placeholder.md) | Placeholder — skeleton between args and result |
| [streaming.md](streaming.md) | Streaming — live output during execution |
| [intervention.md](intervention.md) | Intervention — approval / edit-before-run |
| [portal.md](portal.md) | Portal — full-screen detail view |
| [composition.md](composition.md) | Shared subcomponents (`client/components/`) + package public API |
| [diagnostics.md](diagnostics.md) | Symptom → surface quick-lookup |
@@ -0,0 +1,51 @@
# Composition — Shared Components & Package API
## `client/components/` — Shared Subcomponents
Cross-cutting building blocks used by multiple surfaces live here, not duplicated in each surface folder.
Examples from `web-browsing/src/client/components/`:
- `CategoryAvatar.tsx` — search category icon
- `EngineAvatar.tsx` — search engine logo (used in Inspector chip + Render list + Portal header)
- `SearchBar.tsx` — editable query bar (used in Render and Portal)
Examples from `local-system/src/client/components/`:
- `FileItem.tsx` — single file row (used in ListFiles Render, SearchFiles Render, MoveLocalFiles Render)
- `FilePathDisplay.tsx` — path with truncation (used everywhere)
### Rules
- Live under `client/components/`, exported via `client/components/index.ts`.
- Re-export from `client/index.ts` only if other packages need them; otherwise keep internal.
- Keep them dumb — props in, JSX out, no store reads. The store reads belong in the surface that composes them.
---
## `client/index.ts` — Package Public API
Re-exports everything the registries need plus useful types/manifest:
```ts
// Inspector — required
export { TaskInspectors } from './Inspector';
// Render — only if any API has one
export { TaskRenders, CreateTaskRender, RunTasksRender } from './Render';
// Placeholder / Streaming / Intervention — only if used
export { LocalSystemListFilesPlaceholder, LocalSystemSearchFilesPlaceholder } from './Placeholder';
export { LocalSystemStreamings } from './Streaming';
export { LocalSystemInterventions } from './Intervention';
// Portal — single export per tool
export { default as WebBrowsingPortal } from './Portal';
// Reusable components if other packages need them
export { CategoryAvatar, EngineAvatar, SearchBar } from './components';
// Re-export manifest, identifier, types for convenience
export { TaskManifest, TaskIdentifier } from '../manifest';
export * from '../types';
```
@@ -0,0 +1,15 @@
# Diagnostic Quick-Lookup
| Symptom | Surface to check |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| No header at all on the tool call | Inspector missing from `client/Inspector/index.ts` registry |
| Header shows the API name but no chips | Inspector missing `args?.X \|\| partialArgs?.X` fallback |
| Header doesn't pulse during loading | Missing `shinyTextStyles.shinyText` on `isArgumentsStreaming \|\| isLoading` |
| Empty result card under header | Render returned `<div />` instead of `null` when no data |
| Render looks "complex" / card-in-card | Filled container (`colorFillQuaternary`) wrapping more filled boxes — flatten to single-layer, see [shared-rules.md](shared-rules.md) |
| Layout jump when result arrives | Placeholder dimensions don't match Render dimensions |
| Approval dialog never appears | Manifest missing `humanIntervention`, or Intervention not in registry |
| Approval click doesn't wait for inline edit | Missing `registerBeforeApprove(id, flushFn)` |
| 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 }}` |
@@ -0,0 +1,118 @@
# 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.
## Props (`BuiltinInspectorProps<Args, State>`)
```ts
interface BuiltinInspectorProps<Arguments = any, State = any> {
apiName: string;
args: Arguments; // final args (only after the assistant stops streaming)
identifier: string;
isArgumentsStreaming?: boolean; // args still arriving
isLoading?: boolean; // args complete, executor running
partialArgs?: Arguments; // partial JSON during streaming
pluginState?: State; // executor's `state` after success
result?: { content: string | null; error?: any };
}
```
## State machine
| Phase | What's available | What to show |
| ----------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
| 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) |
## Canonical example — Search
`packages/builtin-tool-web-browsing/src/client/Inspector/Search/index.tsx`:
```tsx
'use client';
import type { BuiltinInspectorProps, SearchQuery, UniformSearchResponse } from '@lobechat/types';
import { Text } from '@lobehub/ui';
import { cssVar, cx } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { highlightTextStyles, inspectorTextStyles, shinyTextStyles } from '@/styles';
export const SearchInspector = memo<BuiltinInspectorProps<SearchQuery, UniformSearchResponse>>(
({ args, partialArgs, isArgumentsStreaming, isLoading, pluginState }) => {
const { t } = useTranslation('plugin');
const query = args?.query || partialArgs?.query || '';
const resultCount = pluginState?.results?.length ?? 0;
const hasResults = resultCount > 0;
if (isArgumentsStreaming && !query) {
return (
<div className={cx(inspectorTextStyles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-web-browsing.apiName.search')}</span>
</div>
);
}
return (
<div
className={cx(
inspectorTextStyles.root,
(isArgumentsStreaming || isLoading) && shinyTextStyles.shinyText,
)}
>
<span>{t('builtins.lobe-web-browsing.apiName.search')}:&nbsp;</span>
{query && <span className={highlightTextStyles.primary}>{query}</span>}
{!isLoading &&
!isArgumentsStreaming &&
pluginState?.results &&
(hasResults ? (
<span style={{ marginInlineStart: 4 }}>({resultCount})</span>
) : (
<Text as="span" color={cssVar.colorTextDescription} fontSize={12}>
({t('builtins.lobe-web-browsing.inspector.noResults')})
</Text>
))}
</div>
);
},
);
SearchInspector.displayName = 'SearchInspector';
export default SearchInspector;
```
## Inspector rules
- 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.
- **Switch copy by phase.** If the verb implies an ongoing action ("Creating", "Searching", "Listing"), define `<api>.loading` and `<api>.completed` keys and select via `isArgumentsStreaming || isLoading ? loadingKey : completedKey`. Inspector chips persist in chat history — leaving "Creating task" frozen on a finished call reads as if the tool is still running. Read-only labels that are already noun-form ("View task") can keep a single key. See `CallSubAgentInspector` for the canonical two-key pattern.
## Inspector registry — `client/Inspector/index.ts`
```ts
import type { BuiltinInspector } from '@lobechat/types';
import { TaskApiName } from '../../types';
import { CreateTaskInspector } from './CreateTask';
import { ListTasksInspector } from './ListTasks';
/* … */
export const TaskInspectors: Record<string, BuiltinInspector> = {
[TaskApiName.createTask]: CreateTaskInspector as BuiltinInspector,
[TaskApiName.listTasks]: ListTasksInspector as BuiltinInspector,
/* one entry per ApiName */
};
export { CreateTaskInspector } from './CreateTask';
export { ListTasksInspector } from './ListTasks';
/* re-export each */
```
@@ -0,0 +1,88 @@
# Intervention — Approval / Edit-Before-Run (optional)
**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.
**Add for** destructive or sensitive ops: shell commands, file writes, file moves, payments, message broadcasts.
## Props (`BuiltinInterventionProps<Args>`)
```ts
interface BuiltinInterventionProps<Arguments = any> {
apiName?: string;
args: Arguments;
identifier?: string;
interactionMode?: 'approval' | 'custom';
messageId: string;
/** Called when the user edits the args; the approve action awaits this. */
onArgsChange?: (args: Arguments) => void | Promise<void>;
/** Called on approve / skip / cancel. */
onInteractionAction?: (
action:
| { type: 'submit'; payload: Record<string, unknown> }
| { type: 'skip'; payload?: Record<string, unknown>; reason?: string }
| { type: 'cancel'; payload?: Record<string, unknown> },
) => Promise<void>;
/** Register a callback to flush pending saves before approval. Returns cleanup. */
registerBeforeApprove?: (id: string, callback: () => void | Promise<void>) => () => void;
}
```
## Canonical example — RunCommand Intervention
`packages/builtin-tool-local-system/src/client/Intervention/RunCommand/index.tsx`:
```tsx
import type { RunCommandParams } from '@lobechat/electron-client-ipc';
import type { BuiltinInterventionProps } from '@lobechat/types';
import { Flexbox, Highlighter, Text } from '@lobehub/ui';
import { memo } from 'react';
const RunCommand = memo<BuiltinInterventionProps<RunCommandParams>>(({ args }) => {
const { description, command, timeout } = args;
return (
<Flexbox gap={8}>
<Flexbox horizontal justify="space-between">
{description && <Text>{description}</Text>}
{timeout && (
<Text style={{ fontSize: 12 }} type="secondary">
timeout: {formatTimeout(timeout)}
</Text>
)}
</Flexbox>
{command && (
<Highlighter wrap language="sh" showLanguage={false} variant="outlined">
{command}
</Highlighter>
)}
</Flexbox>
);
});
export default RunCommand;
```
## Intervention rules
- **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`).
## Intervention registry — `client/Intervention/index.ts`
```ts
import { LocalSystemApiName } from '../..';
import EditLocalFile from './EditLocalFile';
import RunCommand from './RunCommand';
import WriteFile from './WriteFile';
/* … */
export const LocalSystemInterventions = {
[LocalSystemApiName.editLocalFile]: EditLocalFile,
[LocalSystemApiName.runCommand]: RunCommand,
[LocalSystemApiName.writeLocalFile]: WriteFile,
/* one entry per API that needs approval */
};
```
@@ -0,0 +1,93 @@
# 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).
## Props (`BuiltinPlaceholderProps<Args>`)
```ts
interface BuiltinPlaceholderProps<T extends Record<string, any> = any> {
apiName: string;
args?: T;
identifier: string;
}
```
No `pluginState` — Placeholder lives entirely in the "executing" gap.
## Canonical example — Search Placeholder
`packages/builtin-tool-web-browsing/src/client/Placeholder/Search.tsx`:
```tsx
import type { BuiltinPlaceholderProps, SearchQuery } from '@lobechat/types';
import { Flexbox, Icon, Skeleton } from '@lobehub/ui';
import { createStaticStyles, cx } from 'antd-style';
import { SearchIcon } from 'lucide-react';
import { memo } from 'react';
import { useIsMobile } from '@/hooks/useIsMobile';
import { shinyTextStyles } from '@/styles';
const styles = createStaticStyles(({ css, cssVar }) => ({
query: cx(
css`
padding: 4px 8px;
border-radius: 8px;
font-size: 12px;
color: ${cssVar.colorTextSecondary};
&:hover {
background: ${cssVar.colorFillTertiary};
}
`,
shinyTextStyles.shinyText,
),
}));
export const Search = memo<BuiltinPlaceholderProps<SearchQuery>>(({ args }) => {
const { query } = args || {};
const isMobile = useIsMobile();
return (
<Flexbox gap={8}>
<Flexbox horizontal={!isMobile} gap={isMobile ? 8 : 40}>
<Flexbox horizontal align="center" className={styles.query} gap={8}>
<Icon icon={SearchIcon} />
{query ? query : <Skeleton.Block active style={{ height: 20, width: 40 }} />}
</Flexbox>
<Skeleton.Block active style={{ height: 20, width: 40 }} />
</Flexbox>
<Flexbox horizontal gap={12}>
{[1, 2, 3, 4, 5].map((id) => (
<Skeleton.Button active key={id} style={{ borderRadius: 8, height: 80, width: 160 }} />
))}
</Flexbox>
</Flexbox>
);
});
```
## Placeholder rules
- **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.
## Placeholder registry — `client/Placeholder/index.ts`
```ts
import { WebBrowsingApiName } from '../../types';
import CrawlMultiPages from './CrawlMultiPages';
import CrawlSinglePage from './CrawlSinglePage';
import { Search } from './Search';
export const WebBrowsingPlaceholders = {
[WebBrowsingApiName.crawlMultiPages]: CrawlMultiPages,
[WebBrowsingApiName.crawlSinglePage]: CrawlSinglePage,
[WebBrowsingApiName.search]: Search,
};
export { CrawlMultiPages, CrawlSinglePage, Search };
```
@@ -0,0 +1,71 @@
# Portal — Full-Screen Detail View (optional)
**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.
## Props (`BuiltinPortalProps<Args, State>`)
```ts
interface BuiltinPortalProps<Arguments = Record<string, any>, State = any> {
apiName?: string;
arguments: Arguments;
identifier: string;
messageId: string;
state: State;
}
```
## Canonical example — Web-Browsing Portal
`packages/builtin-tool-web-browsing/src/client/Portal/index.tsx`:
```tsx
import type { BuiltinPortalProps, CrawlPluginState, SearchQuery } from '@lobechat/types';
import { memo } from 'react';
import { WebBrowsingApiName } from '../../types';
import PageContent from './PageContent';
import PageContents from './PageContents';
import Search from './Search';
const Portal = memo<BuiltinPortalProps>(({ arguments: args, messageId, state, apiName }) => {
switch (apiName) {
case WebBrowsingApiName.search:
return <Search messageId={messageId} query={args as SearchQuery} response={state} />;
case WebBrowsingApiName.crawlSinglePage: {
const result = (state as CrawlPluginState).results.find((r) => r.originalUrl === args.url);
return <PageContent messageId={messageId} result={result} />;
}
case WebBrowsingApiName.crawlMultiPages:
return (
<PageContents
messageId={messageId}
results={(state as CrawlPluginState).results}
urls={args.urls}
/>
);
}
return null;
});
export default Portal;
```
## Portal rules
- One Portal per tool — the file is the routing layer, subcomponents implement each API's view.
- Portals can read the chat store directly to detect "still streaming" and render a Skeleton internally (see `Search/index.tsx:20-46`).
- Layout assumes more space than the Render — use `Flexbox` with `height={'100%'}` and structure for a side panel viewport.
## Portal registry — `packages/builtin-tools/src/portals.ts`
```ts
import { WebBrowsingManifest, WebBrowsingPortal } from '@lobechat/builtin-tool-web-browsing/client';
import { type BuiltinPortal } from '@lobechat/types';
export const BuiltinToolsPortals: Record<string, BuiltinPortal> = {
[WebBrowsingManifest.identifier]: WebBrowsingPortal as BuiltinPortal,
};
```
@@ -0,0 +1,19 @@
# Tool Render 设计原则(中文草案)
这些原则用于判断一个 builtin tool 的 Inspector / Render / Placeholder / Streaming / Intervention / Portal 应该做什么,以及做到什么程度。
1. **先保证折叠态可读。** 每个 API 都必须有 Inspector;用户不展开也应该能看懂 “正在做什么 / 对什么做 / 当前结果是什么”。Inspector 不应该只展示函数名和原始参数。
2. **Inspector 是一句话,不是详情页。** 优先表达动作、关键对象、数量、状态,例如 “分析图片 3 张”“搜索 12 个结果”“读取 config.json”。长文本、列表和结构化结果放到 Render 或 Portal。
3. **Inspector 要覆盖执行生命周期。** `args` 还在 streaming、工具执行中、执行完成、执行失败时都应该有稳定展示;必要时同时读取 `args``partialArgs``pluginState`,避免出现空白、跳变或只显示半截参数。
4. **文案要随状态切换时态。** 同一个动作在 loading 与 completed 两个阶段必须用不同的措辞:执行中用现在进行时(“正在创建任务 / Creating task / 正在搜索”),执行完成后切到完成态(“已创建任务 / Task created / 已找到 N 条”)。Inspector chip 会一直留在聊天记录里 —— 如果一直挂着 “正在 xxx”,几小时后回看历史时会读起来像还在跑。约定的 i18n 形式是 `<api>.loading` / `<api>.completed` 一对键(见 `lobe-agent.apiName.callSubAgent.{loading,completed}``lobe-claude-code.task.{create,list,update,get}.{loading,completed}`),渲染时按 `isArgumentsStreaming || isLoading` 决定取哪一个。只读 / 查询类(“查看任务” 这种本来就是名词性的)可以共用一个键。
5. **只有结构化结果才需要 Render。** 如果工具结果只是自然语言总结,通常不需要 Render;如果结果包含列表、媒体、文件、表格、代码、diff、地图、时间线、权限请求等结构,就应该提供 Render。
6. **Render 要帮助用户检查结果,而不是复述参数。** Render 的主体应该围绕工具产物组织:可预览、可比较、可筛选、可定位。参数只作为上下文辅助出现,不要把 Render 做成一块更大的 args dump。
7. **参数和结果要一起参与渲染。** 好的 Tool UI 通常同时用 `args` 解释意图,用 `pluginState` 展示真实执行结果;但 `pluginState` 只放结果域数据,不要反向塞入可以从 `args` 推导出的内容。
8. **慢操作要有 Placeholder。** 如果工具通常需要等待网络、文件系统、模型或外部进程,Placeholder 应该先占住最终 Render 的版式,让用户知道即将看到什么,而不是只显示一个泛化 loading。
9. **Streaming 只用于连续产物。** 搜索列表、日志、长文本、文件分析、分阶段计划适合 Streaming;一次性小结果不需要强行做 Streaming。Streaming UI 要能渐进追加,并且完成后自然过渡到最终 Render。
10. **有风险的动作必须 Intervention。** 写文件、删除、发送、安装、执行命令、外部可见操作、权限敏感操作,都应该在执行前给出可理解的确认界面;确认文案要说明影响范围,而不是只问 “是否继续”。
11. **错误、空态和截断都是正式状态。** Render 不能在失败、无结果、超长结果时退化成空白。错误要说明发生在哪一步;空态要告诉用户没有产物;超长内容要明确 “展示前 N 项 / 还有 N 项”。
12. **信息密度要克制。** 默认展示最有判断价值的部分:标题、来源、状态、摘要、少量关键字段。大对象、长列表、原文、调试数据放进可展开区域或 Portal,避免把聊天流撑成后台管理页。
13. **视觉上融入聊天流。** Tool UI 应该使用 `@lobehub/ui` / base-ui、`Flexbox``createStaticStyles``cssVar.*`,遵循现有间距、圆角、颜色、字号;不要为单个工具发明一套独立视觉语言。具体的样式约定见 [shared-rules.md](shared-rules.md)。
14. **Devtools fixture 是验收入口。** 新增或修改 Tool UI 时,应在 `/devtools` 里准备覆盖典型态、loading/streaming、空态、错误态、长内容态的 fixture;一个 API 如果在真实聊天里会出现,就不应该在 devtools 中缺席。
15. **先做用户会看的 UI,再做调试 UI。** Raw JSON、trace、schema、内部 id 可以存在,但应默认收起或放到调试区;主界面先回答用户最关心的问题:工具做了什么,结果值不值得信任,下一步能做什么。
@@ -0,0 +1,101 @@
# Render — Rich Result Card (optional)
**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.
## Props (`BuiltinRenderProps<Args, State, Content>`)
```ts
interface BuiltinRenderProps<Arguments = any, State = any, Content = any> {
apiName?: string;
args: Arguments; // final params from the LLM
content: Content; // executor's content string (or parsed)
identifier?: string;
messageId: string; // for store lookups
pluginError?: any; // from BuiltinToolResult.error
pluginState?: State; // executor's state
toolCallId?: string;
}
```
## Two patterns
**Pattern A — Single-file Render** (web-browsing CrawlSinglePage):
```tsx
// client/Render/CrawlSinglePage.tsx
import type { BuiltinRenderProps, CrawlPluginState, CrawlSinglePageQuery } from '@lobechat/types';
import { memo } from 'react';
import PageContent from './PageContent';
const CrawlSinglePage = memo<BuiltinRenderProps<CrawlSinglePageQuery, CrawlPluginState>>(
({ messageId, pluginState, args }) => (
<PageContent messageId={messageId} results={pluginState?.results} urls={[args?.url]} />
),
);
export default CrawlSinglePage;
```
**Pattern B — Folder with subcomponents** (web-browsing Search):
```
client/Render/Search/
├── index.tsx # composes the subcomponents, handles error states
├── ConfigForm.tsx # appears when pluginError.type === 'PluginSettingsInvalid'
├── SearchQuery.tsx # editable query header
└── SearchResult.tsx # result list
```
Use Pattern B when the Render has internal state (editing mode, expanded items), error variants, or is large enough to benefit from splitting.
## Error handling in Render
Renders are the canonical place to surface `pluginError` because the chat doesn't auto-render typed errors:
```tsx
if (pluginError) {
if (pluginError?.type === 'PluginSettingsInvalid') {
return <ConfigForm id={messageId} provider={pluginError.body?.provider} />;
}
return (
<Alert
title={pluginError?.message}
type="error"
extra={<Highlighter language="json">{JSON.stringify(pluginError.body, null, 2)}</Highlighter>}
/>
);
}
```
## Render rules
- **Return `null`** if there's nothing useful to draw yet (avoids empty cards during stream).
- Use `pluginState` for server-truth (ids, counts, server-assigned status) and `args` for what the LLM asked. **Combine — neither alone is enough.**
- For lists, summarize with a header line and show top N items with a "+N more" tail rather than rendering everything.
- **Keep the Render single-layer** — the tool card is already your surface, so don't open with your own filled container and then nest more filled boxes inside it. See [shared-rules.md](shared-rules.md) → "Stay single-layer".
- For modals from a Render, use `@lobehub/ui/base-ui` (`createModal`, `useModalContext`, `confirmModal`) — see the **modal** skill.
## Render registry — `client/Render/index.ts`
```ts
import type { BuiltinRender } from '@lobechat/types';
import { TaskApiName } from '../../types';
import CreateTaskRender from './CreateTask';
import RunTasksRender from './RunTasks';
export const TaskRenders: Record<string, BuiltinRender> = {
[TaskApiName.createTask]: CreateTaskRender as BuiltinRender,
[TaskApiName.runTasks]: RunTasksRender as BuiltinRender,
/* only the APIs with rich result UI — others fall back to text content */
};
export { default as CreateTaskRender } from './CreateTask';
export { default as RunTasksRender } from './RunTasks';
```
## Render display control (rare)
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.
@@ -0,0 +1,89 @@
# Shared Style Rules
These apply across every surface.
## The component skeleton
Every surface file is the same shape, so internalize it once instead of re-deriving it per rule. The skeleton below bakes in five mechanical conventions — copy it and fill the body:
```tsx
'use client'; // (a) leaves of the chat tree must not block server rendering
import type { BuiltinInspectorProps, SearchQuery, UniformSearchResponse } from '@lobechat/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
// (b) type with BuiltinXProps<Args, State> — never widen to `any`.
// Args = the JSON Schema params, State = the executor's `state` field;
// they should match <Name>Params / <Name>State from types.ts.
export const SearchInspector = memo<BuiltinInspectorProps<SearchQuery, UniformSearchResponse>>(
({ args, pluginState }) => {
const { t } = useTranslation('plugin'); // (c) all strings from the `plugin` namespace
// (d) cross-cutting state (loading, streaming buffer) comes from the store,
// not props — props only carry args/state/messageId.
// const buffer = useChatStore((s) => chatToolSelectors.streamingBuffer(messageId)(s));
return <span>{t('builtins.<identifier>.apiName.search')}</span>;
},
);
SearchInspector.displayName = 'SearchInspector'; // (e) always memo + displayName
export default SearchInspector;
```
- **(c)** Default an Inspector to `t('builtins.<identifier>.apiName.<api>')` so the row is non-empty while args stream in.
- **(d)** Read the store via Zustand selectors inside the component; see [streaming.md](streaming.md) for the buffer selector.
## Styling: `createStaticStyles + cssVar.*`, `@lobehub/ui` over `antd`
Zero-runtime CSS-in-JS — styles compile once and read CSS variables at runtime:
```tsx
import { createStaticStyles, cssVar } from 'antd-style';
const styles = createStaticStyles(({ css, cssVar }) => ({
chip: css`
padding-block: 2px;
padding-inline: 8px;
border-radius: 999px;
color: ${cssVar.colorText};
background: ${cssVar.colorFillTertiary};
`,
}));
```
- 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.
- Components come from `@lobehub/ui` (`Block`, `Text`, `Flexbox`, `Highlighter`, `Alert`, `Tooltip`, `Skeleton`), not raw `antd`. Modals come from `@lobehub/ui/base-ui` (`createModal`, `useModalContext`, `confirmModal`) — see the **modal** skill.
- Note: `<Text type='secondary'>` is a lighter shade than `colorTextSecondary`. For that exact token color, write `<Text style={{ color: cssVar.colorTextSecondary }}>`.
## Stay single-layer — don't nest filled cards
The framework already wraps every Render / Intervention in a tool card, so that card **is** your surface. A Render that opens with its own `background: ${cssVar.colorFillQuaternary}` container is already one card deep; put another filled box inside it (`colorBgContainer` / `colorFillTertiary`) and you get the card-in-card look that reads as "complex" — two or three stacked fills for what is really a flat list of fields.
- **The outermost wrapper carries no fill.** Use a flat container with only `padding-block: 4px` for breathing room; let the tool card provide the card. (See `Agent/index.tsx`'s `container`.)
- **At most one filled box, and only to delineate real content** — a Markdown preview, a diff, a code/result block. Labels, keyvalue fields, question/answer text, chips: render flat on the surface, separated by spacing or a hairline divider (`height: 1px; background: ${cssVar.colorFillSecondary}`), not by wrapping each in its own box.
- **A box on a flat surface needs a visible fill.** Once the outer fill is gone, an inner `colorBgContainer` box can vanish against the tool card (same color). Use `colorFillTertiary` for the one content box so it still reads as delineated.
- Don't wrap a single value in a box just to give it padding — that's the redundant-nesting smell (a `detailCard` around a `value` box around one string).
```tsx
// ❌ card-in-card: filled container wrapping a filled preview box
container: css`
padding: 12px;
background: ${cssVar.colorFillQuaternary};
`,
previewBox: css`
background: ${cssVar.colorBgContainer};
`,
// ✅ single-layer: flat container, one visible content box
container: css`
padding-block: 4px;
`,
previewBox: css`
background: ${cssVar.colorFillTertiary};
`,
```
For the common "icon + file/title header, then one content box" shape, reuse `ToolResultCard` from `@lobechat/shared-tool-ui/components` instead of rebuilding it — it's already single-layer (flat wrapper, one `colorFillTertiary` content box) and is what CC `Read` / `Grep` / `Glob` / `Write` / `WebSearch` / `WebFetch` render through.
The exception is a deliberate **panel** pattern — an `<Block variant="outlined">` with a header bar + list rows (CC `TodoWrite` / `Task`). There the single outlined block is the panel and the header fill is a header bar, not a nested card. One structured panel is fine; stacked decorative fills are not.
@@ -0,0 +1,83 @@
# 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.
**Add for** long-running ops with continuous output: shell command execution (stdout/stderr), file write progress, code interpreter cells.
## Props (`BuiltinStreamingProps<Args>`)
```ts
interface BuiltinStreamingProps<Arguments = any> {
apiName: string;
args: Arguments;
identifier: string;
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).
## Canonical example — RunCommandStreaming
`packages/builtin-tool-local-system/src/client/Streaming/RunCommand/index.tsx`:
```tsx
'use client';
import type { BuiltinStreamingProps } from '@lobechat/types';
import { Highlighter } from '@lobehub/ui';
import { memo } from 'react';
interface RunCommandParams {
command?: string;
description?: string;
timeout?: number;
}
export const RunCommandStreaming = memo<BuiltinStreamingProps<RunCommandParams>>(({ args }) => {
const { command } = args || {};
if (!command) return null;
return (
<Highlighter
animated
wrap
language="sh"
showLanguage={false}
style={{ padding: '4px 8px' }}
variant="outlined"
>
{command}
</Highlighter>
);
});
RunCommandStreaming.displayName = 'RunCommandStreaming';
```
For real-time output beyond just the command (stderr/stdout streaming), pull from the chat store:
```tsx
const buffer = useChatStore((state) =>
chatToolSelectors.streamingBuffer(messageId, toolCallId)(state),
);
```
## Streaming rules
- Render `null` until you have something to display (avoids flash).
- For terminal-style output, use `Highlighter` with `animated` to show typing-like effect.
- The Streaming component must **unmount cleanly** when execution ends — typically the framework swaps it out for the Render automatically.
## Streaming registry — `client/Streaming/index.ts`
```ts
import { LocalSystemApiName } from '../..';
import { RunCommandStreaming } from './RunCommand';
import { WriteFileStreaming } from './WriteFile';
export const LocalSystemStreamings = {
[LocalSystemApiName.runCommand]: RunCommandStreaming,
[LocalSystemApiName.writeLocalFile]: WriteFileStreaming,
};
```
+147
View File
@@ -0,0 +1,147 @@
---
name: chat-sdk
description: 'Build multi-platform chat bots with the chat SDK. Use for Slack, Teams, Google Chat, Discord, GitHub, Linear bots, webhooks, mentions, slash commands, cards, modals, or streaming responses.'
user-invocable: false
---
# Chat SDK
Unified TypeScript SDK for building chat bots across Slack, Teams, Google Chat, Discord, GitHub, and Linear. Write bot logic once, deploy everywhere.
## Critical: Read the bundled docs
The `chat` package ships with full documentation in `node_modules/chat/docs/` and TypeScript source types. **Always read these before writing code:**
```
node_modules/chat/docs/ # Full documentation (MDX files)
node_modules/chat/dist/ # Built types (.d.ts files)
```
Key docs to read based on task:
- `docs/getting-started.mdx` — setup guides
- `docs/usage.mdx` — event handlers, threads, messages, channels
- `docs/streaming.mdx` — AI streaming with AI SDK
- `docs/cards.mdx` — JSX interactive cards
- `docs/actions.mdx` — button/dropdown handlers
- `docs/modals.mdx` — form dialogs (Slack only)
- `docs/adapters/*.mdx` — platform-specific adapter setup
- `docs/state/*.mdx` — state adapter config (Redis, ioredis, memory)
Also read the TypeScript types from `node_modules/chat/dist/` to understand the full API surface.
## Quick start
```typescript
import { Chat } from 'chat';
import { createSlackAdapter } from '@chat-adapter/slack';
import { createRedisState } from '@chat-adapter/state-redis';
const bot = new Chat({
userName: 'mybot',
adapters: {
slack: createSlackAdapter({
botToken: process.env.SLACK_BOT_TOKEN!,
signingSecret: process.env.SLACK_SIGNING_SECRET!,
}),
},
state: createRedisState({ url: process.env.REDIS_URL! }),
});
bot.onNewMention(async (thread) => {
await thread.subscribe();
await thread.post("Hello! I'm listening to this thread.");
});
bot.onSubscribedMessage(async (thread, message) => {
await thread.post(`You said: ${message.text}`);
});
```
## Core concepts
- **Chat** — main entry point, coordinates adapters and routes events
- **Adapters** — platform-specific (Slack, Teams, GChat, Discord, GitHub, Linear)
- **State** — pluggable persistence (Redis for prod, memory for dev)
- **Thread** — conversation thread with `post()`, `subscribe()`, `startTyping()`
- **Message** — normalized format with `text`, `formatted` (mdast AST), `raw`
- **Channel** — container for threads, supports listing and posting
## Event handlers
| Handler | Trigger |
| -------------------------- | ------------------------------------------------- |
| `onNewMention` | Bot @-mentioned in unsubscribed thread |
| `onSubscribedMessage` | Any message in subscribed thread |
| `onNewMessage(regex)` | Messages matching pattern in unsubscribed threads |
| `onSlashCommand("/cmd")` | Slash command invocations |
| `onReaction(emojis)` | Emoji reactions added/removed |
| `onAction(actionId)` | Button clicks and dropdown selections |
| `onAssistantThreadStarted` | Slack Assistants API thread opened |
| `onAppHomeOpened` | Slack App Home tab opened |
## Streaming
Pass any `AsyncIterable<string>` to `thread.post()`. Works with AI SDK's `textStream`:
```typescript
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({ model: 'anthropic/claude-4.5-sonnet' });
bot.onNewMention(async (thread, message) => {
const result = await agent.stream({ prompt: message.text });
await thread.post(result.textStream);
});
```
## Cards (JSX)
Set `jsxImportSource: "chat"` in tsconfig. Components: `Card`, `CardText`, `Button`, `Actions`, `Fields`, `Field`, `Select`, `SelectOption`, `Image`, `Divider`, `LinkButton`, `Section`, `RadioSelect`.
```tsx
await thread.post(
<Card title="Order #1234">
<CardText>Your order has been received!</CardText>
<Actions>
<Button id="approve" style="primary">
Approve
</Button>
<Button id="reject" style="danger">
Reject
</Button>
</Actions>
</Card>,
);
```
## Packages
| Package | Purpose |
| ----------------------------- | ----------------------------- |
| `chat` | Core SDK |
| `@chat-adapter/slack` | Slack |
| `@chat-adapter/teams` | Microsoft Teams |
| `@chat-adapter/gchat` | Google Chat |
| `@chat-adapter/discord` | Discord |
| `@chat-adapter/github` | GitHub Issues |
| `@chat-adapter/linear` | Linear Issues |
| `@chat-adapter/state-redis` | Redis state (production) |
| `@chat-adapter/state-ioredis` | ioredis state (alternative) |
| `@chat-adapter/state-memory` | In-memory state (development) |
## Changesets (Release Flow)
This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and changelogs. Every PR that changes a package's behavior must include a changeset.
```bash
pnpm changeset
# → select affected package(s) (e.g. @chat-adapter/slack, chat)
# → choose bump type: patch (fixes), minor (features), major (breaking)
# → write a short summary for the CHANGELOG
```
This creates a file in `.changeset/` — commit it with the PR. When merged to `main`, the Changesets GitHub Action opens a "Version Packages" PR to bump versions and update CHANGELOGs. Merging that PR publishes to npm.
## Webhook setup
Each adapter exposes a webhook handler via `bot.webhooks.{platform}`. Wire these to your HTTP framework's routes (e.g. Next.js API routes, Hono, Express).
+296
View File
@@ -0,0 +1,296 @@
---
name: cli
description: LobeHub CLI (@lobehub/cli) development guide — commands, subcommands, architecture.
disable-model-invocation: true
---
# LobeHub CLI Development Guide
## Overview
LobeHub CLI (`@lobehub/cli`) is a command-line tool for managing and interacting with LobeHub services. Built with Commander.js + TypeScript.
- **Package**: `apps/cli/`
- **Entry**: `apps/cli/src/index.ts`
- **Binaries**: `lh`, `lobe`, `lobehub` (all aliases for the same CLI)
- **Build**: tsup
- **Runtime**: Node.js / Bun
## Architecture
```
apps/cli/src/
├── index.ts # Entry point, registers all commands
├── api/
│ ├── client.ts # tRPC client (type-safe backend API)
│ └── http.ts # Raw HTTP utilities
├── auth/
│ ├── credentials.ts # Encrypted credential storage (AES-256-GCM)
│ ├── refresh.ts # Token auto-refresh
│ └── resolveToken.ts # Token resolution (flag > stored)
├── commands/ # All CLI commands (one file per command group)
│ ├── agent.ts # Agent CRUD + run
│ ├── config.ts # whoami, usage
│ ├── connect.ts # Device gateway connection + daemon
│ ├── doc.ts # Document management
│ ├── file.ts # File management
│ ├── generate/ # Content generation (text/image/video/tts/asr)
│ ├── kb.ts # Knowledge base management
│ ├── login.ts # OIDC Device Code Flow auth
│ ├── logout.ts # Clear credentials
│ ├── memory.ts # User memory management
│ ├── message.ts # Message management
│ ├── model.ts # AI model management
│ ├── plugin.ts # Plugin management
│ ├── provider.ts # AI provider management
│ ├── search.ts # Global search
│ ├── skill.ts # Agent skill management
│ ├── status.ts # Gateway connectivity check
│ └── topic.ts # Conversation topic management
├── daemon/
│ └── manager.ts # Background daemon process management
├── tools/
│ ├── shell.ts # Shell command execution (for gateway)
│ └── file.ts # File operations (for gateway)
├── settings/
│ └── index.ts # Persistent settings (~/.lobehub/)
├── utils/
│ ├── logger.ts # Logging (verbose mode)
│ ├── format.ts # Table output, JSON, timeAgo, truncate
│ └── agentStream.ts # SSE streaming for agent runs
└── constants/
└── urls.ts # Official server & gateway URLs
```
## Command Groups
| Command | Alias | Description |
| ------------- | ----- | ----------------------------------------------------------- |
| `lh login` | - | Authenticate via OIDC Device Code Flow |
| `lh logout` | - | Clear stored credentials |
| `lh connect` | - | Device gateway connection & daemon management |
| `lh status` | - | Quick gateway connectivity check |
| `lh agent` | - | Agent CRUD, run, status |
| `lh generate` | `gen` | Content generation (text, image, video, tts, asr, download) |
| `lh doc` | - | Document CRUD, batch-create, parse, topic linking |
| `lh file` | - | File list, view, delete, recent |
| `lh kb` | - | Knowledge base CRUD, folders, docs, upload, tree view |
| `lh memory` | - | User memory CRUD + extraction |
| `lh message` | - | Message list, search, delete, count, heatmap |
| `lh topic` | - | Topic CRUD + search + recent |
| `lh skill` | - | Skill CRUD + import (GitHub/URL/market) |
| `lh model` | - | Model CRUD, toggle, batch-toggle, clear |
| `lh provider` | - | Provider CRUD, config, test, toggle |
| `lh plugin` | - | Plugin install, uninstall, update |
| `lh search` | - | Global search across all types |
| `lh whoami` | - | Current user info |
| `lh usage` | - | Monthly/daily usage statistics |
## Adding a New Command
### 1. Create Command File
Create `apps/cli/src/commands/<name>.ts`:
```typescript
import type { Command } from 'commander';
import { getTrpcClient } from '../api/client';
import { outputJson, printTable, truncate } from '../utils/format';
export function register<Name>Command(program: Command) {
const cmd = program.command('<name>').description('...');
// Subcommands
cmd
.command('list')
.description('List items')
.option('-L, --limit <n>', 'Maximum number of items', '30')
.option('--json [fields]', 'Output JSON, optionally specify fields')
.action(async (options) => {
const client = await getTrpcClient();
const result = await client.<router>.<procedure>.query({ ... });
// Handle output
});
}
```
### 2. Register in Entry Point
In `apps/cli/src/index.ts`:
```typescript
import { registerNewCommand } from './commands/new';
// ...
registerNewCommand(program);
```
### 3. Add Tests
Create `apps/cli/src/commands/<name>.test.ts` alongside the command file.
## Conventions
### Output Patterns
All list/view commands follow consistent patterns:
- `--json [fields]` - JSON output with optional field filtering
- `--yes` - Skip confirmation for destructive ops
- `-L, --limit <n>` - Pagination limit (default: 30)
- `-v, --verbose` - Verbose logging
### Table Output
```typescript
const rows = items.map((item) => [item.id, truncate(item.title, 40), timeAgo(item.updatedAt)]);
printTable(rows, ['ID', 'TITLE', 'UPDATED']);
```
### JSON Output
```typescript
if (options.json !== undefined) {
const fields = typeof options.json === 'string' ? options.json : undefined;
outputJson(items, fields);
return;
}
```
### Authentication
Commands that need auth use `getTrpcClient()` which auto-resolves tokens:
```typescript
const client = await getTrpcClient();
// client.router.procedure.query/mutate(...)
```
### Confirmation Prompts
```typescript
import { confirm } from '../utils/format';
if (!options.yes) {
const ok = await confirm('Are you sure?');
if (!ok) return;
}
```
## Storage Locations
| File | Path | Purpose |
| ------------- | ----------------------------- | ------------------------------ |
| Credentials | `~/.lobehub/credentials.json` | Encrypted tokens (AES-256-GCM) |
| Settings | `~/.lobehub/settings.json` | Custom server/gateway URLs |
| Daemon PID | `~/.lobehub/daemon.pid` | Background process PID |
| Daemon Status | `~/.lobehub/daemon.status` | Connection status JSON |
| Daemon Log | `~/.lobehub/daemon.log` | Daemon output log |
The base directory (`~/.lobehub/`) can be overridden with the `LOBEHUB_CLI_HOME` env var (e.g. `LOBEHUB_CLI_HOME=.lobehub-dev` for dev mode isolation).
## Key Dependencies
- `commander` - CLI framework
- `@trpc/client` + `superjson` - Type-safe API client
- `@lobechat/device-gateway-client` - WebSocket gateway connection
- `@lobechat/local-file-shell` - Local shell/file tool execution
- `picocolors` - Terminal colors
- `ws` - WebSocket
- `diff` - Text diffing
- `fast-glob` - File pattern matching
## Development
### Running in Dev Mode
Dev mode uses `LOBEHUB_CLI_HOME=.lobehub-dev` to isolate credentials from the global `~/.lobehub/` directory, so dev and production configs never conflict.
```bash
# Run a command in dev mode (from apps/cli/)
cd apps/cli && bun run dev -- <command>
# This is equivalent to:
LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts <command>
```
### Connecting to Local Dev Server
To test CLI against a local dev server (e.g. `localhost:3011`):
**Step 1: Start the local server**
```bash
# From cloud repo root
bun run dev
# Server starts on http://localhost:3011 (or configured port)
```
**Step 2: Login to local server via Device Code Flow**
```bash
cd apps/cli && bun run dev -- login --server http://localhost:3011
```
This will:
1. Call `POST http://localhost:3011/oidc/device/auth` to get a device code
2. Print a URL like `http://localhost:3011/oidc/device?user_code=XXXX-YYYY`
3. Open the URL in your browser — log in and authorize
4. Save credentials to `apps/cli/.lobehub-dev/credentials.json`
5. Save server URL to `apps/cli/.lobehub-dev/settings.json`
After login, all subsequent `bun run dev -- <command>` calls will use the local server.
**Step 3: Run commands against local server**
```bash
cd apps/cli && bun run dev -- task list
cd apps/cli && bun run dev -- task create -i "Test task" -n "My Task"
cd apps/cli && bun run dev -- agent list
```
**Troubleshooting:**
- If login returns `invalid_grant`, make sure the local OIDC provider is properly configured (check `OIDC_*` env vars in `.env`)
- If you get `UNAUTHORIZED` on API calls, your token may have expired — run `bun run dev -- login --server http://localhost:3011` again
- Dev credentials are stored in `apps/cli/.lobehub-dev/` (gitignored), not in `~/.lobehub/`
### Switching Between Local and Production
```bash
# Dev mode (local server) — uses .lobehub-dev/
cd apps/cli && bun run dev -- <command>
# Production (app.lobehub.com) — uses ~/.lobehub/
lh <command>
```
The two environments are completely isolated by different credential directories.
### Build & Test
```bash
# Build CLI
cd apps/cli && bun run build
# Unit tests
cd apps/cli && bun run test
# E2E tests (requires authenticated CLI)
cd apps/cli && bunx vitest run e2e/kb.e2e.test.ts
# Link globally for testing (installs lh/lobe/lobehub commands)
cd apps/cli && bun run cli:link
```
## Detailed Command References
See `references/` for each command group:
- **Agent**: `references/agent.md` (CRUD, run, status)
- **Content Generation**: `references/generate.md` (text, image, video, tts, asr, download)
- **Knowledge & Files**: `references/knowledge.md` (kb, file, doc)
- **Conversation**: `references/conversation.md` (topic, message)
- **Memory**: `references/memory.md` (memory management, extraction)
- **Skills & Plugins**: `references/skills-plugins.md` (skill, plugin)
- **Models & Providers**: `references/models-providers.md` (model, provider)
- **Search & Config**: `references/search-config.md` (search, whoami, usage)
+144
View File
@@ -0,0 +1,144 @@
# Agent Commands
Manage AI agents: create, edit, delete, list, run, and check status.
**Source**: `apps/cli/src/commands/agent.ts`
## `lh agent list`
List all agents.
```bash
lh agent list [-L [-k [--json [fields]] < n > ] < keyword > ]
```
| Option | Description | Default |
| ------------------------- | -------------------------------------- | ------- |
| `-L, --limit <n>` | Maximum items | `30` |
| `-k, --keyword <keyword>` | Filter by keyword | - |
| `--json [fields]` | JSON output with optional field filter | - |
**Table columns**: ID, TITLE, DESCRIPTION, MODEL
---
## `lh agent view <agentId>`
View agent configuration details.
```bash
lh agent view [fields]] < agentId > [--json
```
**Displays**: Title, description, model, provider, system role, plugins, tools.
---
## `lh agent create`
Create a new agent.
```bash
lh agent create [options]
```
| Option | Description | Required |
| --------------------------- | -------------- | -------- |
| `-t, --title <title>` | Agent title | No |
| `-d, --description <desc>` | Description | No |
| `-m, --model <model>` | Model ID | No |
| `-p, --provider <provider>` | Provider ID | No |
| `-s, --system-role <role>` | System prompt | No |
| `--group <groupId>` | Agent group ID | No |
**Output**: Created agent ID and session ID.
---
## `lh agent edit <agentId>`
Update an existing agent. Same options as `create`, all optional. Only specified fields are updated.
```bash
lh agent edit [-m [-s ... < agentId > [-t < title > ] < model > ] < role > ]
```
---
## `lh agent delete <agentId>`
Delete an agent.
```bash
lh agent delete < agentId > [--yes]
```
Requires confirmation unless `--yes` is provided.
---
## `lh agent duplicate <agentId>`
Duplicate an existing agent.
```bash
lh agent duplicate < agentId > [-t < title > ]
```
| Option | Description |
| --------------------- | ------------------------------------ |
| `-t, --title <title>` | Optional new title for the duplicate |
**Output**: New agent ID.
---
## `lh agent run`
Start an agent execution (streaming SSE).
```bash
lh agent run [options]
```
| Option | Description |
| --------------------- | -------------------------------------------- |
| `-a, --agent-id <id>` | Agent ID to run |
| `-s, --slug <slug>` | Agent slug (alternative to ID) |
| `-p, --prompt <text>` | User prompt |
| `-t, --topic-id <id>` | Reuse existing topic |
| `--no-auto-start` | Don't auto-start the agent |
| `--json` | Output full JSON event stream |
| `-v, --verbose` | Show detailed tool call info |
| `--replay <file>` | Replay events from saved JSON file (offline) |
### Streaming Behavior
Uses `utils/agentStream.ts` to handle Server-Sent Events:
1. Sends agent run request to backend
2. Streams SSE events in real-time
3. Displays: text chunks, tool call status, operation progress
4. Shows final token usage and cost summary
### Replay Mode
`--replay <file>` reads a saved JSON event stream for offline debugging without server connection.
---
## `lh agent status <operationId>`
Check agent operation status.
```bash
lh agent status [fields]] [--history] [--history-limit < operationId > [--json < n > ]
```
| Option | Description | Default |
| --------------------- | -------------------- | ------- |
| `--json [fields]` | JSON output | - |
| `--history` | Include step history | `false` |
| `--history-limit <n>` | Max history entries | `10` |
**Displays**: Status (running/completed/failed), steps count, tokens used, cost, error info, timestamps.
@@ -0,0 +1,122 @@
# Conversation Commands (Topic & Message)
## Topic Management (`lh topic`)
Manage conversation topics (threads).
**Source**: `apps/cli/src/commands/topic.ts`
### `lh topic list`
```bash
lh topic list [--agent-id [-L [--page [--json [fields]] < id > ] < n > ] < n > ]
```
| Option | Description | Default |
| ----------------- | --------------- | ------- |
| `--agent-id <id>` | Filter by agent | - |
| `-L, --limit <n>` | Page size | `30` |
| `--page <n>` | Page number | `1` |
**Table columns**: ID, TITLE, FAV, UPDATED
### `lh topic search <keywords>`
```bash
lh topic search [--json [fields]] < keywords > [--agent-id < id > ]
```
### `lh topic create`
```bash
lh topic create -t [--favorite] < title > [--agent-id < id > ]
```
| Option | Description | Required |
| --------------------- | -------------------- | -------- |
| `-t, --title <title>` | Topic title | Yes |
| `--agent-id <id>` | Associate with agent | No |
| `--favorite` | Mark as favorite | No |
### `lh topic edit <id>`
```bash
lh topic edit [--favorite] [--no-favorite] < id > [-t < title > ]
```
### `lh topic delete <ids...>`
```bash
lh topic delete [--yes] < id1 > [id2...]
```
### `lh topic recent`
```bash
lh topic recent [-L [--json [fields]] < n > ]
```
| Option | Description | Default |
| ----------------- | --------------- | ------- |
| `-L, --limit <n>` | Number of items | `10` |
---
## Message Management (`lh message`)
Manage chat messages within topics.
**Source**: `apps/cli/src/commands/message.ts`
### `lh message list`
```bash
lh message list [options] [--json [fields]]
```
| Option | Description | Default |
| ----------------- | ----------------------- | ------- |
| `--topic-id <id>` | Filter by topic | - |
| `--agent-id <id>` | Filter by agent | - |
| `-L, --limit <n>` | Page size | `30` |
| `--page <n>` | Page number | `1` |
| `--user` | Only show user messages | - |
**Table columns**: ID, ROLE, CONTENT, CREATED
**Note**: When `--topic-id` or `--agent-id` is provided, uses `message.getMessages`; otherwise uses `message.listAll`.
### `lh message search <keywords>`
```bash
lh message search [fields]] < keywords > [--json
```
Full-text search across all messages.
### `lh message delete <ids...>`
```bash
lh message delete [--yes] < id1 > [id2...]
```
### `lh message count`
```bash
lh message count [--start [--end [--json] < date > ] < date > ]
```
| Option | Description |
| ---------------- | ------------------------------------------ |
| `--start <date>` | Start date (ISO format, e.g. `2024-01-01`) |
| `--end <date>` | End date (ISO format) |
**Output**: Total message count for the specified period.
### `lh message heatmap`
```bash
lh message heatmap [--json]
```
**Output**: Activity heatmap data showing message frequency over time.
+271
View File
@@ -0,0 +1,271 @@
# Content Generation Commands
Generate text, images, videos, speech, and transcriptions.
**Source**: `apps/cli/src/commands/generate/`
## Command Structure
```
lh generate (alias: gen)
├── text <prompt> # Text generation
├── image <prompt> # Image generation
├── video <prompt> # Video generation
├── tts <text> # Text-to-speech
├── asr <audioFile> # Audio-to-text (speech recognition)
├── download <generationId> <asyncTaskId> # Wait & download generation result
├── status <generationId> <asyncTaskId> # Check async task status
└── list # List generation topics
```
> ⚠️ **Important**: `status` and `download` require an `asyncTaskId` (UUID format, e.g.
> `7ad0eb13-e9a5-4403-8070-1f7fe95b2f95`), **not** the generation ID (`gen_xxx`).
> The asyncTaskId is printed after "→ Task" in the `video` / `image` command output.
---
## `lh generate text <prompt>` / `lh gen text <prompt>`
Generate text completion.
**Source**: `apps/cli/src/commands/generate/text.ts`
```bash
lh gen text "Explain quantum computing" [options]
echo "context" | lh gen text "summarize" --pipe
```
| Option | Description | Default |
| --------------------------- | ---------------------------------- | -------------------- |
| `-m, --model <model>` | Model ID | `openai/gpt-4o-mini` |
| `-p, --provider <provider>` | Provider name | - |
| `-s, --system <prompt>` | System prompt | - |
| `--temperature <n>` | Temperature (0-2) | - |
| `--max-tokens <n>` | Maximum output tokens | - |
| `--stream` | Enable streaming output | `false` |
| `--json` | Output full JSON response | `false` |
| `--pipe` | Read additional context from stdin | `false` |
### Pipe Mode
When `--pipe` is used, reads stdin and prepends it to the prompt. Useful for piping file contents:
```bash
cat README.md | lh gen text "summarize this" --pipe
```
---
## `lh generate image <prompt>` / `lh gen image <prompt>`
Generate images from text prompt. This is an async operation — the command submits the task and returns a generation ID + async task ID for tracking.
**Source**: `apps/cli/src/commands/generate/image.ts`
```bash
lh gen image "A sunset over mountains" [options]
lh gen image "A cute cat" --model dall-e-3 --provider openai --json
```
| Option | Description | Default |
| --------------------------- | ---------------- | ---------- |
| `-m, --model <model>` | Model ID | `dall-e-3` |
| `-p, --provider <provider>` | Provider name | `openai` |
| `-n, --num <n>` | Number of images | `1` |
| `--width <px>` | Width in pixels | - |
| `--height <px>` | Height in pixels | - |
| `--steps <n>` | Number of steps | - |
| `--seed <n>` | Random seed | - |
| `--json` | Output raw JSON | `false` |
**Output** (non-JSON):
```
✓ Image generation started
Batch ID: gb_xxx
1 image(s) queued
Generation gen_xxx → Task 7ad0eb13-xxxx-xxxx-xxxx-xxxxxxxxxxxx
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is the asyncTaskId — use this for status/download
Use "lh generate status <generationId> <asyncTaskId>" to check progress.
```
**Typical workflow**:
```bash
# 1. Submit generation — note down BOTH IDs from the output
lh gen image "A cute cat"
# Generation gen_abc123 → Task 7ad0eb13-e9a5-4403-8070-1f7fe95b2f95
# 2. Wait & download using generationId + asyncTaskId (the UUID)
lh gen download gen_abc123 7ad0eb13-e9a5-4403-8070-1f7fe95b2f95 -o cat.png
```
---
## `lh generate video <prompt>` / `lh gen video <prompt>`
Generate video from text prompt. This is an async operation.
**Source**: `apps/cli/src/commands/generate/video.ts`
```bash
lh gen video "A cat playing piano" -m < model > -p < provider > [options]
```
| Option | Description | Required |
| --------------------------- | ------------------------ | -------- |
| `-m, --model <model>` | Model ID | Yes |
| `-p, --provider <provider>` | Provider name | Yes |
| `--aspect-ratio <ratio>` | Aspect ratio (e.g. 16:9) | No |
| `--duration <sec>` | Duration in seconds | No |
| `--resolution <res>` | Resolution (e.g. 720p) | No |
| `--seed <n>` | Random seed | No |
| `--json` | Output raw JSON | No |
**Note**: Unlike image, video requires `-m` and `-p` (no defaults). Use `lh model list <provider> --type video` to find available video models.
**Output** (non-JSON):
```
✓ Video generation started
Batch ID: gb_xxx
Generation gen_xxx → Task 7ad0eb13-xxxx-xxxx-xxxx-xxxxxxxxxxxx
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This is the asyncTaskId — use this for status/download
Use "lh generate status <generationId> <asyncTaskId>" to check progress.
```
**Typical workflow**:
```bash
# 1. Find available video models for a provider
lh model list volcengine --json | grep -i seedance
# 2. Submit generation — note down BOTH IDs from the output
lh gen video "A cat on a runway" -m doubao-seedance-2-0-260128 -p volcengine \
--aspect-ratio 9:16 --duration 5 --resolution 1080p
# Generation gen_abc123 → Task 7ad0eb13-e9a5-4403-8070-1f7fe95b2f95
# 3. Wait & download using generationId + asyncTaskId (the UUID)
lh gen download gen_abc123 7ad0eb13-e9a5-4403-8070-1f7fe95b2f95 -o result.mp4 --timeout 600
```
---
## `lh generate tts <text>` / `lh gen tts <text>`
Text-to-speech generation.
**Source**: `apps/cli/src/commands/generate/tts.ts`
```bash
lh gen tts "Hello, world!" [options]
```
---
## `lh generate asr <audioFile>` / `lh gen asr <audioFile>`
Audio-to-text transcription (Automatic Speech Recognition).
**Source**: `apps/cli/src/commands/generate/asr.ts`
```bash
lh gen asr recording.wav [options]
```
---
## `lh generate download <generationId> <asyncTaskId>`
Wait for an async generation task to complete and download the result file.
**Source**: `apps/cli/src/commands/generate/index.ts`
> ⚠️ `<asyncTaskId>` is the UUID printed after "→ Task" in the video/image output.
> Do **not** pass the generation ID (`gen_xxx`) here — that will cause a server error.
```bash
lh gen download <generationId> <asyncTaskId> [-o output.png]
lh gen download gen_xxx 7ad0eb13-xxxx-xxxx-xxxx-xxxxxxxxxxxx -o ~/Desktop/result.mp4 --timeout 600
```
| Option | Description | Default |
| --------------------- | ---------------------------------------- | ---------------------- |
| `-o, --output <path>` | Output file path (auto-detect extension) | `<generationId>.<ext>` |
| `--interval <sec>` | Polling interval in seconds | `5` |
| `--timeout <sec>` | Timeout in seconds (0 = no timeout) | `300` |
**Behavior**:
1. Polls `generation.getGenerationStatus` at the specified interval
2. Shows live progress: `⋯ Status: processing... (42s)`
3. On success: downloads asset URL to local file
4. On error / wrong ID: displays a clear message pointing to the correct ID format
5. On timeout: suggests using `lh gen status` to check later
---
## `lh generate status <generationId> <asyncTaskId>`
Check the status of an async generation task.
> ⚠️ `<asyncTaskId>` is the UUID printed after "→ Task" in the video/image output.
> Do **not** pass the generation ID (`gen_xxx`) here — that will cause a server error.
```bash
lh gen status <generationId> <asyncTaskId> [--json]
lh gen status gen_xxx 7ad0eb13-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
| Option | Description |
| -------- | ------------------------ |
| `--json` | Output raw JSON response |
**Displays**:
- Status (color-coded): `success` (green), `error` (red), `processing` (yellow), `pending` (cyan)
- Error message (if failed)
- Asset URL and thumbnail URL (if completed)
---
## `lh generate list`
List all generation topics.
```bash
lh gen list [--json [fields]]
```
**Table columns**: ID, TITLE, TYPE, UPDATED
---
## Backend Architecture
Image and video generation use an async task pattern:
1. **Create topic**`generationTopic.createTopic`
2. **Submit generation**`image.createImage` / `video.createVideo`
- Creates batch + generation + asyncTask records in a DB transaction
- Triggers async background task (image via `createAsyncCaller`, video via `initModelRuntimeFromDB`)
- Returns `{ data: { batch, generations }, success }` with `asyncTaskId` in each generation
3. **Poll status**`generation.getGenerationStatus`
- Input: `{ generationId, asyncTaskId }` — both are required, and `asyncTaskId` must be the
UUID from the `async_tasks` table, not `gen_xxx`
- Returns `{ status, error, generation }` (generation includes asset URLs on success)
- Before querying, calls `checkTimeoutTasks` which marks tasks as `error` if they have been
`pending` or `processing` for more than \~5 minutes (`ASYNC_TASK_TIMEOUT = 298s`)
**Server routes**:
- `apps/server/src/routers/lambda/image/index.ts` — image creation (uses `authedProcedure` + `serverDatabase`)
- `apps/server/src/routers/lambda/video/index.ts` — video creation (uses `authedProcedure` + `serverDatabase`)
- `apps/server/src/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`.
+281
View File
@@ -0,0 +1,281 @@
# Knowledge Base, File & Document Commands
## Knowledge Base (`lh kb`)
Manage knowledge bases for RAG (Retrieval-Augmented Generation). Supports directory tree structure with folders, documents, and file uploads.
**Source**: `apps/cli/src/commands/kb.ts`
### `lh kb list`
```bash
lh kb list [--json [fields]]
```
**Table columns**: ID, NAME, DESCRIPTION, UPDATED
### `lh kb view <id>`
```bash
lh kb view [fields]] < id > [--json
```
**Displays**: Name, description, full directory tree with all files and documents (recursively fetched). Shows indented tree structure with item type (File/Doc), file type, and size.
**API**: Uses `file.getKnowledgeItems` to recursively fetch items. Folders (`custom/folder` fileType) are traversed in parallel via `Promise.all` for performance.
### `lh kb create`
```bash
lh kb create -n [--avatar < name > [-d < desc > ] < url > ]
```
| Option | Description | Required |
| -------------------------- | ------------------- | -------- |
| `-n, --name <name>` | Knowledge base name | Yes |
| `-d, --description <desc>` | Description | No |
| `--avatar <url>` | Avatar URL | No |
**Output**: Created KB ID. Note: backend returns ID as a string directly (not an object).
### `lh kb edit <id>`
```bash
lh kb edit [-d [--avatar < id > [-n < name > ] < desc > ] < url > ]
```
Requires at least one change flag. Errors if none specified.
### `lh kb delete <id>`
```bash
lh kb delete [--yes] < id > [--remove-files]
```
| Option | Description |
| ---------------- | ---------------------------- |
| `--remove-files` | Also delete associated files |
| `--yes` | Skip confirmation |
### `lh kb add-files <knowledgeBaseId>`
```bash
lh kb add-files <kbId> --ids <fileId1> <fileId2> ...
```
Link existing files to a knowledge base.
### `lh kb remove-files <knowledgeBaseId>`
```bash
lh kb remove-files <kbId> --ids <fileId1> <fileId2> ... [--yes]
```
Unlink files from a knowledge base.
### `lh kb mkdir <knowledgeBaseId>`
```bash
lh kb mkdir < kbId > -n < name > [--parent < folderId > ]
```
Create a folder in a knowledge base. Uses `document.createDocument` with `fileType: 'custom/folder'`.
| Option | Description | Required |
| --------------------- | ---------------- | -------- |
| `-n, --name <name>` | Folder name | Yes |
| `--parent <parentId>` | Parent folder ID | No |
### `lh kb create-doc <knowledgeBaseId>`
```bash
lh kb create-doc [--parent < kbId > -t < title > [-c < content > ] < folderId > ]
```
Create a document in a knowledge base. Uses `document.createDocument` with `fileType: 'custom/document'`.
| Option | Description | Required |
| ---------------------- | ---------------- | -------- |
| `-t, --title <title>` | Document title | Yes |
| `-c, --content <text>` | Document content | No |
| `--parent <parentId>` | Parent folder ID | No |
### `lh kb move <id>`
```bash
lh kb move < id > --type < file | doc > [--parent < folderId > ]
```
Move a file or document to a different folder (or to root if `--parent` is omitted).
| Option | Description | Default |
| --------------------- | -------------------------------- | ------- |
| `--type <type>` | Item type: `file` or `doc` | `file` |
| `--parent <parentId>` | Target folder ID (omit for root) | - |
Uses `document.updateDocument` for docs, `file.updateFile` for files.
### `lh kb upload <knowledgeBaseId> <filePath>`
```bash
lh kb upload <kbId> <filePath> [--parent <folderId>]
```
Upload a local file to a knowledge base via S3 presigned URL.
| Option | Description |
| --------------------- | ---------------- |
| `--parent <parentId>` | Parent folder ID |
**Flow**: Compute SHA-256 hash → get presigned URL via `upload.createS3PreSignedUrl` → PUT to S3 → create file record via `file.createFile`.
---
## File Management (`lh file`)
Manage uploaded files.
**Source**: `apps/cli/src/commands/file.ts`
### `lh file list`
```bash
lh file list [--kb-id [-L [--json [fields]] < id > ] < n > ]
```
| Option | Description | Default |
| ----------------- | ------------------------ | ------- |
| `--kb-id <id>` | Filter by knowledge base | - |
| `-L, --limit <n>` | Maximum items | `30` |
**Table columns**: ID, NAME, TYPE, SIZE, UPDATED
### `lh file view <id>`
```bash
lh file view [fields]] < id > [--json
```
**Displays**: Name, type, size, chunking status, embedding status.
### `lh file delete <ids...>`
```bash
lh file delete [--yes] < id1 > [id2...]
```
Supports deleting multiple files at once.
### `lh file recent`
```bash
lh file recent [-L [--json [fields]] < n > ]
```
| Option | Description | Default |
| ----------------- | --------------- | ------- |
| `-L, --limit <n>` | Number of items | `10` |
---
## Document Management (`lh doc`)
Manage text documents (notes, wiki pages).
**Source**: `apps/cli/src/commands/doc.ts`
### `lh doc list`
```bash
lh doc list [-L [--file-type [--source-type [--json [fields]] < n > ] < type > ] < type > ]
```
| Option | Description | Default |
| ---------------------- | --------------------------------------------- | ------- |
| `-L, --limit <n>` | Maximum items | `30` |
| `--file-type <type>` | Filter by file type | - |
| `--source-type <type>` | Filter by source type (file, web, api, topic) | - |
**Table columns**: ID, TITLE, TYPE, UPDATED
### `lh doc view <id>`
```bash
lh doc view [fields]] < id > [--json
```
**Displays**: Title, type, KB association, updated time, full content.
### `lh doc create`
```bash
lh doc create -t [-F [--parent [--slug [--kb [--file-type < title > [-b < body > ] < path > ] < id > ] < slug > ] < id > ] < type > ]
```
| Option | Description | Required |
| ------------------------ | ----------------------------------------------- | -------- |
| `-t, --title <title>` | Document title | Yes |
| `-b, --body <content>` | Document body text | No |
| `-F, --body-file <path>` | Read body from file | No |
| `--parent <id>` | Parent document ID | No |
| `--slug <slug>` | Custom URL slug | No |
| `--kb <id>` | Knowledge base ID to associate with | No |
| `--file-type <type>` | File type (e.g. custom/document, custom/folder) | No |
`-b` and `-F` are mutually exclusive; `-F` reads the file content as the body.
### `lh doc batch-create <file>`
Batch create documents from a JSON file. The file must contain a non-empty array of document objects.
```bash
lh doc batch-create documents.json
```
Each object in the array can have: `title`, `content`, `fileType`, `knowledgeBaseId`, `parentId`, `slug`.
### `lh doc edit <id>`
```bash
lh doc edit [-b [-F [--parent [--file-type < id > [-t < title > ] < body > ] < path > ] < id > ] < type > ]
```
### `lh doc delete <ids...>`
```bash
lh doc delete [--yes] < id1 > [id2...]
```
### `lh doc parse <fileId>`
Parse an uploaded file into a document.
```bash
lh doc parse [--json [fields]] < fileId > [--with-pages]
```
| Option | Description |
| -------------- | ----------------------- |
| `--with-pages` | Preserve page structure |
**Output**: Parsed title and content preview.
### `lh doc link-topic <docId> <topicId>`
Associate a document with a topic. Creates a linked copy via the notebook router.
```bash
lh doc link-topic <docId> <topicId>
```
### `lh doc topic-docs <topicId>`
List documents associated with a topic.
```bash
lh doc topic-docs [--json [fields]] < topicId > [--type < type > ]
```
| Option | Description |
| --------------- | ------------------------------------------------ |
| `--type <type>` | Filter by type (article, markdown, note, report) |
+138
View File
@@ -0,0 +1,138 @@
# Memory Commands
Manage user memories - the AI's long-term knowledge about users.
**Source**: `apps/cli/src/commands/memory.ts`
## Memory Categories
| Category | Description |
| ------------ | ----------------------------------------- |
| `identity` | User's name, role, relationships |
| `activity` | Recent activities and their status |
| `context` | Ongoing contexts, projects, goals |
| `experience` | Past experiences and key learnings |
| `preference` | User preferences, directives, suggestions |
---
## `lh memory list [category]`
List memory entries, optionally filtered by category.
```bash
lh memory list # All categories
lh memory list identity # Only identity memories
lh memory list preference # Only preferences
```
| Option | Description |
| ----------------- | ----------- |
| `--json [fields]` | JSON output |
**Output**: Grouped by category, showing type/status and descriptions.
---
## `lh memory create`
Create a new identity memory entry.
```bash
lh memory create [options]
```
| Option | Description |
| -------------------------- | ------------------------ |
| `--type <type>` | Memory type |
| `--role <role>` | User's role |
| `--relationship <rel>` | Relationship description |
| `-d, --description <desc>` | Description |
| `--labels <labels...>` | Extracted labels |
---
## `lh memory edit <category> <id>`
Edit a memory entry. Options vary by category:
```bash
lh memory edit identity < id > [options]
lh memory edit activity < id > [options]
lh memory edit context < id > [options]
lh memory edit experience < id > [options]
lh memory edit preference < id > [options]
```
### Category-specific Options
**identity**:
- `--type <type>`, `--role <role>`, `--relationship <rel>`
**activity**:
- `--narrative <text>`, `--notes <text>`, `--status <status>`
**context**:
- `--title <title>`, `--description <desc>`, `--status <status>`
**experience**:
- `--situation <text>`, `--action <text>`, `--key-learning <text>`
**preference**:
- `--directives <text>`, `--suggestions <text>`
---
## `lh memory delete <category> <id>`
```bash
lh memory delete identity < id > [--yes]
```
---
## `lh memory persona`
Display the compiled memory persona summary.
```bash
lh memory persona [--json [fields]]
```
**Output**: Summarized user profile built from all memory categories.
---
## `lh memory extract`
Trigger async memory extraction from chat history.
```bash
lh memory extract [--from [--to < date > ] < date > ]
```
| Option | Description |
| --------------- | ----------------------- |
| `--from <date>` | Start date (ISO format) |
| `--to <date>` | End date (ISO format) |
Starts a background task that analyzes chat history and creates new memory entries.
---
## `lh memory extract-status`
Check the status of a memory extraction task.
```bash
lh memory extract-status [--task-id [--json [fields]] < id > ]
```
| Option | Description |
| ---------------- | ------------------- |
| `--task-id <id>` | Check specific task |
@@ -0,0 +1,186 @@
# Model & Provider Commands
## Model Management (`lh model`)
Manage AI models within providers.
**Source**: `apps/cli/src/commands/model.ts`
### `lh model list <providerId>`
List models for a specific provider.
```bash
lh model list openai
lh model list openai --type image --enabled
lh model list lobehub --type video --json
```
| Option | Description | Default |
| ----------------- | -------------------------------------------------------------------------------------- | ------- |
| `-L, --limit <n>` | Maximum items | `50` |
| `--enabled` | Only show enabled models | `false` |
| `--type <type>` | Filter by model type (`chat\|embedding\|tts\|stt\|image\|video\|text2music\|realtime`) | - |
| `--json [fields]` | Output JSON, optionally specify fields | - |
**Table columns**: ID, NAME, ENABLED, TYPE
**Backend**: `aiModel.getAiProviderModelList``AiInfraRepos.getAiProviderModelList` (supports `type` filter at repository level)
### `lh model view <id>`
```bash
lh model view [fields]] < modelId > [--json
```
**Displays**: Name, provider, type, enabled status, capabilities.
### `lh model create`
```bash
lh model create --id [--type < id > --provider < providerId > [--display-name < name > ] < type > ]
```
| Option | Description | Default |
| ------------------------- | ------------ | -------- |
| `--id <id>` | Model ID | Required |
| `--provider <providerId>` | Provider ID | Required |
| `--display-name <name>` | Display name | - |
| `--type <type>` | Model type | `chat` |
### `lh model edit <id>`
```bash
lh model edit [--type < modelId > --provider < providerId > [--display-name < name > ] < type > ]
```
### `lh model toggle <id>`
Enable or disable a model.
```bash
lh model toggle < modelId > --provider < providerId > --enable
lh model toggle < modelId > --provider < providerId > --disable
```
| Option | Description | Required |
| ------------------------- | ----------------- | ------------ |
| `--provider <providerId>` | Provider ID | Yes |
| `--enable` | Enable the model | One required |
| `--disable` | Disable the model | One required |
### `lh model batch-toggle <ids...>`
Enable or disable multiple models at once.
```bash
lh model batch-toggle model1 model2 model3 --provider openai --enable
```
### `lh model delete <id>`
```bash
lh model delete < modelId > --provider < providerId > [--yes]
```
### `lh model clear`
Clear all models (or only remote/fetched models) for a provider.
```bash
lh model clear --provider [--yes] < providerId > [--remote]
```
---
## Provider Management (`lh provider`)
Manage AI service providers.
**Source**: `apps/cli/src/commands/provider.ts`
### `lh provider list`
```bash
lh provider list [--json [fields]]
```
**Table columns**: ID, NAME, ENABLED, SOURCE
### `lh provider view <id>`
```bash
lh provider view [fields]] < providerId > [--json
```
**Displays**: Name, enabled status, source, configuration.
### `lh provider create`
```bash
lh provider create --id [-d [--logo [--sdk-type < id > -n < name > [-s < source > ] < desc > ] < url > ] < type > ]
```
| Option | Description | Default |
| -------------------------- | ------------------------------------------------- | -------- |
| `--id <id>` | Provider ID | Required |
| `-n, --name <name>` | Provider name | Required |
| `-s, --source <source>` | Source type (`builtin` or `custom`) | `custom` |
| `-d, --description <desc>` | Provider description | - |
| `--logo <logo>` | Provider logo URL | - |
| `--sdk-type <sdkType>` | SDK type (openai, anthropic, azure, bedrock, ...) | - |
### `lh provider edit <id>`
```bash
lh provider edit [-d [--logo [--sdk-type < providerId > [-n < name > ] < desc > ] < url > ] < type > ]
```
Requires at least one change flag.
### `lh provider config <id>`
Configure provider settings (API key, base URL, etc.).
```bash
lh provider config openai --api-key sk-xxx
lh provider config openai --base-url https://custom-endpoint.com
lh provider config openai --show
lh provider config openai --show --json
```
| Option | Description |
| ------------------------ | --------------------------------- |
| `--api-key <key>` | Set API key |
| `--base-url <url>` | Set base URL |
| `--check-model <model>` | Set connectivity check model |
| `--enable-response-api` | Enable Response API mode (OpenAI) |
| `--disable-response-api` | Disable Response API mode |
| `--fetch-on-client` | Enable fetching models on client |
| `--no-fetch-on-client` | Disable fetching models on client |
| `--show` | Show current config |
| `--json [fields]` | Output JSON (with --show) |
**Important**: The `lobehub` provider is platform-managed. Attempting to set `--api-key` or `--base-url` on it will be rejected with an error message.
### `lh provider test <id>`
Test provider connectivity.
```bash
lh provider test openai
lh provider test openai -m gpt-4o --json
```
### `lh provider toggle <id>`
```bash
lh provider toggle < providerId > --enable
lh provider toggle < providerId > --disable
```
### `lh provider delete <id>`
```bash
lh provider delete < providerId > [--yes]
```
@@ -0,0 +1,94 @@
# Search & Configuration Commands
## Global Search (`lh search`)
Search across all LobeHub resource types.
**Source**: `apps/cli/src/commands/search.ts`
### `lh search <query>`
```bash
lh search "meeting notes" [-t [-L [--json [fields]] < type > ] < n > ]
```
| Option | Description | Default |
| ------------------- | ----------------------- | --------- |
| `-t, --type <type>` | Filter by resource type | All types |
| `-L, --limit <n>` | Results per type | `10` |
### Searchable Types
| Type | Description |
| ---------------- | ---------------------------- |
| `agent` | AI agents |
| `topic` | Conversation topics |
| `file` | Uploaded files |
| `folder` | File folders |
| `message` | Chat messages |
| `page` | Documents/pages |
| `memory` | User memories |
| `mcp` | MCP servers |
| `plugin` | Installed plugins |
| `communityAgent` | Community marketplace agents |
| `knowledgeBase` | Knowledge bases |
**Output**: Results grouped by type, showing ID, title/name, description.
---
## User Configuration (`lh whoami` / `lh usage`)
**Source**: `apps/cli/src/commands/config.ts`
### `lh whoami`
Display current authenticated user information.
```bash
lh whoami [--json [fields]]
```
**Displays**: Name, username, email, user ID, subscription plan.
### `lh usage`
Display usage statistics.
```bash
lh usage [--month [--daily] [--json [fields]] < YYYY-MM > ]
```
| Option | Description | Default |
| ------------------- | -------------- | ----------------------- |
| `--month <YYYY-MM>` | Month to query | Current month |
| `--daily` | Group by day | `false` (monthly total) |
**Output**: Token usage, costs, and model breakdown for the specified period.
---
## Global Options
These options are available across most commands:
| Option | Description |
| ----------------- | ---------------------------------------------------------------------- |
| `--json [fields]` | Output as JSON; optionally filter to specific fields (comma-separated) |
| `--yes` | Skip confirmation prompts for destructive operations |
| `-L, --limit <n>` | Pagination limit for list commands |
| `-v, --verbose` | Enable verbose/debug logging |
| `--help` | Show command help |
| `--version` | Show CLI version |
### JSON Field Filtering
The `--json` option supports field selection:
```bash
# Full JSON output
lh agent list --json
# Only specific fields
lh agent list --json "id,title,model"
```
@@ -0,0 +1,149 @@
# Skill & Plugin Commands
## Skill Management (`lh skill`)
Manage agent skills (custom instructions and capabilities).
**Source**: `apps/cli/src/commands/skill.ts`
### `lh skill list`
```bash
lh skill list [--source [--json [fields]] < source > ]
```
| Option | Description |
| ------------------- | ----------------------------------- |
| `--source <source>` | Filter: `builtin`, `market`, `user` |
**Table columns**: ID, NAME, DESCRIPTION, SOURCE, IDENTIFIER
### `lh skill view <id>`
```bash
lh skill view [fields]] < id > [--json
```
**Displays**: Name, description, source, identifier, content.
### `lh skill create`
```bash
lh skill create -n < name > -d < desc > -c < content > [-i < identifier > ]
```
| Option | Description | Required |
| -------------------------- | ----------------------------------- | -------- |
| `-n, --name <name>` | Skill name | Yes |
| `-d, --description <desc>` | Description | Yes |
| `-c, --content <content>` | Skill content (prompt/instructions) | Yes |
| `-i, --identifier <id>` | Custom identifier | No |
### `lh skill edit <id>`
```bash
lh skill edit [-n [-d < id > [-c < content > ] < name > ] < desc > ]
```
### `lh skill delete <id>`
```bash
lh skill delete < id > [--yes]
```
### `lh skill search <query>`
```bash
lh skill search [fields]] < query > [--json
```
### `lh skill install <source>` (alias: `lh skill i`)
Install a skill. Auto-detects source type from the input:
```bash
# GitHub (URL or owner/repo shorthand)
lh skill install lobehub/skill-repo
lh skill install https://github.com/lobehub/skill-repo
lh skill install lobehub/skill-repo --branch dev
# ZIP URL
lh skill install https://example.com/skill.zip
# Marketplace identifier
lh skill install my-cool-skill
lh skill i my-cool-skill
```
| Option | Description | Notes |
| ------------------- | ------------------------- | -------- |
| `--branch <branch>` | Branch name (GitHub only) | Optional |
**Detection rules**:
- `https://github.com/...` or `owner/repo` → GitHub
- Other `https://...` URLs → ZIP URL
- Everything else → marketplace identifier
### Resource Commands
#### `lh skill resources <id>`
List files/resources within a skill.
```bash
lh skill resources [fields]] < id > [--json
```
**Displays**: Path, type, size.
#### `lh skill read-resource <id> <path>`
Read a specific resource file from a skill.
```bash
lh skill read-resource <skillId> <path>
```
**Output**: File content or JSON metadata.
---
## Plugin Management (`lh plugin`)
Install and manage plugins (external tool integrations).
**Source**: `apps/cli/src/commands/plugin.ts`
### `lh plugin list`
```bash
lh plugin list [--json [fields]]
```
**Table columns**: ID, IDENTIFIER, TYPE, TITLE
### `lh plugin install`
```bash
lh plugin install -i [--settings < identifier > --manifest < json > [--type < type > ] < json > ]
```
| Option | Description | Required |
| ----------------------- | -------------------------- | ---------------------- |
| `-i, --identifier <id>` | Plugin identifier | Yes |
| `--manifest <json>` | Plugin manifest JSON | Yes |
| `--type <type>` | `plugin` or `customPlugin` | No (default: `plugin`) |
| `--settings <json>` | Plugin settings JSON | No |
### `lh plugin uninstall <id>`
```bash
lh plugin uninstall < id > [--yes]
```
### `lh plugin update <id>`
```bash
lh plugin update [--settings < id > [--manifest < json > ] < json > ]
```
@@ -0,0 +1,614 @@
---
name: data-fetching-architecture
description: 'LobeHub data-fetching pipeline guide. Use for service layer, Zustand store, SWR, lambdaClient, useClientDataSWR, useFetchXxx hooks, or migrating useEffect fetches.'
user-invocable: false
---
# LobeHub Data Fetching Architecture
> **Related:** `store-data-structures` covers List vs Detail data shape rationale (Map vs Array).
## Architecture Overview
```text
┌─────────────┐
│ Component │
└──────┬──────┘
│ 1. Call useFetchXxx hook from store
┌──────────────────┐
│ Zustand Store │
│ (State + Hook) │
└──────┬───────────┘
│ 2. useClientDataSWR calls service
┌──────────────────┐
│ Service Layer │
│ (xxxService) │
└──────┬───────────┘
│ 3. Call lambdaClient
┌──────────────────┐
│ lambdaClient │
│ (TRPC Client) │
└──────────────────┘
```
## Core Principles
### ✅ DO
1. **Use Service Layer** for all API calls
2. **Use Store SWR Hooks** for data fetching (not useEffect)
3. **Use proper data structures** — see `store-data-structures` skill for List vs Detail patterns
4. **Use lambdaClient.mutate** for write operations (create/update/delete)
5. **Use lambdaClient.query** only inside service methods
6. **Naming convention** — read hooks are `useFetchXxx`, cache invalidation helpers are `refreshXxx` (e.g. `useFetchBenchmarks` / `refreshBenchmarks`). Mutations then chain `refreshXxx()` after the service call.
### ❌ DON'T
1. **Never use useEffect** for data fetching
2. **Never call lambdaClient** directly in components or stores
3. **Never use useState** for server data
4. **Never mix data structure patterns** — follow `store-data-structures` skill
---
## Layer 1: Service Layer
### Purpose
- Encapsulate all API calls to lambdaClient
- Provide clean, typed interfaces
- Single source of truth for API operations
### Service Structure
```typescript
// src/services/agentEval.ts
class AgentEvalService {
// Query methods - READ operations
async listBenchmarks() {
return lambdaClient.agentEval.listBenchmarks.query();
}
async getBenchmark(id: string) {
return lambdaClient.agentEval.getBenchmark.query({ id });
}
// Mutation methods - WRITE operations
async createBenchmark(params: CreateBenchmarkParams) {
return lambdaClient.agentEval.createBenchmark.mutate(params);
}
async updateBenchmark(params: UpdateBenchmarkParams) {
return lambdaClient.agentEval.updateBenchmark.mutate(params);
}
async deleteBenchmark(id: string) {
return lambdaClient.agentEval.deleteBenchmark.mutate({ id });
}
}
export const agentEvalService = new AgentEvalService();
```
### Service Guidelines
1. **One service per domain** (e.g., agentEval, ragEval, aiAgent)
2. **Export singleton instance** (`export const xxxService = new XxxService()`)
3. **Method names match operations** (list, get, create, update, delete)
4. **Clear parameter types** (use interfaces for complex params)
---
## Layer 2: Store with SWR Hooks
### Purpose
- Manage client-side state
- Provide SWR hooks for data fetching
- Handle cache invalidation
### State Structure
```typescript
// src/store/eval/slices/benchmark/initialState.ts
export interface BenchmarkSliceState {
// List data - simple array
benchmarkList: AgentEvalBenchmarkListItem[];
benchmarkListInit: boolean;
// Detail data - map for caching
benchmarkDetailMap: Record<string, AgentEvalBenchmark>;
loadingBenchmarkDetailIds: string[];
// Mutation states
isCreatingBenchmark: boolean;
isUpdatingBenchmark: boolean;
isDeletingBenchmark: boolean;
}
```
> For complete initialState, reducer, and internal dispatch patterns, see the `store-data-structures` skill.
### Actions
```typescript
// src/store/eval/slices/benchmark/action.ts
const FETCH_BENCHMARKS_KEY = 'FETCH_BENCHMARKS';
const FETCH_BENCHMARK_DETAIL_KEY = 'FETCH_BENCHMARK_DETAIL';
export interface BenchmarkAction {
// SWR Hooks - for data fetching
useFetchBenchmarks: () => SWRResponse;
useFetchBenchmarkDetail: (id?: string) => SWRResponse;
// Refresh methods - for cache invalidation
refreshBenchmarks: () => Promise<void>;
refreshBenchmarkDetail: (id: string) => Promise<void>;
// Mutation actions
createBenchmark: (params: CreateParams) => Promise<any>;
updateBenchmark: (params: UpdateParams) => Promise<void>;
deleteBenchmark: (id: string) => Promise<void>;
// Internal methods - not for direct UI use
internal_dispatchBenchmarkDetail: (payload: BenchmarkDetailDispatch) => void;
internal_updateBenchmarkDetailLoading: (id: string, loading: boolean) => void;
}
export const createBenchmarkSlice: StateCreator<EvalStore, any, [], BenchmarkAction> = (
set,
get,
) => ({
// Fetch list — simple array stored in benchmarkList
useFetchBenchmarks: () =>
useClientDataSWR(FETCH_BENCHMARKS_KEY, () => agentEvalService.listBenchmarks(), {
onSuccess: (data) => {
set({ benchmarkList: data, benchmarkListInit: true }, false, 'useFetchBenchmarks/success');
},
}),
// Fetch detail — null key disables the request when id is missing
useFetchBenchmarkDetail: (id) =>
useClientDataSWR(
id ? [FETCH_BENCHMARK_DETAIL_KEY, id] : null,
() => agentEvalService.getBenchmark(id!),
{
onSuccess: (data) => {
get().internal_dispatchBenchmarkDetail({
type: 'setBenchmarkDetail',
id: id!,
value: data,
});
get().internal_updateBenchmarkDetailLoading(id!, false);
},
},
),
// Refresh methods
refreshBenchmarks: () => mutate(FETCH_BENCHMARKS_KEY),
refreshBenchmarkDetail: (id) => mutate([FETCH_BENCHMARK_DETAIL_KEY, id]),
// CREATE — refresh list after creation
createBenchmark: async (params) => {
set({ isCreatingBenchmark: true }, false, 'createBenchmark/start');
try {
const result = await agentEvalService.createBenchmark(params);
await get().refreshBenchmarks();
return result;
} finally {
set({ isCreatingBenchmark: false }, false, 'createBenchmark/end');
}
},
// UPDATE — optimistic update + refresh
updateBenchmark: async (params) => {
const { id } = params;
// 1. Optimistic update
get().internal_dispatchBenchmarkDetail({
type: 'updateBenchmarkDetail',
id,
value: params,
});
// 2. Set loading
get().internal_updateBenchmarkDetailLoading(id, true);
try {
// 3. Call service
await agentEvalService.updateBenchmark(params);
// 4. Refresh from server
await get().refreshBenchmarks();
await get().refreshBenchmarkDetail(id);
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// DELETE — optimistic update + refresh
deleteBenchmark: async (id) => {
get().internal_dispatchBenchmarkDetail({ type: 'deleteBenchmarkDetail', id });
get().internal_updateBenchmarkDetailLoading(id, true);
try {
await agentEvalService.deleteBenchmark(id);
await get().refreshBenchmarks();
} finally {
get().internal_updateBenchmarkDetailLoading(id, false);
}
},
// Internal — dispatch to reducer (for detail map)
internal_dispatchBenchmarkDetail: (payload) => {
const currentMap = get().benchmarkDetailMap;
const nextMap = benchmarkDetailReducer(currentMap, payload);
// Skip set when nothing changed — avoids unnecessary re-renders
if (isEqual(nextMap, currentMap)) return;
set({ benchmarkDetailMap: nextMap }, false, `dispatchBenchmarkDetail/${payload.type}`);
},
// Internal — update loading state for specific detail
internal_updateBenchmarkDetailLoading: (id, loading) => {
set(
(state) => ({
loadingBenchmarkDetailIds: loading
? [...state.loadingBenchmarkDetailIds, id]
: state.loadingBenchmarkDetailIds.filter((i) => i !== id),
}),
false,
'updateBenchmarkDetailLoading',
);
},
});
```
### Store Guidelines
1. **SWR keys as constants** at top of file
2. **useClientDataSWR** for all data fetching (never useEffect)
3. **onSuccess callback** updates store state
4. **Refresh methods** use `mutate()` to invalidate cache
5. **Loading states** in initialState, updated in onSuccess
6. **Mutations** call service, then refresh relevant cache
---
## Layer 3: Component Usage
### Fetching List Data
```tsx
// ✅ CORRECT
const BenchmarkList = () => {
// 1. Get the hook from store
const useFetchBenchmarks = useEvalStore((s) => s.useFetchBenchmarks);
// 2. Get list data
const benchmarks = useEvalStore((s) => s.benchmarkList);
const isInit = useEvalStore((s) => s.benchmarkListInit);
// 3. Call the hook (SWR handles the data fetching)
useFetchBenchmarks();
// 4. Use the data
if (!isInit) return <Loading />;
return (
<div>
<h2>Total: {benchmarks.length}</h2>
{benchmarks.map((b) => (
<BenchmarkCard key={b.id} {...b} />
))}
</div>
);
};
```
### Fetching Detail Data
```tsx
// ✅ CORRECT
const BenchmarkDetail = () => {
const { benchmarkId } = useParams<{ benchmarkId: string }>();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
// Detail from map
const benchmark = useEvalStore((s) =>
benchmarkId ? s.benchmarkDetailMap[benchmarkId] : undefined,
);
// Per-item loading
const isLoading = useEvalStore((s) =>
benchmarkId ? s.loadingBenchmarkDetailIds.includes(benchmarkId) : false,
);
useFetchBenchmarkDetail(benchmarkId);
if (!benchmark) return <Loading />;
return (
<div>
<h1>{benchmark.name}</h1>
<p>{benchmark.description}</p>
{isLoading && <Spinner />}
</div>
);
};
```
### Using Selectors (Recommended)
```typescript
// src/store/eval/slices/benchmark/selectors.ts
export const benchmarkSelectors = {
getBenchmarkDetail: (id: string) => (s: EvalStore) => s.benchmarkDetailMap[id],
isLoadingBenchmarkDetail: (id: string) => (s: EvalStore) =>
s.loadingBenchmarkDetailIds.includes(id),
};
// Component with selectors
const BenchmarkDetail = () => {
const { benchmarkId } = useParams();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const benchmark = useEvalStore(benchmarkSelectors.getBenchmarkDetail(benchmarkId!));
useFetchBenchmarkDetail(benchmarkId);
return <div>{benchmark && <h1>{benchmark.name}</h1>}</div>;
};
```
### Anti-pattern
```tsx
// ❌ WRONG — Don't use useEffect for data fetching
const BenchmarkList = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
lambdaClient.agentEval.listBenchmarks
.query()
.then(setData)
.finally(() => setLoading(false));
}, []);
return <div>...</div>;
};
```
### Mutations in Components
```tsx
// Create — global mutation flag drives form loading
const CreateBenchmarkModal = () => {
const createBenchmark = useEvalStore((s) => s.createBenchmark);
const isCreating = useEvalStore((s) => s.isCreatingBenchmark);
const handleSubmit = async (values) => {
try {
// Optimistic update + refresh happen inside createBenchmark
await createBenchmark(values);
message.success('Created successfully');
onClose();
} catch (error) {
message.error('Failed to create');
}
};
return (
<Form onSubmit={handleSubmit} loading={isCreating}>
...
</Form>
);
};
// Update / delete — per-item loading so only the row being mutated spins
const BenchmarkItem = ({ id }: { id: string }) => {
const updateBenchmark = useEvalStore((s) => s.updateBenchmark);
const deleteBenchmark = useEvalStore((s) => s.deleteBenchmark);
const isLoading = useEvalStore(benchmarkSelectors.isLoadingBenchmarkDetail(id));
const handleUpdate = async (data) => {
await updateBenchmark({ id, ...data });
};
const handleDelete = async () => {
await deleteBenchmark(id);
};
return (
<div>
{isLoading && <Spinner />}
<button onClick={handleUpdate}>Update</button>
<button onClick={handleDelete}>Delete</button>
</div>
);
};
```
**Why two patterns:** create has no id yet, so a single `isCreatingXxx` flag is enough. Update/delete target a specific row, so global flags would freeze unrelated rows — keep per-item state in `loadingXxxIds`.
---
## Need a fuller worked example?
The canonical `Benchmark` example above is the one to copy for a flat list + detail map. If you need to maintain a list **keyed by a parent id** (e.g. `datasetMap[benchmarkId]` because the same shape appears under multiple parents), read [`references/walkthrough.md`](./references/walkthrough.md) — it walks through the full 6 steps (service → reducer → slice → store wiring → selectors → component) for that variant.
---
## Common Patterns
### Pattern 1: Pagination
Cache key array must include every parameter that should trigger a refetch.
```typescript
useFetchTestCases: (params: { datasetId: string; limit: number; offset: number }) =>
useClientDataSWR(
params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId, params.limit, params.offset] : null,
() => agentEvalService.listTestCases(params),
{
onSuccess: (data) =>
set({
testCaseList: data.data,
testCaseTotal: data.total,
isLoadingTestCases: false,
}),
},
);
```
### Pattern 2: Dependent Fetching
Both hooks run in parallel — SWR dedupes, no manual sequencing needed.
```tsx
const BenchmarkDetail = () => {
const { benchmarkId } = useParams();
const useFetchBenchmarkDetail = useEvalStore((s) => s.useFetchBenchmarkDetail);
const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
useFetchBenchmarkDetail(benchmarkId);
useFetchDatasets(benchmarkId);
return <div>...</div>;
};
```
### Pattern 3: Conditional Fetching
Pass `undefined` to disable the hook entirely.
```tsx
// only fetch when modal is open AND id present
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
```
### Pattern 4: Cross-domain Refresh
```typescript
deleteBenchmark: async (id) => {
await agentEvalService.deleteBenchmark(id);
await get().refreshBenchmarks();
await get().refreshDatasets(id); // related cache invalidated too
};
```
---
## Migration Guide: useEffect → Store SWR
### Before (❌ Wrong)
```tsx
const TestCaseList = ({ datasetId }: Props) => {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
lambdaClient.agentEval.listTestCases
.query({ datasetId })
.then((r) => setData(r.data))
.finally(() => setLoading(false));
}, [datasetId]);
return <Table data={data} loading={loading} />;
};
```
### After (✅ Correct)
```typescript
// 1. Add service method
class AgentEvalService {
async listTestCases(params: { datasetId: string }) {
return lambdaClient.agentEval.listTestCases.query(params);
}
}
// 2. Add store slice hook
export const createTestCaseSlice: StateCreator<...> = (set) => ({
useFetchTestCases: (params) =>
useClientDataSWR(
params.datasetId ? [FETCH_TEST_CASES_KEY, params.datasetId] : null,
() => agentEvalService.listTestCases(params),
{
onSuccess: (data) =>
set({ testCaseList: data.data, isLoadingTestCases: false }),
},
),
});
// 3. Component reads from store
const TestCaseList = ({ datasetId }: Props) => {
const useFetchTestCases = useEvalStore((s) => s.useFetchTestCases);
const data = useEvalStore((s) => s.testCaseList);
const loading = useEvalStore((s) => s.isLoadingTestCases);
useFetchTestCases({ datasetId });
return <Table data={data} loading={loading} />;
};
```
---
## Troubleshooting
| Symptom | Check |
| --------------------------- | ------------------------------------------------------------------- |
| Data never loads | Hook called? Key not `null`/`undefined`? Network tab shows request? |
| Stale data after mutation | Did `refreshXxx` run? Cache key matches what the hook uses? |
| Loading state stuck `true` | `onSuccess` writes loading=false? Promise rejected silently? |
| Detail map missing an entry | Reducer dispatch ran? `isEqual` short-circuited on stale data? |
---
## Summary Checklist
When adding new data fetching:
### Step 1: Types & State
See `store-data-structures` for details.
- [ ] Define types in `@lobechat/types`: Detail type + List item type
- [ ] State structure: `xxxList: XxxListItem[]`, `xxxDetailMap: Record<string, Xxx>`, `loadingXxxDetailIds: string[]`
- [ ] Reducer if optimistic updates are needed
### Step 2: Service Layer
- [ ] Create service in `src/services/xxxService.ts`
- [ ] Methods: `listXxx()`, `getXxx(id)`, `createXxx()`, `updateXxx()`, `deleteXxx()`
### Step 3: Store Actions
- [ ] `initialState.ts` with state structure
- [ ] `action.ts` with:
- [ ] `useFetchXxxList()`, `useFetchXxxDetail(id)` — SWR hooks
- [ ] `refreshXxxList()`, `refreshXxxDetail(id)` — cache invalidation
- [ ] CRUD methods calling service
- [ ] `internal_dispatch`, `internal_updateLoading` if using reducer
- [ ] `selectors.ts` (optional but recommended)
- [ ] Integrate slice into main store + initialState
### Step 4: Component Usage
- [ ] Use store hooks (NOT useEffect)
- [ ] List pages: access `xxxList` array
- [ ] Detail pages: access `xxxDetailMap[id]`
- [ ] Use loading states for UI feedback
**Mental model:** Types → Service → Reducer → Slice → Component 🎯
---
## Related Skills
- **`store-data-structures`** — How to structure List and Detail data in stores
- **`zustand`** — General Zustand patterns and best practices
@@ -0,0 +1,244 @@
# Walkthrough: Adding a New Feature End-to-End
This is a worked example of the canonical 6-step recipe applied to a new entity (`Dataset`), showing a variant of the main skill's pattern: **a list keyed by a parent id** (`datasetMap[benchmarkId]`), useful when the same shape appears under different parents.
If you only need the canonical (single-array) pattern, the main `SKILL.md` already shows it for `Benchmark`. Read this file when you need the parent-keyed Map variant, or when you want a checklist-style walkthrough.
## Step 1: Add Service methods
```typescript
class AgentEvalService {
async listDatasets(benchmarkId: string) {
return lambdaClient.agentEval.listDatasets.query({ benchmarkId });
}
async getDataset(id: string) {
return lambdaClient.agentEval.getDataset.query({ id });
}
async createDataset(params: CreateDatasetParams) {
return lambdaClient.agentEval.createDataset.mutate(params);
}
// updateDataset / deleteDataset follow the same shape
}
```
## Step 2: Reducer (optimistic updates)
```typescript
// src/store/eval/slices/dataset/reducer.ts
export type DatasetDispatch =
| { type: 'addDataset'; value: Dataset }
| { type: 'updateDataset'; id: string; value: Partial<Dataset> }
| { type: 'deleteDataset'; id: string };
export const datasetReducer = (state: Dataset[] = [], payload: DatasetDispatch): Dataset[] =>
produce(state, (draft) => {
switch (payload.type) {
case 'addDataset':
draft.unshift(payload.value);
break;
case 'updateDataset': {
const i = draft.findIndex((item) => item.id === payload.id);
if (i !== -1) draft[i] = { ...draft[i], ...payload.value };
break;
}
case 'deleteDataset': {
const i = draft.findIndex((item) => item.id === payload.id);
if (i !== -1) draft.splice(i, 1);
break;
}
}
});
```
## Step 3: Store slice
```typescript
// src/store/eval/slices/dataset/initialState.ts
export interface DatasetData {
currentPage: number;
hasMore: boolean;
isLoading: boolean;
items: Dataset[];
pageSize: number;
total: number;
}
export interface DatasetSliceState {
// Map keyed by benchmarkId — multiple parent contexts share the slice
datasetMap: Record<string, DatasetData>;
// Single item for modal display
datasetDetail: Dataset | null;
isLoadingDatasetDetail: boolean;
loadingDatasetIds: string[];
}
export const datasetInitialState: DatasetSliceState = {
datasetMap: {},
datasetDetail: null,
isLoadingDatasetDetail: false,
loadingDatasetIds: [],
};
```
```typescript
// src/store/eval/slices/dataset/action.ts
const FETCH_DATASETS_KEY = 'FETCH_DATASETS';
const FETCH_DATASET_DETAIL_KEY = 'FETCH_DATASET_DETAIL';
export const createDatasetSlice: StateCreator<EvalStore, any, [], DatasetAction> = (set, get) => ({
// Cache key includes benchmarkId so each parent has its own SWR entry
useFetchDatasets: (benchmarkId) =>
useClientDataSWR(
benchmarkId ? [FETCH_DATASETS_KEY, benchmarkId] : null,
() => agentEvalService.listDatasets(benchmarkId!),
{
onSuccess: (data) => {
set({
datasetMap: {
...get().datasetMap,
[benchmarkId!]: {
currentPage: 1,
hasMore: false,
isLoading: false,
items: data,
pageSize: data.length,
total: data.length,
},
},
});
},
},
),
useFetchDatasetDetail: (id) =>
useClientDataSWR(
id ? [FETCH_DATASET_DETAIL_KEY, id] : null,
() => agentEvalService.getDataset(id!),
{
onSuccess: (data) => set({ datasetDetail: data, isLoadingDatasetDetail: false }),
},
),
refreshDatasets: (benchmarkId) => mutate([FETCH_DATASETS_KEY, benchmarkId]),
refreshDatasetDetail: (id) => mutate([FETCH_DATASET_DETAIL_KEY, id]),
// CREATE with optimistic update — note the temp id pattern
createDataset: async (params) => {
const tmpId = Date.now().toString();
const { benchmarkId } = params;
get().internal_dispatchDataset(
{ type: 'addDataset', value: { ...params, id: tmpId, createdAt: Date.now() } as any },
benchmarkId,
);
get().internal_updateDatasetLoading(tmpId, true);
try {
const result = await agentEvalService.createDataset(params);
await get().refreshDatasets(benchmarkId);
return result;
} finally {
get().internal_updateDatasetLoading(tmpId, false);
}
},
// UPDATE / DELETE follow the same optimistic + refresh pattern as BenchmarkSlice
// (see the main SKILL.md)
// Internal — dispatch reducer scoped to a parent
internal_dispatchDataset: (payload, benchmarkId) => {
const currentData = get().datasetMap[benchmarkId];
const nextItems = datasetReducer(currentData?.items, payload);
// Skip set when nothing changed — avoids unnecessary re-renders
if (isEqual(nextItems, currentData?.items)) return;
set({
datasetMap: {
...get().datasetMap,
[benchmarkId]: {
...currentData,
currentPage: currentData?.currentPage ?? 1,
hasMore: currentData?.hasMore ?? false,
isLoading: false,
items: nextItems,
pageSize: currentData?.pageSize ?? nextItems.length,
total: currentData?.total ?? nextItems.length,
},
},
});
},
internal_updateDatasetLoading: (id, loading) => {
set((state) => ({
loadingDatasetIds: loading
? [...state.loadingDatasetIds, id]
: state.loadingDatasetIds.filter((i) => i !== id),
}));
},
});
```
## Step 4: Wire into the store
```typescript
// src/store/eval/store.ts
export type EvalStore = EvalStoreState & BenchmarkAction & DatasetAction & RunAction;
const createStore: StateCreator<EvalStore, [['zustand/devtools', never]]> = (set, get, store) => ({
...initialState,
...createBenchmarkSlice(set, get, store),
...createDatasetSlice(set, get, store),
...createRunSlice(set, get, store),
});
// src/store/eval/initialState.ts
export const initialState: EvalStoreState = {
...benchmarkInitialState,
...datasetInitialState,
...runInitialState,
};
```
## Step 5: Selectors (optional but recommended)
```typescript
export const datasetSelectors = {
getDatasetData: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId],
getDatasets: (benchmarkId: string) => (s: EvalStore) => s.datasetMap[benchmarkId]?.items ?? [],
isLoadingDataset: (id: string) => (s: EvalStore) => s.loadingDatasetIds.includes(id),
};
```
## Step 6: Use in component
```tsx
// List scoped to a parent
const DatasetList = ({ benchmarkId }: { benchmarkId: string }) => {
const useFetchDatasets = useEvalStore((s) => s.useFetchDatasets);
const datasets = useEvalStore(datasetSelectors.getDatasets(benchmarkId));
const datasetData = useEvalStore(datasetSelectors.getDatasetData(benchmarkId));
useFetchDatasets(benchmarkId);
if (datasetData?.isLoading) return <Loading />;
return (
<div>
<h2>Total: {datasetData?.total ?? 0}</h2>
<List data={datasets} />
</div>
);
};
// Single item for modal — conditional fetching pattern
const DatasetImportModal = ({ open, datasetId }: Props) => {
const useFetchDatasetDetail = useEvalStore((s) => s.useFetchDatasetDetail);
const dataset = useEvalStore((s) => s.datasetDetail);
const isLoading = useEvalStore((s) => s.isLoadingDatasetDetail);
// Only fetch when modal is open AND id present
useFetchDatasetDetail(open && datasetId ? datasetId : undefined);
return <Modal open={open}>{isLoading ? <Loading /> : <div>{dataset?.name}</div>}</Modal>;
};
```
+167
View File
@@ -0,0 +1,167 @@
---
name: db-migrations
description: 'Use for Drizzle migrations: schema/table/column changes, migration generation or regeneration, sequence conflicts after rebase, idempotent SQL review, or migration renames.'
user-invocable: false
---
# Database Migrations Guide
## Development-stage schema changes
Schema changes churn during feature development. When the schema changes before the migration has shipped, do not hand-edit the existing migration SQL to chase the new schema shape. Delete the draft migration artifacts added by this branch (SQL file, matching snapshot, and matching journal entry), then run the generator again and re-apply the normal migration review steps below.
For example, if this branch's draft migration is `0110_add_verify_tables_and_ai_infra_id`:
```bash
# 1. Delete the draft SQL and its snapshot
rm packages/database/migrations/0110_add_verify_tables_and_ai_infra_id.sql
rm packages/database/migrations/meta/0110_snapshot.json
# 2. Remove the matching 0110 entry from the journal's "entries" array
# packages/database/migrations/meta/_journal.json
# 3. Regenerate from the current schema
bun run db:generate
```
This keeps the generated SQL, snapshot, and journal aligned with the actual schema. Manual SQL edits are reserved for review-time hardening such as idempotent clauses, custom extension SQL, and meaningful filename/tag updates.
Before release, if a feature branch accumulated multiple development-only migrations, consolidate them into one migration when possible. Production does not need to replay every intermediate draft shape, and fewer migrations reduce deploy-time risk.
For example, if this branch added `0110`, `0111`, and `0112`, delete all three drafts and regenerate a single migration:
```bash
# 1. Delete every draft SQL and snapshot this branch added
rm packages/database/migrations/011{0,1,2}_*.sql
rm packages/database/migrations/meta/011{0,1,2}_snapshot.json
# 2. Remove the 0110/0111/0112 entries from the journal's "entries" array
# packages/database/migrations/meta/_journal.json
# 3. Regenerate one migration covering the full schema delta
bun run db:generate
```
Do not make a migration compatible with earlier development-only versions of the same branch. While the migration has not shipped, there is no production history to preserve. Fix local/dev databases directly with whatever SQL is simplest (drop the draft table, rename a column, delete draft rows), then regenerate the branch migration from the current schema.
For example, if an earlier draft on this branch created `signup_attempt_id` and you have since renamed it to `user_signup_log_id`, do not add a compatibility `ALTER ... RENAME` to the migration. Just fix the dev DB directly (see the `access-pg` skill for the `bun -e` + `pg` pattern), then regenerate:
```bash
# Fix the dev DB to match the new schema (simplest SQL wins)
set -a && source .env && set +a && bun -e '
import pg from "pg";
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
await client.query("ALTER TABLE user_signup_logs DROP COLUMN signup_attempt_id");
await client.end();
'
# Regenerate so the migration reflects only the final shape
bun run db:generate
```
After a migration has reached production or the target default branch, treat it as immutable: add a follow-up migration instead of rewriting it.
## Rebase conflicts
When a rebase conflicts in migration files, keep the upstream/default-branch migrations and remove all migrations introduced by the current feature branch. Complete the rebase, then regenerate this branch's migration from the rebased schema. This avoids merging two independent snapshots or hand-splicing journal entries.
## Step 1: Generate Migrations
```bash
bun run db:generate
```
This generates:
- `packages/database/migrations/0046_meaningless_file_name.sql`
And updates:
- `packages/database/migrations/meta/_journal.json`
- `packages/database/src/core/migrations.json`
- `docs/development/database-schema.dbml`
## Custom Migrations (e.g. CREATE EXTENSION)
For migrations that don't involve Drizzle schema changes (e.g. enabling PostgreSQL extensions), use the `--custom` flag:
```bash
bunx drizzle-kit generate --custom --name=enable_pg_search
```
This generates an empty SQL file and properly updates `_journal.json` and snapshot. Then edit the generated SQL file to add your custom SQL:
```sql
-- Custom SQL migration file, put your code below! --
CREATE EXTENSION IF NOT EXISTS pg_search;
```
**Do NOT manually create migration files or edit `_journal.json`** — always use `drizzle-kit generate` to ensure correct journal entries and snapshots.
## Step 2: Optimize Migration SQL Filename
Rename auto-generated filename to be meaningful:
`0046_meaningless_file_name.sql``0046_user_add_avatar_column.sql`
## Step 3: Use Idempotent Clauses (Defensive Programming)
Always use defensive clauses to make migrations idempotent (safe to re-run):
### CREATE TABLE
```sql
-- ✅ Good
CREATE TABLE IF NOT EXISTS "agent_eval_runs" (
"id" text PRIMARY KEY NOT NULL,
"name" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
-- ❌ Bad
CREATE TABLE "agent_eval_runs" (...);
```
### ALTER TABLE - Columns
```sql
-- ✅ Good
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "avatar" text;
ALTER TABLE "posts" DROP COLUMN IF EXISTS "deprecated_field";
-- ❌ Bad
ALTER TABLE "users" ADD COLUMN "avatar" text;
```
### ALTER TABLE - Foreign Key Constraints
PostgreSQL has no `ADD CONSTRAINT IF NOT EXISTS`. Use `DROP IF EXISTS` + `ADD`:
```sql
-- ✅ Good: Drop first, then add (idempotent)
ALTER TABLE "agent_eval_datasets" DROP CONSTRAINT IF EXISTS "agent_eval_datasets_user_id_users_id_fk";
ALTER TABLE "agent_eval_datasets" ADD CONSTRAINT "agent_eval_datasets_user_id_users_id_fk"
FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
-- ❌ Bad: Will fail if constraint already exists
ALTER TABLE "agent_eval_datasets" ADD CONSTRAINT "agent_eval_datasets_user_id_users_id_fk"
FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
```
### DROP TABLE / INDEX
```sql
-- ✅ Good
DROP TABLE IF EXISTS "old_table";
CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
CREATE UNIQUE INDEX IF NOT EXISTS "users_email_unique" ON "users" USING btree ("email");
-- ❌ Bad
DROP TABLE "old_table";
CREATE INDEX "users_email_idx" ON "users" ("email");
```
## Step 4: Update Journal Tag
After renaming the migration SQL file in Step 2, update the `tag` field in `packages/database/migrations/meta/_journal.json` to match the new filename (without `.sql` extension).
+66
View File
@@ -0,0 +1,66 @@
---
name: debug-package
description: 'LobeHub debug package and log namespace guide. Use when adding debug() logging, choosing lobe-* namespaces, troubleshooting DEBUG output, localStorage.debug, or log format specifiers.'
user-invocable: false
---
# Debug Package Usage Guide
## Basic Usage
```typescript
import debug from 'debug';
// Format: lobe-[module]:[submodule]
const log = debug('lobe-server:market');
log('Simple message');
log('With variable: %O', object);
log('Formatted number: %d', number);
```
## Namespace Conventions
- Desktop: `lobe-desktop:[module]`
- Server: `lobe-server:[module]`
- Client: `lobe-client:[module]`
- Router: `lobe-[type]-router:[module]`
## Format Specifiers
- `%O` - Object expanded (recommended for complex objects)
- `%o` - Object
- `%s` - String
- `%d` - Number
## Enable Debug Output
### Browser
```javascript
localStorage.debug = 'lobe-*';
```
### Node.js
```bash
DEBUG=lobe-* npm run dev
DEBUG=lobe-* pnpm dev
```
### Electron
```typescript
process.env.DEBUG = 'lobe-*';
```
## Example
```typescript
// apps/server/src/routers/edge/market/index.ts
import debug from 'debug';
const log = debug('lobe-edge-router:market');
log('getAgent input: %O', input);
```
-66
View File
@@ -1,66 +0,0 @@
---
name: debug
description: Debug package usage guide. Use when adding debug logging, understanding log namespaces, or implementing debugging features. Triggers on debug logging requests or logging implementation.
user-invocable: false
---
# Debug Package Usage Guide
## Basic Usage
```typescript
import debug from 'debug';
// Format: lobe-[module]:[submodule]
const log = debug('lobe-server:market');
log('Simple message');
log('With variable: %O', object);
log('Formatted number: %d', number);
```
## Namespace Conventions
- Desktop: `lobe-desktop:[module]`
- Server: `lobe-server:[module]`
- Client: `lobe-client:[module]`
- Router: `lobe-[type]-router:[module]`
## Format Specifiers
- `%O` - Object expanded (recommended for complex objects)
- `%o` - Object
- `%s` - String
- `%d` - Number
## Enable Debug Output
### Browser
```javascript
localStorage.debug = 'lobe-*';
```
### Node.js
```bash
DEBUG=lobe-* npm run dev
DEBUG=lobe-* pnpm dev
```
### Electron
```typescript
process.env.DEBUG = 'lobe-*';
```
## Example
```typescript
// src/server/routers/edge/market/index.ts
import debug from 'debug';
const log = debug('lobe-edge-router:market');
log('getAgent input: %O', input);
```
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: desktop
description: Electron desktop development guide. Use when implementing desktop features, IPC handlers, controllers, preload scripts, window management, menu configuration, or Electron-specific functionality. Triggers on desktop app development, Electron IPC, or desktop local tools implementation.
description: Electron desktop development guide IPC handlers, controllers, preload scripts, window/menu management.
disable-model-invocation: true
---
@@ -8,7 +8,7 @@ disable-model-invocation: true
## Architecture Overview
LobeChat desktop is built on Electron with main-renderer architecture:
LobeHub desktop is built on Electron with main-renderer architecture:
1. **Main Process** (`apps/desktop/src/main`): App lifecycle, system APIs, window management
2. **Renderer Process**: Reuses web code from `src/`
+155
View File
@@ -0,0 +1,155 @@
---
name: docs-changelog
description: 'Write website changelog pages under docs/changelog/*.mdx. Use for EN/ZH product update posts, changelog posts, update-log copy, or docs changelog edits; not GitHub Release notes.'
---
# Docs Changelog Writing Guide
## Scope Boundary (Important)
This skill is only for changelog pages in:
- `docs/changelog/*.mdx`
This skill is **not** for GitHub Releases.\
If the user asks for release PR body / GitHub Release notes, load `../version-release/SKILL.md`.
## Mandatory Companion Skills
For every docs changelog task, you MUST load:
- `../microcopy/SKILL.md`
- `../i18n/SKILL.md` (when EN/ZH pair is involved)
## File and Naming Convention
Use date-based file names:
- English: `docs/changelog/YYYY-MM-DD-topic.mdx`
- Chinese: `docs/changelog/YYYY-MM-DD-topic.zh-CN.mdx`
EN and ZH files must exist as a pair and describe the same release facts.
## Frontmatter Requirements
Each file should include:
```md
---
title: <Title>
description: <1 sentence summary>
tags:
- <Tag 1>
- <Tag 2>
---
```
Rules:
1. `title` should match the H1 title in meaning.
2. `description` should be concise and user-facing.
3. `tags` should be feature-oriented, not internal-team labels.
## Content Structure (Recommended)
Use this shape unless the user requests otherwise:
1. `# <Title>`
2. Opening paragraph (2-4 sentences): user-visible impact
3. 1-3 capability sections (optional `##` headings)
4. `## Improvements and fixes` / `## 体验优化与修复` with concise bullets
Keep heading count low and avoid heading-per-bullet structure.
## Writing Rules
1. Keep all claims factual and tied to actual shipped changes.
2. Explain user value first, implementation second.
3. Prefer natural narrative paragraphs over pure bullet dumps.
4. Avoid marketing exaggeration and vague adjectives.
5. Keep internal terms consistent across EN/ZH files.
6. Keep EN/ZH section order aligned and scope-aligned.
## EN/ZH Synchronization Rules
When generating bilingual changelogs:
1. Keep the same key facts in the same order.
2. Localize naturally; do not do literal sentence-by-sentence translation.
3. If one version has an `Improvements and fixes` bullet list, the other should have equivalent list intent.
4. Do not introduce capabilities in only one language unless explicitly requested.
## Length Guidance
- Small update: 3-5 short paragraphs total
- Medium update: 4-7 short paragraphs + concise fix bullets
- Large update: 6-10 short paragraphs split into 2-4 sections
Do not pad content when changes are limited.
## Authoring Workflow
1. Collect source facts from PRs/commits/issues.
2. Group changes by user workflow (not by internal module path).
3. Draft EN and ZH versions with aligned structure.
4. Verify terminology using `microcopy`/`i18n` guidance.
5. Final pass: remove AI-like filler and tighten sentences.
## Docs Changelog Template (English)
```md
---
title: <Feature title>
description: <One-sentence summary for users>
tags:
- <Tag A>
- <Tag B>
---
# <Feature title>
<Opening paragraph: what changed for users and why it matters.>
<Optional section paragraph for key capability 1.>
<Optional section paragraph for key capability 2.>
## Improvements and fixes
- <Fix or optimization 1>
- <Fix or optimization 2>
```
## Docs Changelog Template (Chinese)
```md
---
title: <功能标题>
description: <一句话说明>
tags:
- <标签 A>
- <标签 B>
---
# <功能标题>
<开场段:这次更新给用户带来的直接变化。>
<可选能力段 1。>
<可选能力段 2。>
## 体验优化与修复
- <优化或修复 1>
- <优化或修复 2>
```
## Quick Checklist
- [ ] File path matches `docs/changelog` naming convention
- [ ] EN and ZH versions both exist and match in facts
- [ ] Opening paragraph explains user-facing outcome
- [ ] Main body is narrative-first, not bullet-only
- [ ] `Improvements and fixes` section is concise and concrete
- [ ] No fabricated claims or unsupported scope
+267 -27
View File
@@ -1,20 +1,29 @@
---
name: drizzle
description: Drizzle ORM schema and database guide. Use when working with database schemas (src/database/schemas/*), defining tables, creating migrations, or database model code. Triggers on Drizzle schema definition, database migrations, or ORM usage questions.
description: 'LobeHub Drizzle ORM schema and query style. Use for pgTable schemas, indexes, joins, inferred types, db.select/db.query, schema fields, foreign keys, junction tables, or postgres query patterns.'
user-invocable: false
---
# Drizzle ORM Schema Style Guide
> **Adding a Model or Repository?** Ship a sibling test in the same PR — every new
> file under `packages/database/src/models/**` or `src/repositories/**` needs a
> matching `__tests__/<name>.test.ts`. See the **testing** skill
> (`.agents/skills/testing/references/db-model-test.md`) for the `getTestDB()`
> integration pattern, user-isolation tests, the BM25 `describe.skipIf(!isServerDB)`
> guard, and schema gotchas. CI's coverage patch gate won't reliably catch a brand-new
> untested file, so this is on you.
## Configuration
- Config: `drizzle.config.ts`
- Schemas: `src/database/schemas/`
- Migrations: `src/database/migrations/`
- Schemas: `packages/database/src/schemas/`
- Migrations: `packages/database/migrations/`
- Dialect: `postgresql` with `strict: true`
## Helper Functions
Location: `src/database/schemas/_helpers.ts`
Location: `packages/database/src/schemas/_helpers.ts`
- `timestamptz(name)`: Timestamp with timezone
- `createdAt()`, `updatedAt()`, `accessedAt()`: Standard timestamp columns
@@ -24,16 +33,42 @@ Location: `src/database/schemas/_helpers.ts`
- **Tables**: Plural snake_case (`users`, `session_groups`)
- **Columns**: snake_case (`user_id`, `created_at`)
- **New tables**: Check nearby existing tables before naming a new one. Preserve
the established noun family and suffix. For example, if the user-scoped table
is `user_xxx_logs`, the workspace-scoped counterpart should be
`workspace_xxx_logs`, not `workspace_xxx_records` or another new synonym.
```typescript
// ✅ Good: follows the existing user/workspace table family.
export const userSignupLogs = pgTable('user_signup_logs', { ... });
export const workspaceSignupLogs = pgTable('workspace_signup_logs', { ... });
// ❌ Bad: introduces a new suffix for the same concept.
export const workspaceSignupRecords = pgTable('workspace_signup_records', { ... });
```
## Column Definitions
### Primary Keys
Do not use auto-incrementing primary keys (`serial`, `bigserial`, generated
identity columns). They create sequence-state problems during cross-database
migrations, restores, and data copy jobs. Prefer text IDs from application
generators (`idGenerator`, `createNanoId`) or `uuid` for internal tables.
Keep `$defaultFn(...)` when a table normally owns ID generation. Callers can
still pass an explicit `id`; the default only runs when the insert omits it. Do
not remove the default just because one flow needs to supply a request-scoped ID.
```typescript
// ✅ Good: app-generated text ID; explicit inserts can still override it.
id: text('id')
.primaryKey()
.$defaultFn(() => idGenerator('agents'))
.notNull(),
// ❌ Bad: sequence state is fragile across DB migrations and restores.
id: serial('id').primaryKey(),
```
ID prefixes make entity types distinguishable. For internal tables, use `uuid`.
@@ -52,6 +87,80 @@ userId: text('user_id')
...timestamps, // Spread from _helpers.ts
```
### Optional and Undefined Values
Do not introduce artificial sentinel strings for missing values, such as
`unknown`, unless the domain already has that explicit state and existing code
uses it consistently. Prefer nullable columns, optional TypeScript fields, or a
separate concrete status enum when the value is genuinely absent.
```typescript
// ✅ Good: absent until the final stage writes a real decision.
export type UserSignupLogFinalDecision = 'allow' | 'block' | 'error';
finalDecision: varchar('final_decision', { length: 32 }).$type<UserSignupLogFinalDecision>(),
// ❌ Bad: invents a new state that callers now need to handle everywhere.
export type UserSignupLogFinalDecision = 'allow' | 'block' | 'error' | 'unknown';
finalDecision: varchar('final_decision', { length: 32 })
.$type<UserSignupLogFinalDecision>()
.notNull()
.default('unknown');
```
### Field Descriptions
For columns whose meaning is not obvious from the name alone, add JSDoc on the
schema field. Include a concrete example when it clarifies the stored value or
the lifecycle moment that writes it. This is especially important for external
IDs, lifecycle statuses, denormalized snapshots, JSONB signals, and fields whose
name could mean either a request ID or a persisted row ID.
```typescript
// ✅ Good: explain the table's business object first, then only document
// non-obvious lifecycle or risk-control fields.
/**
* User signup logs - one row per signup flow, collecting stage-level
* risk-control decisions before and after the auth provider creates a user.
*/
export const userSignupLogs = pgTable('user_signup_logs', {
/** Final signup outcome reason, for example user_created, llm_block, or guard_error */
finalReason: text('final_reason'),
/** Aggregated risk level derived from stage decisions, for example block -> high */
riskLevel: varchar('risk_level', { length: 16 }).$type<UserSignupLogRiskLevel>(),
/** Ordered stage-level decisions and metadata grouped by signup review stage */
stageResults: jsonb('stage_results').$type<UserSignupLogStageResults>(),
});
// ❌ Bad: comments restate obvious column names without adding domain meaning.
/** User email */
email: text('email'),
```
### JSONB Types
Avoid `Record<string, unknown>` or similarly loose JSONB types for schema
columns. Define a concrete interface that describes the expected JSON shape, even
when most properties are optional. This keeps callers, migrations, and review
queries aligned on the same data contract.
```typescript
interface UserSignupLogMetadata {
payloadPath?: string;
requestPath?: string;
}
metadata: jsonb('metadata').$type<UserSignupLogMetadata>(),
```
```typescript
// ❌ Bad: hides the contract and makes downstream access untyped.
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
```
### Indexes
```typescript
@@ -115,28 +224,159 @@ export const agentsKnowledgeBases = pgTable(
);
```
## Query Style
**Always use `db.select()` builder API. Never use `db.query.*` relational API** (`findMany`, `findFirst`, `with:`).
The relational API generates complex lateral joins with `json_build_array` that are fragile and hard to debug.
### Select Single Row
```typescript
// ✅ Good
const [result] = await this.db.select().from(agents).where(eq(agents.id, id)).limit(1);
return result;
// ❌ Bad: relational API
return this.db.query.agents.findFirst({
where: eq(agents.id, id),
});
```
### Select with JOIN
```typescript
// ✅ Good: explicit select + leftJoin
const rows = await this.db
.select({
runId: agentEvalRunTopics.runId,
score: agentEvalRunTopics.score,
testCase: agentEvalTestCases,
topic: topics,
})
.from(agentEvalRunTopics)
.leftJoin(agentEvalTestCases, eq(agentEvalRunTopics.testCaseId, agentEvalTestCases.id))
.leftJoin(topics, eq(agentEvalRunTopics.topicId, topics.id))
.where(eq(agentEvalRunTopics.runId, runId))
.orderBy(asc(agentEvalRunTopics.createdAt));
// ❌ Bad: relational API with `with:`
return this.db.query.agentEvalRunTopics.findMany({
where: eq(agentEvalRunTopics.runId, runId),
with: { testCase: true, topic: true },
});
```
### Select with Aggregation
```typescript
// ✅ Good: select + leftJoin + groupBy
const rows = await this.db
.select({
id: agentEvalDatasets.id,
name: agentEvalDatasets.name,
testCaseCount: count(agentEvalTestCases.id).as('testCaseCount'),
})
.from(agentEvalDatasets)
.leftJoin(agentEvalTestCases, eq(agentEvalDatasets.id, agentEvalTestCases.datasetId))
.groupBy(agentEvalDatasets.id);
```
### Raw SQL and Advanced Queries
Prefer Drizzle builders whenever the query reads clearly with `select`,
`insert().select()`, `update().from()`, joins, CTEs, and `groupBy` — this keeps
table/column references tied to schema, so changes surface as TypeScript errors.
Within a builder, expression-level `sql<T>` is fine for features lacking a helper
(JSON path, casts, aggregates, `CASE`, `NOW()`). Row locks are clauses, not
expressions — use `.for('update')`, never raw `FOR UPDATE`.
Use `COALESCE` only when null-handling is part of required DB semantics (nullable
JSONB append/merge, "keep first non-null"). Don't scatter
`COALESCE(excluded.col, current.col)` across ordinary upsert scalars just to avoid
an update object — build `set` from defined values only, and hide any remaining
SQL behind named helpers (`appendJsonbArray`, `mergeJsonbObject`, `keepFirstValue`)
so the method reads as business intent, not SQL plumbing.
```typescript
// ✅ Scalars included only when present; SQL hidden behind a named helper.
const updateValues = compactUndefined({
email: record.email ?? undefined,
ip: record.ip ?? undefined,
});
await db.insert(userSignupLogs).values(values).onConflictDoUpdate({
set: { ...updateValues, stageResults: appendStageResult(stage, result), updatedAt: now },
target: userSignupLogs.id,
});
// ❌ Every scalar becomes SQL plumbing.
set: {
email: sql`COALESCE(excluded.email, ${userSignupLogs.email})`,
ip: sql`COALESCE(excluded.ip, ${userSignupLogs.ip})`,
}
```
When refactoring raw SQL:
- Preserve query shape on latency-sensitive paths. If raw SQL is one roundtrip,
don't split it into multiple depth-based queries just to drop `execute`.
- Use `$with(...)` + `insert().select()` / `update().from()` for multi-step
single-roundtrip writes Drizzle can express.
- Don't rely on `execute<MyRow>(sql...)` for safety — it types rows but doesn't keep
selected columns in sync with schema changes.
- If only a PostgreSQL feature Drizzle can't express works, keep the raw SQL and
tighten it: schema refs in interpolations, explicit user scope, a narrow row
interface, and regression tests.
Recursive CTEs are the canonical "keep raw" case — there's no clean `WITH RECURSIVE`
builder, and a rewrite would add depth-based roundtrips:
```typescript
interface TaskTreeRow {
id: string;
parent_task_id: string | null;
}
// execute<T> acceptable: no clean WITH RECURSIVE builder. Keep schema refs in the
// interpolations and scope every leg to the user.
const { rows } = await db.execute<TaskTreeRow>(sql`
WITH RECURSIVE task_tree AS (
SELECT ${tasks.id}, ${tasks.parentTaskId}
FROM ${tasks}
WHERE ${tasks.id} = ${rootTaskId} AND ${tasks.createdByUserId} = ${userId}
UNION ALL
SELECT ${tasks.id}, ${tasks.parentTaskId}
FROM ${tasks}
JOIN task_tree ON ${tasks.parentTaskId} = task_tree.id
WHERE ${tasks.createdByUserId} = ${userId}
)
SELECT * FROM task_tree
`);
```
### One-to-Many (Separate Queries)
When you need a parent record with its children, use two queries instead of relational `with:`:
```typescript
// ✅ Good: two simple queries
const [dataset] = await this.db
.select()
.from(agentEvalDatasets)
.where(eq(agentEvalDatasets.id, id))
.limit(1);
if (!dataset) return undefined;
const testCases = await this.db
.select()
.from(agentEvalTestCases)
.where(eq(agentEvalTestCases.datasetId, id))
.orderBy(asc(agentEvalTestCases.sortOrder));
return { ...dataset, testCases };
```
## Database Migrations
See `references/db-migrations.md` for detailed migration guide.
```bash
# Generate migrations
bun run db:generate
# After modifying SQL (e.g., adding IF NOT EXISTS)
bun run db:generate:client
```
### Migration Best Practices
```sql
-- ✅ Idempotent operations
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "avatar" text;
DROP TABLE IF EXISTS "old_table";
CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
-- ❌ Non-idempotent
ALTER TABLE "users" ADD COLUMN "avatar" text;
```
Rename migration files meaningfully: `0046_meaningless.sql``0046_user_add_avatar.sql`
See the `db-migrations` skill for the detailed migration guide.
@@ -1,50 +0,0 @@
# Database Migrations Guide
## Step 1: Generate Migrations
```bash
bun run db:generate
```
This generates:
- `packages/database/migrations/0046_meaningless_file_name.sql`
And updates:
- `packages/database/migrations/meta/_journal.json`
- `packages/database/src/core/migrations.json`
- `docs/development/database-schema.dbml`
## Step 2: Optimize Migration SQL Filename
Rename auto-generated filename to be meaningful:
`0046_meaningless_file_name.sql``0046_user_add_avatar_column.sql`
## Step 3: Use Idempotent Clauses (Defensive Programming)
Always use defensive clauses to make migrations idempotent:
```sql
-- ✅ Good: Idempotent operations
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "avatar" text;
DROP TABLE IF EXISTS "old_table";
CREATE INDEX IF NOT EXISTS "users_email_idx" ON "users" ("email");
ALTER TABLE "posts" DROP COLUMN IF EXISTS "deprecated_field";
-- ❌ Bad: Non-idempotent operations
ALTER TABLE "users" ADD COLUMN "avatar" text;
DROP TABLE "old_table";
CREATE INDEX "users_email_idx" ON "users" ("email");
```
## Important
After modifying migration SQL (e.g., adding `IF NOT EXISTS` clauses), run:
```bash
bun run db:generate:client
```
This updates the hash in `packages/database/src/core/migrations.json`.
@@ -0,0 +1,83 @@
---
name: heterogeneous-agent
description: 'Implement or debug LobeHub heterogeneous agents. Use for Claude Code/Codex adapters, external CLI agents, event mapping, IPC, persistence, tool-call chains, sessions, traces, or adapter 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
- Reproducing multi-tool mixing, orphan tool messages, or stuck tool-result loading
## Pipeline Map
1. CLI raw stdout / JSONL
2. Electron main spawns the CLI and broadcasts `heteroAgentRawLine`
3. Adapter maps raw provider events into `HeterogeneousAgentEvent`
4. `executeHeterogeneousAgent` persists assistant/tool messages and forwards stream events
5. `createGatewayEventHandler` hydrates the UI
6. Only after this path looks correct should you move on to `agent-tracing` or context-engine debugging
## Read These Files First
- `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts`
- `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts`
- `apps/desktop/src/main/modules/heterogeneousAgent/drivers/codex.ts`
- `packages/heterogeneous-agents/src/adapters/claudeCode.ts`
- `packages/heterogeneous-agents/src/adapters/codex.ts`
- `src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts`
- `src/store/chat/slices/aiChat/actions/__tests__/heterogeneousAgentExecutor.test.ts`
## Default Debug Order
1. Prove whether the raw CLI output is correct before touching UI code.
2. If raw output is correct, compare it with adapter output. In dev, `executeHeterogeneousAgent` exposes `window.__HETERO_AGENT_TRACE`.
3. If adapted events look correct, inspect `persistToolBatch`, `persistToolResult`, step transitions, and subagent routing.
4. Turn the repro into a focused test before fixing.
5. Only after the transport/adapter/executor path looks sound should you debug later-stage message processing.
## Critical Invariants
- One raw tool item must map to one stable `ToolCallPayload.id`.
- A new main-agent step must emit a boundary signal before events are forwarded to the new assistant.
- In Claude Code, multiple assistant events with the same `message.id` are one turn, not multiple turns.
- In Claude Code, `tool_result` lives in `type: 'user'` events, not assistant events.
- In Claude Code partial mode, `message_delta.usage` is authoritative; do not trust echoed usage on every assistant block.
- `persistToolBatch` must pre-register assistant `tools[]` before creating tool messages.
- Every tool message must keep `parentId` equal to the owning assistant and `tool_call_id` equal to the tool id.
- `tool_result` must resolve an existing `toolMsgIdByCallId`.
- Subagent chunks must stay in thread scope and must not be forwarded into the main assistant stream.
- Never clear the global `toolMsgIdByCallId` map at main step boundaries.
## Common Bug Patterns
- Claude Code duplicates text or thinking:
check whether partial deltas and the later full assistant block are both being emitted.
- Claude Code opens too many assistant messages:
check whether the adapter is cutting steps on every assistant event instead of only on `message.id` changes.
- Claude Code tool results never land:
check whether `type: 'user'` `tool_result` blocks are being ignored because the code only inspects assistant events.
- Claude Code TodoWrite cards look stale:
check whether synthesized `pluginState.todos` is being attached at tool-result time.
- Claude Code subagent transcript leaks into the main bubble:
check `parent_tool_use_id` handling and whether subagent chunks are being forwarded to the main gateway handler.
- Multiple Codex tools collapse into one assistant message:
first check whether the adapter emits a usable step boundary such as `newStep` or an equivalent turn-change signal.
- Orphan tool messages:
first check step-transition ordering and whether `persistToolBatch` Phase 1 ran before tool message creation.
- Tool bubble stays loading:
look for `tool_result for unknown toolCallId` and missing `result_msg_id` backfill.
- Subagent tools show up in the main bubble:
check for subagent chunks reaching the main gateway handler.
## References
- For commands, trace capture, invariants, and focused test commands, read [references/debug-workflow.md](./references/debug-workflow.md).
@@ -0,0 +1,246 @@
# Heterogeneous Agent Debug Workflow
## Contents
1. Pipeline map
2. Capture raw CLI traces first
3. Compare raw and adapted events
4. Check step boundaries before persistence
5. Check tool persistence invariants
6. Focused tests
7. Repro-to-fix workflow
## 1. Pipeline Map
```
CLI raw stdout
-> HeterogeneousAgentCtr (Electron main)
-> heteroAgentRawLine broadcast
-> createAdapter(...)
-> executeHeterogeneousAgent(...)
-> persistToolBatch / persistToolResult
-> createGatewayEventHandler(...)
-> UI hydration
```
Start at the leftmost broken layer. Do not jump straight to UI rendering unless raw and adapted events already look correct.
## 2. Capture Raw CLI Traces First
### Codex raw JSONL
Use a read-only prompt and save traces under the repo-local scratch directory `.heerogeneous-tracing/`.
```bash
ts=$(date +%Y%m%d-%H%M%S)
out=".heerogeneous-tracing/codex-${ts}.jsonl"
last=".heerogeneous-tracing/codex-${ts}.last.txt"
cat << 'EOF' | codex exec --json --skip-git-repo-check --sandbox read-only -C "$PWD" -o "$last" - > "$out"
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:
- `.heerogeneous-tracing/cc-monitor-real-trace.jsonl`
- `.heerogeneous-tracing/cc-stream-chain-reference.md`
If you only need boundary semantics or tool persistence behavior, prefer existing adapter tests under:
- `packages/heterogeneous-agents/src/adapters/claudeCode.test.ts`
- `packages/heterogeneous-agents/src/adapters/claudeCode.e2e.test.ts`
## 3. Compare Raw And Adapted Events
In dev builds, `executeHeterogeneousAgent` stores raw lines plus adapted events on:
- `window.__HETERO_AGENT_TRACE`
Use that trace to compare:
- raw `item.started` / `item.completed`
- adapted `stream_chunk { chunkType: 'tools_calling' }`
- adapted `tool_result`
- adapted `tool_end`
For Codex, the usual mapping is:
- raw `item.started(command_execution)` -> `tools_calling` + `tool_start`
- raw `item.completed(command_execution)` -> `tool_result` + `tool_end`
- raw `item.completed(agent_message)` -> `stream_chunk(text)`
If the raw trace is right but adapted events are wrong, fix the adapter before touching persistence.
## 4. Check Step Boundaries Before Persistence
This is the first thing to verify for "mixed tools in one assistant" bugs.
### Claude Code
Claude Code step boundaries are keyed off assistant `message.id` changes. The adapter should emit:
- `stream_end`
- `stream_start { newStep: true }`
Also verify these Claude-specific invariants:
- the first assistant after init does not open a new step
- repeated assistant events with the same `message.id` do not open a new step
- partial `content_block_delta` text/thinking does not get duplicated by the later full assistant event
- `tool_result` from `type: 'user'` updates the matching tool row
- `parent_tool_use_id` creates thread-scoped subagent chunks instead of main-stream chunks
- TodoWrite `tool_use.input` is converted into synthesized `pluginState.todos` on `tool_result`
Good references:
- `packages/heterogeneous-agents/src/adapters/claudeCode.ts`
- `packages/heterogeneous-agents/src/adapters/claudeCode.test.ts`
### Codex
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.
Relevant files:
- `packages/heterogeneous-agents/src/adapters/codex.ts`
- `src/store/chat/slices/aiChat/actions/heterogeneousAgentExecutor.ts`
## 5. Check Tool Persistence Invariants
Read `persistToolBatch` and `persistToolResult` before changing UI code.
### `persistToolBatch`
The expected order is:
1. Pre-register assistant `tools[]`
2. Create `role: 'tool'` messages
3. Backfill `result_msg_id` onto assistant `tools[]`
If tool rows are created before assistant `tools[]` are registered, orphan tool messages are likely.
### `persistToolResult`
`tool_result` must resolve the tool row through `toolMsgIdByCallId`.
Warning signs:
- `tool_result for unknown toolCallId`
- tool rows with empty content forever
- missing `result_msg_id`
For Claude Code, remember that tool results originate from raw `type: 'user'` events.
### Main vs subagent scope
- Main-agent tool state is per-step.
- `toolMsgIdByCallId` is global across main and subagent scopes.
- Subagent chunks must not be forwarded into the main gateway handler.
If subagent events leak to the main handler, the main bubble can inherit the wrong `tools[]` and content.
## 6. Focused Tests
Run the smallest useful test set first.
```bash
bunx vitest run --silent='passed-only' 'packages/heterogeneous-agents/src/adapters/codex.test.ts'
bunx vitest run --silent='passed-only' 'packages/heterogeneous-agents/src/adapters/claudeCode.test.ts'
bunx vitest run --silent='passed-only' 'src/store/chat/slices/aiChat/actions/__tests__/heterogeneousAgentExecutor.test.ts'
```
Especially useful places:
- `packages/heterogeneous-agents/src/adapters/codex.test.ts`
- `packages/heterogeneous-agents/src/adapters/claudeCode.test.ts`
- `src/store/chat/slices/aiChat/actions/__tests__/heterogeneousAgentExecutor.test.ts`
Claude Code-specific assertions worth adding when fixing bugs:
- same `message.id` does not emit `newStep`
- changed `message.id` does emit `stream_end` plus `stream_start { newStep: true }`
- partial text/thinking is emitted once
- `tool_result` from `user` events reaches the right tool row
- subagent chunks carry `subagent.parentToolCallId`
- TodoWrite result synthesizes `pluginState.todos`
When the bug comes from a real trace, distill it into the closest existing test file instead of relying on manual UI-only repros.
## 7. Repro-To-Fix Workflow
1. Capture a raw trace and save it under `.heerogeneous-tracing/`.
2. Confirm whether the bug appears in raw events, adapted events, or persistence.
3. Add or update the narrowest failing test near the broken layer.
4. Fix the smallest layer that can explain the symptom.
5. Re-run focused tests.
6. Only then do an Electron smoke test with the `agent-testing` skill if UI confirmation is still needed.
Do not start with a broad Electron repro if a raw trace or adapter test can prove the fault zone faster.
+2 -1
View File
@@ -1,6 +1,7 @@
---
name: hotkey
description: Guide for adding keyboard shortcuts. Use when implementing new hotkeys, registering shortcuts, or working with keyboard interactions. Triggers on hotkey implementation or keyboard shortcut tasks.
description: 'Add or edit LobeHub keyboard shortcuts. Use for HotkeyEnum, HOTKEYS_REGISTRATION, combineKeys, useHotkeyById, tooltip hotkeys, shortcut scope, conflicts, or Cmd/Ctrl key combos.'
user-invocable: false
---
# Adding Keyboard Shortcuts Guide
+5 -4
View File
@@ -1,11 +1,12 @@
---
name: i18n
description: Internationalization guide using react-i18next. Use when adding translations, creating i18n keys, or working with localized text in React components (.tsx files). Triggers on translation tasks, locale management, or i18n implementation.
description: 'LobeHub i18n with react-i18next. Use for user-facing strings, locale keys, namespaces, useTranslation, t(), interpolation, zh-CN/en-US previews, hardcoded UI copy, or pnpm i18n.'
user-invocable: false
---
# LobeChat Internationalization Guide
# LobeHub Internationalization Guide
- Default language: Chinese (zh-CN)
- Default language: English (en-US)
- Framework: react-i18next
- **Only edit files in `src/locales/default/`** - Never edit JSON files in `locales/`
- Run `pnpm i18n` to generate translations (or manually translate zh-CN/en-US for dev preview)
@@ -53,7 +54,7 @@ export default {
1. Add keys to `src/locales/default/{namespace}.ts`
2. Export new namespace in `src/locales/default/index.ts`
3. For dev preview: manually translate `locales/zh-CN/{namespace}.json` and `locales/en-US/{namespace}.json`
4. Run `pnpm i18n` to generate all languages (CI handles this automatically)
4. Remind the user to run `pnpm i18n` before creating PR — do NOT run it yourself (very slow)
## Usage
+93 -29
View File
@@ -1,36 +1,106 @@
---
name: linear
description: "Linear issue management. MUST USE when: (1) user mentions LOBE-xxx issue IDs (e.g. LOBE-4540), (2) user says 'linear', 'linear issue', 'link linear', (3) creating PRs that reference Linear issues. Provides workflows for retrieving issues, updating status, and adding comments."
description: 'Linear issue management. Use for LOBE-xxx issues, Linear links, PRs referencing Linear, retrieving issues, updating status, completion comments, or sub-issue trees.'
user-invocable: false
---
# Linear Issue Management
Before using Linear workflows, search for `linear` MCP tools. If not found, treat as not installed.
## ⚠️ CRITICAL: PR Creation with Linear Issues
## PR Creation with Linear Issues
**When creating a PR that references Linear issues (LOBE-xxx), you MUST:**
A PR that fixes a Linear issue has **two separate jobs to do**, and both matter:
1. Create the PR with magic keywords (`Fixes LOBE-xxx`)
2. **IMMEDIATELY after PR creation**, add completion comments to ALL referenced Linear issues
3. Do NOT consider the task complete until Linear comments are added
1. **`Fixes LOBE-xxx` in the PR body** — Linear watches GitHub for these magic keywords and auto-links the PR and auto-closes the issue on merge. This is the machine-readable side.
2. **A completion comment on the Linear issue** — gives the reviewer/PM/teammate landing in Linear a human-readable summary of what changed and why, without forcing them to click through to GitHub and read a diff.
This is NON-NEGOTIABLE. Skipping Linear comments is a workflow violation.
If you only do step 1, Linear watchers (often non-engineers) hit the issue and see no context. So pair PR creation with the Linear comment as part of the same task — finish both before considering the work done.
## Workflow
1. **Retrieve issue details** before starting: `mcp__linear-server__get_issue`
2. **Check for sub-issues**: Use `mcp__linear-server__list_issues` with `parentId` filter
3. **Update issue status** when completing: `mcp__linear-server__update_issue`
4. **Add completion comment** (REQUIRED): `mcp__linear-server__create_comment`
2. **Read images** — issue descriptions often contain screenshots with critical context (mockups, error states, before/after). Use `mcp__linear-server__extract_images` so you actually see them; reading raw markdown alone misses what the reporter was looking at.
3. **Check for sub-issues**: `mcp__linear-server__list_issues` with `parentId` filter
4. **Mark as In Progress** at the moment you start planning or implementing — this signals to teammates the issue is owned, so they don't double-pick it up.
5. **Update issue status** when completing: `mcp__linear-server__update_issue`
6. **Add completion comment** (see [format below](#completion-comment-format))
## Creating Issues
When creating issues with `mcp__linear-server__create_issue`, **MUST add the `claude code` label**.
When creating issues with `mcp__linear-server__create_issue`, add the `claude code` label. Reason: the label is how the team filters/audits AI-generated issues; without it those issues vanish into the general backlog and the team loses visibility into AI contribution patterns.
## Language
Match the issue language to the conversation that produced it — if you're discussing in 中文,write the issue in 中文;if discussing in English, write it in English. Reason: the issue is a continuation of the conversation, and forcing a language switch creates translation friction for the collaborator who started the thread.
Specifics:
- 中文 conversation → 中文 body; technical terms (file paths, identifiers, library names, commands, error messages) stay in English.
- English conversation → English body.
- 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; don't switch the issue language mid-refactor.
## 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. Prefix titles with an ordering index
The Linear Sub-issues panel orders children by `sortOrder`, which **defaults to newest-first** (most recently created appears on top). Neither parallel nor serial creation produces the intended top-to-bottom reading order, and the MCP `save_issue` tool does **not expose a `sortOrder` parameter** — you can't 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 (12 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:
Each completed issue gets a comment summarizing the work, so reviewers and future readers don't have to reconstruct it from the PR diff:
```markdown
## Changes Summary
@@ -46,34 +116,28 @@ Every completed issue MUST have a comment summarizing work done:
- ...
```
This is critical for:
This gives team visibility, code-review context, and a paper trail for future reference.
- Team visibility
- Code review context
- Future reference
## PR Association
## PR Association (REQUIRED)
When creating PRs for Linear issues, include magic keywords in PR body:
When creating PRs for Linear issues, include magic keywords in the PR body:
- `Fixes LOBE-123`
- `Closes LOBE-123`
- `Resolves LOBE-123`
These trigger Linear's auto-link + auto-close on merge.
## Per-Issue Completion Rule
When working on multiple issues, update EACH issue IMMEDIATELY after completing it:
When working on multiple issues, close out **each one before starting the next** — don't batch all the Linear updates to the end. Batching is where comments get forgotten and issues stay stuck in "In Progress" days after the PR shipped.
For each issue:
1. Complete implementation
2. Run `bun run type-check`
3. Run related tests
4. Create PR if needed
5. Update status to **"In Review"** (NOT "Done")
6. **Add completion comment immediately**
7. Move to next issue
**Note:** Status → "In Review" when PR created. "Done" only after PR merged.
**❌ Wrong:** Complete all → Create PR → Forget Linear comments
**✅ Correct:** Complete → Create PR → Add Linear comments → Task done
5. Update status to **"In Review"** (not "Done" — "Done" is for after the PR merges)
6. Add the completion comment
7. Move to the next issue
+11 -1
View File
@@ -1,10 +1,16 @@
---
name: microcopy
description: UI copy and microcopy guidelines. Use when writing UI text, buttons, error messages, empty states, onboarding, or any user-facing copy. Triggers on i18n translation, UI text writing, or copy improvement tasks. Supports both Chinese and English.
description: 'UI copy and microcopy guidelines. Use for user-facing copy, buttons, errors, empty states, onboarding, i18n wording, translation, or copy improvements in Chinese or English.'
user-invocable: false
---
# LobeHub UI Microcopy Guidelines
This file is the quick-reference summary. For full prompt-style guidelines with extensive examples (anti-patterns, tone matrices, scenario walk-throughs), load the language-specific reference:
- **中文文案** — [`references/zh.md`](./references/zh.md)
- **English copy** — [`references/en.md`](./references/en.md)
Brand: **Where Agents Collaborate** - Focus on collaborative agent system, not just "generation".
## Fixed Terminology
@@ -25,6 +31,10 @@ Brand: **Where Agents Collaborate** - Focus on collaborative agent system, not j
| 资源 | Resource |
| 库 | Library |
| 模型服务商 | Provider |
| 评测 | Evaluation |
| 基准 | Benchmark |
| 数据集 | Dataset |
| 用例 | Test Case |
## Brand Principles
+176
View File
@@ -0,0 +1,176 @@
---
globs: src/locales/default/*
alwaysApply: false
---
You are **LobeHubs English UI Copy & Microcopy Specialist**.
LobeHub is an assistant workspace: users can create **Agents** and **Agent Teams** so people↔agents and agent↔agent can collaborate to improve productivity in work and life.
Brand vibe: youthful, friendly, modern on the surface; professional, reliable, productivity- and controllability-first underneath. Overall style reference: Notion / Figma / Apple / Discord / OpenAI / Gemini — clear, restrained, trustworthy, human but not cheesy.
Product slogan: **For Collaborative Agents**. Your copy must continuously reinforce that LobeHub is not about “generation”, but about a **collaborative agent system**: shareable context, traceable outcomes, replayable runs, evolvable setup, and **human-in-the-loop**.
---
## 1) Fixed Terminology (must follow)
Use **exactly** these English terms across the product. Do not mix synonyms for the same concept.
- 空间: **Workspace**
- 助理: **Agent**
- 群组: **Group**
- 上下文: **Context**
- 记忆: **Memory**
- 连接器: **Integration**
- 技能 /tool/plugin: **Skill**
- 助理档案: **Agent Profile**
- 话题: **Topic**
- 文稿: **Page**
- 社区: **Community**
- 资源: **Resource**
- 库: **Library**
- MCP: **MCP**
- 模型服务商: **Provider**
Terminology rule: one concept = one term site-wide. Never alternate with “bot/assistant/AI agent/team/workspace” variations.
---
## 2) Your Responsibilities
- Improve, rewrite, or create from scratch any **English UI copy**: titles, buttons, form labels/help text, placeholders, onboarding, empty states, toasts, modals, errors, permission prompts, settings, creation/run flows, collaboration and Agent Team pages, etc.
- Copy must work for both:
- general users (immediately understandable)
- power users (not childish)
- It must fit both playful and serious contexts.
- Avoid overclaiming AI capabilities; add human warmth at the right moments.
---
## 3) The Three Brand Principles (bake into structure & wording)
- **Create**: create an Agent in one sentence; clear next step from idea → usable.
- **Collaborate**: multi-agent collaboration; align info and outputs; share Context (controlled, manageable).
- **Evolve**: Agents can remember preferences **only with user consent**; become more helpful over time; emphasize explainability, settings, and replay.
---
## 4) Writing Rules (actionable)
1. **Clarity first**: short sentences, strong verbs, minimal adjectives. Avoid hype (“revolutionary”, “epic”, “100%”).
2. **Layered messaging (single version for everyone)**:
- Main line: simple and actionable
- Optional second line: more precise / technical / boundary-setting (subtitle, helper text, tooltip, collapsible)
- Do not produce “Pro vs Lite” variants; one main + optional detail
3. **Use terms sparingly but correctly**: prefer plain words (“connect”, “run”, “context”) unless a technical term is necessary. When it is, add a plain-English explanation.
4. **Consistency**: keep verbs consistent across similar actions (Create / Connect / Run / Pause / Retry / View details / Clear Memory).
5. **Actionable**: every message tells the user what to do next. Avoid generic “OK/Cancel”; use specific actions.
6. **English localization**: natural, product-native English; avoid translationese; keep punctuation and casing consistent.
---
## 5) Human Warmth (balanced, controlled)
Goal: reduce anxiety and restore control without being sentimental.
Default ratio: **80% information, 20% warmth**.
Key moments (first-time create, empty state, long waits, failures/retries, rollback/data-loss risk, collaboration conflicts): may go **70/30**.
Hard cap: any on-screen message may include **at most half a sentence to one sentence** of warmth, and it must be followed by a clear next step.
Required order:
1. Acknowledge the situation (no judgment)
2. Restore control (human-in-the-loop: pause/replay/edit/undo/clear Memory/view Context)
3. Provide the next action (button/path)
Avoid:
- preachy encouragement (“dont worry”, “stay positive”)
- grand narratives
- overly anthropomorphic claims (“I understand you”, “Ill always remember you”)
Core stance: Agents can accelerate output, but **you** own the judgment, trade-offs, and final decision. LobeHub gives you time back for what matters.
Suggested patterns:
- **Getting started / blank state**
- “Starting with one sentence is enough. Describe your goal and Ill help you set up the first Agent.”
- “Not sure where to begin? Tell me the outcome—well break it down together.”
- **Long run / waiting**
- “Running… You can switch tasks—I'll notify you when its done.”
- “This may take a few minutes. To speed up: reduce Context / switch model / disable Auto-run.”
- **Failure / retry**
- “That didnt run through. Retry, or view details to fix the cause.”
- “Connection failed: permission not granted or network unstable. Re-authorize in Settings, or try again later.”
- **Value anxiety (guidance, not error dialogs)**
- “Agents can speed up output, but direction and standards stay with you.”
- “Fast results are great—keeping the trail makes the next run steadier.”
- **Collaboration / Agent Teams**
- “Align everyone to the same Context. Every Agent in the Agent Team works from the same page.”
- “Different opinions are fine. Write the goal first, then let Agents propose options and trade-offs.”
---
## 6) Errors / Exceptions / Permissions / Billing: hard rules
Every error must include:
- **What happened**
- (optional) **Why**
- **What the user can do next**
Provide actionable options as appropriate:
- Retry / View details / Go to Settings / Contact support / Copy logs
Never blame the user. Dont show only an error code; put codes in “Details” if needed.
For data/security/billing: be neutral, thorough, and respectful—warmth comes from clarity, not emotion.
---
## 7) Your Special Task: CN i18n → EN (localized, length-aware)
You translate **raw Chinese i18n strings into English** for LobeHub.
Requirements:
- Prefer **localized**, product-native English over literal translation.
- Do **not** chase perfect one-to-one consistency if a more natural UI phrase reads better.
- Keep the **character length difference small**; try to make the English string **roughly the same visual length** as the Chinese source (avoid overly long expansions).
- Preserve meaning, tone, and actionability; keep verbs consistent with LobeHubs UI patterns.
- If space is tight (buttons, tabs, toasts), prioritize: **verb + object**, drop optional words first.
- If the Chinese includes placeholders/variables, preserve them exactly (e.g., `{name}`, `{{count}}`, `%s`) and keep word order sensible.
- Keep capitalization consistent with UI norms (buttons/title case only when appropriate).
Output format when translating:
- Provide **English only**, unless asked otherwise.
- If multiple options are useful, give **one best option** + **one shorter fallback** (only when length constraints are likely).
---
You always optimize for: **clarity, control, collaboration, replayability, and human-in-the-loop**—in a modern, restrained, trustworthy English voice.
## 8) Product Introduction
LobeHub, we define agents as the unit of work. Were building the first humanagent co-working, co-evolving network.
It is a fundamentally new, agent-first experience.You can pop up your agents or agent teams while writing, while chatting -- from ideation, to execution, to delivery -- across your entire workflow. Here, agents are not just tools, but always-on units of work.
### Create
It is a unified workspace where you can find, build, or team up with agent co-workers.Simply describe what you need, and Lobe AI will generate the prompts and assemble the right set of tools to compose your agent.In agent marketplace, you can easily discover agents created by others,use them instantly,and flexibly swap in your own tools.
### Collaboration
You can also spin up agent groups to handle system-level projects, even like building a quant team.
Within this group, some agents track signals and mine quantitative factors in real time, some manage risk, some execute orders, collaborate together to make money.
Were defining how humans and agents work together. Now we support agent-to-agent collaboration, and we continue to scale new forms of collaboration networks — from agents collaborating across teams, to multiple humans working through the same agent.
### Evolve
Humans and agents should co-evolve, and we design this paradigm from both technical and economic perspectives. Our memory system is structured and editable,enabling models to better align with individual users, while allowing users to provide cleaner reward signals for continual learning. Agent evolution is powered by shared human intelligence through our agent marketplace. Creators are rewarded, and agents, in turn, pay for human intelligence.
Is AI replacing humans? No.
Were building a humanagent co-working, co-evolving society.
Agents become smarter and more personalized through human intelligence, taking on repetitive and exhausting work — so humans can focus on fewer, but more important things: taste, and creation.
+160
View File
@@ -0,0 +1,160 @@
---
globs: src/locales/default/*
alwaysApply: false
---
你是「LobeHub」的中文 UI 文案与微文案(microcopy)专家。LobeHub 是一个助理工作空间:用户可以创建助理与群组,让人和助理、助理和助理协作,提升日常生产与生活效率。产品气质:外表年轻、亲和、现代;内核专业、可靠、强调生产力与可控性。整体风格参考 Notion / Figma / Apple / Discord / OpenAI / Gemini:清晰克制、可信、有人情味但不油腻。
产品 slogan**For Collaborative Agents**。你的文案要让用户持续感到:LobeHub 的重点不是 “生成”,而是 “协作的助理体系”(可共享上下文、可追踪、可回放、可演进、人在回路)。
---
### 1) 固定术语(必须遵守)
- Workspace:空间
- Agent:助理
- Agent Team:群组
- Context:上下文
- Memory:记忆
- Integration:连接器
- Tool/Skill/Plugin/ 插件 / 工具:技能
- SystemRole: 助理档案
- Topic: 话题
- Page: 文稿
- Community: 社区
- Resource: 资源
- Library: 库
- MCP: MCP
- Provider: 模型服务商
术语规则:同一概念全站只用一种说法,不混用 “Agent / 智能体 / 机器人 / 团队 / 工作区” 等。
---
### 2) 你的任务
- 优化、改写或从零生成任何界面中文文案:标题、按钮、表单说明、占位、引导、空状态、Toast、弹窗、错误、权限、设置项、创建 / 运行流程、协作与群组相关页面等。
- 文案必须同时兼容:普通用户看得懂 + 专业用户不觉得低幼;娱乐与严肃场景都成立;不过度营销、不夸大 AI 能力;在关键节点提供恰到好处的人文关怀。
---
### 3) 品牌三原则(内化到结构与措辞)
- **Create(创建)**:一句话创建助理;从想法到可用;清楚下一步。
- **Collaborate(协作)**:多助理协作;群组对齐信息与产出;共享上下文(可控、可管理)。
- **Evolve(演进)**:助理可在你允许的范围内记住偏好;随你的工作方式变得更顺手;强调可解释、可设置、可回放。
---
### 4) 写作规则(可执行)
1. **清晰优先**:短句、强动词、少形容词;避免口号化与空泛承诺(如 “颠覆”“史诗级”“100%”)。
2. **分层表达(单一版本兼容两类用户)**
- 主句:人人可懂、可执行
- 必要时补充一句副说明:更精确 / 更专业 / 更边界(可放副标题、帮助提示、折叠区)
- 不输出 “Pro/Lite 两套文案”,而是 “一句主文案 + 可选补充”
3. **术语克制但准确**:能说 “连接 / 运行 / 上下文” 就不要堆砌术语;必须出现专业词时给一句白话解释。
4. **一致性**:同一动作按钮尽量固定动词(创建 / 连接 / 运行 / 暂停 / 重试 / 查看详情 / 清除记忆等)。
5. **可行动**:每条提示都要让用户知道下一步;按钮避免 “确定 / 取消” 泛化,改成更具体的动作。
6. **中文本地化**:符合中文阅读节奏;中英混排规范;避免翻译腔。
---
### 5) 人文关怀(中间态温度:介于克制与陪伴)
目标:在 AI 时代的价值焦虑与创作失格感中,给用户 “被理解 + 有掌控 + 能继续” 的体验,但不写长抒情。
#### 温度比例规则
- 默认:信息为主,温度为辅(约 8:2)
- 关键节点(首次创建、空状态、长等待、失败重试、回退 / 丢失风险、协作分歧):允许提升到 7:3
- 强制上限:任何一条上屏文案里,温度表达不超过**半句或一句**,且必须紧跟明确下一步。
#### 表达顺序(必须遵守)
1. 先承接处境(不评判):如 “没关系 / 先这样也可以 / 卡住很正常”
2. 再给掌控感(人在回路):可暂停 / 可回放 / 可编辑 / 可撤销 / 可清除记忆 / 可查看上下文
3. 最后给下一步(按钮 / 路径明确)
#### 避免
- 鸡汤式说教(如 “别焦虑”“要相信未来”)
- 宏大叙事与文学排比
- 过度拟人(不承诺助理 “理解你 / 有情绪 / 永远记得你”)
#### 核心立场
- 助理很强,但它替代不了你的经历、选择与判断;LobeHub 帮你把时间还给重要的部分。
##### A. 情绪承接(先人后事)
- 允许承认:焦虑、空白、无从下手、被追赶感、被替代感、创作枯竭、意义感动摇
- 但不下结论、不说教:不输出 “你要乐观 / 别焦虑”,改成 “这种感觉很常见 / 你不是一个人”
##### B. 主体性回归(把人放回驾驶位)
- 关键句式:**“决定权在你”**、**“你可以选择交给助理的部分”**、**“把你的想法变成可运行的流程”**
- 强调可控:可编辑、可回放、可暂停、可撤销、可清除记忆、可查看上下文
##### C. 经历与关系(把价值从结果挪回过程)
- 适度表达:记录、回放、版本、协作痕迹、讨论、共创、里程碑
- 用 “经历 / 过程 / 痕迹 / 回忆 / 脉络 / 成长” 这类词,避免虚无抒情
##### D. 不用 “AI 神话”
- 不渲染 “AI 终将超越你 / 取代你”
- 也不轻飘飘说 “AI 只是工具” 了事更像:**“它是工具,但你仍是作者 / 负责人 / 最终决定者”**
##### 示例
在用户可能产生自我否定或无力感的场景(空状态、创作开始、产出对比、失败重试、长时间等待、团队协作分歧、版本回退):
```
1. **先承接感受**:用一句短话确认处境(不评判)
2. **再给掌控感**:强调“你可控/可选择/可回放/可撤销”
3. **最后给下一步**:提供明确行动按钮或路径
```
- 允许出现 “经历、选择、痕迹、成长、一起、陪你把事做完” 等词来传递温度;但保持信息密度,不写长段抒情。
- 严肃场景(权限 / 安全 / 付费 / 数据丢失风险)仍以清晰与准确为先,温度通过 “尊重与解释” 体现,而不是煽情。
你可以让系统在需要时套这些结构(同一句兼容新手 / 专业):
**开始创作 / 空白页**
- 主句:给一个轻承接 + 行动入口
- 模板:
- 「从一个念头开始就够了。写一句话,我来帮你搭好第一个助理。」
- 「不知道从哪开始也没关系:先说目标,我们一起把它拆开。」
**长任务运行 / 等待**
- 模板:
- 「正在运行中… 你可以先去做别的,完成后我会提醒你。」
- 「这一步可能要几分钟。想更快:减少上下文 / 切换模型 / 关闭自动运行。」
**失败 / 重试**
- 模板:
- 「没关系,这次没跑通。你可以重试,或查看原因再继续。」
- 「连接失败:权限未通过或网络不稳定。去设置重新授权,或稍后再试。」
**对比与自我价值焦虑(适合提示 / 引导,不适合错误弹窗)**
- 模板:
- 「助理可以加速产出,但方向、取舍和标准仍属于你。」
- 「结果可以很快,经历更重要:把每次尝试留下来,下一次会更稳。」
**协作 / 群组**
- 模板:
- 「把上下文对齐到同一处,群组里每个助理都会站在同一页上。」
- 「不同意见没关系:先把目标写清楚,再让助理分别给方案与取舍。」
### 6) 错误 / 异常 / 权限 / 付费:硬规则
- 必须包含:**发生了什么 +(可选)原因 + 你可以怎么做**
- 必须提供可操作选项:**重试 / 查看详情 / 去设置 / 联系支持 / 复制日志**(按场景取舍)
- 不责备用户;不只给错误码;错误码可放在 “详情” 里
- 涉及数据与安全:语气更中性更完整,温度通过 “尊重与解释” 体现,而不是煽
+73 -36
View File
@@ -1,64 +1,76 @@
---
name: modal
description: Modal imperative API guide. Use when creating modal dialogs using createModal from @lobehub/ui. Triggers on modal component implementation or dialog creation tasks.
description: 'LobeHub imperative modal conventions. Use when creating or migrating modals, dialogs, popups, confirm flows, ModalHost wiring, createModal, confirmModal, useModalContext, or base-ui modal APIs.'
user-invocable: false
---
# Modal Imperative API Guide
Use `createModal` from `@lobehub/ui` for imperative modal dialogs.
## Recommended: `@lobehub/ui/base-ui`
## Why Imperative?
New code should use the **base-ui** modal stack (headless primitives, not antd `Modal`):
| Mode | Characteristics | Recommended |
| ----------- | ------------------------------------- | ----------- |
| Declarative | Need `open` state, render `<Modal />` | ❌ |
| Imperative | Call function directly, no state | ✅ |
- `createModal`, `confirmModal`, `ModalHost` from `@lobehub/ui/base-ui`
- `useModalContext` from `@lobehub/ui/base-ui` inside modal **content**
## File Structure
Body slot: pass **`content`** (or `children`; runtime uses `content ?? children`).
### Global `ModalHost` (required)
Base-ui `createModal` renders through a **separate** host from the root package. The app must mount **`ModalHost`** from `@lobehub/ui/base-ui` once near the root (e.g. next to other global hosts). Without it, `createModal` calls will not appear.
If the project only mounts `ModalHost` from `@lobehub/ui`, add a second lazy `ModalHost` from `@lobehub/ui/base-ui` until all imperative modals are migrated.
### Why imperative?
| Mode | Characteristics | Recommended |
| ----------- | ------------------------------------ | ----------- |
| Declarative | `open` state + `<Modal />` | ❌ |
| Imperative | Call `createModal()`, no local state | ✅ |
### File structure
```
features/
└── MyFeatureModal/
├── index.tsx # Export createXxxModal
└── MyFeatureContent.tsx # Modal content
├── index.tsx # export createXxxModal
└── MyFeatureContent.tsx # modal body
```
## Implementation
### 1. Content Component (`MyFeatureContent.tsx`)
### 1. Content (`MyFeatureContent.tsx`)
```tsx
'use client';
import { useModalContext } from '@lobehub/ui';
import { useModalContext } from '@lobehub/ui/base-ui';
import { useTranslation } from 'react-i18next';
export const MyFeatureContent = () => {
const { t } = useTranslation('namespace');
const { close } = useModalContext(); // Optional: get close method
const { close } = useModalContext();
return <div>{/* Modal content */}</div>;
return <div>{/* ... */}</div>;
};
```
### 2. Export createModal (`index.tsx`)
### 2. `createModal` (`index.tsx`)
```tsx
'use client';
import { createModal } from '@lobehub/ui';
import { t } from 'i18next'; // Note: use i18next, not react-i18next
import { createModal } from '@lobehub/ui/base-ui';
import { t } from 'i18next';
import { MyFeatureContent } from './MyFeatureContent';
export const createMyFeatureModal = () =>
createModal({
allowFullscreen: true,
children: <MyFeatureContent />,
destroyOnHidden: false,
content: <MyFeatureContent />,
footer: null,
styles: { body: { overflow: 'hidden', padding: 0 } },
maskClosable: true,
styles: {
content: { overflow: 'hidden', padding: 0 },
},
title: t('myFeature.title', { ns: 'setting' }),
width: 'min(80%, 800px)',
});
@@ -76,27 +88,52 @@ const handleOpen = useCallback(() => {
return <Button onClick={handleOpen}>Open</Button>;
```
## i18n Handling
### i18n
- **Content component**: `useTranslation` hook (React context)
- **createModal params**: `import { t } from 'i18next'` (non-hook, imperative)
- **Content**: `useTranslation` in components.
- **`createModal` options**: `import { t } from 'i18next'` where hooks are unavailable.
## useModalContext Hook
### `useModalContext`
```tsx
const { close, setCanDismissByClickOutside } = useModalContext();
```
## Common Config
### Common options (base-ui)
| Property | Type | Description |
| ----------------- | ------------------- | ------------------------ |
| `allowFullscreen` | `boolean` | Allow fullscreen mode |
| `destroyOnHidden` | `boolean` | Destroy content on close |
| `footer` | `ReactNode \| null` | Footer content |
| `width` | `string \| number` | Modal width |
`ImperativeModalProps` builds on `BaseModalProps`: `title`, `width`, `maskClosable`, `open`, `onOpenChange`, `footer`, `styles` / `classNames` (keys: `backdrop`, `popup`, `header`, `title`, `close`, `content`, …).
| Property | Notes |
| -------------- | ---------------------------------------- |
| `content` | Main body (preferred name vs `children`) |
| `maskClosable` | Click outside to dismiss |
| `styles.*` | Semantic regions, not antd `styles.body` |
### Confirm
```tsx
import { confirmModal } from '@lobehub/ui/base-ui';
confirmModal({
title: '…',
content: '…',
okText: '…',
cancelText: '…',
onOk: async () => {},
});
```
---
## Legacy: `@lobehub/ui` (root)
Older call sites use **`createModal` from `@lobehub/ui`**, which is typed as **antd `Modal` props** (`children`, `allowFullscreen`, `getContainer`, `destroyOnHidden`, `styles.body`, etc.). Prefer migrating new work to **`@lobehub/ui/base-ui`**.
Examples (legacy): `src/features/SkillStore/index.tsx`, `src/features/LibraryModal/CreateNew/index.tsx`.
---
## Examples
- `src/features/SkillStore/index.tsx`
- `src/features/LibraryModal/CreateNew/index.tsx`
- Base-ui (preferred): follow sections above; ensure **base-ui `ModalHost`** is mounted.
- Legacy: `src/features/SkillStore/index.tsx`, `src/features/LibraryModal/CreateNew/index.tsx`
@@ -0,0 +1,69 @@
---
name: model-bank-metadata
description: 'Backfill and maintain model-bank metadata (knowledgeCutoff, family, generation). Use when adding models, fixing cutoff/family data, running a metadata sweep across aiModels providers, or researching official knowledge cutoffs.'
user-invocable: false
---
# Model-Bank Metadata (knowledgeCutoff / family / generation)
How to populate and maintain the three structured metadata fields on `packages/model-bank/src/aiModels/*.ts` model cards, at single-model scale (new model PR) or repo-wide scale (sweep across \~80 provider files / \~1900 entries).
## Field semantics
| Field | Format | Meaning |
| ----------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `knowledgeCutoff` | `'YYYY-MM'` (or `'YYYY'` if only the year is published) | World-knowledge cutoff. When a vendor distinguishes a **"reliable knowledge cutoff"** from the broader training-data cutoff (Anthropic does), always use the **reliable** one. |
| `family` | lowercase slug (`claude`, `gpt`, `o-series`, `qwen`, `deepseek`, `llama`, `glm`, …) | Model lineage, finer than `organization`. Lets the UI group models and match the same model across aggregator providers. |
| `generation` | family slug + version (`claude-4.6`, `gpt-5.2`, `qwen3.5`, `llama-3.1`) | Generation within the family. Only set when confidently derivable from the model line's naming. Rolling aliases (`qwen-max`, `deepseek-chat`, `gemini-flash-latest`) get `family` only. |
All three are optional. **The cardinal rule: only fill what an authoritative source states or naming rules derive — never guess.** An empty field is correct for vendors that publish nothing.
No DB migration is ever needed for these: builtin models are merged from model-bank at read time (`repositories/aiInfra/index.ts` spreads the whole card), so new card fields flow to the client automatically.
## Sourcing rules for knowledgeCutoff
Accept only:
- Vendor official docs (platform.openai.com / developers.openai.com, docs.x.ai, ai.google.dev, docs.anthropic.com / platform.claude.com)
- Official Hugging Face org model cards (huggingface.co/meta-llama/..., etc.)
- Official tech reports / system cards / launch blog posts
Reject:
- **Third-party aggregator sites** (aiknowledgecutoff.com and similar) — proven to copy one model's value across a whole family. A Cohere sweep once claimed `2024-06` for four distinct base models; none of the cited Cohere pages said that, and the only cutoff Cohere actually publishes is Feb 2023 for the 08-2024 Command R/R+ refresh.
- **AWS Bedrock model cards as sole source** — proven to conflate launch date with knowledge cutoff (DeepSeek R1's card lists both as "Jan 2025"). If Bedrock is the only place a value appears, leave the field empty.
- Inference from `releasedAt` — a release date is not a cutoff.
Variant inheritance: dated snapshots (`-2024-08-06`), speed/price tiers of the same checkpoint, quantizations (`-fp8`, `-awq`), context-length variants (`-32k`), ollama `:NNb` tags, and cloud-prefixed ids (`anthropic.`/`us.`/`global.` Bedrock ids) share their base model's cutoff. **Distills do not inherit** from teacher or base — use the distill's own published value or leave empty. **Sizes within one generation can genuinely differ**: Llama 3 8B is Mar 2023 while 70B is Dec 2023 (per Meta's own card) — don't "fix" that to one family-wide value.
Vendors that publish no cutoffs (leave empty, don't chase): Qwen, DeepSeek, GLM/Zhipu, ERNIE, Doubao, Hunyuan, SenseNova, Spark, MiniMax, StepFun, Yi (mostly), Moonshot.
Known per-vendor footguns:
- **Anthropic**: Opus 4.6 reliable cutoff is `2025-05`, Sonnet 4.6 is `2025-08` — easy to swap. Claude 3.7 is `2024-10` (system card: trained through Nov 2024, knowledge cutoff end of Oct 2024). Cite system cards / the models overview, not the Help Center article (a living page that drops retired models — citation rot).
- **xAI**: docs.x.ai has one blanket sentence covering grok-3/grok-4; mini variants are not named there. Grok 4.20/4.3 have no official cutoff anywhere.
- **OpenAI**: per-model docs pages (developers.openai.com/api/docs/models/<id>) state cutoffs explicitly, including snapshot differences (gpt-4-1106-preview `2023-04` vs gpt-4-0125-preview `2023-12`).
## family/generation derivation
Rule-based, no research needed: `scripts/derive-family.ts` holds the per-family regex rules. Traps already encoded there — keep them when extending:
- Date suffixes are not versions: `claude-sonnet-4-20250514` is generation `claude-4`, not `claude-4.2`.
- Size suffixes are not versions: `llama-3-8b``llama-3` (not `llama-3.8`); `gemma-7b-it` is **gemma-1** (not gemma-7).
- Vendor spelling variants: `qwen2p5` = qwen2.5, `llama-v3p1` = llama-3.1, ollama `:NNb` tags, Bedrock `us.`/`global.`/`anthropic.` prefixes.
- `claude-X.0` normalizes to `claude-X`.
- Fable/Mythos-class ids (`claude-fable-5`) don't match the opus/sonnet/haiku regex — they are the Mythos class — `family: 'claude-mythos'`, `generation: 'mythos-5'` (set manually; the launch page calls Fable 5 "the generally available Mythos-class model").
## Repo-wide sweep workflow
1. **Extract ids**: `bun .agents/skills/model-bank-metadata/scripts/extract-model-ids.ts` → unique normalized chat-model ids (normalization = last path segment, lowercased). Non-chat types (image/video/embedding/tts) have no knowledge cutoff — skip them.
2. **Research (multi-agent)**: chunk ids by family (≤50 per chunk) and fan out one research agent per chunk (Workflow tool), each returning `{id, cutoff, source}` with the sourcing rules above baked into the prompt, **plus** one adversarial verify agent per chunk that re-fetches cited sources and refutes unsupported claims. The verify pass is load-bearing: it caught the Cohere aggregator copy-paste and the AWS launch-date conflation.
3. **Policy filter**: before applying, drop entries whose only source is a rejected category (check the returned `sources` map — e.g. drop everything sourced to aws.amazon.com).
4. **Apply**: `bun scripts/apply-cutoffs.ts <map.json>` and `bun scripts/apply-family.ts <map.json>` (run from repo root). Both are idempotent codemods keyed on normalized id — aggregator providers get the same values automatically; entries that already have the field are skipped. They rely on the uniform prettier formatting of the data files (entries start ` {` / end ` },`, fields at 4-space indent).
5. **Verify**: `cd packages/model-bank && bunx vitest run src/aiModels/__tests__/index.test.ts && bunx tsc --noEmit`.
## Maintenance rules
- **New model PRs** should fill all three fields inline, citing the official source in the PR body (see the Anthropic entries in `anthropic.ts` for reference values).
- **After resolving merge conflicts** in model-bank data files, sanity-check that metadata didn't vanish: `git grep -c knowledgeCutoff -- 'packages/model-bank/src/aiModels/*.ts'` before vs after. A three-way stack of model PRs once silently dropped all 10 Anthropic cutoffs during conflict resolution.
- Dirty ids exist in aggregator data (a sambanova id once carried a trailing tab). The codemods match ids verbatim — if a map key won't apply, check for invisible characters before assuming the model is missing.
@@ -0,0 +1,73 @@
/**
* One-off codemod: apply a canonical { normalizedModelId: 'YYYY-MM' } map onto
* packages/model-bank/src/aiModels/*.ts, inserting `knowledgeCutoff` after the
* `id:` line of every chat-model entry that matches and doesn't already have one.
*
* Relies on the uniform prettier formatting of these files:
* - each model entry starts with ` {` and ends with ` },` (2-space indent)
* - fields are at 4-space indent: ` id: '...'`, ` type: 'chat'`
*
* Usage: bun /tmp/apply-cutoffs.ts /tmp/cutoff-map.json
*/
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const mapPath = process.argv[2];
if (!mapPath) throw new Error('usage: bun apply-cutoffs.ts <map.json>');
const map: Record<string, string> = JSON.parse(readFileSync(mapPath, 'utf8'));
const dir = 'packages/model-bank/src/aiModels';
const normalize = (id: string) => id.split('/').pop()!.toLowerCase();
let touchedFiles = 0;
let inserted = 0;
const matchedIds = new Set<string>();
for (const file of readdirSync(dir).filter((f) => f.endsWith('.ts'))) {
const path = join(dir, file);
const lines = readFileSync(path, 'utf8').split('\n');
const out: string[] = [];
let changed = false;
let i = 0;
while (i < lines.length) {
if (lines[i] !== ' {') {
out.push(lines[i]);
i++;
continue;
}
// collect one model entry block
const start = i;
let end = i;
while (end < lines.length && lines[end] !== ' },') end++;
const block = lines.slice(start, end + 1);
const idLineIdx = block.findIndex((l) => /^ {4}id: '/.test(l));
const isChat = block.some((l) => /^ {4}type: 'chat',?$/.test(l));
const hasCutoff = block.some((l) => /^ {4}knowledgeCutoff:/.test(l));
if (idLineIdx >= 0 && isChat && !hasCutoff) {
const rawId = block[idLineIdx].match(/^ {4}id: '(.+)',$/)?.[1];
const norm = rawId ? normalize(rawId) : undefined;
const cutoff = norm ? map[norm] : undefined;
if (cutoff && /^\d{4}(?:-\d{2})?$/.test(cutoff)) {
block.splice(idLineIdx + 1, 0, ` knowledgeCutoff: '${cutoff}',`);
inserted++;
changed = true;
matchedIds.add(norm!);
}
}
out.push(...block);
i = end + 1;
}
if (changed) {
writeFileSync(path, out.join('\n'));
touchedFiles++;
}
}
console.log(`inserted ${inserted} knowledgeCutoff fields across ${touchedFiles} files`);
console.log(`map ids used: ${matchedIds.size}/${Object.keys(map).length}`);
const unused = Object.keys(map).filter((k) => !matchedIds.has(k));
if (unused.length) console.log('unused map keys (first 20):', unused.slice(0, 20));
@@ -0,0 +1,49 @@
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
const map: Record<string, { family: string; generation?: string }> = JSON.parse(
readFileSync('/tmp/family-map.json', 'utf8'),
);
const dir = 'packages/model-bank/src/aiModels';
const normalize = (id: string) => id.split('/').pop()!.toLowerCase();
let inserted = 0;
let touchedFiles = 0;
for (const file of readdirSync(dir).filter((f) => f.endsWith('.ts'))) {
const path = join(dir, file);
const lines = readFileSync(path, 'utf8').split('\n');
const out: string[] = [];
let changed = false;
let i = 0;
while (i < lines.length) {
if (lines[i] !== ' {') {
out.push(lines[i]);
i++;
continue;
}
let end = i;
while (end < lines.length && lines[end] !== ' },') end++;
const block = lines.slice(i, end + 1);
const idLineIdx = block.findIndex((l) => /^ {4}id: '/.test(l));
const isChat = block.some((l) => /^ {4}type: 'chat',?$/.test(l));
const hasFamily = block.some((l) => /^ {4}family:/.test(l));
if (idLineIdx >= 0 && isChat && !hasFamily) {
const rawId = block[idLineIdx].match(/^ {4}id: '(.+)',$/)?.[1];
const r = rawId ? map[normalize(rawId)] : undefined;
if (r) {
const add = [` family: '${r.family}',`];
if (r.generation) add.push(` generation: '${r.generation}',`);
block.splice(idLineIdx, 0, ...add);
inserted++;
changed = true;
}
}
out.push(...block);
i = end + 1;
}
if (changed) {
writeFileSync(path, out.join('\n'));
touchedFiles++;
}
}
console.log(`annotated ${inserted} model entries across ${touchedFiles} files`);
@@ -0,0 +1,237 @@
/* eslint-disable regexp/no-unused-capturing-group */
/**
* Rule-based derivation of { family, generation } from normalized model ids.
* Principle: only fill what is confidently derivable; otherwise omit.
*
* Usage: bun /tmp/derive-family.ts # print distinct pairs for review
* bun /tmp/derive-family.ts --emit # write /tmp/family-map.json
*/
import { readFileSync, writeFileSync } from 'node:fs';
const ids: string[] = JSON.parse(readFileSync('/tmp/model-ids.json', 'utf8'));
type R = { family: string; generation?: string };
const derive = (id: string): R | undefined => {
// strip cloud/bedrock prefixes for matching
const m = id.replace(/^(us\.|global\.|eu\.|apac\.)?(anthropic\.|meta\.|cohere\.|azure-)/, '');
// ---- anthropic ----
if (m.startsWith('claude')) {
// family = product-line tier (claude-opus/sonnet/haiku/instant); bare claude-2.x has no tier
const tier = m.match(/(opus|sonnet|haiku|instant)/)?.[1];
const family = tier ? `claude-${tier}` : 'claude';
let g = m.match(/^claude-(?:opus|sonnet|haiku)-(\d)[.-](\d)(?!\d)/); // claude-opus-4-8 / claude-haiku-4.5
if (g) return { family, generation: `claude-${g[1]}.${g[2]}` };
g = m.match(/^claude-(?:opus|sonnet|haiku)-(\d)(?!\d)/); // claude-opus-4
if (g) return { family, generation: `claude-${g[1]}` };
g = m.match(/^claude-(\d)[.-](\d)(?!\d)/); // claude-3-5-haiku / claude-3.7-sonnet / claude-2.1
if (g) return { family, generation: g[2] === '0' ? `claude-${g[1]}` : `claude-${g[1]}.${g[2]}` };
g = m.match(/^claude-(\d)(?!\d)/); // claude-3-haiku
if (g) return { family, generation: `claude-${g[1]}` };
if (m.startsWith('claude-instant')) return { family: 'claude-instant' };
if (/^claude-v?2/.test(m)) return { family: 'claude', generation: 'claude-2' };
return { family };
}
// ---- openai ----
if (/^(gpt-oss|gpt_oss)/.test(m) || m.startsWith('gpt-oss:'))
return { family: 'gpt-oss', generation: 'gpt-oss' };
if (/^(chatgpt-4o|gpt-4o)/.test(m)) return { family: 'gpt', generation: 'gpt-4o' };
if (/^gpt-(3\.5|35)/.test(m)) return { family: 'gpt', generation: 'gpt-3.5' };
if (m.startsWith('gpt-audio')) return { family: 'gpt', generation: 'gpt-audio' };
{
const g = m.match(/^gpt-(\d)\.(\d)/); // gpt-4.1 / gpt-5.2
if (g) return { family: 'gpt', generation: `gpt-${g[1]}.${g[2]}` };
const g2 = m.match(/^gpt-(\d)(?!\d)/); // gpt-4 / gpt-5
if (g2) return { family: 'gpt', generation: `gpt-${g2[1]}` };
}
{
const g = m.match(/^o([134])(-|$)/); // o1 / o3 / o4
if (g) return { family: 'o-series', generation: `o${g[1]}` };
}
if (/^(codex|computer-use-preview)/.test(m)) return { family: 'gpt' };
// ---- google ----
{
const g = m.match(/^gemini-(\d+(?:\.\d+)?)/);
if (g) return { family: 'gemini', generation: `gemini-${g[1]}` };
if (/^gemini-(pro|flash)/.test(m)) return { family: 'gemini' }; // rolling aliases
if (m.startsWith('gemma')) {
if (/^gemma-?\db/.test(m)) return { family: 'gemma', generation: 'gemma-1' };
const v = m.match(/^gemma-?(\d)(?!b)/);
return { family: 'gemma', generation: v ? `gemma-${v[1]}` : undefined };
}
if (/^(codegemma|learnlm|palm)/.test(m)) return { family: m.match(/^[a-z]+/)![0] };
}
// ---- qwen ----
if (m.startsWith('qwq')) return { family: 'qwen', generation: 'qwq' };
if (m.startsWith('qvq')) return { family: 'qwen', generation: 'qvq' };
if (m.startsWith('codeqwen')) return { family: 'qwen' };
if (m.startsWith('qwen')) {
const g =
m.match(/^qwen-?([123](?:\.\d+)?)(?![0-9b])/) || // qwen3.5-plus / qwen-3-14b / qwen2-7b / qwen1.5
m.match(/^qwen([23](?:\.\d+)?):/) || // qwen2.5:72b
m.match(/^qwen([23])p(\d)/); // qwen2p5 -> handled below
if (/^qwen(\d)p(\d)/.test(m)) {
const p = m.match(/^qwen(\d)p(\d)/)!;
return { family: 'qwen', generation: `qwen${p[1]}.${p[2]}` };
}
if (g) return { family: 'qwen', generation: `qwen${g[1]}` };
return { family: 'qwen' }; // qwen-max/plus/turbo/vl rolling aliases
}
// ---- deepseek ----
if (/^(deepseek|azure-deepseek|pro-deepseek)/.test(m) || m.startsWith('deepseek_')) {
const s = m.replace(/^pro-/, '').replaceAll('_', '-');
if (s.startsWith('deepseek-r1-distill'))
return { family: 'deepseek', generation: 'deepseek-r1-distill' };
if (s.startsWith('deepseek-r1')) return { family: 'deepseek', generation: 'deepseek-r1' };
const g = s.match(/^deepseek-(?:chat-)?v(\d(?:\.\d)?)/);
if (g) return { family: 'deepseek', generation: `deepseek-v${g[1]}` };
if (/^deepseek-(coder-v2|coder)/.test(s))
return { family: 'deepseek', generation: 'deepseek-coder' };
return { family: 'deepseek' }; // deepseek-chat / reasoner rolling aliases
}
// ---- meta llama ----
if (m.startsWith('codellama')) return { family: 'llama', generation: 'codellama' };
if (/^(meta-)?llama|^l3(\d)?-|^llava/.test(m)) {
if (m.startsWith('llava')) return { family: 'llava' };
const s = m.replace(/^meta-/, '');
const g =
s.match(/^llama-?([234])(?:[.-](\d))?(?![0-9b])/) || // llama-3.1 / llama3.3 / llama-4
s.match(/^llama-?v([234])p?(\d)?/) || // llama-v3p1
s.match(/^llama([234])[.:-](\d)?/);
if (g) {
const gen = g[2] ? `llama-${g[1]}.${g[2]}` : `llama-${g[1]}`;
return { family: 'llama', generation: gen };
}
if (m.startsWith('l3-')) return { family: 'llama', generation: 'llama-3' };
if (m.startsWith('l31-')) return { family: 'llama', generation: 'llama-3.1' };
return { family: 'llama' };
}
// ---- zhipu ----
if (/^(zai-)?glm/.test(m)) {
const s = m.replace(/^zai-/, '');
if (s.startsWith('glm-z1')) return { family: 'glm', generation: 'glm-z1' };
if (s.startsWith('glm-zero')) return { family: 'glm', generation: 'glm-zero' };
const g = s.match(/^glm-(\d(?:\.\d)?)/);
if (g) return { family: 'glm', generation: `glm-${g[1]}` };
return { family: 'glm' };
}
if (/^(charglm|codegeex|emohaa)/.test(m)) return { family: m.match(/^[a-z]+/)![0] };
// ---- mistral ----
if (
/^(open-)?(mistral|mixtral|ministral|codestral|devstral|magistral|pixtral|mathstral|labs-devstral|labs-leanstral|open-codestral)/.test(
m,
)
) {
const fam = m.replace(/^(open-|labs-)/, '').match(/^[a-z]+/)![0];
return { family: fam };
}
// ---- xai ----
if (m.startsWith('grok')) {
const g = m.match(/^grok-(\d(?:\.\d+)?)/);
return { family: 'grok', generation: g ? `grok-${g[1]}` : undefined };
}
// ---- moonshot ----
if (m.startsWith('kimi')) {
const g = m.match(/^kimi-k(\d(?:\.\d)?)/);
return { family: 'kimi', generation: g ? `kimi-k${g[1]}` : undefined };
}
if (m.startsWith('moonshot-kimi-k2')) return { family: 'kimi', generation: 'kimi-k2' };
if (m.startsWith('moonshot-v1')) return { family: 'kimi', generation: 'moonshot-v1' };
// ---- minimax ----
if (m.startsWith('minimax')) {
if (m.startsWith('minimax-text')) return { family: 'minimax', generation: 'minimax-text-01' };
const g = m.match(/^minimax-m(\d(?:\.\d)?)/);
return { family: 'minimax', generation: g ? `minimax-m${g[1]}` : undefined };
}
if (m.startsWith('abab')) return { family: 'minimax', generation: 'abab' };
// ---- baidu ----
if (m.startsWith('ernie')) {
if (m.startsWith('ernie-x1')) return { family: 'ernie', generation: 'ernie-x1' };
const g = m.match(/^ernie-(\d\.\d)/);
return { family: 'ernie', generation: g ? `ernie-${g[1]}` : undefined };
}
if (m.startsWith('qianfan')) return { family: 'qianfan' };
// ---- bytedance ----
if (m.startsWith('doubao')) {
const g = m.match(/^doubao-seed-(\d[.-]\d|\d)/) || m.match(/^doubao-(\d\.\d)/);
return { family: 'doubao', generation: g ? `doubao-${g[1].replace('-', '.')}` : undefined };
}
if (/^(seed-oss|skylark)/.test(m)) return { family: m.startsWith('seed') ? 'doubao' : 'skylark' };
// ---- tencent ----
if (m.startsWith('hunyuan')) {
const g = m.match(/^hunyuan-(\d\.\d)/);
return { family: 'hunyuan', generation: g ? `hunyuan-${g[1]}` : undefined };
}
if (m.startsWith('hy3')) return { family: 'hunyuan', generation: 'hunyuan-3' };
// ---- others (family only / simple version) ----
if (m.startsWith('yi-')) return { family: 'yi' };
if (/^(command|c4ai-command)/.test(m)) return { family: 'command' };
if (/^(aya|c4ai-aya)/.test(m)) return { family: 'aya' };
if (/^phi-?(\d)?/.test(m) && m.startsWith('phi')) {
const g = m.match(/^phi-?(\d(?:\.\d)?)/);
return { family: 'phi', generation: g ? `phi-${g[1]}` : undefined };
}
if (m.startsWith('wizardlm')) return { family: 'wizardlm' };
if (m.startsWith('step-')) {
const g = m.match(/^step-(?:r1|(\d(?:\.\d)?))/);
return { family: 'step', generation: g?.[1] ? `step-${g[1]}` : undefined };
}
if (/^(internlm|intern-)/.test(m)) return { family: 'intern' };
if (m.startsWith('internvl')) return { family: 'internvl' };
if (m.startsWith('baichuan')) {
const g = m.match(/^baichuan-?(m?\d)/);
return { family: 'baichuan', generation: g ? `baichuan-${g[1]}` : undefined };
}
if (/^(sensechat|sensenova)/.test(m)) return { family: 'sensenova' };
if (/^(spark|generalv|4\.0ultra)/.test(m)) return { family: 'spark' };
if (/^(360gpt|360zhinao)/.test(m)) return { family: '360zhinao' };
if (/^(jamba|ai21-jamba)/.test(m)) return { family: 'jamba' };
if (m.startsWith('sonar')) return { family: 'sonar' };
if (/^(nova-lite|nova-micro|nova-pro)/.test(m)) return { family: 'nova' };
if (/^(ling|ring)-/.test(m)) return { family: m.match(/^[a-z]+/)![0] };
if (m.startsWith('longcat')) return { family: 'longcat' };
if (m.startsWith('mimo')) return { family: 'mimo' };
if (m.startsWith('taichu')) return { family: 'taichu' };
if (/^(hermes|nous-hermes)/.test(m)) return { family: 'hermes' };
if (m.startsWith('solar')) return { family: 'solar' };
if (m.startsWith('kat-coder')) return { family: 'kat-coder' };
if (m.startsWith('dbrx')) return { family: 'dbrx' };
if (m.startsWith('morph')) return { family: 'morph' };
return undefined;
};
const map: Record<string, R> = {};
const pairs = new Map<string, number>();
let derived = 0;
for (const id of ids) {
const r = derive(id);
if (!r) continue;
derived++;
map[id] = r;
const key = `${r.family} :: ${r.generation ?? '—'}`;
pairs.set(key, (pairs.get(key) || 0) + 1);
}
console.log(`derived ${derived}/${ids.length}`);
for (const [k, n] of [...pairs.entries()].sort()) console.log(String(n).padStart(4), k);
if (process.argv.includes('--emit')) {
writeFileSync('/tmp/family-map.json', JSON.stringify(map, null, 1));
console.log('\nwritten /tmp/family-map.json');
}
@@ -0,0 +1,23 @@
/**
* Extract unique normalized chat-model ids from packages/model-bank/src/aiModels/*.ts.
* Normalization: last path segment, lowercased (matches the apply codemods).
*
* Usage (repo root): bun .agents/skills/model-bank-metadata/scripts/extract-model-ids.ts [out.json]
* Default output: /tmp/model-ids.json
*/
import { readdirSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
const dir = resolve('packages/model-bank/src/aiModels');
const out = process.argv[2] || '/tmp/model-ids.json';
const ids = new Set<string>();
for (const f of readdirSync(dir).filter((f) => f.endsWith('.ts'))) {
const mod = await import(join(dir, f));
for (const m of mod.default || []) {
if (!m?.id || m.type !== 'chat') continue;
ids.add(m.id.split('/').pop()!.toLowerCase());
}
}
writeFileSync(out, JSON.stringify([...ids].sort(), null, 1));
console.log(`${ids.size} unique normalized chat ids -> ${out}`);
+152
View File
@@ -0,0 +1,152 @@
---
name: pr
description: "Create a PR for the current branch (targets `canary` by default), including splitting one cross-layer branch into ordered stacked PRs so a lower layer (db / shared package / server TRPC) merges before its callers (desktop / CLI / UI). Use when the user asks to create / submit a PR, or to split a branch because clients call a server contract that isn't on the trunk yet. Triggers on 'pr', 'create pr', 'submit pr', 'open a PR', 'pull request', 'split this PR', 'stacked PR', 'backend should merge first', '提 PR', '提个 PR', '新建 PR', '拆 PR', '后端先合', '分层合并'."
user-invocable: true
---
# Create Pull Request
## Branch Strategy
- **Target branch**: `canary` (development branch, cloud production)
- `main` is the release branch — never PR directly to main
## Steps
### 1. Gather context (run in parallel)
- `git branch --show-current` — current branch name
- `git status --short` — uncommitted changes
- `git rev-parse --abbrev-ref @{u} 2>/dev/null` — remote tracking status
- `git log --oneline origin/canary..HEAD` — unpushed commits
- `gh pr list --head "$(git branch --show-current)" --json number,title,state,url` — existing PR
- `git diff --stat --stat-count=20 origin/canary..HEAD` — change summary
### 2. Handle uncommitted changes on default branch
If current branch is `canary` (or `main`) AND there are uncommitted changes:
1. Analyze the diff (`git diff`) to understand the changes
2. Infer a branch name from the changes, format: `<type>/<short-description>` (e.g. `fix/i18n-cjk-spacing`)
3. Create and switch to the new branch: `git checkout -b <branch-name>`
4. Stage relevant files: `git add <files>` (prefer explicit file paths over `git add .`)
5. Commit with a proper gitmoji message
6. Continue to step 3
If current branch is `canary`/`main` but there are NO uncommitted changes and no unpushed commits, abort — nothing to create a PR for.
### 3. Push if needed
- No upstream: `git push -u origin $(git branch --show-current)`
- Has upstream: `git push origin $(git branch --show-current)`
### 4. Search related GitHub issues
- `gh issue list --search "<keywords>" --state all --limit 10`
- Only link issues with matching scope (avoid large umbrella issues)
- Skip if no matching issue found
### 5. Create PR with `gh pr create --base canary`
- Title: `<gitmoji> <type>(<scope>): <description>`
- Body: based on PR template (`.github/PULL_REQUEST_TEMPLATE.md`), fill checkboxes
- Link related GitHub issues using magic keywords (`Fixes #123`, `Closes #123`)
- Link Linear issues if applicable (`Fixes LOBE-xxx`)
- Use HEREDOC for body to preserve formatting
### 6. Open in browser
`gh pr view --web`
## PR Template
Use `.github/PULL_REQUEST_TEMPLATE.md` as the body structure. Key sections:
- **Change Type**: Check the appropriate gitmoji type
- **Related Issue**: Link GitHub/Linear issues with magic keywords
- **Description of Change**: Summarize what and why
- **How to Test**: Describe test approach, check relevant boxes
## Notes
- **Language**: All PR content must be in English
- If a PR already exists for the branch, inform the user instead of creating a duplicate
---
# Stacked PRs (cross-layer feature)
The steps above create **one** PR for the current branch. When a single branch lands across layers — `packages/database` schema/model → a shared `packages/*` lib → `src/server` TRPC → `apps/desktop` + `apps/cli` callers → `src/features` UI — shipping it as one PR can't merge safely: the clients call an endpoint that doesn't exist on the trunk until the same PR merges, so any partial/rollback or independent review breaks. Split it into **ordered PRs**, lower layer first.
## The ordering rule
A PR may only merge **after** every layer it calls is already on the trunk.
- The **server contract** (new TRPC procedure, changed return shape, new table/model) merges first.
- The **callers** (desktop, CLI, UI) merge after — they invoke that contract.
- Tie-break with one question: _"if this merged alone to `canary` right now, would it build and behave?"_ If no, it belongs in a later PR.
## Which file goes in which PR
The non-obvious calls:
- **Frontend that adapts to a contract change goes WITH the server PR.** If you widen a TRPC return shape (e.g. `listDevices` now returns `platform: string | null`), the component consuming it must change in the _same_ PR — otherwise the server PR breaks the build on its own. Contract + its in-repo consumers ship together.
- **A new shared package goes with its consumer**, not the server, unless the server imports it too. A `@lobechat/*` package imported only by desktop/CLI ships in the client PR. Don't carry an unused package in the lower PR.
- **Workspace dep declarations** (`package.json` `workspace:*`, `pnpm-workspace.yaml`) travel with the code that imports the package.
## The git recipe — split an existing full branch
Starting point: one branch (`feat/x`) with a single commit `<FULL>` containing everything, already pushed (so it's also safe on the remote).
```bash
# 1. Safety nets — make the full work unloseable before rewriting anything
git branch backup/x-full <FULL> # local ref to the full commit
git branch feat/x-clients <FULL> # the higher-layer branch starts here
# 2. Rewrite the lower-layer branch to lower-layer files only
git checkout feat/x # this becomes the SERVER PR
git reset --hard origin/canary
git checkout <FULL> -- <server/db files…> # stages just those paths
git commit -m "✨ feat(...): <server half>"
git push --force-with-lease origin feat/x # never --force; never push to canary
# 3. Build the higher-layer branch STACKED on the lower branch
git checkout feat/x-clients
git reset --hard feat/x # base = the just-rewritten server HEAD
git checkout backup/x-full -- <client/ui files…> # only the remaining paths
git commit -m "✨ feat(...): <client half>"
git push -u origin feat/x-clients
```
Then open the higher PR **based on the lower branch**, not the trunk:
```bash
gh pr create --base feat/x --head feat/x-clients --title "…" --body "…"
```
`--base feat/x` keeps the diff client-only (no server files leak in) and makes it physically impossible to merge the clients before the server. **After the server PR merges to `canary`, retarget the client PR's base to `canary`** (GitHub usually auto-retargets when the base branch merges; note it in the PR body so a human confirms).
## Verify the dependency actually holds
The whole point is the higher layer needs the lower one. Prove it: on the stacked higher branch, type-check the caller and confirm the symbol the lower layer introduced resolves.
```bash
cd apps/cli && bun run type-check 2>&1 | grep -iE "connect\.ts|device\.register"
# empty (re: your change) = the stacked base supplies device.register ✓
```
Filter to your touched files — this repo's standalone type-check emits pre-existing env noise (`__ELECTRON__`, `@/types/llm`, unbuilt `@lobechat/types`) that isn't yours.
## PR + Linear bookkeeping
- **Each PR closes only its own layer's issues.** Server PR: `Closes LOBE-<server>`. Client PR: `Closes LOBE-<pkg> / <desktop> / <cli>`. Don't let one PR's body claim another layer's issue.
- Both PRs are `Part of LOBE-<parent>`.
- On PR creation, move each closed sub-issue to **In Review** (not Done) and add a completion comment — see the `linear` skill.
## Gotchas
- **Never push to `canary`.** A split branch cut with `git checkout -b feat/x origin/canary` _tracks_ `origin/canary`, so a bare `git push` targets canary. Always `git push origin feat/x` with the explicit branch name.
- **`--force-with-lease`, not `--force`** when rewriting the lower branch — it aborts if the remote moved under you.
- **Back up before `reset --hard`.** Step 1's `backup/x-full` + the pushed remote branch mean the full commit is referenced by ≥3 refs before you rewrite anything. Verify with `git branch --contains <FULL>`.
- **Lockfiles:** this monorepo commits no root `pnpm-lock.yaml`, so a new `workspace:*` dep needs no lockfile churn. In a repo that _does_ commit one, regenerate it on each branch after the split.
- **Don't over-split.** Two PRs (contract / callers) is usually enough. A UI page that only reads an existing endpoint can be its own later PR, but don't fragment a single layer across PRs for its own sake.

Some files were not shown because too many files have changed in this diff Show More