Skip to content

feat: list_mentions — a cross-document work queue for agents#9

Merged
plosson merged 6 commits into
mainfrom
claude/project-overview-19bq1q
Jul 15, 2026
Merged

feat: list_mentions — a cross-document work queue for agents#9
plosson merged 6 commits into
mainfrom
claude/project-overview-19bq1q

Conversation

@plosson

@plosson plosson commented Jul 14, 2026

Copy link
Copy Markdown
Owner

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=&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

claude and others added 6 commits July 14, 2026 04:16
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
@plosson
plosson merged commit b13e997 into main Jul 15, 2026
2 checks passed
@plosson
plosson deleted the claude/project-overview-19bq1q branch July 15, 2026 14:05
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.

2 participants