@gmickel/gno 0.25.2 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/assets/skill/SKILL.md +5 -0
- package/assets/skill/cli-reference.md +8 -6
- package/package.json +1 -1
- package/src/cli/commands/get.ts +21 -0
- package/src/cli/commands/skill/install.ts +2 -2
- package/src/cli/commands/skill/paths.ts +26 -4
- package/src/cli/commands/skill/uninstall.ts +2 -2
- package/src/cli/program.ts +18 -12
- package/src/core/document-capabilities.ts +113 -0
- package/src/mcp/tools/get.ts +10 -0
- package/src/mcp/tools/index.ts +434 -110
- package/src/sdk/documents.ts +12 -0
- package/src/serve/doc-events.ts +69 -0
- package/src/serve/public/app.tsx +81 -24
- package/src/serve/public/components/CaptureModal.tsx +138 -3
- package/src/serve/public/components/QuickSwitcher.tsx +248 -0
- package/src/serve/public/components/ShortcutHelpModal.tsx +1 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +74 -26
- package/src/serve/public/components/editor/CodeMirrorEditor.tsx +51 -0
- package/src/serve/public/components/ui/command.tsx +2 -2
- package/src/serve/public/hooks/use-doc-events.ts +34 -0
- package/src/serve/public/hooks/useCaptureModal.tsx +12 -3
- package/src/serve/public/hooks/useKeyboardShortcuts.ts +2 -2
- package/src/serve/public/lib/deep-links.ts +68 -0
- package/src/serve/public/lib/document-availability.ts +22 -0
- package/src/serve/public/lib/local-history.ts +44 -0
- package/src/serve/public/lib/wiki-link.ts +36 -0
- package/src/serve/public/pages/Browse.tsx +11 -0
- package/src/serve/public/pages/Dashboard.tsx +2 -2
- package/src/serve/public/pages/DocView.tsx +241 -18
- package/src/serve/public/pages/DocumentEditor.tsx +399 -9
- package/src/serve/public/pages/Search.tsx +20 -1
- package/src/serve/routes/api.ts +359 -28
- package/src/serve/server.ts +48 -1
- package/src/serve/watch-service.ts +149 -0
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
LinkIcon,
|
|
22
22
|
Loader2Icon,
|
|
23
23
|
PenIcon,
|
|
24
|
+
SquareArrowOutUpRightIcon,
|
|
24
25
|
UnlinkIcon,
|
|
25
26
|
} from "lucide-react";
|
|
26
27
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -52,7 +53,19 @@ import {
|
|
|
52
53
|
TooltipProvider,
|
|
53
54
|
TooltipTrigger,
|
|
54
55
|
} from "../components/ui/tooltip";
|
|
56
|
+
import {
|
|
57
|
+
type WikiLinkDoc,
|
|
58
|
+
WikiLinkAutocomplete,
|
|
59
|
+
} from "../components/WikiLinkAutocomplete";
|
|
55
60
|
import { apiFetch } from "../hooks/use-api";
|
|
61
|
+
import { useDocEvents } from "../hooks/use-doc-events";
|
|
62
|
+
import { buildEditDeepLink, parseDocumentDeepLink } from "../lib/deep-links";
|
|
63
|
+
import { waitForDocumentAvailability } from "../lib/document-availability";
|
|
64
|
+
import {
|
|
65
|
+
appendLocalHistory,
|
|
66
|
+
loadLatestLocalHistory,
|
|
67
|
+
} from "../lib/local-history";
|
|
68
|
+
import { getActiveWikiLinkQuery } from "../lib/wiki-link";
|
|
56
69
|
|
|
57
70
|
interface PageProps {
|
|
58
71
|
navigate: (to: string | number) => void;
|
|
@@ -67,13 +80,47 @@ interface DocData {
|
|
|
67
80
|
collection: string;
|
|
68
81
|
relPath: string;
|
|
69
82
|
source: {
|
|
83
|
+
absPath?: string;
|
|
70
84
|
mime: string;
|
|
71
85
|
ext: string;
|
|
72
86
|
modifiedAt?: string;
|
|
73
87
|
sizeBytes?: number;
|
|
88
|
+
sourceHash?: string;
|
|
89
|
+
};
|
|
90
|
+
capabilities: {
|
|
91
|
+
editable: boolean;
|
|
92
|
+
tagsEditable: boolean;
|
|
93
|
+
tagsWriteback: boolean;
|
|
94
|
+
canCreateEditableCopy: boolean;
|
|
95
|
+
mode: "editable" | "read_only";
|
|
96
|
+
reason?: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface CreateEditableCopyResponse {
|
|
101
|
+
uri: string;
|
|
102
|
+
path: string;
|
|
103
|
+
jobId: string | null;
|
|
104
|
+
note?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface UpdateDocResponse {
|
|
108
|
+
success: boolean;
|
|
109
|
+
docId: string;
|
|
110
|
+
uri: string;
|
|
111
|
+
path: string;
|
|
112
|
+
jobId: string | null;
|
|
113
|
+
writeBack?: "applied" | "skipped_unsupported";
|
|
114
|
+
version: {
|
|
115
|
+
sourceHash: string;
|
|
116
|
+
modifiedAt?: string;
|
|
74
117
|
};
|
|
75
118
|
}
|
|
76
119
|
|
|
120
|
+
interface DocsAutocompleteResponse {
|
|
121
|
+
docs: WikiLinkDoc[];
|
|
122
|
+
}
|
|
123
|
+
|
|
77
124
|
type SaveStatus = "saved" | "saving" | "unsaved" | "error";
|
|
78
125
|
|
|
79
126
|
function useDebouncedCallback<T extends unknown[]>(
|
|
@@ -128,6 +175,10 @@ function formatTime(date: Date): string {
|
|
|
128
175
|
}
|
|
129
176
|
|
|
130
177
|
export default function DocumentEditor({ navigate }: PageProps) {
|
|
178
|
+
const currentTarget = useMemo(
|
|
179
|
+
() => parseDocumentDeepLink(window.location.search),
|
|
180
|
+
[]
|
|
181
|
+
);
|
|
131
182
|
const [doc, setDoc] = useState<DocData | null>(null);
|
|
132
183
|
const [error, setError] = useState<string | null>(null);
|
|
133
184
|
const [loading, setLoading] = useState(true);
|
|
@@ -137,10 +188,25 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
137
188
|
const [saveStatus, setSaveStatus] = useState<SaveStatus>("saved");
|
|
138
189
|
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
139
190
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
191
|
+
const [creatingCopy, setCreatingCopy] = useState(false);
|
|
192
|
+
const [copyError, setCopyError] = useState<string | null>(null);
|
|
193
|
+
const [externalChangeNotice, setExternalChangeNotice] = useState<
|
|
194
|
+
string | null
|
|
195
|
+
>(null);
|
|
196
|
+
const [hasLocalSnapshot, setHasLocalSnapshot] = useState(false);
|
|
140
197
|
|
|
141
198
|
const [showPreview, setShowPreview] = useState(true);
|
|
142
199
|
const [syncScroll, setSyncScroll] = useState(true);
|
|
143
200
|
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
|
|
201
|
+
const [wikiLinkDocs, setWikiLinkDocs] = useState<WikiLinkDoc[]>([]);
|
|
202
|
+
const [wikiLinkOpen, setWikiLinkOpen] = useState(false);
|
|
203
|
+
const [wikiLinkQuery, setWikiLinkQuery] = useState("");
|
|
204
|
+
const [wikiLinkRange, setWikiLinkRange] = useState<{
|
|
205
|
+
start: number;
|
|
206
|
+
end: number;
|
|
207
|
+
} | null>(null);
|
|
208
|
+
const [wikiLinkPosition, setWikiLinkPosition] = useState({ x: 48, y: 120 });
|
|
209
|
+
const [wikiLinkActiveIndex, setWikiLinkActiveIndex] = useState(-1);
|
|
144
210
|
/** Where to navigate after dialog action (-1 for back, or URL string) */
|
|
145
211
|
const [pendingNavigation, setPendingNavigation] = useState<
|
|
146
212
|
string | number | null
|
|
@@ -150,6 +216,8 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
150
216
|
// Event-based suppression: ignore the echo event caused by programmatic scroll
|
|
151
217
|
const ignoreNextEditorScroll = useRef(false);
|
|
152
218
|
const ignoreNextPreviewScroll = useRef(false);
|
|
219
|
+
const ignoreDocEventsUntilRef = useRef(0);
|
|
220
|
+
const latestDocEvent = useDocEvents();
|
|
153
221
|
|
|
154
222
|
const hasUnsavedChanges = content !== originalContent;
|
|
155
223
|
const parsedContent = useMemo(() => parseFrontmatter(content), [content]);
|
|
@@ -228,11 +296,15 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
228
296
|
setSaveStatus("saving");
|
|
229
297
|
setSaveError(null);
|
|
230
298
|
|
|
231
|
-
const { error: err } = await apiFetch(
|
|
299
|
+
const { data, error: err } = await apiFetch<UpdateDocResponse>(
|
|
232
300
|
`/api/docs/${encodeURIComponent(doc.docid)}`,
|
|
233
301
|
{
|
|
234
302
|
method: "PUT",
|
|
235
|
-
body: JSON.stringify({
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
content: contentToSave,
|
|
305
|
+
expectedSourceHash: doc.source.sourceHash,
|
|
306
|
+
expectedModifiedAt: doc.source.modifiedAt,
|
|
307
|
+
}),
|
|
236
308
|
}
|
|
237
309
|
);
|
|
238
310
|
|
|
@@ -240,14 +312,120 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
240
312
|
setSaveStatus("error");
|
|
241
313
|
setSaveError(err);
|
|
242
314
|
} else {
|
|
315
|
+
ignoreDocEventsUntilRef.current = Date.now() + 5_000;
|
|
316
|
+
if (originalContent !== contentToSave) {
|
|
317
|
+
appendLocalHistory(doc.docid, originalContent);
|
|
318
|
+
setHasLocalSnapshot(true);
|
|
319
|
+
}
|
|
243
320
|
setSaveStatus("saved");
|
|
244
321
|
setOriginalContent(contentToSave);
|
|
245
322
|
setLastSaved(new Date());
|
|
323
|
+
if (data) {
|
|
324
|
+
setDoc((currentDoc) =>
|
|
325
|
+
currentDoc
|
|
326
|
+
? {
|
|
327
|
+
...currentDoc,
|
|
328
|
+
source: {
|
|
329
|
+
...currentDoc.source,
|
|
330
|
+
sourceHash: data.version.sourceHash,
|
|
331
|
+
modifiedAt: data.version.modifiedAt,
|
|
332
|
+
},
|
|
333
|
+
}
|
|
334
|
+
: currentDoc
|
|
335
|
+
);
|
|
336
|
+
}
|
|
246
337
|
}
|
|
247
338
|
},
|
|
248
339
|
[doc]
|
|
249
340
|
);
|
|
250
341
|
|
|
342
|
+
const handleCreateEditableCopy = useCallback(async () => {
|
|
343
|
+
if (!doc?.capabilities.canCreateEditableCopy) return;
|
|
344
|
+
|
|
345
|
+
setCreatingCopy(true);
|
|
346
|
+
setCopyError(null);
|
|
347
|
+
const { data, error: err } = await apiFetch<CreateEditableCopyResponse>(
|
|
348
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/editable-copy`,
|
|
349
|
+
{ method: "POST" }
|
|
350
|
+
);
|
|
351
|
+
setCreatingCopy(false);
|
|
352
|
+
|
|
353
|
+
if (err) {
|
|
354
|
+
setCopyError(err);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (data) {
|
|
359
|
+
ignoreDocEventsUntilRef.current = Date.now() + 5_000;
|
|
360
|
+
const ready = await waitForDocumentAvailability(data.uri);
|
|
361
|
+
if (!ready) {
|
|
362
|
+
setCopyError(
|
|
363
|
+
"Created the markdown copy, but it is still indexing. Try again in a moment."
|
|
364
|
+
);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
navigate(`/edit?uri=${encodeURIComponent(data.uri)}`);
|
|
368
|
+
}
|
|
369
|
+
}, [doc, navigate]);
|
|
370
|
+
|
|
371
|
+
const insertWikiLink = useCallback(
|
|
372
|
+
(title: string) => {
|
|
373
|
+
if (!wikiLinkRange) return;
|
|
374
|
+
const didReplace = editorRef.current?.replaceRange(
|
|
375
|
+
wikiLinkRange.start,
|
|
376
|
+
wikiLinkRange.end,
|
|
377
|
+
`[[${title}]]`
|
|
378
|
+
);
|
|
379
|
+
if (didReplace) {
|
|
380
|
+
setWikiLinkOpen(false);
|
|
381
|
+
setWikiLinkActiveIndex(-1);
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
[wikiLinkRange]
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const handleCreateLinkedNote = useCallback(
|
|
388
|
+
async (title: string) => {
|
|
389
|
+
if (!doc) return;
|
|
390
|
+
const filename = title
|
|
391
|
+
.toLowerCase()
|
|
392
|
+
.trim()
|
|
393
|
+
.replaceAll(/[^\w\s-]/g, "")
|
|
394
|
+
.replaceAll(/\s+/g, "-")
|
|
395
|
+
.replaceAll(/-+/g, "-")
|
|
396
|
+
.replace(/^-|-$/g, "");
|
|
397
|
+
const relPath = `${filename || "untitled"}.md`;
|
|
398
|
+
const { data, error: err } = await apiFetch<CreateEditableCopyResponse>(
|
|
399
|
+
"/api/docs",
|
|
400
|
+
{
|
|
401
|
+
method: "POST",
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
collection: doc.collection,
|
|
404
|
+
relPath,
|
|
405
|
+
content: `# ${title}\n`,
|
|
406
|
+
}),
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
if (err) {
|
|
410
|
+
setCopyError(err);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
insertWikiLink(title);
|
|
414
|
+
if (data) {
|
|
415
|
+
setWikiLinkDocs((current) => [
|
|
416
|
+
...current,
|
|
417
|
+
{
|
|
418
|
+
title,
|
|
419
|
+
uri: data.uri,
|
|
420
|
+
docid: data.uri,
|
|
421
|
+
collection: doc.collection,
|
|
422
|
+
},
|
|
423
|
+
]);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
[doc, insertWikiLink]
|
|
427
|
+
);
|
|
428
|
+
|
|
251
429
|
// Debounced auto-save
|
|
252
430
|
const { debouncedFn: debouncedSave } = useDebouncedCallback(
|
|
253
431
|
saveDocument,
|
|
@@ -262,10 +440,47 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
262
440
|
setSaveStatus("unsaved");
|
|
263
441
|
debouncedSave(newContent);
|
|
264
442
|
}
|
|
443
|
+
|
|
444
|
+
const cursor = editorRef.current?.getCursorInfo();
|
|
445
|
+
if (!cursor) {
|
|
446
|
+
setWikiLinkOpen(false);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const activeQuery = getActiveWikiLinkQuery(newContent, cursor.pos);
|
|
451
|
+
if (!activeQuery) {
|
|
452
|
+
setWikiLinkOpen(false);
|
|
453
|
+
setWikiLinkRange(null);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
setWikiLinkRange({ start: activeQuery.start, end: activeQuery.end });
|
|
458
|
+
setWikiLinkQuery(activeQuery.query);
|
|
459
|
+
setWikiLinkPosition({ x: cursor.x, y: cursor.y + 8 });
|
|
460
|
+
setWikiLinkOpen(true);
|
|
461
|
+
setWikiLinkActiveIndex(0);
|
|
265
462
|
},
|
|
266
463
|
[originalContent, debouncedSave]
|
|
267
464
|
);
|
|
268
465
|
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
if (!wikiLinkOpen) return;
|
|
468
|
+
|
|
469
|
+
const params = new URLSearchParams({
|
|
470
|
+
limit: "8",
|
|
471
|
+
query: wikiLinkQuery,
|
|
472
|
+
});
|
|
473
|
+
if (doc?.collection) {
|
|
474
|
+
params.set("collection", doc.collection);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
void apiFetch<DocsAutocompleteResponse>(
|
|
478
|
+
`/api/docs/autocomplete?${params.toString()}`
|
|
479
|
+
).then(({ data }) => {
|
|
480
|
+
setWikiLinkDocs(data?.docs ?? []);
|
|
481
|
+
});
|
|
482
|
+
}, [doc?.collection, wikiLinkOpen, wikiLinkQuery]);
|
|
483
|
+
|
|
269
484
|
// Force save (Cmd+S) - saves and triggers embedding
|
|
270
485
|
const handleForceSave = useCallback(async () => {
|
|
271
486
|
if (!hasUnsavedChanges || !doc) return;
|
|
@@ -274,11 +489,15 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
274
489
|
setSaveError(null);
|
|
275
490
|
|
|
276
491
|
// Save document
|
|
277
|
-
const { error: err } = await apiFetch(
|
|
492
|
+
const { data, error: err } = await apiFetch<UpdateDocResponse>(
|
|
278
493
|
`/api/docs/${encodeURIComponent(doc.docid)}`,
|
|
279
494
|
{
|
|
280
495
|
method: "PUT",
|
|
281
|
-
body: JSON.stringify({
|
|
496
|
+
body: JSON.stringify({
|
|
497
|
+
content,
|
|
498
|
+
expectedSourceHash: doc.source.sourceHash,
|
|
499
|
+
expectedModifiedAt: doc.source.modifiedAt,
|
|
500
|
+
}),
|
|
282
501
|
}
|
|
283
502
|
);
|
|
284
503
|
|
|
@@ -288,18 +507,31 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
288
507
|
return;
|
|
289
508
|
}
|
|
290
509
|
|
|
510
|
+
ignoreDocEventsUntilRef.current = Date.now() + 5_000;
|
|
511
|
+
if (originalContent !== content) {
|
|
512
|
+
appendLocalHistory(doc.docid, originalContent);
|
|
513
|
+
setHasLocalSnapshot(true);
|
|
514
|
+
}
|
|
291
515
|
setSaveStatus("saved");
|
|
292
516
|
setOriginalContent(content);
|
|
293
517
|
setLastSaved(new Date());
|
|
518
|
+
if (data) {
|
|
519
|
+
setDoc({
|
|
520
|
+
...doc,
|
|
521
|
+
source: {
|
|
522
|
+
...doc.source,
|
|
523
|
+
sourceHash: data.version.sourceHash,
|
|
524
|
+
modifiedAt: data.version.modifiedAt,
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
}
|
|
294
528
|
|
|
295
529
|
// Trigger embedding (fire and forget - don't block on result)
|
|
296
530
|
void apiFetch("/api/embed", { method: "POST" });
|
|
297
531
|
}, [hasUnsavedChanges, doc, content]);
|
|
298
532
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const params = new URLSearchParams(window.location.search);
|
|
302
|
-
const uri = params.get("uri");
|
|
533
|
+
const loadDocument = useCallback(() => {
|
|
534
|
+
const uri = currentTarget.uri;
|
|
303
535
|
|
|
304
536
|
if (!uri) {
|
|
305
537
|
setError("No document URI provided");
|
|
@@ -317,14 +549,49 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
317
549
|
const docContent = data.content ?? "";
|
|
318
550
|
setContent(docContent);
|
|
319
551
|
setOriginalContent(docContent);
|
|
552
|
+
setHasLocalSnapshot(Boolean(loadLatestLocalHistory(data.docid)));
|
|
320
553
|
// Ensure CodeMirror reflects content after async load
|
|
321
554
|
requestAnimationFrame(() => {
|
|
322
555
|
editorRef.current?.setValue(docContent);
|
|
556
|
+
if (currentTarget.lineStart) {
|
|
557
|
+
editorRef.current?.revealLine(currentTarget.lineStart);
|
|
558
|
+
}
|
|
323
559
|
});
|
|
324
560
|
}
|
|
325
561
|
}
|
|
326
562
|
);
|
|
327
|
-
}, []);
|
|
563
|
+
}, [currentTarget.lineStart, currentTarget.uri]);
|
|
564
|
+
|
|
565
|
+
// Load document
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
loadDocument();
|
|
568
|
+
}, [loadDocument]);
|
|
569
|
+
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (!doc || latestDocEvent?.uri !== doc.uri) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (Date.now() < ignoreDocEventsUntilRef.current) {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
setExternalChangeNotice(
|
|
578
|
+
"This document changed on disk. Reload before continuing."
|
|
579
|
+
);
|
|
580
|
+
}, [doc, latestDocEvent?.changedAt, latestDocEvent?.uri]);
|
|
581
|
+
|
|
582
|
+
const reloadDocument = useCallback(() => {
|
|
583
|
+
setExternalChangeNotice(null);
|
|
584
|
+
loadDocument();
|
|
585
|
+
}, [loadDocument]);
|
|
586
|
+
|
|
587
|
+
const restoreLatestSnapshot = useCallback(() => {
|
|
588
|
+
if (!doc) return;
|
|
589
|
+
const latest = loadLatestLocalHistory(doc.docid);
|
|
590
|
+
if (!latest) return;
|
|
591
|
+
setContent(latest.content);
|
|
592
|
+
editorRef.current?.setValue(latest.content);
|
|
593
|
+
setSaveStatus("unsaved");
|
|
594
|
+
}, [doc]);
|
|
328
595
|
|
|
329
596
|
// Keyboard shortcuts
|
|
330
597
|
useEffect(() => {
|
|
@@ -510,6 +777,70 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
510
777
|
);
|
|
511
778
|
}
|
|
512
779
|
|
|
780
|
+
if (!doc.capabilities.editable) {
|
|
781
|
+
return (
|
|
782
|
+
<div className="flex min-h-screen items-center justify-center px-6">
|
|
783
|
+
<div className="w-full max-w-xl rounded-lg border border-border/50 bg-card p-6">
|
|
784
|
+
<div className="mb-4 flex items-center gap-3">
|
|
785
|
+
<AlertCircleIcon className="size-8 text-amber-500" />
|
|
786
|
+
<div>
|
|
787
|
+
<h2 className="font-semibold text-xl">Read-only document</h2>
|
|
788
|
+
<p className="text-muted-foreground text-sm">
|
|
789
|
+
{doc.capabilities.reason ??
|
|
790
|
+
"This document cannot be edited in place."}
|
|
791
|
+
</p>
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
{copyError && (
|
|
796
|
+
<div className="mb-4 rounded-md border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500 text-sm">
|
|
797
|
+
{copyError}
|
|
798
|
+
</div>
|
|
799
|
+
)}
|
|
800
|
+
|
|
801
|
+
<div className="flex flex-wrap gap-3">
|
|
802
|
+
{doc.capabilities.canCreateEditableCopy && (
|
|
803
|
+
<Button
|
|
804
|
+
disabled={creatingCopy}
|
|
805
|
+
onClick={() => {
|
|
806
|
+
void handleCreateEditableCopy();
|
|
807
|
+
}}
|
|
808
|
+
>
|
|
809
|
+
{creatingCopy ? (
|
|
810
|
+
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
|
811
|
+
) : (
|
|
812
|
+
<PenIcon className="mr-2 size-4" />
|
|
813
|
+
)}
|
|
814
|
+
Create editable copy
|
|
815
|
+
</Button>
|
|
816
|
+
)}
|
|
817
|
+
<Button
|
|
818
|
+
onClick={() =>
|
|
819
|
+
navigate(`/doc?uri=${encodeURIComponent(doc.uri)}`)
|
|
820
|
+
}
|
|
821
|
+
variant="outline"
|
|
822
|
+
>
|
|
823
|
+
<BookOpenIcon className="mr-2 size-4" />
|
|
824
|
+
View document
|
|
825
|
+
</Button>
|
|
826
|
+
{doc.source.absPath && (
|
|
827
|
+
<Button asChild variant="outline">
|
|
828
|
+
<a
|
|
829
|
+
href={`file://${doc.source.absPath}`}
|
|
830
|
+
rel="noopener noreferrer"
|
|
831
|
+
target="_blank"
|
|
832
|
+
>
|
|
833
|
+
<SquareArrowOutUpRightIcon className="mr-2 size-4" />
|
|
834
|
+
Open original
|
|
835
|
+
</a>
|
|
836
|
+
</Button>
|
|
837
|
+
)}
|
|
838
|
+
</div>
|
|
839
|
+
</div>
|
|
840
|
+
</div>
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
513
844
|
return (
|
|
514
845
|
<div className="flex h-screen flex-col overflow-hidden">
|
|
515
846
|
{/* Toolbar */}
|
|
@@ -564,6 +895,15 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
564
895
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
565
896
|
<PenIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
566
897
|
<h1 className="truncate font-medium">{doc.title || doc.relPath}</h1>
|
|
898
|
+
{currentTarget.lineStart && (
|
|
899
|
+
<Badge className="shrink-0 font-mono" variant="outline">
|
|
900
|
+
L{currentTarget.lineStart}
|
|
901
|
+
{currentTarget.lineEnd &&
|
|
902
|
+
currentTarget.lineEnd !== currentTarget.lineStart
|
|
903
|
+
? `-${currentTarget.lineEnd}`
|
|
904
|
+
: ""}
|
|
905
|
+
</Badge>
|
|
906
|
+
)}
|
|
567
907
|
{hasUnsavedChanges && (
|
|
568
908
|
<Badge
|
|
569
909
|
className="shrink-0 bg-yellow-500/20 text-yellow-500"
|
|
@@ -579,6 +919,22 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
579
919
|
|
|
580
920
|
<Separator className="h-5" orientation="vertical" />
|
|
581
921
|
|
|
922
|
+
<Button
|
|
923
|
+
onClick={() => {
|
|
924
|
+
void navigator.clipboard.writeText(
|
|
925
|
+
`${window.location.origin}${buildEditDeepLink({
|
|
926
|
+
uri: doc.uri,
|
|
927
|
+
lineStart: currentTarget.lineStart,
|
|
928
|
+
lineEnd: currentTarget.lineEnd,
|
|
929
|
+
})}`
|
|
930
|
+
);
|
|
931
|
+
}}
|
|
932
|
+
size="sm"
|
|
933
|
+
variant="ghost"
|
|
934
|
+
>
|
|
935
|
+
<LinkIcon className="size-4" />
|
|
936
|
+
</Button>
|
|
937
|
+
|
|
582
938
|
{/* Preview toggle */}
|
|
583
939
|
<TooltipProvider>
|
|
584
940
|
<Tooltip>
|
|
@@ -647,6 +1003,26 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
647
1003
|
</div>
|
|
648
1004
|
</header>
|
|
649
1005
|
|
|
1006
|
+
{externalChangeNotice && (
|
|
1007
|
+
<div className="border-amber-500/30 border-b bg-amber-500/10 px-4 py-3">
|
|
1008
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1009
|
+
<p className="text-amber-500 text-sm">{externalChangeNotice}</p>
|
|
1010
|
+
<Button onClick={reloadDocument} size="sm" variant="outline">
|
|
1011
|
+
Reload
|
|
1012
|
+
</Button>
|
|
1013
|
+
{hasLocalSnapshot && (
|
|
1014
|
+
<Button
|
|
1015
|
+
onClick={restoreLatestSnapshot}
|
|
1016
|
+
size="sm"
|
|
1017
|
+
variant="outline"
|
|
1018
|
+
>
|
|
1019
|
+
Restore snapshot
|
|
1020
|
+
</Button>
|
|
1021
|
+
)}
|
|
1022
|
+
</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
)}
|
|
1025
|
+
|
|
650
1026
|
{/* Editor area */}
|
|
651
1027
|
<div className="flex min-h-0 flex-1">
|
|
652
1028
|
{/* Editor pane */}
|
|
@@ -684,6 +1060,20 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
684
1060
|
)}
|
|
685
1061
|
</div>
|
|
686
1062
|
|
|
1063
|
+
<WikiLinkAutocomplete
|
|
1064
|
+
activeIndex={wikiLinkActiveIndex}
|
|
1065
|
+
docs={wikiLinkDocs}
|
|
1066
|
+
isOpen={wikiLinkOpen}
|
|
1067
|
+
onActiveIndexChange={setWikiLinkActiveIndex}
|
|
1068
|
+
onCreateNew={(title) => {
|
|
1069
|
+
void handleCreateLinkedNote(title);
|
|
1070
|
+
}}
|
|
1071
|
+
onDismiss={() => setWikiLinkOpen(false)}
|
|
1072
|
+
onSelect={(title) => insertWikiLink(title)}
|
|
1073
|
+
position={wikiLinkPosition}
|
|
1074
|
+
searchQuery={wikiLinkQuery}
|
|
1075
|
+
/>
|
|
1076
|
+
|
|
687
1077
|
{/* Unsaved changes dialog */}
|
|
688
1078
|
<Dialog onOpenChange={setShowUnsavedDialog} open={showUnsavedDialog}>
|
|
689
1079
|
<DialogContent>
|
|
@@ -35,7 +35,9 @@ import {
|
|
|
35
35
|
} from "../components/ui/select";
|
|
36
36
|
import { Textarea } from "../components/ui/textarea";
|
|
37
37
|
import { apiFetch } from "../hooks/use-api";
|
|
38
|
+
import { useDocEvents } from "../hooks/use-doc-events";
|
|
38
39
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
|
40
|
+
import { buildDocDeepLink } from "../lib/deep-links";
|
|
39
41
|
import {
|
|
40
42
|
applyFiltersToUrl,
|
|
41
43
|
parseFiltersFromSearch,
|
|
@@ -194,6 +196,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
194
196
|
const [queryModeText, setQueryModeText] = useState("");
|
|
195
197
|
const [queryModeError, setQueryModeError] = useState<string | null>(null);
|
|
196
198
|
const [showMobileTags, setShowMobileTags] = useState(false);
|
|
199
|
+
const latestDocEvent = useDocEvents();
|
|
197
200
|
|
|
198
201
|
const hybridAvailable = capabilities?.hybrid ?? false;
|
|
199
202
|
const structuredQueryState = useMemo(
|
|
@@ -446,6 +449,15 @@ export default function Search({ navigate }: PageProps) {
|
|
|
446
449
|
until,
|
|
447
450
|
]);
|
|
448
451
|
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (!latestDocEvent?.changedAt) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (searched && query.trim()) {
|
|
457
|
+
void handleSearch();
|
|
458
|
+
}
|
|
459
|
+
}, [handleSearch, latestDocEvent?.changedAt, query, searched]);
|
|
460
|
+
|
|
449
461
|
const thoroughnessDesc =
|
|
450
462
|
thoroughness === "fast"
|
|
451
463
|
? "Keyword search (BM25)"
|
|
@@ -1037,7 +1049,14 @@ export default function Search({ navigate }: PageProps) {
|
|
|
1037
1049
|
className="group animate-fade-in cursor-pointer opacity-0 transition-all hover:border-primary/50 hover:bg-card/80"
|
|
1038
1050
|
key={`${result.docid}-${i}`}
|
|
1039
1051
|
onClick={() =>
|
|
1040
|
-
navigate(
|
|
1052
|
+
navigate(
|
|
1053
|
+
buildDocDeepLink({
|
|
1054
|
+
uri: result.uri,
|
|
1055
|
+
view: result.snippetRange ? "source" : "rendered",
|
|
1056
|
+
lineStart: result.snippetRange?.startLine,
|
|
1057
|
+
lineEnd: result.snippetRange?.endLine,
|
|
1058
|
+
})
|
|
1059
|
+
)
|
|
1041
1060
|
}
|
|
1042
1061
|
style={{ animationDelay: `${i * 0.05}s` }}
|
|
1043
1062
|
>
|