@peaske7/readit 0.1.5 → 0.1.7
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/biome.json +1 -1
- package/bun.lock +86 -72
- package/docs/plans/2026-03-13-client-mode-design.md +86 -0
- package/docs/plans/2026-03-13-client-mode-plan.md +605 -0
- package/package.json +12 -11
- package/src/App.tsx +23 -6
- package/src/cli/index.ts +312 -25
- package/src/components/ActionsMenu.tsx +12 -10
- package/src/components/DocumentViewer/CodeBlock.tsx +131 -54
- package/src/components/DocumentViewer/DocumentViewer.tsx +6 -6
- package/src/components/DocumentViewer/InlineCode.tsx +60 -0
- package/src/components/FloatingTOC.tsx +4 -2
- package/src/components/Header.tsx +3 -1
- package/src/components/InlineEditor.tsx +4 -2
- package/src/components/MarginNote.tsx +17 -8
- package/src/components/RawModal.tsx +9 -7
- package/src/components/ReanchorConfirm.tsx +6 -3
- package/src/components/SettingsModal.tsx +112 -23
- package/src/components/ShortcutCapture.tsx +4 -1
- package/src/components/ShortcutList.tsx +50 -9
- package/src/components/comments/CommentBadge.tsx +7 -1
- package/src/components/comments/CommentInput.tsx +13 -18
- package/src/components/comments/CommentListItem.tsx +15 -5
- package/src/components/comments/CommentManager.tsx +14 -7
- package/src/components/comments/CommentNav.tsx +8 -3
- package/src/contexts/CommentContext.tsx +16 -9
- package/src/contexts/LayoutContext.tsx +17 -5
- package/src/contexts/LocaleContext.tsx +35 -0
- package/src/hooks/useClipboard.ts +11 -8
- package/src/hooks/useDocument.ts +35 -10
- package/src/hooks/useEditorScheme.ts +51 -0
- package/src/hooks/useFontPreference.ts +5 -22
- package/src/hooks/useKeybindings.ts +6 -18
- package/src/hooks/useLocalePreference.ts +42 -0
- package/src/index.css +87 -26
- package/src/lib/editor-links.ts +59 -0
- package/src/lib/highlight/dom.ts +126 -54
- package/src/lib/highlight/highlighter.ts +10 -10
- package/src/lib/i18n/completeness.test.ts +51 -0
- package/src/lib/i18n/en.ts +139 -0
- package/src/lib/i18n/index.ts +3 -0
- package/src/lib/i18n/ja.ts +141 -0
- package/src/lib/i18n/translations.test.ts +39 -0
- package/src/lib/i18n/translations.ts +27 -0
- package/src/lib/i18n/types.ts +145 -0
- package/src/lib/shortcut-registry.ts +1 -1
- package/src/lib/utils.ts +11 -0
- package/src/main.tsx +4 -1
- package/src/server/index.ts +263 -103
- package/src/store/index.test.ts +22 -0
- package/src/store/index.ts +24 -4
- package/src/types/index.ts +12 -0
package/src/App.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { TableOfContents } from "./components/TableOfContents";
|
|
|
13
13
|
import { textVariants } from "./components/ui/Text";
|
|
14
14
|
import { CommentContext, CommentProvider } from "./contexts/CommentContext";
|
|
15
15
|
import { LayoutContext, LayoutProvider } from "./contexts/LayoutContext";
|
|
16
|
+
import { useLocale } from "./contexts/LocaleContext";
|
|
16
17
|
import { useClipboard } from "./hooks/useClipboard";
|
|
17
18
|
import { useDocument } from "./hooks/useDocument";
|
|
18
19
|
import { useHeadings } from "./hooks/useHeadings";
|
|
@@ -38,6 +39,7 @@ const TOASTER_OPTIONS = {
|
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
function AppContent() {
|
|
42
|
+
const { t } = useLocale();
|
|
41
43
|
const {
|
|
42
44
|
comments,
|
|
43
45
|
sortedComments,
|
|
@@ -74,6 +76,7 @@ function AppContent() {
|
|
|
74
76
|
document: document ?? undefined,
|
|
75
77
|
selection: selection ?? undefined,
|
|
76
78
|
clearSelection,
|
|
79
|
+
t,
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
const { shortcuts, isFullscreen } = use(LayoutContext)!;
|
|
@@ -275,6 +278,7 @@ function AppContent() {
|
|
|
275
278
|
/>
|
|
276
279
|
) : (
|
|
277
280
|
<CommentInput
|
|
281
|
+
key={selection.text}
|
|
278
282
|
selectedText={selection.text}
|
|
279
283
|
onSubmit={handleAddComment}
|
|
280
284
|
onCancel={clearSelection}
|
|
@@ -302,7 +306,7 @@ function AppContent() {
|
|
|
302
306
|
<CommentNav />
|
|
303
307
|
|
|
304
308
|
<footer className="py-4 text-center text-sm text-zinc-400 dark:text-zinc-500">
|
|
305
|
-
|
|
309
|
+
{t("app.footer")}
|
|
306
310
|
</footer>
|
|
307
311
|
</div>
|
|
308
312
|
);
|
|
@@ -333,6 +337,7 @@ function useTabKeyboardShortcuts() {
|
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
function App() {
|
|
340
|
+
const { t } = useLocale();
|
|
336
341
|
const { document, error, isInitialized } = useDocument();
|
|
337
342
|
const documentOrder = useAppStore((s) => s.documentOrder);
|
|
338
343
|
|
|
@@ -349,7 +354,9 @@ function App() {
|
|
|
349
354
|
if (!isInitialized) {
|
|
350
355
|
return (
|
|
351
356
|
<div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex items-center justify-center">
|
|
352
|
-
<div className="text-zinc-500 dark:text-zinc-400">
|
|
357
|
+
<div className="text-zinc-500 dark:text-zinc-400">
|
|
358
|
+
{t("app.loading")}
|
|
359
|
+
</div>
|
|
353
360
|
</div>
|
|
354
361
|
);
|
|
355
362
|
}
|
|
@@ -358,9 +365,17 @@ function App() {
|
|
|
358
365
|
return (
|
|
359
366
|
<div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex flex-col">
|
|
360
367
|
<TabBar />
|
|
361
|
-
<div className="flex-1 flex items-center justify-center">
|
|
368
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-3">
|
|
362
369
|
<p className="text-zinc-400 dark:text-zinc-500 text-sm">
|
|
363
|
-
|
|
370
|
+
{t("app.noDocuments")}
|
|
371
|
+
</p>
|
|
372
|
+
<p className="text-zinc-400 dark:text-zinc-500 text-xs">
|
|
373
|
+
{t("app.noDocumentsHintPrefix")}
|
|
374
|
+
{t("app.noDocumentsHintPrefix") && " "}
|
|
375
|
+
<code className="bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-xs">
|
|
376
|
+
readit open <file.md>
|
|
377
|
+
</code>{" "}
|
|
378
|
+
{t("app.noDocumentsHintSuffix")}
|
|
364
379
|
</p>
|
|
365
380
|
</div>
|
|
366
381
|
</div>
|
|
@@ -370,7 +385,9 @@ function App() {
|
|
|
370
385
|
if (!document) {
|
|
371
386
|
return (
|
|
372
387
|
<div className="min-h-screen bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 flex items-center justify-center">
|
|
373
|
-
<div className="text-zinc-500 dark:text-zinc-400">
|
|
388
|
+
<div className="text-zinc-500 dark:text-zinc-400">
|
|
389
|
+
{t("app.loading")}
|
|
390
|
+
</div>
|
|
374
391
|
</div>
|
|
375
392
|
);
|
|
376
393
|
}
|
|
@@ -378,7 +395,7 @@ function App() {
|
|
|
378
395
|
return (
|
|
379
396
|
<>
|
|
380
397
|
<TabBar />
|
|
381
|
-
<LayoutProvider
|
|
398
|
+
<LayoutProvider>
|
|
382
399
|
<CommentProvider
|
|
383
400
|
filePath={document.filePath}
|
|
384
401
|
clean={document.clean}
|
package/src/cli/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
lstatSync,
|
|
6
6
|
readdirSync,
|
|
7
7
|
readFileSync,
|
|
8
|
+
realpathSync,
|
|
8
9
|
statSync,
|
|
9
10
|
} from "node:fs";
|
|
10
11
|
import * as fs from "node:fs/promises";
|
|
@@ -13,22 +14,13 @@ import { join, resolve } from "node:path";
|
|
|
13
14
|
import { Command } from "commander";
|
|
14
15
|
import open from "open";
|
|
15
16
|
import { getCommentPath, parseCommentFile } from "../lib/comment-storage.js";
|
|
17
|
+
import { getFileType } from "../lib/utils.js";
|
|
16
18
|
import type { FileEntry } from "../server/index.js";
|
|
17
|
-
import { startServer } from "../server/index.js";
|
|
19
|
+
import { removeServerInfo, startServer } from "../server/index.js";
|
|
18
20
|
import type { DocumentType } from "../types/index.js";
|
|
19
21
|
|
|
20
22
|
const program = new Command();
|
|
21
23
|
|
|
22
|
-
function getFileType(filePath: string): DocumentType | null {
|
|
23
|
-
if (filePath.endsWith(".md") || filePath.endsWith(".markdown")) {
|
|
24
|
-
return "markdown";
|
|
25
|
-
}
|
|
26
|
-
if (filePath.endsWith(".html") || filePath.endsWith(".htm")) {
|
|
27
|
-
return "html";
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
24
|
function isPermissionError(err: unknown): boolean {
|
|
33
25
|
return (
|
|
34
26
|
err instanceof Error &&
|
|
@@ -37,6 +29,39 @@ function isPermissionError(err: unknown): boolean {
|
|
|
37
29
|
);
|
|
38
30
|
}
|
|
39
31
|
|
|
32
|
+
interface ServerInfo {
|
|
33
|
+
port: number;
|
|
34
|
+
pid: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function discoverServer(): Promise<ServerInfo | null> {
|
|
38
|
+
const serverInfoPath = join(os.homedir(), ".readit", "server.json");
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(serverInfoPath, "utf-8");
|
|
42
|
+
const info: ServerInfo = JSON.parse(content);
|
|
43
|
+
|
|
44
|
+
// Verify the process is alive
|
|
45
|
+
try {
|
|
46
|
+
process.kill(info.pid, 0);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Verify health endpoint responds
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`http://127.0.0.1:${info.port}/api/health`);
|
|
54
|
+
if (!res.ok) return null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return info;
|
|
60
|
+
} catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
/**
|
|
41
66
|
* Recursively find all .comments.md files in a directory.
|
|
42
67
|
*/
|
|
@@ -92,7 +117,6 @@ function findReviewableFiles(dir: string): FileEntry[] {
|
|
|
92
117
|
const type = getFileType(entry);
|
|
93
118
|
if (type) {
|
|
94
119
|
results.push({
|
|
95
|
-
content: readFileSync(fullPath, "utf-8"),
|
|
96
120
|
type,
|
|
97
121
|
filePath: fullPath,
|
|
98
122
|
});
|
|
@@ -121,13 +145,15 @@ function resolveFiles(args: string[]): FileEntry[] {
|
|
|
121
145
|
const files: FileEntry[] = [];
|
|
122
146
|
|
|
123
147
|
for (const arg of args) {
|
|
124
|
-
const
|
|
148
|
+
const inputPath = resolve(process.cwd(), arg);
|
|
125
149
|
|
|
126
|
-
if (!existsSync(
|
|
127
|
-
console.error(`error: not found: ${
|
|
150
|
+
if (!existsSync(inputPath)) {
|
|
151
|
+
console.error(`error: not found: ${inputPath}`);
|
|
128
152
|
process.exit(1);
|
|
129
153
|
}
|
|
130
154
|
|
|
155
|
+
const filePath = realpathSync(inputPath);
|
|
156
|
+
|
|
131
157
|
const stat = statSync(filePath);
|
|
132
158
|
|
|
133
159
|
if (stat.isDirectory()) {
|
|
@@ -151,7 +177,6 @@ function resolveFiles(args: string[]): FileEntry[] {
|
|
|
151
177
|
|
|
152
178
|
seen.add(filePath);
|
|
153
179
|
files.push({
|
|
154
|
-
content: readFileSync(filePath, "utf-8"),
|
|
155
180
|
type,
|
|
156
181
|
filePath,
|
|
157
182
|
});
|
|
@@ -161,6 +186,114 @@ function resolveFiles(args: string[]): FileEntry[] {
|
|
|
161
186
|
return files;
|
|
162
187
|
}
|
|
163
188
|
|
|
189
|
+
// ─── Onboarding ──────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const SETTINGS_PATH = join(os.homedir(), ".readit", "settings.json");
|
|
192
|
+
|
|
193
|
+
function isOnboarded(): boolean {
|
|
194
|
+
try {
|
|
195
|
+
const content = readFileSync(SETTINGS_PATH, "utf-8");
|
|
196
|
+
const settings = JSON.parse(content);
|
|
197
|
+
return settings.onboarded === true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function markOnboarded(): Promise<void> {
|
|
204
|
+
let settings: Record<string, unknown> = {};
|
|
205
|
+
try {
|
|
206
|
+
const content = readFileSync(SETTINGS_PATH, "utf-8");
|
|
207
|
+
settings = JSON.parse(content);
|
|
208
|
+
} catch {
|
|
209
|
+
// No existing settings
|
|
210
|
+
}
|
|
211
|
+
settings.onboarded = true;
|
|
212
|
+
const dir = join(os.homedir(), ".readit");
|
|
213
|
+
await fs.mkdir(dir, { recursive: true });
|
|
214
|
+
await fs.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const WELCOME_CONTENT = `# Welcome to readit
|
|
218
|
+
|
|
219
|
+
A simple tool for reviewing markdown with inline comments.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## How It Works
|
|
224
|
+
|
|
225
|
+
readit follows a simple loop: **read → comment → extract**.
|
|
226
|
+
|
|
227
|
+
### 1. Read
|
|
228
|
+
|
|
229
|
+
You're already doing this. Open any markdown file with \`readit <file.md>\` and it renders in your browser with a clean reading experience.
|
|
230
|
+
|
|
231
|
+
### 2. Comment
|
|
232
|
+
|
|
233
|
+
Select any text to add a comment. Try it now — **select this sentence** and type your first comment.
|
|
234
|
+
|
|
235
|
+
Your comments appear as margin notes next to the highlighted text, just like reviewing a document in Google Docs. Add as many as you need.
|
|
236
|
+
|
|
237
|
+
### 3. Extract
|
|
238
|
+
|
|
239
|
+
When you're done reviewing, click the menu in the top-right and choose **Copy as Prompt**. This exports all your comments in a format ready for Claude, ChatGPT, or any AI assistant.
|
|
240
|
+
|
|
241
|
+
You can also export as JSON if you prefer structured data.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Everything is Plain Markdown
|
|
246
|
+
|
|
247
|
+
Your comments are saved as \`.comments.md\` files in \`~/.readit/comments/\`. No database, no lock-in — just readable markdown files you can version control, search, or edit by hand.
|
|
248
|
+
|
|
249
|
+
Each comment file looks something like this:
|
|
250
|
+
|
|
251
|
+
\`\`\`markdown
|
|
252
|
+
## Comment 1
|
|
253
|
+
**Selected:** "select this sentence"
|
|
254
|
+
**Comment:** This is my first comment!
|
|
255
|
+
**Created:** 2024-01-15T10:30:00Z
|
|
256
|
+
\`\`\`
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Navigating Comments
|
|
261
|
+
|
|
262
|
+
Once you have multiple comments, use the navigation bar at the bottom of the screen to jump between them. You can also use keyboard shortcuts:
|
|
263
|
+
|
|
264
|
+
| Shortcut | Action |
|
|
265
|
+
|----------|--------|
|
|
266
|
+
| \`Alt + ↑\` | Previous comment |
|
|
267
|
+
| \`Alt + ↓\` | Next comment |
|
|
268
|
+
| \`⌘ + C\` | Copy selected text (raw) |
|
|
269
|
+
| \`⌘ + Shift + C\` | Copy selected text with context (for AI) |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Quick Start
|
|
274
|
+
|
|
275
|
+
\`\`\`bash
|
|
276
|
+
# Review a markdown file
|
|
277
|
+
readit document.md
|
|
278
|
+
|
|
279
|
+
# Use a custom port
|
|
280
|
+
readit document.md --port 3000
|
|
281
|
+
|
|
282
|
+
# Start fresh (clear existing comments)
|
|
283
|
+
readit document.md --clean
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Try It Now
|
|
289
|
+
|
|
290
|
+
Go ahead and add a few comments to this document. When you're done, export them and see the output. That's the entire workflow — simple, transparent, and designed for reviewing AI-generated content.
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
const WELCOME_PATH = join(os.homedir(), ".readit", "welcome.md");
|
|
294
|
+
|
|
295
|
+
// ─── Program ─────────────────────────────────────────────────────────
|
|
296
|
+
|
|
164
297
|
program
|
|
165
298
|
.name("readit")
|
|
166
299
|
.description("Review Markdown and HTML documents with inline comments")
|
|
@@ -249,9 +382,9 @@ program
|
|
|
249
382
|
}
|
|
250
383
|
});
|
|
251
384
|
|
|
252
|
-
// Main review command (default) — accepts
|
|
385
|
+
// Main review command (default) — accepts zero or more files/directories
|
|
253
386
|
program
|
|
254
|
-
.argument("
|
|
387
|
+
.argument("[files...]", "Markdown or HTML files/directories to review")
|
|
255
388
|
.option("-p, --port <number>", "Port to run server on", "4567")
|
|
256
389
|
.option("--host <address>", "Host address to bind to", "127.0.0.1")
|
|
257
390
|
.option("--no-open", "Don't automatically open browser")
|
|
@@ -266,11 +399,27 @@ program
|
|
|
266
399
|
clean: boolean;
|
|
267
400
|
},
|
|
268
401
|
) => {
|
|
269
|
-
|
|
402
|
+
let files: FileEntry[];
|
|
270
403
|
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
404
|
+
if (fileArgs.length === 0) {
|
|
405
|
+
if (isOnboarded()) {
|
|
406
|
+
files = [];
|
|
407
|
+
} else {
|
|
408
|
+
files = [
|
|
409
|
+
{
|
|
410
|
+
content: WELCOME_CONTENT,
|
|
411
|
+
type: "markdown" as DocumentType,
|
|
412
|
+
filePath: WELCOME_PATH,
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
files = resolveFiles(fileArgs);
|
|
418
|
+
|
|
419
|
+
if (files.length === 0) {
|
|
420
|
+
console.error("error: no reviewable files found");
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
274
423
|
}
|
|
275
424
|
|
|
276
425
|
const preferredPort = Number.parseInt(options.port, 10);
|
|
@@ -292,9 +441,21 @@ program
|
|
|
292
441
|
clean: options.clean,
|
|
293
442
|
});
|
|
294
443
|
|
|
295
|
-
|
|
444
|
+
if (files.length === 0) {
|
|
445
|
+
console.log(`
|
|
446
|
+
readit - Document Review Tool
|
|
296
447
|
|
|
297
|
-
|
|
448
|
+
URL: ${url}
|
|
449
|
+
|
|
450
|
+
No files specified. Add files with:
|
|
451
|
+
readit open <file.md>
|
|
452
|
+
|
|
453
|
+
Server running. Press Ctrl+C to stop.
|
|
454
|
+
`);
|
|
455
|
+
} else {
|
|
456
|
+
const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
|
|
457
|
+
|
|
458
|
+
console.log(`
|
|
298
459
|
readit - Document Review Tool
|
|
299
460
|
|
|
300
461
|
${files.length === 1 ? "File:" : "Files:"}
|
|
@@ -304,15 +465,141 @@ ${fileList.join("\n")}
|
|
|
304
465
|
Server running. Close browser tab to stop.
|
|
305
466
|
Press Ctrl+C to force stop.
|
|
306
467
|
`);
|
|
468
|
+
}
|
|
307
469
|
|
|
308
470
|
if (options.open) {
|
|
309
471
|
open(url);
|
|
310
472
|
}
|
|
311
473
|
|
|
474
|
+
// Mark onboarding complete on first server start
|
|
475
|
+
if (fileArgs.length === 0) {
|
|
476
|
+
await markOnboarded();
|
|
477
|
+
}
|
|
478
|
+
|
|
312
479
|
// Graceful shutdown on Ctrl+C
|
|
313
|
-
process.on("SIGINT", () => {
|
|
480
|
+
process.on("SIGINT", async () => {
|
|
314
481
|
console.log("\n\nShutting down...");
|
|
315
482
|
server.stop();
|
|
483
|
+
await removeServerInfo();
|
|
484
|
+
process.exit(0);
|
|
485
|
+
});
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.error(
|
|
488
|
+
"error: failed to start server:",
|
|
489
|
+
error instanceof Error ? error.message : error,
|
|
490
|
+
);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
// Open command: add files to running server or start new one
|
|
497
|
+
program
|
|
498
|
+
.command("open")
|
|
499
|
+
.argument("<files...>", "Markdown or HTML files to add to running server")
|
|
500
|
+
.description("Add files to a running readit server, or start a new one")
|
|
501
|
+
.option("-p, --port <number>", "Port for new server (if starting)", "4567")
|
|
502
|
+
.option("--host <address>", "Host for new server (if starting)", "127.0.0.1")
|
|
503
|
+
.action(
|
|
504
|
+
async (fileArgs: string[], options: { port: string; host: string }) => {
|
|
505
|
+
// Resolve and validate files
|
|
506
|
+
const resolvedFiles: { path: string; type: DocumentType }[] = [];
|
|
507
|
+
for (const arg of fileArgs) {
|
|
508
|
+
const inputPath = resolve(process.cwd(), arg);
|
|
509
|
+
|
|
510
|
+
if (!existsSync(inputPath)) {
|
|
511
|
+
console.error(`error: not found: ${inputPath}`);
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const filePath = realpathSync(inputPath);
|
|
516
|
+
|
|
517
|
+
const type = getFileType(filePath);
|
|
518
|
+
if (!type) {
|
|
519
|
+
console.error(
|
|
520
|
+
`error: unsupported file type: ${arg} (expected .md, .markdown, .html, or .htm)`,
|
|
521
|
+
);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
resolvedFiles.push({ path: filePath, type });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Try to find running server
|
|
529
|
+
const server = await discoverServer();
|
|
530
|
+
|
|
531
|
+
if (server) {
|
|
532
|
+
// Send files to running server
|
|
533
|
+
for (const file of resolvedFiles) {
|
|
534
|
+
try {
|
|
535
|
+
const res = await fetch(
|
|
536
|
+
`http://127.0.0.1:${server.port}/api/documents`,
|
|
537
|
+
{
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers: { "Content-Type": "application/json" },
|
|
540
|
+
body: JSON.stringify({ path: file.path }),
|
|
541
|
+
},
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
if (!res.ok) {
|
|
545
|
+
const data = await res.json();
|
|
546
|
+
console.error(`error: failed to add ${file.path}: ${data.error}`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const data = await res.json();
|
|
551
|
+
if (data.status === "added") {
|
|
552
|
+
console.log(`Added: ${data.fileName} (${data.type})`);
|
|
553
|
+
} else {
|
|
554
|
+
console.log(`Present: ${data.fileName} (${data.type})`);
|
|
555
|
+
}
|
|
556
|
+
} catch (err) {
|
|
557
|
+
console.error(
|
|
558
|
+
"error: failed to connect to server:",
|
|
559
|
+
err instanceof Error ? err.message : err,
|
|
560
|
+
);
|
|
561
|
+
process.exit(1);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
console.log(`\nServer: http://127.0.0.1:${server.port}`);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// No running server — start one
|
|
570
|
+
console.log("No running server found, starting new one...\n");
|
|
571
|
+
|
|
572
|
+
const files = resolvedFiles.map((f) => ({
|
|
573
|
+
type: f.type,
|
|
574
|
+
filePath: f.path,
|
|
575
|
+
}));
|
|
576
|
+
|
|
577
|
+
const preferredPort = Number.parseInt(options.port, 10);
|
|
578
|
+
try {
|
|
579
|
+
const { url, server: newServer } = await startServer({
|
|
580
|
+
files,
|
|
581
|
+
port: preferredPort,
|
|
582
|
+
host: options.host,
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const fileList = files.map((f) => ` ${f.filePath} (${f.type})`);
|
|
586
|
+
console.log(`
|
|
587
|
+
readit - Document Review Tool
|
|
588
|
+
|
|
589
|
+
${files.length === 1 ? "File:" : "Files:"}
|
|
590
|
+
${fileList.join("\n")}
|
|
591
|
+
URL: ${url}
|
|
592
|
+
|
|
593
|
+
Server running. Close browser tab to stop.
|
|
594
|
+
Press Ctrl+C to force stop.
|
|
595
|
+
`);
|
|
596
|
+
|
|
597
|
+
open(url);
|
|
598
|
+
|
|
599
|
+
process.on("SIGINT", async () => {
|
|
600
|
+
console.log("\n\nShutting down...");
|
|
601
|
+
newServer.stop();
|
|
602
|
+
await removeServerInfo();
|
|
316
603
|
process.exit(0);
|
|
317
604
|
});
|
|
318
605
|
} catch (error) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { useState } from "react";
|
|
13
13
|
import { useCommentContext } from "../contexts/CommentContext";
|
|
14
14
|
import { useLayoutContext } from "../contexts/LayoutContext";
|
|
15
|
+
import { useLocale } from "../contexts/LocaleContext";
|
|
15
16
|
import { RawModal } from "./RawModal";
|
|
16
17
|
import { SettingsModal } from "./SettingsModal";
|
|
17
18
|
import { Button } from "./ui/Button";
|
|
@@ -38,6 +39,7 @@ export function ActionsMenu({
|
|
|
38
39
|
}: ActionsMenuProps) {
|
|
39
40
|
const { commentCount } = useCommentContext();
|
|
40
41
|
const { isFullscreen, toggleLayoutMode } = useLayoutContext();
|
|
42
|
+
const { t } = useLocale();
|
|
41
43
|
|
|
42
44
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
43
45
|
const [rawModalOpen, setRawModalOpen] = useState(false);
|
|
@@ -51,7 +53,7 @@ export function ActionsMenu({
|
|
|
51
53
|
variant="ghost"
|
|
52
54
|
size="icon"
|
|
53
55
|
className="size-7"
|
|
54
|
-
aria-label="
|
|
56
|
+
aria-label={t("actions.ariaLabel")}
|
|
55
57
|
>
|
|
56
58
|
<MoreHorizontal className="w-4 h-4" />
|
|
57
59
|
</Button>
|
|
@@ -59,40 +61,40 @@ export function ActionsMenu({
|
|
|
59
61
|
<DropdownMenuContent align="end" className="min-w-[160px]">
|
|
60
62
|
<DropdownMenuItem onSelect={() => toggleLayoutMode()}>
|
|
61
63
|
{isFullscreen ? <Minimize2 /> : <Maximize2 />}
|
|
62
|
-
{isFullscreen ? "
|
|
64
|
+
{isFullscreen ? t("actions.centered") : t("actions.fullscreen")}
|
|
63
65
|
</DropdownMenuItem>
|
|
64
66
|
<DropdownMenuItem onSelect={() => setSettingsOpen(true)}>
|
|
65
67
|
<Settings />
|
|
66
|
-
|
|
68
|
+
{t("actions.settings")}
|
|
67
69
|
</DropdownMenuItem>
|
|
68
70
|
<DropdownMenuSeparator />
|
|
69
71
|
<DropdownMenuItem onSelect={() => onReload()}>
|
|
70
72
|
<RefreshCw />
|
|
71
|
-
|
|
73
|
+
{t("actions.reload")}
|
|
72
74
|
</DropdownMenuItem>
|
|
73
75
|
{commentCount > 0 && (
|
|
74
76
|
<>
|
|
75
77
|
<DropdownMenuItem
|
|
76
78
|
onSelect={() => onCopyAll()}
|
|
77
|
-
title="
|
|
79
|
+
title={t("actions.copyAllAITitle")}
|
|
78
80
|
>
|
|
79
81
|
<BotMessageSquare />
|
|
80
|
-
|
|
82
|
+
{t("actions.copyAllAI")}
|
|
81
83
|
</DropdownMenuItem>
|
|
82
84
|
<DropdownMenuItem
|
|
83
85
|
onSelect={() => onCopyAllRaw()}
|
|
84
|
-
title="
|
|
86
|
+
title={t("actions.copyAllRawTitle")}
|
|
85
87
|
>
|
|
86
88
|
<TextQuote />
|
|
87
|
-
|
|
89
|
+
{t("actions.copyAllRaw")}
|
|
88
90
|
</DropdownMenuItem>
|
|
89
91
|
<DropdownMenuItem onSelect={() => onExportJson()}>
|
|
90
92
|
<FileDown />
|
|
91
|
-
|
|
93
|
+
{t("actions.exportJson")}
|
|
92
94
|
</DropdownMenuItem>
|
|
93
95
|
<DropdownMenuItem onSelect={() => setRawModalOpen(true)}>
|
|
94
96
|
<FileText />
|
|
95
|
-
|
|
97
|
+
{t("actions.viewRaw")}
|
|
96
98
|
</DropdownMenuItem>
|
|
97
99
|
</>
|
|
98
100
|
)}
|