@filipc77/cowrite 0.1.0 → 0.2.1
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 +235 -119
- package/dist/bin/cowrite.js.map +1 -1
- package/package.json +7 -2
- package/ui/client.js +50 -0
- package/ui/index.html +5 -1
- package/ui/styles.css +25 -0
package/dist/bin/cowrite.js
CHANGED
|
@@ -319,17 +319,136 @@ function createMcpServer(store, projectDir) {
|
|
|
319
319
|
|
|
320
320
|
// src/preview-server.ts
|
|
321
321
|
import { createServer } from "http";
|
|
322
|
-
import { readFile as
|
|
323
|
-
import {
|
|
322
|
+
import { readFile as readFile4, readdir } from "fs/promises";
|
|
323
|
+
import { existsSync } from "fs";
|
|
324
|
+
import { join as join2, resolve as resolve4 } from "path";
|
|
324
325
|
import { WebSocketServer } from "ws";
|
|
325
|
-
|
|
326
|
+
|
|
327
|
+
// src/file-watcher.ts
|
|
328
|
+
import { watch } from "chokidar";
|
|
329
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
330
|
+
import { resolve as resolve3 } from "path";
|
|
331
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
332
|
+
var FileWatcher = class extends EventEmitter2 {
|
|
333
|
+
watcher = null;
|
|
334
|
+
filePath;
|
|
335
|
+
lastContent = "";
|
|
336
|
+
constructor(filePath) {
|
|
337
|
+
super();
|
|
338
|
+
this.filePath = resolve3(filePath);
|
|
339
|
+
}
|
|
340
|
+
async start() {
|
|
341
|
+
this.lastContent = await readFile3(this.filePath, "utf-8");
|
|
342
|
+
this.watcher = watch(this.filePath, {
|
|
343
|
+
persistent: true,
|
|
344
|
+
ignoreInitial: true,
|
|
345
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
346
|
+
});
|
|
347
|
+
this.watcher.on("change", async () => {
|
|
348
|
+
try {
|
|
349
|
+
const newContent = await readFile3(this.filePath, "utf-8");
|
|
350
|
+
if (newContent !== this.lastContent) {
|
|
351
|
+
const oldContent = this.lastContent;
|
|
352
|
+
this.lastContent = newContent;
|
|
353
|
+
this.emit("change", {
|
|
354
|
+
file: this.filePath,
|
|
355
|
+
content: newContent,
|
|
356
|
+
oldContent
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
process.stderr.write(`File watch read error: ${err}
|
|
361
|
+
`);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
return this.lastContent;
|
|
365
|
+
}
|
|
366
|
+
getContent() {
|
|
367
|
+
return this.lastContent;
|
|
368
|
+
}
|
|
369
|
+
getFilePath() {
|
|
370
|
+
return this.filePath;
|
|
371
|
+
}
|
|
372
|
+
async stop() {
|
|
373
|
+
if (this.watcher) {
|
|
374
|
+
await this.watcher.close();
|
|
375
|
+
this.watcher = null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/preview-server.ts
|
|
381
|
+
function findUiDir() {
|
|
382
|
+
const dir = import.meta.dirname ?? new URL(".", import.meta.url).pathname;
|
|
383
|
+
const candidates = [
|
|
384
|
+
join2(dir, "..", "ui"),
|
|
385
|
+
// dev: src/../ui
|
|
386
|
+
join2(dir, "..", "..", "ui")
|
|
387
|
+
// built: dist/bin/../../ui
|
|
388
|
+
];
|
|
389
|
+
return candidates.find((d) => {
|
|
390
|
+
try {
|
|
391
|
+
return existsSync(join2(d, "index.html"));
|
|
392
|
+
} catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}) ?? candidates[0];
|
|
396
|
+
}
|
|
397
|
+
var UI_DIR = findUiDir();
|
|
326
398
|
var MIME_TYPES = {
|
|
327
399
|
".html": "text/html",
|
|
328
400
|
".css": "text/css",
|
|
329
401
|
".js": "application/javascript"
|
|
330
402
|
};
|
|
331
|
-
|
|
403
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", ".cache", "coverage", "__pycache__"]);
|
|
404
|
+
function createPreviewServer(store, projectDir, port, initialFile) {
|
|
332
405
|
const clients = /* @__PURE__ */ new Set();
|
|
406
|
+
const clientFiles = /* @__PURE__ */ new Map();
|
|
407
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
408
|
+
const watcherListeners = /* @__PURE__ */ new Map();
|
|
409
|
+
const resolvedProjectDir = resolve4(projectDir);
|
|
410
|
+
function isInsideProject(filePath) {
|
|
411
|
+
const resolved = resolve4(resolvedProjectDir, filePath);
|
|
412
|
+
return resolved.startsWith(resolvedProjectDir);
|
|
413
|
+
}
|
|
414
|
+
async function getOrCreateWatcher(absPath) {
|
|
415
|
+
let watcher = watchers.get(absPath);
|
|
416
|
+
if (!watcher) {
|
|
417
|
+
watcher = new FileWatcher(absPath);
|
|
418
|
+
await watcher.start();
|
|
419
|
+
watchers.set(absPath, watcher);
|
|
420
|
+
const listener = (event) => {
|
|
421
|
+
store.adjustOffsets(event.file, event.oldContent, event.content);
|
|
422
|
+
const html = renderToHtml(event.content, event.file);
|
|
423
|
+
for (const [ws, file] of clientFiles) {
|
|
424
|
+
if (file === absPath) {
|
|
425
|
+
send(ws, { type: "file_update", file: event.file, content: event.content, html });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
watcher.on("change", listener);
|
|
430
|
+
watcherListeners.set(absPath, listener);
|
|
431
|
+
}
|
|
432
|
+
return watcher;
|
|
433
|
+
}
|
|
434
|
+
async function listFiles(dir, prefix = "") {
|
|
435
|
+
const files = [];
|
|
436
|
+
try {
|
|
437
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
|
|
440
|
+
const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
441
|
+
if (entry.isDirectory()) {
|
|
442
|
+
const sub = await listFiles(join2(dir, entry.name), relPath);
|
|
443
|
+
files.push(...sub);
|
|
444
|
+
} else {
|
|
445
|
+
files.push(relPath);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
return files;
|
|
451
|
+
}
|
|
333
452
|
const httpServer = createServer(async (req, res) => {
|
|
334
453
|
const url = new URL(req.url ?? "/", `http://localhost:${port}`);
|
|
335
454
|
const pathname = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
@@ -337,67 +456,104 @@ function createPreviewServer(store, watcher, port) {
|
|
|
337
456
|
const mimeType = MIME_TYPES[ext];
|
|
338
457
|
if (mimeType) {
|
|
339
458
|
try {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
uiDir = join2(import.meta.dirname, "..", "ui");
|
|
343
|
-
} else {
|
|
344
|
-
uiDir = join2(new URL(".", import.meta.url).pathname, "..", "ui");
|
|
345
|
-
}
|
|
346
|
-
const filePath = join2(uiDir, pathname);
|
|
347
|
-
const content = await readFile3(filePath, "utf-8");
|
|
459
|
+
const filePath = join2(UI_DIR, pathname);
|
|
460
|
+
const content = await readFile4(filePath, "utf-8");
|
|
348
461
|
res.writeHead(200, { "Content-Type": mimeType });
|
|
349
462
|
res.end(content);
|
|
350
463
|
return;
|
|
351
464
|
} catch {
|
|
352
465
|
}
|
|
353
466
|
}
|
|
354
|
-
if (pathname === "/api/
|
|
355
|
-
const
|
|
356
|
-
const html = renderToHtml(fileContent, watcher.getFilePath());
|
|
357
|
-
const comments = store.getForFile(watcher.getFilePath());
|
|
467
|
+
if (pathname === "/api/files") {
|
|
468
|
+
const files = await listFiles(resolvedProjectDir);
|
|
358
469
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
359
|
-
res.end(JSON.stringify({
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
470
|
+
res.end(JSON.stringify({ files }));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (pathname === "/api/state") {
|
|
474
|
+
const fileParam = url.searchParams.get("file");
|
|
475
|
+
if (!fileParam) {
|
|
476
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
477
|
+
res.end(JSON.stringify({ error: "Missing file parameter" }));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const absPath = resolve4(resolvedProjectDir, fileParam);
|
|
481
|
+
if (!isInsideProject(absPath)) {
|
|
482
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
483
|
+
res.end(JSON.stringify({ error: "Path outside project" }));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const content = await readFile4(absPath, "utf-8");
|
|
488
|
+
const html = renderToHtml(content, absPath);
|
|
489
|
+
const comments = store.getForFile(absPath);
|
|
490
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
491
|
+
res.end(JSON.stringify({ file: absPath, content, html, comments }));
|
|
492
|
+
} catch {
|
|
493
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
494
|
+
res.end(JSON.stringify({ error: "File not found" }));
|
|
495
|
+
}
|
|
365
496
|
return;
|
|
366
497
|
}
|
|
367
498
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
368
499
|
res.end("Not found");
|
|
369
500
|
});
|
|
370
501
|
const wss = new WebSocketServer({ server: httpServer });
|
|
371
|
-
wss.on("connection", (ws) => {
|
|
502
|
+
wss.on("connection", async (ws) => {
|
|
372
503
|
clients.add(ws);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
send(ws, { type: "comments_update", comments });
|
|
504
|
+
if (initialFile) {
|
|
505
|
+
const absPath = resolve4(resolvedProjectDir, initialFile);
|
|
506
|
+
await switchClientFile(ws, absPath);
|
|
507
|
+
}
|
|
378
508
|
ws.on("message", (data) => {
|
|
379
509
|
try {
|
|
380
510
|
const msg = JSON.parse(data.toString());
|
|
381
|
-
handleClientMessage(msg);
|
|
511
|
+
handleClientMessage(ws, msg);
|
|
382
512
|
} catch (err) {
|
|
383
513
|
send(ws, { type: "error", message: `Invalid message: ${err}` });
|
|
384
514
|
}
|
|
385
515
|
});
|
|
386
516
|
ws.on("close", () => {
|
|
387
517
|
clients.delete(ws);
|
|
518
|
+
clientFiles.delete(ws);
|
|
388
519
|
});
|
|
389
520
|
});
|
|
390
|
-
function
|
|
521
|
+
async function switchClientFile(ws, absPath) {
|
|
522
|
+
if (!isInsideProject(absPath)) {
|
|
523
|
+
send(ws, { type: "error", message: "Path outside project" });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const watcher = await getOrCreateWatcher(absPath);
|
|
528
|
+
clientFiles.set(ws, absPath);
|
|
529
|
+
const content = watcher.getContent();
|
|
530
|
+
const html = renderToHtml(content, absPath);
|
|
531
|
+
const comments = store.getForFile(absPath);
|
|
532
|
+
send(ws, { type: "file_update", file: absPath, content, html });
|
|
533
|
+
send(ws, { type: "comments_update", comments });
|
|
534
|
+
} catch (err) {
|
|
535
|
+
send(ws, { type: "error", message: `Cannot open file: ${err}` });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function handleClientMessage(ws, msg) {
|
|
391
539
|
switch (msg.type) {
|
|
392
|
-
case "
|
|
540
|
+
case "switch_file": {
|
|
541
|
+
const absPath = resolve4(resolvedProjectDir, msg.file);
|
|
542
|
+
switchClientFile(ws, absPath);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case "comment_add": {
|
|
546
|
+
const file = clientFiles.get(ws);
|
|
547
|
+
if (!file) break;
|
|
393
548
|
store.add({
|
|
394
|
-
file
|
|
549
|
+
file,
|
|
395
550
|
offset: msg.offset,
|
|
396
551
|
length: msg.length,
|
|
397
552
|
selectedText: msg.selectedText,
|
|
398
553
|
comment: msg.comment
|
|
399
554
|
});
|
|
400
555
|
break;
|
|
556
|
+
}
|
|
401
557
|
case "comment_reply":
|
|
402
558
|
store.addReply(msg.commentId, "user", msg.text);
|
|
403
559
|
break;
|
|
@@ -406,25 +562,19 @@ function createPreviewServer(store, watcher, port) {
|
|
|
406
562
|
break;
|
|
407
563
|
}
|
|
408
564
|
}
|
|
409
|
-
store.on("change", () => {
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
broadcast({ type: "file_update", file: event.file, content: event.content, html });
|
|
565
|
+
store.on("change", (comment) => {
|
|
566
|
+
for (const [ws, file] of clientFiles) {
|
|
567
|
+
if (!comment || comment.file === file) {
|
|
568
|
+
const comments = store.getForFile(file);
|
|
569
|
+
send(ws, { type: "comments_update", comments });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
417
572
|
});
|
|
418
573
|
function send(ws, msg) {
|
|
419
574
|
if (ws.readyState === ws.OPEN) {
|
|
420
575
|
ws.send(JSON.stringify(msg));
|
|
421
576
|
}
|
|
422
577
|
}
|
|
423
|
-
function broadcast(msg) {
|
|
424
|
-
for (const client of clients) {
|
|
425
|
-
send(client, msg);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
578
|
return {
|
|
429
579
|
start: () => new Promise((resolve6) => {
|
|
430
580
|
httpServer.listen(port, () => {
|
|
@@ -433,83 +583,53 @@ function createPreviewServer(store, watcher, port) {
|
|
|
433
583
|
resolve6();
|
|
434
584
|
});
|
|
435
585
|
}),
|
|
436
|
-
stop:
|
|
586
|
+
stop: async () => {
|
|
437
587
|
for (const client of clients) {
|
|
438
588
|
client.close();
|
|
439
589
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
590
|
+
for (const [path, watcher] of watchers) {
|
|
591
|
+
const listener = watcherListeners.get(path);
|
|
592
|
+
if (listener) watcher.off("change", listener);
|
|
593
|
+
await watcher.stop();
|
|
594
|
+
}
|
|
595
|
+
watchers.clear();
|
|
596
|
+
watcherListeners.clear();
|
|
597
|
+
await new Promise((resolvePromise, reject) => {
|
|
598
|
+
httpServer.close((err) => {
|
|
599
|
+
if (err) reject(err);
|
|
600
|
+
else resolvePromise();
|
|
601
|
+
});
|
|
443
602
|
});
|
|
444
|
-
}
|
|
603
|
+
}
|
|
445
604
|
};
|
|
446
605
|
}
|
|
447
606
|
|
|
448
|
-
// src/file-watcher.ts
|
|
449
|
-
import { watch } from "chokidar";
|
|
450
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
451
|
-
import { resolve as resolve4 } from "path";
|
|
452
|
-
import { EventEmitter as EventEmitter2 } from "events";
|
|
453
|
-
var FileWatcher = class extends EventEmitter2 {
|
|
454
|
-
watcher = null;
|
|
455
|
-
filePath;
|
|
456
|
-
lastContent = "";
|
|
457
|
-
constructor(filePath) {
|
|
458
|
-
super();
|
|
459
|
-
this.filePath = resolve4(filePath);
|
|
460
|
-
}
|
|
461
|
-
async start() {
|
|
462
|
-
this.lastContent = await readFile4(this.filePath, "utf-8");
|
|
463
|
-
this.watcher = watch(this.filePath, {
|
|
464
|
-
persistent: true,
|
|
465
|
-
ignoreInitial: true,
|
|
466
|
-
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
|
|
467
|
-
});
|
|
468
|
-
this.watcher.on("change", async () => {
|
|
469
|
-
try {
|
|
470
|
-
const newContent = await readFile4(this.filePath, "utf-8");
|
|
471
|
-
if (newContent !== this.lastContent) {
|
|
472
|
-
const oldContent = this.lastContent;
|
|
473
|
-
this.lastContent = newContent;
|
|
474
|
-
this.emit("change", {
|
|
475
|
-
file: this.filePath,
|
|
476
|
-
content: newContent,
|
|
477
|
-
oldContent
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
} catch (err) {
|
|
481
|
-
process.stderr.write(`File watch read error: ${err}
|
|
482
|
-
`);
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
return this.lastContent;
|
|
486
|
-
}
|
|
487
|
-
getContent() {
|
|
488
|
-
return this.lastContent;
|
|
489
|
-
}
|
|
490
|
-
getFilePath() {
|
|
491
|
-
return this.filePath;
|
|
492
|
-
}
|
|
493
|
-
async stop() {
|
|
494
|
-
if (this.watcher) {
|
|
495
|
-
await this.watcher.close();
|
|
496
|
-
this.watcher = null;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
607
|
// bin/cowrite.ts
|
|
502
608
|
var USAGE = `
|
|
503
609
|
cowrite \u2014 Live commenting plugin for coding agent sessions
|
|
504
610
|
|
|
505
611
|
Usage:
|
|
506
|
-
cowrite preview <file> [--port N] Open browser preview + start MCP server
|
|
507
|
-
cowrite serve
|
|
612
|
+
cowrite preview <file> [--port N] Open browser preview for a specific file + start MCP server
|
|
613
|
+
cowrite serve [--port N] Start MCP server + preview server (browse any file)
|
|
508
614
|
|
|
509
615
|
Options:
|
|
510
616
|
--port, -p Port for preview server (default: 3377)
|
|
511
617
|
--help, -h Show this help
|
|
512
618
|
`;
|
|
619
|
+
function setupShutdown(store, preview) {
|
|
620
|
+
let shuttingDown = false;
|
|
621
|
+
const shutdown = () => {
|
|
622
|
+
if (shuttingDown) {
|
|
623
|
+
process.exit(1);
|
|
624
|
+
}
|
|
625
|
+
shuttingDown = true;
|
|
626
|
+
process.stderr.write("Shutting down...\n");
|
|
627
|
+
Promise.allSettled([store.stopWatching(), preview.stop()]).finally(() => process.exit(0));
|
|
628
|
+
setTimeout(() => process.exit(0), 2e3);
|
|
629
|
+
};
|
|
630
|
+
process.on("SIGINT", shutdown);
|
|
631
|
+
process.on("SIGTERM", shutdown);
|
|
632
|
+
}
|
|
513
633
|
async function main() {
|
|
514
634
|
const { values, positionals } = parseArgs({
|
|
515
635
|
args: process.argv.slice(2),
|
|
@@ -526,14 +646,21 @@ async function main() {
|
|
|
526
646
|
}
|
|
527
647
|
const command = positionals[0];
|
|
528
648
|
const projectDir = process.cwd();
|
|
649
|
+
const port = parseInt(values.port, 10);
|
|
529
650
|
if (command === "serve") {
|
|
530
651
|
const store = new CommentStore(projectDir);
|
|
531
652
|
await store.load();
|
|
532
653
|
await store.startWatching();
|
|
654
|
+
const preview = createPreviewServer(store, projectDir, port);
|
|
655
|
+
await preview.start();
|
|
533
656
|
const mcpServer = createMcpServer(store, projectDir);
|
|
534
657
|
const transport = new StdioServerTransport();
|
|
535
658
|
await mcpServer.connect(transport);
|
|
536
|
-
process.stderr.write(
|
|
659
|
+
process.stderr.write(`Cowrite MCP server running on stdio
|
|
660
|
+
`);
|
|
661
|
+
process.stderr.write(`Preview: http://localhost:${port}
|
|
662
|
+
`);
|
|
663
|
+
setupShutdown(store, preview);
|
|
537
664
|
} else if (command === "preview") {
|
|
538
665
|
const filePath = positionals[1];
|
|
539
666
|
if (!filePath) {
|
|
@@ -542,13 +669,10 @@ async function main() {
|
|
|
542
669
|
process.exit(1);
|
|
543
670
|
}
|
|
544
671
|
const resolvedFile = resolve5(projectDir, filePath);
|
|
545
|
-
const port = parseInt(values.port, 10);
|
|
546
672
|
const store = new CommentStore(projectDir);
|
|
547
673
|
await store.load();
|
|
548
674
|
await store.startWatching();
|
|
549
|
-
const
|
|
550
|
-
await watcher.start();
|
|
551
|
-
const preview = createPreviewServer(store, watcher, port);
|
|
675
|
+
const preview = createPreviewServer(store, projectDir, port, resolvedFile);
|
|
552
676
|
await preview.start();
|
|
553
677
|
const mcpServer = createMcpServer(store, projectDir);
|
|
554
678
|
const transport = new StdioServerTransport();
|
|
@@ -557,15 +681,7 @@ async function main() {
|
|
|
557
681
|
`);
|
|
558
682
|
process.stderr.write(`Preview: http://localhost:${port}
|
|
559
683
|
`);
|
|
560
|
-
|
|
561
|
-
process.stderr.write("Shutting down...\n");
|
|
562
|
-
await store.stopWatching();
|
|
563
|
-
await watcher.stop();
|
|
564
|
-
await preview.stop();
|
|
565
|
-
process.exit(0);
|
|
566
|
-
};
|
|
567
|
-
process.on("SIGINT", shutdown);
|
|
568
|
-
process.on("SIGTERM", shutdown);
|
|
684
|
+
setupShutdown(store, preview);
|
|
569
685
|
} else {
|
|
570
686
|
process.stderr.write(`Unknown command: ${command}
|
|
571
687
|
`);
|
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\";\nimport { FileWatcher } from \"../src/file-watcher.js\";\n\nconst USAGE = `\ncowrite — Live commenting plugin for coding agent sessions\n\nUsage:\n cowrite preview <file> [--port N] Open browser preview + start MCP server\n cowrite serve MCP-only mode (stdio, no preview)\n\nOptions:\n --port, -p Port for preview server (default: 3377)\n --help, -h Show this help\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\n if (command === \"serve\") {\n // MCP-only mode\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n const mcpServer = createMcpServer(store, projectDir);\n const transport = new StdioServerTransport();\n await mcpServer.connect(transport);\n process.stderr.write(\"Cowrite MCP server running on stdio\\n\");\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 const port = parseInt(values.port as string, 10);\n\n const store = new CommentStore(projectDir);\n await store.load();\n await store.startWatching();\n\n // Start file watcher\n const watcher = new FileWatcher(resolvedFile);\n await watcher.start();\n\n // Start preview server\n const preview = createPreviewServer(store, watcher, 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 // Graceful shutdown\n const shutdown = async () => {\n process.stderr.write(\"Shutting down...\\n\");\n await store.stopWatching();\n await watcher.stop();\n await preview.stop();\n process.exit(0);\n };\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\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 } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { WebSocketServer, type WebSocket } from \"ws\";\nimport type { CommentStore } from \"./comment-store.js\";\nimport type { FileWatcher } from \"./file-watcher.js\";\nimport { renderToHtml } from \"./utils.js\";\nimport type { WSClientMessage, WSServerMessage } from \"./types.js\";\n\nconst UI_DIR = join(import.meta.dirname ?? new URL(\".\", import.meta.url).pathname, \"..\", \"ui\");\n\nconst MIME_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".css\": \"text/css\",\n \".js\": \"application/javascript\",\n};\n\nexport function createPreviewServer(\n store: CommentStore,\n watcher: FileWatcher,\n port: number\n): { start: () => Promise<void>; stop: () => Promise<void> } {\n const clients = new Set<WebSocket>();\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 // Resolve UI_DIR properly for both dev (tsx) and built (dist) modes\n let uiDir: string;\n if (import.meta.dirname) {\n uiDir = join(import.meta.dirname, \"..\", \"ui\");\n } else {\n uiDir = join(new URL(\".\", import.meta.url).pathname, \"..\", \"ui\");\n }\n const filePath = join(uiDir, 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/state — initial state for the client\n if (pathname === \"/api/state\") {\n const fileContent = watcher.getContent();\n const html = renderToHtml(fileContent, watcher.getFilePath());\n const comments = store.getForFile(watcher.getFilePath());\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({\n file: watcher.getFilePath(),\n content: fileContent,\n html,\n comments,\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\", (ws: WebSocket) => {\n clients.add(ws);\n\n // Send initial state\n const fileContent = watcher.getContent();\n const html = renderToHtml(fileContent, watcher.getFilePath());\n const comments = store.getForFile(watcher.getFilePath());\n\n send(ws, { type: \"file_update\", file: watcher.getFilePath(), content: fileContent, html });\n send(ws, { type: \"comments_update\", comments });\n\n ws.on(\"message\", (data) => {\n try {\n const msg: WSClientMessage = JSON.parse(data.toString());\n handleClientMessage(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 });\n });\n\n function handleClientMessage(msg: WSClientMessage): void {\n switch (msg.type) {\n case \"comment_add\":\n store.add({\n file: watcher.getFilePath(),\n offset: msg.offset,\n length: msg.length,\n selectedText: msg.selectedText,\n comment: msg.comment,\n });\n break;\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 updates when comments change\n store.on(\"change\", () => {\n const comments = store.getForFile(watcher.getFilePath());\n broadcast({ type: \"comments_update\", comments });\n });\n\n // Broadcast file changes\n watcher.on(\"change\", (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 broadcast({ type: \"file_update\", file: event.file, content: event.content, html });\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 function broadcast(msg: WSServerMessage): void {\n for (const client of clients) {\n send(client, 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: () =>\n new Promise<void>((resolvePromise, reject) => {\n for (const client of clients) {\n client.close();\n }\n httpServer.close((err) => {\n if (err) reject(err);\n else resolvePromise();\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","join","readFile","resolve","readFile","resolve","EventEmitter","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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@filipc77/cowrite",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Live commenting plugin for coding agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
"dist",
|
|
18
18
|
"ui"
|
|
19
19
|
],
|
|
20
|
-
"keywords": [
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"claude",
|
|
23
|
+
"comments",
|
|
24
|
+
"live-preview"
|
|
25
|
+
],
|
|
21
26
|
"license": "MIT",
|
|
22
27
|
"dependencies": {
|
|
23
28
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
package/ui/client.js
CHANGED
|
@@ -12,6 +12,8 @@ const statusEl = $("#status");
|
|
|
12
12
|
const popup = $("#commentPopup");
|
|
13
13
|
const popupSelection = $("#popupSelection");
|
|
14
14
|
const commentInput = $("#commentInput");
|
|
15
|
+
const filePicker = $("#filePicker");
|
|
16
|
+
const fileList = $("#fileList");
|
|
15
17
|
|
|
16
18
|
/** @type {Comment[]} */
|
|
17
19
|
let comments = [];
|
|
@@ -20,6 +22,45 @@ let currentContent = "";
|
|
|
20
22
|
let ws = null;
|
|
21
23
|
let selectionInfo = null;
|
|
22
24
|
|
|
25
|
+
// --- File Picker ---
|
|
26
|
+
|
|
27
|
+
async function loadFileList() {
|
|
28
|
+
try {
|
|
29
|
+
const res = await fetch("/api/files");
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
fileList.innerHTML = "";
|
|
32
|
+
for (const file of data.files) {
|
|
33
|
+
const option = document.createElement("option");
|
|
34
|
+
option.value = file;
|
|
35
|
+
fileList.appendChild(option);
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Will retry on reconnect
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function switchFile(file) {
|
|
43
|
+
if (!file || !ws || ws.readyState !== WebSocket.OPEN) return;
|
|
44
|
+
send({ type: "switch_file", file });
|
|
45
|
+
filePicker.value = "";
|
|
46
|
+
// Update URL without reload
|
|
47
|
+
const url = new URL(location.href);
|
|
48
|
+
url.searchParams.set("file", file);
|
|
49
|
+
history.replaceState(null, "", url.toString());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
filePicker.addEventListener("change", () => {
|
|
53
|
+
const file = filePicker.value.trim();
|
|
54
|
+
if (file) switchFile(file);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
filePicker.addEventListener("keydown", (e) => {
|
|
58
|
+
if (e.key === "Enter") {
|
|
59
|
+
const file = filePicker.value.trim();
|
|
60
|
+
if (file) switchFile(file);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
23
64
|
// --- WebSocket ---
|
|
24
65
|
|
|
25
66
|
function connect() {
|
|
@@ -29,6 +70,15 @@ function connect() {
|
|
|
29
70
|
ws.onopen = () => {
|
|
30
71
|
statusEl.textContent = "Connected";
|
|
31
72
|
statusEl.className = "status connected";
|
|
73
|
+
|
|
74
|
+
// If URL has ?file= param, switch to that file
|
|
75
|
+
const params = new URLSearchParams(location.search);
|
|
76
|
+
const fileParam = params.get("file");
|
|
77
|
+
if (fileParam) {
|
|
78
|
+
send({ type: "switch_file", file: fileParam });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
loadFileList();
|
|
32
82
|
};
|
|
33
83
|
|
|
34
84
|
ws.onclose = () => {
|
package/ui/index.html
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<header>
|
|
11
11
|
<h1>Cowrite</h1>
|
|
12
|
-
<
|
|
12
|
+
<div class="file-picker" id="filePickerWrapper">
|
|
13
|
+
<input type="text" id="filePicker" list="fileList" placeholder="Open file..." autocomplete="off" />
|
|
14
|
+
<datalist id="fileList"></datalist>
|
|
15
|
+
</div>
|
|
16
|
+
<span class="file-path" id="filePath"></span>
|
|
13
17
|
<span class="status" id="status">Connecting...</span>
|
|
14
18
|
</header>
|
|
15
19
|
|
package/ui/styles.css
CHANGED
|
@@ -46,6 +46,31 @@ header h1 {
|
|
|
46
46
|
letter-spacing: 0.5px;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
.file-picker {
|
|
50
|
+
position: relative;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.file-picker input {
|
|
54
|
+
font-family: var(--font-mono);
|
|
55
|
+
font-size: 13px;
|
|
56
|
+
color: var(--text);
|
|
57
|
+
background: var(--bg);
|
|
58
|
+
border: 1px solid var(--border);
|
|
59
|
+
border-radius: 6px;
|
|
60
|
+
padding: 5px 10px;
|
|
61
|
+
width: 280px;
|
|
62
|
+
outline: none;
|
|
63
|
+
transition: border-color 0.15s;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.file-picker input:focus {
|
|
67
|
+
border-color: var(--accent);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.file-picker input::placeholder {
|
|
71
|
+
color: var(--text-dim);
|
|
72
|
+
}
|
|
73
|
+
|
|
49
74
|
.file-path {
|
|
50
75
|
font-family: var(--font-mono);
|
|
51
76
|
font-size: 13px;
|