Skip to content

feat(web): per-tool run history with re-run and diff - #2894

Merged
cliffhall merged 4 commits into
v2/mainfrom
v2/feat/2830-tool-run-history
Nov 5, 2026
Merged

cliffhall merged 4 commits into
v2/mainfrom
v2/feat/2830-tool-run-history

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #2830

What changed

The form stays visible beside the result. The Tools screen used to replace the input form with the Results panel. Now the form keeps its own column and the result sits beside it, so you can re-run or tweak the form without closing the result first. The Results header shows the call's duration, and a collapsed Arguments section shows what was actually sent (the form may have been edited since).

Per-tool run history. Every completed tools/call this session is recorded per tool name: arguments, start time, duration, status, and the full result envelope (or the rejection's message + code). A rejected call counts as a run, since "it used to fail" is often the diff you want. A deliberate cancel or an auth-recovery hand-off does not count. The History card under the form lists the selected tool's runs, newest first, each with:

  • Re-run: loads that run's arguments into the form and sends them (disabled while a call is in flight)
  • an expand toggle showing the run's arguments + envelope as JSON
  • a Compare checkbox. Tick two runs and the Compare button opens a modal with a line diff of the two (older → newer). The diff runs over key-sorted JSON, so a reordered key never shows up as a change. Durations appear in the diff header but are left out of the diff itself, since they differ on every run.

The history is capped at 20 runs per tool and is session-scoped: it is dropped together with the other result panels on disconnect / server switch (clearResultPanels).

Implementation notes

  • utils/toolRunHistory.ts (pure): the ToolRun type, append/cap/numbering, the compared envelope, formatDuration, compare-selection toggling.
  • utils/lineDiff.ts (pure): canonical JSON plus an LCS line diff with common prefix/suffix trimming. The table is bounded (MAX_DIFF_CELLS); past that bound the modal says the runs are too large to compare and does not stall the tab. No new dependency.
  • useResultPanels owns the history state next to toolCallState, and onCallTool records each run and puts durationMs / args on the call state.
  • New ToolRunHistoryPanel and ToolRunDiff group components, with stories. Diff line styling is a Text theme variant backed by two new --inspector-diff-* tokens.
  • Diffs open in a modal rather than inline because the history column is narrow next to the form and the Monitor sidebar, and a JSON diff needs the width.

Screenshots

Two runs of echo with the form, latest result (duration + Arguments) and history side by side (1280×900, Monitor sidebar at its default width):

Result beside the form, with run history

Comparing the two runs:

Compare runs diff

Testing

Unit tests for both utils, both new components, the ToolsScreen layout/re-run wiring, and the history recording in useServerCommands (success, isError, each rejection path, cancel excluded, cleared on session reset). npm run local:gate passes.

🤖 Generated with Claude Code

The Tools screen's result no longer replaces the input form: the form stays
in its own column with the result beside it, so a tool can be re-run without
dismissing the result first. The result header now shows the call's duration
and a collapsed "Arguments" section with what was actually sent.

Every completed tools/call (including a rejected one, but not a deliberate
cancel) is recorded per tool name for the session: arguments, start time,
duration, status and the full result envelope or rejection. The history
lists under the form with Re-run (loads the run's arguments into the form
and sends them) and an expandable envelope view; ticking two runs enables
Compare, which opens a key-sorted JSON line diff of arguments + result in a
modal. The history is capped at 20 runs per tool and is dropped with the
other session-scoped panels on disconnect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Nov 5, 2026
@cliffhall cliffhall linked an issue Nov 5, 2026 that may be closed by this pull request
2 tasks done
@cliffhall
cliffhall requested a balanced review from Copilot November 5, 2026 06:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved bugs can mislabel tool failures as successful, expose prior-session arguments after reconnecting, and re-run saved arguments while displaying mismatched form fields.

6 open issues
What changed in this PR

Adds per-tool run history to the web client for #2830, letting users revisit and compare calls without copying results out of Inspector.

Changes:

  • Keeps the tool form beside results and displays call duration and submitted arguments.
  • Adds capped, session-scoped history with re-run controls and a comparison modal.
  • Implements key-sorted JSON line diffs, supporting styles, unit tests, and Storybook examples.
File Description
clients/​web/​src/​utils/​toolRunHistory.ts Defines run records, history limits, and comparison helpers.
clients/​web/​src/​utils/​toolRunHistory.test.ts Tests history numbering, limits, and helpers.
clients/​web/​src/​utils/​lineDiff.ts Adds canonical JSON rendering and bounded line comparison.
clients/​web/​src/​utils/​lineDiff.test.ts Tests canonicalization and diff behavior.
clients/​web/​src/​theme/​Text.ts Adds diff-line text variants.
clients/​web/​src/​hooks/​useServerCommands.tsx Records call outcomes, timing, and session history.
clients/​web/​src/​hooks/​useServerCommands.test.tsx Tests recording, rejection, cancellation, and clearing.
clients/​web/​src/​components/​views/​InspectorView/​types.ts Adds history to Tools panel props.
clients/​web/​src/​components/​views/​InspectorView/​InspectorView.tsx Passes history to the Tools screen.
clients/​web/​src/​components/​screens/​ToolsScreen/​ToolsScreen.tsx Arranges form, results, and history; wires re-run.
clients/​web/​src/​components/​screens/​ToolsScreen/​ToolsScreen.test.tsx Tests layout, history visibility, and re-run wiring.
clients/​web/​src/​components/​screens/​ToolsScreen/​ToolsScreen.stories.tsx Adds a combined results-and-history example.
clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunHistoryPanel.tsx Displays runs, expandable details, and comparison controls.
clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunHistoryPanel.test.tsx Tests history actions and comparison selection.
clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunHistoryPanel.stories.tsx Demonstrates history states and modal interaction.
clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunDiff.tsx Displays differences, durations, and comparison limits.
clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunDiff.test.tsx Tests diff rendering and oversized comparisons.
clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunDiff.stories.tsx Demonstrates changed and identical runs.
clients/​web/​src/​components/​groups/​ToolResultPanel/​ToolResultPanel.tsx Displays duration and expandable submitted arguments.
clients/​web/​src/​components/​groups/​ToolResultPanel/​ToolResultPanel.stories.tsx Adds a duration-and-arguments example.
clients/​web/​src/​App.tsx Connects history state to the Inspector view.
clients/​web/​src/​App.css Defines added- and removed-line color tokens.

Review effort: Balanced

Comment thread clients/web/src/utils/lineDiff.ts Resolved
- A resolved result carrying isError is recorded as a failed run.
- A call that settles after its session was cleared is not recorded, so a
  disconnect cannot leak one session's arguments into the next's history.
- The URLElicitationRequired rejection keeps its JSON-RPC code.
- canonicalJson preserves a `__proto__` key as data.
- A history row serializes its envelope only once expanded.
- Re-run remounts the schema form so a union branch or field draft matches
  the arguments being sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 1: 6 findings, all fixed in f1724e4 (each one answered in its thread):

  • An isError result is now recorded as a failed run.
  • A call that settles after its session was cleared is ignored (session generation guard).
  • The URLElicitationRequired rejection keeps its -32042 code.
  • canonicalJson keeps __proto__ keys.
  • A history row serializes its envelope only once expanded.
  • Re-run remounts the schema form so it matches the arguments being sent.

local:gate is green. Requesting round 2.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Re-run can incorrectly dispatch task-forbidden tools through the task path, while unbounded diff rendering and eager serialization of collapsed arguments risk UI stalls.

No open issues 🎉

3 previously missed issues

In code that hasn't changed since last review

Medium severity Avoid serializing arguments while the viewer is collapsed

clients/​web/​src/​components/​groups/​ToolResultPanel/​ToolResultPanel.tsx:290

JSON.stringify(args) runs while constructing these children even when Arguments is collapsed. Since the form now stays beside the result, every subsequent form edit re-serializes the previous call's potentially large arguments without displaying them. Gate creation of the viewer on expanded, as the history panel does for its envelope.

Medium severity Bound rendering of unchanged comparison lines

clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunDiff.tsx:112

A small change in a large structured result passes the LCS cell budget because that budget covers only the changed middle, but this loop still creates a Mantine Text element for every unchanged line too. The scroll area does not virtualize those rows, so comparing mostly identical results can still stall the tab. Bound the rendered output, collapse unchanged context, or virtualize the rows.

Medium severity Re-run ignores effective task support checks

clients/​web/​src/​components/​screens/​ToolsScreen/​ToolsScreen.tsx:241

Re-run forwards the stored task preference rather than the effective choice used by Execute. After enabling “Run as task” on an optional tool and switching to a task-forbidden tool, Re-run incorrectly uses callToolStream, bypassing ordinary-call cancellation and URL-elicitation retry handling. Apply the same server-capability and per-tool task-support checks as ToolDetailPanel before dispatching.

6 resolved since last review

Review effort: Balanced

- Re-run applies the same task routing as Execute (a shared
  effectiveRunAsTask in utils/toolUtils), so the screen-wide "Run as task"
  toggle no longer sends a task-forbidden tool down the task path.
- The compare diff folds long unchanged stretches to three lines of
  context and caps the rendered rows, so a small change in a large result
  or a wholesale replacement can't mount thousands of rows.
- The result's Arguments section serializes only once expanded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 2: no inline comments. The three "previously missed" findings (they have no threads, so I'm answering them here) are fixed in db7849d:

  • Re-run ignores effective task support: Execute and Re-run now share effectiveRunAsTask(tool, serverSupportsTaskToolCalls, runAsTask) in utils/toolUtils, so a task-forbidden tool is never re-run through callToolStream. Tests cover a forbidden tool (false) and an optional one (true).
  • Bound rendering of unchanged comparison lines: foldDiff folds unchanged stretches to 3 lines of context either side of a change and caps the rendered rows at 1000 with a "N more lines not shown" note. That also bounds a wholesale replacement, which the LCS cell budget does not.
  • Arguments serialized while collapsed: the result panel's Arguments viewer now mounts only once expanded.

local:gate is green. Requesting round 3.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Pruned runs leave stale selection IDs and comparison state, causing valid selections to be dropped and the comparison modal to reopen unexpectedly.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Close comparison when pruning leaves fewer than two selected runs

clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunHistoryPanel.tsx:236

When a pending run completes and prunes an entry from an open comparison, the modal disappears but compareOpen remains true. Selecting a replacement run then reopens the modal without clicking Compare. Reset compareOpen when fewer than two selected runs remain so pruning actually closes the comparison.

Medium severity Remove pruned run IDs before enforcing the selection limit

clients/​web/​src/​components/​groups/​ToolRunHistoryPanel/​ToolRunHistoryPanel.tsx:267

Pruned runs remain in selectedIds, so they still consume the two-selection limit. For example, select Run #2 and then Run #1; after Run #21 evicts #1, ticking Run #3 drops the still-valid #2 instead of the stale #1, leaving Compare disabled. Filter out IDs no longer present in runs before applying toggleCompare.

Review effort: Balanced

…2830)

A pruned run no longer holds one of the two comparison slots, and losing a
compared run closes the comparison instead of leaving it armed to reopen
when a replacement is ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 3: no inline comments. Both "previously missed" findings are fixed in 0c52ec4:

  • Remove pruned run IDs before enforcing the selection limit: ticking a run first drops ids no longer in runs, so a pruned run can't take one of the two slots.
  • Close comparison when pruning leaves fewer than two runs: useValueChange on the comparable flag resets compareOpen, so a replacement tick no longer reopens the modal by itself.

The existing pruning test now covers both. local:gate is green. Requesting round 4.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed because saved-run replay can introduce omitted defaults or silently coerce argument types, causing the form and history to diverge from executed payloads.

No open issues 🎉

Review effort: Balanced

@cliffhall

Copy link
Copy Markdown
Member Author

Copilot review loop closed. Round 4 came back clean: no inline comments, no suppressed findings, "No open issues". On its headline note: Re-run sends the recorded run.args verbatim (the same object the history stores and diffs), so the sent payload and the history cannot diverge. The form only displays those args, with the same schema-default presentation Execute already uses. Four rounds total: 6 + 3 + 2 findings, all fixed; round 4 clean.

@cliffhall
cliffhall merged commit b953987 into v2/main Nov 5, 2026
11 checks passed
@cliffhall
cliffhall deleted the v2/feat/2830-tool-run-history branch November 5, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Issues and PRs for v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Web: per-tool run history with re-run and diff

2 participants