Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,30 @@ export type PromptInfo = {
}

const MAX_HISTORY_ENTRIES = 50
type SearchInfo = {
term: string
pos: number
value: string
}

export function search(list: PromptInfo[], input: string, last?: SearchInfo) {
const keep = last && (input === last.value || input === last.term)
const term = keep ? last.term : input
const from = keep ? last.pos - 1 : list.length - 1
for (let i = from; i >= 0; i--) {
const item = list[i]
if (!item) continue
if (term && !item.input.includes(term)) continue
return {
item,
next: {
term,
pos: i,
value: item.input,
} satisfies SearchInfo,
}
}
}

export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
name: "PromptHistory",
Expand Down Expand Up @@ -58,6 +82,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
const [store, setStore] = createStore({
index: 0,
history: [] as PromptInfo[],
search: undefined as SearchInfo | undefined,
})

return {
Expand All @@ -68,6 +93,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
if (current.input !== input && input.length) return
setStore(
produce((draft) => {
draft.search = undefined
const next = store.index + direction
if (Math.abs(next) > store.history.length) return
if (next > 0) return
Expand All @@ -78,9 +104,20 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
return {
input: "",
parts: [],
}
}
return store.history.at(store.index)
},
search(input: string) {
const found = search(store.history, input, store.search)
if (!found) return
setStore(
produce((draft) => {
draft.index = 0
draft.search = found.next
}),
)
return found.item
},
append(item: PromptInfo) {
const entry = structuredClone(unwrap(item))
let trimmed = false
Expand All @@ -92,6 +129,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
trimmed = true
}
draft.index = 0
draft.search = undefined
}),
)

Expand Down
12 changes: 12 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,18 @@ export function Prompt(props: PromptProps) {
}
if (store.mode === "normal") autocomplete.onKeyDown(e)
if (!autocomplete.visible) {
if (keybind.match("history_search", e)) {
const item = history.search(input.plainText)
e.preventDefault()
if (!item) return
input.setText(item.input)
setStore("prompt", item)
setStore("mode", item.mode ?? "normal")
restoreExtmarksFromParts(item.parts)
input.gotoBufferEnd()
return
}

if (
(keybind.match("history_previous", e) && input.cursorOffset === 0) ||
(keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ export namespace Config {
.optional()
.default("ctrl+w,ctrl+backspace,alt+backspace")
.describe("Delete word backward in input"),
history_search: z.string().optional().default("ctrl+r").describe("Search prompt history"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Next history item"),
session_child_first: z.string().optional().default("<leader>down").describe("Go to first child session"),
Expand Down
41 changes: 41 additions & 0 deletions packages/opencode/test/cli/cmd/tui/prompt-history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test"
import { search, type PromptInfo } from "../../../../src/cli/cmd/tui/component/prompt/history"

function item(input: string): PromptInfo {
return {
input,
parts: [],
}
}

describe("prompt history", () => {
test("search finds the latest matching prompt", () => {
const list = [item("fix lint"), item("fix tests"), item("review docs")]

const found = search(list, "fix")

expect(found?.item.input).toBe("fix tests")
expect(found?.next.term).toBe("fix")
})

test("search walks backward through matching prompts", () => {
const list = [item("fix lint"), item("fix tests"), item("review docs"), item("fix docs")]

const a = search(list, "fix")
const b = search(list, "fix docs", a?.next)
const c = search(list, "fix tests", b?.next)

expect(a?.item.input).toBe("fix docs")
expect(b?.item.input).toBe("fix tests")
expect(c?.item.input).toBe("fix lint")
})

test("search falls back to all prompts when the query is empty", () => {
const list = [item("fix lint"), item("fix tests")]

const found = search(list, "")

expect(found?.item.input).toBe("fix tests")
expect(found?.next.term).toBe("")
})
})
Loading