diff --git a/packages/app/component-tests/composer.spec.ts b/packages/app/component-tests/composer.spec.ts index c37bd337d81..cb2a0c7db6d 100644 --- a/packages/app/component-tests/composer.spec.ts +++ b/packages/app/component-tests/composer.spec.ts @@ -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) diff --git a/packages/app/src/composer/editor/editor.tsx b/packages/app/src/composer/editor/editor.tsx index 77e806092f3..7404e1cf497 100644 --- a/packages/app/src/composer/editor/editor.tsx +++ b/packages/app/src/composer/editor/editor.tsx @@ -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: { {(search) => ( -
+
requestAnimationFrame(() => element.isConnected && element.focus())} value={search().value} @@ -1260,16 +1245,6 @@ function ComposerEditorPopover(props: {
)} - - -
{item.label || item.description || i18n.t("ui.promptInput.history.contextOnly")} diff --git a/packages/app/src/composer/editor/interaction.ts b/packages/app/src/composer/editor/interaction.ts index 386d9d7c2a7..d59b058751b 100644 --- a/packages/app/src/composer/editor/interaction.ts +++ b/packages/app/src/composer/editor/interaction.ts @@ -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) && diff --git a/packages/app/src/composer/model.ts b/packages/app/src/composer/model.ts index c72816d07e4..d348fd5e175 100644 --- a/packages/app/src/composer/model.ts +++ b/packages/app/src/composer/model.ts @@ -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"), diff --git a/packages/app/src/composer/suggestions/machine.ts b/packages/app/src/composer/suggestions/machine.ts index 6b882c039cf..33c7a48b93b 100644 --- a/packages/app/src/composer/suggestions/machine.ts +++ b/packages/app/src/composer/suggestions/machine.ts @@ -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, ): 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) } diff --git a/packages/ui/src/i18n/en.ts b/packages/ui/src/i18n/en.ts index 7ed8e6d2d4e..a9690493127 100644 --- a/packages/ui/src/i18n/en.ts +++ b/packages/ui/src/i18n/en.ts @@ -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",