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 <matteopietro.dazzi@gmail.com>

---------

Co-authored-by: Matteo Pietro Dazzi <matteopietro.dazzi@gmail.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-27 15:59:03 +00:00
committed by GitHub
co-authored by Matteo Pietro Dazzi
parent 5b7b8d6e81
commit de50243885
3 changed files with 67 additions and 1 deletions
@@ -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);
});
}
});
@@ -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: [
{
@@ -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],
},