mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 04:27:20 +03:00
fix(app): follow the first palette result until explicit selection (#54275)
This commit is contained in:
committed by
GitHub
Unverified
parent
0d61729b3f
commit
e31136bdbd
@@ -85,13 +85,52 @@ test("appends search results without resetting the selected command", async ({ p
|
||||
await expect(dialog.getByRole("option", { name: "Copy Project ID", exact: true })).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("keeps the automatically selected file when session results arrive later", async ({ page }) => {
|
||||
const warnings = captureConsoleWarnings(page)
|
||||
test("selects the first session when it arrives above automatically selected files", async ({ page }) => {
|
||||
const { dialog, input } = await openCommandPalette(page)
|
||||
const sessions = Promise.withResolvers<void>()
|
||||
await page.route("**/api/fs/find?*", (route) =>
|
||||
route.fulfill({ json: { data: [{ path: "README.md", type: "file" }] } }),
|
||||
)
|
||||
await page.route("**/api/session?*", async (route) => {
|
||||
await sessions.promise
|
||||
await route.fulfill({
|
||||
json: {
|
||||
data: Array.from({ length: 18 }, (_, index) => ({
|
||||
...paletteSession,
|
||||
id: index === 0 ? paletteSession.id : `ses_palette_readme_${index}`,
|
||||
location: { directory: paletteSession.directory },
|
||||
title: `README work ${index + 1}`,
|
||||
})),
|
||||
},
|
||||
})
|
||||
})
|
||||
await input.fill("README")
|
||||
await expect(dialog.getByRole("option", { name: "/ README.md", exact: true })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
)
|
||||
sessions.resolve()
|
||||
const first = dialog.getByRole("option", { name: /README work 1 command-palette/ })
|
||||
const second = dialog.getByRole("option", { name: /README work 2 command-palette/ })
|
||||
await expect(first).toHaveAttribute("aria-selected", "true")
|
||||
await expect(first).toBeInViewport()
|
||||
await input.press("ArrowDown")
|
||||
await expect(second).toHaveAttribute("aria-selected", "true")
|
||||
await expect(second).toBeInViewport()
|
||||
await input.press("ArrowUp")
|
||||
await expect(first).toHaveAttribute("aria-selected", "true")
|
||||
await input.press("Enter")
|
||||
await expect(dialog).toHaveCount(0)
|
||||
await expect(page.getByRole("heading", { name: paletteSession.title, exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test("keeps a keyboard-selected file when session results arrive later", async ({ page }) => {
|
||||
const warnings = captureConsoleWarnings(page)
|
||||
const { dialog, input } = await openCommandPalette(page)
|
||||
const sessions = Promise.withResolvers<void>()
|
||||
await page.route("**/api/fs/find?*", (route) =>
|
||||
route.fulfill({ json: { data: [{ path: "README.txt", type: "file" }, { path: "README.md", type: "file" }] } }),
|
||||
)
|
||||
await page.route("**/api/session?*", async (route) => {
|
||||
await sessions.promise
|
||||
await route.fulfill({
|
||||
@@ -102,6 +141,11 @@ test("keeps the automatically selected file when session results arrive later",
|
||||
})
|
||||
await input.fill("README")
|
||||
const file = dialog.getByRole("option", { name: "/ README.md", exact: true })
|
||||
await expect(dialog.getByRole("option", { name: "/ README.txt", exact: true })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
)
|
||||
await input.press("ArrowDown")
|
||||
await expect(file).toHaveAttribute("aria-selected", "true")
|
||||
sessions.resolve()
|
||||
await expect(dialog.getByRole("option", { name: /README work/ })).toBeVisible()
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dialog, DialogBody } from "@opencode/ui/dialog"
|
||||
import { Icon } from "@opencode/ui/icon"
|
||||
import { Keybind } from "@opencode/ui/keybind"
|
||||
import { TextInput } from "@opencode/ui/text-input"
|
||||
import { createEffect, createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { formatKeybindParts } from "@/shell/commands/command"
|
||||
import { useLanguage } from "@/runtime/i18n/language"
|
||||
@@ -70,13 +70,13 @@ export function CommandPaletteView(props: {
|
||||
}) {
|
||||
const language = useLanguage()
|
||||
const tabs = useTabs()
|
||||
const [store, setStore] = createStore({ query: "", active: undefined as string | undefined })
|
||||
const [store, setStore] = createStore<{ query: string; active?: string }>({ query: "" })
|
||||
|
||||
const search = createCommandPaletteSearch({ query: () => store.query, items: props.items, sources: props.sources })
|
||||
const visibleEntries = search.items
|
||||
const groupedEntries = createMemo(() => groups(visibleEntries()))
|
||||
|
||||
// Keep keyboard selection stable when another search source adds results.
|
||||
// Follow the first result until the user explicitly chooses a row.
|
||||
const activeEntry = createMemo(
|
||||
() => visibleEntries().find((entry) => entry.id === store.active) ?? visibleEntries()[0],
|
||||
)
|
||||
@@ -85,28 +85,31 @@ export function CommandPaletteView(props: {
|
||||
() => new Set(tabs.store.flatMap((tab) => (tab.type === "session" ? [`${tab.server}\0${tab.sessionId}`] : []))),
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
// Pin automatic selection too: a later source can insert rows before it.
|
||||
const id = activeEntry()?.id
|
||||
|
||||
if (store.active !== id) setStore("active", id)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.highlight(activeEntry())
|
||||
})
|
||||
|
||||
let resultsRef: HTMLDivElement | undefined
|
||||
|
||||
createEffect(() => {
|
||||
// Incoming results can move an explicitly selected row below the viewport.
|
||||
visibleEntries()
|
||||
|
||||
if (!activeEntry()) return
|
||||
|
||||
const frame = requestAnimationFrame(() => {
|
||||
resultsRef?.querySelector("[data-active]")?.scrollIntoView({ block: "nearest" })
|
||||
})
|
||||
|
||||
onCleanup(() => cancelAnimationFrame(frame))
|
||||
})
|
||||
|
||||
const move = (delta: -1 | 1) => {
|
||||
const count = visibleEntries().length
|
||||
|
||||
if (count === 0) return
|
||||
const index = visibleEntries().findIndex((entry) => entry.id === activeEntry()?.id)
|
||||
setStore("active", visibleEntries()[(index + delta + count) % count].id)
|
||||
requestAnimationFrame(() => {
|
||||
resultsRef?.querySelector("[data-active]")?.scrollIntoView({ block: "nearest" })
|
||||
})
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user