Antigravity-style inline markdown commenting as an installable Claude plugin. - MCP Apps webview (markdown-it renderer + comment UI) - three lanes: Answer Now (sendMessage), Add to batch, Submit All (persist+send) - edit/review mode toggle; sidecar comment persistence - self-contained prebuilt server/dist (zero runtime node_modules) - self-marketplace manifest for /plugin install - 11/11 stdio smoke tests passing Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
185 lines
10 KiB
Markdown
185 lines
10 KiB
Markdown
# markdown-review — Design Doc
|
|
|
|
> Antigravity-style **comment-on-markdown → send-to-agent** as a Claude plugin (MCP Apps / MCP-UI).
|
|
> Status: SPEC (Phase 0 complete — capability verified, API grounded). Author: JARVIS / CDT. 2026-06-09.
|
|
|
|
---
|
|
|
|
## 0. Why this is buildable (verification result)
|
|
|
|
The mechanism is **already proven on this machine**: Anthropic's `pdf-viewer` plugin is installed and
|
|
its `pdf` MCP server is live. It renders an interactive viewer with zero Electron/browser — it ships a
|
|
single-file HTML app as an **MCP resource** (`ui://…`, mimetype `text/html;profile=mcp-app`) that
|
|
**Claude Desktop renders in its own sandboxed webview**. We replicate that pattern with a markdown
|
|
renderer + comment UI.
|
|
|
|
**Grounded stack** (same as the working pdf server): Node 24, `@modelcontextprotocol/ext-apps@^1.7`,
|
|
`@modelcontextprotocol/sdk@^1.29`, `zod@^4`, Vite + `vite-plugin-singlefile` to bundle `app.html`.
|
|
|
|
Key ext-apps primitives we use (real, verified):
|
|
- `registerAppTool(server, name, { _meta: { ui: { resourceUri } } }, cb)` — model-facing tool that opens the view.
|
|
- `registerAppResource(server, title, "ui://…", {}, readCb)` — serves the HTML, with CSP/permission `_meta.ui`.
|
|
- Tool **visibility** `_meta.ui.visibility: ["app"]` — hide plumbing tools from the model (`isToolVisibilityAppOnly`).
|
|
- App side (`@modelcontextprotocol/ext-apps/app` + `app-bridge`): the webview can **call server tools** and
|
|
**`updateModelContext`** — push text directly into the model's turn. This is what powers "Answer Now".
|
|
|
|
> **Host note:** the live comment panel renders in **Claude Desktop** (the MCP-Apps host). Claude Code
|
|
> builds/installs the plugin; the interactive surface is Desktop's. A headless Claude Code fallback
|
|
> (render to a temp HTML + Read) is a secondary, non-interactive mode.
|
|
|
|
---
|
|
|
|
## 1. The interaction model — three lanes
|
|
|
|
Antigravity has two effective behaviors (batch artifact comments; select-text→chat). Rav's refinement
|
|
adds a third, and makes the batch-result behavior a per-session toggle. The result is **three lanes**:
|
|
|
|
### Lane A — Reading-phase batch (the default, flow-preserving)
|
|
You read top-to-bottom and drop comments as you go. **No AI is invoked.** Comments accumulate silently.
|
|
A single **Submit All** flushes the whole set into *one* agent turn. This is the steering loop.
|
|
|
|
### Lane B — "Answer Now" (single, ephemeral, mid-read) ⟵ Rav's key addition
|
|
A **separate** button on an individual comment. Fires **only that one item + that one comment** to the
|
|
agent **immediately**, via `updateModelContext`. It:
|
|
- does **not** enter the batch store,
|
|
- does **not** disturb the other pending batch comments,
|
|
- is for a quick correction or question *without breaking the reading phase*.
|
|
|
|
Mechanically distinct from Lane A: different button, different storage (memory/none), different trigger.
|
|
|
|
### Lane C — what Submit All *does* (per-session mode toggle)
|
|
Orthogonal to A/B. Controls the *effect* of a batch flush:
|
|
- **`edit` mode** — comments become edit instructions; agent modifies the anchored ranges in the real
|
|
`.md`, re-renders, verifies. (Antigravity's steering loop.)
|
|
- **`review` mode** — comments become a punch-list/review thread; the file is left untouched until you
|
|
approve items one by one.
|
|
- Switchable per session (a toolbar toggle + a tool arg). Default `edit`.
|
|
|
|
| Lane | Trigger | Scope | Storage | Effect |
|
|
|------|---------|-------|---------|--------|
|
|
| A Batch | Submit All | all pending | sidecar (default) | edit **or** review (Lane C) |
|
|
| B Answer Now | per-comment button | that one | ephemeral (memory) | immediate single ask |
|
|
| C (mode) | toggle | — | — | governs A's effect |
|
|
|
|
---
|
|
|
|
## 2. Anchor model
|
|
|
|
Because we operate on **real files** (unlike Antigravity's opaque rendered-span anchor), comments anchor
|
|
richer and survive edits better:
|
|
|
|
```jsonc
|
|
"anchor": {
|
|
"line_start": 12, "line_end": 14, // 1-based, inclusive
|
|
"char_start": 340, "char_end": 410, // byte/char offsets into the raw file
|
|
"block_id": "h2-architecture-3", // stable-ish slug of the enclosing md block
|
|
"quote": "We should use Flask here", // verbatim selected text (re-anchor fallback)
|
|
"context_before": "…", "context_after": "…" // ~120 chars each, for fuzzy re-anchoring
|
|
}
|
|
```
|
|
Re-anchoring on a changed file: try `char_start` → verify `quote` matches; else search `quote` near
|
|
`block_id`; else fuzzy-match `quote` in `context`. Stale anchors are flagged, never silently dropped.
|
|
|
|
---
|
|
|
|
## 3. Storage — "all of the above, intelligently"
|
|
|
|
Three backends, **selected by intent**, with explicit override. Combos are first-class.
|
|
|
|
| Backend | Lives in | Lifetime | Default use (scenario) |
|
|
|---------|----------|----------|------------------------|
|
|
| **memory** | server process, per `viewUUID` | session | **Lane B Answer-Now**; throwaway brainstorm; "don't clutter anything" |
|
|
| **sidecar** | `.claude/md-comments/<sha1(path)>.json` | durable, resumable | **Lane A batch review threads**; multi-file reviews; keeps `.md` clean (Antigravity-like) |
|
|
| **inline** | `<!-- mdrev:{id} … -->` markers in the `.md` | travels with file | hand-off to a colleague, **git-committed review**, portable, want comment visible in raw source |
|
|
|
|
### Intelligent routing (defaults; all overridable per call / toolbar)
|
|
- Answer-Now → **memory** (never persisted).
|
|
- Batch comment authored during reading → **sidecar** (source of truth), auto-resumes next open.
|
|
- Explicit **"Export for sharing"** → materialize sidecar threads as **inline** markers.
|
|
- On **open**, if the file already contains inline `mdrev:` markers → **import** them into the sidecar
|
|
working set (combo: inline-authored → sidecar-managed).
|
|
- **Promote**: a memory/Answer-Now comment you want to keep → "pin" → sidecar.
|
|
|
|
### Combos we explicitly support
|
|
1. **sidecar SoT + inline export** — manage privately, publish a shareable copy.
|
|
2. **inline import → sidecar** — colleague sent a `.md` with `mdrev:` comments; we ingest + manage them.
|
|
3. **memory → sidecar promote** — a quick ask turned out worth keeping.
|
|
4. **sidecar + inline mirror (synced)** — opt-in two-way: edits in either reflect (sidecar is authority on conflict).
|
|
|
|
Storage backend is a field on every comment (`store: "memory"|"sidecar"|"inline"`) so a single view can
|
|
mix all three. `resolve`/`delete` operate uniformly across backends.
|
|
|
|
---
|
|
|
|
## 4. MCP surface
|
|
|
|
### Model-facing (the only tools the model sees)
|
|
- `open_markdown(path, { mode?: "edit"|"review" })` → `{ viewUUID, fileMeta, importedComments }`.
|
|
Opens the view (returns `_meta.ui.resourceUri`), imports inline markers + loads sidecar.
|
|
- `interact(viewUUID, commands[])` → batched ops the model issues:
|
|
`scroll_to(anchor) | highlight(anchor) | get_comments(filter?) | get_state | set_mode(edit|review) |
|
|
resolve(id) | export_inline | get_pages(rendered+source)`.
|
|
- (`get_comments` is how the model receives a **Submit All** payload after the user clicks it.)
|
|
|
|
### App-only (hidden, `_meta.ui.visibility:["app"]`) — the plumbing
|
|
- `poll_md_commands(viewUUID)` — long-poll queue draining `interact` commands to the webview.
|
|
- `submit_batch(viewUUID, comments[], mode)` — Lane A: user clicked Submit All; resolves the model's
|
|
pending `get_comments`/blocks the `interact` promise (pdf-viewer pattern).
|
|
- `persist_comment(viewUUID, comment)` — webview writes a single batch comment to its backend as authored.
|
|
- `read_md_bytes(viewUUID, range?)` — webview pulls source to render.
|
|
- `save_md(viewUUID, bytes)` — when edit mode writes back inline markers.
|
|
|
|
### App→model direct (no server tool) — Lane B
|
|
- "Answer Now" calls the bridge `updateModelContext({ item, comment })` → injected into the live turn.
|
|
No queue, no store. (Falls back to a dedicated `answer_now` app-tool if a host lacks updateModelContext.)
|
|
|
|
---
|
|
|
|
## 5. Round-trip (mirrors the verified pdf-viewer mechanism)
|
|
|
|
```
|
|
model → interact(cmds) server enqueues to commandQueues[viewUUID]
|
|
webview → poll_md_commands(...) long-poll; executes in the renderer (scroll/highlight/extract)
|
|
for data cmds: interact() BLOCKS on Promise(requestId, timeout)
|
|
user clicks "Submit All" webview → submit_batch(...) → resolves the pending model call
|
|
user clicks "Answer Now" webview → updateModelContext(...) → straight into the turn
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Plugin packaging
|
|
|
|
```
|
|
markdown-review/
|
|
├── .claude-plugin/plugin.json # name, version, author, components
|
|
├── .mcp.json # { mcpServers: { "md": { command: node, args:[server/index.js,--stdio] } } }
|
|
├── commands/ # /open.md /comment.md /review.md
|
|
├── skills/review-markdown/SKILL.md # orchestration brain (open-once, batched interact, mode handling)
|
|
├── server/ # Node MCP server (queue + blocking-promise plumbing)
|
|
│ ├── index.js · server.js · storage.js · anchor.js
|
|
└── app/ # Vite single-file → bundled app.html (md renderer + comment UI)
|
|
└── src/ → built to ../server/app.html
|
|
```
|
|
|
|
Renderer: `markdown-it` (CommonMark + tables/footnotes) in the sandboxed webview. Comment UI = margin
|
|
threads anchored to selection; toolbar = mode toggle, Submit All, Export-inline, store selector;
|
|
per-comment = Answer-Now + Submit + store-pick + resolve.
|
|
|
|
---
|
|
|
|
## 7. Build phases (slow-is-fast)
|
|
|
|
0. ✅ Verify host support + ground the API. *(done)*
|
|
1. **Spike the round-trip** — minimal `md` server: open a file, render in webview, prove
|
|
`interact`/`poll`/`submit` + `updateModelContext` fire end-to-end. De-risks everything.
|
|
2. **Comment UI** — selection → anchored thread → Submit All payload + Answer-Now.
|
|
3. **Storage layer** — sidecar + inline + memory + the intelligent router + combos.
|
|
4. **Agent wiring** — SKILL.md + commands turn submitted comments into edits (edit mode) / list (review mode).
|
|
5. **Anchor robustness** — re-anchoring across edits; stale flagging.
|
|
6. **Package + install** — plugin.json + local marketplace entry; smoke-test in Claude Desktop.
|
|
|
|
## 8. Open questions / risks
|
|
- `updateModelContext` host support in this Desktop build — confirm in Phase 1 spike; fallback = `answer_now` app-tool.
|
|
- markdown-it bundle size in single-file (acceptable; pdf-viewer ships 4.4MB).
|
|
- Two-way sidecar↔inline sync (combo #4) deferred past v1 — conflict policy needs care.
|
|
```
|