Add a per-project Notes tab with a send-to-agent action #48

Merged
jknapp merged 20 commits from feat/project-notes into main 2026-09-02 20:42:07 +00:00
2 changed files with 37 additions and 14 deletions
Showing only changes of commit 0f3fff92f4 - Show all commits
+10 -2
View File
@@ -30,15 +30,22 @@ export default function NoteEditor({
}: Props) {
return (
<div className="flex flex-col h-full min-h-0 gap-2 p-3">
<div className="flex items-center gap-2">
{/* Wraps rather than overflows. The two buttons are a group with a fixed
appetite (~190px) and the title field can shrink only so far, so in a
narrow dock the title takes the first row and the buttons the second.
Without the wrap the group is simply clipped by the dock's
`overflow-hidden`, which puts Delete off-window with no scrollbar to
reach it. */}
<div className="flex flex-wrap items-center gap-2">
<input
value={title}
onChange={(e) => onTitleChange(e.target.value)}
onBlur={onCommit}
placeholder="Note title"
aria-label="Note title"
className="flex-1 min-w-0 px-2 h-8 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] text-[13px] text-[var(--text-primary)] focus:border-[var(--accent)] transition-colors"
className="flex-1 min-w-24 px-2 h-8 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-[var(--radius-control)] text-[13px] text-[var(--text-primary)] focus:border-[var(--accent)] transition-colors"
/>
<div className="flex items-center gap-2 flex-shrink-0">
{/* The live editor text, not `note.body` — what is on screen is what
gets sent. */}
<SendToAgentButton projectId={projectId} body={body} />
@@ -46,6 +53,7 @@ export default function NoteEditor({
Delete
</Button>
</div>
</div>
<textarea
value={body}
onChange={(e) => onBodyChange(e.target.value)}
+21 -6
View File
@@ -14,9 +14,21 @@ const UNTITLED = "Untitled note";
* The notes surface itself, shared by the Project Home tab and the dock so the
* two cannot drift into different behaviour.
*
* Master/detail: titles on the left, one editor on the right. The editor holds
* draft text locally and commits on blur, which is how every other editable
* field in the app behaves (`ClaudeInstructionsEditor`, the Config tab).
* Master/detail: titles beside the editor when there is room, stacked above it
* when there is not. That is a **container** query, not a viewport one, because
* the two surfaces differ in width while sharing a viewport — the dock opens at
* 352px and the tab is the width of the main area. A `md:` breakpoint would
* read the window and give both the same answer, which is the wrong answer for
* one of them.
*
* The threshold is arithmetic, not taste: side by side needs the 192px list,
* plus an editor wide enough for its own action row (~280px), plus the divider.
* Below ~473px the editor is narrower than its buttons, so `@lg` (512px) is the
* first stop that clears it.
*
* The editor holds draft text locally and commits on blur, which is how every
* other editable field in the app behaves (`ClaudeInstructionsEditor`, the
* Config tab).
*/
export default function NotesPanel({ projectId }: Props) {
const { notes, loading, saveState, createNote, saveNote, deleteNote } =
@@ -83,7 +95,7 @@ export default function NotesPanel({ projectId }: Props) {
}
return (
<div className="flex flex-col h-full min-h-0">
<div className="@container flex flex-col h-full min-h-0">
<div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-[var(--border-color)]">
<Button variant="primary" onClick={onCreate}>
New note
@@ -99,8 +111,11 @@ export default function NotesPanel({ projectId }: Props) {
</p>
</div>
) : (
<div className="flex-1 min-h-0 flex">
<ul className="w-48 flex-shrink-0 overflow-y-auto border-r border-[var(--border-color)] py-1">
<div className="flex-1 min-h-0 flex flex-col @lg:flex-row">
{/* Stacked: a capped strip of titles above the editor, so the note
being written keeps most of the height. Side by side: a full-height
column of the fixed width the editor's arithmetic assumes. */}
<ul className="flex-shrink-0 overflow-y-auto py-1 max-h-32 border-b @lg:max-h-none @lg:w-48 @lg:border-b-0 @lg:border-r border-[var(--border-color)]">
{notes.map((n) => (
<li key={n.id}>
<button