mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
e13ad41733
* ✨ 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>