Skip to content

🚀 release: 20260604#15447

Merged
arvinxx merged 90 commits into
mainfrom
release/weekly-20260604
Jun 4, 2026
Merged

🚀 release: 20260604#15447
arvinxx merged 90 commits into
mainfrom
release/weekly-20260604

Conversation

@arvinxx

@arvinxx arvinxx commented Jun 3, 2026

Copy link
Copy Markdown
Member

🚀 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


🏗️ Core Agent & Architecture

Agent Signal & Runtime


🖥️ Execution Devices & Gateway


🖥️ CLI & Desktop


🗂️ Pages, Library & Knowledge


💬 Chat & User Experience


📊 Analytics, Tasks & Notifications


🧩 Models & Providers


🎨 UI & Modals


🔒 Reliability


👥 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

Innei and others added 30 commits May 28, 2026 22:42
…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>
arvinxx and others added 13 commits June 3, 2026 17:02
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>
@arvinxx arvinxx requested review from nekomeowww and tjx666 as code owners June 3, 2026 17:33
@vercel

vercel Bot commented Jun 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lobehub (staging) Ready Ready Preview, Comment Jun 3, 2026 6:28pm

Request Review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 300 files, and this pull request has 1012

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. release trigger:build-docker Trigger Docker image build trigger:build-desktop Trigger Desktop build labels Jun 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🐳 Database Docker Build Completed!

Version: pr-release-weekly-20260604-5bf7a60
Build Time: 2026-06-03T18:01:05.955Z
🔗 View all tags on Docker Hub: https://hub.docker.com/r/lobehub/lobehub/tags

Pull Image

Download the Docker image to your local machine:

docker pull lobehub/lobehub:pr-release-weekly-20260604-5bf7a60

Important

This build is for testing and validation purposes.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🚀 Desktop App Build Completed!

Version: 0.0.0-nightly.pr15447.14889
Build Time: 2026-06-03T18:34:50.854Z

📦 Release Download · 📥 Actions Artifacts

Build Artifacts

Platform File Size
macOS (Apple Silicon) LobeHub-Nightly-0.0.0-nightly.pr15447.14889-arm64-mac.zip 147.56 MB
macOS (Apple Silicon) LobeHub-Nightly-0.0.0-nightly.pr15447.14889-arm64.dmg 140.35 MB
macOS (Intel) LobeHub-Nightly-0.0.0-nightly.pr15447.14889-mac.zip 156.17 MB
macOS (Intel) LobeHub-Nightly-0.0.0-nightly.pr15447.14889-x64.dmg 147.45 MB
Windows LobeHub-Nightly-0.0.0-nightly.pr15447.14889-setup.exe 134.68 MB
Linux LobeHub-Nightly-0.0.0-nightly.pr15447.14889.AppImage 165.18 MB

Warning

Note: This is a temporary build for testing purposes only.

@rdmclin2 rdmclin2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@arvinxx arvinxx merged commit 6532cd1 into main Jun 4, 2026
406 of 425 checks passed
@arvinxx arvinxx deleted the release/weekly-20260604 branch June 4, 2026 03:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release size:XXL This PR changes 1000+ lines, ignoring generated files. trigger:build-desktop Trigger Desktop build trigger:build-docker Trigger Docker image build

Projects

None yet

Development

Successfully merging this pull request may close these issues.