Skip to content

feat(staged): surface note revisions and chat replies on note rows - #1005

Merged
matt2e merged 3 commits into
mainfrom
created-at-updated-at
Oct 31, 2026
Merged

matt2e merged 3 commits into
mainfrom
created-at-updated-at

Conversation

@matt2e

@matt2e matt2e commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

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

  • New noteTimestamps helper (isNoteRevised, noteLastWrittenAt, formatNoteTimestamp) typed on the shared createdAt/updatedAt/completedAt columns so it serves both NoteTimelineItem and ProjectNote.
  • Rows read "Updated 5m ago" once updatedAt has moved past completedAt, otherwise the existing bare "5m ago". Migration-backfilled rows (completedAt = updatedAt) read as unrevised.
  • Both lists sort on the same value, still ascending. Backend ORDER BY is 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):

  • none (no L, or L <= U): unchanged hover-only icon.
  • read (L > U, R >= L): MessageSquareText in --text-muted, visible without hover.
  • unread (L > U, R missing or R < L): same, plus a 6px --ui-accent dot 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_at and resets the button; that is intentionally left alone.

Backend:

  • Migration 0035 adds nullable sessions.chat_read_at (ms, backend clock), backfilled to migration time so the upgrade doesn't light up every old note.
  • session_chat_activity returns the latest visible assistant reply and chat_read_at in one query; the visible-reply condition is a constant shared with count_assistant_messages_after.
  • mark_session_chat_read writes server time only when a reply is newer than both the last read and the owning note's updated_at, and publishes StoreChange::Notes for the owning branch or project note when it does. The updated_at guard avoids a spurious write/publish (and full list rebuild) on the first "View session" click for post-migration sessions whose chat_read_at starts NULL.
  • NoteTimelineItem and ProjectNote carry last_chat_message_at and chat_read_at. The three project-note *_with_status lookups share one resolver helper. Chat-activity lookup errors fall back to no activity so a detail can't blank a list.
  • 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, covering replies that streamed in while open.
  • TimelineRow hides 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.ts and noteFreshness.test.ts cover the label, sort key and chat-state decisions.
  • Migration test for 0035 backfill; store change-feed tests for replies that predate their note (no write, no publish) and a later reply past the note (writes and publishes).
  • Pre-push CI (crates fmt/lint/test, differ-ci, staged-ci) passed.

🤖 Generated with Claude Code

matt2e and others added 3 commits October 30, 2026 11:38
…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>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T03:55:47.950104Z 7378fb8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@matt2e
matt2e merged commit bbfbba5 into main Oct 31, 2026
4 checks passed
@matt2e
matt2e deleted the created-at-updated-at branch October 31, 2026 04:02
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>
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