@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,145 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Plus, CircleDot, Circle } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useMesh } from "../../hooks/useMesh";
|
|
5
|
+
import { PairingDialog } from "../mesh/PairingDialog";
|
|
6
|
+
import type { NodeInfo } from "@lattice/shared";
|
|
7
|
+
|
|
8
|
+
interface NodeRowProps {
|
|
9
|
+
node: NodeInfo;
|
|
10
|
+
onUnpair: (nodeId: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function NodeRow(props: NodeRowProps) {
|
|
14
|
+
var [confirming, setConfirming] = useState(false);
|
|
15
|
+
|
|
16
|
+
function handleUnpair() {
|
|
17
|
+
if (!confirming) {
|
|
18
|
+
setConfirming(true);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
props.onUnpair(props.node.id);
|
|
22
|
+
setConfirming(false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex items-center gap-3 p-3 sm:p-2.5 px-3.5 rounded-lg border border-base-content/15 bg-base-300 mb-2">
|
|
27
|
+
{props.node.online ? (
|
|
28
|
+
<CircleDot size={10} className="text-success flex-shrink-0" aria-label="Online" />
|
|
29
|
+
) : (
|
|
30
|
+
<Circle size={10} className="text-base-content/30 flex-shrink-0" aria-label="Offline" />
|
|
31
|
+
)}
|
|
32
|
+
|
|
33
|
+
<div className="flex-1 min-w-0">
|
|
34
|
+
<div className="text-[13px] font-medium text-base-content truncate">
|
|
35
|
+
{props.node.name}
|
|
36
|
+
{props.node.isLocal && (
|
|
37
|
+
<span className="ml-1.5 text-[10px] font-semibold text-primary uppercase tracking-[0.06em]">
|
|
38
|
+
local
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="text-[11px] text-base-content/40 truncate">
|
|
43
|
+
{props.node.address}:{props.node.port}
|
|
44
|
+
{!props.node.online && (
|
|
45
|
+
<span className="ml-2 text-base-content/30 italic">offline</span>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{!props.node.isLocal && (
|
|
51
|
+
<div className="flex gap-1.5 flex-shrink-0">
|
|
52
|
+
{confirming ? (
|
|
53
|
+
<>
|
|
54
|
+
<button
|
|
55
|
+
onClick={handleUnpair}
|
|
56
|
+
className="btn btn-error btn-xs"
|
|
57
|
+
>
|
|
58
|
+
Confirm
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
onClick={function () { setConfirming(false); }}
|
|
62
|
+
className="btn btn-ghost btn-xs"
|
|
63
|
+
>
|
|
64
|
+
Cancel
|
|
65
|
+
</button>
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleUnpair}
|
|
70
|
+
className="btn btn-ghost btn-xs border border-base-content/20 hover:btn-error hover:border-error"
|
|
71
|
+
>
|
|
72
|
+
Unpair
|
|
73
|
+
</button>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function MeshStatus() {
|
|
82
|
+
var ws = useWebSocket();
|
|
83
|
+
var { nodes } = useMesh();
|
|
84
|
+
var [pairingOpen, setPairingOpen] = useState(false);
|
|
85
|
+
|
|
86
|
+
function handleUnpair(nodeId: string) {
|
|
87
|
+
ws.send({ type: "mesh:unpair", nodeId });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var localNode = nodes.find(function (n) { return n.isLocal; });
|
|
91
|
+
var remoteNodes = nodes.filter(function (n) { return !n.isLocal; });
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="py-2">
|
|
95
|
+
<div className="mb-5">
|
|
96
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2 tracking-[0.06em]">
|
|
97
|
+
This Node
|
|
98
|
+
</div>
|
|
99
|
+
{localNode ? (
|
|
100
|
+
<NodeRow node={localNode} onUnpair={handleUnpair} />
|
|
101
|
+
) : (
|
|
102
|
+
<div className="text-[12px] text-base-content/40 italic">
|
|
103
|
+
Waiting for node info...
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div className="mb-5">
|
|
109
|
+
<div className="flex items-center justify-between mb-2">
|
|
110
|
+
<div className="text-[12px] font-semibold text-base-content/40 tracking-[0.06em]">
|
|
111
|
+
Paired Nodes
|
|
112
|
+
</div>
|
|
113
|
+
<button
|
|
114
|
+
onClick={function () { setPairingOpen(true); }}
|
|
115
|
+
className="btn btn-primary btn-sm sm:btn-xs gap-1"
|
|
116
|
+
>
|
|
117
|
+
<Plus size={10} />
|
|
118
|
+
Pair New Node
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{remoteNodes.length === 0 ? (
|
|
123
|
+
<div className="p-4 rounded-lg border border-dashed border-base-content/15 text-center text-[12px] text-base-content/40 italic">
|
|
124
|
+
No paired nodes yet.
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
remoteNodes.map(function (node) {
|
|
128
|
+
return (
|
|
129
|
+
<NodeRow
|
|
130
|
+
key={node.id}
|
|
131
|
+
node={node}
|
|
132
|
+
onUnpair={handleUnpair}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
})
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<PairingDialog
|
|
140
|
+
isOpen={pairingOpen}
|
|
141
|
+
onClose={function () { setPairingOpen(false); }}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useSidebar } from "../../hooks/useSidebar";
|
|
2
|
+
import { Menu } from "lucide-react";
|
|
3
|
+
import { Appearance } from "./Appearance";
|
|
4
|
+
import { ClaudeSettings } from "./ClaudeSettings";
|
|
5
|
+
import { Environment } from "./Environment";
|
|
6
|
+
import { MeshStatus } from "./MeshStatus";
|
|
7
|
+
import { GlobalMcp } from "./GlobalMcp";
|
|
8
|
+
import { GlobalSkills } from "./GlobalSkills";
|
|
9
|
+
import type { SettingsSection } from "../../stores/sidebar";
|
|
10
|
+
|
|
11
|
+
var SECTION_CONFIG: Record<string, { title: string }> = {
|
|
12
|
+
appearance: { title: "Appearance" },
|
|
13
|
+
claude: { title: "Claude Settings" },
|
|
14
|
+
environment: { title: "Environment" },
|
|
15
|
+
mcp: { title: "MCP Servers" },
|
|
16
|
+
skills: { title: "Skills" },
|
|
17
|
+
nodes: { title: "Mesh Nodes" },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function renderSection(section: SettingsSection) {
|
|
21
|
+
if (section === "appearance") return <Appearance />;
|
|
22
|
+
if (section === "claude") return <ClaudeSettings />;
|
|
23
|
+
if (section === "environment") return <Environment />;
|
|
24
|
+
if (section === "mcp") return <GlobalMcp />;
|
|
25
|
+
if (section === "skills") return <GlobalSkills />;
|
|
26
|
+
if (section === "nodes") return <MeshStatus />;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function SettingsView() {
|
|
31
|
+
var { activeView, toggleDrawer } = useSidebar();
|
|
32
|
+
|
|
33
|
+
if (activeView.type !== "settings") {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var section = activeView.section;
|
|
38
|
+
var config = SECTION_CONFIG[section];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex-1 overflow-auto px-4 sm:px-8 py-4 sm:py-6 max-w-3xl">
|
|
42
|
+
{config && (
|
|
43
|
+
<div className="mb-6 flex items-center gap-3">
|
|
44
|
+
<button
|
|
45
|
+
className="btn btn-ghost btn-sm btn-square lg:hidden"
|
|
46
|
+
aria-label="Toggle sidebar"
|
|
47
|
+
onClick={toggleDrawer}
|
|
48
|
+
>
|
|
49
|
+
<Menu size={18} />
|
|
50
|
+
</button>
|
|
51
|
+
<h1 className="text-lg font-mono font-bold text-base-content">{config.title}</h1>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
{renderSection(section)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Search, Download, ChevronDown, Loader2 } from "lucide-react";
|
|
3
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
|
+
import { useProjects } from "../../hooks/useProjects";
|
|
5
|
+
import type { ServerMessage, MarketplaceSkill } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
interface SkillMarketplaceProps {
|
|
8
|
+
defaultScope?: "global" | "project";
|
|
9
|
+
defaultProjectSlug?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SkillMarketplace({ defaultScope, defaultProjectSlug }: SkillMarketplaceProps) {
|
|
13
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
14
|
+
var { projects } = useProjects();
|
|
15
|
+
var [query, setQuery] = useState("");
|
|
16
|
+
var [results, setResults] = useState<MarketplaceSkill[]>([]);
|
|
17
|
+
var [count, setCount] = useState(0);
|
|
18
|
+
var [error, setError] = useState<string | null>(null);
|
|
19
|
+
var [searching, setSearching] = useState(false);
|
|
20
|
+
var [installing, setInstalling] = useState<string | null>(null);
|
|
21
|
+
var [scopeOpen, setScopeOpen] = useState<string | null>(null);
|
|
22
|
+
var [lastInstallScope, setLastInstallScope] = useState<"global" | "project">("global");
|
|
23
|
+
var [lastInstallProjectSlug, setLastInstallProjectSlug] = useState<string | undefined>(undefined);
|
|
24
|
+
var debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(function () {
|
|
27
|
+
function handleResults(msg: ServerMessage) {
|
|
28
|
+
if (msg.type !== "skills:search_results") return;
|
|
29
|
+
var data = msg as ServerMessage & { query: string; skills: MarketplaceSkill[]; count: number; error?: string };
|
|
30
|
+
setResults(data.skills);
|
|
31
|
+
setCount(data.count);
|
|
32
|
+
setError(data.error ?? null);
|
|
33
|
+
setSearching(false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleInstallResult(msg: ServerMessage) {
|
|
37
|
+
if (msg.type !== "skills:install_result") return;
|
|
38
|
+
var result = msg as { type: "skills:install_result"; success: boolean };
|
|
39
|
+
if (result.success && lastInstallScope === "project" && lastInstallProjectSlug) {
|
|
40
|
+
send({ type: "project-settings:get", projectSlug: lastInstallProjectSlug });
|
|
41
|
+
}
|
|
42
|
+
setInstalling(null);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
subscribe("skills:search_results", handleResults);
|
|
46
|
+
subscribe("skills:install_result", handleInstallResult);
|
|
47
|
+
|
|
48
|
+
return function () {
|
|
49
|
+
unsubscribe("skills:search_results", handleResults);
|
|
50
|
+
unsubscribe("skills:install_result", handleInstallResult);
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
useEffect(function () {
|
|
55
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
56
|
+
var trimmed = query.trim();
|
|
57
|
+
if (!trimmed) {
|
|
58
|
+
setResults([]);
|
|
59
|
+
setCount(0);
|
|
60
|
+
setError(null);
|
|
61
|
+
setSearching(false);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
setSearching(true);
|
|
65
|
+
debounceRef.current = setTimeout(function () {
|
|
66
|
+
send({ type: "skills:search", query: trimmed });
|
|
67
|
+
}, 300);
|
|
68
|
+
}, [query]);
|
|
69
|
+
|
|
70
|
+
function handleInstall(skill: MarketplaceSkill, scope: "global" | "project", projectSlug?: string) {
|
|
71
|
+
setInstalling(skill.id);
|
|
72
|
+
setScopeOpen(null);
|
|
73
|
+
setLastInstallScope(scope);
|
|
74
|
+
setLastInstallProjectSlug(projectSlug);
|
|
75
|
+
send({
|
|
76
|
+
type: "skills:install",
|
|
77
|
+
source: skill.source,
|
|
78
|
+
scope: scope,
|
|
79
|
+
projectSlug: projectSlug,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatInstalls(n: number): string {
|
|
84
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
|
|
85
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + "K";
|
|
86
|
+
return String(n);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Skill Marketplace</div>
|
|
92
|
+
<div className="relative mb-3">
|
|
93
|
+
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/30" />
|
|
94
|
+
<input
|
|
95
|
+
type="text"
|
|
96
|
+
value={query}
|
|
97
|
+
onChange={function (e) { setQuery(e.target.value); }}
|
|
98
|
+
placeholder="Search skills.sh..."
|
|
99
|
+
className="w-full h-9 sm:h-7 pl-9 pr-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]"
|
|
100
|
+
/>
|
|
101
|
+
{searching && (
|
|
102
|
+
<Loader2 size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/30 animate-spin" />
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{error && (
|
|
107
|
+
<div className="text-[12px] text-warning mb-2">{error}</div>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{results.length > 0 && (
|
|
111
|
+
<div className="flex flex-col gap-1.5">
|
|
112
|
+
{results.slice(0, 15).map(function (skill) {
|
|
113
|
+
var isInstalling = installing === skill.id;
|
|
114
|
+
var isScopeOpen = scopeOpen === skill.id;
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
key={skill.id}
|
|
118
|
+
className="flex items-center gap-3 px-3 py-2 rounded-xl bg-base-300 border border-base-content/15"
|
|
119
|
+
>
|
|
120
|
+
<div className="flex-1 min-w-0">
|
|
121
|
+
<div className="text-[12px] font-semibold text-base-content truncate">{skill.name}</div>
|
|
122
|
+
<div className="text-[11px] font-mono text-base-content/30 truncate">{skill.source}</div>
|
|
123
|
+
</div>
|
|
124
|
+
<span className="text-[10px] text-base-content/30 font-mono flex-shrink-0">
|
|
125
|
+
{formatInstalls(skill.installs)}
|
|
126
|
+
</span>
|
|
127
|
+
<div className="relative flex-shrink-0">
|
|
128
|
+
{isInstalling ? (
|
|
129
|
+
<Loader2 size={14} className="text-primary animate-spin" />
|
|
130
|
+
) : (
|
|
131
|
+
<button
|
|
132
|
+
onClick={function () { setScopeOpen(isScopeOpen ? null : skill.id); }}
|
|
133
|
+
className="btn btn-ghost btn-xs text-[11px] text-base-content/50 hover:text-primary gap-1"
|
|
134
|
+
>
|
|
135
|
+
<Download size={12} />
|
|
136
|
+
Install
|
|
137
|
+
<ChevronDown size={10} />
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
{isScopeOpen && (
|
|
141
|
+
<div className="absolute right-0 top-full mt-1 z-50 bg-base-200 border border-base-content/15 rounded-xl shadow-lg py-1 min-w-[160px]">
|
|
142
|
+
<button
|
|
143
|
+
onClick={function () { handleInstall(skill, "global"); }}
|
|
144
|
+
className="w-full text-left px-3 py-1.5 text-[12px] text-base-content/60 hover:bg-base-content/5 hover:text-base-content"
|
|
145
|
+
>
|
|
146
|
+
Install globally
|
|
147
|
+
</button>
|
|
148
|
+
{projects.map(function (p) {
|
|
149
|
+
return (
|
|
150
|
+
<button
|
|
151
|
+
key={p.slug}
|
|
152
|
+
onClick={function () { handleInstall(skill, "project", p.slug); }}
|
|
153
|
+
className="w-full text-left px-3 py-1.5 text-[12px] text-base-content/60 hover:bg-base-content/5 hover:text-base-content truncate"
|
|
154
|
+
>
|
|
155
|
+
Install to {p.title}
|
|
156
|
+
</button>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{query.trim() && !searching && results.length === 0 && !error && (
|
|
169
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
170
|
+
No skills found.
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { McpServerConfig } from "@lattice/shared";
|
|
2
|
+
|
|
3
|
+
export type ServerType = "stdio" | "http" | "sse";
|
|
4
|
+
|
|
5
|
+
export interface FormState {
|
|
6
|
+
name: string;
|
|
7
|
+
serverType: ServerType;
|
|
8
|
+
command: string;
|
|
9
|
+
args: string;
|
|
10
|
+
env: string;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function emptyForm(): FormState {
|
|
15
|
+
return { name: "", serverType: "stdio", command: "", args: "", env: "", url: "" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formFromConfig(name: string, config: McpServerConfig): FormState {
|
|
19
|
+
var serverType: ServerType = config.type === "http" ? "http" : config.type === "sse" ? "sse" : "stdio";
|
|
20
|
+
if (serverType === "stdio" && "command" in config) {
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
serverType,
|
|
24
|
+
command: config.command,
|
|
25
|
+
args: (config.args ?? []).join(", "),
|
|
26
|
+
env: Object.entries(config.env ?? {}).map(function ([k, v]) { return k + "=" + v; }).join("\n"),
|
|
27
|
+
url: "",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if ("url" in config) {
|
|
31
|
+
return { name, serverType, command: "", args: "", env: "", url: config.url };
|
|
32
|
+
}
|
|
33
|
+
return { ...emptyForm(), name, serverType };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formToConfig(form: FormState): McpServerConfig {
|
|
37
|
+
if (form.serverType === "stdio") {
|
|
38
|
+
var argsArr = form.args.trim()
|
|
39
|
+
? form.args.split(",").map(function (s) { return s.trim(); }).filter(Boolean)
|
|
40
|
+
: [];
|
|
41
|
+
var envObj: Record<string, string> = {};
|
|
42
|
+
form.env.split("\n").forEach(function (line) {
|
|
43
|
+
var idx = line.indexOf("=");
|
|
44
|
+
if (idx > 0) {
|
|
45
|
+
envObj[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
var result: McpServerConfig = { type: "stdio", command: form.command.trim(), args: argsArr };
|
|
49
|
+
if (Object.keys(envObj).length > 0) {
|
|
50
|
+
(result as { env?: Record<string, string> }).env = envObj;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
return { type: form.serverType, url: form.url.trim() };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function typeBadge(config: McpServerConfig) {
|
|
58
|
+
var t = config.type === "http" ? "http" : config.type === "sse" ? "sse" : "stdio";
|
|
59
|
+
return (
|
|
60
|
+
<span className="px-1.5 py-0.5 rounded-lg text-[10px] uppercase tracking-wider font-mono bg-base-content/10 text-base-content/40 flex-shrink-0">
|
|
61
|
+
{t}
|
|
62
|
+
</span>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function configSummary(config: McpServerConfig) {
|
|
67
|
+
if (config.type === "http" || config.type === "sse") {
|
|
68
|
+
return <div className="font-mono text-[11px] text-base-content/40 truncate">{config.url}</div>;
|
|
69
|
+
}
|
|
70
|
+
var cmd = config.command + ((config.args?.length ?? 0) > 0 ? " " + config.args!.join(" ") : "");
|
|
71
|
+
return <div className="font-mono text-[11px] text-base-content/40 truncate">{cmd}</div>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function ServerForm({
|
|
75
|
+
form,
|
|
76
|
+
setForm,
|
|
77
|
+
onSave,
|
|
78
|
+
onCancel,
|
|
79
|
+
existingNames,
|
|
80
|
+
idPrefix,
|
|
81
|
+
}: {
|
|
82
|
+
form: FormState;
|
|
83
|
+
setForm: (f: FormState) => void;
|
|
84
|
+
onSave: () => void;
|
|
85
|
+
onCancel: () => void;
|
|
86
|
+
existingNames: Set<string>;
|
|
87
|
+
idPrefix: string;
|
|
88
|
+
}) {
|
|
89
|
+
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]";
|
|
90
|
+
var nameConflict = form.name.trim() !== "" && existingNames.has(form.name.trim());
|
|
91
|
+
var canSave = form.name.trim() !== "" && !nameConflict &&
|
|
92
|
+
(form.serverType === "stdio" ? form.command.trim() !== "" : form.url.trim() !== "");
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="border border-base-content/15 rounded-xl bg-base-300/40 p-4 flex flex-col gap-3">
|
|
96
|
+
<div>
|
|
97
|
+
<label htmlFor={idPrefix + "-name"} className="block text-[11px] text-base-content/40 mb-1">Server Name</label>
|
|
98
|
+
<input
|
|
99
|
+
id={idPrefix + "-name"}
|
|
100
|
+
type="text"
|
|
101
|
+
value={form.name}
|
|
102
|
+
onChange={function (e) { setForm({ ...form, name: e.target.value }); }}
|
|
103
|
+
placeholder="my-server"
|
|
104
|
+
className={"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] " + (nameConflict ? "border-warning" : "border-base-content/15")}
|
|
105
|
+
/>
|
|
106
|
+
{nameConflict && (
|
|
107
|
+
<div className="text-[10px] text-warning mt-0.5" role="alert">Name already in use</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<fieldset>
|
|
112
|
+
<legend className="block text-[11px] text-base-content/40 mb-1.5">Type</legend>
|
|
113
|
+
<div className="flex gap-3">
|
|
114
|
+
{(["stdio", "http", "sse"] as ServerType[]).map(function (t) {
|
|
115
|
+
return (
|
|
116
|
+
<label key={t} className="flex items-center gap-1.5 cursor-pointer">
|
|
117
|
+
<input
|
|
118
|
+
type="radio"
|
|
119
|
+
name={idPrefix + "-type"}
|
|
120
|
+
value={t}
|
|
121
|
+
checked={form.serverType === t}
|
|
122
|
+
onChange={function () { setForm({ ...form, serverType: t }); }}
|
|
123
|
+
className="radio radio-xs radio-primary"
|
|
124
|
+
/>
|
|
125
|
+
<span className="text-[12px] font-mono text-base-content uppercase">{t}</span>
|
|
126
|
+
</label>
|
|
127
|
+
);
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
</fieldset>
|
|
131
|
+
|
|
132
|
+
{form.serverType === "stdio" && (
|
|
133
|
+
<>
|
|
134
|
+
<div>
|
|
135
|
+
<label htmlFor={idPrefix + "-command"} className="block text-[11px] text-base-content/40 mb-1">Command</label>
|
|
136
|
+
<input
|
|
137
|
+
id={idPrefix + "-command"}
|
|
138
|
+
type="text"
|
|
139
|
+
value={form.command}
|
|
140
|
+
onChange={function (e) { setForm({ ...form, command: e.target.value }); }}
|
|
141
|
+
placeholder="npx"
|
|
142
|
+
className={inputClass}
|
|
143
|
+
/>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<label htmlFor={idPrefix + "-args"} className="block text-[11px] text-base-content/40 mb-1">Args (comma-separated)</label>
|
|
147
|
+
<input
|
|
148
|
+
id={idPrefix + "-args"}
|
|
149
|
+
type="text"
|
|
150
|
+
value={form.args}
|
|
151
|
+
onChange={function (e) { setForm({ ...form, args: e.target.value }); }}
|
|
152
|
+
placeholder="-y, @modelcontextprotocol/server-filesystem"
|
|
153
|
+
className={inputClass}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
156
|
+
<div>
|
|
157
|
+
<label htmlFor={idPrefix + "-env"} className="block text-[11px] text-base-content/40 mb-1">Environment (KEY=value, one per line)</label>
|
|
158
|
+
<textarea
|
|
159
|
+
id={idPrefix + "-env"}
|
|
160
|
+
value={form.env}
|
|
161
|
+
onChange={function (e) { setForm({ ...form, env: e.target.value }); }}
|
|
162
|
+
placeholder={"API_KEY=abc123\nDEBUG=true"}
|
|
163
|
+
rows={3}
|
|
164
|
+
className="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"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{(form.serverType === "http" || form.serverType === "sse") && (
|
|
171
|
+
<div>
|
|
172
|
+
<label htmlFor={idPrefix + "-url"} className="block text-[11px] text-base-content/40 mb-1">URL</label>
|
|
173
|
+
<input
|
|
174
|
+
id={idPrefix + "-url"}
|
|
175
|
+
type="text"
|
|
176
|
+
value={form.url}
|
|
177
|
+
onChange={function (e) { setForm({ ...form, url: e.target.value }); }}
|
|
178
|
+
placeholder="http://localhost:3000/mcp"
|
|
179
|
+
className={inputClass}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
<div className="flex items-center gap-2 justify-end pt-1">
|
|
185
|
+
<button onClick={onCancel} className="btn btn-ghost btn-sm text-[12px]">
|
|
186
|
+
Cancel
|
|
187
|
+
</button>
|
|
188
|
+
<button onClick={onSave} disabled={!canSave} className={"btn btn-primary btn-sm text-[12px]" + (!canSave ? " opacity-50 cursor-not-allowed" : "")}>
|
|
189
|
+
Save
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|