@cryptiklemur/lattice 0.0.0 → 1.2.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.
Files changed (53) hide show
  1. package/.github/workflows/release.yml +4 -4
  2. package/.releaserc.json +2 -1
  3. package/client/src/components/auth/PassphrasePrompt.tsx +70 -70
  4. package/client/src/components/mesh/NodeBadge.tsx +24 -24
  5. package/client/src/components/mesh/PairingDialog.tsx +281 -281
  6. package/client/src/components/panels/FileBrowser.tsx +241 -241
  7. package/client/src/components/panels/StickyNotes.tsx +187 -187
  8. package/client/src/components/project-settings/ProjectMemory.tsx +471 -0
  9. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  10. package/client/src/components/settings/Appearance.tsx +151 -151
  11. package/client/src/components/settings/MeshStatus.tsx +145 -145
  12. package/client/src/components/settings/SettingsView.tsx +57 -57
  13. package/client/src/components/setup/SetupWizard.tsx +750 -750
  14. package/client/src/components/sidebar/AddProjectModal.tsx +432 -0
  15. package/client/src/components/sidebar/ProjectRail.tsx +8 -4
  16. package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
  17. package/client/src/components/ui/ErrorBoundary.tsx +56 -56
  18. package/client/src/hooks/useSidebar.ts +16 -0
  19. package/client/src/router.tsx +453 -391
  20. package/client/src/stores/sidebar.ts +28 -0
  21. package/client/vite.config.ts +20 -20
  22. package/package.json +1 -1
  23. package/server/src/daemon.ts +1 -0
  24. package/server/src/handlers/chat.ts +194 -194
  25. package/server/src/handlers/fs.ts +159 -0
  26. package/server/src/handlers/memory.ts +179 -0
  27. package/server/src/handlers/settings.ts +114 -109
  28. package/shared/src/messages.ts +97 -2
  29. package/shared/src/project-settings.ts +1 -1
  30. package/themes/amoled.json +20 -20
  31. package/themes/ayu-light.json +9 -9
  32. package/themes/catppuccin-latte.json +9 -9
  33. package/themes/catppuccin-mocha.json +9 -9
  34. package/themes/clay-light.json +10 -10
  35. package/themes/clay.json +10 -10
  36. package/themes/dracula.json +9 -9
  37. package/themes/everforest-light.json +9 -9
  38. package/themes/everforest.json +9 -9
  39. package/themes/github-light.json +9 -9
  40. package/themes/gruvbox-dark.json +9 -9
  41. package/themes/gruvbox-light.json +9 -9
  42. package/themes/monokai.json +9 -9
  43. package/themes/nord-light.json +9 -9
  44. package/themes/nord.json +9 -9
  45. package/themes/one-dark.json +9 -9
  46. package/themes/one-light.json +9 -9
  47. package/themes/rose-pine-dawn.json +9 -9
  48. package/themes/rose-pine.json +9 -9
  49. package/themes/solarized-dark.json +9 -9
  50. package/themes/solarized-light.json +9 -9
  51. package/themes/tokyo-night-light.json +9 -9
  52. package/themes/tokyo-night.json +9 -9
  53. package/.serena/project.yml +0 -138
@@ -0,0 +1,432 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { X, FolderOpen, FileText, Loader2 } from "lucide-react";
3
+ import { useWebSocket } from "../../hooks/useWebSocket";
4
+ import { useProjects } from "../../hooks/useProjects";
5
+ import type { ServerMessage } from "@lattice/shared";
6
+
7
+ interface BrowseEntry {
8
+ name: string;
9
+ path: string;
10
+ hasClaudeMd: boolean;
11
+ projectName: string | null;
12
+ }
13
+
14
+ interface AddProjectModalProps {
15
+ isOpen: boolean;
16
+ onClose: () => void;
17
+ }
18
+
19
+ export function AddProjectModal({ isOpen, onClose }: AddProjectModalProps) {
20
+ var { send, subscribe, unsubscribe } = useWebSocket();
21
+ var { projects } = useProjects();
22
+
23
+ var [path, setPath] = useState("");
24
+ var [entries, setEntries] = useState<BrowseEntry[]>([]);
25
+ var [title, setTitle] = useState("");
26
+ var [titleManuallySet, setTitleManuallySet] = useState(false);
27
+ var [dropdownOpen, setDropdownOpen] = useState(false);
28
+ var [highlightIndex, setHighlightIndex] = useState(-1);
29
+ var [error, setError] = useState<string | null>(null);
30
+ var [adding, setAdding] = useState(false);
31
+ var [suggestions, setSuggestions] = useState<Array<{ path: string; name: string; hasClaudeMd: boolean }>>([]);
32
+ var [homedir, setHomedir] = useState("");
33
+ var debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
34
+ var inputRef = useRef<HTMLInputElement>(null);
35
+ var dropdownRef = useRef<HTMLDivElement>(null);
36
+ var inputFocusedRef = useRef(false);
37
+ var addingRef = useRef(false);
38
+
39
+ useEffect(function () {
40
+ if (!isOpen) return;
41
+
42
+ setPath("");
43
+ setEntries([]);
44
+ setTitle("");
45
+ setTitleManuallySet(false);
46
+ setDropdownOpen(false);
47
+ setError(null);
48
+ setAdding(false);
49
+ setSuggestions([]);
50
+ addingRef.current = false;
51
+
52
+ send({ type: "browse:list", path: "~" } as any);
53
+ send({ type: "browse:suggestions" } as any);
54
+
55
+ function handleBrowseResult(msg: ServerMessage) {
56
+ if (msg.type !== "browse:list_result") return;
57
+ var data = msg as { type: "browse:list_result"; path: string; homedir: string; entries: BrowseEntry[] };
58
+ setEntries(data.entries);
59
+ setHomedir(data.homedir);
60
+ setHighlightIndex(-1);
61
+ if (inputFocusedRef.current && data.entries.length > 0) {
62
+ setDropdownOpen(true);
63
+ }
64
+ }
65
+
66
+ function handleSuggestions(msg: ServerMessage) {
67
+ if (msg.type !== "browse:suggestions_result") return;
68
+ var data = msg as { type: "browse:suggestions_result"; suggestions: Array<{ path: string; name: string; hasClaudeMd: boolean }> };
69
+ setSuggestions(data.suggestions);
70
+ }
71
+
72
+ function handleProjectsList(msg: ServerMessage) {
73
+ if (msg.type !== "projects:list") return;
74
+ if (addingRef.current) {
75
+ addingRef.current = false;
76
+ setAdding(false);
77
+ onClose();
78
+ }
79
+ }
80
+
81
+ subscribe("browse:list_result", handleBrowseResult);
82
+ subscribe("browse:suggestions_result", handleSuggestions);
83
+ subscribe("projects:list", handleProjectsList);
84
+
85
+ return function () {
86
+ unsubscribe("browse:list_result", handleBrowseResult);
87
+ unsubscribe("browse:suggestions_result", handleSuggestions);
88
+ unsubscribe("projects:list", handleProjectsList);
89
+ };
90
+ }, [isOpen]);
91
+
92
+ useEffect(function () {
93
+ if (!isOpen) return;
94
+
95
+ function handleMouseDown(e: MouseEvent) {
96
+ if (
97
+ dropdownRef.current &&
98
+ !dropdownRef.current.contains(e.target as Node) &&
99
+ inputRef.current &&
100
+ !inputRef.current.contains(e.target as Node)
101
+ ) {
102
+ setDropdownOpen(false);
103
+ }
104
+ }
105
+
106
+ document.addEventListener("mousedown", handleMouseDown);
107
+ return function () {
108
+ document.removeEventListener("mousedown", handleMouseDown);
109
+ };
110
+ }, [isOpen]);
111
+
112
+ useEffect(function () {
113
+ if (!isOpen) return;
114
+ if (debounceRef.current) clearTimeout(debounceRef.current);
115
+
116
+ var trimmed = path.trim();
117
+ if (!trimmed) {
118
+ send({ type: "browse:list", path: "~" } as any);
119
+ return;
120
+ }
121
+
122
+ debounceRef.current = setTimeout(function () {
123
+ var parentDir = trimmed;
124
+ if (!trimmed.endsWith("/")) {
125
+ var lastSlash = trimmed.lastIndexOf("/");
126
+ parentDir = lastSlash >= 0 ? trimmed.slice(0, lastSlash + 1) : trimmed;
127
+ }
128
+ send({ type: "browse:list", path: parentDir } as any);
129
+ }, 200);
130
+ }, [path, isOpen]);
131
+
132
+ function resolvePath(p: string): string {
133
+ if (p.startsWith("~/") && homedir) return homedir + p.slice(1);
134
+ if (p === "~" && homedir) return homedir;
135
+ return p;
136
+ }
137
+
138
+ function handleSelectEntry(entry: BrowseEntry) {
139
+ var newPath = entry.path + "/";
140
+ setPath(newPath);
141
+ setDropdownOpen(false);
142
+ setError(null);
143
+
144
+ if (!titleManuallySet) {
145
+ if (entry.projectName) {
146
+ setTitle(entry.projectName);
147
+ } else {
148
+ setTitle(entry.name);
149
+ }
150
+ }
151
+
152
+ send({ type: "browse:list", path: entry.path } as any);
153
+
154
+ if (inputRef.current) {
155
+ inputRef.current.focus();
156
+ }
157
+ }
158
+
159
+ function handleTitleChange(value: string) {
160
+ setTitle(value);
161
+ setTitleManuallySet(value.length > 0);
162
+ }
163
+
164
+ function handleInputFocus() {
165
+ inputFocusedRef.current = true;
166
+ if (getFilteredEntries().length > 0) {
167
+ setDropdownOpen(true);
168
+ }
169
+ }
170
+
171
+ function handleInputBlur() {
172
+ inputFocusedRef.current = false;
173
+ }
174
+
175
+ function getFilteredEntries(): BrowseEntry[] {
176
+ var trimmed = path.trim();
177
+ if (!trimmed || trimmed.endsWith("/")) return entries;
178
+
179
+ var lastSlash = trimmed.lastIndexOf("/");
180
+ var filter = lastSlash >= 0 ? trimmed.slice(lastSlash + 1).toLowerCase() : trimmed.toLowerCase();
181
+ if (!filter) return entries;
182
+
183
+ return entries.filter(function (e) {
184
+ return e.name.toLowerCase().startsWith(filter);
185
+ });
186
+ }
187
+
188
+ function isValidPath(): boolean {
189
+ var trimmed = path.trim();
190
+ if (!trimmed) return false;
191
+ var resolved = resolvePath(trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed);
192
+ var alreadyAdded = projects.some(function (p) { return p.path === resolved; });
193
+ if (alreadyAdded) return false;
194
+ return true;
195
+ }
196
+
197
+ function getValidationMessage(): { type: "info" | "error" | "success"; text: string } | null {
198
+ var trimmed = path.trim();
199
+ if (!trimmed) return { type: "info", text: "Type a path to browse directories" };
200
+
201
+ var resolved = resolvePath(trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed);
202
+ var alreadyAdded = projects.some(function (p) { return p.path === resolved; });
203
+ if (alreadyAdded) return { type: "error", text: "Already added as a project" };
204
+
205
+ return null;
206
+ }
207
+
208
+ function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
209
+ var filtered = getFilteredEntries();
210
+ if (!dropdownOpen || filtered.length === 0) {
211
+ if (e.key === "Enter" && canAdd) {
212
+ e.preventDefault();
213
+ handleAdd();
214
+ }
215
+ return;
216
+ }
217
+
218
+ if (e.key === "ArrowDown") {
219
+ e.preventDefault();
220
+ setHighlightIndex(function (prev) {
221
+ var next = prev + 1;
222
+ if (next >= filtered.length) next = 0;
223
+ scrollHighlightIntoView(next);
224
+ return next;
225
+ });
226
+ } else if (e.key === "ArrowUp") {
227
+ e.preventDefault();
228
+ setHighlightIndex(function (prev) {
229
+ var next = prev - 1;
230
+ if (next < 0) next = filtered.length - 1;
231
+ scrollHighlightIntoView(next);
232
+ return next;
233
+ });
234
+ } else if (e.key === "Tab" || e.key === "Enter") {
235
+ if (highlightIndex >= 0 && highlightIndex < filtered.length) {
236
+ e.preventDefault();
237
+ handleSelectEntry(filtered[highlightIndex]);
238
+ setHighlightIndex(-1);
239
+ } else if (e.key === "Enter" && canAdd) {
240
+ e.preventDefault();
241
+ handleAdd();
242
+ }
243
+ } else if (e.key === "Escape") {
244
+ setDropdownOpen(false);
245
+ setHighlightIndex(-1);
246
+ }
247
+ }
248
+
249
+ function scrollHighlightIntoView(index: number) {
250
+ if (!dropdownRef.current) return;
251
+ var items = dropdownRef.current.children;
252
+ if (items[index]) {
253
+ (items[index] as HTMLElement).scrollIntoView({ block: "nearest" });
254
+ }
255
+ }
256
+
257
+ var canAdd = isValidPath() && !adding;
258
+
259
+ function handleAdd() {
260
+ var trimmed = path.trim();
261
+ if (!trimmed || adding) return;
262
+
263
+ var resolved = resolvePath(trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed);
264
+ var finalTitle = title.trim() || resolved.split("/").pop() || "Untitled";
265
+
266
+ setAdding(true);
267
+ addingRef.current = true;
268
+ send({
269
+ type: "settings:update",
270
+ settings: {
271
+ projects: [{ path: resolved, title: finalTitle }],
272
+ },
273
+ } as any);
274
+ }
275
+
276
+ if (!isOpen) return null;
277
+
278
+ var filtered = getFilteredEntries();
279
+ var validation = getValidationMessage();
280
+
281
+ return (
282
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center">
283
+ <div className="absolute inset-0 bg-black/50" onClick={onClose} />
284
+ <div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
285
+ <div className="flex items-center justify-between px-5 py-4 border-b border-base-content/15">
286
+ <h2 className="text-[15px] font-mono font-bold text-base-content">Add Project</h2>
287
+ <button
288
+ onClick={onClose}
289
+ aria-label="Close"
290
+ className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
291
+ >
292
+ <X size={16} />
293
+ </button>
294
+ </div>
295
+
296
+ <div className="px-5 py-4 space-y-4">
297
+ <div>
298
+ <label htmlFor="project-path" className="block text-[12px] font-semibold text-base-content/40 mb-1.5">
299
+ Project Path
300
+ </label>
301
+ <div className="relative">
302
+ <input
303
+ ref={inputRef}
304
+ id="project-path"
305
+ type="text"
306
+ value={path}
307
+ onChange={function (e) { setPath(e.target.value); setDropdownOpen(true); setHighlightIndex(-1); setError(null); }}
308
+ onFocus={handleInputFocus}
309
+ onBlur={handleInputBlur}
310
+ onKeyDown={handleKeyDown}
311
+ placeholder="~/projects/my-app"
312
+ autoFocus
313
+ className="w-full h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content font-mono text-[12px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
314
+ />
315
+
316
+ {dropdownOpen && filtered.length > 0 && (
317
+ <div
318
+ ref={dropdownRef}
319
+ className="absolute top-full left-0 right-0 mt-1 z-50 bg-base-200 border border-base-content/15 rounded-xl shadow-lg max-h-48 overflow-y-auto"
320
+ >
321
+ {filtered.map(function (entry, idx) {
322
+ var isHighlighted = idx === highlightIndex;
323
+ return (
324
+ <button
325
+ key={entry.path}
326
+ onMouseDown={function (e) { e.preventDefault(); }}
327
+ onClick={function () { handleSelectEntry(entry); setHighlightIndex(-1); }}
328
+ onMouseEnter={function () { setHighlightIndex(idx); }}
329
+ className={
330
+ "w-full flex items-center gap-2 px-3 py-2 text-left text-[12px] font-mono transition-colors duration-[80ms] " +
331
+ (isHighlighted
332
+ ? "bg-base-content/10 text-base-content"
333
+ : "text-base-content/60 hover:bg-base-content/5 hover:text-base-content")
334
+ }
335
+ >
336
+ <FolderOpen size={12} className="text-base-content/25 flex-shrink-0" />
337
+ <span className="flex-1 truncate">{entry.name}/</span>
338
+ {entry.projectName && (
339
+ <span className="text-[10px] px-1.5 py-0.5 rounded-md bg-accent/15 text-accent/70 flex-shrink-0 truncate max-w-[100px]">
340
+ {entry.projectName}
341
+ </span>
342
+ )}
343
+ {entry.hasClaudeMd && (
344
+ <span className="flex items-center gap-0.5 text-[10px] px-1.5 py-0.5 rounded-md bg-primary/15 text-primary/70 flex-shrink-0">
345
+ <FileText size={8} />
346
+ CLAUDE.md
347
+ </span>
348
+ )}
349
+ </button>
350
+ );
351
+ })}
352
+ </div>
353
+ )}
354
+ </div>
355
+ <div className="text-[10px] text-base-content/25 mt-1">Type a path or click directories to navigate</div>
356
+
357
+ {!path.trim() && suggestions.length > 0 && !dropdownOpen && (
358
+ <div className="mt-2">
359
+ <div className="text-[11px] font-semibold text-base-content/30 mb-1.5">Projects Claude has worked in</div>
360
+ <div className="flex flex-col gap-1 max-h-32 overflow-y-auto">
361
+ {suggestions.map(function (s) {
362
+ return (
363
+ <button
364
+ key={s.path}
365
+ onClick={function () {
366
+ setPath(s.path + "/");
367
+ if (!titleManuallySet) setTitle(s.name);
368
+ setSuggestions([]);
369
+ send({ type: "browse:list", path: s.path } as any);
370
+ }}
371
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-left text-[12px] font-mono text-base-content/50 hover:bg-base-content/5 hover:text-base-content rounded-lg transition-colors duration-[80ms]"
372
+ >
373
+ <FolderOpen size={12} className="text-base-content/25 flex-shrink-0" />
374
+ <span className="flex-1 truncate">{s.path}</span>
375
+ {s.hasClaudeMd && (
376
+ <span className="flex items-center gap-0.5 text-[10px] px-1.5 py-0.5 rounded-md bg-primary/15 text-primary/70 flex-shrink-0">
377
+ <FileText size={8} />
378
+ CLAUDE.md
379
+ </span>
380
+ )}
381
+ </button>
382
+ );
383
+ })}
384
+ </div>
385
+ </div>
386
+ )}
387
+ </div>
388
+
389
+ <div>
390
+ <label htmlFor="project-title" className="block text-[12px] font-semibold text-base-content/40 mb-1.5">
391
+ Title <span className="font-normal opacity-60">(optional)</span>
392
+ </label>
393
+ <input
394
+ id="project-title"
395
+ type="text"
396
+ value={title}
397
+ onChange={function (e) { handleTitleChange(e.target.value); }}
398
+ placeholder="Auto-derived from path"
399
+ className="w-full h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[13px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
400
+ />
401
+ </div>
402
+
403
+ {validation && (
404
+ <div className={
405
+ "flex items-center gap-2 px-3 py-2 rounded-xl border text-[11px] " +
406
+ (validation.type === "error"
407
+ ? "bg-error/10 border-error/20 text-error"
408
+ : validation.type === "success"
409
+ ? "bg-success/10 border-success/20 text-success"
410
+ : "bg-base-content/5 border-base-content/10 text-base-content/40")
411
+ }>
412
+ {validation.text}
413
+ </div>
414
+ )}
415
+ </div>
416
+
417
+ <div className="px-5 py-3 border-t border-base-content/15 flex justify-end gap-2">
418
+ <button onClick={onClose} className="btn btn-ghost btn-sm text-[12px]">
419
+ Cancel
420
+ </button>
421
+ <button
422
+ onClick={handleAdd}
423
+ disabled={!canAdd}
424
+ className={"btn btn-primary btn-sm text-[12px]" + (!canAdd ? " opacity-50 cursor-not-allowed" : "")}
425
+ >
426
+ {adding ? <Loader2 size={14} className="animate-spin" /> : "Add Project"}
427
+ </button>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ );
432
+ }
@@ -230,10 +230,9 @@ export function ProjectRail(props: ProjectRailProps) {
230
230
  )}
231
231
 
232
232
  <button
233
- onClick={function () {}}
234
- className="w-[42px] h-[42px] flex items-center justify-center rounded-full border-2 border-dashed border-base-content/25 text-base-content/20 transition-colors duration-[120ms] flex-shrink-0 cursor-not-allowed opacity-60"
235
- title="Add project (coming soon)"
236
- disabled
233
+ onClick={function () { sidebar.openAddProject(); }}
234
+ className="w-[42px] h-[42px] flex items-center justify-center rounded-full border-2 border-dashed border-base-content/25 text-base-content/20 hover:border-base-content/40 hover:text-base-content/40 transition-colors duration-[120ms] flex-shrink-0 cursor-pointer"
235
+ title="Add project"
237
236
  >
238
237
  <Plus size={18} />
239
238
  </button>
@@ -279,13 +278,18 @@ export function ProjectRail(props: ProjectRailProps) {
279
278
  role="menuitem"
280
279
  className="w-full text-left px-3 py-1.5 text-sm text-error hover:bg-error/10 transition-colors"
281
280
  onClick={function () {
281
+ var slug = contextMenu.slug;
282
282
  setContextMenu(function (prev) { return { ...prev, visible: false }; });
283
+ if (slug) {
284
+ sidebar.openConfirmRemove(slug);
285
+ }
283
286
  }}
284
287
  >
285
288
  Remove Project
286
289
  </button>
287
290
  </div>
288
291
  )}
292
+
289
293
  </div>
290
294
  );
291
295
  }
@@ -1,4 +1,4 @@
1
- import { ArrowLeft, Palette, FileText, Terminal, Plug, Puzzle, Network, Settings, ScrollText, Shield } from "lucide-react";
1
+ import { ArrowLeft, Palette, FileText, Terminal, Plug, Puzzle, Network, Settings, ScrollText, Shield, Brain } from "lucide-react";
2
2
  import { useSidebar } from "../../hooks/useSidebar";
3
3
  import type { SettingsSection, ProjectSettingsSection } from "../../stores/sidebar";
4
4
 
@@ -52,6 +52,7 @@ var PROJECT_SETTINGS_NAV = [
52
52
  items: [
53
53
  { id: "rules" as ProjectSettingsSection, label: "Rules", icon: <ScrollText size={14} /> },
54
54
  { id: "permissions" as ProjectSettingsSection, label: "Permissions", icon: <Shield size={14} /> },
55
+ { id: "memory" as ProjectSettingsSection, label: "Memory", icon: <Brain size={14} /> },
55
56
  ],
56
57
  },
57
58
  ];
@@ -1,56 +1,56 @@
1
- import { Component } from "react";
2
- import type { ReactNode, ErrorInfo } from "react";
3
-
4
- interface Props {
5
- children: ReactNode;
6
- fallback?: ReactNode;
7
- }
8
-
9
- interface State {
10
- hasError: boolean;
11
- error: Error | null;
12
- }
13
-
14
- export class ErrorBoundary extends Component<Props, State> {
15
- constructor(props: Props) {
16
- super(props);
17
- this.state = { hasError: false, error: null };
18
- }
19
-
20
- static getDerivedStateFromError(error: Error): State {
21
- return { hasError: true, error };
22
- }
23
-
24
- componentDidCatch(error: Error, info: ErrorInfo) {
25
- console.error("[lattice] Render error:", error, info.componentStack);
26
- }
27
-
28
- handleReload() {
29
- window.location.reload();
30
- }
31
-
32
- render() {
33
- if (this.state.hasError) {
34
- if (this.props.fallback) {
35
- return this.props.fallback;
36
- }
37
- return (
38
- <div className="min-h-screen bg-base-100 flex items-center justify-center">
39
- <div className="text-center max-w-[400px] p-8">
40
- <h2 className="text-[20px] font-semibold text-error mb-3">Something went wrong</h2>
41
- <p className="text-[14px] text-base-content/60 mb-5">
42
- {this.state.error?.message || "An unexpected error occurred."}
43
- </p>
44
- <button
45
- onClick={this.handleReload}
46
- className="btn btn-primary btn-sm"
47
- >
48
- Reload
49
- </button>
50
- </div>
51
- </div>
52
- );
53
- }
54
- return this.props.children;
55
- }
56
- }
1
+ import { Component } from "react";
2
+ import type { ReactNode, ErrorInfo } from "react";
3
+
4
+ interface Props {
5
+ children: ReactNode;
6
+ fallback?: ReactNode;
7
+ }
8
+
9
+ interface State {
10
+ hasError: boolean;
11
+ error: Error | null;
12
+ }
13
+
14
+ export class ErrorBoundary extends Component<Props, State> {
15
+ constructor(props: Props) {
16
+ super(props);
17
+ this.state = { hasError: false, error: null };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: Error): State {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ componentDidCatch(error: Error, info: ErrorInfo) {
25
+ console.error("[lattice] Render error:", error, info.componentStack);
26
+ }
27
+
28
+ handleReload() {
29
+ window.location.reload();
30
+ }
31
+
32
+ render() {
33
+ if (this.state.hasError) {
34
+ if (this.props.fallback) {
35
+ return this.props.fallback;
36
+ }
37
+ return (
38
+ <div className="min-h-screen bg-base-100 flex items-center justify-center">
39
+ <div className="text-center max-w-[400px] p-8">
40
+ <h2 className="text-[20px] font-semibold text-error mb-3">Something went wrong</h2>
41
+ <p className="text-[14px] text-base-content/60 mb-5">
42
+ {this.state.error?.message || "An unexpected error occurred."}
43
+ </p>
44
+ <button
45
+ onClick={this.handleReload}
46
+ className="btn btn-primary btn-sm"
47
+ >
48
+ Reload
49
+ </button>
50
+ </div>
51
+ </div>
52
+ );
53
+ }
54
+ return this.props.children;
55
+ }
56
+ }
@@ -18,6 +18,10 @@ import {
18
18
  openNodeSettings,
19
19
  closeNodeSettings,
20
20
  navigateToSession,
21
+ openAddProject,
22
+ closeAddProject,
23
+ openConfirmRemove,
24
+ closeConfirmRemove,
21
25
  } from "../stores/sidebar";
22
26
  import type { SidebarState, SettingsSection, ProjectSettingsSection } from "../stores/sidebar";
23
27
 
@@ -39,6 +43,12 @@ export function useSidebar(): SidebarState & {
39
43
  openNodeSettings: () => void;
40
44
  closeNodeSettings: () => void;
41
45
  navigateToSession: (projectSlug: string, sessionId: string) => void;
46
+ openAddProject: () => void;
47
+ closeAddProject: () => void;
48
+ addProjectOpen: boolean;
49
+ openConfirmRemove: (slug: string) => void;
50
+ closeConfirmRemove: () => void;
51
+ confirmRemoveSlug: string | null;
42
52
  } {
43
53
  var store = getSidebarStore();
44
54
  var state = useStore(store, function (s) { return s; });
@@ -70,5 +80,11 @@ export function useSidebar(): SidebarState & {
70
80
  openNodeSettings: openNodeSettings,
71
81
  closeNodeSettings: closeNodeSettings,
72
82
  nodeSettingsOpen: state.nodeSettingsOpen,
83
+ openAddProject: openAddProject,
84
+ closeAddProject: closeAddProject,
85
+ addProjectOpen: state.addProjectOpen,
86
+ openConfirmRemove: openConfirmRemove,
87
+ closeConfirmRemove: closeConfirmRemove,
88
+ confirmRemoveSlug: state.confirmRemoveSlug,
73
89
  };
74
90
  }