🚀 release: 20260604#15447
Conversation
…ighlight (#15298) * ✨ feat(portal): editable CodeMirror viewer for LocalFile + Document highlight Replace the read-only Highlighter in the LocalFile portal preview and the Document portal highlight mode with a shared `CodeEditorPane` powered by `@lobehub/editor/codemirror`. Pane supports inline editing, Cmd/Ctrl+S to save, lobeTheme tokens, and language-aware syntax highlighting. LocalFile flow - Track per-path edit buffers + save action in the chat portal store (`dirtyLocalFileContents`, `setLocalFileBuffer`, `saveLocalFile`). - Show a filled dot on the tab close button when the file is dirty; hovering still reveals the X. Closing a dirty tab (via X or the context menu's "Close") prompts a confirmation modal via `confirmModal` from `@lobehub/ui/base-ui`. - After save, mutate the SWR cache to the just-saved content before clearing the buffer so CodeMirror does not see a stale `value` prop and reset the cursor. Document flow - For non-markdown documents (`getDocumentRenderMode` → `highlight`), render `CodeEditorPane` with a local edit buffer keyed by `documentId`. - Save calls `documentService.updateDocument({ saveSource: 'manual' })`, mutates the document-meta SWR cache, then clears the buffer. Bump `@lobehub/editor` to ^4.15.0 to pick up the new `@lobehub/editor/codemirror` subpath export. * 🐛 fix(portal): force read-only on truncated local file previews When a file exceeds MAX_PREVIEW_CHARS the preview only holds the first 500k character prefix. Editing and saving against that prefix would silently overwrite the rest of the file with the truncated content. Pass `readOnly={truncated}` to the editor, ignore any stale buffer when truncated, and short-circuit handleSave so Cmd/Ctrl+S is a no-op in this mode. * ♻️ refactor(portal): drop MAX_PREVIEW_CHARS truncation for local files Always pass the full file content to the editor instead of slicing at 500k characters. The truncation existed only to avoid losing data when saving the previously-Highlighter-rendered prefix, but with full content available the editor can both display and persist the file safely. Removes the `truncated` / `truncatedLabel` plumbing, the truncated banner, and the associated read-only short-circuit in handleSave. * ✅ test(portal): update document body highlight editor test
`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.
…message (#15303) * 🐛 fix(conversation): keep open ActionBar popup when hovering another message When a dropdown inside the singleton message ActionBar is open, hovering another message used to move the singleton host's DOM and swap the rendered actionType, which unanchored or unmounted the open popup. Freeze both the host placement target and the rendered actionType while any descendant has `data-popup-open`, and re-commit the latest live values once the popup closes (observed via MutationObserver). * ♻️ refactor(conversation): freeze message ActionBar subtree while popup is open Replace the manual committed-state freeze with `@lobehub/ui` `Freeze`: split the host migration effect + portal render into `ActionBarBody`, and wrap it with `<Freeze frozen={isPopupOpen}>` in `SingletonMessageActionsBar`. While any descendant of the host has `data-popup-open`, the inner body is suspended — its migration effect doesn't run and its render is paused, so hovering another message no longer DOM-moves the trigger or unmounts the dropdown's React subtree. Once the popup closes, the body resumes with the latest live `actionType` / `portalElement` and migrates the host normally. * Revert "♻️ refactor(conversation): freeze message ActionBar subtree while popup is open" This reverts commit a8d47be.
…witcher lab (#15315) Add a "Devices" tab under the General settings group (above Hotkeys) that lists the user's registered devices. Each device is keyed by deviceId; the gateway's live WS connections are nested as channel rows under their device rather than shown as separate devices. The tab is gated behind the `enableExecutionDeviceSwitcher` lab flag. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…5290) 🐛 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>
✨ 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>
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.
…vice list (#15322) * ✨ feat(device): connectionId + channel routing in gateway client & device list Shared client + server + settings-UI half of decoupling the gateway connection routing key from the stable deviceId (the gateway DO change lives in the device-gateway repo). - GatewayClient gains `connectionId` (per-install routing UUID) + `channel` (freeform label) options, both sent on the WS URL; `currentConnectionId` getter - consume the gateway's device-centric `/api/device/devices` shape: deviceProxy maps it to runtime devices + nested channels (tolerant of a legacy flat shape via `?? []`); device.listDevices flattens channels; DeviceItem shows the label Part of LOBE-9572. Closes LOBE-9781. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🔥 chore(device): remove unused in-repo apps/device-gateway `apps/device-gateway` was a stale, non-deployed mirror of the device-gateway Cloudflare worker (the real one lives in its own repo and already diverged — it has AdminDO / geo / message-api / the tool-call-timeout refactor this copy never got, and no CI here deploys this directory). Keeping it around just makes the in-repo gateway look like it ignores the connectionId/channel this client now sends. Drop it; the gateway contract is owned by the service repo. - delete apps/device-gateway/** - drop its tsconfig `exclude` entry - retarget the protocol-mirror comment in device-gateway-client to the service 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
…#15326) The S3 publish action was missing *.blockmap from its upload glob, causing electron-updater to always fall back to full downloads.
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`.
…15309) * ✨ feat(page-share): add document share flow * ✨ improve page share probe fallback * ♻️ refactor(page-share): extract to business slot stubs * ♻️ refactor(page-share): move shared-page viewer to /share/page/:id - Drop anonymous handling on /page/:id: 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.
…umb fetch (#15335) Resource Explorer kept showing the previous folder's items when sidebar hierarchy clicks switched the URL slug. SWR `onSuccess` only fires after revalidate completes, so cache-hit navigations could not update the zustand mirror that the Explorer reads from. - Move SWR data → store sync into a `useEffect` so cache hits also push fresh items into `useFileStore` immediately, while keeping the 30s deduping window to avoid wasted background revalidations. - Reuse the Breadcrumb SWR cache in `LibraryHierarchy`: replace `tree.navigateTo(slug)` (which fetched the breadcrumb directly) with `tree.expandAncestors(ids)`, and let `useFetchFolderBreadcrumb` feed the ids so a folder switch no longer issues two parallel `document.getFolderBreadcrumb` requests. Fixes LOBE-4293
* feat(cli): add `lh topic view` command to display topic details and messages * test(cli): add unit tests for `lh topic view` command * fix(cli): improve topic view - fix --no-messages bug, add tool calls, threads, pagination * test(cli): update view tests - fix mock, add tool/thread/pagination cases * feat(topic): add getTopicDetail trpc procedure for structured topic metadata * refactor(cli): use getTopicDetail for view command metadata, show full fields * test(cli): update view tests to use getTopicDetail mock
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>
…ard (#15342) * 💄 style(imessage): wrap BlueBubbles bridge config into a connection card Regroup the iMessage BlueBubbles bridge settings into a single bordered card with a clearer top status / middle form / bottom action layout: - Header shows the connection title + overall test status badge (Pending Test / Connected / Failed), with breathing room before the form fields. - Server URL field gains an inline hint box (127.0.0.1 vs LAN IP). - A full-width bridge service bar at the bottom: running/stopped status with the listening address on the left, the primary Enable Bridge toggle on the right, and the less-frequent Refresh / Test actions on a second row. Test status is tracked locally and reset on any field edit so the badge never shows a stale pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 💄 style(claude-code): fix WebFetch inspector URL truncation and align chip with Bash Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 💄 style(imessage): use BlueBubbles logo for the bridge status icon Swap the generic plug glyph for the BlueBubbles app logo so the bridge service card reads more recognizably. The icon sits in a white rounded tile; the running state is already conveyed by the Running tag. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 💄 style(imessage): move BlueBubbles logo to the connection header Promote the BlueBubbles logo next to the section title so it identifies the integration up front, and drop the icon tile from the bridge service row — the running/stopped state reads fine as text + status tag there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 💄 style(imessage): enlarge bridge logo, fix disabled status, clarify relay copy - Logo now spans both header lines (44px) for a stronger section anchor. - Bridge status reflects this config's Enable toggle (running && enabled), so flipping it off no longer keeps showing "Running" until the next save. - Service descriptions now explain the bridge relays iMessage messages to LobeHub, so the local server's purpose is clear. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ♻️ refactor(imessage): make Electron main the SoT for the bridge status Read the bridge status via SWR (revalidates on focus + after each mutation) instead of caching a divergent copy, and drop the manual Refresh button. - `enabled` / `running` / `serverUrl` / `passwordSet` now derive from the main-process status, not local form state. - Enable is a write-through toggle: it auto-persists the current Server URL + password and starts/stops the bridge immediately (option B), surfacing real connection errors on enable. - Test is ungated from enable — it pings BlueBubbles directly and only needs a Server URL + password. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ne ID (#15300) ✨ feat(device): auto-register desktop & CLI devices; send connectionId + channel App layer — wires desktop and `lh connect` to the device registry and the connection-routing scheme. Depends on @lobechat/device-identity and the gateway-client connectionId/channel options (earlier PRs in this stack), plus the device.register / listDevices endpoints (already on canary). - desktop derives the stable deviceId on gateway connect (old per-install random UUID demoted to the routing `connectionId`), registers via device.register, and tags channel `desktop` / `desktop-dev` - `lh connect` derives + registers before opening the WS (explicit --device-id still pins a VM); channel `cli` (env-overridable); connectionId persisted in `~/.lobehub/connection-id` - CLI api client preserves explicit --token connects during registration Part of LOBE-9572. Closes LOBE-9576 / LOBE-9577. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ 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>
* style: add step-3.7-flash support * chore: support step-3.5 reasoning effort
…15351) Add a full-width "Add directory" button to pick a folder via the native picker, make the recent directories list reorderable via SortableList, and drop the Save button so all device edits (name, default cwd, recent dirs) persist immediately. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…variables (#15352) * 🐛 fix(creds): replace hardcoded session_context values with template variables - Replace hardcoded `Current user`, `Session date`, `Sandbox mode` in systemRole.ts with {{username}}, {{session_date}}, {{sandbox_enabled}} - Inject {{session_date}} via Intl.DateTimeFormat in RuntimeExecutors - Remove isCredsEnabled gate so {{CREDS_LIST}} / {{KLAVIS_SERVICES_LIST}} are always substituted when userId is available, regardless of execution path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🧪 test(creds): mock klavisEnv to prevent t3-oss jsdom throws in tests klavisEnv uses @t3-oss/env-nextjs which throws in jsdom (vitest treats it as a client context). Previously the isCredsEnabled gate short-circuited before the access; now that the gate is removed, the mock is needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🐛 fix(creds): add client-side generators and restore isCredsEnabled gate - Add session_date and sandbox_enabled variable generators to contextEngineering.ts so client-side renders substitute them correctly - Restore isCredsEnabled gate in RuntimeExecutors to avoid fetching creds on every call_llm step; now checks both enabledToolIds (client-activated path) and manifestMap (execAgent path) to cover all execution paths Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * 🔨 chore(creds): revert isCredsEnabled gate in RuntimeExecutors Remove the isCredsEnabled OR-condition that caused execAgent test failures. Keep session_date, sandbox_enabled, and always-inject CREDS_LIST/KLAVIS_SERVICES_LIST. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…one (#15355) When a stalled tool loop made the model return an empty completion (no content, no tool calls, ~0 output tokens), the harness finalized the operation to `done` and persisted a blank assistant message — an empty bubble with `status=done, error=null`, completely silent. The call_llm executor now detects this "gave up" turn and throws `ModelEmptyError`, which its existing LLM retry loop catches and re-issues (a retry usually yields real content). Empty completions use a dedicated retry budget (EMPTY_COMPLETION_MAX_RETRIES) so the branded provider — which has 0 general retries because its own fallback chain re-routes failed requests — still re-issues an HTTP-200-but-empty turn (the LOBE-9834 repro path). If every retry is also empty, it propagates to a readable, dashboard-visible terminal error (`ModelEmptyCompletion`, E8014, provider attribution, countAsFailure) instead of a silent done. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* ✨ 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>
chore: remove LOBE-XXX markers from code comments - match.test.ts: replace (LOBE-9913) marker with inline comment context - nightly-review.golden.json: replace (LOBE-9434) marker with execAgent migration context Co-authored-by: Arvin Xu <arvin@lobehub.com>
* 📝 docs: add code style guidance for hook extraction and file splitting * 📝 docs: tighten file-splitting guidance * 📝 docs: clarify agent guidance wording
* ✨ feat(minimax): add MiniMax M3 model with pricing and update tests * Update minimax.ts * fix test
… + completion projection + async memoryWriter + executeSelfIteration removal (#15392) * 🚧 wip(agent-signal): S1 — self-iteration tools as static primitives, no side-channel Rewrite all three self-iteration execAgent tool surfaces (review / reflection / feedback-intent) as static, named primitives instead of reusing the dynamic createServerToolSet / createToolSet factory (which carries the legacy reserveOperation / receipt / completeOperation side channel the migration removes). Package (builtin-tool-agent-signal): - AgentSignalToolService.invoke (generic bag) → AgentSignalRuntimeService, a narrow named DB-primitive seam (skillManagement precedent). Artifact recorders echo their input; reads/mutations route to one primitive each. The runtime carries no dedupe / receipt / operation-state side channel — idempotency and receipt projection live on the completion path, not the tool call. Server primitives (pure live-DB reads + writes, keyed to api names): - review/server.ts createReviewRuntimePrimitives — proposal lifecycle + resource tools, parameterized by window scalars from the operation marker, reusing the existing snapshot/preflight/projection/brief helpers. - tools/runtimePrimitives.ts createResourceRuntimePrimitives — the skill-read / skill-write / writeMemory surface shared by reflection and feedback-intent. - No context blob and no getEvidenceDigest: evidence is embedded in the agent prompt, so tools only touch live state. serverRuntimes: agentSignalReview / agentSignalReflection / agentSignalFeedbackIntent thin factories wiring ToolExecutionContext → primitives → package runtime, all registered. createServerToolSet / createToolSet left untouched (legacy executeSelfIteration path, removed in S4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚧 wip(agent-signal): S2 — completion-path receipt projection from finalState Replace the in-runtime receipt accumulator with finalState-driven projection on the completion path. finalState is only in hand inside the completion lifecycle (S3 final snapshots are write-only — get() is a null stub; the operation row has no messages; prod webhook hooks strip finalState), so receipts must be projected from the one point state exists. - CompletionLifecycle.emitSignalEvents: extract the compact, kind-tagged tool outcomes from the terminal state (extractSelfIterationCompletionPayload) and carry them on the agent.execution.completed payload — only for marked self-iteration runs, never the full message history. - completionPolicy: forward the payload to onSelfIterationCompleted. - completion/buildSelfIterationReceipts: project mutations + artifacts into user-visible receipts, mirroring the legacy createReceipts kind/status/target mapping. Deterministic receipt ids (sourceId + tool call id) → idempotent re-projection; the store dedupes by id. - completion/selfIterationCompletionHandler: build + persist receipts. - orchestrator: wire the handler into createDefaultAgentSignalPolicies. - agent-signal source type: add an opaque selfIteration field to the agent.execution.completed payload. Inert until the dispatch side stamps the operation marker (S3 / S4): without a marker the extractor returns undefined and the handler no-ops. Tests: buildSelfIterationReceipts (5) + extractCompletionPayload (4); completion policy + CompletionLifecycle + orchestrator suites green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚧 wip(agent-signal): S3 part 1 — completion-side memory receipt support (inert) Foundation for migrating the memory writer to the async execAgent path: teach the completion path to project a memory receipt from a finished memory-writer run. Inert until the dispatch side stamps a kind:'memory' marker (part 2). - completion routing is now keyed on the operation MARKER (the selfIteration payload), not the agent slug — a memory writer runs as the user's own agent, so a slug check would miss it. completionPolicy gates on payload presence; agentId loosened to string. - extractCompletionPayload: for a kind:'memory' run, synthesize a writeMemory mutation from the run's finalState (the memory builtin tool results are not kind-tagged, so extractMutations finds nothing) via resolveMemoryActionResultFromState. - buildSelfIterationReceipts: a memory run surfaces as just its action receipt, no aggregate review summary. - extract the pure memory finalState parsers into a dependency-light ./memoryActionResult module so the completion lifecycle can reuse them without dragging the heavy memory-runner module (ModelRuntime/AgentService/…) into its graph. userMemory re-exports them for backward compat. - bump a too-tight (5s) timeout on the real-orchestration integration test. Tests: completion (12) + completionPolicy (8) + userMemory (12) green; agentSignal policies + orchestrator suites (138) green; type-check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ♻️ refactor(agent-signal): S3 — migrate memoryWriter to async execAgent + completion receipt Flip the memory-writer action from a blocking executeSync run to an async queued operation (autoStart) stamped with an agent-signal `memory` marker. The user-visible "memory saved" receipt is no longer projected synchronously from the action result — it is projected on the completion path from the run's finalState (extractMemoryMutations → buildSelfIterationReceipts), so the receipt appears a few seconds later once the run completes. - userMemory.ts: add `dispatch` path enqueuing via createOperation(autoStart), stamping appContext.agentSignal so completion can project the receipt. - receiptService.ts: drop the synchronous memory receipt projection (would duplicate the async one, with a premature empty target). - types.ts: add `agentSignal` marker to OperationCreationParams.appContext. - tests: cover the memory-kind completion loop end-to-end (single memory receipt, correct target + anchor, no aggregate summary). Note: the memory run uses createOperation (not execAgent), so it never synthesises a user message and cannot recurse into analyzeIntent — no suppressSignal needed on this path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚧 wip(agent-signal): S4 step 0 — forward agentSignal marker through execAgent Foundation for migrating self-iteration onto execAgent: let a background run carry its agent-signal marker so the S2 completion path can project receipts. - Move AgentSignalOperationMarker / AgentSignalOperationKind into @lobechat/types (ExecAgentAppContext can now reference it); operationMarker.ts re-exports the type and keeps the runtime parse/validate helpers. - ExecAgentAppContext: add `agentSignal?` field. - execAgent: forward `appContext.agentSignal` into createOperation's appContext (it was dropped by the curated passthrough), so it lands in state.metadata.agentSignal — the key the completion extractor reads. No behaviour change yet: nothing sets appContext.agentSignal on the execAgent path until the self-iteration dispatch helper lands. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚧 wip(agent-signal): S4 step 0b — self-iteration execAgent dispatch helper Shared primitive for migrating the 3 self-iteration modes off the hand-rolled runtime onto async execAgent (used by reflection/feedback/nightly-review next). - enqueueSelfIterationRun(): create an isolated thread (when anchored), then execAgent the builtin slug with suppressSignal + the agent-signal marker on appContext, autoStart, headless. Returns immediately (fire-and-forget). - marker: add `agentId` (the reviewed user agent). A slug run resolves the operation agentId to the builtin agent, so receipts must attribute to the reviewed agent carried on the marker. - buildSelfIterationReceipts: attribute to `marker.agentId ?? agentId` (memory runs leave it unset and fall back to the run agentId — unchanged). Not wired into the mode handlers yet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ♻️ refactor(agent-signal): S4 — migrate executeSelfIteration to async execAgent Replace the hand-rolled `executeSelfIteration` runtime (new AgentRuntime + custom call_llm executor + 6 closure side-channels) with the standard async `execAgent` queue path. nightly-review / self-reflection / self-feedback-intent now enqueue via `enqueueSelfIterationRun → execAgent` and project their receipts/briefs on the `agent.execution.completed` completion path. - Delete `execute.ts` (1500 lines) + `execute.test.ts`; gut the three server adapters (review/reflection/feedback) to drop the synchronous run path and the legacy receipt/runtime wiring. - `aiAgent`: background runs execute under a builtin slug but attribute their resource tools + receipts to the *reviewed* user agent via the run marker. - Drop the orchestrator's `writeDailyBrief` default — nightly review writes its brief in-run via the builtin review serverRuntime primitive. - Add `ReviewRunStatus.Dispatched` for enqueued background runs. - Completion-path debug logging across CompletionLifecycle / completionPolicy / completion handler. Part of LOBE-9434 (S4 · LOBE-9876). * 🐛 fix(agent-signal): make execAgent resolve builtin slugs + give self-iteration agents a mini model Live-testing the S4 self-iteration → execAgent path surfaced two gaps that kept background runs (nightly-review / self-reflection / self-feedback-intent) from ever dispatching: - execAgent threw `Agent not found: <slug>` when addressed purely by a builtin slug (the self-iteration dispatch path) because getAgentConfig only resolves persisted rows. Lazily materialize the virtual builtin row via AgentModel.getBuiltinAgent — mirrors the inbox/task precedent — then re-resolve. - The three self-iteration builtin agents had no `persist` model, so runs fell back to the user's default chat model. Give them `persist: { DEFAULT_MINI_MODEL, DEFAULT_MINI_PROVIDER }` (gpt-5.4-mini), matching the legacy executeSelfIteration behavior. Verified live: self-reflection now dispatches, the async operation reaches `done`, and a `review` completion receipt is projected on the completion path. Adds two execAgent.builtinRuntime tests (builtin-slug materialization + unknown-id still throws). Part of LOBE-9434 (S4). * 🚨 fix(agent-signal): use type-only import for createServerSelfReviewBriefWriter After the S4 gutting, review/server.ts only uses createServerSelfReviewBriefWriter in a `ReturnType<typeof ...>` position — split it into a type-only import to satisfy @typescript-eslint/consistent-type-imports (the lone lint:ts error). * 🐛 fix(agent-signal): carry tool apiName in result content so action receipts project The agent runtime persists tool messages with only content/role/tool_call_id (no message-level apiName), so the completion extractor's `message.apiName` read was always undefined in live runs — buildSelfIterationReceipts then dropped every mutation via `if (!apiName) return []`, so durable skill/proposal writes produced no action receipt (only the summary survived; memory was exempt via a hard-coded apiName). Fix the extraction channel, not the shared runtime: - ExecutionRuntime stamps `apiName` into the result content alongside `kind`. - extractFromFinalState reads apiName from the content (message.apiName fallback). Tests reworked to the real persisted shape (apiName in content, no message-level apiName) — the prior mocks hid the bug. Part of LOBE-9434 (S4). * 🐛 fix(agent-signal): persist run marker to operation metadata for server tools Self-iteration server tools (nightly-review etc.) read the run marker from `agent_operations.metadata` via readAgentSignalMarker, but recordStart only persisted a trimmed appContext and never wrote metadata — so in live runs the marker was always undefined and review/proposal writes fell back to a 1970 window/localDate + operationId source (non-idempotent). recordStart now persists `metadata: { agentSignal }` from appContext.agentSignal, so the tool path matches the completion path (which reads it from finalState). Part of LOBE-9434 (S4). --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…et (#15435) * ⚡️ perf(device): preset local device on first LLM request for 本机 target When the desktop runs an agent against the local machine (executionTarget 'local'), resolve this desktop's own gateway deviceId client-side and pass it as the run's `deviceId`. The server then presets `activeDeviceId` and injects `lobe-local-system` into the very first LLM payload, skipping the extra `activateDevice` round-trip the model was forced to make whenever more than one device was online. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ✅ test(device): cover local deviceId resolution in executeGatewayAgent Asserts the client forwards this desktop's deviceId only for the local (本机) target — including the unset-on-desktop fallback — and never for sandbox, explicit remote device, or off-desktop runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(device): gate local-device binding on effective runtime mode `resolveLocalDeviceId` defaulted an unset `agencyConfig.executionTarget` to 'local' and sent this desktop's deviceId. But the legacy ModeSelector writes only `runtimeMode`, leaving executionTarget unset — so an explicit cloud/none run would still get a deviceId, which the server turns into activeDeviceId and injects lobe-local-system, wrongly routing a cloud run to the local machine. Gate on `isLocalSystemEnabledById` (effective runtimeMode === 'local'), the source of truth both selectors agree on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🚨 fix(test): use import-type alias instead of inline import() type Satisfies @typescript-eslint/consistent-type-imports (CI lint). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 💄 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>
…on (#15427) * 🐛 fix(agent-doc): default new files to .md and preserve IME composition - Append `.md` to newly-created agent documents; pre-select only the stem in the inline rename input so the extension stays intact. - Wire `useIMECompositionEvent` on the explorer container so Enter pressed during IME composition (e.g. Chinese pinyin) no longer commits the half-formed name through pierre/trees' shadow-DOM input. * 🐛 fix(agent-doc): use native capture listener for IME guard React `onKeyDownCapture` can lose to pierre/trees' bubble handler in some event ordering edge cases, and the original guard missed IMEs that report `keyCode === 229` or fire Enter just after compositionend in the same task. - Bind a native `keydown` capture listener on the container so we can inspect `composedPath()` and confirm the keydown originated inside the shadow-DOM rename input. - Extend the IME guard with an `imeSessionRef` that stays true through one extra microtask after compositionend. - Drop the React `onKeyDownCapture` prop in favour of the native listener. * ⏪ revert(agent-doc): drop IME guard pending pierre/trees upstream fix The inline rename input lives in pierre/trees' shadow DOM and we can't reliably suppress its IME-composing Enter commit from the outside. Roll back the local hack and track the issue upstream instead. The default `.md` extension and stem-only selection on rename stay in place. * ✨ feat(agent-doc): preselect stem on inline rename too Existing files entering inline rename (right-click → Rename, or F2) now narrow the selection to the stem after pierre/trees' `input.select()`, matching the new-file flow so the user never has to retype `.md`. * 🐛 fix(agent-doc): preserve extension on filename collisions
…5437) * ✨ 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>
… flows (#15440) 🚸 fix(ui): restructure confirmModal title and content across deletion flows Move long warning sentences from `title` into `content` and use short verb titles ("Delete", "Uninstall", "Wipe Data", etc.). Add `okText`/`cancelText` i18n for all fixed sites so confirm buttons match the action language. Covers topic/thread/agent/group/library/file/model/skill/storage delete flows.
* ✨ 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>
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>
…ndary (#15438) A fast hetero-agent (Claude Code) tool can have its parent assistant's `tools[]` momentarily dropped (stale/out-of-order `replaceMessages` snapshot, or an optimistic `updateMessage{tools}` on the wrong assistant during a step boundary) while the `role:'tool'` row + parentId survive. Since conversation- flow binds a tool into its assistant solely via `assistant.tools[].id`, the tool then renders as a top-level orphan bubble (`inspector.orphanedToolCall`). Fix at the RAW `dbMessagesMap` write boundary — shared by `replaceMessages` and `internal_dispatchMessage` (the optimistic-update path) — so the Source of Truth stays consistent for optimistic updates, not just the parsed display. `reconcileAssistantToolLinks` re-attaches the missing `tools[]` entry for any present tool row whose parentId resolves to an assistant in the same bucket; it only acts on present rows (never resurrects deletions) and never removes or reorders entries. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#15442) * 🐛 fix(cli): preserve content/state for connect local file/shell tools Route file/shell tool calls in connect mode through LocalSystemExecutionRuntime so the result carries formatted prompt `content` plus structured `state`, and forward `state` over the gateway tool-call response — aligning the CLI with the desktop gateway path (PR #15114). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(cli): preserve getCommandOutput timeout when polling running commands Routing getCommandOutput through the runtime dropped the per-call/gateway timeout: the CLI mapping didn't forward it and LocalSystemExecutionRuntime's denormalizeParams stripped it before ShellProcessManager.getOutput, so polling fell back to the 30s default and could block past the gateway budget. Carry timeout through the runtime param type, denormalize, and the CLI mapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72ea0f94f7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const handleSubmit = useCallback(async () => { | ||
| const instruction = instructionRef.current.trim(); | ||
| if (!instruction && !title.trim()) return; | ||
| const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; | ||
| if (!instruction && !title.trim() && !hasFiles) return; |
There was a problem hiding this comment.
Keep task instruction non-empty for file-only creates
When the modal has attachments but no title/instruction, this guard now allows submission, but the payload later sends instruction: instruction || title.trim(), i.e. an empty string. The task.create router still validates instruction with z.string().min(1), so attachment-only task creation reaches the backend and is rejected instead of creating the task with the attached editor data.
Useful? React with 👍 / 👎.
| const json = editor?.getDocument?.('json') as unknown; | ||
| const markdown = String(editor?.getDocument?.('markdown') ?? '').trim(); | ||
| const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; | ||
| if (!markdown && !hasFiles) return; |
There was a problem hiding this comment.
Avoid sending empty comment content with attachments
For an attachment-only comment, this new condition permits submission, but addComment(taskId, markdown, ...) is called with markdown === ''. The lambda router still requires content: z.string().min(1) for addComment, so users who attach a file without text will hit a validation failure and lose the intended comment path.
Useful? React with 👍 / 👎.
| const json = editor?.getDocument?.('json') as unknown; | ||
| const markdown = String(editor?.getDocument?.('markdown') ?? '').trim(); | ||
| const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; | ||
| if (!markdown && !hasFiles) return; |
There was a problem hiding this comment.
Avoid sending empty feedback content with attachments
This feedback drawer now treats attachments as enough to submit, but the call below still sends the trimmed markdown as content; with only files that is ''. Since the shared addComment endpoint validates content with z.string().min(1), attachment-only feedback comments are rejected before the server can extract the files from editorData.
Useful? React with 👍 / 👎.
| if (!next || submitting) return; | ||
| const json = editor?.getDocument?.('json') as unknown; | ||
| const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; | ||
| if (!next && !hasFiles) return; |
There was a problem hiding this comment.
Avoid saving attachment-only edits as empty content
When editing a comment, this path now allows saving if files are present even when next is empty, but updateComment(commentId, next, ...) sends that empty string to a router schema that still requires content: z.string().min(1). Deleting the text and keeping/adding an attachment therefore fails validation instead of saving the attachment-only edit.
Useful? React with 👍 / 👎.
🐳 Database Docker Build Completed!Version: Pull ImageDownload the Docker image to your local machine: docker pull lobehub/lobehub:pr-release-weekly-20260604-5bf7a60Important This build is for testing and validation purposes. |
🚀 Desktop App Build Completed!Version: 📦 Release Download · 📥 Actions Artifacts Build Artifacts
Warning Note: This is a temporary build for testing purposes only. |
🚀 LobeHub Release (20260604)
Release Date: June 4, 2026
Since v2.2.1: 88 merged PRs · 11 contributors
✨ Highlights
🏗️ Core Agent & Architecture
Agent Signal & Runtime
executeSelfIterationpath. (♻️ refactor(agent-signal): execAgent migration — serverRuntime bridge + completion projection + async memoryWriter + executeSelfIteration removal #15392)ConversationParentMissingfor clearer recovery. (🐛 fix(agent-runtime): classify topic/agent/session FK violations as ConversationParentMissing #15408)createAgentagainst LLM double-encoded array fields. (🐛 fix(agent-manager): guard createAgent against LLM double-encoded array fields #15381)🖥️ Execution Devices & Gateway
@lobechat/device-identitypackage. (✨ feat(device): auto-register desktop & CLI devices with stable machine ID #15300, ✨ chore(device): add@lobechat/device-identity#15321)connectionId+ channel routing across the gateway client and device list; preset the local device on the first LLM request for the 本机 target. (✨ feat(device): connectionId + channel routing in gateway client & device list #15322, ⚡️ perf(device): preset local device on first LLM request for 本机 target #15435)🖥️ CLI & Desktop
runCommandtool result card. (🐛 fix(github): render runCommand tool result card #15441, 🐛 fix(cli): preserve content/state for connect local file/shell tools #15442)lh topic viewcommand; CLI now auto-registers its device on login, matching desktop. (feat(cli): addlh topic viewcommand #15340, 🐛 fix(cli): auto-register device on login, matching desktop #15377)PATH, and clarify local command session handling. (fix(desktop): resolve CLI tools from shell PATH #15368, 🐛 fix: clarify local command session handling #15389)@lobechat/constto fix a renderer crash; upload.blockmapfiles to S3 for differential updates. (🐛 fix(desktop): upload .blockmap files to S3 for differential updates #15326, 🐛 fix(desktop): relocate visual-ref helpers to @lobechat/const to fix renderer crash #15369)🗂️ Pages, Library & Knowledge
.mdand preserve IME composition; refresh folder data on slug switch and dedupe breadcrumb fetches. (🐛 fix(library): refresh folder data on slug switch and dedupe breadcrumb fetch #15335, 🐛 fix(agent-doc): default new files to .md and preserve IME composition #15427)💬 Chat & User Experience
📊 Analytics, Tasks & Notifications
PushChannel, receipt cron, andpushTokentRPC API. (✨ feat(push): add PushChannel, receipt cron, and pushToken tRPC API #15233)🧩 Models & Providers
intern-s2-previewwiththinking_mode, andstep-3.7-flashsupport. (💄 style: addintern-s2-previewsupport, supportthinking_mode#15308, 💄 style: addstep-3.7-flashsupport #15317)🎨 UI & Modals
@lobehub/ui/base-ui(LOBE-9711 + eval batch), including the create-custom-model and feedback/changelog modals. (♻️ refactor(modal): convert create custom model modal to base-ui imperative API #15401, ♻️ refactor: migrate modals to @lobehub/ui/base-ui (LOBE-9711 + eval batch) #15416)@lobehub/uito v5.15.5. (⬆️ chore: update @lobehub/ui to v5.15.5 #15325, 💄 style(imessage): wrap BlueBubbles bridge config into a connection card #15342)🔒 Reliability
session_contextvalues with template variables in credentials. (🐛 fix(creds): replace hardcoded session_context values with template variables #15352)CHANGELOG_URLto/changelog. (🐛 fix(const): 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