From f629e8b4790c24e055fc2f8a9bdbca319d4d6946 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Tue, 13 Oct 2026 15:58:15 +0000 Subject: [PATCH 01/12] Code's popups stay inside its panel; the tour dims the panel around each control --- README.md | 7 +- docs/configuration.md | 80 +++--- plugins/code/generator/decision-dialog.tsx | 94 ++----- plugins/code/generator/decision-model.ts | 37 +-- plugins/code/generator/generator-pane.tsx | 7 +- plugins/code/generator/models-sheet.tsx | 18 +- plugins/code/generator/panel-layer.tsx | 216 +++++++++++++++ plugins/code/generator/styles.css | 94 ++++--- plugins/code/generator/tutorial-model.ts | 57 ++-- plugins/code/generator/tutorial.tsx | 295 +++++++++++++++------ plugins/code/generator/web.tsx | 79 +++--- plugins/scripts/verify-browser.ts | 10 +- plugins/test/decision-model.test.ts | 8 +- 13 files changed, 663 insertions(+), 339 deletions(-) create mode 100644 plugins/code/generator/panel-layer.tsx diff --git a/README.md b/README.md index 6eac9be..d89dd21 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,10 @@ quota preservation and Code-owned upstream fallback repair are not planned. - **Code workbench:** one main view under a top bar of tabs (Generator, Accounts, Sessions) and one **More** menu (Models, Setup, Options, Shortcuts, Tutorial); the panel - reads its inputs again on its own. A person's first open offers a short tutorial - that points at the real controls one at a time, once per person and replayable - from More. The **generator** is five rows of plain + reads its inputs again on its own. Everything it shows over itself (questions, Shortcuts, + the tutorial) stays inside the panel. A person's first open offers a short tutorial + that dims the panel around one real control at a time, in the order Code is used, + once per person and replayable from More. The **generator** is five rows of plain words (lane, model, thinking, advisor, fallbacks) changed in place by pointer, keys or wheel, each model shown by its short alias per tier, with **Defaults** and **Revert** in its head and a readout line that says what a pointed word does, diff --git a/docs/configuration.md b/docs/configuration.md index 61273f8..bdb11ed 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -334,10 +334,17 @@ menu shows each key beside its item). While a staged model list waits beside the one, More reads **More · staged** and its item **Models · staged**, since nothing changes until it is reviewed there. When session options are set, More and its **Options** item carry their summary ("· restricted", "· skills off", "· 2 skills"). **Shortcuts** opens a -native modal dialog that lists every key under the view it acts in (Generator, Views, +dialog inside the panel that lists every key under the view it acts in (Generator, Views, Accounts, Sessions, Tutorial, Anywhere), with `m`, `u` and `o` under Generator, where they act; -`Esc` or its Close button closes it and it owns its keys while open. **Tutorial** walks the -tutorial again (below). +`Esc`, a press on the dimmed panel or its Close button closes it and it owns its keys while +open. **Tutorial** walks the tutorial again (below). + +Everything Code shows over itself (a question a press waits on, Shortcuts, Models' import +and the tutorial) stands inside the Code panel and never over the rest of Manifold: only +the panel dims under it, and it stands in the part of the panel the window shows, +scrolling within itself where the panel is short. While a question, Shortcuts or the +import is open, the panel under it is inert, so neither a pointer nor a key reaches it, +while the rest of Manifold stays as it was; the panel's live region goes on speaking. The workbench reads its inputs again on its own: the workspace profile, OMP's setup, defaults, bundled model list, skills and accounts, and the machine list, once a minute @@ -359,32 +366,40 @@ sheet reads everything in one pass and restarts both. ### The tutorial -On a person's first open of the panel, wherever they open it, a question asks "Welcome -to Code" with **Take the tour** and **Skip** (`Esc` and the backdrop skip), once the -generator shows its profile in the main view with no sheet open, nothing running, no -charge waiting and no other question open. It is offered to people only: an agent or a +On a person's first open of the panel, wherever they open it, the panel dims and a card in +its middle says "Welcome to Code", what Code is in a sentence, and offers **Take the tour** +and **Skip** (`Esc` and a settled press on the dim skip), once the generator shows its +profile in the main view with no sheet open, nothing running, no charge waiting and no +question open; the panel under it is inert until it is answered, and the welcome keeps +the question dialog's guards (focus on its title, no press in its first moments, a key +only on a choice that has held focus). It is offered to people only: an agent or a service is never asked. Either answer marks the tutorial seen for that person (`seeTutorial`, or the browser for a sign-in that keeps no record), so it is never offered -again; **Tutorial** in More walks it whenever they like. The walk starts on the generator -(a sheet closes and the view goes back to it) and keeps to the steps the panel shows -there, so its count holds when a step opens another view: the profile's rows, the -readout, Defaults and Revert, routing, usage, the machine, the launch, the Accounts and -Sessions tabs, and More. Each step draws a thin ring in the accent around its control and -a card beside it (under it where the panel shows room, else over it), both laid over the -panel so nothing in it moves, and both kept to the part of the panel the window shows: the -step's name, where the walk stands ("3 of 10"), one sentence, and **Next** (**Done** at -the last), **Back** and **Skip**; `→` and `←` step, `Esc` skips, and a held key steps once. -Each step's card is its own, so moving to it moves focus to its Next and it is read out, -and its control is scrolled into view. Routing and usage are pointed at where the layout -puts them: beside the generator, else their tab or Show control. A step may be tried: -choosing a word in the rows, or opening Accounts, goes on by itself once done since the -step began, and Next always works too; Back onto a step another view hides brings the -generator back. The walk launches nothing and spends nothing; a word tried changes the -rows as any choice does, which Revert puts back. A step whose control is not shown (another -view or a sheet has the panel), or any step while a popover of the panel is open (More's -menu, the machine list), hides its card until that changes. The walk's end, finished or -skipped, puts focus on More, where it is replayed, so a second `↵` of a double press opens -a menu rather than launching. +again; **Tutorial** in More walks it whenever they like. + +The walk starts on the generator (a sheet closes and the view goes back to it) and keeps +to the steps the panel shows there, so its progress holds when a step opens another view. +It goes in the order a person uses Code: the profile's rows with the readout under them, +cost and speed, routing, usage, the launch with its machine, then the Accounts and +Sessions tabs and More. The dim opens around the first step's control, growing from where +the welcome stood, and each step glides the opening to its control, a thin rule in the +accent round it, with a card beside it (under it where the panel shows room, else over it, +else beside it), everything kept to the part of the panel the window shows and nothing in +the panel moved: the step's name, how far the walk has come (a row of blocks, read out as +"Step 3 of 8"), one or two sentences, and **Next** (**Done** at the last), **Back** and +**Skip**; `→` and `←` step, `Esc` ends it, a held key steps once, and `Tab` goes round the +card. Each step's card is its own and comes in as the opening lands, so moving to it moves +focus to its Next and it is read out, and its control is scrolled into view. Routing and +usage are lit where the layout puts them: beside the generator, else their tab or Show +control. A step may be tried through its opening: pointing at a word says in the readout +what it would change, and choosing one, or opening Accounts, goes on by itself once done +since the step began; Next always works too. Every other press on the dim or on a control +that is only shown is held, so the walk launches nothing and spends nothing; a word tried +changes the rows as any choice does, which Revert puts back. Back onto a step another view +hides brings the generator back, and a step whose control is not shown hides its card +until it is. The walk's end, finished or skipped, puts focus on More, where it is +replayed, so a second `↵` of a double press opens a menu rather than launching, and the +dim fades as the opening closes. Reduced motion places all of it without movement. ### The generator @@ -562,8 +577,8 @@ OMP's defaults again itself and refuses when they moved under its review. Choosi saved session does not stop a launch. Outcome and refusal lines clear whenever the team, machine or account pool changes. -**Questions and notices.** Whatever a press waits on the person for is asked in a modal -dialog over the whole page rather than said beside the launch, so it cannot be missed: a +**Questions and notices.** Whatever a press waits on the person for is asked in a dialog +over the dimmed panel rather than said beside the launch, so it cannot be missed: a check's charge (from the launch, Models or a struck lane), a review that differs, a check that changed the profile, and a refused Launch press. Each question says what it waits on and, where it can, why in a sentence, what it is about one fact per line, then its choices @@ -579,9 +594,10 @@ title, never on a choice, since a charge or a review comes back while the person typing elsewhere: `Tab` reaches the choices, the next action first. No press answers it in the 400 ms after it opens or its question changes, and a key answers with a choice only once that choice has held focus for 300 ms, so `Tab` then `↵` in one burst answers nothing; -neither does the second click of a double click or a held key. `Esc`, a press that starts -and ends on the backdrop, and a close the browser forces take the way out at any time. -Closing gives focus back to what had it. Reads on their own wait while a question is open. +neither does the second click of a double click or a held key. `Esc` takes the way out at +any time, and so does a press that starts and ends on the dimmed panel once the question +has settled. Closing gives focus back to what had it. Reads on their own wait while a +question is open. How a press ended is also posted to Manifold's one notice stack, which shows it at the foot of the page, each outcome replacing the last: "Launched on . Type your task in its terminal.", "Resumed on .", a save, a failed step in its words, and a check the diff --git a/plugins/code/generator/decision-dialog.tsx b/plugins/code/generator/decision-dialog.tsx index 5b8107b..58751e2 100644 --- a/plugins/code/generator/decision-dialog.tsx +++ b/plugins/code/generator/decision-dialog.tsx @@ -1,89 +1,34 @@ -import { useId, useLayoutEffect, useRef, type KeyboardEvent, type MouseEvent } from "react"; +import { useId, useLayoutEffect, useRef } from "react"; import type { Decision, DecisionChoice } from "./decision-model.ts"; +import { PanelDialog, useDeliberate } from "./panel-layer.tsx"; /** Generator-panel class prefix; every part hangs from the generator root (styles.css). */ const G = "plugin-atyrode_code_generator__"; -/** - * How long a press is ignored once a question opens or changes: one that lands this soon was aimed at - * what stood there before, as a click on Launch the moment its review or check comes back. - */ -export const DECISION_SETTLE_MS = 400; -/** - * How long a choice must have held focus before `↵` or `Space` answers with it: `Tab` then `↵` in one - * burst, as typing meant for somewhere else lands, is never an answer. It holds keys only, so an - * assistive tool's activation, which sends no key, is never refused. - */ -export const DECISION_FOCUS_MS = 300; /** - * THE QUESTION THE PANEL WAITS ON, in a modal dialog over the whole page, so it cannot be missed - * beside the launch. It opens with focus on its title, never on a choice, since a check's charge or a - * review comes back while the person may be typing elsewhere: `Tab` reaches the choices, the next - * action first. No press answers it in the first moments after it opens or its question changes, and - * a key answers with a choice only once that choice has held focus a moment, so a key meant for - * something else never answers it; neither does the second click of a double click or a held key. - * `Esc`, a press that starts and ends on the backdrop, and a close the browser forces take the way out - * (the last choice) at any time, since it never spends or launches. Closing gives focus back to what - * had it before, as a modal dialog does. + * THE QUESTION THE PANEL WAITS ON, in a dialog over the dimmed panel (panel-layer.tsx), so it cannot be + * missed beside the launch and covers nothing outside Code. It opens with focus on its title, never on + * a choice, since a check's charge or a review comes back while the person may be typing elsewhere: + * `Tab` reaches the choices, the next action first. No press answers it in the first moments after it + * opens or its question changes, and a key answers with a choice only once that choice has held focus + * a moment, so a key meant for something else never answers it; neither does the second click of a + * double click or a held key. `Esc`, and a settled press that starts and ends on the dimmed panel, + * take the way out (the last choice) at any time, since it never spends or launches. Closing gives + * focus back to what had it before. */ export function DecisionDialog({ decision, onChoose }: { decision: Decision | null; onChoose: (choice: DecisionChoice, detail: number) => void }) { const id = useId(); - const dialog = useRef(null); const heading = useRef(null); - const openedAt = useRef(0); - // When each choice took focus, by label; a choice without focus has none. - const focusedAt = useRef(new Map()); - // A press that began on the backdrop rather than in the box or on its edge. - const pressedOutside = useRef(false); - // Closes the dialog makes itself, because the question was answered or went: each fires a close event later, which is not a - // way out the browser forced. - const closes = useRef(0); // The question as it stands when an event arrives. const current = useRef(decision); current.current = decision; - // Any change of what it asks, its words or its choices, is a new question to settle on. + // Any change of what it asks, its words or its choices, is a new question to settle on, with focus back on its title. const asked = decision === null ? null : JSON.stringify([decision.kind, decision.title, decision.text, decision.items, decision.choices.map(choice => choice.label)]); - useLayoutEffect(() => { - const node = dialog.current; - if (!node) return; - if (asked === null) { - if (node.open) { closes.current += 1; node.close(); } - return; - } - if (!node.open) node.showModal(); - openedAt.current = performance.now(); - focusedAt.current.clear(); - heading.current?.focus({ preventScroll: true }); - }, [asked]); - useLayoutEffect(() => () => { if (dialog.current?.open) { closes.current += 1; dialog.current.close(); } }, []); - + const deliberate = useDeliberate(asked); + useLayoutEffect(() => { if (asked !== null) heading.current?.focus({ preventScroll: true }); }, [asked]); const wayOut = () => { const out = current.current?.choices.at(-1); if (out) onChoose(out, 0); }; - /** A press of a choice, once the question has settled, never a multi-click's second; keys are held to the focus rule before they press (`keys`). */ - function choose(choice: DecisionChoice, detail: number) { - if (detail > 1 || performance.now() - openedAt.current < DECISION_SETTLE_MS) return; - onChoose(choice, detail); - } - const outside = (event: MouseEvent) => { - const box = event.currentTarget.getBoundingClientRect(); - return event.target === event.currentTarget && - (event.clientX < box.left || event.clientX > box.right || event.clientY < box.top || event.clientY > box.bottom); - }; - /** `↵` or `Space` presses a choice only once it has held focus `DECISION_FOCUS_MS`, and never as a held key's repeat. */ - const keys = (choice: DecisionChoice) => (event: KeyboardEvent) => { - if (event.key !== "Enter" && event.key !== " ") return; - const at = focusedAt.current.get(choice.label); - if (event.repeat || at === undefined || performance.now() - at < DECISION_FOCUS_MS) event.preventDefault(); - }; - return { event.preventDefault(); wayOut(); }} - onClose={() => { if (closes.current > 0) closes.current -= 1; else wayOut(); }} - onPointerDown={event => { pressedOutside.current = outside(event); }} - onClick={event => { - if (!pressedOutside.current || !outside(event) || event.detail > 1 || performance.now() - openedAt.current < DECISION_SETTLE_MS) return; - pressedOutside.current = false; - wayOut(); - }}> + return heading.current} settled={deliberate.settled} onDismiss={wayOut}> {decision &&

{decision.title}

@@ -92,10 +37,9 @@ export function DecisionDialog({ decision, onChoose }: { decision: Decision | nu
{decision.choices.map((choice, index) => )} + data-choice={choice.does.kind === "fix" ? choice.does.action.key : choice.does.kind} {...deliberate.choice(choice.label)} + onClick={event => { if (deliberate.press(event.detail)) onChoose(choice, event.detail); }}>{choice.label})}
} -
; + ; } diff --git a/plugins/code/generator/decision-model.ts b/plugins/code/generator/decision-model.ts index e1fdd96..9edace1 100644 --- a/plugins/code/generator/decision-model.ts +++ b/plugins/code/generator/decision-model.ts @@ -4,34 +4,31 @@ import type { VerificationMode } from "./verification.ts"; /* * WHAT THE PANEL WAITS ON THE PERSON FOR, as one question: a model check's charge to approve, a - * reviewed session that differs from what the panel showed, a check that changed the profile, a - * refused Launch and its fixes, or, on a person's first open, whether to take the tutorial. Pure, so - * which question is asked, which choices it offers, which is the way out and what answering does are - * tested apart from the dialog that asks it (decision-dialog.tsx). Only a press the person made asks - * about a check: a check the panel runs on its own never does. + * reviewed session that differs from what the panel showed, a check that changed the profile, or a + * refused Launch and its fixes. Pure, so which question is asked, which choices it offers, which is + * the way out and what answering does are tested apart from the dialog that asks it + * (decision-dialog.tsx). Only a press the person made asks about a check: a check the panel runs on + * its own never does. */ -/** What a choice does: approve or cancel the waiting charge, take the launch's own press, start the tutorial, leave the question, or run a fix. */ +/** What a choice does: approve or cancel the waiting charge, take the launch's own press, leave the question, or run a fix. */ export type DecisionDo = | { readonly kind: "approve" } | { readonly kind: "cancel" } | { readonly kind: "launch" } - | { readonly kind: "tour" } | { readonly kind: "dismiss" } | { readonly kind: "fix"; readonly action: Extract }; export type DecisionChoice = { readonly label: string; readonly does: DecisionDo }; export type Decision = { - readonly kind: "charge" | "differs" | "narrowed" | "refused" | "tutorial"; + readonly kind: "charge" | "differs" | "narrowed" | "refused"; readonly title: string; /** What is asked and why, a plain sentence each. */ readonly text: readonly string[]; /** What the question is about, one fact per line: what a charge spends, each setting that moves. */ readonly items: readonly string[]; - /** The choices in reading order. The first is the next action; the last is the way out, which Esc and the backdrop take, and which never spends or launches. */ + /** The choices in reading order. The first is the next action; the last is the way out, which Esc and the dimmed panel take, and which never spends or launches. */ readonly choices: readonly DecisionChoice[]; }; -/** The questions a press waits on, which `decisionFor` asks and `answer` answers; the tutorial's offer is the workbench's own. */ -export type PressDecision = Decision & { readonly kind: Exclude }; /** A check's charge as it waits: the requests in all and per provider. */ export type ChargeFacts = { readonly requests: number; readonly providers: readonly { readonly provider: string; readonly requests: number }[] }; @@ -57,7 +54,7 @@ export type DecisionFacts = { * struck lane, never from a check the panel runs on its own), a review that differs, a check that * changed the profile, then a refused press; null when nothing waits on the person. */ -export function decisionFor(facts: DecisionFacts, vocab: Vocabulary): PressDecision | null { +export function decisionFor(facts: DecisionFacts, vocab: Vocabulary): Decision | null { if (facts.charge && facts.mode !== null && facts.mode !== "background") return chargeDecision(facts.charge, facts.mode, facts.keepsChecks, vocab); if (facts.differs && facts.ready) return differsDecision(facts.differs); if (facts.narrowed && facts.ready) return narrowedDecision(facts.narrowed); @@ -79,7 +76,7 @@ export type DecisionEffect = * only with the launch's press, and Not now drops it, so the next press reviews again. A narrowed * stop and a refused press end their question, then run what was chosen. */ -export function answer(decision: PressDecision, choice: DecisionChoice): DecisionEffect[] { +export function answer(decision: Decision, choice: DecisionChoice): DecisionEffect[] { const does = choice.does; const then: DecisionEffect[] = does.kind === "launch" ? [{ kind: "launch" }] : does.kind === "fix" ? [{ kind: "fix", action: does.action }] : []; switch (decision.kind) { @@ -97,7 +94,7 @@ const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? * once lets later checks run without asking (only for a person who keeps checks). A charge of nothing * asks nothing: it says so, offers the accounts, and its way out cancels the check. */ -function chargeDecision(charge: ChargeFacts, mode: Exclude, keepsChecks: boolean, vocab: Vocabulary): PressDecision { +function chargeDecision(charge: ChargeFacts, mode: Exclude, keepsChecks: boolean, vocab: Vocabulary): Decision { if (charge.requests === 0) { return { kind: "charge", title: "Nothing to check", text: ["None of the models your included accounts offer can be checked."], items: [], choices: [{ label: "Show accounts", does: { kind: "fix", action: { kind: "fix", key: "accounts", label: "Show accounts", fix: { kind: "open", place: "accounts" } } } }, @@ -122,25 +119,19 @@ function chargeDecision(charge: ChargeFacts, mode: Exclude ({ label: action.label, does: { kind: "fix", action } }) as const), { label: "Close", does: { kind: "dismiss" } }] }; } - -/** A person's first open: what Code is, and whether to take the short tutorial now. Skip is the way out; More replays it. */ -export function tutorialOffer(): Decision { - return { kind: "tutorial", title: "Welcome to Code", text: ["Code starts OMP coding sessions with the models you choose. A short tour shows where everything is."], - items: [], choices: [{ label: "Take the tour", does: { kind: "tour" } }, { label: "Skip", does: { kind: "dismiss" } }] }; -} diff --git a/plugins/code/generator/generator-pane.tsx b/plugins/code/generator/generator-pane.tsx index f6284cc..0c2bd5e 100644 --- a/plugins/code/generator/generator-pane.tsx +++ b/plugins/code/generator/generator-pane.tsx @@ -1,5 +1,4 @@ import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode, type RefObject } from "react"; -import { createPortal } from "react-dom"; import type { MachineSummary } from "@manifold/protocol"; import { useNotice } from "@manifold/plugin/hooks"; import type { OmpPresence } from "../machine-web.ts"; @@ -118,8 +117,6 @@ export type GeneratorPaneProps = { /** Hears the profile switches kept outside the rows (the session options sheet draws them) whenever they change. */ readonly onExtras: (extras: readonly SlotOption[]) => void; readonly controls: RefObject; - /** Where the dialog a press waits on mounts: outside the stage and the sheets, so it shows whichever of them is shown. */ - readonly decisionHost: HTMLElement | null; }; /** What the readout says: a value in bold, then what it means or does, warm when it refuses or strains. */ @@ -142,7 +139,7 @@ type Scrub = { readonly row: RowId; readonly origin: Review | null; readonly cat * is pointed, pressed or scrubbed in the readout. */ export function GeneratorPane(props: GeneratorPaneProps) { - const { model, pools, machines, presence, selectMachine, recents, connected, ledger, shown, profile, hidden, listFailure, reveal, announce, onOpen, onRefresh, onLaunchState, onExtras, controls, decisionHost } = props; + const { model, pools, machines, presence, selectMachine, recents, connected, ledger, shown, profile, hidden, listFailure, reveal, announce, onOpen, onRefresh, onLaunchState, onExtras, controls } = props; const id = useId(); const root = useRef(null); const launchButton = useRef(null); @@ -602,7 +599,7 @@ export function GeneratorPane(props: GeneratorPaneProps) { - {decisionHost && createPortal(, decisionHost)} + ; } diff --git a/plugins/code/generator/models-sheet.tsx b/plugins/code/generator/models-sheet.tsx index 3745423..0a4ae85 100644 --- a/plugins/code/generator/models-sheet.tsx +++ b/plugins/code/generator/models-sheet.tsx @@ -11,6 +11,7 @@ import { canWriteCodeWorkspace, codeOperationFailure, codeWorkflow } from "../ma import { accountWord, familyWord, hueOf, since, useMinuteTick } from "../ui.tsx"; import { WorkflowError, type CodeCall } from "../workflow.ts"; import { editHoldsCatalogWrite, type CatalogWrite, type GateRefusal } from "./launch-step.ts"; +import { PanelDialog } from "./panel-layer.tsx"; import { Blocks, pressesGo, SheetCue, SheetGo, SheetHead, SheetKeys, SheetReadout, sheetKeyFree, useReadout, useSheetMode, type GoAction, type GoFix, type GoHandle, type GoPress, type GoTone, type Said } from "./sheet-frame.tsx"; import { contextWords, exclusionWords, followPress, listChanges, modelRows, modelsPhase, money, saveFailure, speedLevel, thinkingRange, TIERS, @@ -171,7 +172,7 @@ export function ModelsSheet({ host, model, machine, onBack, backRef, onPlace }: } // ------------------------------------------------------------ import and export: two quiet actions in the head - const dialog = useRef(null); + const [importing, setImporting] = useState(false); const [paste, setPaste] = useState(""); const [importError, setImportError] = useState(null); const imported = useMemo(() => paste.trim() ? parseList(paste) : null, [paste]); @@ -184,7 +185,11 @@ export function ModelsSheet({ host, model, machine, onBack, backRef, onPlace }: const exportRefusal = running ? "A model check is running." : !(active?.document ?? inUseDocument) ? "There is no list to export." : null; function openImport() { setPaste(""); setImportError(null); - dialog.current?.showModal(); + setImporting(true); + } + function closeImport() { + setImporting(false); + setPaste(""); } function stageImport() { const document = imported; @@ -201,7 +206,7 @@ export function ModelsSheet({ host, model, machine, onBack, backRef, onPlace }: if (!base) wrote(absentAt, initialized); const staged = await code("stageCatalog", { containerId, expectedRevision: initialized.revision, document }); wrote(initialized.revision, staged); - dialog.current?.close(); + closeImport(); const beside = `${count(added, "change")} beside the list in use`; return { value: "staged", text: replacing ? `${beside}; it replaced the ${replacing.provenance ? "checked " : ""}list staged before` : beside }; }, setImportError); @@ -505,7 +510,8 @@ export function ModelsSheet({ host, model, machine, onBack, backRef, onPlace }: } - setPaste("")}> + dialog.querySelector("textarea")} onDismiss={closeImport}>

import a model list

Paste a Code model list, or drop its file here. {importEffect}