@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,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
+ }