From a80ca68c9639976bb8318dd775137ea1cd54c46a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:51:02 +0800 Subject: [PATCH] fix(app): focus session search when the Home shortcut opens Home (#53761) Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com> --- packages/app/e2e/regression/home.spec.ts | 31 ++++++++++++++++++-- packages/app/src/home/sessions/search.ts | 17 +++++++++-- packages/app/src/shell/state/layout.tsx | 14 +++++++++ packages/app/src/shell/titlebar/titlebar.tsx | 5 +++- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/packages/app/e2e/regression/home.spec.ts b/packages/app/e2e/regression/home.spec.ts index 87a8471e915..ddc15b83842 100644 --- a/packages/app/e2e/regression/home.spec.ts +++ b/packages/app/e2e/regression/home.spec.ts @@ -1,9 +1,9 @@ import { expect, test, type Page } from "@playwright/test" -import { holdRoute, NO_PROVIDER, project, REMOTE_SERVER, seed, sessionHref } from "../utils/app" +import { expectPath, holdRoute, NO_PROVIDER, project, REMOTE_SERVER, seed, sessionHref } from "../utils/app" import { mockOpenCodeServer } from "../utils/mock-server" import { fixture, mockStressTimeline } from "../utils/session-fixture" import { mockRemoteServer } from "../utils/workspace" -import { expectAppVisible } from "../utils/waits" +import { APP_READY_TIMEOUT, expectAppVisible } from "../utils/waits" test.use({ serviceWorkers: "block" }) @@ -77,6 +77,33 @@ test("the session context menu renames, exports, and deletes a Home session", as await expect(renamedRow).toBeHidden() }) +test("the Home shortcut focuses session search, and the Home button leaves focus alone", async ({ page }) => { + await mockStressTimeline(page) + await seed(page, { + projects: { local: [{ worktree: fixture.directory, expanded: true }] }, + lastProject: { local: fixture.directory }, + tabs: [fixture.sourceID], + }) + await page.goto(sessionHref(fixture.sourceID)) + const editor = page.locator('[data-component="composer-editor"]') + await expect(editor).toBeEditable({ timeout: APP_READY_TIMEOUT }) + await editor.click() + + await page.keyboard.press("ControlOrMeta+b") + const search = page.getByRole("textbox", { name: /Search sessions/ }) + await expect(search).toBeFocused() + await page.keyboard.type("jump") + await expect(page.getByRole("option")).toHaveCount(1) + await expect(page.getByRole("option")).toContainText(fixture.expected.targetTitle) + + await page.keyboard.press("ControlOrMeta+b") + await expectPath(page, sessionHref(fixture.sourceID)) + + await page.getByRole("button", { name: "Home", exact: true }).click() + await expect(search).toHaveValue("") + await expect(search).not.toBeFocused() +}) + test("Home shows loaded sessions before the location request resolves", async ({ page }) => { const location = await holdRoute(page, (url) => url.pathname === "/api/location") await openHome(page) diff --git a/packages/app/src/home/sessions/search.ts b/packages/app/src/home/sessions/search.ts index 649570766e8..9898a21bc1d 100644 --- a/packages/app/src/home/sessions/search.ts +++ b/packages/app/src/home/sessions/search.ts @@ -1,10 +1,11 @@ import { useCommand } from "@/shell/commands/command" +import { useLayout } from "@/shell/state/layout" import { useLanguage } from "@/runtime/i18n/language" import { serverName } from "@/runtime/server/registry" import { displayName } from "@opencode/ui/project-avatar" import { sessionLabel } from "@/session/title" import { makeEventListener } from "@solid-primitives/event-listener" -import { createMemo, onCleanup } from "solid-js" +import { createMemo, onCleanup, onMount } from "solid-js" import { createStore } from "solid-js/store" import type { HomeController } from "../model" import { homeSessionSearchKey, type HomeSessionRecord, type HomeSessionsController } from "./controller" @@ -15,12 +16,19 @@ type HomeSessionSearchSource = Pick export function createHomeSessionSearchController(home: HomeController, sessions: HomeSessionSearchSource) { const command = useCommand() const language = useLanguage() + const layout = useLayout() - const [state, setState] = createStore({ + const [state, setState] = createStore<{ + value: string + focused: boolean + highlighted: string + exact: HomeSessionRecord | undefined + lookingUp: boolean + }>({ value: "", focused: false, highlighted: "", - exact: undefined as HomeSessionRecord | undefined, + exact: undefined, lookingUp: false, }) @@ -78,6 +86,9 @@ export function createHomeSessionSearchController(home: HomeController, sessions }), ) onCleanup(() => lookup++) + onMount(() => { + if (layout.home.searchFocus.take()) focus() + }) command.register("home.search", () => [ { diff --git a/packages/app/src/shell/state/layout.tsx b/packages/app/src/shell/state/layout.tsx index fc9f001451d..e696fc1a751 100644 --- a/packages/app/src/shell/state/layout.tsx +++ b/packages/app/src/shell/state/layout.tsx @@ -214,6 +214,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( sessionTabPreview: {}, }) + // Opening Home with its shortcut focuses session search, so typing filters immediately. + let homeSearchFocus = false + // Names of other session-scoped stores, e.g. extension storage, so pruning drops them with the layout state. const [scoped, setScoped, , scopedReady] = persisted( Persist.global("layout.scoped"), @@ -355,6 +358,17 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext( setSelection(selection: HomeProjectSelection) { setStore("home", "selection", reconcile(selection)) }, + searchFocus: { + request() { + homeSearchFocus = true + }, + take() { + const requested = homeSearchFocus + homeSearchFocus = false + + return requested + }, + }, }, sessionState: { /** Records a session-scoped store, so pruning a session's layout state drops it too. */ diff --git a/packages/app/src/shell/titlebar/titlebar.tsx b/packages/app/src/shell/titlebar/titlebar.tsx index 01fd6a1241e..f3780f58e3c 100644 --- a/packages/app/src/shell/titlebar/titlebar.tsx +++ b/packages/app/src/shell/titlebar/titlebar.tsx @@ -452,7 +452,10 @@ export function Titlebar(props: { verticalTabs?: { mount?: HTMLElement } }) { category: language.t("command.category.view"), keybind: windows() ? "alt+home" : "mod+b", hidden: true, - onSelect: toggleHome, + onSelect: () => { + if (layout.route().type !== "home") layout.home.searchFocus.request() + toggleHome() + }, }, ])