@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.
Files changed (52) hide show
  1. package/README.md +10 -2
  2. package/package.json +1 -1
  3. package/src/app/constants.ts +4 -2
  4. package/src/cli/commands/mcp/install.ts +4 -4
  5. package/src/cli/commands/mcp/status.ts +7 -7
  6. package/src/cli/commands/skill/install.ts +5 -5
  7. package/src/cli/program.ts +2 -2
  8. package/src/collection/add.ts +10 -0
  9. package/src/collection/types.ts +1 -0
  10. package/src/config/types.ts +12 -2
  11. package/src/core/depth-policy.ts +1 -1
  12. package/src/core/file-ops.ts +203 -1
  13. package/src/llm/registry.ts +20 -4
  14. package/src/serve/AGENTS.md +16 -16
  15. package/src/serve/CLAUDE.md +16 -16
  16. package/src/serve/config-sync.ts +32 -1
  17. package/src/serve/connectors.ts +243 -0
  18. package/src/serve/context.ts +9 -0
  19. package/src/serve/doc-events.ts +31 -1
  20. package/src/serve/embed-scheduler.ts +12 -0
  21. package/src/serve/import-preview.ts +173 -0
  22. package/src/serve/public/app.tsx +101 -7
  23. package/src/serve/public/components/AIModelSelector.tsx +383 -145
  24. package/src/serve/public/components/AddCollectionDialog.tsx +123 -7
  25. package/src/serve/public/components/BootstrapStatus.tsx +133 -0
  26. package/src/serve/public/components/CaptureModal.tsx +5 -2
  27. package/src/serve/public/components/CollectionsEmptyState.tsx +63 -0
  28. package/src/serve/public/components/FirstRunWizard.tsx +622 -0
  29. package/src/serve/public/components/HealthCenter.tsx +128 -0
  30. package/src/serve/public/components/IndexingProgress.tsx +21 -2
  31. package/src/serve/public/components/QuickSwitcher.tsx +62 -36
  32. package/src/serve/public/components/TagInput.tsx +5 -1
  33. package/src/serve/public/components/WikiLinkAutocomplete.tsx +15 -6
  34. package/src/serve/public/components/WorkspaceTabs.tsx +60 -0
  35. package/src/serve/public/hooks/use-doc-events.ts +48 -4
  36. package/src/serve/public/lib/local-history.ts +40 -7
  37. package/src/serve/public/lib/navigation-state.ts +156 -0
  38. package/src/serve/public/lib/workspace-tabs.ts +235 -0
  39. package/src/serve/public/pages/Ask.tsx +11 -1
  40. package/src/serve/public/pages/Browse.tsx +73 -0
  41. package/src/serve/public/pages/Collections.tsx +29 -13
  42. package/src/serve/public/pages/Connectors.tsx +178 -0
  43. package/src/serve/public/pages/Dashboard.tsx +493 -67
  44. package/src/serve/public/pages/DocView.tsx +192 -34
  45. package/src/serve/public/pages/DocumentEditor.tsx +127 -5
  46. package/src/serve/public/pages/Search.tsx +12 -1
  47. package/src/serve/routes/api.ts +541 -62
  48. package/src/serve/server.ts +79 -2
  49. package/src/serve/status-model.ts +149 -0
  50. package/src/serve/status.ts +706 -0
  51. package/src/serve/watch-service.ts +73 -8
  52. 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
- { method: "POST" }
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 { error: err } = await apiFetch(
395
- `/api/docs/${encodeURIComponent(doc.docid)}/deactivate`,
396
- { method: "POST" }
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
- <Button className="gap-1.5" onClick={handleEdit} size="sm">
516
- <PencilIcon className="size-4" />
517
- Edit
518
- </Button>
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
- <Button asChild size="sm" variant="outline">
540
- <a
541
- href={`file://${doc.source.absPath}`}
542
- rel="noopener noreferrer"
543
- target="_blank"
614
+ <>
615
+ <Button
616
+ className="gap-1.5"
617
+ onClick={() => {
618
+ void handleReveal();
619
+ }}
620
+ size="sm"
621
+ variant="outline"
544
622
  >
545
- <SquareArrowOutUpRightIcon className="mr-1.5 size-4" />
546
- Open original
547
- </a>
548
- </Button>
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
- Remove from index?
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
- <span className="block">
950
- This will remove <strong>
951
- "{doc?.title || doc?.relPath}"
952
- </strong>{" "}
953
- from the GNO search index.
954
- </span>
955
- <span className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/10 p-3 text-amber-500">
956
- <AlertTriangleIcon className="mt-0.5 size-4 shrink-0" />
957
- <span className="text-sm">
958
- The file will NOT be deleted from disk. It may be re-indexed
959
- on next sync unless you add it to the collection's exclude
960
- pattern.
961
- </span>
962
- </span>
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
- Remove from index
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 [hasLocalSnapshot, setHasLocalSnapshot] = useState(false);
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
- setHasLocalSnapshot(true);
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
- { method: "POST" }
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
- setHasLocalSnapshot(true);
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
- setHasLocalSnapshot(Boolean(loadLatestLocalHistory(data.docid)));
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)}