diff --git a/packages/app/e2e/browser/agent-profiles-picker.spec.ts b/packages/app/e2e/browser/agent-profiles-picker.spec.ts index d67de4da36..5dd5734710 100644 --- a/packages/app/e2e/browser/agent-profiles-picker.spec.ts +++ b/packages/app/e2e/browser/agent-profiles-picker.spec.ts @@ -6,13 +6,16 @@ import { expectAgentProfilesEmptyPrompt, expectProfileEditTooltip, expectComposerMode, + expectCreateProfileFromModelRow, expectComposerModel, + expectModelRowProfileActionBesideRow, expectModelRowSelected, expectProfileEditIsPencilOnly, expectProfileVisibleForProvider, openModelPicker, openAgentProfilesFromEmptyPrompt, seedAgentProfiles, + selectModelRow, } from "../support/helpers/agent-profiles"; import { expectWorkspaceAgentConfiguration } from "../support/helpers/command-center-agent-controls"; import { expectComposerVisible } from "../support/helpers/composer"; @@ -115,4 +118,34 @@ test.describe("Agent profiles in the model picker", () => { await seed.restore(); } }); + + test("a model row and its create-profile action are separate buttons", async ({ page }) => { + const seed = await seedAgentProfiles([]); + const workspace = await seedMockAgentWorkspace({ + repoPrefix: "agent-profiles-row-action-", + title: "Agent profiles row action", + model: "ten-second-stream", + modeId: "load-test", + }); + + try { + const oneMinute = { + provider: "mock", + modelId: "one-minute-stream", + modelLabel: "One minute stream", + }; + await openAgentRoute(page, workspace); + await expectComposerVisible(page); + await openModelPicker(page); + await expectModelRowProfileActionBesideRow(page, oneMinute); + await expectCreateProfileFromModelRow(page, oneMinute); + await expectComposerModel(page, "Ten second stream"); + await openModelPicker(page); + await selectModelRow(page, oneMinute); + await expectComposerModel(page, oneMinute.modelLabel); + } finally { + await workspace.cleanup(); + await seed.restore(); + } + }); }); diff --git a/packages/app/e2e/support/helpers/agent-profiles.ts b/packages/app/e2e/support/helpers/agent-profiles.ts index d9bd9c4044..a538d256c0 100644 --- a/packages/app/e2e/support/helpers/agent-profiles.ts +++ b/packages/app/e2e/support/helpers/agent-profiles.ts @@ -482,6 +482,50 @@ export async function expectModelRowSelected( ).toHaveAttribute("aria-selected", "true", { timeout: 30_000 }); } +function modelRow(page: Page, input: { provider: string; modelId: string }): Locator { + return pickerViewport(page).getByTestId(`model-row-${input.provider}-${input.modelId}`); +} + +/** + * A model row's profile action is its own button. On web, a button inside the + * row's button is invalid HTML that React reports on every render. + */ +export async function expectModelRowProfileActionBesideRow( + page: Page, + input: { provider: string; modelId: string }, +): Promise { + const row = modelRow(page, input); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(pickerViewport(page).locator("button button")).toHaveCount(0); + await expect(row.getByRole("button")).toHaveCount(0); +} + +/** The hover-revealed plus on an unprofiled model row opens a seeded profile form. */ +export async function expectCreateProfileFromModelRow( + page: Page, + input: { provider: string; modelId: string; modelLabel: string }, +): Promise { + await modelRow(page, input).hover(); + await pickerViewport(page) + .getByTestId(`model-create-profile-${input.provider}-${input.modelId}`) + .click(); + const modal = editModal(page); + await expect(modal).toBeVisible({ timeout: 30_000 }); + await expect(modal.getByRole("textbox", { name: "Name", exact: true })).toHaveValue( + input.modelLabel, + ); + await expect(modal.getByRole("button", { name: `Model (${input.modelLabel})` })).toBeVisible(); + await modal.getByRole("button", { name: "Cancel", exact: true }).click(); + await expect(modal).toHaveCount(0, { timeout: 30_000 }); +} + +export async function selectModelRow( + page: Page, + input: { provider: string; modelId: string }, +): Promise { + await modelRow(page, input).click(); +} + // ─── Cross-provider model search ─────────────────────────────────────────── export async function searchAllModels(page: Page, query: string): Promise { diff --git a/packages/app/src/components/model-browser.tsx b/packages/app/src/components/model-browser.tsx index 67cb8d23ce..9d0c8f8bc3 100644 --- a/packages/app/src/components/model-browser.tsx +++ b/packages/app/src/components/model-browser.tsx @@ -625,13 +625,6 @@ function ModelRowProfileAction({ }) { const isCompact = useIsCompactFormFactor(); const visible = hovered || isNative || isCompact; - const handlePress = useCallback( - (event: GestureResponderEvent) => { - event.stopPropagation(); - onPress(); - }, - [onPress], - ); const pressableStyle = useCallback( ({ hovered: buttonHovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [ styles.rowIconButton, @@ -645,7 +638,7 @@ function ModelRowProfileAction({ [ styles.browserRow, - styles.browserModelRow, Boolean(hovered) && styles.browserRowHovered, pressed && styles.browserRowPressed, ], @@ -787,7 +779,7 @@ function ModelRow({ return ( @@ -815,10 +807,17 @@ function ModelRow({ ) : null} - {profileAction} + {profileAction ? : null} + {/* The row renders a