diff --git a/.changeset/stop-session-user-message-duplication.md b/.changeset/stop-session-user-message-duplication.md new file mode 100644 index 0000000..c693e24 --- /dev/null +++ b/.changeset/stop-session-user-message-duplication.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Keep a new prompt separate from the stopped prompt after aborting a session turn. diff --git a/src/client/src/chatTranscript.test.ts b/src/client/src/chatTranscript.test.ts index dcecc0c..9c8df44 100644 --- a/src/client/src/chatTranscript.test.ts +++ b/src/client/src/chatTranscript.test.ts @@ -132,6 +132,25 @@ describe("applyTranscriptEvent", () => { ]); }); + it("does not merge optimistic user messages after an aborted turn", () => { + const messages = [textMessage("user", "stopped prompt")]; + + expect(applyTranscriptEvent(messages, { type: "message.append", message: { role: "user", content: "new prompt" } })).toEqual([ + textMessage("user", "stopped prompt"), + textMessage("user", "new prompt"), + ]); + }); + + it("replaces a new optimistic user message instead of duplicating it after an aborted turn", () => { + let messages: ChatLine[] = [textMessage("user", "stopped prompt")]; + messages = applyTranscriptEvent(messages, { type: "message.append", message: { role: "user", content: "new prompt" } }) ?? messages; + + expect(applyTranscriptEvent(messages, { type: "message.end", message: { role: "user", content: "new prompt", timestamp: "2026-05-09T12:00:00.000Z" } })).toEqual([ + textMessage("user", "stopped prompt"), + { ...textMessage("user", "new prompt"), meta: { timestamp: "2026-05-09T12:00:00.000Z" } }, + ]); + }); + it("replaces an optimistic user message when the finalized text matches", () => { const messages = [textMessage("user", "sent prompt")]; diff --git a/src/client/src/chatTranscript.ts b/src/client/src/chatTranscript.ts index 799c9a3..13f1c86 100644 --- a/src/client/src/chatTranscript.ts +++ b/src/client/src/chatTranscript.ts @@ -4,7 +4,7 @@ import { appendShellChunk, finalizeShellMessage, shellStartMessage } from "./she import type { SessionUiEvent } from "./sessionSocket"; export function applyTranscriptEvent(messages: ChatLine[], event: SessionUiEvent): ChatLine[] | undefined { - if (event.type === "message.append") return appendNormalized(messages, event.message); + if (event.type === "message.append") return appendNewMessage(messages, event.message); if (event.type === "assistant.delta") return appendText(messages, "assistant", event.text); if (event.type === "assistant.thinking.delta") return appendThinking(messages, event.text); if (event.type === "tool.start") return appendNormalized(messages, { role: "assistant", content: [{ type: "toolCall", name: event.toolName, arguments: event.args }] }); @@ -72,6 +72,11 @@ function messageText(message: ChatLine): string { .join("\n\n"); } +function appendNewMessage(messages: ChatLine[], rawMessage: unknown): ChatLine[] { + const lines = normalizeMessage(rawMessage); + return lines.length === 0 ? messages : [...messages, ...lines]; +} + function appendNormalized(messages: ChatLine[], rawMessage: unknown): ChatLine[] { return normalizeMessage(rawMessage).reduce(appendLine, messages); }