Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/app/e2e/regression/command-palette.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>()
Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/home/sessions/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
createCommandPaletteCommandEntry,
createServerSessionEntries,
type CommandPaletteEntry,
type PaletteHighlight,
} from "@/shell/commands/palette"
import { CommandPaletteView, matchesCommandPaletteEntry } from "@/shell/commands/dialog"

Expand All @@ -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")
Expand All @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/runtime/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 11 additions & 10 deletions packages/app/src/shell/commands/palette.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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

Expand All @@ -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",
() => {
Expand All @@ -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,
() => [],
Expand All @@ -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,
Expand All @@ -300,5 +299,7 @@ export function createServerSessionEntries(props: {
},
),
)

return search ? entries : entries.slice(0, ENTRY_LIMIT)
}
}
Loading