fix(app): keep command palette results steady while searching (#57479)

This commit is contained in:
Kit Langton
2026-10-20 12:05:31 -07:00
committed by GitHub
Unverified
parent 690b47887c
commit da84835eee
8 changed files with 480 additions and 113 deletions
@@ -263,3 +263,118 @@ test("navigation replaces commands without retaining disposed owners", async ({
)
expect(warnings).toEqual([])
})
test("keeps matching session and file results steady across keystrokes while searches are pending", async ({
page,
}) => {
await openPaletteWorkspace(page)
const mac = await page.evaluate(() => /(Mac|iPod|iPhone|iPad)/.test(navigator.platform))
await page.keyboard.press(mac ? "Meta+Shift+P" : "Control+Shift+P")
const dialog = page.getByRole("dialog")
const input = dialog.getByRole("textbox")
await expect(input).toBeFocused()
const sessionOption = dialog.getByRole("option", { name: /Palette fixture session/ })
const matchingFile = dialog.getByRole("option", { name: "packages/desktop/ palette.ts", exact: true })
const nonMatchingFile = dialog.getByRole("option", { name: "src/app/ other.ts", exact: true })
await expect(sessionOption).toBeVisible()
const pendingFiles = Promise.withResolvers<void>()
const pendingSessions = Promise.withResolvers<void>()
await page.route("**/api/fs/find?*", async (route) => {
const query = new URL(route.request().url()).searchParams.get("query") ?? ""
if (query !== "p") await pendingFiles.promise
await route.fulfill({
json: {
data:
query === "p"
? [
{ path: "packages/desktop/palette.ts", type: "file" },
{ path: "src/app/other.ts", type: "file" },
]
: [{ path: "packages/desktop/palette.ts", type: "file" }],
},
})
})
await page.route("**/api/session?*", async (route) => {
await pendingSessions.promise
await route.fulfill({
json: {
data: [{ ...paletteSession, location: { directory: paletteSession.directory } }],
},
})
})
await input.press("p")
await expect(matchingFile).toBeVisible()
await expect(nonMatchingFile).toBeVisible()
await expect(sessionOption).toBeVisible()
const probe = await page.evaluateHandle(() => {
const results = document.querySelector(".command-palette-results")!
const state = {
emptyFrames: 0,
initialSessionEl:
Array.from(results.querySelectorAll('[role="option"]')).find((el) =>
el.textContent?.includes("Palette fixture session"),
) ?? null,
initialFileEl:
Array.from(results.querySelectorAll('[role="option"]')).find((el) => el.textContent?.includes("palette.ts")) ??
null,
}
const observer = new MutationObserver(() => {
if (results.querySelectorAll('[role="option"]').length === 0 || results.querySelector(".command-palette-state")) {
state.emptyFrames++
}
})
observer.observe(results, { childList: true, subtree: true })
return {
finish() {
observer.disconnect()
const currentSessionEl = Array.from(results.querySelectorAll('[role="option"]')).find((el) =>
el.textContent?.includes("Palette fixture session"),
)
const currentFileEl = Array.from(results.querySelectorAll('[role="option"]')).find((el) =>
el.textContent?.includes("palette.ts"),
)
return {
emptyFrames: state.emptyFrames,
sameSessionNode: Boolean(state.initialSessionEl && state.initialSessionEl === currentSessionEl),
sameFileNode: Boolean(state.initialFileEl && state.initialFileEl === currentFileEl),
}
},
}
})
for (const key of "alette") {
await input.press(key)
await expect(sessionOption).toBeVisible()
await expect(matchingFile).toBeVisible()
await expect(nonMatchingFile).toHaveCount(0)
}
await expect(sessionOption).toHaveAttribute("aria-selected", "true")
pendingFiles.resolve()
pendingSessions.resolve()
await expect(sessionOption).toBeVisible()
await expect(matchingFile).toBeVisible()
const stability = await probe.evaluate((tracker) => tracker.finish())
expect(stability).toEqual({
emptyFrames: 0,
sameSessionNode: true,
sameFileNode: true,
})
})
@@ -6,6 +6,7 @@ import { useLanguage } from "@/runtime/i18n/language"
import { ServerConnection } from "@/runtime/server/registry"
import {
createCommandPaletteCommandEntry,
createKnownServerSessions,
createServerSessionEntries,
type CommandPaletteEntry,
type PaletteHighlight,
@@ -31,6 +32,7 @@ export function HomeCommandPalette(props: {
const sessions = createServerSessionEntries({
server: ServerConnection.key(props.server),
known: createKnownServerSessions(server),
projects: server.projects.list,
load: (search, signal) => server.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
get: (sessionID, signal) => server.sdk.api.session.get({ sessionID }, { signal }),
@@ -63,9 +65,12 @@ export function HomeCommandPalette(props: {
}
const items = (query: string) => {
if (!query) return commandEntries().slice(0, 5)
if (!query) return [...commandEntries().slice(0, 5), ...sessions.items("")]
return commandEntries().filter((entry) => matchesCommandPaletteEntry(entry, query))
return [
...commandEntries().filter((entry) => matchesCommandPaletteEntry(entry, query)),
...sessions.items(query),
]
}
onCleanup(() => {
@@ -77,7 +82,7 @@ export function HomeCommandPalette(props: {
<CommandPaletteView
placeholder={language.t("palette.search.placeholder.home")}
items={items}
sources={[sessions]}
sources={[sessions.search]}
highlight={highlight}
select={select}
close={() => dialog.close()}
+72 -45
View File
@@ -13,32 +13,25 @@ import { useTabs } from "@/shell/tabs/tabs"
import { SessionTabAvatar } from "@/shell/layout/session-tab-avatar"
import { getRelativeTime } from "@/shell/time"
import { createCommandPaletteFileEntry, createCommandPaletteModel, type CommandPaletteEntry } from "./palette"
import { createCommandPaletteSearch } from "./search"
import { createCommandPaletteSearch, matchesCommandPaletteEntry } from "./search"
import "./dialog.css"
function groups(entries: CommandPaletteEntry[]) {
const map = new Map<string, CommandPaletteEntry[]>()
export { matchesCommandPaletteEntry }
for (const entry of entries) map.set(entry.category, [...(map.get(entry.category) ?? []), entry])
return Array.from(map.entries()).map(([category, entries]) => ({ category, entries }))
}
export function matchesCommandPaletteEntry(entry: CommandPaletteEntry, query: string) {
const value = query.toLowerCase()
return [entry.title, entry.description, entry.category, entry.keywords].some((text) =>
text?.toLowerCase().includes(value),
)
function groupKey(entry: CommandPaletteEntry) {
return entry.type === "session" ? "session" : `${entry.type}:${entry.category}`
}
export function DialogCommandPalette(props: { onOpenFile?: (path: string) => void }) {
const palette = createCommandPaletteModel(props)
const items = (q: string) => {
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries()]
if (!q) return [...palette.preferredCommandEntries(), ...palette.recentFileEntries(), ...palette.sessions.items("")]
return palette.commandEntries().filter((entry) => matchesCommandPaletteEntry(entry, q))
return [
...palette.commandEntries().filter((entry) => matchesCommandPaletteEntry(entry, q)),
...palette.sessions.items(q),
]
}
return (
@@ -46,9 +39,9 @@ export function DialogCommandPalette(props: { onOpenFile?: (path: string) => voi
placeholder={palette.language.t("palette.search.placeholder")}
items={items}
sources={[
palette.sessions,
palette.sessions.search,
async (query, signal) => {
if (!query) return []
if (!query) return palette.recentFileEntries()
const files = await palette.file.searchFiles(query, { signal })
const category = palette.language.t("palette.group.files")
@@ -76,7 +69,13 @@ export function CommandPaletteView(props: {
const search = createCommandPaletteSearch({ query: () => store.query, items: props.items, sources: props.sources })
const visibleEntries = search.items
const groupedEntries = createMemo(() => groups(visibleEntries()))
const entryById = createMemo(() => new Map(visibleEntries().map((entry) => [entry.id, entry] as const)))
const groupKeys = createMemo<string[]>((prev) => {
const next = [...new Set(visibleEntries().map(groupKey))]
return prev && prev.length === next.length && prev.every((key, index) => key === next[index]) ? prev : next
})
// Follow the first result until the user explicitly chooses a row.
const activeEntry = createMemo(
@@ -154,7 +153,14 @@ export function CommandPaletteView(props: {
appearance="large"
placeholder={props.placeholder}
leadingIcon={<Icon name="magnifying-glass" />}
onInput={(event) => setStore({ query: event.currentTarget.value, active: undefined })}
onInput={(event) => {
const query = event.currentTarget.value
const active = store.active ? entryById().get(store.active) : undefined
setStore({
query,
active: active && matchesCommandPaletteEntry(active, query) ? store.active : undefined,
})
}}
onKeyDown={handleKeyDown}
/>
</div>
@@ -164,34 +170,55 @@ export function CommandPaletteView(props: {
when={visibleEntries().length > 0}
fallback={
<div class="command-palette-state">
{search.loading() ? language.t("common.loading") : language.t("palette.empty")}
{search.loading()
? language.t("common.loading")
: search.failed()
? language.t("common.requestFailed")
: language.t("palette.empty")}
</div>
}
>
<For each={groupedEntries()}>
{(group) => (
<div class="command-palette-group">
<Show when={group.category}>
<div class="command-palette-group-title">{group.category}</div>
</Show>
<For each={group.entries}>
{(item) => (
<PaletteRow
item={item}
active={activeEntry()?.id === item.id}
language={language}
sessionOpen={
item.server && item.sessionID
? openSessions().has(`${item.server}\0${item.sessionID}`)
: false
}
onActive={() => setStore("active", item.id)}
onSelect={() => props.select(item)}
/>
)}
</For>
</div>
)}
<For each={groupKeys()}>
{(key) => {
const entries = createMemo(() => visibleEntries().filter((entry) => groupKey(entry) === key))
const category = createMemo(() => entries()[0]?.category ?? "")
const ids = createMemo<string[]>((prev) => {
const next = entries().map((entry) => entry.id)
return prev && prev.length === next.length && prev.every((id, index) => id === next[index])
? prev
: next
})
return (
<div class="command-palette-group">
<Show when={category()}>
<div class="command-palette-group-title">{category()}</div>
</Show>
<For each={ids()}>
{(id) => {
const item = () => entryById().get(id) ?? entries()[0]!
return (
<PaletteRow
item={item()}
active={activeEntry()?.id === id}
language={language}
sessionOpen={
item().server && item().sessionID
? openSessions().has(`${item().server}\0${item().sessionID}`)
: false
}
onActive={() => setStore("active", id)}
onSelect={() => props.select(item())}
/>
)
}}
</For>
</div>
)
}}
</For>
</Show>
</div>
+98 -33
View File
@@ -2,10 +2,12 @@ import { getFilename } from "@opencode/util/path"
import type { Project } from "@/runtime/server/types"
import type { SessionInfo } from "@opencode/client/promise"
import { useDialog } from "@opencode/ui/context/dialog"
import { useQueryClient } from "@tanstack/solid-query"
import { createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { commandPaletteOptions, useCommand, type CommandOption } from "@/shell/commands/command"
import { useFile } from "@/workspaces/files/model"
import { useGlobal } from "@/runtime/server/runtime"
import { useGlobal, type ServerCtx } from "@/runtime/server/runtime"
import { useLanguage } from "@/runtime/i18n/language"
import type { LocalProject } from "@/shell/state/layout"
import { ServerConnection } from "@/runtime/server/registry"
@@ -16,6 +18,7 @@ import { projectForSession } from "@/shell/layout/helpers"
import { useExtensionHost } from "@/runtime/extension/host"
import { useExtensionAttachment } from "@/runtime/extension/host-apis"
import { looksLikeSessionID } from "@/session/search"
import { matchesCommandPaletteEntry } from "./search"
export type CommandPaletteEntry = {
id: string
@@ -143,6 +146,7 @@ export function createCommandPaletteModel(props: { filesOnly?: () => boolean; on
const sessions = createServerSessionEntries({
server: ServerConnection.key(serverSDK.server),
known: createKnownServerSessions(serverCtx),
projects: serverCtx.projects.list,
load: (search, signal) => serverSDK.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
get: (sessionID, signal) => serverSDK.api.session.get({ sessionID }, { signal }),
@@ -223,8 +227,13 @@ export function createCommandPaletteCommandEntry(option: CommandOption, category
}
}
type SessionSearchState = {
abort?: AbortController
}
export function createServerSessionEntries(props: {
server: ServerConnection.Key
known?: () => SessionInfo[]
projects: () => Project[]
load: (search: string, signal: AbortSignal) => Promise<{ data: SessionInfo[] }>
get: (sessionID: string, signal: AbortSignal) => Promise<SessionInfo>
@@ -233,16 +242,54 @@ export function createServerSessionEntries(props: {
recentCategory: () => string
recentLimit?: number
}) {
let abort: AbortController | undefined
const state: SessionSearchState = {}
const [store, setStore] = createStore<{ loaded: SessionInfo[] }>({ loaded: [] })
onCleanup(() => abort?.abort())
// Let the newest metadata win duplicates before ranking the merged sessions newest-first.
const known = createMemo(() =>
[
...new Map(
[...(props.known?.() ?? []), ...store.loaded]
.toSorted((a, b) => a.time.updated - b.time.updated)
.map((session) => [session.id, session] as const),
).values(),
].toSorted((a, b) => b.time.updated - a.time.updated),
)
return async (text: string): Promise<CommandPaletteEntry[]> => {
onCleanup(() => state.abort?.abort())
const entries = (sessions: SessionInfo[], search: string): CommandPaletteEntry[] => {
const projects = props.projects()
const category = search ? props.category() : props.recentCategory()
return sessions.flatMap((session) => {
if (session.time.archived) return []
const project = projectForSession(session, projects)
return [
{
id: `session:${props.server}:${session.id}`,
type: "session" as const,
title: session.title || props.untitled(),
description: project ? displayName(project) : getFilename(session.location.directory),
category,
directory: session.location.directory,
sessionID: session.id,
server: props.server,
project,
updated: session.time.updated,
},
]
})
}
const search = async (text: string): Promise<CommandPaletteEntry[]> => {
const search = text.trim()
abort?.abort()
state.abort?.abort()
const current = new AbortController()
abort = current
state.abort = current
// Typed searches wait for a pause; an empty query lists recent sessions right away.
await new Promise<void>((resolve) => {
@@ -258,13 +305,11 @@ export function createServerSessionEntries(props: {
})
if (current.signal.aborted) return []
const projects = props.projects()
const category = search ? props.category() : props.recentCategory()
const entries = await Promise.all([
const [listed, exact] = await Promise.all([
props.load(search, current.signal).then(
(result) => result.data,
() => [],
(result) => ({ items: result.data, failed: false }),
() => ({ items: [], failed: true }),
),
looksLikeSessionID(search)
? props.get(search, current.signal).then(
@@ -272,31 +317,51 @@ export function createServerSessionEntries(props: {
() => [],
)
: Promise.resolve([]),
]).then(([listed, exact]) =>
[...new Map([...exact, ...listed].map((session) => [session.id, session] as const)).values()].flatMap(
(session) => {
if (session.time.archived) return []
])
const project = projectForSession(session, projects)
if (current.signal.aborted) return []
return [
{
id: `session:${props.server}:${session.id}`,
type: "session" as const,
title: session.title || props.untitled(),
description: project ? displayName(project) : getFilename(session.location.directory),
category,
directory: session.location.directory,
sessionID: session.id,
server: props.server,
project,
updated: session.time.updated,
},
]
},
),
if (listed.failed && exact.length === 0) throw new Error("Session search failed")
const loaded = [...new Map([...exact, ...listed.items].map((session) => [session.id, session] as const)).values()]
// Keep names learned while this palette is open available to subsequent keystrokes.
setStore("loaded", (previous) => [
...new Map([...previous, ...loaded].map((session) => [session.id, session] as const)).values(),
])
// The local collection owns the recent rows, including newly loaded ones.
return search ? entries(loaded, search) : []
}
return Object.assign(search, {
search,
items(text: string) {
const search = text.trim()
const all = entries(known(), search)
if (!search) return all.slice(0, props.recentLimit ?? ENTRY_LIMIT)
return all.filter((entry) => matchesCommandPaletteEntry(entry, search)).slice(0, 50)
},
})
}
export function createKnownServerSessions(server: ServerCtx) {
const queryClient = useQueryClient()
return () => {
// Reuse the Home index already loaded before opening the palette; do not fetch an index for search.
const cached =
queryClient.getQueryData<SessionInfo[]>(["home-sessions", server.sdk.server]) ??
queryClient.getQueryData<SessionInfo[]>(["home-sessions", ServerConnection.key(server.sdk.server)]) ??
[]
const merged = new Map(
[...cached, ...server.data.session.list()].map((session) => [session.id, session] as const),
)
return search ? entries : entries.slice(0, props.recentLimit ?? ENTRY_LIMIT)
return server.data.session.apply(
[...merged.values()].filter((session) => !session.parentID && !session.time.archived),
)
}
}
+88 -20
View File
@@ -1,6 +1,29 @@
import { createMemo, createResource, onCleanup } from "solid-js"
import { uniqueCommandPaletteEntries, type CommandPaletteEntry } from "./palette"
type SourceResult = {
query: string
items: CommandPaletteEntry[]
failed: boolean
}
type SourceState = {
abort?: AbortController
seen: CommandPaletteEntry[]
}
const emptyResult: SourceResult = { query: "", items: [], failed: false }
export function matchesCommandPaletteEntry(entry: CommandPaletteEntry, query: string) {
const value = query.trim().toLowerCase()
if (!value) return true
return [entry.title, entry.description, entry.category, entry.keywords, entry.path, entry.sessionID].some((text) =>
text?.toLowerCase().includes(value),
)
}
export function createCommandPaletteSearch(props: {
query: () => string
items: (query: string) => CommandPaletteEntry[]
@@ -10,37 +33,82 @@ export function createCommandPaletteSearch(props: {
const local = createMemo(() => props.items(query()))
const sources = props.sources.map((load) => {
let abort: AbortController | undefined
onCleanup(() => abort?.abort())
const state: SourceState = { seen: [] }
onCleanup(() => state.abort?.abort())
const [result] = createResource(
const [result] = createResource<SourceResult, string>(
query,
async (query) => {
abort?.abort()
async (query, info) => {
state.abort?.abort()
const current = new AbortController()
abort = current
state.abort = current
const previous = info.value ?? emptyResult
return { query, items: await load(query, current.signal).catch(() => []) }
return load(query, current.signal).then(
(items) => {
if (current.signal.aborted) return previous
if (query && items.length > 0) {
state.seen = uniqueCommandPaletteEntries([...items, ...state.seen])
}
return { query, items, failed: false }
},
() => (current.signal.aborted ? previous : { query, items: [], failed: true }),
)
},
// Remote searches must not suspend the dialog's local results on first render.
{ initialValue: { query: "", items: [] as CommandPaletteEntry[] } },
{ initialValue: emptyResult },
)
return result
return { result, state }
})
const loading = () => sources.some((source) => source.result.loading)
const items = createMemo<CommandPaletteEntry[]>(() => {
const current = query()
const entries = uniqueCommandPaletteEntries([
...local(),
...sources.flatMap((source) => {
const result =
source.result.state === "ready" || source.result.state === "refreshing" ? source.result.latest : emptyResult
if (result.query === current) return result.items
if (!current) return []
const matched = uniqueCommandPaletteEntries([...result.items, ...source.state.seen]).filter((item) =>
matchesCommandPaletteEntry(item, current),
)
if (matched.length > 0) return matched
return result.query && current.toLowerCase().startsWith(result.query.toLowerCase()) ? result.items : []
}),
])
if (!current) return entries
const order = { command: 0, session: 1, file: 2 }
return entries.toSorted(
(a, b) =>
order[a.type] - order[b.type] ||
(a.type === "session" && b.type === "session" ? (b.updated ?? 0) - (a.updated ?? 0) : 0),
)
})
return {
items: createMemo(() =>
uniqueCommandPaletteEntries([
...local(),
...sources.flatMap((source) => {
// Never keep results for an older query selectable while the next one loads.
const result = source.latest
items,
loading,
failed: () =>
sources.some((source) => {
const result =
source.result.state === "ready" || source.result.state === "refreshing" ? source.result.latest : emptyResult
return result.query === query() ? result.items : []
}),
]),
),
loading: () => sources.some((source) => source.loading),
return result.query === query() && result.failed
}),
}
}
@@ -4,7 +4,7 @@ import { useGlobal } from "@/runtime/server/runtime"
import { useLanguage } from "@/runtime/i18n/language"
import { ServerConnection, useServers } from "@/runtime/server/registry"
import { useTabs } from "@/shell/tabs/tabs"
import { createServerSessionEntries, type CommandPaletteEntry } from "./palette"
import { createKnownServerSessions, createServerSessionEntries, type CommandPaletteEntry } from "./palette"
import { CommandPaletteView } from "./dialog"
const RECENT_LIMIT = 10
@@ -15,14 +15,17 @@ export function DialogSessionSearch() {
const language = useLanguage()
const tabs = useTabs()
const registry = useServers()
const servers = registry.visible.filter(
(conn) => global.servers.health[ServerConnection.key(conn)]?.healthy !== false,
)
const loaders = servers.map((conn) => {
const ctx = global.ensureServerCtx(conn)
const load = createServerSessionEntries({
server: ServerConnection.key(conn),
known: createKnownServerSessions(ctx),
projects: ctx.projects.list,
load: (search, signal) => ctx.sdk.api.session.list({ parentID: null, search, limit: 50 }, { signal }),
get: (sessionID, signal) => ctx.sdk.api.session.get({ sessionID }, { signal }),
@@ -34,21 +37,32 @@ export function DialogSessionSearch() {
const tag = registry.tag(conn)
if (servers.length < 2 || !tag) return load
const format = (entries: CommandPaletteEntry[]) =>
servers.length < 2 || !tag
? entries
: entries.map((entry) => ({
...entry,
description: language.t("palette.session.location", { project: entry.description ?? "", server: tag }),
}))
return (query: string) =>
load(query).then((entries) =>
entries.map((entry) => ({
...entry,
description: language.t("palette.session.location", { project: entry.description ?? "", server: tag }),
})),
)
return {
items: (query: string) => format(load.items(query)),
search: (query: string) => load.search(query).then(format),
}
})
const items = (query: string) => {
const entries = loaders
.flatMap((loader) => loader.items(query))
.toSorted((a, b) => (b.updated ?? 0) - (a.updated ?? 0))
return query ? entries : entries.slice(0, RECENT_LIMIT)
}
const sessions = async (query: string) => {
const entries = (await Promise.all(loaders.map((load) => load(query))))
const entries = (await Promise.all(loaders.map((loader) => loader.search(query))))
.flat()
.sort((a, b) => (b.updated ?? 0) - (a.updated ?? 0))
.toSorted((a, b) => (b.updated ?? 0) - (a.updated ?? 0))
return query ? entries : entries.slice(0, RECENT_LIMIT)
}
@@ -68,7 +82,7 @@ export function DialogSessionSearch() {
return (
<CommandPaletteView
placeholder={language.t("home.sessions.search.placeholder")}
items={() => []}
items={items}
sources={[sessions]}
highlight={() => {}}
select={select}
@@ -111,4 +111,36 @@ describe("command palette search", () => {
expect(root.search.items()).toEqual([copy, file])
root.dispose()
})
test("filters previous remote results synchronously while a refined query is pending", async () => {
const first = Promise.withResolvers<CommandPaletteEntry[]>()
const second = Promise.withResolvers<CommandPaletteEntry[]>()
const otherFile: CommandPaletteEntry = { id: "file: readme.md", type: "file", title: "readme.md", category: "Files" }
const ready = Promise.withResolvers<void>()
const root = createRoot((dispose) => {
const [query, setQuery] = createSignal("c")
const search = createCommandPaletteSearch({
query,
items: (q) => (q === "c" ? [copy] : []),
sources: [(q) => (q === "c" ? first.promise : second.promise)],
})
createComputed(() => {
if (search.items().some((entry) => entry.id === file.id)) ready.resolve()
})
return { search, setQuery, dispose }
})
first.resolve([file, otherFile])
await ready.promise
expect(root.search.items()).toEqual([copy, file, otherFile])
root.setQuery("copy")
expect(root.search.loading()).toBe(true)
expect(root.search.items()).toEqual([file])
root.dispose()
})
})
@@ -121,6 +121,47 @@ describe("command palette sessions", () => {
expect(gets).toEqual([exact.id])
expect(result).toMatchObject([{ sessionID: exact.id, title: exact.title }])
})
test("matches known and loaded sessions synchronously while remote search is pending", async () => {
const server = ServerConnection.Key.make("selected-server")
const remote = {
...session,
id: "session-2",
title: "Remote palette session",
time: { created: 2, updated: 4 },
}
await createRoot(async (dispose) => {
try {
const sessions = createServerSessionEntries({
server,
known: () => [session],
projects: () => [project],
load: async () => ({ data: [remote] }),
get: async () => session,
untitled: () => "Untitled",
category: () => "Session",
recentCategory: () => "Recent sessions",
})
expect(sessions.items("")).toMatchObject([
{ sessionID: session.id, title: session.title, category: "Recent sessions" },
])
expect(sessions.items("palette")).toMatchObject([
{ sessionID: session.id, title: session.title, category: "Session" },
])
await sessions.search("remote")
expect(sessions.items("palette")).toMatchObject([
{ sessionID: remote.id, title: remote.title, category: "Session" },
{ sessionID: session.id, title: session.title, category: "Session" },
])
} finally {
dispose()
}
})
})
})
describe("command palette commands", () => {