@filipc77/cowrite 0.4.28 → 0.6.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.
- package/README.md +31 -18
- package/dist/bin/cowrite.js +154 -45
- package/dist/bin/cowrite.js.map +1 -1
- package/package.json +5 -2
- package/ui/client.js +1011 -112
- package/ui/index.html +20 -2
- package/ui/styles.css +435 -147
package/README.md
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
# Cowrite
|
|
2
2
|
|
|
3
|
-
Live commenting plugin for coding agent sessions. Select text
|
|
3
|
+
Live commenting and inline editing plugin for coding agent sessions. Select text to comment, click to edit — your coding agent receives all changes in real time via MCP.
|
|
4
4
|
|
|
5
5
|
**The problem:** When working with AI coding agents, there's no way to give inline, contextual feedback on specific parts of a file while the agent is working. You either interrupt with a chat message (losing spatial context) or wait until the agent is done.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
The solution: Cowrite opens a live preview of any text file where you can select text and leave comments. The comments propagate directly into the agent ssss — so the agent can act on your feedback immediately. Any agent that supports MCP can use Cowrite's tools (get_pending_comments, resolve_comment, etc.). Claude Code users get additional integration: auto-installed hooks that surface comments on every prompt, and /review + /watch skills. The preview also supports inline editing: click any block to edit it, insert new blocks with the + button, and undo with Cmd+Z — all changes are saved to disk instantly.
|
|
8
8
|
|
|
9
9
|
## How it works
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
Browser (Preview UI) Node.js Process Claude Code
|
|
13
|
-
|
|
14
|
-
│ File preview │ │ HTTP + WebSocket │ │
|
|
15
|
-
│ Text selection │◄───►│ server (port 3377) │ │ MCP tools:
|
|
16
|
-
│ Comment creation │ │ │ │
|
|
17
|
-
│ Comment sidebar │ │ ┌────────────────┐ │ │ get_pending
|
|
18
|
-
|
|
19
|
-
│ │ (shared memory)│ │ │ reply
|
|
20
|
-
│ └───────┬────────┘ │ │ get_annotated│
|
|
21
|
-
│ │ │ │
|
|
22
|
-
│ ┌───────▼────────┐ │ │
|
|
23
|
-
│ │ MCP Server │◄─╋────►│
|
|
24
|
-
│ │ (stdio) │ │ │
|
|
25
|
-
│ └────────────────┘ │
|
|
13
|
+
┌──────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
|
|
14
|
+
│ File preview │ │ HTTP + WebSocket │ │ │
|
|
15
|
+
│ Text selection │◄───►│ server (port 3377) │ │ MCP tools: │
|
|
16
|
+
│ Comment creation │ │ │ │ │
|
|
17
|
+
│ Comment sidebar │ │ ┌────────────────┐ │ │ get_pending │
|
|
18
|
+
└──────────────────┘ │ │ CommentStore │ │ │ resolve │
|
|
19
|
+
│ │ (shared memory)│ │ │ reply │
|
|
20
|
+
│ └───────┬────────┘ │ │ get_annotated │
|
|
21
|
+
│ │ │ │ wait_for_comment │
|
|
22
|
+
│ ┌───────▼────────┐ │ │ │
|
|
23
|
+
│ │ MCP Server │◄─╋────►│ │
|
|
24
|
+
│ │ (stdio) │ │ │ │
|
|
25
|
+
│ └────────────────┘ │ └────────────────────┘
|
|
26
26
|
│ │
|
|
27
27
|
│ File Watcher │
|
|
28
28
|
└──────────────────────┘
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
A single Node.js process runs both the HTTP/WebSocket preview server and the MCP stdio server, sharing one in-memory comment store.
|
|
31
|
+
A single Node.js process runs both the HTTP/WebSocket preview server and the MCP stdio server, sharing one in-memory comment store. Inline edits from the browser are applied to the file on disk via WebSocket, and the file watcher propagates changes back to all connected clients.
|
|
32
32
|
|
|
33
33
|
## Quick Start
|
|
34
34
|
|
|
@@ -67,9 +67,11 @@ cowrite open
|
|
|
67
67
|
|
|
68
68
|
Pick a file and you'll see a live preview.
|
|
69
69
|
|
|
70
|
-
### 4.
|
|
70
|
+
### 4. Comment or edit
|
|
71
71
|
|
|
72
|
-
Select any text in the preview. A **Comment** button appears — click it to open the comment form. Your text selection stays intact, so you can still copy-paste normally.
|
|
72
|
+
**Commenting:** Select any text in the preview. A **Comment** button appears — click it to open the comment form. Your text selection stays intact, so you can still copy-paste normally.
|
|
73
|
+
|
|
74
|
+
**Editing:** Click any block in the preview to edit it inline (contentEditable for text, textarea for code blocks and tables). Press the **+** button between blocks to insert a new block from a searchable menu. Press **Cmd+Enter** to save your edit, **Escape** to cancel, or **Cmd+Z** to undo (up to 50 levels).
|
|
73
75
|
|
|
74
76
|
### 5. The agent picks up your comments
|
|
75
77
|
|
|
@@ -168,6 +170,9 @@ Blocks until a new comment is posted in the live preview, then returns it. If pe
|
|
|
168
170
|
- **Agent replies** — The agent can reply to comments, and replies appear in the browser sidebar in real time.
|
|
169
171
|
- **Auto-port selection** — If port 3377 is in use (e.g. running cowrite in multiple repos), it automatically tries the next port (3378, 3379, etc.).
|
|
170
172
|
- **Auto-installed hooks** — `cowrite serve` automatically installs Claude Code `UserPromptSubmit` and `Stop` hooks that surface pending comments to the agent. Merges with existing settings.
|
|
173
|
+
- **Inline editing** — click any block to edit directly (contentEditable for text, textarea for code/tables)
|
|
174
|
+
- **Block insertion** — + button between blocks, searchable menu
|
|
175
|
+
- **Undo** — Cmd+Z or header button, up to 50 levels, persisted across file switches
|
|
171
176
|
|
|
172
177
|
## Skills
|
|
173
178
|
|
|
@@ -190,6 +195,14 @@ Cowrite ships with two built-in Claude Code skills (auto-installed to `.claude/s
|
|
|
190
195
|
6. The agent makes the change, calls `reply_to_comment` (visible in the browser), then `resolve_comment`
|
|
191
196
|
7. The browser receives the update via WebSocket and shows the reply and resolved state
|
|
192
197
|
|
|
198
|
+
## How Edits Flow
|
|
199
|
+
|
|
200
|
+
1. You click a block in the browser preview to enter edit mode
|
|
201
|
+
2. After editing, press Cmd+Enter to save — the browser sends an `edit_apply` message via WebSocket
|
|
202
|
+
3. The server writes the updated content to the file on disk
|
|
203
|
+
4. The file watcher detects the change and broadcasts the new content to all connected clients
|
|
204
|
+
5. Comments re-anchor to their selected text in the updated file
|
|
205
|
+
|
|
193
206
|
## Development
|
|
194
207
|
|
|
195
208
|
```bash
|
package/dist/bin/cowrite.js
CHANGED
|
@@ -108,6 +108,26 @@ var CommentStore = class extends EventEmitter {
|
|
|
108
108
|
`));
|
|
109
109
|
return comment;
|
|
110
110
|
}
|
|
111
|
+
reopen(commentId) {
|
|
112
|
+
const comment = this.comments.get(commentId);
|
|
113
|
+
if (!comment || comment.status !== "resolved") return null;
|
|
114
|
+
comment.status = "pending";
|
|
115
|
+
comment.resolvedAt = null;
|
|
116
|
+
this.emit("change", comment);
|
|
117
|
+
this.emit("comment_reopened", comment);
|
|
118
|
+
this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
|
|
119
|
+
`));
|
|
120
|
+
return comment;
|
|
121
|
+
}
|
|
122
|
+
delete(commentId) {
|
|
123
|
+
const existed = this.comments.delete(commentId);
|
|
124
|
+
if (existed) {
|
|
125
|
+
this.emit("change", null);
|
|
126
|
+
this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
|
|
127
|
+
`));
|
|
128
|
+
}
|
|
129
|
+
return existed;
|
|
130
|
+
}
|
|
111
131
|
addReply(commentId, from, text) {
|
|
112
132
|
const comment = this.comments.get(commentId);
|
|
113
133
|
if (!comment) return null;
|
|
@@ -118,6 +138,13 @@ var CommentStore = class extends EventEmitter {
|
|
|
118
138
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
119
139
|
};
|
|
120
140
|
comment.replies.push(reply);
|
|
141
|
+
if (from === "agent" && comment.status === "pending") {
|
|
142
|
+
comment.status = "answered";
|
|
143
|
+
}
|
|
144
|
+
if (from === "user" && comment.status === "answered") {
|
|
145
|
+
comment.status = "pending";
|
|
146
|
+
this.emit("comment_reopened", comment);
|
|
147
|
+
}
|
|
121
148
|
this.emit("change", comment);
|
|
122
149
|
this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
|
|
123
150
|
`));
|
|
@@ -144,6 +171,7 @@ var CommentStore = class extends EventEmitter {
|
|
|
144
171
|
const fileComments = this.getForFile(file);
|
|
145
172
|
if (fileComments.length === 0) return;
|
|
146
173
|
for (const comment of fileComments) {
|
|
174
|
+
if (!comment.selectedText) continue;
|
|
147
175
|
const searchStart = Math.max(0, comment.offset - 200);
|
|
148
176
|
const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);
|
|
149
177
|
const searchRegion = newContent.slice(searchStart, searchEnd);
|
|
@@ -170,7 +198,8 @@ import { z } from "zod";
|
|
|
170
198
|
|
|
171
199
|
// src/utils.ts
|
|
172
200
|
import { exec } from "child_process";
|
|
173
|
-
import { marked } from "marked";
|
|
201
|
+
import { marked, Renderer } from "marked";
|
|
202
|
+
import hljs from "highlight.js";
|
|
174
203
|
function openBrowser(url) {
|
|
175
204
|
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `cmd /c start "" "${url}"` : `xdg-open "${url}"`;
|
|
176
205
|
return new Promise((resolve6) => {
|
|
@@ -189,19 +218,77 @@ function renderToHtml(content, filePath) {
|
|
|
189
218
|
return renderPlainTextWithOffsets(content);
|
|
190
219
|
}
|
|
191
220
|
function renderMarkdownWithOffsets(content) {
|
|
192
|
-
const
|
|
193
|
-
|
|
221
|
+
const tokens = marked.lexer(content);
|
|
222
|
+
const blocks = [];
|
|
223
|
+
let blockOffset = 0;
|
|
224
|
+
for (const token of tokens) {
|
|
225
|
+
if (token.type !== "space") {
|
|
226
|
+
blocks.push({ sourceStart: blockOffset, sourceEnd: blockOffset + token.raw.length });
|
|
227
|
+
}
|
|
228
|
+
blockOffset += token.raw.length;
|
|
229
|
+
}
|
|
230
|
+
const renderer = new Renderer();
|
|
231
|
+
const defaultCodeRenderer = renderer.code.bind(renderer);
|
|
232
|
+
renderer.code = function(token) {
|
|
233
|
+
if (token.lang === "mermaid") {
|
|
234
|
+
return `<div class="mermaid-container"><pre class="mermaid">${token.text}</pre></div>`;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
let highlighted;
|
|
238
|
+
let lang;
|
|
239
|
+
if (token.lang && hljs.getLanguage(token.lang)) {
|
|
240
|
+
const result = hljs.highlight(token.text, { language: token.lang });
|
|
241
|
+
highlighted = result.value;
|
|
242
|
+
lang = token.lang;
|
|
243
|
+
} else {
|
|
244
|
+
const result = hljs.highlightAuto(token.text);
|
|
245
|
+
highlighted = result.value;
|
|
246
|
+
lang = result.language || "plaintext";
|
|
247
|
+
}
|
|
248
|
+
return `<div class="code-block-wrapper" data-lang="${lang}"><div class="code-block-header"><span class="code-block-lang">${lang}</span><button class="code-copy-btn" type="button">Copy</button></div><pre><code class="hljs language-${lang}">${highlighted}</code></pre></div>`;
|
|
249
|
+
} catch {
|
|
250
|
+
return defaultCodeRenderer(token);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const html = marked.parse(content, { async: false, renderer });
|
|
254
|
+
const blocksAttr = JSON.stringify(blocks).replace(/"/g, """);
|
|
255
|
+
return `<div class="markdown-body" data-source-length="${content.length}" data-blocks="${blocksAttr}">${html}</div>`;
|
|
194
256
|
}
|
|
195
257
|
function renderPlainTextWithOffsets(content) {
|
|
196
258
|
const lines = content.split("\n");
|
|
197
259
|
let offset = 0;
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
260
|
+
const blocks = [];
|
|
261
|
+
const htmlParts = [];
|
|
262
|
+
let currentLines = [];
|
|
263
|
+
let blockStart = 0;
|
|
264
|
+
let blockIndex = 0;
|
|
265
|
+
for (let i = 0; i < lines.length; i++) {
|
|
266
|
+
const line = lines[i];
|
|
267
|
+
const lineOffset = offset;
|
|
202
268
|
offset += line.length + 1;
|
|
269
|
+
if (line.trim() === "") {
|
|
270
|
+
if (currentLines.length > 0) {
|
|
271
|
+
blocks.push({ sourceStart: blockStart, sourceEnd: lineOffset });
|
|
272
|
+
htmlParts.push(`<div class="text-block" data-block-index="${blockIndex}">${currentLines.join("\n")}</div>`);
|
|
273
|
+
blockIndex++;
|
|
274
|
+
currentLines = [];
|
|
275
|
+
}
|
|
276
|
+
htmlParts.push("");
|
|
277
|
+
blockStart = offset;
|
|
278
|
+
} else {
|
|
279
|
+
if (currentLines.length === 0) {
|
|
280
|
+
blockStart = lineOffset;
|
|
281
|
+
}
|
|
282
|
+
const escaped = escapeHtml(line);
|
|
283
|
+
currentLines.push(`<span class="line" data-offset="${lineOffset}" data-length="${line.length}">${escaped}</span>`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (currentLines.length > 0) {
|
|
287
|
+
blocks.push({ sourceStart: blockStart, sourceEnd: Math.min(offset, content.length) });
|
|
288
|
+
htmlParts.push(`<div class="text-block" data-block-index="${blockIndex}">${currentLines.join("\n")}</div>`);
|
|
203
289
|
}
|
|
204
|
-
|
|
290
|
+
const blocksAttr = JSON.stringify(blocks).replace(/"/g, """);
|
|
291
|
+
return `<pre class="plain-text" data-source-length="${content.length}" data-blocks="${blocksAttr}">${htmlParts.join("\n")}</pre>`;
|
|
205
292
|
}
|
|
206
293
|
function escapeHtml(text) {
|
|
207
294
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -210,6 +297,11 @@ function annotateFileWithComments(content, comments) {
|
|
|
210
297
|
const sorted = [...comments].sort((a, b) => b.offset - a.offset);
|
|
211
298
|
let result = content;
|
|
212
299
|
for (const c of sorted) {
|
|
300
|
+
if (!c.selectedText) {
|
|
301
|
+
result = `[FILE COMMENT #${c.id.slice(0, 8)}: "${c.comment}"]
|
|
302
|
+
` + result;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
213
305
|
const marker = `[COMMENT #${c.id.slice(0, 8)}: "${c.comment}"]`;
|
|
214
306
|
const end = c.offset + c.length;
|
|
215
307
|
result = result.slice(0, end) + " " + marker + result.slice(end);
|
|
@@ -230,7 +322,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
230
322
|
"Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.",
|
|
231
323
|
{
|
|
232
324
|
file: z.string().optional().describe("Filter by file path"),
|
|
233
|
-
status: z.enum(["pending", "resolved", "all"]).optional().describe("Filter by status (default: pending)")
|
|
325
|
+
status: z.enum(["pending", "answered", "resolved", "all"]).optional().describe("Filter by status (default: pending)")
|
|
234
326
|
},
|
|
235
327
|
async ({ file, status }) => {
|
|
236
328
|
const filter = {};
|
|
@@ -247,28 +339,9 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
247
339
|
};
|
|
248
340
|
}
|
|
249
341
|
);
|
|
250
|
-
server.tool(
|
|
251
|
-
"resolve_comment",
|
|
252
|
-
"Mark a comment as resolved/addressed.",
|
|
253
|
-
{
|
|
254
|
-
commentId: z.string().describe("The comment ID to resolve")
|
|
255
|
-
},
|
|
256
|
-
async ({ commentId }) => {
|
|
257
|
-
const comment = store.resolve(commentId);
|
|
258
|
-
if (!comment) {
|
|
259
|
-
return {
|
|
260
|
-
content: [{ type: "text", text: `Comment ${commentId} not found.` }],
|
|
261
|
-
isError: true
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
return {
|
|
265
|
-
content: [{ type: "text", text: `Comment ${commentId} resolved.` }]
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
);
|
|
269
342
|
server.tool(
|
|
270
343
|
"reply_to_comment",
|
|
271
|
-
"Reply to a comment from the agent.",
|
|
344
|
+
"Reply to a comment from the agent. Your reply automatically marks the comment as 'answered'. The user reviews it and can resolve or reply back.",
|
|
272
345
|
{
|
|
273
346
|
commentId: z.string().describe("The comment ID to reply to"),
|
|
274
347
|
reply: z.string().describe("The reply text")
|
|
@@ -355,9 +428,11 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
355
428
|
const cleanup = () => {
|
|
356
429
|
clearTimeout(timer);
|
|
357
430
|
store.off("new_comment", onComment);
|
|
431
|
+
store.off("comment_reopened", onComment);
|
|
358
432
|
signal?.removeEventListener("abort", onAbort);
|
|
359
433
|
};
|
|
360
434
|
store.on("new_comment", onComment);
|
|
435
|
+
store.on("comment_reopened", onComment);
|
|
361
436
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
362
437
|
if (signal?.aborted) {
|
|
363
438
|
onAbort();
|
|
@@ -407,7 +482,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
407
482
|
}).catch(() => {
|
|
408
483
|
});
|
|
409
484
|
});
|
|
410
|
-
|
|
485
|
+
function notifyCommentNeedsAttention(comment, prefix) {
|
|
411
486
|
if (!server.isConnected()) return;
|
|
412
487
|
const count = store.getAll({ status: "pending" }).length;
|
|
413
488
|
const file = relative(projectDir, comment.file);
|
|
@@ -419,15 +494,22 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
419
494
|
server.sendToolListChanged();
|
|
420
495
|
} catch {
|
|
421
496
|
}
|
|
497
|
+
const logMsg = comment.selectedText ? `${prefix} on ${file}: "${comment.comment}" (selected: "${selectedPreview}"). Call get_pending_comments to see it.` : `${prefix} on ${file}: "${comment.comment}". Call get_pending_comments to see it.`;
|
|
422
498
|
server.sendLoggingMessage({
|
|
423
499
|
level: "warning",
|
|
424
|
-
data:
|
|
500
|
+
data: logMsg
|
|
425
501
|
}).catch(() => {
|
|
426
502
|
});
|
|
427
503
|
server.server.notification({
|
|
428
504
|
method: "notifications/resources/list_changed"
|
|
429
505
|
}).catch(() => {
|
|
430
506
|
});
|
|
507
|
+
}
|
|
508
|
+
store.on("new_comment", (comment) => {
|
|
509
|
+
notifyCommentNeedsAttention(comment, "NEW COMMENT");
|
|
510
|
+
});
|
|
511
|
+
store.on("comment_reopened", (comment) => {
|
|
512
|
+
notifyCommentNeedsAttention(comment, "COMMENT REOPENED");
|
|
431
513
|
});
|
|
432
514
|
store.on("change", () => {
|
|
433
515
|
const count = store.getAll({ status: "pending" }).length;
|
|
@@ -440,7 +522,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
440
522
|
});
|
|
441
523
|
server.prompt(
|
|
442
524
|
"cowrite-workflow",
|
|
443
|
-
"How to process live preview comments in a wait-handle-
|
|
525
|
+
"How to process live preview comments in a wait-handle-reply loop",
|
|
444
526
|
() => ({
|
|
445
527
|
messages: [
|
|
446
528
|
{
|
|
@@ -450,17 +532,21 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
450
532
|
text: [
|
|
451
533
|
"You are monitoring a live code preview where users leave comments on selected text.",
|
|
452
534
|
"",
|
|
535
|
+
"Comment lifecycle: pending \u2192 answered (auto on your reply) \u2192 resolved (user only).",
|
|
536
|
+
"If the user disagrees with your answer, they reply back and it returns to pending.",
|
|
537
|
+
"",
|
|
453
538
|
"Follow this loop:",
|
|
454
539
|
"1. Call `get_pending_comments` to check for any comments already posted.",
|
|
455
|
-
"2. Process each pending comment: read the file, make the requested change
|
|
456
|
-
"
|
|
540
|
+
"2. Process each pending comment: read the file, make the requested change, then call `reply_to_comment` to explain what you did.",
|
|
541
|
+
" Your reply automatically marks the comment as 'answered'. The user will review and resolve it.",
|
|
542
|
+
"3. Call `wait_for_comment` to block until the next comment (or reopened comment) arrives.",
|
|
457
543
|
"4. When a comment arrives, process it the same way (step 2).",
|
|
458
544
|
"5. Go back to step 3 and keep listening.",
|
|
459
545
|
"",
|
|
460
546
|
"Tips:",
|
|
461
547
|
"- Use `get_file_with_annotations` to see comments in context within the file.",
|
|
462
548
|
"- Use `reply_to_comment` to acknowledge or ask clarifying questions.",
|
|
463
|
-
"-
|
|
549
|
+
"- Do NOT resolve comments \u2014 the user does that after reviewing your work."
|
|
464
550
|
].join("\n")
|
|
465
551
|
}
|
|
466
552
|
}
|
|
@@ -472,7 +558,7 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
472
558
|
|
|
473
559
|
// src/preview-server.ts
|
|
474
560
|
import { createServer } from "http";
|
|
475
|
-
import { readFile as readFile4, readdir } from "fs/promises";
|
|
561
|
+
import { readFile as readFile4, readdir, writeFile as writeFile2 } from "fs/promises";
|
|
476
562
|
import { existsSync } from "fs";
|
|
477
563
|
import { join as join2, resolve as resolve4 } from "path";
|
|
478
564
|
import { WebSocketServer } from "ws";
|
|
@@ -660,10 +746,10 @@ function createPreviewServer(store, projectDir, port, initialFile) {
|
|
|
660
746
|
const absPath = resolve4(resolvedProjectDir, initialFile);
|
|
661
747
|
await switchClientFile(ws, absPath);
|
|
662
748
|
}
|
|
663
|
-
ws.on("message", (data) => {
|
|
749
|
+
ws.on("message", async (data) => {
|
|
664
750
|
try {
|
|
665
751
|
const msg = JSON.parse(data.toString());
|
|
666
|
-
handleClientMessage(ws, msg);
|
|
752
|
+
await handleClientMessage(ws, msg);
|
|
667
753
|
} catch (err) {
|
|
668
754
|
send(ws, { type: "error", message: `Invalid message: ${err}` });
|
|
669
755
|
}
|
|
@@ -690,11 +776,11 @@ function createPreviewServer(store, projectDir, port, initialFile) {
|
|
|
690
776
|
send(ws, { type: "error", message: `Cannot open file: ${err}` });
|
|
691
777
|
}
|
|
692
778
|
}
|
|
693
|
-
function handleClientMessage(ws, msg) {
|
|
779
|
+
async function handleClientMessage(ws, msg) {
|
|
694
780
|
switch (msg.type) {
|
|
695
781
|
case "switch_file": {
|
|
696
782
|
const absPath = resolve4(resolvedProjectDir, msg.file);
|
|
697
|
-
switchClientFile(ws, absPath);
|
|
783
|
+
await switchClientFile(ws, absPath);
|
|
698
784
|
break;
|
|
699
785
|
}
|
|
700
786
|
case "comment_add": {
|
|
@@ -715,6 +801,26 @@ function createPreviewServer(store, projectDir, port, initialFile) {
|
|
|
715
801
|
case "comment_resolve":
|
|
716
802
|
store.resolve(msg.commentId);
|
|
717
803
|
break;
|
|
804
|
+
case "comment_reopen":
|
|
805
|
+
store.reopen(msg.commentId);
|
|
806
|
+
break;
|
|
807
|
+
case "comment_delete":
|
|
808
|
+
store.delete(msg.commentId);
|
|
809
|
+
break;
|
|
810
|
+
case "edit_apply": {
|
|
811
|
+
const file = clientFiles.get(ws);
|
|
812
|
+
if (!file) break;
|
|
813
|
+
const watcher = watchers.get(file);
|
|
814
|
+
if (!watcher) break;
|
|
815
|
+
const content = watcher.getContent();
|
|
816
|
+
if (msg.offset < 0 || msg.offset + msg.length > content.length) {
|
|
817
|
+
send(ws, { type: "error", message: "Edit offset/length out of bounds" });
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
const newContent = content.slice(0, msg.offset) + msg.newText + content.slice(msg.offset + msg.length);
|
|
821
|
+
await writeFile2(file, newContent, "utf-8");
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
718
824
|
}
|
|
719
825
|
}
|
|
720
826
|
store.on("change", (comment) => {
|
|
@@ -779,6 +885,9 @@ function createPreviewServer(store, projectDir, port, initialFile) {
|
|
|
779
885
|
}
|
|
780
886
|
|
|
781
887
|
// bin/cowrite.ts
|
|
888
|
+
import updateNotifier from "update-notifier";
|
|
889
|
+
import "module";
|
|
890
|
+
var version = true ? "0.6.0" : createRequire(import.meta.url)("../package.json").version;
|
|
782
891
|
var USAGE = `
|
|
783
892
|
cowrite \u2014 Live commenting plugin for coding agent sessions
|
|
784
893
|
|
|
@@ -834,7 +943,7 @@ COMMENTS_FILE="\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json"
|
|
|
834
943
|
if [ ! -f "$COMMENTS_FILE" ] || [ ! -s "$COMMENTS_FILE" ]; then exit 0; fi
|
|
835
944
|
PENDING=$(jq '[.[] | select(.status == "pending")] | length' "$COMMENTS_FILE" 2>/dev/null || echo 0)
|
|
836
945
|
if [ "$PENDING" -eq 0 ]; then exit 0; fi
|
|
837
|
-
jq -r '[.[] | select(.status == "pending")] | "COWRITE: \\(length) pending comment(s) from the live preview. For EACH comment: (1) make the requested change, (2) call reply_to_comment to explain what you did
|
|
946
|
+
jq -r '[.[] | select(.status == "pending")] | "COWRITE: \\(length) pending comment(s) from the live preview. For EACH comment: (1) make the requested change, (2) call reply_to_comment to explain what you did. Your reply automatically marks it as answered. The user will review and resolve it.\\n" + ([.[] | "- [\\(.id)] File: \\(.file | split("/") | last) | Text: \\"\\(.selectedText)\\" | Comment: \\(.comment)"] | join("\\n"))' "$COMMENTS_FILE" 2>/dev/null
|
|
838
947
|
`;
|
|
839
948
|
var HOOK_ENTRY = {
|
|
840
949
|
matcher: "",
|
|
@@ -860,8 +969,7 @@ Check for any pending comments left in the Cowrite live preview and address them
|
|
|
860
969
|
a. Read the comment text and the selected text it refers to.
|
|
861
970
|
b. Use \`get_file_with_annotations\` to see the comment in context.
|
|
862
971
|
c. Make the requested change or reply explaining why you can't.
|
|
863
|
-
d. Call \`reply_to_comment\` to acknowledge the feedback.
|
|
864
|
-
e. Call \`resolve_comment\` to mark it as addressed.
|
|
972
|
+
d. Call \`reply_to_comment\` to acknowledge the feedback. Your reply automatically marks it as "answered". The user will review and resolve it.
|
|
865
973
|
3. Summarize what was done.
|
|
866
974
|
`;
|
|
867
975
|
var SKILL_WATCH = `---
|
|
@@ -878,12 +986,12 @@ Start a background agent that watches for cowrite comments and handles them as t
|
|
|
878
986
|
|
|
879
987
|
1. First, handle any existing pending comments:
|
|
880
988
|
a. Call \`get_pending_comments\` to check for unresolved comments.
|
|
881
|
-
b. For each pending comment, use \`get_file_with_annotations\` to see context, make the change, call \`reply_to_comment
|
|
989
|
+
b. For each pending comment, use \`get_file_with_annotations\` to see context, make the change, and call \`reply_to_comment\`. Your reply automatically marks it as "answered".
|
|
882
990
|
|
|
883
991
|
2. Then, launch a **background** watcher using the Task tool:
|
|
884
992
|
- Use \`subagent_type: "general-purpose"\` and \`run_in_background: true\`
|
|
885
993
|
- The background agent should call \`wait_for_comment\` in a loop
|
|
886
|
-
- When a comment arrives, it handles it (read file, make change, reply
|
|
994
|
+
- When a comment arrives, it handles it (read file, make change, reply)
|
|
887
995
|
- On timeout, it re-calls \`wait_for_comment\` immediately
|
|
888
996
|
- The loop continues until the user says stop
|
|
889
997
|
|
|
@@ -944,6 +1052,7 @@ async function main() {
|
|
|
944
1052
|
process.exit(positionals.length === 0 && !values.help ? 1 : 0);
|
|
945
1053
|
}
|
|
946
1054
|
const command = positionals[0];
|
|
1055
|
+
updateNotifier({ pkg: { name: "@filipc77/cowrite", version } }).notify({ isGlobal: true });
|
|
947
1056
|
const projectDir = process.cwd();
|
|
948
1057
|
const port = parseInt(values.port, 10);
|
|
949
1058
|
installClaudeIntegration(projectDir);
|