@filipc77/cowrite 0.6.4 → 0.6.5
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 +15 -4
- package/dist/bin/cowrite.js +110 -2
- package/dist/bin/cowrite.js.map +1 -1
- package/package.json +1 -1
- package/ui/client.js +201 -94
- package/ui/index.html +6 -7
- package/ui/styles.css +244 -5
package/README.md
CHANGED
|
@@ -14,11 +14,12 @@ Browser (Preview UI) Node.js Process Claude Code
|
|
|
14
14
|
│ File preview │ │ HTTP + WebSocket │ │ │
|
|
15
15
|
│ Text selection │◄───►│ server (port 3377) │ │ MCP tools: │
|
|
16
16
|
│ Comment creation │ │ │ │ │
|
|
17
|
-
│ Comment sidebar │ │ ┌────────────────┐ │ │ get_pending │
|
|
17
|
+
│ Comment sidebar │ │ ┌────────────────┐ │ │ get_pending │
|
|
18
18
|
└──────────────────┘ │ │ CommentStore │ │ │ resolve │
|
|
19
19
|
│ │ (shared memory)│ │ │ reply │
|
|
20
|
-
│ └───────┬────────┘ │ │
|
|
21
|
-
│ │ │ │
|
|
20
|
+
│ └───────┬────────┘ │ │ propose_change │
|
|
21
|
+
│ │ │ │ get_annotated │
|
|
22
|
+
│ │ │ │ wait_for_comment │
|
|
22
23
|
│ ┌───────▼────────┐ │ │ │
|
|
23
24
|
│ │ MCP Server │◄─╋────►│ │
|
|
24
25
|
│ │ (stdio) │ │ │ │
|
|
@@ -135,6 +136,16 @@ Sends a reply from the agent, visible in the browser comment sidebar.
|
|
|
135
136
|
| `commentId` | string | The comment ID to reply to |
|
|
136
137
|
| `reply` | string | The reply text |
|
|
137
138
|
|
|
139
|
+
### `propose_change`
|
|
140
|
+
|
|
141
|
+
Proposes a text replacement for a comment's selected text. The user sees a diff in the browser with Apply/Reject buttons. Preferred over direct file edits when responding to comments on selected text.
|
|
142
|
+
|
|
143
|
+
| Parameter | Type | Description |
|
|
144
|
+
|-----------|------|-------------|
|
|
145
|
+
| `commentId` | string | The comment ID to propose a change for |
|
|
146
|
+
| `newText` | string | The replacement text |
|
|
147
|
+
| `explanation` | string | Explanation of the change |
|
|
148
|
+
|
|
138
149
|
### `get_file_with_annotations`
|
|
139
150
|
|
|
140
151
|
Returns the file content with inline comment markers at the positions where comments are anchored:
|
|
@@ -192,7 +203,7 @@ Cowrite ships with two built-in Claude Code skills (auto-installed to `.claude/s
|
|
|
192
203
|
- The `UserPromptSubmit` hook injecting it on the next user message
|
|
193
204
|
- `wait_for_comment` returning immediately (if `/watch` is active)
|
|
194
205
|
- The agent calling `get_pending_comments` directly
|
|
195
|
-
6. The agent
|
|
206
|
+
6. The agent responds — either calling `propose_change` (user sees a diff with Apply/Reject) or `reply_to_comment` + `resolve_comment`
|
|
196
207
|
7. The browser receives the update via WebSocket and shows the reply and resolved state
|
|
197
208
|
|
|
198
209
|
## How Edits Flow
|
package/dist/bin/cowrite.js
CHANGED
|
@@ -150,6 +150,46 @@ var CommentStore = class extends EventEmitter {
|
|
|
150
150
|
`));
|
|
151
151
|
return reply;
|
|
152
152
|
}
|
|
153
|
+
addProposalReply(commentId, newText, explanation) {
|
|
154
|
+
const comment = this.comments.get(commentId);
|
|
155
|
+
if (!comment) return null;
|
|
156
|
+
const proposal = {
|
|
157
|
+
oldText: comment.selectedText,
|
|
158
|
+
newText,
|
|
159
|
+
explanation,
|
|
160
|
+
status: "pending"
|
|
161
|
+
};
|
|
162
|
+
const reply = {
|
|
163
|
+
id: randomUUID(),
|
|
164
|
+
from: "agent",
|
|
165
|
+
text: explanation,
|
|
166
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
167
|
+
proposal
|
|
168
|
+
};
|
|
169
|
+
comment.replies.push(reply);
|
|
170
|
+
if (comment.status === "pending") {
|
|
171
|
+
comment.status = "answered";
|
|
172
|
+
}
|
|
173
|
+
this.emit("change", comment);
|
|
174
|
+
this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
|
|
175
|
+
`));
|
|
176
|
+
return reply;
|
|
177
|
+
}
|
|
178
|
+
updateProposalStatus(commentId, replyId, status) {
|
|
179
|
+
const comment = this.comments.get(commentId);
|
|
180
|
+
if (!comment) return false;
|
|
181
|
+
const reply = comment.replies.find((r) => r.id === replyId);
|
|
182
|
+
if (!reply?.proposal) return false;
|
|
183
|
+
reply.proposal.status = status;
|
|
184
|
+
if (status === "applied") {
|
|
185
|
+
comment.selectedText = reply.proposal.newText;
|
|
186
|
+
comment.length = reply.proposal.newText.length;
|
|
187
|
+
}
|
|
188
|
+
this.emit("change", comment);
|
|
189
|
+
this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
|
|
190
|
+
`));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
153
193
|
get(commentId) {
|
|
154
194
|
return this.comments.get(commentId) ?? null;
|
|
155
195
|
}
|
|
@@ -359,6 +399,40 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
359
399
|
};
|
|
360
400
|
}
|
|
361
401
|
);
|
|
402
|
+
server.tool(
|
|
403
|
+
"propose_change",
|
|
404
|
+
"Propose a text change for a comment's selected text. The user sees a diff and can Apply or Reject. Preferred over direct edits when responding to comments on selected text.",
|
|
405
|
+
{
|
|
406
|
+
commentId: z.string().describe("The comment ID to propose a change for"),
|
|
407
|
+
newText: z.string().describe("The replacement text"),
|
|
408
|
+
explanation: z.string().describe("Explanation of the change")
|
|
409
|
+
},
|
|
410
|
+
async ({ commentId, newText, explanation }) => {
|
|
411
|
+
const comment = store.get(commentId);
|
|
412
|
+
if (!comment) {
|
|
413
|
+
return {
|
|
414
|
+
content: [{ type: "text", text: `Comment ${commentId} not found.` }],
|
|
415
|
+
isError: true
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (!comment.selectedText) {
|
|
419
|
+
return {
|
|
420
|
+
content: [{ type: "text", text: `Comment ${commentId} is a file-level comment with no selected text. Use reply_to_comment instead.` }],
|
|
421
|
+
isError: true
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const reply = store.addProposalReply(commentId, newText, explanation);
|
|
425
|
+
if (!reply) {
|
|
426
|
+
return {
|
|
427
|
+
content: [{ type: "text", text: `Failed to add proposal to comment ${commentId}.` }],
|
|
428
|
+
isError: true
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: "text", text: `Proposal added to comment ${commentId}. The user can now review and apply or reject it.` }]
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
);
|
|
362
436
|
server.tool(
|
|
363
437
|
"get_file_with_annotations",
|
|
364
438
|
"Get file content with inline comment markers showing where comments are anchored.",
|
|
@@ -572,7 +646,9 @@ function createMcpServer(store, projectDir, getPreviewPort) {
|
|
|
572
646
|
"",
|
|
573
647
|
"Follow this loop:",
|
|
574
648
|
"1. Call `get_pending_comments` to check for any comments already posted.",
|
|
575
|
-
"2. Process each pending comment: read the file,
|
|
649
|
+
"2. Process each pending comment: read the file, then respond:",
|
|
650
|
+
" - If the comment is on selected text, prefer `propose_change` to suggest a diff the user can Apply/Reject.",
|
|
651
|
+
" - For file-level comments or questions, use `reply_to_comment` to explain what you did.",
|
|
576
652
|
" Your reply automatically marks the comment as 'answered'. The user will review and resolve it.",
|
|
577
653
|
"3. Call `wait_for_comment` to block until the next comment (or reopened comment) arrives.",
|
|
578
654
|
"4. When a comment arrives, process it the same way (step 2).",
|
|
@@ -856,6 +932,38 @@ function createPreviewServer(store, projectDir, port, initialFile) {
|
|
|
856
932
|
await writeFile2(file, newContent, "utf-8");
|
|
857
933
|
break;
|
|
858
934
|
}
|
|
935
|
+
case "proposal_apply": {
|
|
936
|
+
const comment = store.get(msg.commentId);
|
|
937
|
+
if (!comment) {
|
|
938
|
+
send(ws, { type: "error", message: "Comment not found" });
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
const reply = comment.replies.find((r) => r.id === msg.replyId);
|
|
942
|
+
if (!reply?.proposal || reply.proposal.status !== "pending") {
|
|
943
|
+
send(ws, { type: "error", message: "Proposal not found or not pending" });
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
const pFile = comment.file;
|
|
947
|
+
const pWatcher = watchers.get(pFile);
|
|
948
|
+
if (!pWatcher) {
|
|
949
|
+
send(ws, { type: "error", message: "File not being watched" });
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
const pContent = pWatcher.getContent();
|
|
953
|
+
const pSlice = pContent.slice(comment.offset, comment.offset + comment.length);
|
|
954
|
+
if (pSlice !== comment.selectedText) {
|
|
955
|
+
send(ws, { type: "error", message: "File content has changed \u2014 selected text no longer matches at this position" });
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
const pNewContent = pContent.slice(0, comment.offset) + reply.proposal.newText + pContent.slice(comment.offset + comment.length);
|
|
959
|
+
await writeFile2(pFile, pNewContent, "utf-8");
|
|
960
|
+
store.updateProposalStatus(msg.commentId, msg.replyId, "applied");
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
case "proposal_reject": {
|
|
964
|
+
store.updateProposalStatus(msg.commentId, msg.replyId, "rejected");
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
859
967
|
}
|
|
860
968
|
}
|
|
861
969
|
store.on("change", (comment) => {
|
|
@@ -922,7 +1030,7 @@ function createPreviewServer(store, projectDir, port, initialFile) {
|
|
|
922
1030
|
// bin/cowrite.ts
|
|
923
1031
|
import updateNotifier from "update-notifier";
|
|
924
1032
|
import "module";
|
|
925
|
-
var version = true ? "0.6.
|
|
1033
|
+
var version = true ? "0.6.5" : createRequire(import.meta.url)("../package.json").version;
|
|
926
1034
|
var USAGE = `
|
|
927
1035
|
cowrite \u2014 Live commenting plugin for coding agent sessions
|
|
928
1036
|
|
package/dist/bin/cowrite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../bin/cowrite.ts","../../src/comment-store.ts","../../src/mcp-server.ts","../../src/utils.ts","../../src/preview-server.ts","../../src/file-watcher.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { resolve, join } from \"node:path\";\nimport { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync, unlinkSync } from \"node:fs\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CommentStore } from \"../src/comment-store.js\";\nimport { createMcpServer } from \"../src/mcp-server.js\";\nimport { createPreviewServer } from \"../src/preview-server.js\";\nimport { openBrowser } from \"../src/utils.js\";\nimport updateNotifier from \"update-notifier\";\nimport { createRequire } from \"node:module\";\n\ndeclare const __COWRITE_VERSION__: string | undefined;\nconst version: string = typeof __COWRITE_VERSION__ !== \"undefined\"\n ? __COWRITE_VERSION__\n : (createRequire(import.meta.url)(\"../package.json\") as { version: string }).version;\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite init Install hooks and skills into .claude/ (run once per project)\n cowrite serve [--port N] Start MCP server + preview server (browse any file)\n cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server\n cowrite open [--port N] Open the browser to the preview URL\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --no-open Don't auto-open the browser\n --help, -h Show this help\n`;\n\nconst PORT_FILE = \".cowrite-port\";\n\nfunction writePortFile(projectDir: string, port: number): void {\n writeFileSync(join(projectDir, PORT_FILE), String(port), \"utf-8\");\n}\n\nfunction removePortFile(projectDir: string): void {\n try { unlinkSync(join(projectDir, PORT_FILE)); } catch {}\n}\n\nfunction readPortFile(projectDir: string): number | null {\n try {\n const content = readFileSync(join(projectDir, PORT_FILE), \"utf-8\").trim();\n const port = parseInt(content, 10);\n return isNaN(port) ? null : port;\n } catch {\n return null;\n }\n}\n\nfunction setupShutdown(store: CommentStore, preview: { stop: () => Promise<void>; [k: string]: any }, projectDir: string) {\n let shuttingDown = false;\n const shutdown = () => {\n if (shuttingDown) {\n // Second signal — force exit immediately\n process.exit(1);\n }\n shuttingDown = true;\n process.stderr.write(\"Shutting down...\\n\");\n removePortFile(projectDir);\n // Best-effort cleanup with a hard timeout\n Promise.allSettled([store.stopWatching(), preview.stop()])\n .finally(() => process.exit(0));\n setTimeout(() => process.exit(0), 2000);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nconst HOOK_SCRIPT = `#!/bin/bash\n# Auto-installed by cowrite — injects pending comments into Claude Code context.\n# Only outputs when there are pending comments. Silent otherwise.\nCOMMENTS_FILE=\"\\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json\"\nif [ ! -f \"$COMMENTS_FILE\" ] || [ ! -s \"$COMMENTS_FILE\" ]; then exit 0; fi\nPENDING=$(jq '[.[] | select(.status == \"pending\")] | length' \"$COMMENTS_FILE\" 2>/dev/null || echo 0)\nif [ \"$PENDING\" -eq 0 ]; then exit 0; fi\njq -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\n`;\n\nconst HOOK_ENTRY = {\n matcher: \"\",\n hooks: [{\n type: \"command\",\n command: `bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-comments.sh\"`,\n }],\n};\n\nconst HOOK_SETTINGS = {\n hooks: {\n UserPromptSubmit: [HOOK_ENTRY],\n Stop: [HOOK_ENTRY],\n },\n};\n\nconst SKILL_REVIEW = `---\nname: review\ndescription: Check and address cowrite comments left by the user in the live preview\nuser_invocable: true\n---\n\n# Review Cowrite Comments\n\nCheck for any pending comments left in the Cowrite live preview and address them.\n\n## Steps\n\n1. Call the \\`get_pending_comments\\` tool to retrieve all unresolved comments.\n2. For each pending comment:\n a. Read the comment text and the selected text it refers to.\n b. Use \\`get_file_with_annotations\\` to see the comment in context.\n c. Make the requested change or reply explaining why you can't.\n d. Call \\`reply_to_comment\\` to acknowledge the feedback. Your reply automatically marks it as \"answered\". The user will review and resolve it.\n3. Summarize what was done.\n`;\n\nconst SKILL_WATCH = `---\nname: watch\ndescription: Start a background watcher for cowrite comments — does not block the main conversation\nuser_invocable: true\n---\n\n# Watch for Live Comments (Background)\n\nStart a background agent that watches for cowrite comments and handles them as they arrive. The main conversation stays free for other work.\n\n## Steps\n\n1. First, handle any existing pending comments:\n a. Call \\`get_pending_comments\\` to check for unresolved comments.\n 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\".\n\n2. Then, launch a **background** watcher using the Task tool:\n - Use \\`subagent_type: \"general-purpose\"\\` and \\`run_in_background: true\\`\n - The background agent should call \\`wait_for_comment\\` in a loop\n - When a comment arrives, it handles it (read file, make change, reply)\n - On timeout, it re-calls \\`wait_for_comment\\` immediately\n - The loop continues until the user says stop\n\n3. Tell the user the background watcher is running and they can continue working normally. Comments will be handled automatically.\n`;\n\nfunction installClaudeIntegration(projectDir: string): void {\n const claudeDir = join(projectDir, \".claude\");\n const hooksDir = join(claudeDir, \"hooks\");\n const hookPath = join(hooksDir, \"inject-comments.sh\");\n const settingsPath = join(claudeDir, \"settings.json\");\n const reviewDir = join(claudeDir, \"skills\", \"review\");\n const watchDir = join(claudeDir, \"skills\", \"watch\");\n\n // Create directories\n for (const dir of [hooksDir, reviewDir, watchDir]) {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n // Write hook script (always overwrite to keep in sync with cowrite version)\n writeFileSync(hookPath, HOOK_SCRIPT, \"utf-8\");\n chmodSync(hookPath, 0o755);\n\n // Write skills (always overwrite to keep in sync)\n writeFileSync(join(reviewDir, \"SKILL.md\"), SKILL_REVIEW, \"utf-8\");\n writeFileSync(join(watchDir, \"SKILL.md\"), SKILL_WATCH, \"utf-8\");\n\n // Merge cowrite hooks into settings.json (preserve existing settings)\n let settings: any = {};\n if (existsSync(settingsPath)) {\n try {\n settings = JSON.parse(readFileSync(settingsPath, \"utf-8\"));\n } catch {\n // Corrupt JSON — overwrite\n }\n }\n if (!settings.hooks) settings.hooks = {};\n let changed = false;\n for (const eventType of [\"UserPromptSubmit\", \"Stop\"] as const) {\n if (!Array.isArray(settings.hooks[eventType])) settings.hooks[eventType] = [];\n const hasCowriteHook = settings.hooks[eventType].some((entry: any) =>\n entry.hooks?.some((h: any) => h.command?.includes(\"inject-comments.sh\"))\n );\n if (!hasCowriteHook) {\n settings.hooks[eventType].push(HOOK_ENTRY);\n changed = true;\n }\n }\n if (changed) {\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\", \"utf-8\");\n }\n}\n\nasync function main() {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n port: { type: \"string\", short: \"p\", default: \"3377\" },\n \"no-open\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n strict: false,\n });\n\n if (values.help || positionals.length === 0) {\n process.stderr.write(USAGE);\n process.exit(positionals.length === 0 && !values.help ? 1 : 0);\n }\n\n const command = positionals[0];\n\n updateNotifier({\n pkg: { name: \"@filipc77/cowrite\", version },\n updateCheckInterval: 1000 * 60 * 60, // 1 hour (default is 1 day)\n }).notify({ isGlobal: true, defer: false });\n\n const projectDir = process.cwd();\n const port = parseInt(values.port as string, 10);\n\n // Auto-install Claude Code hooks for comment propagation\n installClaudeIntegration(projectDir);\n\n if (command === \"serve\") {\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port);\n let previewRunning = false;\n try {\n await preview.start();\n previewRunning = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"preview\") {\n const filePath = positionals[1];\n if (!filePath) {\n process.stderr.write(\"Error: preview command requires a file path\\n\");\n process.stderr.write(USAGE);\n process.exit(1);\n }\n\n const resolvedFile = resolve(projectDir, filePath);\n\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port, resolvedFile);\n let previewRunning2 = false;\n try {\n await preview.start();\n previewRunning2 = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning2 ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"init\") {\n process.stderr.write(\"Installed cowrite hooks and skills into .claude/\\n\");\n } else if (command === \"open\") {\n const discoveredPort = readPortFile(projectDir) ?? port;\n const url = `http://localhost:${discoveredPort}`;\n process.stderr.write(`Opening ${url}\\n`);\n await openBrowser(url);\n } else {\n process.stderr.write(`Unknown command: ${command}\\n`);\n process.stderr.write(USAGE);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`Fatal error: ${err}\\n`);\n process.exit(1);\n});\n","import { EventEmitter } from \"node:events\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { watch as chokidarWatch, type FSWatcher } from \"chokidar\";\nimport type { Comment, Reply } from \"./types.js\";\n\nconst PERSIST_FILE = \".cowrite-comments.json\";\n\nexport class CommentStore extends EventEmitter {\n private comments: Map<string, Comment> = new Map();\n private persistPath: string;\n private lastWriteTime = 0;\n private watcher: FSWatcher | null = null;\n\n constructor(projectDir: string) {\n super();\n this.persistPath = join(resolve(projectDir), PERSIST_FILE);\n }\n\n async load(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n } catch {\n // No existing file, start fresh\n }\n }\n\n private async persist(): Promise<void> {\n this.lastWriteTime = Date.now();\n const arr = Array.from(this.comments.values());\n await writeFile(this.persistPath, JSON.stringify(arr, null, 2), \"utf-8\");\n }\n\n async reload(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n const oldIds = new Set(this.comments.keys());\n this.comments.clear();\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n // Emit \"new_comment\" for comments that didn't exist before\n for (const c of arr) {\n if (!oldIds.has(c.id)) {\n this.emit(\"new_comment\", c);\n }\n }\n this.emit(\"change\", null);\n } catch {\n // File missing or invalid, clear state\n this.comments.clear();\n this.emit(\"change\", null);\n }\n }\n\n async startWatching(): Promise<void> {\n if (this.watcher) return;\n this.watcher = chokidarWatch(this.persistPath, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n this.watcher.on(\"change\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n this.watcher.on(\"add\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n }\n\n async stopWatching(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n\n add(params: {\n file: string;\n offset: number;\n length: number;\n selectedText: string;\n comment: string;\n }): Comment {\n const comment: Comment = {\n id: randomUUID(),\n file: params.file,\n offset: params.offset,\n length: params.length,\n selectedText: params.selectedText,\n comment: params.comment,\n status: \"pending\",\n replies: [],\n createdAt: new Date().toISOString(),\n resolvedAt: null,\n };\n this.comments.set(comment.id, comment);\n this.emit(\"change\", comment);\n this.emit(\"new_comment\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n resolve(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n comment.status = \"resolved\";\n comment.resolvedAt = new Date().toISOString();\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n reopen(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment || comment.status !== \"resolved\") return null;\n comment.status = \"pending\";\n comment.resolvedAt = null;\n this.emit(\"change\", comment);\n this.emit(\"comment_reopened\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n delete(commentId: string): boolean {\n const existed = this.comments.delete(commentId);\n if (existed) {\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n return existed;\n }\n\n addReply(commentId: string, from: \"user\" | \"agent\", text: string): Reply | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n const reply: Reply = {\n id: randomUUID(),\n from,\n text,\n createdAt: new Date().toISOString(),\n };\n comment.replies.push(reply);\n // Agent reply on pending → answered\n if (from === \"agent\" && comment.status === \"pending\") {\n comment.status = \"answered\";\n }\n // User reply on answered → back to pending (re-opens conversation)\n if (from === \"user\" && comment.status === \"answered\") {\n comment.status = \"pending\";\n this.emit(\"comment_reopened\", comment);\n }\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return reply;\n }\n\n get(commentId: string): Comment | null {\n return this.comments.get(commentId) ?? null;\n }\n\n getAll(filter?: { file?: string; status?: \"pending\" | \"answered\" | \"resolved\" | \"all\" }): Comment[] {\n let results = Array.from(this.comments.values());\n if (filter?.file) {\n results = results.filter((c) => c.file === filter.file);\n }\n if (filter?.status && filter.status !== \"all\") {\n results = results.filter((c) => c.status === filter.status);\n }\n return results.sort((a, b) => a.offset - b.offset);\n }\n\n getForFile(file: string): Comment[] {\n return this.getAll({ file });\n }\n\n /** Adjust comment offsets when file content changes */\n adjustOffsets(file: string, oldContent: string, newContent: string): void {\n const fileComments = this.getForFile(file);\n if (fileComments.length === 0) return;\n\n for (const comment of fileComments) {\n if (!comment.selectedText) continue; // file comments don't re-anchor\n // Try to find the selected text in the new content near original offset\n const searchStart = Math.max(0, comment.offset - 200);\n const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);\n const searchRegion = newContent.slice(searchStart, searchEnd);\n const idx = searchRegion.indexOf(comment.selectedText);\n if (idx !== -1) {\n comment.offset = searchStart + idx;\n }\n // If not found, leave offset as-is (orphaned comment)\n }\n\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n\n clear(): void {\n this.comments.clear();\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n}\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { annotateFileWithComments } from \"./utils.js\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\n\nexport function createMcpServer(store: CommentStore, projectDir: string, getPreviewPort?: () => number | null): McpServer {\n const server = new McpServer(\n { name: \"cowrite\", version: \"0.1.0\" },\n { capabilities: { logging: {} } },\n );\n\n // Tool: get_pending_comments\n const getPendingTool = server.tool(\n \"get_pending_comments\",\n \"Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.\",\n {\n file: z.string().optional().describe(\"Filter by file path\"),\n status: z.enum([\"pending\", \"answered\", \"resolved\", \"all\"]).optional().describe(\"Filter by status (default: pending)\"),\n },\n async ({ file, status }) => {\n const filter: { file?: string; status?: \"pending\" | \"answered\" | \"resolved\" | \"all\" } = {};\n if (file) filter.file = resolve(projectDir, file);\n filter.status = status ?? \"pending\";\n const comments = store.getAll(filter);\n return {\n content: [\n {\n type: \"text\" as const,\n text: comments.length === 0\n ? \"No comments found.\"\n : JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Tool: reply_to_comment\n server.tool(\n \"reply_to_comment\",\n \"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.\",\n {\n commentId: z.string().describe(\"The comment ID to reply to\"),\n reply: z.string().describe(\"The reply text\"),\n },\n async ({ commentId, reply }) => {\n const replyObj = store.addReply(commentId, \"agent\", reply);\n if (!replyObj) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} not found.` }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `Reply added to comment ${commentId}.` }],\n };\n }\n );\n\n // Tool: get_file_with_annotations\n server.tool(\n \"get_file_with_annotations\",\n \"Get file content with inline comment markers showing where comments are anchored.\",\n {\n file: z.string().describe(\"File path to annotate\"),\n },\n async ({ file }) => {\n const filePath = resolve(projectDir, file);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const comments = store.getForFile(filePath);\n const annotated = annotateFileWithComments(content, comments);\n return {\n content: [{ type: \"text\" as const, text: annotated }],\n };\n } catch (err) {\n return {\n content: [{ type: \"text\" as const, text: `Error reading file: ${err}` }],\n isError: true,\n };\n }\n }\n );\n\n // Tool: wait_for_comment\n // Track reply counts at the time each comment was last returned, so we can\n // detect new user replies on already-handled comments in the early pending check.\n const lastSeenReplyCounts = new Map<string, number>();\n\n function formatCommentPayload(\n comment: { id: string; file: string; selectedText: string; comment: string; replies?: Array<{ from: string; text: string }> },\n event: \"new_comment\" | \"follow_up\",\n ) {\n const file = relative(projectDir, comment.file);\n const replies = comment.replies ?? [];\n lastSeenReplyCounts.set(comment.id, replies.length);\n const payload: Record<string, unknown> = { ...comment, file, event };\n if (event === \"follow_up\" && replies.length > 0) {\n // Include the latest user reply so the agent can see what changed\n const userReplies = replies.filter((r) => r.from === \"user\");\n if (userReplies.length > 0) {\n payload.latestUserReply = userReplies[userReplies.length - 1].text;\n }\n }\n return payload;\n }\n\n server.tool(\n \"wait_for_comment\",\n \"Block until a new or follow-up comment is posted in the live preview, then return it. Returns an 'event' field: 'new_comment' for brand-new comments, 'follow_up' when the user replied to an already-answered comment. For follow-ups, 'latestUserReply' contains the new reply text. Call again immediately after handling each result to keep listening.\",\n {\n timeout: z.number().optional().describe(\"Max seconds to wait (default: 30)\"),\n },\n ({ timeout }, { signal }: { signal?: AbortSignal }) => {\n const maxWait = (timeout ?? 30) * 1000;\n\n // Check for comments that arrived while no one was listening.\n // Only return a pending comment if it's brand-new (never seen) or\n // has new replies since we last returned it.\n const pending = store.getAll({ status: \"pending\" });\n for (const c of pending) {\n const prevCount = lastSeenReplyCounts.get(c.id);\n if (prevCount === undefined) {\n // Brand-new comment we've never returned\n const payload = formatCommentPayload(c, \"new_comment\");\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n };\n }\n if (c.replies.length > prevCount) {\n // Existing comment with new replies (user follow-up)\n const payload = formatCommentPayload(c, \"follow_up\");\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n };\n }\n }\n\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n const count = store.getAll({ status: \"pending\" }).length;\n resolve({\n content: [{ type: \"text\" as const, text: count > 0\n ? `Timeout, but ${count} pending comment(s) exist. Call get_pending_comments now.`\n : \"No new comments yet. Call wait_for_comment again to keep listening.\" }],\n });\n }, maxWait);\n\n const onNewComment = (comment: { id: string; file: string; selectedText: string; comment: string; replies?: Array<{ from: string; text: string }> }) => {\n cleanup();\n const payload = formatCommentPayload(comment, \"new_comment\");\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n });\n };\n\n const onReopened = (comment: { id: string; file: string; selectedText: string; comment: string; replies?: Array<{ from: string; text: string }> }) => {\n cleanup();\n const payload = formatCommentPayload(comment, \"follow_up\");\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n });\n };\n\n const onAbort = () => {\n cleanup();\n resolve({\n content: [{ type: \"text\" as const, text: \"Cancelled. Call wait_for_comment again to resume listening.\" }],\n });\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n store.off(\"new_comment\", onNewComment);\n store.off(\"comment_reopened\", onReopened);\n signal?.removeEventListener(\"abort\", onAbort);\n };\n\n store.on(\"new_comment\", onNewComment);\n store.on(\"comment_reopened\", onReopened);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n // If already aborted before we set up\n if (signal?.aborted) {\n onAbort();\n }\n });\n }\n );\n\n // Tool: get_preview_url\n server.tool(\n \"get_preview_url\",\n \"Get the URL of the Cowrite live preview. Share this with the user so they can open it in their browser.\",\n {},\n async () => {\n const port = getPreviewPort?.();\n if (!port) {\n return {\n content: [{ type: \"text\" as const, text: \"Preview server is not running.\" }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `http://localhost:${port}` }],\n };\n }\n );\n\n // Resource: cowrite://comments\n server.resource(\n \"all-comments\",\n \"cowrite://comments\",\n { description: \"Live list of all comments\", mimeType: \"application/json\" },\n async () => {\n const comments = store.getAll();\n return {\n contents: [\n {\n uri: \"cowrite://comments\",\n mimeType: \"application/json\",\n text: JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Wire store changes to MCP resource notifications\n store.on(\"change\", () => {\n if (!server.isConnected()) return;\n server.server.notification({\n method: \"notifications/resources/updated\",\n params: { uri: \"cowrite://comments\" },\n }).catch(() => {});\n });\n\n // --- Comment propagation signals ---\n // Only send signals when an MCP client is actually connected.\n // 1. Update tool description with pending count + sendToolListChanged\n // 2. sendLoggingMessage as additional context\n // Primary real-time mechanism is wait_for_comment via the /watch skill.\n function notifyCommentNeedsAttention(comment: { file: string; selectedText: string; comment: string }, prefix: string) {\n if (!server.isConnected()) return;\n\n const count = store.getAll({ status: \"pending\" }).length;\n const file = relative(projectDir, comment.file);\n const selectedPreview = comment.selectedText.length > 80\n ? comment.selectedText.slice(0, 80) + \"...\"\n : comment.selectedText;\n\n // Signal 1: Update tool description + notify tool list changed\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n server.sendToolListChanged();\n } catch {\n // Not connected or transport issue — skip\n }\n\n // Signal 2: Logging message\n const logMsg = comment.selectedText\n ? `${prefix} on ${file}: \"${comment.comment}\" (selected: \"${selectedPreview}\"). Call get_pending_comments to see it.`\n : `${prefix} on ${file}: \"${comment.comment}\". Call get_pending_comments to see it.`;\n server.sendLoggingMessage({\n level: \"warning\",\n data: logMsg,\n }).catch(() => {});\n\n // Signal 3: Resource list changed\n server.server.notification({\n method: \"notifications/resources/list_changed\",\n }).catch(() => {});\n }\n\n store.on(\"new_comment\", (comment: { file: string; selectedText: string; comment: string }) => {\n notifyCommentNeedsAttention(comment, \"NEW COMMENT\");\n });\n\n store.on(\"comment_reopened\", (comment: { file: string; selectedText: string; comment: string }) => {\n notifyCommentNeedsAttention(comment, \"COMMENT REOPENED\");\n });\n\n // Update description count when comments are resolved\n store.on(\"change\", () => {\n const count = store.getAll({ status: \"pending\" }).length;\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n } catch {\n // Ignore — may not be connected yet\n }\n });\n\n // Prompt: cowrite-workflow\n server.prompt(\n \"cowrite-workflow\",\n \"How to process live preview comments in a wait-handle-reply loop\",\n () => ({\n messages: [\n {\n role: \"user\" as const,\n content: {\n type: \"text\" as const,\n text: [\n \"You are monitoring a live code preview where users leave comments on selected text.\",\n \"\",\n \"Comment lifecycle: pending → answered (auto on your reply) → resolved (user only).\",\n \"If the user disagrees with your answer, they reply back and it returns to pending.\",\n \"\",\n \"Follow this loop:\",\n \"1. Call `get_pending_comments` to check for any comments already posted.\",\n \"2. Process each pending comment: read the file, make the requested change, then call `reply_to_comment` to explain what you did.\",\n \" Your reply automatically marks the comment as 'answered'. The user will review and resolve it.\",\n \"3. Call `wait_for_comment` to block until the next comment (or reopened comment) arrives.\",\n \"4. When a comment arrives, process it the same way (step 2).\",\n \"5. Go back to step 3 and keep listening.\",\n \"\",\n \"Tips:\",\n \"- Use `get_file_with_annotations` to see comments in context within the file.\",\n \"- Use `reply_to_comment` to acknowledge or ask clarifying questions.\",\n \"- Do NOT resolve comments — the user does that after reviewing your work.\",\n ].join(\"\\n\"),\n },\n },\n ],\n })\n );\n\n return server;\n}\n","import { exec } from \"node:child_process\";\nimport { marked, Renderer, type Tokens } from \"marked\";\nimport hljs from \"highlight.js\";\nimport type { Comment } from \"./types.js\";\n\n/**\n * Open a URL in the user's default browser.\n * Returns a promise so callers can await if needed (e.g. before process exit).\n */\nexport function openBrowser(url: string): Promise<void> {\n const cmd = process.platform === \"darwin\" ? `open \"${url}\"`\n : process.platform === \"win32\" ? `cmd /c start \"\" \"${url}\"`\n : `xdg-open \"${url}\"`;\n return new Promise((resolve) => {\n exec(cmd, (err) => {\n if (err) process.stderr.write(`Could not open browser: ${err.message}\\n`);\n resolve();\n });\n });\n}\n\n/**\n * Render file content as HTML. Markdown files get full rendering;\n * other text files are wrapped in <pre> with offset-tagged spans.\n */\nexport function renderToHtml(content: string, filePath: string): string {\n const isMarkdown = /\\.(md|markdown|mdx)$/i.test(filePath);\n if (isMarkdown) {\n return renderMarkdownWithOffsets(content);\n }\n return renderPlainTextWithOffsets(content);\n}\n\nfunction renderMarkdownWithOffsets(content: string): string {\n // Build block offset map from lexer tokens\n const tokens = marked.lexer(content);\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n let blockOffset = 0;\n for (const token of tokens) {\n if (token.type !== \"space\") {\n blocks.push({ sourceStart: blockOffset, sourceEnd: blockOffset + token.raw.length });\n }\n blockOffset += token.raw.length;\n }\n\n const renderer = new Renderer();\n const defaultCodeRenderer = renderer.code.bind(renderer);\n\n renderer.code = function (token: Tokens.Code) {\n if (token.lang === \"mermaid\") {\n return `<div class=\"mermaid-container\"><pre class=\"mermaid\">${token.text}</pre></div>`;\n }\n try {\n let highlighted: string;\n let lang: string;\n if (token.lang && hljs.getLanguage(token.lang)) {\n const result = hljs.highlight(token.text, { language: token.lang });\n highlighted = result.value;\n lang = token.lang;\n } else {\n const result = hljs.highlightAuto(token.text);\n highlighted = result.value;\n lang = result.language || \"plaintext\";\n }\n 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>`;\n } catch {\n return defaultCodeRenderer(token);\n }\n };\n\n const html = marked.parse(content, { async: false, renderer }) as string;\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \""\");\n return `<div class=\"markdown-body\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${html}</div>`;\n}\n\nfunction renderPlainTextWithOffsets(content: string): string {\n const lines = content.split(\"\\n\");\n let offset = 0;\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n const htmlParts: string[] = [];\n let currentLines: string[] = [];\n let blockStart = 0;\n let blockIndex = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineOffset = offset;\n offset += line.length + 1; // +1 for the newline\n\n if (line.trim() === \"\") {\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: lineOffset });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n blockIndex++;\n currentLines = [];\n }\n htmlParts.push(\"\");\n blockStart = offset;\n } else {\n if (currentLines.length === 0) {\n blockStart = lineOffset;\n }\n const escaped = escapeHtml(line);\n currentLines.push(`<span class=\"line\" data-offset=\"${lineOffset}\" data-length=\"${line.length}\">${escaped}</span>`);\n }\n }\n\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: Math.min(offset, content.length) });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n }\n\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \""\");\n return `<pre class=\"plain-text\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${htmlParts.join(\"\\n\")}</pre>`;\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\n/**\n * Annotate file content with inline comment markers for the agent.\n * Inserts `[COMMENT #id: \"text\"]` at the comment offsets.\n */\nexport function annotateFileWithComments(content: string, comments: Comment[]): string {\n // Sort by offset descending so insertions don't shift earlier offsets\n const sorted = [...comments].sort((a, b) => b.offset - a.offset);\n let result = content;\n\n for (const c of sorted) {\n if (!c.selectedText) {\n // File comment — prepend to file\n result = `[FILE COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]\\n` + result;\n continue;\n }\n const marker = `[COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]`;\n const end = c.offset + c.length;\n // Insert marker after the selected text\n result = result.slice(0, end) + \" \" + marker + result.slice(end);\n }\n\n return result;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve, relative } from \"node:path\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { FileWatcher } from \"./file-watcher.js\";\nimport { renderToHtml } from \"./utils.js\";\nimport type { WSClientMessage, WSServerMessage } from \"./types.js\";\n\n// In dev (tsx): import.meta.dirname is src/, so ../ui works.\n// In built (dist/bin/cowrite.js): import.meta.dirname is dist/bin/, so ../../ui.\n// We find ui/ by checking which path actually contains index.html.\nfunction findUiDir(): string {\n const dir = import.meta.dirname ?? new URL(\".\", import.meta.url).pathname;\n // Try common locations relative to this file\n const candidates = [\n join(dir, \"..\", \"ui\"), // dev: src/../ui\n join(dir, \"..\", \"..\", \"ui\"), // built: dist/bin/../../ui\n ];\n return candidates.find((d) => {\n try { return existsSync(join(d, \"index.html\")); } catch { return false; }\n }) ?? candidates[0];\n}\nconst UI_DIR = findUiDir();\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n};\n\nconst IGNORED_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \".next\", \".cache\", \"coverage\", \"__pycache__\"]);\n\nexport function createPreviewServer(\n store: CommentStore,\n projectDir: string,\n port: number,\n initialFile?: string\n): { port: number; start: () => Promise<void>; stop: () => Promise<void> } {\n const clients = new Set<WebSocket>();\n const clientFiles = new Map<WebSocket, string>(); // ws -> absolute file path\n const watchers = new Map<string, FileWatcher>(); // absolute path -> watcher\n const watcherListeners = new Map<string, (...args: any[]) => void>(); // path -> change listener\n\n const resolvedProjectDir = resolve(projectDir);\n\n function isInsideProject(filePath: string): boolean {\n const resolved = resolve(resolvedProjectDir, filePath);\n return resolved.startsWith(resolvedProjectDir);\n }\n\n async function getOrCreateWatcher(absPath: string): Promise<FileWatcher> {\n let watcher = watchers.get(absPath);\n if (!watcher) {\n watcher = new FileWatcher(absPath);\n await watcher.start();\n watchers.set(absPath, watcher);\n\n // Subscribe to file changes and broadcast to relevant clients\n const listener = (event: { file: string; content: string; oldContent: string }) => {\n store.adjustOffsets(event.file, event.oldContent, event.content);\n const html = renderToHtml(event.content, event.file);\n for (const [ws, file] of clientFiles) {\n if (file === absPath) {\n send(ws, { type: \"file_update\", file: event.file, content: event.content, html });\n }\n }\n };\n watcher.on(\"change\", listener);\n watcherListeners.set(absPath, listener);\n }\n return watcher;\n }\n\n async function listFiles(dir: string, prefix = \"\"): Promise<string[]> {\n const files: string[] = [];\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || IGNORED_DIRS.has(entry.name)) continue;\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n const sub = await listFiles(join(dir, entry.name), relPath);\n files.push(...sub);\n } else {\n files.push(relPath);\n }\n }\n } catch {\n // Permission denied or gone — skip\n }\n return files;\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n const pathname = url.pathname === \"/\" ? \"/index.html\" : url.pathname;\n\n // Serve static UI files\n const ext = pathname.slice(pathname.lastIndexOf(\".\"));\n const mimeType = MIME_TYPES[ext];\n if (mimeType) {\n try {\n const filePath = join(UI_DIR, pathname);\n const content = await readFile(filePath, \"utf-8\");\n res.writeHead(200, { \"Content-Type\": mimeType });\n res.end(content);\n return;\n } catch {\n // Fall through to 404\n }\n }\n\n // API: GET /api/files — list project files for the file picker\n if (pathname === \"/api/files\") {\n const files = await listFiles(resolvedProjectDir);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ files }));\n return;\n }\n\n // API: GET /api/state?file=... — state for a specific file\n if (pathname === \"/api/state\") {\n const fileParam = url.searchParams.get(\"file\");\n if (!fileParam) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing file parameter\" }));\n return;\n }\n const absPath = resolve(resolvedProjectDir, fileParam);\n if (!isInsideProject(absPath)) {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Path outside project\" }));\n return;\n }\n try {\n const content = await readFile(absPath, \"utf-8\");\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ file: absPath, content, html, comments }));\n } catch {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"File not found\" }));\n }\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"Not found\");\n });\n\n const wss = new WebSocketServer({ server: httpServer });\n\n // Prevent unhandled WSS errors from crashing the process (e.g. EADDRINUSE)\n wss.on(\"error\", () => {});\n\n wss.on(\"connection\", async (ws: WebSocket) => {\n clients.add(ws);\n\n // If there's an initial file (preview mode), auto-assign it\n if (initialFile) {\n const absPath = resolve(resolvedProjectDir, initialFile);\n await switchClientFile(ws, absPath);\n }\n\n ws.on(\"message\", async (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n await handleClientMessage(ws, msg);\n } catch (err) {\n send(ws, { type: \"error\", message: `Invalid message: ${err}` });\n }\n });\n\n ws.on(\"close\", () => {\n clients.delete(ws);\n clientFiles.delete(ws);\n });\n });\n\n async function switchClientFile(ws: WebSocket, absPath: string): Promise<void> {\n if (!isInsideProject(absPath)) {\n send(ws, { type: \"error\", message: \"Path outside project\" });\n return;\n }\n try {\n const watcher = await getOrCreateWatcher(absPath);\n clientFiles.set(ws, absPath);\n const content = watcher.getContent();\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n send(ws, { type: \"file_update\", file: absPath, content, html });\n send(ws, { type: \"comments_update\", comments });\n } catch (err) {\n send(ws, { type: \"error\", message: `Cannot open file: ${err}` });\n }\n }\n\n async function handleClientMessage(ws: WebSocket, msg: WSClientMessage): Promise<void> {\n switch (msg.type) {\n case \"switch_file\": {\n const absPath = resolve(resolvedProjectDir, msg.file);\n await switchClientFile(ws, absPath);\n break;\n }\n case \"comment_add\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n store.add({\n file,\n offset: msg.offset,\n length: msg.length,\n selectedText: msg.selectedText,\n comment: msg.comment,\n });\n break;\n }\n case \"comment_reply\":\n store.addReply(msg.commentId, \"user\", msg.text);\n break;\n case \"comment_resolve\":\n store.resolve(msg.commentId);\n break;\n case \"comment_reopen\":\n store.reopen(msg.commentId);\n break;\n case \"comment_delete\":\n store.delete(msg.commentId);\n break;\n case \"edit_apply\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n const watcher = watchers.get(file);\n if (!watcher) break;\n const content = watcher.getContent();\n if (msg.offset < 0 || msg.offset + msg.length > content.length) {\n send(ws, { type: \"error\", message: \"Edit offset/length out of bounds\" });\n break;\n }\n const newContent = content.slice(0, msg.offset) + msg.newText + content.slice(msg.offset + msg.length);\n await writeFile(file, newContent, \"utf-8\");\n break;\n }\n }\n }\n\n // Broadcast comment updates to clients viewing the affected file\n store.on(\"change\", (comment: any) => {\n for (const [ws, file] of clientFiles) {\n // If we know which file changed, only notify relevant clients\n // If comment is null (e.g. adjustOffsets, reload), notify all\n if (!comment || comment.file === file) {\n const comments = store.getForFile(file);\n send(ws, { type: \"comments_update\", comments });\n }\n }\n });\n\n function send(ws: WebSocket, msg: WSServerMessage): void {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify(msg));\n }\n }\n\n let actualPort = port;\n\n return {\n get port() { return actualPort; },\n start: () => {\n const maxRetries = 10;\n const tryListen = (p: number, attempt: number): Promise<void> =>\n new Promise<void>((res, rej) => {\n const onError = (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" && attempt < maxRetries) {\n httpServer.removeListener(\"error\", onError);\n res(tryListen(p + 1, attempt + 1));\n } else {\n rej(err);\n }\n };\n httpServer.on(\"error\", onError);\n httpServer.listen(p, () => {\n httpServer.removeListener(\"error\", onError);\n actualPort = p;\n process.stderr.write(`Cowrite preview server running at http://localhost:${p}\\n`);\n res();\n });\n });\n return tryListen(port, 0);\n },\n stop: async () => {\n for (const client of clients) {\n client.close();\n }\n for (const [path, watcher] of watchers) {\n const listener = watcherListeners.get(path);\n if (listener) watcher.off(\"change\", listener);\n await watcher.stop();\n }\n watchers.clear();\n watcherListeners.clear();\n await new Promise<void>((resolvePromise, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolvePromise();\n });\n });\n },\n };\n}\n","import { watch, type FSWatcher } from \"chokidar\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { EventEmitter } from \"node:events\";\n\nexport interface FileChangeEvent {\n file: string;\n content: string;\n}\n\nexport class FileWatcher extends EventEmitter {\n private watcher: FSWatcher | null = null;\n private filePath: string;\n private lastContent: string = \"\";\n\n constructor(filePath: string) {\n super();\n this.filePath = resolve(filePath);\n }\n\n async start(): Promise<string> {\n this.lastContent = await readFile(this.filePath, \"utf-8\");\n\n this.watcher = watch(this.filePath, {\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n\n this.watcher.on(\"change\", async () => {\n try {\n const newContent = await readFile(this.filePath, \"utf-8\");\n if (newContent !== this.lastContent) {\n const oldContent = this.lastContent;\n this.lastContent = newContent;\n this.emit(\"change\", {\n file: this.filePath,\n content: newContent,\n oldContent,\n } as FileChangeEvent & { oldContent: string });\n }\n } catch (err) {\n process.stderr.write(`File watch read error: ${err}\\n`);\n }\n });\n\n return this.lastContent;\n }\n\n getContent(): string {\n return this.lastContent;\n }\n\n getFilePath(): string {\n return this.filePath;\n }\n\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n}\n"],"mappings":"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","names":["resolve","join","existsSync","resolve","readFile","resolve","readFile","writeFile","join","resolve","readFile","resolve","EventEmitter","join","resolve","readFile","writeFile","join","join","existsSync","resolve"]}
|
|
1
|
+
{"version":3,"sources":["../../bin/cowrite.ts","../../src/comment-store.ts","../../src/mcp-server.ts","../../src/utils.ts","../../src/preview-server.ts","../../src/file-watcher.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { resolve, join } from \"node:path\";\nimport { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync, unlinkSync } from \"node:fs\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CommentStore } from \"../src/comment-store.js\";\nimport { createMcpServer } from \"../src/mcp-server.js\";\nimport { createPreviewServer } from \"../src/preview-server.js\";\nimport { openBrowser } from \"../src/utils.js\";\nimport updateNotifier from \"update-notifier\";\nimport { createRequire } from \"node:module\";\n\ndeclare const __COWRITE_VERSION__: string | undefined;\nconst version: string = typeof __COWRITE_VERSION__ !== \"undefined\"\n ? __COWRITE_VERSION__\n : (createRequire(import.meta.url)(\"../package.json\") as { version: string }).version;\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite init Install hooks and skills into .claude/ (run once per project)\n cowrite serve [--port N] Start MCP server + preview server (browse any file)\n cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server\n cowrite open [--port N] Open the browser to the preview URL\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --no-open Don't auto-open the browser\n --help, -h Show this help\n`;\n\nconst PORT_FILE = \".cowrite-port\";\n\nfunction writePortFile(projectDir: string, port: number): void {\n writeFileSync(join(projectDir, PORT_FILE), String(port), \"utf-8\");\n}\n\nfunction removePortFile(projectDir: string): void {\n try { unlinkSync(join(projectDir, PORT_FILE)); } catch {}\n}\n\nfunction readPortFile(projectDir: string): number | null {\n try {\n const content = readFileSync(join(projectDir, PORT_FILE), \"utf-8\").trim();\n const port = parseInt(content, 10);\n return isNaN(port) ? null : port;\n } catch {\n return null;\n }\n}\n\nfunction setupShutdown(store: CommentStore, preview: { stop: () => Promise<void>; [k: string]: any }, projectDir: string) {\n let shuttingDown = false;\n const shutdown = () => {\n if (shuttingDown) {\n // Second signal — force exit immediately\n process.exit(1);\n }\n shuttingDown = true;\n process.stderr.write(\"Shutting down...\\n\");\n removePortFile(projectDir);\n // Best-effort cleanup with a hard timeout\n Promise.allSettled([store.stopWatching(), preview.stop()])\n .finally(() => process.exit(0));\n setTimeout(() => process.exit(0), 2000);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nconst HOOK_SCRIPT = `#!/bin/bash\n# Auto-installed by cowrite — injects pending comments into Claude Code context.\n# Only outputs when there are pending comments. Silent otherwise.\nCOMMENTS_FILE=\"\\${CLAUDE_PROJECT_DIR:-.}/.cowrite-comments.json\"\nif [ ! -f \"$COMMENTS_FILE\" ] || [ ! -s \"$COMMENTS_FILE\" ]; then exit 0; fi\nPENDING=$(jq '[.[] | select(.status == \"pending\")] | length' \"$COMMENTS_FILE\" 2>/dev/null || echo 0)\nif [ \"$PENDING\" -eq 0 ]; then exit 0; fi\njq -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\n`;\n\nconst HOOK_ENTRY = {\n matcher: \"\",\n hooks: [{\n type: \"command\",\n command: `bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-comments.sh\"`,\n }],\n};\n\nconst HOOK_SETTINGS = {\n hooks: {\n UserPromptSubmit: [HOOK_ENTRY],\n Stop: [HOOK_ENTRY],\n },\n};\n\nconst SKILL_REVIEW = `---\nname: review\ndescription: Check and address cowrite comments left by the user in the live preview\nuser_invocable: true\n---\n\n# Review Cowrite Comments\n\nCheck for any pending comments left in the Cowrite live preview and address them.\n\n## Steps\n\n1. Call the \\`get_pending_comments\\` tool to retrieve all unresolved comments.\n2. For each pending comment:\n a. Read the comment text and the selected text it refers to.\n b. Use \\`get_file_with_annotations\\` to see the comment in context.\n c. Make the requested change or reply explaining why you can't.\n d. Call \\`reply_to_comment\\` to acknowledge the feedback. Your reply automatically marks it as \"answered\". The user will review and resolve it.\n3. Summarize what was done.\n`;\n\nconst SKILL_WATCH = `---\nname: watch\ndescription: Start a background watcher for cowrite comments — does not block the main conversation\nuser_invocable: true\n---\n\n# Watch for Live Comments (Background)\n\nStart a background agent that watches for cowrite comments and handles them as they arrive. The main conversation stays free for other work.\n\n## Steps\n\n1. First, handle any existing pending comments:\n a. Call \\`get_pending_comments\\` to check for unresolved comments.\n 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\".\n\n2. Then, launch a **background** watcher using the Task tool:\n - Use \\`subagent_type: \"general-purpose\"\\` and \\`run_in_background: true\\`\n - The background agent should call \\`wait_for_comment\\` in a loop\n - When a comment arrives, it handles it (read file, make change, reply)\n - On timeout, it re-calls \\`wait_for_comment\\` immediately\n - The loop continues until the user says stop\n\n3. Tell the user the background watcher is running and they can continue working normally. Comments will be handled automatically.\n`;\n\nfunction installClaudeIntegration(projectDir: string): void {\n const claudeDir = join(projectDir, \".claude\");\n const hooksDir = join(claudeDir, \"hooks\");\n const hookPath = join(hooksDir, \"inject-comments.sh\");\n const settingsPath = join(claudeDir, \"settings.json\");\n const reviewDir = join(claudeDir, \"skills\", \"review\");\n const watchDir = join(claudeDir, \"skills\", \"watch\");\n\n // Create directories\n for (const dir of [hooksDir, reviewDir, watchDir]) {\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n }\n\n // Write hook script (always overwrite to keep in sync with cowrite version)\n writeFileSync(hookPath, HOOK_SCRIPT, \"utf-8\");\n chmodSync(hookPath, 0o755);\n\n // Write skills (always overwrite to keep in sync)\n writeFileSync(join(reviewDir, \"SKILL.md\"), SKILL_REVIEW, \"utf-8\");\n writeFileSync(join(watchDir, \"SKILL.md\"), SKILL_WATCH, \"utf-8\");\n\n // Merge cowrite hooks into settings.json (preserve existing settings)\n let settings: any = {};\n if (existsSync(settingsPath)) {\n try {\n settings = JSON.parse(readFileSync(settingsPath, \"utf-8\"));\n } catch {\n // Corrupt JSON — overwrite\n }\n }\n if (!settings.hooks) settings.hooks = {};\n let changed = false;\n for (const eventType of [\"UserPromptSubmit\", \"Stop\"] as const) {\n if (!Array.isArray(settings.hooks[eventType])) settings.hooks[eventType] = [];\n const hasCowriteHook = settings.hooks[eventType].some((entry: any) =>\n entry.hooks?.some((h: any) => h.command?.includes(\"inject-comments.sh\"))\n );\n if (!hasCowriteHook) {\n settings.hooks[eventType].push(HOOK_ENTRY);\n changed = true;\n }\n }\n if (changed) {\n writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + \"\\n\", \"utf-8\");\n }\n}\n\nasync function main() {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n port: { type: \"string\", short: \"p\", default: \"3377\" },\n \"no-open\": { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n },\n allowPositionals: true,\n strict: false,\n });\n\n if (values.help || positionals.length === 0) {\n process.stderr.write(USAGE);\n process.exit(positionals.length === 0 && !values.help ? 1 : 0);\n }\n\n const command = positionals[0];\n\n updateNotifier({\n pkg: { name: \"@filipc77/cowrite\", version },\n updateCheckInterval: 1000 * 60 * 60, // 1 hour (default is 1 day)\n }).notify({ isGlobal: true, defer: false });\n\n const projectDir = process.cwd();\n const port = parseInt(values.port as string, 10);\n\n // Auto-install Claude Code hooks for comment propagation\n installClaudeIntegration(projectDir);\n\n if (command === \"serve\") {\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port);\n let previewRunning = false;\n try {\n await preview.start();\n previewRunning = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"preview\") {\n const filePath = positionals[1];\n if (!filePath) {\n process.stderr.write(\"Error: preview command requires a file path\\n\");\n process.stderr.write(USAGE);\n process.exit(1);\n }\n\n const resolvedFile = resolve(projectDir, filePath);\n\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (non-fatal — MCP works even if port is taken)\n const preview = createPreviewServer(store, projectDir, port, resolvedFile);\n let previewRunning2 = false;\n try {\n await preview.start();\n previewRunning2 = true;\n writePortFile(projectDir, preview.port);\n const previewUrl = `http://localhost:${preview.port}`;\n process.stderr.write(`Preview: ${previewUrl}\\n`);\n if (!values[\"no-open\"]) openBrowser(previewUrl);\n } catch (err) {\n process.stderr.write(`Preview server failed: ${err}\\n`);\n process.stderr.write(`MCP server will still run — comments sync via .cowrite-comments.json\\n`);\n }\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir, () => previewRunning2 ? preview.port : null);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n\n setupShutdown(store, preview, projectDir);\n } else if (command === \"init\") {\n process.stderr.write(\"Installed cowrite hooks and skills into .claude/\\n\");\n } else if (command === \"open\") {\n const discoveredPort = readPortFile(projectDir) ?? port;\n const url = `http://localhost:${discoveredPort}`;\n process.stderr.write(`Opening ${url}\\n`);\n await openBrowser(url);\n } else {\n process.stderr.write(`Unknown command: ${command}\\n`);\n process.stderr.write(USAGE);\n process.exit(1);\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(`Fatal error: ${err}\\n`);\n process.exit(1);\n});\n","import { EventEmitter } from \"node:events\";\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { watch as chokidarWatch, type FSWatcher } from \"chokidar\";\nimport type { Comment, Proposal, Reply } from \"./types.js\";\n\nconst PERSIST_FILE = \".cowrite-comments.json\";\n\nexport class CommentStore extends EventEmitter {\n private comments: Map<string, Comment> = new Map();\n private persistPath: string;\n private lastWriteTime = 0;\n private watcher: FSWatcher | null = null;\n\n constructor(projectDir: string) {\n super();\n this.persistPath = join(resolve(projectDir), PERSIST_FILE);\n }\n\n async load(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n } catch {\n // No existing file, start fresh\n }\n }\n\n private async persist(): Promise<void> {\n this.lastWriteTime = Date.now();\n const arr = Array.from(this.comments.values());\n await writeFile(this.persistPath, JSON.stringify(arr, null, 2), \"utf-8\");\n }\n\n async reload(): Promise<void> {\n try {\n const data = await readFile(this.persistPath, \"utf-8\");\n const arr: Comment[] = JSON.parse(data);\n const oldIds = new Set(this.comments.keys());\n this.comments.clear();\n for (const c of arr) {\n this.comments.set(c.id, c);\n }\n // Emit \"new_comment\" for comments that didn't exist before\n for (const c of arr) {\n if (!oldIds.has(c.id)) {\n this.emit(\"new_comment\", c);\n }\n }\n this.emit(\"change\", null);\n } catch {\n // File missing or invalid, clear state\n this.comments.clear();\n this.emit(\"change\", null);\n }\n }\n\n async startWatching(): Promise<void> {\n if (this.watcher) return;\n this.watcher = chokidarWatch(this.persistPath, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n this.watcher.on(\"change\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n this.watcher.on(\"add\", async () => {\n if (Date.now() - this.lastWriteTime < 200) return;\n await this.reload();\n });\n }\n\n async stopWatching(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n\n add(params: {\n file: string;\n offset: number;\n length: number;\n selectedText: string;\n comment: string;\n }): Comment {\n const comment: Comment = {\n id: randomUUID(),\n file: params.file,\n offset: params.offset,\n length: params.length,\n selectedText: params.selectedText,\n comment: params.comment,\n status: \"pending\",\n replies: [],\n createdAt: new Date().toISOString(),\n resolvedAt: null,\n };\n this.comments.set(comment.id, comment);\n this.emit(\"change\", comment);\n this.emit(\"new_comment\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n resolve(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n comment.status = \"resolved\";\n comment.resolvedAt = new Date().toISOString();\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n reopen(commentId: string): Comment | null {\n const comment = this.comments.get(commentId);\n if (!comment || comment.status !== \"resolved\") return null;\n comment.status = \"pending\";\n comment.resolvedAt = null;\n this.emit(\"change\", comment);\n this.emit(\"comment_reopened\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return comment;\n }\n\n delete(commentId: string): boolean {\n const existed = this.comments.delete(commentId);\n if (existed) {\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n return existed;\n }\n\n addReply(commentId: string, from: \"user\" | \"agent\", text: string): Reply | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n const reply: Reply = {\n id: randomUUID(),\n from,\n text,\n createdAt: new Date().toISOString(),\n };\n comment.replies.push(reply);\n // Agent reply on pending → answered\n if (from === \"agent\" && comment.status === \"pending\") {\n comment.status = \"answered\";\n }\n // User reply on answered → back to pending (re-opens conversation)\n if (from === \"user\" && comment.status === \"answered\") {\n comment.status = \"pending\";\n this.emit(\"comment_reopened\", comment);\n }\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return reply;\n }\n\n addProposalReply(commentId: string, newText: string, explanation: string): Reply | null {\n const comment = this.comments.get(commentId);\n if (!comment) return null;\n const proposal: Proposal = {\n oldText: comment.selectedText,\n newText,\n explanation,\n status: \"pending\",\n };\n const reply: Reply = {\n id: randomUUID(),\n from: \"agent\",\n text: explanation,\n createdAt: new Date().toISOString(),\n proposal,\n };\n comment.replies.push(reply);\n if (comment.status === \"pending\") {\n comment.status = \"answered\";\n }\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return reply;\n }\n\n updateProposalStatus(commentId: string, replyId: string, status: \"applied\" | \"rejected\"): boolean {\n const comment = this.comments.get(commentId);\n if (!comment) return false;\n const reply = comment.replies.find((r) => r.id === replyId);\n if (!reply?.proposal) return false;\n reply.proposal.status = status;\n // When applied, update the comment's anchor to match the new text\n if (status === \"applied\") {\n comment.selectedText = reply.proposal.newText;\n comment.length = reply.proposal.newText.length;\n }\n this.emit(\"change\", comment);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n return true;\n }\n\n get(commentId: string): Comment | null {\n return this.comments.get(commentId) ?? null;\n }\n\n getAll(filter?: { file?: string; status?: \"pending\" | \"answered\" | \"resolved\" | \"all\" }): Comment[] {\n let results = Array.from(this.comments.values());\n if (filter?.file) {\n results = results.filter((c) => c.file === filter.file);\n }\n if (filter?.status && filter.status !== \"all\") {\n results = results.filter((c) => c.status === filter.status);\n }\n return results.sort((a, b) => a.offset - b.offset);\n }\n\n getForFile(file: string): Comment[] {\n return this.getAll({ file });\n }\n\n /** Adjust comment offsets when file content changes */\n adjustOffsets(file: string, oldContent: string, newContent: string): void {\n const fileComments = this.getForFile(file);\n if (fileComments.length === 0) return;\n\n for (const comment of fileComments) {\n if (!comment.selectedText) continue; // file comments don't re-anchor\n // Try to find the selected text in the new content near original offset\n const searchStart = Math.max(0, comment.offset - 200);\n const searchEnd = Math.min(newContent.length, comment.offset + comment.length + 200);\n const searchRegion = newContent.slice(searchStart, searchEnd);\n const idx = searchRegion.indexOf(comment.selectedText);\n if (idx !== -1) {\n comment.offset = searchStart + idx;\n }\n // If not found, leave offset as-is (orphaned comment)\n }\n\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n\n clear(): void {\n this.comments.clear();\n this.emit(\"change\", null);\n this.persist().catch((err) => process.stderr.write(`Persist error: ${err}\\n`));\n }\n}\n","import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { annotateFileWithComments } from \"./utils.js\";\nimport { readFile } from \"node:fs/promises\";\nimport { relative, resolve } from \"node:path\";\n\nexport function createMcpServer(store: CommentStore, projectDir: string, getPreviewPort?: () => number | null): McpServer {\n const server = new McpServer(\n { name: \"cowrite\", version: \"0.1.0\" },\n { capabilities: { logging: {} } },\n );\n\n // Tool: get_pending_comments\n const getPendingTool = server.tool(\n \"get_pending_comments\",\n \"Get comments from the live preview (0 pending). Call this first to catch comments posted before you started listening.\",\n {\n file: z.string().optional().describe(\"Filter by file path\"),\n status: z.enum([\"pending\", \"answered\", \"resolved\", \"all\"]).optional().describe(\"Filter by status (default: pending)\"),\n },\n async ({ file, status }) => {\n const filter: { file?: string; status?: \"pending\" | \"answered\" | \"resolved\" | \"all\" } = {};\n if (file) filter.file = resolve(projectDir, file);\n filter.status = status ?? \"pending\";\n const comments = store.getAll(filter);\n return {\n content: [\n {\n type: \"text\" as const,\n text: comments.length === 0\n ? \"No comments found.\"\n : JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Tool: reply_to_comment\n server.tool(\n \"reply_to_comment\",\n \"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.\",\n {\n commentId: z.string().describe(\"The comment ID to reply to\"),\n reply: z.string().describe(\"The reply text\"),\n },\n async ({ commentId, reply }) => {\n const replyObj = store.addReply(commentId, \"agent\", reply);\n if (!replyObj) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} not found.` }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `Reply added to comment ${commentId}.` }],\n };\n }\n );\n\n // Tool: propose_change\n server.tool(\n \"propose_change\",\n \"Propose a text change for a comment's selected text. The user sees a diff and can Apply or Reject. Preferred over direct edits when responding to comments on selected text.\",\n {\n commentId: z.string().describe(\"The comment ID to propose a change for\"),\n newText: z.string().describe(\"The replacement text\"),\n explanation: z.string().describe(\"Explanation of the change\"),\n },\n async ({ commentId, newText, explanation }) => {\n const comment = store.get(commentId);\n if (!comment) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} not found.` }],\n isError: true,\n };\n }\n if (!comment.selectedText) {\n return {\n content: [{ type: \"text\" as const, text: `Comment ${commentId} is a file-level comment with no selected text. Use reply_to_comment instead.` }],\n isError: true,\n };\n }\n const reply = store.addProposalReply(commentId, newText, explanation);\n if (!reply) {\n return {\n content: [{ type: \"text\" as const, text: `Failed to add proposal to comment ${commentId}.` }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `Proposal added to comment ${commentId}. The user can now review and apply or reject it.` }],\n };\n }\n );\n\n // Tool: get_file_with_annotations\n server.tool(\n \"get_file_with_annotations\",\n \"Get file content with inline comment markers showing where comments are anchored.\",\n {\n file: z.string().describe(\"File path to annotate\"),\n },\n async ({ file }) => {\n const filePath = resolve(projectDir, file);\n try {\n const content = await readFile(filePath, \"utf-8\");\n const comments = store.getForFile(filePath);\n const annotated = annotateFileWithComments(content, comments);\n return {\n content: [{ type: \"text\" as const, text: annotated }],\n };\n } catch (err) {\n return {\n content: [{ type: \"text\" as const, text: `Error reading file: ${err}` }],\n isError: true,\n };\n }\n }\n );\n\n // Tool: wait_for_comment\n // Track reply counts at the time each comment was last returned, so we can\n // detect new user replies on already-handled comments in the early pending check.\n const lastSeenReplyCounts = new Map<string, number>();\n\n function formatCommentPayload(\n comment: { id: string; file: string; selectedText: string; comment: string; replies?: Array<{ from: string; text: string }> },\n event: \"new_comment\" | \"follow_up\",\n ) {\n const file = relative(projectDir, comment.file);\n const replies = comment.replies ?? [];\n lastSeenReplyCounts.set(comment.id, replies.length);\n const payload: Record<string, unknown> = { ...comment, file, event };\n if (event === \"follow_up\" && replies.length > 0) {\n // Include the latest user reply so the agent can see what changed\n const userReplies = replies.filter((r) => r.from === \"user\");\n if (userReplies.length > 0) {\n payload.latestUserReply = userReplies[userReplies.length - 1].text;\n }\n }\n return payload;\n }\n\n server.tool(\n \"wait_for_comment\",\n \"Block until a new or follow-up comment is posted in the live preview, then return it. Returns an 'event' field: 'new_comment' for brand-new comments, 'follow_up' when the user replied to an already-answered comment. For follow-ups, 'latestUserReply' contains the new reply text. Call again immediately after handling each result to keep listening.\",\n {\n timeout: z.number().optional().describe(\"Max seconds to wait (default: 30)\"),\n },\n ({ timeout }, { signal }: { signal?: AbortSignal }) => {\n const maxWait = (timeout ?? 30) * 1000;\n\n // Check for comments that arrived while no one was listening.\n // Only return a pending comment if it's brand-new (never seen) or\n // has new replies since we last returned it.\n const pending = store.getAll({ status: \"pending\" });\n for (const c of pending) {\n const prevCount = lastSeenReplyCounts.get(c.id);\n if (prevCount === undefined) {\n // Brand-new comment we've never returned\n const payload = formatCommentPayload(c, \"new_comment\");\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n };\n }\n if (c.replies.length > prevCount) {\n // Existing comment with new replies (user follow-up)\n const payload = formatCommentPayload(c, \"follow_up\");\n return {\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n };\n }\n }\n\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n const count = store.getAll({ status: \"pending\" }).length;\n resolve({\n content: [{ type: \"text\" as const, text: count > 0\n ? `Timeout, but ${count} pending comment(s) exist. Call get_pending_comments now.`\n : \"No new comments yet. Call wait_for_comment again to keep listening.\" }],\n });\n }, maxWait);\n\n const onNewComment = (comment: { id: string; file: string; selectedText: string; comment: string; replies?: Array<{ from: string; text: string }> }) => {\n cleanup();\n const payload = formatCommentPayload(comment, \"new_comment\");\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n });\n };\n\n const onReopened = (comment: { id: string; file: string; selectedText: string; comment: string; replies?: Array<{ from: string; text: string }> }) => {\n cleanup();\n const payload = formatCommentPayload(comment, \"follow_up\");\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify(payload, null, 2),\n }],\n });\n };\n\n const onAbort = () => {\n cleanup();\n resolve({\n content: [{ type: \"text\" as const, text: \"Cancelled. Call wait_for_comment again to resume listening.\" }],\n });\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n store.off(\"new_comment\", onNewComment);\n store.off(\"comment_reopened\", onReopened);\n signal?.removeEventListener(\"abort\", onAbort);\n };\n\n store.on(\"new_comment\", onNewComment);\n store.on(\"comment_reopened\", onReopened);\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n // If already aborted before we set up\n if (signal?.aborted) {\n onAbort();\n }\n });\n }\n );\n\n // Tool: get_preview_url\n server.tool(\n \"get_preview_url\",\n \"Get the URL of the Cowrite live preview. Share this with the user so they can open it in their browser.\",\n {},\n async () => {\n const port = getPreviewPort?.();\n if (!port) {\n return {\n content: [{ type: \"text\" as const, text: \"Preview server is not running.\" }],\n isError: true,\n };\n }\n return {\n content: [{ type: \"text\" as const, text: `http://localhost:${port}` }],\n };\n }\n );\n\n // Resource: cowrite://comments\n server.resource(\n \"all-comments\",\n \"cowrite://comments\",\n { description: \"Live list of all comments\", mimeType: \"application/json\" },\n async () => {\n const comments = store.getAll();\n return {\n contents: [\n {\n uri: \"cowrite://comments\",\n mimeType: \"application/json\",\n text: JSON.stringify(comments, null, 2),\n },\n ],\n };\n }\n );\n\n // Wire store changes to MCP resource notifications\n store.on(\"change\", () => {\n if (!server.isConnected()) return;\n server.server.notification({\n method: \"notifications/resources/updated\",\n params: { uri: \"cowrite://comments\" },\n }).catch(() => {});\n });\n\n // --- Comment propagation signals ---\n // Only send signals when an MCP client is actually connected.\n // 1. Update tool description with pending count + sendToolListChanged\n // 2. sendLoggingMessage as additional context\n // Primary real-time mechanism is wait_for_comment via the /watch skill.\n function notifyCommentNeedsAttention(comment: { file: string; selectedText: string; comment: string }, prefix: string) {\n if (!server.isConnected()) return;\n\n const count = store.getAll({ status: \"pending\" }).length;\n const file = relative(projectDir, comment.file);\n const selectedPreview = comment.selectedText.length > 80\n ? comment.selectedText.slice(0, 80) + \"...\"\n : comment.selectedText;\n\n // Signal 1: Update tool description + notify tool list changed\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n server.sendToolListChanged();\n } catch {\n // Not connected or transport issue — skip\n }\n\n // Signal 2: Logging message\n const logMsg = comment.selectedText\n ? `${prefix} on ${file}: \"${comment.comment}\" (selected: \"${selectedPreview}\"). Call get_pending_comments to see it.`\n : `${prefix} on ${file}: \"${comment.comment}\". Call get_pending_comments to see it.`;\n server.sendLoggingMessage({\n level: \"warning\",\n data: logMsg,\n }).catch(() => {});\n\n // Signal 3: Resource list changed\n server.server.notification({\n method: \"notifications/resources/list_changed\",\n }).catch(() => {});\n }\n\n store.on(\"new_comment\", (comment: { file: string; selectedText: string; comment: string }) => {\n notifyCommentNeedsAttention(comment, \"NEW COMMENT\");\n });\n\n store.on(\"comment_reopened\", (comment: { file: string; selectedText: string; comment: string }) => {\n notifyCommentNeedsAttention(comment, \"COMMENT REOPENED\");\n });\n\n // Update description count when comments are resolved\n store.on(\"change\", () => {\n const count = store.getAll({ status: \"pending\" }).length;\n try {\n getPendingTool.update({\n description: `Get comments from the live preview (${count} pending). Call this first to catch comments posted before you started listening.`,\n });\n } catch {\n // Ignore — may not be connected yet\n }\n });\n\n // Prompt: cowrite-workflow\n server.prompt(\n \"cowrite-workflow\",\n \"How to process live preview comments in a wait-handle-reply loop\",\n () => ({\n messages: [\n {\n role: \"user\" as const,\n content: {\n type: \"text\" as const,\n text: [\n \"You are monitoring a live code preview where users leave comments on selected text.\",\n \"\",\n \"Comment lifecycle: pending → answered (auto on your reply) → resolved (user only).\",\n \"If the user disagrees with your answer, they reply back and it returns to pending.\",\n \"\",\n \"Follow this loop:\",\n \"1. Call `get_pending_comments` to check for any comments already posted.\",\n \"2. Process each pending comment: read the file, then respond:\",\n \" - If the comment is on selected text, prefer `propose_change` to suggest a diff the user can Apply/Reject.\",\n \" - For file-level comments or questions, use `reply_to_comment` to explain what you did.\",\n \" Your reply automatically marks the comment as 'answered'. The user will review and resolve it.\",\n \"3. Call `wait_for_comment` to block until the next comment (or reopened comment) arrives.\",\n \"4. When a comment arrives, process it the same way (step 2).\",\n \"5. Go back to step 3 and keep listening.\",\n \"\",\n \"Tips:\",\n \"- Use `get_file_with_annotations` to see comments in context within the file.\",\n \"- Use `reply_to_comment` to acknowledge or ask clarifying questions.\",\n \"- Do NOT resolve comments — the user does that after reviewing your work.\",\n ].join(\"\\n\"),\n },\n },\n ],\n })\n );\n\n return server;\n}\n","import { exec } from \"node:child_process\";\nimport { marked, Renderer, type Tokens } from \"marked\";\nimport hljs from \"highlight.js\";\nimport type { Comment } from \"./types.js\";\n\n/**\n * Open a URL in the user's default browser.\n * Returns a promise so callers can await if needed (e.g. before process exit).\n */\nexport function openBrowser(url: string): Promise<void> {\n const cmd = process.platform === \"darwin\" ? `open \"${url}\"`\n : process.platform === \"win32\" ? `cmd /c start \"\" \"${url}\"`\n : `xdg-open \"${url}\"`;\n return new Promise((resolve) => {\n exec(cmd, (err) => {\n if (err) process.stderr.write(`Could not open browser: ${err.message}\\n`);\n resolve();\n });\n });\n}\n\n/**\n * Render file content as HTML. Markdown files get full rendering;\n * other text files are wrapped in <pre> with offset-tagged spans.\n */\nexport function renderToHtml(content: string, filePath: string): string {\n const isMarkdown = /\\.(md|markdown|mdx)$/i.test(filePath);\n if (isMarkdown) {\n return renderMarkdownWithOffsets(content);\n }\n return renderPlainTextWithOffsets(content);\n}\n\nfunction renderMarkdownWithOffsets(content: string): string {\n // Build block offset map from lexer tokens\n const tokens = marked.lexer(content);\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n let blockOffset = 0;\n for (const token of tokens) {\n if (token.type !== \"space\") {\n blocks.push({ sourceStart: blockOffset, sourceEnd: blockOffset + token.raw.length });\n }\n blockOffset += token.raw.length;\n }\n\n const renderer = new Renderer();\n const defaultCodeRenderer = renderer.code.bind(renderer);\n\n renderer.code = function (token: Tokens.Code) {\n if (token.lang === \"mermaid\") {\n return `<div class=\"mermaid-container\"><pre class=\"mermaid\">${token.text}</pre></div>`;\n }\n try {\n let highlighted: string;\n let lang: string;\n if (token.lang && hljs.getLanguage(token.lang)) {\n const result = hljs.highlight(token.text, { language: token.lang });\n highlighted = result.value;\n lang = token.lang;\n } else {\n const result = hljs.highlightAuto(token.text);\n highlighted = result.value;\n lang = result.language || \"plaintext\";\n }\n 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>`;\n } catch {\n return defaultCodeRenderer(token);\n }\n };\n\n const html = marked.parse(content, { async: false, renderer }) as string;\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \""\");\n return `<div class=\"markdown-body\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${html}</div>`;\n}\n\nfunction renderPlainTextWithOffsets(content: string): string {\n const lines = content.split(\"\\n\");\n let offset = 0;\n const blocks: Array<{ sourceStart: number; sourceEnd: number }> = [];\n const htmlParts: string[] = [];\n let currentLines: string[] = [];\n let blockStart = 0;\n let blockIndex = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n const lineOffset = offset;\n offset += line.length + 1; // +1 for the newline\n\n if (line.trim() === \"\") {\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: lineOffset });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n blockIndex++;\n currentLines = [];\n }\n htmlParts.push(\"\");\n blockStart = offset;\n } else {\n if (currentLines.length === 0) {\n blockStart = lineOffset;\n }\n const escaped = escapeHtml(line);\n currentLines.push(`<span class=\"line\" data-offset=\"${lineOffset}\" data-length=\"${line.length}\">${escaped}</span>`);\n }\n }\n\n if (currentLines.length > 0) {\n blocks.push({ sourceStart: blockStart, sourceEnd: Math.min(offset, content.length) });\n htmlParts.push(`<div class=\"text-block\" data-block-index=\"${blockIndex}\">${currentLines.join(\"\\n\")}</div>`);\n }\n\n const blocksAttr = JSON.stringify(blocks).replace(/\"/g, \""\");\n return `<pre class=\"plain-text\" data-source-length=\"${content.length}\" data-blocks=\"${blocksAttr}\">${htmlParts.join(\"\\n\")}</pre>`;\n}\n\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\n/**\n * Annotate file content with inline comment markers for the agent.\n * Inserts `[COMMENT #id: \"text\"]` at the comment offsets.\n */\nexport function annotateFileWithComments(content: string, comments: Comment[]): string {\n // Sort by offset descending so insertions don't shift earlier offsets\n const sorted = [...comments].sort((a, b) => b.offset - a.offset);\n let result = content;\n\n for (const c of sorted) {\n if (!c.selectedText) {\n // File comment — prepend to file\n result = `[FILE COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]\\n` + result;\n continue;\n }\n const marker = `[COMMENT #${c.id.slice(0, 8)}: \"${c.comment}\"]`;\n const end = c.offset + c.length;\n // Insert marker after the selected text\n result = result.slice(0, end) + \" \" + marker + result.slice(end);\n }\n\n return result;\n}\n","import { createServer, type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, resolve, relative } from \"node:path\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport { FileWatcher } from \"./file-watcher.js\";\nimport { renderToHtml } from \"./utils.js\";\nimport type { WSClientMessage, WSServerMessage } from \"./types.js\";\n\n// In dev (tsx): import.meta.dirname is src/, so ../ui works.\n// In built (dist/bin/cowrite.js): import.meta.dirname is dist/bin/, so ../../ui.\n// We find ui/ by checking which path actually contains index.html.\nfunction findUiDir(): string {\n const dir = import.meta.dirname ?? new URL(\".\", import.meta.url).pathname;\n // Try common locations relative to this file\n const candidates = [\n join(dir, \"..\", \"ui\"), // dev: src/../ui\n join(dir, \"..\", \"..\", \"ui\"), // built: dist/bin/../../ui\n ];\n return candidates.find((d) => {\n try { return existsSync(join(d, \"index.html\")); } catch { return false; }\n }) ?? candidates[0];\n}\nconst UI_DIR = findUiDir();\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n};\n\nconst IGNORED_DIRS = new Set([\"node_modules\", \".git\", \"dist\", \".next\", \".cache\", \"coverage\", \"__pycache__\"]);\n\nexport function createPreviewServer(\n store: CommentStore,\n projectDir: string,\n port: number,\n initialFile?: string\n): { port: number; start: () => Promise<void>; stop: () => Promise<void> } {\n const clients = new Set<WebSocket>();\n const clientFiles = new Map<WebSocket, string>(); // ws -> absolute file path\n const watchers = new Map<string, FileWatcher>(); // absolute path -> watcher\n const watcherListeners = new Map<string, (...args: any[]) => void>(); // path -> change listener\n\n const resolvedProjectDir = resolve(projectDir);\n\n function isInsideProject(filePath: string): boolean {\n const resolved = resolve(resolvedProjectDir, filePath);\n return resolved.startsWith(resolvedProjectDir);\n }\n\n async function getOrCreateWatcher(absPath: string): Promise<FileWatcher> {\n let watcher = watchers.get(absPath);\n if (!watcher) {\n watcher = new FileWatcher(absPath);\n await watcher.start();\n watchers.set(absPath, watcher);\n\n // Subscribe to file changes and broadcast to relevant clients\n const listener = (event: { file: string; content: string; oldContent: string }) => {\n store.adjustOffsets(event.file, event.oldContent, event.content);\n const html = renderToHtml(event.content, event.file);\n for (const [ws, file] of clientFiles) {\n if (file === absPath) {\n send(ws, { type: \"file_update\", file: event.file, content: event.content, html });\n }\n }\n };\n watcher.on(\"change\", listener);\n watcherListeners.set(absPath, listener);\n }\n return watcher;\n }\n\n async function listFiles(dir: string, prefix = \"\"): Promise<string[]> {\n const files: string[] = [];\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.name.startsWith(\".\") || IGNORED_DIRS.has(entry.name)) continue;\n const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n const sub = await listFiles(join(dir, entry.name), relPath);\n files.push(...sub);\n } else {\n files.push(relPath);\n }\n }\n } catch {\n // Permission denied or gone — skip\n }\n return files;\n }\n\n const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? \"/\", `http://localhost:${port}`);\n const pathname = url.pathname === \"/\" ? \"/index.html\" : url.pathname;\n\n // Serve static UI files\n const ext = pathname.slice(pathname.lastIndexOf(\".\"));\n const mimeType = MIME_TYPES[ext];\n if (mimeType) {\n try {\n const filePath = join(UI_DIR, pathname);\n const content = await readFile(filePath, \"utf-8\");\n res.writeHead(200, { \"Content-Type\": mimeType });\n res.end(content);\n return;\n } catch {\n // Fall through to 404\n }\n }\n\n // API: GET /api/files — list project files for the file picker\n if (pathname === \"/api/files\") {\n const files = await listFiles(resolvedProjectDir);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ files }));\n return;\n }\n\n // API: GET /api/state?file=... — state for a specific file\n if (pathname === \"/api/state\") {\n const fileParam = url.searchParams.get(\"file\");\n if (!fileParam) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Missing file parameter\" }));\n return;\n }\n const absPath = resolve(resolvedProjectDir, fileParam);\n if (!isInsideProject(absPath)) {\n res.writeHead(403, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Path outside project\" }));\n return;\n }\n try {\n const content = await readFile(absPath, \"utf-8\");\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ file: absPath, content, html, comments }));\n } catch {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"File not found\" }));\n }\n return;\n }\n\n res.writeHead(404, { \"Content-Type\": \"text/plain\" });\n res.end(\"Not found\");\n });\n\n const wss = new WebSocketServer({ server: httpServer });\n\n // Prevent unhandled WSS errors from crashing the process (e.g. EADDRINUSE)\n wss.on(\"error\", () => {});\n\n wss.on(\"connection\", async (ws: WebSocket) => {\n clients.add(ws);\n\n // If there's an initial file (preview mode), auto-assign it\n if (initialFile) {\n const absPath = resolve(resolvedProjectDir, initialFile);\n await switchClientFile(ws, absPath);\n }\n\n ws.on(\"message\", async (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n await handleClientMessage(ws, msg);\n } catch (err) {\n send(ws, { type: \"error\", message: `Invalid message: ${err}` });\n }\n });\n\n ws.on(\"close\", () => {\n clients.delete(ws);\n clientFiles.delete(ws);\n });\n });\n\n async function switchClientFile(ws: WebSocket, absPath: string): Promise<void> {\n if (!isInsideProject(absPath)) {\n send(ws, { type: \"error\", message: \"Path outside project\" });\n return;\n }\n try {\n const watcher = await getOrCreateWatcher(absPath);\n clientFiles.set(ws, absPath);\n const content = watcher.getContent();\n const html = renderToHtml(content, absPath);\n const comments = store.getForFile(absPath);\n send(ws, { type: \"file_update\", file: absPath, content, html });\n send(ws, { type: \"comments_update\", comments });\n } catch (err) {\n send(ws, { type: \"error\", message: `Cannot open file: ${err}` });\n }\n }\n\n async function handleClientMessage(ws: WebSocket, msg: WSClientMessage): Promise<void> {\n switch (msg.type) {\n case \"switch_file\": {\n const absPath = resolve(resolvedProjectDir, msg.file);\n await switchClientFile(ws, absPath);\n break;\n }\n case \"comment_add\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n store.add({\n file,\n offset: msg.offset,\n length: msg.length,\n selectedText: msg.selectedText,\n comment: msg.comment,\n });\n break;\n }\n case \"comment_reply\":\n store.addReply(msg.commentId, \"user\", msg.text);\n break;\n case \"comment_resolve\":\n store.resolve(msg.commentId);\n break;\n case \"comment_reopen\":\n store.reopen(msg.commentId);\n break;\n case \"comment_delete\":\n store.delete(msg.commentId);\n break;\n case \"edit_apply\": {\n const file = clientFiles.get(ws);\n if (!file) break;\n const watcher = watchers.get(file);\n if (!watcher) break;\n const content = watcher.getContent();\n if (msg.offset < 0 || msg.offset + msg.length > content.length) {\n send(ws, { type: \"error\", message: \"Edit offset/length out of bounds\" });\n break;\n }\n const newContent = content.slice(0, msg.offset) + msg.newText + content.slice(msg.offset + msg.length);\n await writeFile(file, newContent, \"utf-8\");\n break;\n }\n case \"proposal_apply\": {\n const comment = store.get(msg.commentId);\n if (!comment) {\n send(ws, { type: \"error\", message: \"Comment not found\" });\n break;\n }\n const reply = comment.replies.find((r) => r.id === msg.replyId);\n if (!reply?.proposal || reply.proposal.status !== \"pending\") {\n send(ws, { type: \"error\", message: \"Proposal not found or not pending\" });\n break;\n }\n const pFile = comment.file;\n const pWatcher = watchers.get(pFile);\n if (!pWatcher) {\n send(ws, { type: \"error\", message: \"File not being watched\" });\n break;\n }\n const pContent = pWatcher.getContent();\n const pSlice = pContent.slice(comment.offset, comment.offset + comment.length);\n if (pSlice !== comment.selectedText) {\n send(ws, { type: \"error\", message: \"File content has changed — selected text no longer matches at this position\" });\n break;\n }\n const pNewContent = pContent.slice(0, comment.offset) + reply.proposal.newText + pContent.slice(comment.offset + comment.length);\n await writeFile(pFile, pNewContent, \"utf-8\");\n store.updateProposalStatus(msg.commentId, msg.replyId, \"applied\");\n break;\n }\n case \"proposal_reject\": {\n store.updateProposalStatus(msg.commentId, msg.replyId, \"rejected\");\n break;\n }\n }\n }\n\n // Broadcast comment updates to clients viewing the affected file\n store.on(\"change\", (comment: any) => {\n for (const [ws, file] of clientFiles) {\n // If we know which file changed, only notify relevant clients\n // If comment is null (e.g. adjustOffsets, reload), notify all\n if (!comment || comment.file === file) {\n const comments = store.getForFile(file);\n send(ws, { type: \"comments_update\", comments });\n }\n }\n });\n\n function send(ws: WebSocket, msg: WSServerMessage): void {\n if (ws.readyState === ws.OPEN) {\n ws.send(JSON.stringify(msg));\n }\n }\n\n let actualPort = port;\n\n return {\n get port() { return actualPort; },\n start: () => {\n const maxRetries = 10;\n const tryListen = (p: number, attempt: number): Promise<void> =>\n new Promise<void>((res, rej) => {\n const onError = (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" && attempt < maxRetries) {\n httpServer.removeListener(\"error\", onError);\n res(tryListen(p + 1, attempt + 1));\n } else {\n rej(err);\n }\n };\n httpServer.on(\"error\", onError);\n httpServer.listen(p, () => {\n httpServer.removeListener(\"error\", onError);\n actualPort = p;\n process.stderr.write(`Cowrite preview server running at http://localhost:${p}\\n`);\n res();\n });\n });\n return tryListen(port, 0);\n },\n stop: async () => {\n for (const client of clients) {\n client.close();\n }\n for (const [path, watcher] of watchers) {\n const listener = watcherListeners.get(path);\n if (listener) watcher.off(\"change\", listener);\n await watcher.stop();\n }\n watchers.clear();\n watcherListeners.clear();\n await new Promise<void>((resolvePromise, reject) => {\n httpServer.close((err) => {\n if (err) reject(err);\n else resolvePromise();\n });\n });\n },\n };\n}\n","import { watch, type FSWatcher } from \"chokidar\";\nimport { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { EventEmitter } from \"node:events\";\n\nexport interface FileChangeEvent {\n file: string;\n content: string;\n}\n\nexport class FileWatcher extends EventEmitter {\n private watcher: FSWatcher | null = null;\n private filePath: string;\n private lastContent: string = \"\";\n\n constructor(filePath: string) {\n super();\n this.filePath = resolve(filePath);\n }\n\n async start(): Promise<string> {\n this.lastContent = await readFile(this.filePath, \"utf-8\");\n\n this.watcher = watch(this.filePath, {\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },\n });\n\n this.watcher.on(\"change\", async () => {\n try {\n const newContent = await readFile(this.filePath, \"utf-8\");\n if (newContent !== this.lastContent) {\n const oldContent = this.lastContent;\n this.lastContent = newContent;\n this.emit(\"change\", {\n file: this.filePath,\n content: newContent,\n oldContent,\n } as FileChangeEvent & { oldContent: string });\n }\n } catch (err) {\n process.stderr.write(`File watch read error: ${err}\\n`);\n }\n });\n\n return this.lastContent;\n }\n\n getContent(): string {\n return this.lastContent;\n }\n\n getFilePath(): string {\n return this.filePath;\n }\n\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n }\n}\n"],"mappings":"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","names":["resolve","join","existsSync","resolve","readFile","resolve","readFile","writeFile","join","resolve","readFile","resolve","EventEmitter","join","resolve","readFile","writeFile","join","join","existsSync","resolve"]}
|
package/package.json
CHANGED
package/ui/client.js
CHANGED
|
@@ -14,9 +14,6 @@ const popupSelection = $("#popupSelection");
|
|
|
14
14
|
const commentInput = $("#commentInput");
|
|
15
15
|
const selectionToolbar = $("#selectionToolbar");
|
|
16
16
|
const commentTrigger = $("#commentTrigger");
|
|
17
|
-
const highlightPopover = $("#highlightPopover");
|
|
18
|
-
const highlightPopoverText = $("#highlightPopoverText");
|
|
19
|
-
const highlightEditBtn = $("#highlightEditBtn");
|
|
20
17
|
const undoBtn = $("#undoBtn");
|
|
21
18
|
const filePicker = $("#filePicker");
|
|
22
19
|
const fileList = $("#fileList");
|
|
@@ -34,7 +31,6 @@ let currentBlocks = [];
|
|
|
34
31
|
let insertBtn = null;
|
|
35
32
|
let insertLine = null;
|
|
36
33
|
let activeGapIndex = -1;
|
|
37
|
-
let activeHighlightCommentId = null;
|
|
38
34
|
let undoStack = [];
|
|
39
35
|
const MAX_UNDO = 50;
|
|
40
36
|
|
|
@@ -47,6 +43,43 @@ let pendingFileUpdate = null;
|
|
|
47
43
|
let pendingEditAfterInsert = -1;
|
|
48
44
|
let contentEditableActive = false;
|
|
49
45
|
|
|
46
|
+
// --- Resizable Sidebar ---
|
|
47
|
+
(function initResizableSidebar() {
|
|
48
|
+
const handle = document.getElementById("sidebarDragHandle");
|
|
49
|
+
const sidebar = document.getElementById("sidebar");
|
|
50
|
+
if (!handle || !sidebar) return;
|
|
51
|
+
|
|
52
|
+
// Restore saved width
|
|
53
|
+
const saved = localStorage.getItem("cowrite-sidebar-width");
|
|
54
|
+
if (saved) document.documentElement.style.setProperty("--sidebar-width", saved + "px");
|
|
55
|
+
|
|
56
|
+
let startX = 0;
|
|
57
|
+
let startWidth = 0;
|
|
58
|
+
|
|
59
|
+
handle.addEventListener("mousedown", (e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
startX = e.clientX;
|
|
62
|
+
startWidth = sidebar.offsetWidth;
|
|
63
|
+
document.body.classList.add("sidebar-resizing");
|
|
64
|
+
document.addEventListener("mousemove", onMouseMove);
|
|
65
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function onMouseMove(e) {
|
|
69
|
+
const delta = startX - e.clientX; // sidebar is on the right
|
|
70
|
+
const newWidth = Math.min(Math.max(startWidth + delta, 300), window.innerWidth * 0.5);
|
|
71
|
+
document.documentElement.style.setProperty("--sidebar-width", newWidth + "px");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onMouseUp() {
|
|
75
|
+
document.body.classList.remove("sidebar-resizing");
|
|
76
|
+
document.removeEventListener("mousemove", onMouseMove);
|
|
77
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
78
|
+
const width = sidebar.offsetWidth;
|
|
79
|
+
localStorage.setItem("cowrite-sidebar-width", String(width));
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
|
|
50
83
|
const BLOCK_TYPES = [
|
|
51
84
|
{ id: "text", label: "Text", category: "Basic blocks", icon: "Aa", template: "\u200B" },
|
|
52
85
|
{ id: "h1", label: "Heading 1", category: "Basic blocks", icon: "H1", template: "# " },
|
|
@@ -91,15 +124,36 @@ function switchFile(file) {
|
|
|
91
124
|
history.replaceState(null, "", url.toString());
|
|
92
125
|
}
|
|
93
126
|
|
|
127
|
+
// Track meta key for Cmd+Click to open in new tab
|
|
128
|
+
let lastClickHadMeta = false;
|
|
129
|
+
document.addEventListener("mousedown", (e) => { lastClickHadMeta = e.metaKey || e.ctrlKey; });
|
|
130
|
+
|
|
131
|
+
function openFileInNewTab(file) {
|
|
132
|
+
const url = new URL(location.href);
|
|
133
|
+
url.searchParams.set("file", file);
|
|
134
|
+
window.open(url.toString(), "_blank");
|
|
135
|
+
}
|
|
136
|
+
|
|
94
137
|
filePicker.addEventListener("change", () => {
|
|
95
138
|
const file = filePicker.value.trim();
|
|
96
|
-
if (file)
|
|
139
|
+
if (!file) return;
|
|
140
|
+
if (lastClickHadMeta) {
|
|
141
|
+
openFileInNewTab(file);
|
|
142
|
+
filePicker.value = "";
|
|
143
|
+
} else {
|
|
144
|
+
switchFile(file);
|
|
145
|
+
}
|
|
97
146
|
});
|
|
98
147
|
|
|
99
148
|
filePicker.addEventListener("keydown", (e) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
149
|
+
const file = filePicker.value.trim();
|
|
150
|
+
if (!file) return;
|
|
151
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
openFileInNewTab(file);
|
|
154
|
+
filePicker.value = "";
|
|
155
|
+
} else if (e.key === "Enter") {
|
|
156
|
+
switchFile(file);
|
|
103
157
|
}
|
|
104
158
|
});
|
|
105
159
|
|
|
@@ -526,48 +580,99 @@ function renderComments() {
|
|
|
526
580
|
return;
|
|
527
581
|
}
|
|
528
582
|
|
|
529
|
-
commentListEl.innerHTML = comments.map((c) =>
|
|
530
|
-
|
|
531
|
-
<
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
583
|
+
commentListEl.innerHTML = comments.map((c) => {
|
|
584
|
+
const repliesHtml = c.replies.length > 0 ? `
|
|
585
|
+
<div class="comment-replies">
|
|
586
|
+
${c.replies.map((r) => r.proposal ? `
|
|
587
|
+
<div class="reply agent proposal-reply proposal-${r.proposal.status}">
|
|
588
|
+
<div class="reply-from agent">agent — proposal</div>
|
|
589
|
+
<div class="proposal-explanation">${escapeHtml(r.proposal.explanation)}</div>
|
|
590
|
+
${r.proposal.status === "pending" ? `
|
|
591
|
+
<div class="proposal-diff">
|
|
592
|
+
<div class="proposal-old"><span class="proposal-label">Current</span><pre>${escapeHtml(r.proposal.oldText)}</pre></div>
|
|
593
|
+
<div class="proposal-new"><span class="proposal-label">Proposed</span><pre>${escapeHtml(r.proposal.newText)}</pre></div>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="proposal-actions">
|
|
596
|
+
<button class="proposal-apply-btn" onclick="applyProposal('${c.id}', '${r.id}')">Apply</button>
|
|
597
|
+
<button class="proposal-reject-btn" onclick="rejectProposal('${c.id}', '${r.id}')">Reject</button>
|
|
598
|
+
</div>
|
|
599
|
+
` : r.proposal.status === "applied" ? `
|
|
600
|
+
<div class="proposal-diff">
|
|
601
|
+
<div class="proposal-new"><span class="proposal-label">✓ Applied</span><pre>${escapeHtml(r.proposal.newText)}</pre></div>
|
|
602
|
+
</div>
|
|
603
|
+
` : `
|
|
604
|
+
<div class="proposal-diff">
|
|
605
|
+
<div class="proposal-old"><span class="proposal-label">✗ Rejected</span><pre>${escapeHtml(r.proposal.oldText)}</pre></div>
|
|
606
|
+
</div>
|
|
607
|
+
`}
|
|
608
|
+
</div>
|
|
609
|
+
` : `
|
|
610
|
+
<div class="reply ${r.from}">
|
|
611
|
+
<div class="reply-from ${r.from}">${r.from}</div>
|
|
612
|
+
<div>${escapeHtml(r.text)}</div>
|
|
613
|
+
</div>
|
|
614
|
+
`).join("")}
|
|
615
|
+
</div>
|
|
616
|
+
` : "";
|
|
617
|
+
|
|
618
|
+
if (c.status === "resolved") {
|
|
619
|
+
const truncated = c.comment.length > 60 ? c.comment.slice(0, 60) + "..." : c.comment;
|
|
620
|
+
return `
|
|
621
|
+
<div class="comment-card resolved" data-id="${c.id}">
|
|
622
|
+
<button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
|
|
623
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
624
|
+
</button>
|
|
625
|
+
<div class="resolved-summary" onclick="toggleResolvedExpand('${c.id}')">
|
|
626
|
+
<svg class="resolved-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
|
|
627
|
+
<span class="comment-status resolved">resolved</span>
|
|
628
|
+
<span class="resolved-summary-text">${escapeHtml(truncated)}</span>
|
|
629
|
+
</div>
|
|
630
|
+
<div class="resolved-details" hidden>
|
|
631
|
+
${c.selectedText
|
|
632
|
+
? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
|
|
633
|
+
: `<div class="comment-file-badge">Whole file</div>`
|
|
634
|
+
}
|
|
635
|
+
<div class="comment-text">${escapeHtml(c.comment)}</div>
|
|
636
|
+
${repliesHtml}
|
|
637
|
+
<div class="comment-meta">
|
|
638
|
+
<span>${timeAgo(c.createdAt)}</span>
|
|
545
639
|
</div>
|
|
546
|
-
|
|
640
|
+
<div class="comment-actions">
|
|
641
|
+
<button onclick="reopenComment('${c.id}')">Reopen</button>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
</div>
|
|
645
|
+
`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return `
|
|
649
|
+
<div class="comment-card ${c.status}" data-id="${c.id}">
|
|
650
|
+
<button class="comment-delete-btn" onclick="deleteComment('${c.id}')" title="Delete comment">
|
|
651
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
652
|
+
</button>
|
|
653
|
+
${c.selectedText
|
|
654
|
+
? `<div class="comment-selected-text">${escapeHtml(c.selectedText)}</div>`
|
|
655
|
+
: `<div class="comment-file-badge">Whole file</div>`
|
|
656
|
+
}
|
|
657
|
+
<div class="comment-text">${escapeHtml(c.comment)}</div>
|
|
658
|
+
${repliesHtml}
|
|
659
|
+
<div class="comment-meta">
|
|
660
|
+
<span>${timeAgo(c.createdAt)}</span>
|
|
661
|
+
<span class="comment-status ${c.status}">${c.status}</span>
|
|
547
662
|
</div>
|
|
548
|
-
` : ""}
|
|
549
|
-
<div class="comment-meta">
|
|
550
|
-
<span>${timeAgo(c.createdAt)}</span>
|
|
551
|
-
<span class="comment-status ${c.status}">${c.status}</span>
|
|
552
|
-
</div>
|
|
553
|
-
${c.status !== "resolved" ? `
|
|
554
663
|
<div class="comment-actions">
|
|
555
664
|
<button onclick="showReplyForm('${c.id}')">Reply</button>
|
|
556
665
|
<button onclick="resolveComment('${c.id}')">Resolve</button>
|
|
557
666
|
</div>
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
<div class="reply-form" id="reply-form-${c.id}" hidden>
|
|
564
|
-
<textarea rows="2" placeholder="Reply..."></textarea>
|
|
565
|
-
<div class="reply-form-actions">
|
|
566
|
-
<button onclick="submitReply('${c.id}')">Send</button>
|
|
667
|
+
<div class="reply-form" id="reply-form-${c.id}" hidden>
|
|
668
|
+
<textarea rows="2" placeholder="Reply..."></textarea>
|
|
669
|
+
<div class="reply-form-actions">
|
|
670
|
+
<button onclick="submitReply('${c.id}')">Send</button>
|
|
671
|
+
</div>
|
|
567
672
|
</div>
|
|
568
673
|
</div>
|
|
569
|
-
|
|
570
|
-
|
|
674
|
+
`;
|
|
675
|
+
}).join("");
|
|
571
676
|
|
|
572
677
|
// Click to scroll to highlight
|
|
573
678
|
for (const card of commentListEl.querySelectorAll(".comment-card")) {
|
|
@@ -615,6 +720,25 @@ window.submitReply = function (id) {
|
|
|
615
720
|
form.hidden = true;
|
|
616
721
|
};
|
|
617
722
|
|
|
723
|
+
window.applyProposal = function (commentId, replyId) {
|
|
724
|
+
send({ type: "proposal_apply", commentId, replyId });
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
window.rejectProposal = function (commentId, replyId) {
|
|
728
|
+
send({ type: "proposal_reject", commentId, replyId });
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
window.toggleResolvedExpand = function (id) {
|
|
732
|
+
const card = commentListEl.querySelector(`.comment-card[data-id="${id}"]`);
|
|
733
|
+
if (!card) return;
|
|
734
|
+
const details = card.querySelector(".resolved-details");
|
|
735
|
+
const chevron = card.querySelector(".resolved-chevron");
|
|
736
|
+
if (!details) return;
|
|
737
|
+
const expanding = details.hidden;
|
|
738
|
+
details.hidden = !expanding;
|
|
739
|
+
card.classList.toggle("resolved-expanded", expanding);
|
|
740
|
+
};
|
|
741
|
+
|
|
618
742
|
// --- Highlights ---
|
|
619
743
|
|
|
620
744
|
function applyHighlights() {
|
|
@@ -743,6 +867,24 @@ themeToggle.addEventListener("change", () => {
|
|
|
743
867
|
applyTheme(theme);
|
|
744
868
|
});
|
|
745
869
|
|
|
870
|
+
// --- Font size toggle ---
|
|
871
|
+
const FONT_SIZE_KEY = "cowrite-font-size";
|
|
872
|
+
(function initFontSize() {
|
|
873
|
+
const saved = localStorage.getItem(FONT_SIZE_KEY) || "large";
|
|
874
|
+
if (saved === "large") document.body.classList.add("font-large");
|
|
875
|
+
for (const btn of document.querySelectorAll(".font-size-btn")) {
|
|
876
|
+
btn.setAttribute("aria-pressed", btn.dataset.size === saved ? "true" : "false");
|
|
877
|
+
btn.addEventListener("click", () => {
|
|
878
|
+
const size = btn.dataset.size;
|
|
879
|
+
document.body.classList.toggle("font-large", size === "large");
|
|
880
|
+
localStorage.setItem(FONT_SIZE_KEY, size);
|
|
881
|
+
for (const b of document.querySelectorAll(".font-size-btn")) {
|
|
882
|
+
b.setAttribute("aria-pressed", b.dataset.size === size ? "true" : "false");
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
})();
|
|
887
|
+
|
|
746
888
|
// Hide trigger when selection is cleared
|
|
747
889
|
document.addEventListener("selectionchange", () => {
|
|
748
890
|
const selection = window.getSelection();
|
|
@@ -1490,15 +1632,23 @@ fileContentEl.addEventListener("click", (e) => {
|
|
|
1490
1632
|
const target = e.target;
|
|
1491
1633
|
if (target.closest("a, .mermaid-container, .block-insert-btn, .block-type-menu, .inline-editor, .block-edit-wrapper, .block-editing, .code-block-header")) return;
|
|
1492
1634
|
|
|
1493
|
-
// Handle comment highlight clicks
|
|
1635
|
+
// Handle comment highlight clicks — scroll to sidebar card (skip resolved, let them edit)
|
|
1494
1636
|
const highlightEl = target.closest(".comment-highlight");
|
|
1495
1637
|
if (highlightEl) {
|
|
1496
1638
|
const commentId = highlightEl.dataset.commentId;
|
|
1497
1639
|
const comment = comments.find(c => c.id === commentId);
|
|
1498
|
-
if (comment) {
|
|
1499
|
-
|
|
1640
|
+
if (comment && comment.status !== "resolved") {
|
|
1641
|
+
for (const card of commentListEl.querySelectorAll(".comment-card")) {
|
|
1642
|
+
card.classList.remove("active");
|
|
1643
|
+
}
|
|
1644
|
+
const card = commentListEl.querySelector(`.comment-card[data-id="${comment.id}"]`);
|
|
1645
|
+
if (card) {
|
|
1646
|
+
card.classList.add("active");
|
|
1647
|
+
card.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1648
|
+
}
|
|
1649
|
+
return;
|
|
1500
1650
|
}
|
|
1501
|
-
|
|
1651
|
+
// Resolved highlights fall through to block editing
|
|
1502
1652
|
}
|
|
1503
1653
|
|
|
1504
1654
|
if (!currentBlocks.length) return;
|
|
@@ -1523,55 +1673,12 @@ fileContentEl.addEventListener("click", (e) => {
|
|
|
1523
1673
|
});
|
|
1524
1674
|
});
|
|
1525
1675
|
|
|
1526
|
-
// --- Highlight
|
|
1527
|
-
|
|
1528
|
-
function showHighlightPopover(el, comment) {
|
|
1529
|
-
activeHighlightCommentId = comment.id;
|
|
1530
|
-
highlightPopoverText.textContent = comment.comment;
|
|
1531
|
-
|
|
1532
|
-
const rect = el.getBoundingClientRect();
|
|
1533
|
-
highlightPopover.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`;
|
|
1534
|
-
highlightPopover.style.top = `${rect.bottom + 8}px`;
|
|
1535
|
-
highlightPopover.hidden = false;
|
|
1536
|
-
|
|
1537
|
-
// Highlight and scroll to the corresponding comment card in the sidebar
|
|
1538
|
-
for (const card of commentListEl.querySelectorAll(".comment-card")) {
|
|
1539
|
-
card.classList.remove("active");
|
|
1540
|
-
}
|
|
1541
|
-
const card = commentListEl.querySelector(`.comment-card[data-id="${comment.id}"]`);
|
|
1542
|
-
if (card) {
|
|
1543
|
-
card.classList.add("active");
|
|
1544
|
-
card.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
function hideHighlightPopover() {
|
|
1549
|
-
highlightPopover.hidden = true;
|
|
1550
|
-
activeHighlightCommentId = null;
|
|
1551
|
-
for (const card of commentListEl.querySelectorAll(".comment-card.active")) {
|
|
1552
|
-
card.classList.remove("active");
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
highlightEditBtn.addEventListener("click", () => {
|
|
1557
|
-
if (!activeHighlightCommentId) return;
|
|
1558
|
-
const comment = comments.find(c => c.id === activeHighlightCommentId);
|
|
1559
|
-
if (!comment) { hideHighlightPopover(); return; }
|
|
1560
|
-
|
|
1561
|
-
for (let i = 0; i < currentBlocks.length; i++) {
|
|
1562
|
-
const b = currentBlocks[i];
|
|
1563
|
-
if (comment.offset >= b.sourceStart && comment.offset < b.sourceEnd) {
|
|
1564
|
-
hideHighlightPopover();
|
|
1565
|
-
enterBlockEditDispatch(i);
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
hideHighlightPopover();
|
|
1570
|
-
});
|
|
1571
|
-
|
|
1676
|
+
// --- Highlight click: clear active on outside click ---
|
|
1572
1677
|
document.addEventListener("mousedown", (e) => {
|
|
1573
|
-
if (!
|
|
1574
|
-
|
|
1678
|
+
if (!e.target.closest(".comment-highlight")) {
|
|
1679
|
+
for (const card of commentListEl.querySelectorAll(".comment-card.active")) {
|
|
1680
|
+
card.classList.remove("active");
|
|
1681
|
+
}
|
|
1575
1682
|
}
|
|
1576
1683
|
});
|
|
1577
1684
|
|
package/ui/index.html
CHANGED
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
<span class="file-path" id="filePath"></span>
|
|
27
27
|
</div>
|
|
28
28
|
<div class="header-right">
|
|
29
|
+
<div class="font-size-toggle" id="fontSizeToggle">
|
|
30
|
+
<button class="font-size-btn" data-size="regular" title="Regular size"><span>A</span></button>
|
|
31
|
+
<button class="font-size-btn" data-size="large" title="Large size" aria-pressed="true"><span>A</span></button>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="header-divider"></div>
|
|
29
34
|
<button class="undo-btn" id="undoBtn" disabled title="Undo (Cmd+Z)">
|
|
30
35
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
|
|
31
36
|
</button>
|
|
@@ -49,6 +54,7 @@
|
|
|
49
54
|
<div id="fileContent"></div>
|
|
50
55
|
</div>
|
|
51
56
|
|
|
57
|
+
<div class="sidebar-drag-handle" id="sidebarDragHandle"></div>
|
|
52
58
|
<div class="sidebar" id="sidebar">
|
|
53
59
|
<div class="sidebar-header">
|
|
54
60
|
<h2>Comments <span class="comment-count" id="commentCount">0</span></h2>
|
|
@@ -84,13 +90,6 @@
|
|
|
84
90
|
</div>
|
|
85
91
|
</div>
|
|
86
92
|
|
|
87
|
-
<!-- Highlight popover (appears when clicking a comment highlight) -->
|
|
88
|
-
<div class="highlight-popover" id="highlightPopover" hidden>
|
|
89
|
-
<div class="highlight-popover-text" id="highlightPopoverText"></div>
|
|
90
|
-
<div class="highlight-popover-actions">
|
|
91
|
-
<button id="highlightEditBtn">Edit</button>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
93
|
|
|
95
94
|
<script type="module">
|
|
96
95
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
package/ui/styles.css
CHANGED
|
@@ -185,6 +185,53 @@ header h1 {
|
|
|
185
185
|
letter-spacing: 0.2px;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/* ---- Font size toggle ---- */
|
|
189
|
+
.font-size-toggle {
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
gap: 2px;
|
|
193
|
+
background: var(--bg);
|
|
194
|
+
border: 1px solid var(--border);
|
|
195
|
+
border-radius: var(--radius-sm);
|
|
196
|
+
padding: 2px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.font-size-btn {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
justify-content: center;
|
|
203
|
+
border: none;
|
|
204
|
+
background: transparent;
|
|
205
|
+
color: var(--text-faint);
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
border-radius: 6px;
|
|
208
|
+
padding: 3px 8px;
|
|
209
|
+
font-family: var(--font-sans);
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
transition: all 0.15s ease;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.font-size-btn[data-size="regular"] span { font-size: 11px; }
|
|
215
|
+
.font-size-btn[data-size="large"] span { font-size: 15px; }
|
|
216
|
+
|
|
217
|
+
.font-size-btn:hover {
|
|
218
|
+
color: var(--text-dim);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.font-size-btn[aria-pressed="true"] {
|
|
222
|
+
background: var(--surface-hover);
|
|
223
|
+
color: var(--accent);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Large font size mode */
|
|
227
|
+
body.font-large #fileContent .markdown-body {
|
|
228
|
+
font-size: 19px !important;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
body.font-large #fileContent .plain-text {
|
|
232
|
+
font-size: 15px !important;
|
|
233
|
+
}
|
|
234
|
+
|
|
188
235
|
/* ---- Undo button ---- */
|
|
189
236
|
.undo-btn {
|
|
190
237
|
display: flex;
|
|
@@ -443,6 +490,7 @@ main {
|
|
|
443
490
|
.comment-highlight.resolved {
|
|
444
491
|
background: transparent;
|
|
445
492
|
border-bottom: none;
|
|
493
|
+
cursor: text;
|
|
446
494
|
}
|
|
447
495
|
|
|
448
496
|
[data-theme="light"] .comment-highlight:hover {
|
|
@@ -453,10 +501,31 @@ main {
|
|
|
453
501
|
background: rgba(58, 114, 160, 0.12);
|
|
454
502
|
}
|
|
455
503
|
|
|
504
|
+
/* ---- Sidebar drag handle ---- */
|
|
505
|
+
.sidebar-drag-handle {
|
|
506
|
+
width: 5px;
|
|
507
|
+
cursor: col-resize;
|
|
508
|
+
background: transparent;
|
|
509
|
+
flex-shrink: 0;
|
|
510
|
+
transition: background 0.15s ease;
|
|
511
|
+
z-index: 10;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.sidebar-drag-handle:hover,
|
|
515
|
+
body.sidebar-resizing .sidebar-drag-handle {
|
|
516
|
+
background: var(--accent);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
body.sidebar-resizing {
|
|
520
|
+
cursor: col-resize;
|
|
521
|
+
user-select: none;
|
|
522
|
+
}
|
|
523
|
+
|
|
456
524
|
/* ---- Sidebar ---- */
|
|
457
525
|
.sidebar {
|
|
458
|
-
width: 360px;
|
|
459
|
-
min-width:
|
|
526
|
+
width: var(--sidebar-width, 360px);
|
|
527
|
+
min-width: 300px;
|
|
528
|
+
max-width: 50vw;
|
|
460
529
|
background: var(--surface);
|
|
461
530
|
border-left: 1px solid var(--border);
|
|
462
531
|
overflow-y: auto;
|
|
@@ -596,12 +665,58 @@ main {
|
|
|
596
665
|
}
|
|
597
666
|
|
|
598
667
|
.comment-card.resolved {
|
|
599
|
-
opacity: 0.5;
|
|
600
668
|
border-left: 3px solid var(--green);
|
|
669
|
+
padding: 10px 14px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.comment-card.resolved .comment-text,
|
|
673
|
+
.comment-card.resolved .comment-selected-text,
|
|
674
|
+
.comment-card.resolved .comment-meta {
|
|
675
|
+
color: var(--text-faint);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.comment-card.resolved .comment-selected-text {
|
|
679
|
+
border-left-color: var(--green);
|
|
680
|
+
background: var(--green-bg);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/* Resolved collapsed summary */
|
|
684
|
+
.resolved-summary {
|
|
685
|
+
display: flex;
|
|
686
|
+
align-items: center;
|
|
687
|
+
gap: 8px;
|
|
688
|
+
cursor: pointer;
|
|
689
|
+
user-select: none;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.resolved-summary:hover .resolved-summary-text {
|
|
693
|
+
color: var(--text-dim);
|
|
601
694
|
}
|
|
602
695
|
|
|
603
|
-
.
|
|
604
|
-
|
|
696
|
+
.resolved-chevron {
|
|
697
|
+
flex-shrink: 0;
|
|
698
|
+
color: var(--text-faint);
|
|
699
|
+
transition: transform 0.15s ease;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.comment-card.resolved-expanded .resolved-chevron {
|
|
703
|
+
transform: rotate(90deg);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.resolved-summary-text {
|
|
707
|
+
font-size: 12px;
|
|
708
|
+
color: var(--text-faint);
|
|
709
|
+
white-space: nowrap;
|
|
710
|
+
overflow: hidden;
|
|
711
|
+
text-overflow: ellipsis;
|
|
712
|
+
min-width: 0;
|
|
713
|
+
transition: color 0.15s ease;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.resolved-details {
|
|
717
|
+
margin-top: 12px;
|
|
718
|
+
padding-top: 12px;
|
|
719
|
+
border-top: 1px solid var(--border);
|
|
605
720
|
}
|
|
606
721
|
|
|
607
722
|
@keyframes card-enter {
|
|
@@ -1269,6 +1384,130 @@ main {
|
|
|
1269
1384
|
padding-right: 2px;
|
|
1270
1385
|
}
|
|
1271
1386
|
|
|
1387
|
+
/* ---- Proposal diff ---- */
|
|
1388
|
+
.proposal-explanation {
|
|
1389
|
+
font-size: 12px;
|
|
1390
|
+
line-height: 1.5;
|
|
1391
|
+
margin-bottom: 8px;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.proposal-diff {
|
|
1395
|
+
border: 1px solid var(--border);
|
|
1396
|
+
border-radius: var(--radius-sm);
|
|
1397
|
+
overflow: hidden;
|
|
1398
|
+
margin-bottom: 8px;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
.proposal-diff pre {
|
|
1402
|
+
margin: 0;
|
|
1403
|
+
padding: 8px 10px;
|
|
1404
|
+
font-family: var(--font-mono);
|
|
1405
|
+
font-size: 11px;
|
|
1406
|
+
line-height: 1.5;
|
|
1407
|
+
white-space: pre-wrap;
|
|
1408
|
+
word-wrap: break-word;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.proposal-label {
|
|
1412
|
+
display: block;
|
|
1413
|
+
font-family: var(--font-sans);
|
|
1414
|
+
font-size: 10px;
|
|
1415
|
+
font-weight: 600;
|
|
1416
|
+
text-transform: uppercase;
|
|
1417
|
+
letter-spacing: 0.5px;
|
|
1418
|
+
padding: 4px 10px 0;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.proposal-old {
|
|
1422
|
+
background: rgba(212, 97, 110, 0.08);
|
|
1423
|
+
border-bottom: 1px solid var(--border);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
.proposal-old .proposal-label { color: var(--red); }
|
|
1427
|
+
|
|
1428
|
+
.proposal-new {
|
|
1429
|
+
background: rgba(111, 191, 138, 0.08);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
.proposal-new .proposal-label { color: var(--green); }
|
|
1433
|
+
|
|
1434
|
+
.proposal-actions {
|
|
1435
|
+
display: flex;
|
|
1436
|
+
gap: 6px;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.proposal-apply-btn {
|
|
1440
|
+
font-family: var(--font-sans);
|
|
1441
|
+
font-size: 11px;
|
|
1442
|
+
font-weight: 600;
|
|
1443
|
+
padding: 5px 14px;
|
|
1444
|
+
border-radius: var(--radius-sm);
|
|
1445
|
+
border: none;
|
|
1446
|
+
background: var(--green);
|
|
1447
|
+
color: var(--bg);
|
|
1448
|
+
cursor: pointer;
|
|
1449
|
+
transition: opacity 0.15s ease;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.proposal-apply-btn:hover { opacity: 0.85; }
|
|
1453
|
+
|
|
1454
|
+
.proposal-reject-btn {
|
|
1455
|
+
font-family: var(--font-sans);
|
|
1456
|
+
font-size: 11px;
|
|
1457
|
+
font-weight: 500;
|
|
1458
|
+
padding: 5px 14px;
|
|
1459
|
+
border-radius: var(--radius-sm);
|
|
1460
|
+
border: 1px solid var(--border);
|
|
1461
|
+
background: var(--surface);
|
|
1462
|
+
color: var(--text-dim);
|
|
1463
|
+
cursor: pointer;
|
|
1464
|
+
transition: all 0.15s ease;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
.proposal-reject-btn:hover {
|
|
1468
|
+
color: var(--red);
|
|
1469
|
+
border-color: var(--red);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.proposal-status-badge {
|
|
1473
|
+
display: inline-block;
|
|
1474
|
+
font-size: 10px;
|
|
1475
|
+
font-weight: 600;
|
|
1476
|
+
padding: 3px 10px;
|
|
1477
|
+
border-radius: 100px;
|
|
1478
|
+
text-transform: uppercase;
|
|
1479
|
+
letter-spacing: 0.4px;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.proposal-status-badge.applied {
|
|
1483
|
+
color: var(--green);
|
|
1484
|
+
background: var(--green-bg);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
.proposal-status-badge.rejected {
|
|
1488
|
+
color: var(--red);
|
|
1489
|
+
background: rgba(212, 97, 110, 0.08);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.proposal-reply.proposal-applied .proposal-diff {
|
|
1493
|
+
border-left: 3px solid var(--green);
|
|
1494
|
+
}
|
|
1272
1495
|
|
|
1496
|
+
.proposal-reply.proposal-rejected .proposal-diff {
|
|
1497
|
+
border-left: 3px solid var(--text-faint);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
.proposal-reply.proposal-rejected .proposal-diff pre {
|
|
1501
|
+
text-decoration: line-through;
|
|
1502
|
+
color: var(--text-faint);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
.proposal-reply.proposal-rejected .proposal-label {
|
|
1506
|
+
color: var(--text-faint);
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.proposal-reply.proposal-rejected .proposal-old {
|
|
1510
|
+
background: var(--surface-hover);
|
|
1511
|
+
}
|
|
1273
1512
|
|
|
1274
1513
|
|