Copilot's ninth pass, three comments, two of them consequences of round 8.
- The coercion check resolved the tool from the *entry's* name while the modal
lets `name` be edited, so retargeting an `echo` replay at `add` validated
against `echo`. The modal now takes the whole tool list and resolves from the
draft's current name, which is the one the dispatch will look up.
- `IMPRECISE_INTEGER_ERROR` (was `ROUNDED_INTEGER_ERROR`) no longer names the
safe-integer range, because the range is not the rule: a whole number past
±(2^53 − 1) that is exactly representable round-trips and is accepted. It
talks about exact representability instead, and its doc links the helper that
actually exists.
- `SchemaJsonField` carries the parser's reason instead of flattening every
case to "Not valid JSON", which misdiagnosed a document that parses fine and
holds a number this client cannot send — leaving the user reading about
syntax while looking at syntactically fine JSON.
The last one had a bug the gate caught rather than the tests: empty text is not
a *problem*, so routing the value through the reason check let `JSON.parse("")`
throw out of the change handler. It surfaced as five unhandled errors across
four suites with every assertion still green, which is why `npm run ci` and not
`npm run test` is the gate. Excluded explicitly, with a regression test.
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>
Copilot's eighth pass, three comments, all real.
- The precision check now reads the **source text**. Inferring the literal from
`String(value)` was wrong in both directions: it accepted every full-form
integer at or above 1e21 (`1000000000000000000001` parses to `1e21` and is
written back as `1e+21`), and refused `2^54`, which is written out in full
and exactly representable. `hasImpreciseIntegerLiteral` scans the draft for
numeric tokens — skipping strings, so a digit-string value is untouched — and
compares `BigInt(literal)` against `BigInt(Number(literal))`, which is exact.
- `""` is now reported as a surviving cursor for `tools/list` only. It builds
its params with `cursor !== undefined` and carries the empty string
deliberately; the other four list adapters use a truthiness check and drop
it, so the editor showed `{"cursor":""}` while `{}` went out. The asymmetry
looks like a latent bug in those four, but this function's job is to describe
what the dispatch does.
- `callTool` runs every string-valued argument through `convertToolParameters`,
so `{"count":"2"}` against a numeric schema is sent as `{"count":2}`.
`reshapedReplayParam` now takes the tool and detects that by running the same
conversion and comparing, rather than restating its rules — the two cannot
drift. `tools` is threaded to `ProtocolEntry` for it, optional throughout: an
unknown tool skips the check rather than blocking a replay.
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>
Copilot's seventh pass, three comments, all the same class: the editor showing
one value while the wire carries another.
- `JSON.parse` rounds a whole number past 2^53−1 to the nearest double, so a
draft reading `{"id":9007199254740993}` submits `…992`. New
`hasRoundedInteger`, applied by all four JSON-draft parsers — the raw
arguments editor, `SchemaJsonField`, the Edit-and-replay modal, and
`parseJsonObjectDraft`, which Copilot did not flag but shares the defect.
It is narrower than "unsafe integer": it also requires the number's shortest
form to be written out in full, because JS stringifies from 1e21 upward in
exponent form and such a value was typed that way — `{"n":1e308}` is a
legitimate `_meta` value that `parseJsonObjectDraft`'s tests already pin as
accepted. It still refuses the rare full-form integer that happens to be
exactly representable, erring toward refusing a sendable value rather than
misreporting an unsendable one — the trade `toNumericValue` already makes.
- `getPrompt` runs `convertPromptArguments`, which JSON-stringifies every
non-string argument, so `{"count":2}` leaves as `{"count":"2"}` while the
editor still shows a number. `reshapedReplayParam` now rejects a non-string
prompt argument. Not a workaround: the spec types
`GetPromptRequest.params.arguments` as `Record<string, string>`, so a string
is the only thing a prompt argument can be. `tools/call` is untouched —
`callTool` sends its arguments as given.
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>
Copilot's sixth pass generated no new inline comments; both suppressed ones
were right.
- `arguments` surviving the key check is not the same as being sent as
written. `replayProtocolRequest` applies `arguments ?? {}`, so a `null` shown
in the editor reaches the wire as `{}`; an array or scalar is not nullish and
rides the cast into a call whose type says it is a named-argument record.
`reshapedReplayParam` rejects both before Send. `name` and `uri` are
deliberately not checked there — the dispatch already refuses those with a
reason the caller shows as a toast, so they fail visibly rather than
silently.
- `NetworkEntry` never forwarded the body's content type, so the Network tab
could not reach the declared-JSON branch at all and a malformed
`application/json` response read as plain text with no sign it was meant to
be JSON. Forwarded — but only when the declared type *is* JSON: passing it
wholesale would also route an HTML body into the sandboxed frame and a CSV
one into a table, which is a change to what the Network tab is and not one
this makes.
The `NetworkEntry > Auth Success` play function moved off `textContent` for
body assertions. Ace repaints its rows on an animation frame and virtualizes
them, so reading the DOM straight after Reveal returned the previous rows
under load — it passed in isolation and failed in the full run. It now reads
the editors' documents alongside the DOM text, and waits.
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>
Copilot's fifth pass: one comment, and the completion of the previous round's
fix. Seeding the editor from `replayableParams` stopped it *offering* a key
the dispatcher would drop, but nothing stopped one being typed in — adding
`_meta` to a `tools/list` draft still let Send close the modal and discard it.
`parseParamsDraft` now takes the method and rejects any key the dispatch would
not read, so Send is disabled with the offending names rather than silently
dropping them. It asks the same `replayableParams` the seed is built from, so
the editor and the dispatcher cannot disagree about which keys survive.
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>
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>
Two more from Copilot's second pass, both real.
- Every JSON-draft parser now rejects a value `JSON.stringify` cannot write
back. `JSON.parse("1e400")` yields `Infinity`, which serializes as `null` —
so an editor showing `1e400` would have sent `null`, which is exactly the
misreport `isSerializableJson` was written for. `parseJsonObjectDraft`
already guarded; the raw-arguments editor and the Edit-and-replay modal did
not, and neither did `SchemaJsonField`'s `parseJsonDraft` — the last
predates this PR but is the same defect in the same family, so it is fixed
with them rather than left as the odd one out.
- Editing a root union's discriminator in the raw document now moves the
Variant picker. Nothing in `values` names a branch, so the index is held by
the form and was only re-derived on `resetKey` or a rewritten union; a raw
edit had no other signal, so `{"kind":"email"}` → `{"kind":"sms",…}` left
the picker on Email and switching back to the widgets rendered the Email
branch over SMS values. The current branch is kept when the values identify
none, since the discriminator is absent every time it is retyped.
The raw editor is keyed by entity only, not by entity-and-branch like the
per-field editors: it holds the whole arguments object, so a branch move must
re-sync it rather than remount it — a remount would reformat the text being
typed and drop the caret to the top.
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>
Extracts the Ace integration #2094 carried inline into a shared
`elements/JsonEditor`, and renders it on every surface where the web
client types or displays JSON.
The element is deliberately text in, text out. The two editing contracts
above it disagree about what an unparseable draft means — JsonObjectInput
keeps the last valid object and says nothing to its parent, SchemaJsonField
reports `undefined` and blocks submission (#2020) — so parsing, and the
draft/value split that goes with it, stays with each caller.
Surfaces: SchemaJsonField (inherited by all four SchemaForm consumers), a
new "Edit as JSON" switch for the whole arguments object, the server.json
File Contents box, the raw JSON-RPC request box, ContentViewer's JSON
branch (read-only, inherited by its eleven consumers), and a new "Edit and
replay" modal on a Protocol entry.
Ace read-only replaces the CodeHighlight JSON path and the Prism `json`
grammar loader is dropped: folding, not colorization, is what it adds, and
two highlighters for one language drift. `wrap={false}` callers and untyped
text that only looks like JSON keep the plain renderer.
Closes#2151
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>