@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,151 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
3
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
4
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
5
|
+
import type { ServerMessage, SettingsDataMessage, SettingsUpdateMessage } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
var CLAUDE_MODELS = [
|
|
8
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
9
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
10
|
+
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
11
|
+
{ id: "claude-opus-4-5", label: "Claude Opus 4.5" },
|
|
12
|
+
{ id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
var EFFORT_LEVELS = [
|
|
16
|
+
{ id: "low", label: "Low" },
|
|
17
|
+
{ id: "normal", label: "Normal" },
|
|
18
|
+
{ id: "high", label: "High" },
|
|
19
|
+
{ id: "max", label: "Max" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function ClaudeSettings() {
|
|
23
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
24
|
+
var [claudeMd, setClaudeMd] = useState("");
|
|
25
|
+
var [model, setModel] = useState(CLAUDE_MODELS[0].id);
|
|
26
|
+
var [effort, setEffort] = useState("normal");
|
|
27
|
+
var save = useSaveState();
|
|
28
|
+
|
|
29
|
+
useEffect(function () {
|
|
30
|
+
function handleMessage(msg: ServerMessage) {
|
|
31
|
+
if (msg.type !== "settings:data") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
var data = msg as SettingsDataMessage;
|
|
35
|
+
var cfg = data.config as unknown as Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
var newClaudeMd = cfg.claudeMd ? String(cfg.claudeMd) : "";
|
|
38
|
+
var newModel = cfg.defaultModel ? String(cfg.defaultModel) : CLAUDE_MODELS[0].id;
|
|
39
|
+
var newEffort = cfg.defaultEffort ? String(cfg.defaultEffort) : "normal";
|
|
40
|
+
|
|
41
|
+
if (save.saving) {
|
|
42
|
+
save.confirmSave();
|
|
43
|
+
} else {
|
|
44
|
+
setClaudeMd(newClaudeMd);
|
|
45
|
+
setModel(newModel);
|
|
46
|
+
setEffort(newEffort);
|
|
47
|
+
save.resetFromServer();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
subscribe("settings:data", handleMessage);
|
|
52
|
+
send({ type: "settings:get" });
|
|
53
|
+
|
|
54
|
+
return function () {
|
|
55
|
+
unsubscribe("settings:data", handleMessage);
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
function handleModelChange(value: string) {
|
|
60
|
+
setModel(value);
|
|
61
|
+
save.markDirty();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleEffortChange(value: string) {
|
|
65
|
+
setEffort(value);
|
|
66
|
+
save.markDirty();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function handleClaudeMdChange(value: string) {
|
|
70
|
+
setClaudeMd(value);
|
|
71
|
+
save.markDirty();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleSave() {
|
|
75
|
+
save.startSave();
|
|
76
|
+
var updateMsg: SettingsUpdateMessage = {
|
|
77
|
+
type: "settings:update",
|
|
78
|
+
settings: { claudeMd, defaultModel: model, defaultEffort: effort } as SettingsUpdateMessage["settings"],
|
|
79
|
+
};
|
|
80
|
+
send(updateMsg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="py-2">
|
|
85
|
+
<div className="mb-5">
|
|
86
|
+
<label htmlFor="claude-default-model" className="block text-[12px] font-semibold text-base-content/40 mb-2">Default Model</label>
|
|
87
|
+
<select
|
|
88
|
+
id="claude-default-model"
|
|
89
|
+
value={model}
|
|
90
|
+
onChange={function (e) { handleModelChange(e.target.value); }}
|
|
91
|
+
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]"
|
|
92
|
+
>
|
|
93
|
+
{CLAUDE_MODELS.map(function (m) {
|
|
94
|
+
return (
|
|
95
|
+
<option key={m.id} value={m.id} className="bg-base-300">
|
|
96
|
+
{m.label}
|
|
97
|
+
</option>
|
|
98
|
+
);
|
|
99
|
+
})}
|
|
100
|
+
</select>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="mb-6" role="radiogroup" aria-label="Default Effort">
|
|
104
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Default Effort</div>
|
|
105
|
+
<div className="flex gap-2">
|
|
106
|
+
{EFFORT_LEVELS.map(function (e) {
|
|
107
|
+
var active = effort === e.id;
|
|
108
|
+
return (
|
|
109
|
+
<button
|
|
110
|
+
key={e.id}
|
|
111
|
+
role="radio"
|
|
112
|
+
aria-checked={active}
|
|
113
|
+
onClick={function () { handleEffortChange(e.id); }}
|
|
114
|
+
className={
|
|
115
|
+
"flex-1 py-2.5 sm:py-1.5 rounded-lg border text-[12px] transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100 " +
|
|
116
|
+
(active
|
|
117
|
+
? "border-primary bg-base-300 text-base-content font-semibold"
|
|
118
|
+
: "border-base-content/15 bg-base-300 text-base-content/40 hover:border-base-content/30 hover:text-base-content/60")
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
{e.label}
|
|
122
|
+
</button>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div className="mb-5">
|
|
129
|
+
<div className="flex items-center justify-between mb-2">
|
|
130
|
+
<label htmlFor="claude-global-md" className="text-[12px] font-semibold text-base-content/40">Global CLAUDE.md</label>
|
|
131
|
+
<div className="text-[11px] text-base-content/30 font-mono">~/.claude/CLAUDE.md</div>
|
|
132
|
+
</div>
|
|
133
|
+
<textarea
|
|
134
|
+
id="claude-global-md"
|
|
135
|
+
value={claudeMd}
|
|
136
|
+
onChange={function (e) { handleClaudeMdChange(e.target.value); }}
|
|
137
|
+
placeholder={"# Global instructions for Claude\n\nAdd your global instructions here..."}
|
|
138
|
+
rows={14}
|
|
139
|
+
className="w-full px-3 py-2.5 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[12px] font-mono leading-relaxed resize-y focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<SaveFooter
|
|
144
|
+
dirty={save.dirty}
|
|
145
|
+
saving={save.saving}
|
|
146
|
+
saveState={save.saveState}
|
|
147
|
+
onSave={handleSave}
|
|
148
|
+
/>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { X, Plus, AlertTriangle } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
6
|
+
import { findDuplicateKeys } from "../../utils/findDuplicateKeys";
|
|
7
|
+
import type { ServerMessage, SettingsDataMessage } from "@lattice/shared";
|
|
8
|
+
|
|
9
|
+
interface EnvEntry {
|
|
10
|
+
id: string;
|
|
11
|
+
key: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function genId(): string {
|
|
16
|
+
return Math.random().toString(36).slice(2, 10);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Environment() {
|
|
20
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
21
|
+
var [entries, setEntries] = useState<EnvEntry[]>([]);
|
|
22
|
+
var save = useSaveState();
|
|
23
|
+
|
|
24
|
+
useEffect(function () {
|
|
25
|
+
function handleMessage(msg: ServerMessage) {
|
|
26
|
+
if (msg.type !== "settings:data") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
var data = msg as SettingsDataMessage;
|
|
30
|
+
var env = data.config.globalEnv ?? {};
|
|
31
|
+
|
|
32
|
+
if (save.saving) {
|
|
33
|
+
save.confirmSave();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var rows = Object.entries(env).map(function ([k, v]) {
|
|
37
|
+
return { id: genId(), key: k, value: v };
|
|
38
|
+
});
|
|
39
|
+
setEntries(rows);
|
|
40
|
+
if (!save.saving) {
|
|
41
|
+
save.resetFromServer();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
subscribe("settings:data", handleMessage);
|
|
46
|
+
send({ type: "settings:get" });
|
|
47
|
+
|
|
48
|
+
return function () {
|
|
49
|
+
unsubscribe("settings:data", handleMessage);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
function handleAddRow() {
|
|
54
|
+
setEntries(function (prev) {
|
|
55
|
+
return [...prev, { id: genId(), key: "", value: "" }];
|
|
56
|
+
});
|
|
57
|
+
save.markDirty();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleDelete(id: string) {
|
|
61
|
+
setEntries(function (prev) {
|
|
62
|
+
return prev.filter(function (e) { return e.id !== id; });
|
|
63
|
+
});
|
|
64
|
+
save.markDirty();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function handleKeyChange(id: string, key: string) {
|
|
68
|
+
setEntries(function (prev) {
|
|
69
|
+
return prev.map(function (e) {
|
|
70
|
+
return e.id === id ? { ...e, key } : e;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
save.markDirty();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleValueChange(id: string, value: string) {
|
|
77
|
+
setEntries(function (prev) {
|
|
78
|
+
return prev.map(function (e) {
|
|
79
|
+
return e.id === id ? { ...e, value } : e;
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
save.markDirty();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleSave() {
|
|
86
|
+
var env: Record<string, string> = {};
|
|
87
|
+
entries.forEach(function (e) {
|
|
88
|
+
if (e.key.trim()) {
|
|
89
|
+
env[e.key.trim()] = e.value;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
save.startSave();
|
|
93
|
+
send({
|
|
94
|
+
type: "settings:update",
|
|
95
|
+
settings: { globalEnv: env },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var duplicateKeys = useMemo(function () { return findDuplicateKeys(entries); }, [entries]);
|
|
100
|
+
var hasDuplicates = duplicateKeys.size > 0;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="py-2">
|
|
104
|
+
<div className="flex flex-col gap-3 sm:gap-1.5 mb-3">
|
|
105
|
+
{entries.length === 0 && (
|
|
106
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
107
|
+
No environment variables configured.
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
{entries.map(function (entry, idx) {
|
|
111
|
+
var isDupe = entry.key.trim() !== "" && duplicateKeys.has(entry.key.trim());
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
key={entry.id}
|
|
115
|
+
className="flex flex-col sm:grid sm:grid-cols-[1fr_1fr_auto] gap-1.5 sm:items-center"
|
|
116
|
+
>
|
|
117
|
+
<div className="relative">
|
|
118
|
+
<input
|
|
119
|
+
type="text"
|
|
120
|
+
value={entry.key}
|
|
121
|
+
onChange={function (e) { handleKeyChange(entry.id, e.target.value); }}
|
|
122
|
+
placeholder="VARIABLE_NAME"
|
|
123
|
+
aria-label={"Variable name for row " + (idx + 1)}
|
|
124
|
+
aria-invalid={isDupe}
|
|
125
|
+
className={
|
|
126
|
+
"w-full h-9 sm:h-7 px-3 bg-base-300 border rounded-xl text-base-content font-mono text-[12px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms] " +
|
|
127
|
+
(isDupe ? "border-warning" : "border-base-content/15")
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
130
|
+
{isDupe && (
|
|
131
|
+
<div className="flex items-center gap-1 mt-0.5 text-[10px] text-warning sm:absolute sm:-bottom-3.5 sm:left-0" role="alert">
|
|
132
|
+
<AlertTriangle size={9} />
|
|
133
|
+
Duplicate key
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="flex gap-1.5 items-center">
|
|
138
|
+
<input
|
|
139
|
+
type="text"
|
|
140
|
+
value={entry.value}
|
|
141
|
+
onChange={function (e) { handleValueChange(entry.id, e.target.value); }}
|
|
142
|
+
placeholder="value"
|
|
143
|
+
aria-label={"Value for " + (entry.key || "row " + (idx + 1))}
|
|
144
|
+
className="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]"
|
|
145
|
+
/>
|
|
146
|
+
<button
|
|
147
|
+
onClick={function () { handleDelete(entry.id); }}
|
|
148
|
+
aria-label={"Delete " + (entry.key || "row " + (idx + 1))}
|
|
149
|
+
title="Delete"
|
|
150
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error w-9 h-9 flex-shrink-0 sm:hidden focus-visible:ring-2 focus-visible:ring-primary"
|
|
151
|
+
>
|
|
152
|
+
<X size={14} />
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
<button
|
|
156
|
+
onClick={function () { handleDelete(entry.id); }}
|
|
157
|
+
aria-label={"Delete " + (entry.key || "row " + (idx + 1))}
|
|
158
|
+
title="Delete"
|
|
159
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error w-7 h-7 hidden sm:flex focus-visible:ring-2 focus-visible:ring-primary"
|
|
160
|
+
>
|
|
161
|
+
<X size={12} />
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<button
|
|
169
|
+
onClick={handleAddRow}
|
|
170
|
+
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"
|
|
171
|
+
>
|
|
172
|
+
<Plus size={12} />
|
|
173
|
+
Add Variable
|
|
174
|
+
</button>
|
|
175
|
+
|
|
176
|
+
<SaveFooter
|
|
177
|
+
dirty={save.dirty}
|
|
178
|
+
saving={save.saving}
|
|
179
|
+
saveState={save.saveState}
|
|
180
|
+
onSave={handleSave}
|
|
181
|
+
extraStatus={hasDuplicates ? "Duplicate keys will be merged" : undefined}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
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 { useWebSocket } from "../../hooks/useWebSocket";
|
|
6
|
+
import type { ServerMessage, SettingsDataMessage, McpServerConfig } from "@lattice/shared";
|
|
7
|
+
import {
|
|
8
|
+
type FormState,
|
|
9
|
+
emptyForm,
|
|
10
|
+
formFromConfig,
|
|
11
|
+
formToConfig,
|
|
12
|
+
typeBadge,
|
|
13
|
+
configSummary,
|
|
14
|
+
ServerForm,
|
|
15
|
+
} from "./mcp-shared";
|
|
16
|
+
|
|
17
|
+
export function GlobalMcp() {
|
|
18
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
19
|
+
var [servers, setServers] = useState<Record<string, McpServerConfig>>({});
|
|
20
|
+
var save = useSaveState();
|
|
21
|
+
|
|
22
|
+
var [adding, setAdding] = useState(false);
|
|
23
|
+
var [addForm, setAddForm] = useState<FormState>(emptyForm);
|
|
24
|
+
var [editingName, setEditingName] = useState<string | null>(null);
|
|
25
|
+
var [editForm, setEditForm] = useState<FormState>(emptyForm);
|
|
26
|
+
var [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
27
|
+
|
|
28
|
+
useEffect(function () {
|
|
29
|
+
function handleMessage(msg: ServerMessage) {
|
|
30
|
+
if (msg.type !== "settings:data") {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
var data = msg as SettingsDataMessage;
|
|
34
|
+
var mcpServers = data.mcpServers ?? {};
|
|
35
|
+
|
|
36
|
+
if (save.saving) {
|
|
37
|
+
save.confirmSave();
|
|
38
|
+
} else {
|
|
39
|
+
setServers({ ...mcpServers });
|
|
40
|
+
save.resetFromServer();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
subscribe("settings:data", handleMessage);
|
|
45
|
+
send({ type: "settings:get" });
|
|
46
|
+
|
|
47
|
+
return function () {
|
|
48
|
+
unsubscribe("settings:data", handleMessage);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
var entries = Object.entries(servers);
|
|
53
|
+
|
|
54
|
+
function handleAddSave() {
|
|
55
|
+
var name = addForm.name.trim();
|
|
56
|
+
if (!name) return;
|
|
57
|
+
var next = { ...servers, [name]: formToConfig(addForm) };
|
|
58
|
+
setServers(next);
|
|
59
|
+
setAdding(false);
|
|
60
|
+
setAddForm(emptyForm());
|
|
61
|
+
save.markDirty();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleEditStart(name: string) {
|
|
65
|
+
setEditingName(name);
|
|
66
|
+
setEditForm(formFromConfig(name, servers[name]));
|
|
67
|
+
setAdding(false);
|
|
68
|
+
setConfirmDelete(null);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleEditSave() {
|
|
72
|
+
if (!editingName) return;
|
|
73
|
+
var newName = editForm.name.trim();
|
|
74
|
+
if (!newName) return;
|
|
75
|
+
var next = { ...servers };
|
|
76
|
+
if (newName !== editingName) {
|
|
77
|
+
delete next[editingName];
|
|
78
|
+
}
|
|
79
|
+
next[newName] = formToConfig(editForm);
|
|
80
|
+
setServers(next);
|
|
81
|
+
setEditingName(null);
|
|
82
|
+
save.markDirty();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleDelete(name: string) {
|
|
86
|
+
if (confirmDelete !== name) {
|
|
87
|
+
setConfirmDelete(name);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
var next = { ...servers };
|
|
91
|
+
delete next[name];
|
|
92
|
+
setServers(next);
|
|
93
|
+
if (editingName === name) setEditingName(null);
|
|
94
|
+
setConfirmDelete(null);
|
|
95
|
+
save.markDirty();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleSave() {
|
|
99
|
+
save.startSave();
|
|
100
|
+
send({
|
|
101
|
+
type: "settings:update",
|
|
102
|
+
settings: { mcpServers: servers } as unknown as import("@lattice/shared").SettingsUpdateMessage["settings"],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
var existingNamesForAdd = new Set(Object.keys(servers));
|
|
107
|
+
var existingNamesForEdit = new Set(
|
|
108
|
+
Object.keys(servers).filter(function (n) { return n !== editingName; })
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="py-2">
|
|
113
|
+
{entries.length === 0 && !adding && (
|
|
114
|
+
<div className="py-4 text-center text-[13px] text-base-content/30 mb-3">
|
|
115
|
+
No global MCP servers configured.
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<div className="flex flex-col gap-2 mb-3">
|
|
120
|
+
{entries.map(function ([name, config]) {
|
|
121
|
+
if (editingName === name) {
|
|
122
|
+
return (
|
|
123
|
+
<ServerForm
|
|
124
|
+
key={name}
|
|
125
|
+
form={editForm}
|
|
126
|
+
setForm={setEditForm}
|
|
127
|
+
onSave={handleEditSave}
|
|
128
|
+
onCancel={function () { setEditingName(null); }}
|
|
129
|
+
existingNames={existingNamesForEdit}
|
|
130
|
+
idPrefix="global-mcp-edit"
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
var isConfirming = confirmDelete === name;
|
|
135
|
+
return (
|
|
136
|
+
<div
|
|
137
|
+
key={name}
|
|
138
|
+
className="flex items-center gap-3 px-3 py-2 rounded-xl bg-base-300 border border-base-content/15"
|
|
139
|
+
>
|
|
140
|
+
<span className="font-mono text-[12px] text-base-content font-semibold flex-shrink-0">{name}</span>
|
|
141
|
+
{typeBadge(config)}
|
|
142
|
+
<div className="flex-1 min-w-0">{configSummary(config)}</div>
|
|
143
|
+
{isConfirming ? (
|
|
144
|
+
<div className="flex gap-1.5 flex-shrink-0">
|
|
145
|
+
<button
|
|
146
|
+
onClick={function () { handleDelete(name); }}
|
|
147
|
+
className="btn btn-error btn-xs"
|
|
148
|
+
>
|
|
149
|
+
Confirm
|
|
150
|
+
</button>
|
|
151
|
+
<button
|
|
152
|
+
onClick={function () { setConfirmDelete(null); }}
|
|
153
|
+
className="btn btn-ghost btn-xs"
|
|
154
|
+
>
|
|
155
|
+
Cancel
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<div className="flex gap-1 flex-shrink-0">
|
|
160
|
+
<button
|
|
161
|
+
onClick={function () { handleEditStart(name); }}
|
|
162
|
+
aria-label={"Edit " + name}
|
|
163
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-primary focus-visible:ring-2 focus-visible:ring-primary"
|
|
164
|
+
>
|
|
165
|
+
<Pencil size={12} />
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
onClick={function () { handleDelete(name); }}
|
|
169
|
+
aria-label={"Delete " + name}
|
|
170
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error focus-visible:ring-2 focus-visible:ring-primary"
|
|
171
|
+
>
|
|
172
|
+
<Trash2 size={12} />
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
})}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{adding && (
|
|
182
|
+
<div className="mb-3">
|
|
183
|
+
<ServerForm
|
|
184
|
+
form={addForm}
|
|
185
|
+
setForm={setAddForm}
|
|
186
|
+
onSave={handleAddSave}
|
|
187
|
+
onCancel={function () { setAdding(false); setAddForm(emptyForm()); }}
|
|
188
|
+
existingNames={existingNamesForAdd}
|
|
189
|
+
idPrefix="global-mcp-add"
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{!adding && (
|
|
195
|
+
<button
|
|
196
|
+
onClick={function () { setAdding(true); setEditingName(null); setConfirmDelete(null); }}
|
|
197
|
+
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"
|
|
198
|
+
>
|
|
199
|
+
<Plus size={12} />
|
|
200
|
+
Add Server
|
|
201
|
+
</button>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
3
|
+
import { SkillMarketplace } from "./SkillMarketplace";
|
|
4
|
+
import { SkillItem, SkillActions, SkillViewModal } from "./skill-shared";
|
|
5
|
+
import type { ServerMessage, SkillInfo, SettingsDataMessage } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
export function GlobalSkills() {
|
|
8
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
9
|
+
var [skills, setSkills] = useState<SkillInfo[]>([]);
|
|
10
|
+
var [loaded, setLoaded] = useState(false);
|
|
11
|
+
var [viewContent, setViewContent] = useState<{ path: string; content: string } | null>(null);
|
|
12
|
+
var [deletingPath, setDeletingPath] = useState<string | null>(null);
|
|
13
|
+
var [updatingName, setUpdatingName] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(function () {
|
|
16
|
+
function handleData(msg: ServerMessage) {
|
|
17
|
+
if (msg.type !== "settings:data") return;
|
|
18
|
+
var data = msg as SettingsDataMessage;
|
|
19
|
+
setSkills(data.globalSkills ?? []);
|
|
20
|
+
setLoaded(true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function handleViewResult(msg: ServerMessage) {
|
|
24
|
+
if (msg.type !== "skills:view_result") return;
|
|
25
|
+
var data = msg as { type: "skills:view_result"; path: string; content: string };
|
|
26
|
+
setViewContent({ path: data.path, content: data.content });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handleDeleteResult(msg: ServerMessage) {
|
|
30
|
+
if (msg.type !== "skills:delete_result") return;
|
|
31
|
+
setDeletingPath(null);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleInstallResult(msg: ServerMessage) {
|
|
35
|
+
if (msg.type !== "skills:install_result") return;
|
|
36
|
+
setUpdatingName(null);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
subscribe("settings:data", handleData);
|
|
40
|
+
subscribe("skills:view_result", handleViewResult);
|
|
41
|
+
subscribe("skills:delete_result", handleDeleteResult);
|
|
42
|
+
subscribe("skills:install_result", handleInstallResult);
|
|
43
|
+
send({ type: "settings:get" });
|
|
44
|
+
|
|
45
|
+
return function () {
|
|
46
|
+
unsubscribe("settings:data", handleData);
|
|
47
|
+
unsubscribe("skills:view_result", handleViewResult);
|
|
48
|
+
unsubscribe("skills:delete_result", handleDeleteResult);
|
|
49
|
+
unsubscribe("skills:install_result", handleInstallResult);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
function handleView(skill: SkillInfo) {
|
|
54
|
+
send({ type: "skills:view", path: skill.path } as any);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleDelete(skill: SkillInfo) {
|
|
58
|
+
setDeletingPath(skill.path);
|
|
59
|
+
send({ type: "skills:delete", path: skill.path } as any);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleUpdate(skill: SkillInfo) {
|
|
63
|
+
var source = guessSource(skill);
|
|
64
|
+
if (!source) return;
|
|
65
|
+
setUpdatingName(skill.name);
|
|
66
|
+
send({ type: "skills:update", source: source } as any);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function guessSource(skill: SkillInfo): string | null {
|
|
70
|
+
var pathParts = skill.path.split("/");
|
|
71
|
+
var skillsIdx = pathParts.lastIndexOf("skills");
|
|
72
|
+
if (skillsIdx >= 0 && skillsIdx + 1 < pathParts.length) {
|
|
73
|
+
return pathParts[skillsIdx + 1];
|
|
74
|
+
}
|
|
75
|
+
return skill.name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!loaded) {
|
|
79
|
+
return <div className="text-[13px] text-base-content/40 py-4">Loading...</div>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="py-2 space-y-6">
|
|
84
|
+
<div>
|
|
85
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Installed Skills</div>
|
|
86
|
+
{skills.length === 0 ? (
|
|
87
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
88
|
+
No global skills installed.
|
|
89
|
+
</div>
|
|
90
|
+
) : (
|
|
91
|
+
<div className="space-y-2">
|
|
92
|
+
{skills.map(function (skill) {
|
|
93
|
+
return (
|
|
94
|
+
<SkillItem
|
|
95
|
+
key={skill.path}
|
|
96
|
+
skill={skill}
|
|
97
|
+
onClick={function () { handleView(skill); }}
|
|
98
|
+
actions={
|
|
99
|
+
<SkillActions
|
|
100
|
+
skill={skill}
|
|
101
|
+
onDelete={function () { handleDelete(skill); }}
|
|
102
|
+
onUpdate={function () { handleUpdate(skill); }}
|
|
103
|
+
isDeleting={deletingPath === skill.path}
|
|
104
|
+
isUpdating={updatingName === skill.name}
|
|
105
|
+
/>
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
})}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<SkillMarketplace defaultScope="global" />
|
|
115
|
+
|
|
116
|
+
{viewContent && (
|
|
117
|
+
<SkillViewModal
|
|
118
|
+
path={viewContent.path}
|
|
119
|
+
content={viewContent.content}
|
|
120
|
+
onClose={function () { setViewContent(null); }}
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|