openwriter 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -108,6 +108,24 @@ export function forceSnapshot(docId, filePath) {
108
108
  writeFileSync(join(docDir(docId), `${now}.md`), markdown, 'utf-8');
109
109
  lastSnapshot.set(docId, { time: now, hash });
110
110
  }
111
+ /**
112
+ * Write a snapshot from an arbitrary markdown string (rather than copying
113
+ * the current file). Used by `restore_version` so the safety checkpoint
114
+ * can represent the canonical-only state (pending reverted) rather than the
115
+ * flattened pending+canonical body that lives on disk.
116
+ *
117
+ * Returns the timestamp the snapshot was written at.
118
+ */
119
+ export function writeSnapshotMarkdown(docId, markdown) {
120
+ if (!docId)
121
+ return 0;
122
+ const hash = contentHash(markdown);
123
+ const now = Date.now();
124
+ ensureDocDir(docId);
125
+ writeFileSync(join(docDir(docId), `${now}.md`), markdown, 'utf-8');
126
+ lastSnapshot.set(docId, { time: now, hash });
127
+ return now;
128
+ }
111
129
  // ============================================================================
112
130
  // LIST / GET
113
131
  // ============================================================================
@@ -437,6 +437,21 @@ export function getWorkspaceAssignedFiles() {
437
437
  }
438
438
  return assigned;
439
439
  }
440
+ /** Return every workspace manifest filename that contains this doc. A doc may
441
+ * appear in multiple workspaces; callers that want one usually pick the first. */
442
+ export function findWorkspacesContainingDoc(file) {
443
+ const workspaces = listWorkspaces();
444
+ const result = [];
445
+ for (const info of workspaces) {
446
+ try {
447
+ const ws = readWorkspace(info.filename);
448
+ if (collectAllFiles(ws.root).includes(file))
449
+ result.push(info);
450
+ }
451
+ catch { /* skip corrupt manifests */ }
452
+ }
453
+ return result;
454
+ }
440
455
  /**
441
456
  * Walk every workspace and return true if `file` is inside one where auto-accept
442
457
  * is on at the workspace level or on any ancestor container. Returns false when
package/dist/server/ws.js CHANGED
@@ -2,21 +2,42 @@
2
2
  * WebSocket handler: pushes NodeChanges to browser, receives doc updates + signals.
3
3
  */
4
4
  import { WebSocketServer, WebSocket } from 'ws';
5
- import { updateDocument, getDocument, getTitle, getFilePath, getDocId, getMetadata, setMetadata, save, onChanges, isAgentLocked, setAgentLock, getDocVersion, isVersionCurrent, getPendingDocInfo, updatePendingCacheForActiveDoc, stripPendingAttrs, saveDocToFile, stripPendingAttrsFromFile, } from './state.js';
5
+ import { updateDocument, syncBrowserDocUpdate, getDocument, getTitle, getFilePath, getDocId, getMetadata, setMetadata, save, debouncedSave, cancelDebouncedSave, onChanges, onIdRewrites, isAgentLocked, setAgentLockActive, getDocVersion, isVersionCurrent, getPendingDocInfo, updatePendingCacheForActiveDoc, stripPendingAttrs, saveDocToFile, stripPendingAttrsFromFile, onExternalWriteConflict, onDocumentReloaded, isAgentStub, unmarkAgentStub, } from './state.js';
6
6
  import { switchDocument, createDocument, deleteDocument, getActiveFilename, promoteTempFile } from './documents.js';
7
7
  import { removeDocFromAllWorkspaces } from './workspaces.js';
8
+ import { canonicalizeIdentifier } from './helpers.js';
9
+ import { nodeTextPreview, diagLog } from './pending-overlay.js';
10
+ import { generateRequestId, withRequestId } from './logger.js';
11
+ /** Walk a doc and return a per-pending-node summary for diagnostic logging.
12
+ * Produces lines like "nodeId/status text=\"...\" orig=\"...\"" — empty
13
+ * if the doc has no pending nodes. adr: adr/pending-overlay-model.md */
14
+ function pendingSummary(doc) {
15
+ if (!doc?.content)
16
+ return '<none>';
17
+ const lines = [];
18
+ function walk(nodes) {
19
+ if (!Array.isArray(nodes))
20
+ return;
21
+ for (const node of nodes) {
22
+ const status = node?.attrs?.pendingStatus;
23
+ if (status && node?.attrs?.id) {
24
+ const text = nodeTextPreview(node);
25
+ const orig = node.attrs.pendingOriginalContent ? nodeTextPreview(node.attrs.pendingOriginalContent) : '<none>';
26
+ const identity = (status === 'rewrite' && text === orig) ? ' IDENTITY!' : '';
27
+ lines.push(`${node.attrs.id}/${status} text="${text}" orig="${orig}"${identity}`);
28
+ }
29
+ if (node.content)
30
+ walk(node.content);
31
+ }
32
+ }
33
+ walk(doc.content);
34
+ return lines.length === 0 ? '<none>' : lines.join(' | ');
35
+ }
8
36
  const clients = new Set();
9
37
  let currentAgentConnected = false;
10
- // Debounced auto-save: persist to disk 2s after last doc-update
11
- let saveTimer = null;
12
- function debouncedSave() {
13
- if (saveTimer)
14
- clearTimeout(saveTimer);
15
- saveTimer = setTimeout(() => {
16
- save();
17
- console.log('[WS] Auto-saved to disk');
18
- }, 2000);
19
- }
38
+ // Debounced auto-save lives in state.ts now one timer for the whole process.
39
+ // Both browser doc-update (here) and MCP write paths (state.ts) call into the
40
+ // same timer so a single TTL governs all save activity.
20
41
  // Debounced sidebar refresh: notify clients after title changes settle
21
42
  let docsChangedTimer = null;
22
43
  function debouncedBroadcastDocumentsChanged() {
@@ -64,7 +85,7 @@ export function setupWebSocket(server) {
64
85
  // Re-set agent lock so the 3s window starts NOW, not from the original insert.
65
86
  // Tweet thread resyncs recreate all editors which fire onUpdate → stale doc-updates.
66
87
  // Without this reset, the lock expires before the browser finishes recreating editors.
67
- setAgentLock();
88
+ setAgentLockActive();
68
89
  const filePath = getFilePath();
69
90
  const filename = filePath ? filePath.split(/[/\\]/).pop() || '' : '';
70
91
  const msg = JSON.stringify({
@@ -90,6 +111,76 @@ export function setupWebSocket(server) {
90
111
  // Notify browser of updated pending docs list (debounced)
91
112
  broadcastPendingDocsChanged();
92
113
  });
114
+ // Push matcher-driven ID rewrites to clients so their in-memory TipTap state
115
+ // tracks the server's authoritative ID assignment. Without this, the browser
116
+ // can hold stale IDs after a save-time matcher reassignment, subsequent
117
+ // server→browser messages targeting the new IDs silently fail to resolve at
118
+ // the bridge anchor lookup, and the browser's debounced autosave eventually
119
+ // overwrites fresh server state with the stale view (the v0.14.0 bug pattern).
120
+ // adr: adr/node-identity-matcher.md
121
+ onIdRewrites((rewrites) => {
122
+ if (rewrites.length === 0)
123
+ return;
124
+ const msg = JSON.stringify({ type: 'id-rewrites', rewrites });
125
+ for (const ws of clients) {
126
+ if (ws.readyState === WebSocket.OPEN)
127
+ ws.send(msg);
128
+ }
129
+ console.log(`[WS] Broadcast id-rewrites (${rewrites.length} block(s))`);
130
+ });
131
+ // Push-based: the fs.watch on the active doc fires this listener when an
132
+ // external writer (Edit tool, VSCode, a script) modifies the file. The
133
+ // server has already reloaded its in-memory state from disk and bumped
134
+ // docVersion — we just need to swap the browser's TipTap view so it
135
+ // matches and surface a toast. Stale autosaves from before the external
136
+ // write are rejected by the existing version check in the doc-update
137
+ // handler.
138
+ //
139
+ // Version sync: we include the freshly-bumped docVersion in the message.
140
+ // The browser adopts it as its new baseline so subsequent autosaves (from
141
+ // the user typing on top of the reloaded content) pass the isVersionCurrent
142
+ // check. Without this, browser sits at version 0 while the server is at
143
+ // N+1, and every browser edit gets BLOCKED — typing silently fails to save.
144
+ // adr: adr/active-doc-watcher.md
145
+ onDocumentReloaded((event) => {
146
+ const msg = JSON.stringify({
147
+ type: 'document-reloaded',
148
+ filename: event.filename,
149
+ document: event.document,
150
+ title: event.title,
151
+ docId: event.docId,
152
+ metadata: event.metadata,
153
+ version: getDocVersion(),
154
+ orphanCount: event.orphans.length,
155
+ staleBaselineCount: event.staleBaseline.length,
156
+ });
157
+ for (const ws of clients) {
158
+ if (ws.readyState === WebSocket.OPEN)
159
+ ws.send(msg);
160
+ }
161
+ // Pending count may have shifted (orphan rewrites convert to inserts).
162
+ broadcastPendingDocsChanged();
163
+ });
164
+ // Legacy: surface external-write conflicts when writeToDisk's mtime
165
+ // guard fires. With the active-doc watcher in place, the watcher should
166
+ // reload before any save races, but the guard remains as a backstop —
167
+ // if it fires, we still want to tell the user.
168
+ // adr: adr/external-write-guard.md
169
+ onExternalWriteConflict((conflict) => {
170
+ const filename = conflict.filePath.split(/[/\\]/).pop() || conflict.filePath;
171
+ const msg = JSON.stringify({
172
+ type: 'external-write-conflict',
173
+ filePath: conflict.filePath,
174
+ filename,
175
+ diskMtime: conflict.diskMtime,
176
+ loadedMtime: conflict.loadedMtime,
177
+ });
178
+ for (const ws of clients) {
179
+ if (ws.readyState === WebSocket.OPEN)
180
+ ws.send(msg);
181
+ }
182
+ console.warn(`[WS] Broadcast external-write-conflict for ${filename}`);
183
+ });
93
184
  wss.on('connection', (ws) => {
94
185
  clients.add(ws);
95
186
  console.log(`[WS] Client connected (total: ${clients.size})`);
@@ -103,9 +194,12 @@ export function setupWebSocket(server) {
103
194
  // (prevents stale browser tabs from displaying old content)
104
195
  const filePath = getFilePath();
105
196
  const filename = filePath ? filePath.split(/[/\\]/).pop() || '' : '';
197
+ const docOnConnect = getDocument();
198
+ const pendingOnConnect = pendingSummary(docOnConnect);
199
+ diagLog(`[WS] document-switched SEND on-connect docId=${getDocId()} v=${getDocVersion()} pending=[${pendingOnConnect}]`);
106
200
  ws.send(JSON.stringify({
107
201
  type: 'document-switched',
108
- document: getDocument(),
202
+ document: docOnConnect,
109
203
  title: getTitle(),
110
204
  filename,
111
205
  docId: getDocId(),
@@ -124,22 +218,57 @@ export function setupWebSocket(server) {
124
218
  ws.on('message', async (data) => {
125
219
  try {
126
220
  const msg = JSON.parse(data.toString());
221
+ // Every WS message gets a request ID — every downstream log emitted
222
+ // while handling this message inherits it. Trace one request through
223
+ // the whole system with: jq 'select(.requestId=="ws-xxxxxx")'.
224
+ // adr: adr/logging-system.md
225
+ const reqId = generateRequestId(`ws-${msg.type || 'unknown'}`);
226
+ return await withRequestId(reqId, async () => { return await handleMessage(msg); });
227
+ }
228
+ catch (err) {
229
+ console.error('[WS] Message error:', err.message);
230
+ }
231
+ });
232
+ /** Inner handler — body extracted so it runs inside the withRequestId
233
+ * scope above. Returns void. */
234
+ async function handleMessage(msg) {
235
+ try {
127
236
  if (msg.type === 'doc-update' && msg.document) {
128
237
  const docContent = msg.document?.content || [];
129
238
  const nodeCount = docContent.length;
130
239
  const currentNodeCount = getDocument()?.content?.length || 0;
131
240
  const browserVersion = typeof msg.version === 'number' ? msg.version : -1;
132
241
  const serverVersion = getDocVersion();
133
- if (isAgentLocked()) {
134
- console.log(`[WS] doc-update BLOCKED by agent lock (browser: ${nodeCount} nodes, server: ${currentNodeCount} nodes)`);
242
+ // Canonicalize the browser-sent filename so comparison against
243
+ // getActiveFilename() doesn't trip on separator/case differences
244
+ // for the same file. Without this, a browser that cached the
245
+ // pre-canonicalization spelling sends doc-updates that look like
246
+ // they're for a different doc, triggering saveDocToFile() to
247
+ // write to the old non-canonical path — re-creating the
248
+ // duplicate-document bug we just fixed.
249
+ // adr: adr/path-canonicalization.md
250
+ const browserFilename = msg.filename ? canonicalizeIdentifier(msg.filename) : msg.filename;
251
+ if (isAgentLocked(browserFilename || getActiveFilename())) {
252
+ diagLog(`[WS] doc-update BLOCKED by agent lock (browser: ${nodeCount} nodes, server: ${currentNodeCount} nodes) filename=${browserFilename || '<active>'} browserPending=[${pendingSummary(msg.document)}]`);
135
253
  }
136
254
  else if (browserVersion >= 0 && !isVersionCurrent(browserVersion)) {
137
- console.log(`[WS] doc-update BLOCKED by stale version (browser: v${browserVersion}, server: v${serverVersion})`);
255
+ // Stale-version doc-update: instead of rejecting (which silently
256
+ // discarded the user's typing), MERGE — keep browser's view of
257
+ // canonical, union the overlays with server-recent additions
258
+ // preserved. adr: adr/pending-overlay-model.md
259
+ if (msg.document.content) {
260
+ msg.document.content = msg.document.content.filter((n) => n.type !== 'imageLoading');
261
+ }
262
+ const result = syncBrowserDocUpdate(msg.document, browserVersion);
263
+ diagLog(`[WS] doc-update SYNC-MERGED stale v${browserVersion}→v${serverVersion} preservedServerEntries=${result.preservedServerEntries}`);
264
+ updatePendingCacheForActiveDoc();
265
+ debouncedSave();
138
266
  }
139
- else if (msg.filename && msg.filename !== getActiveFilename()) {
267
+ else if (browserFilename && browserFilename !== getActiveFilename()) {
140
268
  // Browser sent a doc-update for a different document (race: server switched away).
141
269
  // Save directly to that file on disk instead of corrupting the active doc.
142
- saveDocToFile(msg.filename, msg.document);
270
+ diagLog(`[WS] doc-update ROUTED-TO-NON-ACTIVE filename=${browserFilename} browserPending=[${pendingSummary(msg.document)}]`);
271
+ saveDocToFile(browserFilename, msg.document);
143
272
  }
144
273
  else {
145
274
  // Strip ephemeral imageLoading nodes — they're transient placeholders that should
@@ -148,7 +277,16 @@ export function setupWebSocket(server) {
148
277
  msg.document.content = msg.document.content.filter((n) => n.type !== 'imageLoading');
149
278
  }
150
279
  const cleanedCount = msg.document.content?.length || 0;
151
- console.log(`[WS] doc-update ACCEPTED (browser: ${nodeCount} nodes, cleaned: ${cleanedCount}, server: ${currentNodeCount} nodes)`);
280
+ // Capture server's BEFORE state so we can diff against the incoming.
281
+ // adr: adr/pending-overlay-model.md
282
+ const serverBefore = pendingSummary(getDocument());
283
+ const browserAfter = pendingSummary(msg.document);
284
+ if (serverBefore !== browserAfter) {
285
+ diagLog(`[WS] doc-update PENDING-DIFF v${browserVersion}→v${serverVersion}`);
286
+ diagLog(` BEFORE(server): ${serverBefore}`);
287
+ diagLog(` AFTER (browser): ${browserAfter}`);
288
+ }
289
+ diagLog(`[WS] doc-update ACCEPTED (browser: ${nodeCount} nodes, cleaned: ${cleanedCount}, server: ${currentNodeCount} nodes)`);
152
290
  updateDocument(msg.document);
153
291
  updatePendingCacheForActiveDoc(); // Keep cache in sync after browser edits/reject-all
154
292
  debouncedSave();
@@ -256,17 +394,22 @@ export function setupWebSocket(server) {
256
394
  const action = msg.action; // 'accept' or 'reject'
257
395
  const resolvedFilename = msg.filename;
258
396
  const isActiveDoc = resolvedFilename === getActiveFilename();
259
- // Get metadata from the correct source (active state or disk file)
260
- const metadata = isActiveDoc ? getMetadata() : null;
261
- if (action === 'reject' && metadata?.agentCreated) {
262
- // Agent-created doc with all content rejected → delete the file
263
- // Cancel debounced save (doc-update may have queued one for the now-empty doc)
264
- if (saveTimer) {
265
- clearTimeout(saveTimer);
266
- saveTimer = null;
267
- }
397
+ // Stub-cleanup: when the user rejects all pending decorations on a
398
+ // doc that's still a fresh agent stub, delete the file. The stub
399
+ // had no real content; the user said no to the populated content;
400
+ // there's nothing left to keep.
401
+ //
402
+ // Stub status is consulted from the in-memory registry — NEVER
403
+ // from disk frontmatter. The previous on-disk `agentCreated: true`
404
+ // model was a silent-data-loss landmine: the flag survived across
405
+ // sessions, restarts, and the doc's entire useful lifetime, so a
406
+ // reject-all years later would destroy real work. The in-memory
407
+ // model can only mark a doc as a stub during the brief window
408
+ // between create_document and the first accepted save.
409
+ // adr: adr/agent-stub-model.md
410
+ if (action === 'reject' && isAgentStub(resolvedFilename)) {
411
+ cancelDebouncedSave();
268
412
  try {
269
- // Remove from any workspace manifests before deleting the file
270
413
  removeDocFromAllWorkspaces(resolvedFilename);
271
414
  const result = await deleteDocument(resolvedFilename);
272
415
  if (result.switched && result.newDoc) {
@@ -278,15 +421,17 @@ export function setupWebSocket(server) {
278
421
  return; // File deleted — no strip/save needed
279
422
  }
280
423
  catch (err) {
281
- console.error('[WS] Failed to delete rejected agent doc:', err.message);
424
+ console.error('[WS] Failed to delete rejected agent stub:', err.message);
282
425
  // Fall through to normal strip+save (e.g. only doc remaining)
283
426
  }
284
427
  }
285
428
  if (isActiveDoc) {
286
- // Normal path: resolved doc is the active one
287
- if (action === 'accept' && metadata?.agentCreated) {
288
- delete metadata.agentCreated;
289
- }
429
+ // Normal path: resolved doc is the active one. Accept-all
430
+ // graduates the doc out of stub status (it now has accepted
431
+ // content); the writeToDisk graduation does the same for
432
+ // saves with mixed pending+accepted content.
433
+ if (action === 'accept')
434
+ unmarkAgentStub(resolvedFilename);
290
435
  stripPendingAttrs();
291
436
  save();
292
437
  updatePendingCacheForActiveDoc(); // Sync cache after strip (prevents stale "has changes" indicator)
@@ -294,6 +439,8 @@ export function setupWebSocket(server) {
294
439
  else {
295
440
  // Race path: resolved doc is NOT the active one (server switched away).
296
441
  // Strip pending attrs directly from the file on disk.
442
+ if (action === 'accept')
443
+ unmarkAgentStub(resolvedFilename);
297
444
  stripPendingAttrsFromFile(resolvedFilename, action === 'accept');
298
445
  }
299
446
  broadcastPendingDocsChanged();
@@ -302,7 +449,7 @@ export function setupWebSocket(server) {
302
449
  catch {
303
450
  // Ignore malformed messages
304
451
  }
305
- });
452
+ }
306
453
  ws.on('close', () => {
307
454
  clients.delete(ws);
308
455
  console.log(`[WS] Client disconnected (total: ${clients.size})`);
@@ -457,8 +604,8 @@ export function getPendingWritesSnapshot() {
457
604
  startedAt,
458
605
  }));
459
606
  }
460
- export function broadcastMarksChanged(filename) {
461
- const msg = JSON.stringify({ type: 'marks-changed', filename });
607
+ export function broadcastCommentsChanged(filename) {
608
+ const msg = JSON.stringify({ type: 'comments-changed', filename });
462
609
  for (const ws of clients) {
463
610
  if (ws.readyState === WebSocket.OPEN)
464
611
  ws.send(msg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openwriter",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "The open-source writing surface for AI agents. Markdown-native editor with pending change review — your agent writes, you accept or reject.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/skill/SKILL.md CHANGED
@@ -16,7 +16,7 @@ description: |
16
16
  Requires: OpenWriter MCP server configured. Browser UI at localhost:5050.
17
17
  metadata:
18
18
  author: travsteward
19
- version: "0.7.0"
19
+ version: "0.7.3"
20
20
  repository: https://github.com/travsteward/openwriter
21
21
  license: MIT
22
22
  ---
@@ -118,7 +118,7 @@ Every document has an immutable **docId** (8-char hex, e.g. `a1b2c3d4`) in its Y
118
118
  | Tool | Key Params | Description |
119
119
  |------|-----------|-------------|
120
120
  | `list_documents` | — | List all documents with title, docId, word count, active status |
121
- | `switch_document` | `docId` | Switch to a different document by docId |
121
+ | `switch_document` | `docId` | Change the user's view to a different document. **Rarely needed** — every tool targets docs by docId directly, so reads, writes, and creations never require switching. Use ONLY when you want to pull the user's attention to a specific doc (e.g. "I've loaded this up for your review"). The user may be perusing other docs — don't yank their view as part of normal work. |
122
122
  | `create_document` | `content_type`, `title?`, ... | Create a new document. `content_type` is required: "document", "tweet", "reply", "quote", "article", "linkedin", "newsletter", or "blog" |
123
123
  | `open_file` | `path` | Open an existing .md file from any location on disk |
124
124
  | `delete_document` | `docId` | Delete a document file (moves to OS trash, recoverable) |
@@ -153,12 +153,14 @@ Every document has an immutable **docId** (8-char hex, e.g. `a1b2c3d4`) in its Y
153
153
  | `move_item` | Move or reorder a doc, container, or workspace (type: doc/container/workspace) |
154
154
  | `rename_item` | Rename a workspace, container, or document (type: workspace/container/document) |
155
155
 
156
- ### Agent Marks
156
+ ### Comments
157
157
 
158
158
  | Tool | Key Params | Description |
159
159
  |------|-----------|-------------|
160
- | `get_agent_marks` | `docId?` | Get inline feedback marks left by the user (optional docId omit for all docs) |
161
- | `resolve_agent_marks` | `mark_ids` | Remove marks after addressing feedback (pass mark IDs) |
160
+ | `get_comments` | `docId?`, `scope?` | Get comments left by the user. Default scope is `workspace` when a docId is given (returns comments for every doc in the same project); pass `scope: "document"` to narrow, or `scope: "all"` for every doc on disk |
161
+ | `resolve_comments` | `comment_ids` | Remove comments after addressing feedback (pass comment IDs) |
162
+
163
+ The older names `get_agent_marks` and `resolve_agent_marks` remain as deprecated aliases.
162
164
 
163
165
  ### Task Management
164
166
 
@@ -233,7 +235,7 @@ The user can turn on **auto-accept** on a per-doc basis (right-click the doc in
233
235
  **Rules:**
234
236
  - `create_document` does NOT accept a `content` parameter — it always creates an empty doc
235
237
  - Step 1 (`create_document`) — shows spinner, creates empty doc, does NOT switch the editor
236
- - Step 2 (`populate_document`) — writes content to the active doc, marks as pending decorations, switches the editor, clears the spinner
238
+ - Step 2 (`populate_document`) — pass the `docId` from step 1 to write content directly to that doc, marks as pending decorations, clears the spinner. Does NOT switch the user's view — they keep working wherever they are.
237
239
  - Never use `write_to_pad` for the initial population — use `populate_document` exclusively
238
240
 
239
241
  ### Workspace-Integrated Creation
@@ -301,7 +303,7 @@ For voice-matched drafting without a custom Author's Voice profile, install the
301
303
 
302
304
  ```
303
305
  1. list_documents → see all docs with title + [docId]
304
- 2. read_pad read active doc (or switch_document({ docId }) first)
306
+ 2. read_pad({ docId: "e5f6a7b8" }) reads that doc directly, no switch needed
305
307
  3. write_to_pad({ docId: "e5f6a7b8", changes: [...] })
306
308
  → edits go to the identified doc, no view switch needed
307
309
  ```
@@ -333,21 +335,23 @@ For voice-matched drafting without a custom Author's Voice profile, install the
333
335
 
334
336
  The workspace and containers are auto-created on the first `create_document` call. Subsequent calls reuse the existing workspace/containers (matched case-insensitively).
335
337
 
336
- ### Agent marks (inline feedback)
338
+ ### Comments (inline feedback)
337
339
 
338
- Users can select text in the browser, right-click, and leave an "Agent Mark" — a note attached to a specific text range. Marks appear as dotted underlines in the editor. This is the user's way of marking up a document with feedback for you to address.
340
+ Users can select text in the browser, right-click, and leave a comment — a note attached to a specific text range. Comments appear as dotted underlines in the editor. This is the user's way of marking up a document with feedback for you to address.
339
341
 
340
342
  ```
341
- 1. User says "check my marks" (or you see the hint in read_pad output)
342
- 2. get_agent_marks all marks across all docs, grouped by document
343
- 3. Address each mark → rewrite, insert, delete via write_to_pad (use docId)
344
- 4. resolve_agent_marks([ids]) → clears decorations in browser
343
+ 1. User says "check my comments" (or you see the hint in read_pad output)
344
+ 2. get_comments({ docId }) comments for the current workspace by default
345
+ 3. Address each comment → rewrite, insert, delete via write_to_pad (use docId)
346
+ 4. resolve_comments([ids]) → clears decorations in browser
345
347
  ```
346
348
 
347
- - `read_pad` automatically shows mark counts: this doc + other docs
348
- - Always resolve marks after addressing them — the dotted underlines clear immediately
349
- - A mark with an empty note means "fix this" use your judgment
350
- - A mark with a note is specific feedback follow the instruction
349
+ - `read_pad` automatically shows comment counts: this doc + other docs
350
+ - Default scope is `workspace` when a docId is provided you see comments across every doc in the user's current project, not just the one they're viewing
351
+ - Pass `scope: "document"` to narrow to one doc, `scope: "all"` to span everything on disk
352
+ - Always resolve comments after addressing them — `resolve_comments` is a state change ("addressed, archive it"), not a destructive delete. The record stays in storage; only the decoration disappears. `get_comments` skips resolved ones by default
353
+ - A comment with an empty note means "fix this" — use your judgment
354
+ - A comment with a note is specific feedback — follow the instruction
351
355
 
352
356
  ### Book workspace guidelines
353
357
 
@@ -638,7 +642,14 @@ Then restart your Claude Code session (`/mcp` to reconnect).
638
642
 
639
643
  **MCP tools not available** — The OpenWriter MCP server isn't configured yet. Follow the [setup instructions](#mcp-tools-are-not-available-skill-first-install) above. After adding the MCP config, the user must restart their Claude Code session.
640
644
 
641
- **Browser dies mid-session** — The MCP stdio pipe can break during context compaction or session resets. The HTTP server survives (crash guards), but MCP tools stop working. Tell the user: **run `/mcp` to reconnect.** The new process enters client mode and proxies MCP calls to the surviving HTTP server. The browser will auto-reconnect.
645
+ **Browser dies mid-session** — The MCP stdio pipe can break during context compaction or session resets. The HTTP server survives (crash guards), but MCP tools stop working. Reconnect by [restarting the MCP server](#restarting-the-mcp-server) (see below). The new process enters client mode and proxies MCP calls to the surviving HTTP server. The browser will auto-reconnect.
646
+
647
+ ### Restarting the MCP server
648
+
649
+ Depends on how OpenWriter is configured:
650
+
651
+ - **Claude Code with `~/.claude.json`** — run `/mcp` to reconnect.
652
+ - **Claude Desktop with settings → developer** — there's no explicit restart button. Call `list_documents` (zero params, read-only, fast). If the previous process is dead, Claude auto-spawns a fresh one to satisfy the call. After code changes, kill the old process (`taskkill /F /PID <pid>` on Windows, `kill <pid>` on macOS/Linux) first so the spawn picks up the new build.
642
653
 
643
654
  **Port 5050 busy** — Another OpenWriter instance owns the port. New sessions auto-enter client mode (proxying via HTTP) — tools still work. No action needed.
644
655
 
@@ -648,6 +659,6 @@ Then restart your Claude Code session (`/mcp` to reconnect).
648
659
 
649
660
  **Server not starting** — Ensure `openwriter` works from your terminal (`npm install -g openwriter` first). If on Windows and the global command isn't found, the MCP config may need `"command": "cmd"` with `"args": ["/c", "openwriter", "--no-open"]`.
650
661
 
651
- **After code changes** — Run `npm run build` in `packages/openwriter`, then `/mcp` to restart with the new build.
662
+ **After code changes** — Run `npm run build` in `packages/openwriter`, kill the running openwriter process, then [restart the MCP server](#restarting-the-mcp-server). `/mcp` alone only reconnects to the existing process; it won't pick up new code unless the old process dies first.
652
663
 
653
664
  **Slow to load / loads last** — MCP servers load sequentially in config order. Move `openwriter` to the first position in `mcpServers` in `~/.claude.json`. See setup instructions above.