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.
133 lines
4 KiB
TypeScript
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"}`,
|
|
},
|
|
];
|
|
}
|