Skip to content

fix(desktop): give tool rows the names, arguments and results the UI renders - #821

Merged
agentforce314 merged 1 commit into
mainfrom
fix/desktop-tool-vocabulary
Aug 9, 2026
Merged

fix(desktop): give tool rows the names, arguments and results the UI renders#821
agentforce314 merged 1 commit into
mainfrom
fix/desktop-tool-vocabulary

Conversation

@agentforce314

Copy link
Copy Markdown
Owner

Follow-up to #820. Tool rows still looked nothing like the TUI's: plain untitled entries, no icons, no diffs, and — for anything that returned text — no output at all.

Why

The desktop renderer keys every per-tool formatter off its own tool vocabulary (read_file, terminal, edit_file, …) and reads a tool's output from named result fields. The agent speaks Claude Code's vocabulary (Read, Bash, Edit) and returns plain text. Nothing matched:

  • no name in TOOL_META → the unlabelled generic path: no title, no icon, no tone
  • firstStringField(args, ['path','file','filepath']) never sees file_path
  • a plain-string result is discarded by the renderer's parseMaybeJsonObject, so finished tools rendered empty

What changed

All three are adapted in the gateway, mirroring the TUI's own summarization (ui-tui/src/gatewayClient.ts toolContext / formatToolResult) so both surfaces describe a run the same way.

before after
Read Read · (empty) Read file seed.py · file contents · "Read 2 lines"
Bash Bash · (empty) Ran command · $ ls src/ transcript · ANSI output
Edit Edit · (empty) Edited file a.py · rendered diff, "Added 1 line, removed 1 line"
Glob Glob · (empty) Listed files · the matches · 3 files chip
WebSearch WebSearch · (empty) Searched web · 3 results · 2.4s
failure Bash · (empty) error row · cleaned reason, no diff

Unknown tools (Task, MCP tools) pass through unchanged and keep the generic treatment, which is the right fallback.

Details worth calling out:

  • The single argument worth showing rides along as context in the TUI's priority order, so a Grep row shows its pattern rather than its search path. path is aliased in alongside file_path because that is the spelling the file formatters look for.
  • An edit's inline_diff is rebuilt from the structuredPatch display envelope, so it renders as a real diff with add/remove counts — the TUI treats the diff AS the result, and now so does the desktop.
  • Failure text has its model-facing markup (<tool_use_error>, <sandbox_violations>) stripped before display.

Testing

The wire payloads are frozen into gateway-tool-events.fixture.json, generated by the Python translator and rendered through the real renderer by the new ui-desktop/src/lib/gateway-tool-contract.test.ts, which asserts what a user actually sees.

That pairing is the point. This class of bug is invisible to either language alone — each side happily tests a field the other never reads, which is precisely how the blank rows shipped. Writing the test immediately caught two more live mismatches that the Python tests were happy with: the argument context outranking a Glob's matches on the renderer's generic path, and a diff that never reached the view.

  • Full Python suite: 9902 passed, 15 skipped
  • tsc --noEmit: clean
  • New contract test: 10 passed

🤖 Generated with Claude Code

…renders

Tool rows still looked nothing like the TUI's after the payload-shape fix:
plain untitled entries, no icons, no diffs, and — for anything that returned
text — no output at all. The renderer keys every per-tool formatter off its
own tool vocabulary (read_file/terminal/edit_file/…) and reads output from
named result fields, while the agent speaks Claude Code's (Read/Bash/Edit/…)
and returns plain text. Nothing matched, so every tool fell through to the
unlabelled generic path, and a plain-string result was dropped outright by
the renderer's parseMaybeJsonObject.

The gateway now adapts all three, mirroring the TUI's own summarization
(ui-tui/src/gatewayClient.ts toolContext/formatToolResult) so both surfaces
describe a run the same way:

- Names map onto the renderer's vocabulary, which is what turns on its
  titles ("Read file", "Ran command"), icons and tones. Unknown tools pass
  through and keep the generic treatment.
- The one argument worth showing rides along as `context`, in the TUI's
  priority order — a Grep row shows its pattern rather than its search path.
  `path` is aliased in alongside `file_path` because that is the spelling
  the file formatters look for.
- Results land where each tool family reads them: `content` for a read,
  `output` for a shell run, `inline_diff` for an edit — rebuilt from the
  structuredPatch display envelope so an edit renders as a real diff with
  add/remove counts, as it does in the TUI. Glob/Grep get a count chip, a
  web search its "3 results / 2.4s". Failures show a cleaned reason with
  the model-facing markup stripped, and no diff.

Testing: the wire payloads are frozen into a fixture that the Python suite
generates and ui-desktop's new gateway-tool-contract.test.ts renders through
the real renderer, asserting what a user actually sees. That pairing is the
point — this class of bug is invisible to either language alone, since each
side happily tests a field the other never reads. Writing it immediately
caught two more live mismatches: the argument context outranking a Glob's
matches, and a diff that never reached the view.

Full Python suite green; ui-desktop typecheck clean.

Co-Authored-By: Claude <noreply@anthropic.com>
@agentforce314
agentforce314 merged commit 2149d06 into main Aug 9, 2026
3 checks passed
agentforce314 added a commit that referenced this pull request Aug 9, 2026
… Desktop

The Electron app now builds, runs, tests, and packages natively on
Windows 10/11. Most of the machinery shipped with the upstream import and
needed verification rather than porting; this commit fixes what actually
broke and adds the missing gates.

Verified live on Windows 11, end to end
- `npm run dist:win:nsis` → release/ClawCodex-0.17.0-win-x64.exe
  (unsigned; icon + version metadata stamped via rcedit; node-pty conpty
  binaries staged; install-stamp pins the backend commit).
- Silent NSIS install → %LOCALAPPDATA%\Programs\ClawCodex → app boot →
  backend spawned from the shared %USERPROFILE%\.clawcodex\clawcodex venv
  (the install.ps1 layout; resolveClawCodexHome honors an existing
  ~/.clawcodex) → healthy loopback gateway → a real DeepSeek
  (deepseek-v4-flash) chat turn driven through the app's own JSON-RPC
  gateway (session.create → prompt.submit → reply streamed back).
- Packaged-app Playwright spec: 4/4 against the real win-unpacked binary.
- scripts/test-desktop.mjs bundle validation passes.

Fixes
- src/entrypoints/desktop_cli.py: spawn the shutil.which-resolved
  npm.cmd — Popen never PATHEXT-searches a bare `npm` on Windows, so
  `clawcodex desktop` died with FileNotFoundError. Docstring named the
  wrong env var (CLAWCODEX_DESKTOP_CLAWCODEX_ROOT is what main.ts reads).
- electron/update-relaunch.ts + main.ts: the updater's release-dir
  resolvers used the upstream `apps/desktop/` layout, which never matches
  this repo's `ui-desktop/` — in-app updates always ended in "reinstall
  the GUI" and the mac bundle swap never found the rebuilt .app. The
  relaunch handoff (a bash script) is now explicitly skipped on Windows
  in favor of the honest manual-restart state.
- scripts/stage-native-deps.mjs: dropped the hashbang — a `#!` line in a
  module IMPORTED by vitest tests lands mid-source after the SSR wrapper
  and kills collection of both packaging test files with
  "SyntaxError: Invalid or unexpected token".
- src/themes: the legacy "nous" alias key points at the same theme object,
  so Object.values() listed the clawcodex theme twice in every picker and
  /skin (and made the registry-order test unpassable). BUILTIN_THEME_LIST
  now dedupes by identity and listAllThemes consumes it; a regression test
  pins that the alias still resolves.
- src/components/onboarding tests: still asserted the upstream "Nous
  Portal" featured provider; realigned to this product's FEATURED_ID
  (anthropic) with equal assertion strength.
- package.json `fmt`: single quotes are literal under cmd.exe — prettier
  matched nothing on Windows. Double quotes work everywhere.
- src/lib/gateway-tool-contract.test.ts: import-group spacing error that
  shipped in #821 broke `npm run lint` (its one error).
- e2e/launch-packaged-app.spec.ts: the sandboxed packaged boot terminates
  at first-run onboarding by design (isolated config has no providers);
  the overlay heuristic now recognizes that state instead of timing out.

Windows test enablement (electron + ui vitest projects green)
- electron project: 934 passed / 0 failed. POSIX mode-bit asserts gated;
  EBUSY-tolerant async rm where a probe child still holds the cwd; ssh
  fixtures normalize host separators; mux lifecycle tests pin mux:true
  (Windows OpenSSH has no ControlMaster, so the constructor defaults it
  off there); bash relaunch-script lint feeds stdin on Windows (MSYS
  mangles C:\ argv paths); drive-qualification fix for rootless POSIX
  fixtures under path.resolve.
- ui project: 3596 passed / 0 failed, twice consecutively. Beyond the
  theme/onboarding fixes above, the load flakes' mechanism was a starved
  worker's first test dying mid-import at the 15s deadline, its abandoned
  render then leaking into the NEXT test's DOM ("Found multiple
  elements" cascades); widened the two existing centralized knobs
  (testTimeout 15s→30s, RTL asyncUtilTimeout 5s→10s) — success still
  resolves the instant a node appears.

CI
- New "Desktop (os)" job on ubuntu-latest + windows-latest: npm ci,
  typecheck (3 tsconfigs), vitest run (both projects). Packaging and
  Playwright stay local/release-time.

Docs: ui-desktop/README status refreshed (works end to end, packaging on
both OSes), root README news entry, CHANGELOG under [Unreleased].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant