@filipc77/cowrite 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cowrite.js +53 -5
- package/dist/bin/cowrite.js.map +1 -1
- package/package.json +1 -1
- package/ui/client.js +25 -2
- package/ui/index.html +6 -0
- package/ui/styles.css +283 -110
package/dist/bin/cowrite.js
CHANGED
|
@@ -40,10 +40,16 @@ var CommentStore = class extends EventEmitter {
|
|
|
40
40
|
try {
|
|
41
41
|
const data = await readFile(this.persistPath, "utf-8");
|
|
42
42
|
const arr = JSON.parse(data);
|
|
43
|
+
const oldIds = new Set(this.comments.keys());
|
|
43
44
|
this.comments.clear();
|
|
44
45
|
for (const c of arr) {
|
|
45
46
|
this.comments.set(c.id, c);
|
|
46
47
|
}
|
|
48
|
+
for (const c of arr) {
|
|
49
|
+
if (!oldIds.has(c.id)) {
|
|
50
|
+
this.emit("new_comment", c);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
47
53
|
this.emit("change", null);
|
|
48
54
|
} catch {
|
|
49
55
|
this.comments.clear();
|
|
@@ -86,6 +92,7 @@ var CommentStore = class extends EventEmitter {
|
|
|
86
92
|
};
|
|
87
93
|
this.comments.set(comment.id, comment);
|
|
88
94
|
this.emit("change", comment);
|
|
95
|
+
this.emit("new_comment", comment);
|
|
89
96
|
this.persist().catch((err) => process.stderr.write(`Persist error: ${err}
|
|
90
97
|
`));
|
|
91
98
|
return comment;
|
|
@@ -200,12 +207,12 @@ function annotateFileWithComments(content, comments) {
|
|
|
200
207
|
|
|
201
208
|
// src/mcp-server.ts
|
|
202
209
|
import { readFile as readFile2 } from "fs/promises";
|
|
203
|
-
import { resolve as resolve2 } from "path";
|
|
210
|
+
import { relative, resolve as resolve2 } from "path";
|
|
204
211
|
function createMcpServer(store, projectDir) {
|
|
205
|
-
const server = new McpServer(
|
|
206
|
-
name: "cowrite",
|
|
207
|
-
|
|
208
|
-
|
|
212
|
+
const server = new McpServer(
|
|
213
|
+
{ name: "cowrite", version: "0.1.0" },
|
|
214
|
+
{ capabilities: { logging: {} } }
|
|
215
|
+
);
|
|
209
216
|
server.tool(
|
|
210
217
|
"get_pending_comments",
|
|
211
218
|
"Get comments from the live preview. Returns unresolved comments by default.",
|
|
@@ -290,6 +297,39 @@ function createMcpServer(store, projectDir) {
|
|
|
290
297
|
}
|
|
291
298
|
}
|
|
292
299
|
);
|
|
300
|
+
server.tool(
|
|
301
|
+
"wait_for_comment",
|
|
302
|
+
"Block until a new comment is posted in the live preview, then return it. Call this to listen for incoming user comments in real time.",
|
|
303
|
+
{
|
|
304
|
+
timeout: z.number().optional().describe("Max seconds to wait (default: 120)")
|
|
305
|
+
},
|
|
306
|
+
({ timeout }) => {
|
|
307
|
+
const maxWait = (timeout ?? 120) * 1e3;
|
|
308
|
+
return new Promise((resolve6) => {
|
|
309
|
+
const timer = setTimeout(() => {
|
|
310
|
+
cleanup();
|
|
311
|
+
resolve6({
|
|
312
|
+
content: [{ type: "text", text: "No new comments within timeout." }]
|
|
313
|
+
});
|
|
314
|
+
}, maxWait);
|
|
315
|
+
const onComment = (comment) => {
|
|
316
|
+
cleanup();
|
|
317
|
+
const file = relative(projectDir, comment.file);
|
|
318
|
+
resolve6({
|
|
319
|
+
content: [{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: JSON.stringify({ ...comment, file }, null, 2)
|
|
322
|
+
}]
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
const cleanup = () => {
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
store.off("new_comment", onComment);
|
|
328
|
+
};
|
|
329
|
+
store.on("new_comment", onComment);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
);
|
|
293
333
|
server.resource(
|
|
294
334
|
"all-comments",
|
|
295
335
|
"cowrite://comments",
|
|
@@ -314,6 +354,14 @@ function createMcpServer(store, projectDir) {
|
|
|
314
354
|
}).catch(() => {
|
|
315
355
|
});
|
|
316
356
|
});
|
|
357
|
+
store.on("new_comment", (comment) => {
|
|
358
|
+
const file = relative(projectDir, comment.file);
|
|
359
|
+
server.sendLoggingMessage({
|
|
360
|
+
level: "info",
|
|
361
|
+
data: `New comment on ${file}: "${comment.comment}" (on: "${comment.selectedText}")`
|
|
362
|
+
}).catch(() => {
|
|
363
|
+
});
|
|
364
|
+
});
|
|
317
365
|
return server;
|
|
318
366
|
}
|
|
319
367
|
|
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 } from \"node:path\";\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\";\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server\n cowrite serve [--port N] Start MCP server + preview server (browse any file)\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --help, -h Show this help\n`;\n\nfunction setupShutdown(store: CommentStore, preview: { stop: () => Promise<void> }) {\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 // 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\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 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 const projectDir = process.cwd();\n const port = parseInt(values.port as string, 10);\n\n if (command === \"serve\") {\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (no initial file — user picks from browser)\n const preview = createPreviewServer(store, projectDir, port);\n await preview.start();\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n process.stderr.write(`Preview: http://localhost:${port}\\n`);\n\n setupShutdown(store, preview);\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 with initial file\n const preview = createPreviewServer(store, projectDir, port, resolvedFile);\n await preview.start();\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n process.stderr.write(`Preview: http://localhost:${port}\\n`);\n\n setupShutdown(store, preview);\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 this.comments.clear();\n for (const c of arr) {\n this.comments.set(c.id, c);\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.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 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 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\" | \"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 // 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 { resolve } from \"node:path\";\n\nexport function createMcpServer(store: CommentStore, projectDir: string): McpServer {\n const server = new McpServer({\n name: \"cowrite\",\n version: \"0.1.0\",\n });\n\n // Tool: get_pending_comments\n server.tool(\n \"get_pending_comments\",\n \"Get comments from the live preview. Returns unresolved comments by default.\",\n {\n file: z.string().optional().describe(\"Filter by file path\"),\n status: z.enum([\"pending\", \"resolved\", \"all\"]).optional().describe(\"Filter by status (default: pending)\"),\n },\n async ({ file, status }) => {\n const filter: { file?: string; status?: \"pending\" | \"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: resolve_comment\n server.tool(\n \"resolve_comment\",\n \"Mark a comment as resolved/addressed.\",\n {\n commentId: z.string().describe(\"The comment ID to resolve\"),\n },\n async ({ commentId }) => {\n const comment = store.resolve(commentId);\n if (!comment) {\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: `Comment ${commentId} resolved.` }],\n };\n }\n );\n\n // Tool: reply_to_comment\n server.tool(\n \"reply_to_comment\",\n \"Reply to a comment from the agent.\",\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 // 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 server.server.notification({\n method: \"notifications/resources/updated\",\n params: { uri: \"cowrite://comments\" },\n }).catch(() => {\n // Notification may fail if client doesn't support it, that's OK\n });\n });\n\n return server;\n}\n","import { marked } from \"marked\";\nimport type { Comment } from \"./types.js\";\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 const html = marked.parse(content, { async: false }) as string;\n // Wrap the rendered HTML in a container with data attributes for offset mapping\n // The client-side JS will handle offset computation from the rendered text nodes\n return `<div class=\"markdown-body\" data-source-length=\"${content.length}\">${html}</div>`;\n}\n\nfunction renderPlainTextWithOffsets(content: string): string {\n const lines = content.split(\"\\n\");\n let offset = 0;\n const htmlLines: string[] = [];\n\n for (const line of lines) {\n const escaped = escapeHtml(line);\n htmlLines.push(`<span class=\"line\" data-offset=\"${offset}\" data-length=\"${line.length}\">${escaped}</span>`);\n offset += line.length + 1; // +1 for the newline\n }\n\n return `<pre class=\"plain-text\" data-source-length=\"${content.length}\">${htmlLines.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 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 } 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): { 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 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\", (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n 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 function handleClientMessage(ws: WebSocket, msg: WSClientMessage): void {\n switch (msg.type) {\n case \"switch_file\": {\n const absPath = resolve(resolvedProjectDir, msg.file);\n 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 }\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 return {\n start: () =>\n new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n process.stderr.write(`Cowrite preview server running at http://localhost:${port}\\n`);\n resolve();\n });\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","readFile","resolve","readFile","join","resolve","readFile","resolve","EventEmitter","join","resolve","readFile","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 } from \"node:path\";\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\";\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server\n cowrite serve [--port N] Start MCP server + preview server (browse any file)\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --help, -h Show this help\n`;\n\nfunction setupShutdown(store: CommentStore, preview: { stop: () => Promise<void> }) {\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 // 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\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 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 const projectDir = process.cwd();\n const port = parseInt(values.port as string, 10);\n\n if (command === \"serve\") {\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start preview server (no initial file — user picks from browser)\n const preview = createPreviewServer(store, projectDir, port);\n await preview.start();\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n process.stderr.write(`Preview: http://localhost:${port}\\n`);\n\n setupShutdown(store, preview);\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 with initial file\n const preview = createPreviewServer(store, projectDir, port, resolvedFile);\n await preview.start();\n\n // Start MCP server on stdio\n const mcpServer = createMcpServer(store, projectDir);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n\n process.stderr.write(`Cowrite MCP server running on stdio\\n`);\n process.stderr.write(`Preview: http://localhost:${port}\\n`);\n\n setupShutdown(store, preview);\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 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 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\" | \"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 // 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): McpServer {\n const server = new McpServer(\n { name: \"cowrite\", version: \"0.1.0\" },\n { capabilities: { logging: {} } },\n );\n\n // Tool: get_pending_comments\n server.tool(\n \"get_pending_comments\",\n \"Get comments from the live preview. Returns unresolved comments by default.\",\n {\n file: z.string().optional().describe(\"Filter by file path\"),\n status: z.enum([\"pending\", \"resolved\", \"all\"]).optional().describe(\"Filter by status (default: pending)\"),\n },\n async ({ file, status }) => {\n const filter: { file?: string; status?: \"pending\" | \"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: resolve_comment\n server.tool(\n \"resolve_comment\",\n \"Mark a comment as resolved/addressed.\",\n {\n commentId: z.string().describe(\"The comment ID to resolve\"),\n },\n async ({ commentId }) => {\n const comment = store.resolve(commentId);\n if (!comment) {\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: `Comment ${commentId} resolved.` }],\n };\n }\n );\n\n // Tool: reply_to_comment\n server.tool(\n \"reply_to_comment\",\n \"Reply to a comment from the agent.\",\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 server.tool(\n \"wait_for_comment\",\n \"Block until a new comment is posted in the live preview, then return it. Call this to listen for incoming user comments in real time.\",\n {\n timeout: z.number().optional().describe(\"Max seconds to wait (default: 120)\"),\n },\n ({ timeout }) => {\n const maxWait = (timeout ?? 120) * 1000;\n return new Promise((resolve) => {\n const timer = setTimeout(() => {\n cleanup();\n resolve({\n content: [{ type: \"text\" as const, text: \"No new comments within timeout.\" }],\n });\n }, maxWait);\n\n const onComment = (comment: { id: string; file: string; selectedText: string; comment: string }) => {\n cleanup();\n const file = relative(projectDir, comment.file);\n resolve({\n content: [{\n type: \"text\" as const,\n text: JSON.stringify({ ...comment, file }, null, 2),\n }],\n });\n };\n\n const cleanup = () => {\n clearTimeout(timer);\n store.off(\"new_comment\", onComment);\n };\n\n store.on(\"new_comment\", onComment);\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 server.server.notification({\n method: \"notifications/resources/updated\",\n params: { uri: \"cowrite://comments\" },\n }).catch(() => {\n // Notification may fail if client doesn't support it, that's OK\n });\n });\n\n // Notify the agent when a new comment is added\n store.on(\"new_comment\", (comment: { file: string; selectedText: string; comment: string }) => {\n const file = relative(projectDir, comment.file);\n server.sendLoggingMessage({\n level: \"info\",\n data: `New comment on ${file}: \"${comment.comment}\" (on: \"${comment.selectedText}\")`,\n }).catch(() => {});\n });\n\n return server;\n}\n","import { marked } from \"marked\";\nimport type { Comment } from \"./types.js\";\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 const html = marked.parse(content, { async: false }) as string;\n // Wrap the rendered HTML in a container with data attributes for offset mapping\n // The client-side JS will handle offset computation from the rendered text nodes\n return `<div class=\"markdown-body\" data-source-length=\"${content.length}\">${html}</div>`;\n}\n\nfunction renderPlainTextWithOffsets(content: string): string {\n const lines = content.split(\"\\n\");\n let offset = 0;\n const htmlLines: string[] = [];\n\n for (const line of lines) {\n const escaped = escapeHtml(line);\n htmlLines.push(`<span class=\"line\" data-offset=\"${offset}\" data-length=\"${line.length}\">${escaped}</span>`);\n offset += line.length + 1; // +1 for the newline\n }\n\n return `<pre class=\"plain-text\" data-source-length=\"${content.length}\">${htmlLines.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 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 } 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): { 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 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\", (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n 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 function handleClientMessage(ws: WebSocket, msg: WSClientMessage): void {\n switch (msg.type) {\n case \"switch_file\": {\n const absPath = resolve(resolvedProjectDir, msg.file);\n 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 }\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 return {\n start: () =>\n new Promise<void>((resolve) => {\n httpServer.listen(port, () => {\n process.stderr.write(`Cowrite preview server running at http://localhost:${port}\\n`);\n resolve();\n });\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","readFile","resolve","readFile","join","resolve","readFile","resolve","EventEmitter","join","resolve","readFile","resolve"]}
|
package/package.json
CHANGED
package/ui/client.js
CHANGED
|
@@ -252,8 +252,8 @@ function renderComments() {
|
|
|
252
252
|
` : ""}
|
|
253
253
|
<div class="reply-form" id="reply-form-${c.id}" hidden>
|
|
254
254
|
<textarea rows="2" placeholder="Reply..."></textarea>
|
|
255
|
-
<div class="
|
|
256
|
-
<button onclick="submitReply('${c.id}')"
|
|
255
|
+
<div class="reply-form-actions">
|
|
256
|
+
<button onclick="submitReply('${c.id}')">Send</button>
|
|
257
257
|
</div>
|
|
258
258
|
</div>
|
|
259
259
|
</div>
|
|
@@ -384,5 +384,28 @@ function timeAgo(iso) {
|
|
|
384
384
|
return `${Math.floor(hours / 24)}d ago`;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// --- Theme Toggle ---
|
|
388
|
+
|
|
389
|
+
const themeToggle = $("#themeToggle");
|
|
390
|
+
const THEME_KEY = "cowrite-theme";
|
|
391
|
+
|
|
392
|
+
function applyTheme(theme) {
|
|
393
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
394
|
+
themeToggle.checked = theme === "light";
|
|
395
|
+
// Update toggle icon
|
|
396
|
+
const icon = themeToggle.closest(".theme-toggle").querySelector(".toggle-icon");
|
|
397
|
+
if (icon) icon.textContent = theme === "light" ? "\u2600" : "\u263E";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Load saved preference, default to dark
|
|
401
|
+
const savedTheme = localStorage.getItem(THEME_KEY) || "dark";
|
|
402
|
+
applyTheme(savedTheme);
|
|
403
|
+
|
|
404
|
+
themeToggle.addEventListener("change", () => {
|
|
405
|
+
const theme = themeToggle.checked ? "light" : "dark";
|
|
406
|
+
localStorage.setItem(THEME_KEY, theme);
|
|
407
|
+
applyTheme(theme);
|
|
408
|
+
});
|
|
409
|
+
|
|
387
410
|
// --- Init ---
|
|
388
411
|
connect();
|
package/ui/index.html
CHANGED
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
</div>
|
|
16
16
|
<span class="file-path" id="filePath"></span>
|
|
17
17
|
<span class="status" id="status">Connecting...</span>
|
|
18
|
+
<label class="theme-toggle" title="Toggle light/dark mode">
|
|
19
|
+
<input type="checkbox" id="themeToggle" />
|
|
20
|
+
<span class="toggle-track">
|
|
21
|
+
<span class="toggle-thumb"><span class="toggle-icon"></span></span>
|
|
22
|
+
</span>
|
|
23
|
+
</label>
|
|
18
24
|
</header>
|
|
19
25
|
|
|
20
26
|
<main>
|
package/ui/styles.css
CHANGED
|
@@ -4,19 +4,59 @@
|
|
|
4
4
|
box-sizing: border-box;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/* ---- Dark theme (default) ---- */
|
|
7
8
|
:root {
|
|
8
|
-
--bg: #
|
|
9
|
-
--
|
|
10
|
-
--surface
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
9
|
+
--bg: #0f0f17;
|
|
10
|
+
--bg-subtle: #161622;
|
|
11
|
+
--surface: #1c1c2e;
|
|
12
|
+
--surface-hover: #252540;
|
|
13
|
+
--surface-raised: #22223a;
|
|
14
|
+
--text: #e0e0f0;
|
|
15
|
+
--text-dim: #7a7a9a;
|
|
16
|
+
--text-faint: #55556e;
|
|
17
|
+
--accent: #7c9ff5;
|
|
18
|
+
--accent-hover: #93b0ff;
|
|
19
|
+
--accent-glow: rgba(124, 159, 245, 0.12);
|
|
20
|
+
--yellow: #edc97a;
|
|
21
|
+
--yellow-bg: rgba(237, 201, 122, 0.10);
|
|
22
|
+
--green: #7ed3a0;
|
|
23
|
+
--green-bg: rgba(126, 211, 160, 0.10);
|
|
24
|
+
--red: #e87a95;
|
|
25
|
+
--border: #2a2a42;
|
|
26
|
+
--border-hover: #3a3a58;
|
|
27
|
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
28
|
+
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
29
|
+
--shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.5);
|
|
30
|
+
--font-mono: "SF Mono", "Fira Code", "JetBrains Mono", "Cascadia Code", monospace;
|
|
31
|
+
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", sans-serif;
|
|
32
|
+
--radius-sm: 6px;
|
|
33
|
+
--radius-md: 10px;
|
|
34
|
+
--radius-lg: 14px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ---- Light theme ---- */
|
|
38
|
+
[data-theme="light"] {
|
|
39
|
+
--bg: #f5f5fa;
|
|
40
|
+
--bg-subtle: #ededf4;
|
|
41
|
+
--surface: #ffffff;
|
|
42
|
+
--surface-hover: #f0f0f8;
|
|
43
|
+
--surface-raised: #f8f8fc;
|
|
44
|
+
--text: #1a1a2e;
|
|
45
|
+
--text-dim: #6b6b88;
|
|
46
|
+
--text-faint: #9999b0;
|
|
47
|
+
--accent: #4a6ee0;
|
|
48
|
+
--accent-hover: #5b7eec;
|
|
49
|
+
--accent-glow: rgba(74, 110, 224, 0.08);
|
|
50
|
+
--yellow: #c49220;
|
|
51
|
+
--yellow-bg: rgba(196, 146, 32, 0.08);
|
|
52
|
+
--green: #2a9d5c;
|
|
53
|
+
--green-bg: rgba(42, 157, 92, 0.08);
|
|
54
|
+
--red: #d04060;
|
|
55
|
+
--border: #dcdce8;
|
|
56
|
+
--border-hover: #c0c0d4;
|
|
57
|
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
|
|
58
|
+
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
|
|
59
|
+
--shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.12);
|
|
20
60
|
}
|
|
21
61
|
|
|
22
62
|
body {
|
|
@@ -27,53 +67,60 @@ body {
|
|
|
27
67
|
display: flex;
|
|
28
68
|
flex-direction: column;
|
|
29
69
|
overflow: hidden;
|
|
70
|
+
transition: background 0.3s, color 0.3s;
|
|
30
71
|
}
|
|
31
72
|
|
|
73
|
+
/* ---- Header ---- */
|
|
32
74
|
header {
|
|
33
75
|
display: flex;
|
|
34
76
|
align-items: center;
|
|
35
|
-
gap:
|
|
36
|
-
padding:
|
|
77
|
+
gap: 14px;
|
|
78
|
+
padding: 0 20px;
|
|
79
|
+
height: 52px;
|
|
37
80
|
background: var(--surface);
|
|
38
81
|
border-bottom: 1px solid var(--border);
|
|
39
82
|
flex-shrink: 0;
|
|
83
|
+
transition: background 0.3s, border-color 0.3s;
|
|
40
84
|
}
|
|
41
85
|
|
|
42
86
|
header h1 {
|
|
43
|
-
font-size:
|
|
44
|
-
font-weight:
|
|
87
|
+
font-size: 15px;
|
|
88
|
+
font-weight: 700;
|
|
45
89
|
color: var(--accent);
|
|
46
|
-
letter-spacing: 0.
|
|
90
|
+
letter-spacing: -0.2px;
|
|
91
|
+
user-select: none;
|
|
47
92
|
}
|
|
48
93
|
|
|
94
|
+
/* File picker */
|
|
49
95
|
.file-picker {
|
|
50
96
|
position: relative;
|
|
51
97
|
}
|
|
52
98
|
|
|
53
99
|
.file-picker input {
|
|
54
100
|
font-family: var(--font-mono);
|
|
55
|
-
font-size:
|
|
101
|
+
font-size: 12px;
|
|
56
102
|
color: var(--text);
|
|
57
103
|
background: var(--bg);
|
|
58
104
|
border: 1px solid var(--border);
|
|
59
|
-
border-radius:
|
|
60
|
-
padding:
|
|
61
|
-
width:
|
|
105
|
+
border-radius: var(--radius-sm);
|
|
106
|
+
padding: 6px 12px;
|
|
107
|
+
width: 260px;
|
|
62
108
|
outline: none;
|
|
63
|
-
transition: border-color 0.
|
|
109
|
+
transition: border-color 0.2s, box-shadow 0.2s, background 0.3s;
|
|
64
110
|
}
|
|
65
111
|
|
|
66
112
|
.file-picker input:focus {
|
|
67
113
|
border-color: var(--accent);
|
|
114
|
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
68
115
|
}
|
|
69
116
|
|
|
70
117
|
.file-picker input::placeholder {
|
|
71
|
-
color: var(--text-
|
|
118
|
+
color: var(--text-faint);
|
|
72
119
|
}
|
|
73
120
|
|
|
74
121
|
.file-path {
|
|
75
122
|
font-family: var(--font-mono);
|
|
76
|
-
font-size:
|
|
123
|
+
font-size: 12px;
|
|
77
124
|
color: var(--text-dim);
|
|
78
125
|
flex: 1;
|
|
79
126
|
overflow: hidden;
|
|
@@ -81,49 +128,116 @@ header h1 {
|
|
|
81
128
|
white-space: nowrap;
|
|
82
129
|
}
|
|
83
130
|
|
|
131
|
+
/* Status badge */
|
|
84
132
|
.status {
|
|
85
|
-
font-size:
|
|
133
|
+
font-size: 11px;
|
|
134
|
+
font-weight: 500;
|
|
86
135
|
padding: 4px 10px;
|
|
87
|
-
border-radius:
|
|
136
|
+
border-radius: 100px;
|
|
88
137
|
background: var(--surface-hover);
|
|
89
138
|
color: var(--text-dim);
|
|
139
|
+
transition: all 0.3s;
|
|
90
140
|
}
|
|
91
141
|
|
|
92
142
|
.status.connected {
|
|
93
143
|
color: var(--green);
|
|
94
|
-
background:
|
|
144
|
+
background: var(--green-bg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ---- Theme toggle ---- */
|
|
148
|
+
.theme-toggle {
|
|
149
|
+
position: relative;
|
|
150
|
+
width: 44px;
|
|
151
|
+
height: 24px;
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.theme-toggle input {
|
|
157
|
+
opacity: 0;
|
|
158
|
+
width: 0;
|
|
159
|
+
height: 0;
|
|
160
|
+
position: absolute;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.toggle-track {
|
|
164
|
+
position: absolute;
|
|
165
|
+
inset: 0;
|
|
166
|
+
background: var(--border);
|
|
167
|
+
border-radius: 100px;
|
|
168
|
+
transition: background 0.3s;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.theme-toggle input:checked + .toggle-track {
|
|
172
|
+
background: var(--accent);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.toggle-thumb {
|
|
176
|
+
position: absolute;
|
|
177
|
+
top: 3px;
|
|
178
|
+
left: 3px;
|
|
179
|
+
width: 18px;
|
|
180
|
+
height: 18px;
|
|
181
|
+
background: var(--text);
|
|
182
|
+
border-radius: 50%;
|
|
183
|
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s;
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
justify-content: center;
|
|
95
187
|
}
|
|
96
188
|
|
|
189
|
+
.theme-toggle input:checked + .toggle-track .toggle-thumb {
|
|
190
|
+
transform: translateX(20px);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.toggle-icon {
|
|
194
|
+
font-size: 10px;
|
|
195
|
+
line-height: 1;
|
|
196
|
+
user-select: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ---- Main layout ---- */
|
|
97
200
|
main {
|
|
98
201
|
display: flex;
|
|
99
202
|
flex: 1;
|
|
100
203
|
overflow: hidden;
|
|
101
204
|
}
|
|
102
205
|
|
|
103
|
-
/* Content
|
|
206
|
+
/* Content panel */
|
|
104
207
|
.content-panel {
|
|
105
208
|
flex: 1;
|
|
106
209
|
overflow-y: auto;
|
|
107
|
-
padding:
|
|
210
|
+
padding: 28px 36px;
|
|
108
211
|
}
|
|
109
212
|
|
|
110
213
|
#fileContent {
|
|
111
|
-
max-width:
|
|
214
|
+
max-width: 780px;
|
|
112
215
|
margin: 0 auto;
|
|
113
|
-
line-height: 1.
|
|
216
|
+
line-height: 1.75;
|
|
114
217
|
}
|
|
115
218
|
|
|
116
|
-
/* Markdown styles */
|
|
219
|
+
/* ---- Markdown styles ---- */
|
|
117
220
|
.markdown-body h1, .markdown-body h2, .markdown-body h3,
|
|
118
221
|
.markdown-body h4, .markdown-body h5, .markdown-body h6 {
|
|
119
|
-
margin: 1.
|
|
222
|
+
margin: 1.4em 0 0.6em;
|
|
120
223
|
font-weight: 600;
|
|
121
224
|
color: var(--text);
|
|
225
|
+
line-height: 1.3;
|
|
122
226
|
}
|
|
123
227
|
|
|
124
|
-
.markdown-body h1 {
|
|
125
|
-
|
|
126
|
-
|
|
228
|
+
.markdown-body h1 {
|
|
229
|
+
font-size: 1.75em;
|
|
230
|
+
border-bottom: 1px solid var(--border);
|
|
231
|
+
padding-bottom: 0.35em;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.markdown-body h2 {
|
|
235
|
+
font-size: 1.35em;
|
|
236
|
+
border-bottom: 1px solid var(--border);
|
|
237
|
+
padding-bottom: 0.3em;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.markdown-body h3 { font-size: 1.15em; }
|
|
127
241
|
|
|
128
242
|
.markdown-body p { margin: 0.8em 0; }
|
|
129
243
|
.markdown-body ul, .markdown-body ol { margin: 0.8em 0; padding-left: 2em; }
|
|
@@ -131,16 +245,16 @@ main {
|
|
|
131
245
|
|
|
132
246
|
.markdown-body code {
|
|
133
247
|
font-family: var(--font-mono);
|
|
134
|
-
font-size: 0.
|
|
248
|
+
font-size: 0.88em;
|
|
135
249
|
background: var(--surface-hover);
|
|
136
|
-
padding: 2px
|
|
250
|
+
padding: 2px 7px;
|
|
137
251
|
border-radius: 4px;
|
|
138
252
|
}
|
|
139
253
|
|
|
140
254
|
.markdown-body pre {
|
|
141
255
|
background: var(--surface);
|
|
142
|
-
padding:
|
|
143
|
-
border-radius:
|
|
256
|
+
padding: 18px;
|
|
257
|
+
border-radius: var(--radius-md);
|
|
144
258
|
overflow-x: auto;
|
|
145
259
|
margin: 1em 0;
|
|
146
260
|
border: 1px solid var(--border);
|
|
@@ -168,31 +282,36 @@ main {
|
|
|
168
282
|
/* Plain text */
|
|
169
283
|
.plain-text {
|
|
170
284
|
font-family: var(--font-mono);
|
|
171
|
-
font-size:
|
|
172
|
-
line-height: 1.
|
|
285
|
+
font-size: 13px;
|
|
286
|
+
line-height: 1.65;
|
|
173
287
|
white-space: pre-wrap;
|
|
174
288
|
word-wrap: break-word;
|
|
175
289
|
color: var(--text);
|
|
176
290
|
}
|
|
177
291
|
|
|
178
|
-
/* Comment highlights */
|
|
292
|
+
/* ---- Comment highlights ---- */
|
|
179
293
|
.comment-highlight {
|
|
180
|
-
background:
|
|
294
|
+
background: var(--yellow-bg);
|
|
181
295
|
border-bottom: 2px solid var(--yellow);
|
|
182
296
|
cursor: pointer;
|
|
183
297
|
position: relative;
|
|
298
|
+
transition: background 0.15s;
|
|
184
299
|
}
|
|
185
300
|
|
|
186
301
|
.comment-highlight.resolved {
|
|
187
|
-
background:
|
|
188
|
-
border-bottom-color:
|
|
302
|
+
background: transparent;
|
|
303
|
+
border-bottom-color: transparent;
|
|
189
304
|
}
|
|
190
305
|
|
|
191
306
|
.comment-highlight:hover {
|
|
192
|
-
background: rgba(
|
|
307
|
+
background: rgba(237, 201, 122, 0.22);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
[data-theme="light"] .comment-highlight:hover {
|
|
311
|
+
background: rgba(196, 146, 32, 0.14);
|
|
193
312
|
}
|
|
194
313
|
|
|
195
|
-
/* Sidebar */
|
|
314
|
+
/* ---- Sidebar ---- */
|
|
196
315
|
.sidebar {
|
|
197
316
|
width: 340px;
|
|
198
317
|
min-width: 340px;
|
|
@@ -201,26 +320,31 @@ main {
|
|
|
201
320
|
overflow-y: auto;
|
|
202
321
|
display: flex;
|
|
203
322
|
flex-direction: column;
|
|
323
|
+
transition: background 0.3s, border-color 0.3s;
|
|
204
324
|
}
|
|
205
325
|
|
|
206
326
|
.sidebar h2 {
|
|
207
|
-
font-size:
|
|
327
|
+
font-size: 13px;
|
|
208
328
|
font-weight: 600;
|
|
209
|
-
padding:
|
|
329
|
+
padding: 14px 16px;
|
|
210
330
|
border-bottom: 1px solid var(--border);
|
|
211
331
|
display: flex;
|
|
212
332
|
align-items: center;
|
|
213
333
|
gap: 8px;
|
|
214
334
|
flex-shrink: 0;
|
|
335
|
+
letter-spacing: -0.1px;
|
|
215
336
|
}
|
|
216
337
|
|
|
217
338
|
.comment-count {
|
|
218
339
|
background: var(--accent);
|
|
219
340
|
color: var(--bg);
|
|
220
|
-
font-size:
|
|
341
|
+
font-size: 10px;
|
|
221
342
|
font-weight: 700;
|
|
343
|
+
min-width: 20px;
|
|
344
|
+
text-align: center;
|
|
222
345
|
padding: 2px 7px;
|
|
223
|
-
border-radius:
|
|
346
|
+
border-radius: 100px;
|
|
347
|
+
transition: background 0.3s;
|
|
224
348
|
}
|
|
225
349
|
|
|
226
350
|
#commentList {
|
|
@@ -229,42 +353,48 @@ main {
|
|
|
229
353
|
padding: 8px;
|
|
230
354
|
}
|
|
231
355
|
|
|
232
|
-
/* Comment
|
|
356
|
+
/* ---- Comment card ---- */
|
|
233
357
|
.comment-card {
|
|
234
|
-
background: var(--bg);
|
|
358
|
+
background: var(--bg-subtle);
|
|
235
359
|
border: 1px solid var(--border);
|
|
236
|
-
border-radius:
|
|
360
|
+
border-radius: var(--radius-md);
|
|
237
361
|
padding: 12px;
|
|
238
362
|
margin-bottom: 8px;
|
|
239
363
|
cursor: pointer;
|
|
240
|
-
transition: border-color 0.
|
|
364
|
+
transition: border-color 0.2s, box-shadow 0.2s, background 0.3s;
|
|
241
365
|
}
|
|
242
366
|
|
|
243
367
|
.comment-card:hover {
|
|
244
|
-
border-color: var(--
|
|
368
|
+
border-color: var(--border-hover);
|
|
369
|
+
box-shadow: var(--shadow-sm);
|
|
245
370
|
}
|
|
246
371
|
|
|
247
372
|
.comment-card.active {
|
|
248
373
|
border-color: var(--accent);
|
|
249
|
-
box-shadow: 0 0 0
|
|
374
|
+
box-shadow: 0 0 0 2px var(--accent-glow);
|
|
250
375
|
}
|
|
251
376
|
|
|
252
377
|
.comment-card.resolved {
|
|
253
|
-
opacity: 0.
|
|
378
|
+
opacity: 0.55;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.comment-card.resolved:hover {
|
|
382
|
+
opacity: 0.75;
|
|
254
383
|
}
|
|
255
384
|
|
|
256
385
|
.comment-selected-text {
|
|
257
386
|
font-family: var(--font-mono);
|
|
258
|
-
font-size:
|
|
387
|
+
font-size: 11px;
|
|
259
388
|
color: var(--text-dim);
|
|
260
389
|
background: var(--surface-hover);
|
|
261
390
|
padding: 6px 8px;
|
|
262
|
-
border-radius:
|
|
391
|
+
border-radius: var(--radius-sm);
|
|
263
392
|
margin-bottom: 8px;
|
|
264
|
-
max-height:
|
|
393
|
+
max-height: 44px;
|
|
265
394
|
overflow: hidden;
|
|
266
395
|
white-space: pre;
|
|
267
396
|
text-overflow: ellipsis;
|
|
397
|
+
border-left: 2px solid var(--yellow);
|
|
268
398
|
}
|
|
269
399
|
|
|
270
400
|
.comment-text {
|
|
@@ -278,30 +408,32 @@ main {
|
|
|
278
408
|
align-items: center;
|
|
279
409
|
justify-content: space-between;
|
|
280
410
|
font-size: 11px;
|
|
281
|
-
color: var(--text-
|
|
411
|
+
color: var(--text-faint);
|
|
282
412
|
}
|
|
283
413
|
|
|
284
414
|
.comment-status {
|
|
285
|
-
font-size:
|
|
415
|
+
font-size: 10px;
|
|
286
416
|
font-weight: 600;
|
|
287
417
|
padding: 2px 8px;
|
|
288
|
-
border-radius:
|
|
418
|
+
border-radius: 100px;
|
|
419
|
+
text-transform: uppercase;
|
|
420
|
+
letter-spacing: 0.3px;
|
|
289
421
|
}
|
|
290
422
|
|
|
291
423
|
.comment-status.pending {
|
|
292
424
|
color: var(--yellow);
|
|
293
|
-
background:
|
|
425
|
+
background: var(--yellow-bg);
|
|
294
426
|
}
|
|
295
427
|
|
|
296
428
|
.comment-status.resolved {
|
|
297
429
|
color: var(--green);
|
|
298
|
-
background:
|
|
430
|
+
background: var(--green-bg);
|
|
299
431
|
}
|
|
300
432
|
|
|
301
|
-
/* Replies */
|
|
433
|
+
/* ---- Replies ---- */
|
|
302
434
|
.comment-replies {
|
|
303
|
-
margin-top:
|
|
304
|
-
padding-top:
|
|
435
|
+
margin-top: 10px;
|
|
436
|
+
padding-top: 10px;
|
|
305
437
|
border-top: 1px solid var(--border);
|
|
306
438
|
}
|
|
307
439
|
|
|
@@ -309,42 +441,43 @@ main {
|
|
|
309
441
|
font-size: 12px;
|
|
310
442
|
line-height: 1.5;
|
|
311
443
|
margin-bottom: 6px;
|
|
312
|
-
padding:
|
|
313
|
-
border-radius:
|
|
444
|
+
padding: 8px 10px;
|
|
445
|
+
border-radius: var(--radius-sm);
|
|
314
446
|
}
|
|
315
447
|
|
|
316
448
|
.reply.user {
|
|
317
|
-
background:
|
|
449
|
+
background: var(--accent-glow);
|
|
318
450
|
}
|
|
319
451
|
|
|
320
452
|
.reply.agent {
|
|
321
|
-
background:
|
|
453
|
+
background: var(--green-bg);
|
|
322
454
|
}
|
|
323
455
|
|
|
324
456
|
.reply-from {
|
|
325
457
|
font-weight: 600;
|
|
326
|
-
font-size:
|
|
458
|
+
font-size: 10px;
|
|
327
459
|
text-transform: uppercase;
|
|
328
460
|
letter-spacing: 0.5px;
|
|
329
|
-
margin-bottom:
|
|
461
|
+
margin-bottom: 3px;
|
|
330
462
|
}
|
|
331
463
|
|
|
332
464
|
.reply-from.user { color: var(--accent); }
|
|
333
465
|
.reply-from.agent { color: var(--green); }
|
|
334
466
|
|
|
335
|
-
/* Comment card actions */
|
|
467
|
+
/* ---- Comment card actions ---- */
|
|
336
468
|
.comment-actions {
|
|
337
469
|
display: flex;
|
|
338
470
|
gap: 6px;
|
|
339
|
-
margin-top:
|
|
471
|
+
margin-top: 10px;
|
|
340
472
|
}
|
|
341
473
|
|
|
342
474
|
.comment-actions button {
|
|
343
475
|
font-size: 11px;
|
|
344
|
-
|
|
345
|
-
|
|
476
|
+
font-weight: 500;
|
|
477
|
+
padding: 5px 12px;
|
|
478
|
+
border-radius: var(--radius-sm);
|
|
346
479
|
border: 1px solid var(--border);
|
|
347
|
-
background: var(--surface
|
|
480
|
+
background: var(--surface);
|
|
348
481
|
color: var(--text-dim);
|
|
349
482
|
cursor: pointer;
|
|
350
483
|
transition: all 0.15s;
|
|
@@ -353,6 +486,7 @@ main {
|
|
|
353
486
|
.comment-actions button:hover {
|
|
354
487
|
color: var(--text);
|
|
355
488
|
border-color: var(--accent);
|
|
489
|
+
background: var(--accent-glow);
|
|
356
490
|
}
|
|
357
491
|
|
|
358
492
|
/* Reply inline form */
|
|
@@ -367,40 +501,66 @@ main {
|
|
|
367
501
|
background: var(--bg);
|
|
368
502
|
color: var(--text);
|
|
369
503
|
border: 1px solid var(--border);
|
|
370
|
-
border-radius:
|
|
371
|
-
padding:
|
|
504
|
+
border-radius: var(--radius-sm);
|
|
505
|
+
padding: 8px 10px;
|
|
372
506
|
resize: vertical;
|
|
507
|
+
transition: border-color 0.2s, box-shadow 0.2s, background 0.3s;
|
|
373
508
|
}
|
|
374
509
|
|
|
375
510
|
.reply-form textarea:focus {
|
|
376
511
|
outline: none;
|
|
377
512
|
border-color: var(--accent);
|
|
513
|
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.reply-form-actions {
|
|
517
|
+
margin-top: 6px;
|
|
518
|
+
display: flex;
|
|
519
|
+
justify-content: flex-end;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.reply-form-actions button {
|
|
523
|
+
font-size: 11px;
|
|
524
|
+
font-weight: 600;
|
|
525
|
+
padding: 5px 14px;
|
|
526
|
+
border-radius: var(--radius-sm);
|
|
527
|
+
border: none;
|
|
528
|
+
background: var(--accent);
|
|
529
|
+
color: #fff;
|
|
530
|
+
cursor: pointer;
|
|
531
|
+
transition: background 0.15s;
|
|
378
532
|
}
|
|
379
533
|
|
|
380
|
-
|
|
534
|
+
.reply-form-actions button:hover {
|
|
535
|
+
background: var(--accent-hover);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* ---- Comment popup ---- */
|
|
381
539
|
.comment-popup {
|
|
382
540
|
position: fixed;
|
|
383
541
|
z-index: 1000;
|
|
384
542
|
background: var(--surface);
|
|
385
543
|
border: 1px solid var(--border);
|
|
386
|
-
border-radius:
|
|
387
|
-
padding:
|
|
544
|
+
border-radius: var(--radius-lg);
|
|
545
|
+
padding: 16px;
|
|
388
546
|
width: 320px;
|
|
389
|
-
box-shadow:
|
|
547
|
+
box-shadow: var(--shadow-lg);
|
|
548
|
+
transition: background 0.3s, border-color 0.3s;
|
|
390
549
|
}
|
|
391
550
|
|
|
392
551
|
.popup-selection {
|
|
393
552
|
font-family: var(--font-mono);
|
|
394
|
-
font-size:
|
|
553
|
+
font-size: 11px;
|
|
395
554
|
color: var(--text-dim);
|
|
396
555
|
background: var(--bg);
|
|
397
556
|
padding: 8px 10px;
|
|
398
|
-
border-radius:
|
|
399
|
-
margin-bottom:
|
|
400
|
-
max-height:
|
|
557
|
+
border-radius: var(--radius-sm);
|
|
558
|
+
margin-bottom: 12px;
|
|
559
|
+
max-height: 56px;
|
|
401
560
|
overflow: hidden;
|
|
402
561
|
white-space: pre;
|
|
403
562
|
text-overflow: ellipsis;
|
|
563
|
+
border-left: 2px solid var(--accent);
|
|
404
564
|
}
|
|
405
565
|
|
|
406
566
|
.comment-popup textarea {
|
|
@@ -410,28 +570,30 @@ main {
|
|
|
410
570
|
background: var(--bg);
|
|
411
571
|
color: var(--text);
|
|
412
572
|
border: 1px solid var(--border);
|
|
413
|
-
border-radius:
|
|
414
|
-
padding:
|
|
573
|
+
border-radius: var(--radius-sm);
|
|
574
|
+
padding: 10px 12px;
|
|
415
575
|
resize: vertical;
|
|
576
|
+
transition: border-color 0.2s, box-shadow 0.2s, background 0.3s;
|
|
416
577
|
}
|
|
417
578
|
|
|
418
579
|
.comment-popup textarea:focus {
|
|
419
580
|
outline: none;
|
|
420
581
|
border-color: var(--accent);
|
|
582
|
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
421
583
|
}
|
|
422
584
|
|
|
423
585
|
.popup-actions {
|
|
424
586
|
display: flex;
|
|
425
587
|
gap: 8px;
|
|
426
|
-
margin-top:
|
|
588
|
+
margin-top: 12px;
|
|
427
589
|
justify-content: flex-end;
|
|
428
590
|
}
|
|
429
591
|
|
|
430
592
|
.popup-actions button {
|
|
431
|
-
font-size:
|
|
432
|
-
font-weight:
|
|
433
|
-
padding:
|
|
434
|
-
border-radius:
|
|
593
|
+
font-size: 12px;
|
|
594
|
+
font-weight: 600;
|
|
595
|
+
padding: 7px 18px;
|
|
596
|
+
border-radius: var(--radius-sm);
|
|
435
597
|
border: none;
|
|
436
598
|
cursor: pointer;
|
|
437
599
|
transition: all 0.15s;
|
|
@@ -439,11 +601,11 @@ main {
|
|
|
439
601
|
|
|
440
602
|
.popup-actions button#commentSubmit {
|
|
441
603
|
background: var(--accent);
|
|
442
|
-
color:
|
|
604
|
+
color: #fff;
|
|
443
605
|
}
|
|
444
606
|
|
|
445
607
|
.popup-actions button#commentSubmit:hover {
|
|
446
|
-
|
|
608
|
+
background: var(--accent-hover);
|
|
447
609
|
}
|
|
448
610
|
|
|
449
611
|
.popup-actions button.secondary {
|
|
@@ -453,24 +615,35 @@ main {
|
|
|
453
615
|
|
|
454
616
|
.popup-actions button.secondary:hover {
|
|
455
617
|
color: var(--text);
|
|
618
|
+
background: var(--border);
|
|
456
619
|
}
|
|
457
620
|
|
|
458
|
-
/* Empty state */
|
|
621
|
+
/* ---- Empty state ---- */
|
|
459
622
|
.empty-state {
|
|
460
623
|
text-align: center;
|
|
461
|
-
padding:
|
|
462
|
-
color: var(--text-
|
|
624
|
+
padding: 48px 24px;
|
|
625
|
+
color: var(--text-faint);
|
|
463
626
|
font-size: 13px;
|
|
464
|
-
line-height: 1.
|
|
627
|
+
line-height: 1.7;
|
|
465
628
|
}
|
|
466
629
|
|
|
467
|
-
|
|
468
|
-
|
|
630
|
+
.empty-state p:first-child {
|
|
631
|
+
font-size: 14px;
|
|
632
|
+
color: var(--text-dim);
|
|
633
|
+
margin-bottom: 4px;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* ---- Scrollbar ---- */
|
|
637
|
+
::-webkit-scrollbar { width: 7px; }
|
|
469
638
|
::-webkit-scrollbar-track { background: transparent; }
|
|
470
|
-
::-webkit-scrollbar-thumb { background: var(--border); border-radius:
|
|
471
|
-
::-webkit-scrollbar-thumb:hover { background: var(--text-
|
|
639
|
+
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 100px; }
|
|
640
|
+
::-webkit-scrollbar-thumb:hover { background: var(--text-faint); }
|
|
472
641
|
|
|
473
|
-
/* Selection */
|
|
642
|
+
/* ---- Selection ---- */
|
|
474
643
|
::selection {
|
|
475
|
-
background: rgba(
|
|
644
|
+
background: rgba(124, 159, 245, 0.3);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
[data-theme="light"] ::selection {
|
|
648
|
+
background: rgba(74, 110, 224, 0.25);
|
|
476
649
|
}
|