Skip to content

feat: add child note aggregation for project sessions - #923

Merged
matt2e merged 11 commits into
mainfrom
child-notes
Aug 21, 2026
Merged

feat: add child note aggregation for project sessions#923
matt2e merged 11 commits into
mainfrom
child-notes

Conversation

@matt2e

@matt2e matt2e commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add parent note tracking so child session outcomes can aggregate into project notes
  • render and navigate #note hashtags in note bodies, including foreign project notes
  • support attaching text snippets and images from the new session dialog
  • clean up orphaned child sessions and stale hashtag/clipboard paths

matt2e added 11 commits August 21, 2026 09:55
Phase 1 (backend-only) of the parent/child notes plan v2: introduce a
single conservative `parent_project_note_id` column on `notes` so notes
can be aggregated as children under a project note.

- Migration 0016-add-note-parent: ALTER TABLE notes ADD COLUMN
  parent_project_note_id TEXT plus a supporting index. No FK constraint —
  the notes and project_notes tables have independent lifecycles, so
  cleanup is enforced in code (see delete_project_note below).
- Bump the schema-version assertions to 16 and assert the new column
  exists in the bootstrap migration test. The two user_version
  repair-test fixtures gain a minimal `notes` table so the new migration
  applies against them (a real v12/v13 database always has it from the
  baseline).
- Note model: add the parent_project_note_id field (defaults to None) and
  a with_parent_project_note builder mirroring with_session.
- Store: thread the new column through create_note and row_to_note;
  exclude children from list_notes_for_branch (hiding them from the
  timeline and #-autocomplete in unrelated sessions); add list_child_notes
  as the dedicated path the parent-note view will use.
- delete_project_note now cascades to its child notes in the same
  transaction, which fires the existing note-delete trigger so any
  sessions orphaned by removed children are cleaned up too.
- Add store tests for list_child_notes, timeline exclusion, child
  cascade-delete, and child-session cleanup.

Per AGENTS.md this kind of data-model change normally needs human
sign-off; the user explicitly authorized it for this phase.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 2 (backend/plumbing only) of the parent/child notes plan v2: let a
project session that calls `start_repo_session` create a CHILD note —
attached to the project's parent project note and hidden from the repo
timeline — via a new expected-outcome variant. No frontend changes and no
prompt-wording changes (Phase 4 owns prompt wording), so the new variant
is not yet advertised in the tool description or `expected_outcome` field
docs; the backend is simply ready to honor it.

- RepoSessionOutcome: add a `ChildNote` variant (wire value `child_note`
  via the existing serde snake_case). The existing `note_in_repo` and
  `commit` variants are untouched, so this stays non-breaking.
- Thread the parent project-note id to the handler via SessionConfig
  (recommended route "a"): add `parent_project_note_id: Option<String>`
  to `ProjectToolsHandler`, its constructor, and `start_project_mcp_server`,
  and add the matching field to `SessionConfig`, passed through to
  `start_project_mcp_server` in the runner. Both project-session creation
  sites (session_commands and web_server) populate it with the project
  note id created just above. Resume sites also populate it from the
  resumed session's project note, so `child_note` keeps working across
  follow-up turns. All other call sites (branch sessions, plain sessions,
  pipeline handoff) set `None`.
- ChildNote match arm in `start_repo_session`: builds the note like the
  `note_in_repo` arm
  (`Note::new(branch, instructions, "").with_session(session)`) but also
  calls `.with_parent_project_note(parent_id)` when a parent is in scope.
  Note-stub construction for both note outcomes is factored into a small
  `build_repo_note_stub` helper.

Behavior when `parent_project_note_id` is `None` (a `child_note` requested
with no parent in scope, e.g. a non-project session): fall back to creating
a plain detached note — equivalent to `note_in_repo` — rather than
erroring. This is the safer choice: a misrouted child request still yields
a usable note instead of a hard failure, and a detached note is the
conservative default the rest of the system already handles. The response
still returns `artifact.id` (the child note id) immediately at stub
creation, so the parent agent receives the child id before the child
session finishes.

Add focused tests in project_mcp: a child-note stub with a parent attaches
to it, is returned by `list_child_notes`, and is excluded from
`list_notes_for_branch`; without a parent it is detached and visible in the
timeline.

Verified with `cargo fmt --check`, `cargo clippy -- -D warnings`, and
`cargo test --lib --bins --tests` (324 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 3 (frontend) of the parent/child notes plan v2: `#kind:id`
hashtags inside a rendered NOTE BODY now display as styled, clickable
badges, and clicking a `#note:<id>` badge opens that note inline —
including child notes that live on other repo branches.

Badge styling moves from inline `style` to CSS classes so badges survive
sanitization:
- app.css: add `.hashtag-badge.type-<kind>` rules sourced from the
  existing `hashtagTypeColors` map (all colours via `var(--*)` tokens),
  plus a pointer cursor for clickable badges.
- hashtagItems.ts: factor badge HTML into a shared `renderHashtagBadge`
  helper that emits `class="hashtag-badge type-<kind>"` +
  `data-hashtag-kind`/`data-hashtag-id` (no inline `style`).
  `renderHashtagTokens` (titles/chat bubbles) now uses it too, so all
  badge contexts unify on the CSS classes.

Parsing instead of post-processing HTML:
- hashtagItems.ts: add `createHashtagMarked(items)`, a `marked` instance
  with an inline tokenizer extension that turns `#kind:id` tokens into
  badge spans during parse. Because it runs inside `marked`, fenced and
  inline code are already separate tokens and are left untouched (no
  regex over produced HTML). Token grammar reuses `HASHTAG_TOKEN_RE`.

Sanitizer:
- sanitize.ts: allow `data-hashtag-kind`/`data-hashtag-id` on `span`
  only. `style` stays stripped — the point of class-based styling.

Title resolution incl. cross-branch children:
- Backend `get_note` and `list_child_notes` Tauri commands (registered
  in lib.rs and the web_server dispatch) return `NoteTimelineItem`s with
  resolved session status; `getNote`/`listChildNotes` wrappers added to
  commands.ts.
- ProjectSection merges `buildProjectHashtagItems` with the open note's
  children (fetched via `list_child_notes`) — children are excluded from
  per-branch timelines (Phase 1), so they must be merged in explicitly to
  resolve `#note:<childId>` to a title.

Navigation:
- NoteModal renders via the items-bound marked instance, adds a delegated
  click handler on the body that reads `data-hashtag-*` off the clicked
  badge and calls `onHashtagClick(kind, id)`, and gains a back button.
- ProjectSection wires `onHashtagClick` for `kind === 'note'` to
  `getNote` and opens the result, keeping a small back-stack so the user
  can return to the parent note (non-destructive). Non-note kinds stay as
  styled badges (no-op).

Tests:
- hashtagItems.test.ts: `createHashtagMarked` output for `#note:id`
  (span.hashtag-badge.type-note + data attrs + resolved title), per-kind
  type classes, raw-id fallback, and that inline/fenced code is untouched.
- sanitize.test.ts (new): data-hashtag attributes survive on span while
  `style` is stripped, and they are not allowed on other tags.

Verified with `just check-all`: cargo fmt, clippy -D warnings,
svelte-check (0 errors/0 warnings), cargo test (324 passing), and
frontend vitest (249 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 4 (prompt/documentation wording only) of the parent/child notes
plan v2: teach the project-session agent to prefer the `child_note`
outcome for spawned research/planning and to organize its note into
sections that reference each child via `#note:<id>`. No Rust logic
changes, no new fields, no frontend — only string literals and doc
comments.

- expected_outcome field docs (project_mcp.rs): enumerate all three
  variants. Document `child_note` first and steer toward it ("Prefer
  this for research/planning spawned by this project session, so the
  work attaches to this project note and stays out of the repo's
  visible timeline"), note that the returned `artifact.id` is the child
  id to reference as `#note:<id>`, and relabel `note_in_repo`
  conceptually as the "detached note" outcome (stands alone in the
  repo's visible timeline). Wire values are unchanged.
- RepoSessionOutcome::NoteInRepo enum doc: relabel as a "detached" note
  for internal consistency with the agent-facing wording.
- start_repo_session tool description (project_mcp.rs): mention all
  three outcomes with the same prefer-child_note steering.
- action_instructions (session_commands.rs + web_server.rs): add a
  paragraph instructing the agent to organize the END of its note into
  sections grouping delegated work (e.g. `## Research`, `## Plans`,
  `## Collected logs`) and reference each spawned child note as
  `#note:<id>`, where `<id>` is the `artifact.id` returned by
  start_repo_session.
- start_repo_session_desc (session_commands.rs + web_server.rs, both
  local and remote variants): swap the "Use note_in_repo for repo
  notes" wording for the prefer-child_note / note_in_repo-is-detached
  guidance.

The two prompt strings live in duplicate copies — the Tauri command
path (session_commands.rs) and the web-server dispatch path
(web_server.rs) — so both were updated to keep the desktop and web
runtimes coherent. The `child_note` steering is confined to
project-session-only paths (the project-session prompt and the project
MCP tool, which is only attached to project sessions), so standalone
branch sessions, which have no parent note, are unaffected.

No test or snapshot asserts on these strings (existing tests cover
note-attachment behavior, not prompt text), so none needed updating.

Verified with `cargo fmt --check`, `cargo clippy -- -D warnings`, and
`cargo test --lib --bins --tests` (324 passing). Frontend untouched.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 5 of the parent/child notes plan v2: let the New Session dialog
attach text snippets that ride along in the prompt. This phase is
INDEPENDENT of phases 1-4 and stays entirely backend-free and
modal-local — no data-model change, no new backend entity, no runner
change, and the `onSubmit({ prompt, mode, imageIds })` signature is
unchanged. Snippets are folded into the `prompt` string on submit.

Per the plan's resolved decisions: snippets are DECOUPLED from mode
(Option A — they ride along in note/commit/review prompts; no mode-error
path, Option B skipped), and drag-dropped text-file PATHS keep their
existing insert-at-cursor behavior (additive — snippet creation is a
separate capability that does not replace muscle memory).

- Clipboard read capability: add `clipboard-manager:allow-read-text` to
  src-tauri/capabilities/default.json and a `readClipboardText()` sibling
  in transport.ts using the plugin's `readText()` (preferred over
  navigator.clipboard.readText() — more reliable in the WebView, fewer
  focus/permission constraints).
- Pure, testable helpers in sessionModalHelpers.ts: `TextSnippet`,
  `CLIPBOARD_SNIPPET_MIN_LENGTH` (64), `shouldOfferClipboardSnippet`
  (gates strictly above the threshold), `snippetLabel` (single-line
  truncated preview), and `foldSnippetsIntoPrompt` (appends each snippet
  wrapped as `\n\n<attached-snippet>\n…\n</attached-snippet>`).
- ImageAttachment.svelte now handles snippets too: relabel the attach
  control "Attach images" -> "Attach images or text snippets", render
  snippet chips (truncated label + remove X) in the SAME flex row as the
  image thumbnails reusing the chip styling, and show an "Attach
  clipboard" button (icon in the row state, labeled when empty) when the
  parent supplies eligible clipboard text. New optional props keep the
  component usable for images alone.
- NewSessionModal.svelte: add `textSnippets` state mirroring `imageIds`,
  read the clipboard on open and on window focus (failures simply hide
  the button), and wire add/remove/attach handlers. handleSubmit folds
  snippets into the trimmed prompt before calling onSubmit; having a
  snippet (like having a prompt) is now a valid reason to enable submit
  in non-review modes (submit-disabled check, Cmd+Enter, and the guard
  all updated). Both ImageAttachment instances are rendered whenever
  there are images OR snippets and receive the snippet props.

Deviation from the plan as written: snippet creation is limited to the
"Attach clipboard" button (the plan explicitly permits this — "it's fine
to limit snippet creation to the clipboard button"). No drag-drop snippet
affordance and no readTextFile-based file picker were added, so the
existing drop-to-insert-path behavior is left completely untouched and no
new muscle memory is introduced.

Tests: sessionModalHelpers.test.ts gains focused cases for the
`<attached-snippet>` fold delimiter (incl. snippet-only empty prompt),
the strict >64 clipboard threshold, and label collapsing/truncation.

Verified with `just check-all`: cargo fmt, clippy -D warnings,
svelte-check (0 errors/0 warnings), cargo test, and frontend vitest
(257 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Phase 1 of the parent/child notes work filtered children out of
`Store::list_notes_for_branch` so they stay out of the UI timeline and
`#`-autocomplete. That query also backs the agent-facing `<branch-history>`
block, so child notes silently vanished from agent context — while Phase 4
steers project sessions to cite them as `#note:<id>` in the parent project
note, which IS in branch history. Agents saw dangling references to notes
they could no longer read.

`list_notes_for_branch` has exactly two production callers (the UI timeline
in timeline.rs and `note_timeline_entries` in session_commands.rs), and
`note_timeline_entries` is the single choke point for all three context
builders (local, remote/Blox, and the per-branch loop in the project
context), so one listing swap plus one formatting change covers everything.

- store/notes.rs: add `list_all_notes_for_branch` — identical to
  `list_notes_for_branch` minus the `parent_project_note_id IS NULL`
  predicate and with the same ordering. `list_notes_for_branch` is
  untouched; its doc comment (and `list_child_notes`') now spells out that
  the exclusion is a UI-visibility rule and that branch history uses the
  new method.
- session_commands.rs: `note_timeline_entries` lists via
  `list_all_notes_for_branch`. Children carry normal timestamps, so they
  interleave chronologically like any other note, and the existing
  empty-content guard still skips a still-generating child.
- session_commands.rs: `format_note_for_context` now takes `&Note` (its one
  caller had already been passing the note's fields, and the `Option`
  return was vestigial) and owns the layout so it can render a reference
  line for a child:

      ### Note: <title>

      Child note #note:<child-id> of project note #project-note:<pid>.

      See: `/tmp/staged-note-<child-id>.md`

  The parent hashtag is the point — the parent project note appears in the
  same branch history, so the agent can correlate. The self-reference is
  included so the agent doesn't have to infer the child's id from its
  temp-file name to match the `#note:<id>` citations in the parent's body.
  The inline fallback (temp-file write failure) emits the same line.
  `format_note_with_heading` stays generic for the project-note formatter.

Explicit non-changes: the UI timeline and `#`-autocomplete keep using
`list_notes_for_branch`; no migration, no frontend, no prompt wording, and
no change to child-note creation in project_mcp.rs.

Tests: `list_all_notes_for_branch` returns a child alongside a standalone
note in the right order while the timeline query still hides it; the
rendered child entry carries `#note:<child-id>` and `#project-note:<pid>`
while a parentless note gets no reference line; the project_mcp child-note
test additionally asserts branch-history inclusion.

Also folds in a one-line rustfmt fix in project_mcp.rs's test imports that
was already failing `cargo fmt --check` on HEAD.

Verified with `cargo fmt --check`, `cargo clippy -- -D warnings`, and
`cargo test --lib --bins --tests` (717 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Resolves all seven comments from review fcd78345 on the parent/child notes
branch.

Child-session lifecycle (the repeat finding, reported twice):
- `Store::delete_project_note` now returns `DeletedProjectNoteSessions`
  (the project note's own session id plus every child note's) instead of
  just the parent's. The child ids are read inside the same transaction,
  before the cascade deletes those rows.
- `note_commands::delete_project_note` takes the `SessionRegistry` and
  cancels each orphaned session before deleting it. Previously a child
  session that was still `running` when its parent note was deleted
  survived: `trg_cleanup_session_after_note_delete` deliberately skips
  running sessions, so the process kept going with no note row to write
  into, and the session row was never cleaned up either (the trigger had
  already fired). The web-server `delete_project_note` arm mirrors this
  via the `session_registry` already in dispatch scope.
- `DeletedProjectNoteSessions` is a function return type, not a schema or
  persisted-model change — no migration, no new columns.

Duplication and dead code:
- `note_commands::note_to_timeline_item` is now `pub(crate)` and the two
  web-server dispatch arms (`get_note`, `list_child_notes`) call it
  instead of re-inlining the Note -> NoteTimelineItem mapping, so session
  status resolves identically in both runtimes.
- Removed `createHashtagMarked` (and its tests): the rebase left it with
  no production caller — note bodies render through `renderMarkdown`'s
  `renderInlineText` hook, which stashes badge HTML as trusted
  placeholders. Its per-kind type-class coverage moves to a new
  `renderHashtagTokens` case on that live path.
- Reverted the `sanitize` span allowlist to `['class']` and deleted
  sanitize.test.ts. The `data-hashtag-*` entries were inert: badge HTML
  is restored after `sanitize` runs, never through it. The file is now
  identical to main.
- Dropped `data-hashtag-kind` (a duplicate of `data-hashtag-type`, which
  is what the click handlers actually read) and the now-unused
  `hashtagTypeColors` map, whose only remaining "consumer" was an app.css
  comment claiming the classes mirrored it.

Unresolved `#note:<id>` references:
- `ProjectSection.handleHashtagClick` falls back to `getNote` when a note
  reference isn't in the merged item list (e.g. a child of a different
  project note), so the click opens the note instead of silently
  no-opping. This gives the `get_note` command, web dispatch arm and
  `getNote` wrapper — all callerless after the rebase — their intended
  consumer. A capture of the open note id guards against the dialog
  moving on while the fetch is in flight.

Clipboard snippets:
- The clipboard is now read only when "Attach clipboard" is clicked.
  Reading on modal open and on every window focus meant web-mode users
  got a paste-permission prompt just for opening the new-session dialog,
  and the attach used text cached at the last read rather than what was
  on the clipboard at click time. The button is now shown whenever the
  parent passes `onAttachClipboard`, so `ImageAttachment`'s
  `clipboardText` prop is gone.
- Consequently `shouldOfferClipboardSnippet` / `CLIPBOARD_SNIPPET_MIN_LENGTH`
  are removed: the length threshold existed only to decide whether to
  passively offer the button. An explicit click is intent, so any
  non-empty clipboard text attaches; `addSnippet` skips text identical to
  an already-attached snippet so a double click doesn't duplicate it
  (the third comment from the earlier review).
- `handleClose` folds attached snippets into the preserved draft prompt.
  Snippets are modal-local state, so an accidental Esc previously dropped
  them while prompt and images survived; the draft now reopens with the
  same text a submit would have sent.

Tests: new store case asserting a still-running child session is reported
(and left behind by the trigger) for the caller to clean up; the existing
child-session cleanup test now also asserts the returned ids.

Verified with `just check-all`: cargo fmt, clippy -D warnings,
svelte-check (0 errors/0 warnings), cargo test (718 passing), and
frontend vitest (635 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ct notes

Resolves both comments from review ed66ec10 on the parent/child notes
branch.

Punctuation-adjacent references (first comment):
- HASHTAG_TOKEN_RE captured the id as `[^\s]+`, so a prose citation like
  `collected in #note:abc123.` yielded the id `abc123.` — exactly the
  shape Phase 4's prompt wording asks agents to write. The badge then
  rendered with a raw punctuated id instead of the note title, and a
  click resolved nothing: the punctuated id missed the merged item list
  AND the `getNote` fallback added in 634c7e3, so the click silently
  no-opped.
- Fixed at the tokenizer rather than in the click fallback (the review's
  suggestion): the id may now run to the next whitespace but not END in
  `.,;:!?)]}'"`. That fixes rendering, `#`-autocomplete round-tripping
  and clicking in one place, for every badge context, and leaves the
  trailing punctuation in the surrounding prose where it belongs.
  Interior punctuation is untouched (`#note:a.b).` -> id `a.b`), and no
  real id can end in that set — note/project-note/review/image ids are
  uuids and commit ids are hex shas.
- Because the badge is the only click source (both delegated handlers
  read `data-hashtag-*` off it), no strip is needed downstream.

Unresolved `#project-note:<id>` references (second comment):
- Branch history renders a child as "Child note #note:<id> of project
  note #project-note:<pid>", so an agent can quote a project-note
  reference into a body later read from a different project — where it
  is absent from the merged item list and, until now, silently
  no-opped. `get_note` reads the notes table and can't serve it.
- Added the symmetric `get_project_note` path: store
  `get_project_note_with_status` (by id, no project scope, session
  status resolved like its by-session sibling), the Tauri command
  registered in lib.rs, the matching web-server dispatch arm, and a
  `getProjectNote` wrapper in commands.ts.
- ProjectSection's fetch fallback now covers `project-note` clicks as
  well as `note` clicks, via a small `fetchUnresolvedNoteItem` helper.
  A `note` click still tries `getNote` first and falls through to the
  project-note lookup, because `#note:<id>` also aliases project notes
  in the existing hashtag lookup keys. The in-flight staleness guard is
  unchanged.

Tests: renderHashtagTokens resolves `#note:<id>.` and `(#note:<id>),` to
the titled badge while the punctuation stays as text, and keeps interior
punctuation in the id; a store case asserts `get_project_note_with_status`
finds another project's note by id with its session status resolved and
returns None for a missing id.

Verified with `just check-all`: cargo fmt, clippy -D warnings,
svelte-check (0 errors/0 warnings), cargo test (719 passing), and
frontend vitest (637 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Resolves the remaining comment from review 48c84f07 on the parent/child
notes branch: HASHTAG_TOKEN_RE's excluded-trailing-punctuation set was
ASCII-only, so Unicode sentence punctuation (`#note:abc—`, `#note:abc”`,
`#note:abc…`) was still swallowed into the id — reproducing exactly the
raw-id-badge/dead-click bug the denylist was added to fix for `.`/`,`/`)`.

Since this class of bug had already bitten once, take the review's
stronger suggestion and anchor on the known id shape instead of growing
the denylist. The id now matches an allowlist of id characters —
alphanumerics and `_`, with interior (never leading/trailing) hyphen
runs — which covers every real id (note/project-note/review/image ids
are uuids, commit ids are hex shas) as well as the alias-style fixture
ids used in tests (`note-1`, `project-note-1`).

This is strictly stronger than any denylist could be: a trailing-only
exclusion can never handle em-dashes and ellipses, which bind with NO
surrounding whitespace (`#note:<id>—also prose` puts the punctuation
mid-"word", not token-final). With the allowlist, any character outside
the id shape ends the token at any position, ASCII or Unicode, known or
not-yet-encountered.

Behavior change at the margin: interior punctuation is no longer kept in
an id (`#note:a1.b2` now tokenizes as id `a1` with `.b2` left as prose,
where the denylist kept `a1.b2`). No real id contains such characters —
that test-documented behavior was a side effect of the old regex, not a
requirement — so the affected test is rewritten to document the new
id-shape rule. A strict uuid/hex anchor was considered and rejected: the
alias ids above are not pure hex, and the looser word-shape gains the
same punctuation immunity without constraining id formats.

All four regex consumers (renderHashtagTokens, hasHashtagTokens, and the
two HashtagInput paths — selected-token keys and the full-token dropdown
close check) share HASHTAG_TOKEN_RE.source, so the single source change
covers rendering, clicking, and `#`-autocomplete round-tripping alike.
The createExtractedValueBuilder doc comment drops its stale "up to
whitespace" wording; its space-guard rationale still holds since hex
text adjacent to a token would still merge into the id.

Tests: the sentence-punctuation case gains em-dash (with bound trailing
prose), curly-quote, and ellipsis citations; a new id-shape case asserts
the token stops at the first non-id character, keeps interior hyphens,
and leaves a trailing hyphen as text.

Verified with `just check-all`: cargo fmt, clippy -D warnings,
svelte-check (0 errors/0 warnings), cargo test (719 passing), and
frontend vitest (637 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Resolves the remaining UI comment from review 48c84f07 on the
parent/child notes branch: the empty-state attach button in
ImageAttachment.svelte was relabeled "Attach images or text snippets"
in Phase 5, but it only ever opens the image file picker — snippets are
created exclusively through the separate "Attach clipboard" button
rendered right next to it. A user clicking the button to attach a
snippet got an image-only picker.

Per the review's suggestion, fix by focusing the label on images:
revert the button text to "Attach images". The component doc comment
keeps its images-or-snippets wording since the component as a whole
still renders snippet chips and the clipboard-attach control.

String-only change; no logic touched. Verified with `just check-all`:
cargo fmt, clippy -D warnings, svelte-check (0 errors/0 warnings),
cargo test (719 passing), and frontend vitest (637 passing).

Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e merged commit 30bcb12 into main Aug 21, 2026
2 checks passed
@matt2e
matt2e deleted the child-notes branch August 21, 2026 03:28

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b5ea5bc7a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +677 to +684
let note = build_repo_note_stub(
&target.branch.id,
&p.instructions,
&session.id,
self.parent_project_note_id.as_deref(),
);
let note_id = note.id.clone();
if let Err(e) = self.store.create_note(&note) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate the parent before inserting a child note

When a project note is deleted while start_repo_session is already in flight, deletion can finish before this insert acquires the store lock. The handler never checks its cancellation token or verifies that parent_project_note_id still exists, and the new column has no foreign key, so it can create and start a note whose deleted parent can no longer display or clean it up. Make validating the parent and creating the child atomic, or reject the request after parent cancellation/deletion.

Useful? React with 👍 / 👎.

Comment on lines 380 to +384
function openProjectNote(note: ProjectNote, chatOpen = false) {
openNote = projectNoteToOpenState(note, chatOpen);
childHashtagItems = [];
void ensureHashtagItems();
void loadChildHashtagItems(note.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh child note items after their sessions finish

When the parent modal is opened while one of its child sessions is still running, this is the only child-note load performed. The session-status-changed handler later invalidates the ordinary branch/project hashtag data, but never reloads childHashtagItems; because child notes are intentionally excluded from branch timelines, the badge retains the stub title/content until the user closes and reopens the parent. Reload the open parent's children on matching terminal session events or timeline invalidation.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant