Commit Graph

3577 Commits

Author SHA1 Message Date
Arvin Xu e13ad41733 ✨ feat(server): serve tool results as render-facing view models on the v2 read path (#19666)
* ✨ feat(server): add a tool view-model layer to the UI message read path

Tool messages carry one blob that serves two audiences: the model replays the
whole thing into the LLM context, while the screen picks a handful of fields out
of it. Measured on production, the screen's share is tiny — a crawlSinglePage
render reads title/url/description and `content.length`, and never the 37 kB page
body sitting next to them.

This adds the seam that lets the two be separated: a pure, dependency-free
projector per `identifier/apiName`, applied over `UIChatMessage[]` on the way
out of `MessageService.queryMessages`.

The split is a layer boundary rather than a query flag. The UI read path goes
through the service; the LLM context read calls `MessageModel.query` directly and
so cannot reach a projector even by accident — no caller has a parameter to
forget.

The registry ships empty, so this commit changes no bytes on the wire. Projectors
land per tool in follow-ups; a tool without one keeps today's behaviour.

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

* ✨ feat(tools): project the four heaviest tool payloads to what the UI renders

Registers projectors for the tools production measurements put at the top of the
tool payload, and moves the surfaces that need the stored bytes onto an
on-demand fetch.

What each projector keeps, from an audit of the components that consume it:

- crawlSinglePage / crawlMultiPages — the card reads title, url, description,
  crawler and `content.length`; the 37 kB page body it never touches becomes a
  200-char preview, and the tool message body (a second copy, for the model)
  goes entirely. `length` is pinned before truncating so the character count
  stays honest.
- runCommand — the output is stored three times (message body, `state.stdout`,
  legacy `state.output`) and the render reads one. The other two go. The
  surviving copy is NOT truncated; it renders inline in a scrollable block.
- readDocument — the only surface is an inspector chip showing `state.title`.
  `state.content` and `state.xml`, two representations of the same document,
  go with the body.

Nothing is lost, only moved. `message.getToolResultPayload` returns the stored
content and plugin state for one message, and three surfaces call it when they
are opened — never on mount, which would pull everything back down on load:

- the crawl detail portal, when its local body is shorter than the pinned length
- the raw/debug panel, which only mounts once toggled on
- the fallback renderer, behind an explicit control, since an empty body there
  is ambiguous between "returned nothing" and "projected away"

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

* 🐛 fix(server): only project tool payloads for the WebSocket v2 cohort

The projected read path was applied to every caller of
`MessageService.queryMessages`, on the assumption that the LLM context is
assembled server-side and so can never see it. That holds on the server — the
context read goes through `MessageModel.query` directly — but not in the
browser: a client-mode run builds its context from `dbMessagesMap`
(`executeClientAgent({ messages: displayMessages })`), which is filled by the
very read being projected. Tool results would have gone missing from the
model's context, silently.

Gate on the `gatewayMux` lab opt-in, read server-side from `users.preference`.
That cohort is exactly the one whose runs always execute server-side, so the
browser there never needs the stored payload for anything but rendering. The
preference is memoized per service instance — a gateway run queries once per
step — and any failure to read it falls back to the whole payload.

Turning the lab off does not invalidate already-cached projected lists. Left
alone on purpose: the mux is on its way to being the only runtime, and the gate
goes with it.

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

* 🐛 fix(server): project the conversation read, and name the load control

Two defects a live run surfaced that the unit tests could not.

`message.getMessages` builds its own `MessageModel` — it passes different query
options than `MessageService.queryMessages` — so the conversation load never
reached the projector at all. Measured against a seeded topic, the response was
byte-identical to the unprojected one (193,619 bytes) while the WS snapshot path
was already projecting. The projection step is now public on the service and the
router applies it explicitly; the shared-topic branch stays unprojected, since an
anonymous visitor has no authenticated way to fetch the stored payload back.

The fallback renderer's on-demand control reused `debug.response`, so it rendered
as a button labelled "Response" — a section heading, not an action, giving no
hint that it loads what the read path left behind. It gets its own key.

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

* 📝 docs(acceptance): record two worktree bootstrap traps

L-S23: a hand-built node_modules symlink farm passes vitest and tsgo but cannot
start the dev server, and a fresh install cannot be the fallback in a repo that
commits no lockfile. L-S24: the dev port is allocated per run and `/` answers 302
to /signin, so a remembered port and a 200 predicate both read as "never came up".

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

* 💄 style(chat): load a projected tool result on expand, not on a button

Expanding a tool row is already the user asking to see its result, so a second
click to fetch what the read path left behind is a step that earns nothing. The
fallback renderer now fetches as it mounts and shows a skeleton under the
Response heading until the body lands.

Mounting is the right trigger rather than a render-time effect: this component
is the Accordion item's child and only mounts once the row opens. Verified in
the app — expanding the assistant group fires no request, expanding one row
fires exactly one.

The control's dedicated string goes with it.

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

* ✨ feat(tools): move the command output out of the conversation too

Review asked why `runCommand` still shipped ~10 kB of state after projection.
Because the projector kept `stdout`: the card renders the output inline, so it
looked like the card needed it up front. It doesn't — the card lives inside the
row's detail, which only mounts when the row is expanded, the same property that
made load-on-expand safe for the fallback renderer.

So the output goes with the other two copies, and what stays is the settled
metadata the collapsed row reads: exit code, success, background flag, command
id, stderr, produced files. Measured on the same seeded topic, that state went
from 10,040 to 112 bytes and the whole response from 17,494 to 7,580 — 25.5x
against the unprojected 193,619.

Which surface has to fetch the payload back is now something the projector
declares (`storedPayloadNeededBy`), carried to the client on the existing
`payloadOmitted` field as `'detail' | 'render'`, rather than a second registry
the projectors could drift from. `ToolRender` hydrates the `'render'` tools as
it mounts; `'detail'` tools keep a self-sufficient card and only their detail
portal fetches. Measured per step: loading the conversation, expanding the
assistant group, and expanding the crawl row each fetch nothing; expanding the
command row and the document row fetch exactly once each.

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

* ✨ feat(server): stop pushing the whole AgentState on agent_runtime_init

`agent_runtime_init` shipped the raw `initialState` to every non-visitor
subscriber: the entire `AgentState`, which carries the LLM context in `messages`
plus the tool-set maps that `stripStateForStream` already treats as the size
problem on `finalState`. It reached the wire unstripped because
`publishAgentRuntimeInit` pushes directly rather than through
`publishStreamEvent`, and because the existing chokepoint only looks for a
`finalState` key.

Nothing reads it. The web handler logs the event and breaks, the CLI prints a
fixed line, and the gateway transport does not handle the type at all. Shared-
agent visitors have been receiving `{ status }` alone for exactly this reason;
every run now gets the same shape.

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

* 🐛 fix(server): close three gaps Codex review found in the projected read path

**Recovery RPC skipped the conversation guard.** `getToolResultPayload` returned
a tool's stored content and state after only the model's ownership scoping,
which in a workspace is workspace-wide. A member holding a message id — including
one kept after access was revoked — could read output from a conversation they
cannot open. It now runs `assertCanViewMessageTargets` first, the same way the
list read guards with `assertCanUseTopicTargets`.

**Share visitors could never recover a projected payload.** `queryUiMessages`
runs under the CREATOR's identity, so the gate read the creator's lab preference
and pushed the visitor a projected snapshot; the recovery RPC then runs as the
VISITOR against ownership-scoped reads that cannot see a creator-owned row, so
the crawl portal, the raw viewer and the fallback had no way to fill it back in.
Visitor snapshots now keep their payloads whole.

**A projected tool read as still running.** The completion test was
`!!result.content`, which an emptied body fails, so while the assistant message
was busy a finished tool was swapped back to its loading placeholder. The stored
length now rides `ChatToolResult` and one shared `hasToolResultBody` answers the
question for both the group's completion check and the status indicator — the
acceptance round missed this because its fixture was a settled conversation.

Codex's fourth finding no longer applies: it described the projector deleting
`output` while keeping an empty `stdout`, and that branch is gone — both keys
are dropped together now and the card hydrates on expand.

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

* ✅ test(server): cover tool projection options in runtime snapshots

* ♻️ refactor: slim ws2 step message transport

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 18:40:09 +08:00
Innei 9fd05abfed ♻️ refactor(task): migrate subtasks tree to base-ui Tree and animate detail sections (#19567)
* ♻️ refactor(task): migrate subtasks tree to base-ui Tree and animate detail sections

Claude-Session: https://claude.ai/code/session_01NS6gTHyi2Deg87BH4jox28

* ⬆️ chore(deps): bump @lobehub/ui to 5.47.0

Claude-Session: https://claude.ai/code/session_01NS6gTHyi2Deg87BH4jox28
2026-09-14 18:11:45 +08:00
YuTengjing 41ce61b35d ⚡️ perf(search): skip ineligible message indexing with versioned capture (#19537) 2026-09-14 12:14:38 +08:00
Innei d6cfa75d93 ♻️ refactor: migrate Collapse and legacy Accordion to base-ui Accordion (#19532)
* ♻️ refactor: migrate Collapse and legacy Accordion to base-ui Accordion

Replace antd `Collapse` and the legacy root `@lobehub/ui` `Accordion`/`AccordionItem` with `Accordion` (items API) and its atoms from `@lobehub/ui/base-ui` across sidebars, conversation messages, settings, eval, community detail and MCP detail. The root Accordion only styled the header; the base-ui one wraps the whole list, which is the intended visual. `@lobehub/ui/eslint` bans the root export going forward.

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* 💄 style(mcp): rework plugin detail schema section

Tool / Prompt / Resource lists become one outlined base-ui Accordion each (mono name, one-line desc, param count), section spacing 64→40, mode Tabs pinned right, dashed one-line empty state.

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* 💄 style: use inline Accordion indicator to keep the compact sidebar look

Bump @lobehub/ui to 5.45.0 and set `indicatorPlacement="inline"` wherever the legacy Accordion relied on its default (small arrow after the title, no panel indent).

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* ⬆️ chore: bump @lobehub/ui to 5.45.1

inline Accordion indicator now matches the legacy header metrics.

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* ⬆️ chore: bump @lobehub/ui to 5.45.2

AccordionRoot multiple by default, 32px inline headers with actions, lucide arrow.

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* 💄 style: inline Accordion indicator for tool cards and remaining panels

AssistantGroup Tool, ModelDetailPanel, AcceptanceListPanel, ArtworkStudio, AgentSidebar Task and RunCreateModal still used the default start placement (chevron before the title next to the tool icon).

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* ⬆️ chore: bump @lobehub/ui to 5.46.0

7px inline arrow, auto-height panel motion with content fade, outlined header/panel spacing split.

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* 💄 style: let tool titles truncate by width instead of a 32-char cap

Keywords were sliced at 32 characters regardless of available space; the title row already ellipsizes, so drop the cap and let the label keep its width while the keyword shrinks.

Claude-Session: https://claude.ai/code/session_01SnPUiLfRXgaeBU3jSRZXHc

* 🐛 fix: keep collapsed Accordion form fields mounted so saves don't drop them

Claude-Session: https://claude.ai/code/session_01AZXinQXZWhf6P1rMrjLPsi

* ✅ test: mock the base-ui Accordion in WorkflowCollapse test

Claude-Session: https://claude.ai/code/session_01AZXinQXZWhf6P1rMrjLPsi

* ✅ test: drop the dead Collapse mock from QuotaMenu test

Claude-Session: https://claude.ai/code/session_01AZXinQXZWhf6P1rMrjLPsi
2026-09-14 01:47:58 +08:00
Arvin Xu 1e8767eb2a ♻️ refactor(mecha): extract agent config resolution into a shared package (#19476)
There are two Mecha layers doing the same job against different data sources.
The browser assembles an agent's effective run config in
`src/services/chat/mecha` (~2300 lines including its context builder); the
server assembles the same thing in `apps/server/src/modules/Mecha` plus an
851-line `serverCallLlmContextBuilder` that inlines the resolution alongside
reads of six database models. Both wrap the same shared `MessagesEngine`, and
the primitives underneath (`getActivePluginIds`, `ToolResolver`,
`resolveTopicReferences`) were extracted long ago — only the orchestration
above them was left duplicated.

Spot-checked rather than inferred from size: both sides independently compute
`enabledToolIds`, `enabledManifests` and the merged `systemRole` from an agent
config, producing the same shape from different sources.

This moves the browser's resolution rules into `@lobechat/mecha` unchanged,
behind an `AgentConfigSnapshot` of everything they previously read from
Zustand. The browser now gathers that snapshot — 76 lines — and delegates.
Nothing else changes: the barrel re-exports the same names, so no call site
moved, and `getTargetAgentId` stays behind because it reads the active agent.

The package takes the name both hosts already use for this layer, because the
rest of it follows: context engineering, tool composition and model-parameter
resolution are duplicated the same way and belong in the same home. A resolver
is the first inhabitant, not the whole tenancy.

It is a package of its own rather than part of `@lobechat/agent-runtime`
because the runtime keeps exactly three runtime dependencies — everything
heavy is type-only — and that is what let the CLI adopt it. These rules need
`builtin-agents`, which alone pulls in eight builtin-tool packages. Folding
them in would invert the property that makes the runtime portable. The runtime
also never resolves config; it reads `state.metadata.agentConfig` as an opaque
cast for a single field, so resolution belongs to the host that starts it.

`canManage` is an input rather than something the rules compute. The browser
answers it from the cache its picker populates and the server from
`isResourceAuthorOrAdmin`, which is asynchronous — a seam that only works if
the decision arrives already made.

Behaviour is pinned by the existing suite: all 77 of
`agentConfigResolver.test.ts` pass untouched, and they mock the stores, so they
exercise the new gathering and the extracted rules end to end. The package also
has its own suite that builds the snapshot by hand and never mocks a store, so
a store read creeping back in fails there even while the client tests still
pass.

The server and a device adopt this in later PRs — one host at a time, so a
regression stays attributable. For the device the payoff is the point: its
context port stops being an 851-line problem and becomes a snapshot to fill.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 18:16:27 +08:00
Innei 0f526c7fc9 ⬆️ chore(deps): bump @lobehub/ui to 5.43.0 for the global keyboard focus ring (#19483)
Claude-Session: https://claude.ai/code/session_01F6CQCZ4qgVppo4NeNxXA1b
2026-09-12 16:47:06 +08:00
LobeHub Bot 189bd0abe5 🚀 release: sync main branch to canary (#19435)
Co-authored-by: Innei <i@innei.in>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: AmAzing- <115673583+AmAzing129@users.noreply.github.com>
2026-09-12 13:30:35 +08:00
Innei 0b39ca88f3 💄 style: drop ScrollArea content typography workarounds after @lobehub/ui 5.42.7 (#19463)
@lobehub/ui 5.42.7 stops forcing 12px font, fixed line-height and a 16px flex gap
on ScrollArea content, which is what shrank the nav panel. Bump and remove the
per-callsite inherit/gap:0 overrides that only existed to undo that.

Claude-Session: https://claude.ai/code/session_01FYDWLFnDqY7ngaqrECg49A
2026-09-12 12:35:43 +08:00
Innei d2f17fe2bf 🐛 fix(chat-input): stop serializing a composer that is being torn down (#19425)
* 🐛 fix(chat-input): stop serializing a composer that is being torn down

Navigating away from a chat page unmounts the composer while its editor
still has debounced work in flight: onChange is 100ms debounced and the
draft save 500ms. When one of those lands, the kernel it reaches may have
no data sources left — they are registered when the root mounts and
dropped on destroy — and getDocument/setDocument throw:

  DataSource for type "markdown" is not registered.
    at getMarkdownContent → updateMarkdownContent → onChange

Worse than the throw is what a swallowed read would do: persistDraftFor
reads an unserializable composer as an empty one, and an empty read during
a draftKey transition removes the draft the user actually typed.

Route every composer read/write through editorDocument, which skips the
work when the kernel can no longer serialize instead of reporting empty
content, and bail out of the draft persist, restore and key-transition
paths on the same condition.

Claude-Session: https://claude.ai/code/session_01FQvhSDp3pfnpYKbYt2mbS2

* ⬆️ chore(deps): bump @lobehub/editor to ^4.27.3

4.27.3 carries lobehub/lobe-editor#215, which cancels useEditorState's two
debounced toolbar updates on cleanup. Without it a trailing call still reads
the torn-down editor and throws "Cannot read properties of null (reading
'getEditorState')" — the second half of the crash this branch fixes.

Claude-Session: https://claude.ai/code/session_01FQvhSDp3pfnpYKbYt2mbS2
2026-09-11 18:38:43 +08:00
YuTengjing f6d3b1cd96 🔨 chore: upgrade pnpm to 12.4.1 (#19437) 2026-09-11 18:31:57 +08:00
AmAzing- 3bb887ca4c 🐛 fix(ci): restore release metadata sync and dispatch on main (#19424)
Co-authored-by: lobehubbot <i@lobehub.com>
2026-09-11 14:57:59 +08:00
Innei 08cbe3236b ♻️ refactor(html-artifact): extract the workspace HTML publish core into a package (#19356)
* ♻️ refactor(html-artifact): extract the workspace HTML publish core into a package

`src/features/Portal/LocalFile` held the whole publish pipeline — relative-path
analysis, the CSS/JS dependency crawl, the inline/sidecar packer, the path
canonicaliser — inside a React portal directory. All of it is pure and has no
UI dependency, and the CLI needs the same logic.

Moved as-is to `@lobechat/html-artifact` (git sees renames, no rewrites). The
one real split: `readWorkspaceAsset.ts` kept its browser/sandbox transport in
`src` but its size limits, content-type resolution and result types move to the
package, because importing that module for a constant would drag the browser
services into a Node bundle. `getFileExtension` moves too, dropping one of the
two copies that existed in `src`.

Also fixes a silent drop in the gatherer. `addRemotes` only recorded `remote`
skips, and only for the entry HTML — a reference rejected by the extension
allowlist vanished with no record, and an out-of-root reference inside a nested
stylesheet was never reported either. Both now flow through one `collectSkipped`
that buckets every skip reason at any depth, with `unsupported` surfaced in the
publish confirmation alongside the existing lists.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT

* 🐛 fix(html-artifact): keep the moved module paths importable for the business overlay

The share micro-app build failed on `verify`: the closed overlay's
`PublishHtmlArtifactButton.tsx`, `useWorkspaceHtmlArtifactPublish.ts` and
`publishRunAction.ts` import `@/features/Portal/LocalFile/workspaceHtmlPath`,
`…/workspaceHtmlArtifact` and `…/publishWorkspaceHtmlArtifact`, which this
branch moved into the package. Nothing in the open repo imports them any more,
so neither type-check nor the OSS build could see the break — only the overlay
build does.

Re-export exactly what each module used to export, so the move is
backwards-compatible and this can merge without the overlay changing first. The
overlay switches to `@lobechat/html-artifact` after its submodule bump, and
these three files are deleted then.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT

* 🐛 fix(html-artifact): strip CSS comments by scanning instead of a backtracking regex

CodeQL flagged `js/polynomial-redos` on the lazy `/\*[\s\S]*?\*\//g` used to
strip comments before collecting `url()` references: on a stylesheet full of
unterminated `/*` the match restarts from every opener, so cost grows with the
square of the input. Measured on the old implementation — 20k openers 251ms,
40k 997ms; the scanner returns in 0ms for both, and the stripped output is
identical for ordinary input.

This matters here because the CSS is read from files the user names — a page in
a workspace, or any local file handed to `lh artifact publish`.

Claude-Session: https://claude.ai/code/session_01558yVdNvoxocvnWAs11DGT
2026-09-10 23:10:57 +08:00
Innei aba9e8150c 👷 build: migrate the test suite to vitest 5 (#19355)
* 👷 build: upgrade vitest to 5.0.0

Bump vitest and @vitest/coverage-v8 to 5.0.0 and migrate the configs for Vitest 4/5: drop the removed coverage.all option and replace poolOptions.forks.singleFork with maxWorkers/isolate.

* 👷 build: shim jest-dom matcher types for vitest 5

Vitest 5 exposes Assertion<R, T> and no longer reads the jest.Matchers augmentation that @testing-library/jest-dom ships, so its matchers vanish from expect(). Re-declare them on the Vitest interface, and replace the removed describe.sequential with { concurrent: false }.

* 👷 build: migrate the test suite to vitest 5

Vitest 5 (and @vitest/coverage-v8) replaces the esbuild dep optimizer with Rolldown, removed environmentMatchGlobs/coverage.all/poolOptions, forwards `new` to mock implementations, requires top-level vi.mock, and no longer resets automocks in restoreAllMocks. Migrate the root config to app/server projects with Rolldown optimizer options, shim the jest-dom matchers on Assertion<R, T>, and update the affected tests (constructor mocks, hoisted mocks, awaited assertions, explicit mock types, per-test mock defaults).

* 👷 build: make the openapi knowledge-base FileService mock constructable

tests/ was outside the per-area sweep; vitest 5 forwards `new` to the mock implementation, so the arrow implementation threw "is not a constructor".

* 👷 build: point the CI blob-report artifacts at the vitest 5 output dir

Vitest 5 writes blob reports to .vitest/blob instead of .vitest-reports, so the sharded jobs uploaded nothing and the merge jobs failed with ENOENT. Track the new directory in .gitignore too.

* 👷 build: refresh the desktop lockfile for vitest 5

apps/desktop/pnpm-lock.yaml is committed and the desktop build action installs with --frozen-lockfile, so the vitest/override change has to be reflected in it.

* 👷 build: keep the pre-vitest-4 coverage exclusions explicit

Vitest 4+ ships an empty coverageConfigDefaults.exclude, so configs that spread it silently lost the default exclusions (node_modules, dist, *.config.*) and counted more files. Inline the previous list.

* 🎨 style: format the migrated vitest configs

* 👷 build: keep canary's new class mocks constructable under vitest 5

exploreGraph/dispatchAuthorization/scopedDevices (canary #19375, #19352) mock classes with arrow implementations; vitest 5 constructs the implementation when the mock is newed, so the arrows throw 'is not a constructor'.
2026-09-10 22:18:49 +08:00
Innei 84b089450c ♻️ refactor(ui): move DraggablePanel to the antd-free base-ui build (#19365)
* ♻️ refactor(ui): move DraggablePanel to the antd-free base-ui build

@lobehub/ui@5.41.0 ships a headless DraggablePanel under
`@lobehub/ui/base-ui`, rewritten on motion values so a drag never
re-renders React.

- switch all 10 import sites to `@lobehub/ui/base-ui`
- drop `stableLayout`: the new panel holds the content size while the
  box animates, so the behaviour is unconditional
- drop ChatTerminal's `smoothResize` workaround and align its unmount
  delay with the panel's own 250ms collapse

Claude-Session: https://claude.ai/code/session_016Yc2kk25CBo6DXmeGJo1k9

* ♻️ refactor(share): move the share portal to the antd-free DraggablePanel

The original sweep only covered `src/`, so this render site in the share app
kept importing the antd component.

Claude-Session: https://claude.ai/code/session_016Yc2kk25CBo6DXmeGJo1k9

* ⬆️ chore(deps): bump @lobehub/ui to 5.42.1

5.42.1 carries the fix that makes `collapseThreshold` work again behind
`RightPanel`'s `expandable={false}`, which this migration depends on.

Claude-Session: https://claude.ai/code/session_016Yc2kk25CBo6DXmeGJo1k9

* ⬆️ chore(deps): bump @lobehub/ui to 5.42.2

Picks up the drag-collapse clipping fix (lobehub/ui#651).

Claude-Session: https://claude.ai/code/session_012BkEgtdLyHGbfKsHAL4Z5U

* 🔧 chore: exclude the whole @lobehub scope from minimumReleaseAge

Every release of ours is one we cut ourselves, so the release-age window only
costs a version-pinned exclude entry on each bump.

Claude-Session: https://claude.ai/code/session_012BkEgtdLyHGbfKsHAL4Z5U
2026-09-10 21:33:53 +08:00
Innei b796bb7e15 ✨ feat(desktop): font fallback stacks and title-bar-safe floating popups (#19332)
The Appearance → Font page now edits `fontFamily` / `terminalFontFamily` as an
ordered stack: the primary Select is unchanged, and a new "Fallback Fonts" row
under each lets the user add up to three fallbacks, drag to reorder, and remove.
The stored value stays a comma-joined `font-family` string, so nothing changes
in the store or the CSS generation; fonts missing on this device keep their
"unavailable" label instead of being dropped.

Bumps `@lobehub/ui` to 5.40.3, which clamps Select popups to the viewport
(the search field used to be pushed off screen when the popup flipped upward)
and exposes `setFloatingCollisionPadding`. The desktop and popup entries now
reserve `TITLE_BAR_HEIGHT` so every base-ui floating layer stays below the
Electron title bar.

Claude-Session: https://claude.ai/code/session_016gCNG3jtPnmQdrKTY7r7Jg
2026-09-09 20:12:15 +08:00
Innei b65aae96b6 👷 build: migrate workspace to pnpm 12 (#19324)
* 👷 build: migrate workspace to pnpm 12

pnpm 12 no longer reads the package.json "pnpm" field, only honours auth and registry keys from .npmrc, and requires allowBuilds instead of onlyBuiltDependencies. Move every project setting into pnpm-workspace.yaml and keep the previously effective overrides and build allowlist.

* 👷 build: resolve desktop and cli workspaces on pnpm 12

pnpm 12 no longer accepts './'-prefixed workspace package globs, so the desktop stub path is now relative without the prefix. Migrate the remaining package.json "pnpm" fields into the desktop and cli pnpm-workspace.yaml files.

* 👷 build: list protobufjs in the desktop build allowlist

pnpm 12 fails the install with ERR_PNPM_IGNORED_BUILDS when a script-bearing dependency is missing from allowBuilds. protobufjs only appears in the desktop graph on Linux, so it was absent from the previous allowlist.

* 👷 build: relink root workspace members before the desktop typecheck

pnpm 12 writes broken relative symlinks for workspace members that live outside the desktop workspace root (../../packages/*), so tsgo cannot resolve sibling packages from source. Installing the root workspace after the desktop install rewrites those links correctly.
2026-09-09 19:43:11 +08:00
Innei 6329f97e14 🐛 fix(vite): stop assuming node_modules sits at the repo root (#19325)
* 🐛 fix(vite): resolve the editor provider without assuming node_modules layout

GlobalProvider/Editor.tsx reached @lobehub/editor's provider through a
relative `../../../node_modules/...` path, which assumes node_modules sits at
the repo root. Business-overlay desktop builds mount this repo as the overlay
root's `lobehub/` submodule, so the install hoists to the parent directory and
the path does not exist — every desktop platform failed with UNRESOLVED_IMPORT.

The provider subpath is not in the package's export map, so it still cannot be
imported on its own; the dev-only resolver now runs in builds too and derives
the provider file from the resolved package entry, wherever it lives.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S

* 🐛 fix(vite): resolve the @lobehub/ui barrel through the package

The barrel parser reached the package's `es` directory through
`../../../node_modules/@lobehub/ui/es`, which assumes node_modules sits at the
repo root — the same assumption that broke the desktop build. Business-overlay
share builds hoist the install above this checkout, where that path does not
exist and `realpathSync` throws before the plugin can load.

Resolving the package entry instead finds it wherever it is installed. The
plugin is not loaded on the desktop platform, so this never surfaced there.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S

* ♻️ refactor(vite): take the editor provider entry @lobehub/editor now exports

@lobehub/editor 4.27.0 exports `./react/EditorProvider`, so the provider no
longer has to be reached by file path and the build-time resolver and its
ambient shim can go. The dev redirect stays: Vite prebundles the provider entry
separately from `@lobehub/editor/react`, which the editors use, and the app
would end up with two EditorContexts.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S

* 🐛 fix(desktop): let pnpm find the business-const stub

pnpm does not match the `./stubs/business-const` workspace entry, so an OSS
install from apps/desktop fails to resolve `@lobechat/business-const` — the
package the stub exists to provide — and the desktop build cannot be run
locally at all. Cloud builds resolve the real package from the overlay, and
the renderer redirects the module to `@cloud/business-const` either way.

Claude-Session: https://claude.ai/code/session_01LNsSw6SDv3UEGNfBEwPa8S
2026-09-09 14:54:22 +08:00
Neko 29acee69f6 ✨ feat(desktop): added Computer Use with auv (#19051) 2026-09-09 00:01:59 +08:00
Rdmclin2 42f6dd299d ✨ feat(home): merge workspace quota into the usage widget (#19272)
* ✨ feat(home): merge workspace quota into the usage widget

- Rename the home usage widget to "Usage & Quota" and add the `userPanel.quota` copy so workspace subscription credits, top-up credits and member seats can live in the right-side widget (LOBE-13903)
- Sort library hierarchy rows newest-first within folders so a just-created file shows up at the top, and expose a per-row menu button (LOBE-13814)
- Keep the caret out of Lexical's root node around block images in comment editors via an opt-in `blockImageCaretGuard` (LOBE-13882)
- Honour user-resized image widths in document comments and freeze the editor height while resizing (LOBE-13874)
- Derive plugin display meta from marketplace-shaped manifests so community MCP rows never render an empty label
- Bump `@lobehub/editor` to ^4.26.1

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TxoeVh86uEY7oEyzuhyU6y

* 🌐 style(i18n): translate usage widget quota copy

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TxoeVh86uEY7oEyzuhyU6y

* 🐛 fix(comments): match resized images whose width is the first inline declaration

`LexicalRenderer` only emits `max-width` when the node carries `maxWidth`, so a
resized image can serialize as `style="width: 320px"`. The leading-space
selector missed that form and reapplied the thumbnail `width: auto` cap.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TxoeVh86uEY7oEyzuhyU6y

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 14:08:44 +08:00
YuTengjing 8d852279db ✨ feat(provider): add Unsloth provider support (#18339)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-06 23:28:17 +08:00
YuTengjing 8ea955ddc5 ✨ feat(model-runtime): add Meta AI provider with Muse Spark 1.3 (#19152)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 14:15:05 +08:00
Tsuki 645ebf4998 🔨 chore: prepare v2.2.16 release ancestry 2026-09-03 22:55:31 -07:00
LiJian a2890d4903 chore(sandbox): bump @lobehub/market-sdk to 0.41.0 (WAF false-positive fix) (#19130)
🔧 chore(sandbox): bump @lobehub/market-sdk to 0.41.0

runCommand/executeCode payloads sent to Market's run-buildin-tools
endpoint naturally contain shell metacharacters (; curl, backticks,
pipes), which Cloudflare's managed command-injection WAF rule was
mistaking for an attack when they hit the wire as literal JSON text.
Root-caused on topic tpc_rQ2lXneGO59w: tool calls came back with a bare
"Forbidden", which the model misread as an auth problem and burned
~30min retrying credential checks.

0.41.0 makes PluginsService.runBuildInTool send params base64-encoded
(paramsB64/paramsEncoding) instead of literal JSON. No code change
needed on this side — MarketSandboxProvider (apps/server/src/services/
sandbox/providers/market.ts) already just forwards toolName/params
straight into sdk.plugins.runBuildInTool.

Rollout safety: Market's run-buildin-tools endpoint accepts both the
old plain-`params` body and the new base64 form simultaneously, so a
multi-instance/rolling Vercel deploy where old and new server
instances briefly run side by side is not a compatibility hazard here
either way.


Claude-Session: https://claude.ai/code/session_01H7U5raw93D3toG4gCcLKYE

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 13:09:02 +08:00
Innei 9c81af8ad2 ♻️ refactor(skeleton): migrate to the base-ui Skeleton and make the route skeleton the only loading layer 2026-09-03 00:57:20 +08:00
Rdmclin2 522d001ee3 🐛 fix(bot): always verify Telegram webhooks and pin Chat SDK to ~4.38.0 (#18930)
* 🐛 fix(bot): always verify Telegram webhooks with a derived secret and pin Chat SDK to ~4.38.0

* 🐛 fix(bot): await webhook re-registration and share its cooldown via Redis

* ✅ test: type test fixtures against chat 4.38 typings
2026-09-02 00:57:49 +08:00
Innei 3ba4e60221 🐛 fix(inspector): shimmer the label span instead of the whole row (#18978)
* 🐛 fix(inspector): shimmer the label span instead of the whole row

textStyles.shiny clips to text via mask-clip: text, and that mask applies
to the whole subtree — icons, chips and status glyphs inside a shiny row
were erased. Move the class onto the pure-text label span so the mask
stays on the compositor path and the rest of the row paints normally.

TodoInspectorSummary takes a shiny prop for the rows that delegate their
label to it.

* ♻️ refactor(inspector): unwrap the redundant nested cx call

* ✨ feat(inspector): give inspector rows the shiny sweep coordinate space

The row now holds position and hands its spans --shiny-origin: static, so
several shimmering spans in one row share a single row-wide sweep instead
of each playing its own. Rows that keep a plain-text tail after an avatar
shimmer that tail with the label again.

* ⬆️ chore(deps): bump @lobehub/ui to ^5.37.1

* 🐛 fix(inspector): shimmer the Vent label span instead of the whole row

* ⬆️ chore(deps): bump @lobehub/ui to ^5.38.0 and use its shinyGroup
2026-09-01 17:47:57 +08:00
Innei 9fc8a23881 🐛 fix(acceptance): center reject modal instead of pinning it left (#18951)
* 🐛 fix(acceptance): center reject modal instead of pinning it left

createModal maps styles.popup onto the full-screen overlay
(position:fixed; inset:0). Sizing that overlay with height/maxWidth
makes left:0 win when max-width underconstrains, so the dialog
sticks to the left. Apply panel size to the inner dialog instead.

* 💄 style(acceptance): align reject modal title and body on one inset

The dialog header uses 16px padding-inline while the body kept the
library's padding-inline plus a 20px footer, so the title sat left
of the form. Zero the content logical padding and share 16px.

* ♻️ refactor(acceptance): inherit modal content padding for reject dialog

The shell was zeroing content padding and re-applying 16px on the
header and footer. Title and body already share the upstream
padding-inline, so stop restyling the inset.

* 💄 style(acceptance): flush reject-modal attach with textarea

Bump @lobehub/ui to 5.37.0 for Button outdent, let the attach text
button cancel its padding-inline, and make antd Upload display:contents
so the negative margin shifts the row. Match AcceptanceCheck portal
header padding with the 24px body.

* 💄 style(acceptance): align portal header title with body

Match Acceptance portal chrome padding to the 24px viewer body so the
title lines up with the page heading.

* ✅ test(acceptance): assert reject modal style keys safely
2026-09-01 15:22:07 +08:00
Arvin Xu d8722c7a84 ✨ feat(goal): plan the exploration structure before executing (#18942)
* 📌 fix(deps): unpin @ant-design/icons now that 6.3.4 is fixed

Upstream shipped @ant-design/icons@6.3.4 with a matching
@ant-design/icons-svg@4.6.0, so the MetaFilled resolve failure that forced
the 6.3.2 pin (#18923) is gone. Drop both override entries and float the
direct dependency from ^6.3.4 — starting the range at the fixed release so
a stale registry mirror can never resolve the broken 6.3.3.

Verified locally: fresh install resolves 6.3.4 + icons-svg 4.6.0,
es/asn/MetaFilled exists, and `bun run build:spa:raw` exits 0.

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

* 💄 style(goal): size the inline exploration map to its content

- The inline canvas was a fixed 560px frame, so a two-node graph rendered as
  a small cluster adrift in empty margin (fitView shrinks nodes to fill the
  frame). Size the frame from the layout bounds instead; 560px stays the
  ceiling for large graphs.
- Move each card's state chip (and the @ human marker) from the title row's
  trailing edge to the card's bottom-left: a task folds them into its metric
  strip, other kinds get a light footer, and the title keeps its full width.

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

* ✨ feat(goal): plan the exploration structure before executing

Creating a goal used to copy the raw ask verbatim into exactly one problem
node + one task and dispatch it immediately: the problem node's description
was the full requirement (acceptance boilerplate included), the single task
mirrored the whole request, and its acceptance objective concatenated the
entire goal contract again. No decomposition ever happened.

- New `plan_decomposition` coordinator branch: a goal with no work is a
  planning cue, not a dead end. On first advance an LLM plans the core
  problem statement plus 1–5 independent work directions (chainGoalDecompose,
  traced as goal_decompose); planner failure degrades to a single work seeded
  from the requirement so the goal never stalls.
- Callers stop seeding the mirror task; they pass `problemDescription` (the
  user's own words) for the problem node instead of the requirement blob.
- A Work's acceptance objective no longer embeds the full goal requirement —
  each direction is judged on its own outcome; the complete contract stays
  with the terminal acceptance task.
- `GoalGraphModel.updateNodeDescription` lets the planner replace the seeded
  description with its own problem statement.

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

* 💄 style(goal): use the task family's running color on graph node chips

The running chip was a lone colorInfo blue while every other running signal
(frontier glyph, task rows) draws from TASK_STATUS_VISUALS — the odd color
read as a different state. Reference the shared visual instead.

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

* 💄 style(goal): icon-only verifier metric; CLI passes problemDescription

- With the state chip sharing the metric strip, the verifier label no longer
  fit the card width and wrapped mid-word; the tooltip already carries the
  full meaning, so keep the shield icon only.
- `lh goal create` gains `-i/--instruction` so the CLI can seed the problem
  node with the user's own words like the modal and the /goal tool do.

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

* 💄 style(goal): move node state chips to the bottom-right (review feedback)

Bottom-left read poorly in review; the state now sits on the card's
bottom-right — a task folds it into the right edge of its metric strip
(running clock rides along), other kinds right-align their footer.

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

* 💄 style(goal): status reads first — its own top row with the running clock

Neither bottom corner sat well in review. The state chip now opens the card
as its own left-aligned top row, with the elapsed clock right behind it when
the task runs; the title keeps the full width below, and with the chip out
of the metric strip the verifier label fits again.

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

* 💄 style(goal): every task state reads from the top status row

"Not dispatched" was still hiding in the metric strip while other states
had moved to the top row. A task now always carries a status chip — queued,
waiting, unassigned included — and the metric strip keeps only verifier,
attempts and artifacts.

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

* 💄 style(goal): status chips draw the task family's glyphs, not dots

A colored dot said less than it could: the status row now renders the same
glyphs the task surfaces use — the animated running ring, the dashed circle
for not-yet-dispatched, CircleX for lost, CircleCheck for done — so a node's
state reads identically everywhere.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 09:19:52 +08:00
Innei 192f60157d ✨ feat(devdock): integrate Mesurer inspector (#18955) 2026-09-01 02:18:16 +08:00
Arvin Xu de5e0d52fd 📌 fix(deps): pin @ant-design/icons to 6.3.2 (#18923)
📌 fix(deps): pin @ant-design/icons to 6.3.2 (#18922)

* 📌 fix(deps): pin @ant-design/icons to 6.3.2

@ant-design/icons 6.3.3 (published 2026-08-31 06:53 UTC) ships a MetaFilled
icon that imports "@ant-design/icons-svg/es/asn/MetaFilled", which does not
exist in the latest @ant-design/icons-svg (4.5.0). Any fresh install
resolving the caret range breaks the SPA build:

  Error: [vite]: Rolldown failed to resolve import
  "@ant-design/icons-svg/es/asn/MetaFilled" from
  ".../@ant-design/icons/es/icons/MetaFilled.js"

Every CI run since the release fails (Test Web App + Vercel). Pin to the
last good 6.3.2 until upstream ships a matching icons-svg.



* 📌 fix(deps): pin the direct @ant-design/icons dependency to match the override

npm requires an override spec to be identical to the direct dependency spec
(EOVERRIDE otherwise), and Vercel's `npx pnpm install` runs that validation.
Follow the repo's existing pin pattern (antd, better-auth, lexical): exact
version in both dependencies and overrides.



---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 18:59:26 +08:00
Arvin Xu a4c4814d45 Revert "📌 fix(deps): pin @ant-design/icons to 6.3.2 (#18922)"
This reverts commit 65c35a1b93.
2026-08-31 17:06:32 +08:00
Arvin Xu 65c35a1b93 📌 fix(deps): pin @ant-design/icons to 6.3.2 (#18922)
* 📌 fix(deps): pin @ant-design/icons to 6.3.2

@ant-design/icons 6.3.3 (published 2026-08-31 06:53 UTC) ships a MetaFilled
icon that imports "@ant-design/icons-svg/es/asn/MetaFilled", which does not
exist in the latest @ant-design/icons-svg (4.5.0). Any fresh install
resolving the caret range breaks the SPA build:

  Error: [vite]: Rolldown failed to resolve import
  "@ant-design/icons-svg/es/asn/MetaFilled" from
  ".../@ant-design/icons/es/icons/MetaFilled.js"

Every CI run since the release fails (Test Web App + Vercel). Pin to the
last good 6.3.2 until upstream ships a matching icons-svg.

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

* 📌 fix(deps): pin the direct @ant-design/icons dependency to match the override

npm requires an override spec to be identical to the direct dependency spec
(EOVERRIDE otherwise), and Vercel's `npx pnpm install` runs that validation.
Follow the repo's existing pin pattern (antd, better-auth, lexical): exact
version in both dependencies and overrides.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 17:03:48 +08:00
Innei bf5a10fd57 💄 style(skill-store): drop dead modal header overrides (#18920)
* 💄 style(skill-store): drop dead modal header overrides

These three modals pass no `title`, so `createModal`'s StackItem never
renders a header — `styles: { header: { display: 'none' } }` hid
nothing. Removing it lets them pick up base-ui's header-less body
inset instead of relying on the shared content padding.

Also add ConfigProvider and motion/react to the design-prototype
runtime: base-ui Button throws without a MotionProvider, which made
any prototype using it fail to mount.

* 🔧 chore(deps): pin @ant-design/icons to 6.3.2

6.3.3 imports @ant-design/icons-svg/lib/asn/MetaFilled, which does not exist in icons-svg 4.5.0 (latest).
2026-08-31 16:51:28 +08:00
YuTengjing 321bfe8623 ⚡ perf: accelerate and harden Elasticsearch reindex recovery (#18902) 2026-08-31 12:42:02 +08:00
YuTengjing d74e897191 ✨ feat: add Elasticsearch full-text search provider (#18807) 2026-08-31 02:17:23 +08:00
Rdmclin2 14b711e783 🐛 fix(page-editor): publish lock ownerId reliably and self-heal CONFLICT saves (#18848)
* 🐛 fix(page-editor): publish lock ownerId reliably and self-heal CONFLICT saves

The lockOwnerId dispatch in useDocumentLock lands before the document row
exists in the store, so the reducer drops it and every save goes out with an
undefined ownerId — the server then rejects the write as a CONFLICT against
the user's own lease. Republish the ownerId once the document is loaded,
retry a CONFLICT save once after reclaiming the same-user lease, and stop
surfacing save failures with the file-library network error copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🌐 chore(i18n): translate pending locale keys

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🐛 fix(page-editor): guard the CONFLICT retry against a collaborator's newer version

A conflicting lease can belong to another member who saved and released it
before the recovery ran; reclaiming the now-free lease and resending the
stale payload would overwrite their save. Compare the server copy with the
session's last-known content and retry only on an exact match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🐛 fix(page-editor): make the CONFLICT retry an atomic server-side version check

The client-side content comparison left a TOCTOU window between the fetch
and the reclaim, and ignored editorData-only collaborator changes. The
recovery now compares content AND editorData against the session's
last-known version and pins the retried save to that verified version via a
new optional expectedUpdatedAt predicate, which updateDocument re-checks
under FOR UPDATE inside the write transaction — an interleaved collaborator
save now fails the retry with CONFLICT instead of being overwritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🐛 fix(page-editor): hand back the reclaimed lease when the CONFLICT retry fails

A failed CAS retry left this session holding the lease it had just
reclaimed, so the lock recovery read the tab as the legitimate holder,
cleared the save block, and reopened a stale editor. Release the lease
before rethrowing so the lock state reflects reality.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🐛 fix(page-editor): add expectedUpdatedAt to the manual UpdateDocumentInput type

The tsgo lint step failed: the manually-maintained UpdateDocumentInput
interface lagged the zod schema, and the retry helper's version check did
not narrow `latest` for TypeScript.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🐛 fix(page-editor): bind conflict recovery to the failed save's base version

The recovery compared the fetched server copy against the LIVE store
snapshot, so when an overlapping newer save from the same tab completed
while the older request was in flight, the check matched the newer version
and the retry replayed the older payload over it. Compare against the
immutable store snapshot captured when the failed save started instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🐛 fix(page-editor): self-heal only plain content saves

A save carrying title/emoji or a history restore replays fields the
recovery's content+editorData comparison cannot vouch for — a
collaborator's metadata-only change would be silently overwritten by the
retry. Those saves keep the plain CONFLICT flow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

* 🔧 chore(test): pin happy-dom below 20.12 to unblock CI

happy-dom 20.12.0 (released 2026-08-29T16:12Z) changed Web Animations
behavior in a way that makes motion-dom animation teardown reject with
unhandled 'AbortError: The animation was canceled' — every Test App shard
run after the release fails at exit despite all tests passing (canary flaps
identically). Pin to ~20.11.15 until happy-dom/motion settle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011X64zT9QD5FHwJkekYAQgE

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 09:46:01 +08:00
Innei fc519ef74f ⚡️ perf(styles): reuse the lobe-ui shiny text style (#18850)
* ⚡️ perf(styles): reuse the lobe-ui shiny text style

Both local shinyText copies animated background-position, which repaints
every glyph each frame. @lobehub/ui now ships the mask + transform
version as textStyles.shiny, so drop the duplicates and point at it.

Claude-Session: https://claude.ai/code/session_012Pjd1gcsbxQmrVkuRkPYdu

* ⬆️ chore(deps): bump @lobehub/ui to ^5.36.0

Ships textStyles.shiny, which this branch now depends on.

Claude-Session: https://claude.ai/code/session_012Pjd1gcsbxQmrVkuRkPYdu
2026-08-29 22:54:17 +08:00
Neko 5cb99eecff 🚀 release: 20260827 (#18747)
# 🚀 LobeHub Release (20260827)

**Release Date:** August 27, 2026  
**Since v2.2.14:** 235 merged PRs · 13 contributors

> This cycle makes long-running Agent work easier to direct and recover,
expands provider-backed heterogeneous Agents, delivers safer
restart-free Desktop updates, and deepens Artifact, Workspace, and
acceptance workflows.

---

## ✨ Highlights

- **Provider-backed heterogeneous Agents** — Claude Code and Codex gain
deployment-backed model options, while Kimi Code and Pi add new provider
paths across web and Desktop. (#18468, #18558, #18609, #18658, #18720,
#18721)
- **Restart-free Desktop updates** — Signed renderer updates apply in
place with compatibility checks, compressed delta delivery,
release-scoped artifacts, and automatic rollback. (#18648, #18694,
#18716, #18724)
- **Agent approval and intervention flows** — Agent approvals now align
across surfaces, and generic interventions persist so long-running work
can resume with the right user decision. (#18417, #18645)
- **Agent self-learning** — A Labs-gated growth portrait shows learned
domains, rules, and practice history while operation-scoped expertise
improves future Work. (#18195, #18394, #18420)
- **Public Artifacts and Workbench** — Local HTML and site assets can be
packaged into versioned Artifacts, while acceptance and verification run
in a standalone SSR Workbench. (#18424, #18473, #18569, #18728)
- **Workspace collaboration** — Per-member Resource grants, transfer
requests, and a redesigned Notification Center make shared work easier
to control and act on. (#18389, #18432, #18536, #18545)
- **More adaptable everyday UI** — Global interface fonts, consolidated
Home promotions, clearer model availability, and refined recommendation
visuals make the app easier to personalize and navigate. (#18712,
#18722, #18730, #18741)

---

## 🏗️ Core Agent & Architecture

### Goals, Tasks & Acceptance

- Goal state moves to a durable graph with dependency-aware scheduling,
CLI controls, autonomous recovery, and terminal Acceptance Work.
(#18574, #18597, #18635)
- Scheduled Tasks get a dedicated view, successful subtasks complete
automatically, and Kanban grouping matches the list view with clearer
loading states. (#18586, #18591, #18631, #18707)
- Acceptance deliveries group by Project and can be merged without
losing checks, evidence, or prior review decisions; the review round now
always reports its outcome. (#18448, #18537, #18548, #18672)
- Acceptance features now share one consistent product boundary, while
abandoned Goal Work and stale scoped tool calls recover instead of
silently ending the run. (#18656, #18664, #18703)

### Agent Learning & Identity

- Self-learning gains dedicated domain creation, editable anchors,
operation-scoped Context, and safer lesson history management. (#18418,
#18420, #18517, #18556)
- Agent profiles gain a character sheet and reusable full-body artwork
that retains its style and direction. (#18446, #18464)
- Agent authors can restrict Topic sharing, while Agent transfers
preserve accumulated expertise. (#18445, #18557)
- Approval workflows align with web behavior, and generic interventions
persist across Agent runs. (#18417, #18645)

---

## 📱 Platforms & Integrations

### Heterogeneous Agents

- Claude Code supports local Provider bindings, while supported Claude
Code and Codex runs can use deployment-backed models with
operation-scoped authorization. (#18468, #18558, #18595, #18609, #18621)
- Kimi Code joins heterogeneous Agent provider bindings, and Pi gains a
dedicated Desktop provider path. (#18720, #18721)
- Each Topic keeps its own heterogeneous model choice instead of
rewriting the Agent default. (#18658)
- Cursor moves to a bidirectional ACP session with permission prompts,
questions, approvals, and safe resume recovery. (#18475)
- OpenClaw and Hermes detection now covers official install paths,
refreshed shell environments, and Windows launchers. (#18646)

### Desktop, Messaging & Gateway

- Desktop applies verified renderer updates without restarting, keeps
compact rollback deltas, and scopes OTA artifacts to the matching
release. (#18648, #18694, #18716, #18724)
- macOS communication notifications include Agent avatars, Workspace
windows retain their bounds, and the terminal adds links and split
panes. (#18430, #18433, #18441, #18500)
- Gateways can pull the connection assignments they should hold, while
message routing can still split platforms across deployments without
changing the Integration protocol. (#18628, #18680)
- Linked chat platforms receive Resource files and attachments with
per-platform budgets, while scheduled Task results and rich Agent pushes
appear in the Notification Center. (#18391, #18467, #18511, #18535,
#18561, #18608)

---

## 🖥️ User Experience

- Realtime voice dictation adds finalized speech to the editable draft
without auto-sending or disturbing existing text and attachments, and
its entry point now sits with the left-side chat actions. (#18132,
#18578, #18709)
- Home restores its marketing hierarchy, consolidates promotional
surfaces, sharpens recommendation visuals, and brings back richer model
shortcuts and the Apps catalog. (#18438, #18528, #18553, #18562, #18580,
#18722, #18741)
- A global interface font setting and locale-aware font stacks make
reading preferences consistent across the app. (#18712)
- GLM-5.3-Flash is available, disabled models can be re-enabled from
chat input, and model prices survive OAuth redirects. (#18723, #18727,
#18730)
- Dead Topic locks no longer swallow sent messages, and completed
gateway runs clear stale loading state in the sidebar. (#18497, #18682)
- Agent document edits recover malformed persisted content and reject
operations that would unexpectedly clear a non-empty document. (#18643)
- Settings search covers more destinations and matches user queries more
reliably. (#18390)

---

## 🔧 Tooling

- Agent Chaos adds deterministic timeout, cleanup, tracing, and
failure-injection contracts for long-running Agent tests. (#18667)
- Evaluation Run Topics restore Test Case history and environment
Context, and tool calls can be forwarded to mock servers for end-to-end
verification. (#18516, #18604)
- Workbench CI checks Worker bundle size and publishes PR previews after
assets reach R2. (#18492, #18592)
- Desktop release automation dispatches renderer OTA before native
releases, and shared UI primitives continue moving to the base component
layer. (#18716, #18729)
- Obsolete Orb repository setup scripts are removed from the development
path. (#18740)

---

## 🔒 Security & Reliability

- **Security:** Resource checks stay scoped to the active Workspace and
support per-member grants without bypassing privacy or role ceilings.
(#18396, #18406, #18525, #18536, #18545)
- **Security:** Server-default heterogeneous operations bind
authorization to the user, operation, Workspace, and allowed
capabilities. (#18558)
- **Reliability:** Context compression avoids repeat work, removes
compressed messages from queued payloads, and settles in-flight tool
calls when interrupted. (#18506, #18587, #18610, #18626)
- **Reliability:** Completed heterogeneous child Agents resume their
parked parent, while RouterRuntime preserves request Context and retries
the next configured channel on insufficient Provider balance. (#18596,
#18668)
- **Reliability:** Multi-tab settings changes no longer overwrite newer
tool approval choices with stale state. (#18705)
- **Performance:** Message-plugin and heterogeneous tool-call lookups
avoid broad scans, and Task lists use a leaner database path. (#18457,
#18519, #18618)

---

## 👥 Contributors

Huge thanks to **13 contributors** who shipped **235 merged PRs** this
cycle.

@ONLY-yours · @orangeboyChen · @jalenzz · @NickShee · @BittuBarnwal7479
· @arvinxx · @Innei · @AmAzing129 · @rdmclin2 · @tjx666 · @sudongyuer ·
@cy948 · @nekomeowww

---

**Full Changelog**:
https://github.com/lobehub/lobehub/compare/v2.2.14...release/weekly-20260827
2026-08-28 22:15:11 +08:00
Arvin Xu 4dc26d48ef ✨ feat(goal): add process control UI and server-driven execution (#18762)
* ✨ feat(goal): add the process-control surface to the Goal detail page

Move the prototyped process-control UI into the product: the frontier list
(what can move now), the exploration map on react-flow, the findings list and
a per-node activity feed.

Data comes from one `goal.graph` read. The server has no frontier projection,
no per-node attempt roll-up and no artifact hydration, so the client
reproduces the coordinator's own selection rule and derives the attempt ledger
from the `goal_events` trail. Anything that cannot be derived honestly is
omitted rather than invented.

A Goal Graph goal has no carrier task, so the detail route falls back to a
graph-only page when the identifier resolves to no task.

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

* ♻️ refactor(goal): make the Goal Graph the only goal model

A goal used to be a `goals` row bound to one carrier task, driven by an outer
loop in the verify settle path. That was the early attempt; the Goal Graph
supersedes it — the graph owns the decomposition and dispatches a Task per
Work node, each with its own acceptance contract, and stops on a decision gate
when a Work runs out of attempts.

Server
- `GoalModel.list` is goal-centric: it reads `goals` and derives the roll-up
  (work done/total, findings, pending gates, cost) from the graph, so a goal
  without a carrier task is no longer invisible.
- Delete the task-carried outer loop: `verify/goalLoop`, `verify/goalBudget`,
  the goal branches in `settle` / `acceptanceService` / `planInstantiation`,
  and the goal binding in `TaskService.createTask` and `task.create`.
- `/goal` now creates a Goal Graph and advances it once; the coordinator
  creates the responsible task and its verifier.
- New `goal.delete`; `task.deleteGoal`, `hasGoal` filters and `TaskItem.goal`
  are gone with the flow that needed them.

Client
- Goal list, detail, home rail and the conversation card read the graph.
- The goal detail route is keyed by the `goals` row id.

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

* 🐛 fix(goal): read the attempt ledger from real attempt boundaries

Found while driving a goal end to end: the frontier row of a running task
showed no clock, and a finished task's attempts were labelled from a
bookkeeping event.

`goal_events` writes `updated` for housekeeping too ("Attached Work version
…"), and every `updated` was closing the open attempt. That ended the live
attempt one event early — so `startedAt` was never set and the running clock
disappeared — and stamped the housekeeping reason onto the previous attempt.

Only `activated` now opens an attempt, and only a boundary closes it: the next
`activated` (the coordinator started another attempt, so this one did not
deliver) or a terminal event. A Work parked at a decision gate leaves its
attempt open, since the gate is written as `updated`, so the node's own status
closes it.

A gate row also showed an empty ledger, because attempts live on the Work and
the row is the decision node. The gate now carries the Work it was opened for
and renders that Work's ledger — which is the case the user is deciding.

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

* 💄 style(goal): quiet the exploration map and make one press run the goal

Review feedback on the acceptance round, item by item.

The map was shouting the same thing in three places at once — a pill floating
on the card's outline, an emphasis border, and a drop shadow — which left no
room for the edges routed around it:

- cards go flat; state becomes a dot plus a word on the header's trailing edge,
  where the "you took part" mark now sits too;
- edges become bezier curves instead of right-angled steps;
- the frontier emphasis border is gone. It was drawing a heavy outline around a
  *completed* card, which is the wrong signal entirely; selection keeps a single
  light border;
- inline, the map is a picture: no zoom controls, no pan, no zoom. Fullscreen is
  where it becomes navigable.

The advance button ran a single coordinator tick. That is an implementation
unit, not one a person cares about — dispatching a work takes two ticks, so one
press usually looked like it did nothing. It now advances until the goal is
achieved, needs a decision, or has nothing ready, the way `lh goal run` does.

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

* ✨ feat(goal): drive goals from the server instead of a client tick loop

A Goal Graph only moved while something kept calling `goal.tick` — a browser
loop or `lh goal run`. Close the tab and a long-horizon goal stopped, which is
the opposite of what "long-horizon" is supposed to mean.

Goals now advance on their own, event-first with a sweep behind it:

- `advanceGoal` runs the coordinator until it is waiting on something that is
  not another tick. `waiting_external` is a stop, not a poll: a Work Task is
  executing, and that task's own settle is the next event.
- Something that may have unblocked a goal queues an advance — creating it,
  resolving a decision gate, resuming it, raising a budget, and a Work Task
  settling in the verify path.
- `/api/workflows/goal/sweep` is the safety net a purely event-driven chain
  needs: it picks up goals with no fresh active Work and no pending gate, so a
  dropped queue message, a runner that died mid-dispatch, or a Work that
  outlived its operation lease cannot strand a goal forever.
- The scheduler mirrors `taskScheduler`: QStash in queue mode, in-process
  timers for dev and desktop, so both exercise the same runner.

The page's advance button hands off to the server in one call rather than
holding a tick loop open, and `goal.advance` is the nudge for "don't wait for
the next event". The loop that used to live in the browser now lives where it
can keep running without one.

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

* 🐛 fix(goal): actually queue the advance when a goal is created

The create procedure was the one enqueue site that never landed, so a goal sat
in `planning` until something else poked it — the exact failure the automatic
driver exists to remove. Caught by running a goal end to end with no client
attached: creation published nothing to the queue.

A dispatch failure is now logged loudly rather than only through `debug`. This
is the only thing that makes a goal move on its own; when it breaks, the
symptom is goals that quietly stop, which is close to impossible to attribute
from the outside.

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

* ♻️ refactor(goal): import Text/Tag/ActionIcon from base-ui

The base-ui migration (#18729) landed on canary while this branch was open,
so the Goal process-control components still pulled the deprecated antd
wrappers. Move them over and drop the acceptance imports the Goal detail
page no longer uses.

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

* 🐛 fix(goal): close the gaps the review found in the graph-only migration

Seven problems, all from making the Goal Graph the only goal model:

The create modal still showed a visibility control whose value went nowhere,
so a goal marked private dispatched Work Tasks that fell back to the assignee
agent's visibility and published its output to the workspace. Visibility now
rides on the goal's `config` — no new column — and the coordinator stamps every
task it creates.

Goals from the earlier task-carried flow have no `goal_nodes`, and the new
goal-centric list returned them alongside graph goals: a goal page with no
work, no frontier and no way forward. They stay out of both the list and the
sweep's work list until something backfills them.

`task.create` no longer accepts a `goal`, but Zod was stripping the field
rather than rejecting it, so a released client would report that a goal started
when only an ordinary task existed. It now fails with a message that says to
reload.

The sweep is the safety net for an event-driven chain, and self-hosted
deployments only ever provisioned the task dispatcher schedule, so nothing
called it. `startServer.js` now registers both.

Two advances can overlap — an event hook, a manual nudge, the sweep — and
`runTask` decides whether a run is in flight by reading topics before creating
one, so both could dispatch the same Work and bill it twice. The coordinator
claims the task with a conditional status update first, and releases it if the
dispatch throws.

Deleting a goal cascaded its graph away while leaving its Work Tasks and their
agent operations running, spending budget with nothing left on screen to stop
them. Deletion now cancels running topics and pauses those tasks first.

Project goal cards had no `aid` in the route, so every card on the project
Goals page rendered as a link that did nothing. They fall back to the goal's
own agent.

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

* 🐛 fix(goal): stop a public goal on a private agent from wedging its Work

Two problems with the visibility the previous commit added.

`GoalService.create` still declared its own narrower config type, so the
shared `GoalConfig` field did not type-check on the server — which the CI
type-check caught and the runtime tests could not.

Forwarding an explicit `public` to `createTask` also buys nothing: it is what
the service derives anyway, and stating it outright trips the "a public task
can never run on a private agent" invariant, leaving the Work throwing on
every tick. Only `private` — the value that actually has to be preserved — is
passed through now.

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

* 🐛 fix(goal): keep the page live, claim recovery retries, close terminal goals

Three more from the review, all consequences of the server now driving goals.

The graph snapshot only revalidated on focus. That was fine when the client
ticked the goal itself and refreshed after every step; now the server advances
it, so an open page sat on its first snapshot — a spinner still turning on a
goal that had already finished. It polls while the goal is in a status the
server can move, and stops for `review`, `paused` and the terminal ones, where
the next change comes from a person and their action refreshes it anyway.

The dispatch claim added in the previous commit only covered starting a backlog
Work. `WorkRecoveryCoordinator.recover` spawns its own run for the two
automatic-retry paths, so overlapping advances could still each pay for an
attempt. The claim now lives in `recover`, where the spawn is. Both callers
hand it a task row read before they paused it, so it compares against what the
row says now — and treats an already-`running` task as someone else's claim
rather than its own.

The goal page still offered Advance and Add Task on an achieved, failed or
canceled goal. The coordinator returns immediately for those, so Advance
reported nothing happened and a Work added there stayed `proposed` with no way
to run it. Those controls are gone once a goal is closed.

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

* 🐛 fix(goal): stop the client racing the queued driver, free orphaned claims

Three more from the review.

Creating a goal queued an advance on the server and then fired a single tick
from the client. A tick is one step: the one that binds the Work's task, not
the one that starts it. If that tick won the node claim, the queued driver saw
the claim, reported `waiting_external` and stopped — leaving the task in
backlog with nobody to start it until the sweep noticed. Both creation paths
now run the full driver, so whichever one claims the Work carries it through to
starting it and the loser stops harmlessly.

The dispatch claim is taken just before `runTask` creates the task's topic. A
worker that died in that sliver left the task `running` with no operation
behind it, and `recoverAbandonedWork` returned immediately because there was
nothing to reclaim — so every later advance reported `waiting_external` and the
Work never ran again. A claim with no run behind it is now handed back once it
is older than the operation lease, which is also long enough that a run about
to start is never mistaken for an abandoned one.

The Advance tooltip still told users graph goals have no server-side driver and
only move while a client advances them — the opposite of what this branch
implements, and an invitation to sit on the page pressing the button.

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

* 🐛 fix(goal): scope private goals, and stop attempts capping the whole graph

Five more from the review.

A goal marked private only made its Work Tasks private. The goal row itself has
no visibility column, so workspace scoping alone left its requirement, its
graph, its findings and its whole event log readable by every member — the one
thing the private toggle exists to prevent. `buildGoalScopeWhere` now enforces
the stored `config.visibility` on every goal and graph read. A real column that
`buildWorkspaceWhere` can take directly is the better long-term shape; this
holds the guarantee without a migration.

The creation form's only iteration field means "attempts on one Work", and it
was also being sent as `maxRounds` — which the coordinator counts across every
Work in the graph. With the default of three, three finished tasks exhausted
the budget and a fourth never started despite having run nothing. The two
limits are separate now, and the same conflation is gone from both `/goal`
paths.

`null` in that field is the manifest's documented "no user-specified cap", but
it was folded into the default of three, so clearing it quietly capped the goal
anyway. It now leaves the setting off and lets the recovery policy default
apply.

Raising the budget of a goal the budget had stopped left it `paused`, and
`tick` refuses to move a paused goal — so the advance this mutation queues
returned immediately and the user had to find Resume as a second gesture. A
goal whose budget was actually binding is reopened; one somebody paused
deliberately is left alone.

Goals created from a project page have no responsible agent, so the earlier
`goal.agentId` fallback was still empty and every card there was a dead link.
They route to a bare `/goal/:goalId`, mirroring how tasks already fall back to
`/task/:taskId`.

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

* 🔒 fix(goal): gate goal deletion and inherit a private agent's visibility

Four more from the review.

Deleting a goal only checked that the caller holds `agent:update` in the
workspace — which says a member may change goals, not whose. Any member could
delete a colleague's goal and cascade its whole graph away. It now asserts the
same creator-or-workspace-owner rule that task deletion already applies.

`/goal` runs as an agent and never states a visibility, so a goal created by a
private agent was workspace-public even though the task it dispatched was
inferred private. `GoalService.create` derives it from the responsible agent
the way `createTask` does, which covers both the client executor and the server
runtime instead of asking each to remember.

Deleting a goal cancelled its running work but swallowed a cancellation
failure. If the runtime or a device gateway was unreachable the graph was
deleted anyway, leaving an operation spending with nothing left that could stop
it. That failure now aborts the deletion and says why.

The bare `/goal/:goalId` route rendered only a title, and the actions menu it
omitted is the only place a goal can be deleted — so a goal created from a
project could never be removed. The menu is no longer nested under the agent
breadcrumb, and both it and the post-delete destination fall back to the
project scope.

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

* 🐛 fix(goal): park unmovable goals and keep a failed kickoff from looking unsaved

Two more from the review.

A goal with no Work — or whose every remaining Work is blocked with nothing
running to unblock it — can only ever report `no_progress`, but it stayed
`running`. The sweep takes the newest stalled goals up to a limit, so enough of
these permanently occupy that window and every older stalled goal stops being
swept at all. They are now parked the same way a goal waiting on a decision
gate is: the row says what it is, and the scan moves on to goals that can
actually move.

Creating a goal through `/goal` reported a kickoff failure as `GoalCreateFailed`
even though the goal was already committed — so the agent, told nothing was
created, would create a second one and both would do the same paid work. Once
creation succeeds the goal id is always reported; a failure to start it now
says so and points out that the server will pick it up. Both the client
executor and the server runtime had the same boundary.

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

* 🐛 fix(goal): queue the advance for goals created by the server runtime

The previous commit stopped reporting a failed kickoff as a failed creation,
telling the agent the server would pick the goal up instead. That is true for
`/goal` from a client, which goes through the `goal.create` route, but the
server-side tool runtime calls `GoalService.create` directly and nothing
scheduled anything. Outside queue mode there is no local timer for the goal and
no recurring sweep either, so a kickoff failure left it stranded in `planning`
while the agent had been told not to create it again.

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

* ⚡️ perf(goal): stop every goal card refetching the whole graph each second

The card's timer drove both the elapsed clock and a full graph revalidation,
from when the client had to keep a goal moving itself. The store already polls
the snapshot while the goal is one the server can move, so this was a second,
five-times-faster copy of that: ten cards in a conversation pulled the whole
node and event history six hundred times a minute. Worse, a card whose fetch
had failed had no status to read, resolved as unsettled, and kept asking
forever. The timer now only ticks the clock.

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

* ⏪️ revert(goal): drop the goal visibility control

Goals do not carry a privacy promise for now, so the whole mechanism comes out
rather than sitting half-wired: the `config.visibility` field, the creation
input that set it, the agent-derived default, the stamp the coordinator put on
each Work Task, and the `buildGoalScopeWhere` predicate that enforced it on
reads. Goal and graph reads are back to plain workspace scoping.

The chooser goes with it. Leaving the control on the creation form while
nothing acts on it is exactly the state this branch was reported for: a user
marks a goal private and it is not.

Deleting a goal still requires being its creator or a workspace owner — that is
about who may destroy someone else's work, not about who may read it, and it
matches what tasks already enforce.

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

---------

Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 22:00:49 +08:00
Rdmclin2 fad16ebb1d 🐛 fix(chat-adapter): adapt to chat@4.39.0 fetchData Buffer | ArrayBuffer contract (#18790)
Claude-Session: https://claude.ai/code/session_01523f6A6kqKpJQSM6HmgzyE

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 20:42:53 +08:00
Innei 76fecbc714 🐛 fix(chat): download chat images through the file proxy (#18784)
The image viewer downloads by reading the source as a blob, which the file
proxy URL can never satisfy: `/f/:id` redirects to a presigned URL on an
origin that sends no CORS headers, and a cross-origin redirect strips the
Origin header to `null`, so no bucket rule can allow it either.

Chat image previews now resolve the proxy's `download=1` response instead,
which carries a Content-Disposition and saves the file under its stored name.
Other sources keep the blob path.

Requires @lobehub/ui 5.35.0 for the `preview.onDownload` option.
2026-08-28 18:57:43 +08:00
Neko 8ad5b8bfd7 🔨 chore: sync release/weekly-20260827 with latest canary (#18786)
* ✨ feat(heterogeneous-agent): support Grok provider bindings (#18737)

* ✨ feat(db): add workspace document comment schema (#18726)

* ✨ feat(db): add workspace document comment schema

* ♻️ refactor(db): use UUIDs for document comments

* 🐛 fix(db): sync comments during document transfers

* 🐛 fix(db): harden document comment transfers

* 🔨 chore(db): regenerate document comment migration

* 🐛 fix: align chat output cost estimate ratio (#18748)

* ♻️ refactor(desktop): migrate renderer OTA to V2 packs (#18745)

* ♻️ refactor(desktop): migrate renderer OTA to V2 packs

* ⚡️ perf(desktop): decode renderer packs asynchronously

* 💄 style(toast): align toast radius and offsets with desktop layout container (#18755)

* 👷 ci(desktop): read the overlay repository from a variable instead of hardcoding (#18759)

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): add PR-time verify workflow for the share micro app (#18757)

* 👷 ci(share): add PR-time verify workflow for the share micro app

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): build same-repo PR previews with the cloud overlay

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 📝 docs(skill): record dual-repo share verify and skeleton layout parity

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): keep overlay naming neutral in public workflow

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 👷 ci(share): reuse the existing overlay token secret

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* ♻️ refactor: migrate lobe-cron to lobe-task (#18753)

* 💄 style(share): center skeleton hero to match hydrated layout (#18756)

Claude-Session: https://claude.ai/code/session_01TQcoZnDo12NGCxWZDJpciZ

* 💄 style: register dedicated skeletons for all lazy routes (#18752)

* 💄 style: add dedicated home route skeleton

* 💄 style: register route skeletons for all lazy routes

* ✅ test: align unit test mocks with base-ui component migration

* ✅ test: complete base-ui mock alignment for migrated components

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* 📌 chore: pin @lobehub/ui to 5.33.1 until base-ui migration completes

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ♻️ refactor: finish ActionIcon, Avatar, Tag and Text base-ui migration

Completes #18729: migrates the 45 remaining call sites (plus type-only
imports) to @lobehub/ui/base-ui and drops the temporary 5.33.1 pin now
that the repo is clean under 5.33.3's expanded deprecation list.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ♻️ refactor: migrate direct antd Tag and Avatar imports to base-ui

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✅ test: stub base-ui ActionIcon in input banner queue case (#18760)

* ✅ test: stub base-ui ActionIcon in input banner queue case

The real base-ui ActionIcon requires the app-level MotionProvider, which
the unit env does not set up.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✅ test: render real base-ui components via a global MotionProvider stub

Redirect @lobehub/ui's internal MotionProvider module to a static stub in
vitest config so base-ui components render in unit tests without the
app-level ConfigProvider, and add composable canonical base-ui stubs
(~base-ui-stubs) for tests that want simplified DOM.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✅ test: unblock closed antd-style factories against migrated base-ui imports

Six antd-style mocks were closed factories missing createStaticStyles /
keyframes / createGlobalStyle that real base-ui style modules now need;
compose them over importOriginal. PluginTag and GroupItem retarget their
UI stubs to the base-ui import path the components migrated to.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ✨ feat: support workspace document comments (#18750)

* ✨ feat: support workspace document comments

* ✨ feat: enhance document comments with mentions and attachments

* 🐛 fix: synchronize document comment activity

* 🐛 fix: guard document comment attachment uploads

* 🐛 fix: preserve comment synchronization metadata

* 🐛 fix: scope document comment API routes

* ✅ test: align base-ui component mocks

* ✅ test: complete base-ui mock migration

* ✅ test: clean up rebased component mock

* ♻️ refactor: align comment imports with base-ui

* ✅ test: align mocks with latest canary

* 🐛 fix(connect-agent): prevent device loading state flash (#18768)

* 🐛 fix(connector): canonicalize connector ownership (#18763)

* 🐛 fix(eval): rm eval hints in run topic titles (#18771)

* 🐛 fix(eval): isolate run topic titles

* 🐛 fix(eval): keep ordinal topic titles

* 🐛 fix(eval): retain run topic input

* 🐛 fix(eval): remove fallback case label

* 🐛 fix(agent): scope heterogeneous default names to shared workspaces (#18770)

* 👷 ci: upgrade pnpm setup action to v6 (#18777)

* 🐛 fix(queue): encode QStash deduplication IDs (#18778)

* 🐛 fix(agent): recover sub-agent bridge answer from isolation thread for hetero children (#18781)

* 🐛 fix(agent): recover the sub-agent bridge's answer from its isolation thread for hetero children

callAgent-invoked heterogeneous (CLI-driven) sub-agents backfill their
parent's placeholder tool message with "Sub-agent completed without a
textual answer." even when the CLI produced a real reply — always, in
production's queue mode (AGENT_RUNTIME_MODE=queue), not intermittently.

completeSubAgentBridge/completeGroupActionMember resolve content from
params.finalState, falling back to coordinator.loadAgentState(operationId)
(the standard runtime's Redis-backed step state) when finalState isn't
passed in-process. Both createSubAgentBridgeHook and
createGroupActionMemberBridgeHook's webhook config deliberately strip
finalState/lastAssistantContent from the QStash payload via `eventFields:
['operationId', 'reason', 'status']` (to keep the payload lean), so the
queue-mode callback always arrives with finalState undefined — and hetero
children never write into the coordinator's runtime state at all (confirmed
zero `saveAgentState` calls under services/heterogeneousAgent/), so the
fallback is structurally dead for them. heteroFinish already resolves the
real answer server-side (via topics.metadata.heteroCurrentMsgId ->
messageModel.findById) before dispatching, but that resolution never
reaches the callback.

Add resolveLastAssistantContentFromThread(threadId): queries the child's own
isolation thread directly (thread ids are globally unique, no agentId/topicId
needed to disambiguate) — the same source heteroFinish itself reads. Used as
a fallback in both completeSubAgentBridge and completeGroupActionMember
(isolated mode) when the existing finalState/coordinator resolution comes up
empty, before falling through to the "no textual answer" stub.

Two regression tests added (one per bridge method), reproducing the actual
hetero shape (loadAgentState resolves null, not a message-stripped state) —
verified to fail against the pre-fix code and pass with it. Full
AgentRuntimeService.test.ts suite (114 tests) still passes.

Fixes LOBE-13586

* 🐛 fix(agent): gate the thread content fallback on missing finalState

Address a P2 flagged by Codex review on 15a401edd1: the thread-fallback
guard checked only `!lastAssistantContent`, which also fires when a REAL,
authoritative finalState is present but its last turn is legitimately
textless (image-only, or the "preserve an empty leaf" case in
normalizeCompletionMessages). In that case the fallback would still query
the child's isolation thread — and a lagging read that surfaces an EARLIER
real reply from the same thread would silently show stale text instead of
the correct "no textual answer" stub.

Narrow the guard to `!finalState` in both completeSubAgentBridge and
completeGroupActionMember: this is exactly the heterogeneous-child case the
fallback exists for (finalState is never populated for them at all), and
`!finalState` already implies an empty lastAssistantContent, so nothing is
lost for the case the fallback is meant to cover.

Two regression tests added (one per bridge method) proving the fallback is
never invoked when a real finalState says the answer is textless — verified
to fail against the pre-fix (`!lastAssistantContent`) guard and pass with
the narrowed one. Full AgentRuntimeService.test.ts suite (116 tests) passes.

---------

Co-authored-by: AmAzing- <115673583+AmAzing129@users.noreply.github.com>
Co-authored-by: Rdmclin2 <rdmclin2@gmail.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Co-authored-by: Innei <i@innei.in>
Co-authored-by: Rylan Cai <67412196+cy948@users.noreply.github.com>
Co-authored-by: Tsuki <76603360+sudongyuer@users.noreply.github.com>
Co-authored-by: LiJian <onlyyoulove3@gmail.com>
2026-08-28 17:40:47 +08:00
Innei 7da5da7052 💄 style: register dedicated skeletons for all lazy routes (#18752)
* 💄 style: add dedicated home route skeleton

* 💄 style: register route skeletons for all lazy routes

* ✅ test: align unit test mocks with base-ui component migration

* ✅ test: complete base-ui mock alignment for migrated components

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* 📌 chore: pin @lobehub/ui to 5.33.1 until base-ui migration completes

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ♻️ refactor: finish ActionIcon, Avatar, Tag and Text base-ui migration

Completes #18729: migrates the 45 remaining call sites (plus type-only
imports) to @lobehub/ui/base-ui and drops the temporary 5.33.1 pin now
that the repo is clean under 5.33.3's expanded deprecation list.

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ

* ♻️ refactor: migrate direct antd Tag and Avatar imports to base-ui

Claude-Session: https://claude.ai/code/session_01UTDivfaszDuBjpPn7PV7gQ
2026-08-28 01:59:58 +08:00
Innei f32a4eb9c5 ♻️ refactor: migrate ActionIcon, Avatar, Tag and Text to @lobehub/ui/base-ui (#18729)
The antd-based wrappers are deprecated since @lobehub/ui@5.33.0. This moves every call site to the base-ui subpath.

Beyond the import path, the migration required:

- ActionIcon now renders a native <button> instead of a div: dropped the hardcoded 'nativeButton: false' in ActionDropdown's trigger props (it overrode the library's own inference, so Base UI applied role/aria-disabled to a real button), and retyped the dictation control refs to HTMLButtonElement
- Avatar is no longer antd's: replaced src/icon with avatar, dropped gap (no equivalent)
- Tag is no longer antd's: dropped bordered={false}, which antd v6 already ignored whenever variant was set — and the wrapper always set it
- prop types (ActionIconProps, AvatarProps) now come from the base-ui subpath, since the legacy ones carry antd props and a div ref
- seven files that still imported Button from @lobehub/ui or antd are migrated too, since touching them surfaced the existing no-restricted-imports error: variant=filled -> type=fill, variant=outlined + color=default -> type=default

Verified: type-check clean, 503 tests pass across ChatInput/NavPanel/User, and the migrated surfaces render with no Base UI warnings.
2026-08-27 15:43:45 +08:00
Arvin Xu e1bc7e1bd6 🐛 fix: harden Goal acceptance runtime (#18670)
* 🧪 test: bootstrap local device gateway

* 🐛 fix: preserve goal acceptance contracts

* ♻️ refactor: keep goal criteria domain neutral

* 🐛 fix: collect hetero acceptance evidence server-side

* 🐛 fix: detect hetero evidence by provider

* 🐛 fix: require self-contained goal evidence

* 📝 docs: bound long-running health probes

* 🐛 fix: pin installable UI dependency

* 🐛 fix: pin installable UI in desktop workspace

* ⬆️ chore: unpin repaired UI dependency

* 🐛 fix: charge stale work before recovery

---------

Co-authored-by: Arvin Xu <arvin.x@lobehub.com>
2026-08-26 13:38:36 +08:00
Arvin Xu e41b6cb0de ✨ feat(goal): gate work tasks with acceptance (#18623) 2026-08-23 23:42:06 +08:00
Innei 2df9829a5e ✨ feat(share): extract share pages into a standalone SSR micro app (#18496)
* ✨ feat(share): extract share pages into a standalone SSR micro app

`/share/t/:id` and `/share/page/:id` were client-only routes inside the main
SPA, so a shared link opened on a blank shell and link previews carried the
generic app card. They now live in `apps/share`, built the same way as
`apps/workbench`: a React Router framework-mode app that server-renders on a
Cloudflare Worker for cloud, plus a plain SPA build that Next serves for
self-hosted deployments.

What the server renders is the shell, the hero and a complete meta set — title,
description and the OG/Twitter card — resolved from the route loader through a
per-request tRPC client. The conversation and page-viewer stacks stay on the
client: `clientOnly()` gates them and, in the SSR environment, a resolver stub
cuts every `*.client` module out of the worker graph entirely. Gating the render
alone does not help, because the dynamic import still pulls the graph into the
bundle.

Keeping that worker deployable took three more cuts, each found by tracing the
module graph rather than guessing:

- `@/store/user` is stubbed for SSR — `GroupAvatar` reads the viewer profile,
  and the store drags the model catalog and every provider behind it (39MB→19MB)
- `shiki` and `shiki/wasm` are stubbed for SSR — the `@lobehub/ui` barrel reads
  the bundled-language catalog at module scope, registering ~400 grammars for a
  highlighter the server never runs (19MB→7.6MB, 1.78MB gzip)
- SSR i18n resolves only the namespaces the shell renders; the generic loader
  would register every locale x namespace pair as a worker chunk

The conversation list keeps its `headerSlot` while it shows the skeleton. It
returned early before, so mounting the list to fetch blanked a title the server
had already rendered.

`/share` is removed from the desktop and mobile routers and rewritten by the
Next middleware to `/spa-share/<locale>/…`, mirroring how `/verify` reaches
workbench. The cloud worker is published from lobehub-cloud, which builds this
same app against its business overlay, so this repo ships no deploy workflow
for it.

* 🐛 fix(share): preload the error namespace so the boundary renders translated

The root ErrorBoundary reaches for the `error` namespace, which SSR did not
bundle, so a failed share page rendered `error.title error.desc error.retry`
verbatim. Fetching it on demand does not fix that case: a chunk-load failure is
exactly when the client can never load the dictionary to correct itself.

* 🐛 fix(share): drop the dev template fetch that served the main SPA

The handler asked the main Vite server for `/index.share.html`, which does not
exist at the repo root. Vite answers that with the HTML fallback — 200, main SPA
shell — so `/share/*` in `bun run dev` silently booted an app that no longer has
those routes. Share is developed against its own Vite server, so the branch goes
rather than the file being added.

`spaDevShells.test.ts` guards both directions: every remaining dev template must
exist at the root and point at its own app entry, and share must not reintroduce
one.

* 📝 docs(skill): record the cloud-overlay split in split-micro-app

Share is the second micro app and the first whose rendering code lives in the
lobehub-cloud business overlay, so the skill needed the ownership rules that
decides which repo builds and deploys, and the Vite 8 tsconfig-nearest-importer
landmine that makes an overlay build ship the open-source fallback with a green
build.

Also folds in what the extraction turned up: the full Docker chain, the dev shell
that answers 200 with the wrong app, shiki and @lobehub/ui bundle cuts, the error
namespace, hydration and virtualized-prefill rules, the asset prefix convention,
and the prod promote gate.
2026-08-20 17:53:41 +08:00
Innei 93b20ef2b6 ✨ feat(terminal): clickable links, split panes and Ghostty command-key bindings (#18500)
✨ feat(terminal): clickable links, split panes and Ghostty ⌘ keybindings

Links: xterm 5.5 already parses OSC 8, so a linkHandler plus
@xterm/addon-web-links covers explicit hyperlinks and bare URLs alike, both
on a single click. The renderer gates on http(s) before handing the URL to
shell.openExternal, which does no checking of its own — terminal output is
attacker-reachable.

Split: a tab now holds a list of panes (one PTY session each) with relative
widths, rendered side by side with a draggable divider. Closing the last pane
of a tab closes the tab, so the existing collapse-on-empty flow still holds.

Keybindings: xterm bails on metaKey for arrow keys, leaving ⌘ to the embedder,
so ⌘←/→/⌫ sent nothing at all. Wired to Ghostty 1.3.1's defaults, and the
handled keys are cancelled so Chromium can't read ⌘← as history-back.
2026-08-20 03:48:23 +08:00
Innei 1001a58a22 ✨ feat(workbench): React Router framework mode SSR on Cloudflare Workers (#18473)
* ♻️ refactor(spa): extract verify/acceptance into apps/workbench

Move the Workbench SPA into apps/workbench and serve /verify and
/acceptance from that runtime for every web UA. The main SPA no
longer registers those page routes, so they drop out of its bundle.

* 🔧 chore(docker): wire the workbench workspace into image builds

Copy apps/workbench/package.json before pnpm i so Docker can resolve
the extracted SPA. Keep generateSpaTemplates optional for Cloud, and
require the HTML only when WORKBENCH_REQUIRED=1 (build:docker).

* ♻️ refactor(workbench): hard-navigate only from markdown entity links

Keep Link and useWorkspaceAwareNavigate in-router. Markdown verify,
acceptance, and mobile agent-doc links document-navigate. Drop the
__WORKBENCH__ Vite define; Electron still skips hard-nav for portals.

* ♻️ refactor(verify): slot-inject origin conversation and decouple shared components

* 🐛 fix(i18n): make vite glob loaders independent of the vite root

* ✨ feat(workbench): React Router framework mode SSR on Cloudflare Workers

* 🐛 fix(verify): render viewer-timezone timestamps only after hydration

* 💚 ci(workbench): deploy with affected detection and PR build verification

* 📝 docs(skills): micro-app split and gateway routing playbook

* 🐛 fix(workbench): track app/lib swallowed by the root lib/ ignore rules

* ✨ feat(workbench): signed-in acceptance list sidebar on detail pages

* 🐛 fix(workbench): survive shared-origin gateway quirks in client navigation

* 💄 style(workbench): surface error detail on the boundary page

* 💄 style(workbench): adopt the shared ErrorCapture page for the root boundary

* 💄 style(verify): drop the app-home back arrow from the acceptance list panel

* 🔧 chore(workbench): persist only error logs in wrangler observability

* 📝 docs(skills): record Docker vs Cloud hosting for micro apps

OSS Docker still builds and serves the SPA in-image. Cloud Next must
not build, rewrite, or CDN-upload it — gateway routes to the Worker.

* 💄 style(workbench): pin the acceptance logo while the list panel slides

* 🐛 fix(workbench): only show the acceptance sidebar when signed in

* 🐛 fix(verify): drop namespace prefix on bound verify translations
2026-08-19 18:21:00 +08:00
Arvin Xu 3c551a9dfc ♻️ refactor(verify): unify acceptance criterion UI and remove lobe-delivery-checker (#18376)
* ♻️ refactor(verify): unify acceptance criterion UI and remove lobe-delivery-checker

- Add shared CriterionList / CriterionRow / CriterionRequiredChip and a single
  CriterionEditor + openCriterionEditModal under src/features/Verify/CriterionList
- Migrate the /goal intervention card, CreateGoalModal, TaskVerifyConfig,
  PendingAcceptanceCheckList, TaskAcceptance and GoalDetailPage to the shared
  row/editor, deleting the two duplicated criterion edit modals
- Unify the required chip to a blue info tag labelled 必选 (verify:criterion.*)
  and drop the divergent 必要/必需/必选 key trio across chat/plugin namespaces
- Delete the unused builtin-tool-lobe-delivery-checker package with its server
  runtime, registry entries, i18n keys and the orphaned createPlanFromCriteria

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

* 📝 docs(acceptance): note stale-cache fingerprint of a cross-wired browser session

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

* ♻️ refactor(verify): apply acceptance review feedback on criterion editing

- Drop the pending-status icon and C{seq} anchor from goal-creation draft
  rows — those belong to the post-creation check list, not authoring
- Remove TaskVerifyConfig's edit mode (SortableList reorder, side drawer,
  Done button): every row now edits itself through the shared criterion
  modal, matching the goal-creation interaction; regenerate moves into the
  header overflow and add/save-as-template become always-visible footer
  actions
- Simplify the criterion edit modal title — no C{seq} interpolation

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

* 📝 docs(acceptance): record ingest-side identifier/internal-id gap in probe patterns

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

* ♻️ refactor(verify): apply round-2 acceptance review feedback

- Keep the C{seq} anchor on goal-creation draft rows; only the pending
  status circle stays out of authoring
- Drop the at-rest delete button from task criterion rows — removal lives
  inside the editor modal
- Move save-as-template from the footer into the header overflow menu
- Show the verifier tag and required chip on goal-detail check rows

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

* ♻️ refactor(verify): fold task criterion row edit and delete into a per-row overflow

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 17:01:04 +08:00