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 (
{loaded && ( - + )}