Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1d58cdd
fix(builderlab): move model configuration into identity
tellaho Oct 29, 2026
aa2419b
fix(builderlab): enable default setup and explain unavailable harnesses
tellaho Oct 29, 2026
3418fb8
fix(builderlab): match agent model control layout
tellaho Oct 29, 2026
4913121
fix(builderlab): group model choices by catalog family
tellaho Oct 29, 2026
8570cf4
feat(agents): add shared model-family brand marks
tellaho Oct 29, 2026
85a5a60
fix(builderlab): save identity changes from the shared footer
tellaho Oct 29, 2026
4a572fb
fix(builderlab): remove redundant instructions help text
tellaho Oct 29, 2026
3c18028
revert(builderlab): defer the availability prerequisite
tellaho Oct 29, 2026
d34b3f9
fix(builderlab): explain the disabled harness selector
tellaho Oct 29, 2026
4dac1ba
fix(ui): mute focusable disabled compact selectors
tellaho Oct 29, 2026
665af85
fix(builderlab): clarify hosted identity and runtime settings
tellaho Oct 29, 2026
90a1a97
Merge remote-tracking branch 'origin/main' into tho/builderlab-identi…
tellaho Oct 29, 2026
55296f0
fix(agents): explain unavailable hosted harness choices
tellaho Oct 29, 2026
11e58c1
fix(builderlab): simplify runtime and thinking descriptions
tellaho Oct 29, 2026
9a8f4de
fix(agents): preserve model family inference for catalog names
tellaho Oct 29, 2026
94195a7
test(builderlab): preserve pane dismissal after harness help
tellaho Oct 29, 2026
e0498ea
fix(builderlab): describe loading and editing agent settings
tellaho Oct 29, 2026
952c5de
test(reminders): preserve mainline picker focus synchronization
tellaho Oct 29, 2026
712eb43
Merge origin/main into tho/builderlab-identity-config
tellaho Oct 29, 2026
be68c0d
fix(design-system): preserve compact picker loading colors
tellaho Oct 29, 2026
435a776
fix(builderlab): keep keyboard-opened harness help visible
tellaho Oct 29, 2026
df6470a
fix(builderlab): use consistent thinking labels in creation
tellaho Oct 29, 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
30 changes: 30 additions & 0 deletions NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,36 @@ cropped the border, padded it to square, resized to 64×64 and quantised it to
16 colours. The MIT license terms above also apply to this artwork, with the
Nous Research copyright notice.

## Model-family brand marks

The additional artwork in `src/shared/design-system/icons/HarnessLogos.tsx`
identifies model families in shared pickers, including Builderlab and Goose.
All marks remain the property of their respective owners; their inclusion
identifies supported models and does not imply endorsement.

DeepSeek: [DeepSeek-icon.svg](https://commons.wikimedia.org/wiki/File:DeepSeek-icon.svg),
file revision June 30, 2025. Copyright © DeepSeek, MIT (Expat) license.
The MIT terms reproduced above apply with this copyright notice. The original
blue fill and path geometry are preserved; this app adds its shared SVG wrapper.

Grok: [Grok-feb-2025-logo.svg](https://commons.wikimedia.org/wiki/File:Grok-feb-2025-logo.svg),
by X.AI Corp., classified by Wikimedia Commons as public domain because it
consists of simple geometric shapes or text. This app retains the two mark
paths, omits the wordmark, crops the viewBox to square, and preserves the
original `currentColor` fill. Grok is a trademark of xAI.

Z.ai / GLM: [Z.ai (company logo).svg](https://commons.wikimedia.org/wiki/File:Z.ai_(company_logo).svg),
by Z.ai, sourced from its [official favicon](https://z-cdn.chatglm.cn/z-ai/static/logo.svg).
Wikimedia Commons classifies the artwork as public domain because it consists
of simple geometric shapes or text. This app preserves the favicon geometry,
colors and border, removes unused styles and groups, and adds its shared SVG
wrapper. Z.ai and GLM are marks of their respective owner.

Kimi: the official [Moonshot Kimi favicon](https://www.kimi.com/favicon-light.ico),
retrieved October 28, 2026, converted from its 48×48 ICO frame to the colocated
`icons/kimi.png` without resizing or changing colors. No open-source license is
asserted for this official brand asset. Kimi is a trademark of Moonshot AI.

## Plugin catalog artwork

The generated plugin PNG artwork in `src/bundled/*/icon.png` was created by
Expand Down
178 changes: 178 additions & 0 deletions src/bundled/agents/AgentSettingsHost.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// @vitest-environment jsdom
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, expect, it, vi } from "vitest";
import type {
Agent,
Agents,
RegisteredAgentType,
} from "../../features/agents/service";
import { RobotIcon } from "../../shared/design-system/icons";
import { useAgentSettingsHost } from "./AgentSettingsHost";

afterEach(cleanup);

function fixture({ hosted = true, anotherLocal = true, place = "here" } = {}) {
const agent = {
key: "a".repeat(64),
pubkey: "a".repeat(64),
name: "Ada",
place,
type: "local/echo",
config: {},
} as Agent;
const local = {
key: agent.type,
pluginId: "local",
revision: "1",
id: "echo",
title: "Echo",
defaults: () => ({ config: {} }),
} satisfies RegisteredAgentType;
const remote = {
...local,
key: "hosted/builderlab",
pluginId: "hosted",
id: "builderlab",
title: "Builderlab",
icon: RobotIcon,
remote: {
provision: vi.fn(),
activate: vi.fn(),
profile: vi.fn(),
remove: vi.fn(),
},
} satisfies RegisteredAgentType;
const next = { ...local, key: "local/next", id: "next", title: "Next" };
const types = [
local,
...(hosted ? [remote] : []),
...(anotherLocal ? [next] : []),
];
const previewRetype = vi.fn(() => ({
config: {},
moved: [],
dropped: [],
settings: [],
}));
const retype = vi.fn();
const agents = { previewRetype, retype } as unknown as Agents;
function Host() {
const host = useAgentSettingsHost({ agents, agent, type: local, types });
return (
<>
{host.settingsHost.typeControl?.(false)}
<output aria-label="Selected harness">{host.agent.type}</output>
<output aria-label="Pending change">
{String(Boolean(host.settingsHost.pendingSave))}
</output>
</>
);
}
render(<Host />);
return { previewRetype, retype, remote };
}

it.each([false, true])(
"explains hosted choices without staging a migration (another local %s)",
async (anotherLocal) => {
const user = userEvent.setup();
const { previewRetype, retype, remote } = fixture({ anotherLocal });
await user.click(
screen.getByRole("button", { name: "Change harness from Echo" }),
);
const hosted = await screen.findByRole("menuitemradio", {
name: "Builderlab",
});
expect(hosted).toHaveAttribute("aria-disabled", "true");
expect(hosted).not.toBeChecked();
expect(hosted.querySelector("svg")).not.toBeNull();
await user.hover(hosted);
const help = await screen.findByRole("dialog", {
name: "Builderlab hosting",
});
expect(help).toHaveTextContent(
"This agent’s signing key is stored on this device. Buzz can’t move that identity to Builderlab yet. To run hosted, create a new Builderlab agent.",
);
await user.hover(help);
expect(screen.getByRole("menu")).toBeVisible();
expect(help).toBeVisible();
await user.hover(hosted);
await user.click(hosted);
expect(screen.getByRole("menu")).toBeVisible();
expect(screen.getByLabelText("Selected harness")).toHaveTextContent(
"local/echo",
);
expect(screen.getByLabelText("Pending change")).toHaveTextContent("false");
expect(previewRetype).not.toHaveBeenCalled();
expect(retype).not.toHaveBeenCalled();
expect(remote.remote.provision).not.toHaveBeenCalled();
await user.unhover(hosted);
await waitFor(() =>
expect(
screen.queryByRole("dialog", { name: "Builderlab hosting" }),
).toBeNull(),
);
await user.keyboard("{Home}{ArrowDown}{Enter}");
expect(hosted).toHaveFocus();
expect(
await screen.findByRole("dialog", { name: "Builderlab hosting" }),
).toBeVisible();
expect(hosted).toHaveAccessibleDescription(/signing key/);
await user.keyboard("{ArrowUp}");
expect(screen.getByRole("menuitemradio", { name: "Echo" })).toHaveFocus();
await waitFor(() =>
expect(
screen.queryByRole("dialog", { name: "Builderlab hosting" }),
).toBeNull(),
);
await user.keyboard("{ArrowDown}{Enter}");
expect(
await screen.findByRole("dialog", { name: "Builderlab hosting" }),
).toBeVisible();
await user.keyboard("{Escape}");
await waitFor(() =>
expect(
screen.queryByRole("dialog", { name: "Builderlab hosting" }),
).toBeNull(),
);
expect(screen.getByRole("menu")).toBeVisible();
await user.keyboard("{Enter}");
expect(
await screen.findByRole("dialog", { name: "Builderlab hosting" }),
).toBeVisible();
expect(previewRetype).not.toHaveBeenCalled();
expect(retype).not.toHaveBeenCalled();
},
);

it("keeps the existing local migration flow available", async () => {
const user = userEvent.setup();
const { previewRetype, retype } = fixture({ hosted: false });
await user.click(
screen.getByRole("button", { name: "Change harness from Echo" }),
);
expect(
screen.queryByRole("menuitemradio", { name: "Builderlab" }),
).toBeNull();
await user.click(await screen.findByRole("menuitemradio", { name: "Next" }));
expect(
await screen.findByRole("dialog", { name: "Move Ada to Next?" }),
).toBeVisible();
expect(previewRetype).toHaveBeenCalledWith("a".repeat(64), "local/next");
await user.click(screen.getByRole("button", { name: "Continue" }));
expect(screen.getByLabelText("Selected harness")).toHaveTextContent(
"local/next",
);
expect(screen.getByLabelText("Pending change")).toHaveTextContent("true");
expect(retype).not.toHaveBeenCalled();
});

it.each(["hosted", "elsewhere"])(
"does not offer local migration for an agent %s",
(place) => {
fixture({ place });
expect(screen.queryByRole("button", { name: /Change harness/ })).toBeNull();
},
);
99 changes: 91 additions & 8 deletions src/bundled/agents/AgentSettingsHost.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ProviderDialog } from "./ProviderDialog";
import { useRef, useState } from "react";
import { useId, useRef, useState } from "react";
import type {
Agent,
AgentSettingsHost,
Expand All @@ -20,6 +20,12 @@ import {
MenuRoot,
MenuTrigger,
} from "../../shared/design-system/ui/Menu";
import {
PopoverRoot,
PopoverTrigger,
PopoverPopup,
PopoverDescription,
} from "../../shared/design-system/ui/Popover";

const message = (error: unknown) =>
error instanceof Error ? error.message : String(error);
Expand Down Expand Up @@ -82,9 +88,8 @@ export function useAgentSettingsHost({
setPendingType(undefined);
}
: undefined;
const canRetype =
agent?.place === "here" &&
types.some((item) => !item.remote && item.key !== agent.type);
const showTypeControl =
agent?.place === "here" && types.some((item) => item.key !== agent.type);
return {
agent: settingsAgent,
type: settingsType,
Expand All @@ -93,7 +98,7 @@ export function useAgentSettingsHost({
agents && agent
? {
...(pendingTypeSave ? { pendingSave: pendingTypeSave } : {}),
...(canRetype
...(showTypeControl
? {
typeControl: (dirty: boolean) => (
<TypeField
Expand Down Expand Up @@ -136,8 +141,7 @@ function TypeField({
selected: string;
onSelect(type: { key: string; config: unknown } | undefined): void;
}) {
const available =
agent.place === "here" ? types.filter((item) => !item.remote) : [];
const available = agent.place === "here" ? types : [];
const others = available.filter((item) => item.key !== agent.type);
const selectedType = available.find((item) => item.key === selected) ?? type;
const TypeIcon = selectedType?.icon;
Expand All @@ -156,7 +160,7 @@ function TypeField({
return;
}
const target = available.find((item) => item.key === next);
if (!target) return;
if (!target || target.remote) return;
setError("");
try {
setMigration({
Expand Down Expand Up @@ -224,6 +228,10 @@ function TypeField({
onValueChange={(next) => choose(String(next))}
>
{available.map((item) => {
if (item.remote)
return (
<HostedHarnessChoice key={item.key} type={item} />
);
const Icon = item.icon;
return (
<MenuRadioItem
Expand Down Expand Up @@ -308,6 +316,81 @@ function TypeField({
);
}

function HostedHarnessChoice({ type }: { type: RegisteredAgentType }) {
const triggerId = useId();
const trigger = useRef<HTMLElement>(null);
const description = useId();
const [open, setOpen] = useState(false);
const Icon = type.icon;
return (
<PopoverRoot
open={open}
triggerId={triggerId}
onOpenChange={(next, details) => {
// The menu returns focus to itself when the pointer enters this card.
// Let hover dismissal handle that move; another item still closes it.
if (
details.reason === "focus-out" &&
"relatedTarget" in details.event &&
details.event.relatedTarget ===
trigger.current?.closest('[role="menu"]')
) {
details.cancel();
return;
}
setOpen(next);
}}
>
<PopoverTrigger
id={triggerId}
openOnHover="preview"
nativeButton={false}
role="menuitemradio"
onFocus={(event) => {
if (event.currentTarget.matches(":focus-visible")) setOpen(true);
}}
onKeyDownCapture={(event) => {
if (event.key === "Escape" && open) {
event.preventDefault();
event.stopPropagation();
setOpen(false);
} else if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setOpen(true);
}
}}
aria-describedby={open ? description : undefined}
render={
<MenuRadioItem
ref={trigger}
value={type.key}
selection="highlight"
disabled
>
<ChoiceRow
label={type.title}
leading={Icon ? <Icon size={20} /> : undefined}
/>
</MenuRadioItem>
}
/>
<PopoverPopup
side="right"
align="start"
aria-label={`${type.title} hosting`}
initialFocus={false}
finalFocus={false}
>
<PopoverDescription id={description}>
This agent’s signing key is stored on this device. Buzz can’t move
that identity to {type.title} yet. To run hosted, create a new{" "}
{type.title} agent.
</PopoverDescription>
</PopoverPopup>
</PopoverRoot>
);
}

export function MigrationPreview({
source,
target,
Expand Down
Loading
Loading