From de50243885ff47e0dc9c26233327fe066befa684 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:59:03 +0000 Subject: [PATCH] Keep model-discovery errors away from picker edges (#8039) * fix(app): pad the model picker empty state on all sides The empty state carried vertical padding only, so the provider refresh error it renders wrapped from edge to edge and read as attached to the model picker's left border on web and mobile. One padding value keeps the message off the border on every platform. * test(app): keep model discovery errors inside picker edges Exercise a real catalog deadline through the existing Pi RPC fixture at desktop and compact browser widths. Both gutter assertions fail without the shared padding fix. Co-authored-by: Matteo Pietro Dazzi --------- Co-authored-by: Matteo Pietro Dazzi Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../browser/provider-settings-refresh.spec.ts | 65 +++++++++++++++++++ packages/app/e2e/fixtures/fake-pi-rpc.mjs | 1 + packages/app/src/components/model-browser.tsx | 2 +- 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/packages/app/e2e/browser/provider-settings-refresh.spec.ts b/packages/app/e2e/browser/provider-settings-refresh.spec.ts index 961b518435..6c9739e894 100644 --- a/packages/app/e2e/browser/provider-settings-refresh.spec.ts +++ b/packages/app/e2e/browser/provider-settings-refresh.spec.ts @@ -1,5 +1,7 @@ +import path from "node:path"; import type { Locator } from "@playwright/test"; import { expect, test, type Page } from "../support/fixtures"; +import { clickNewChat, gotoWorkspace } from "../support/helpers/launcher"; import { expectComposerVisible } from "../support/helpers/composer"; import { openAgentRoute, seedMockAgentWorkspace } from "../support/helpers/mock-agent"; @@ -214,3 +216,66 @@ test.describe("provider settings overlay stack", () => { } }); }); + +async function openTimedOutProvider(page: Page, workspaceId: string, width: number) { + await gotoWorkspace(page, workspaceId); + await clickNewChat(page); + await expectComposerVisible(page); + await page.setViewportSize({ width, height: 900 }); + await page.getByRole("button", { name: /Select model/ }).click(); + if (width < 600) { + await page.getByTestId("agent-controls-model").click(); + } else { + await page.getByRole("button", { name: "Back", exact: true }).click(); + } + await page.getByTestId("model-provider-picker-timeout").click(); + await expect(page.getByText(/Timed out refreshing Pi timeout/)).toBeVisible(); +} + +async function expectErrorHasSideGutters(page: Page) { + const error = page.getByText(/Timed out refreshing Pi timeout/); + const bounds = await error.evaluate((element) => { + const text = element.getBoundingClientRect(); + const state = element.parentElement!.getBoundingClientRect(); + return { left: text.left - state.left, right: state.right - text.right }; + }); + expect(bounds.left, "error text stays inside the empty state's left edge").toBeGreaterThan(0); + expect(bounds.right, "error text stays inside the empty state's right edge").toBeGreaterThan(0); + await expect(page.getByRole("button", { name: "Retry", exact: true })).toBeVisible(); +} + +test.use({ + e2eDaemonConfig: { + version: 1, + agents: { + catalogRefreshTimeoutMs: 2000, + providers: { + "picker-timeout": { + extends: "pi", + label: "Pi timeout", + enabled: true, + command: [ + process.execPath, + path.resolve("e2e/fixtures/fake-pi-rpc.mjs"), + "--stall-model-discovery", + ], + }, + }, + }, + }, +}); + +test.describe("model discovery timeout", () => { + for (const width of [1200, 390]) { + test(`keeps a wrapped discovery error away from both picker edges at ${width}px`, async ({ + page, + withWorkspace, + }, testInfo) => { + await page.setViewportSize({ width: 1200, height: 900 }); + const workspace = await withWorkspace({ prefix: "model-error-spacing-" }); + await openTimedOutProvider(page, workspace.workspaceId, width); + await page.screenshot({ path: testInfo.outputPath("model-error.png") }); + await expectErrorHasSideGutters(page); + }); + } +}); diff --git a/packages/app/e2e/fixtures/fake-pi-rpc.mjs b/packages/app/e2e/fixtures/fake-pi-rpc.mjs index b6d156ddfc..372e29c44b 100644 --- a/packages/app/e2e/fixtures/fake-pi-rpc.mjs +++ b/packages/app/e2e/fixtures/fake-pi-rpc.mjs @@ -6,6 +6,7 @@ for await (const line of lines) { const command = JSON.parse(line); let data = {}; if (command.type === "get_available_models") { + if (process.argv.includes("--stall-model-discovery")) continue; data = { models: [ { diff --git a/packages/app/src/components/model-browser.tsx b/packages/app/src/components/model-browser.tsx index 17924b233d..2ca6b6a9c2 100644 --- a/packages/app/src/components/model-browser.tsx +++ b/packages/app/src/components/model-browser.tsx @@ -1692,7 +1692,7 @@ const styles = StyleSheet.create((theme) => ({ opacity: 0, }, emptyState: { - paddingVertical: theme.spacing[4], + padding: theme.spacing[4], alignItems: "center", gap: theme.spacing[2], },