@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,232 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { Plus, Pencil, Trash2 } from "lucide-react";
|
|
3
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
4
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
5
|
+
import type { ProjectSettings, McpServerConfig } from "@lattice/shared";
|
|
6
|
+
import {
|
|
7
|
+
type FormState,
|
|
8
|
+
emptyForm,
|
|
9
|
+
formFromConfig,
|
|
10
|
+
formToConfig,
|
|
11
|
+
typeBadge,
|
|
12
|
+
configSummary,
|
|
13
|
+
ServerForm,
|
|
14
|
+
} from "../settings/mcp-shared";
|
|
15
|
+
|
|
16
|
+
export function ProjectMcp({
|
|
17
|
+
settings,
|
|
18
|
+
updateSection,
|
|
19
|
+
}: {
|
|
20
|
+
settings: ProjectSettings;
|
|
21
|
+
updateSection: (section: string, data: Record<string, unknown>) => void;
|
|
22
|
+
}) {
|
|
23
|
+
var globalServers = settings.global.mcpServers ?? {};
|
|
24
|
+
var globalEntries = Object.entries(globalServers);
|
|
25
|
+
|
|
26
|
+
var [servers, setServers] = useState<Record<string, McpServerConfig>>(function () {
|
|
27
|
+
return { ...(settings.mcpServers ?? {}) };
|
|
28
|
+
});
|
|
29
|
+
var save = useSaveState();
|
|
30
|
+
|
|
31
|
+
var [adding, setAdding] = useState(false);
|
|
32
|
+
var [addForm, setAddForm] = useState<FormState>(emptyForm);
|
|
33
|
+
var [editingName, setEditingName] = useState<string | null>(null);
|
|
34
|
+
var [editForm, setEditForm] = useState<FormState>(emptyForm);
|
|
35
|
+
var [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(function () {
|
|
38
|
+
if (save.saving) {
|
|
39
|
+
save.confirmSave();
|
|
40
|
+
} else {
|
|
41
|
+
setServers({ ...(settings.mcpServers ?? {}) });
|
|
42
|
+
save.resetFromServer();
|
|
43
|
+
}
|
|
44
|
+
}, [settings]);
|
|
45
|
+
|
|
46
|
+
var projectEntries = Object.entries(servers);
|
|
47
|
+
|
|
48
|
+
function handleAddSave() {
|
|
49
|
+
var name = addForm.name.trim();
|
|
50
|
+
if (!name) return;
|
|
51
|
+
var next = { ...servers, [name]: formToConfig(addForm) };
|
|
52
|
+
setServers(next);
|
|
53
|
+
setAdding(false);
|
|
54
|
+
setAddForm(emptyForm());
|
|
55
|
+
save.markDirty();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleEditStart(name: string) {
|
|
59
|
+
setEditingName(name);
|
|
60
|
+
setEditForm(formFromConfig(name, servers[name]));
|
|
61
|
+
setAdding(false);
|
|
62
|
+
setConfirmDelete(null);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handleEditSave() {
|
|
66
|
+
if (!editingName) return;
|
|
67
|
+
var newName = editForm.name.trim();
|
|
68
|
+
if (!newName) return;
|
|
69
|
+
var next = { ...servers };
|
|
70
|
+
if (newName !== editingName) {
|
|
71
|
+
delete next[editingName];
|
|
72
|
+
}
|
|
73
|
+
next[newName] = formToConfig(editForm);
|
|
74
|
+
setServers(next);
|
|
75
|
+
setEditingName(null);
|
|
76
|
+
save.markDirty();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleDelete(name: string) {
|
|
80
|
+
if (confirmDelete !== name) {
|
|
81
|
+
setConfirmDelete(name);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
var next = { ...servers };
|
|
85
|
+
delete next[name];
|
|
86
|
+
setServers(next);
|
|
87
|
+
if (editingName === name) setEditingName(null);
|
|
88
|
+
setConfirmDelete(null);
|
|
89
|
+
save.markDirty();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleSave() {
|
|
93
|
+
save.startSave();
|
|
94
|
+
updateSection("mcp", { mcpServers: servers });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var existingNamesForAdd = new Set(Object.keys(servers));
|
|
98
|
+
var existingNamesForEdit = new Set(
|
|
99
|
+
Object.keys(servers).filter(function (n) { return n !== editingName; })
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="py-2">
|
|
104
|
+
<div className="mb-6">
|
|
105
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
106
|
+
Global MCP Servers
|
|
107
|
+
</h2>
|
|
108
|
+
{globalEntries.length === 0 && (
|
|
109
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
110
|
+
No global MCP servers.
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
{globalEntries.length > 0 && (
|
|
114
|
+
<div className="flex flex-col gap-2">
|
|
115
|
+
{globalEntries.map(function ([name, config]) {
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
key={name}
|
|
119
|
+
className="flex items-center gap-3 px-3 py-2 rounded-xl bg-base-300/50 border border-base-content/10"
|
|
120
|
+
>
|
|
121
|
+
<span className="font-mono text-[12px] text-base-content/40 font-semibold flex-shrink-0">{name}</span>
|
|
122
|
+
{typeBadge(config)}
|
|
123
|
+
<div className="flex-1 min-w-0">{configSummary(config)}</div>
|
|
124
|
+
<span className="text-[10px] uppercase tracking-wider text-base-content/30 flex-shrink-0">global</span>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
})}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div>
|
|
133
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
134
|
+
Project MCP Servers
|
|
135
|
+
</h2>
|
|
136
|
+
|
|
137
|
+
{projectEntries.length === 0 && !adding && (
|
|
138
|
+
<div className="py-4 text-center text-[13px] text-base-content/30 mb-3">
|
|
139
|
+
No project MCP servers.
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
<div className="flex flex-col gap-2 mb-3">
|
|
144
|
+
{projectEntries.map(function ([name, config]) {
|
|
145
|
+
if (editingName === name) {
|
|
146
|
+
return (
|
|
147
|
+
<ServerForm
|
|
148
|
+
key={name}
|
|
149
|
+
form={editForm}
|
|
150
|
+
setForm={setEditForm}
|
|
151
|
+
onSave={handleEditSave}
|
|
152
|
+
onCancel={function () { setEditingName(null); }}
|
|
153
|
+
existingNames={existingNamesForEdit}
|
|
154
|
+
idPrefix="mcp-edit"
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
var isConfirming = confirmDelete === name;
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
key={name}
|
|
162
|
+
className="flex items-center gap-3 px-3 py-2 rounded-xl bg-base-300 border border-base-content/15"
|
|
163
|
+
>
|
|
164
|
+
<span className="font-mono text-[12px] text-base-content font-semibold flex-shrink-0">{name}</span>
|
|
165
|
+
{typeBadge(config)}
|
|
166
|
+
<div className="flex-1 min-w-0">{configSummary(config)}</div>
|
|
167
|
+
{isConfirming ? (
|
|
168
|
+
<div className="flex gap-1.5 flex-shrink-0">
|
|
169
|
+
<button
|
|
170
|
+
onClick={function () { handleDelete(name); }}
|
|
171
|
+
className="btn btn-error btn-xs"
|
|
172
|
+
>
|
|
173
|
+
Confirm
|
|
174
|
+
</button>
|
|
175
|
+
<button
|
|
176
|
+
onClick={function () { setConfirmDelete(null); }}
|
|
177
|
+
className="btn btn-ghost btn-xs"
|
|
178
|
+
>
|
|
179
|
+
Cancel
|
|
180
|
+
</button>
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<div className="flex gap-1 flex-shrink-0">
|
|
184
|
+
<button
|
|
185
|
+
onClick={function () { handleEditStart(name); }}
|
|
186
|
+
aria-label={"Edit " + name}
|
|
187
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-primary focus-visible:ring-2 focus-visible:ring-primary"
|
|
188
|
+
>
|
|
189
|
+
<Pencil size={12} />
|
|
190
|
+
</button>
|
|
191
|
+
<button
|
|
192
|
+
onClick={function () { handleDelete(name); }}
|
|
193
|
+
aria-label={"Delete " + name}
|
|
194
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error focus-visible:ring-2 focus-visible:ring-primary"
|
|
195
|
+
>
|
|
196
|
+
<Trash2 size={12} />
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{adding && (
|
|
206
|
+
<div className="mb-3">
|
|
207
|
+
<ServerForm
|
|
208
|
+
form={addForm}
|
|
209
|
+
setForm={setAddForm}
|
|
210
|
+
onSave={handleAddSave}
|
|
211
|
+
onCancel={function () { setAdding(false); setAddForm(emptyForm()); }}
|
|
212
|
+
existingNames={existingNamesForAdd}
|
|
213
|
+
idPrefix="mcp-add"
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{!adding && (
|
|
219
|
+
<button
|
|
220
|
+
onClick={function () { setAdding(true); setEditingName(null); setConfirmDelete(null); }}
|
|
221
|
+
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"
|
|
222
|
+
>
|
|
223
|
+
<Plus size={12} />
|
|
224
|
+
Add Server
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { X, Plus } from "lucide-react";
|
|
3
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
4
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
|
+
import type { ProjectSettings } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
function RuleList({
|
|
8
|
+
rules,
|
|
9
|
+
onDelete,
|
|
10
|
+
readOnly,
|
|
11
|
+
labelPrefix,
|
|
12
|
+
}: {
|
|
13
|
+
rules: string[];
|
|
14
|
+
onDelete?: (index: number) => void;
|
|
15
|
+
readOnly?: boolean;
|
|
16
|
+
labelPrefix?: string;
|
|
17
|
+
}) {
|
|
18
|
+
if (rules.length === 0) {
|
|
19
|
+
return (
|
|
20
|
+
<div className="py-3 text-center text-[13px] text-base-content/30">
|
|
21
|
+
No rules defined.
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex flex-col gap-1.5">
|
|
28
|
+
{rules.map(function (rule, idx) {
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
key={rule + "-" + idx}
|
|
32
|
+
className={
|
|
33
|
+
"flex items-center gap-2 h-9 sm:h-7 px-3 rounded-xl border font-mono text-[12px] " +
|
|
34
|
+
(readOnly
|
|
35
|
+
? "bg-base-300/50 border-base-content/10 text-base-content/40"
|
|
36
|
+
: "bg-base-300 border-base-content/15 text-base-content")
|
|
37
|
+
}
|
|
38
|
+
>
|
|
39
|
+
<span className="flex-1 truncate">{rule}</span>
|
|
40
|
+
{readOnly && (
|
|
41
|
+
<span className="text-[10px] uppercase tracking-wider text-base-content/30 flex-shrink-0">
|
|
42
|
+
global
|
|
43
|
+
</span>
|
|
44
|
+
)}
|
|
45
|
+
{!readOnly && onDelete && (
|
|
46
|
+
<button
|
|
47
|
+
onClick={function () { onDelete(idx); }}
|
|
48
|
+
aria-label={"Delete " + (labelPrefix || "") + " rule: " + rule}
|
|
49
|
+
title="Delete"
|
|
50
|
+
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"
|
|
51
|
+
>
|
|
52
|
+
<X size={12} />
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function AddRuleRow({ onAdd, label }: { onAdd: (rule: string) => void; label: string }) {
|
|
63
|
+
var [value, setValue] = useState("");
|
|
64
|
+
|
|
65
|
+
function handleAdd() {
|
|
66
|
+
var trimmed = value.trim();
|
|
67
|
+
if (!trimmed) return;
|
|
68
|
+
onAdd(trimmed);
|
|
69
|
+
setValue("");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleKeyDown(e: React.KeyboardEvent) {
|
|
73
|
+
if (e.key === "Enter") {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
handleAdd();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className="flex items-center gap-1.5 mt-1.5">
|
|
81
|
+
<input
|
|
82
|
+
type="text"
|
|
83
|
+
value={value}
|
|
84
|
+
onChange={function (e) { setValue(e.target.value); }}
|
|
85
|
+
onKeyDown={handleKeyDown}
|
|
86
|
+
placeholder="e.g. Bash(curl:*)"
|
|
87
|
+
aria-label={label}
|
|
88
|
+
className="flex-1 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]"
|
|
89
|
+
/>
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleAdd}
|
|
92
|
+
disabled={!value.trim()}
|
|
93
|
+
className={
|
|
94
|
+
"flex items-center gap-1.5 px-3 h-9 sm:h-7 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] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary" +
|
|
95
|
+
(!value.trim() ? " opacity-50 cursor-not-allowed" : "")
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
<Plus size={12} />
|
|
99
|
+
Add
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ProjectPermissions({
|
|
106
|
+
settings,
|
|
107
|
+
updateSection,
|
|
108
|
+
}: {
|
|
109
|
+
settings: ProjectSettings;
|
|
110
|
+
updateSection: (section: string, data: Record<string, unknown>) => void;
|
|
111
|
+
}) {
|
|
112
|
+
var [allow, setAllow] = useState<string[]>(function () {
|
|
113
|
+
return [...(settings.permissions.allow ?? [])];
|
|
114
|
+
});
|
|
115
|
+
var [deny, setDeny] = useState<string[]>(function () {
|
|
116
|
+
return [...(settings.permissions.deny ?? [])];
|
|
117
|
+
});
|
|
118
|
+
var save = useSaveState();
|
|
119
|
+
|
|
120
|
+
useEffect(function () {
|
|
121
|
+
if (save.saving) {
|
|
122
|
+
save.confirmSave();
|
|
123
|
+
} else {
|
|
124
|
+
setAllow([...(settings.permissions.allow ?? [])]);
|
|
125
|
+
setDeny([...(settings.permissions.deny ?? [])]);
|
|
126
|
+
save.resetFromServer();
|
|
127
|
+
}
|
|
128
|
+
}, [settings]);
|
|
129
|
+
|
|
130
|
+
function handleDeleteAllow(idx: number) {
|
|
131
|
+
setAllow(function (prev) {
|
|
132
|
+
var next = [...prev];
|
|
133
|
+
next.splice(idx, 1);
|
|
134
|
+
return next;
|
|
135
|
+
});
|
|
136
|
+
save.markDirty();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleDeleteDeny(idx: number) {
|
|
140
|
+
setDeny(function (prev) {
|
|
141
|
+
var next = [...prev];
|
|
142
|
+
next.splice(idx, 1);
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
save.markDirty();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleAddAllow(rule: string) {
|
|
149
|
+
setAllow(function (prev) { return [...prev, rule]; });
|
|
150
|
+
save.markDirty();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleAddDeny(rule: string) {
|
|
154
|
+
setDeny(function (prev) { return [...prev, rule]; });
|
|
155
|
+
save.markDirty();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function handleSave() {
|
|
159
|
+
save.startSave();
|
|
160
|
+
updateSection("permissions", { allow, deny });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var globalAllow = settings.global.permissions?.allow ?? [];
|
|
164
|
+
var globalDeny = settings.global.permissions?.deny ?? [];
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="py-2">
|
|
168
|
+
<div className="mb-6">
|
|
169
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
170
|
+
Allow Rules
|
|
171
|
+
</h2>
|
|
172
|
+
<RuleList rules={allow} onDelete={handleDeleteAllow} labelPrefix="allow" />
|
|
173
|
+
<AddRuleRow onAdd={handleAddAllow} label="New allow rule" />
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="mb-6">
|
|
177
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
178
|
+
Deny Rules
|
|
179
|
+
</h2>
|
|
180
|
+
<RuleList rules={deny} onDelete={handleDeleteDeny} labelPrefix="deny" />
|
|
181
|
+
<AddRuleRow onAdd={handleAddDeny} label="New deny rule" />
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<div className="mb-8">
|
|
185
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{(globalAllow.length > 0 || globalDeny.length > 0) && (
|
|
189
|
+
<div>
|
|
190
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
191
|
+
Global Permissions
|
|
192
|
+
</h2>
|
|
193
|
+
{globalAllow.length > 0 && (
|
|
194
|
+
<div className="mb-4">
|
|
195
|
+
<div className="text-[11px] text-base-content/40 uppercase tracking-wider mb-1.5">Allow</div>
|
|
196
|
+
<RuleList rules={globalAllow} readOnly />
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
{globalDeny.length > 0 && (
|
|
200
|
+
<div className="mb-4">
|
|
201
|
+
<div className="text-[11px] text-base-content/40 uppercase tracking-wider mb-1.5">Deny</div>
|
|
202
|
+
<RuleList rules={globalDeny} readOnly />
|
|
203
|
+
</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|