@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.
- package/.github/workflows/release.yml +4 -4
- package/.releaserc.json +2 -1
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -70
- package/client/src/components/mesh/NodeBadge.tsx +24 -24
- package/client/src/components/mesh/PairingDialog.tsx +281 -281
- package/client/src/components/panels/FileBrowser.tsx +241 -241
- package/client/src/components/panels/StickyNotes.tsx +187 -187
- package/client/src/components/project-settings/ProjectMemory.tsx +471 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/Appearance.tsx +151 -151
- package/client/src/components/settings/MeshStatus.tsx +145 -145
- package/client/src/components/settings/SettingsView.tsx +57 -57
- package/client/src/components/setup/SetupWizard.tsx +750 -750
- package/client/src/components/sidebar/AddProjectModal.tsx +432 -0
- package/client/src/components/sidebar/ProjectRail.tsx +8 -4
- package/client/src/components/sidebar/SettingsSidebar.tsx +2 -1
- package/client/src/components/ui/ErrorBoundary.tsx +56 -56
- package/client/src/hooks/useSidebar.ts +16 -0
- package/client/src/router.tsx +453 -391
- package/client/src/stores/sidebar.ts +28 -0
- package/client/vite.config.ts +20 -20
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/chat.ts +194 -194
- package/server/src/handlers/fs.ts +159 -0
- package/server/src/handlers/memory.ts +179 -0
- package/server/src/handlers/settings.ts +114 -109
- package/shared/src/messages.ts +97 -2
- package/shared/src/project-settings.ts +1 -1
- package/themes/amoled.json +20 -20
- package/themes/ayu-light.json +9 -9
- package/themes/catppuccin-latte.json +9 -9
- package/themes/catppuccin-mocha.json +9 -9
- package/themes/clay-light.json +10 -10
- package/themes/clay.json +10 -10
- package/themes/dracula.json +9 -9
- package/themes/everforest-light.json +9 -9
- package/themes/everforest.json +9 -9
- package/themes/github-light.json +9 -9
- package/themes/gruvbox-dark.json +9 -9
- package/themes/gruvbox-light.json +9 -9
- package/themes/monokai.json +9 -9
- package/themes/nord-light.json +9 -9
- package/themes/nord.json +9 -9
- package/themes/one-dark.json +9 -9
- package/themes/one-light.json +9 -9
- package/themes/rose-pine-dawn.json +9 -9
- package/themes/rose-pine.json +9 -9
- package/themes/solarized-dark.json +9 -9
- package/themes/solarized-light.json +9 -9
- package/themes/tokyo-night-light.json +9 -9
- package/themes/tokyo-night.json +9 -9
- 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-
|
|
235
|
-
title="Add project
|
|
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
|
}
|