Skip to content

feat(web): History — Pin & Replay, plus two history-status fixes (#1438)#1441

Open
cliffhall wants to merge 15 commits into
v2/mainfrom
feat/history-pin-replay-1438
Open

feat(web): History — Pin & Replay, plus two history-status fixes (#1438)#1441
cliffhall wants to merge 15 commits into
v2/mainfrom
feat/history-pin-replay-1438

Conversation

@cliffhall
Copy link
Copy Markdown
Member

@cliffhall cliffhall commented Jun 6, 2026

History-screen work. Three related changes on one branch.

1. Pin & Replay (#1438)

Both HistoryEntry actions were rendered but wired to todoNoop in App.

  • PinApp owns a session-scoped Set<string> of pinned entry ids, toggled by onTogglePinHistory and passed down as pinnedHistoryIds. HistoryListPanel already sorts pinned entries to the top. The set resets with the rest of the per-screen state on disconnect / server-switch.
  • ReplayonReplayHistory re-issues the entry's original request by method: tools/callcallTool, prompts/getgetPrompt, resources/readreadResource. The call flows through InspectorClient → tracked transport → message log, so the fresh request+response surface as a new History entry (history-local) without touching the Tools/Prompts/Resources panels. An unsupported method or a removed tool surfaces a toast.

2. Notifications no longer stuck "Pending" (#1439)

extractStatus derived the badge purely from whether a response was attached, so a fire-and-forget notification (no id, no response) — and any unmatched standalone response — showed Pending forever. The pending→OK/Error lifecycle only applies to direction === "request" entries; non-request entries now render no request-style badge (the method badge already labels them).

3. Server→client requests resolve in history (#1440)

MessageTrackingTransport.send only tracked outgoing requests, so the client's response to a server→client request (roots/list, sampling, elicitation) was never recorded — the request sat Pending with no response body. send is now symmetric with onmessage: it tracks outgoing responses (by id + result/error) too, so the response folds into its request entry and resolves to OK/Error. (Verified the integration "no orphan responses" assertion still holds.)

Testing

  • npm run validate — 2070 unit/integration tests pass; per-file coverage gate green.
  • npm run test:storybook — 333 pass.

Closes #1438, closes #1439, closes #1440.

🤖 Generated with Claude Code

4. Direction badge on History & Network (#1437)

A shared dual-state badge at each entry's top-left shows travel direction: client → server (green, inspector-originated) / client ← server (violet, server-originated). History derives it from a new MessageEntry.origin field set at tracking time (MessageTrackingTransport tags outgoing send as client, incoming onmessage as server). Network fetches are always inspector-originated, so the badge there is constant (outgoing) — the light "tap" noted in the issue.

Closes #1437.

cliffhall and others added 3 commits June 6, 2026 18:48
…ns (#1439)

`extractStatus` derived the history status badge purely from whether a
`response` was attached, so a fire-and-forget notification (no id, no
response ever) — and any unmatched standalone response — was labelled
"Pending" forever. The pending → OK/Error lifecycle only applies to
request entries (messageLogState attaches the response by JSON-RPC id),
so scope the status to `direction === "request"` and render no
request-style badge for notifications/responses. The method badge already
labels them.

Part of the History-screen work on #1438.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in history (#1440)

MessageTrackingTransport.send only tracked outgoing requests, so the
client's response to a server→client request (roots/list, sampling,
elicitation) was never recorded. The inbound request entry never got its
response folded in, leaving it stuck "Pending" with no response body.

Make send symmetric with onmessage: track outgoing responses (by id +
result/error) as well as outgoing requests. The response now correlates
to its request entry, which resolves to OK/Error with the body. Outgoing
notifications remain untracked (separate gap).

Part of the History-screen work on #1438.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both HistoryEntry actions were rendered but stubbed to todoNoop in App.

- Pin: App now owns a session-scoped Set<string> of pinned entry ids,
  toggled by onTogglePinHistory and passed down as pinnedHistoryIds.
  HistoryListPanel already sorts pinned entries to the top. The set resets
  with the rest of the per-screen state on disconnect/server-switch.
- Replay: onReplayHistory re-issues the entry's original request by method
  (tools/call → callTool, prompts/get → getPrompt, resources/read →
  readResource). The call flows through InspectorClient → tracked
  transport → message log, so the fresh request+response surface as a new
  History entry (history-local) without touching the Tools/Prompts/
  Resources panels. Unsupported methods / a removed tool surface a toast.

Tests: pin toggle propagation, replay dispatch for each supported method,
unsupported-method and missing-tool toasts.

Closes #1438.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall cliffhall added the v2 Issues and PRs for v2 label Jun 6, 2026
cliffhall and others added 2 commits June 6, 2026 19:23
Replaying a tools/list entry reported "Replay isn't supported". Extend
replayHistoryRequest beyond the call-style requests to the read/discovery
methods, re-issuing each via its InspectorClient method (preserving the
pagination cursor):

- tools/list → listTools, prompts/list → listPrompts,
  resources/list → listResources,
  resources/templates/list → listResourceTemplates, ping → ping.

Tests: tools/list replay (cursor preserved) and an unsupported-method
toast (logging/setLevel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ttons

- Add tasks/list to the replayable set (re-issued via listRequestorTasks),
  completing the client-issued */list methods.
- Reverse the HistoryEntry actions to Pin then Replay, and hide Replay for
  entries whose method can't be replayed.
- Introduce a single source of truth — REPLAYABLE_HISTORY_METHODS /
  isReplayableHistoryMethod in historyUtils — used by HistoryEntry to
  show/hide the button and by App's replayHistoryRequest to gate dispatch,
  so the two can't drift.

Tests: tasks/list replay, Replay hidden for a non-replayable method, and
Pin-before-Replay ordering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cliffhall and others added 9 commits June 6, 2026 19:42
…1437)

A dual-state badge at the top-left of each entry shows which way it
traveled: "client → server" (green) for inspector-originated, "client ←
server" (yellow) for server-originated.

- New shared element MessageDirectionBadge (Elements).
- History: MessageEntry gains an `origin` ("client" | "server") set at
  tracking time — MessageTrackingTransport tags `send` as client and
  `onmessage` as server, threaded through the track callbacks. HistoryEntry
  maps origin → badge. (direction stays the request/response/notification
  message-type used for response correlation; origin is the flow.)
- Network: fetches are always inspector-originated, so NetworkEntry renders
  the badge as outgoing.

Tests + stories for the badge; HistoryEntry direction tests; transport
test asserts the origin argument.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… badge

Yellow + green read as caution/ok status; purple vs green reads as
direction. Incoming (server → client) is now violet; outgoing stays green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Pinned Requests (N)" and "History (N)" section headers are now
clickable toggle buttons that expand/collapse their entries — styled like
the LogControls level toggles (UnstyledButton listItem variant with the
active background) and wrapping the entries in a Collapse. Both default
open and collapse independently.

Tests assert the per-section aria-expanded toggling and independence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
History, Network, and Task cards now use a shared ExpandToggle ActionIcon
instead of the "Expand"/"Collapse" text button: LuArrowDownToLine when
collapsed (expand), LuArrowUpFromLine when expanded (collapse). The
aria-label stays "Expand"/"Collapse" so the control reads the same.

- New shared element ExpandToggle (+ test + stories).
- HistoryEntry: actions split into a left group (Pin/Replay) with the
  toggle pushed right via space-between.
- NetworkEntry / TaskCard: drop their now-unused SubtleButton const.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the Pin/Unpin text button with a shared PinToggle ActionIcon
(TiPinOutline when unpinned, TiPin when pinned; aria-label stays
Pin/Unpin). Move it to the right action group, just left of the
expand/collapse toggle — Replay stays on the left.

- New shared element PinToggle (+ test + stories).
- HistoryEntry action row: Replay left; Pin + expand toggle right.
- Drop the now-unused formatPinLabel helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each section header now shows Clear and Export links on the right, but
only when both sections are present (a single section is already covered
by the panel-level Clear/Export). They act on just that section's entries:

- Clear: removes that section's entries by pin membership (clearing the
  Pinned section also drops the now-stale pinned id set).
- Export: downloads just that section's entries
  (inspector-history-pinned/-unpinned-...json).

Threaded onClearSection/onExportSection App → InspectorView → HistoryScreen
→ HistoryListPanel; App computes each section from messages + pinned ids.
Extracted a CollapsibleSection subcomponent and a SectionActions links
group. Extended ExportKind with history-pinned/history-unpinned.

Tests cover both-sections visibility gating and per-section dispatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Filter by Message Direction" section to HistoryControls, modeled on
LogControls' "Filter by Level": a Select/Deselect All control plus two
listItem toggles — "client → server" and "client ← server" — that show or
hide entries by origin.

- HistoryUiState gains visibleDirections (Record<MessageOrigin, boolean>),
  defaulting to both visible; persists with the other per-screen state.
- HistoryScreen wires the toggles (single + all) through onUiChange and
  passes the filter to HistoryListPanel, where matchesFilters drops entries
  whose direction is off (entries without a recorded origin are never
  filtered out).

Tests cover the controls (render, single toggle, select/deselect all), the
HistoryScreen onUiChange wiring, and the list-level direction filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the Select/Deselect All control in HistoryControls: the per-section
Clear/Export on the History/Pinned headers now use a subtle Button
(variant subtle, size xs) instead of an Anchor link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "client → server" / "client ← server" toggles in HistoryControls now
color their labels green / violet, matching the MessageDirectionBadge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall
Copy link
Copy Markdown
Member Author

@claude review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 7, 2026

Claude finished @cliffhall's task in 2m 17s —— View job


Code Review — PR #1441 (History: Pin & Replay + status fixes)

I reviewed the full diff against origin/v2/main. Overall this is clean, well-factored, and well-tested — single source of truth for replayable methods, a shared MessageDirectionBadge/ExpandToggle/PinToggle extracted from the duplicated subtle-buttons, and the core transport fix is symmetric and correct. Below are findings, none blocking.

✅ What's solid

  • messageTrackingTransport.send (History: server→client requests (roots/list, sampling, elicitation) stuck Pending with no response body #1440) — the new outgoing-response branch correctly uses message.id !== null && message.id !== undefined rather than a falsy check, so a JSON-RPC id of 0 is still tracked. The classification (result/error → response, else method → request) mirrors onmessage exactly, and the new test file covers request/response/error/notification cases plus base-transport forwarding.
  • extractStatus"none" (History: server notifications show a permanent PENDING status badge #1439) — gating the pending→OK/Error lifecycle on direction === "request" is the right fix; notifications and unmatched standalone responses no longer render a permanent "Pending". The method badge already labels them.
  • REPLAYABLE_HISTORY_METHODS single source of truthHistoryEntry (button visibility) and App.replayHistoryRequest (dispatch gate) both read it, so they can't drift. Nice.
  • clearMessages predicate semantics — verified against messageLogState.ts:130 (predicate = remove). onClearHistorySection passes has(id) for pinned / !has(id) for history, which is correct, and the pinned set is reset when the pinned section is cleared.
  • EMPTY_HISTORY_UI was updated with visibleDirections: { client: true, server: true } and is reset on disconnect — no stale-state leak.

🔸 Observations / minor

  1. Outgoing client notifications remain untracked — the one residual asymmetry. send only tracks messages with an id, so the client's own notifications (notifications/initialized, notifications/progress, notifications/roots/list_changed) never reach history, while incoming server notifications are tracked via onmessage. Your test documents this as intentional, but it slightly undercuts the "send is now symmetric with onmessage" framing in the PR body. Worth confirming this is deliberate — if outgoing notifications should appear in History, send needs an else if ("method" in message)trackNotification(…, "client") branch.

  2. Per-section Clear/Export ignore the active direction & search filters. onClearHistorySection/onExportHistorySection operate on the full unfiltered log by pin membership. So clearing the "pinned" section while a direction filter hides some pinned entries still removes all pinned entries — including the ones not currently visible. Defensible ("clear the section = clear the section"), but it can surprise a user who's filtered the view. Consider either respecting the visible set or leaving as-is intentionally.

  3. PR description vs. code: incoming badge color. Section 4 of the description says incoming is yellow; the code uses violet (and MessageDirectionBadge's comment explains why not yellow — green+yellow reads as ok/caution status, not direction). Code is internally consistent (HistoryControls also uses violet); just the prose is stale.

  4. replayHistoryRequest default: branch is dead code. It's gated by isReplayableHistoryMethod and the switch enumerates every member of the set, so default is unreachable. Harmless defensive belt-and-suspenders, but you could drop it (or keep for safety — your call).

  5. Replaying a tools/call re-runs side effects with no confirmation. Inherent to the feature and fine, but a destructive tool will execute again silently on Replay. Not a change request — just flagging the behavior is by design.

Nits

  • MessageDirectionBadge color names (green/violet) and HistoryControls' Text c={color} use Mantine palette names — consistent with the AGENTS.md token rules (named colors, no hex). 👍
  • New element components aren't enumerated in any README, and the READMEs don't list individual elements, so no doc update is owed.

No correctness blockers found. Items 1 and 2 are the only ones I'd ask you to consciously confirm-or-tweak before merge.

Review follow-up (#1441): MessageTrackingTransport.send now also tracks
the client's own notifications (initialized, progress,
roots/list_changed) as origin "client", making send fully symmetric with
onmessage. Previously only id-bearing outgoing messages (requests,
responses) were tracked, so client notifications never reached History
while incoming server notifications did.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cliffhall
Copy link
Copy Markdown
Member Author

Thanks for the thorough review! Addressed in 96471f8d.

1. Outgoing client notifications untracked — fixed. Good call; I made send truly symmetric with onmessage. It now also tracks the client's own notifications (initialized, progress, roots/list_changed) as origin client via the else if ("method" in message) branch, so they surface in History (showing client → server) just like incoming server notifications. Updated the transport test (the old "does not track an outgoing notification" case now asserts it is tracked as a client notification). Integration suite still green.

2. Per-section Clear/Export ignore active filters — intentional, left as-is. They operate on the full section by pin membership, matching the existing panel-level Clear/Export (which also ignore the search/method/direction filters). Keeping all three consistent felt less surprising than having section actions respect filters while the panel ones don't — "clear the section = clear the section." Happy to revisit if we later make the panel-level ones filter-aware too.

3. PR description (yellow vs violet) — fixed. Updated section 4 of the description to say violet; the code/comment were already correct.

4. replayHistoryRequest default branch — keeping it. It's unreachable at runtime (gated by isReplayableHistoryMethod + the switch enumerates every set member), but TS can't prove the gate guarantees membership, so without default the function fails "not all code paths return." So it stays as the required return — belt-and-suspenders that also satisfies the compiler.

5. Replay re-runs side effects — by design, acknowledged. Replaying a tools/call re-executes it (including destructive tools) with no confirmation, same as invoking it from the Tools screen. Noting it's intentional; a confirm-on-destructive affordance would be a separate enhancement.

Nits — agreed, no changes owed.

Re-validated: npm run validate (2094) + npm run test:storybook (339) green.

@claude review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 7, 2026

Claude finished @cliffhall's task in 1m 0s —— View job


Follow-up review — 96471f8d (outgoing client notifications)

  • Review outgoing client notification tracking change
  • Confirm transport send/onmessage symmetry
  • Check test coverage for new branch
  • Post review

Thanks for the thorough write-up — I re-read the diff against origin/v2/main and the follow-up commit. All five items are handled correctly; nothing left to change.

✅ Item 1 — outgoing client notifications (the one substantive change)

MessageTrackingTransport.send is now genuinely symmetric with onmessage:

if ("id" in message && message.id !== null && message.id !== undefined) {
  if ("result" in message || "error" in message) trackResponse(, "client");
  else if ("method" in message)                  trackRequest(, "client");
} else if ("method" in message) {
  trackNotification(message as JSONRPCNotification, "client");   // ← new
}

This now mirrors the onmessage branch structure exactly (messageTrackingTransport.ts:56-70 vs :115-136), the only difference being the "client" vs "server" origin tag — which is precisely the intended asymmetry. I traced it through the rest of the pipeline and it holds end-to-end:

  • No permanent "Pending". A client notification lands as direction: "notification", and extractStatus returns "none" for anything that isn't a request (HistoryEntry.tsx:83) — so no request-style badge. ✔
  • Correct direction badge. origin: "client"MessageDirectionBadge direction="outgoing" → green client → server (HistoryEntry.tsx:127-129). ✔
  • Respects the direction filter. matchesFilters keys visibility off entry.origin (HistoryListPanel.tsx:156), so the new client notifications honor the client/server toggle. ✔
  • Test updated, not just flipped. The old "does not track" case is now a positive assertion that trackNotification is called with (notification, "client") while trackRequest/trackResponse are not — a precise replacement, not a loosened one (messageTrackingTransport.test.ts:66-79). ✔

One thing worth a passing note (no action needed): send will now also track notifications/initialized and any notifications/cancelled the client emits, not just the three you listed — which is the correct, fully-symmetric behavior, just slightly broader than the commit message's examples imply.

✅ Items 2–5

  • 2 (per-section Clear/Export ignore filters) — agreed, consistency with the panel-level actions is the right call. "Clear the section = clear the section" is defensible and now matches the existing behavior rather than diverging from it.
  • 3 (yellow→violet prose) — description updated; code/comment were already correct.
  • 4 (default: branch) — correct, it's the required return that satisfies TS's "not all code paths return," not redundant defensiveness. Keep it.
  • 5 (replay side effects) — by design, acknowledged; confirm-on-destructive is a fair follow-up.

Verdict

No correctness issues. The transport fix is the load-bearing change and it's clean, symmetric, and covered. 👍 LGTM.
feat/history-pin-replay-1438

@cliffhall
Copy link
Copy Markdown
Member Author

Thanks — appreciate the careful re-trace. Confirming your passing note: yes, send now tracks all client-originated notifications (initialized, cancelled, progress, roots/list_changed, …), not just the three I listed — that's the intended fully-symmetric behavior; the commit examples were just illustrative.

No further changes. Ready to merge whenever. 🙏

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.

History: make Pin and Replay work on HistoryEntry items Add source / destination on History and Network screens

1 participant