feat(web): use the Ace JSON editor everywhere, and restore the raw-JSON toggle

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>
This commit is contained in:
cliffhall 2026-08-27 02:24:22 -04:00
commit 1a2a36f0b8

View file

@ -7,6 +7,7 @@ import {
screen,
waitFor,
} from "../../../test/renderWithMantine";
import { getAceText } from "../../../test/aceEditor";
import { NetworkEntry } from "./NetworkEntry";
const baseEntry: FetchRequestEntry = {
@ -232,28 +233,29 @@ describe("NetworkEntry", () => {
token_type: "Bearer",
}),
};
const { container } = renderWithMantine(
<NetworkEntry entry={authEntry} isListExpanded={true} />,
);
// Masked by default: the reveal affordance is present and the raw secret
// is nowhere in the DOM, but non-secret fields still render.
renderWithMantine(<NetworkEntry entry={authEntry} isListExpanded={true} />);
// Masked by default: the reveal affordance is present and the raw secret is
// nowhere in the rendered body, but non-secret fields still render. Read
// through the JSON editor rather than the DOM — Ace virtualizes its lines,
// so what the document holds is a function of a viewport happy-dom has no
// layout to give it.
expect(screen.getByText("Secrets hidden")).toBeInTheDocument();
expect(container.textContent).not.toContain("super-secret-token");
expect(container.textContent).toContain("••••••••");
expect(container.textContent).toContain("Bearer");
expect(getAceText()).not.toContain("super-secret-token");
expect(getAceText()).toContain("••••••••");
expect(getAceText()).toContain("Bearer");
await user.click(
screen.getByRole("button", { name: "Reveal secrets in body" }),
);
expect(screen.getByText("Secrets revealed")).toBeInTheDocument();
expect(container.textContent).toContain("super-secret-token");
expect(getAceText()).toContain("super-secret-token");
// Toggling back re-masks.
await user.click(
screen.getByRole("button", { name: "Hide secrets in body" }),
);
expect(container.textContent).not.toContain("super-secret-token");
expect(getAceText()).not.toContain("super-secret-token");
});
it("masks a form-encoded request body (code/verifier) until revealed", async () => {