@cryptiklemur/lattice 1.1.0 → 1.3.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.
@@ -0,0 +1,433 @@
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-3xl 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-48 overflow-y-auto">
361
+ {suggestions.map(function (s) {
362
+ return (
363
+ <button
364
+ key={s.path}
365
+ title={s.path}
366
+ onClick={function () {
367
+ setPath(s.path + "/");
368
+ if (!titleManuallySet) setTitle(s.name);
369
+ setSuggestions([]);
370
+ send({ type: "browse:list", path: s.path } as any);
371
+ }}
372
+ 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]"
373
+ >
374
+ <FolderOpen size={12} className="text-base-content/25 flex-shrink-0" />
375
+ <span className="flex-1 truncate">{s.path}</span>
376
+ {s.hasClaudeMd && (
377
+ <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">
378
+ <FileText size={8} />
379
+ CLAUDE.md
380
+ </span>
381
+ )}
382
+ </button>
383
+ );
384
+ })}
385
+ </div>
386
+ </div>
387
+ )}
388
+ </div>
389
+
390
+ <div>
391
+ <label htmlFor="project-title" className="block text-[12px] font-semibold text-base-content/40 mb-1.5">
392
+ Title <span className="font-normal opacity-60">(optional)</span>
393
+ </label>
394
+ <input
395
+ id="project-title"
396
+ type="text"
397
+ value={title}
398
+ onChange={function (e) { handleTitleChange(e.target.value); }}
399
+ placeholder="Auto-derived from path"
400
+ 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]"
401
+ />
402
+ </div>
403
+
404
+ {validation && (
405
+ <div className={
406
+ "flex items-center gap-2 px-3 py-2 rounded-xl border text-[11px] " +
407
+ (validation.type === "error"
408
+ ? "bg-error/10 border-error/20 text-error"
409
+ : validation.type === "success"
410
+ ? "bg-success/10 border-success/20 text-success"
411
+ : "bg-base-content/5 border-base-content/10 text-base-content/40")
412
+ }>
413
+ {validation.text}
414
+ </div>
415
+ )}
416
+ </div>
417
+
418
+ <div className="px-5 py-3 border-t border-base-content/15 flex justify-end gap-2">
419
+ <button onClick={onClose} className="btn btn-ghost btn-sm text-[12px]">
420
+ Cancel
421
+ </button>
422
+ <button
423
+ onClick={handleAdd}
424
+ disabled={!canAdd}
425
+ className={"btn btn-primary btn-sm text-[12px]" + (!canAdd ? " opacity-50 cursor-not-allowed" : "")}
426
+ >
427
+ {adding ? <Loader2 size={14} className="animate-spin" /> : "Add Project"}
428
+ </button>
429
+ </div>
430
+ </div>
431
+ </div>
432
+ );
433
+ }
@@ -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
  ];
@@ -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
  }
@@ -9,6 +9,7 @@ import { ProjectSettingsView } from "./components/project-settings/ProjectSettin
9
9
  import { DashboardView } from "./components/dashboard/DashboardView";
10
10
  import { ProjectDashboardView } from "./components/dashboard/ProjectDashboardView";
11
11
  import { NodeSettingsModal } from "./components/sidebar/NodeSettingsModal";
12
+ import { AddProjectModal } from "./components/sidebar/AddProjectModal";
12
13
  import { useSidebar } from "./hooks/useSidebar";
13
14
  import { useWebSocket } from "./hooks/useWebSocket";
14
15
  import { exitSettings, getSidebarStore, handlePopState, closeDrawer } from "./stores/sidebar";
@@ -251,6 +252,62 @@ function LoadingScreen() {
251
252
  );
252
253
  }
253
254
 
255
+ function RemoveProjectConfirm() {
256
+ var sidebar = useSidebar();
257
+ var ws = useWebSocket();
258
+ var slug = sidebar.confirmRemoveSlug;
259
+
260
+ if (!slug) return null;
261
+
262
+ var projects = (function () {
263
+ try {
264
+ var store = getSidebarStore();
265
+ return store.state;
266
+ } catch {
267
+ return null;
268
+ }
269
+ })();
270
+
271
+ return (
272
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center">
273
+ <div className="absolute inset-0 bg-black/50" onClick={sidebar.closeConfirmRemove} />
274
+ <div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden">
275
+ <div className="px-5 py-4 border-b border-base-content/15">
276
+ <h2 className="text-[15px] font-mono font-bold text-base-content">Remove Project</h2>
277
+ </div>
278
+ <div className="px-5 py-4">
279
+ <p className="text-[13px] text-base-content/60">
280
+ Remove <span className="font-semibold text-base-content">{slug}</span> from Lattice? This won't delete any files on disk.
281
+ </p>
282
+ </div>
283
+ <div className="px-5 py-3 border-t border-base-content/15 flex justify-end gap-2">
284
+ <button
285
+ onClick={sidebar.closeConfirmRemove}
286
+ className="btn btn-ghost btn-sm text-[12px]"
287
+ >
288
+ Cancel
289
+ </button>
290
+ <button
291
+ onClick={function () {
292
+ ws.send({
293
+ type: "settings:update",
294
+ settings: { removeProject: slug },
295
+ } as any);
296
+ if (sidebar.activeProjectSlug === slug) {
297
+ sidebar.goToDashboard();
298
+ }
299
+ sidebar.closeConfirmRemove();
300
+ }}
301
+ className="btn btn-error btn-sm text-[12px]"
302
+ >
303
+ Remove
304
+ </button>
305
+ </div>
306
+ </div>
307
+ </div>
308
+ );
309
+ }
310
+
254
311
  function RootLayout() {
255
312
  var [setupComplete, setSetupComplete] = useState(function () {
256
313
  return localStorage.getItem("lattice-setup-complete") === "1";
@@ -313,6 +370,11 @@ function RootLayout() {
313
370
  isOpen={sidebar.nodeSettingsOpen}
314
371
  onClose={sidebar.closeNodeSettings}
315
372
  />
373
+ <AddProjectModal
374
+ isOpen={sidebar.addProjectOpen}
375
+ onClose={sidebar.closeAddProject}
376
+ />
377
+ <RemoveProjectConfirm />
316
378
  </div>
317
379
  );
318
380
  }
@@ -26,6 +26,8 @@ export interface SidebarState {
26
26
  projectDropdownOpen: boolean;
27
27
  drawerOpen: boolean;
28
28
  nodeSettingsOpen: boolean;
29
+ addProjectOpen: boolean;
30
+ confirmRemoveSlug: string | null;
29
31
  }
30
32
 
31
33
  var SETTINGS_SECTIONS: SettingsSection[] = ["appearance", "claude", "environment", "mcp", "skills", "nodes"];
@@ -70,6 +72,8 @@ var sidebarStore = new Store<SidebarState>({
70
72
  projectDropdownOpen: false,
71
73
  drawerOpen: false,
72
74
  nodeSettingsOpen: false,
75
+ addProjectOpen: false,
76
+ confirmRemoveSlug: null,
73
77
  });
74
78
 
75
79
  function pushUrl(projectSlug: string | null, sessionId: string | null): void {
@@ -334,3 +338,27 @@ export function closeNodeSettings(): void {
334
338
  return { ...state, nodeSettingsOpen: false };
335
339
  });
336
340
  }
341
+
342
+ export function openAddProject(): void {
343
+ sidebarStore.setState(function (state) {
344
+ return { ...state, addProjectOpen: true };
345
+ });
346
+ }
347
+
348
+ export function closeAddProject(): void {
349
+ sidebarStore.setState(function (state) {
350
+ return { ...state, addProjectOpen: false };
351
+ });
352
+ }
353
+
354
+ export function openConfirmRemove(slug: string): void {
355
+ sidebarStore.setState(function (state) {
356
+ return { ...state, confirmRemoveSlug: slug };
357
+ });
358
+ }
359
+
360
+ export function closeConfirmRemove(): void {
361
+ sidebarStore.setState(function (state) {
362
+ return { ...state, confirmRemoveSlug: null };
363
+ });
364
+ }
@@ -1,6 +1,14 @@
1
1
  @import "tailwindcss";
2
2
  @plugin "daisyui";
3
3
  @plugin "@tailwindcss/typography";
4
+
5
+ @layer base {
6
+ button:not(:disabled),
7
+ a[href],
8
+ [role="button"]:not([aria-disabled="true"]) {
9
+ cursor: pointer;
10
+ }
11
+ }
4
12
  @plugin "daisyui/theme" {
5
13
  name: "lattice-dark";
6
14
  default: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
5
5
  "license": "MIT",
6
6
  "author": "Aaron Scherer <me@aaronscherer.me>",
@@ -24,6 +24,7 @@ import "./handlers/loop";
24
24
  import "./handlers/scheduler";
25
25
  import "./handlers/notes";
26
26
  import "./handlers/skills";
27
+ import "./handlers/memory";
27
28
  import { startScheduler } from "./features/scheduler";
28
29
  import { loadNotes } from "./features/sticky-notes";
29
30
  import { cleanupClientTerminals } from "./handlers/terminal";