@gmickel/gno 0.28.2 → 0.29.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/README.md +10 -2
- package/package.json +1 -1
- package/src/app/constants.ts +4 -2
- package/src/cli/commands/mcp/install.ts +4 -4
- package/src/cli/commands/mcp/status.ts +7 -7
- package/src/cli/commands/skill/install.ts +5 -5
- package/src/cli/program.ts +2 -2
- package/src/collection/add.ts +10 -0
- package/src/collection/types.ts +1 -0
- package/src/config/types.ts +12 -2
- package/src/core/depth-policy.ts +1 -1
- package/src/core/file-ops.ts +203 -1
- package/src/llm/registry.ts +20 -4
- package/src/serve/AGENTS.md +16 -16
- package/src/serve/CLAUDE.md +16 -16
- package/src/serve/config-sync.ts +32 -1
- package/src/serve/connectors.ts +243 -0
- package/src/serve/context.ts +9 -0
- package/src/serve/doc-events.ts +31 -1
- package/src/serve/embed-scheduler.ts +12 -0
- package/src/serve/import-preview.ts +173 -0
- package/src/serve/public/app.tsx +101 -7
- package/src/serve/public/components/AIModelSelector.tsx +383 -145
- package/src/serve/public/components/AddCollectionDialog.tsx +123 -7
- package/src/serve/public/components/BootstrapStatus.tsx +133 -0
- package/src/serve/public/components/CaptureModal.tsx +5 -2
- package/src/serve/public/components/CollectionsEmptyState.tsx +63 -0
- package/src/serve/public/components/FirstRunWizard.tsx +622 -0
- package/src/serve/public/components/HealthCenter.tsx +128 -0
- package/src/serve/public/components/IndexingProgress.tsx +21 -2
- package/src/serve/public/components/QuickSwitcher.tsx +62 -36
- package/src/serve/public/components/TagInput.tsx +5 -1
- package/src/serve/public/components/WikiLinkAutocomplete.tsx +15 -6
- package/src/serve/public/components/WorkspaceTabs.tsx +60 -0
- package/src/serve/public/hooks/use-doc-events.ts +48 -4
- package/src/serve/public/lib/local-history.ts +40 -7
- package/src/serve/public/lib/navigation-state.ts +156 -0
- package/src/serve/public/lib/workspace-tabs.ts +235 -0
- package/src/serve/public/pages/Ask.tsx +11 -1
- package/src/serve/public/pages/Browse.tsx +73 -0
- package/src/serve/public/pages/Collections.tsx +29 -13
- package/src/serve/public/pages/Connectors.tsx +178 -0
- package/src/serve/public/pages/Dashboard.tsx +493 -67
- package/src/serve/public/pages/DocView.tsx +192 -34
- package/src/serve/public/pages/DocumentEditor.tsx +127 -5
- package/src/serve/public/pages/Search.tsx +12 -1
- package/src/serve/routes/api.ts +541 -62
- package/src/serve/server.ts +79 -2
- package/src/serve/status-model.ts +149 -0
- package/src/serve/status.ts +706 -0
- package/src/serve/watch-service.ts +73 -8
- package/src/types/electrobun-shell.d.ts +43 -0
|
@@ -97,6 +97,13 @@ interface CreateEditableCopyResponse {
|
|
|
97
97
|
note?: string;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
interface RenameDocResponse {
|
|
101
|
+
success: boolean;
|
|
102
|
+
uri: string;
|
|
103
|
+
path: string;
|
|
104
|
+
relPath: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
interface UpdateDocResponse {
|
|
101
108
|
success: boolean;
|
|
102
109
|
docId: string;
|
|
@@ -219,6 +226,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
219
226
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
220
227
|
const [deleting, setDeleting] = useState(false);
|
|
221
228
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
229
|
+
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
|
|
230
|
+
const [renaming, setRenaming] = useState(false);
|
|
231
|
+
const [renameError, setRenameError] = useState<string | null>(null);
|
|
232
|
+
const [renameValue, setRenameValue] = useState("");
|
|
222
233
|
const [showRawView, setShowRawView] = useState(false);
|
|
223
234
|
const [creatingCopy, setCreatingCopy] = useState(false);
|
|
224
235
|
const [copyError, setCopyError] = useState<string | null>(null);
|
|
@@ -364,7 +375,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
364
375
|
setCopyError(null);
|
|
365
376
|
const { data, error: err } = await apiFetch<CreateEditableCopyResponse>(
|
|
366
377
|
`/api/docs/${encodeURIComponent(doc.docid)}/editable-copy`,
|
|
367
|
-
{
|
|
378
|
+
{
|
|
379
|
+
method: "POST",
|
|
380
|
+
body: JSON.stringify({ uri: doc.uri }),
|
|
381
|
+
}
|
|
368
382
|
);
|
|
369
383
|
setCreatingCopy(false);
|
|
370
384
|
|
|
@@ -391,10 +405,10 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
391
405
|
setDeleting(true);
|
|
392
406
|
setDeleteError(null);
|
|
393
407
|
|
|
394
|
-
const
|
|
395
|
-
`/api/docs/${encodeURIComponent(doc.docid)}/
|
|
396
|
-
|
|
397
|
-
);
|
|
408
|
+
const endpoint = doc.capabilities.editable
|
|
409
|
+
? `/api/docs/${encodeURIComponent(doc.docid)}/trash?uri=${encodeURIComponent(doc.uri)}`
|
|
410
|
+
: `/api/docs/${encodeURIComponent(doc.docid)}/deactivate?uri=${encodeURIComponent(doc.uri)}`;
|
|
411
|
+
const { error: err } = await apiFetch(endpoint, { method: "POST" });
|
|
398
412
|
|
|
399
413
|
setDeleting(false);
|
|
400
414
|
|
|
@@ -407,6 +421,55 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
407
421
|
navigate(-1);
|
|
408
422
|
};
|
|
409
423
|
|
|
424
|
+
const handleStartRename = useCallback(() => {
|
|
425
|
+
if (!doc) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const filename = doc.relPath.split("/").pop() ?? doc.relPath;
|
|
429
|
+
setRenameValue(filename);
|
|
430
|
+
setRenameError(null);
|
|
431
|
+
setRenameDialogOpen(true);
|
|
432
|
+
}, [doc]);
|
|
433
|
+
|
|
434
|
+
const handleRename = useCallback(async () => {
|
|
435
|
+
if (!doc) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
setRenaming(true);
|
|
439
|
+
setRenameError(null);
|
|
440
|
+
const { data, error: err } = await apiFetch<RenameDocResponse>(
|
|
441
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/rename`,
|
|
442
|
+
{
|
|
443
|
+
method: "POST",
|
|
444
|
+
body: JSON.stringify({ name: renameValue, uri: doc.uri }),
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
setRenaming(false);
|
|
448
|
+
|
|
449
|
+
if (err) {
|
|
450
|
+
setRenameError(err);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
setRenameDialogOpen(false);
|
|
455
|
+
if (data?.uri) {
|
|
456
|
+
navigate(`/doc?uri=${encodeURIComponent(data.uri)}`);
|
|
457
|
+
}
|
|
458
|
+
}, [doc, navigate, renameValue]);
|
|
459
|
+
|
|
460
|
+
const handleReveal = useCallback(async () => {
|
|
461
|
+
if (!doc) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const { error: err } = await apiFetch(
|
|
465
|
+
`/api/docs/${encodeURIComponent(doc.docid)}/reveal?uri=${encodeURIComponent(doc.uri)}`,
|
|
466
|
+
{ method: "POST" }
|
|
467
|
+
);
|
|
468
|
+
if (err) {
|
|
469
|
+
setDeleteError(err);
|
|
470
|
+
}
|
|
471
|
+
}, [doc]);
|
|
472
|
+
|
|
410
473
|
// Start editing tags
|
|
411
474
|
const handleStartEditTags = useCallback(() => {
|
|
412
475
|
if (doc) {
|
|
@@ -440,6 +503,7 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
440
503
|
tags: editedTags,
|
|
441
504
|
expectedSourceHash: doc.source.sourceHash,
|
|
442
505
|
expectedModifiedAt: doc.source.modifiedAt,
|
|
506
|
+
uri: doc.uri,
|
|
443
507
|
}),
|
|
444
508
|
}
|
|
445
509
|
);
|
|
@@ -512,10 +576,21 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
512
576
|
<Separator className="h-6" orientation="vertical" />
|
|
513
577
|
<div className="flex items-center gap-2">
|
|
514
578
|
{doc.capabilities.editable ? (
|
|
515
|
-
|
|
516
|
-
<
|
|
517
|
-
|
|
518
|
-
|
|
579
|
+
<>
|
|
580
|
+
<Button className="gap-1.5" onClick={handleEdit} size="sm">
|
|
581
|
+
<PencilIcon className="size-4" />
|
|
582
|
+
Edit
|
|
583
|
+
</Button>
|
|
584
|
+
<Button
|
|
585
|
+
className="gap-1.5"
|
|
586
|
+
onClick={handleStartRename}
|
|
587
|
+
size="sm"
|
|
588
|
+
variant="outline"
|
|
589
|
+
>
|
|
590
|
+
<TextIcon className="size-4" />
|
|
591
|
+
Rename
|
|
592
|
+
</Button>
|
|
593
|
+
</>
|
|
519
594
|
) : (
|
|
520
595
|
<>
|
|
521
596
|
{doc.capabilities.canCreateEditableCopy && (
|
|
@@ -536,19 +611,45 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
536
611
|
</Button>
|
|
537
612
|
)}
|
|
538
613
|
{doc.source.absPath && (
|
|
539
|
-
|
|
540
|
-
<
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
614
|
+
<>
|
|
615
|
+
<Button
|
|
616
|
+
className="gap-1.5"
|
|
617
|
+
onClick={() => {
|
|
618
|
+
void handleReveal();
|
|
619
|
+
}}
|
|
620
|
+
size="sm"
|
|
621
|
+
variant="outline"
|
|
544
622
|
>
|
|
545
|
-
<
|
|
546
|
-
|
|
547
|
-
</
|
|
548
|
-
|
|
623
|
+
<FolderOpen className="size-4" />
|
|
624
|
+
Reveal
|
|
625
|
+
</Button>
|
|
626
|
+
<Button asChild size="sm" variant="outline">
|
|
627
|
+
<a
|
|
628
|
+
href={`file://${doc.source.absPath}`}
|
|
629
|
+
rel="noopener noreferrer"
|
|
630
|
+
target="_blank"
|
|
631
|
+
>
|
|
632
|
+
<SquareArrowOutUpRightIcon className="mr-1.5 size-4" />
|
|
633
|
+
Open original
|
|
634
|
+
</a>
|
|
635
|
+
</Button>
|
|
636
|
+
</>
|
|
549
637
|
)}
|
|
550
638
|
</>
|
|
551
639
|
)}
|
|
640
|
+
{doc.capabilities.editable && doc.source.absPath && (
|
|
641
|
+
<Button
|
|
642
|
+
className="gap-1.5"
|
|
643
|
+
onClick={() => {
|
|
644
|
+
void handleReveal();
|
|
645
|
+
}}
|
|
646
|
+
size="sm"
|
|
647
|
+
variant="outline"
|
|
648
|
+
>
|
|
649
|
+
<FolderOpen className="size-4" />
|
|
650
|
+
Reveal
|
|
651
|
+
</Button>
|
|
652
|
+
)}
|
|
552
653
|
<Button
|
|
553
654
|
className="gap-1.5 text-muted-foreground hover:text-destructive"
|
|
554
655
|
onClick={() => setDeleteDialogOpen(true)}
|
|
@@ -943,23 +1044,42 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
943
1044
|
<DialogHeader>
|
|
944
1045
|
<DialogTitle className="flex items-center gap-2">
|
|
945
1046
|
<TrashIcon className="size-5 text-destructive" />
|
|
946
|
-
|
|
1047
|
+
{doc?.capabilities.editable
|
|
1048
|
+
? "Move to Trash?"
|
|
1049
|
+
: "Remove from index?"}
|
|
947
1050
|
</DialogTitle>
|
|
948
1051
|
<DialogDescription className="space-y-3 pt-2">
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1052
|
+
{doc?.capabilities.editable ? (
|
|
1053
|
+
<>
|
|
1054
|
+
<span className="block">
|
|
1055
|
+
This will move{" "}
|
|
1056
|
+
<strong>"{doc?.title || doc?.relPath}"</strong> to your
|
|
1057
|
+
system Trash and remove it from the current index.
|
|
1058
|
+
</span>
|
|
1059
|
+
<span className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500">
|
|
1060
|
+
<AlertTriangleIcon className="mt-0.5 size-4 shrink-0" />
|
|
1061
|
+
<span className="text-sm">
|
|
1062
|
+
This is reversible through Trash. GNO will stop showing
|
|
1063
|
+
the file after the current collection refresh.
|
|
1064
|
+
</span>
|
|
1065
|
+
</span>
|
|
1066
|
+
</>
|
|
1067
|
+
) : (
|
|
1068
|
+
<>
|
|
1069
|
+
<span className="block">
|
|
1070
|
+
This will remove{" "}
|
|
1071
|
+
<strong>"{doc?.title || doc?.relPath}"</strong> from the GNO
|
|
1072
|
+
search index.
|
|
1073
|
+
</span>
|
|
1074
|
+
<span className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500">
|
|
1075
|
+
<AlertTriangleIcon className="mt-0.5 size-4 shrink-0" />
|
|
1076
|
+
<span className="text-sm">
|
|
1077
|
+
The source file stays on disk. It may be re-indexed on the
|
|
1078
|
+
next sync unless you exclude it.
|
|
1079
|
+
</span>
|
|
1080
|
+
</span>
|
|
1081
|
+
</>
|
|
1082
|
+
)}
|
|
963
1083
|
</DialogDescription>
|
|
964
1084
|
</DialogHeader>
|
|
965
1085
|
|
|
@@ -984,7 +1104,45 @@ export default function DocView({ navigate }: PageProps) {
|
|
|
984
1104
|
{deleting && (
|
|
985
1105
|
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
986
1106
|
)}
|
|
987
|
-
|
|
1107
|
+
{doc?.capabilities.editable
|
|
1108
|
+
? "Move to Trash"
|
|
1109
|
+
: "Remove from index"}
|
|
1110
|
+
</Button>
|
|
1111
|
+
</DialogFooter>
|
|
1112
|
+
</DialogContent>
|
|
1113
|
+
</Dialog>
|
|
1114
|
+
|
|
1115
|
+
<Dialog onOpenChange={setRenameDialogOpen} open={renameDialogOpen}>
|
|
1116
|
+
<DialogContent>
|
|
1117
|
+
<DialogHeader>
|
|
1118
|
+
<DialogTitle>Rename document</DialogTitle>
|
|
1119
|
+
<DialogDescription>
|
|
1120
|
+
Rename the file on disk inside its current folder. This does not
|
|
1121
|
+
move it to another collection yet.
|
|
1122
|
+
</DialogDescription>
|
|
1123
|
+
</DialogHeader>
|
|
1124
|
+
<input
|
|
1125
|
+
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm"
|
|
1126
|
+
onChange={(event) => setRenameValue(event.target.value)}
|
|
1127
|
+
value={renameValue}
|
|
1128
|
+
/>
|
|
1129
|
+
{renameError && (
|
|
1130
|
+
<div className="rounded-lg bg-destructive/10 p-3 text-destructive text-sm">
|
|
1131
|
+
{renameError}
|
|
1132
|
+
</div>
|
|
1133
|
+
)}
|
|
1134
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
1135
|
+
<Button
|
|
1136
|
+
onClick={() => setRenameDialogOpen(false)}
|
|
1137
|
+
variant="outline"
|
|
1138
|
+
>
|
|
1139
|
+
Cancel
|
|
1140
|
+
</Button>
|
|
1141
|
+
<Button disabled={renaming} onClick={() => void handleRename()}>
|
|
1142
|
+
{renaming && (
|
|
1143
|
+
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
1144
|
+
)}
|
|
1145
|
+
Rename
|
|
988
1146
|
</Button>
|
|
989
1147
|
</DialogFooter>
|
|
990
1148
|
</DialogContent>
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
BookOpenIcon,
|
|
16
16
|
CheckIcon,
|
|
17
17
|
CloudIcon,
|
|
18
|
+
HistoryIcon,
|
|
18
19
|
EyeIcon,
|
|
19
20
|
EyeOffIcon,
|
|
20
21
|
HomeIcon,
|
|
@@ -63,7 +64,9 @@ import { buildEditDeepLink, parseDocumentDeepLink } from "../lib/deep-links";
|
|
|
63
64
|
import { waitForDocumentAvailability } from "../lib/document-availability";
|
|
64
65
|
import {
|
|
65
66
|
appendLocalHistory,
|
|
67
|
+
loadLocalHistory,
|
|
66
68
|
loadLatestLocalHistory,
|
|
69
|
+
type LocalHistoryEntry,
|
|
67
70
|
} from "../lib/local-history";
|
|
68
71
|
import { getActiveWikiLinkQuery } from "../lib/wiki-link";
|
|
69
72
|
|
|
@@ -193,7 +196,9 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
193
196
|
const [externalChangeNotice, setExternalChangeNotice] = useState<
|
|
194
197
|
string | null
|
|
195
198
|
>(null);
|
|
196
|
-
const [
|
|
199
|
+
const [historyEntries, setHistoryEntries] = useState<LocalHistoryEntry[]>([]);
|
|
200
|
+
const [historyDialogOpen, setHistoryDialogOpen] = useState(false);
|
|
201
|
+
const [selectedHistoryIndex, setSelectedHistoryIndex] = useState(0);
|
|
197
202
|
|
|
198
203
|
const [showPreview, setShowPreview] = useState(true);
|
|
199
204
|
const [syncScroll, setSyncScroll] = useState(true);
|
|
@@ -220,9 +225,20 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
220
225
|
const latestDocEvent = useDocEvents();
|
|
221
226
|
|
|
222
227
|
const hasUnsavedChanges = content !== originalContent;
|
|
228
|
+
const hasLocalSnapshot = historyEntries.length > 0;
|
|
223
229
|
const parsedContent = useMemo(() => parseFrontmatter(content), [content]);
|
|
224
230
|
const hasFrontmatter = Object.keys(parsedContent.data).length > 0;
|
|
225
231
|
|
|
232
|
+
const refreshHistoryEntries = useCallback((docId: string) => {
|
|
233
|
+
const next = loadLocalHistory(docId);
|
|
234
|
+
setHistoryEntries(next);
|
|
235
|
+
if (next.length === 0) {
|
|
236
|
+
setSelectedHistoryIndex(0);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
setSelectedHistoryIndex((current) => Math.min(current, next.length - 1));
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
226
242
|
// Reset ignore flags when sync is toggled to prevent stale state
|
|
227
243
|
useEffect(() => {
|
|
228
244
|
ignoreNextEditorScroll.current = false;
|
|
@@ -304,6 +320,7 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
304
320
|
content: contentToSave,
|
|
305
321
|
expectedSourceHash: doc.source.sourceHash,
|
|
306
322
|
expectedModifiedAt: doc.source.modifiedAt,
|
|
323
|
+
uri: doc.uri,
|
|
307
324
|
}),
|
|
308
325
|
}
|
|
309
326
|
);
|
|
@@ -315,7 +332,7 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
315
332
|
ignoreDocEventsUntilRef.current = Date.now() + 5_000;
|
|
316
333
|
if (originalContent !== contentToSave) {
|
|
317
334
|
appendLocalHistory(doc.docid, originalContent);
|
|
318
|
-
|
|
335
|
+
refreshHistoryEntries(doc.docid);
|
|
319
336
|
}
|
|
320
337
|
setSaveStatus("saved");
|
|
321
338
|
setOriginalContent(contentToSave);
|
|
@@ -346,7 +363,10 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
346
363
|
setCopyError(null);
|
|
347
364
|
const { data, error: err } = await apiFetch<CreateEditableCopyResponse>(
|
|
348
365
|
`/api/docs/${encodeURIComponent(doc.docid)}/editable-copy`,
|
|
349
|
-
{
|
|
366
|
+
{
|
|
367
|
+
method: "POST",
|
|
368
|
+
body: JSON.stringify({ uri: doc.uri }),
|
|
369
|
+
}
|
|
350
370
|
);
|
|
351
371
|
setCreatingCopy(false);
|
|
352
372
|
|
|
@@ -497,6 +517,7 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
497
517
|
content,
|
|
498
518
|
expectedSourceHash: doc.source.sourceHash,
|
|
499
519
|
expectedModifiedAt: doc.source.modifiedAt,
|
|
520
|
+
uri: doc.uri,
|
|
500
521
|
}),
|
|
501
522
|
}
|
|
502
523
|
);
|
|
@@ -510,7 +531,7 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
510
531
|
ignoreDocEventsUntilRef.current = Date.now() + 5_000;
|
|
511
532
|
if (originalContent !== content) {
|
|
512
533
|
appendLocalHistory(doc.docid, originalContent);
|
|
513
|
-
|
|
534
|
+
refreshHistoryEntries(doc.docid);
|
|
514
535
|
}
|
|
515
536
|
setSaveStatus("saved");
|
|
516
537
|
setOriginalContent(content);
|
|
@@ -549,7 +570,7 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
549
570
|
const docContent = data.content ?? "";
|
|
550
571
|
setContent(docContent);
|
|
551
572
|
setOriginalContent(docContent);
|
|
552
|
-
|
|
573
|
+
refreshHistoryEntries(data.docid);
|
|
553
574
|
// Ensure CodeMirror reflects content after async load
|
|
554
575
|
requestAnimationFrame(() => {
|
|
555
576
|
editorRef.current?.setValue(docContent);
|
|
@@ -593,6 +614,18 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
593
614
|
setSaveStatus("unsaved");
|
|
594
615
|
}, [doc]);
|
|
595
616
|
|
|
617
|
+
const selectedHistoryEntry = historyEntries[selectedHistoryIndex] ?? null;
|
|
618
|
+
|
|
619
|
+
const restoreSelectedHistory = useCallback(() => {
|
|
620
|
+
if (!selectedHistoryEntry) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
setContent(selectedHistoryEntry.content);
|
|
624
|
+
editorRef.current?.setValue(selectedHistoryEntry.content);
|
|
625
|
+
setSaveStatus("unsaved");
|
|
626
|
+
setHistoryDialogOpen(false);
|
|
627
|
+
}, [selectedHistoryEntry]);
|
|
628
|
+
|
|
596
629
|
// Keyboard shortcuts
|
|
597
630
|
useEffect(() => {
|
|
598
631
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
@@ -988,6 +1021,15 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
988
1021
|
)}
|
|
989
1022
|
|
|
990
1023
|
{/* Save button */}
|
|
1024
|
+
<Button
|
|
1025
|
+
disabled={!hasLocalSnapshot}
|
|
1026
|
+
onClick={() => setHistoryDialogOpen(true)}
|
|
1027
|
+
size="sm"
|
|
1028
|
+
variant="outline"
|
|
1029
|
+
>
|
|
1030
|
+
<HistoryIcon className="mr-1.5 size-4" />
|
|
1031
|
+
History
|
|
1032
|
+
</Button>
|
|
991
1033
|
<Button
|
|
992
1034
|
disabled={!hasUnsavedChanges || saveStatus === "saving"}
|
|
993
1035
|
onClick={handleForceSave}
|
|
@@ -1060,6 +1102,86 @@ export default function DocumentEditor({ navigate }: PageProps) {
|
|
|
1060
1102
|
)}
|
|
1061
1103
|
</div>
|
|
1062
1104
|
|
|
1105
|
+
<Dialog onOpenChange={setHistoryDialogOpen} open={historyDialogOpen}>
|
|
1106
|
+
<DialogContent className="max-w-3xl">
|
|
1107
|
+
<DialogHeader>
|
|
1108
|
+
<DialogTitle>Local history</DialogTitle>
|
|
1109
|
+
<DialogDescription>
|
|
1110
|
+
Restore a recent local snapshot captured before an in-app save.
|
|
1111
|
+
</DialogDescription>
|
|
1112
|
+
</DialogHeader>
|
|
1113
|
+
<div className="grid gap-4 md:grid-cols-[220px_1fr]">
|
|
1114
|
+
<div className="space-y-2">
|
|
1115
|
+
{historyEntries.length === 0 ? (
|
|
1116
|
+
<p className="text-muted-foreground text-sm">
|
|
1117
|
+
No local snapshots yet.
|
|
1118
|
+
</p>
|
|
1119
|
+
) : (
|
|
1120
|
+
historyEntries.map((entry, index) => (
|
|
1121
|
+
<Button
|
|
1122
|
+
className="h-auto w-full justify-start px-3 py-2 text-left"
|
|
1123
|
+
key={entry.savedAt}
|
|
1124
|
+
onClick={() => setSelectedHistoryIndex(index)}
|
|
1125
|
+
variant={
|
|
1126
|
+
index === selectedHistoryIndex ? "secondary" : "ghost"
|
|
1127
|
+
}
|
|
1128
|
+
>
|
|
1129
|
+
<div>
|
|
1130
|
+
<div className="font-medium">
|
|
1131
|
+
{formatTime(new Date(entry.savedAt))}
|
|
1132
|
+
</div>
|
|
1133
|
+
<div className="line-clamp-2 text-muted-foreground text-xs">
|
|
1134
|
+
{entry.content.slice(0, 80) || "(empty)"}
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
</Button>
|
|
1138
|
+
))
|
|
1139
|
+
)}
|
|
1140
|
+
</div>
|
|
1141
|
+
<div className="rounded-lg border border-border/60 bg-background/70 p-4">
|
|
1142
|
+
{selectedHistoryEntry ? (
|
|
1143
|
+
<div className="space-y-3">
|
|
1144
|
+
<p className="text-muted-foreground text-sm">
|
|
1145
|
+
Snapshot from{" "}
|
|
1146
|
+
{new Date(selectedHistoryEntry.savedAt).toLocaleString(
|
|
1147
|
+
"en-US",
|
|
1148
|
+
{
|
|
1149
|
+
year: "numeric",
|
|
1150
|
+
month: "short",
|
|
1151
|
+
day: "numeric",
|
|
1152
|
+
hour: "2-digit",
|
|
1153
|
+
minute: "2-digit",
|
|
1154
|
+
}
|
|
1155
|
+
)}
|
|
1156
|
+
</p>
|
|
1157
|
+
<pre className="max-h-[320px] overflow-auto whitespace-pre-wrap rounded-md bg-muted/40 p-3 text-sm">
|
|
1158
|
+
{selectedHistoryEntry.content || "(empty)"}
|
|
1159
|
+
</pre>
|
|
1160
|
+
</div>
|
|
1161
|
+
) : (
|
|
1162
|
+
<p className="text-muted-foreground text-sm">
|
|
1163
|
+
Select a snapshot to preview it.
|
|
1164
|
+
</p>
|
|
1165
|
+
)}
|
|
1166
|
+
</div>
|
|
1167
|
+
</div>
|
|
1168
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
1169
|
+
<Button
|
|
1170
|
+
onClick={() => setHistoryDialogOpen(false)}
|
|
1171
|
+
variant="outline"
|
|
1172
|
+
>
|
|
1173
|
+
Close
|
|
1174
|
+
</Button>
|
|
1175
|
+
<Button
|
|
1176
|
+
disabled={!selectedHistoryEntry}
|
|
1177
|
+
onClick={restoreSelectedHistory}
|
|
1178
|
+
>
|
|
1179
|
+
Restore selected snapshot
|
|
1180
|
+
</Button>
|
|
1181
|
+
</DialogFooter>
|
|
1182
|
+
</DialogContent>
|
|
1183
|
+
</Dialog>
|
|
1184
|
+
|
|
1063
1185
|
<WikiLinkAutocomplete
|
|
1064
1186
|
activeIndex={wikiLinkActiveIndex}
|
|
1065
1187
|
docs={wikiLinkDocs}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
ArrowLeft,
|
|
3
3
|
ChevronDown,
|
|
4
4
|
FileText,
|
|
5
|
+
HomeIcon,
|
|
5
6
|
Search as SearchIcon,
|
|
6
7
|
SlidersHorizontal,
|
|
7
8
|
XIcon,
|
|
@@ -159,7 +160,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
159
160
|
const [searched, setSearched] = useState(false);
|
|
160
161
|
const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
|
|
161
162
|
const [collections, setCollections] = useState<Collection[]>([]);
|
|
162
|
-
const [activePreset, setActivePreset] = useState("slim");
|
|
163
|
+
const [activePreset, setActivePreset] = useState("slim-tuned");
|
|
163
164
|
|
|
164
165
|
const [showAdvanced, setShowAdvanced] = useState(
|
|
165
166
|
Boolean(
|
|
@@ -437,6 +438,7 @@ export default function Search({ navigate }: PageProps) {
|
|
|
437
438
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
438
439
|
}, [
|
|
439
440
|
activeTags,
|
|
441
|
+
activePreset,
|
|
440
442
|
author,
|
|
441
443
|
candidateLimit,
|
|
442
444
|
category,
|
|
@@ -517,6 +519,15 @@ export default function Search({ navigate }: PageProps) {
|
|
|
517
519
|
<header className="glass sticky top-0 z-10 border-border/50 border-b">
|
|
518
520
|
<div className="flex flex-wrap items-center justify-between gap-4 px-8 py-4">
|
|
519
521
|
<div className="flex items-center gap-4">
|
|
522
|
+
<Button
|
|
523
|
+
className="gap-2 text-primary"
|
|
524
|
+
onClick={() => navigate("/")}
|
|
525
|
+
size="sm"
|
|
526
|
+
variant="ghost"
|
|
527
|
+
>
|
|
528
|
+
<HomeIcon className="size-4" />
|
|
529
|
+
GNO
|
|
530
|
+
</Button>
|
|
520
531
|
<Button
|
|
521
532
|
className="gap-2"
|
|
522
533
|
onClick={() => navigate(-1)}
|