feat(app): show recent sessions in the empty command palette (#53727)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-10-07 10:58:35 +00:00
committed by GitHub
Unverified
parent 69219470a2
commit 68aa4f8017
4 changed files with 25 additions and 11 deletions
@@ -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<void>()
@@ -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) => {
+1
View File
@@ -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",
+11 -10
View File
@@ -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<SessionInfo>
untitled: () => string
category: () => string
recentCategory: () => string
}) {
let abort: AbortController | undefined
@@ -240,17 +242,13 @@ export function createServerSessionEntries(props: {
return async (text: string): Promise<CommandPaletteEntry[]> => {
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<void>((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)
}
}