diff --git a/packages/app/e2e/browser/add-project-flow.spec.ts b/packages/app/e2e/browser/add-project-flow.spec.ts index 027d6156e7..9fd7f9413d 100644 --- a/packages/app/e2e/browser/add-project-flow.spec.ts +++ b/packages/app/e2e/browser/add-project-flow.spec.ts @@ -223,6 +223,7 @@ test.describe("Add Project command-center flow", () => { await page.keyboard.press("Enter"); await expectAddProjectPage(page, "directory-search"); + await expect(addProjectFlowInput(page)).toBeFocused(); await page.keyboard.type(projectPickerFixture.fuzzyQuery); await expect(addProjectFlow(page)).toContainText(projectPickerFixture.projectName, { timeout: 30_000, diff --git a/packages/app/e2e/browser/creation-old-daemon.spec.ts b/packages/app/e2e/browser/creation-old-daemon.spec.ts index ff28d513ef..4b418b304a 100644 --- a/packages/app/e2e/browser/creation-old-daemon.spec.ts +++ b/packages/app/e2e/browser/creation-old-daemon.spec.ts @@ -116,6 +116,7 @@ for (const version of ["0.2.5", "0.7.2", "0.8.0"]) { ); expect(workspaces).toHaveLength(1); gate.release(); + await gate.waitForCreated(); await expect.poll(async () => (await client.fetchAgents()).entries.length).toBe(1); const first = (await client.fetchAgents()).entries[0]!.agent; await client.waitForFinish(first.id, 20_000); @@ -128,6 +129,7 @@ for (const version of ["0.2.5", "0.7.2", "0.8.0"]) { const secondPrompt = "Create another agent: emit 1 coalesced agent stream updates"; await fillComposerDraft(page, secondPrompt); await pressSubmitBeforeTheNextRender(page, "Send message"); + await gate.waitForCreated(); await expect.poll(async () => (await client.fetchAgents()).entries.length).toBe(2); const second = (await client.fetchAgents()).entries.find( ({ agent }) => agent.id !== first.id, @@ -206,21 +208,40 @@ for (const version of ["0.2.5", "0.7.2", "0.8.0"]) { async function holdLegacyAgentCreation(page: Page, port: number) { const readers = await loadSessionMessageReaders(); const arrived = Promise.withResolvers(); + let created = Promise.withResolvers(); + let createRequestId: string | undefined; let release = () => {}; let holding = true; await page.routeWebSocket(new RegExp(`:${port}/ws`), (browser) => { const server = browser.connectToServer(); browser.onMessage((frame) => { - if (holding && readers.client(frame)?.type === "create_agent_request") { + const message = readers.client(frame); + if (message?.type === "create_agent_request") createRequestId = message.requestId; + if (holding && message?.type === "create_agent_request") { holding = false; release = () => server.send(frame); arrived.resolve(); } else server.send(frame); }); - server.onMessage((frame) => browser.send(frame)); + server.onMessage((frame) => { + const message = readers.server(frame); + if ( + message?.type === "status" && + message.payload.status === "agent_created" && + message.payload.requestId === createRequestId + ) { + created.resolve(); + } + browser.send(frame); + }); }); return { waitForRequest: () => arrived.promise, + async waitForCreated() { + // Agent-list membership includes initialization, before the initial prompt starts. + await created.promise; + created = Promise.withResolvers(); + }, release() { const send = release; release = () => {}; diff --git a/packages/app/e2e/browser/custom-model-removal.spec.ts b/packages/app/e2e/browser/custom-model-removal.spec.ts new file mode 100644 index 0000000000..dc6fd84616 --- /dev/null +++ b/packages/app/e2e/browser/custom-model-removal.spec.ts @@ -0,0 +1,29 @@ +import { test } from "../support/fixtures"; +import { + addCustomModel, + expectCustomModelRetained, + openCustomModels, + removeCustomModel, +} from "../support/helpers/custom-models"; + +const longModelId = "custom-model-with-a-long-name-".repeat(6); +const shortModelId = "custom-short"; + +for (const width of [1200, 390]) { + test(`removes a long custom model at ${width}px without removing another model`, async ({ + page, + }) => { + await page.setViewportSize({ width: 1200, height: 1000 }); + await openCustomModels(page); + await addCustomModel(page, shortModelId); + await addCustomModel(page, longModelId); + + await test.step("remove the long model at the target width", async () => { + await page.setViewportSize({ width, height: 1000 }); + await removeCustomModel(page, longModelId); + await expectCustomModelRetained(page, shortModelId); + }); + + await removeCustomModel(page, shortModelId); + }); +} diff --git a/packages/app/e2e/support/helpers/custom-models.ts b/packages/app/e2e/support/helpers/custom-models.ts new file mode 100644 index 0000000000..11933a0dfa --- /dev/null +++ b/packages/app/e2e/support/helpers/custom-models.ts @@ -0,0 +1,37 @@ +import { expect, type Page } from "@playwright/test"; +import { gotoAppShell, openSettings } from "./app"; +import { openSettingsHostSection } from "./settings"; +import { getServerId } from "./server-id"; + +export async function openCustomModels(page: Page): Promise { + await gotoAppShell(page); + await openSettings(page); + await openSettingsHostSection(page, getServerId(), "providers"); + await page.getByRole("button", { name: "Claude provider details", exact: true }).click(); + await expect(page.getByRole("textbox", { name: "Search models" })).toBeVisible(); +} + +export async function addCustomModel(page: Page, id: string): Promise { + await page.getByRole("button", { name: "Add model", exact: true }).click(); + await page.getByPlaceholder("e.g. openai/gpt-5").fill(id); + await page + .getByTestId("add-custom-model-sheet") + .getByRole("button", { name: "Add", exact: true }) + .click(); + await expect(page.getByRole("button", { name: `Remove ${id}`, exact: true })).toHaveCount(1); +} + +export async function removeCustomModel(page: Page, id: string): Promise { + await page.getByRole("textbox", { name: "Search models" }).fill(id); + const remove = page.getByRole("button", { name: `Remove ${id}`, exact: true }); + await expect(remove).toBeInViewport({ ratio: 1 }); + await remove.click(); + await expect(remove).toHaveCount(0); +} + +export async function expectCustomModelRetained(page: Page, id: string): Promise { + await page.getByRole("textbox", { name: "Search models" }).fill(id); + await expect(page.getByRole("button", { name: `Remove ${id}`, exact: true })).toBeInViewport({ + ratio: 1, + }); +} diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index bf2d1c1b95..61d42882e8 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -95,11 +95,11 @@ function CustomModelRow({ return ( - + {model.label} ({ iconButton: { width: 28, height: 28, + flexShrink: 0, borderRadius: theme.borderRadius.full, alignItems: "center", justifyContent: "center", @@ -807,6 +808,10 @@ const sheetStyles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.base, flexShrink: 0, }, + customModelText: { + flexShrink: 1, + minWidth: 0, + }, modelRowFiller: { flex: 1, },