diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-history-search.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-history-search.tsx new file mode 100644 index 000000000..07423261b --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-history-search.tsx @@ -0,0 +1,37 @@ +import { createMemo } from "solid-js" +import { DialogSelect } from "@tui/ui/dialog-select" +import { useDialog } from "@tui/ui/dialog" +import type { PromptInfo } from "./prompt/history" + +export interface DialogHistorySearchProps { + items: PromptInfo[] + onSelect: (item: PromptInfo) => void +} + +export function DialogHistorySearch(props: DialogHistorySearchProps) { + const dialog = useDialog() + + const options = createMemo(() => { + const seen = new Set() + return props.items + .slice() + .reverse() + .filter((item) => { + if (!item.input.trim().length) return false + if (seen.has(item.input)) return false + seen.add(item.input) + return true + }) + .map((item) => ({ + title: item.input, + value: item, + description: undefined, + onSelect: () => { + props.onSelect(item) + dialog.clear() + }, + })) + }) + + return +} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index e90503e9f..36e90b950 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -25,7 +25,7 @@ export type PromptInfo = { )[] } -const MAX_HISTORY_ENTRIES = 50 +const MAX_HISTORY_ENTRIES = 100 export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ name: "PromptHistory", @@ -61,6 +61,9 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create }) return { + get items() { + return store.history + }, move(direction: 1 | -1, input: string) { if (!store.history.length) return undefined const current = store.history.at(store.index) @@ -103,6 +106,9 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create appendFile(historyFile.name!, JSON.stringify(entry) + "\n").catch(() => {}) }, + reset() { + setStore("index", 0) + }, } }, }) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 47940d0e2..78313cf90 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -27,6 +27,7 @@ import { useDialog } from "@tui/ui/dialog" import { DialogProvider as DialogProviderConnect } from "../dialog-provider" import { DialogAlert } from "../../ui/dialog-alert" import { useToast } from "../../ui/toast" +import { DialogHistorySearch } from "../dialog-history-search" export type PromptProps = { sessionID?: string @@ -788,6 +789,25 @@ export function Prompt(props: PromptProps) { } if (store.mode === "normal") autocomplete.onKeyDown(e) if (!autocomplete.visible) { + if (keybind.match("history_search", e)) { + e.preventDefault() + const historyItems = history.items + dialog.replace(() => ( + { + input.setText(item.input) + setStore("prompt", item) + setStore("mode", item.mode ?? "normal") + restoreExtmarksFromParts(item.parts) + input.cursorOffset = input.plainText.length + history.reset() + }} + /> + )) + return + } + if ( (keybind.match("history_previous", e) && input.cursorOffset === 0) || (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index cb93c0ebf..a9976e9a2 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -561,6 +561,7 @@ export namespace Config { .describe("Delete word backward in input"), history_previous: z.string().optional().default("up").describe("Previous history item"), history_next: z.string().optional().default("down").describe("Next history item"), + history_search: z.string().optional().default("ctrl+r").describe("Search history"), session_child_cycle: z.string().optional().default("right").describe("Next child session"), session_child_cycle_reverse: z.string().optional().default("left").describe("Previous child session"), session_parent: z.string().optional().default("up").describe("Go to parent session"), diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 1372765e3..9f3adfcac 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1114,6 +1114,10 @@ export type KeybindsConfig = { * Next history item */ history_next?: string + /** + * Search history + */ + history_search?: string /** * Next child session */ diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 588b130b9..24cfa684f 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -7623,6 +7623,11 @@ "default": "down", "type": "string" }, + "history_search": { + "description": "Search history", + "default": "ctrl+r", + "type": "string" + }, "session_child_cycle": { "description": "Next child session", "default": "right",