Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/swarm-ui/src/pages/CreateAgentSteps.ts
iris 339da0aebe swarm-ui: make agent creation a five-step dialog with a fixed header and footer
The create dialog was one tall form that scrolled as a whole, so its header
and button moved out of view. It is now steps you go through in order:
identity, instructions (AGENTS.md in a box of its own), permissions,
resources and inference, then a summary of what will be sent and what the
agent creation queues, where create is. The header, step tabs and footer stay
put and only the step's own fields scroll. A refused AGENTS.md text sends the
dialog back to its step. The request is unchanged.
2026-10-25 01:40:20 +02:00

133 lines
4 KiB
TypeScript

// 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<Step, string> = {
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"}`,
},
];
}