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 630f725c79..5b7be5d32b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -27,6 +27,11 @@ export type PromptInfo = { const MAX_HISTORY_ENTRIES = 50 +export function isDuplicateEntry(previous: PromptInfo | undefined, next: PromptInfo): boolean { + if (!previous) return false + return JSON.stringify(previous) === JSON.stringify(next) +} + export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ name: "PromptHistory", init: () => { @@ -83,6 +88,10 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create }, append(item: PromptInfo) { const entry = structuredClone(unwrap(item)) + if (isDuplicateEntry(store.history.at(-1), entry)) { + setStore("index", 0) + return + } let trimmed = false setStore( produce((draft) => { diff --git a/packages/opencode/test/cli/cmd/tui/prompt-history.test.ts b/packages/opencode/test/cli/cmd/tui/prompt-history.test.ts new file mode 100644 index 0000000000..f92b34d905 --- /dev/null +++ b/packages/opencode/test/cli/cmd/tui/prompt-history.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" +import { isDuplicateEntry, type PromptInfo } from "../../../../src/cli/cmd/tui/component/prompt/history" + +const entry = (input: string, parts: PromptInfo["parts"] = []): PromptInfo => ({ input, parts }) + +describe("prompt history dedupe", () => { + test("returns false when there is no previous entry", () => { + expect(isDuplicateEntry(undefined, entry("hello"))).toBe(false) + }) + + test("dedupes identical consecutive entries", () => { + const a = entry("hello world this is over twenty chars") + const b = entry("hello world this is over twenty chars") + expect(isDuplicateEntry(a, b)).toBe(true) + }) + + test("does not dedupe when input text differs", () => { + expect(isDuplicateEntry(entry("foo"), entry("bar"))).toBe(false) + }) + + test("does not dedupe when parts differ", () => { + const a = entry("describe this", [ + { + type: "file", + mime: "image/png", + filename: "a.png", + url: "data:image/png;base64,AAA", + }, + ]) + const b = entry("describe this", [ + { + type: "file", + mime: "image/png", + filename: "b.png", + url: "data:image/png;base64,BBB", + }, + ]) + expect(isDuplicateEntry(a, b)).toBe(false) + }) + + test("does not dedupe when mode differs", () => { + expect(isDuplicateEntry({ ...entry("ls"), mode: "normal" }, { ...entry("ls"), mode: "shell" })).toBe(false) + }) +})