diff --git a/.claude/skills/openbot-copy/SKILL.md b/.claude/skills/openbot-copy/SKILL.md index 17afc18ddb..0ef475c4cb 100644 --- a/.claude/skills/openbot-copy/SKILL.md +++ b/.claude/skills/openbot-copy/SKILL.md @@ -40,7 +40,8 @@ because the admin is the one who sets them. A page anybody can open may not. 1. **Say what happened, then what to do.** "Gmail isn't connected. Connect it to let Bots read your mail." Not why the screen is built the way it is. -2. **One idea per sentence. Short sentences.** A description is one or two sentences, about 20 words. +2. **Use complete sentences for explanations.** Page descriptions, helper text, notices, and empty-state bodies need a subject and a verb, or a direct instruction ("Connect your accounts"). Avoid fragments such as "The apps your Bots use" or "What this Bot can do." Headings, labels, buttons, and short status labels may be fragments. + **Keep only useful explanation.** State the action, outcome, or constraint the person needs. Remove copy that merely repeats the heading. Use natural sentence lengths; do not chop prose into fragments to meet a word count. 3. **Plain words.** "use", not "hold a grant on". "can't", not "is unable to". Contractions are fine. 4. **No "rather than", no em dashes, no "which is why", no "so that nothing … that may be wrong".** These are the marks of text that is explaining itself. Cut the clause or make it its own sentence. @@ -59,6 +60,7 @@ because the admin is the one who sets them. A page anybody can open may not. | Before | After | |---|---| +| The apps and accounts your Bots use as you. Open one to connect it or change it. | Connect your accounts so Bots can use apps on your behalf. | | Your connected accounts could not be loaded, so nothing is listed here rather than a list that may be wrong. Reload the page, and tell an administrator if it persists. | Couldn't load your connections. Reload the page, or ask an admin if it keeps happening. | | Sent to the coworker it is for. Type @ to choose one yourself. | OpenBot picks the right Bot. Type @ to choose one. | | Gmail is a Composio app, and this deployment has no Composio key. (to a non-admin) | Gmail isn't set up yet. An admin has to turn it on. | @@ -73,5 +75,6 @@ because the admin is the one who sets them. A page anybody can open may not. | "this deployment" on a page anybody can open | OpenBot, or say what is missing | | A sentence over ~25 words | Split it, or cut the clause that justifies the screen | | "rather than", an em dash, "which is" in UI text | Rewrite as two short sentences | +| A description beginning "The apps…", "What…", "Who…", or "Where…" without a main verb | State the action or behavior in a complete sentence; remove redundant explanation | | An env var, OAuth, Composio or "grant" outside Admin | Say what the person sees | | A test asserting long explanatory copy | Assert the short sentence, or the role and the key words | diff --git a/agent-computer/tests/enter-text.test.ts b/agent-computer/tests/enter-text.test.ts index ac9510cfb0..9983b5e059 100644 --- a/agent-computer/tests/enter-text.test.ts +++ b/agent-computer/tests/enter-text.test.ts @@ -40,7 +40,8 @@ let browser: Browser; beforeAll(async () => { if (!asked) return; const { chromium } = await import("playwright"); - browser = await chromium.launch(); + // Match the computer image: full Chromium is installed without the headless shell. + browser = await chromium.launch({ channel: "chromium" }); }); afterAll(async () => { diff --git a/agent-langgraph-agui/tests/test_provider_boundaries.py b/agent-langgraph-agui/tests/test_provider_boundaries.py index a0a824f134..f00ca5bcf7 100644 --- a/agent-langgraph-agui/tests/test_provider_boundaries.py +++ b/agent-langgraph-agui/tests/test_provider_boundaries.py @@ -12,6 +12,7 @@ import httpx2 import pytest +from anthropic import Anthropic sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -337,8 +338,14 @@ async def test_anthropic_selection_reaches_anthropic_boundary_without_openai_key assert result["messages"][0].content == "anthropic proof" assert "OPENAI_API_KEY" not in os.environ assert captured[0]["url"] == "http://127.0.0.1:4311/v1/messages" - assert captured[0]["headers"]["x-api-key"] == "sk-ant-openbot-ci" - assert captured[0]["headers"]["anthropic-version"] == "2023-06-01" + # Anthropic 2.3 uses Bearer authentication; older SDKs send x-api-key. + headers = captured[0]["headers"] + assert ( + headers.get("x-api-key") == "sk-ant-openbot-ci" + or headers.get("authorization") == "Bearer sk-ant-openbot-ci" + ) + with Anthropic(api_key="sk-ant-openbot-ci") as sdk: + assert headers["anthropic-version"] == sdk.default_headers["anthropic-version"] assert captured[0]["body"]["model"] == request_model assert captured[0]["body"]["messages"] == [ {"role": "user", "content": "Say hello."} diff --git a/app/src/components/agents/agent-profile.tsx b/app/src/components/agents/agent-profile.tsx index 2197c09c3a..ca4df90cd7 100644 --- a/app/src/components/agents/agent-profile.tsx +++ b/app/src/components/agents/agent-profile.tsx @@ -84,9 +84,11 @@ export function AgentProfile({

{profile.name}

-

- {profile.title} -

+ {profile.title && ( +

+ {profile.title} +

+ )}
diff --git a/app/src/components/agents/handoff-panel.tsx b/app/src/components/agents/handoff-panel.tsx index 9a26e584d6..6a693c4910 100644 --- a/app/src/components/agents/handoff-panel.tsx +++ b/app/src/components/agents/handoff-panel.tsx @@ -77,7 +77,7 @@ export function HandoffPanel({ agentId }: { agentId: string }) { ) : null } - description="The Bots this one can ask for help. This doesn't change who can ask it. Their answers come back into the same conversation, under their name." + description="Choose which Bots this Bot can ask for help. Their answers appear in the same conversation under their own names. This does not change who can ask this Bot for help." title="Bots it can ask" > {/* Facts about the deployment rather than rows to act on, so said as sentences. */} diff --git a/app/src/components/agents/new-bot-sheet.tsx b/app/src/components/agents/new-bot-sheet.tsx index 87393f7930..c6f4dbdb3f 100644 --- a/app/src/components/agents/new-bot-sheet.tsx +++ b/app/src/components/agents/new-bot-sheet.tsx @@ -2,7 +2,7 @@ import { summaryFor, WEB_HARNESSES } from "@openbot/shared/harnesses"; import { IconCheck, IconLock, - IconSparkles, + IconPlugConnected, IconUsers, } from "@tabler/icons-react"; import { useMutation, useQuery } from "@tanstack/react-query"; @@ -48,18 +48,13 @@ import { } from "@/lib/app-access/queries"; import { isComposing } from "@/lib/composing"; import { pluginKeys } from "@/lib/plugins/queries"; -import { cn } from "@/lib/utils"; import { queryClient } from "@/query-client"; /** * Where a new Bot is made: a wide sheet floating in from the right, so the roster it is being added * to stays in view behind it. * - * No visible header and no corner ✕ — Cancel, Escape and a click outside all close it. It replaced - * a centred wizard dialog that asked the same questions over four screens, one of them only to say - * who could see the Bot. - * - * The title is still there for screen readers: a dialog with no name is announced as just "dialog". + * New Bot details open first. Cancel, Escape and a click outside close the sheet. */ export function NewBotSheet({ open, @@ -84,7 +79,6 @@ export function NewBotSheet({ side="right" variant="inset" > - New Bot {/* Inside the popup, so it unmounts on close and a reopened sheet starts empty. */} @@ -93,28 +87,26 @@ export function NewBotSheet({ } /** - * The steps, in the order they are asked. - * - * WHERE IT RUNS COMES FIRST because it decides what the rest of the sheet means. For a Bot that runs - * here, its instructions are the system prompt it runs on; for one somebody else runs, they are a - * line added on top of a prompt written elsewhere. Asking for them before knowing which left the - * field unable to say what it was for. The address is asked only of a Bot somebody else runs, and is - * checked before the person spends time naming it. About it is always last, and always creates. + * Details come first; custom agents then choose a framework and verify their endpoint. + * Advancing from the endpoint also tests it, so there is no separate Test action to forget. */ -const STEPS = ["runtime", "endpoint", "about"] as const; +const STEPS = ["about", "runtime", "endpoint"] as const; +const CONNECTION_OPTIONS = [ + ...WEB_HARNESSES.filter((harness) => harness.id === "byo-url"), + ...WEB_HARNESSES.filter((harness) => harness.id !== "byo-url"), +]; type Step = (typeof STEPS)[number]; /** * Where the Bot runs: on this deployment, or behind a framework somebody hosts. * - * Which framework is kept so Back returns to it still chosen, and so a Bot made without a title can - * be given one. Every framework is reached the same way and stored the same way — an AG-UI address — + * The framework is kept for its connection instructions and default role description. Every framework is reached the same way and stored the same way — an AG-UI address — * so the server never hears which was picked. */ type Runtime = { kind: "builtin" } | { kind: "managed"; harness: string }; -/** What a created Bot is called beneath its name when the person gave it no title. */ -function defaultTitle(runtime: Runtime): string { +/** Framework name used in default custom-agent instructions. */ +function frameworkName(runtime: Runtime): string { if (runtime.kind === "builtin") return "Assistant"; if (runtime.harness === "byo-url") return "AG-UI agent"; return ( @@ -134,7 +126,7 @@ function defaultInstructions(name: string, runtime: Runtime): string { if (runtime.kind === "builtin") { return `You are ${name}. Help the people you work with on whatever they bring you, clearly and briefly.`; } - return `Connected from ${defaultTitle(runtime)}.`; + return `Connected from ${frameworkName(runtime)}.`; } /** @@ -154,15 +146,16 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) { */ const { data: capabilities } = useQuery(agentCapabilitiesQueryOptions()); const builtInAvailable = capabilities?.builtInAvailable ?? true; - const [step, setStep] = useState("runtime"); + const [step, setStep] = useState("about"); const [direction, setDirection] = useState(1); const [name, setName] = useState(""); - const [title, setTitle] = useState(""); + const [nameError, setNameError] = useState(); const [instructions, setInstructions] = useState(""); /** Private until said otherwise: a Bot shown to everybody by accident is the worse mistake. */ const [visibility, setVisibility] = useState("private"); /** Run it here unless the person picks otherwise: the answer that needs nothing set up. */ const [runtime, setRuntime] = useState({ kind: "builtin" }); + const custom = !builtInAvailable; const [endpoint, setEndpoint] = useState(""); const [key, setKey] = useState(""); /** Whether Continue was pressed on the address, which is when its error becomes worth showing. */ @@ -216,9 +209,9 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) { const canContinue = !busy && (step === "about" - ? named + ? named && (custom || builtInAvailable) : step === "runtime" - ? runtime !== null + ? runtime?.kind === "managed" : endpointIssue === undefined || !tried); const go = (to: Step) => { @@ -234,7 +227,7 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) { const agent = await createAgent.mutateAsync( agentInputFrom({ name: name.trim(), - title: title.trim() || defaultTitle(chosen), + title: "", roleDescription: instructions.trim() || defaultInstructions(name.trim(), chosen), visibility, @@ -259,25 +252,17 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) { const advance = () => { if (step === "runtime" && runtime) { - return go(runtime.kind === "builtin" ? "about" : "endpoint"); + return go("endpoint"); } if (step === "endpoint") return connect(); - if (step === "about" && runtime) return create(runtime); + if (step === "about") + return custom ? go("runtime") : create({ kind: "builtin" }); }; - /** Where Back goes: the address for a Bot somebody else runs, where it runs otherwise. */ - const previous = (): Step => - step === "about" && runtime?.kind === "managed" ? "endpoint" : "runtime"; + /** Back returns to the previous screen without resetting any answers. */ + const previous = (): Step => (step === "endpoint" ? "runtime" : "about"); - /** - * Continue on the address: dial it from the server, and move on only if it answers. - * - * There is no separate Test button. A test that could be skipped let a Bot be made pointed at - * nothing, and one that could not be skipped was a second button doing Continue's job. So Continue - * tests: an agent that answers moves on, and one that does not says why under its address, where - * the fix will be typed. Continue stays pressable after that, because the usual fix is starting the - * agent, not editing what was typed. - */ + /** Verify the endpoint before creating a custom Bot. */ const connect = async () => { const focusAddress = () => // Focus would otherwise fall to the page while the button below is busy or disabled. @@ -297,12 +282,13 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) { focusAddress(); return; } - go("about"); + if (runtime?.kind === "managed") await create(runtime); }; if (createdId) { return ( + New Bot ); @@ -344,12 +330,17 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) {
{/* Shares its layout id with the orb on the created view, so it travels there. */} - - - +
+ + + + + New Bot + +
void }) { transition={{ duration: 0.2, ease: "easeOut" }} > {step === "about" ? ( - - setLeftOut((current) => { - const next = new Set(current); - if (used) next.delete(app); - else next.add(app); - return next; - }) - } - runtime={runtime ?? { kind: "builtin" }} - onInstructions={setInstructions} - onName={setName} - onTitle={setTitle} - onVisibility={setVisibility} - title={title} - visibility={visibility} - /> + <> + + setLeftOut((current) => { + const next = new Set(current); + if (used) next.delete(app); + else next.add(app); + return next; + }) + } + onInstructions={setInstructions} + onName={(value) => { + setName(value); + setNameError(undefined); + }} + onVisibility={setVisibility} + visibility={visibility} + /> + {!builtInAvailable && ( +

+ An admin must enable built-in hosting before you can run a + Bot here. +

+ )} + ) : step === "runtime" ? ( { + setRuntime(chosen); + go("endpoint"); + }} /> ) : ( void }) { ) : null}
- - {step === "runtime" ? ( + + {step === "about" ? ( } > @@ -432,20 +433,45 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) { Back )} - +
+ {step === "about" && builtInAvailable && ( + + )} + {step !== "runtime" && ( + + )} +
); @@ -528,58 +554,40 @@ const VISIBILITY_OPTIONS: Array<{ { value: "public", title: "Public", - description: "Everyone in your OpenBot can find and use it.", + description: "Everyone in your company can find and use it.", icon: IconUsers, }, ]; /** - * The last step: what to call it, who can see it, and — only if the person wants to be more - * specific — a line describing it and the instructions it follows. - * - * Title and instructions are optional and say so; who can see it always holds an answer, so a Bot - * named and nothing else is a complete answer, and Create waits for nothing more. - * - * The instructions field says what it is for the Bot being made. For one that runs here it is the - * prompt it runs on, so it is called Instructions and says how it behaves without them; for one - * somebody else runs it is a line added to a prompt written elsewhere, so it is called Role. The - * title's example is the framework's name, which is what it becomes when left blank. - * - * Who can see it was the old wizard's last step. It is here because this step had the room, and a - * question that does not change what is being made is cheaper answered alongside the name than as a - * screen of its own. - * - * The apps it can use come last and arrive ticked: the person's own connected apps an admin lets - * owners give. Drawn only when there are some, so a person with none connected is asked nothing. + * Shared details for both hosting paths. Name is required; instructions are optional. + * Visibility sits alongside the name because it does not need a separate screen. + * Connected apps are selected initially and only shown when some can be granted. + * These choices stay in the parent flow while navigating between screens. */ function AboutStep({ apps, leftOut, onApp, name, - title, + nameError, instructions, - runtime, visibility, onName, - onTitle, onInstructions, onVisibility, }: { name: string; - title: string; + nameError?: string; instructions: string; - runtime: Runtime; visibility: Visibility; onName: (value: string) => void; - onTitle: (value: string) => void; onInstructions: (value: string) => void; onVisibility: (value: Visibility) => void; apps: GiveableApp[]; leftOut: Set; onApp: (app: string, used: boolean) => void; }) { - const runsHere = runtime.kind === "builtin"; return ( @@ -588,30 +596,26 @@ function AboutStep({ autoComplete="off" autoFocus id="new-bot-name" + aria-invalid={Boolean(nameError)} + aria-describedby={nameError ? "new-bot-name-error" : undefined} maxLength={80} onChange={(event) => onName(event.target.value)} placeholder="Expense Review" value={name} /> - - - - Title - Optional - - onTitle(event.target.value)} - placeholder={runsHere ? "Finance Operations" : defaultTitle(runtime)} - value={title} - /> - A few words shown under its name. + {nameError && ( + + )} - {runsHere ? "Instructions" : "Role"} + Instructions Optional