@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
@@ -0,0 +1,156 @@
1
+ export interface RecentDoc {
2
+ uri: string;
3
+ href: string;
4
+ label: string;
5
+ }
6
+
7
+ export interface FavoriteDoc {
8
+ uri: string;
9
+ href: string;
10
+ label: string;
11
+ }
12
+
13
+ export interface FavoriteCollection {
14
+ name: string;
15
+ href: string;
16
+ label: string;
17
+ }
18
+
19
+ export interface NavigationStorageLike {
20
+ getItem(key: string): string | null;
21
+ setItem(key: string, value: string): void;
22
+ }
23
+
24
+ export const RECENT_DOCS_STORAGE_KEY = "gno.recent-docs";
25
+ export const FAVORITE_DOCS_STORAGE_KEY = "gno.favorite-docs";
26
+ export const FAVORITE_COLLECTIONS_STORAGE_KEY = "gno.favorite-collections";
27
+
28
+ function getStorage(
29
+ storage?: NavigationStorageLike
30
+ ): NavigationStorageLike | null {
31
+ if (storage) {
32
+ return storage;
33
+ }
34
+ if (typeof localStorage === "undefined") {
35
+ return null;
36
+ }
37
+ return localStorage;
38
+ }
39
+
40
+ function loadList<T>(
41
+ key: string,
42
+ isValid: (value: unknown) => value is T,
43
+ storage?: NavigationStorageLike
44
+ ): T[] {
45
+ try {
46
+ const resolved = getStorage(storage);
47
+ if (!resolved) {
48
+ return [];
49
+ }
50
+ const raw = resolved.getItem(key);
51
+ if (!raw) {
52
+ return [];
53
+ }
54
+ const parsed = JSON.parse(raw) as unknown;
55
+ if (!Array.isArray(parsed)) {
56
+ return [];
57
+ }
58
+ return parsed.filter(isValid);
59
+ } catch {
60
+ return [];
61
+ }
62
+ }
63
+
64
+ function saveList<T>(
65
+ key: string,
66
+ value: T[],
67
+ storage?: NavigationStorageLike
68
+ ): void {
69
+ const resolved = getStorage(storage);
70
+ if (!resolved) {
71
+ return;
72
+ }
73
+ resolved.setItem(key, JSON.stringify(value));
74
+ }
75
+
76
+ function isRecentDoc(value: unknown): value is RecentDoc {
77
+ if (!value || typeof value !== "object") {
78
+ return false;
79
+ }
80
+ const candidate = value as Record<string, unknown>;
81
+ return (
82
+ typeof candidate.uri === "string" &&
83
+ typeof candidate.href === "string" &&
84
+ typeof candidate.label === "string"
85
+ );
86
+ }
87
+
88
+ function isFavoriteCollection(value: unknown): value is FavoriteCollection {
89
+ if (!value || typeof value !== "object") {
90
+ return false;
91
+ }
92
+ const candidate = value as Record<string, unknown>;
93
+ return (
94
+ typeof candidate.name === "string" &&
95
+ typeof candidate.href === "string" &&
96
+ typeof candidate.label === "string"
97
+ );
98
+ }
99
+
100
+ export function loadRecentDocuments(
101
+ storage?: NavigationStorageLike
102
+ ): RecentDoc[] {
103
+ return loadList(RECENT_DOCS_STORAGE_KEY, isRecentDoc, storage);
104
+ }
105
+
106
+ export function saveRecentDocument(
107
+ doc: RecentDoc,
108
+ storage?: NavigationStorageLike
109
+ ): void {
110
+ const current = loadRecentDocuments(storage).filter(
111
+ (entry) => entry.href !== doc.href
112
+ );
113
+ saveList(RECENT_DOCS_STORAGE_KEY, [doc, ...current].slice(0, 8), storage);
114
+ }
115
+
116
+ export function loadFavoriteDocuments(
117
+ storage?: NavigationStorageLike
118
+ ): FavoriteDoc[] {
119
+ return loadList(FAVORITE_DOCS_STORAGE_KEY, isRecentDoc, storage);
120
+ }
121
+
122
+ export function toggleFavoriteDocument(
123
+ doc: FavoriteDoc,
124
+ storage?: NavigationStorageLike
125
+ ): FavoriteDoc[] {
126
+ const current = loadFavoriteDocuments(storage);
127
+ const exists = current.some((entry) => entry.href === doc.href);
128
+ const next = exists
129
+ ? current.filter((entry) => entry.href !== doc.href)
130
+ : [doc, ...current].slice(0, 12);
131
+ saveList(FAVORITE_DOCS_STORAGE_KEY, next, storage);
132
+ return next;
133
+ }
134
+
135
+ export function loadFavoriteCollections(
136
+ storage?: NavigationStorageLike
137
+ ): FavoriteCollection[] {
138
+ return loadList(
139
+ FAVORITE_COLLECTIONS_STORAGE_KEY,
140
+ isFavoriteCollection,
141
+ storage
142
+ );
143
+ }
144
+
145
+ export function toggleFavoriteCollection(
146
+ collection: FavoriteCollection,
147
+ storage?: NavigationStorageLike
148
+ ): FavoriteCollection[] {
149
+ const current = loadFavoriteCollections(storage);
150
+ const exists = current.some((entry) => entry.name === collection.name);
151
+ const next = exists
152
+ ? current.filter((entry) => entry.name !== collection.name)
153
+ : [collection, ...current].slice(0, 12);
154
+ saveList(FAVORITE_COLLECTIONS_STORAGE_KEY, next, storage);
155
+ return next;
156
+ }
@@ -0,0 +1,235 @@
1
+ export interface WorkspaceTab {
2
+ id: string;
3
+ label: string;
4
+ location: string;
5
+ }
6
+
7
+ export interface WorkspaceState {
8
+ tabs: WorkspaceTab[];
9
+ activeTabId: string;
10
+ }
11
+
12
+ export interface WorkspaceStorageLike {
13
+ getItem(key: string): string | null;
14
+ setItem(key: string, value: string): void;
15
+ }
16
+
17
+ export const WORKSPACE_TABS_STORAGE_KEY = "gno.workspace-tabs";
18
+
19
+ function getStorage(
20
+ storage?: WorkspaceStorageLike
21
+ ): WorkspaceStorageLike | null {
22
+ if (storage) {
23
+ return storage;
24
+ }
25
+ if (typeof localStorage === "undefined") {
26
+ return null;
27
+ }
28
+ return localStorage;
29
+ }
30
+
31
+ function createTabId(): string {
32
+ return `tab-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
33
+ }
34
+
35
+ function getLocationLabel(location: string): string {
36
+ const [path, search = ""] = location.split("?");
37
+ const params = new URLSearchParams(search);
38
+
39
+ switch (path) {
40
+ case "/":
41
+ return "Home";
42
+ case "/search":
43
+ return "Search";
44
+ case "/browse": {
45
+ const collection = params.get("collection");
46
+ return collection ? `Browse: ${collection}` : "Browse";
47
+ }
48
+ case "/ask":
49
+ return "Ask";
50
+ case "/collections":
51
+ return "Collections";
52
+ case "/connectors":
53
+ return "Connectors";
54
+ case "/graph":
55
+ return "Graph";
56
+ case "/doc":
57
+ case "/edit": {
58
+ const uri = params.get("uri") ?? "";
59
+ const fallback = path === "/edit" ? "Editor" : "Document";
60
+ if (!uri) {
61
+ return fallback;
62
+ }
63
+ const label = decodeURIComponent(uri.split("/").pop() ?? fallback);
64
+ return path === "/edit" ? `Edit: ${label}` : label;
65
+ }
66
+ default:
67
+ return "Workspace";
68
+ }
69
+ }
70
+
71
+ function isWorkspaceTab(value: unknown): value is WorkspaceTab {
72
+ if (!value || typeof value !== "object") {
73
+ return false;
74
+ }
75
+ const candidate = value as Record<string, unknown>;
76
+ return (
77
+ typeof candidate.id === "string" &&
78
+ typeof candidate.label === "string" &&
79
+ typeof candidate.location === "string"
80
+ );
81
+ }
82
+
83
+ export function loadWorkspaceState(
84
+ currentLocation: string,
85
+ storage?: WorkspaceStorageLike
86
+ ): WorkspaceState {
87
+ const resolved = getStorage(storage);
88
+ const fallbackTab: WorkspaceTab = {
89
+ id: createTabId(),
90
+ label: getLocationLabel(currentLocation),
91
+ location: currentLocation,
92
+ };
93
+
94
+ if (!resolved) {
95
+ return { tabs: [fallbackTab], activeTabId: fallbackTab.id };
96
+ }
97
+
98
+ try {
99
+ const raw = resolved.getItem(WORKSPACE_TABS_STORAGE_KEY);
100
+ if (!raw) {
101
+ return { tabs: [fallbackTab], activeTabId: fallbackTab.id };
102
+ }
103
+ const parsed = JSON.parse(raw) as unknown;
104
+ if (!parsed || typeof parsed !== "object") {
105
+ return { tabs: [fallbackTab], activeTabId: fallbackTab.id };
106
+ }
107
+ const candidate = parsed as Record<string, unknown>;
108
+ const tabs = Array.isArray(candidate.tabs)
109
+ ? candidate.tabs.filter(isWorkspaceTab)
110
+ : [];
111
+ const activeTabId =
112
+ typeof candidate.activeTabId === "string" ? candidate.activeTabId : "";
113
+ if (tabs.length === 0) {
114
+ return { tabs: [fallbackTab], activeTabId: fallbackTab.id };
115
+ }
116
+
117
+ const explicitLocation = currentLocation !== "/";
118
+ if (!explicitLocation) {
119
+ return {
120
+ tabs,
121
+ activeTabId: tabs.some((tab) => tab.id === activeTabId)
122
+ ? activeTabId
123
+ : tabs[0]!.id,
124
+ };
125
+ }
126
+
127
+ const existing = tabs.find((tab) => tab.location === currentLocation);
128
+ if (existing) {
129
+ return { tabs, activeTabId: existing.id };
130
+ }
131
+
132
+ const nextTabs = [
133
+ ...tabs,
134
+ {
135
+ id: createTabId(),
136
+ label: getLocationLabel(currentLocation),
137
+ location: currentLocation,
138
+ },
139
+ ];
140
+ return {
141
+ tabs: nextTabs,
142
+ activeTabId: nextTabs.at(-1)?.id ?? fallbackTab.id,
143
+ };
144
+ } catch {
145
+ return { tabs: [fallbackTab], activeTabId: fallbackTab.id };
146
+ }
147
+ }
148
+
149
+ export function saveWorkspaceState(
150
+ state: WorkspaceState,
151
+ storage?: WorkspaceStorageLike
152
+ ): void {
153
+ const resolved = getStorage(storage);
154
+ if (!resolved) {
155
+ return;
156
+ }
157
+ resolved.setItem(WORKSPACE_TABS_STORAGE_KEY, JSON.stringify(state));
158
+ }
159
+
160
+ export function updateActiveTabLocation(
161
+ state: WorkspaceState,
162
+ location: string
163
+ ): WorkspaceState {
164
+ const nextTabs = state.tabs.map((tab) =>
165
+ tab.id === state.activeTabId
166
+ ? { ...tab, location, label: getLocationLabel(location) }
167
+ : tab
168
+ );
169
+ return {
170
+ tabs: nextTabs,
171
+ activeTabId: state.activeTabId,
172
+ };
173
+ }
174
+
175
+ export function createWorkspaceTab(
176
+ state: WorkspaceState,
177
+ location: string
178
+ ): WorkspaceState {
179
+ const tab: WorkspaceTab = {
180
+ id: createTabId(),
181
+ label: getLocationLabel(location),
182
+ location,
183
+ };
184
+ return {
185
+ tabs: [...state.tabs, tab],
186
+ activeTabId: tab.id,
187
+ };
188
+ }
189
+
190
+ export function activateWorkspaceTab(
191
+ state: WorkspaceState,
192
+ tabId: string
193
+ ): WorkspaceState {
194
+ return {
195
+ tabs: state.tabs,
196
+ activeTabId: tabId,
197
+ };
198
+ }
199
+
200
+ export function closeWorkspaceTab(
201
+ state: WorkspaceState,
202
+ tabId: string
203
+ ): WorkspaceState {
204
+ const index = state.tabs.findIndex((tab) => tab.id === tabId);
205
+ if (index === -1) {
206
+ return state;
207
+ }
208
+
209
+ const nextTabs = state.tabs.filter((tab) => tab.id !== tabId);
210
+ if (nextTabs.length === 0) {
211
+ const fallback = {
212
+ id: createTabId(),
213
+ label: "Home",
214
+ location: "/",
215
+ };
216
+ return {
217
+ tabs: [fallback],
218
+ activeTabId: fallback.id,
219
+ };
220
+ }
221
+
222
+ if (state.activeTabId !== tabId) {
223
+ return {
224
+ tabs: nextTabs,
225
+ activeTabId: state.activeTabId,
226
+ };
227
+ }
228
+
229
+ const nextActive =
230
+ nextTabs[index - 1] ?? nextTabs[index] ?? nextTabs[nextTabs.length - 1]!;
231
+ return {
232
+ tabs: nextTabs,
233
+ activeTabId: nextActive.id,
234
+ };
235
+ }
@@ -4,6 +4,7 @@ import {
4
4
  ChevronDown,
5
5
  CornerDownLeft,
6
6
  FileText,
7
+ HomeIcon,
7
8
  SlidersHorizontal,
8
9
  Sparkles,
9
10
  XIcon,
@@ -184,7 +185,7 @@ export default function Ask({ navigate }: PageProps) {
184
185
  const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
185
186
  const [collections, setCollections] = useState<Collection[]>([]);
186
187
  const [thoroughness, setThoroughness] = useState<Thoroughness>("balanced");
187
- const [activePreset, setActivePreset] = useState("slim");
188
+ const [activePreset, setActivePreset] = useState("slim-tuned");
188
189
 
189
190
  const [showAdvanced, setShowAdvanced] = useState(false);
190
191
  const [selectedCollection, setSelectedCollection] = useState("");
@@ -448,6 +449,15 @@ export default function Ask({ navigate }: PageProps) {
448
449
  <div className="flex h-dvh min-h-0 flex-col overflow-hidden">
449
450
  <header className="glass sticky top-0 z-10 border-border/50 border-b">
450
451
  <div className="flex items-center gap-4 px-8 py-4">
452
+ <Button
453
+ className="gap-2 text-primary"
454
+ onClick={() => navigate("/")}
455
+ size="sm"
456
+ variant="ghost"
457
+ >
458
+ <HomeIcon className="size-4" />
459
+ GNO
460
+ </Button>
451
461
  <Button
452
462
  className="gap-2"
453
463
  onClick={() => navigate(-1)}
@@ -3,7 +3,9 @@ import {
3
3
  ChevronRight,
4
4
  FileText,
5
5
  FolderOpen,
6
+ HomeIcon,
6
7
  RefreshCw,
8
+ StarIcon,
7
9
  } from "lucide-react";
8
10
  import { Fragment, useEffect, useState } from "react";
9
11
 
@@ -28,6 +30,12 @@ import {
28
30
  } from "../components/ui/table";
29
31
  import { apiFetch } from "../hooks/use-api";
30
32
  import { useDocEvents } from "../hooks/use-doc-events";
33
+ import {
34
+ loadFavoriteCollections,
35
+ loadFavoriteDocuments,
36
+ toggleFavoriteCollection,
37
+ toggleFavoriteDocument,
38
+ } from "../lib/navigation-state";
31
39
 
32
40
  interface PageProps {
33
41
  navigate: (to: string | number) => void;
@@ -78,6 +86,8 @@ export default function Browse({ navigate }: PageProps) {
78
86
  const [syncTarget, setSyncTarget] = useState<SyncTarget>(null);
79
87
  const [syncError, setSyncError] = useState<string | null>(null);
80
88
  const [refreshToken, setRefreshToken] = useState(0);
89
+ const [favoriteDocHrefs, setFavoriteDocHrefs] = useState<string[]>([]);
90
+ const [favoriteCollections, setFavoriteCollections] = useState<string[]>([]);
81
91
  const latestDocEvent = useDocEvents();
82
92
  const limit = 25;
83
93
 
@@ -96,6 +106,10 @@ export default function Browse({ navigate }: PageProps) {
96
106
  setCollections(data);
97
107
  }
98
108
  });
109
+ setFavoriteDocHrefs(loadFavoriteDocuments().map((entry) => entry.href));
110
+ setFavoriteCollections(
111
+ loadFavoriteCollections().map((entry) => entry.name)
112
+ );
99
113
  }, []);
100
114
 
101
115
  useEffect(() => {
@@ -237,6 +251,15 @@ export default function Browse({ navigate }: PageProps) {
237
251
  <header className="glass sticky top-0 z-10 border-border/50 border-b">
238
252
  <div className="flex flex-wrap items-center justify-between gap-4 px-8 py-4">
239
253
  <div className="flex items-center gap-4">
254
+ <Button
255
+ className="gap-2 text-primary"
256
+ onClick={() => navigate("/")}
257
+ size="sm"
258
+ variant="ghost"
259
+ >
260
+ <HomeIcon className="size-4" />
261
+ GNO
262
+ </Button>
240
263
  <Button
241
264
  className="gap-2"
242
265
  onClick={() => navigate(-1)}
@@ -300,6 +323,31 @@ export default function Browse({ navigate }: PageProps) {
300
323
  <FolderOpen className="size-4" />
301
324
  Collections
302
325
  </Button>
326
+ {selected && (
327
+ <Button
328
+ className="gap-2"
329
+ onClick={() =>
330
+ setFavoriteCollections(
331
+ toggleFavoriteCollection({
332
+ name: selected,
333
+ href: `/browse?collection=${encodeURIComponent(selected)}`,
334
+ label: selected,
335
+ }).map((entry) => entry.name)
336
+ )
337
+ }
338
+ size="sm"
339
+ variant="outline"
340
+ >
341
+ <StarIcon
342
+ className={`size-4 ${
343
+ favoriteCollections.includes(selected)
344
+ ? "fill-current text-secondary"
345
+ : ""
346
+ }`}
347
+ />
348
+ {favoriteCollections.includes(selected) ? "Pinned" : "Pin"}
349
+ </Button>
350
+ )}
303
351
  <Button
304
352
  className="gap-2"
305
353
  disabled={Boolean(syncJobId)}
@@ -423,6 +471,31 @@ export default function Browse({ navigate }: PageProps) {
423
471
  {doc.relPath}
424
472
  </div>
425
473
  </div>
474
+ <Button
475
+ onClick={(event) => {
476
+ event.stopPropagation();
477
+ const next = toggleFavoriteDocument({
478
+ uri: doc.uri,
479
+ href: `/doc?uri=${encodeURIComponent(doc.uri)}`,
480
+ label: doc.title || doc.relPath,
481
+ });
482
+ setFavoriteDocHrefs(
483
+ next.map((entry) => entry.href)
484
+ );
485
+ }}
486
+ size="icon-sm"
487
+ variant="ghost"
488
+ >
489
+ <StarIcon
490
+ className={`size-4 ${
491
+ favoriteDocHrefs.includes(
492
+ `/doc?uri=${encodeURIComponent(doc.uri)}`
493
+ )
494
+ ? "fill-current text-secondary"
495
+ : "text-muted-foreground"
496
+ }`}
497
+ />
498
+ </Button>
426
499
  <ChevronRight className="ml-auto size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
427
500
  </div>
428
501
  </TableCell>
@@ -17,6 +17,7 @@ import {
17
17
  FolderIcon,
18
18
  FolderMinusIcon,
19
19
  FolderPlusIcon,
20
+ HomeIcon,
20
21
  LayersIcon,
21
22
  Loader2Icon,
22
23
  MoreVerticalIcon,
@@ -24,8 +25,11 @@ import {
24
25
  } from "lucide-react";
25
26
  import { useCallback, useEffect, useState } from "react";
26
27
 
28
+ import type { AppStatusResponse } from "../../status-model";
29
+
27
30
  import { AddCollectionDialog } from "../components/AddCollectionDialog";
28
31
  import { Loader } from "../components/ai-elements/loader";
32
+ import { CollectionsEmptyState } from "../components/CollectionsEmptyState";
29
33
  import { IndexingProgress } from "../components/IndexingProgress";
30
34
  import { Badge } from "../components/ui/badge";
31
35
  import { Button } from "../components/ui/button";
@@ -75,6 +79,7 @@ interface StatusResponse {
75
79
  totalDocuments: number;
76
80
  lastUpdated: string | null;
77
81
  healthy: boolean;
82
+ onboarding: AppStatusResponse["onboarding"];
78
83
  }
79
84
 
80
85
  interface SyncResponse {
@@ -249,6 +254,9 @@ function CollectionCard({
249
254
 
250
255
  export default function Collections({ navigate }: PageProps) {
251
256
  const [collections, setCollections] = useState<CollectionStats[]>([]);
257
+ const [onboarding, setOnboarding] = useState<
258
+ AppStatusResponse["onboarding"] | null
259
+ >(null);
252
260
  const [loading, setLoading] = useState(true);
253
261
  const [error, setError] = useState<string | null>(null);
254
262
  const [refreshing, setRefreshing] = useState(false);
@@ -260,6 +268,9 @@ export default function Collections({ navigate }: PageProps) {
260
268
  );
261
269
  const [removing, setRemoving] = useState(false);
262
270
  const [addDialogOpen, setAddDialogOpen] = useState(false);
271
+ const [initialCollectionPath, setInitialCollectionPath] = useState<
272
+ string | undefined
273
+ >(undefined);
263
274
 
264
275
  const loadCollections = useCallback(async () => {
265
276
  const { data, error: err } = await apiFetch<StatusResponse>("/api/status");
@@ -267,6 +278,7 @@ export default function Collections({ navigate }: PageProps) {
267
278
  setError(err);
268
279
  } else if (data) {
269
280
  setCollections(data.collections);
281
+ setOnboarding(data.onboarding);
270
282
  setError(null);
271
283
  }
272
284
  }, []);
@@ -340,6 +352,15 @@ export default function Collections({ navigate }: PageProps) {
340
352
  <div className="flex flex-wrap items-start justify-between gap-4 px-8 py-4">
341
353
  <div className="space-y-2">
342
354
  <div className="flex flex-wrap items-center gap-3">
355
+ <Button
356
+ className="gap-2 text-primary"
357
+ onClick={() => navigate("/")}
358
+ size="sm"
359
+ variant="ghost"
360
+ >
361
+ <HomeIcon className="size-4" />
362
+ GNO
363
+ </Button>
343
364
  <Button
344
365
  className="gap-2"
345
366
  onClick={() => navigate(-1)}
@@ -452,19 +473,13 @@ export default function Collections({ navigate }: PageProps) {
452
473
 
453
474
  {/* Empty state */}
454
475
  {!error && collections.length === 0 && (
455
- <div className="mx-auto max-w-md py-16 text-center">
456
- <div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
457
- <FolderIcon className="size-8 text-muted-foreground" />
458
- </div>
459
- <h2 className="mb-2 font-semibold text-xl">No collections yet</h2>
460
- <p className="mb-6 text-muted-foreground">
461
- Add your first collection to start indexing documents.
462
- </p>
463
- <Button onClick={() => setAddDialogOpen(true)}>
464
- <FolderPlusIcon className="mr-2 size-4" />
465
- Add Collection
466
- </Button>
467
- </div>
476
+ <CollectionsEmptyState
477
+ onAddCollection={(path) => {
478
+ setInitialCollectionPath(path);
479
+ setAddDialogOpen(true);
480
+ }}
481
+ suggestedCollections={onboarding?.suggestedCollections ?? []}
482
+ />
468
483
  )}
469
484
 
470
485
  {/* Collections grid */}
@@ -495,6 +510,7 @@ export default function Collections({ navigate }: PageProps) {
495
510
 
496
511
  {/* Add collection dialog */}
497
512
  <AddCollectionDialog
513
+ initialPath={initialCollectionPath}
498
514
  onOpenChange={setAddDialogOpen}
499
515
  onSuccess={() => void loadCollections()}
500
516
  open={addDialogOpen}