feat: add child note aggregation for project sessions - #923
Conversation
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>
There was a problem hiding this comment.
💡 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".
| 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(¬e) { |
There was a problem hiding this comment.
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 👍 / 👎.
| function openProjectNote(note: ProjectNote, chatOpen = false) { | ||
| openNote = projectNoteToOpenState(note, chatOpen); | ||
| childHashtagItems = []; | ||
| void ensureHashtagItems(); | ||
| void loadChildHashtagItems(note.id); |
There was a problem hiding this comment.
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 👍 / 👎.
Summary