Skip to content

feat: report the tool a turn is running - #8

Merged
omartelo merged 1 commit into
mainfrom
feat/tool-report
Aug 10, 2026
Merged

omartelo merged 1 commit into
mainfrom
feat/tool-report

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

Client side of lich's extended session-state contract: lichdotdev/lich#200 must merge first — the fixtures-drift job here reads lich@main, so CI stays red until it does.

A busy lich card showed a spinning ring and nothing else. report-tool.sh rides PreToolUse in both harnesses and reports the same busy with two more fields — the tool's name and what it acts on. lich draws them under the session's label and clears them when the state leaves busy, so this script never has to report an end.

The payload shapes were measured, not assumed

Both CLIs were driven against a stub HTTP listener in an isolated home (CODEX_HOME / CLAUDE_CONFIG_DIR), and the first draft of this script was wrong twice:

expected actually sends
Codex, shell call shell, command as a list Bash, command as a plain string
Codex, file edit a path the whole patch as command
both, file tools a workable path an absolute one

So the detail is read by field, never by tool name — command, then file_path / path, then pattern / url / query. One rule covers both harnesses because a Codex shell call arrives wearing Claude Code's own name and field.

Two shapes needed more:

  • apply_patch — the file its *** Add/Update/Delete File: line names goes on the card instead of *** Begin Patch. A patch touching several shows the first; one naming none sends no detail at all.
  • A path — shortened against the payload's cwd, because a lich card is 240px wide and Write · /home/…/worktrees/…/probe.txt is all prefix. Outside that directory only the file name survives. Commands are never shortened: a leading slash there is a binary.

Without jq (Windows, usually) the tool name still goes out, read with sed like the session id is; the detail does not, because a hand-built body cannot escape arbitrary text.

The critical path

PreToolUse is the first event this plugin rides that the agent waits on: in both harnesses a hook exiting 2 there blocks the tool call. Until now the worst a broken script could do was lose a status report. The client rules were already right; this is the first hook where they are load-bearing rather than polite.

Test plan

  • node --test tests/*.test.mjs — 85 pass. The new script is covered by the shared per-registration suite (accepted shape, endpoint, token, no deprecated alias, no-op outside lich, exit 0 on 500 and on refused connection) plus its own: no tool name → no report, detail per field, the two patch shapes, path shortening in and out of cwd, a command that starts with a slash left uncut, JSON escaping, one line of a multi-line command, and the no-jq fallback.
  • binWithout() gained cat, which this script uses to read stdin once.
  • Real codex exec run: Bash · ls -la, apply_patch · probe.txt, Bash · cat probe.txt, then done.
  • Real claude -p run: Write · probe.txt, Read · probe.txt, Bash · grep -rn "hello" ., then done.
  • Rendered in lich itself (isolated task dev, driven over CDP) — the line holds through a busy naming no tool and disappears on done.

Needs lich >= 0.29.0; an older one ignores the two fields.

A busy lich card showed a spinning ring and nothing else, so three
seconds into a file read and three minutes into a test run looked the
same. `report-tool.sh` rides `PreToolUse` in both harnesses and reports
the same `busy` with two more fields: the tool's name and what it acts
on. lich draws them under the session's label and clears them when the
state leaves `busy`, so this script never has to report an end.

The detail is read by field, never by tool name — `command`, then
`file_path` / `path`, then `pattern` / `url` / `query`. That is what
lets one rule cover both harnesses: Codex reports a shell call as `Bash`
carrying the same `command` string Claude Code sends, and routes reading
and searching through it. Every payload shape here was taken off a real
run of both CLIs against a stub listener, not off their docs.

Two shapes needed more than the plain rule. `apply_patch` — Codex's one
word of its own — passes the whole patch as its command, so the file its
`*** Add/Update/Delete File:` line names goes on the card instead of
`*** Begin Patch`. And a path is shortened against the payload's `cwd`,
because both harnesses report absolute ones and a card is 240px wide;
outside that directory only the file name survives. Commands are never
shortened, since a leading slash there is a binary.

Without jq the tool name still goes out, read with sed like the session
id is; the detail does not, because a hand-built body cannot escape
arbitrary text.

`PreToolUse` is the first contract event on the agent's critical path: a
hook exiting 2 there blocks the tool call, which makes the client rules
load-bearing rather than polite. The suite covers it as it covers the
others, plus the two shapes above and the no-jq fallback.

Needs lich >= 0.29.0; an older one ignores the two fields.
@omartelo
omartelo merged commit 8f7de2e into main Aug 10, 2026
4 of 5 checks passed
@omartelo omartelo mentioned this pull request Aug 10, 2026
3 tasks done
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