- fix REPO: lobehub/lobe-chat -> lobehub/lobehub (P1)
- add lobe-linux-arm64 and lobe-macos-x64 matrix targets so all
OS/arch combinations advertised by install.sh are actually built
and uploaded to the release (P2)
- Add .github/workflows/release-cli.yml: builds standalone binaries via
bun build --compile on ubuntu/macos runners and uploads to GitHub Release
- Add apps/cli/install.sh: POSIX-compatible curl installer that detects
OS/arch, installs to /usr/local/bin (or ~/.local/bin fallback), and
creates lobe + lobehub symlinks pointing to lh
* 🐛 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>
* 💄 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>
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>
* 🐛 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>
* 🐛 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>
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>
* ✨ 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>
* ✨ 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>
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.
* ✨ 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>
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>
* ✨ 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>
* ✨ 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.
* ✨ 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>
* 💄 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>
* 🐛 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
* ✨ 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>
* ✨ 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>
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>
* 📝 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>
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>
* 🐛 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>
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>
* 🗃️ 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>
✨ 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>
* 🎨 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>
* ♻️ 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.
🗃️ 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>
* ✨ 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>
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>
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>
* ✨ 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>
* ✨ 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>
✨ 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>
* ✨ 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>
🐛 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.
* ✨ 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>
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>
* ✨ 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>
* ✨ 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>
* ✨ 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>
* ✨ 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>
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>
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>
* ✨ 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>
* ✨ 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>
* 🐛 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
* ✨ 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
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>
* 🗑️ 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.
🔨 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>
* 🗃️ 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>
* ✨ 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>
* 🗃️ 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>
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>
* 🗃️ 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>
* ♻️ 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>
* 🗃️ 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>
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>
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>
# 🚀 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
* ✨ 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).
* 🐛 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>
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>
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>
* ✨ 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>
🚸 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.
* ✨ 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>
* 🐛 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
* 💄 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>
* ⚡️ 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>
* 🚧 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>
- 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
* ✨ 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>
* ♻️ 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.
* ✨ 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>
* 🐛 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>
- 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.
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>
✨ 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>
* ♻️ 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
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>
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.
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
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
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.
* ♻️ 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>
* ✨ 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>
* 🐛 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>
* 🐛 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>
* 🐛 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>
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>
* ✨ 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>
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>
🐛 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>
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>
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>
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).
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>
✨ 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>
* 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
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>
* ✨ 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>
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>
* 🐛 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>
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>
* ✨ 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>
✨ 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>
* 💄 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>
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>
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
* ✨ 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.
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`.
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.
* ✨ 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>
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.
✨ 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>
🐛 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>
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>
* 🐛 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.
`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.
* ✨ 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
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>
✨ 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>
* ✨ 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
* 🔨 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>
♻️ 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>
* ✨ 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.
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>
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.
* 🐛 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
* 🐛 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>
* ♻️ 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>
* ✨ 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>
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>
✨ 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>
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>
* ♻️ 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
* ✨ 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>
* 🐛 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.
* ✨ 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>
* 💄 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).
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>
* 💄 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.
* ✨ 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>
* 🐛 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>
* 🐛 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.
Run DynamicMetaRunner for every tab via TabCacheBridges so background
tabs receive auto-named topic titles instead of staying on "Default Topic".
Fixes LOBE-9492
* ✨ 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.
* 🐛 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>
* 🐛 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>
* 💄 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>
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).
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.
* 🐛 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>
- 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>
* ✨ 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>
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>
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`.
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.
* ✨ 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>
🐛 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>
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.
🐛 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>
* 🐛 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>
🐛 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
`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>
* ✨ 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>
* 🧹 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>
* ✨ 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>
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.
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>
* 🐛 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>
* 🐛 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>
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>
* ♻️ 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>
* 🐛 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>
* 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>
* 🧪 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>
* 🐛 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>
* 🐛 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>
* ✨ 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>
* 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>
* ✨ 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
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.
* ✨ 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
🔨 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>
* 💄 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>
* 🐛 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>
* ✨ 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>
* 🐛 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>
* ♻️ 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>
* ♻️ 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.
* 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>
* ✨ 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>
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>
* ♻️ 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>
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>
`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.
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
* 🐛 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>
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>
* ✨ 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>
* 📝 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.
* 💄 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.
* ♻️ 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>
✨ 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
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.
* ♻️ 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>
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`.
🐛 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>
* ♻️ 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
* ♻️ 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>
* 🐛 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
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
- 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)
- 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
* 🐛 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
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>
🐛 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>
* 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
* 🔥 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>
* ✨ 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
* 🐛 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>
* 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.
🐛 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>
* ✨ 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
* 🐛 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.
* 🐛 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
🐛 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>
* 🐛 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>
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>
description: Build or extend LobeHub Agent Signal pipelines for background or quiet agent work driven by event sources, semantic signals, and action handlers. Use when adding a new Agent Signal source, signal or action type, policy, middleware handler, workflow handoff, dedupe or scope behavior, or observability around `src/server/services/agentSignal/**`, `packages/agent-signal`, or `packages/observability-otel/src/modules/agent-signal`.
description: 'Build or extend LobeHub Agent Signal pipelines. Use for signal sources, signal/action types, policies, middleware, workflow handoff, dedupe, scope behavior, or observability.'
description: "Agent tracing CLI for inspecting agent execution snapshots. Use when user mentions 'agent-tracing', 'trace', 'snapshot', wants to debug agent execution, inspect LLM calls, view context engine data, or analyze agent steps. Triggers on agent debugging, trace inspection, or execution analysis tasks."
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
---
@@ -14,7 +14,7 @@ In `NODE_ENV=development`, `AgentRuntimeService.executeStep()` automatically rec
**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 emits a `context_engine_result` event after `serverMessagesEngine()` processes messages. This event carries the full`contextEngineInput` (DB messages, systemRole, model, knowledge, tools, userMemory, etc.) and the processed `output` messages (the final LLM payload).
**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).
@@ -216,5 +217,5 @@ When using `--messages`, the output shows three sections (if context engine data
## Integration Points
- **Recording**: `src/server/services/agentRuntime/AgentRuntimeService.ts` — in the `executeStep()` method, after building `stepPresentationData`, writes partial snapshot in dev mode
- **Context engine event**: `src/server/modules/AgentRuntime/RuntimeExecutors.ts` — in `call_llm` executor, after `serverMessagesEngine()` returns, emits `context_engine_result` event
- **Context engine capture**: `src/server/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()`
description: Build a new builtin tool package under `packages/builtin-tool-<name>/`. Use when adding a new agent-callable toolset, designing its API surface (manifest / ApiName / Params / State), implementing the Executor + ExecutionRuntime, building the Inspector / Render / Placeholder / Streaming / Intervention / Portal UI, or wiring a tool into the central registries (`packages/builtin-tools/src/{index,identifiers,inspectors,renders,placeholders,streamings,interventions,portals}.ts` and `src/store/tool/slices/builtin/executors/index.ts`). Triggers on "new builtin tool", "add a tool", "tool inspector", "tool render", "tool placeholder", "tool streaming", "tool intervention", "BuiltinToolManifest", "BaseExecutor", "ExecutionRuntime".
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
@@ -23,7 +23,7 @@ A builtin tool is a package the agent runtime can call. It ships **five faces**:
This doc covers everything that **isn't UI**: the tool's identifier, API surface, manifest, types, system prompt, ExecutionRuntime, and the executor that wires it into the frontend.
For UI surfaces (Inspector / Render / Placeholder / Streaming / Intervention / Portal), see [ui.md](ui.md).
For 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).
A builtin tool can ship up to **six client-side surfaces**, each with a different role in the chat UI. Only `Inspector` is required; the other five are added on demand and registered in their own central files.
| Surface | Required? | When the chat shows it | Registered in |
Fall back to `createStyles + token` only when you need runtime token computation (rare). Inline `style={{ color: cssVar.colorTextSecondary }}` is fine for one-off dynamic values.
### 0.3 Use `@lobehub/ui`, not raw `antd`
`Block`, `Text`, `Flexbox`, `Highlighter`, `Alert`, `Tooltip`, `Skeleton` all come from `@lobehub/ui`. Modals come from `@lobehub/ui/base-ui` (`createModal`, `useModalContext`, `confirmModal`) — see the **modal** skill.
Memory note: `@lobehub/ui`'s `<Text type='secondary'>` is a lighter shade than `colorTextSecondary`. If you need that exact token color, write `<Text style={{ color: cssVar.colorTextSecondary }}>`.
### 0.5 Always type with `BuiltinXProps<Args, State>` generics
Don't widen to `any`. The Args generic is the JSON Schema params, the State generic is the executor's `state` field. The two should match `<Name>Params` and `<Name>State` from `types.ts`.
### 0.6 Pull strings from `t('plugin')`
```tsx
const{t}=useTranslation('plugin');
t('builtins.<identifier>.apiName.<api>');
```
Every Inspector should default to `t('builtins.<identifier>.apiName.<api>')` so it shows something while args stream in.
### 0.7 Read store state from `@/store/chat`, not props
Tool surfaces sometimes need cross-cutting state (loading, streaming buffer). Read it inside the component via Zustand selectors, not from props — props only carry args/state/messageId.
---
## 1. Inspector — Header Chip (required)
**Lifecycle:** Inspector renders for **every phase** of a tool call: while args are streaming in, while the executor is running, and after results come back. It's the only surface that's always visible.
**Goal:** keep it to a single line. Show what's happening with as much context as is currently available.
| Args streaming, no useful field yet | `isArgumentsStreaming === true`, `partialArgs.X` undefined | Just the API title with `shinyTextStyles.shinyText` |
| Args streaming, key field arrived | `partialArgs.X` populated | Title + key field chip, still pulse-animated |
| Args complete, executor running | `args` populated, `isLoading === true` | Same as above, still pulse-animated |
| Result arrived | `pluginState` populated, `isLoading === false` | Title + chips + result summary (count, identifier, status) |
- Wrap the whole row with `inspectorTextStyles.root` (provides correct flex / line-height baseline).
- Pulse with `shinyTextStyles.shinyText` whenever `isArgumentsStreaming || isLoading`.
- Show the i18n title first so the row is non-empty during the earliest streaming phase.
- Read both `args?.X` and `partialArgs?.X` together — `args` is final, `partialArgs` is in-stream.
- Use chips/tags for distinct facets (identifier, name, parent, status, count). Each chip should clip with `text-overflow: ellipsis` and have a `max-width` so long values don't blow out the chat bubble.
- Append `pluginState`-derived suffixes only **after** loading finishes — count or "(no results)" should not appear while still searching.
**Lifecycle:** rendered **once the result arrives** (after Placeholder/Streaming hand off). Sits below the Inspector header.
**Skip if** the API is read-only or the result is just text — the framework already shows the executor's `content` string. Add a Render only when there's a structured artifact worth seeing: a card, a chart, a diff, a list of files.
If the Render should hide for certain results (e.g. ClaudeCode's TodoWrite hides when the agent is mid-stream), add a `RenderDisplayControl` to `packages/builtin-tools/src/displayControls.ts`. See `ClaudeCodeRenderDisplayControls` for the pattern.
---
## 3. Placeholder — Skeleton Between Args and Result (optional)
**Lifecycle:** rendered when the args have finished streaming but the executor hasn't returned yet. Disappears when `pluginState` arrives. Bridges the moment of perceived lag.
**Add for** APIs with noticeable execution time: web search, network crawl, file list, large grep. **Skip for** instant ops (status flips, calculator).
- **Mirror the eventual Render's layout.** When the result arrives the Placeholder unmounts and the Render mounts; if they share dimensions, the chat doesn't jump.
- Use `Skeleton.Block` / `Skeleton.Button` from `@lobehub/ui` for placeholder shapes.
- Embed any args you have (e.g. the query text) — context helps the user know what's loading.
- Pulse with `shinyTextStyles.shinyText` if the Placeholder includes literal text.
## 4. Streaming — Live Output During Execution (optional)
**Lifecycle:** rendered **while the executor is still running** for APIs that emit incremental output. The component is responsible for fetching the in-flight stream from the chat store and rendering it.
messageId: string;// use to fetch the streaming buffer from store
toolCallId: string;
}
```
Note there's **no `state` or `result` prop** — the Streaming component is for the in-flight phase. It pulls the live buffer from the store itself (typically via `chatToolSelectors.streamingContent(messageId)` or similar).
**Lifecycle:** rendered **before the executor runs** for APIs whose manifest sets `humanIntervention`. The user sees a preview of the args, can edit them, then approves or skips/cancels.
- **Show a preview, not a form by default.** Editing UI is opt-in via `onArgsChange` and is usually inline (click to edit a code block, etc.).
- For args with debounced edit state (text fields), use `registerBeforeApprove(id, flushFn)` so the approve action waits for the debounce to flush. Always return the cleanup function.
- Call `onInteractionAction({ type: 'submit', payload })` when the user approves; `'skip'` if they skip with a reason; `'cancel'` if they cancel the whole turn.
- Add a corresponding `interventionAudit.ts` in the package root if the tool needs scope/path validation before approval (see `local-system/src/interventionAudit.ts`).
**Lifecycle:** rendered when the user opens the tool message in a side panel or full-screen modal. One Portal per **tool**, not per API — the Portal switches on `apiName` internally.
**Add for** tools whose results deserve a deep-dive view: search results with editable filters, page content with reader mode, code interpreter sessions.
| Portal opens but blank | Switch in `Portal/index.tsx` doesn't cover the apiName | | |
| Strings show as `builtins.lobe-foo.apiName.bar` | Missing i18n key in `src/locales/default/plugin.ts` (or not seeded in dev locale files) | | |
| Wrong color shade on `<Text type="secondary">` | `type='secondary'` is lighter than `colorTextSecondary` — pass via `style={{ color: cssVar.colorTextSecondary }}` | | |
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 |
| 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 }}` |
**Lifecycle:** Inspector renders for **every phase** of a tool call: while args are streaming in, while the executor is running, and after results come back. It's the only surface that's always visible.
**Goal:** keep it to a single line. Show what's happening with as much context as is currently available.
| Args streaming, no useful field yet | `isArgumentsStreaming === true`, `partialArgs.X` undefined | Just the API title with `shinyTextStyles.shinyText` |
| Args streaming, key field arrived | `partialArgs.X` populated | Title + key field chip, still pulse-animated |
| Args complete, executor running | `args` populated, `isLoading === true` | Same as above, still pulse-animated |
| Result arrived | `pluginState` populated, `isLoading === false` | Title + chips + result summary (count, identifier, status) |
- Wrap the whole row with `inspectorTextStyles.root` (provides correct flex / line-height baseline).
- Pulse with `shinyTextStyles.shinyText` whenever `isArgumentsStreaming || isLoading`.
- Show the i18n title first so the row is non-empty during the earliest streaming phase.
- Read both `args?.X` and `partialArgs?.X` together — `args` is final, `partialArgs` is in-stream.
- Use chips/tags for distinct facets (identifier, name, parent, status, count). Each chip should clip with `text-overflow: ellipsis` and have a `max-width` so long values don't blow out the chat bubble.
- Append `pluginState`-derived suffixes only **after** loading finishes — count or "(no results)" should not appear while still searching.
- **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.
**Lifecycle:** rendered **before the executor runs** for APIs whose manifest sets `humanIntervention`. The user sees a preview of the args, can edit them, then approves or skips/cancels.
- **Show a preview, not a form by default.** Editing UI is opt-in via `onArgsChange` and is usually inline (click to edit a code block, etc.).
- For args with debounced edit state (text fields), use `registerBeforeApprove(id, flushFn)` so the approve action waits for the debounce to flush. Always return the cleanup function.
- Call `onInteractionAction({ type: 'submit', payload })` when the user approves; `'skip'` if they skip with a reason; `'cancel'` if they cancel the whole turn.
- Add a corresponding `interventionAudit.ts` in the package root if the tool needs scope/path validation before approval (see `local-system/src/interventionAudit.ts`).
# Placeholder — Skeleton Between Args and Result (optional)
**Lifecycle:** rendered when the args have finished streaming but the executor hasn't returned yet. Disappears when `pluginState` arrives. Bridges the moment of perceived lag.
**Add for** APIs with noticeable execution time: web search, network crawl, file list, large grep. **Skip for** instant ops (status flips, calculator).
- **Mirror the eventual Render's layout.** When the result arrives the Placeholder unmounts and the Render mounts; if they share dimensions, the chat doesn't jump.
- Use `Skeleton.Block` / `Skeleton.Button` from `@lobehub/ui` for placeholder shapes.
- Embed any args you have (e.g. the query text) — context helps the user know what's loading.
- Pulse with `shinyTextStyles.shinyText` if the Placeholder includes literal text.
**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.
**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.
- **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.
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.
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
- 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, key–value 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.
# Streaming — Live Output During Execution (optional)
**Lifecycle:** rendered **while the executor is still running** for APIs that emit incremental output. The component is responsible for fetching the in-flight stream from the chat store and rendering it.
messageId: string;// use to fetch the streaming buffer from store
toolCallId: string;
}
```
Note there's **no `state` or `result` prop** — the Streaming component is for the in-flight phase. It pulls the live buffer from the store itself (typically via `chatToolSelectors.streamingContent(messageId)` or similar).
building bots that work across multiple chat platforms.
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.'
- **If file exists and contains `"serverUrl": "http://localhost:3011"`**: already authenticated. Skip to Step 3.
- **If file missing or points to wrong server**: login is needed. Ask the user to run:
- **If file exists and contains `"serverUrl": "http://localhost:3011"`**: skip to Step 3.
- **If missing or wrong server**: ask the user to run:
```bash
! cd lobehub/apps/cli &&LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts login --server http://localhost:3011
```
> Login requires interactive browser authorization (OIDC Device Code Flow), so the user must run it themselves via `!` prefix. After login, credentials are saved to `lobehub/apps/cli/.lobehub-dev/` and persist across sessions.
> Login requires interactive browser authorization (OIDC Device Code Flow), so the user must run it themselves via `!` prefix. Credentials persist in `lobehub/apps/cli/.lobehub-dev/`.
### Step 3: Test with CLI Commands
CLI runs from source (`bun src/index.ts`), so CLI-side code changes take effect immediately without rebuilding.
CLI runs from source, so CLI-side code changes take effect immediately without rebuilding.
```bash
cd lobehub/apps/cli
LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts <command>
$CLI <command>
```
### Step 4: Clean Up Test Data
Delete any test data created during verification:
```bash
LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts task delete < id > -y
LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts agent delete < id > -y
$CLI task delete < id > -y
$CLI agent delete < id > -y
```
## Common Testing Patterns
@@ -103,51 +95,30 @@ LOBEHUB_CLI_HOME=.lobehub-dev bun src/index.ts agent delete < id > -y
description: Datafetching architecture guide using Service layer + Zustand Store + SWR. Use when implementing data fetching, creating services, working with store hooks, or migrating from useEffect. Triggers on data loading, API calls, service creation, or store data fetching tasks.
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.'
description: 'Use when generating or regenerating Drizzle migration files, changing database schematables or columns, resolving migration sequence conflicts after rebase, reviewing migration SQL for idempotent patterns, or renaming migration files.'
description: 'Use for Drizzle migrations: schema/table/column changes, migration generation or regeneration, sequence conflicts after rebase, idempotent SQL review, or migration renames.'
description: "Guide for the `debug` npm package and LobeHub log namespaces (lobe-server:*, lobe-desktop:*, lobe-client:*, lobe-*-router:*). Use whenever adding a `debug(...)` logger, picking a namespace for new server/desktop/client/router code, troubleshooting why DEBUG=lobe-* logs don't show up, or when the user asks to 'add logging', 'add a logger', 'instrument this', 'trace this call', 'why isn't my log printing', or mentions `debug(`, `DEBUG=`, `localStorage.debug`, or log format specifiers like %O / %o / %s / %d in a LobeHub codebase."
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.'
description: 'Writing guide for website changelog pages under docs/changelog/*.mdx. Use when creating or editing product update posts in EN/ZH. Not for GitHub Release notes.'
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.'
description: "Drizzle ORM schema authoring and query style for LobeHub (postgres, strict mode). Use when editing anything under `src/database/schemas/`, defining `pgTable` columns/indexes/junction tables, spreading `...timestamps`, generating `createInsertSchema`/`$inferSelect`/`$inferInsert` types, writing `db.select().from(...).leftJoin(...)` queries, or deciding when to split a relational `with:` into two queries. Triggers on `pgTable`, `db.select`, `db.query`, `eq()`/`and()`/`inArray()`, `uniqueIndex`, `primaryKey`, `references({ onDelete })`, 'add a column', 'new table', 'foreign key', 'junction table', 'schema field'. For migration files specifically, see the `db-migrations` skill."
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.'
description: Guide for implementing and debugging LobeHub heterogeneous agent integrations such as Claude Code, Codex, and future external CLI agents. Use when working on adapter event mapping, Electron IPC transport, renderer persistence, tool-call chaining, subagent threads, resume/session handling, or regressions like mixed multi-tool messages, broken step boundaries, stuck tool loading, and orphan tool messages. Triggers on 'heterogeneous agent', 'hetero agent', '异构 agent', 'claude code adapter', 'codex adapter', 'external agent CLI', '孤立 tool 消息', 'raw Codex trace', or adapter/executor bugs.
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.'
description: "Adding or editing keyboard shortcuts in LobeHub. Use when registering a new hotkey, changing a key combo, scoping a shortcut to chat vs global, or wiring a hotkey hook + tooltip. Covers the 5-step flow: add to `HotkeyEnum` in `src/types/hotkey.ts`, register in `HOTKEYS_REGISTRATION` (`src/const/hotkeys.ts`) with `combineKeys([Key.Mod, …])`, add i18n in `src/locales/default/hotkey.ts`, expose via `useHotkeyById` in `src/hooks/useHotkeys/`, and render `<Tooltip hotkey={…}>`. Triggers on `HotkeyEnum`, `HOTKEYS_REGISTRATION`, `useHotkeyById`, `combineKeys`, `Key.Mod`/`Key.Shift`, 'add a hotkey', 'add a shortcut', '加快捷键', '快捷键', 'Cmd+K', 'keyboard shortcut', 'hotkey scope', 'hotkey conflict'."
description: 'Add or edit LobeHub keyboard shortcuts. Use for HotkeyEnum, HOTKEYS_REGISTRATION, combineKeys, useHotkeyById, tooltip hotkeys, shortcut scope, conflicts, or Cmd/Ctrl key combos.'
description: "LobeHub internationalization with react-i18next. Use when adding any user-facing string in `.tsx`/`.ts` files, creating or renaming a key under `src/locales/default/{namespace}.ts`, deciding the `{feature}.{context}.{action}` flat-key pattern, wiring a new namespace into `src/locales/default/index.ts`, or translating zh-CN/en-US JSON for dev preview. Triggers on `useTranslation`, `t('foo.bar')`, `i18next.t`, `{{variable}}` interpolation, hardcoded UI strings (zh or en) that should be extracted, 'add i18n', '加 i18n key', '翻译', 'locale key', 'namespace', 'pnpm i18n'."
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.'
description: "Linear issue management. Use when the user mentions LOBE-xxx issue IDs (e.g. LOBE-4540), says 'linear' / 'linear issue' / 'link linear', or when creating PRs that reference Linear issues. Covers retrieving issues, updating status, adding completion comments, and creating sub-issue trees."
description: 'Linear issue management. Use for LOBE-xxx issues, Linear links, PRs referencing Linear, retrieving issues, updating status, completion comments, or sub-issue trees.'
Each channel has its own folder under `bot/<channel>/` containing an `index.md`
(activation, navigation, send-message, and verification snippets specific to
that app) and its test script:
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [references/osascript-common.md](./references/osascript-common.md). Read this first if you're new to osascript automation.
For **shared osascript patterns** (activate, type, paste, screenshot, read accessibility, common workflow template, gotchas), see [bot/osascript-common.md](./bot/osascript-common.md). Read this first if you're new to osascript automation.
## Bridge-based channels (no native app)
Some channels have no native app to drive with osascript — they connect through
a local bridge inside the Desktop app. These are tested with agent-browser
(IPC + UI) plus the bridge's own HTTP/REST endpoints, not osascript:
Each script: activates the app, navigates to the channel/contact, pastes the message via clipboard, sends, waits, and takes a screenshot. Use the `Read` tool on the screenshot for visual verification.
### iMessage bridge regression script
`test-imessage-bridge.sh` does **not** follow the osascript bot interface — it
drives the Desktop bridge's IPC + HTTP layers and asserts the result, then
self-cleans. Needs BlueBubbles running and Electron up with CDP.
See [references/osascript-common.md](./references/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
See [bot/osascript-common.md](./bot/osascript-common.md#gotchas) for the full osascript gotchas list (accessibility permissions, `keystroke` non-ASCII issues, locale-specific app names, rate limiting, etc.).
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();
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.'
description: "LobeHub imperative-modal conventions. Use whenever creating, editing, opening, or migrating a modal/dialog/popup — prefer `createModal` / `confirmModal` / `useModalContext` from `@lobehub/ui/base-ui` (headless) over the legacy root `@lobehub/ui``createModal` (antd Modal props) and over any declarative `open` state + `<Modal />` pattern. Covers required `ModalHost` mounting, the `Content` + `index.tsx` file layout, `content` vs `children` slot, i18n inside `createModal()` (`import { t } from 'i18next'`), and migration notes. Triggers on `createModal`, `confirmModal`, `useModalContext`, `ModalHost`, `antd Modal`, `<Modal open>`, 'open a modal', 'popup', 'dialog', 'confirm dialog', '弹框', '弹窗', '确认框', 'migrate to base-ui'."
description: 'LobeHub imperativemodal conventions. Use when creating or migrating modals, dialogs, popups, confirm flows, ModalHost wiring, createModal, confirmModal, useModalContext, or base-ui modal APIs.'
description: "Create a PR for the current branch. Use when the user asks to create a pull request, submit PR, or says '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
---
@@ -71,3 +71,82 @@ Use `.github/PULL_REQUEST_TEMPLATE.md` as the body structure. Key sections:
- **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:
`--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.
description: Complete project architecture and structure guide. Use when exploring the codebase, understanding project organization, finding files, or needing comprehensive architectural context. Triggers on architecture questions, directory navigation, or project overview needs.
description: 'LobeHub open-source monorepo architecture map. Use when locating code layers, understanding apps/packages/src layout, business stubs, project structure, or onboarding to the repository.'
user-invocable: false
---
@@ -13,11 +13,12 @@ user-invocable: false
## Project Description
Open-source, modern-design AI Agent Workspace: **LobeHub** (previously LobeChat).
This repo is the **open-source root** (`github.com/lobehub/lobehub`, package `@lobehub/lobehub`).
**Supported platforms:**
- Web desktop/mobile
- Desktop (Electron)
- Desktop (Electron) — `apps/desktop`
- Mobile app (React Native) — **separate repo, already launched** (not in this monorepo)
This is a monorepo extending the open-source `lobehub` submodule. Two repos:
- **cloud repo root** — `src/` and `packages/business/` (`config`, `const`, `model-runtime`) hold cloud-only SaaS code that overrides/extends the submodule. See `AGENTS.md` for the override mechanism.
- **`lobehub/` submodule** — the open-source product core.
### `lobehub/` submodule — key directories
Flat layout — `apps/`, `packages/`, and `src/` all sit at the repo root. No
description: 'Use when writing or editing any `.tsx` under `src/**`. Triggers: createStaticStyles, createStyles, cssVar, antd-style, Flexbox, Center, Select, Modal, Drawer, Button, Tooltip, DropdownMenu, Popover, Switch, ScrollArea, Link, useNavigate, react-router-dom, next/link, desktopRouter, componentMap.desktop, .desktop.tsx, new component, new page, edit layout, add styles, zustand selector, @lobehub/ui, antd import.'
description: 'LobeHub React component conventions. Use when editing TSX UI, choosing base-ui vs @lobehub/ui vs antd, styling with antd-style, routing, desktop variants, layouts, or component state.'
4. **Custom implementation** — last resort; never reach for antd directly
2. **`@lobehub/ui/base-ui`** — headless primitives. **If the component lives here, use it. Do NOT import the same-named root export.**
3. **`@lobehub/ui`** — higher-level / antd-wrapping components (only when no base-ui equivalent)
4. **antd** — only when neither base-ui nor `@lobehub/ui` root provides it
5. **Custom implementation** — true last resort
If unsure about available components, search existing code or check `node_modules/@lobehub/ui/es/index.mjs`.
If unsure about available components, search existing code or check `node_modules/@lobehub/ui/es/index.mjs` and `node_modules/@lobehub/ui/es/base-ui/`.
### Common @lobehub/ui Components
### `@lobehub/ui/base-ui` — always prefer for these
| `Modal` (imperative API) | `import { createModal, confirmModal, useModalContext, type ModalInstance } from '@lobehub/ui/base-ui';` |
| `DropdownMenu` | `import { DropdownMenu } from '@lobehub/ui/base-ui';` |
| `ContextMenu` | `import { ContextMenu } from '@lobehub/ui/base-ui';` |
| `Popover` | `import { Popover } from '@lobehub/ui/base-ui';` |
| `ScrollArea` | `import { ScrollArea } from '@lobehub/ui/base-ui';` |
| `Switch` | `import { Switch } from '@lobehub/ui/base-ui';` |
| `Toast` | `import { Toast } from '@lobehub/ui/base-ui';` |
| `FloatingSheet` | `import { FloatingSheet } from '@lobehub/ui/base-ui';` |
For Modal specifically, see the dedicated **modal** skill — use the imperative `createModal({ content: … })` pattern over the legacy `<Modal open … />` declarative pattern. base-ui has its own `ModalHost` already mounted in `SPAGlobalProvider`.
> Common slip: `import { Select } from '@lobehub/ui'` looks fine but it's the antd-backed Select. Use base-ui Select. Same for `Modal`, `DropdownMenu`, etc.
### `@lobehub/ui` root — use when base-ui has no equivalent
| Layout | Center, DraggablePanel, Flexbox, Grid, Header, MaskShadow |
| Navigation | Burger, Menu, SideNav, Tabs |
## State
When a feature component manages more than 3 pieces of state (`useState`/`useReducer`/derived state), extract the logic into a custom hook (e.g. `useXxx`). Keep the component focused on rendering — the hook holds state and handlers, so logic can be unit-tested without rendering the component.
description: OpenResponses API compliance testing. Use when testing the Response API endpoint, running compliance tests, or debugging Response API schema issues. Triggers on 'compliance', 'response api test', 'openresponses test'.
description: 'OpenResponses API compliance testing. Use for Response API endpoint tests, compliance runs, schema debugging, response api test, or openresponses test tasks.'
description: 'Common recurring mistakes in LobeHub code review — console leftovers, missing return await, hardcoded secrets, hardcoded i18n strings, desktop router pair drift, antd vs @lobehub/ui, non-idempotent migrations, cloud impact red flags. Use as a quick checklist when reviewing PRs, diffs, or branch changes.'
description: 'LobeHub code review checklist. Use when reviewing a PR, diff, or branch for console leftovers, return await, secrets, i18n, desktop router drift, UI imports, migrations, or cloud impact.'
description: 'Audit .agents/skills SKILL.md files. Use for recurring checks of duplicate, overlapping, stale, inconsistent, or broken skills and merge/delete candidates.'
disable-model-invocation: true
argument-hint: '[--verbose | --apply]'
---
# Skills Audit
Periodic review of the project-local skill set under `.agents/skills/`. The goal is to catch drift before the catalog becomes confusing — too many skills, overlapping triggers, descriptions that no longer match the body, references to skills that were renamed/deleted.
**Recommended cadence:** weekly, or after any week where >1 skill was added/renamed.
## Procedure
### 1 — Inventory
Build a fresh census of all SKILL.md files. Do NOT trust any prior cached list.
```bash
find .agents/skills -name SKILL.md | wc -l # total count
find .agents/skills -name SKILL.md -exec wc -l {} \; | sort -rn # by body length
```
Group by domain in a mental table (DB / state / UI / agent / testing / workflow / docs / etc.). Note new arrivals since last audit (`git log --since="1 week ago" -- .agents/skills/`).
### 2 — Pull frontmatter for all skills
```bash
# Extract name + description for each SKILL.md
for f in .agents/skills/*/SKILL.md; do
echo "=== $(basename $(dirname $f)) ==="
awk '/^---$/{c++; next} c==1' "$f" | head -20
done
```
Read the description block of every skill. The body can stay unread unless step 4 flags it.
### 3 — Detect overlap / redundancy
For each pair within the same domain, ask:
- **Same description**? → likely duplicate (one is probably a stale rename leftover, or a global-vs-local collision).
- **Trigger keywords substantially overlap**? → either merge, OR tighten one description so the model can choose unambiguously.
- **One skill's body says "see also: foo"**? → confirm `foo` still exists, AND confirm the cross-reference is still meaningful (the referenced skill may have absorbed the referrer's concerns).
- **Skill duplicates content from `AGENTS.md`**? → fold into AGENTS.md or slim the skill to just the delta.
Common false positives (do NOT merge):
- `db-migrations` vs `drizzle` — distinct workflows (migration files vs schema authoring).
- `microcopy` vs `i18n` — content vs mechanics.
- `agent-runtime-hooks` vs `agent-tracing` vs `agent-signal` — different surfaces of the agent system.
- `testing` vs `local-testing` vs `cli-backend-testing` — different test types.
### 4 — Description format consistency
Apply the **standard template**:
```
{Topic + key conventions or scope}. Use when {scenarios — verbs + nouns}. Triggers on {`code-symbols`, 'natural phrases', '中文'}.
```
Skills with `disable-model-invocation: true` (user-invoked only, slash commands) don't need `Triggers on` — they're never auto-routed.
Flag descriptions that:
- ❌ Have NO `Use when` clause (model can't decide when to load it).
- ❌ Have NO `Triggers on` clause (and aren't `disable-model-invocation`).
- ❌ Use weird formats (numbered lists `(1)(2)(3)`, `Triggers:` colon instead of `Triggers on`, `MUST use when ...` as opening word).
- ❌ Are dramatically terse for a 200+ line body, or dramatically verbose for a 60-line body.
- ❌ Reference deleted/renamed skills.
### 5 — Stale-skill check
For narrow domain skills (e.g. `response-compliance`, one-off CLI workflows):
```bash
# Confirm the referenced code surface still exists
rg -l "response-compliance|openresponses" packages/ src/ # adjust per skill
git log --since="3 months ago" -- .agents/skills/ < skill > /SKILL.md # is it being maintained?
```
If the underlying surface is gone and the skill hasn't been edited in 3+ months → flag for archival.
For each name extracted, confirm `.agents/skills/<name>/SKILL.md` exists. Broken references happen after renames — fix them in the same audit pass.
### 7 — Output report
Produce a markdown summary back to the user with the same structure as the original audit (this skill was created during one):
```markdown
## 📊 Inventory
{count, domain breakdown}
## 🎯 Recommendations
### 🔴 High confidence
- {action} — {reason}
### 🟡 Medium confidence
- {action} — {reason needs verification}
### 🟢 Low confidence / no-op
- {item considered but skipping because ...}
## 📋 Suggested order
{table of actions with risk + LOC estimate}
```
End by asking the user which actions to apply — do NOT auto-apply unless the user passed `--apply` and even then confirm destructive deletes individually.
## Output rules
- Be specific. "Skill X overlaps with Y" is useless without naming the overlapping triggers.
- Cite line numbers when flagging description / body issues.
- Don't recommend merges unless the call sites would actually load the merged skill in the same context.
- Don't recommend deletes for skills that haven't been touched recently — "unused" can mean "stable", not "dead".
## What NOT to do
- ❌ Don't rename skill directories without checking for cross-references AND user memory entries that name the old slug.
- ❌ Don't normalize a description by removing trigger keywords just to fit the template — the keywords are the routing signal.
- ❌ Don't fold a heavy 200+ line skill into another just because they share a domain — large skills get loaded selectively and merging makes everything load.
- ❌ Don't propose `.agents/skills/INDEX.md` or `<domain>-<skill>` prefix renames unless the user explicitly asks — costs > benefits for cosmetic reorgs.
## Related history
- First audit: `chore/skills-audit` branch (2026-05-25) — deleted `source-command-dedupe`, renamed `data-fetching` → `data-fetching-architecture`, normalized 9 descriptions, created this skill.
Use this skill when the user asks to run the migrated source command `dedupe`.
## Command Template
Find up to 3 likely duplicate issues for a given GitHub issue.
To do this, follow these steps precisely:
1. Use an agent to check if the Github issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. Use an agent to view a Github issue, and ask the agent to return a summary of the issue
3. Then, launch 5 parallel agents to search Github for duplicates of this issue, using diverse keywords and search approaches, using the summary from #1
4. Next, feed the results from #1 and #2 into another agent, so that it can filter out false positives, that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates)
Notes (be sure to tell this to your agents, too):
- Use `gh` to interact with Github, rather than web fetch
- Do not use other tools, beyond `gh` (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- For your comment, follow the following format precisely (assuming for this example that you found 3 suspected duplicates):
---
Found 3 possible duplicate issues:
1. <link to issue>
2. <link to issue>
3. <link to issue>
This issue will be automatically closed as a duplicate in 3 days.
- If your issue is a duplicate, please close it and 👍 the existing issue instead
- To prevent auto-closure, add a comment or 👎 this comment
description: MUST use when editing src/routes/ segments, src/spa/router/desktopRouter.config.tsx or desktopRouter.config.desktop.tsx (always change both together), mobileRouter.config.tsx, or when moving UI/logic between routes and src/features/.
description: 'LobeHub SPA route architecture. Use when editing src/routes, src/features delegation, desktop/mobile/popup routerconfigs, .desktop variants, route segments, redirects, or new pages.'
user-invocable: false
---
@@ -94,6 +94,27 @@ Anything that changes the tree (new segment, renamed `path`, moved layout, new c
---
## 3b. Other `.desktop.{ts,tsx}` variants inside `src/routes/`
The router pair is **not** the only `.desktop` variant pattern in this repo. Some route trees colocate a `<name>.desktop.{ts,tsx}` next to its base `<name>.{ts,tsx}` — Vite's resolver swaps in the `.desktop` file for Electron builds. Same drift risk as the router pair: editing only one side can break Electron silently.
| `src/routes/(main)/agent/index.tsx` | `src/routes/(main)/agent/index.desktop.tsx` | Page entry. Desktop variant overrides the web page wholesale (e.g. extra popup guards). |
| `src/routes/(main)/group/index.tsx` | `src/routes/(main)/group/index.desktop.tsx` | Same pattern as agent. |
**Rules:**
1. After editing **any**`.ts`/`.tsx` under `src/routes/`, glob the same directory for a `<filename>.desktop.{ts,tsx}` sibling. If one exists, apply the equivalent change there in the same commit.
2. When adding a new SettingsTab, register it in **both**`componentMap.ts` (with `dynamic(...)`) and `componentMap.desktop.ts` (with a sync `import`). `componentMap.sync.test.ts` will fail the build otherwise.
3. When adding a new desktop-only page wholesale-override, prefer a single base file with platform-aware code over introducing a new `.desktop.tsx` variant — only add a new variant when the two trees genuinely diverge (different store wiring, different popup guards, etc.).
4. When deleting, remove **both** files together.
---
## 4. How to Divide Files (route vs feature)
| Question | Put in `src/routes/` | Put in `src/features/` |
description: Zustand store data structure patterns for LobeHub. Covers List vs Detail data structures, Map + Reducer patterns, type definitions, and when to use each pattern. Use when designing store state, choosing data structures, or implementing list/detail pages.
description: 'LobeHub Zustand store data-shape patterns. Use when designing store state, list/detail splits, normalized maps, reducers, messagesMap, topicsMap, or choosing shared type sources.'
description: Testing guide using Vitest. Use when writing tests (.test.ts, .test.tsx), fixing failing tests, improving test coverage, or debugging test issues. Triggers on test creation, test debugging, mock setup, or test-related questions.
description: 'Vitest testing guide. Use when writing or updating tests, fixing failing tests, improving coverage, debugging test issues, or setting up mocks.'
description: TRPC router development guide. Use when creating or modifying TRPC routers (src/server/routers/**), adding procedures, or working with server-side API endpoints. Triggers on TRPC router creation, procedure implementation, or API endpoint tasks.
description: 'TRPC router development guide. Use when creating or modifying src/server/routers, adding procedures, or implementing server-side API endpoints.'
description: "TypeScript code style and type-safety guide for LobeHub. Read before writing or editing any `.ts` / `.tsx` / `.mts` — covers `interface` vs `type`, `Record<PropertyKey, unknown>` over `any`/`object`, `as const satisfies`, `@ts-expect-error` over `@ts-ignore`, `import type` (`separate-type-imports`), `async`/`await` + `Promise.all`, `for…of` over indexed `for`, and the no-silent-`.catch(() => fallback)` rule. Also use when reviewing type quality, deciding module augmentation (`declare module`) over `namespace`, or designing extensible types (e.g. `PipelineContext.metadata`). Triggers on any TypeScript file edit, 'fix the type', 'why is this `any`', 'should this be interface or type', 'eslint type-import', 'ts-expect-error'."
description: 'LobeHub TypeScript style and type-safety guide. Use when editing TS/TSX/MTS, fixing types, choosing interface vs type, avoiding any/object, import type, async flow, or ts-expect-error.'
user-invocable: false
---
@@ -48,6 +48,7 @@ user-invocable: false
- Replace magic numbers/strings with well-named constants
- Defer formatting to tooling
- Prefer **named exports** over `export default` — keeps refactor renames and IDE auto-import in sync, and avoids the `default` re-naming drift you get with `import Foo from './foo'`. Reserve `export default` for files where the framework requires it (Next.js page/route/layout, React.lazy targets, config files like `vitest.config.ts`)
- Before adding local helpers for common guards/parsing/normalization (record checks, string extraction, empty-string handling, timing helpers, JSON-safe utilities, etc.), search `packages/utils` first. If the helper already exists or clearly belongs there, import it from `@lobechat/utils` (or the relevant `@lobechat/utils/*` subpath) instead of duplicating tiny helpers across feature files.
description: 'Upstash Workflow implementation guide. Use when creating async workflows with QStash, implementing fan-out patterns, or building 3-layer workflow architecture (process → paginate → execute).'
description: 'LobeHub Upstash Workflow and QStash guide. Use for async workflows, process/paginate/execute fan-out, serve handlers, context.run/call/sleep, or workflow triggers.'
description: "LobeHub Zustand store conventions: public/internal/dispatch action layers, optimistic update pattern, slice composition via `flattenActions`, and class-based action migration. Use whenever working under `src/store/**`, adding a `createXxxSlice`, writing `internal_*` or `internal_dispatch*` actions, designing `messagesMap`/`topicsMap` reducers, refactoring a `StateCreator` object slice into a `XxxActionImpl` class, or debugging stale store reads. Triggers on `useChatStore`/`useUserStore`/`useGlobalStore`, `createStore`, `flattenActions`, `StoreSetter`, `internal_dispatch`, 'add an action', 'zustand selector', 'store slice', 'class action', 'optimistic update'."
description: 'LobeHub Zustand store conventions. Use when editing src/store, store slices, public/internal actions, dispatch actions, flattenActions, optimistic updates, selectors, maps, or class action migration.'
When a slice doesn't write local state at the moment — e.g. it reads context
from `#get()` and forwards calls to another store, or just runs hooks — drop
the `#set` field. Otherwise ESLint's `no-unused-vars` flags the unused private
field.
Mark the constructor's `set` param as `_set` and `void _set` it to keep the
`(set, get, api)` shape aligned with `StateCreator`. This is **a snapshot of
the current need, not a permanent contract** — if a later change needs `set`,
restore the `#set` field and use it; do not invent a workaround to keep the
"unused" form.
When a slice doesn't write local state (e.g. it delegates to another store or just runs hooks), drop `#set` and mark the constructor param as `_set` with `void _set` to keep the `(set, get, api)` shape:
This guide is used for batch triaging GitHub issues - analyzing issues and applying appropriate labels.
This guide is used for triaging GitHub issues — analyzing issues and applying only the most essential business-domain labels.
## Core Principle
**Each issue should have 1-3 labels that describe its core business domain.** Do NOT apply redundant labels that can be inferred from other labels. Less is more.
The runtime environment or technology wrapper where the issue occurs:
#### Provider Detection
| Label | When to apply |
|-------|--------------|
| `electron` | Desktop/Electron-specific issues. This REPLACES `platform:desktop`, `os:*`, `deployment:*`, `hosting:*` — do NOT add those. |
| `pwa` | PWA/mobile-app-specific issues |
| `docker` | Docker-specific deployment issues |
**IMPORTANT**: Always check issue title and body for provider mentions!
**Rule**: If `electron` is applied, do NOT add `platform:desktop`, `os:*`, `deployment:*`, or `hosting:*`. The `electron` label already implies all of these.
**Official Providers** (check for these keywords in title/body):
#### Category 2: Feature / Component
The functional area affected. Select the 1-2 MOST relevant:
- Check environment variables like `AIHUBMIX_*` in issue body
- `zenmux` → `provider:zenmux`
**Multiple Providers**: If issue mentions multiple providers, add ALL applicable provider labels.
**Rule**: Only add a provider label if the issue is specifically about that provider's behavior (e.g., "Gemini returns error X"). Do NOT add provider labels just because the issue template mentions a provider.
| `🐛 Bug`, `💄 Design`, `📝 Documentation`, `⚡️ Performance` | Issue type is already indicated by GitHub issue template |
| `Inactive` | Handled separately; do NOT add during triage |
## Examples
### Example 1: Electron desktop bug
**Issue**: "Connection failure when executing tasks on macOS desktop app"
**Analysis**: Desktop Electron app issue with task scheduling.
**Labels**: `electron,feature:schedule-task`
**Why**: `electron` covers the desktop platform. `feature:schedule-task` identifies the affected feature. No need for `platform:desktop`, `os:macos`, `hosting:cloud`, `priority:*`, or `Bug`.
### Example 2: Provider-specific issue
**Issue**: "Gemini tool calling returns empty response on desktop"
**Analysis**: Desktop app issue, but the core problem is Gemini provider behavior with tool calling.
**Labels**: `electron,provider:gemini`
**Why**: `electron` for the desktop context. `provider:gemini` because the issue is about Gemini's behavior. The tool calling aspect is secondary — the provider is the key domain.
### Example 3: Feature-specific issue
**Issue**: "Underscore auto-escaped in markdown editor"
**Analysis**: Markdown rendering bug in the editor component.
**Labels**: `feature:markdown`
**Why**: Single label is sufficient — the issue is purely about markdown rendering. No need for platform, OS, or priority labels.
### Example 4: Web-only feature request
**Issue**: "Add search functionality to plugin marketplace"
**Analysis**: Feature request for marketplace search. Web platform, no specific provider.
**Labels**: `feature:marketplace,feature:search`
**Why**: Two feature labels capture the core domain. No platform label needed — it's a web app by default.
### Example 5: Ollama self-hosted issue
**Issue**: "Ollama model not loading on self-hosted Docker deployment"
**Analysis**: Provider-specific issue with Ollama on Docker.
**Labels**: `docker,provider:ollama`
**Why**: `docker` for the deployment context, `provider:ollama` for the model provider. No need for `hosting:self-host` or `platform:*`.
## Important Rules
1. **Read Carefully**: Read issue template fields AND issue body/title for complete context
2. **Provider Detection**: ALWAYS check title and body for provider keywords (including aihubmix, etc.)
3. **Multiple Categories**: Use ALL applicable labels from different categories
**Reasoning**: AIHubMix provider discount feature not working. Client mode deployment on Windows with Docker. Provider detection from title keyword "aihubmix".
1. **1-3 labels per issue** — Never exceed 3 labels. If you find yourself adding more, you're being too granular.
2. **`electron` replaces all platform/OS/deployment labels** — Never combine `electron` with `platform:desktop`, `os:*`, `deployment:*`, or `hosting:*`.
3. **Provider only when relevant** — Only add `provider:*` if the issue is specifically about that provider's behavior.
4. **No priority, no type** — Do NOT add `priority:*`, `🐛 Bug`, `💄 Design`, etc. Maintainers handle these.
5. **No comments** — Only apply labels. Do NOT post comments to issues.
6. **Remove `unconfirm`** — Always remove the `unconfirm` label when applying triage labels.
echo "📦 Uploading release files to s3://$S3_BUCKET/$CHANNEL/$VERSION/"
for file in release/*.dmg release/*.zip release/*.exe release/*.AppImage release/*.deb release/*.rpm release/*.snap release/*.tar.gz; do
for file in release/*.dmg release/*.zip release/*.exe release/*.AppImage release/*.deb release/*.rpm release/*.snap release/*.tar.gz release/*.blockmap; do
issues:write# for actions-cool/issues-helper to update issues
pull-requests:write# for actions-cool/issues-helper to update PRs
issues:write
pull-requests:write
runs-on:ubuntu-latest
steps:
- name:Auto Comment on Issues Closed
uses:wow-actions/auto-comment@v1
with:
GITHUB_TOKEN:${{ secrets.GH_TOKEN}}
GITHUB_TOKEN:${{ secrets.GH_TOKEN}}
issuesClosed:|
✅ @{{ author }}
@@ -51,11 +51,4 @@ jobs:
The growth of project is inseparable from user feedback and contribution, thanks for your contribution! If you are interesting with the lobehub developer community, please join our [discord](https://discord.com/invite/AYFPHvv2jT) and then dm @arvinxx or @canisminor1990. They will invite you to our private developer channel. We are talking about the lobe-chat development or sharing ai newsletter around the world.
issues:write# for actions-cool/issues-helper to update issues
pull-requests:write# for actions-cool/issues-helper to update PRs
runs-on:ubuntu-latest
steps:
- name:check-inactive
uses:actions-cool/issues-helper@v3
with:
actions:'check-inactive'
token:${{ secrets.GH_TOKEN }}
inactive-label:'Inactive'
inactive-day:60
issue-close-require:
permissions:
issues:write# for actions-cool/issues-helper to update issues
pull-requests:write# for actions-cool/issues-helper to update PRs
runs-on:ubuntu-latest
steps:
- name:need reproduce
uses:actions-cool/issues-helper@v3
with:
actions:'close-issues'
token:${{ secrets.GH_TOKEN }}
labels:'✅ Fixed'
inactive-day:3
body:|
👋 @{{ author }}
<br/>
Since the issue was labeled with `✅ Fixed`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
- name:need reproduce
uses:actions-cool/issues-helper@v3
with:
actions:'close-issues'
token:${{ secrets.GH_TOKEN }}
labels:'🤔 Need Reproduce'
inactive-day:3
body:|
👋 @{{ author }}
<br/>
Since the issue was labeled with `🤔 Need Reproduce`, but no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
- name:need reproduce
uses:actions-cool/issues-helper@v3
with:
actions:'close-issues'
token:${{ secrets.GH_TOKEN }}
labels:"🙅🏻♀️ WON'T DO"
inactive-day:3
body:|
👋 @{{ github.event.issue.user.login }}
<br/>
Since the issue was labeled with `🙅🏻♀️ WON'T DO`, and no response in 3 days. This issue will be closed. If you have any questions, you can comment and reply.
@@ -7,6 +7,7 @@ Guidelines for using AI coding agents in this LobeHub repository.
- Next.js 16 + React 19 + TypeScript
- SPA inside Next.js with `react-router-dom`
- `@lobehub/ui`, antd for components; antd-style for CSS-in-JS — **prefer `createStaticStyles` with `cssVar.*`** (zero-runtime); only fall back to `createStyles` + `token` when styles genuinely need runtime computation. See `.cursor/docs/createStaticStyles_migration_guide.md`.
- **Component priority**: `@lobehub/ui/base-ui` (headless primitives) **first**, then `@lobehub/ui` root, then antd as last resort. When the component exists in base-ui, use it — never reach for the root or antd counterpart. Base-ui covers `Select`, `Modal` / `createModal` / `confirmModal`, `DropdownMenu`, `ContextMenu`, `Popover`, `ScrollArea`, `Switch`, `Toast`, `FloatingSheet`. Prefer `@lobehub/ui/base-ui` for new code and migrate root-package call sites opportunistically.
- react-i18next for i18n; zustand for state management
- SWR for data fetching; TRPC for type-safe backend
- Drizzle ORM with PostgreSQL; Vitest for testing
@@ -114,14 +115,23 @@ cd packages/database && bunx vitest run --silent='passed-only' '[file]'
```
- Prefer `vi.spyOn` over `vi.mock`
- Tests must pass type check: `bun run type-check`
- After 2 failed fix attempts, stop and ask for help
### Type Checking
```bash
bun run type-check
```
### i18n
- Add keys to a namespace file under `src/locales/default/` (e.g. `agent.ts`, `auth.ts`)
- For dev preview: translate `locales/zh-CN/` and `locales/en-US/`
- `pnpm i18n`is slow; run it manually when locale keys need updating (e.g. before opening a PR).
- Ship en-US and zh-CN by hand in the same PR: write the English source in `src/locales/default/*.ts` and mirror it to `locales/en-US/`; hand-translate `locales/zh-CN/`. Leave all other locales to CI.
- Don't run`pnpm i18n`manually by default — a daily CI workflow (`auto-i18n.yml`) runs it and opens an automated translation PR for any missing keys.
- Run `pnpm i18n` manually only when your branch needs the translated locales immediately, instead of waiting for the daily job (slow; requires `OPENAI_API_KEY`). Note it only fills keys missing from other locales — value-only edits never need it.
### Code Style
- When a single file grows beyond \~800 lines, consider splitting it into multiple files (extract sub-components, hooks, helpers, or types). Smaller, focused files are friendly to humans and agents.
- **desktop**: show zoom level HUD on Cmd+/- and Cmd+0, closes [#15294](https://github.com/lobehub/lobe-chat/issues/15294) ([109545c](https://github.com/lobehub/lobe-chat/commit/109545c))
@@ -104,9 +85,9 @@ By adopting the Bootstrapping approach, we aim to provide developers and users w
Whether for users or professional developers, LobeHub will be your AI Agent playground. Please be aware that LobeHub is currently under active development, and feedback is welcome for any [issues][issues-link] encountered.
| [](https://www.producthunt.com/products/lobehub?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-lobehub) | We are live on Product Hunt! We are thrilled to bring LobeHub to the world. If you believe in a future where humans and agents co-evolve, please support our journey. |
| [![][discord-shield-badge]][discord-link] | Join our Discord community! This is where you can connect with developers and other enthusiastic users of LobeHub. |
| [](https://www.producthunt.com/products/lobehub?launch=lobehub-2&embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-lobehub) | We are live on Product Hunt! We are thrilled to bring LobeHub to the world. If you believe in a future where humans and agents co-evolve, please support our journey. |
| [![][discord-shield-badge]][discord-link] | Join our Discord community! This is where you can connect with developers and other enthusiastic users of LobeHub. |
> \[!IMPORTANT]
>
@@ -130,7 +111,26 @@ Today’s agents are one-off, task-driven tools. They lack context, live in isol
LobeHub is a work-and-lifestyle space to find, build, and collaborate with agent teammates that grow with you. In LobeHub, we treat **Agents as the unit of work**, providing an infrastructure where humans and agents co-evolve.
@@ -175,113 +179,7 @@ The best AI is one that understands you deeply. LobeHub features **Personal Memo
- **Continual Learning**: Your agents learn from how you work, adapting their behavior to act at the right moment.
- **White-Box Memory**: We believe in transparency. Your agents use structured, editable memory, giving you full control over what they remember.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
<details>
<summary>More Features</summary>
![][image-feat-mcp]
### MCP Plugin One-Click Installation
**Seamlessly Connect Your AI to the World**
Unlock the full potential of your AI by enabling smooth, secure, and dynamic interactions with external tools, data sources, and services. LobeHub's MCP (Model Context Protocol) plugin system breaks down the barriers between your AI and the digital ecosystem, allowing for unprecedented connectivity and functionality.
Transform your conversations into powerful workflows by connecting to databases, APIs, file systems, and more. Experience the freedom of AI that truly understands and interacts with your world.
[![][back-to-top]](#readme-top)
![][image-feat-mcp-market]
### MCP Marketplace
**Discover, Connect, Extend**
Browse a growing library of MCP plugins to expand your AI's capabilities and streamline your workflows effortlessly. Visit [lobehub.com/mcp](https://lobehub.com/mcp) to explore the MCP Marketplace, which offers a curated collection of integrations that enhance your AI's ability to work with various tools and services.
From productivity tools to development environments, discover new ways to extend your AI's reach and effectiveness. Connect with the community and find the perfect plugins for your specific needs.
[![][back-to-top]](#readme-top)
![][image-feat-desktop]
### Desktop App
**Peak Performance, Zero Distractions**
Get the full LobeHub experience without browser limitations—comprehensive, focused, and always ready to go. Our desktop application provides a dedicated environment for your AI interactions, ensuring optimal performance and minimal distractions.
Experience faster response times, better resource management, and a more stable connection to your AI assistant. The desktop app is designed for users who demand the best performance from their AI tools.
[![][back-to-top]](#readme-top)
![][image-feat-web-search]
### Smart Internet Search
**Online Knowledge On Demand**
With real-time internet access, your AI keeps up with the world—news, data, trends, and more. Stay informed and get the most current information available, enabling your AI to provide accurate and up-to-date responses.
Access live information, verify facts, and explore current events without leaving your conversation. Your AI becomes a gateway to the world's knowledge, always current and comprehensive.
[![][back-to-top]](#readme-top)
[![][image-feat-cot]][docs-feat-cot]
### [Chain of Thought][docs-feat-cot]
Experience AI reasoning like never before. Watch as complex problems unfold step by step through our innovative Chain of Thought (CoT) visualization. This breakthrough feature provides unprecedented transparency into AI's decision-making process, allowing you to observe how conclusions are reached in real-time.
By breaking down complex reasoning into clear, logical steps, you can better understand and validate the AI's problem-solving approach. Whether you're debugging, learning, or simply curious about AI reasoning, CoT visualization transforms abstract thinking into an engaging, interactive experience.
[![][back-to-top]](#readme-top)
[![][image-feat-branch]][docs-feat-branch]
### [Branching Conversations][docs-feat-branch]
Introducing a more natural and flexible way to chat with AI. With Branch Conversations, your discussions can flow in multiple directions, just like human conversations do. Create new conversation branches from any message, giving you the freedom to explore different paths while preserving the original context.
Choose between two powerful modes:
- **Continuation Mode:** Seamlessly extend your current discussion while maintaining valuable context
- **Standalone Mode:** Start fresh with a new topic based on any previous message
This groundbreaking feature transforms linear conversations into dynamic, tree-like structures, enabling deeper exploration of ideas and more productive interactions.
[![][back-to-top]](#readme-top)
[![][image-feat-artifacts]][docs-feat-artifacts]
### [Artifacts Support][docs-feat-artifacts]
Experience the power of Claude Artifacts, now integrated into LobeHub. This revolutionary feature expands the boundaries of AI-human interaction, enabling real-time creation and visualization of diverse content formats.
Create and visualize with unprecedented flexibility:
- Generate and display dynamic SVG graphics
- Build and render interactive HTML pages in real-time
- Produce professional documents in multiple formats
LobeHub supports file upload and knowledge base functionality. You can upload various types of files including documents, images, audio, and video, as well as create knowledge bases, making it convenient for users to manage and search for files. Additionally, you can utilize files and knowledge base features during conversations, enabling a richer dialogue experience.
@@ -289,277 +187,6 @@ LobeHub supports file upload and knowledge base functionality. You can upload va
</div>
[![][image-feat-privoder]][docs-feat-provider]
### [Multi-Model Service Provider Support][docs-feat-provider]
In the continuous development of LobeHub, we deeply understand the importance of diversity in model service providers for meeting the needs of the community when providing AI conversation services. Therefore, we have expanded our support to multiple model service providers, rather than being limited to a single one, in order to offer users a more diverse and rich selection of conversations.
In this way, LobeHub can more flexibly adapt to the needs of different users, while also providing developers with a wider range of choices.
#### Supported Model Service Providers
We have implemented support for the following model service providers:
<!-- PROVIDER LIST -->
<details><summary><kbd>See more providers (+-10)</kbd></summary>
</details>
> 📊 Total providers: [<kbd>**0**</kbd>](https://lobechat.com/discover/providers)
<!-- PROVIDER LIST -->
At the same time, we are also planning to support more model service providers. If you would like LobeHub to support your favorite service provider, feel free to join our [💬 community discussion](https://github.com/lobehub/lobehub/discussions/1284).
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-local]][docs-feat-local]
### [Local Large Language Model (LLM) Support][docs-feat-local]
To meet the specific needs of users, LobeHub also supports the use of local models based on [Ollama](https://ollama.ai), allowing users to flexibly use their own or third-party models.
> \[!TIP]
>
> Learn more about [📘 Using Ollama in LobeHub][docs-usage-ollama] by checking it out.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-vision]][docs-feat-vision]
### [Model Visual Recognition][docs-feat-vision]
LobeHub now supports OpenAI's latest [`gpt-4-vision`](https://platform.openai.com/docs/guides/vision) model with visual recognition capabilities,
a multimodal intelligence that can perceive visuals. Users can easily upload or drag and drop images into the dialogue box,
and the agent will be able to recognize the content of the images and engage in intelligent conversation based on this,
creating smarter and more diversified chat scenarios.
This feature opens up new interactive methods, allowing communication to transcend text and include a wealth of visual elements.
Whether it's sharing images in daily use or interpreting images within specific industries, the agent provides an outstanding conversational experience.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-tts]][docs-feat-tts]
### [TTS & STT Voice Conversation][docs-feat-tts]
LobeHub supports Text-to-Speech (TTS) and Speech-to-Text (STT) technologies, enabling our application to convert text messages into clear voice outputs,
allowing users to interact with our conversational agent as if they were talking to a real person. Users can choose from a variety of voices to pair with the agent.
Moreover, TTS offers an excellent solution for those who prefer auditory learning or desire to receive information while busy.
In LobeHub, we have meticulously selected a range of high-quality voice options (OpenAI Audio, Microsoft Edge Speech) to meet the needs of users from different regions and cultural backgrounds.
Users can choose the voice that suits their personal preferences or specific scenarios, resulting in a personalized communication experience.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-t2i]][docs-feat-t2i]
### [Text to Image Generation][docs-feat-t2i]
With support for the latest text-to-image generation technology, LobeHub now allows users to invoke image creation tools directly within conversations with the agent. By leveraging the capabilities of AI tools such as [`DALL-E 3`](https://openai.com/dall-e-3), [`MidJourney`](https://www.midjourney.com/), and [`Pollinations`](https://pollinations.ai/), the agents are now equipped to transform your ideas into images.
This enables a more private and immersive creative process, allowing for the seamless integration of visual storytelling into your personal dialogue with the agent.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-plugin]][docs-feat-plugin]
### [Plugin System (Function Calling)][docs-feat-plugin]
The plugin ecosystem of LobeHub is an important extension of its core functionality, greatly enhancing the practicality and flexibility of the LobeHub assistant.
By utilizing plugins, LobeHub assistants can obtain and process real-time information, such as searching for web information and providing users with instant and relevant news.
In addition, these plugins are not limited to news aggregation, but can also extend to other practical functions, such as quickly searching documents, generating images, obtaining data from various platforms like Bilibili, Steam, and interacting with various third-party services.
> \[!TIP]
>
> Learn more about [📘 Plugin Usage][docs-usage-plugin] by checking it out.
| [Shopping tools](https://lobechat.com/discover/plugin/ShoppingTools)<br/><sup>By **shoppingtools** on **2026-01-12**</sup> | Search for products on eBay & AliExpress, find eBay events & coupons. Get prompt examples.<br/>`shopping``e-bay``ali-express``coupons` |
| [SEO Assistant](https://lobechat.com/discover/plugin/seo_assistant)<br/><sup>By **webfx** on **2026-01-12**</sup> | The SEO Assistant can generate search engine keyword information in order to aid the creation of content.<br/>`seo``keyword` |
| [Video Captions](https://lobechat.com/discover/plugin/VideoCaptions)<br/><sup>By **maila** on **2025-12-13**</sup> | Convert Youtube links into transcribed text, enable asking questions, create chapters, and summarize its content.<br/>`video-to-text``youtube` |
| [WeatherGPT](https://lobechat.com/discover/plugin/WeatherGPT)<br/><sup>By **steven-tey** on **2025-12-13**</sup> | Get current weather information for a specific location.<br/>`weather` |
> 📊 Total plugins: [<kbd>**40**</kbd>](https://lobechat.com/discover/plugins)
<!-- PLUGIN LIST -->
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-agent]][docs-feat-agent]
### [Agent Market (GPTs)][docs-feat-agent]
In LobeHub Agent Marketplace, creators can discover a vibrant and innovative community that brings together a multitude of well-designed agents,
which not only play an important role in work scenarios but also offer great convenience in learning processes.
Our marketplace is not just a showcase platform but also a collaborative space. Here, everyone can contribute their wisdom and share the agents they have developed.
> \[!TIP]
>
> By [🤖/🏪 Submit Agents][submit-agents-link], you can easily submit your agent creations to our platform.
> Importantly, LobeHub has established a sophisticated automated internationalization (i18n) workflow,
> capable of seamlessly translating your agent into multiple language versions.
> This means that no matter what language your users speak, they can experience your agent without barriers.
> \[!IMPORTANT]
>
> We welcome all users to join this growing ecosystem and participate in the iteration and optimization of agents.
> Together, we can create more interesting, practical, and innovative agents, further enriching the diversity and practicality of the agent offerings.
| [Turtle Soup Host](https://lobechat.com/discover/assistant/lateral-thinking-puzzle)<br/><sup>By **[CSY2022](https://github.com/CSY2022)** on **2025-06-19**</sup> | A turtle soup host needs to provide the scenario, the complete story (truth of the event), and the key point (the condition for guessing correctly).<br/>`turtle-soup``reasoning``interaction``puzzle``role-playing` |
| [Academic Writing Assistant](https://lobechat.com/discover/assistant/academic-writing-assistant)<br/><sup>By **[swarfte](https://github.com/swarfte)** on **2025-06-17**</sup> | Expert in academic research paper writing and formal documentation<br/>`academic-writing``research``formal-style` |
| [Minecraft Senior Developer](https://lobechat.com/discover/assistant/java-development)<br/><sup>By **[iamyuuk](https://github.com/iamyuuk)** on **2025-06-17**</sup> | Expert in advanced Java development and Minecraft mod and server plugin development<br/>`development``programming``minecraft``java` |
> 📊 Total agents: [<kbd>**505**</kbd> ](https://lobechat.com/discover/assistants)
<!-- AGENT LIST -->
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-database]][docs-feat-database]
### [Support Local / Remote Database][docs-feat-database]
LobeHub supports the use of both server-side and local databases. Depending on your needs, you can choose the appropriate deployment solution:
- **Local database**: suitable for users who want more control over their data and privacy protection. LobeHub uses CRDT (Conflict-Free Replicated Data Type) technology to achieve multi-device synchronization. This is an experimental feature aimed at providing a seamless data synchronization experience.
- **Server-side database**: suitable for users who want a more convenient user experience. LobeHub supports PostgreSQL as a server-side database. For detailed documentation on how to configure the server-side database, please visit [Configure Server-side Database](https://lobehub.com/docs/self-hosting/advanced/server-database).
Regardless of which database you choose, LobeHub can provide you with an excellent user experience.
LobeHub supports multi-user management and provides flexible user authentication solutions:
- **Better Auth**: LobeHub integrates `Better Auth`, a modern and flexible authentication library that supports multiple authentication methods, including OAuth, email login, credential login, magic links, and more. With `Better Auth`, you can easily implement user registration, login, session management, social login, multi-factor authentication (MFA), and other functions to ensure the security and privacy of user data.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-pwa]][docs-feat-pwa]
### [Progressive Web App (PWA)][docs-feat-pwa]
We deeply understand the importance of providing a seamless experience for users in today's multi-device environment.
Therefore, we have adopted Progressive Web Application ([PWA](https://support.google.com/chrome/answer/9658361)) technology,
a modern web technology that elevates web applications to an experience close to that of native apps.
Through PWA, LobeHub can offer a highly optimized user experience on both desktop and mobile devices while maintaining high-performance characteristics.
Visually and in terms of feel, we have also meticulously designed the interface to ensure it is indistinguishable from native apps,
providing smooth animations, responsive layouts, and adapting to different device screen resolutions.
> \[!NOTE]
>
> If you are unfamiliar with the installation process of PWA, you can add LobeHub as your desktop application (also applicable to mobile devices) by following these steps:
>
> - Launch the Chrome or Edge browser on your computer.
> - Visit the LobeHub webpage.
> - In the upper right corner of the address bar, click on the <kbd>Install</kbd> icon.
> - Follow the instructions on the screen to complete the PWA Installation.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-mobile]][docs-feat-mobile]
### [Mobile Device Adaptation][docs-feat-mobile]
We have carried out a series of optimization designs for mobile devices to enhance the user's mobile experience. Currently, we are iterating on the mobile user experience to achieve smoother and more intuitive interactions. If you have any suggestions or ideas, we welcome you to provide feedback through GitHub Issues or Pull Requests.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
[![][image-feat-theme]][docs-feat-theme]
### [Custom Themes][docs-feat-theme]
As a design-engineering-oriented application, LobeHub places great emphasis on users' personalized experiences,
hence introducing flexible and diverse theme modes, including a light mode for daytime and a dark mode for nighttime.
Beyond switching theme modes, a range of color customization options allow users to adjust the application's theme colors according to their preferences.
Whether it's a desire for a sober dark blue, a lively peach pink, or a professional gray-white, users can find their style of color choices in LobeHub.
> \[!TIP]
>
> The default configuration can intelligently recognize the user's system color mode and automatically switch themes to ensure a consistent visual experience with the operating system.
> For users who like to manually control details, LobeHub also offers intuitive setting options and a choice between chat bubble mode and document mode for conversation scenarios.
<div align="right">
[![][back-to-top]](#readme-top)
</div>
### `*` What's more
Beside these features, LobeHub also have much better basic technique underground:
- [x] 💨 **Quick Deployment**: Using the Vercel platform or docker image, you can deploy with just one click and complete the process within 1 minute without any complex configuration.
- [x] 🌐 **Custom Domain**: If users have their own domain, they can bind it to the platform for quick access to the dialogue agent from anywhere.
- [x] 🔒 **Privacy Protection**: All data is stored locally in the user's browser, ensuring user privacy.
- [x] 💎 **Exquisite UI Design**: With a carefully designed interface, it offers an elegant appearance and smooth interaction. It supports light and dark themes and is mobile-friendly. PWA support provides a more native-like experience.
- [x] 🗣️ **Smooth Conversation Experience**: Fluid responses ensure a smooth conversation experience. It fully supports Markdown rendering, including code highlighting, LaTex formulas, Mermaid flowcharts, and more.
</details>
> ✨ more features will be added when LobeHub evolve.
<div align="right">
@@ -855,28 +482,10 @@ This project is [LobeHub Community License](./LICENSE) licensed.
| [SEO 助手](https://lobechat.com/discover/plugin/seo_assistant)<br/><sup>By **webfx** on **2026-01-12**</sup> | SEO 助手可以生成搜索引擎关键词信息,以帮助创建内容。<br/>`seo``关键词` |
| [视频字幕](https://lobechat.com/discover/plugin/VideoCaptions)<br/><sup>By **maila** on **2025-12-13**</sup> | 将 Youtube 链接转换为转录文本,使其能够提问,创建章节,并总结其内容。<br/>`视频转文字``you-tube` |
| [天气 GPT](https://lobechat.com/discover/plugin/WeatherGPT)<br/><sup>By **steven-tey** on **2025-12-13**</sup> | 获取特定位置的当前天气信息。<br/>`天气` |
> 📊 Total plugins: [<kbd>**40**</kbd>](https://lobechat.com/discover/plugins)
"description":"Desensitized golden snapshot of one nightly-review self-iteration run. Used as a structural regression baseline by the execAgent migration which converges all agent execution paths (chat, self-iteration, memoryWriter, skillManagement) onto a single execAgent entry point. Assert structure, never byte-for-byte: the LLM output is non-deterministic.",
"finalState":{
"messages":[
{
"content":"Run the nightly self-review for the local window.",
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.