feat(tui): use canonical prompt attachments

This commit is contained in:
Dax Raad 2026-07-06 14:34:57 -04:00
commit c13f06c30c
24 changed files with 611 additions and 466 deletions

View file

@ -5,9 +5,9 @@ import { describe, expect, test } from "bun:test"
//
// Before the fix, two concurrent `submit()` calls (e.g. a double-pressed
// Enter, or the input's native onSubmit racing another dispatch) each
// passed the `if (!store.prompt.input) return false` guard, each
// passed the `if (!store.prompt.text) return false` guard, each
// `await sdk.client.session.create(...)`, and each only captured
// `inputText = store.prompt.input` AFTER that await. The first invocation
// `inputText = store.prompt.text` AFTER that await. The first invocation
// finished, sent the prompt, and cleared the store; the second invocation,
// now past its await, read the cleared store and sent an empty prompt to a
// second freshly-created session - leaving an orphaned session with the

View file

@ -1,7 +1,12 @@
import { describe, expect, test } from "bun:test"
import { isDuplicateEntry, MAX_HISTORY_ENTRIES, parsePromptHistory, type PromptInfo } from "../../src/prompt/history"
const entry = (input: string, parts: PromptInfo["parts"] = []): PromptInfo => ({ input, parts })
const entry = (text: string, files: PromptInfo["files"] = []): PromptInfo => ({
text,
files,
agents: [],
pasted: [],
})
describe("prompt history", () => {
test("recovers valid JSONL entries around corruption", () => {
@ -11,13 +16,17 @@ describe("prompt history", () => {
])
})
test("ignores the legacy parts shape", () => {
expect(parsePromptHistory(JSON.stringify({ input: "old", parts: [] }))).toEqual([])
})
test("retains only the newest entries", () => {
const input = Array.from({ length: MAX_HISTORY_ENTRIES + 5 }, (_, index) =>
JSON.stringify(entry(String(index))),
).join("\n")
const result = parsePromptHistory(input)
expect(result).toHaveLength(MAX_HISTORY_ENTRIES)
expect(result[0]?.input).toBe("5")
expect(result[0]?.text).toBe("5")
})
test("dedupes only identical consecutive entries", () => {
@ -27,13 +36,10 @@ describe("prompt history", () => {
expect(isDuplicateEntry({ ...entry("ls"), mode: "normal" }, { ...entry("ls"), mode: "shell" })).toBe(false)
})
test("does not dedupe entries with different parts", () => {
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" },
])
test("does not dedupe entries with different attachments", () => {
const a = entry("describe this", [{ name: "a.png", uri: "data:image/png;base64,AAA" }])
const b = entry("describe this", [{ name: "b.png", uri: "data:image/png;base64,BBB" }])
expect(isDuplicateEntry(a, b)).toBe(false)
})
})

View file

@ -4,12 +4,12 @@ import { MAX_STASH_ENTRIES, parsePromptStash } from "../../src/prompt/stash"
test("stash JSONL skips corruption and retains newest entries", () => {
const entries = Array.from({ length: MAX_STASH_ENTRIES + 2 }, (_, index) =>
JSON.stringify({ input: String(index), parts: [], timestamp: index }),
JSON.stringify({ prompt: { text: String(index), files: [], agents: [], pasted: [] }, timestamp: index }),
)
entries.splice(2, 0, "broken")
const result = parsePromptStash(entries.join("\n"))
expect(result).toHaveLength(MAX_STASH_ENTRIES)
expect(result[0]?.input).toBe("2")
expect(result[0]?.prompt.text).toBe("2")
})
test("frecency JSONL skips corruption, keeps latest path state, and limits entries", () => {

View file

@ -1,26 +1,7 @@
import { describe, expect, test } from "bun:test"
import { expandTrackedPastedText, stripPromptPartIDs } from "../../src/prompt/part"
import { expandTrackedPastedText } from "../../src/prompt/part"
describe("prompt part", () => {
test("strips persisted IDs from reused parts", () => {
expect(
stripPromptPartIDs({
id: "prt_old",
sessionID: "ses_old",
messageID: "msg_old",
type: "file" as const,
mime: "image/png",
filename: "tiny.png",
url: "data:image/png;base64,abc",
}),
).toEqual({
type: "file",
mime: "image/png",
filename: "tiny.png",
url: "data:image/png;base64,abc",
})
})
test("preserves wide characters around pasted text", () => {
const marker = "[Pasted ~3 lines]"
const prefix = "你好你好\n"