From 1d58cddaf16743830205bf1a9ede3d9bdb527f83 Mon Sep 17 00:00:00 2001
From: Taylor Ho
Date: Wed, 28 Oct 2026 19:06:53 -0700
Subject: [PATCH 01/20] fix(builderlab): move model configuration into identity
Co-authored-by: Carl
Signed-off-by: Taylor Ho
---
src/bundled/builderlab/README.md | 6 +-
.../adapter-type/Instructions.test.tsx | 71 ++++++++++++++++---
.../builderlab/adapter-type/Instructions.tsx | 44 ++++--------
src/bundled/builderlab/index.test.tsx | 4 +-
4 files changed, 84 insertions(+), 41 deletions(-)
diff --git a/src/bundled/builderlab/README.md b/src/bundled/builderlab/README.md
index b42cf5598f..df11f4b6a8 100644
--- a/src/bundled/builderlab/README.md
+++ b/src/bundled/builderlab/README.md
@@ -7,7 +7,7 @@ Create agents that Builderlab hosts for you, so they keep working even when Buzz
1. Go to **Settings → Plugins → Builderlab agents** and choose **Sign in with Builderlab**.
2. In **Agents**, choose **New agent** and pick **Builderlab (hosted)** as the **Harness**.
3. Choose a model and reasoning effort, optionally add instructions, then create the agent.
-4. Edit instructions later in **Identity**, and model settings in **Settings**.
+4. Edit model settings and instructions later in **Identity**.
---
@@ -68,8 +68,8 @@ The agent's Identity tab reads its current instructions from Builderlab and
saves changes directly to the provider. Loading and saving use the same login;
failed saves keep the draft, and switching accounts requires the original
account before saving. Empty instructions can be saved to clear them.
-The Settings tab loads a separate model catalog from Builderlab. Choose a model
-and one of that model's reasoning efforts, then save the pair for new tasks.
+The Identity tab also loads a separate model catalog from Builderlab. Choose a
+model and one of that model's reasoning efforts, then save the pair for new tasks.
There is no default or fallback selection. Missing or unavailable saved choices
show the server's setup status and require a supported pair. Changing the model
clears the effort choice. Instructions and model settings save independently.
diff --git a/src/bundled/builderlab/adapter-type/Instructions.test.tsx b/src/bundled/builderlab/adapter-type/Instructions.test.tsx
index a6c7627d47..95a650c2fd 100644
--- a/src/bundled/builderlab/adapter-type/Instructions.test.tsx
+++ b/src/bundled/builderlab/adapter-type/Instructions.test.tsx
@@ -183,7 +183,16 @@ it("keeps hosted instructions on cancellation and resets a confirmed discard", a
const user = userEvent.setup();
renderTabs(h);
expect(screen.queryByRole("tab", { name: "Instructions" })).toBeNull();
+ expect(screen.getByRole("tab", { name: "Identity" })).toHaveAttribute(
+ "aria-selected",
+ "true",
+ );
const field = await screen.findByRole("textbox", { name: "Instructions" });
+ expect(await screen.findByRole("combobox", { name: "Model" })).toBeVisible();
+ expect(
+ screen.getByRole("combobox", { name: "Reasoning effort" }),
+ ).toBeVisible();
+ expect(screen.queryByText("Workspace")).toBeNull();
expect(screen.getByRole("link", { name: "Builderlab" })).toHaveAttribute(
"href",
"https://builderlab.example",
@@ -226,14 +235,17 @@ it("keeps hosted instructions on cancellation and resets a confirmed discard", a
expect(screen.getByText("Workspace")).toBeVisible();
expect(screen.getByText("One session per thread")).toBeVisible();
expect(screen.getByText("Agent settings")).toBeVisible();
- expect(await screen.findByRole("combobox", { name: "Model" })).toBeVisible();
+ expect(screen.queryByRole("combobox", { name: "Model" })).toBeNull();
+ expect(
+ screen.queryByRole("combobox", { name: "Reasoning effort" }),
+ ).toBeNull();
await user.click(screen.getByRole("tab", { name: "Identity" }));
const fresh = await screen.findByRole("textbox", { name: "Instructions" });
expect(fresh).not.toBe(field);
expect(fresh).toHaveValue("First instructions.");
expect(screen.queryByRole("alert")).toBeNull();
expect(screen.queryByText("Workspace")).toBeNull();
- expect(screen.queryByRole("combobox", { name: "Model" })).toBeNull();
+ expect(await screen.findByRole("combobox", { name: "Model" })).toBeVisible();
});
it("guards hosted model drafts and a pending model save", async () => {
@@ -241,13 +253,12 @@ it("guards hosted model drafts and a pending model save", async () => {
const user = userEvent.setup();
renderTabs(h);
await screen.findByRole("textbox", { name: "Instructions" });
- await user.click(screen.getByRole("tab", { name: "Settings" }));
await user.click(await screen.findByRole("combobox", { name: "Model" }));
await user.click(
await screen.findByRole("option", { name: "Friendly Model" }),
);
expect(screen.getByRole("button", { name: "Save model" })).toBeDisabled();
- await user.click(screen.getByRole("tab", { name: "Identity" }));
+ await user.click(screen.getByRole("tab", { name: "Settings" }));
expect(await screen.findByRole("alertdialog")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Keep editing" }));
expect(screen.getByRole("combobox", { name: "Model" })).toHaveTextContent(
@@ -259,11 +270,11 @@ it("guards hosted model drafts and a pending model save", async () => {
h.request.mockReturnValueOnce(pending.promise);
try {
await user.click(screen.getByRole("button", { name: "Save model" }));
- expect(screen.getByRole("tab", { name: "Identity" })).toHaveAttribute(
+ expect(screen.getByRole("tab", { name: "Settings" })).toHaveAttribute(
"aria-disabled",
"true",
);
- await user.click(screen.getByRole("tab", { name: "Identity" }));
+ await user.click(screen.getByRole("tab", { name: "Settings" }));
expect(screen.getByRole("combobox", { name: "Model" })).toBeVisible();
} finally {
await act(async () => pending.reject(new Error("offline")));
@@ -271,12 +282,14 @@ it("guards hosted model drafts and a pending model save", async () => {
expect(await screen.findByRole("alert")).toHaveTextContent(
"Could not reach Builderlab",
);
- await user.click(screen.getByRole("tab", { name: "Identity" }));
+ await user.click(screen.getByRole("tab", { name: "Settings" }));
await user.click(screen.getByRole("button", { name: "Discard changes" }));
+ expect(screen.getByText("Agent settings")).toBeVisible();
+ expect(screen.queryByRole("combobox", { name: "Model" })).toBeNull();
+ await user.click(screen.getByRole("tab", { name: "Identity" }));
expect(
await screen.findByRole("textbox", { name: "Instructions" }),
).toHaveValue("First instructions.");
- await user.click(screen.getByRole("tab", { name: "Settings" }));
expect(
await screen.findByRole("combobox", { name: "Model" }),
).toHaveTextContent("Choose a model");
@@ -285,3 +298,45 @@ it("guards hosted model drafts and a pending model save", async () => {
).toHaveTextContent("Choose reasoning effort");
expect(screen.queryByRole("alert")).toBeNull();
});
+
+it("keeps the model draft guarded after independently saving instructions", async () => {
+ const h = fixture();
+ const user = userEvent.setup();
+ renderTabs(h);
+ const field = await screen.findByRole("textbox", { name: "Instructions" });
+ await user.click(await screen.findByRole("combobox", { name: "Model" }));
+ await user.click(
+ await screen.findByRole("option", { name: "Friendly Model" }),
+ );
+ await user.type(field, " Updated.");
+ const save = screen.getByRole("button", { name: "Save instructions" });
+ await user.click(save);
+ await waitFor(() => expect(save).toHaveAttribute("aria-disabled", "true"));
+ expect(field).toHaveValue("First instructions. Updated.");
+ expect(screen.getByRole("combobox", { name: "Model" })).toHaveTextContent(
+ "Friendly Model",
+ );
+ await user.click(screen.getByRole("tab", { name: "Settings" }));
+ expect(await screen.findByRole("alertdialog")).toBeInTheDocument();
+ await user.click(screen.getByRole("button", { name: "Keep editing" }));
+ expect(field).toHaveValue("First instructions. Updated.");
+});
+
+it("keeps runtime settings available without loading Builderlab configuration", () => {
+ const h = fixture();
+ render(
+ Agent settings
}
+ />,
+ { wrapper: StrictMode },
+ );
+ expect(screen.getByText("Workspace")).toBeVisible();
+ expect(screen.getByText("One session per thread")).toBeVisible();
+ expect(screen.getByText("Agent settings")).toBeVisible();
+ expect(screen.queryByRole("textbox", { name: "Instructions" })).toBeNull();
+ expect(screen.queryByRole("combobox", { name: "Model" })).toBeNull();
+ expect(h.request).not.toHaveBeenCalled();
+});
diff --git a/src/bundled/builderlab/adapter-type/Instructions.tsx b/src/bundled/builderlab/adapter-type/Instructions.tsx
index 9fcd9d97b7..2a6c8b88b2 100644
--- a/src/bundled/builderlab/adapter-type/Instructions.tsx
+++ b/src/bundled/builderlab/adapter-type/Instructions.tsx
@@ -23,15 +23,7 @@ const message = (reason: unknown) =>
/** Hosted settings belong to Builderlab, rather than Agents' local config. */
export function createSettingsTab(client: AgentClient, session: OAuthSession) {
- function Editor({
- pubkey,
- retry,
- section,
- }: {
- pubkey: string;
- retry(): void;
- section: AgentViewProps["settingsSection"];
- }) {
+ function Editor({ pubkey, retry }: { pubkey: string; retry(): void }) {
const [loaded, setLoaded] = useState<{
agent: RemoteAgent;
subject: string;
@@ -42,7 +34,6 @@ export function createSettingsTab(client: AgentClient, session: OAuthSession) {
useAgentSaveState(
!!loaded && draft !== (loaded.agent.instructions ?? ""),
!!loaded && busy,
- section !== "runtime",
);
const controller = useRef(null);
useEffect(() => {
@@ -101,18 +92,14 @@ export function createSettingsTab(client: AgentClient, session: OAuthSession) {
return (