You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(web): per-tool run history with re-run and diff - #2894
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):
Comparing the two runs:
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.
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>
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.
- 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>
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
Avoid serializing arguments while the viewer is collapsed
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.
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.
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.
- 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>
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.
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.
Remove pruned run IDs before enforcing the selection limit
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.
…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>
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/callthis 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: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): theToolRuntype, 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.useResultPanelsowns the history state next totoolCallState, andonCallToolrecords each run and putsdurationMs/argson the call state.ToolRunHistoryPanelandToolRunDiffgroup components, with stories. Diff line styling is aTexttheme variant backed by two new--inspector-diff-*tokens.Screenshots
Two runs of
echowith the form, latest result (duration + Arguments) and history side by side (1280×900, Monitor sidebar at its default width):Comparing the two runs:
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:gatepasses.🤖 Generated with Claude Code