feat: list_mentions — a cross-document work queue for agents#9
Merged
Conversation
Comments already carry @mentions and list_comments has a `mentioning` filter, but only for the open document — an agent had no way to ask "where in my whole project am I needed?" without opening every doc. Adds GET /api/projects/:p/mentions?who=<name>&open=<bool>: scans every document in the project (the live room when one is open, else a throwaway doc rebuilt from the state sidecar — never opening or creating a room) and returns open threads that @mention the peer, each with the requesting comment and its current anchored text. A thread is "handled" once it is resolved or the peer has replied; handled threads drop out unless open=false. RoomRegistry gains peek() for the side-effect-free read. The MCP wraps this as the list_mentions tool (edit-only-safe, read-only, no open document required), so an agent's loop is list_mentions → open_document → edit → reply_comment → resolve_comment, and the queue empties as it works. Skill documents the workflow. Tests: two mentions across two docs surface in one queue without opening anything; handling one (reply + resolve) drops it from the default view; includeHandled surfaces it flagged resolved + answered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au
Adds snapshots: named checkpoints of a document you can roll back to.
The room Y.Doc is already gc:false "so authorship (and later, snapshot
history) survives" — this builds on that anticipated seam.
Server (human-only REST, consistent with the CRUD/edit-only split):
GET /api/projects/:p/docs/*d/snapshots list versions
POST /api/projects/:p/docs/*d/snapshots {label} capture current state
POST /api/projects/:p/docs/*d/snapshots/:id/restore roll back to a version
A snapshot stores the full Y.encodeStateAsUpdate (base64) plus display
metadata in a <path>.snapshots.json sidecar, which travels on rename/move
and is removed on delete like .yjs/.log. The list endpoint strips the
heavy state blob.
Restore converges the live text forward via the existing reconcileText
middle-splice, authored to the restorer — so it lands in blame and
history and is itself reversible. It deliberately does NOT re-apply the
old CRDT state (that would fork live peers); other collaborators simply
see the text change. RoomRegistry.peek() (added for #1) is unrelated here;
capture/restore use registry.open + the new Room.snapshotState /
Room.restoreContent.
Web UI: a "versions" button opens a panel to name+save the current state
and restore any prior version (confirm dialog, API errors surfaced).
Tests: server capture/list/restore, blame attribution of a restore,
sidecar travel on move + removal on delete, 404/400 guards; a Playwright
flow that saves a checkpoint, diverges, and restores it in the real UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au
Awareness already carried status: 'idle' | 'composing' (set across
begin_edit / commit_edit / abort_edit) but nothing surfaced it. This
enriches presence with the section under edit and renders it.
MCP: setStatus now also broadcasts the nearest markdown heading above the
edit point ("section"). begin_edit announces composing + section; commit
and abort return to idle and clear it. Computed locally from the peer's
own replica, no new contract.
Web UI: a subtle banner under the header shows other peers that are
composing — "plosson/claude is writing in §Introduction" — and hides when
they go idle (own writing is not narrated back to you).
Skill: agents commit/abort promptly rather than holding a session open,
and yield to a human actively typing the same region (courtesy, not a
lock — there are none).
Tests: an MCP watcher peer observes composing + section 'Details' reach it
and idle clear it; a Playwright check that the human sees the banner while
an agent holds an open session and it clears on abort.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au
…ar box search_text only ever searched the open document; nothing let a human or an agent ask "which document mentions X?" across a project. As projects grow past a couple of docs (the model added in #8), that gap bites. Server: GET /api/projects/:p/search?q=&limit= — case-insensitive substring across every document, returning {doc, line, column, snippet}. Reads the live room for open documents (freshest) and the markdown file for the rest (the canonical source of truth; there is no watcher, so an unopened doc has no unsaved state to miss). One hit per line, capped. MCP: search_project(query, maxResults?) wraps it — an agent locates the right document before open_document, staying within its project. Fits the edit-only split: it finds documents, it can't create them. Web UI: a search box in the sidebar; results (doc:line + snippet) replace the document list while searching and clicking one opens that document. Stale in-flight queries are ignored; switching projects clears it. Tests: MCP search across two docs with line numbers, case-insensitivity, and empty results; REST disk-read with column/snippet, project-scoping (no cross-project leak), 400/404 guards; a Playwright search-and-open flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au
Agent edits land directly in the CRDT, which not everyone wants for prose. This adds reviewable suggestions: an agent proposes a change, a human accepts or rejects it. New shared contract src/shared/suggestions.ts (sibling to comments): a `suggestions` Y.Map of insert/delete/replace proposals, each anchored with Yjs relative positions so it follows concurrent edits and orphans if its target text is deleted. Accepting applies the change to `content`; rejecting leaves it untouched. Both sync/persist like comments. MCP (agents propose, they don't decide): suggest_insert / suggest_replace / suggest_delete create proposals without mutating the text; list_suggestions shows status + who resolved each (the feedback loop); withdraw_suggestion retracts your own. There is deliberately no accept/reject MCP tool — that is the human's call in the web UI, mirroring the CRUD/edit-only split. Web UI: proposals render inline (ghost text for inserts, struck-through / highlighted ranges for deletes and replaces) and in a "Suggested edits" panel with Accept / Reject / Withdraw per item. Design note: the approved sketch imagined a per-session "suggest mode" that silently rerouted the normal edit tools. I implemented explicit suggest_* tools instead — same product outcome (propose vs. commit), but the agent's intent is unambiguous per call and existing edit tools keep one meaning. Attribution is a v1 tradeoff: accept applies via the accepting peer, so blame credits whoever accepted (the human taking responsibility); the suggestion retains its author for provenance. CRDT blame is by inserting clientID, and the acceptor is the one inserting. Tests: 11 shared-contract cases (anchoring under concurrent edits, orphaning, accept/reject/withdraw, guards, ordering); an MCP flow (agent proposes → human accepts → agent observes the accepted status; pending view empties); a Playwright accept-in-browser flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au
restoreContent registered the restorer under the room doc's own clientID — the same ID the hydrate-time "disk" reconcile uses — so a restore retroactively flipped every character that ID ever inserted to the restorer (and successive restores by different users overwrote each other). Blame keys authors by inserting clientID, so the splice now runs in a scratch doc with a fresh clientID and lands in the room as a normal peer update: the restored characters carry their own identity permanently. Regression test: disk-hydrated lines stay blamed to "disk" across a restore that only re-adds the rewritten line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gnrgu8jdLx12jkWY1B8vb3
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.
Comments already carry @mentions and list_comments has a
mentioningfilter, but only for the open document — an agent had no way to ask
"where in my whole project am I needed?" without opening every doc.
Adds GET /api/projects/:p/mentions?who=&open=: scans every
document in the project (the live room when one is open, else a throwaway
doc rebuilt from the state sidecar — never opening or creating a room)
and returns open threads that @mention the peer, each with the requesting
comment and its current anchored text. A thread is "handled" once it is
resolved or the peer has replied; handled threads drop out unless
open=false. RoomRegistry gains peek() for the side-effect-free read.
The MCP wraps this as the list_mentions tool (edit-only-safe, read-only,
no open document required), so an agent's loop is list_mentions →
open_document → edit → reply_comment → resolve_comment, and the queue
empties as it works. Skill documents the workflow.
Tests: two mentions across two docs surface in one queue without opening
anything; handling one (reply + resolve) drops it from the default view;
includeHandled surfaces it flagged resolved + answered.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_017Q1wmZU3s6DvjypiSHm2au