// --------------------------------------------------------------------------- // customer-grid / ScriptViewPanel.tsx — owner item 6's renderer (W36-T04, contract C3). // // ⛔ ITS OWN MODULE, AND THAT IS A HARNESS FACT AS WELL AS A TIDINESS ONE. It began inside // `viewModes.tsx`, where the other display modes live — and that module `require`s `./cells`, // which imports glide-data-grid, whose CJS build cannot load under this repo's node harness. So // putting it there made the whole component untestable: the ONE thing this surface must prove is // that a hostile spec is rendered as text, and that claim can only be made by RENDERING. Here the // import graph is `react` + `./scriptViews` and nothing else, so `gridUx.test.ts` renders it for // real and asserts on the HTML a person would receive. // ⚠ The stylesheet is imported by `CustomerGrid.tsx` (the mount) for the same reason: tsc // preserves a side-effect CSS import into the emitted CommonJS and node dies on it. // --------------------------------------------------------------------------- import { memo, useEffect, useRef, useState } from "react"; import { barScale, renderableSpec, specKindName } from "./scriptViews"; import type { DrawableSpec, ScriptRun, ScriptView as ScriptViewRecord } from "./scriptViews"; // ═══════════════════════════════════════════════════════════════════════════════════════════ // ⭐⭐ W36-T04 (owner item 6 · rulings R3/R10 · contract C3) — THE CODE-SCRIPT VIEW // // Owner, verbatim: *"Add code script as an interface (database View) so a user can build whatever // they want through the Agent chat interface. be able to create any dashboard they want. User // should have the ability to see the code AND the dashboard output of course."* Both halves are // on screen at once, which is the whole of that sentence. // // ⛔⛔ NO BRANCH OF THIS RENDERER EXECUTES A STRING, AND THAT IS THE TICKET'S OWN `done-when`. // R10 runs the Python in a SERVER sandbox exactly so the browser never has to trust the result; // a renderer that then evaluated one would hand back everything the sandbox contains. So there is // no `dangerouslySetInnerHTML`, no `new Function`, no `eval`, no `href`/`src` taken from a spec, // and no `style` built from spec data anywhere below. Every value reaching the DOM is a SCALAR // that `renderableSpec` admitted, rendered as TEXT — the one exception being a bar's width, which // is a number clamped to a percentage here and never a string from the wire. // // ⛔ AN UNKNOWN `kind` IS A SENTENCE, NOT A BLANK. `spec` is whatever the script emitted (E's C3 // note 1: there is no fixed vocabulary, deliberately), so a view whose author invented // `kind: "sankey"` is a legitimate thing this build cannot draw. Saying so, by name, is the // difference between "this build does not know that shape yet" and "your script is broken". // // ⚠ `ok: false` IS A 200. A script that timed out or divided by zero asked a well-formed question // whose ANSWER is that no view was produced; it is rendered beside the code, never thrown, and // never a toast that disappears while the reader is looking at the line that caused it. // ═══════════════════════════════════════════════════════════════════════════════════════════ function SpecTable({ spec }: { spec: DrawableSpec }) { return ( {(spec.columns ?? []).map((c, i) => )} {(spec.rows ?? []).map((row, r) => ( {row.map((cell, c) => )} ))}
{c}
{cell}
); } function SpecMetrics({ spec }: { spec: DrawableSpec }) { return (
{(spec.items ?? []).map((item, i) => (
{item.label}
{item.value}
{item.note ?
{item.note}
: null}
))}
); } function SpecBars({ spec }: { spec: DrawableSpec }) { const series = spec.series ?? []; const scale = barScale(series); return (
{series.map((s, i) => (
{s.label} {/* ⚠ The ONE computed style on this surface, and it is arithmetic on a NUMBER the spec reader already validated as finite, clamped here. Never a string from the wire, which is what would make this an injection point. */} {s.display}
))}
); } /** The refusal, and the four limit fields PRINTED rather than re-worded (C3 note 2). */ function ScriptRefusal({ run }: { run: ScriptRun }) { return (

This script did not produce a view

{run.error ?? "The run ended without emitting anything."}

Reason
{run.code}
{run.limit ? <>
Subject
{run.limit.subject}
: null} {run.limit ? <>
Effect
{run.limit.effect}
: null} {run.limit ? <>
Cause
{run.limit.cause}
: null} {run.limit ? <>
What to do
{run.limit.recommendation}
: null}
); } /** * ⭐⭐ W37-T41 — THE SHAPE OF A SCRIPT, SHOWN AS A PLACEHOLDER RATHER THAN PREFILLED. * * Picking Custom View mints a view with an EMPTY source (verified: `script_sandbox.check_source("")` * accepts it, which is why "mint on pick" was safe to hand lane C in mailbox E-7). Without this, the * person who has just created one meets a blank box and a Run button, which is not an invitation. * * ⛔ A PLACEHOLDER, NEVER A PREFILLED `value`. Prefilling would make the view DIRTY the moment it * opened, arm Save on code nobody wrote, and put a version nobody asked for into a 40-deep history. * A placeholder vanishes on the first keystroke and is never stored. * ⚠ It is the two lines the sandbox's own fixture uses, so the example a person is shown is the * example the gate proves runs [[test-double-built-from-the-producer]]. */ const SCRIPT_STARTER = "rows = scoped_table()\nemit({'kind': 'kpi', 'label': 'Records', 'value': len(rows)})"; export function ScriptOutput({ run }: { run: ScriptRun | null }) { if (run === null) return

Run the script to see what it draws.

; const drawable = run.ok ? renderableSpec(run.spec) : null; const claimed = specKindName(run.spec); return ( <> {!run.ok ? : null} {run.ok && drawable === null ? (

This build cannot draw that shape yet

{claimed ? `The script emitted a view of kind "${claimed}", which this version does not render. It knows table, metrics, bars and text.` : `The script emitted a view with no recognisable kind. This version renders table, metrics, bars and text.`}

) : null} {drawable !== null ? ( <> {drawable.title ?

{drawable.title}

: null} {drawable.kind === "table" ? : null} {drawable.kind === "metrics" ? : null} {drawable.kind === "bars" ? : null} {drawable.kind === "text" ?

{drawable.body}

: null} ) : null} {run.stdout ? (
          {run.stdout}
          {run.truncated ? "\n[output truncated]" : ""}
        
) : null} ); } /** * The whole View: the source on the left, what it drew on the right. * * ⚠ A DRAFT RUN NEEDS NO SAVE (C3 note 3), which is what makes this usable: type, run, save when * it works. The Save button is separate and deliberately not automatic, because a PUT is a NEW * VERSION and an autosaving editor would fill the 40-deep history with keystrokes. */ export const ScriptView = memo(function ScriptView({ view, running, run, saving, readOnly = false, loading = false, reverting = false, onRun, onSave, onRevert, }: { view: ScriptViewRecord | null; running: boolean; run: ScriptRun | null; saving?: boolean; readOnly?: boolean; /** ⚠ The host knows whether a fetch is outstanding; the panel cannot tell that from a null view. * Defaulted so no existing caller breaks, which is safe HERE because the default is the older, * more pessimistic sentence rather than a silently disabled feature. */ loading?: boolean; reverting?: boolean; onRun: (draft: string) => void; onSave?: (source: string) => void; /** * ⭐ W37-T46 / D-338. Optional for the SAME reason `onSave` is: a read-only reader gets the * editor without the controls, and a caller that cannot revert simply does not pass this. The * control is not rendered at all when it is absent, rather than rendered and refused * [[permitted-is-not-answerable]]. */ onRevert?: (version: number) => void; }) { const [draft, setDraft] = useState(view?.source ?? ""); const loadedId = useRef(null); useEffect(() => { // Re-seed only when a DIFFERENT view is opened. Re-seeding on every `view` identity would // discard what somebody is typing the moment the list refreshes underneath them. if (!view || loadedId.current === view.id) return; loadedId.current = view.id; setDraft(view.source); }, [view]); const dirty = view !== null && draft !== view.source; // ⛔ W37-T41: "STILL LOADING" AND "COULD NOT BE OPENED" ARE DIFFERENT FACTS AND USED TO SHARE A // SENTENCE. With mint-on-pick (E-7) the normal path now passes through `view === null` for as // long as the fetch takes, so the old copy told every person who created a Custom View that it // was broken, for a moment, every time. A state that renames itself is exactly what cost this // product a QA pass one wave ago [[a-store-outage-renames-itself-at-every-layer]]. if (view === null) return (

{loading === true ? "Opening this view." : "This script view could not be opened."}

); return (
Code {`v${view.version}`} {/* ⚠ "v5" alone cannot tell a roll-back from a coincidence of matching code. This is the sentence that makes the history read as what happened. */} {view.restoredFrom ? `, restored from v${view.restoredFrom}` : ""} {dirty ? ", unsaved changes" : ""}
{/* ⭐⭐ W37-T41 — THE PLACEHOLDER IS THE CREATE FLOW'S WHOLE EMPTY STATE, and it is the half that made item 10 worth reopening. Picking Custom View mints a view with an EMPTY source (verified: `script_sandbox.check_source("")` accepts it, which is why "mint on pick" was safe to hand lane C in mailbox E-7). Without this the person who just created one meets a blank box and a Run button, which is not an invitation, it is a puzzle. ⛔ A PLACEHOLDER, NOT A PREFILLED `value`. Prefilling would make the view DIRTY the moment it opened, arm Save on code nobody wrote, and put a version nobody asked for into a 40-deep history. It vanishes on the first keystroke and is never saved. */} {/* ⭐⭐ W37-T46 / D-338 — THE CONTROL THAT WAS MISSING. The API already returned `history` newest-first and the header already showed `v`; there was simply no way to go back, which is the whole of D-338. It matters more now that a chat writes the code (R2): generation without roll-back means one bad answer costs somebody their view, and the person cannot tell in advance which answer that will be. ⛔ THE STRIP IS ABSENT, NOT DISABLED, WHEN THERE IS NOTHING TO GO BACK TO. A first version has no history, and a row of dead controls reads as a broken feature rather than as an empty one. ⚠ `trimmed` is PRINTED when it is non-zero. A history capped at 40 that silently showed 40 would let a reader conclude the view was only ever saved 40 times; saying how many were dropped is the difference between a partial record and a wrong one. */} {/* ⛔ W37-T46 — THE STRIP IS ABSENT WHEN THE CALLER CANNOT REVERT, which is what the prop's own doc twelve lines up already prescribed ("The control is not rendered at all when it is absent, rather than rendered and refused"). Line 293 shipped `onRevert === undefined` folded into `disabled` instead, so the one mount in the product rendered a permanently dead "Earlier | v1" pill and the comment disowned the code beside it. Two facts, two shapes: NOTHING TO GO BACK TO is an absent strip, and BUSY is a disabled button. */} {onRevert !== undefined && (view.history?.length ?? 0) > 0 ? (
Earlier {[...(view.history ?? [])].map((h) => ( ))} {view.trimmed > 0 ? ( {`${view.trimmed} older ${view.trimmed === 1 ? "version" : "versions"} dropped`} ) : null} {reverting ? Going back : null}
) : null}