@cryptiklemur/lattice 0.0.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 (162) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release.yml +44 -0
  3. package/.impeccable.md +66 -0
  4. package/.releaserc.json +32 -0
  5. package/.serena/project.yml +138 -0
  6. package/CLAUDE.md +35 -0
  7. package/CONTRIBUTING.md +93 -0
  8. package/LICENSE +21 -0
  9. package/README.md +83 -0
  10. package/bun.lock +1459 -0
  11. package/bunfig.toml +2 -0
  12. package/client/index.html +32 -0
  13. package/client/package.json +37 -0
  14. package/client/public/icons/icon-192.svg +11 -0
  15. package/client/public/icons/icon-512.svg +11 -0
  16. package/client/public/manifest.json +24 -0
  17. package/client/public/sw.js +61 -0
  18. package/client/src/App.tsx +28 -0
  19. package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
  20. package/client/src/components/chat/ChatInput.tsx +241 -0
  21. package/client/src/components/chat/ChatView.tsx +727 -0
  22. package/client/src/components/chat/Message.tsx +362 -0
  23. package/client/src/components/chat/ModelSelector.tsx +87 -0
  24. package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
  25. package/client/src/components/chat/StatusBar.tsx +50 -0
  26. package/client/src/components/chat/ToolGroup.tsx +129 -0
  27. package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
  28. package/client/src/components/chat/toolSummary.ts +41 -0
  29. package/client/src/components/dashboard/DashboardView.tsx +219 -0
  30. package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
  31. package/client/src/components/mesh/NodeBadge.tsx +24 -0
  32. package/client/src/components/mesh/PairingDialog.tsx +281 -0
  33. package/client/src/components/panels/FileBrowser.tsx +241 -0
  34. package/client/src/components/panels/StickyNotes.tsx +187 -0
  35. package/client/src/components/panels/Terminal.tsx +128 -0
  36. package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
  37. package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
  38. package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
  39. package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
  40. package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
  41. package/client/src/components/project-settings/ProjectRules.tsx +277 -0
  42. package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
  43. package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
  44. package/client/src/components/settings/Appearance.tsx +151 -0
  45. package/client/src/components/settings/ClaudeSettings.tsx +151 -0
  46. package/client/src/components/settings/Environment.tsx +185 -0
  47. package/client/src/components/settings/GlobalMcp.tsx +207 -0
  48. package/client/src/components/settings/GlobalSkills.tsx +125 -0
  49. package/client/src/components/settings/MeshStatus.tsx +145 -0
  50. package/client/src/components/settings/SettingsView.tsx +57 -0
  51. package/client/src/components/settings/SkillMarketplace.tsx +175 -0
  52. package/client/src/components/settings/mcp-shared.tsx +194 -0
  53. package/client/src/components/settings/skill-shared.tsx +177 -0
  54. package/client/src/components/setup/SetupWizard.tsx +750 -0
  55. package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
  56. package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
  57. package/client/src/components/sidebar/ProjectRail.tsx +291 -0
  58. package/client/src/components/sidebar/SearchFilter.tsx +52 -0
  59. package/client/src/components/sidebar/SessionList.tsx +384 -0
  60. package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
  61. package/client/src/components/sidebar/Sidebar.tsx +209 -0
  62. package/client/src/components/sidebar/UserIsland.tsx +59 -0
  63. package/client/src/components/sidebar/UserMenu.tsx +101 -0
  64. package/client/src/components/ui/CommandPalette.tsx +321 -0
  65. package/client/src/components/ui/ErrorBoundary.tsx +56 -0
  66. package/client/src/components/ui/IconPicker.tsx +209 -0
  67. package/client/src/components/ui/LatticeLogomark.tsx +19 -0
  68. package/client/src/components/ui/PopupMenu.tsx +98 -0
  69. package/client/src/components/ui/SaveFooter.tsx +38 -0
  70. package/client/src/components/ui/Toast.tsx +112 -0
  71. package/client/src/hooks/useMesh.ts +89 -0
  72. package/client/src/hooks/useProjectSettings.ts +56 -0
  73. package/client/src/hooks/useProjects.ts +66 -0
  74. package/client/src/hooks/useSaveState.ts +59 -0
  75. package/client/src/hooks/useSession.ts +317 -0
  76. package/client/src/hooks/useSidebar.ts +74 -0
  77. package/client/src/hooks/useSkills.ts +30 -0
  78. package/client/src/hooks/useTheme.ts +114 -0
  79. package/client/src/hooks/useWebSocket.ts +26 -0
  80. package/client/src/main.tsx +10 -0
  81. package/client/src/providers/WebSocketProvider.tsx +146 -0
  82. package/client/src/router.tsx +391 -0
  83. package/client/src/stores/mesh.ts +78 -0
  84. package/client/src/stores/session.ts +322 -0
  85. package/client/src/stores/sidebar.ts +336 -0
  86. package/client/src/stores/theme.ts +44 -0
  87. package/client/src/styles/global.css +167 -0
  88. package/client/src/styles/theme-vars.css +18 -0
  89. package/client/src/themes/index.ts +79 -0
  90. package/client/src/utils/findDuplicateKeys.ts +12 -0
  91. package/client/tsconfig.json +14 -0
  92. package/client/vite.config.ts +20 -0
  93. package/package.json +46 -0
  94. package/server/package.json +22 -0
  95. package/server/src/auth/passphrase.ts +48 -0
  96. package/server/src/config.ts +55 -0
  97. package/server/src/daemon.ts +338 -0
  98. package/server/src/features/ralph-loop.ts +173 -0
  99. package/server/src/features/scheduler.ts +281 -0
  100. package/server/src/features/sticky-notes.ts +102 -0
  101. package/server/src/handlers/chat.ts +194 -0
  102. package/server/src/handlers/fs.ts +84 -0
  103. package/server/src/handlers/loop.ts +37 -0
  104. package/server/src/handlers/mesh.ts +125 -0
  105. package/server/src/handlers/notes.ts +45 -0
  106. package/server/src/handlers/project-settings.ts +174 -0
  107. package/server/src/handlers/scheduler.ts +47 -0
  108. package/server/src/handlers/session.ts +159 -0
  109. package/server/src/handlers/settings.ts +109 -0
  110. package/server/src/handlers/skills.ts +380 -0
  111. package/server/src/handlers/terminal.ts +70 -0
  112. package/server/src/identity.ts +26 -0
  113. package/server/src/index.ts +190 -0
  114. package/server/src/mesh/connector.ts +209 -0
  115. package/server/src/mesh/discovery.ts +123 -0
  116. package/server/src/mesh/pairing.ts +94 -0
  117. package/server/src/mesh/peers.ts +52 -0
  118. package/server/src/mesh/proxy.ts +103 -0
  119. package/server/src/mesh/session-sync.ts +107 -0
  120. package/server/src/project/context-breakdown.ts +289 -0
  121. package/server/src/project/file-browser.ts +106 -0
  122. package/server/src/project/project-files.ts +267 -0
  123. package/server/src/project/registry.ts +57 -0
  124. package/server/src/project/sdk-bridge.ts +566 -0
  125. package/server/src/project/session.ts +432 -0
  126. package/server/src/project/terminal.ts +69 -0
  127. package/server/src/tls.ts +51 -0
  128. package/server/src/ws/broadcast.ts +31 -0
  129. package/server/src/ws/router.ts +104 -0
  130. package/server/src/ws/server.ts +2 -0
  131. package/server/tsconfig.json +16 -0
  132. package/shared/package.json +11 -0
  133. package/shared/src/constants.ts +7 -0
  134. package/shared/src/index.ts +4 -0
  135. package/shared/src/messages.ts +638 -0
  136. package/shared/src/models.ts +136 -0
  137. package/shared/src/project-settings.ts +45 -0
  138. package/shared/tsconfig.json +11 -0
  139. package/themes/amoled.json +20 -0
  140. package/themes/ayu-light.json +9 -0
  141. package/themes/catppuccin-latte.json +9 -0
  142. package/themes/catppuccin-mocha.json +9 -0
  143. package/themes/clay-light.json +10 -0
  144. package/themes/clay.json +10 -0
  145. package/themes/dracula.json +9 -0
  146. package/themes/everforest-light.json +9 -0
  147. package/themes/everforest.json +9 -0
  148. package/themes/github-light.json +9 -0
  149. package/themes/gruvbox-dark.json +9 -0
  150. package/themes/gruvbox-light.json +9 -0
  151. package/themes/monokai.json +9 -0
  152. package/themes/nord-light.json +9 -0
  153. package/themes/nord.json +9 -0
  154. package/themes/one-dark.json +9 -0
  155. package/themes/one-light.json +9 -0
  156. package/themes/rose-pine-dawn.json +9 -0
  157. package/themes/rose-pine.json +9 -0
  158. package/themes/solarized-dark.json +9 -0
  159. package/themes/solarized-light.json +9 -0
  160. package/themes/tokyo-night-light.json +9 -0
  161. package/themes/tokyo-night.json +9 -0
  162. package/tsconfig.json +26 -0
@@ -0,0 +1,384 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { SessionSummary, SessionListMessage, SessionCreatedMessage } from "@lattice/shared";
3
+ import type { ServerMessage } from "@lattice/shared";
4
+ import { useWebSocket } from "../../hooks/useWebSocket";
5
+ import { markSessionHasUpdates, sessionHasUpdates, markSessionRead } from "../../stores/session";
6
+
7
+ interface SessionGroup {
8
+ label: string;
9
+ sessions: SessionSummary[];
10
+ }
11
+
12
+ function groupByTime(sessions: SessionSummary[]): SessionGroup[] {
13
+ var todayStart = new Date();
14
+ todayStart.setHours(0, 0, 0, 0);
15
+ var todayMs = todayStart.getTime();
16
+ var yesterdayStart = new Date(todayStart);
17
+ yesterdayStart.setDate(yesterdayStart.getDate() - 1);
18
+ var yesterdayMs = yesterdayStart.getTime();
19
+ var weekMs = todayMs - 6 * 86400000;
20
+ var monthMs = todayMs - 29 * 86400000;
21
+
22
+ var groups: Record<string, SessionSummary[]> = {
23
+ Today: [],
24
+ Yesterday: [],
25
+ "This Week": [],
26
+ "This Month": [],
27
+ Older: [],
28
+ };
29
+
30
+ for (var i = 0; i < sessions.length; i++) {
31
+ var ts = sessions[i].updatedAt;
32
+ if (ts >= todayMs) {
33
+ groups["Today"].push(sessions[i]);
34
+ } else if (ts >= yesterdayMs && ts < todayMs) {
35
+ groups["Yesterday"].push(sessions[i]);
36
+ } else if (ts >= weekMs) {
37
+ groups["This Week"].push(sessions[i]);
38
+ } else if (ts >= monthMs) {
39
+ groups["This Month"].push(sessions[i]);
40
+ } else {
41
+ groups["Older"].push(sessions[i]);
42
+ }
43
+ }
44
+
45
+ var order = ["Today", "Yesterday", "This Week", "This Month", "Older"];
46
+ var result: SessionGroup[] = [];
47
+ for (var j = 0; j < order.length; j++) {
48
+ if (groups[order[j]].length > 0) {
49
+ result.push({ label: order[j], sessions: groups[order[j]] });
50
+ }
51
+ }
52
+ return result;
53
+ }
54
+
55
+ var knownUpdatedAt = new Map<string, number>();
56
+
57
+ interface ContextMenu {
58
+ x: number;
59
+ y: number;
60
+ session: SessionSummary;
61
+ }
62
+
63
+ interface SessionListProps {
64
+ projectSlug: string | null;
65
+ activeSessionId: string | null;
66
+ onSessionActivate: (session: SessionSummary) => void;
67
+ onSessionDeactivate?: () => void;
68
+ filter?: string;
69
+ }
70
+
71
+ function formatDate(ts: number): string {
72
+ var d = new Date(ts);
73
+ var now = new Date();
74
+ var diff = now.getTime() - d.getTime();
75
+ var day = 86400000;
76
+
77
+ if (diff < day && d.getDate() === now.getDate()) {
78
+ return d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
79
+ }
80
+ if (diff < 7 * day) {
81
+ return d.toLocaleDateString(undefined, { weekday: "short" });
82
+ }
83
+ return d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
84
+ }
85
+
86
+ function SessionSkeleton() {
87
+ return (
88
+ <div className="flex flex-col gap-2 px-4 py-3">
89
+ <div className="h-2 bg-base-content/10 rounded animate-pulse w-3/4" />
90
+ <div className="h-2 bg-base-content/10 rounded animate-pulse w-1/2" />
91
+ <div className="h-2 bg-base-content/10 rounded animate-pulse w-2/3" />
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export function SessionList(props: SessionListProps) {
97
+ var ws = useWebSocket();
98
+ var [sessions, setSessions] = useState<SessionSummary[]>([]);
99
+ var [loading, setLoading] = useState<boolean>(false);
100
+ var [renameId, setRenameId] = useState<string | null>(null);
101
+ var [renameValue, setRenameValue] = useState<string>("");
102
+ var [contextMenu, setContextMenu] = useState<ContextMenu | null>(null);
103
+ var [unreadTick, setUnreadTick] = useState<number>(0);
104
+ var renameInputRef = useRef<HTMLInputElement | null>(null);
105
+ var handleRef = useRef<(msg: ServerMessage) => void>(function () {});
106
+ var activeSessionIdRef = useRef<string | null>(props.activeSessionId);
107
+ activeSessionIdRef.current = props.activeSessionId;
108
+
109
+ useEffect(function () {
110
+ handleRef.current = function (msg: ServerMessage) {
111
+ if (msg.type === "session:list") {
112
+ var listMsg = msg as SessionListMessage;
113
+ if (listMsg.projectSlug === props.projectSlug) {
114
+ var sorted = listMsg.sessions.slice().sort(function (a, b) { return b.updatedAt - a.updatedAt; });
115
+ var hadChanges = false;
116
+ for (var i = 0; i < sorted.length; i++) {
117
+ var s = sorted[i];
118
+ var prev = knownUpdatedAt.get(s.id);
119
+ if (prev !== undefined && s.updatedAt > prev && s.id !== activeSessionIdRef.current) {
120
+ markSessionHasUpdates(s.id);
121
+ hadChanges = true;
122
+ }
123
+ knownUpdatedAt.set(s.id, s.updatedAt);
124
+ }
125
+ setSessions(sorted);
126
+ setLoading(false);
127
+ if (hadChanges) {
128
+ setUnreadTick(function (t) { return t + 1; });
129
+ }
130
+ }
131
+ } else if (msg.type === "session:created") {
132
+ var createdMsg = msg as SessionCreatedMessage;
133
+ if (createdMsg.session.projectSlug === props.projectSlug) {
134
+ knownUpdatedAt.set(createdMsg.session.id, createdMsg.session.updatedAt);
135
+ setSessions(function (prev2) {
136
+ return [createdMsg.session, ...prev2];
137
+ });
138
+ props.onSessionActivate(createdMsg.session);
139
+ }
140
+ }
141
+ };
142
+ });
143
+
144
+ useEffect(function () {
145
+ function handler(msg: ServerMessage) {
146
+ handleRef.current(msg);
147
+ }
148
+ ws.subscribe("session:list", handler);
149
+ ws.subscribe("session:created", handler);
150
+ return function () {
151
+ ws.unsubscribe("session:list", handler);
152
+ ws.unsubscribe("session:created", handler);
153
+ };
154
+ }, [ws]);
155
+
156
+ var sendRef = useRef(ws.send);
157
+ sendRef.current = ws.send;
158
+
159
+ useEffect(function () {
160
+ if (props.projectSlug && ws.status === "connected") {
161
+ setSessions([]);
162
+ setLoading(true);
163
+ sendRef.current({ type: "session:list_request", projectSlug: props.projectSlug });
164
+ var interval = setInterval(function () {
165
+ if (props.projectSlug) {
166
+ sendRef.current({ type: "session:list_request", projectSlug: props.projectSlug });
167
+ }
168
+ }, 10000);
169
+ return function () { clearInterval(interval); };
170
+ }
171
+ }, [props.projectSlug, ws.status]);
172
+
173
+ useEffect(function () {
174
+ if (renameId && renameInputRef.current) {
175
+ renameInputRef.current.focus();
176
+ renameInputRef.current.select();
177
+ }
178
+ }, [renameId]);
179
+
180
+ useEffect(function () {
181
+ if (!contextMenu) {
182
+ return;
183
+ }
184
+ function dismiss() {
185
+ setContextMenu(null);
186
+ }
187
+ document.addEventListener("mousedown", dismiss);
188
+ return function () {
189
+ document.removeEventListener("mousedown", dismiss);
190
+ };
191
+ }, [contextMenu]);
192
+
193
+ function handleActivate(session: SessionSummary) {
194
+ if (!props.projectSlug) {
195
+ return;
196
+ }
197
+ if (sessionHasUpdates(session.id)) {
198
+ markSessionRead(session.id, 0);
199
+ setUnreadTick(function (t) { return t + 1; });
200
+ }
201
+ props.onSessionActivate(session);
202
+ }
203
+
204
+ function handleContextMenu(e: React.MouseEvent, session: SessionSummary) {
205
+ e.preventDefault();
206
+ setContextMenu({ x: e.clientX, y: e.clientY, session });
207
+ }
208
+
209
+ function handleRenameStart(session: SessionSummary) {
210
+ setContextMenu(null);
211
+ setRenameId(session.id);
212
+ setRenameValue(session.title);
213
+ }
214
+
215
+ function handleRenameCommit() {
216
+ if (renameId && renameValue.trim()) {
217
+ var originalSession = sessions.find(function (s) { return s.id === renameId; });
218
+ if (originalSession && renameValue.trim() === originalSession.title) {
219
+ setRenameId(null);
220
+ setRenameValue("");
221
+ return;
222
+ }
223
+ ws.send({ type: "session:rename", sessionId: renameId, title: renameValue.trim() });
224
+ setSessions(function (prev) {
225
+ return prev.map(function (s) {
226
+ return s.id === renameId ? { ...s, title: renameValue.trim() } : s;
227
+ });
228
+ });
229
+ }
230
+ setRenameId(null);
231
+ setRenameValue("");
232
+ }
233
+
234
+ function handleRenameKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
235
+ if (e.key === "Enter") {
236
+ handleRenameCommit();
237
+ } else if (e.key === "Escape") {
238
+ setRenameId(null);
239
+ setRenameValue("");
240
+ }
241
+ }
242
+
243
+ function handleDeleteSession(session: SessionSummary) {
244
+ setContextMenu(null);
245
+ ws.send({ type: "session:delete", sessionId: session.id });
246
+ setSessions(function (prev) {
247
+ return prev.filter(function (s) { return s.id !== session.id; });
248
+ });
249
+ if (props.activeSessionId === session.id && props.onSessionDeactivate) {
250
+ props.onSessionDeactivate();
251
+ }
252
+ }
253
+
254
+ if (!props.projectSlug) {
255
+ return (
256
+ <div className="flex-1 flex items-start px-3 py-1.5">
257
+ <span className="text-[13px] text-base-content/40 italic">Select a project</span>
258
+ </div>
259
+ );
260
+ }
261
+
262
+ if (loading && sessions.length === 0) {
263
+ return (
264
+ <div className="flex flex-col flex-1 overflow-hidden min-h-0">
265
+ <div className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-hidden py-0.5 pb-16">
266
+ <SessionSkeleton />
267
+ </div>
268
+ </div>
269
+ );
270
+ }
271
+
272
+ var displayed = props.filter
273
+ ? sessions.filter(function (s) {
274
+ return s.title.toLowerCase().includes(props.filter!.toLowerCase());
275
+ })
276
+ : sessions;
277
+
278
+ var grouped = groupByTime(displayed);
279
+
280
+ return (
281
+ <div className="flex flex-col flex-1 overflow-hidden min-h-0">
282
+ <div className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-hidden py-0.5 pb-16">
283
+ {grouped.length === 0 ? (
284
+ <div className="px-3 py-2 text-sm text-base-content/40 italic">
285
+ {props.filter ? "No matches" : "No sessions yet"}
286
+ </div>
287
+ ) : (
288
+ grouped.map(function (group) {
289
+ return (
290
+ <div key={group.label}>
291
+ <div className="text-[10px] uppercase tracking-widest text-base-content/30 px-3 pt-3 pb-1 select-none">
292
+ {group.label}
293
+ </div>
294
+ <div className="flex flex-col gap-0.5 overflow-hidden">
295
+ {group.sessions.map(function (session) {
296
+ var isActive = props.activeSessionId === session.id;
297
+ var isRenaming = renameId === session.id;
298
+ var isUnread = !isActive && sessionHasUpdates(session.id);
299
+ return (
300
+ <button
301
+ key={session.id}
302
+ type="button"
303
+ aria-label={"Session: " + session.title}
304
+ aria-current={isActive ? "true" : undefined}
305
+ onClick={function () { handleActivate(session); }}
306
+ onContextMenu={function (e) { handleContextMenu(e, session); }}
307
+ className={
308
+ "flex flex-row items-start gap-2 px-2.5 py-[5px] mx-1 rounded w-[calc(100%-8px)] min-w-0 overflow-hidden cursor-pointer select-none transition-colors duration-[120ms] text-left focus-visible:ring-2 focus-visible:ring-primary/30 focus-visible:outline-none " +
309
+ (isActive ? "bg-primary/20 text-base-content font-medium" : "hover:bg-base-300/50")
310
+ }
311
+ >
312
+ {isUnread && (
313
+ <span className="shrink-0 mt-[7px] w-2 h-2 rounded-full bg-primary" />
314
+ )}
315
+ <div className="flex flex-col min-w-0 flex-1">
316
+ {isRenaming ? (
317
+ <input
318
+ ref={renameInputRef}
319
+ value={renameValue}
320
+ onChange={function (e) { setRenameValue(e.target.value); }}
321
+ onBlur={handleRenameCommit}
322
+ onKeyDown={handleRenameKeyDown}
323
+ onClick={function (e) { e.stopPropagation(); }}
324
+ className="input input-xs input-bordered w-full text-[13px]"
325
+ />
326
+ ) : (
327
+ <span
328
+ className={
329
+ "text-[13px] truncate leading-snug " +
330
+ (isActive ? "" : isUnread ? "text-base-content font-semibold" : "text-base-content/55")
331
+ }
332
+ >
333
+ {session.title}
334
+ </span>
335
+ )}
336
+ <div className="flex items-center gap-1.5 mt-0.5">
337
+ <span className="text-[11px] text-base-content/40">
338
+ {formatDate(session.updatedAt)}
339
+ </span>
340
+ {session.messageCount != null && (
341
+ <>
342
+ <span className="text-[11px] text-base-content/40">&middot;</span>
343
+ <span className="text-[11px] text-base-content/40">
344
+ {session.messageCount} msg{session.messageCount !== 1 ? "s" : ""}
345
+ </span>
346
+ </>
347
+ )}
348
+ </div>
349
+ </div>
350
+ </button>
351
+ );
352
+ })}
353
+ </div>
354
+ </div>
355
+ );
356
+ })
357
+ )}
358
+ </div>
359
+
360
+ {contextMenu !== null && (
361
+ <div
362
+ role="menu"
363
+ aria-label="Session actions"
364
+ onMouseDown={function (e) { e.stopPropagation(); }}
365
+ className="fixed z-[9999] bg-base-200 border border-base-content/20 rounded-md shadow-xl p-1 min-w-[140px]"
366
+ style={{ top: contextMenu.y, left: contextMenu.x }}
367
+ >
368
+ <button
369
+ onClick={function () { handleRenameStart(contextMenu!.session); }}
370
+ className="block w-full px-2.5 py-1.5 rounded text-[13px] text-base-content/70 text-left hover:bg-base-300 hover:text-base-content transition-colors duration-[120ms] cursor-pointer"
371
+ >
372
+ Rename
373
+ </button>
374
+ <button
375
+ onClick={function () { handleDeleteSession(contextMenu!.session); }}
376
+ className="block w-full px-2.5 py-1.5 rounded text-[13px] text-error text-left hover:bg-base-300 transition-colors duration-[120ms] cursor-pointer"
377
+ >
378
+ Delete
379
+ </button>
380
+ </div>
381
+ )}
382
+ </div>
383
+ );
384
+ }
@@ -0,0 +1,128 @@
1
+ import { ArrowLeft, Palette, FileText, Terminal, Plug, Puzzle, Network, Settings, ScrollText, Shield } from "lucide-react";
2
+ import { useSidebar } from "../../hooks/useSidebar";
3
+ import type { SettingsSection, ProjectSettingsSection } from "../../stores/sidebar";
4
+
5
+ interface SettingsSidebarProps {
6
+ projectName: string;
7
+ onBack: () => void;
8
+ }
9
+
10
+ var SETTINGS_NAV = [
11
+ {
12
+ group: "GENERAL",
13
+ items: [
14
+ { id: "appearance" as SettingsSection, label: "Appearance", icon: <Palette size={14} /> },
15
+ { id: "claude" as SettingsSection, label: "Claude Settings", icon: <FileText size={14} /> },
16
+ { id: "environment" as SettingsSection, label: "Environment", icon: <Terminal size={14} /> },
17
+ ],
18
+ },
19
+ {
20
+ group: "INTEGRATIONS",
21
+ items: [
22
+ { id: "mcp" as SettingsSection, label: "MCP Servers", icon: <Plug size={14} /> },
23
+ { id: "skills" as SettingsSection, label: "Skills", icon: <Puzzle size={14} /> },
24
+ ],
25
+ },
26
+ {
27
+ group: "MESH",
28
+ items: [
29
+ { id: "nodes" as SettingsSection, label: "Nodes", icon: <Network size={14} /> },
30
+ ],
31
+ },
32
+ ];
33
+
34
+ var PROJECT_SETTINGS_NAV = [
35
+ {
36
+ group: "GENERAL",
37
+ items: [
38
+ { id: "general" as ProjectSettingsSection, label: "General", icon: <Settings size={14} /> },
39
+ { id: "claude" as ProjectSettingsSection, label: "Claude", icon: <FileText size={14} /> },
40
+ { id: "environment" as ProjectSettingsSection, label: "Environment", icon: <Terminal size={14} /> },
41
+ ],
42
+ },
43
+ {
44
+ group: "INTEGRATIONS",
45
+ items: [
46
+ { id: "mcp" as ProjectSettingsSection, label: "MCP Servers", icon: <Plug size={14} /> },
47
+ { id: "skills" as ProjectSettingsSection, label: "Skills", icon: <Puzzle size={14} /> },
48
+ ],
49
+ },
50
+ {
51
+ group: "CONFIGURATION",
52
+ items: [
53
+ { id: "rules" as ProjectSettingsSection, label: "Rules", icon: <ScrollText size={14} /> },
54
+ { id: "permissions" as ProjectSettingsSection, label: "Permissions", icon: <Shield size={14} /> },
55
+ ],
56
+ },
57
+ ];
58
+
59
+ export function SettingsSidebar({ projectName, onBack }: SettingsSidebarProps) {
60
+ var { activeView, setSettingsSection, setProjectSettingsSection } = useSidebar();
61
+ var isProjectSettings = activeView.type === "project-settings";
62
+ var activeSection = activeView.type === "settings"
63
+ ? activeView.section
64
+ : activeView.type === "project-settings"
65
+ ? activeView.section
66
+ : null;
67
+
68
+ var nav = isProjectSettings ? PROJECT_SETTINGS_NAV : SETTINGS_NAV;
69
+ var headerLabel = isProjectSettings ? "Project Settings" : "Settings";
70
+
71
+ function handleItemClick(id: string) {
72
+ if (isProjectSettings) {
73
+ setProjectSettingsSection(id as ProjectSettingsSection);
74
+ } else {
75
+ setSettingsSection(id as SettingsSection);
76
+ }
77
+ }
78
+
79
+ return (
80
+ <div className="flex flex-col h-full w-full overflow-hidden bg-base-200">
81
+ <div className="px-4 py-3 border-b border-base-300 flex-shrink-0">
82
+ <span className="text-[13px] font-mono font-bold text-base-content">{headerLabel}</span>
83
+ </div>
84
+
85
+ <div className="flex flex-col flex-1 overflow-y-auto min-h-0 py-2 pb-16">
86
+ {nav.map(function (group) {
87
+ return (
88
+ <div key={group.group} className="mb-2">
89
+ <div className="px-4 pt-3 pb-1">
90
+ <span className="text-[10px] font-bold tracking-wider uppercase text-base-content/40">
91
+ {group.group}
92
+ </span>
93
+ </div>
94
+ {group.items.map(function (item) {
95
+ var isActive = activeSection === item.id;
96
+ return (
97
+ <button
98
+ key={item.id}
99
+ onClick={function () { handleItemClick(item.id); }}
100
+ className={
101
+ "w-full flex items-center gap-2.5 px-4 py-2.5 sm:py-1.5 text-[13px] transition-colors duration-[100ms] text-left " +
102
+ (isActive
103
+ ? "bg-primary/20 text-base-content font-medium"
104
+ : "text-base-content/55 hover:bg-base-content/5 hover:text-base-content")
105
+ }
106
+ >
107
+ {item.icon}
108
+ {item.label}
109
+ </button>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ })}
115
+ </div>
116
+
117
+ <div className="border-t border-base-300 flex-shrink-0">
118
+ <button
119
+ onClick={onBack}
120
+ className="w-full flex items-center gap-2 px-4 py-3.5 sm:py-3 text-[12px] text-base-content/50 hover:text-base-content transition-colors duration-[100ms]"
121
+ >
122
+ <ArrowLeft size={13} />
123
+ Back to {projectName}
124
+ </button>
125
+ </div>
126
+ </div>
127
+ );
128
+ }