Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a85a8c8
Draw both browsers as a real browser window
ataibarkai Nov 8, 2026
61d33a5
Name the site a Bot browsed in the conversation
ataibarkai Nov 8, 2026
711d051
Drop Explore Bots from the personal edition's home
ataibarkai Nov 8, 2026
20d12fa
Make General settings a list of answers, not a form
ataibarkai Nov 8, 2026
3642756
Show only the notification channels that are set up
ataibarkai Nov 8, 2026
ddfeb16
Say Bot and skill where the shipped skills said coworker and command
ataibarkai Nov 8, 2026
5286884
Say "nothing yet" in one line under the Bot's screen
ataibarkai Nov 8, 2026
34ea44b
Float the sidebar as frosted glass over the orb's colours
ataibarkai Nov 8, 2026
58bd8d5
Put Take control on the Bot's screen, in a frosted status
ataibarkai Nov 8, 2026
909c716
Set OpenBot in Atkinson Hyperlegible Next, the face open.bot uses
ataibarkai Nov 8, 2026
ed40994
Say "Working" on a conversation while its Bot works
ataibarkai Nov 8, 2026
8702cf9
Give the Bot's browser a readable panel with a real address bar
ataibarkai Nov 8, 2026
8d2c43a
Put OpenBot's orb beside its name in the sidebar
ataibarkai Nov 8, 2026
b122590
Say on Home which Bot is working, with the way to it
ataibarkai Nov 8, 2026
b7eda88
Keep the browser's toolbar to one line: Record asks for the task in a…
ataibarkai Nov 8, 2026
ccb23a8
Show the Browser's narration status bar only while a recording runs
ataibarkai Nov 8, 2026
36251df
Say "open a skill" on Learning only when there are skills to open
ataibarkai Nov 8, 2026
9dc8eca
Keep the Bot's browser beside a conversation a glimpse
ataibarkai Nov 8, 2026
2e5bb9f
Fit browser windows to their page, so nothing blank sits under it
ataibarkai Nov 8, 2026
2a204eb
Make Teach a Bot one clear page: what it is, the browser, and your re…
ataibarkai Nov 8, 2026
4f928b5
Open a new conversation with the Bot menu, not a Bot already picked
ataibarkai Nov 8, 2026
6010834
Keep OpenBot's name in the sidebar still on hover
ataibarkai Nov 8, 2026
8aea079
Say how to turn update channels on, where none is set up yet
ataibarkai Nov 8, 2026
371cdcd
Show Teach a Bot laid out while its browser opens
ataibarkai Nov 8, 2026
026f79e
Explain narration with an (i) beside its switch in Record
ataibarkai Nov 8, 2026
9302da2
Note the Teach a Bot, browser and look changes in the changelog
ataibarkai Nov 8, 2026
b7b5293
Match the Recorder notice's background to the app's lighter page
ataibarkai Nov 8, 2026
7fe24d9
Merge branch 'main' of https://github.com/CopilotKit/OpenBot into cla…
ataibarkai Nov 8, 2026
b59cff2
Merge main: place this branch's changelog lines in 0.3.4's sections
ataibarkai Nov 8, 2026
1a46da7
Notifications: text messages, its question only above switches, no se…
ataibarkai Nov 8, 2026
1509d1c
Teach a Bot: the browser's size no longer depends on the recordings, …
ataibarkai Nov 8, 2026
1a32338
Keep the blur to the sidebar: solid surfaces over live content, in th…
ataibarkai Nov 8, 2026
47104a0
Test the browser panel's status and address, the start page, Working,…
ataibarkai Nov 8, 2026
bd235eb
Settings: icons on the Appearance rows, as on About you
ataibarkai Nov 8, 2026
8e5b718
Say in the changelog what Notifications says in OpenBot Desktop
ataibarkai Nov 8, 2026
8b64e46
Start + with the Bot picked again, as 0.3.3 does
davidmckayv Nov 8, 2026
008fc55
Merge remote-tracking branch 'origin/main' into rv1306-pr
davidmckayv Nov 8, 2026
02d7b5f
Move this branch's changelog entries under Unreleased, out of 0.3.4
davidmckayv Nov 8, 2026
b59deb8
Show four lead frameworks in a quiet strip above the New Bot footer, …
claude Nov 8, 2026
18fa2a8
Give New Bot's name and instructions the same quiet labels, and draw …
claude Nov 8, 2026
9daa4bc
Name Claude Agent SDK, LangChain, Google ADK and AWS Strands on New B…
claude Nov 8, 2026
edec3eb
Show custom agents below New Bot's form, in place of the strip above …
claude Nov 9, 2026
eed8b37
Merge remote-tracking branch 'origin/main' into claude/new-bot-first-…
claude Nov 9, 2026
092df70
Draw New Bot's custom agents as one-line rows under a small label, an…
claude Nov 9, 2026
5020fd6
Merge branch 'main' into claude/new-bot-first-screen
davidmckayv Nov 9, 2026
4d58508
Call LangChain LangChain, with LangChain's own symbol, where it said …
claude Nov 9, 2026
f61a51c
Format the three files the LangChain rename left over-wrapped
claude Nov 9, 2026
e5922fd
Merge remote-tracking branch 'origin/claude/new-bot-first-screen' int…
claude Nov 9, 2026
f9a1cc6
Merge remote-tracking branch 'origin/main' into land-1321
davidmckayv Nov 9, 2026
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

### Smaller changes

- LangChain is listed as **LangChain**, with LangChain's own symbol, where it said "LangChain
(LangGraph)". This covers New Bot, a Bot's "Powered by" subtitle, its setup row and OpenBot
Desktop's plan note. A subtitle saved under the old name still follows a change of harness. New
Bot's custom agents are one line each, and the four named frameworks show without scrolling.
- **New Bot** shows custom agents on its first screen. Below the simple form, **Connect a custom
agent** lists every framework with its logo, Claude Agent SDK, LangChain, Google ADK and AWS
Strands first, and on a desktop its heading and first row show without scrolling. Pressing one
Expand Down
107 changes: 42 additions & 65 deletions app/src/components/agents/new-bot-sheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,6 @@ function NewBotFlow({ onCreated }: { onCreated: (agentId: string) => void }) {
<CustomAgents
disabled={busy}
onChoose={(chosen) => connectCustom(chosen)}
reduceMotion={reduceMotion}
/>
) : (
<p className="mt-3 text-sm text-muted-foreground">
Expand Down Expand Up @@ -655,25 +654,14 @@ function CreatedView({ name }: { name: string }) {

type Visibility = AgentFormValues["visibility"];

/** The two ways a Bot can be seen, in the words the old wizard used for them. */
/** The two ways a Bot can be seen: a word and an icon each, which say enough between them. */
const VISIBILITY_OPTIONS: Array<{
value: Visibility;
title: string;
description: string;
icon: typeof IconLock;
}> = [
{
value: "private",
title: "Private",
description: "Only you can see it and talk to it.",
icon: IconLock,
},
{
value: "public",
title: "Public",
description: "Everyone in your company can find and use it.",
icon: IconUsers,
},
{ value: "private", title: "Private", icon: IconLock },
{ value: "public", title: "Public", icon: IconUsers },
];

/**
Expand Down Expand Up @@ -776,14 +764,7 @@ function AboutStep({
type="button"
>
<Icon className="size-5 shrink-0 text-muted-foreground" />
<span className="min-w-0">
<span className="block font-medium text-sm">
{option.title}
</span>
<span className="block text-pretty text-muted-foreground text-xs">
{option.description}
</span>
</span>
<span className="font-medium text-sm">{option.title}</span>
</button>
);
})}
Expand Down Expand Up @@ -889,9 +870,9 @@ type IsolationChoice = {
* needs nothing and costs nothing more; Isolated is the exception somebody opts into. What it means
* is in the tooltip beside it, and for a public Bot that includes who else ends up on that computer.
*
* NEVER HIDDEN. Where it cannot be ticked it is drawn disabled with the reason in place of its
* summary, so somebody who heard about Isolated Bots finds out here why they cannot have one rather
* than wondering where it went.
* NEVER HIDDEN. Where it cannot be ticked it is drawn disabled with the reason below it, so
* somebody who heard about Isolated Bots finds out here why they cannot have one rather than
* wondering where it went.
*
* A label for the word only, not the whole tile, so the tooltip's button beside it does not tick the
* box when pressed.
Expand All @@ -915,7 +896,7 @@ function IsolatedOption({
</FieldLegend>
<div className="flex items-start gap-3">
<Checkbox
aria-describedby="new-bot-isolated-summary"
aria-describedby={reason ? "new-bot-isolated-reason" : undefined}
checked={checked && reason === null}
className="mt-0.5"
disabled={reason !== null}
Expand Down Expand Up @@ -952,12 +933,14 @@ function IsolatedOption({
</TooltipContent>
</Tooltip>
</div>
<p
className="text-pretty text-muted-foreground text-xs"
id="new-bot-isolated-summary"
>
{reason ?? "A computer of its own, instead of sharing yours."}
</p>
{reason && (
<p
className="text-pretty text-muted-foreground text-xs"
id="new-bot-isolated-reason"
>
{reason}
</p>
)}
</div>
</div>
</FieldSet>
Expand Down Expand Up @@ -989,8 +972,8 @@ function RuntimeStep({ onChoose }: { onChoose: (runtime: Runtime) => void }) {
useEffect(() => heading.current?.focus(), []);

return (
<div className="flex flex-col gap-2">
<h2 className={cn("mb-2", RUNTIME_HEADING)} ref={heading} tabIndex={-1}>
<div className="flex flex-col">
<h2 className={cn("mb-3", RUNTIME_HEADING)} ref={heading} tabIndex={-1}>
Connect a custom agent
</h2>
<FrameworkRows onChoose={onChoose} />
Expand All @@ -1002,7 +985,14 @@ function RuntimeStep({ onChoose }: { onChoose: (runtime: Runtime) => void }) {
const RUNTIME_HEADING =
"break-words font-semibold text-lg tracking-tight outline-none";

/** Every framework the sheet offers, one row each; pressing one picks it. */
/**
* A framework row: no fill until hovered, so the list reads as a list and not as a stack of tiles.
* Pressing one acts at once, so there is no chosen state to draw.
*/
const FRAMEWORK_ROW_CLASSES =
"-mx-3 flex items-center gap-3 rounded-lg px-3 py-2.5 text-left transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset";

/** Every framework the sheet offers, one line each, name then summary; pressing one picks it. */
function FrameworkRows({
disabled,
onChoose,
Expand All @@ -1012,16 +1002,16 @@ function FrameworkRows({
}) {
return CONNECTION_OPTIONS.map((harness) => (
<button
className={OPTION_CLASSES}
className={FRAMEWORK_ROW_CLASSES}
disabled={disabled}
key={harness.id}
onClick={() => onChoose({ kind: "managed", harness: harness.id })}
type="button"
>
<HarnessMark id={harness.mark} name={harness.name} />
<span className="min-w-0">
<span className="block font-medium text-sm">{harness.name}</span>
<span className="block text-pretty text-muted-foreground text-xs">
<span className="min-w-0 truncate text-sm">
<span className="font-medium">{harness.name}</span>
<span className="ml-2 text-muted-foreground">
{summaryFor(harness)}
</span>
</span>
Expand All @@ -1045,44 +1035,31 @@ function showCustom(reduceMotion: boolean | null) {
}

/**
* The first screen's second way to make a Bot, below the simple form: RuntimeStep's heading and rows,
* without taking focus from Name. A wide gap sets it apart from the form, and on a desktop sheet its
* heading and first row show above the footer before anything is scrolled; the chevron beside the
* heading brings the list up. Each row acts on its own press, so Create Bot stays the form's.
* The first screen's second way to make a Bot, below the simple form: RuntimeStep's rows under a
* section name like the form's own, without taking focus from Name. A wide gap sets it apart from the
* form, and on a desktop sheet its name and first rows show above the footer before anything is
* scrolled. Each row acts on its own press, so Create Bot stays the form's.
*/
function CustomAgents({
disabled,
onChoose,
reduceMotion,
}: {
disabled: boolean;
onChoose: (runtime: Runtime) => void;
reduceMotion: boolean | null;
}) {
return (
<section
aria-labelledby={CUSTOM_HEADING_ID}
className="mt-14 flex scroll-mt-6 flex-col gap-2"
className="mt-12 flex scroll-mt-6 flex-col"
id={CUSTOM_ID}
>
<div className="flex items-center gap-1">
<h2 className={RUNTIME_HEADING} id={CUSTOM_HEADING_ID} tabIndex={-1}>
Connect a custom agent
</h2>
<Button
aria-label="Show custom agents"
className="text-muted-foreground"
onClick={() => showCustom(reduceMotion)}
size="icon-xs"
type="button"
variant="ghost"
>
<IconChevronDown className="size-4" />
</Button>
</div>
<p className="mb-2 text-pretty text-muted-foreground text-sm">
Already run an agent? Pick its framework, then add its address.
</p>
<h2
className={cn("mb-2 font-medium outline-none", SECTION_LABEL)}
id={CUSTOM_HEADING_ID}
tabIndex={-1}
>
Connect a custom agent
</h2>
<FrameworkRows disabled={disabled} onChoose={onChoose} />
</section>
);
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/agents/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export type AgentProfile = {
*/
instructions?: string | null;
/**
* What runs it, by the name a person reads ("LangChain (LangGraph)", "OpenBot"). Absent when
* What runs it, by the name a person reads ("LangChain", "OpenBot"). Absent when
* OpenBot doesn't know, such as an agent connected by address with no framework named.
*/
framework?: string;
Expand Down
6 changes: 3 additions & 3 deletions app/tests/bot-settings-rows.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const ON_HARNESS: AgentProfile = {
endpoint: "http://agent-harness:4206/ag-ui",
builtIn: false,
keyFromOpenBot: true,
framework: "LangChain (LangGraph)",
framework: "LangChain",
};

function withEdition(edition: "team" | "personal") {
Expand All @@ -224,7 +224,7 @@ test("the personal edition's Setup row names the Bot's framework, not the harnes
serving("admin");
withEdition("personal");
const view = draw(ON_HARNESS);
expect(await view.findByText("Runs on LangChain (LangGraph)")).toBeTruthy();
expect(await view.findByText("Runs on LangChain")).toBeTruthy();
expect(view.queryByText("agent-harness:4206")).toBeNull();
});

Expand All @@ -235,5 +235,5 @@ test("a team deployment's Setup row still shows the address", async () => {
expect(await view.findByText("Setup")).toBeTruthy();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(view.getByText("agent-harness:4206")).toBeTruthy();
expect(view.queryByText("Runs on LangChain (LangGraph)")).toBeNull();
expect(view.queryByText("Runs on LangChain")).toBeNull();
});
6 changes: 2 additions & 4 deletions app/tests/bot-summaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,9 @@ test("the personal edition names what runs a Bot instead of its address", () =>
const harness = {
builtIn: false,
endpoint: "http://agent-harness:4206/ag-ui",
framework: "LangChain (LangGraph)",
framework: "LangChain",
};
expect(setupSummary(harness, "personal")).toBe(
"Runs on LangChain (LangGraph)",
);
expect(setupSummary(harness, "personal")).toBe("Runs on LangChain");
expect(setupSummary(harness, "team")).toBe("agent-harness:4206");
expect(setupSummary(harness)).toBe("agent-harness:4206");
// An agent connected by address with no framework named: the address is all there is.
Expand Down
19 changes: 9 additions & 10 deletions app/tests/new-bot-sheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,14 @@ test("unavailable hosting still permits the custom path", async () => {
expect(view.queryByRole("button", { name: "Continue" })).toBeNull();
await user.click(framework);
expect(await view.findByLabelText("Agent address")).toBeTruthy();
expect(view.getByText("LangChain (LangGraph)")).toBeTruthy();
expect(view.getByText("LangChain")).toBeTruthy();
await user.type(
view.getByLabelText("Agent address"),
"https://agent.example.test/ag-ui",
);
await user.click(view.getByRole("button", { name: "Create Bot" }));
await waitFor(() => expect(creates).toHaveLength(1));
expect(creates[0]?.roleDescription).toBe(
"Connected from LangChain (LangGraph).",
);
expect(creates[0]?.roleDescription).toBe("Connected from LangChain.");
});

test("Create fills in what was left blank, then hands over", async () => {
Expand Down Expand Up @@ -310,7 +308,7 @@ test("checks the connection then creates with its saved name and key", async ()
endpoint: "https://agent.example.com/ag-ui",
auth: { header: "Authorization", value: "Bearer secret" },
// The framework picked from the list, by the name the list showed.
title: "Powered by LangChain (LangGraph)",
title: "Powered by LangChain",
// And by its id, so the Bot is told what runs it whatever its subtitle becomes.
harness: "langgraph",
});
Expand Down Expand Up @@ -476,7 +474,7 @@ test("a framework on the first screen validates the name, then goes straight to
customAgents(view).getByRole("button", { name: /LangChain/ }),
);
expect(await view.findByLabelText("Agent address")).toBeTruthy();
expect(view.getByText("LangChain (LangGraph)")).toBeTruthy();
expect(view.getByText("LangChain")).toBeTruthy();
await user.click(view.getByRole("button", { name: "Back" }));
// Back to the first screen, not the list: the name is there, and so are the custom agents.
expect(((await view.findByLabelText("Name")) as HTMLInputElement).value).toBe(
Expand Down Expand Up @@ -511,7 +509,7 @@ test("the first screen's custom agents lead with Claude Agent SDK, LangChain, Go
.filter(Boolean);
expect(names.slice(0, 4)).toEqual([
"Claude Agent SDK",
"LangChain (LangGraph)",
"LangChain",
"Google ADK",
"AWS Strands",
]);
Expand Down Expand Up @@ -611,10 +609,11 @@ test("team: the sheet offers only the apps the person can give, ticked", async (
expect(requested).not.toContain("/api/plugins");
});

test("LangGraph is offered under LangChain's name, the one people know it by", async () => {
test("the LangGraph harness is offered as LangChain, the name people know it by", async () => {
const { view } = await customFlow();
const framework = customAgents(view).getByRole("button", {
name: /LangGraph/,
name: /^LangChain/,
});
expect(framework.textContent).toContain("LangChain (LangGraph)");
// LangChain's own name and a description that doesn't talk about graphs.
expect(framework.textContent).not.toMatch(/Graph/);
});
6 changes: 2 additions & 4 deletions desktop/src/ProviderPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -839,10 +839,8 @@ export function ProviderPicker({
*/}
<p className="footnote">
Your Bot will use{" "}
{row.id === "anthropic"
? "Claude Agent SDK"
: "LangChain (LangGraph)"}
. It’s the one that can use this plan.
{row.id === "anthropic" ? "Claude Agent SDK" : "LangChain"}.
It’s the one that can use this plan.
</p>
</>
) : signInUrl ? (
Expand Down
2 changes: 1 addition & 1 deletion docs/personal-edition.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The server's container is given the supervisor by name in `docker-compose.yml` u
A new installation starts with one Bot, the Personal Assistant. When a Bot was picked during setup,
the Personal Assistant runs on what was picked: it takes the `picked-harness` row's id and
connection, and the Personal Assistant's instructions, skills and conversation, and its subtitle
says what runs it ("Everyday life. Powered by LangChain (LangGraph)."). With nothing picked, as
says what runs it ("Everyday life. Powered by LangChain."). With nothing picked, as
`start.sh` leaves it, `PICKED_HARNESS_URL` is empty, the `picked-harness` row in
[`examples/personal/agents.yaml`](../examples/personal/agents.yaml) is dropped, and the Personal
Assistant runs inside the server ("Everyday life. Powered by OpenBot."). Either way it is an
Expand Down
2 changes: 1 addition & 1 deletion server/src/agents/framework.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Which agent framework a Bot runs on, by what runs it rather than by what its subtitle says.
*
* The subtitle ("Everyday life. Powered by LangChain (LangGraph).") is the person's to write, and a
* The subtitle ("Everyday life. Powered by LangChain.") is the person's to write, and a
* Bot told only that could name its framework only while the subtitle happened to. So the answer is
* read from the Bot itself:
*
Expand Down
2 changes: 1 addition & 1 deletion server/src/agents/profile-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type AgentProfile = {
*/
instructions?: string | null;
/**
* What runs it, by the name a person reads ("LangChain (LangGraph)", "OpenBot"), or absent when
* What runs it, by the name a person reads ("LangChain", "OpenBot"), or absent when
* OpenBot doesn't know, such as an agent connected by address with no framework named. Read from
* what runs it, never its subtitle (`frameworkOf`).
*/
Expand Down
4 changes: 2 additions & 2 deletions server/src/tenant-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,7 @@ const PICKED_HARNESS = "picked-harness";
* the current name.
*/
const EARLIER_HARNESS_NAMES: Readonly<Record<string, readonly string[]>> = {
langgraph: ["LangGraph", "LangChain"],
langgraph: ["LangGraph", "LangChain (LangGraph)"],
};

function catalogueNamesOf(harness: Harness): string[] {
Expand All @@ -859,7 +859,7 @@ function catalogueNamesOf(harness: Harness): string[] {

/**
* "Powered by <framework>" for any framework the installer can run, longest name first so that
* "LangChain (LangGraph)" is read whole rather than as "LangChain" followed by text.
* an earlier "LangChain (LangGraph)" is read whole rather than as "LangChain" followed by text.
*/
const POWERED_BY = new RegExp(
`Powered by (?:${HARNESSES.filter(({ image }) => image !== null)
Expand Down
4 changes: 1 addition & 3 deletions server/tests/agent-profile-store.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,9 +1023,7 @@ describe("agent profile store integration", () => {
).json()) as { agent: Record<string, unknown> }
).agent;

expect((await detail(picked.agentId)).framework).toBe(
"LangChain (LangGraph)",
);
expect((await detail(picked.agentId)).framework).toBe("LangChain");
// An agent connected by address with no framework named: OpenBot doesn't know, so it says none.
expect(await detail(unnamed.agentId)).not.toHaveProperty("framework");
} finally {
Expand Down
2 changes: 1 addition & 1 deletion server/tests/bot-knows-its-framework.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ describe("a Bot at an address", () => {
test.each([
[
"ghcr.io/copilotkit/openbot-agent-langgraph-agui@sha256:0123",
"LangChain (LangGraph)",
"LangChain",
{ provider: "openai", defaultModel: "gpt-5.5" },
"GPT-6.1 Terra",
],
Expand Down
Loading
Loading