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 `