diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index aa002080b1dd..cbd9cda7b4a6 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -424,12 +424,7 @@ export function Prompt(props: PromptProps) { dialog.clear() // replace summarized text parts with the actual text - const text = store.prompt.parts - .filter((p) => p.type === "text") - .reduce((acc, p) => { - if (!p.source) return acc - return acc.replace(p.source.text.value, p.text) - }, store.prompt.input) + const text = expandPastedTextPlaceholders(store.prompt.input, store.prompt.parts) const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") diff --git a/packages/tui/src/prompt/part.ts b/packages/tui/src/prompt/part.ts index 0027b9aaed77..577af65b42a0 100644 --- a/packages/tui/src/prompt/part.ts +++ b/packages/tui/src/prompt/part.ts @@ -8,7 +8,7 @@ export function stripPromptPartIDs((result, part) => { if (!isPastedTextPart(part)) return result - return result.replace(part.source.text.value, part.text) + return result.replace(part.source.text.value, () => part.text) }, text) } diff --git a/packages/tui/test/prompt/part.test.ts b/packages/tui/test/prompt/part.test.ts index 2d5605ac0c49..ffd8eb399051 100644 --- a/packages/tui/test/prompt/part.test.ts +++ b/packages/tui/test/prompt/part.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "bun:test" -import { expandTrackedPastedText, stripPromptPartIDs } from "../../src/prompt/part" +import { expandPastedTextPlaceholders, expandTrackedPastedText, stripPromptPartIDs } from "../../src/prompt/part" + +function pastedTextPart(marker: string, text: string) { + return { type: "text" as const, text, source: { text: { value: marker } } } +} describe("prompt part", () => { test("strips persisted IDs from reused parts", () => { @@ -50,4 +54,33 @@ describe("prompt part", () => { ]), ).toBe(`keep ${marker} then alpha\nbeta\ngamma tail`) }) + + test("expands placeholders with dollar sequences literally", () => { + const marker = "[Pasted ~1 lines]" + for (const pasted of ["const cost = ${price}$$", "echo $&foo", "a $` b", "x $' y", "price $1 each"]) { + expect(expandPastedTextPlaceholders(`see ${marker} here`, [pastedTextPart(marker, pasted)])).toBe( + `see ${pasted} here`, + ) + } + }) + + test("expands repeated same-line-count placeholders in order", () => { + const marker = "[Pasted ~2 lines]" + expect( + expandPastedTextPlaceholders(`${marker} and ${marker}`, [ + pastedTextPart(marker, "first"), + pastedTextPart(marker, "second"), + ]), + ).toBe("first and second") + }) + + test("leaves non pasted-text parts untouched", () => { + const marker = "[Pasted ~1 lines]" + expect( + expandPastedTextPlaceholders(`see ${marker} here`, [ + { type: "file", url: "data:," }, + pastedTextPart(marker, "$$$"), + ]), + ).toBe("see $$$ here") + }) })