@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.
- package/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { X, Plus, ChevronDown, ChevronRight } from "lucide-react";
|
|
3
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
4
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
|
+
import type { ProjectSettings } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
interface RuleEntry {
|
|
8
|
+
filename: string;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProjectRules({
|
|
13
|
+
settings,
|
|
14
|
+
updateSection,
|
|
15
|
+
}: {
|
|
16
|
+
settings: ProjectSettings;
|
|
17
|
+
updateSection: (section: string, data: Record<string, unknown>) => void;
|
|
18
|
+
}) {
|
|
19
|
+
var globalRules = settings.global.rules ?? [];
|
|
20
|
+
|
|
21
|
+
var [rules, setRules] = useState<RuleEntry[]>(function () {
|
|
22
|
+
return (settings.rules ?? []).map(function (r) {
|
|
23
|
+
return { filename: r.filename, content: r.content };
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
var [expandedGlobal, setExpandedGlobal] = useState<Set<number>>(new Set());
|
|
27
|
+
var [expandedProject, setExpandedProject] = useState<Set<number>>(new Set());
|
|
28
|
+
var [adding, setAdding] = useState(false);
|
|
29
|
+
var [newFilename, setNewFilename] = useState("");
|
|
30
|
+
var [newContent, setNewContent] = useState("");
|
|
31
|
+
var save = useSaveState();
|
|
32
|
+
|
|
33
|
+
useEffect(function () {
|
|
34
|
+
if (save.saving) {
|
|
35
|
+
save.confirmSave();
|
|
36
|
+
} else {
|
|
37
|
+
setRules((settings.rules ?? []).map(function (r) {
|
|
38
|
+
return { filename: r.filename, content: r.content };
|
|
39
|
+
}));
|
|
40
|
+
save.resetFromServer();
|
|
41
|
+
}
|
|
42
|
+
}, [settings]);
|
|
43
|
+
|
|
44
|
+
function toggleGlobal(idx: number) {
|
|
45
|
+
setExpandedGlobal(function (prev) {
|
|
46
|
+
var next = new Set(prev);
|
|
47
|
+
if (next.has(idx)) {
|
|
48
|
+
next.delete(idx);
|
|
49
|
+
} else {
|
|
50
|
+
next.add(idx);
|
|
51
|
+
}
|
|
52
|
+
return next;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toggleProject(idx: number) {
|
|
57
|
+
setExpandedProject(function (prev) {
|
|
58
|
+
var next = new Set(prev);
|
|
59
|
+
if (next.has(idx)) {
|
|
60
|
+
next.delete(idx);
|
|
61
|
+
} else {
|
|
62
|
+
next.add(idx);
|
|
63
|
+
}
|
|
64
|
+
return next;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleContentChange(idx: number, content: string) {
|
|
69
|
+
setRules(function (prev) {
|
|
70
|
+
return prev.map(function (r, i) {
|
|
71
|
+
return i === idx ? { ...r, content } : r;
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
save.markDirty();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleDelete(idx: number) {
|
|
78
|
+
setRules(function (prev) {
|
|
79
|
+
return prev.filter(function (_, i) { return i !== idx; });
|
|
80
|
+
});
|
|
81
|
+
setExpandedProject(new Set());
|
|
82
|
+
save.markDirty();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleAdd() {
|
|
86
|
+
var fn = newFilename.trim();
|
|
87
|
+
if (!fn || !fn.endsWith(".md")) return;
|
|
88
|
+
setRules(function (prev) {
|
|
89
|
+
return [...prev, { filename: fn, content: newContent }];
|
|
90
|
+
});
|
|
91
|
+
setNewFilename("");
|
|
92
|
+
setNewContent("");
|
|
93
|
+
setAdding(false);
|
|
94
|
+
save.markDirty();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleCancelAdd() {
|
|
98
|
+
setAdding(false);
|
|
99
|
+
setNewFilename("");
|
|
100
|
+
setNewContent("");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleSave() {
|
|
104
|
+
save.startSave();
|
|
105
|
+
updateSection("rules", { rules });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function preview(content: string): string {
|
|
109
|
+
var trimmed = content.trim();
|
|
110
|
+
if (trimmed.length <= 80) return trimmed;
|
|
111
|
+
return trimmed.slice(0, 80) + "...";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var textareaClass = "w-full px-3 py-2 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] resize-y min-h-[120px]";
|
|
115
|
+
var inputClass = "w-full h-9 sm:h-7 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]";
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className="py-2">
|
|
119
|
+
<div className="mb-6">
|
|
120
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
121
|
+
Global Rules
|
|
122
|
+
</h2>
|
|
123
|
+
{globalRules.length === 0 && (
|
|
124
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
125
|
+
No global rules.
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
{globalRules.length > 0 && (
|
|
129
|
+
<div className="flex flex-col gap-1.5">
|
|
130
|
+
{globalRules.map(function (rule, idx) {
|
|
131
|
+
var isExpanded = expandedGlobal.has(idx);
|
|
132
|
+
return (
|
|
133
|
+
<div key={rule.filename + "-" + idx} className="border border-base-content/10 rounded-xl overflow-hidden">
|
|
134
|
+
<button
|
|
135
|
+
onClick={function () { toggleGlobal(idx); }}
|
|
136
|
+
className="w-full flex items-center gap-2 px-3 py-2 bg-base-300/50 hover:bg-base-300/70 transition-colors duration-[120ms] cursor-pointer text-left"
|
|
137
|
+
>
|
|
138
|
+
{isExpanded
|
|
139
|
+
? <ChevronDown size={12} className="text-base-content/40 flex-shrink-0" />
|
|
140
|
+
: <ChevronRight size={12} className="text-base-content/40 flex-shrink-0" />
|
|
141
|
+
}
|
|
142
|
+
<span className="font-mono text-[12px] text-base-content/40 flex-1 truncate">
|
|
143
|
+
{rule.filename}
|
|
144
|
+
</span>
|
|
145
|
+
<span className="text-[10px] uppercase tracking-wider text-base-content/30">
|
|
146
|
+
global
|
|
147
|
+
</span>
|
|
148
|
+
</button>
|
|
149
|
+
{!isExpanded && (
|
|
150
|
+
<div className="px-3 py-1.5 text-[11px] text-base-content/30 font-mono truncate">
|
|
151
|
+
{preview(rule.content)}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
{isExpanded && (
|
|
155
|
+
<pre className="px-3 py-2 text-[12px] text-base-content/40 font-mono whitespace-pre-wrap break-words bg-base-300/30 max-h-[300px] overflow-auto">
|
|
156
|
+
{rule.content}
|
|
157
|
+
</pre>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div>
|
|
167
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
168
|
+
Project Rules
|
|
169
|
+
</h2>
|
|
170
|
+
{rules.length === 0 && !adding && (
|
|
171
|
+
<div className="py-4 text-center text-[13px] text-base-content/30 mb-3">
|
|
172
|
+
No project rules.
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
{rules.length > 0 && (
|
|
176
|
+
<div className="flex flex-col gap-1.5 mb-3">
|
|
177
|
+
{rules.map(function (rule, idx) {
|
|
178
|
+
var isExpanded = expandedProject.has(idx);
|
|
179
|
+
return (
|
|
180
|
+
<div key={rule.filename + "-" + idx} className="border border-base-content/15 rounded-xl overflow-hidden">
|
|
181
|
+
<div className="w-full flex items-center gap-2 px-3 py-2 bg-base-300 text-left">
|
|
182
|
+
<button
|
|
183
|
+
onClick={function () { toggleProject(idx); }}
|
|
184
|
+
className="flex items-center gap-2 flex-1 min-w-0 cursor-pointer hover:text-base-content/80 transition-colors"
|
|
185
|
+
>
|
|
186
|
+
{isExpanded
|
|
187
|
+
? <ChevronDown size={12} className="text-base-content/60 flex-shrink-0" />
|
|
188
|
+
: <ChevronRight size={12} className="text-base-content/60 flex-shrink-0" />
|
|
189
|
+
}
|
|
190
|
+
<span className="font-mono text-[12px] text-base-content flex-1 truncate text-left">
|
|
191
|
+
{rule.filename}
|
|
192
|
+
</span>
|
|
193
|
+
</button>
|
|
194
|
+
<button
|
|
195
|
+
onClick={function () { handleDelete(idx); }}
|
|
196
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error flex-shrink-0 focus-visible:ring-2 focus-visible:ring-primary"
|
|
197
|
+
aria-label={"Delete " + rule.filename}
|
|
198
|
+
>
|
|
199
|
+
<X size={14} />
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
{!isExpanded && (
|
|
203
|
+
<div className="px-3 py-1.5 text-[11px] text-base-content/50 font-mono truncate">
|
|
204
|
+
{preview(rule.content)}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
{isExpanded && (
|
|
208
|
+
<div className="px-3 py-2 bg-base-300/30">
|
|
209
|
+
<textarea
|
|
210
|
+
value={rule.content}
|
|
211
|
+
onChange={function (e) { handleContentChange(idx, e.target.value); }}
|
|
212
|
+
className={textareaClass}
|
|
213
|
+
rows={8}
|
|
214
|
+
aria-label={"Content for " + rule.filename}
|
|
215
|
+
/>
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
})}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{adding && (
|
|
225
|
+
<div className="border border-dashed border-base-content/20 rounded-xl p-3 mb-3 flex flex-col gap-2">
|
|
226
|
+
<input
|
|
227
|
+
type="text"
|
|
228
|
+
value={newFilename}
|
|
229
|
+
onChange={function (e) { setNewFilename(e.target.value); }}
|
|
230
|
+
placeholder="filename.md"
|
|
231
|
+
aria-label="New rule filename"
|
|
232
|
+
className={inputClass}
|
|
233
|
+
/>
|
|
234
|
+
<textarea
|
|
235
|
+
value={newContent}
|
|
236
|
+
onChange={function (e) { setNewContent(e.target.value); }}
|
|
237
|
+
placeholder="Rule content..."
|
|
238
|
+
aria-label="New rule content"
|
|
239
|
+
className={textareaClass}
|
|
240
|
+
rows={5}
|
|
241
|
+
/>
|
|
242
|
+
<div className="flex gap-2 justify-end">
|
|
243
|
+
<button
|
|
244
|
+
onClick={handleCancelAdd}
|
|
245
|
+
className="btn btn-ghost btn-sm text-[12px]"
|
|
246
|
+
>
|
|
247
|
+
Cancel
|
|
248
|
+
</button>
|
|
249
|
+
<button
|
|
250
|
+
onClick={handleAdd}
|
|
251
|
+
disabled={!newFilename.trim() || !newFilename.trim().endsWith(".md")}
|
|
252
|
+
className={
|
|
253
|
+
"btn btn-sm btn-primary text-[12px]" +
|
|
254
|
+
(!newFilename.trim() || !newFilename.trim().endsWith(".md") ? " opacity-50 cursor-not-allowed" : "")
|
|
255
|
+
}
|
|
256
|
+
>
|
|
257
|
+
Add
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
|
|
263
|
+
{!adding && (
|
|
264
|
+
<button
|
|
265
|
+
onClick={function () { setAdding(true); }}
|
|
266
|
+
className="flex items-center gap-1.5 px-3 py-2.5 sm:py-1.5 rounded-xl border border-dashed border-base-content/20 bg-transparent text-base-content/40 text-[12px] hover:text-base-content/60 hover:border-base-content/30 transition-colors duration-[120ms] mb-5 cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100"
|
|
267
|
+
>
|
|
268
|
+
<Plus size={12} />
|
|
269
|
+
Add Rule
|
|
270
|
+
</button>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
2
|
+
import { useProjectSettings } from "../../hooks/useProjectSettings";
|
|
3
|
+
import { Menu } from "lucide-react";
|
|
4
|
+
import type { ProjectSettingsSection } from "../../stores/sidebar";
|
|
5
|
+
import type { ProjectSettings } from "@lattice/shared";
|
|
6
|
+
import { ProjectGeneral } from "./ProjectGeneral";
|
|
7
|
+
import { ProjectClaude } from "./ProjectClaude";
|
|
8
|
+
import { ProjectEnvironment } from "./ProjectEnvironment";
|
|
9
|
+
import { ProjectPermissions } from "./ProjectPermissions";
|
|
10
|
+
import { ProjectSkills } from "./ProjectSkills";
|
|
11
|
+
import { ProjectRules } from "./ProjectRules";
|
|
12
|
+
import { ProjectMcp } from "./ProjectMcp";
|
|
13
|
+
|
|
14
|
+
var SECTION_CONFIG: Record<string, { title: string }> = {
|
|
15
|
+
general: { title: "General" },
|
|
16
|
+
claude: { title: "Claude" },
|
|
17
|
+
environment: { title: "Environment" },
|
|
18
|
+
mcp: { title: "MCP Servers" },
|
|
19
|
+
skills: { title: "Skills" },
|
|
20
|
+
rules: { title: "Rules" },
|
|
21
|
+
permissions: { title: "Permissions" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function renderSection(
|
|
25
|
+
section: ProjectSettingsSection,
|
|
26
|
+
settings: ProjectSettings,
|
|
27
|
+
updateSection: (section: string, data: Record<string, unknown>) => void,
|
|
28
|
+
projectSlug?: string,
|
|
29
|
+
) {
|
|
30
|
+
if (section === "general") {
|
|
31
|
+
return <ProjectGeneral settings={settings} updateSection={updateSection} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (section === "claude") {
|
|
35
|
+
return <ProjectClaude settings={settings} updateSection={updateSection} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (section === "environment") {
|
|
39
|
+
return <ProjectEnvironment settings={settings} updateSection={updateSection} />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (section === "permissions") {
|
|
43
|
+
return <ProjectPermissions settings={settings} updateSection={updateSection} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (section === "skills") {
|
|
47
|
+
return <ProjectSkills settings={settings} projectSlug={projectSlug} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (section === "rules") {
|
|
51
|
+
return <ProjectRules settings={settings} updateSection={updateSection} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (section === "mcp") {
|
|
55
|
+
return <ProjectMcp settings={settings} updateSection={updateSection} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="py-2 text-[13px] text-base-content/40">
|
|
60
|
+
{section} section coming soon.
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ProjectSettingsView() {
|
|
66
|
+
var { activeView, activeProjectSlug, toggleDrawer } = useSidebar();
|
|
67
|
+
|
|
68
|
+
if (activeView.type !== "project-settings") {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var section = activeView.section;
|
|
73
|
+
var config = SECTION_CONFIG[section];
|
|
74
|
+
var { settings, loading, error, updateSection } = useProjectSettings(activeProjectSlug);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="flex-1 overflow-auto px-4 sm:px-8 py-4 sm:py-6 max-w-3xl">
|
|
78
|
+
{config && (
|
|
79
|
+
<div className="mb-6 flex items-center gap-3">
|
|
80
|
+
<button
|
|
81
|
+
className="btn btn-ghost btn-sm btn-square lg:hidden"
|
|
82
|
+
aria-label="Toggle sidebar"
|
|
83
|
+
onClick={toggleDrawer}
|
|
84
|
+
>
|
|
85
|
+
<Menu size={18} />
|
|
86
|
+
</button>
|
|
87
|
+
<h1 className="text-lg font-mono font-bold text-base-content">{config.title}</h1>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
{loading && (
|
|
91
|
+
<div className="text-[13px] text-base-content/40 py-4">Loading...</div>
|
|
92
|
+
)}
|
|
93
|
+
{error && (
|
|
94
|
+
<div className="text-[13px] text-error py-4">{error}</div>
|
|
95
|
+
)}
|
|
96
|
+
{!loading && !error && settings && renderSection(section, settings, updateSection, activeProjectSlug ?? undefined)}
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
3
|
+
import { SkillMarketplace } from "../settings/SkillMarketplace";
|
|
4
|
+
import { SkillItem, SkillViewModal } from "../settings/skill-shared";
|
|
5
|
+
import type { ProjectSettings, ServerMessage } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
interface ProjectSkillsProps {
|
|
8
|
+
settings: ProjectSettings;
|
|
9
|
+
projectSlug?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProjectSkills({ settings, projectSlug }: ProjectSkillsProps) {
|
|
13
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
14
|
+
var globalSkills = settings.global.skills;
|
|
15
|
+
var projectSkills = settings.skills;
|
|
16
|
+
var hasAny = globalSkills.length > 0 || projectSkills.length > 0;
|
|
17
|
+
var [viewContent, setViewContent] = useState<{ path: string; content: string } | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(function () {
|
|
20
|
+
function handleViewResult(msg: ServerMessage) {
|
|
21
|
+
if (msg.type !== "skills:view_result") return;
|
|
22
|
+
var data = msg as { type: "skills:view_result"; path: string; content: string };
|
|
23
|
+
setViewContent({ path: data.path, content: data.content });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
subscribe("skills:view_result", handleViewResult);
|
|
27
|
+
|
|
28
|
+
return function () {
|
|
29
|
+
unsubscribe("skills:view_result", handleViewResult);
|
|
30
|
+
};
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
function handleView(path: string) {
|
|
34
|
+
send({ type: "skills:view", path: path } as any);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="py-2 space-y-6">
|
|
39
|
+
{!hasAny && (
|
|
40
|
+
<div className="py-12 text-center text-[13px] text-base-content/40">
|
|
41
|
+
No skills found.
|
|
42
|
+
</div>
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
{globalSkills.length > 0 && (
|
|
46
|
+
<div>
|
|
47
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Global Skills</div>
|
|
48
|
+
<div className="space-y-2">
|
|
49
|
+
{globalSkills.map(function (skill) {
|
|
50
|
+
return (
|
|
51
|
+
<SkillItem
|
|
52
|
+
key={skill.path}
|
|
53
|
+
skill={skill}
|
|
54
|
+
badge="global"
|
|
55
|
+
onClick={function () { handleView(skill.path); }}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{projectSkills.length > 0 && (
|
|
64
|
+
<div>
|
|
65
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Project Skills</div>
|
|
66
|
+
<div className="space-y-2">
|
|
67
|
+
{projectSkills.map(function (skill) {
|
|
68
|
+
return (
|
|
69
|
+
<SkillItem
|
|
70
|
+
key={skill.path}
|
|
71
|
+
skill={skill}
|
|
72
|
+
onClick={function () { handleView(skill.path); }}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
|
|
80
|
+
<SkillMarketplace defaultScope="project" defaultProjectSlug={projectSlug} />
|
|
81
|
+
|
|
82
|
+
{viewContent && (
|
|
83
|
+
<SkillViewModal
|
|
84
|
+
path={viewContent.path}
|
|
85
|
+
content={viewContent.content}
|
|
86
|
+
onClose={function () { setViewContent(null); }}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { memo, useMemo, useCallback } from "react";
|
|
2
|
+
import { useTheme } from "../../hooks/useTheme";
|
|
3
|
+
import { Sun, Moon, Check } from "lucide-react";
|
|
4
|
+
import type { ThemeEntry } from "../../themes/index";
|
|
5
|
+
|
|
6
|
+
var SWATCH_KEYS = [
|
|
7
|
+
"base00", "base01", "base02", "base03",
|
|
8
|
+
"base04", "base05", "base06", "base07",
|
|
9
|
+
"base08", "base09", "base0A", "base0B",
|
|
10
|
+
"base0C", "base0D", "base0E", "base0F",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
var ThemeCard = memo(function ThemeCard({
|
|
14
|
+
entry,
|
|
15
|
+
active,
|
|
16
|
+
onSelect,
|
|
17
|
+
}: {
|
|
18
|
+
entry: ThemeEntry;
|
|
19
|
+
active: boolean;
|
|
20
|
+
onSelect: (id: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
var t = entry.theme;
|
|
23
|
+
|
|
24
|
+
function handleClick() {
|
|
25
|
+
onSelect(entry.id);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<button
|
|
30
|
+
onClick={handleClick}
|
|
31
|
+
className={
|
|
32
|
+
"flex flex-col gap-2 p-3 sm:p-2.5 px-3 rounded-lg border cursor-pointer text-left transition-colors duration-[120ms] relative focus-visible:ring-2 focus-visible:ring-primary " +
|
|
33
|
+
(active
|
|
34
|
+
? "border-primary bg-base-300 shadow-sm"
|
|
35
|
+
: "border-base-content/15 bg-base-300 hover:border-base-content/30")
|
|
36
|
+
}
|
|
37
|
+
>
|
|
38
|
+
{active && (
|
|
39
|
+
<div className="absolute top-1.5 right-1.5 w-3.5 h-3.5 rounded-full bg-primary flex items-center justify-center">
|
|
40
|
+
<Check size={8} className="text-primary-content" strokeWidth={1.8} />
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
|
|
44
|
+
<div className="flex gap-[3px] flex-wrap w-[80px]">
|
|
45
|
+
{SWATCH_KEYS.map(function (key) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
key={key}
|
|
49
|
+
className="w-[10px] h-[10px] rounded-sm flex-shrink-0 ring-1 ring-base-content/10"
|
|
50
|
+
style={{ background: "#" + t[key] }}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div className="text-[12px] font-medium text-base-content">
|
|
57
|
+
{t.name}
|
|
58
|
+
</div>
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function ThemeGroup({
|
|
64
|
+
label,
|
|
65
|
+
entries,
|
|
66
|
+
currentThemeId,
|
|
67
|
+
onSelect,
|
|
68
|
+
}: {
|
|
69
|
+
label: string;
|
|
70
|
+
entries: ThemeEntry[];
|
|
71
|
+
currentThemeId: string;
|
|
72
|
+
onSelect: (id: string) => void;
|
|
73
|
+
}) {
|
|
74
|
+
if (entries.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="mb-6">
|
|
80
|
+
<div className="text-[11px] font-mono font-bold tracking-[0.1em] uppercase text-base-content/40 mb-3">
|
|
81
|
+
{label}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="grid grid-cols-[repeat(auto-fill,minmax(100px,1fr))] gap-2">
|
|
84
|
+
{entries.map(function (entry) {
|
|
85
|
+
return (
|
|
86
|
+
<ThemeCard
|
|
87
|
+
key={entry.id}
|
|
88
|
+
entry={entry}
|
|
89
|
+
active={entry.id === currentThemeId}
|
|
90
|
+
onSelect={onSelect}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function Appearance() {
|
|
100
|
+
var { mode, currentThemeId, toggleMode, setTheme, themes } = useTheme();
|
|
101
|
+
|
|
102
|
+
var darkThemes = useMemo(function () {
|
|
103
|
+
return themes.filter(function (e) { return e.theme.variant === "dark"; });
|
|
104
|
+
}, [themes]);
|
|
105
|
+
|
|
106
|
+
var lightThemes = useMemo(function () {
|
|
107
|
+
return themes.filter(function (e) { return e.theme.variant === "light"; });
|
|
108
|
+
}, [themes]);
|
|
109
|
+
|
|
110
|
+
var handleThemeSelect = useCallback(function (id: string) {
|
|
111
|
+
setTheme(id);
|
|
112
|
+
}, [setTheme]);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="py-2">
|
|
116
|
+
<div className="flex items-center justify-between mb-6">
|
|
117
|
+
<div className="text-[12px] font-semibold text-base-content/40">Color Mode</div>
|
|
118
|
+
<button
|
|
119
|
+
onClick={toggleMode}
|
|
120
|
+
className="btn btn-ghost btn-sm border border-base-content/20"
|
|
121
|
+
>
|
|
122
|
+
{mode === "dark" ? (
|
|
123
|
+
<>
|
|
124
|
+
<Sun size={12} />
|
|
125
|
+
Switch to Light
|
|
126
|
+
</>
|
|
127
|
+
) : (
|
|
128
|
+
<>
|
|
129
|
+
<Moon size={12} />
|
|
130
|
+
Switch to Dark
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<ThemeGroup
|
|
137
|
+
label="Dark Themes"
|
|
138
|
+
entries={darkThemes}
|
|
139
|
+
currentThemeId={currentThemeId}
|
|
140
|
+
onSelect={handleThemeSelect}
|
|
141
|
+
/>
|
|
142
|
+
|
|
143
|
+
<ThemeGroup
|
|
144
|
+
label="Light Themes"
|
|
145
|
+
entries={lightThemes}
|
|
146
|
+
currentThemeId={currentThemeId}
|
|
147
|
+
onSelect={handleThemeSelect}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|