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
+16 -8
View File
@@ -30,21 +30,29 @@ export default function NoteEditor({
}: Props) { }: Props) {
return ( return (
<div className="flex flex-col h-full min-h-0 gap-2 p-3"> <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 <input
value={title} value={title}
onChange={(e) => onTitleChange(e.target.value)} onChange={(e) => onTitleChange(e.target.value)}
onBlur={onCommit} onBlur={onCommit}
placeholder="Note title" placeholder="Note title"
aria-label="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"
/> />
{/* The live editor text, not `note.body` — what is on screen is what <div className="flex items-center gap-2 flex-shrink-0">
gets sent. */} {/* The live editor text, not `note.body` — what is on screen is what
<SendToAgentButton projectId={projectId} body={body} /> gets sent. */}
<Button variant="danger" onClick={onDelete} aria-label="Delete note"> <SendToAgentButton projectId={projectId} body={body} />
Delete <Button variant="danger" onClick={onDelete} aria-label="Delete note">
</Button> Delete
</Button>
</div>
</div> </div>
<textarea <textarea
value={body} value={body}
+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 * The notes surface itself, shared by the Project Home tab and the dock so the
* two cannot drift into different behaviour. * two cannot drift into different behaviour.
* *
* Master/detail: titles on the left, one editor on the right. The editor holds * Master/detail: titles beside the editor when there is room, stacked above it
* draft text locally and commits on blur, which is how every other editable * when there is not. That is a **container** query, not a viewport one, because
* field in the app behaves (`ClaudeInstructionsEditor`, the Config tab). * 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) { export default function NotesPanel({ projectId }: Props) {
const { notes, loading, saveState, createNote, saveNote, deleteNote } = const { notes, loading, saveState, createNote, saveNote, deleteNote } =
@@ -83,7 +95,7 @@ export default function NotesPanel({ projectId }: Props) {
} }
return ( 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)]"> <div className="flex items-center justify-between gap-2 px-3 py-2 border-b border-[var(--border-color)]">
<Button variant="primary" onClick={onCreate}> <Button variant="primary" onClick={onCreate}>
New note New note
@@ -99,8 +111,11 @@ export default function NotesPanel({ projectId }: Props) {
</p> </p>
</div> </div>
) : ( ) : (
<div className="flex-1 min-h-0 flex"> <div className="flex-1 min-h-0 flex flex-col @lg:flex-row">
<ul className="w-48 flex-shrink-0 overflow-y-auto border-r border-[var(--border-color)] py-1"> {/* 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) => ( {notes.map((n) => (
<li key={n.id}> <li key={n.id}>
<button <button