mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
fix/topic-switch-message-refresh
21 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4367a885eb | 🐛 fix: clarify artifact delivery and preserve tool refusals (#19409) | ||
|
|
6261fa7f84 | 🐛 fix: surface sandbox recreation in tool results (#19329) | ||
|
|
51b7420c95 |
🐛 fix(local-system): report computer-tool failures as failures, and refuse ambiguous edits (#18539)
* 🐛 fix(local-system): report computer-tool failures as failures `ComputerRuntime.errorOutput` hardcoded `success: true` and left `error` unset, so every failing file/shell operation was recorded as a successful call. The visible symptom is `editFile`: an `old_string` that isn't in the file changes nothing, yet the trace inspector prints ✓, `usage.tools.byTool[].errors` stays 0, and the EditLocalFile renderer — which already branches on `pluginError` to draw an "Edit Failed" alert — gets neither a diff nor an error and renders an empty card. 790 such edits in the last 7 days (~100/day) all counted as successes. Report the failure on both fields. `content` is untouched, and `ToolMessageReorder` prefers a non-empty `content` over `pluginError`, so the text the model reads does not change. `error` is a fresh `{ message }` rather than the service's own object: `executeToolWithRetry` escalates on `error.kind === 'retry'`, and these calls were never retried while they claimed success. Also give `editFile` a reason instead of a bare "not found". The miss is almost always re-indented text, a case difference, or content that drifted since the last read — all decidable from the file already in hand. Naming the cause saves the agent a readFile + LLM round trip, which at a 190k context costs 30-60s of wall clock. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(local-system): refuse an ambiguous editFile instead of guessing With `replace_all` unset, an `old_string` matching in several places was resolved by taking the first match and reporting "Successfully replaced 1 occurrence(s)" — the caller never learned there was a choice, let alone that the edit may have landed on the wrong occurrence. Silently editing the wrong line is worse than not editing at all, because nothing downstream flags it. Refuse the call, name the matching lines, and state the two ways out (more surrounding context, or `replace_all: true`). The manifest and the system prompt now carry the same contract so the model writes a unique `old_string` in the first place rather than discovering the rule from an error. Also fix two CLI assertions that pinned the old "failure reported as success" envelope for getCommandOutput / killCommand against an unknown shell id. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 🐛 fix(local-system): stop previewing an edit the runtime will refuse The approval card rendered `String.replace`'s first-match diff and labelled it "Replace first occurrence only", so with a non-unique `old_string` the user could approve a concrete diff that the runtime then refuses without touching the file. Count the matches in the preview and show the reason plus the two remedies instead. The new copy takes a plain `times` variable rather than `count`: i18next reads `count` as a plural selector and would look for `_one` / `_other` keys that do not exist. Also update the desktop `LocalFileCtr` regressions that pinned the old behaviour — one supplied two `Hello` matches and expected the first to be written, the other compared the not-found message by exact string. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
95dfa1d389 |
🐛 fix(device): probe the sandbox helper this app ships, not the one the backend guesses (#18176)
* 🐛 fix(device): probe the helper this app ships, not the one the backend guesses Reported from an installed canary build: the Local Sandbox row said Sandbox Runtime dependencies are unavailable: srt-win.exe not found. Looked in: …\resources\app.asar\vendor\srt-win\x64\srt-win.exe while `resources/sandbox-runtime/vendor/srt-win/x64/srt-win.exe` was sitting right there. The Set-up button was equally useless, because provisioning was never the thing that was missing. `probeSandboxCapability` went through `SandboxManager.checkDependencies()`, which resolves the helper via the backend's own `getSrtWinPath()` — a path relative to the backend's package directory. Bundling that package into the main process makes the computed path land inside `app.asar`, where no file can exist. The launch path was already immune (it passes `windows.srtWin.path`); only the probe still asked the wrong question, so a correctly installed app reported a missing binary and offered a button that could not change the answer. Windows now runs the checks directly against the resolved path, mirroring the backend's own sequence — binary present, sandbox user provisioned with a readable credential, WFP filters installed — and stages the helper first, so the path it validates is the one the launch will use. `cannot-read` on filter enumeration stays a non-failure: it is admin-gated, and the behavioural egress check at `initialize()` is what fails closed. macOS and Linux keep the backend's own check. Neither resolves anything relative to the app, so neither was affected. Verified against the installed build's own layout: the probe that reported "not found" now returns available. Two follow-ons: - ship the backend's package.json beside the binaries, so the staging directory stays keyed on the real version instead of falling back to the binary's size — the fallback worked, but defeated per-version isolation in exactly the case it exists for, an installed app being updated; - make the `createSrtConfig` assertion platform-aware, since the Windows helper override is absent everywhere else and an exact-match expectation would fail on one platform or the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(device): make one function decide the sandbox helper path Review follow-up on both findings. **Probe and launch could disagree.** The probe fell back to the unstaged source when staging failed, while `createSrtConfig` fell back to *no override at all* — letting the backend resolve its own package-relative path, which is inside `app.asar` once bundled. A host where staging cannot happen (ProgramData locked down, disk full) was therefore advertised as available and failed on the first command: exactly the "claims a guarantee it never applied" outcome this feature exists to avoid. `resolveEffectiveSrtWin` is now the single place that resolves and stages, and the probe, the launch, and setup all call it. They cannot diverge by construction. It falls back to the unstaged source rather than giving up, because a per-machine install already sits somewhere the sandbox account can read — staging is an accommodation for per-user installs, not a precondition. **The probe had no test.** The previous commit changed `probeSandboxCapability` but only asserted on `createSrtConfig`, which already used the staging helper — so putting the probe back on `SandboxManager.checkDependencies()` would have gone unnoticed, and AGENTS.md asks for a regression test that fails before the fix. Added one covering the Windows branch directly: it pins that the backend check is *not* consulted, that the resolved path is what gets validated, that an unprovisioned account stays distinguishable from a missing binary (only one of those is what the Set-up button fixes), that `cannot-read` filter enumeration is not a failure, and that other platforms keep the backend check. Five of the eight fail against the pre-fix implementation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(tool-runtime): stop runCommand swallowing the reason it failed Every sandboxed command came back as `[UNKNOWN_EXEC_ERROR] Tool execution failed`, with the actual reason discarded. `normalizeResult` put the daemon's `error` inside `result.result.error`, but `ComputerRuntime.errorOutput` reads the ServiceResult's own `error`, then `state.stderr`, then `state.error`. A command that fails *before spawning* fills none of those — there is no process, so no stderr and no exit code — so the chain ran to its last-resort fallback every time. That is the exact shape of every Local Sandbox refusal ("requires a working directory", "unavailable on this device"), which is how a precise, actionable message turned into a generic one that told the user nothing and gave the model nothing to correct. The same gap was already fixed for editLocalFile / grep / glob; runCommand was missed. Surfaces `raw.error` at the top level like those tools do, and pins both halves: a pre-spawn failure keeps its reason, and a successful run still reports whether it was actually sandboxed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e9b6d00ab3 |
✨ feat(device): add a Local Sandbox execution environment (#18143)
* ✨ feat(device): add a Local Sandbox execution environment Adds a "Local sandbox" row to the execution-environment switcher, next to "Local device". Picking it runs the agent on the same machine, but confines its shell commands: writes limited to the run's working directory plus the OS temp dir, and no network. The sandbox backend (`@lobechat/device-sandbox`, SRT) has been in the tree since #17476 as an opt-in with no caller. This wires it to a user-facing choice. Modelled as `agencyConfig.localSandbox`, a modifier on `executionTarget: 'local'`, rather than a sixth `DeviceExecutionTarget`. Every existing routing rule — web coercion, gateway upgrade, bot-trigger promotion, fixed-workspace policy — stays literally unchanged, and the flag composes if sandboxed execution later extends to `device` targets. Both execution paths converge on `isLocalSandboxEnabled(config, target)`, so they cannot disagree about which runs are fenced: - gateway-routed runs: `ServerToolTransport` resolves it against the run's execution plan, the Local System device-proxy forwards it to `runCommand` - in-process desktop runs: resolved in the client tool context The stored flag alone never decides — it applies only once the effective target actually resolved to `local`, so a config that landed on `sandbox`/`device` is never reported as fenced. Fails closed throughout. `onUnavailable: 'deny'` means an unavailable backend aborts the command instead of silently running it unconfined; a sandboxed run with no working directory is refused rather than confined to the app install directory; and the picker hides the row entirely unless the host reports a working backend (SRT supports macOS and Linux only, and Linux depends on host binaries that may be missing). Scope, deliberately: shell commands only. File tools run in the desktop process, `lh` keeps its in-app route (it needs the credentials and network the sandbox strips), and heterogeneous CLI agents spawn through their own path. User-facing copy says "commands", never "the agent". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ✨ feat(device): add a network toggle to the Local Sandbox, and stop lying about availability Three changes, all driven by running this on a real Windows host. **The platform claim was wrong.** SRT supports Windows — `isSupportedPlatform()` returns true on win32; it needs a one-time `windows-install` that provisions a dedicated `srt-sandbox` account and a user-SID-keyed WFP filter set. Comments and copy that said "macOS and Linux only" are corrected. **The row is disabled, not hidden, when the host can't sandbox.** On Windows "unavailable" usually means "not installed yet", so hiding the feature strands the user with no way to find out why it's missing. It now renders disabled with the host's own reason, matching how the Cloud Sandbox row already handles an unsupported provider. **The cheap probe is not the whole truth.** `checkDependencies()` only verifies the backend is installed; the egress fence and the secondary-logon spawn are exercised only when the first real process starts. Observed on a real machine: probe reports available, then every command fails at `CreateProcessWithLogonW`. `runCommand` now reports a failure to *establish* the fence (never a command's own non-zero exit) through `onSandboxUnavailable`, the desktop downgrades its verdict for the rest of the app run, and the picker revalidates on open — so a broken host greys the row out after one attempt instead of offering an environment that can only fail. **Network toggle.** A switch on the Local Sandbox row, default off, opening the package-registry allowlist (npm / PyPI / crates.io / RubyGems / Go / GitHub / GitLab). It cannot be "allow everything": the backend's schema rejects a bare `*` and patterns like `*.com` outright, so network access is always some allowlist. Copy says so — "common developer domains", never "allow network" — and the hover tip names what is opened and states the limitation. Stored as `agencyConfig.localSandboxNetwork`, a separate field from `localSandbox` so the choice survives toggling the sandbox off and back on, and threaded through both execution paths alongside it. Toggling the switch deliberately does not change which environment is selected: same dormant semantics the sandbox flag already has, so nothing switches behind the user and the popover stays put. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ✨ feat(device): let the desktop app set the sandbox up itself Installing the desktop app should be enough to get the Local Sandbox — a user should never have to find and run `npx sandbox-runtime windows-install` before a product feature works. Two things stood between them and that. **A dead-end row.** When the backend was missing, the row was disabled with a diagnostic and nothing else. It now carries a "Set up" button that provisions the backend on their machine: one elevation prompt, idempotent, and the row becomes usable without leaving the popover. Only offered where the app can actually deliver — Windows. macOS needs nothing (Seatbelt ships with the OS), and Linux needs a distro package only the system's package manager may install, so that case shows the command to run instead of a button that would lie. Dismissing the elevation prompt comes back as `cancelled`, not an error: the user declined, so nothing is reported and the UI stays as it was. Setup also clears the sticky capability downgrade — it is the one thing that must be able to lift it, or the button could never work on the very hosts that need it. Setup never runs implicitly. Creating a Windows account and installing network filters is not something to do behind someone's back, so it hangs off an explicit click. **The binary would not have shipped.** `@anthropic-ai/sandbox-runtime` was neither an external runtime module nor asar-unpacked, while `files` excludes node_modules wholesale — so a packaged build would have contained no `srt-win.exe` at all. The probe would have failed, and the new button with it, on every installed copy while working fine in dev. It is now a direct dependency, externalized from the main bundle, and its `vendor/` helpers are unpacked from asar (a file inside app.asar has no real path to spawn). Verified on a real Windows host: the runtime is externalized rather than inlined, the vendor binary resolves from the desktop package, and both the files config and the unpack patterns pick it up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ✨ feat(device): report whether a command was actually sandboxed The execution-environment chip shows what the user *chose*. Nothing showed what a run actually *did* — so a command that executed unfenced looked exactly like one the sandbox confined. That is not hypothetical. A desktop bound to a server without the sandbox injection runs `local` commands through the device gateway with no sandbox flag at all: the command succeeds, unconfined, while the chip still reads "Local sandbox". A security property nobody can observe is one nobody should trust. `createSandboxLaunchPlan` already knows the answer — `SandboxLaunchPlan.sandboxed` — and it was being discarded. It now rides back on `RunCommandResult` and into `RunCommandState`, so the run itself can state the outcome instead of the request restating its own intent. Deliberately tri-state: `undefined` for a request that never asked for a sandbox, so "not fenced" and "never wanted fencing" stay distinguishable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(device): make the sandbox actually work on Windows Verified end to end on a real Windows host for the first time. Three defects stood between the feature and a working fence; all three are invisible on macOS, which is the only platform the backend integration was ever exercised on. **1. The helper was unreadable by the sandbox account.** The sandbox runs the child as a dedicated `srt-sandbox` local user, so every binary in the launch chain must be readable by *that* account. The desktop installs per-user by default, putting `srt-win.exe` under `C:\Users\<name>\AppData\Local\…` — a tree no other local account may read. `CreateProcessWithLogonW` logged the sandbox user on successfully (confirmed by Security event 4624) and then failed at process creation with a bare ACCESS_DENIED, which reads like a broken sandbox rather than a file-permission problem. The helper is now staged into `%PROGRAMDATA%\LobeHub\sandbox\srt-win-<version>\`, whose default ACL grants `BUILTIN\Users` read+execute, and the backend is pointed at it through the `windows.srtWin.path` override it provides for exactly this. Version-scoped so an app update cannot leave a stale helper behind. Granting an ACE on the install directory would have worked too, but a per-user install directory is replaced wholesale on update, taking the ACE with it and silently breaking the sandbox again. **2. The wrapped command ran under a hardcoded `/bin/sh`.** There is no such path on Windows, so every launch died with `CreateProcessAsUserW(/bin/sh): path not found` even once the fence came up. The caller already resolved the right shell for the platform (`getShellConfig` → Git Bash / PowerShell / cmd, command last), so the sandboxed spawn now reuses it and matches the unsandboxed one exactly, flags included. **3. One policy per process made the sandbox usable once per app launch.** The backend holds a single policy, and the working directory is part of its fingerprint — so switching agents, opening a second project, or flipping the network toggle all hit SANDBOX_POLICY_CONFLICT. The runtime now resets and re-initializes when the policy changes and nothing is running; a live command's fence is still never swapped out from under it, so a busy runtime keeps refusing (loudly) instead. Real-host results, driving the product path (`runCommand` + `createLocalSandboxPolicy`) with the process cwd deliberately left under the user profile: identity whoami → srt-sandbox write inside exit 0, file created write outside exit 1, access denied by the OS secret env LOBE_SECRET_CANARY → empty network off curl exit 56, http 000 network on github.com → 200, example.com → 000 sandboxed=true reported on every run Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(device): resolve the client sandbox decision the way every other layer does Review follow-up. The in-process desktop path merged the member override with a plain `resolveAgencyConfig`, while the picker and server dispatch both go through `resolveAgentAgencyConfig` — which ignores that override for a private Agent, and for an author/admin of a public workspace Agent. A stale override could therefore run a command unfenced while the chip still read "Local sandbox": precisely the claim-without-the-guarantee this feature exists to prevent. It now uses the same resolver. One input is genuinely unavailable here — `canManage` comes from a permission hook with no store to read synchronously — so rather than guess, both readings are computed and a disagreement resolves toward fencing. Over-fencing fails loudly and recoverably (a write outside the working directory is refused with a clear error); under-fencing is silent and unsafe. The network allowance takes the opposite rule and requires agreement, since it *widens* what a fenced command may reach. Also pins the fence root. The sandbox policy is built from `params.cwd` on the device, so whoever controls that controls what the command is confined to. Both injection sites already strip the model's own `cwd` and re-inject the run's configured working directory, but nothing guarded it: a regression test now holds the server device-proxy to it, and the desktop records why it may trust the value it receives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(desktop): ship the sandbox helper as a resource, not a runtime dependency Found by actually running `package:win`, which the previous approach never survived: making `@anthropic-ai/sandbox-runtime` a production dependency pulls its four transitive packages into electron-builder's node_modules traversal, and pnpm's layout does not satisfy it — ⨯ Production dependency @pondwader/socks5-server not found for package @anthropic-ai/sandbox-runtime Its JavaScript never needed to be external; Vite bundles it into the main process fine. Only the helper binaries have to exist as real files, because the sandbox spawns them by path. They now ship via `extraResources`, which sidesteps dependency traversal entirely and drops the asar-unpack rule with it. That relocation breaks the backend's own `getSrtWinPath()`, which resolves relative to its package directory — a path that stops existing once the module is bundled. `resolveSrtWinSource` replaces it with an explicit chain: `LOBE_SRT_WIN_PATH`, then `resourcesPath/sandbox-runtime/vendor/...` for a packaged app, then the backend's own lookup for development and tests, where `process.resourcesPath` is undefined. Verified against the produced `win-unpacked`: the binary sits at `resources/sandbox-runtime/vendor/srt-win/x64/srt-win.exe`, the bundled main process carries the resolution branch that looks exactly there, and the backend JS is inlined as expected. (The NSIS step then failed downloading its toolchain — a network timeout in this environment, unrelated.) Also names ripgrep alongside bubblewrap in the Linux instructions: the backend errors on either being missing, and listing one sends the user round twice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
20afc09c71 | ♻️ refactor(local-system): consolidate tool dispatch into a shared runtime entry with cwd injection (#17982) | ||
|
|
3295b933cc |
🐛 fix(local-system): guard oversized glob and spawn errors (#17792)
* 🐛 fix(local-system): guard oversized glob and spawn errors * 🐛 fix(local-system): scope glob cap to agent runtime |
||
|
|
f4cebaa562 |
🐛 fix(builtin-tool): fix local-system grep always returning 0 matches (#17388)
* 🐛 fix(builtin-tool): fix local-system grep always returning 0 matches The builtin local-system grepContent tool consistently returned 0 matches even when content existed, across all models. Two model-independent plumbing bugs were responsible: 1. Search root fell through to process.cwd(). The grep systemRole promises the model that `scope` "defaults to the working directory" when omitted, but nothing injected the effective working directory. An omitted or relative (`.`) scope fell all the way through to the Electron main process `process.cwd()` (`/` in a packaged app), so every scope-less grep searched the wrong directory. Now the executor resolves the search root against `ctx.workingDirectory` — sourced from the same place as the `{{workingDirectory}}` prompt placeholder — so search and prompt agree. 2. Params stripped at the IPC boundary. `denormalizeParams` collapsed grep args to `{cwd, filePattern, output_mode, pattern}`, dropping `-i/-n/-A/-B/-C/glob/ type/multiline/head_limit` and renaming `glob`->`filePattern` (a field the desktop `buildGrepArgs` never reads). Case-insensitive / typed / glob-scoped searches silently lost their flags. Now the full param set is forwarded. Regression tests exercise the real callService->denormalizeParams path and the omitted/relative/absolute scope resolution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(builtin-tool): bind grep default directory to the executing topic The grep working-directory fallback read global active-topic state via `getEffectiveWorkingDirectoryPath()`. When a response started in topic A is still streaming and the user switches to topic B before its grep tool call arrives, the prompt was generated with A's directory but the call resolved against B's — potentially searching the wrong project. Resolve the working directory from the topic captured at request time (`topicId ?? operation?.context?.topicId`, agent fallback unchanged) instead of the active topic. Adds `topicSelectors.getTopicWorkingDirectory(id)` (by-id, active-topic fallback when no id) and threads the captured topicId through `getEffectiveWorkingDirectoryPath(topicId)` from client tool execution. Prompt build still passes no id (active topic is correct at generation time). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
8284a6e2b2 |
✨ feat(local-system): let readFile return images so vision models can see them (#16912)
* ✨ feat(local-system): let readFile return images so vision models can see them Tool-returned images previously reached the LLM only as `` text — the model never actually saw them. Wire the builtin readFile path so an image file becomes a real `image_url` content part (LOBE-6801 builtin path; mirrors CC Read-on-image LOBE-7786 and MCP resource image #13618). - context-engine MessageContent: new processToolMessage reads pluginState.images → image_url parts (vision) or a downgrade placeholder, keeping tool_call_id so providers can pair the result with its call. - tool-runtime: ComputerRuntime.readFile detects isImage → state.images; LocalSystemExecutionRuntime normalizeResult passes isImage/previewUrl; ReadFileState gains images (+ ToolResultImage type). - desktop LocalFileCtr.readFile: image branch resolves a desktop preview URL via localFileProtocolManager (local-file-shell refuses binary), so DB stores a small URL instead of base64; LocalReadFileResult gains isImage/previewUrl. - ReadLocalFile renderer: PreviewGroup thumbnails when state.images present. Desktop-only for now; CLI/cloud-sandbox readLocalFile still refuses binary. Co-Authored-By: Claude <noreply@anthropic.com> * ♻️ refactor(local-system): upload readFile images instead of localfile:// preview URLs The previous approach persisted desktop-only `localfile://` preview URLs (5-min in-memory tokens) into pluginState.images and expected the server-side send path to fetch them — neither holds. Rework the chain to the same convention as the hetero CC Read-on-image echo (#16786): - LocalFileCtr.readFile returns the image bytes as base64 (`imageData`), dropping createPreviewUrl and the dirname(path) workspaceRoot guess that rejected any image nested below an approved workspace root; paths now resolve via local-file-shell's resolveAgainstCwd. Formats limited to png/jpeg/gif/webp (what vision providers accept; svg reads as text) with a 10MB cap. - ComputerRuntime routes the bytes onto state.images as pre-upload `{ data, mediaType }` entries; the renderer uploads them to file storage (uploadToolResultImages) and rewrites to `{ fileId, url }` BEFORE persistence, degrading to the `[Image: …]` placeholder on failure. The gateway path strips the bytes (no upload client in main yet, same deferral as #16786). - MessageContent.processToolMessage only forwards entries with fetchable http(s)/data URLs. - OpenAI builders no longer emit image parts inside tool messages (the API rejects them): Chat Completions flattens the tool message to text and re-attaches images as a user message after the contiguous tool batch; Responses keeps function_call_output textual and appends an input_image user item. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor(desktop): upload readFile images from the main process Move the upload out of the renderer persistence hook into main, mirroring the CLI's uploadFileBuffer (hash dedup via file.checkFileHash → pre-signed S3 PUT → file.createFile) over the same raw Oidc-Auth fetch pattern GatewayConnectionCtr already uses for agentNotify/device.register. - New RemoteFileUploadService (main): buffer → durable { id, url }; declines when no active remote session. - LocalFileCtr.readFile uploads right after reading the bytes and returns imageFileId/imageUrl — base64 never crosses IPC; on upload failure the content degrades to an "[Image: …] (upload unavailable)" placeholder. - ComputerRuntime emits already-uploaded state.images entries; ToolResultImage loses the pre-upload `data` field (url required again). - Drop the renderer-side uploadToolResultImages step and the gateway imageData strip — the gateway path now gets working image references too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ♻️ refactor(desktop): delegate readFile image upload to the embedded CLI Replace the hand-rolled raw-fetch TRPC upload flow with `lh file upload <path> --json` — the CLI already owns the whole pipeline (hash dedup, pre-signed S3 PUT, file record). Run its embedded script with the app's own binary via ELECTRON_RUN_AS_NODE (what the generated wrapper does), execFile with an args array so the agent-controlled path never touches a shell, and inject the desktop session through LOBEHUB_JWT/LOBEHUB_SERVER like CliCtr and the hetero spawn paths; without a session lh falls back to its own login. LocalFileCtr now passes the resolved path instead of reading bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0746a125a9 |
⚡️ perf: shell output optimization (#15777)
* wip: add impl * ♻️ refactor: tighten shell output file changes * 📝 docs: trim shell output wording * ⚡️ pipe shell output to a single temp file * 🐛 wait for shell output close before final snapshot * 🧹 remove shell output byte delta tracking * 🧹 derive shell running state in runtime * 🧹 remove unused shell output truncation helper * ⚡️ pipe shell output through stdio fd * 🧹 keep background shell output lazy * 🧹 simplify shell output root option * 🐛 kill shell process trees * 🧹 simplify shell output paths * 🧹 unify shell output contract * 🐛 ignore stdin for content search commands * 🐛 add search timeouts * 🧹 align grep timeout handling * Revert "🧹 align grep timeout handling" This reverts commit 48ad495d7facf4fdbe9be9bc554ff3486b22a298. * Revert "🐛 add search timeouts" This reverts commit 7e097fd04a2ea7c3598e01e837d46542eb5be6e8. * 🧹 restore grepContent path resolution * 💬 guide search scopes to cwd * 💬 remove stale command output delta prompt * 🧹 simplify shell output dir handling * ♻️ track shell close as lifecycle promise * ♻️ inline shell output date formatting * ♻️ inline shell output helpers * ♻️ align shell spawn error handling * ♻️ simplify output preview arguments * 💬 simplify command output prompt * ♻️ clarify command result state formatting * ♻️ inline command running check * ♻️ minimize command runtime state diff * 💬 simplify command result output text * 💬 avoid duplicate command output file hints * 🐛 preserve grepContent scope in local runtime * ✅ align shell command controller tests * ✅ document shell output lifecycle tests * ✅ clarify nested shell kill test * ✅ remove obsolete shell truncation tests * 🐛 support grep content output mode * 💬 show shell output file size * ⚡️ increase shell output preview budget * 🐛 sum grep count matches * 🐛 update command output callers * ✅ update shell output tests * 🐛 update desktop shell command output * 🐛 fix desktop os mock tmpdir * ✨ split shell stdout stderr outputs * ♻️ clean shell output compatibility fields * ♻️ tighten shell output file diff * ♻️ remove command result output param * ♻️ remove get command new output state * 🐛 fix cli grep content scope forwarding * 🐛 preserve cli grep content params --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> |
||
|
|
78d34d076e |
🐛 fix(cli): cap local file search factory results (#16281)
* 🐛 fix(cli): cap local file search factory results * ♻️ refactor(cli): keep local file search naming stable * 🧪 test(cli): cover local file search limit forwarding --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> |
||
|
|
825cfc2189 | 🐛 fix(device): tolerate CRLF line endings when editing files & surface real edit errors (#16061) | ||
|
|
99023811d8 |
📝 fix: clarify local system shell result wording (#15745)
* 🔥 remove local system listFiles exposure * 📝 clarify local system shell result wording * 📝 refine local system shell manifest copy * 📝 simplify local system shell prompt semantics * 🐛 fix command wait-window result wording * 📝 limit transient device retry guidance * ✨ show command output duration * 🏷️ narrow command duration result type * 🐛 propagate operation id for device tool calls * 🐛 update project skill discovery hint * 📝 clarify project skill file access * 📝 align project skill discovery comment |
||
|
|
a28fd30719 |
✨ feat: suppport sandbox provider (#15184)
* ✨ feat(cloud-sandbox): add Onlyboxes provider support for self-hosted sandbox (#15136) - Add `SANDBOX_PROVIDER` env var (market | onlyboxes) to select sandbox backend - Add Onlyboxes-specific env vars: `ONLYBOXES_BASE_URL`, `ONLYBOXES_API_TOKEN`, `ONLYBOXES_LEASE_TTL_SEC` - Create `SandboxService` abstraction layer with `MarketSandboxService` and `OnlyboxesSandboxService` implementations - Add `createSandboxService` factory that routes to configured provider - Migrate `execInSandbox` and `exportFile` t * ✨ feat(sandbox): improve Onlyboxes export flow * 🐛 fix(sandbox): pass presigned upload headers to Onlyboxes * ✅ test(sandbox): import tool runtime package * 🐛 fix(sandbox): preserve Market export errors * 🐛 fix(sandbox): allow empty docker env defaults * 🔒 fix: redact sandbox auth params in logs * 🐛 fix: address sandbox provider review comments * 🔐 feat: use onlyboxes jit tokens * 📝 docs: clarify cloud sandbox provider config * 🐛 fix: align cloud sandbox timeout defaults * 🐛 fix(sandbox): lower default Onlyboxes lease TTL to 15 minutes * 🐛 fix(sandbox): cap Onlyboxes task wait time * ♻️ refactor: split sandbox env config |
||
|
|
72ea0f94f7 |
🐛 fix(cli): preserve content/state for connect local file/shell tools (#15442)
* 🐛 fix(cli): preserve content/state for connect local file/shell tools Route file/shell tool calls in connect mode through LocalSystemExecutionRuntime so the result carries formatted prompt `content` plus structured `state`, and forward `state` over the gateway tool-call response — aligning the CLI with the desktop gateway path (PR #15114). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * 🐛 fix(cli): preserve getCommandOutput timeout when polling running commands Routing getCommandOutput through the runtime dropped the per-call/gateway timeout: the CLI mapping didn't forward it and LocalSystemExecutionRuntime's denormalizeParams stripped it before ShellProcessManager.getOutput, so polling fell back to the 30s default and could block past the gateway budget. Carry timeout through the runtime param type, denormalize, and the CLI mapping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
7d1086b096 |
🐛 fix(remote-device): preserve content/state across gateway tool calls (#15114)
The cloud→gateway→desktop path was JSON.stringify-ing the entire IPC result
into `content`, dropping `state` and leaking `{success: true, ...}` into the
LLM-facing prompt. Routes remote tool calls through `LocalSystemExecutionRuntime`
(same runtime the renderer uses) so `content` is the formatted prompt and
`state` flows through `DeviceProxy` → `RuntimeExecutors` into `pluginState`.
Also moves `LocalSystemExecutionRuntime` from `@lobechat/builtin-tool-local-system`
(renderer-coupled, React/antd peers) into `@lobechat/tool-runtime` so the
desktop main process can reuse it without pulling UI deps.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3e0a396968 |
♻️ refactor: + run command observation timeout (#15195)
* ✨ add command observation timeout handling * ✨ hide shell observation timeout from model * 🐛 restore shell observation compatibility * 🐛 clear device proxy caller timeout timer * 🐛 separate shell observation and caller timeouts * 📝 clarify shell output wait semantics * 🐛 align shell manifest timeout semantics * 🐛 restore incremental shell output semantics * 🐛 restore get command output manifest wording * 🐛 restore get command output prompt wording * 🐛 remove running state from command output * 🐛 restore local system server runtime passthrough * 🐛 restore device proxy timeout passthrough * ♻️ tighten shell observation implementation * ♻️ defer completed shell cleanup policy * ♻️ simplify shell observation wait * ♻️ read shell exit code from child process * ♻️ tighten shell output exit code handling * ♻️ clarify shell observation wait race * 🐛 add device gateway HTTP call timeout * 📝 clarify shell command session prompts * ✨ use incremental shell session ids * ✨ pass execution timeout through local system tool chain * 🚑 fix local system timeout CI coverage * ✅ fix desktop shell controller tests |
||
|
|
03c79bfb62 |
🐛 fix: surface stderr in errorOutput fallback and add UNKNOWN_EXEC_ERROR prefix (#14964)
* fix: surface stderr in errorOutput fallback and add UNKNOWN_EXEC_ERROR prefix
When a shell command fails with a non-zero exit code (e.g. git commit
with nothing to commit), the runner puts the error message in stderr
but does not set the error field. This caused errorOutput() to fall
through to the hardcoded 'Tool execution failed' string, losing the
actual error.
Changes:
- errorOutput() now checks state.stderr and state.error before the
final fallback, so real error messages from stderr are surfaced
- Final fallback changed from 'Tool execution failed' to
'[UNKNOWN_EXEC_ERROR] Tool execution failed' for easier grepping
- Same prefix applied to toResult() in the executor for consistency
* fix: pass stderr/stdout into errorOutput state for runCommand failures
runCommand() called errorOutput() with a state that only contained
{ error, isBackground, success }, missing result.result.stderr.
Since normalizeResult() stores the shell stderr under result.result.stderr
(not result.error), the state.stderr fallback in errorOutput() was
never reached for non-zero exit commands like 'git commit' with
nothing to commit.
|
||
|
|
8c3b83f8b3 |
🐛 fix(local-system): tokenize mdfind keywords, scope glob to home, align tool prompts (#14358)
* 🐛 fix(local-system): tokenize mdfind keywords, scope glob to home, align tool prompts - mdfind treats free-form keywords as a single literal substring; "LobeHub Financial Statement" never matches "Financial_Statement_LobeHub.pdf". Split on whitespace and AND each token (still substring-matched) so ordering doesn't matter. - Unix/Windows glob fell back to process.cwd() — meaningless inside a packaged Electron app. Default to os.homedir() instead so unscoped patterns can actually find user files. - systemRole/systemRole.desktop documented `query`/`onlyIn`/`path` for searchLocalFiles/grepContent/globLocalFiles, but the manifest exposes `keywords`/`scope`. The wrong names were silently dropped, so the LLM could never scope its searches. Aligned the prompts with manifest and noted the new keyword-tokenization semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-system): preserve glob/grep error in tool message content + tidy file row UI Two independent bugs that combined to break Glob/Grep tool messages and then made search hits look ugly in the result list. Empty `content` on glob failure - LocalSystemExecutionRuntime.normalizeResult dropped `raw.error` when mapping `globLocalFiles`/`grepContent`, so a failure from the IPC layer (e.g. fast-glob throwing EACCES while traversing the wrong cwd) became `{ result: {...}, success: false }` with no error attached. - ComputerRuntime.errorOutput then did `result.error?.message || JSON.stringify(result.error)`. With error undefined that yields the value `undefined` (not the string), which collapsed into `content: ""` downstream — the chat store still saved `pluginState` so users saw a tool message with state set but the Response panel completely blank. - Propagate `raw.error` through normalizeResult and harden errorOutput with a "Tool execution failed" fallback so the LLM and the debug panel always get a real string. Search results layout - FileItem stacked filename and a redundant full path on a single baseline-aligned row, so the path column repeated the filename and felt visually off-balance. - Switch to a two-line layout: filename on top, parent directory only (collapsed via displayRelativePath when available) underneath, both vertically centered against the file icon. - Promote the "open containing folder" action from hover-only to a permanent right-side button so it's reachable in one click. - Bump the SearchFiles scroll container so the taller rows still show a reasonable number of hits before scrolling, and add a Downloads-style fixture to the dev panel render gallery. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-system): harden executor toResult to never emit empty content and to keep state on failure The earlier fix patched normalizeResult and ComputerRuntime.errorOutput, but the central funnel where every executor return is shaped — LocalSystemExecutor.toResult — still trusted the runtime output blindly: - the success=false branch dropped `state` entirely, which meant any partial pluginState a runtime had built up was thrown away the moment it reported an error (renderers then re-rendered as if the call had produced nothing). - both branches passed `output.content` through verbatim, so an upstream regression that forgot to populate content (the recent Glob EACCES path) would still surface as a blank Response panel. Make toResult the strict gate it claims to be: derive a non-empty content from `output.content -> output.error.message -> "Tool execution failed"`, and always propagate `state` regardless of `success`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🔒 chore(devtools): sanitize searchLocalFiles fixture to use synthetic data Replace real-looking filenames, paths and corporate identifiers in the RenderGallery fixture with neutral sample-user / sample-quarterly-report placeholders. The fixture is checked into the repo and shipped to every contributor's dev panel — it shouldn't carry data that resembles a specific person's Downloads/iMessage/WeChat layout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dc3c48e469 |
🐛 fix(local-system): forward all search params and guard empty mdfind (#14355)
* 🐛 fix(local-system): forward all search params and guard empty mdfind - Pass through all resolved searchFiles params (keywords, fileTypes, date range, scope, etc.) instead of dropping everything except `directory`, which previously caused the executor to call mdfind with no query. - Surface missing fields (`keywords`, `fileTypes`, `contentContains`, date range, sort, etc.) on `SearchFilesParams` so the cross-runtime type matches the actual contract. - Short-circuit Spotlight search when there is no query expression so mdfind doesn't print its usage text and get parsed as phantom file hits, and drop unstattable rows instead of fabricating 0-byte placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(skills): guard empty command and forward description in desktop execScript Desktop skills' execScript dropped `description` before IPC, so when an LLM tool call arrived without `command` (aborted stream, empty args, etc.) the runner crashed on `command.slice(0, 50)` and surfaced as "Failed to execute command: ...". - runner.ts: return a proper error result when `command` is missing instead of throwing - lobe-skills.desktop.ts: forward `options.description` to localFileService.runCommand for better logs and as a fallback when command is absent Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(local-system): show empty state when file search returns no results Previously the SearchFiles result panel rendered an empty Flexbox when there were 0 hits, leaving the area visually blank below "Number of searches: 0". Reuse the same Block + Empty pattern as web-browsing search and the existing `search.emptyResult` i18n key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-file-shell): expand leading ~ in file operation paths Node fs APIs don't expand `~` like a shell would, so paths supplied by the LLM or pasted by users were failing with ENOENT. Apply expandTilde across read/write/edit/move/rename/list/glob/grep/search and the desktop search controller. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(local-system): show empty state when listed directory has no files Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
be99aaebd0 |
♻️ refactor: unify tool content formatting with ComputerRuntime and shared UI (#13470)
* ♻️ refactor: unify tool content formatting with ComputerRuntime and shared UI components Introduce `@lobechat/tool-runtime` with `ComputerRuntime` abstract class to ensure consistent content formatting (via `formatCommandResult`, `formatFileContent`, etc.) across local-system, cloud-sandbox, and skills packages. Create `@lobechat/shared-tool-ui` to share Render and Inspector components, eliminating duplicated UI code across tool packages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: address review issues — state mapping for renders and IPC param denormalization - Add legacy state field mappings in local-system executor (listResults, fileContent, searchResults) for backward compatibility with existing render components - Add denormalizeParams in LocalSystemExecutionRuntime to map ComputerRuntime params back to IPC-expected field names (file_path, items, shell_id, etc.) - Fix i18n type casting for dynamic translation keys in shared-tool-ui inspectors Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ♻️ refactor: inject render capabilities via context, unify state shape for cross-package render reuse - Add ToolRenderContext with injectable capabilities (openFile, openFolder, isLoading, displayRelativePath) to shared-tool-ui - Update local-system render components (ReadLocalFile, ListFiles, SearchFiles, MoveLocalFiles, FileItem) to use context instead of direct Electron imports - Enrich ReadFileState with render-compatible fields (filename, fileType, charCount, loc, totalCharCount) - Cloud-sandbox now fully reuses local-system renders — renders degrade gracefully when capabilities are not provided (no open file buttons in sandbox) - Remove executor-level state mapping hacks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: fix sandbox render bugs — SearchFiles, GrepContent, MoveFiles, GlobFiles - SearchFiles: ensure results is always an array (not object passthrough) - GrepContent: update formatGrepResults to support object matches `{path, content, lineNumber}` alongside string matches - MoveFiles: render now handles both IPC format (items/oldPath/newPath) and ComputerRuntime format (operations/source/destination) - GlobFiles: fallback totalCount to files.length when API returns 0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: unify SearchLocalFiles inspector with shared factory SearchLocalFiles inspector now supports all keyword field variants (keyword, keywords, query) and reads from unified state (results/totalCount). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: handle missing path in grep matches to avoid undefined display Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: improve render field compatibility for sandbox - EditLocalFile render: support both file_path (IPC) and path (sandbox) args - SearchFiles render: support keyword/keywords/query arg variants - FileItem: derive name from path when not provided Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * 🐛 fix: add missing cloud-sandbox i18n key for noResults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |