@cryptiklemur/lattice 1.25.0 → 1.26.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/README.md +94 -84
- package/client/src/components/project-settings/ProjectPlugins.tsx +117 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/GlobalPlugins.tsx +801 -0
- package/client/src/components/settings/SettingsView.tsx +3 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +3 -1
- package/client/src/stores/sidebar.ts +6 -3
- package/client/src/stores/workspace.ts +9 -4
- package/package.json +1 -1
- package/server/src/daemon.ts +1 -0
- package/server/src/handlers/plugins.ts +658 -0
- package/server/src/handlers/project-settings.ts +6 -0
- package/server/src/project/context-breakdown.ts +12 -0
- package/server/src/project/sdk-bridge.ts +6 -0
- package/shared/src/messages.ts +123 -2
- package/shared/src/models.ts +59 -0
- package/shared/src/project-settings.ts +2 -1
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Blocks, Trash2, RefreshCw, Loader2, X, Download, Search,
|
|
4
|
+
ChevronRight, Package, Webhook, ScrollText, Puzzle, ExternalLink,
|
|
5
|
+
AlertTriangle,
|
|
6
|
+
} from "lucide-react";
|
|
7
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
8
|
+
import type {
|
|
9
|
+
ServerMessage,
|
|
10
|
+
PluginInfo,
|
|
11
|
+
PluginDetails,
|
|
12
|
+
PluginError,
|
|
13
|
+
PluginMarketplaceInfo,
|
|
14
|
+
MarketplacePluginEntry,
|
|
15
|
+
} from "@lattice/shared";
|
|
16
|
+
|
|
17
|
+
export function GlobalPlugins() {
|
|
18
|
+
var { send, subscribe, unsubscribe } = useWebSocket();
|
|
19
|
+
var [plugins, setPlugins] = useState<PluginInfo[]>([]);
|
|
20
|
+
var [marketplaces, setMarketplaces] = useState<PluginMarketplaceInfo[]>([]);
|
|
21
|
+
var [loaded, setLoaded] = useState(false);
|
|
22
|
+
var [detailPlugin, setDetailPlugin] = useState<PluginDetails | null>(null);
|
|
23
|
+
var [searchQuery, setSearchQuery] = useState("");
|
|
24
|
+
var [searchResults, setSearchResults] = useState<MarketplacePluginEntry[]>([]);
|
|
25
|
+
var [searching, setSearching] = useState(false);
|
|
26
|
+
var [hasSearched, setHasSearched] = useState(false);
|
|
27
|
+
var [installingKey, setInstallingKey] = useState<string | null>(null);
|
|
28
|
+
var [uninstallingKey, setUninstallingKey] = useState<string | null>(null);
|
|
29
|
+
var [updatingKey, setUpdatingKey] = useState<string | null>(null);
|
|
30
|
+
var [confirmUninstall, setConfirmUninstall] = useState<string | null>(null);
|
|
31
|
+
var [actionMessage, setActionMessage] = useState<{ text: string; success: boolean } | null>(null);
|
|
32
|
+
var [pluginErrors, setPluginErrors] = useState<PluginError[]>([]);
|
|
33
|
+
var [discoverPlugins, setDiscoverPlugins] = useState<MarketplacePluginEntry[]>([]);
|
|
34
|
+
var [discoverLoaded, setDiscoverLoaded] = useState(false);
|
|
35
|
+
var searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(function () {
|
|
38
|
+
function handleListResult(msg: ServerMessage) {
|
|
39
|
+
if (msg.type !== "plugin:list_result") return;
|
|
40
|
+
var data = msg as { type: "plugin:list_result"; plugins: PluginInfo[] };
|
|
41
|
+
setPlugins(data.plugins);
|
|
42
|
+
setLoaded(true);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleMarketplacesResult(msg: ServerMessage) {
|
|
46
|
+
if (msg.type !== "plugin:marketplaces_result") return;
|
|
47
|
+
var data = msg as { type: "plugin:marketplaces_result"; marketplaces: PluginMarketplaceInfo[] };
|
|
48
|
+
setMarketplaces(data.marketplaces);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleSearchResult(msg: ServerMessage) {
|
|
52
|
+
if (msg.type !== "plugin:search_result") return;
|
|
53
|
+
var data = msg as { type: "plugin:search_result"; query: string; plugins: MarketplacePluginEntry[]; count: number };
|
|
54
|
+
setSearchResults(data.plugins);
|
|
55
|
+
setSearching(false);
|
|
56
|
+
setHasSearched(true);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleInstallResult(msg: ServerMessage) {
|
|
60
|
+
if (msg.type !== "plugin:install_result") return;
|
|
61
|
+
var data = msg as { type: "plugin:install_result"; success: boolean; message?: string };
|
|
62
|
+
setInstallingKey(null);
|
|
63
|
+
showAction(data.message ?? (data.success ? "Installed" : "Install failed"), data.success);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleUninstallResult(msg: ServerMessage) {
|
|
67
|
+
if (msg.type !== "plugin:uninstall_result") return;
|
|
68
|
+
var data = msg as { type: "plugin:uninstall_result"; success: boolean; message?: string };
|
|
69
|
+
setUninstallingKey(null);
|
|
70
|
+
showAction(data.message ?? (data.success ? "Uninstalled" : "Uninstall failed"), data.success);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleUpdateResult(msg: ServerMessage) {
|
|
74
|
+
if (msg.type !== "plugin:update_result") return;
|
|
75
|
+
var data = msg as { type: "plugin:update_result"; success: boolean; message?: string };
|
|
76
|
+
setUpdatingKey(null);
|
|
77
|
+
showAction(data.message ?? (data.success ? "Updated" : "Update failed"), data.success);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleDetailsResult(msg: ServerMessage) {
|
|
81
|
+
if (msg.type !== "plugin:details_result") return;
|
|
82
|
+
var data = msg as { type: "plugin:details_result"; plugin: PluginDetails | null };
|
|
83
|
+
setDetailPlugin(data.plugin);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function handleErrorsResult(msg: ServerMessage) {
|
|
87
|
+
if (msg.type !== "plugin:errors_result") return;
|
|
88
|
+
var data = msg as { type: "plugin:errors_result"; errors: PluginError[] };
|
|
89
|
+
setPluginErrors(data.errors);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleDiscoverResult(msg: ServerMessage) {
|
|
93
|
+
if (msg.type !== "plugin:discover_result") return;
|
|
94
|
+
var data = msg as { type: "plugin:discover_result"; plugins: MarketplacePluginEntry[] };
|
|
95
|
+
setDiscoverPlugins(data.plugins);
|
|
96
|
+
setDiscoverLoaded(true);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
subscribe("plugin:list_result", handleListResult);
|
|
100
|
+
subscribe("plugin:marketplaces_result", handleMarketplacesResult);
|
|
101
|
+
subscribe("plugin:search_result", handleSearchResult);
|
|
102
|
+
subscribe("plugin:install_result", handleInstallResult);
|
|
103
|
+
subscribe("plugin:uninstall_result", handleUninstallResult);
|
|
104
|
+
subscribe("plugin:update_result", handleUpdateResult);
|
|
105
|
+
subscribe("plugin:details_result", handleDetailsResult);
|
|
106
|
+
subscribe("plugin:errors_result", handleErrorsResult);
|
|
107
|
+
subscribe("plugin:discover_result", handleDiscoverResult);
|
|
108
|
+
|
|
109
|
+
send({ type: "plugin:list" } as any);
|
|
110
|
+
send({ type: "plugin:marketplaces" } as any);
|
|
111
|
+
send({ type: "plugin:errors" } as any);
|
|
112
|
+
send({ type: "plugin:discover" } as any);
|
|
113
|
+
|
|
114
|
+
return function () {
|
|
115
|
+
unsubscribe("plugin:list_result", handleListResult);
|
|
116
|
+
unsubscribe("plugin:marketplaces_result", handleMarketplacesResult);
|
|
117
|
+
unsubscribe("plugin:search_result", handleSearchResult);
|
|
118
|
+
unsubscribe("plugin:install_result", handleInstallResult);
|
|
119
|
+
unsubscribe("plugin:uninstall_result", handleUninstallResult);
|
|
120
|
+
unsubscribe("plugin:update_result", handleUpdateResult);
|
|
121
|
+
unsubscribe("plugin:details_result", handleDetailsResult);
|
|
122
|
+
unsubscribe("plugin:errors_result", handleErrorsResult);
|
|
123
|
+
unsubscribe("plugin:discover_result", handleDiscoverResult);
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
function showAction(text: string, success: boolean) {
|
|
128
|
+
setActionMessage({ text, success });
|
|
129
|
+
setTimeout(function () { setActionMessage(null); }, 3000);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function handleViewDetails(plugin: PluginInfo) {
|
|
133
|
+
send({ type: "plugin:details", name: plugin.name, marketplace: plugin.marketplace } as any);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function handleInstall(name: string, marketplace: string) {
|
|
137
|
+
var key = name + "@" + marketplace;
|
|
138
|
+
setInstallingKey(key);
|
|
139
|
+
send({ type: "plugin:install", name: name, marketplace: marketplace } as any);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function handleUninstall(plugin: PluginInfo) {
|
|
143
|
+
setUninstallingKey(plugin.key);
|
|
144
|
+
setConfirmUninstall(null);
|
|
145
|
+
send({ type: "plugin:uninstall", name: plugin.name, marketplace: plugin.marketplace } as any);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function handleUpdate(plugin: PluginInfo) {
|
|
149
|
+
setUpdatingKey(plugin.key);
|
|
150
|
+
send({ type: "plugin:update", name: plugin.name, marketplace: plugin.marketplace } as any);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
var handleSearchInput = useCallback(function (value: string) {
|
|
154
|
+
setSearchQuery(value);
|
|
155
|
+
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
156
|
+
if (!value.trim()) {
|
|
157
|
+
setSearchResults([]);
|
|
158
|
+
setHasSearched(false);
|
|
159
|
+
setSearching(false);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setSearching(true);
|
|
163
|
+
searchTimerRef.current = setTimeout(function () {
|
|
164
|
+
send({ type: "plugin:search", query: value.trim() } as any);
|
|
165
|
+
}, 300);
|
|
166
|
+
}, [send]);
|
|
167
|
+
|
|
168
|
+
if (!loaded) {
|
|
169
|
+
return <div className="text-[13px] text-base-content/40 py-4">Loading...</div>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className="py-2 space-y-8">
|
|
174
|
+
{actionMessage && (
|
|
175
|
+
<div className={
|
|
176
|
+
"text-[12px] px-3 py-2 rounded-lg border " +
|
|
177
|
+
(actionMessage.success
|
|
178
|
+
? "bg-success/10 border-success/20 text-success"
|
|
179
|
+
: "bg-error/10 border-error/20 text-error")
|
|
180
|
+
}>
|
|
181
|
+
{actionMessage.text}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{pluginErrors.length > 0 && (
|
|
186
|
+
<PluginErrorsSection errors={pluginErrors} />
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
<InstalledPlugins
|
|
190
|
+
plugins={plugins}
|
|
191
|
+
updatingKey={updatingKey}
|
|
192
|
+
uninstallingKey={uninstallingKey}
|
|
193
|
+
confirmUninstall={confirmUninstall}
|
|
194
|
+
onViewDetails={handleViewDetails}
|
|
195
|
+
onUpdate={handleUpdate}
|
|
196
|
+
onUninstall={handleUninstall}
|
|
197
|
+
onConfirmUninstall={setConfirmUninstall}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<MarketplaceSearch
|
|
201
|
+
query={searchQuery}
|
|
202
|
+
results={searchResults}
|
|
203
|
+
searching={searching}
|
|
204
|
+
hasSearched={hasSearched}
|
|
205
|
+
installingKey={installingKey}
|
|
206
|
+
installedKeys={new Set(plugins.map(function (p) { return p.key; }))}
|
|
207
|
+
onSearch={handleSearchInput}
|
|
208
|
+
onInstall={handleInstall}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<DiscoverSection
|
|
212
|
+
plugins={discoverPlugins}
|
|
213
|
+
loaded={discoverLoaded}
|
|
214
|
+
installingKey={installingKey}
|
|
215
|
+
installedKeys={new Set(plugins.map(function (p) { return p.key; }))}
|
|
216
|
+
onInstall={handleInstall}
|
|
217
|
+
/>
|
|
218
|
+
|
|
219
|
+
<MarketplaceList marketplaces={marketplaces} />
|
|
220
|
+
|
|
221
|
+
{detailPlugin && (
|
|
222
|
+
<PluginDetailModal
|
|
223
|
+
plugin={detailPlugin}
|
|
224
|
+
onClose={function () { setDetailPlugin(null); }}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function InstalledPlugins({
|
|
232
|
+
plugins,
|
|
233
|
+
updatingKey,
|
|
234
|
+
uninstallingKey,
|
|
235
|
+
confirmUninstall,
|
|
236
|
+
onViewDetails,
|
|
237
|
+
onUpdate,
|
|
238
|
+
onUninstall,
|
|
239
|
+
onConfirmUninstall,
|
|
240
|
+
}: {
|
|
241
|
+
plugins: PluginInfo[];
|
|
242
|
+
updatingKey: string | null;
|
|
243
|
+
uninstallingKey: string | null;
|
|
244
|
+
confirmUninstall: string | null;
|
|
245
|
+
onViewDetails: (p: PluginInfo) => void;
|
|
246
|
+
onUpdate: (p: PluginInfo) => void;
|
|
247
|
+
onUninstall: (p: PluginInfo) => void;
|
|
248
|
+
onConfirmUninstall: (key: string | null) => void;
|
|
249
|
+
}) {
|
|
250
|
+
return (
|
|
251
|
+
<div>
|
|
252
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Installed Plugins</div>
|
|
253
|
+
{plugins.length === 0 ? (
|
|
254
|
+
<div className="py-6 text-center text-[13px] text-base-content/30">
|
|
255
|
+
No plugins installed. Search the marketplace below to get started.
|
|
256
|
+
</div>
|
|
257
|
+
) : (
|
|
258
|
+
<div className="space-y-2">
|
|
259
|
+
{plugins.map(function (plugin) {
|
|
260
|
+
return (
|
|
261
|
+
<PluginCard
|
|
262
|
+
key={plugin.key}
|
|
263
|
+
plugin={plugin}
|
|
264
|
+
isUpdating={updatingKey === plugin.key}
|
|
265
|
+
isUninstalling={uninstallingKey === plugin.key}
|
|
266
|
+
isConfirmingUninstall={confirmUninstall === plugin.key}
|
|
267
|
+
onViewDetails={function () { onViewDetails(plugin); }}
|
|
268
|
+
onUpdate={function () { onUpdate(plugin); }}
|
|
269
|
+
onUninstall={function () { onUninstall(plugin); }}
|
|
270
|
+
onConfirmUninstall={function () { onConfirmUninstall(plugin.key); }}
|
|
271
|
+
onCancelUninstall={function () { onConfirmUninstall(null); }}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
})}
|
|
275
|
+
</div>
|
|
276
|
+
)}
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function PluginCard({
|
|
282
|
+
plugin,
|
|
283
|
+
isUpdating,
|
|
284
|
+
isUninstalling,
|
|
285
|
+
isConfirmingUninstall,
|
|
286
|
+
onViewDetails,
|
|
287
|
+
onUpdate,
|
|
288
|
+
onUninstall,
|
|
289
|
+
onConfirmUninstall,
|
|
290
|
+
onCancelUninstall,
|
|
291
|
+
}: {
|
|
292
|
+
plugin: PluginInfo;
|
|
293
|
+
isUpdating: boolean;
|
|
294
|
+
isUninstalling: boolean;
|
|
295
|
+
isConfirmingUninstall: boolean;
|
|
296
|
+
onViewDetails: () => void;
|
|
297
|
+
onUpdate: () => void;
|
|
298
|
+
onUninstall: () => void;
|
|
299
|
+
onConfirmUninstall: () => void;
|
|
300
|
+
onCancelUninstall: () => void;
|
|
301
|
+
}) {
|
|
302
|
+
return (
|
|
303
|
+
<div
|
|
304
|
+
onClick={onViewDetails}
|
|
305
|
+
className="flex items-start gap-3 px-3 py-2.5 bg-base-300 border border-base-content/15 rounded-xl cursor-pointer hover:border-base-content/30 hover:bg-base-300/80 transition-colors duration-[120ms]"
|
|
306
|
+
role="button"
|
|
307
|
+
tabIndex={0}
|
|
308
|
+
onKeyDown={function (e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onViewDetails(); } }}
|
|
309
|
+
>
|
|
310
|
+
<Blocks size={14} className="text-base-content/25 mt-0.5 flex-shrink-0" />
|
|
311
|
+
<div className="flex-1 min-w-0">
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<span className="text-[13px] font-bold text-base-content truncate">{plugin.name}</span>
|
|
314
|
+
<span className="shrink-0 text-[10px] font-mono px-1.5 py-0.5 rounded-md bg-base-content/8 text-base-content/40">
|
|
315
|
+
v{plugin.version}
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="flex items-center gap-3 mt-0.5">
|
|
319
|
+
<span className="text-[11px] text-base-content/30">{plugin.marketplace}</span>
|
|
320
|
+
<span className="text-[11px] text-base-content/25">
|
|
321
|
+
{plugin.skillCount} skill{plugin.skillCount !== 1 ? "s" : ""}
|
|
322
|
+
{plugin.hookCount > 0 ? ", " + plugin.hookCount + " hook" + (plugin.hookCount !== 1 ? "s" : "") : ""}
|
|
323
|
+
{plugin.ruleCount > 0 ? ", " + plugin.ruleCount + " rule" + (plugin.ruleCount !== 1 ? "s" : "") : ""}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
{plugin.description && (
|
|
327
|
+
<div className="text-[12px] text-base-content/40 mt-0.5 line-clamp-1">{plugin.description}</div>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
<div className="flex gap-1 flex-shrink-0 mt-0.5" onClick={function (e) { e.stopPropagation(); }}>
|
|
331
|
+
{isUpdating ? (
|
|
332
|
+
<Loader2 size={12} className="text-primary animate-spin mt-1 mx-1" />
|
|
333
|
+
) : (
|
|
334
|
+
<button
|
|
335
|
+
onClick={onUpdate}
|
|
336
|
+
aria-label={"Update " + plugin.name}
|
|
337
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-info focus-visible:ring-2 focus-visible:ring-primary"
|
|
338
|
+
>
|
|
339
|
+
<RefreshCw size={12} />
|
|
340
|
+
</button>
|
|
341
|
+
)}
|
|
342
|
+
{isConfirmingUninstall ? (
|
|
343
|
+
<div className="flex gap-1">
|
|
344
|
+
<button
|
|
345
|
+
onClick={onUninstall}
|
|
346
|
+
className="btn btn-error btn-xs"
|
|
347
|
+
disabled={isUninstalling}
|
|
348
|
+
>
|
|
349
|
+
{isUninstalling ? <Loader2 size={10} className="animate-spin" /> : "Remove"}
|
|
350
|
+
</button>
|
|
351
|
+
<button onClick={onCancelUninstall} className="btn btn-ghost btn-xs">
|
|
352
|
+
Cancel
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
) : (
|
|
356
|
+
<button
|
|
357
|
+
onClick={onConfirmUninstall}
|
|
358
|
+
aria-label={"Remove " + plugin.name}
|
|
359
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error focus-visible:ring-2 focus-visible:ring-primary"
|
|
360
|
+
>
|
|
361
|
+
<Trash2 size={12} />
|
|
362
|
+
</button>
|
|
363
|
+
)}
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function MarketplaceSearch({
|
|
370
|
+
query,
|
|
371
|
+
results,
|
|
372
|
+
searching,
|
|
373
|
+
hasSearched,
|
|
374
|
+
installingKey,
|
|
375
|
+
installedKeys,
|
|
376
|
+
onSearch,
|
|
377
|
+
onInstall,
|
|
378
|
+
}: {
|
|
379
|
+
query: string;
|
|
380
|
+
results: MarketplacePluginEntry[];
|
|
381
|
+
searching: boolean;
|
|
382
|
+
hasSearched: boolean;
|
|
383
|
+
installingKey: string | null;
|
|
384
|
+
installedKeys: Set<string>;
|
|
385
|
+
onSearch: (q: string) => void;
|
|
386
|
+
onInstall: (name: string, marketplace: string) => void;
|
|
387
|
+
}) {
|
|
388
|
+
return (
|
|
389
|
+
<div>
|
|
390
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Browse Marketplace</div>
|
|
391
|
+
<div className="relative mb-3">
|
|
392
|
+
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-base-content/30 pointer-events-none" />
|
|
393
|
+
<input
|
|
394
|
+
type="text"
|
|
395
|
+
value={query}
|
|
396
|
+
onChange={function (e) { onSearch(e.target.value); }}
|
|
397
|
+
placeholder="Search plugins..."
|
|
398
|
+
className="w-full bg-base-300 border border-base-content/15 rounded-xl pl-8 pr-3 py-2.5 text-[13px] text-base-content placeholder:text-base-content/30 focus:outline-none focus:border-primary transition-colors"
|
|
399
|
+
/>
|
|
400
|
+
{searching && (
|
|
401
|
+
<Loader2 size={13} className="absolute right-3 top-1/2 -translate-y-1/2 text-primary animate-spin" />
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
{hasSearched && results.length === 0 && !searching && (
|
|
406
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
407
|
+
No plugins found for "{query}"
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
|
|
411
|
+
{results.length > 0 && (
|
|
412
|
+
<div className="space-y-2">
|
|
413
|
+
{results.map(function (entry) {
|
|
414
|
+
var key = entry.name + "@" + entry.marketplace;
|
|
415
|
+
var isInstalled = installedKeys.has(key);
|
|
416
|
+
var isInstalling = installingKey === key;
|
|
417
|
+
return (
|
|
418
|
+
<div
|
|
419
|
+
key={key}
|
|
420
|
+
className="flex items-start gap-3 px-3 py-2.5 bg-base-300/50 border border-base-content/10 rounded-xl"
|
|
421
|
+
>
|
|
422
|
+
<Package size={14} className="text-base-content/20 mt-0.5 flex-shrink-0" />
|
|
423
|
+
<div className="flex-1 min-w-0">
|
|
424
|
+
<div className="flex items-center gap-2">
|
|
425
|
+
<span className="text-[13px] font-bold text-base-content truncate">{entry.name}</span>
|
|
426
|
+
<span className="text-[10px] font-mono text-base-content/30">{entry.marketplace}</span>
|
|
427
|
+
</div>
|
|
428
|
+
{entry.description && (
|
|
429
|
+
<div className="text-[12px] text-base-content/40 mt-0.5 line-clamp-2">{entry.description}</div>
|
|
430
|
+
)}
|
|
431
|
+
<div className="flex items-center gap-3 mt-1">
|
|
432
|
+
{entry.author && (
|
|
433
|
+
<span className="text-[10px] text-base-content/25">{entry.author.name}</span>
|
|
434
|
+
)}
|
|
435
|
+
{entry.installs != null && entry.installs > 0 && (
|
|
436
|
+
<span className="text-[10px] text-base-content/25 flex items-center gap-0.5">
|
|
437
|
+
<Download size={9} />
|
|
438
|
+
{formatInstalls(entry.installs)}
|
|
439
|
+
</span>
|
|
440
|
+
)}
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="flex-shrink-0 mt-0.5" onClick={function (e) { e.stopPropagation(); }}>
|
|
444
|
+
{isInstalled ? (
|
|
445
|
+
<span className="text-[10px] font-mono px-2 py-1 rounded-md bg-success/10 text-success/70">
|
|
446
|
+
Installed
|
|
447
|
+
</span>
|
|
448
|
+
) : isInstalling ? (
|
|
449
|
+
<Loader2 size={14} className="text-primary animate-spin mx-2" />
|
|
450
|
+
) : (
|
|
451
|
+
<button
|
|
452
|
+
onClick={function () { onInstall(entry.name, entry.marketplace); }}
|
|
453
|
+
className="btn btn-primary btn-xs"
|
|
454
|
+
>
|
|
455
|
+
Install
|
|
456
|
+
</button>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
);
|
|
461
|
+
})}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function MarketplaceList({ marketplaces }: { marketplaces: PluginMarketplaceInfo[] }) {
|
|
469
|
+
if (marketplaces.length === 0) return null;
|
|
470
|
+
return (
|
|
471
|
+
<div>
|
|
472
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Registered Marketplaces</div>
|
|
473
|
+
<div className="space-y-1.5">
|
|
474
|
+
{marketplaces.map(function (m) {
|
|
475
|
+
return (
|
|
476
|
+
<div key={m.name} className="flex items-center gap-2.5 px-3 py-2 bg-base-300/30 border border-base-content/10 rounded-lg">
|
|
477
|
+
<Package size={12} className="text-base-content/20 flex-shrink-0" />
|
|
478
|
+
<div className="flex-1 min-w-0">
|
|
479
|
+
<span className="text-[12px] font-bold text-base-content/60">{m.name}</span>
|
|
480
|
+
<span className="text-[11px] text-base-content/25 ml-2">{m.source.repo}</span>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
})}
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function PluginDetailModal({ plugin, onClose }: { plugin: PluginDetails; onClose: () => void }) {
|
|
491
|
+
useEffect(function () {
|
|
492
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
493
|
+
if (e.key === "Escape") onClose();
|
|
494
|
+
}
|
|
495
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
496
|
+
return function () { document.removeEventListener("keydown", handleKeyDown); };
|
|
497
|
+
}, [onClose]);
|
|
498
|
+
|
|
499
|
+
return (
|
|
500
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center" role="dialog" aria-modal="true" aria-label={"Plugin: " + plugin.name}>
|
|
501
|
+
<div className="absolute inset-0 bg-black/50" onClick={onClose} />
|
|
502
|
+
<div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col overflow-hidden">
|
|
503
|
+
<div className="flex items-center justify-between px-5 py-4 border-b border-base-content/15 flex-shrink-0">
|
|
504
|
+
<div className="flex items-center gap-2.5 min-w-0">
|
|
505
|
+
<Blocks size={16} className="text-primary flex-shrink-0" />
|
|
506
|
+
<div className="min-w-0">
|
|
507
|
+
<h2 className="text-[15px] font-mono font-bold text-base-content truncate">{plugin.name}</h2>
|
|
508
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
509
|
+
<span className="text-[10px] font-mono text-base-content/40">v{plugin.version}</span>
|
|
510
|
+
<span className="text-[10px] text-base-content/30">{plugin.marketplace}</span>
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
<button
|
|
515
|
+
onClick={onClose}
|
|
516
|
+
aria-label="Close"
|
|
517
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/40 hover:text-base-content"
|
|
518
|
+
>
|
|
519
|
+
<X size={16} />
|
|
520
|
+
</button>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div className="overflow-y-auto flex-1">
|
|
524
|
+
<div className="px-5 py-3 border-b border-base-content/10 bg-base-300/30">
|
|
525
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1.5">
|
|
526
|
+
{plugin.author && (
|
|
527
|
+
<MetaItem label="Author" value={plugin.author.name} />
|
|
528
|
+
)}
|
|
529
|
+
{plugin.license && (
|
|
530
|
+
<MetaItem label="License" value={plugin.license} />
|
|
531
|
+
)}
|
|
532
|
+
<MetaItem label="Installed" value={formatDate(plugin.installedAt)} />
|
|
533
|
+
<MetaItem label="Updated" value={formatDate(plugin.lastUpdated)} />
|
|
534
|
+
{plugin.homepage && (
|
|
535
|
+
<a
|
|
536
|
+
href={plugin.homepage}
|
|
537
|
+
target="_blank"
|
|
538
|
+
rel="noopener noreferrer"
|
|
539
|
+
className="flex items-center gap-1 text-[11px] text-primary/60 hover:text-primary transition-colors"
|
|
540
|
+
>
|
|
541
|
+
<ExternalLink size={10} />
|
|
542
|
+
Homepage
|
|
543
|
+
</a>
|
|
544
|
+
)}
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
|
|
548
|
+
{plugin.description && (
|
|
549
|
+
<div className="px-5 py-3 border-b border-base-content/10">
|
|
550
|
+
<p className="text-[13px] text-base-content/60">{plugin.description}</p>
|
|
551
|
+
</div>
|
|
552
|
+
)}
|
|
553
|
+
|
|
554
|
+
{plugin.keywords && plugin.keywords.length > 0 && (
|
|
555
|
+
<div className="px-5 py-3 border-b border-base-content/10">
|
|
556
|
+
<div className="flex flex-wrap gap-1.5">
|
|
557
|
+
{plugin.keywords.map(function (kw) {
|
|
558
|
+
return (
|
|
559
|
+
<span key={kw} className="text-[10px] font-mono px-1.5 py-0.5 rounded-md bg-base-content/8 text-base-content/40">
|
|
560
|
+
{kw}
|
|
561
|
+
</span>
|
|
562
|
+
);
|
|
563
|
+
})}
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
)}
|
|
567
|
+
|
|
568
|
+
{plugin.skills.length > 0 && (
|
|
569
|
+
<div className="px-5 py-3 border-b border-base-content/10">
|
|
570
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
571
|
+
<Puzzle size={12} className="text-base-content/30" />
|
|
572
|
+
<span className="text-[11px] font-bold text-base-content/40 uppercase tracking-wider">
|
|
573
|
+
Skills ({plugin.skills.length})
|
|
574
|
+
</span>
|
|
575
|
+
</div>
|
|
576
|
+
<div className="space-y-1.5">
|
|
577
|
+
{plugin.skills.map(function (skill) {
|
|
578
|
+
return (
|
|
579
|
+
<div key={skill.name} className="flex items-start gap-2 py-1">
|
|
580
|
+
<ChevronRight size={10} className="text-base-content/20 mt-0.5 flex-shrink-0" />
|
|
581
|
+
<div className="min-w-0">
|
|
582
|
+
<span className="text-[12px] font-bold text-base-content/70">{skill.name}</span>
|
|
583
|
+
{skill.description && (
|
|
584
|
+
<div className="text-[11px] text-base-content/35 line-clamp-1">{skill.description}</div>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
);
|
|
589
|
+
})}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{Object.keys(plugin.hooks).length > 0 && (
|
|
595
|
+
<div className="px-5 py-3 border-b border-base-content/10">
|
|
596
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
597
|
+
<Webhook size={12} className="text-base-content/30" />
|
|
598
|
+
<span className="text-[11px] font-bold text-base-content/40 uppercase tracking-wider">
|
|
599
|
+
Hooks ({Object.keys(plugin.hooks).length})
|
|
600
|
+
</span>
|
|
601
|
+
</div>
|
|
602
|
+
<div className="space-y-1">
|
|
603
|
+
{Object.keys(plugin.hooks).map(function (hookName) {
|
|
604
|
+
return (
|
|
605
|
+
<div key={hookName} className="flex items-center gap-2 py-0.5">
|
|
606
|
+
<ChevronRight size={10} className="text-base-content/20 flex-shrink-0" />
|
|
607
|
+
<span className="text-[12px] font-mono text-base-content/50">{hookName}</span>
|
|
608
|
+
</div>
|
|
609
|
+
);
|
|
610
|
+
})}
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
)}
|
|
614
|
+
|
|
615
|
+
{plugin.rules.length > 0 && (
|
|
616
|
+
<div className="px-5 py-3">
|
|
617
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
618
|
+
<ScrollText size={12} className="text-base-content/30" />
|
|
619
|
+
<span className="text-[11px] font-bold text-base-content/40 uppercase tracking-wider">
|
|
620
|
+
Rules ({plugin.rules.length})
|
|
621
|
+
</span>
|
|
622
|
+
</div>
|
|
623
|
+
<div className="space-y-1">
|
|
624
|
+
{plugin.rules.map(function (rule) {
|
|
625
|
+
return (
|
|
626
|
+
<div key={rule} className="flex items-center gap-2 py-0.5">
|
|
627
|
+
<ChevronRight size={10} className="text-base-content/20 flex-shrink-0" />
|
|
628
|
+
<span className="text-[12px] font-mono text-base-content/50">{rule}</span>
|
|
629
|
+
</div>
|
|
630
|
+
);
|
|
631
|
+
})}
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
)}
|
|
635
|
+
</div>
|
|
636
|
+
|
|
637
|
+
<div className="px-5 py-3 border-t border-base-content/15 flex-shrink-0">
|
|
638
|
+
<div className="text-[11px] font-mono text-base-content/30 truncate">
|
|
639
|
+
{plugin.gitCommitSha.slice(0, 8)} · {plugin.installPath}
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function PluginErrorsSection({ errors }: { errors: PluginError[] }) {
|
|
648
|
+
return (
|
|
649
|
+
<div>
|
|
650
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
651
|
+
<AlertTriangle size={12} className="text-warning" />
|
|
652
|
+
<span className="text-[12px] font-semibold text-warning/80">
|
|
653
|
+
Errors ({errors.length})
|
|
654
|
+
</span>
|
|
655
|
+
</div>
|
|
656
|
+
<div className="space-y-2">
|
|
657
|
+
{errors.map(function (err) {
|
|
658
|
+
return (
|
|
659
|
+
<div key={err.key} className="px-3 py-2.5 bg-warning/5 border border-warning/20 rounded-xl">
|
|
660
|
+
<div className="flex items-center gap-2 mb-1">
|
|
661
|
+
<span className="text-[13px] font-bold text-base-content">{err.name}</span>
|
|
662
|
+
<span className="text-[10px] font-mono text-base-content/30">{err.marketplace}</span>
|
|
663
|
+
</div>
|
|
664
|
+
{err.errors.map(function (e, i) {
|
|
665
|
+
return (
|
|
666
|
+
<div key={i} className="flex items-start gap-1.5 mt-0.5">
|
|
667
|
+
<AlertTriangle size={10} className="text-warning/60 mt-0.5 flex-shrink-0" />
|
|
668
|
+
<span className="text-[12px] text-warning/70">{e}</span>
|
|
669
|
+
</div>
|
|
670
|
+
);
|
|
671
|
+
})}
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
})}
|
|
675
|
+
</div>
|
|
676
|
+
</div>
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function DiscoverSection({
|
|
681
|
+
plugins,
|
|
682
|
+
loaded,
|
|
683
|
+
installingKey,
|
|
684
|
+
installedKeys,
|
|
685
|
+
onInstall,
|
|
686
|
+
}: {
|
|
687
|
+
plugins: MarketplacePluginEntry[];
|
|
688
|
+
loaded: boolean;
|
|
689
|
+
installingKey: string | null;
|
|
690
|
+
installedKeys: Set<string>;
|
|
691
|
+
onInstall: (name: string, marketplace: string) => void;
|
|
692
|
+
}) {
|
|
693
|
+
var [showAll, setShowAll] = useState(false);
|
|
694
|
+
var INITIAL_COUNT = 12;
|
|
695
|
+
|
|
696
|
+
if (!loaded) return null;
|
|
697
|
+
if (plugins.length === 0) return null;
|
|
698
|
+
|
|
699
|
+
var visible = showAll ? plugins : plugins.slice(0, INITIAL_COUNT);
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
<div>
|
|
703
|
+
<div className="flex items-center justify-between mb-2">
|
|
704
|
+
<div className="text-[12px] font-semibold text-base-content/40">
|
|
705
|
+
Discover ({plugins.length} available)
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
<div className="space-y-2">
|
|
709
|
+
{visible.map(function (entry) {
|
|
710
|
+
var key = entry.name + "@" + entry.marketplace;
|
|
711
|
+
var isInstalled = installedKeys.has(key);
|
|
712
|
+
var isInstalling = installingKey === key;
|
|
713
|
+
return (
|
|
714
|
+
<div
|
|
715
|
+
key={key}
|
|
716
|
+
className="flex items-start gap-3 px-3 py-2.5 bg-base-300/50 border border-base-content/10 rounded-xl"
|
|
717
|
+
>
|
|
718
|
+
<Package size={14} className="text-base-content/20 mt-0.5 flex-shrink-0" />
|
|
719
|
+
<div className="flex-1 min-w-0">
|
|
720
|
+
<div className="flex items-center gap-2">
|
|
721
|
+
<span className="text-[13px] font-bold text-base-content truncate">{entry.name}</span>
|
|
722
|
+
<span className="text-[10px] font-mono text-base-content/30">{entry.marketplace}</span>
|
|
723
|
+
</div>
|
|
724
|
+
{entry.description && (
|
|
725
|
+
<div className="text-[12px] text-base-content/40 mt-0.5 line-clamp-2">{entry.description}</div>
|
|
726
|
+
)}
|
|
727
|
+
<div className="flex items-center gap-3 mt-1">
|
|
728
|
+
{entry.author && (
|
|
729
|
+
<span className="text-[10px] text-base-content/25">{entry.author.name}</span>
|
|
730
|
+
)}
|
|
731
|
+
{entry.installs != null && entry.installs > 0 && (
|
|
732
|
+
<span className="text-[10px] text-base-content/25 flex items-center gap-0.5">
|
|
733
|
+
<Download size={9} />
|
|
734
|
+
{formatInstalls(entry.installs)}
|
|
735
|
+
</span>
|
|
736
|
+
)}
|
|
737
|
+
</div>
|
|
738
|
+
</div>
|
|
739
|
+
<div className="flex-shrink-0 mt-0.5" onClick={function (e) { e.stopPropagation(); }}>
|
|
740
|
+
{isInstalled ? (
|
|
741
|
+
<span className="text-[10px] font-mono px-2 py-1 rounded-md bg-success/10 text-success/70">
|
|
742
|
+
Installed
|
|
743
|
+
</span>
|
|
744
|
+
) : isInstalling ? (
|
|
745
|
+
<Loader2 size={14} className="text-primary animate-spin mx-2" />
|
|
746
|
+
) : (
|
|
747
|
+
<button
|
|
748
|
+
onClick={function () { onInstall(entry.name, entry.marketplace); }}
|
|
749
|
+
className="btn btn-primary btn-xs"
|
|
750
|
+
>
|
|
751
|
+
Install
|
|
752
|
+
</button>
|
|
753
|
+
)}
|
|
754
|
+
</div>
|
|
755
|
+
</div>
|
|
756
|
+
);
|
|
757
|
+
})}
|
|
758
|
+
</div>
|
|
759
|
+
{!showAll && plugins.length > INITIAL_COUNT && (
|
|
760
|
+
<button
|
|
761
|
+
onClick={function () { setShowAll(true); }}
|
|
762
|
+
className="mt-3 text-[12px] text-primary/60 hover:text-primary transition-colors"
|
|
763
|
+
>
|
|
764
|
+
Show all {plugins.length} plugins
|
|
765
|
+
</button>
|
|
766
|
+
)}
|
|
767
|
+
{showAll && plugins.length > INITIAL_COUNT && (
|
|
768
|
+
<button
|
|
769
|
+
onClick={function () { setShowAll(false); }}
|
|
770
|
+
className="mt-3 text-[12px] text-primary/60 hover:text-primary transition-colors"
|
|
771
|
+
>
|
|
772
|
+
Show less
|
|
773
|
+
</button>
|
|
774
|
+
)}
|
|
775
|
+
</div>
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function MetaItem({ label, value }: { label: string; value: string }) {
|
|
780
|
+
return (
|
|
781
|
+
<div className="flex items-baseline gap-1.5">
|
|
782
|
+
<span className="text-[11px] font-mono text-base-content/35 uppercase tracking-wider">{label}</span>
|
|
783
|
+
<span className="text-[12px] text-base-content/70">{value}</span>
|
|
784
|
+
</div>
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function formatInstalls(count: number): string {
|
|
789
|
+
if (count >= 1000000) return (count / 1000000).toFixed(1) + "M";
|
|
790
|
+
if (count >= 1000) return (count / 1000).toFixed(1) + "k";
|
|
791
|
+
return String(count);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function formatDate(iso: string): string {
|
|
795
|
+
try {
|
|
796
|
+
var d = new Date(iso);
|
|
797
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
798
|
+
} catch {
|
|
799
|
+
return iso;
|
|
800
|
+
}
|
|
801
|
+
}
|