diff --git a/packages/app/e2e/browser/add-project-flow.spec.ts b/packages/app/e2e/browser/add-project-flow.spec.ts index 3d5bea938..77ab53f96 100644 --- a/packages/app/e2e/browser/add-project-flow.spec.ts +++ b/packages/app/e2e/browser/add-project-flow.spec.ts @@ -13,6 +13,7 @@ import { expectAddProjectPage, expectNewWorkspaceForAddedProject, openAddProjectFlow, + openAddProjectHostSelection, } from "../support/helpers/add-project-flow"; import { gotoAppShell } from "../support/helpers/app"; import { @@ -65,12 +66,14 @@ async function expectProjectHasNoWorkspaces(projectId: string): Promise { test.describe("Add Project command-center flow", () => { test.describe.configure({ timeout: 180_000 }); - test("a single connected host opens directly on method selection", async ({ page }) => { + test("method selection has no search field", async ({ page }) => { await gotoAppShell(page); await openAddProjectFlow(page); await expect(addProjectFlowMethod(page, "directory-search")).toBeVisible(); + await expect(addProjectFlowInput(page)).toHaveCount(0); + await expect(addProjectFlow(page).getByRole("textbox")).toHaveCount(0); await expect(page.getByTestId("add-project-flow-page-host")).toHaveCount(0); }); @@ -146,7 +149,7 @@ test.describe("Add Project command-center flow", () => { serverId: SECONDARY_HOST_ID, endpoint: `localhost:${secondaryHost.port}`, }); - await openAddProjectFlow(page, "host"); + await openAddProjectHostSelection(page); await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); @@ -155,7 +158,7 @@ test.describe("Add Project command-center flow", () => { await expect(addProjectFlow(page)).toContainText(SECONDARY_HOST_LABEL); }); - test("Escape and Back restore page input and active selection before closing at the root", async ({ + test("Escape and Back restore searchable page input before closing at the root", async ({ page, }) => { await gotoAppShell(page); @@ -168,21 +171,20 @@ test.describe("Add Project command-center flow", () => { serverId: SECONDARY_HOST_ID, endpoint: `localhost:${secondaryHost.port}`, }); - await openAddProjectFlow(page, "host"); + await openAddProjectHostSelection(page); await addProjectFlowInput(page).fill("o"); await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); await expectAddProjectPage(page, "method"); - await addProjectFlowInput(page).fill("new"); - await page.keyboard.press("Enter"); + await chooseAddProjectMethod(page, "new-directory"); await expectAddProjectPage(page, "new-directory-parent"); await page.keyboard.press("Escape"); await expectAddProjectPage(page, "method"); - await expect(addProjectFlowInput(page)).toHaveValue("new"); - await page.keyboard.press("Enter"); + await expect(addProjectFlowInput(page)).toHaveCount(0); + await chooseAddProjectMethod(page, "new-directory"); await expectAddProjectPage(page, "new-directory-parent"); await addProjectFlowBack(page).click(); @@ -216,7 +218,7 @@ test.describe("Add Project command-center flow", () => { serverId: SECONDARY_HOST_ID, endpoint: `localhost:${secondaryHost.port}`, }); - await openAddProjectFlow(page, "host"); + await openAddProjectHostSelection(page); await addProjectFlowHost(page, SECONDARY_HOST_ID).click(); await expectAddProjectPage(page, "method"); diff --git a/packages/app/e2e/browser/provider-settings-refresh.spec.ts b/packages/app/e2e/browser/provider-settings-refresh.spec.ts index 85a7dabc3..62e8e793b 100644 --- a/packages/app/e2e/browser/provider-settings-refresh.spec.ts +++ b/packages/app/e2e/browser/provider-settings-refresh.spec.ts @@ -160,7 +160,6 @@ test.describe("provider settings overlay stack", () => { await commandCenter.getByText("Add project", { exact: true }).click(); const addProject = page.getByTestId("add-project-flow"); await expect(addProject).toBeVisible({ timeout: 10_000 }); - await expect(addProject.getByTestId("add-project-flow-input")).toBeFocused(); await page.keyboard.press("Escape"); await expect(addProject).not.toBeVisible({ timeout: 10_000 }); await expect(selector).toBeVisible(); diff --git a/packages/app/e2e/support/helpers/add-project-flow.ts b/packages/app/e2e/support/helpers/add-project-flow.ts index 18e7a0da8..c7b15df45 100644 --- a/packages/app/e2e/support/helpers/add-project-flow.ts +++ b/packages/app/e2e/support/helpers/add-project-flow.ts @@ -43,13 +43,21 @@ export async function expectAddProjectPage(page: Page, kind: AddProjectFlowPage) return currentPage; } -export async function openAddProjectFlow( +async function openAddProjectFlowSurface( page: Page, - expectedPage: "host" | "method" = "method", + expectedPage: "host" | "method", ): Promise { await page.getByTestId("sidebar-add-project").click(); await expect(addProjectFlow(page)).toBeVisible({ timeout: 30_000 }); await expectAddProjectPage(page, expectedPage); +} + +export async function openAddProjectFlow(page: Page): Promise { + await openAddProjectFlowSurface(page, "method"); +} + +export async function openAddProjectHostSelection(page: Page): Promise { + await openAddProjectFlowSurface(page, "host"); await expect(addProjectFlowInput(page)).toBeFocused(); } diff --git a/packages/app/src/add-project-flow/model.test.ts b/packages/app/src/add-project-flow/model.test.ts index ff496f53c..63014a366 100644 --- a/packages/app/src/add-project-flow/model.test.ts +++ b/packages/app/src/add-project-flow/model.test.ts @@ -39,9 +39,9 @@ describe("Add Project navigation", () => { expect(currentAddProjectPage(state)).toEqual({ kind: "method", hostId: "host-1", - query: "", activeIndex: 0, error: null, + isSubmitting: false, }); expect(backAddProjectPage(state)).toBeNull(); }); diff --git a/packages/app/src/add-project-flow/model.ts b/packages/app/src/add-project-flow/model.ts index 3cfd7691a..0de21d152 100644 --- a/packages/app/src/add-project-flow/model.ts +++ b/packages/app/src/add-project-flow/model.ts @@ -17,15 +17,18 @@ export interface GithubRepositoryChoice { updatedAt: string | null; } -interface SearchPageState { - query: string; +interface PageState { activeIndex: number; error: string | null; } +interface SearchPageState extends PageState { + query: string; +} + export type AddProjectPage = | ({ kind: "host" } & SearchPageState) - | ({ kind: "method"; hostId: string } & SearchPageState) + | ({ kind: "method"; hostId: string; isSubmitting: boolean } & PageState) | ({ kind: "directory-search"; hostId: string; isSubmitting: boolean } & SearchPageState) | ({ kind: "github-search"; hostId: string } & SearchPageState) | ({ @@ -57,12 +60,14 @@ export interface OpenAddProjectFlowInput { preferredHostId?: string; } -function searchPage(kind: TKind) { +type SearchPageKind = Extract["kind"]; + +function searchPage(kind: TKind) { return { kind, query: "", activeIndex: 0, error: null } as const; } function methodPage(hostId: string): AddProjectPage { - return { ...searchPage("method"), hostId }; + return { kind: "method", hostId, activeIndex: 0, error: null, isSubmitting: false }; } export function openAddProjectFlow(input: OpenAddProjectFlowInput): AddProjectFlowState { @@ -209,6 +214,7 @@ export function setAddProjectPageInput( if (current.kind === "new-directory-name") { return { ...current, name: value, activeIndex: 0, error: null }; } + if (current.kind === "method") return current; return { ...current, query: value, activeIndex: 0, error: null }; }); if (page.kind !== "github-location") return updated; diff --git a/packages/app/src/components/add-project-flow.tsx b/packages/app/src/components/add-project-flow.tsx index 5bcaab0dc..99fe6c2e9 100644 --- a/packages/app/src/components/add-project-flow.tsx +++ b/packages/app/src/components/add-project-flow.tsx @@ -69,7 +69,7 @@ import { import { Shortcut } from "@/components/ui/shortcut"; import { useKeyboardShortcutsAvailable } from "@/keyboard/availability"; import { getIsElectronRuntime } from "@/constants/layout"; -import { isWeb } from "@/constants/platform"; +import { isNative, isWeb } from "@/constants/platform"; import { pickDirectory } from "@/desktop/pick-directory"; import { useFetchQuery } from "@/data/query"; import { getOpenProjectFailureReason, registerProjectDescriptor } from "@/hooks/open-project"; @@ -222,12 +222,12 @@ function pageTitle(page: AddProjectPage): string { } } -function pagePlaceholder(page: AddProjectPage): string { +type AddProjectInputPage = Exclude; + +function pagePlaceholder(page: AddProjectInputPage): string { switch (page.kind) { case "host": return "Search hosts..."; - case "method": - return "Search methods..."; case "directory-search": return "Search directories or enter a path..."; case "github-search": @@ -240,7 +240,7 @@ function pagePlaceholder(page: AddProjectPage): string { } } -function pageInput(page: AddProjectPage): string { +function pageInput(page: AddProjectInputPage): string { return page.kind === "new-directory-name" ? page.name : page.query; } @@ -374,7 +374,7 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { const inputRef = useRef(null); const submissionInFlightRef = useRef(false); const browseInFlightRef = useRef(false); - const query = page.kind === "new-directory-name" ? "" : page.query; + const query = page.kind === "new-directory-name" || page.kind === "method" ? "" : page.query; const [debouncedQuery, setDebouncedQuery] = useState(query); useEffect(() => { @@ -599,23 +599,15 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { } if (page.kind === "method") { if (!host) return []; - const normalized = page.query.trim().toLowerCase(); - return buildAddProjectMethods(host) - .filter( - (method) => - !normalized || - method.label.toLowerCase().includes(normalized) || - method.description.toLowerCase().includes(normalized), - ) - .map((method) => ({ - id: method.id, - title: method.label, - subtitle: method.description, - icon: methodIcon(method.id), - disabled: method.disabled, - testID: `add-project-flow-method-${method.id}`, - select: () => selectMethod(method.id), - })); + return buildAddProjectMethods(host).map((method) => ({ + id: method.id, + title: method.label, + subtitle: method.description, + icon: methodIcon(method.id), + disabled: method.disabled, + testID: `add-project-flow-method-${method.id}`, + select: () => selectMethod(method.id), + })); } if (page.kind === "directory-search") { return pathOptions.map((option) => { @@ -863,21 +855,41 @@ export function AddProjectFlow({ request, onClose }: AddProjectFlowProps) { ) : null} - + {page.kind === "method" && isNative ? ( + // Native hardware-keyboard events need a focused responder even without a visible field. + + ) : null} + {page.kind !== "method" ? ( + + ) : null} ({ paddingVertical: theme.spacing[1], outlineStyle: "none", } as object, + keyboardCapture: { + position: "absolute", + width: 1, + height: 1, + opacity: 0, + }, results: { flexGrow: 0, flexShrink: 1, minHeight: 0 }, resultsContent: { paddingVertical: theme.spacing[2] }, row: {