Skip to content
Merged
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ Code owns custom launch and continuation choices; OMP supplies the runtime
primitives that execute them. Moving the launcher into Manifold does not move
its product policy into OMP. The retained continuation scope is native fleet
metadata, exact terminal reopening, native resume and explicit next-resume
choices. The skill, automation and continuation additions are source-only,
not included in a published release; see [availability boundaries](docs/status.md#explicit-availability-boundaries).
choices.
Portable archives, general live settings-origin inspection, live per-account
quota preservation and Code-owned upstream fallback repair are not planned.

Expand All @@ -42,10 +41,13 @@ quota preservation and Code-owned upstream fallback repair are not planned.
including to quota where it strands a role or leads on a strained pool. A lane for
a signed-in family the model list lacks is struck, and pressing it checks the models,
which finds them. Cost and speed meters sit above a launch row that names the next step
(Launch, Save & launch, Review) beside a dropdown of the machines it can run
(Launch, Save & launch) beside a dropdown of the machines it can run
on; a machine without OMP is refused like an offline one. When the step is refused it
stays focusable and the launch line names what failed (the accounts, the workspace
profile, OMP missing from the machine) with a fix. **Routing** beside it lists every
profile, OMP missing from the machine) with a fix; pressing it anyway opens a dialog
with the reason, why it applies and the same fix. Whatever a press waits on the person
for (a check's charge, a review that differs) is asked in such a dialog, and how a
press ended also reaches Manifold's notice stack. **Routing** beside it lists every
role's `model:thinking`, with a fallback-chains checkbox, and **usage** under both;
each can be hidden. Below 760 px routing and usage stand under the generator when the
panel is tall enough for the generator and the whole routing at once, and are tabs of
Expand All @@ -65,7 +67,7 @@ quota preservation and Code-owned upstream fallback repair are not planned.
- **Model checks:** a launch needs a model list checked against the account pool,
and Launch runs that check itself when the list in use cannot serve it (a first
launch, a model OMP no longer lists, or a lead the accounts no longer serve), then
goes on to the launch. The first check asks the person to approve its
goes on to the launch. The first check asks the person, in a dialog, to approve its
cost, once: later checks within twice that charge run without asking, and Setup can
withdraw the approval. A finished check is kept as that person's check of the exact
account pool, so another workspace whose choices select the same pool adopts it on its
Expand All @@ -74,7 +76,7 @@ quota preservation and Code-owned upstream fallback repair are not planned.
list that passed meanwhile.
- **Agent launch:** the launch is one press, with no review step: the press reviews the
session immediately before starting it, and stops only when what would run differs
from what the panel showed, saying what differs. Where an Agent Run can carry it,
from what the panel showed, asking in a dialog whether to launch it. Where an Agent Run can carry it,
Code registers one Manifold Agent per sponsor and workspace profile with OMP's TUI
harness, updates it when the profile changes, and launches a Run whose terminal is
OMP's own terminal UI. **Sessions** heads each machine's group with these Runs: their
Expand Down
129 changes: 82 additions & 47 deletions docs/configuration.md

Large diffs are not rendered by default.

101 changes: 101 additions & 0 deletions plugins/code/generator/decision-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useId, useLayoutEffect, useRef, type KeyboardEvent, type MouseEvent } from "react";
import type { Decision, DecisionChoice } from "./decision-model.ts";

/** 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.
*/
export function DecisionDialog({ decision, onChoose }: { decision: Decision | null; onChoose: (choice: DecisionChoice, detail: number) => void }) {
const id = useId();
const dialog = useRef<HTMLDialogElement>(null);
const heading = useRef<HTMLHeadingElement>(null);
const openedAt = useRef(0);
// When each choice took focus, by label; a choice without focus has none.
const focusedAt = useRef(new Map<string, number>());
// 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.
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 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<HTMLDialogElement>) => {
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<HTMLButtonElement>) => {
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 <dialog ref={dialog} className={`${G}sheet-dialog ${G}decision`} aria-labelledby={`${id}-title`} aria-describedby={`${id}-text`}
data-decision={decision?.kind}
onCancel={event => { 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();
}}>
{decision && <div className={`${G}decision-body`}>
<h2 ref={heading} id={`${id}-title`} tabIndex={-1}>{decision.title}</h2>
<div id={`${id}-text`}>
{decision.text.map(text => <p key={text}>{text}</p>)}
{decision.items.length > 0 && <ul className={`${G}decision-items`}>{decision.items.map(item => <li key={item}>{item}</li>)}</ul>}
</div>
<div className={`${G}sheet-dialog-row ${G}decision-choices`}>
{decision.choices.map((choice, index) => <button key={choice.label} type="button" className={index === 0 && decision.choices.length > 1 ? `${G}decision-go` : `${G}go-fix`}
data-choice={choice.does.kind === "fix" ? choice.does.action.key : choice.does.kind} onKeyDown={keys(choice)}
onFocus={() => focusedAt.current.set(choice.label, performance.now())} onBlur={() => focusedAt.current.delete(choice.label)}
onClick={event => choose(choice, event.detail)}>{choice.label}</button>)}
</div>
</div>}
</dialog>;
}
136 changes: 136 additions & 0 deletions plugins/code/generator/decision-model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { orderedFamilies, providerPolicy } from "../../domain/providers.ts";
import type { RefusalView, StatusAction, Vocabulary } from "./statement-model.ts";
import type { VerificationMode } from "./verification.ts";

/*
* WHAT A PRESS 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, 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: 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, leave the question, or run a fix. */
export type DecisionDo =
| { readonly kind: "approve" }
| { readonly kind: "cancel" }
| { readonly kind: "launch" }
| { readonly kind: "dismiss" }
| { readonly kind: "fix"; readonly action: Extract<StatusAction, { kind: "fix" }> };
export type DecisionChoice = { readonly label: string; readonly does: DecisionDo };
export type Decision = {
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. */
readonly choices: readonly DecisionChoice[];
};
/** 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 }[] };

/** What the panel weighs to ask a question (generator-pane.tsx). */
export type DecisionFacts = {
/** The charge of the check in flight while it waits, and who started that check. */
readonly charge: ChargeFacts | null;
readonly mode: VerificationMode | null;
/** The person keeps checks and their approval (`readChecks().keeps`). */
readonly keepsChecks: boolean;
/** A press may launch now (the verb is ready): a held review and a narrowed stop are asked only then. */
readonly ready: boolean;
/** What a held review shows unlike what the panel showed, while it stands. */
readonly differs: readonly string[] | null;
/** The settings a check moved before a Launch press went on, while that stop stands. */
readonly narrowed: readonly string[] | null;
/** A refused Launch press, as the launch line says its refusal, while that refusal stands unchanged. */
readonly refused: RefusalView | null;
};

/**
* The one question the panel asks now, in precedence: a check's charge (from the launch, Models or a
* 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): 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);
if (facts.refused) return refusedDecision(facts.refused);
return null;
}

/** What answering does, in order: approve the charge, cancel the check, drop the held review, end the question, the launch's press, a fix. */
export type DecisionEffect =
| { readonly kind: "approve" }
| { readonly kind: "cancel" }
| { readonly kind: "drop-held" }
| { readonly kind: "end" }
| { readonly kind: "launch" }
| { readonly kind: "fix"; readonly action: Extract<StatusAction, { kind: "fix" }> };
/**
* What answering `decision` with `choice` does. Only Approve approves a charge; every other answer to
* a charge cancels the check first, so nothing is spent, then runs its fix. A held review goes on
* 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: 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) {
case "charge": return does.kind === "approve" ? [{ kind: "approve" }] : [{ kind: "cancel" }, ...then];
case "differs": return does.kind === "dismiss" ? [{ kind: "drop-held" }] : then;
case "narrowed": case "refused": return [{ kind: "end" }, ...then];
}
}

const plural = (count: number, word: string) => `${count} ${word}${count === 1 ? "" : "s"}`;

/**
* A model check's charge, waiting on the person: what it spends in all and per provider family (in
* the panel's family order: GPT, Claude, DeepSeek), why a launch needs it, and whether approving it
* 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<VerificationMode, "background">, 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" } } } },
{ label: "Close", does: { kind: "cancel" } }] };
}
const spent = new Map<string, number>();
for (const entry of charge.providers) {
const family = providerPolicy(entry.provider).family;
if (entry.requests > 0) spent.set(family, (spent.get(family) ?? 0) + entry.requests);
}
const spend = orderedFamilies(spent.keys()).map(family => `${vocab.family(family)} ${spent.get(family)}`).join(" · ");
return {
kind: "charge",
title: mode === "launch" ? "Check your models first" : "Check your models",
text: [
`${mode === "launch" ? "Launch needs to know" : "Code finds out"} which models your accounts can use${mode === "launch" ? ", so Code sends" : " by sending"} each one a tiny request.`,
...keepsChecks ? ["You approve this once. Later checks run on their own."] : [],
],
items: [`${plural(charge.requests, "tiny request")}${spend ? `: ${spend}` : ""}`],
choices: [{ label: mode === "launch" ? "Approve and launch" : "Approve", does: { kind: "approve" } }, { label: "Cancel", does: { kind: "cancel" } }],
};
}

/** A Launch press whose session review differs from what the panel showed: what differs, and both ways on. */
function differsDecision(differs: readonly string[]): Decision {
return { kind: "differs", title: "What will run differs", text: ["The session Code is about to start is not quite what the panel shows."], items: differs,
choices: [{ label: "Launch it", does: { kind: "launch" } }, { label: "Not now", does: { kind: "dismiss" } }] };
}

/** A Launch press whose model check narrowed the profile to what answered: the settings it moved, and both ways on. */
function narrowedDecision(settings: readonly string[]): Decision {
return { kind: "narrowed", title: "The model check changed your profile", text: ["Some models didn't answer, so the check changed these settings."], items: settings,
choices: [{ label: "Launch", does: { kind: "launch" } }, { label: "Not now", does: { kind: "dismiss" } }] };
}

/** A refused Launch press: what stops it, why, the fixes the launch line offers for that refusal, and Close. */
function refusedDecision(view: RefusalView): Decision {
return { kind: "refused", title: view.title, text: view.why ? [view.why] : [], items: [],
choices: [...view.fixes.map(action => ({ label: action.label, does: { kind: "fix", action } }) as const), { label: "Close", does: { kind: "dismiss" } }] };
}
Loading
Loading