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");
|
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" })];
|
notes = [note(), note({ id: "n2", title: "Gotchas" })];
|
||||||
render(<NotesPanel projectId="p1" />);
|
render(<NotesPanel projectId="p1" />);
|
||||||
fireEvent.click(screen.getByRole("button", { name: /delete note/i }));
|
fireEvent.click(screen.getByRole("button", { name: /delete note/i }));
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ describe("toClaudePayload", () => {
|
|||||||
expect(toClaudePayload("one\r\ntwo")).toBe("one\x1b\rtwo");
|
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", () => {
|
it("leaves single-line text untouched", () => {
|
||||||
expect(toClaudePayload("just one line")).toBe("just one line");
|
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
|
* 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
|
* 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.
|
* 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 {
|
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.
|
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`
|
### Why not a field on `Project`
|
||||||
|
|
||||||
`projects.json` is written on **every blur** by the debounced `useProjectSave` path
|
`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
|
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
|
paste and `upload_bytes_to_container` for migration), and a second storage path with a
|
||||||
sync direction is a v2 conversation.
|
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.
|
- Tags, full-text search, manual reordering, note history.
|
||||||
- Any change to `claude_instructions`. The two features stay distinct: ambient context
|
- Any change to `claude_instructions`. The two features stay distinct: ambient context
|
||||||
versus fired-on-demand items.
|
versus fired-on-demand items.
|
||||||
|
|||||||
Reference in New Issue
Block a user