diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e5bc2343..ef3f64d9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,29 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. `code_required` (below). A server and a computer must be upgraded together: an older computer has no `/sign-in/code`. +### A new Bot is made from a sheet that asks where it runs first + +New Bot opens a sheet instead of the four-screen wizard. It asks where the Bot will run, with Run it +here chosen; a framework you run asks for its address next, and Continue checks from the server that +something answers there before going on. The name, title, instructions and who can see it come +last. A Bot given only a name gets a title and instructions the server accepts: for one that runs +here, the instructions are a short prompt naming it. The frameworks list Claude Agent SDK, Google +ADK, Google Antigravity and LangGraph first; AG2, Agno and Langroid now show their marks, and every +mark sits on a light tile in dark mode. + +### A run of tool calls folds into one line + +Consecutive tool calls a Bot makes without drawing anything now collapse into one line ("Used 5 +tools") that opens to show each, as connector calls already did. Calls that draw an interface, and +calls waiting on the person, still stand on their own. + +### Onboarding says which pictures are pictures, and tells a local install more + +The illustrations in onboarding sit in a muted frame and can no longer be clicked, tabbed into or +read out as controls. On a single-user install, three more slides follow: that its data stays on +the machine, which agents it can bring, and how to deploy it for an organization. +`/api/capabilities` gains `localEvaluation`, true in single-user mode. + ### An HTML-only email reaches its trigger as the sender wrote it An email trigger reads an HTML-only mail by stripping its tags, and the character references stayed: diff --git a/app/src/components/agents/create-agent-dialog.tsx b/app/src/components/agents/create-agent-dialog.tsx deleted file mode 100644 index 4c20c50e23..0000000000 --- a/app/src/components/agents/create-agent-dialog.tsx +++ /dev/null @@ -1,752 +0,0 @@ -import { summaryFor, WEB_HARNESSES } from "@openbot/shared/harnesses"; -import { IconSparkles } from "@tabler/icons-react"; -import { useMutation, useQuery } from "@tanstack/react-query"; -import { AnimatePresence, MotionConfig, motion } from "motion/react"; -import { useState } from "react"; -import useMeasure from "react-use-measure"; -import { HarnessMark } from "@/components/agents/harness-mark"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogBody, - DialogContent, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Field, - FieldError, - FieldGroup, - FieldLabel, -} from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; -import { - Questionnaire, - QuestionnaireChoice, - QuestionnaireChoiceDescription, - QuestionnaireChoices, - QuestionnaireDescription, - QuestionnaireItem, - QuestionnaireTitle, -} from "@/components/ui/questionnaire"; -import { Textarea } from "@/components/ui/textarea"; -import { - type AgentFormValues, - agentFormSchema, - agentInputFrom, - emptyAgentForm, -} from "@/lib/agents/form"; -import { createAgentMutationOptions } from "@/lib/agents/mutations"; -import { - agentCapabilitiesQueryOptions, - type ConnectionVerdict, - testAgentConnection, -} from "@/lib/agents/queries"; -import { isComposing } from "@/lib/composing"; -import { queryClient } from "@/query-client"; - -/** - * Creating a coworker, one question at a time. - * - * A wizard rather than a form, because the answers are three different kinds of decision: who this - * coworker is, who may see it, and where it runs. The last one is the fork — a built-in coworker - * needs nothing more, a managed one needs an endpoint — and a flat form showing endpoint fields to - * everybody made the common case read like the hard one. - */ -export function CreateAgentDialog({ - open, - onClose, - onCreated, -}: { - open: boolean; - onClose: () => void; - /** The new coworker's id, so the caller can open its dialog on it. */ - onCreated: (agentId: string) => void; -}) { - return ( - !next && onClose()} open={open}> - {/* A floor of 70% of the viewport, against the popup's usual grow-to-fit. The three steps are - very different heights — two choices on one, three fields on another, four plus an - endpoint test on the last — and a dialog that resized itself between each made the - wizard feel like three unrelated popups. The floor sits under the 85svh ceiling the popup - already carries, so the two never fight: every step lands in that 70–85% band, and the - shortest one pays for it with empty room below the question. */} - - {/* All wizard state lives below DialogContent, whose portal unmounts on close: dismissing - the dialog mid-way discards the half-answered steps rather than pickling them. */} - - - - ); -} - -/** - * The steps, in the order they are asked. The name is the questionnaire item's name. - * - * Visibility is last because it is the only question whose answer does not change what the person - * is making. Asked second, it arrived before they had decided anything that makes private or public - * mean something, and it was the step people had the least to go on. - */ -const STEPS = ["identity", "kind", "endpoint", "visibility"] as const; -type StepName = (typeof STEPS)[number]; - -/** The two ways a coworker can be seen. */ -const VISIBILITY_OPTIONS: Array<{ - value: AgentFormValues["visibility"]; - title: string; - description: string; -}> = [ - { - value: "private", - title: "Private", - description: "Only you can see it and start channels with it.", - }, - { - value: "public", - title: "Public", - description: "Everyone in the deployment can find and use it.", - }, -]; - -/** - * Where the coworker runs. Not a stored field: the server knows only whether an endpoint was - * given, so "built-in" is the empty endpoint and this choice exists to make that fork explicit. - * - * Which framework was picked is not stored either. Every one of them is reached the same way and - * written the same way, so after this dialog closes there is no record of the button that was - * pressed. - */ -type AgentKind = "builtin" | "managed"; - -/** The first step's slice of the form contract, so its errors match the server's limits. */ -const identitySchema = agentFormSchema.pick({ - name: true, - title: true, - roleDescription: true, -}); - -type IdentityField = keyof typeof identitySchema.shape; - -/** First message per field, or nothing when the step parses. */ -function identityIssues( - values: AgentFormValues, -): Partial> { - const parsed = identitySchema.safeParse(values); - if (parsed.success) return {}; - const issues: Partial> = {}; - for (const issue of parsed.error.issues) { - const field = issue.path[0] as IdentityField | undefined; - if (field && !issues[field]) issues[field] = issue.message; - } - return issues; -} - -/** A pane arrives from the side the journey is moving toward, and leaves out the other. */ -const variants = { - initial: (direction: number) => ({ x: `${110 * direction}%`, opacity: 0 }), - active: { x: "0%", opacity: 1 }, - exit: (direction: number) => ({ x: `${-110 * direction}%`, opacity: 0 }), -}; - -function CreateAgentWizard({ - onClose, - onCreated, -}: { - onClose: () => void; - onCreated: (agentId: string) => void; -}) { - const createAgent = useMutation(createAgentMutationOptions(queryClient)); - /* - * Whether "built-in" is a coworker this deployment can actually make. Assumed true while the - * answer is loading, so the common deployment never sees the card flash from disabled to - * enabled; the server refuses the create either way, so an optimistic card risks nothing. - */ - const { data: capabilities } = useQuery(agentCapabilitiesQueryOptions()); - const builtInAvailable = capabilities?.builtInAvailable ?? true; - - const [step, setStep] = useState(0); - const [direction, setDirection] = useState(1); - /** Whether this step's Continue was pressed, which is when its errors become worth showing. */ - const [tried, setTried] = useState(false); - const [values, setValues] = useState(emptyAgentForm); - /** Deliberately unanswered to start with: revealing endpoint fields is the point of asking. */ - const [kind, setKind] = useState(null); - /** - * Which framework's button was pressed, so Back returns to it still chosen. - * - * Nothing else reads it. Every framework leads to the same endpoint step and is stored the same - * way, so after this dialog closes there is no answer to "which did I pick" — by design. - */ - const [harness, setHarness] = useState(null); - - const [connection, setConnection] = useState(null); - const [testing, setTesting] = useState(false); - const [ref, bounds] = useMeasure(); - - /** - * The steps this answer actually asks, which is why there is no counter. - * - * A Bot that runs on this deployment is never asked for an address, so the sequence is three - * questions for it and four for one somebody hosts. `step` indexes this rather than `STEPS`, so - * the same index means the same question whichever way the person went. - */ - const sequence = STEPS.filter( - (name) => name !== "endpoint" || kind === "managed", - ); - const current = sequence[step]; - const last = step === sequence.length - 1; - const set = ( - key: K, - value: AgentFormValues[K], - ) => setValues((current) => ({ ...current, [key]: value })); - - /** Test endpoint reachability from the server, which is what runs will use. */ - const testConnection = async () => { - setTesting(true); - setConnection(null); - try { - setConnection( - await testAgentConnection(values.endpoint, values.authValue), - ); - } finally { - setTesting(false); - } - }; - - const identityErrors = tried ? identityIssues(values) : {}; - const endpointError = !tried - ? undefined - : values.endpoint.trim() === "" - ? "An address is required for an agent you host." - : agentFormSchema.shape.endpoint.safeParse(values.endpoint).error - ?.issues[0]?.message; - - /** - * A framework button, or the Bot that runs here, answered and moved on in one press. - * - * The press is the answer, so there is nothing left for a Continue to confirm — and the step - * this replaced cost the hosted path two presses for one decision. `go` is called with the next - * index directly rather than through `advance`, because `advance` would read `kind` from a - * closure this setState has not updated yet. - */ - const choose = (next: AgentKind, harnessId: string | null) => { - setKind(next); - setHarness(harnessId); - if (next === "builtin") { - // A built-in coworker has no endpoint; whatever was typed on the way past must not ride - // along into the create. - set("endpoint", ""); - set("authValue", ""); - setConnection(null); - } - go(step + 1); - }; - - const stepValid = (): boolean => { - if (current === "identity") { - return identitySchema.safeParse(values).success; - } - // Reachable only by pressing one of its buttons, which is the answer; guarded anyway for Enter. - if (current === "kind") return kind !== null; - if (current === "endpoint") { - return ( - values.endpoint.trim() !== "" && - agentFormSchema.shape.endpoint.safeParse(values.endpoint).success - ); - } - // Visibility always holds an answer; the radio starts on private. - return true; - }; - - const go = (to: number) => { - setDirection(to > step ? 1 : -1); - setTried(false); - setStep(to); - }; - - /** - * Every way forward lands here — the Continue button, Enter in a field, Enter on a choice — as - * the questionnaire form's submit. Backwards never validates; half answers are fine to leave. - */ - const advance = async () => { - if (!stepValid()) { - setTried(true); - return; - } - if (!last) { - go(step + 1); - return; - } - const agent = await createAgent.mutateAsync(agentInputFrom(values)); - onCreated(agent.id); - }; - - return ( - <> - {/* Read aloud, never shown: each step carries its own heading, and a dialog-level title - above them made two heading sizes compete. The popup still needs an accessible name. */} - New coworker - - { - if ( - event.key === "Enter" && - !event.shiftKey && - !isComposing(event) && - event.target instanceof HTMLInputElement - ) { - event.preventDefault(); - void advance(); - } - }} - onSubmit={(event) => event.preventDefault()} - > - {/* No step counter. The wizard is three questions for a Bot that runs here and four for - one at an address somebody hosts, and a counter whose total changes under the reader - mid-way is worse than none: it reads as the thing having got longer. Each step says - what it is asking in its own heading, which is the part that was carrying the reader - anyway. */} - - {/* The frame follows each pane's height, so the buttons glide instead of jumping. - `relative` is load-bearing: popLayout positions the exiting pane absolutely, and an - absolute element is clipped by overflow-hidden only on an ancestor that positions - it. Without this its containing block is the dialog popup, and the old pane slides - across the whole dialog instead of out of this frame. */} - 0 ? bounds.height : "auto" }} - className="relative overflow-hidden" - > -
- - - {current === "identity" ? ( - - ) : current === "visibility" ? ( - - ) : current === "endpoint" ? ( - void testConnection()} - set={set} - testing={testing} - values={values} - /> - ) : ( - - )} - - -
-
-
- - {createAgent.error ? ( -

- {createAgent.error.message} -

- ) : null} - - {/* `mt-auto` pins the row to the bottom when the step is short; `pt-6` is the gap it - keeps once the step is tall enough to have pushed it there itself. */} -
- - {/* No forward button where the choice is the answer: on "where does it run" every - button both answers and moves on, so a Continue beside them would be a - control that confirms a decision already taken. */} - {current === "kind" ? null : ( - - )} -
-
-
- - ); -} - -/** - * `hidden={false}` and `inert={false}` on every item, against the questionnaire's own hiding. - * - * The primitive blanks any item that is not the active one, which is right for its stacked layout - * and wrong here: only the active step is mounted, except for the instant an old pane is sliding - * out under AnimatePresence — exactly when the primitive would blank it mid-slide. The item's - * visibility is the animation's job in this dialog, never the questionnaire's. - */ -function StepItem({ - name, - children, -}: { - name: StepName; - children: React.ReactNode; -}) { - return ( - - ); -} - -function IdentityStep({ - values, - errors, - set, -}: { - values: AgentFormValues; - errors: Partial>; - set: ( - key: K, - value: AgentFormValues[K], - ) => void; -}) { - return ( - - Who is this coworker? - - A name to call it by, a line describing it, and the job it does. - - - - Name - set("name", event.target.value)} - placeholder="Expense Manager" - value={values.name} - /> - {errors.name ? ( - - ) : null} - - - Title - set("title", event.target.value)} - placeholder="Finance Operations" - value={values.title} - /> - {errors.title ? ( - - ) : ( -

- A few words, shown beneath its name in lists. -

- )} -
- - What it does -