// The create-agent dialog's steps: which there are, what lets the operator // leave one, and what the last one says will be sent. Kept out of the // component so `npm test` can run it. export const STEPS = [ "identity", "instructions", "permissions", "resources", "summary", ] as const; export type Step = (typeof STEPS)[number]; export const STEP_TITLES: Record = { identity: "identity", instructions: "instructions", permissions: "permissions", resources: "resources and inference", summary: "summary", }; // Client-side mirror of `hive_types::Ident` (1-63 chars of `[a-z0-9-]`), the // hint the name box shows; the server's own check stays the authority. // // The hyphen is escaped: browsers read a `pattern` attribute in Unicode-set // (`v`) mode, which refuses a bare trailing hyphen in a class. export const NAME_PATTERN = "[a-z0-9\\-]{1,63}"; export function nameProblem(name: string): string | null { return new RegExp(`^${NAME_PATTERN}$`).test(name) ? null : "the name is 1-63 characters: lowercase letters, digits and hyphens"; } /** The step after `step`, or `step` itself on the last. */ export function nextStep(step: Step): Step { return STEPS[Math.min(STEPS.indexOf(step) + 1, STEPS.length - 1)]; } /** The step before `step`, or `step` itself on the first. */ export function previousStep(step: Step): Step { return STEPS[Math.max(STEPS.indexOf(step) - 1, 0)]; } /** What the steps need before the one `step` can be left. */ export interface StepInputs { name: string; hive: string; // The permission list has loaded, so a pick is made against the real set. permissionsReady: boolean; // `draftProblem` of the inference boxes. inferenceProblem: string | null; } /** * Why the operator cannot go on from `step`, or null when they can. Every * step must be gone through, so this gates **next**; the last step has * nothing after it. */ export function stepProblem(step: Step, input: StepInputs): string | null { switch (step) { case "identity": return ( nameProblem(input.name) ?? (input.hive === "" ? "pick a hive" : null) ); case "permissions": return input.permissionsReady ? null : "the permission list is loading"; case "resources": return input.inferenceProblem; default: return null; } } /** One line of the summary: a setting and what will be sent for it. */ export interface SummaryRow { label: string; value: string; } export interface SummaryInput { name: string; hive: string; // The picked archetype's name, or undefined for a custom agent. archetype: string | undefined; // `requestPermissions`: undefined when the default set is kept. permissions: string[] | undefined; // The filled fields only, as `capsBody` and `draftBody` make them. caps: object; inference: object; agentsMd: string; } function settings(values: object): string { const parts = Object.entries(values).map( ([key, value]) => `${key.replace(/_/g, " ")} ${Array.isArray(value) ? value.join(", ") : String(value)}`, ); return parts.length === 0 ? "none set" : parts.join("; "); } /** * What the summary lists: every setting the request carries, and for each * one left alone, what then applies. */ export function summaryRows(s: SummaryInput): SummaryRow[] { const words = s.agentsMd.trim() === "" ? 0 : s.agentsMd.trim().split(/\s+/).length; return [ { label: "name", value: s.name }, { label: "hive", value: s.hive }, { label: "archetype", value: s.archetype ?? "none: a custom agent" }, { label: "permissions", value: s.permissions === undefined ? "the default set" : s.permissions.length === 0 ? "none" : s.permissions.join(", "), }, { label: "resource caps", value: settings(s.caps) }, { label: "inference", value: settings(s.inference) }, { label: "instructions", value: words === 0 ? "none" : `AGENTS.md, ${words} ${words === 1 ? "word" : "words"}`, }, ]; }