Skip to content

annotate-last: pick which message to annotate (fixes #800)#809

Open
GLips wants to merge 6 commits into
backnotprop:mainfrom
GLips:annotate-last-picker
Open

annotate-last: pick which message to annotate (fixes #800)#809
GLips wants to merge 6 commits into
backnotprop:mainfrom
GLips:annotate-last-picker

Conversation

@GLips
Copy link
Copy Markdown

@GLips GLips commented May 28, 2026

Fixes #800.

What this changes for users

Running `/plannotator-last` after `/rewind` no longer silently opens on the orphaned pre-rewind message. The annotation UI now surfaces recent messages so you can pick whichever message you like.

Two affordances, both only visible when there's more than one message to choose from:

  • A "Message N of M" button in the document's sticky-top action bar (alongside Copy / Global comment / Attachments). Stays visible while scrolling.
  • A "Messages" tab in the left sidebar with the full list — newest-first, preview + timestamp, default marked with ★. Mirrors the existing Files / Versions / Archive tab pattern.

Default selection (index 0) matches today's "last message" behavior, so anyone who doesn't interact with the picker sees no change.

CleanShot 2026-06-01 at 12 00 58@2x

Coverage

All six harnesses (Claude Code, Codex, Droid, Copilot, OpenCode, Pi) ship the message picker.

I've tested and confirmed with Claude Code, which should cover CC, Codex, and Droid since they exercise the same message paths. Direct testing with OpenCode, Pi, and Copilot CLI still need to happen.

Notes for reviewers

  • The wire payload (`recentMessages` field on `/api/plan`) is omitted entirely when there's nothing to pick, so unsupported harnesses degrade cleanly with zero conditional branches in the server.
  • Switching messages remounts the Viewer via a shared viewerContentKey (also feeds StickyHeaderLane's remount token). This is required because web-highlighter mutates the content DOM, which would otherwise crash React's reconciler (removeChild) on swap.
  • New `MessagesIcon` lives in `packages/ui/components/icons/` — the grouped-icon convention.
  • Added 6 unit tests covering `extractRecentRenderedMessages` (turn-boundary crossing, limit honoring, default-equals-legacy invariant, edge cases). Full suite still 1228/1228.

Test plan

  • Verify default selection matches pre-PR `/plannotator-last` behavior in a non-rewound session
  • Run `/rewind` then `/plannotator-last` — confirm the picker surfaces both the orphaned and current-branch messages
  • Confirm OpenCode / Pi / Copilot sessions still work (no picker, original message shown)

GLips added 3 commits May 27, 2026 22:17
When running /plannotator-last after /rewind, the newest transcript
entry is no longer the message the user intended to annotate, and there
was no affordance to pick a different one.

Adds a picker UI that surfaces the recent assistant messages so the
user can choose which one to annotate:

- A "Message N of M" button in the Viewer's sticky-top action bar
  (alongside Copy / Global comment / Attachments), so it stays
  accessible while scrolling.
- A "Messages" tab in the left sidebar with the full list
  (newest-first, preview + timestamp, default ★), mirroring the
  existing Files / Versions / Archive tab pattern.

Wired for Claude Code, Codex, and Droid (all share apps/hook/server).
OpenCode, Pi, and Copilot still get the original single-message
behavior — they don't emit recentMessages, so the picker affordances
hide cleanly.

Default selection (index 0) matches today's "last message" behavior,
so users who don't interact with the picker see no change.
The picker UI from backnotprop#800 was wired for Claude / Codex / Droid only. Pull
Copilot and OpenCode onto the same shape so users on those harnesses
also get the recent-messages picker when annotating the last assistant
message.

- Copilot: replace getLastCopilotMessage with getRecentCopilotMessages,
  walking events.jsonl newest-first up to 25 assistant.message events.
- OpenCode: rewrite the session walk to collect up to 25 messages
  (newest first) instead of bailing on the first hit; normalize the SDK
  time.created (ms epoch) to ISO to match the shared picker contract.
- Both pass recentMessages to startAnnotateServer only when length > 1,
  matching the existing Claude/Codex/Droid behavior.

Also trims a leftover narrating comment in MessagesBrowser and refreshes
the stale Copilot session-parser header.

Pi parity follows in the next commit (needs round-trip of the picker
selection through /api/feedback so its post-submit anchoring quotes the
right message).
Extends the picker UI (backnotprop#800) to Pi and fixes a Pi-specific anchoring bug
the picker would otherwise introduce.

Picker plumbing
- assistant-message: getRecentAssistantMessages walks the active branch
  newest-first, returning { messageId, text, timestamp? } in the same
  shape the other harnesses produce.
- Plumbed through plannotator-browser / plannotator-events so the Bun
  server's recentMessages option is populated when the branch has more
  than one assistant message.

Anchoring fix
- Pi quotes the targeted assistant message back to the agent because its
  UX is async — the conversation may have moved on by feedback time.
  With the picker, that target is no longer guaranteed to be the
  snapshot taken when the UI opened. The editor now sends the user's
  selectedMessageId with /api/feedback; Pi looks it up in the current
  branch via findAssistantMessageByEntryId and quotes that message
  instead. Falls back to the original snapshot if the entry is gone.
- The round-trip field is optional and only meaningful in annotate-last
  mode; other harnesses (and other modes) ignore it.

Timestamp safety
- Pi's SDK currently types SessionEntryBase.timestamp as string, but the
  picker contract everywhere else is ISO. Treat the value as unknown and
  normalize string/number(ms)/Date to ISO; drop anything else, rather
  than blind-casting and risking silent drift if the SDK changes.
@GLips
Copy link
Copy Markdown
Author

GLips commented May 28, 2026

Ok I gotta go through some more thorough testing myself tomorrow with Claude Code but this should be pretty close to the final form if you want to take a look @backnotprop

If not I'll explicitly mark it as ready for review once I've had a chance to check it out more deeply.

GLips added 3 commits June 1, 2026 11:32
Comments shouldn't rely on external references — issue numbers age out
of context, link rot is a thing, and a reader shouldn't need to open
GitHub to understand why a line exists. Strip the `(backnotprop#800)` and `(backnotprop#570)`
parentheticals from comments and doc strings across the picker and
review-gate code; the surrounding "why" content is preserved.
Switching the picked message remounted nothing, so React reconciled new
content against DOM that web-highlighter had mutated with <mark> nodes,
throwing removeChild. Drive the Viewer key (and StickyHeaderLane's
remount token) off a shared viewerContentKey so a message switch fully
remounts the Viewer and re-anchors the sticky-header observer.

Also cap MessagesBrowser row previews via previewText() and drop the
redundant 'block' class that was overriding line-clamp-2.
@GLips GLips marked this pull request as ready for review June 1, 2026 19:06
@GLips
Copy link
Copy Markdown
Author

GLips commented Jun 1, 2026

@backnotprop ready for review, think this is good to go :D

@backnotprop
Copy link
Copy Markdown
Owner

nice work will get through the review by later today

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.

annotate-last: picks wrong message after /rewind; consider a message-picker UI

2 participants