Repository navigation
feat(staged): surface note revisions and chat replies on note rows - #1005
Merged
Merged
Conversation
…last update The project notes list and the branch card timeline both labelled and sorted each note by when it first finished (`completedAt ?? createdAt`), so rewrites by follow-up turns or edits were invisible. Add a small `noteTimestamps` helper, typed on the shared `createdAt`/`updatedAt`/`completedAt` columns so it serves both `NoteTimelineItem` and `ProjectNote`: - `isNoteRevised`: `completedAt` is set and `updatedAt` has moved past it. Migration-backfilled rows (`completedAt = updatedAt`) read as unrevised. - `noteLastWrittenAt`: `updatedAt` once revised, otherwise the old `completedAt ?? createdAt`. - `formatNoteTimestamp`: "Updated 5m ago" once revised, otherwise the existing bare "5m ago". Both lists now use these for the row label and the sort key. The sort direction stays ascending, so a just-updated note moves to the bottom. The backend `ORDER BY` is unchanged: both lists re-sort on the client, and the SQL order also feeds other callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The chat button on a note row was a hover-only icon with no state, so replies that arrived in a note's chat after the note was written were invisible from the project notes list and the branch timeline. The note dialog already knew about them, but the lists did not. The button now has three states, decided by `noteChatState` from the chat's latest reply (L), the note's `updatedAt` (U) and the chat's read time (R): - none (L missing or L <= U): unchanged, hover-only "View session". - read (L > U, R >= L): `MessageSquareText` in `--text-muted`, visible without hover. - unread (L > U, R missing or R < L): the same, plus a 6px `--ui-accent` dot on the icon's corner. Only completed note rows get read/unread. A turn that rewrites the note moves U past every reply, so the button drops back to none on its own. Backend: - Migration 0035 adds a nullable `sessions.chat_read_at` (ms, backend clock). Existing rows are backfilled to migration time, so the upgrade doesn't put an unread dot on every old note. - `session_chat_activity` returns the latest visible assistant reply and `chat_read_at` in one query. The visible-reply condition is now one constant shared with `count_assistant_messages_after`. - `mark_session_chat_read` writes the server time only when a reply is newer than the last read. When it writes, it publishes `StoreChange::Notes` for the owning branch or project note, so every window reloads that list. It is the one session-family write that publishes. - `NoteTimelineItem` and `ProjectNote` carry `last_chat_message_at` and `chat_read_at`, filled by the branch timeline, `note_to_timeline_item` and all three project-note `*_with_status` lookups. The project-note lookups share one resolver helper. - New `mark_session_chat_read` command, with web dispatch. Frontend: - `NoteModal` marks the chat read when the chat pane is shown and again when it is hidden or the dialog closes, which covers replies that streamed in while it was open. Clicking the row body is covered too, because the dialog already auto-opens the chat when there are replies after the note. - `TimelineRow` now hides and reveals each action on hover individually instead of fading the whole container, so a chat button with replies can stay visible while Delete and the other actions still wait for hover. Moving a note still bumps `updated_at`, so a moved note's chat button goes back to none. That is intentionally left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…s up on lookup misses Follow-ups from the review of the read/unread chat button. `mark_session_chat_read` wrote and published whenever a reply was newer than the last read. A session created after migration 0035 starts with `chat_read_at` NULL, so the first "View session" click on almost any note wrote and published `StoreChange::Notes`, and so did each open/close while watching a running note's chat. That rebuilt the whole branch timeline or project notes list behind the opening dialog, even though the row stayed in its plain state. The guard now also requires the reply to be newer than the owning note's `updated_at` (branch or project note, 0 when the session owns none). This is safe because the button ignores the read time while every reply predates the note, and a later reply still reads as unread against the older read time. The chat-activity lookup in the branch timeline and in the project-note resolver now falls back to no activity on error, as `note_to_timeline_item` and the session status lookup already do, so a chat-button detail can't blank either list. On touch the session button widens to 44px but its 12px icon stays centred, so the unread dot (`right: 2px`) drifted about 8px off the icon's corner. It is now anchored to the centre (`left: calc(50% + 3px)`), which keeps the same spot at 22px and stays on the corner at 44px. Tests: a new change-feed test covers replies that predate their branch or project note (no write, no publish) and a later reply past the note (writes and publishes). Three existing tests that add a reply right after creating the note now backdate the note, so they don't flake when both land in the same millisecond. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
matt2e
added a commit
that referenced
this pull request
Nov 1, 2026
Rebase onto main, which added lastChatMessageAt and chatReadAt to ProjectNote in #1005 (surface note revisions and chat replies on note rows). The completedNote fixture in ProjectSection.component.test.ts is the only hand-rolled ProjectNote literal that #1005 could not update, since the file exists only on this branch, so the PR's merge-commit typecheck failed while the branch tip stayed green. Set both fields to null; the new noteTimestamps/noteChatState helpers only read note fields, so no other test changes are needed. svelte-check and the ProjectSection component suite pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Note rows in the project notes list and the branch card timeline only reflected when a note first finished. Rewrites by follow-up turns and replies that arrived in the note's chat were invisible from the lists. This PR makes both visible.
Revised notes are labelled and sorted by last update
noteTimestampshelper (isNoteRevised,noteLastWrittenAt,formatNoteTimestamp) typed on the sharedcreatedAt/updatedAt/completedAtcolumns so it serves bothNoteTimelineItemandProjectNote.updatedAthas moved pastcompletedAt, otherwise the existing bare "5m ago". Migration-backfilled rows (completedAt = updatedAt) read as unrevised.ORDER BYis unchanged since both lists re-sort client-side.Chat button shows read and unread replies
The hover-only "View session" icon now has three states, from the chat's latest reply (L), the note's
updatedAt(U) and the chat read time (R):MessageSquareTextin--text-muted, visible without hover.--ui-accentdot on the icon corner.Only completed rows get read/unread. A turn that rewrites the note moves U past every reply, so the button drops back to none on its own. Moving a note also bumps
updated_atand resets the button; that is intentionally left alone.Backend:
sessions.chat_read_at(ms, backend clock), backfilled to migration time so the upgrade doesn't light up every old note.session_chat_activityreturns the latest visible assistant reply andchat_read_atin one query; the visible-reply condition is a constant shared withcount_assistant_messages_after.mark_session_chat_readwrites server time only when a reply is newer than both the last read and the owning note'supdated_at, and publishesStoreChange::Notesfor the owning branch or project note when it does. Theupdated_atguard avoids a spurious write/publish (and full list rebuild) on the first "View session" click for post-migration sessions whosechat_read_atstarts NULL.NoteTimelineItemandProjectNotecarrylast_chat_message_atandchat_read_at. The three project-note*_with_statuslookups share one resolver helper. Chat-activity lookup errors fall back to no activity so a detail can't blank a list.mark_session_chat_readcommand with web dispatch.Frontend:
NoteModalmarks the chat read when the chat pane is shown and again when it is hidden or the dialog closes, covering replies that streamed in while open.TimelineRowhides and reveals each action on hover individually instead of fading the whole container, so a chat button with replies stays visible while Delete and the rest wait for hover. The unread dot is anchored to the icon centre so it stays on the corner at the 44px touch width.Tests
noteTimestamps.test.tsandnoteFreshness.test.tscover the label, sort key and chat-state decisions.🤖 Generated with Claude Code