diff --git a/app/src/components/notes/NotesPanel.test.tsx b/app/src/components/notes/NotesPanel.test.tsx index 7707f48..3545171 100644 --- a/app/src/components/notes/NotesPanel.test.tsx +++ b/app/src/components/notes/NotesPanel.test.tsx @@ -99,7 +99,11 @@ describe("NotesPanel", () => { expect(screen.getByTestId("send")).toHaveTextContent("send:fresh"); }); - it("deletes the selected note and falls back to another", async () => { + it("asks the hook to delete the selected note", async () => { + // Only the call: `useNotes` is mocked here and the mocked list never + // changes, so nothing in this file can exercise what the panel selects + // afterwards. The fallback is covered against the real hook in + // NotesPanel.shared.test.tsx. notes = [note(), note({ id: "n2", title: "Gotchas" })]; render(); fireEvent.click(screen.getByRole("button", { name: /delete note/i })); diff --git a/app/src/lib/claudeInput.test.ts b/app/src/lib/claudeInput.test.ts index 4eaaaf7..0f4f34f 100644 --- a/app/src/lib/claudeInput.test.ts +++ b/app/src/lib/claudeInput.test.ts @@ -16,6 +16,21 @@ describe("toClaudePayload", () => { expect(toClaudePayload("one\r\ntwo")).toBe("one\x1b\rtwo"); }); + it("normalises a lone CR, which would otherwise submit", () => { + // A bare \r is a carriage return: it submits in a Claude prompt and runs + // the line in a shell — the terminator this function promises not to + // append. A textarea cannot make one, but a notes file that was + // hand-edited or written by something else can, and `load_in` hands it + // straight back. + expect(toClaudePayload("one\rtwo")).toBe("one\x1b\rtwo"); + expect(toClaudePayload("one\rtwo\r\nthree\nfour")).toBe( + "one\x1b\rtwo\x1b\rthree\x1b\rfour", + ); + expect(toClaudePayload("text\r").endsWith("\r")).toBe(true); + // …but only as the tail of the soft-newline sequence, never bare. + expect(toClaudePayload("text\r")).toBe("text\x1b\r"); + }); + it("leaves single-line text untouched", () => { expect(toClaudePayload("just one line")).toBe("just one line"); }); diff --git a/app/src/lib/claudeInput.ts b/app/src/lib/claudeInput.ts index a4761ff..42290a2 100644 --- a/app/src/lib/claudeInput.ts +++ b/app/src/lib/claudeInput.ts @@ -23,7 +23,14 @@ export const CLAUDE_SOFT_NEWLINE = "\x1b\r"; * as N truncated prompts. Deliberately appends no terminator: the text lands * in the prompt and the user presses Enter, which is what speech-to-text does * for the same reason — an unsent prompt is recoverable and a sent one is not. + * + * A **lone** `\r` is matched too, not only the one in a CRLF. It is a carriage + * return: it submits in a Claude prompt and runs the line in a shell, which is + * exactly the terminator this function promises never to append. A `