fix(web): seed Edit-and-replay from what replay sends, name each JSON editor
Copilot's fourth pass, one inline plus two suppressed. All three were right. - The modal presented the whole captured frame as editable, but replay does not re-send a frame: it dispatches through the typed InspectorClient methods, whose signatures have no room for `_meta` (a captured `tools/call` carries `_meta.progressToken`) or for any list-request key but `cursor`. So editing `_meta` and pressing Send transmitted something other than what was on screen. `replayableParams` now projects the frame down to what the dispatcher actually reads, the modal seeds from that, and it names what was left out rather than leaving the difference to be discovered. The projection lives beside the dispatch switch because the two are halves of one contract. - `ContentViewer` gains `jsonLabel`, so a read-only JSON editor can say what it holds. Every one of them announced "JSON content", and an expanded Protocol entry has two while the list has many pairs — leaving a screen reader with indistinguishable textboxes and only the visible headings to tell them apart. Protocol entries now name theirs per method and direction, Network entries per request/response body. - `isJsonDocument` became `formatJsonDocument`, returning the formatted text instead of a boolean. The heuristic path parsed the payload to decide and then parsed it again to render; Protocol and Network hand over serialized bodies as untyped text and are not virtualized, so a long history paid twice per payload per render. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
12c00d8c63
commit
5abe289caa
2 changed files with 413 additions and 31 deletions
|
|
@ -221,6 +221,28 @@ describe("ProtocolEntry", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// The entry's frame carries `_meta.progressToken`, which replay cannot
|
||||
// re-send — so it must not appear in the editor as though it could.
|
||||
it("seeds only the params replay will actually read", async () => {
|
||||
const user = userEvent.setup();
|
||||
const withMeta: MessageEntry = {
|
||||
...successEntry,
|
||||
message: {
|
||||
...successEntry.message,
|
||||
params: {
|
||||
name: "get_weather",
|
||||
arguments: { city: "San Francisco" },
|
||||
_meta: { progressToken: 2 },
|
||||
},
|
||||
},
|
||||
} as MessageEntry;
|
||||
renderWithMantine(<ProtocolEntry {...baseProps} entry={withMeta} />);
|
||||
await openEditor(user);
|
||||
|
||||
expect(getAceText()).not.toContain("_meta");
|
||||
expect(screen.getByText(/`_meta` is not re-sent/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("replays with the edited params", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onReplay = vi.fn();
|
||||
|
|
@ -419,6 +441,24 @@ describe("ProtocolEntry", () => {
|
|||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Both payloads render the same kind of control, so without distinct names a
|
||||
// screen reader announces them identically.
|
||||
it("names the parameters and response editors distinctly", () => {
|
||||
renderWithMantine(
|
||||
<ProtocolEntry
|
||||
{...baseProps}
|
||||
entry={successEntry}
|
||||
isListExpanded={true}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByLabelText(/tools\/call request parameters JSON/),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByLabelText(/tools\/call response JSON/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render the Parameters section when message has no params", () => {
|
||||
renderWithMantine(
|
||||
<ProtocolEntry
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ import {
|
|||
extractSubscriptionId,
|
||||
} from "../protocolUtils.js";
|
||||
import { isReplayableProtocolMethod } from "../../../utils/replayableProtocolMethods";
|
||||
import type { ReplayParamsOverride } from "../../../lib/protocolReplay";
|
||||
import {
|
||||
replayableParams,
|
||||
type ReplayParamsOverride,
|
||||
} from "../../../lib/protocolReplay";
|
||||
|
||||
export interface ProtocolEntryProps {
|
||||
entry: MessageEntry;
|
||||
|
|
@ -333,6 +336,7 @@ export function ProtocolEntry({
|
|||
const status = extractStatus(entry);
|
||||
const requestParams = extractParams(entry);
|
||||
const canReplay = isReplayableProtocolMethod(method);
|
||||
const editableParams = replayableParams(method, requestParams);
|
||||
const resultType = extractResultType(entry);
|
||||
const subscriptionId = extractSubscriptionId(entry);
|
||||
|
||||
|
|
@ -480,7 +484,10 @@ export function ProtocolEntry({
|
|||
<EditReplayModal
|
||||
opened={isEditingReplay}
|
||||
method={method}
|
||||
params={requestParams}
|
||||
// Seeded with what replay will actually read, not the whole frame:
|
||||
// the editor must not offer a field that Send would drop.
|
||||
params={editableParams.params}
|
||||
droppedParamKeys={editableParams.dropped}
|
||||
onSend={onReplay}
|
||||
onClose={() => setIsEditingReplay(false)}
|
||||
/>
|
||||
|
|
@ -513,6 +520,10 @@ export function ProtocolEntry({
|
|||
text: serializeMessage(requestParams),
|
||||
}}
|
||||
copyable
|
||||
// Named, because an expanded entry holds two of these and a
|
||||
// list holds many pairs — unnamed they all announce the same
|
||||
// thing, leaving only the visible headings to tell them apart.
|
||||
jsonLabel={`${method} request parameters JSON`}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
|
@ -525,6 +536,7 @@ export function ProtocolEntry({
|
|||
text: serializeMessage(entry.response),
|
||||
}}
|
||||
copyable
|
||||
jsonLabel={`${method} response JSON`}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue