mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
feat(app): focus prompt history search (#59626)
This commit is contained in:
committed by
GitHub
Unverified
parent
4ba064f653
commit
d012e07e63
@@ -96,20 +96,22 @@ story(
|
||||
const height = await composer.evaluate((element) => element.getBoundingClientRect().height)
|
||||
|
||||
await input.press("Shift+ArrowUp")
|
||||
await expect(input).toBeFocused()
|
||||
await expect(search).toHaveCount(0)
|
||||
await expect(search).toBeFocused()
|
||||
await expect(input).toHaveText("Explain the latest changes")
|
||||
await expect(composer).toHaveCSS("height", `${height}px`)
|
||||
const menu = await component.locator('[data-component="composer-suggestions"]').boundingBox()
|
||||
|
||||
if (!menu) throw new Error("Missing history menu")
|
||||
await expect(options).toHaveText([
|
||||
"Run the focused tests",
|
||||
"Review the composer layout Keep keyboard navigation accessible",
|
||||
"Explain the latest changes",
|
||||
"Review the composer layout Keep keyboard navigation accessible",
|
||||
"Run the focused tests",
|
||||
])
|
||||
await expect(component.getByRole("option", { selected: true })).toHaveText("Explain the latest changes")
|
||||
await input.press("ArrowUp")
|
||||
await expect(
|
||||
component.getByRole("option", { name: "Explain the latest changes", exact: true }).locator("bdi"),
|
||||
).toHaveCSS("font-weight", "400")
|
||||
await search.press("ArrowDown")
|
||||
await expect(component.getByRole("option", { selected: true })).toHaveText(
|
||||
"Review the composer layout Keep keyboard navigation accessible",
|
||||
)
|
||||
@@ -140,20 +142,16 @@ story(
|
||||
.toBe(menu.y)
|
||||
await expect(component.getByRole("img", { name: "layout.png", exact: true })).toBeVisible()
|
||||
await expect(component.getByText("Keep the search separate from the draft", { exact: true })).toBeVisible()
|
||||
await input.press("ArrowUp")
|
||||
await search.press("ArrowDown")
|
||||
await expect(input).toHaveText("Run the focused tests")
|
||||
await expect(composer).toHaveCSS("height", `${height}px`)
|
||||
await input.press("ArrowUp")
|
||||
await search.press("ArrowDown")
|
||||
await expect(input).toHaveText("Run the focused tests")
|
||||
await input.press("ArrowDown")
|
||||
await search.press("ArrowUp")
|
||||
await expect(input).toHaveText("Review the composer layout\nKeep keyboard navigation accessible")
|
||||
await input.press("ControlOrMeta+f")
|
||||
await expect(search).toBeFocused()
|
||||
await search.fill("ACCESSIBLE")
|
||||
await expect(composer).toHaveCSS("height", `${height}px`)
|
||||
await search.press("Escape")
|
||||
await expect(input).toBeFocused()
|
||||
await expect(composer).toHaveCSS("height", `${height}px`)
|
||||
await expect
|
||||
.poll(() =>
|
||||
component
|
||||
@@ -161,7 +159,7 @@ story(
|
||||
.evaluate((element) => element.scrollHeight > element.clientHeight),
|
||||
)
|
||||
.toBe(true)
|
||||
await input.press("Enter")
|
||||
await search.press("Enter")
|
||||
await expect(options).toHaveCount(0)
|
||||
await expect(content).toHaveCSS("mask-image", "none")
|
||||
await expect(viewport).not.toHaveCSS("mask-image", "none")
|
||||
@@ -186,7 +184,7 @@ story(
|
||||
await expect(input).toHaveText("Explain the latest changes")
|
||||
await expect(component.getByRole("img", { name: "layout.png", exact: true })).toHaveCount(0)
|
||||
await expect(composer).toHaveCSS("height", `${restoredHeight}px`)
|
||||
await input.press("Escape")
|
||||
await search.press("Escape")
|
||||
await expect(options).toHaveCount(0)
|
||||
await expect(input).toBeFocused()
|
||||
await expect(input).toBeEmpty()
|
||||
@@ -196,13 +194,12 @@ story(
|
||||
},
|
||||
)
|
||||
|
||||
story("searches history on demand, returns to browsing, and keeps shell history separate", async ({ mount, page }) => {
|
||||
story("searches history, cancels with Escape, and keeps shell history separate", async ({ mount, page }) => {
|
||||
const component = await mount("opencode-composer-flow--prompt-history")
|
||||
const input = component.getByRole("textbox", { name: "Prompt", exact: true })
|
||||
const search = component.getByRole("combobox", { name: "Prompt history", exact: true })
|
||||
|
||||
await input.press("Shift+ArrowUp")
|
||||
await input.press("ControlOrMeta+f")
|
||||
await expect(search).toBeFocused()
|
||||
await search.fill("ACCESSIBLE")
|
||||
await expect(component.getByRole("option")).toHaveCount(1)
|
||||
@@ -211,13 +208,10 @@ story("searches history on demand, returns to browsing, and keeps shell history
|
||||
await search.press("Escape")
|
||||
await expect(search).toHaveCount(0)
|
||||
await expect(input).toBeFocused()
|
||||
await expect(component.getByRole("option")).toHaveCount(3)
|
||||
await input.press("Escape")
|
||||
await expect(component.getByRole("option")).toHaveCount(0)
|
||||
await expect(input).toBeEmpty()
|
||||
|
||||
await input.press("Shift+ArrowUp")
|
||||
await component.getByRole("button", { name: /^Search/ }).click()
|
||||
await expect(search).toBeFocused()
|
||||
await search.fill("no matching prompt")
|
||||
await expect(component.getByRole("option")).toHaveCount(0)
|
||||
@@ -246,6 +240,8 @@ story("searches history on demand, returns to browsing, and keeps shell history
|
||||
await input.press("Shift+ArrowUp")
|
||||
await expect(input).toHaveText("Explain the latest changes")
|
||||
await expect(component.getByRole("option")).toHaveCount(3)
|
||||
await expect(search).toBeFocused()
|
||||
await search.press("Enter")
|
||||
await expect(input).toBeFocused()
|
||||
await input.press("Shift+Digit1")
|
||||
await expect(component.getByRole("option")).toHaveCount(0)
|
||||
@@ -256,11 +252,11 @@ story("searches history on demand, returns to browsing, and keeps shell history
|
||||
await input.press("Shift+ArrowUp")
|
||||
await expect(input).toHaveText("bun typecheck")
|
||||
await expect(component.getByRole("option")).toHaveText(["bun typecheck"])
|
||||
await input.press("Escape")
|
||||
await search.press("Escape")
|
||||
await expect(input).toBeFocused()
|
||||
await expect(input).toBeEmpty()
|
||||
await input.press("Shift+ArrowUp")
|
||||
await input.press("Enter")
|
||||
await search.press("Enter")
|
||||
await expect(input).toHaveText("bun typecheck")
|
||||
await expect(component.getByRole("status")).toHaveText("Ready")
|
||||
})
|
||||
@@ -270,7 +266,7 @@ story("pins history search above the scrolling prompts", async ({ mount }) => {
|
||||
const input = component.getByRole("textbox", { name: "Prompt", exact: true })
|
||||
await input.press("Shift+ArrowUp")
|
||||
const menu = component.locator('[data-component="composer-suggestions"]')
|
||||
const search = menu.getByRole("button", { name: /^Search/ })
|
||||
const search = menu.getByRole("combobox", { name: "Prompt history", exact: true })
|
||||
const viewport = menu.locator("[data-scrollable]")
|
||||
await expect.poll(() => viewport.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true)
|
||||
|
||||
@@ -287,7 +283,7 @@ story("pins history search above the scrolling prompts", async ({ mount }) => {
|
||||
|
||||
await expect.poll(clippedTop).toBe(false)
|
||||
|
||||
for (let index = 0; index < 12; index++) await input.press("ArrowUp")
|
||||
for (let index = 0; index < 12; index++) await search.press("ArrowDown")
|
||||
await expect(menu.getByRole("option", { selected: true })).toBeInViewport({ ratio: 1 })
|
||||
await expect.poll(clippedTop).toBe(false)
|
||||
await expect.poll(async () => (await search.boundingBox())!.y - (await viewport.boundingBox())!.y).toBeLessThan(0)
|
||||
|
||||
@@ -45,7 +45,6 @@ import type {
|
||||
import type { ComposerEditorModel, ComposerSelectControl } from "./interaction"
|
||||
import { isAttachment } from "../prompt-parts"
|
||||
import { QuoteCommentEditor } from "./quote-comment"
|
||||
import { formatKeybindParts } from "@/shell/commands/command"
|
||||
import "../attachments/attachments.css"
|
||||
import "./editor.css"
|
||||
import "@/shell/palette-menu.css"
|
||||
@@ -185,8 +184,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
const search = () => {
|
||||
if (state.popover.type !== "history" && state.popover.type !== "command-menu") return
|
||||
|
||||
if (state.popover.type === "history" && !state.popover.searching) return
|
||||
|
||||
return {
|
||||
value: state.popover.query,
|
||||
label: i18n.t(state.popover.type === "history" ? "ui.promptInput.history" : "ui.promptInput.commands"),
|
||||
@@ -219,9 +216,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
activeID={state.popover.type === "closed" ? undefined : state.popover.activeID}
|
||||
search={search()}
|
||||
history={state.popover.type === "history" ? state.mode : undefined}
|
||||
historyBrowsing={state.popover.type === "history" && !state.popover.searching}
|
||||
editor={() => editor}
|
||||
onSearch={() => props.controller.dispatch({ type: "history.search" })}
|
||||
onClose={() => props.controller.dispatch({ type: "popover.close" })}
|
||||
onActiveChange={(item) => props.controller.dispatch({ type: "popover.active", id: item.id })}
|
||||
onSelect={(item) => props.controller.dispatch({ type: "popover.select", item })}
|
||||
@@ -297,11 +291,6 @@ export function ComposerEditor(props: ComposerEditorProps) {
|
||||
aria-multiline="true"
|
||||
aria-label={i18n.t("ui.promptInput.label")}
|
||||
aria-controls={state.popover.type === "history" ? suggestionsID : undefined}
|
||||
aria-activedescendant={
|
||||
state.popover.type === "history" && !state.popover.searching && state.popover.activeID
|
||||
? `${suggestionsID}-${state.popover.activeID}`
|
||||
: undefined
|
||||
}
|
||||
dir={state.mode === "normal" ? "auto" : "ltr"}
|
||||
contenteditable={!props.disabled && !props.readOnly}
|
||||
autocapitalize={state.mode === "normal" ? "sentences" : "off"}
|
||||
@@ -1163,9 +1152,6 @@ function ComposerEditorPopover(props: {
|
||||
items: ComposerSuggestion[]
|
||||
activeID?: string
|
||||
history?: ComposerMode
|
||||
historyBrowsing?: boolean
|
||||
editor?: () => HTMLElement | undefined
|
||||
onSearch?: () => void
|
||||
onClose?: () => void
|
||||
search?: {
|
||||
value: string
|
||||
@@ -1205,7 +1191,6 @@ function ComposerEditorPopover(props: {
|
||||
const dismiss = (event: Event) => {
|
||||
if (!(event.target instanceof Node) || container?.contains(event.target)) return
|
||||
|
||||
if (event.type === "focusin" && props.historyBrowsing && props.editor?.()?.contains(event.target)) return
|
||||
props.onClose?.()
|
||||
}
|
||||
|
||||
@@ -1239,7 +1224,7 @@ function ComposerEditorPopover(props: {
|
||||
<Show when={props.history}>
|
||||
<Show when={props.search}>
|
||||
{(search) => (
|
||||
<div class="shrink-0 px-2 py-1">
|
||||
<div class="flex h-7 shrink-0 items-center px-2 py-1">
|
||||
<input
|
||||
ref={(element) => requestAnimationFrame(() => element.isConnected && element.focus())}
|
||||
value={search().value}
|
||||
@@ -1260,16 +1245,6 @@ function ComposerEditorPopover(props: {
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.historyBrowsing}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-7 shrink-0 items-center justify-between gap-2 rounded-md px-2 text-[13px] leading-4 text-v2-text-text-muted hover:bg-v2-overlay-simple-overlay-hover"
|
||||
onClick={props.onSearch}
|
||||
>
|
||||
<span>{i18n.t("ui.promptInput.history.searchAction")}</span>
|
||||
<Keybind keys={formatKeybindParts("mod+f")} variant="ghost" />
|
||||
</button>
|
||||
</Show>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto" data-scrollable>
|
||||
<div id={listID} role="listbox" aria-label={i18n.t("ui.promptInput.history")} class="flex flex-col">
|
||||
<Show
|
||||
@@ -1296,11 +1271,10 @@ function ComposerEditorPopover(props: {
|
||||
<bdi
|
||||
dir={props.history === "shell" ? "ltr" : "auto"}
|
||||
class="palette-menu-title flex-1 text-start"
|
||||
style={
|
||||
props.history === "shell"
|
||||
? { "font-family": "var(--font-family-mono)", "font-weight": 440 }
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
"font-family": props.history === "shell" ? "var(--font-family-mono)" : undefined,
|
||||
"font-weight": props.history === "shell" ? 440 : "var(--font-weight-regular)",
|
||||
}}
|
||||
>
|
||||
{item.label || item.description || i18n.t("ui.promptInput.history.contextOnly")}
|
||||
</bdi>
|
||||
|
||||
@@ -264,17 +264,14 @@ export function createComposerEditor(input: {
|
||||
if (state.popover.type !== "history") return list().flat()
|
||||
const query = state.popover.query.trim().toLowerCase()
|
||||
|
||||
return history()
|
||||
.filter((item) => `${item.label} ${item.description}`.toLowerCase().includes(query))
|
||||
.toReversed()
|
||||
return history().filter((item) => `${item.label} ${item.description}`.toLowerCase().includes(query))
|
||||
}
|
||||
|
||||
const execute = (command: ComposerInteractionCommand) => {
|
||||
if (command.type === "history.preview") {
|
||||
const item = history().find((entry) => entry.id === command.id)
|
||||
|
||||
if (item?.history)
|
||||
applyHistory(item.history, { focus: state.popover.type === "history" && !state.popover.searching })
|
||||
if (item?.history) applyHistory(item.history, { focus: false })
|
||||
|
||||
return
|
||||
}
|
||||
@@ -324,7 +321,7 @@ export function createComposerEditor(input: {
|
||||
}
|
||||
|
||||
function dispatch(event: ComposerInteractionEvent) {
|
||||
if (event.type === "history.search" || event.type === "popover.close") cancelFocus()
|
||||
if (event.type === "popover.close") cancelFocus()
|
||||
|
||||
if (
|
||||
event.type === "history.open" ||
|
||||
@@ -380,21 +377,6 @@ export function createComposerEditor(input: {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
flushFocus()
|
||||
|
||||
if (
|
||||
state.popover.type === "history" &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
!event.altKey &&
|
||||
!event.shiftKey &&
|
||||
!event.isComposing &&
|
||||
event.key.toLowerCase() === "f"
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dispatch({ type: "history.search" })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
state.mode === "normal" &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
|
||||
@@ -562,15 +562,6 @@ export function createComposerModel(adapter: ComposerAdapter, options?: { queue?
|
||||
Object.defineProperty(controller, "model", { get: () => adapter.controls().model })
|
||||
|
||||
command.register("composer-editor", () => [
|
||||
{
|
||||
id: "prompt.history.search",
|
||||
title: language.t("ui.promptInput.history.search"),
|
||||
keybind: "mod+f",
|
||||
hidden: true,
|
||||
editable: true,
|
||||
when: () => controller.state.popover.type === "history",
|
||||
onSelect: () => void controller.dispatch({ type: "history.search" }),
|
||||
},
|
||||
{
|
||||
id: "file.attach",
|
||||
title: language.t("prompt.action.attachFile"),
|
||||
|
||||
@@ -8,7 +8,7 @@ export type ComposerInteractionState = {
|
||||
| { type: "context"; query: string; activeID?: string }
|
||||
| { type: "command-inline"; query: string; activeID?: string }
|
||||
| { type: "command-menu"; query: string; activeID?: string }
|
||||
| { type: "history"; query: string; searching: boolean; activeID?: string }
|
||||
| { type: "history"; query: string; activeID?: string }
|
||||
drag: "idle" | "active"
|
||||
focus: "editor" | "command-search" | "history-search" | "external"
|
||||
activeContextID?: string
|
||||
@@ -19,8 +19,6 @@ export type ComposerInteractionEvent =
|
||||
| { type: "commands.open" }
|
||||
| { type: "context.open" }
|
||||
| { type: "history.open"; id: string }
|
||||
| { type: "history.search" }
|
||||
| { type: "history.browse" }
|
||||
| { type: "popover.query"; value: string }
|
||||
| { type: "popover.results"; ids: string[] }
|
||||
| { type: "popover.active"; id: string }
|
||||
@@ -73,15 +71,9 @@ export function transitionComposer(
|
||||
if (event.type === "context.open") return openContext(state)
|
||||
|
||||
if (event.type === "history.open")
|
||||
return changed(
|
||||
{ ...state, popover: { type: "history", query: "", searching: false, activeID: event.id }, focus: "editor" },
|
||||
[{ type: "history.preview", id: event.id }],
|
||||
)
|
||||
|
||||
if (event.type === "history.search" && state.popover.type === "history")
|
||||
return changed({ ...state, popover: { ...state.popover, searching: true }, focus: "history-search" })
|
||||
|
||||
if (event.type === "history.browse") return browseHistory(state)
|
||||
return changed({ ...state, popover: { type: "history", query: "", activeID: event.id }, focus: "history-search" }, [
|
||||
{ type: "history.preview", id: event.id },
|
||||
])
|
||||
|
||||
if (event.type === "popover.query") return queryChanged(state, event.value)
|
||||
|
||||
@@ -193,25 +185,10 @@ function queryChanged(state: ComposerInteractionState, query: string): ComposerE
|
||||
])
|
||||
}
|
||||
|
||||
function browseHistory(state: ComposerInteractionState): ComposerEditorTransition {
|
||||
if (state.popover.type !== "history") return unchanged(state)
|
||||
|
||||
return changed(
|
||||
{ ...state, popover: { ...state.popover, searching: false, query: "" }, focus: "editor" },
|
||||
[{ type: "focus.editor" }],
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
function resultsChanged(state: ComposerInteractionState, ids: string[]): ComposerEditorTransition {
|
||||
if (state.popover.type === "closed") return unchanged(state)
|
||||
|
||||
const activeID =
|
||||
state.popover.activeID && ids.includes(state.popover.activeID)
|
||||
? state.popover.activeID
|
||||
: state.popover.type === "history"
|
||||
? ids.at(-1)
|
||||
: ids[0]
|
||||
const activeID = state.popover.activeID && ids.includes(state.popover.activeID) ? state.popover.activeID : ids[0]
|
||||
|
||||
if (activeID === state.popover.activeID) return unchanged(state)
|
||||
|
||||
@@ -269,8 +246,6 @@ function keyDown(
|
||||
event: Extract<ComposerInteractionEvent, { type: "key.down" }>,
|
||||
): ComposerEditorTransition {
|
||||
if (state.popover.type === "history" && (event.key === "Escape" || (event.ctrl && event.key.toLowerCase() === "g"))) {
|
||||
if (state.popover.searching && event.key === "Escape") return browseHistory(state)
|
||||
|
||||
return changed({ ...state, popover: { type: "closed" }, focus: "editor" }, [{ type: "history.restore" }], true)
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,6 @@ const source = {
|
||||
"ui.promptInput.commands": "Commands",
|
||||
"ui.promptInput.history": "Prompt history",
|
||||
"ui.promptInput.history.search": "Search prompt history…",
|
||||
"ui.promptInput.history.searchAction": "Search",
|
||||
"ui.promptInput.history.contextOnly": "Prompt with context",
|
||||
"ui.promptInput.dropFiles": "Drop files to add",
|
||||
"ui.promptInput.dropFiles.image": "Drop images or files to add",
|
||||
|
||||
Reference in New Issue
Block a user