diff --git a/packages/app/e2e/regression/new-session.spec.ts b/packages/app/e2e/regression/new-session.spec.ts index 7246ebd6d66..6e86bdcc090 100644 --- a/packages/app/e2e/regression/new-session.spec.ts +++ b/packages/app/e2e/regression/new-session.spec.ts @@ -584,24 +584,24 @@ test("the location dialog lists every ready server's projects in one list and ta const search = dialog.getByRole("combobox") await expect(search).toBeFocused() const options = dialog.getByRole("option") - const rows = dialog.locator(".command-palette-results").getByRole("option") const add = options.filter({ hasText: "Add project" }) + const rows = options.filter({ has: page.locator(".new-session-location-name") }) const names = options.locator(".new-session-location-name") const paths = options.locator(".command-palette-description") const chip = dialog.getByRole("button", { name: "Back to projects", exact: true }) // Used projects first, then the rest, each by recent activity across servers; only remote ones carry a tag. await expect(names).toHaveText(["opencodeapollo", "opencode", "console", "sstapollo"]) await expect(paths).toHaveText(["/home/dax/opencode", "C:/work/opencode", "C:/work/console", "/home/dax/sst"]) - // "Add project" is pinned above the scrolling list and its heading, yet the first project is still the opening row, - // not the draft's current project (the second row). - await expect(options.first()).toHaveText("Add project") + // "Add project" is the first row of the scrolling list, under its heading, yet the first project is still the + // opening row, not the draft's current project (the second row). + await expect(dialog.locator("[data-scrollable]").getByRole("option").first()).toHaveText("Add project") await expect(rows).toHaveCount(4) await expect .poll(async () => { const row = await add.boundingBox() const heading = await dialog.getByText("Projects", { exact: true }).boundingBox() - return row && heading ? row.y + row.height <= heading.y : false + return row && heading ? row.y >= heading.y + heading.height : false }) .toBe(true) await expect(rows.nth(0)).toHaveAttribute("aria-selected", "true") @@ -612,25 +612,28 @@ test("the location dialog lists every ready server's projects in one list and ta await expect(names.nth(0).locator(".command-palette-title")).toHaveCSS("font-size", "13px") await expect(names.nth(0).locator(".command-palette-title")).toHaveCSS("white-space", "nowrap") await expect(rows.nth(0)).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)") - // The pinned row shares the list's width and icon column. + // "Add project" shares the project rows' width and icon column. await expect .poll(async () => { const field = await dialog.locator('[data-component="text-input-v2"]').boundingBox() const row = await rows.nth(0).boundingBox() - const pinned = await add.boundingBox() + const first = await add.boundingBox() const icon = await add.locator(".command-palette-row-icon").boundingBox() const avatar = await rows.nth(0).locator('[data-component="project-avatar-v2"]').boundingBox() - return field && row && pinned && icon && avatar - ? Math.max(Math.abs(field.width - row.width), Math.abs(pinned.width - row.width), Math.abs(icon.x - avatar.x)) + return field && row && first && icon && avatar + ? Math.max(Math.abs(field.width - row.width), Math.abs(first.width - row.width), Math.abs(icon.x - avatar.x)) : Infinity }) .toBeLessThanOrEqual(1) - // Searching narrows the projects and keeps "Add project" pinned. + // Searching narrows the projects and keeps "Add project" first; with no match it is the active row. await search.fill("apollo") await expect(names).toHaveText(["opencodeapollo", "sstapollo"]) - await expect(add).toBeVisible() + await expect(options.first()).toHaveText("Add project") + await search.fill("nothing matches") + await expect(options).toHaveText(["Add project"]) + await expect(add).toHaveAttribute("aria-selected", "true") // The worktree step keeps the server's tag beside the project. await search.fill("opencode") @@ -656,7 +659,7 @@ test("the location dialog lists every ready server's projects in one list and ta await expect(search).toHaveValue("opencode") await expect(rows.nth(0)).toHaveAttribute("aria-selected", "true") - // ArrowUp from the first project reaches the pinned row above it; adding a project picks one of the ready servers + // ArrowUp from the first project reaches "Add project" above it; adding a project picks one of the ready servers // first, then browses that server. await search.fill("") await expect(rows.nth(0)).toHaveAttribute("aria-selected", "true") diff --git a/packages/app/src/new-session/location/dialog.css b/packages/app/src/new-session/location/dialog.css index 716a40c19b6..37efdecc2e9 100644 --- a/packages/app/src/new-session/location/dialog.css +++ b/packages/app/src/new-session/location/dialog.css @@ -66,19 +66,3 @@ text-overflow: ellipsis; white-space: nowrap; } - -.new-session-location-list { - display: flex; - min-height: 0; - flex: 1; - flex-direction: column; -} - -/* "Add project" stays above the scrolling project list. */ -.new-session-location-pinned { - display: flex; - flex-shrink: 0; - flex-direction: column; - padding: 0 6px 6px; - border-bottom: 0.5px solid var(--v2-border-border-muted); -} diff --git a/packages/app/src/new-session/location/dialog.tsx b/packages/app/src/new-session/location/dialog.tsx index 3f8924a6874..ca6fbd3d358 100644 --- a/packages/app/src/new-session/location/dialog.tsx +++ b/packages/app/src/new-session/location/dialog.tsx @@ -123,8 +123,8 @@ function NewSessionLocationDialog(props: { const addEntry: LocationEntry = { id: "add", type: "add" } - // Remote projects also match their server's name, which their tag shows. "Add project" is pinned above the list but - // navigates last, so the first project stays the opening row and ArrowUp from it wraps to "Add project" above. + // Remote projects also match their server's name, which their tag shows. "Add project" is listed first but navigates + // last, so the first project stays the opening row and the arrow keys' wrap-around still follows the rows on screen. const projectEntries = createMemo(() => [ ...props.project .projects() @@ -166,11 +166,9 @@ function NewSessionLocationDialog(props: { return projectEntries() } - const pinned = () => !props.projectsOnly && !store.drilled && !store.picking - const drillable = (project: PromptProject) => !props.projectsOnly && !!project.id && project.vcs === "git" - const listEntries = () => entries().filter((entry) => entry.type !== "add") + const rows = () => (entries().at(-1) === addEntry ? [addEntry, ...entries().slice(0, -1)] : entries()) const title = () => { if (store.drilled) return language.t("session.new.workspace.runIn") @@ -309,72 +307,6 @@ function NewSessionLocationDialog(props: { typeahead.onKeyDown(event) } - const option = (entry: LocationEntry) => ( - - ) - return ( @@ -421,27 +353,92 @@ function NewSessionLocationDialog(props: { onKeyDown={handleKeyDown} /> -
- -
{option(addEntry)}
-
-
(resultsRef = el)}> -
- 0} - fallback={ - // The worktree list may still be loading; hold the row's height without flashing a label. -
- {language.t("palette.empty")} -
- } - > -
-
{title()}
- {option} +
(resultsRef = el)}> +
+ 0} + fallback={ + // The worktree list may still be loading; hold the row's height without flashing a label. +
+ {language.t("palette.empty")}
-
-
+ } + > +
+
{title()}
+ + {(entry) => ( + + )} + +
+