@peaske7/readit 0.1.8 → 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.
Files changed (221) hide show
  1. package/.claude/CLAUDE.md +118 -76
  2. package/.claude/commands/review.md +1 -1
  3. package/.claude/roadmap.md +32 -9
  4. package/.claude/user-stories.md +100 -15
  5. package/AGENTS.md +30 -26
  6. package/Makefile +32 -0
  7. package/README.md +90 -5
  8. package/biome.json +18 -8
  9. package/bun.lock +426 -710
  10. package/bunfig.toml +2 -0
  11. package/docs/perf-baseline.md +130 -0
  12. package/docs/superpowers/plans/2026-03-26-surgical-pruning.md +1176 -0
  13. package/docs/superpowers/specs/2026-03-27-go-server-rewrite-design.md +284 -0
  14. package/e2e/comments.spec.ts +14 -58
  15. package/e2e/document-load.spec.ts +1 -23
  16. package/e2e/export.spec.ts +4 -4
  17. package/e2e/perf/add-comment.spec.ts +116 -0
  18. package/e2e/perf/fixtures/generate.ts +327 -0
  19. package/e2e/perf/initial-load.spec.ts +49 -0
  20. package/e2e/perf/perf.setup.ts +23 -0
  21. package/e2e/perf/perf.teardown.ts +9 -0
  22. package/e2e/perf/screenshot-final.png +0 -0
  23. package/e2e/perf/scroll.spec.ts +39 -0
  24. package/e2e/perf/tab-switch.spec.ts +69 -0
  25. package/e2e/perf/text-selection.spec.ts +119 -0
  26. package/e2e/perf/utils/metrics.ts +350 -0
  27. package/e2e/perf/utils/perf-cli.ts +86 -0
  28. package/e2e/persistence-file.spec.ts +41 -26
  29. package/e2e/utils/selection.ts +17 -73
  30. package/go/cmd/readit/main.go +416 -0
  31. package/go/go.mod +20 -0
  32. package/go/go.sum +41 -0
  33. package/go/internal/server/anchor.go +302 -0
  34. package/go/internal/server/anchor_test.go +111 -0
  35. package/go/internal/server/comments.go +390 -0
  36. package/go/internal/server/documents.go +113 -0
  37. package/go/internal/server/embed.go +17 -0
  38. package/go/internal/server/headings.go +33 -0
  39. package/go/internal/server/headings_test.go +75 -0
  40. package/go/internal/server/htmltext.go +123 -0
  41. package/go/internal/server/markdown.go +157 -0
  42. package/go/internal/server/markdown_bench_test.go +42 -0
  43. package/go/internal/server/markdown_test.go +79 -0
  44. package/go/internal/server/server.go +453 -0
  45. package/go/internal/server/server_bench_test.go +122 -0
  46. package/go/internal/server/settings.go +110 -0
  47. package/go/internal/server/sse.go +140 -0
  48. package/go/internal/server/storage.go +275 -0
  49. package/go/internal/server/storage_test.go +118 -0
  50. package/go/internal/server/template.go +66 -0
  51. package/go/internal/server/types.go +101 -0
  52. package/go/internal/server/watcher.go +74 -0
  53. package/index.html +4 -14
  54. package/nvim-readit/lua/readit/health.lua +64 -0
  55. package/nvim-readit/lua/readit/init.lua +463 -0
  56. package/nvim-readit/plugin/readit.lua +19 -0
  57. package/package.json +24 -41
  58. package/playwright.config.ts +12 -0
  59. package/shell/_readit +158 -0
  60. package/shell/readit.zsh +87 -0
  61. package/src/App.svelte +881 -0
  62. package/src/{cli/index.ts → cli.ts} +216 -70
  63. package/src/components/ActionsMenu.svelte +95 -0
  64. package/src/components/CommentBadge.svelte +67 -0
  65. package/src/components/CommentErrorBanner.svelte +33 -0
  66. package/src/components/CommentInput.svelte +75 -0
  67. package/src/components/CommentListItem.svelte +95 -0
  68. package/src/components/CommentManager.svelte +129 -0
  69. package/src/components/CommentNav.svelte +109 -0
  70. package/src/components/DocumentViewer.svelte +218 -0
  71. package/src/components/FloatingComment.svelte +107 -0
  72. package/src/components/Header.svelte +76 -0
  73. package/src/components/InlineEditor.svelte +72 -0
  74. package/src/components/MarginNote.svelte +167 -0
  75. package/src/components/MarginNotesContainer.svelte +33 -0
  76. package/src/components/RawModal.svelte +126 -0
  77. package/src/components/ReanchorConfirm.svelte +30 -0
  78. package/src/components/SettingsModal.svelte +220 -0
  79. package/src/components/ShortcutCapture.svelte +82 -0
  80. package/src/components/ShortcutList.svelte +145 -0
  81. package/src/components/TabBar.svelte +52 -0
  82. package/src/components/TableOfContents.svelte +125 -0
  83. package/src/components/ui/ActionLink.svelte +40 -0
  84. package/src/components/ui/Button.svelte +53 -0
  85. package/src/components/ui/Dialog.svelte +97 -0
  86. package/src/components/ui/DropdownMenu.svelte +85 -0
  87. package/src/components/ui/DropdownMenuItem.svelte +38 -0
  88. package/src/components/ui/DropdownMenuSeparator.svelte +11 -0
  89. package/src/components/ui/Text.svelte +42 -0
  90. package/src/env.d.ts +6 -0
  91. package/src/index.css +36 -166
  92. package/src/lib/__fixtures__/bench-data.ts +1 -54
  93. package/src/lib/anchor.bench.ts +47 -68
  94. package/src/lib/anchor.test.ts +5 -9
  95. package/src/lib/anchor.ts +9 -93
  96. package/src/lib/comment-storage.bench.ts +6 -20
  97. package/src/lib/comment-storage.test.ts +45 -37
  98. package/src/lib/comment-storage.ts +23 -64
  99. package/src/lib/export.bench.ts +9 -23
  100. package/src/lib/export.ts +7 -14
  101. package/src/lib/headings.test.ts +103 -0
  102. package/src/lib/headings.ts +44 -0
  103. package/src/lib/highlight/core.test.ts +1 -6
  104. package/src/lib/highlight/dom.ts +53 -280
  105. package/src/lib/highlight/highlight-registry.ts +221 -0
  106. package/src/lib/highlight/highlight.bench.ts +92 -0
  107. package/src/lib/highlight/highlighter.ts +122 -302
  108. package/src/lib/highlight/{core.ts → resolver.ts} +3 -19
  109. package/src/lib/highlight/types.ts +0 -40
  110. package/src/lib/html-text.test.ts +162 -0
  111. package/src/lib/html-text.ts +161 -0
  112. package/src/lib/i18n/en.ts +13 -36
  113. package/src/lib/i18n/ja.ts +14 -37
  114. package/src/lib/i18n/types.ts +13 -36
  115. package/src/lib/margin-layout.bench.ts +48 -15
  116. package/src/lib/margin-layout.ts +2 -31
  117. package/src/lib/markdown-renderer.test.ts +154 -0
  118. package/src/lib/markdown-renderer.ts +177 -0
  119. package/src/lib/mermaid-config.ts +38 -0
  120. package/src/lib/mermaid-renderer.ts +162 -0
  121. package/src/lib/mermaid-worker.ts +60 -0
  122. package/src/lib/positions.ts +157 -0
  123. package/src/lib/shortcut-registry.ts +138 -103
  124. package/src/lib/utils.ts +2 -48
  125. package/src/main.ts +16 -0
  126. package/src/schema.ts +92 -0
  127. package/src/{server/index.ts → server.ts} +427 -163
  128. package/src/stores/app.svelte.ts +231 -0
  129. package/src/stores/locale.svelte.ts +46 -0
  130. package/src/stores/settings.svelte.ts +90 -0
  131. package/src/stores/shortcuts.svelte.ts +104 -0
  132. package/src/stores/ui.svelte.ts +12 -0
  133. package/src/template.ts +104 -0
  134. package/src/test-setup.ts +47 -0
  135. package/svelte.config.js +5 -0
  136. package/tsconfig.json +2 -2
  137. package/vite.config.ts +31 -3
  138. package/vscode-readit/.mcp.json +7 -0
  139. package/vscode-readit/.vscodeignore +7 -0
  140. package/vscode-readit/bun.lock +78 -0
  141. package/vscode-readit/icon.svg +10 -0
  142. package/vscode-readit/package.json +110 -0
  143. package/vscode-readit/src/extension.ts +117 -0
  144. package/vscode-readit/src/server-manager.ts +272 -0
  145. package/vscode-readit/src/webview-provider.ts +204 -0
  146. package/vscode-readit/tsconfig.json +20 -0
  147. package/e2e/fixtures/sample.html +0 -13
  148. package/src/App.tsx +0 -416
  149. package/src/components/ActionsMenu.tsx +0 -112
  150. package/src/components/DocumentViewer/CodeBlock.tsx +0 -160
  151. package/src/components/DocumentViewer/DocumentViewer.tsx +0 -259
  152. package/src/components/DocumentViewer/IframeContainer.tsx +0 -251
  153. package/src/components/DocumentViewer/InlineCode.tsx +0 -60
  154. package/src/components/DocumentViewer/MermaidDiagram.tsx +0 -137
  155. package/src/components/DocumentViewer/index.ts +0 -1
  156. package/src/components/FloatingTOC.tsx +0 -61
  157. package/src/components/Header.tsx +0 -65
  158. package/src/components/InlineEditor.tsx +0 -74
  159. package/src/components/MarginNote.tsx +0 -207
  160. package/src/components/MarginNotes.tsx +0 -50
  161. package/src/components/RawModal.tsx +0 -143
  162. package/src/components/ReanchorConfirm.tsx +0 -36
  163. package/src/components/SettingsModal.tsx +0 -310
  164. package/src/components/ShortcutCapture.tsx +0 -48
  165. package/src/components/ShortcutList.tsx +0 -198
  166. package/src/components/TabBar.tsx +0 -60
  167. package/src/components/TableOfContents.tsx +0 -108
  168. package/src/components/comments/CommentBadge.tsx +0 -49
  169. package/src/components/comments/CommentInput.tsx +0 -114
  170. package/src/components/comments/CommentListItem.tsx +0 -92
  171. package/src/components/comments/CommentManager.tsx +0 -113
  172. package/src/components/comments/CommentMinimap.tsx +0 -62
  173. package/src/components/comments/CommentNav.tsx +0 -109
  174. package/src/components/ui/ActionBar.tsx +0 -16
  175. package/src/components/ui/ActionLink.tsx +0 -32
  176. package/src/components/ui/Button.tsx +0 -55
  177. package/src/components/ui/Dialog.tsx +0 -156
  178. package/src/components/ui/DropdownMenu.tsx +0 -114
  179. package/src/components/ui/SeparatorDot.tsx +0 -9
  180. package/src/components/ui/Text.tsx +0 -54
  181. package/src/contexts/CommentContext.tsx +0 -229
  182. package/src/contexts/LayoutContext.tsx +0 -88
  183. package/src/contexts/LocaleContext.tsx +0 -35
  184. package/src/hooks/useClickOutside.ts +0 -35
  185. package/src/hooks/useClipboard.ts +0 -82
  186. package/src/hooks/useCommentNavigation.ts +0 -130
  187. package/src/hooks/useComments.ts +0 -323
  188. package/src/hooks/useDocument.ts +0 -156
  189. package/src/hooks/useEditorScheme.ts +0 -51
  190. package/src/hooks/useFontPreference.ts +0 -59
  191. package/src/hooks/useHeadings.test.ts +0 -159
  192. package/src/hooks/useHeadings.ts +0 -129
  193. package/src/hooks/useKeybindings.ts +0 -108
  194. package/src/hooks/useKeyboardShortcuts.ts +0 -63
  195. package/src/hooks/useLayoutMode.ts +0 -44
  196. package/src/hooks/useLocalePreference.ts +0 -42
  197. package/src/hooks/useReanchorMode.ts +0 -33
  198. package/src/hooks/useScrollMetrics.ts +0 -56
  199. package/src/hooks/useScrollSpy.ts +0 -81
  200. package/src/hooks/useTextSelection.ts +0 -123
  201. package/src/hooks/useThemePreference.ts +0 -66
  202. package/src/lib/context.bench.ts +0 -41
  203. package/src/lib/context.test.ts +0 -224
  204. package/src/lib/context.ts +0 -193
  205. package/src/lib/editor-links.ts +0 -59
  206. package/src/lib/highlight/colors.ts +0 -37
  207. package/src/lib/highlight/index.ts +0 -23
  208. package/src/lib/highlight/script-builder.ts +0 -485
  209. package/src/lib/html-processor.test.tsx +0 -170
  210. package/src/lib/html-processor.tsx +0 -95
  211. package/src/lib/i18n/completeness.test.ts +0 -51
  212. package/src/lib/i18n/translations.test.ts +0 -39
  213. package/src/lib/layout-constants.ts +0 -12
  214. package/src/lib/scroll.test.ts +0 -118
  215. package/src/lib/scroll.ts +0 -47
  216. package/src/lib/shortcut-registry.test.ts +0 -173
  217. package/src/lib/utils.test.ts +0 -110
  218. package/src/main.tsx +0 -13
  219. package/src/store/index.test.ts +0 -242
  220. package/src/store/index.ts +0 -254
  221. package/src/types/index.ts +0 -127
@@ -13,11 +13,10 @@ import * as os from "node:os";
13
13
  import { join, resolve } from "node:path";
14
14
  import { Command } from "commander";
15
15
  import open from "open";
16
- import { getCommentPath, parseCommentFile } from "../lib/comment-storage.js";
17
- import { getFileType } from "../lib/utils.js";
18
- import type { FileEntry } from "../server/index.js";
19
- import { removeServerInfo, startServer } from "../server/index.js";
20
- import type { DocumentType } from "../types/index.js";
16
+ import { getCommentPath, parseCommentFile } from "./lib/comment-storage.js";
17
+ import { isMarkdownFile } from "./lib/utils.js";
18
+ import type { FileEntry } from "./server.js";
19
+ import { removeServerInfo, startServer } from "./server.js";
21
20
 
22
21
  const program = new Command();
23
22
 
@@ -81,9 +80,7 @@ async function clearStaleServerLock(): Promise<void> {
81
80
  try {
82
81
  const lock = JSON.parse(content) as { pid?: number };
83
82
  pid = lock.pid;
84
- } catch {
85
- // Ignore malformed lock files and fall back to age-based cleanup.
86
- }
83
+ } catch {}
87
84
  }
88
85
 
89
86
  if (age > SERVER_LOCK_MAX_AGE_MS || (pid !== undefined && !isAlive(pid))) {
@@ -139,12 +136,10 @@ async function discoverServer(): Promise<ServerInfo | null> {
139
136
  const content = readFileSync(SERVER_INFO_PATH, "utf-8");
140
137
  const info: ServerInfo = JSON.parse(content);
141
138
 
142
- // Verify the process is alive
143
139
  if (!isAlive(info.pid)) {
144
140
  return null;
145
141
  }
146
142
 
147
- // Verify health endpoint responds
148
143
  try {
149
144
  const res = await fetch(`http://127.0.0.1:${info.port}/api/health`);
150
145
  if (!res.ok) return null;
@@ -160,7 +155,7 @@ async function discoverServer(): Promise<ServerInfo | null> {
160
155
 
161
156
  async function attachFiles(
162
157
  server: ServerInfo,
163
- files: { path: string; type: DocumentType }[],
158
+ files: { path: string }[],
164
159
  ): Promise<void> {
165
160
  for (const file of files) {
166
161
  try {
@@ -178,9 +173,9 @@ async function attachFiles(
178
173
 
179
174
  const data = await res.json();
180
175
  if (data.status === "added") {
181
- console.log(`Added: ${data.fileName} (${data.type})`);
176
+ console.log(`Added: ${data.fileName}`);
182
177
  } else {
183
- console.log(`Present: ${data.fileName} (${data.type})`);
178
+ console.log(`Present: ${data.fileName}`);
184
179
  }
185
180
  } catch (err) {
186
181
  console.error(
@@ -217,9 +212,6 @@ async function getServerTarget(
217
212
  });
218
213
  }
219
214
 
220
- /**
221
- * Recursively find all .comments.md files in a directory.
222
- */
223
215
  function findCommentFiles(dir: string): string[] {
224
216
  const results: string[] = [];
225
217
 
@@ -250,16 +242,12 @@ function findCommentFiles(dir: string): string[] {
250
242
  return results;
251
243
  }
252
244
 
253
- /**
254
- * Recursively find reviewable files (.md, .markdown, .html, .htm) in a directory.
255
- */
256
245
  function findReviewableFiles(dir: string): FileEntry[] {
257
246
  const results: FileEntry[] = [];
258
247
 
259
248
  try {
260
249
  const entries = readdirSync(dir);
261
250
  for (const entry of entries) {
262
- // Skip hidden directories and node_modules
263
251
  if (entry.startsWith(".") || entry === "node_modules") continue;
264
252
 
265
253
  const fullPath = join(dir, entry);
@@ -268,14 +256,8 @@ function findReviewableFiles(dir: string): FileEntry[] {
268
256
  if (lstat.isSymbolicLink()) continue;
269
257
  if (lstat.isDirectory()) {
270
258
  results.push(...findReviewableFiles(fullPath));
271
- } else {
272
- const type = getFileType(entry);
273
- if (type) {
274
- results.push({
275
- type,
276
- filePath: fullPath,
277
- });
278
- }
259
+ } else if (isMarkdownFile(entry)) {
260
+ results.push({ filePath: fullPath });
279
261
  }
280
262
  } catch (err) {
281
263
  if (isPermissionError(err)) {
@@ -292,9 +274,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
292
274
  return results;
293
275
  }
294
276
 
295
- /**
296
- * Resolve CLI arguments into a deduplicated list of FileEntry objects.
297
- */
298
277
  function resolveFiles(args: string[]): FileEntry[] {
299
278
  const seen = new Set<string>();
300
279
  const files: FileEntry[] = [];
@@ -322,27 +301,21 @@ function resolveFiles(args: string[]): FileEntry[] {
322
301
  } else {
323
302
  if (seen.has(filePath)) continue;
324
303
 
325
- const type = getFileType(filePath);
326
- if (!type) {
304
+ if (!isMarkdownFile(filePath)) {
327
305
  console.error(
328
- `error: unsupported file type: ${arg} (expected .md, .markdown, .html, or .htm)`,
306
+ `error: unsupported file type: ${arg} (expected .md or .markdown)`,
329
307
  );
330
308
  process.exit(1);
331
309
  }
332
310
 
333
311
  seen.add(filePath);
334
- files.push({
335
- type,
336
- filePath,
337
- });
312
+ files.push({ filePath });
338
313
  }
339
314
  }
340
315
 
341
316
  return files;
342
317
  }
343
318
 
344
- // ─── Onboarding ──────────────────────────────────────────────────────
345
-
346
319
  const SETTINGS_PATH = join(os.homedir(), ".readit", "settings.json");
347
320
 
348
321
  function isOnboarded(): boolean {
@@ -360,9 +333,7 @@ async function markOnboarded(): Promise<void> {
360
333
  try {
361
334
  const content = readFileSync(SETTINGS_PATH, "utf-8");
362
335
  settings = JSON.parse(content);
363
- } catch {
364
- // No existing settings
365
- }
336
+ } catch {}
366
337
  settings.onboarded = true;
367
338
  const dir = join(os.homedir(), ".readit");
368
339
  await fs.mkdir(dir, { recursive: true });
@@ -447,14 +418,11 @@ Go ahead and add a few comments to this document. When you're done, export them
447
418
 
448
419
  const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
449
420
 
450
- // ─── Program ─────────────────────────────────────────────────────────
451
-
452
421
  program
453
422
  .name("readit")
454
- .description("Review Markdown and HTML documents with inline comments")
455
- .version("0.1.3");
423
+ .description("Review Markdown documents with inline comments")
424
+ .version("0.2.0");
456
425
 
457
- // List command: show all commented files
458
426
  program
459
427
  .command("list")
460
428
  .description("List all files with comments")
@@ -487,13 +455,10 @@ program
487
455
  ` ${commentCount} comment${commentCount !== 1 ? "s" : ""}`,
488
456
  );
489
457
  console.log();
490
- } catch {
491
- // Skip unreadable files
492
- }
458
+ } catch {}
493
459
  }
494
460
  });
495
461
 
496
- // Show command: display comments for a file
497
462
  program
498
463
  .command("show <file>")
499
464
  .description("Show comments for a file")
@@ -525,7 +490,6 @@ program
525
490
  `Selected: "${comment.selectedText.slice(0, 80)}${comment.selectedText.length > 80 ? "..." : ""}"`,
526
491
  );
527
492
  console.log(`Comment: ${comment.comment}`);
528
- console.log(`Created: ${comment.createdAt}`);
529
493
  console.log();
530
494
  }
531
495
  } catch (err) {
@@ -537,9 +501,8 @@ program
537
501
  }
538
502
  });
539
503
 
540
- // Main review command (default) — accepts zero or more files/directories
541
504
  program
542
- .argument("[files...]", "Markdown or HTML files/directories to review")
505
+ .argument("[files...]", "Markdown files/directories to review")
543
506
  .option("-p, --port <number>", "Port to run server on", "4567")
544
507
  .option("--host <address>", "Host address to bind to", "127.0.0.1")
545
508
  .option("--no-open", "Don't automatically open browser")
@@ -563,7 +526,6 @@ program
563
526
  files = [
564
527
  {
565
528
  content: WELCOME_CONTENT,
566
- type: "markdown" as DocumentType,
567
529
  filePath: WELCOME_PATH,
568
530
  },
569
531
  ];
@@ -588,6 +550,14 @@ program
588
550
  process.exit(1);
589
551
  }
590
552
 
553
+ let previousPort: number | undefined;
554
+ try {
555
+ const info = JSON.parse(readFileSync(SERVER_INFO_PATH, "utf-8"));
556
+ if (!isAlive(info.pid)) {
557
+ previousPort = info.port;
558
+ }
559
+ } catch {}
560
+
591
561
  try {
592
562
  const { url, server } = await startServer({
593
563
  files,
@@ -608,7 +578,7 @@ readit - Document Review Tool
608
578
  Server running. Press Ctrl+C to stop.
609
579
  `);
610
580
  } else {
611
- const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
581
+ const fileList = files.map((f) => ` ${f.filePath}`);
612
582
 
613
583
  console.log(`
614
584
  readit - Document Review Tool
@@ -622,16 +592,18 @@ ${fileList.join("\n")}
622
592
  `);
623
593
  }
624
594
 
625
- if (options.open) {
595
+ const browserLikelyOpen =
596
+ previousPort === preferredPort ||
597
+ process.env.NODE_ENV === "development";
598
+
599
+ if (options.open && !browserLikelyOpen) {
626
600
  open(url);
627
601
  }
628
602
 
629
- // Mark onboarding complete on first server start
630
603
  if (fileArgs.length === 0) {
631
604
  await markOnboarded();
632
605
  }
633
606
 
634
- // Graceful shutdown on Ctrl+C
635
607
  process.on("SIGINT", async () => {
636
608
  console.log("\n\nShutting down...");
637
609
  server.stop();
@@ -648,17 +620,15 @@ ${fileList.join("\n")}
648
620
  },
649
621
  );
650
622
 
651
- // Open command: add files to running server or start new one
652
623
  program
653
624
  .command("open")
654
- .argument("<files...>", "Markdown or HTML files to add to running server")
625
+ .argument("<files...>", "Markdown files to add to running server")
655
626
  .description("Add files to a running readit server, or start a new one")
656
627
  .option("-p, --port <number>", "Port for new server (if starting)", "4567")
657
628
  .option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
658
629
  .action(
659
630
  async (fileArgs: string[], options: { port: string; host: string }) => {
660
- // Resolve and validate files
661
- const resolvedFiles: { path: string; type: DocumentType }[] = [];
631
+ const resolvedFiles: { path: string }[] = [];
662
632
  for (const arg of fileArgs) {
663
633
  const inputPath = resolve(process.cwd(), arg);
664
634
 
@@ -669,19 +639,17 @@ program
669
639
 
670
640
  const filePath = realpathSync(inputPath);
671
641
 
672
- const type = getFileType(filePath);
673
- if (!type) {
642
+ if (!isMarkdownFile(filePath)) {
674
643
  console.error(
675
- `error: unsupported file type: ${arg} (expected .md, .markdown, .html, or .htm)`,
644
+ `error: unsupported file type: ${arg} (expected .md or .markdown)`,
676
645
  );
677
646
  process.exit(1);
678
647
  }
679
648
 
680
- resolvedFiles.push({ path: filePath, type });
649
+ resolvedFiles.push({ path: filePath });
681
650
  }
682
651
 
683
652
  const files = resolvedFiles.map((f) => ({
684
- type: f.type,
685
653
  filePath: f.path,
686
654
  }));
687
655
 
@@ -702,7 +670,7 @@ program
702
670
  return;
703
671
  }
704
672
 
705
- const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
673
+ const fileList = files.map((f) => ` ${f.filePath}`);
706
674
  console.log(`
707
675
  readit - Document Review Tool
708
676
 
@@ -732,4 +700,182 @@ ${fileList.join("\n")}
732
700
  },
733
701
  );
734
702
 
703
+ program
704
+ .command("completion")
705
+ .argument("[shell]", "Shell type (zsh, bash, fish)", "zsh")
706
+ .description("Output shell completion and integration script")
707
+ .action((shell: string) => {
708
+ const shellDir = join(import.meta.dir, "..", "shell");
709
+
710
+ switch (shell) {
711
+ case "zsh": {
712
+ // Output the full zsh integration:
713
+ // 1. _readit compdef (loaded into fpath via autoload) - handles @ prefix
714
+ // 2. readit.zsh widget (accept-line bracket stripping + syntax highlighting)
715
+ const widgetPath = join(shellDir, "readit.zsh");
716
+ const compPath = join(shellDir, "_readit");
717
+
718
+ if (!existsSync(widgetPath) || !existsSync(compPath)) {
719
+ console.log(generateInlineZshCompletion());
720
+ return;
721
+ }
722
+
723
+ // Wrap the compdef in an autoload function so eval works cleanly
724
+ const compdefContent = readFileSync(compPath, "utf-8");
725
+ const widgetContent = readFileSync(widgetPath, "utf-8");
726
+
727
+ const lines: string[] = [];
728
+ lines.push("# readit shell integration for zsh");
729
+ lines.push('# Add to your .zshrc: eval "$(readit completion zsh)"');
730
+ lines.push("");
731
+ lines.push("# ── _readit compdef (autoloaded) ──");
732
+ lines.push(
733
+ "# This handles: subcommand/option completion + @ file autocomplete",
734
+ );
735
+ lines.push(
736
+ "# Renders [file.md] in a native multi-column grid via compadd",
737
+ );
738
+ lines.push("");
739
+ // Replace #compdef with autoload -Uz _readit; _readit() { ... }
740
+ lines.push(
741
+ compdefContent
742
+ .replace(
743
+ /^#compdef readit\n/,
744
+ "autoload -Uz _readit\n_readit() {\n",
745
+ )
746
+ .replace(/\n_readit "\$@"\n?$/, "\n}\n"),
747
+ );
748
+ lines.push("");
749
+ lines.push("# ── readit.zsh (sourced) ──");
750
+ lines.push(
751
+ "# This handles: @[...] bracket stripping on Enter + syntax highlighting",
752
+ );
753
+ lines.push("");
754
+ // Strip the shebang and guard from the widget since it's being eval'd
755
+ lines.push(
756
+ widgetContent
757
+ .replace(/^#!/, "#")
758
+ .replace(/\n\(\( \$\+ functions\[_readit_plugin_loaded\] \)\)/, ""),
759
+ );
760
+ console.log(lines.join("\n"));
761
+ break;
762
+ }
763
+ case "bash":
764
+ console.log(generateBashCompletion());
765
+ break;
766
+ case "fish":
767
+ console.log(generateFishCompletion());
768
+ break;
769
+ default:
770
+ console.error(`error: unsupported shell: ${shell}`);
771
+ console.error("Supported shells: zsh, bash, fish");
772
+ process.exit(1);
773
+ }
774
+ });
775
+
735
776
  program.parse();
777
+
778
+ function generateInlineZshCompletion(): string {
779
+ return `
780
+ #compdef readit
781
+
782
+ _readit_markdown_files() {
783
+ local -a files
784
+ files=( \${(f)"$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) -not -path '*/\\.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')"} )
785
+ _describe -t files 'markdown files' files
786
+ }
787
+
788
+ _readit() {
789
+ local context state state_descr line
790
+ typeset -A opt_args
791
+ _arguments -C '1:command:->cmd_or_files' '*::arg:->args'
792
+ case "$state" in
793
+ cmd_or_files)
794
+ local -a commands=(
795
+ 'open:Add files to running server'
796
+ 'list:List files with comments'
797
+ 'show:Show comments for a file'
798
+ 'completion:Output shell completion script'
799
+ )
800
+ _alternative 'commands:command:compadd -a commands' 'files:markdown file:_readit_markdown_files'
801
+ ;;
802
+ args)
803
+ case "\${line[1]}" in
804
+ open) _arguments '*:file:_readit_markdown_files' ;;
805
+ show) _arguments '1:file:_files -g "*.md *.markdown"' ;;
806
+ *) _arguments '*:file:_readit_markdown_files' ;;
807
+ esac
808
+ ;;
809
+ esac
810
+ }
811
+ _readit "$@"
812
+ `.trim();
813
+ }
814
+
815
+ function generateBashCompletion(): string {
816
+ return `
817
+ # readit bash completion
818
+ # Add to .bashrc: eval "$(readit completion bash)"
819
+
820
+ _readit_completions() {
821
+ local cur prev commands
822
+ COMPREPLY=()
823
+ cur="\${COMP_WORDS[COMP_CWORD]}"
824
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
825
+ commands="open list show completion"
826
+
827
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
828
+ COMPREPLY=( $(compgen -W "\${commands}" -- "\${cur}") )
829
+ # Also complete markdown files
830
+ local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
831
+ -not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
832
+ COMPREPLY+=( $(compgen -W "\${files}" -- "\${cur}") )
833
+ return 0
834
+ fi
835
+
836
+ case "\${COMP_WORDS[1]}" in
837
+ open|show)
838
+ local files=$(find . -type f \\( -name '*.md' -o -name '*.markdown' \\) \\
839
+ -not -path '*/.*' -not -path '*/node_modules/*' 2>/dev/null | sed 's|^\\./||')
840
+ COMPREPLY=( $(compgen -W "\${files}" -- "\${cur}") )
841
+ ;;
842
+ completion)
843
+ COMPREPLY=( $(compgen -W "zsh bash fish" -- "\${cur}") )
844
+ ;;
845
+ esac
846
+ return 0
847
+ }
848
+
849
+ complete -F _readit_completions readit
850
+ `.trim();
851
+ }
852
+
853
+ function generateFishCompletion(): string {
854
+ return `
855
+ # readit fish completion
856
+ # Add to config.fish: readit completion fish | source
857
+
858
+ # Disable file completions by default
859
+ complete -c readit -f
860
+
861
+ # Subcommands
862
+ complete -c readit -n '__fish_use_subcommand' -a 'open' -d 'Add files to running server'
863
+ complete -c readit -n '__fish_use_subcommand' -a 'list' -d 'List files with comments'
864
+ complete -c readit -n '__fish_use_subcommand' -a 'show' -d 'Show comments for a file'
865
+ complete -c readit -n '__fish_use_subcommand' -a 'completion' -d 'Output shell completion script'
866
+
867
+ # Options
868
+ complete -c readit -s p -l port -d 'Port to run server on'
869
+ complete -c readit -l host -d 'Host address to bind to'
870
+ complete -c readit -l no-open -d "Don't automatically open browser"
871
+ complete -c readit -l clean -d 'Clear existing comments'
872
+
873
+ # File arguments for default command and open
874
+ complete -c readit -n '__fish_use_subcommand' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
875
+ complete -c readit -n '__fish_seen_subcommand_from open' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
876
+ complete -c readit -n '__fish_seen_subcommand_from show' -F -a '(find . -type f \\( -name "*.md" -o -name "*.markdown" \\) -not -path "*/.*" -not -path "*/node_modules/*" 2>/dev/null | sed "s|^\\./||")'
877
+
878
+ # Shell completions for completion subcommand
879
+ complete -c readit -n '__fish_seen_subcommand_from completion' -a 'zsh bash fish'
880
+ `.trim();
881
+ }
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ import {
3
+ ClipboardCopy,
4
+ FileDown,
5
+ FileText,
6
+ MoreHorizontal,
7
+ RefreshCw,
8
+ Settings,
9
+ } from "lucide-svelte";
10
+ import { t } from "../stores/locale.svelte";
11
+ import RawModal from "./RawModal.svelte";
12
+ import SettingsModal from "./SettingsModal.svelte";
13
+ import Button from "./ui/Button.svelte";
14
+ import DropdownMenu from "./ui/DropdownMenu.svelte";
15
+ import DropdownMenuItem from "./ui/DropdownMenuItem.svelte";
16
+ import DropdownMenuSeparator from "./ui/DropdownMenuSeparator.svelte";
17
+
18
+ interface Props {
19
+ commentCount: number;
20
+ oncopyall: () => void;
21
+ onexportjson: () => void;
22
+ onreload: () => void;
23
+ }
24
+
25
+ let { commentCount, oncopyall, onexportjson, onreload }: Props = $props();
26
+
27
+ let menuOpen = $state(false);
28
+ let rawModalOpen = $state(false);
29
+ let settingsOpen = $state(false);
30
+ </script>
31
+
32
+ <DropdownMenu bind:open={menuOpen} align="end" contentClass="min-w-[160px]">
33
+ {#snippet trigger()}
34
+ <Button
35
+ variant="ghost"
36
+ size="icon"
37
+ class="size-7"
38
+ title={t("actions.ariaLabel")}
39
+ >
40
+ <MoreHorizontal class="w-4 h-4" />
41
+ </Button>
42
+ {/snippet}
43
+
44
+ <DropdownMenuItem
45
+ onselect={() => {
46
+ settingsOpen = true;
47
+ menuOpen = false;
48
+ }}
49
+ >
50
+ <Settings />
51
+ {t("actions.settings")}
52
+ </DropdownMenuItem>
53
+ <DropdownMenuSeparator />
54
+ <DropdownMenuItem
55
+ onselect={() => {
56
+ onreload();
57
+ menuOpen = false;
58
+ }}
59
+ >
60
+ <RefreshCw />
61
+ {t("actions.reload")}
62
+ </DropdownMenuItem>
63
+ {#if commentCount > 0}
64
+ <DropdownMenuItem
65
+ onselect={() => {
66
+ oncopyall();
67
+ menuOpen = false;
68
+ }}
69
+ >
70
+ <ClipboardCopy />
71
+ {t("actions.copyAll")}
72
+ </DropdownMenuItem>
73
+ <DropdownMenuItem
74
+ onselect={() => {
75
+ onexportjson();
76
+ menuOpen = false;
77
+ }}
78
+ >
79
+ <FileDown />
80
+ {t("actions.exportJson")}
81
+ </DropdownMenuItem>
82
+ <DropdownMenuItem
83
+ onselect={() => {
84
+ rawModalOpen = true;
85
+ menuOpen = false;
86
+ }}
87
+ >
88
+ <FileText />
89
+ {t("actions.viewRaw")}
90
+ </DropdownMenuItem>
91
+ {/if}
92
+ </DropdownMenu>
93
+
94
+ <RawModal bind:open={rawModalOpen} onclose={() => (rawModalOpen = false)} />
95
+ <SettingsModal bind:open={settingsOpen} onclose={() => (settingsOpen = false)} />
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { cn } from "../lib/utils";
3
+ import type { Comment } from "../schema";
4
+ import { t } from "../stores/locale.svelte";
5
+ import CommentManager from "./CommentManager.svelte";
6
+ import DropdownMenu from "./ui/DropdownMenu.svelte";
7
+
8
+ interface Props {
9
+ comments: Comment[];
10
+ fileName: string;
11
+ onedit: (id: string, newText: string) => void;
12
+ ondelete: (id: string) => void;
13
+ ondeleteall: () => void;
14
+ onnavigate: (id: string) => void;
15
+ onstartreanchor: (id: string) => void;
16
+ }
17
+
18
+ let {
19
+ comments,
20
+ fileName,
21
+ onedit,
22
+ ondelete,
23
+ ondeleteall,
24
+ onnavigate,
25
+ onstartreanchor,
26
+ }: Props = $props();
27
+
28
+ let commentsOpen = $state(false);
29
+ let commentCount = $derived(comments.length);
30
+ </script>
31
+
32
+ {#if commentCount > 0}
33
+ <DropdownMenu
34
+ bind:open={commentsOpen}
35
+ align="end"
36
+ contentClass="w-80 max-h-96 overflow-hidden p-0"
37
+ >
38
+ {#snippet trigger()}
39
+ <button
40
+ type="button"
41
+ class={cn(
42
+ "inline-flex items-center gap-1 text-xs tabular-nums select-none transition-colors",
43
+ commentsOpen
44
+ ? "text-zinc-600"
45
+ : "text-zinc-400 hover:text-zinc-600",
46
+ )}
47
+ title={commentCount === 1
48
+ ? t("commentBadge.title", { count: commentCount })
49
+ : t("commentBadge.titlePlural", { count: commentCount })}
50
+ >
51
+ <span class="text-zinc-300">·</span>
52
+ {commentCount}
53
+ </button>
54
+ {/snippet}
55
+
56
+ <CommentManager
57
+ {comments}
58
+ {fileName}
59
+ onclose={() => (commentsOpen = false)}
60
+ {onedit}
61
+ {ondelete}
62
+ {ondeleteall}
63
+ {onnavigate}
64
+ {onstartreanchor}
65
+ />
66
+ </DropdownMenu>
67
+ {/if}
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import { AlertCircle, X } from "lucide-svelte";
3
+
4
+ interface Props {
5
+ error: string | null;
6
+ ondismiss: () => void;
7
+ }
8
+
9
+ let { error, ondismiss }: Props = $props();
10
+ </script>
11
+
12
+ {#if error}
13
+ <div
14
+ role="alert"
15
+ aria-live="polite"
16
+ class="fixed top-24 left-1/2 -translate-x-1/2 z-40 w-full max-w-3xl px-4 pointer-events-none"
17
+ >
18
+ <div
19
+ class="flex items-start gap-3 px-4 py-2 text-sm bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-900 text-red-900 dark:text-red-200 rounded-lg shadow-lg pointer-events-auto"
20
+ >
21
+ <AlertCircle class="size-4 shrink-0 mt-0.5" />
22
+ <p class="flex-1 select-text break-words">{error}</p>
23
+ <button
24
+ type="button"
25
+ onclick={ondismiss}
26
+ aria-label="Dismiss"
27
+ class="shrink-0 rounded p-1 -mr-1 hover:bg-red-100 dark:hover:bg-red-900/40 transition-colors"
28
+ >
29
+ <X class="size-4" />
30
+ </button>
31
+ </div>
32
+ </div>
33
+ {/if}