diff --git a/packages/app/e2e/regression/home.spec.ts b/packages/app/e2e/regression/home.spec.ts index 15c8a15ea9b..15d190d8a43 100644 --- a/packages/app/e2e/regression/home.spec.ts +++ b/packages/app/e2e/regression/home.spec.ts @@ -224,11 +224,11 @@ test("Home lists the used server projects by recent activity, hides them, and li await newSessionLocation(page).click() const options = page.getByRole("dialog").getByRole("option") await expect(options.locator(".command-palette-title")).toHaveText([ + "Add project", "oldest", "fresh", "unused", "recent", - "Add project", ]) await options.filter({ hasText: "C:/OpenCode/recent" }).click() await expect(newSessionLocation(page)).toContainText("recent") diff --git a/packages/app/e2e/regression/new-session.spec.ts b/packages/app/e2e/regression/new-session.spec.ts index b2b01b7dd98..075f4135585 100644 --- a/packages/app/e2e/regression/new-session.spec.ts +++ b/packages/app/e2e/regression/new-session.spec.ts @@ -590,6 +590,7 @@ test("the location dialog lists every ready server's projects in one list and ta const paths = options.locator(".command-palette-description") const chip = dialog.getByRole("button", { name: "Back to projects", exact: true }) const heading = dialog.getByText("Projects", { exact: true }) + const addButton = dialog.getByRole("button", { name: "Add project", 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"]) @@ -606,6 +607,10 @@ test("the location dialog lists every ready server's projects in one list and ta }) .toBe(true) await expect(rows.nth(0)).toHaveAttribute("aria-selected", "true") + // The search field also ends with an "Add project" button, next in the Tab order. + await page.keyboard.press("Tab") + await expect(addButton).toBeFocused() + await search.focus() // The picker shares the palette's full-width search, compact rows, and single-line titles. await expect(rows.nth(0)).toHaveCSS("height", "36px") @@ -644,6 +649,7 @@ test("the location dialog lists every ready server's projects in one list and ta await page.keyboard.press("ArrowRight") // The worktree step has no "Add project". await expect(add).toHaveCount(0) + await expect(addButton).toHaveCount(0) await expect(chip).toContainText("opencode") await expect(chip.locator('[data-component="tag"]')).toHaveText("apollo") await expect(options.locator(".command-palette-title")).toHaveText(["opencode", "New worktree", "opencode-beta"]) @@ -672,6 +678,13 @@ test("the location dialog lists every ready server's projects in one list and ta await page.keyboard.press("ArrowUp") await page.keyboard.press("ArrowRight") await expect(chip).toHaveText("Add project") + await expect(addButton).toHaveCount(0) + // The search field's button starts the same flow. + await page.keyboard.press("Backspace") + await expect(addButton).toBeVisible() + await page.keyboard.press("Tab") + await page.keyboard.press("Enter") + await expect(chip).toHaveText("Add project") // The server the app runs beside shows its hostname and "This computer" instead of its address. await expect(options).toHaveText(["hailmaryThis computer", "apollo"]) await options.filter({ hasText: "apollo" }).click() diff --git a/packages/app/src/new-session/location/dialog.css b/packages/app/src/new-session/location/dialog.css index 37efdecc2e9..93a89864b7b 100644 --- a/packages/app/src/new-session/location/dialog.css +++ b/packages/app/src/new-session/location/dialog.css @@ -66,3 +66,41 @@ text-overflow: ellipsis; white-space: nowrap; } + +.new-session-location-add { + display: flex; + height: 24px; + align-items: center; + gap: 4px; + padding: 0 8px 0 6px; + border-radius: 4px; + color: var(--v2-text-text-muted); + font-size: 13px; + font-weight: 440; + line-height: 16px; + white-space: nowrap; + outline: none; +} + +.new-session-location-add:is(:hover, :focus-visible) { + background: var(--v2-overlay-simple-overlay-hover); + color: var(--v2-text-text-base); +} + +.new-session-location-add:focus-visible { + outline: 2px solid var(--v2-border-border-focus); + outline-offset: -2px; +} + +/* Narrow screens keep only the icon; the tooltip names it. */ +@media (max-width: 480px) { + .new-session-location-add { + width: 24px; + justify-content: center; + padding: 0; + } + + .new-session-location-add > span { + display: none; + } +} diff --git a/packages/app/src/new-session/location/dialog.tsx b/packages/app/src/new-session/location/dialog.tsx index 969471e359c..0e2817aff22 100644 --- a/packages/app/src/new-session/location/dialog.tsx +++ b/packages/app/src/new-session/location/dialog.tsx @@ -11,6 +11,7 @@ import { ProjectAvatar, } from "@opencode/ui/project-avatar" import { TextInput } from "@opencode/ui/text-input" +import { Tooltip } from "@opencode/ui/tooltip" import { createTypeahead } from "@opencode/ui/typeahead" import { getFilename } from "@opencode/util/path" import { createEffect, createMemo, createResource, For, type JSX, Match, onCleanup, Show, Switch } from "solid-js" @@ -418,6 +419,21 @@ function NewSessionLocationDialog(props: { aria-activedescendant={typeahead.activeID()} onInput={(event) => setStore("query", event.currentTarget.value)} onKeyDown={handleKeyDown} + trailing={ + + + + + + } />
(resultsRef = el)}>