From af668b33d4e25d228dce3463ccfc6189bdcef403 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Sat, 18 Apr 2026 18:35:10 +0200 Subject: [PATCH] make single owner of prompt copy/equality --- .../opencode/src/cli/cmd/run/prompt.shared.ts | 12 +++++----- .../src/cli/cmd/run/session.shared.ts | 16 +++---------- .../test/cli/run/session.shared.test.ts | 24 +++++++++++++++---- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run/prompt.shared.ts b/packages/opencode/src/cli/cmd/run/prompt.shared.ts index 7871d02149..024fc61e8c 100644 --- a/packages/opencode/src/cli/cmd/run/prompt.shared.ts +++ b/packages/opencode/src/cli/cmd/run/prompt.shared.ts @@ -46,14 +46,14 @@ export type PromptMove = { apply: boolean } -function copy(prompt: RunPrompt): RunPrompt { +export function promptCopy(prompt: RunPrompt): RunPrompt { return { text: prompt.text, parts: structuredClone(prompt.parts), } } -function same(a: RunPrompt, b: RunPrompt): boolean { +export function promptSame(a: RunPrompt, b: RunPrompt): boolean { return a.text === b.text && JSON.stringify(a.parts) === JSON.stringify(b.parts) } @@ -171,10 +171,10 @@ export function promptCycle( } export function createPromptHistory(items?: RunPrompt[]): PromptHistoryState { - const list = (items ?? []).filter((item) => item.text.trim().length > 0).map(copy) + const list = (items ?? []).filter((item) => item.text.trim().length > 0).map(promptCopy) const next: RunPrompt[] = [] for (const item of list) { - if (next.length > 0 && same(next[next.length - 1], item)) { + if (next.length > 0 && promptSame(next[next.length - 1], item)) { continue } @@ -193,8 +193,8 @@ export function pushPromptHistory(state: PromptHistoryState, prompt: RunPrompt): return state } - const next = copy(prompt) - if (state.items[state.items.length - 1] && same(state.items[state.items.length - 1], next)) { + const next = promptCopy(prompt) + if (state.items[state.items.length - 1] && promptSame(state.items[state.items.length - 1], next)) { return { ...state, index: null, diff --git a/packages/opencode/src/cli/cmd/run/session.shared.ts b/packages/opencode/src/cli/cmd/run/session.shared.ts index 14f5c52441..997aa6c9d3 100644 --- a/packages/opencode/src/cli/cmd/run/session.shared.ts +++ b/packages/opencode/src/cli/cmd/run/session.shared.ts @@ -5,6 +5,7 @@ // the current model so the footer can pre-select it. import path from "path" import { fileURLToPath } from "url" +import { promptCopy, promptSame } from "./prompt.shared" import type { RunInput, RunPrompt } from "./types" const LIMIT = 200 @@ -23,17 +24,6 @@ export type RunSession = { turns: Turn[] } -function copy(prompt: RunPrompt): RunPrompt { - return { - text: prompt.text, - parts: structuredClone(prompt.parts), - } -} - -function same(a: RunPrompt, b: RunPrompt): boolean { - return a.text === b.text && JSON.stringify(a.parts) === JSON.stringify(b.parts) -} - function fileName(url: string, filename?: string) { if (filename) { return filename @@ -175,11 +165,11 @@ export function sessionHistory(session: RunSession, limit = LIMIT): RunPrompt[] continue } - if (out[out.length - 1] && same(out[out.length - 1], turn.prompt)) { + if (out[out.length - 1] && promptSame(out[out.length - 1], turn.prompt)) { continue } - out.push(copy(turn.prompt)) + out.push(promptCopy(turn.prompt)) } return out.slice(-limit) diff --git a/packages/opencode/test/cli/run/session.shared.test.ts b/packages/opencode/test/cli/run/session.shared.test.ts index 0f87a14b52..dd8f1f587a 100644 --- a/packages/opencode/test/cli/run/session.shared.test.ts +++ b/packages/opencode/test/cli/run/session.shared.test.ts @@ -133,18 +133,34 @@ describe("run session shared", () => { }) }) - test("dedupes consecutive history entries and drops blank prompts", () => { + test("dedupes consecutive history entries, drops blanks, and copies prompt parts", () => { + const parts = [ + { + type: "agent" as const, + name: "scan", + source: { + start: 0, + end: 5, + value: "@scan", + }, + }, + ] const session: RunSession = { first: false, turns: [ - { prompt: { text: "one", parts: [] }, provider: "openai", model: "gpt-5", variant: "high" }, - { prompt: { text: "one", parts: [] }, provider: "openai", model: "gpt-5", variant: "high" }, + { prompt: { text: "one", parts }, provider: "openai", model: "gpt-5", variant: "high" }, + { prompt: { text: "one", parts: structuredClone(parts) }, provider: "openai", model: "gpt-5", variant: "high" }, { prompt: { text: " ", parts: [] }, provider: "openai", model: "gpt-5", variant: "high" }, { prompt: { text: "two", parts: [] }, provider: "openai", model: "gpt-5", variant: undefined }, ], } - expect(sessionHistory(session).map((item) => item.text)).toEqual(["one", "two"]) + const out = sessionHistory(session) + + expect(out.map((item) => item.text)).toEqual(["one", "two"]) + expect(out[0]?.parts).toEqual(parts) + expect(out[0]?.parts).not.toBe(parts) + expect(out[0]?.parts[0]).not.toBe(parts[0]) }) test("returns the latest matching variant for the active model", () => {