From 68aa4f8017db71e9e4cc6512414df72af9fd59f1 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:58:35 +0000 Subject: [PATCH] feat(app): show recent sessions in the empty command palette (#53727) Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com> --- .../e2e/regression/command-palette.spec.ts | 10 +++++++++ .../app/src/home/sessions/command-palette.tsx | 4 +++- packages/app/src/runtime/i18n/en.ts | 1 + packages/app/src/shell/commands/palette.ts | 21 ++++++++++--------- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/packages/app/e2e/regression/command-palette.spec.ts b/packages/app/e2e/regression/command-palette.spec.ts index f8a79e4a695..04d22150bdf 100644 --- a/packages/app/e2e/regression/command-palette.spec.ts +++ b/packages/app/e2e/regression/command-palette.spec.ts @@ -38,6 +38,16 @@ test("home commands do not wait for session search", async ({ page }) => { release.resolve() }) +test("lists recent sessions before a query is entered", async ({ page }) => { + const { dialog } = await openCommandPalette(page, true) + const session = dialog.getByRole("option", { name: /Palette fixture session/ }) + await expect(dialog.getByText("Recent sessions", { exact: true })).toBeVisible() + await expect(session).toHaveAttribute("aria-selected", "false") + await session.click() + await expect(dialog).toHaveCount(0) + await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible() +}) + test("appends search results without resetting the selected command", async ({ page }) => { const { dialog, input } = await openCommandPalette(page) const files = Promise.withResolvers() diff --git a/packages/app/src/home/sessions/command-palette.tsx b/packages/app/src/home/sessions/command-palette.tsx index 12354eaf1de..e4f2ece2e46 100644 --- a/packages/app/src/home/sessions/command-palette.tsx +++ b/packages/app/src/home/sessions/command-palette.tsx @@ -8,6 +8,7 @@ import { createCommandPaletteCommandEntry, createServerSessionEntries, type CommandPaletteEntry, + type PaletteHighlight, } from "@/shell/commands/palette" import { CommandPaletteView, matchesCommandPaletteEntry } from "@/shell/commands/dialog" @@ -20,7 +21,7 @@ export function HomeCommandPalette(props: { const global = useGlobal() const language = useLanguage() const server = global.ensureServerCtx(props.server) - const state = { cleanup: undefined as (() => void) | void, committed: false } + const state: PaletteHighlight = { cleanup: undefined, committed: false } const commandEntries = createMemo(() => { const category = language.t("palette.group.commands") @@ -36,6 +37,7 @@ export function HomeCommandPalette(props: { get: (sessionID, signal) => server.sdk.api.session.get({ sessionID }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), + recentCategory: () => language.t("palette.group.recentSessions"), }) const highlight = (item: CommandPaletteEntry | undefined) => { diff --git a/packages/app/src/runtime/i18n/en.ts b/packages/app/src/runtime/i18n/en.ts index 3baa0793ac0..d9f30da5b67 100644 --- a/packages/app/src/runtime/i18n/en.ts +++ b/packages/app/src/runtime/i18n/en.ts @@ -86,6 +86,7 @@ export const dict = { "palette.empty": "No results found", "palette.group.commands": "Commands", "palette.group.files": "Files", + "palette.group.recentSessions": "Recent sessions", "dialog.provider.search.placeholder": "Search providers", "dialog.provider.empty": "No providers found", diff --git a/packages/app/src/shell/commands/palette.ts b/packages/app/src/shell/commands/palette.ts index a1f6fda3a1f..d1470f6f991 100644 --- a/packages/app/src/shell/commands/palette.ts +++ b/packages/app/src/shell/commands/palette.ts @@ -73,7 +73,7 @@ export function createCommandPaletteFileOpener(onOpenFile?: (path: string) => vo } /** The highlighted option's preview cleanup, and whether the palette committed a choice. */ -type PaletteHighlight = { cleanup: (() => void) | void; committed: boolean } +export type PaletteHighlight = { cleanup: (() => void) | void; committed: boolean } export function createCommandPaletteModel(props: { filesOnly?: () => boolean; onOpenFile?: (path: string) => void }) { const command = useCommand() @@ -148,6 +148,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on get: (sessionID, signal) => serverSDK.api.session.get({ sessionID }, { signal }), untitled: () => language.t("command.session.new"), category: () => language.t("command.category.session"), + recentCategory: () => language.t("palette.group.recentSessions"), }) const highlight = (item: CommandPaletteEntry | undefined) => { @@ -232,6 +233,7 @@ export function createServerSessionEntries(props: { get: (sessionID: string, signal: AbortSignal) => Promise untitled: () => string category: () => string + recentCategory: () => string }) { let abort: AbortController | undefined @@ -240,17 +242,13 @@ export function createServerSessionEntries(props: { return async (text: string): Promise => { const search = text.trim() - if (!search) { - abort?.abort() - - return [] - } - abort?.abort() const current = new AbortController() abort = current + + // Typed searches wait for a pause; an empty query lists recent sessions right away. await new Promise((resolve) => { - const timer = setTimeout(resolve, 100) + const timer = setTimeout(resolve, search ? 100 : 0) current.signal.addEventListener( "abort", () => { @@ -264,8 +262,9 @@ export function createServerSessionEntries(props: { if (current.signal.aborted) return [] const opened = props.opened() const stored = props.stored().map((project) => ({ ...project, expanded: false })) + const category = search ? props.category() : props.recentCategory() - return Promise.all([ + const entries = await Promise.all([ props.load(search, current.signal).then( (result) => result.data, () => [], @@ -289,7 +288,7 @@ export function createServerSessionEntries(props: { type: "session" as const, title: session.title || props.untitled(), description: project ? displayName(project) : getFilename(session.location.directory), - category: props.category(), + category, directory: session.location.directory, sessionID: session.id, server: props.server, @@ -300,5 +299,7 @@ export function createServerSessionEntries(props: { }, ), ) + + return search ? entries : entries.slice(0, ENTRY_LIMIT) } }