From a0c7e38ccd04d2210f3a07145caa73fa9c0ee4b8 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 19 Jun 2026 09:53:09 -0700 Subject: [PATCH] fix(ui): reorder environment driver dropdown and drop Local option (#8329) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agents run on **environments**, configured in Company Settings → Environments, where each environment has a **driver** (how/where it runs) > - The "new environment" form exposed a Driver `` lists three options in this order: `SSH`, `Sandbox`, `Local`. **Local** is selectable even though a local environment represents the Paperclip host itself and cannot be created more than once, so choosing it in the create flow is not a valid action. **Sandbox**, the most common choice, sits in the middle of the list instead of first. ### Proposed solution Drop **Local** from the create dropdown and order the remaining options **Sandbox** first, then **SSH**. The create flow then only offers drivers you can actually create, with the most-used driver surfaced first. Existing local environments must still render and be editable, so the `"local"` driver value is retained in the type union. ### Alternatives considered Keeping **Local** but disabling it: rejected — a permanently-disabled option is noise and still implies local environments are creatable here. Hiding the whole driver field when only one option remains: rejected — both Sandbox and SSH remain valid, so the selector is still needed. ### Roadmap alignment Not roadmap-tracked. This is a small, self-contained UX correction to an existing form, not new core feature work. ## What Changed - Removed the **Local** ` {sandboxCreationEnabled || environmentForm.driver === "sandbox" ? ( ) : null} - + + {environmentForm.driver === "local" ? ( + + ) : null} diff --git a/ui/src/pages/CompanySettings.test.tsx b/ui/src/pages/CompanySettings.test.tsx index 51a922b3..fa3b60fc 100644 --- a/ui/src/pages/CompanySettings.test.tsx +++ b/ui/src/pages/CompanySettings.test.tsx @@ -165,6 +165,110 @@ describe("CompanyEnvironments", () => { }); }); + it("omits the Local driver option and lists Sandbox before SSH", async () => { + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + mockEnvironmentsApi.capabilities.mockResolvedValue( + getEnvironmentCapabilities(AGENT_ADAPTER_TYPES, { + sandboxProviders: { + "secure-plugin": { + status: "supported", + supportsSavedProbe: true, + supportsUnsavedProbe: true, + supportsRunExecution: true, + supportsReusableLeases: true, + displayName: "Secure Sandbox", + configSchema: { type: "object", properties: {} }, + }, + }, + }), + ); + + await act(async () => { + root.render( + + + + + , + ); + }); + await flushReact(); + await flushReact(); + + const driverSelect = Array.from(container.querySelectorAll("select")) + .find((select) => Array.from(select.options).some((option) => option.value === "ssh")) as + | HTMLSelectElement + | undefined; + expect(driverSelect).toBeTruthy(); + + const driverOptionValues = Array.from(driverSelect!.options).map((option) => option.value); + expect(driverOptionValues).not.toContain("local"); + expect(driverOptionValues).toEqual(["sandbox", "ssh"]); + + await act(async () => { + root.unmount(); + }); + }); + + it("shows the Local driver option when editing an existing local environment", async () => { + const root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + mockEnvironmentsApi.list.mockResolvedValue([ + { + id: "env-local", + companyId: "company-1", + name: "Local host", + description: null, + driver: "local", + status: "active", + config: {}, + metadata: null, + createdAt: new Date("2026-04-25T00:00:00.000Z"), + updatedAt: new Date("2026-04-25T00:00:00.000Z"), + }, + ]); + + await act(async () => { + root.render( + + + + + , + ); + }); + await flushReact(); + await flushReact(); + + const editButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.trim() === "Edit"); + expect(editButton).toBeTruthy(); + + await act(async () => { + editButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + const driverSelect = Array.from(container.querySelectorAll("select")) + .find((select) => Array.from(select.options).some((option) => option.value === "ssh")) as + | HTMLSelectElement + | undefined; + expect(driverSelect).toBeTruthy(); + + const driverOptionValues = Array.from(driverSelect!.options).map((option) => option.value); + expect(driverOptionValues).toContain("local"); + expect(driverSelect!.value).toBe("local"); + + await act(async () => { + root.unmount(); + }); + }); + it("preserves sandbox config when re-selecting the same provider while editing", async () => { const root = createRoot(container); const queryClient = new QueryClient({