Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .claude/skills/openbot-copy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ end. Try again." Name a feature by what it does (Automatic learning, Memory, Ski
docs under `docs/` and `charts/` are not shown to a person and may name it.
`tests/no-intelligence-in-user-copy.test.ts` fails on a shown string that says it.

**One exception: OpenBot Desktop's Account step.** Its lede says "This version of OpenBot
runs self-contained on this computer. It uses a local version of CopilotKit Intelligence, which
requires a free license key." That copy is Atai's, word for word.
That screen asks the person to sign in to another company's account, and the name is what makes
the license key make sense; the CopilotKit sign-in page it opens says the same. It is listed by
exact text in `SHOWN_BY_DECISION` in the test. Anywhere else, the back end.

**Admin is allowed exact names.** An admin page may name `COMPOSIO_API_KEY`, OAuth and "deployment",
because the admin is the one who sets them. A page anybody can open may not.

Expand Down Expand Up @@ -92,3 +99,4 @@ because the admin is the one who sets them. A page anybody can open may not.
| An env var, OAuth, Composio or "grant" outside Admin | Say what the person sees |
| "Intelligence", "CopilotKit Intelligence" or "the Intelligence stack" anywhere | Name the part: the back end, the front end, the database |
| A test asserting long explanatory copy | Assert the short sentence, or the role and the key words |
| "CopilotKit Intelligence" on OpenBot Desktop's Account step | Allowed there only, in the one lede `SHOWN_BY_DECISION` lists (above) |
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ backup taken before the upgrade instead. Three things change for an existing dep
inside its card; a reopened window comes back where it was. The license step links "Read the
license agreement" without the agreement's id, and no step line shows a release name or the
engine's own output.
- **The Account step leads with OpenBot.** Its header reads "OpenBot by CopilotKit" and its
heading "Self-contained OpenBot". It says this version of OpenBot runs on this computer with a
local version of CopilotKit Intelligence, which needs a free license key, and that your
conversations and files stay on this computer. The button reads "Sign in for a free local license
key", and CopilotKit's logo no longer heads the step.
- **A first install on a Mac starts the first time, and a reinstall sets its back end up again.**
Three things a from-zero install of 1.0.0 hit on a Mac with Docker Desktop:
- The first Start could stop with "Something went wrong" because Docker Desktop refused the
Expand Down
25 changes: 14 additions & 11 deletions desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ mock.module("./Mark", () => ({
}));

const { App } = await import("./App");
const { SELF_CONTAINED } = await import("./CopilotKitAccount");
const { ShuttingDownWindow, isShuttingDownPage } = await import(
"./ShuttingDown"
);
Expand Down Expand Up @@ -716,12 +717,10 @@ test("a signed-out first run signs in to CopilotKit and accepts the agreement as
await installLocal(view);
// The first step. OpenBot needs no account, so it installs behind it, exactly once; nothing
// of Intelligence's runs before the agreement. The browser sign-in starts without a click.
await view.findByRole("heading", { name: "Sign in to CopilotKit" });
await view.findByRole("heading", { name: "Self-contained OpenBot" });
await view.findByText("Finish signing in to CopilotKit in your browser…");
expect(
view.getByText(
"Your conversations and files are stored on this computer. Signing in does not send them to CopilotKit.",
),
view.getByText(/Your conversations and files stay on this computer/),
).toBeTruthy();
expect(stepLabel(view)).toBe("Account");
await waitFor(() =>
Expand Down Expand Up @@ -932,7 +931,7 @@ test("the account step waits for a container engine Install is still setting up,
await installLocal(view);
await view.findByRole(
"button",
{ name: "Sign in to CopilotKit" },
{ name: "Sign in for a free local license key" },
{ timeout: 6000 },
);
expect(asked).toBe(2);
Expand Down Expand Up @@ -1018,7 +1017,7 @@ test("the install behind the account step is said on every part of it, signing i
engineAnswers = true;
await view.findByRole(
"button",
{ name: "Sign in to CopilotKit" },
{ name: "Sign in for a free local license key" },
{ timeout: 5000 },
);
expectBehind(view, 1, "Installing the software OpenBot runs on.");
Expand Down Expand Up @@ -1089,11 +1088,15 @@ function behindNow(view: ReturnType<typeof render>): string | null {
}

function technicalOn(beats: Beat[]) {
return beats.flatMap(({ beat, text }) =>
TECHNICAL.filter((word) => word.test(text)).map(
(word) => `${beat}: ${text.match(word)?.[0]}`,
),
);
return beats.flatMap(({ beat, text }) => {
// The Account step's lede names CopilotKit Intelligence by product decision, as the one
// entry in `SHOWN_BY_DECISION` (tests/no-intelligence-in-user-copy.test.ts) records. Only
// that exact sentence is excused; the word anywhere else on any screen still fails.
const plain = text.replaceAll(SELF_CONTAINED, "");
return TECHNICAL.filter((word) => word.test(plain)).map(
(word) => `${beat}: ${plain.match(word)?.[0]}`,
);
});
}

/** What each part of a first run said, from the first screen to OpenBot opening. */
Expand Down
44 changes: 22 additions & 22 deletions desktop/src/CopilotKitAccount.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,9 @@ test("Cancel stops the CLI's sign-in and goes back to the start of the step", as
{ command: "cancel_local_eval", args: { attempt: signInAttempt() } },
]);
expect(
await screen.findByRole("button", { name: "Sign in to CopilotKit" }),
await screen.findByRole("button", {
name: "Sign in for a free local license key",
}),
).toBeTruthy();
expect(screen.queryByText(PAGE.userCode)).toBeNull();
// The stopped sign-in is not reported as a failure.
Expand All @@ -353,35 +355,33 @@ test("Cancel stops the CLI's sign-in and goes back to the start of the step", as
).toBeNull();
});

/** CopilotKit's logotype, light, with the reversed wordmark for a dark window. */
function expectCopilotKitLogo(screen: ReturnType<typeof render>) {
const logo = screen.getByRole("img", { name: "CopilotKit" });
expect(logo.getAttribute("src")).toBe("/copilotkit-logo.svg");
expect(
logo
.closest("picture")
?.querySelector('source[media="(prefers-color-scheme: dark)"]')
?.getAttribute("srcset"),
).toBe("/copilotkit-logo-dark.svg");
}

/*
* The step signs in to a CopilotKit account, so it shows CopilotKit's logotype, signed out and
* while the sign-in waits. It showed OpenBot's mark alone.
* The step is OpenBot's, so it leads with OpenBot by CopilotKit, not CopilotKit's logotype, while
* the sign-in waits and once it is stopped, and says what the free local license key is for.
*/
test("the CopilotKit sign-in shows CopilotKit's logotype, while it waits and once stopped", async () => {
test("the sign-in leads with OpenBot, while it waits and once stopped", async () => {
waitingSignIn();
const screen = await startSigningIn();
await screen.findByText(PAGE.userCode);
expectCopilotKitLogo(screen);
expect(
screen.getByRole("heading", { name: "Self-contained OpenBot" }),
).toBeTruthy();
expect(screen.getByText(/requires a free license key/)).toBeTruthy();
expect(
screen.getByText(/does NOT send traffic data to CopilotKit/),
).toBeTruthy();
expect(screen.getByText("by CopilotKit")).toBeTruthy();
expect(screen.queryByRole("img", { name: "CopilotKit" })).toBeNull();

await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
await screen.findByRole("button", { name: "Sign in to CopilotKit" });
expectCopilotKitLogo(screen);
await screen.findByRole("button", {
name: "Sign in for a free local license key",
});
expect(screen.queryByRole("img", { name: "CopilotKit" })).toBeNull();
});

test("after Accept, the step says it is setting up your apps, not to sign in", async () => {
// Seen on Windows: Accept was followed for two seconds by "Sign in to CopilotKit" and its
// Seen on Windows: Accept was followed for two seconds by the sign-in heading and its
// sign-in lede, over "Connecting your apps…", for a person already signed in and agreed.
let connected!: () => void;
invokeHandler = alreadySignedIn(async ({ command }) => {
Expand Down Expand Up @@ -415,9 +415,9 @@ test("after Accept, the step says it is setting up your apps, not to sign in", a
).toBeTruthy();
expect(screen.getByText("Connecting your apps…")).toBeTruthy();
expect(
screen.queryByRole("heading", { name: "Sign in to CopilotKit" }),
screen.queryByRole("heading", { name: "Self-contained OpenBot" }),
).toBeNull();
expect(screen.queryByText(/Sign in, then accept/)).toBeNull();
expect(screen.queryByText(/requires a free license key/)).toBeNull();
connected();
await waitFor(() => expect(done).toBe("developer-edition-2026-10-01-web"));
});
40 changes: 28 additions & 12 deletions desktop/src/CopilotKitAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
useRef,
useState,
} from "react";
import { CopilotKitLogo } from "./CopilotKitLogo";
import { CopilotKitSignInWait } from "./CopilotKitSignInWait";
import { ExternalLink } from "./ExternalLink";
import {
Expand Down Expand Up @@ -46,6 +45,22 @@ type Seen =
| { at: "unsupported-engine"; said: string; switchEngine: boolean };

const COULD_NOT_CHECK = "Couldn’t check your CopilotKit account. Try again.";

/**
* The signed-out step's footnote, in Atai's words (2026-11-10). One string so a change of wording
* is one line. "Traffic data" is being checked with Atai: setup and usage telemetry go to
* CopilotKit by default (desktop_telemetry.rs, server/src/telemetry/product.ts).
*/
const WHAT_SIGNING_IN_SENDS =
"Your conversations and files stay on this computer. Signing in gets your free local license key and does NOT send traffic data to CopilotKit.";
/**
* The signed-out step's lede, in Atai's words. It names CopilotKit Intelligence on this step alone:
* it is what the license key is for. Exported so the setup walk's plain-words check
* (`App.test.tsx`) excuses exactly this sentence, as `SHOWN_BY_DECISION` does in
* `tests/no-intelligence-in-user-copy.test.ts`.
*/
export const SELF_CONTAINED =
"This version of OpenBot runs self-contained on this computer. It uses a local version of CopilotKit Intelligence, which requires a free license key.";
const SIGN_IN_DID_NOT_FINISH = "Sign-in didn’t finish. Try again.";
const SETTING_UP_ENGINE =
"You're signed in. OpenBot is setting up the software it runs on. The license agreement comes next.";
Expand Down Expand Up @@ -293,10 +308,10 @@ export function CopilotKitAccount({
const asking = goAhead !== null;
return (
<div className="sheet">
{/* OpenBot is what is being set up, so its mark heads this step as it heads every other.
The account is CopilotKit's, so its logotype leads the step itself. */}
<Sections at="Account" />
<CopilotKitLogo />
{/* OpenBot is what is being set up, so its mark heads this step as it heads every other,
and here it says "by CopilotKit" beside the name. CopilotKit's logotype here made the step
read as signing up for a CopilotKit product. */}
<Sections at="Account" byCopilotKit />
<h1>
{agreement
? "Accept the license agreement"
Expand All @@ -310,7 +325,7 @@ export function CopilotKitAccount({
: "Change this computer's setup"
: waitingForEngine
? "Setting up this computer"
: "Sign in to CopilotKit"}
: "Self-contained OpenBot"}
</h1>
<p className="lede">
{agreement
Expand All @@ -323,7 +338,7 @@ export function CopilotKitAccount({
? refused.said
: waitingForEngine
? SETTING_UP_ENGINE
: "OpenBot runs on this computer with your CopilotKit account. Sign in, then accept the license agreement."}
: SELF_CONTAINED}
</p>
{refused?.switchEngine && (
<ol className="blocker-steps">
Expand All @@ -348,10 +363,11 @@ export function CopilotKitAccount({
!appsFailed &&
!refused &&
!waitingForEngine && (
<p className="footnote">
Your conversations and files are stored on this computer. Signing in
does not send them to CopilotKit.
</p>
// What signing in sends, in Atai's words. Never "only gets the license key": once
// signed in, this step also sets up apps. Nothing broader, such as "no data leaves this
// computer": setup and usage telemetry, the AI provider and connected apps all reach the
// network.
<p className="footnote">{WHAT_SIGNING_IN_SENDS}</p>
)}

{/* The agreement waits on the engine. While it does, this says what Install is doing to bring
Expand Down Expand Up @@ -405,7 +421,7 @@ export function CopilotKitAccount({
<div className="row">
{seen.at === "signed-out" && (
<button type="button" onClick={signIn}>
Sign in to CopilotKit
Sign in for a free local license key
</button>
)}
{agreement && (
Expand Down
10 changes: 9 additions & 1 deletion desktop/src/Sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@ const SECTIONS: Section[] = ["Account", "Install", "AI", "Start"];
* step that signs in to a CopilotKit account. The name is decoration here: the window's title
* already says it, so it is hidden from assistive technology rather than read on every screen.
*/
export function Sections({ at }: { at: Section }) {
export function Sections({
at,
byCopilotKit = false,
}: {
at: Section;
/** "by CopilotKit" beside OpenBot's name, on the step that asks for a CopilotKit sign-in. */
byCopilotKit?: boolean;
}) {
const names = SECTIONS;
const current = names.indexOf(at);
return (
<div className="setup-header">
<div className="lockup" aria-hidden="true">
<div className="orb" />
<span>OpenBot</span>
{byCopilotKit && <span className="lockup-by">by CopilotKit</span>}
</div>
<ol className="steps-of sections" aria-label="Setup">
{names.map((name, index) => (
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/setupLayout.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ describe.skipIf(!asked)("a setup step's foot", () => {
.getByRole("button", { name: "Install", exact: true })
.click();
await account
.getByRole("heading", { name: "Sign in to CopilotKit" })
.getByRole("heading", { name: "Self-contained OpenBot" })
.waitFor();
await account.waitForTimeout(600);
await background(account);
Expand Down
7 changes: 7 additions & 0 deletions desktop/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1460,6 +1460,13 @@ a:hover {
letter-spacing: -0.01em;
}

/* "by CopilotKit" beside the name: the same size, quieter, so OpenBot still leads. */
.setup-header .lockup span.lockup-by {
font-weight: 400;
color: var(--text-secondary);
margin-left: -2px;
}

/* The developer half of a failure: present, and never the headline. */
.detail-of {
border: 0;
Expand Down
18 changes: 16 additions & 2 deletions tests/no-intelligence-in-user-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ const NOT_SHOWN: { file: string; text: string; why: string }[] = [
},
];

/**
* Shown text that names CopilotKit Intelligence on purpose. One entry, by exact text: OpenBot
* Desktop's Account step, where the name says what the free license key is for and matches the
* CopilotKit sign-in page that opens next. Everywhere else it is still the back end (see
* .claude/skills/openbot-copy). A new entry is a product decision, not a way past this test.
*/
const SHOWN_BY_DECISION: { file: string; text: string; why: string }[] = [
{
file: "desktop/src/CopilotKitAccount.tsx",
text: "This version of OpenBot runs self-contained on this computer. It uses a local version of CopilotKit Intelligence, which requires a free license key.",
why: "the Account step names what the sign-in gets a license key for",
},
];

/** Calls whose arguments are written to a log, never shown. */
const LOGGING =
/^(console\.\w+|log|logger\.\w+|this\.log|options\.log|note|once|sayUnavailable)$/;
Expand Down Expand Up @@ -183,7 +197,7 @@ function shown(): string[] {
}

const isNotShown = (hit: string) =>
NOT_SHOWN.some(
[...NOT_SHOWN, ...SHOWN_BY_DECISION].some(
(entry) => hit.startsWith(`${entry.file}:`) && hit.includes(entry.text),
);

Expand All @@ -193,7 +207,7 @@ test("no text a person reads in the desktop app, the app, the server or the Reco

test("every exception for text that is never shown still matches", () => {
const found = shown();
const stale = NOT_SHOWN.filter(
const stale = [...NOT_SHOWN, ...SHOWN_BY_DECISION].filter(
(entry) =>
!found.some(
(hit) => hit.startsWith(`${entry.file}:`) && hit.includes(entry.text),
Expand Down
Loading