Add a per-project Notes tab with a send-to-agent action #48
@@ -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(<NotesPanel projectId="p1" />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /delete note/i }));
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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 `<textarea>`
|
||||
* cannot produce one, but a note body is read back from a JSON file that can be
|
||||
* hand-edited or written by something else, so the guarantee has to hold for
|
||||
* whatever `load_in` returns rather than for whatever the editor can type.
|
||||
*/
|
||||
export function toClaudePayload(text: string): string {
|
||||
return text.replace(/\r?\n/g, CLAUDE_SOFT_NEWLINE);
|
||||
return text.replace(/\r\n|\r|\n/g, CLAUDE_SOFT_NEWLINE);
|
||||
}
|
||||
|
||||
@@ -85,6 +85,13 @@ struct ProjectNotes { version: u32, notes: Vec<Note> }
|
||||
|
||||
Order is pinned-first then `updated_at` descending. Manual reordering is deliberately out.
|
||||
|
||||
**`pinned` is reserved, and nothing in v1 sets it.** The field is persisted and sorted on, but
|
||||
there is no pin control and no pinned indicator anywhere in the UI, so in v1 every note sorts
|
||||
by `updated_at` descending and the pinned-first half of the rule is inert. It is carried from
|
||||
the start because it is a field in a file: adding one later means every reader has to tolerate
|
||||
its absence forever, while an unused `bool` with a serde default costs nothing. Pinning itself
|
||||
is out of scope — see §8.
|
||||
|
||||
### Why not a field on `Project`
|
||||
|
||||
`projects.json` is written on **every blur** by the debounced `useProjectSave` path
|
||||
@@ -339,6 +346,9 @@ that — but anything touching real key handling needs a manual check in Chromiu
|
||||
generic write-a-file-to-container command today (only `write_file_to_container` for image
|
||||
paste and `upload_bytes_to_container` for migration), and a second storage path with a
|
||||
sync direction is a v2 conversation.
|
||||
- Pinning. `Note.pinned` exists on both sides of the IPC boundary and the backend sorts on
|
||||
it, but no UI sets it and none indicates it — see §1. A pin control is a user-facing
|
||||
affordance and belongs in the change that adds it, not in the storage that anticipates it.
|
||||
- Tags, full-text search, manual reordering, note history.
|
||||
- Any change to `claude_instructions`. The two features stay distinct: ambient context
|
||||
versus fired-on-demand items.
|
||||
|
||||
Reference in New Issue
Block a user