total-recall-brain 3.35.0 β 3.36.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/bin/total-recall.mjs +17 -4
- package/frontend/dist/assets/{index-BPrkIYkZ.js β index-DZ4ypNEl.js} +135 -135
- package/frontend/dist/brand/plugins/phone.svg +20 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/App.tsx +3 -30
- package/frontend/src/api/plugins.ts +18 -0
- package/frontend/src/components/plugins/previews/CodeQualityPreview.tsx +78 -86
- package/frontend/src/components/plugins/previews/ComposableCliPreview.tsx +74 -84
- package/frontend/src/components/plugins/previews/DecisionPreview.tsx +158 -106
- package/frontend/src/components/plugins/previews/DesignPreview.tsx +109 -75
- package/frontend/src/components/plugins/previews/DomainsPreview.tsx +69 -88
- package/frontend/src/components/plugins/previews/ImageGeneratorPreview.tsx +286 -0
- package/frontend/src/components/plugins/previews/PhonePreview.tsx +490 -76
- package/frontend/src/components/plugins/previews/SigningPreview.tsx +164 -50
- package/frontend/src/components/plugins/previews/TextPreview.tsx +85 -86
- package/frontend/src/components/plugins/previews/UserAutomationsPreview.tsx +30 -0
- package/frontend/src/components/plugins/previews/index.tsx +6 -1
- package/frontend/src/pages/PluginsPage.tsx +603 -136
- package/package.json +2 -5
- package/scaffold/.agent/skills/plugins/SKILL.md +200 -6
- package/src/cli/plugin/create.mjs +156 -11
- package/src/cli/plugin/index.mjs +5 -0
- package/src/core/embeddings.mjs +2 -0
- package/src/core/plugin-loader.mjs +18 -3
- package/src/core/plugin-store.mjs +122 -11
- package/src/core/surface.mjs +29 -1
- package/src/core/vault.mjs +6 -2
- package/src/server/route-manifest.json +22 -2
- package/src/server/routes/plugins-mesh.mjs +9 -1
- package/src/server/routes/plugins.mjs +65 -1
- package/frontend/src/pages/AutomationsPage.spec.tsx +0 -61
- package/frontend/src/pages/AutomationsPage.tsx +0 -354
- package/plugins/creative-search/README.md +0 -33
- package/plugins/creative-search/cli.mjs +0 -415
- package/plugins/creative-search/config.mjs +0 -129
- package/plugins/creative-search/generator.mjs +0 -13
- package/plugins/creative-search/plugin.json +0 -61
- package/plugins/creative-search/ui/DESIGN.md +0 -50
- package/plugins/creative-search/ui/csearch-main.js +0 -314
- package/plugins/creative-search/ui/csearch-settings.js +0 -392
- package/plugins/creative-search/ui/index.js +0 -2
- package/plugins/creative-search/ui/tokens.css +0 -34
- package/plugins/dsh/README.md +0 -20
- package/plugins/dsh/cli.mjs +0 -232
- package/plugins/dsh/generator.mjs +0 -65
- package/plugins/dsh/plugin.json +0 -57
- package/plugins/dsh/ui/DESIGN.md +0 -48
- package/plugins/dsh/ui/dsh-main.js +0 -226
- package/plugins/dsh/ui/dsh-status.js +0 -265
- package/plugins/dsh/ui/index.js +0 -2
- package/plugins/dsh/ui/tokens.css +0 -32
- package/plugins/git-sentinel/README.md +0 -29
- package/plugins/git-sentinel/cli.mjs +0 -89
- package/plugins/git-sentinel/plugin.json +0 -18
- package/plugins/operator-alerts/README.md +0 -32
- package/plugins/operator-alerts/cli.mjs +0 -57
- package/plugins/operator-alerts/notify-core.mjs +0 -216
- package/plugins/operator-alerts/plugin.json +0 -20
- package/plugins/skill-manager/README.md +0 -57
- package/plugins/skill-manager/cli.mjs +0 -123
- package/plugins/skill-manager/plugin.json +0 -25
- package/plugins/skill-manager/route.mjs +0 -106
- package/plugins/system-monitor/README.md +0 -28
- package/plugins/system-monitor/cli.mjs +0 -80
- package/plugins/system-monitor/generator.mjs +0 -19
- package/plugins/system-monitor/plugin.json +0 -29
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
installPlugin,
|
|
8
8
|
removePlugin,
|
|
9
9
|
setPluginShared,
|
|
10
|
+
updatePluginBranding,
|
|
11
|
+
setPluginLocked,
|
|
12
|
+
deployPluginToStore,
|
|
10
13
|
runPluginCommand,
|
|
11
14
|
fetchPluginReadme,
|
|
12
15
|
type PluginInfo,
|
|
@@ -15,13 +18,14 @@ import {
|
|
|
15
18
|
type PeerNode
|
|
16
19
|
} from "../api"
|
|
17
20
|
import { renderMarkdown } from "../components/MarkdownUtils"
|
|
21
|
+
import { getPreviewComponent, PreviewFallback } from "../components/plugins/previews"
|
|
18
22
|
|
|
19
23
|
interface PluginsPageProps {
|
|
20
24
|
activeBrainId?: string | null
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
type Tab = "installed" | "bundled" | "mesh"
|
|
24
|
-
type DetailTab = "run" | "readme" | "details"
|
|
28
|
+
type DetailTab = "preview" | "run" | "readme" | "details"
|
|
25
29
|
type Alert = { type: "success" | "error"; message: string } | null
|
|
26
30
|
|
|
27
31
|
const PEER_STATUS_LABEL: Record<PeerNode["status"], string> = {
|
|
@@ -36,6 +40,33 @@ const PEER_STATUS_LABEL: Record<PeerNode["status"], string> = {
|
|
|
36
40
|
const mono: CSSProperties = { fontFamily: "var(--font-mono)", fontSize: "12px" }
|
|
37
41
|
const muted: CSSProperties = { color: "var(--text-secondary)", fontSize: "13px", lineHeight: 1.5 }
|
|
38
42
|
|
|
43
|
+
const DEFAULT_BRANDING: Record<string, { icon: string; color: string; badge: string }> = {
|
|
44
|
+
phone: { icon: "π", color: "#10b981", badge: "Telephony" },
|
|
45
|
+
domains: { icon: "π", color: "#3b82f6", badge: "DNS" },
|
|
46
|
+
signing: { icon: "βοΈ", color: "#8b5cf6", badge: "Signing" },
|
|
47
|
+
"code-quality": { icon: "π‘οΈ", color: "#06b6d4", badge: "Quality Gates" },
|
|
48
|
+
"composable-cli": { icon: "π»", color: "#14b8a6", badge: "CLI Engine" },
|
|
49
|
+
decision: { icon: "βοΈ", color: "#a855f7", badge: "Decisions" },
|
|
50
|
+
design: { icon: "π¨", color: "#ec4899", badge: "Design System" },
|
|
51
|
+
text: { icon: "π¬", color: "#22c55e", badge: "Messaging" },
|
|
52
|
+
"creative-search": { icon: "π", color: "#f59e0b", badge: "Search" },
|
|
53
|
+
"git-sentinel": { icon: "πΏ", color: "#84cc16", badge: "Git Health" },
|
|
54
|
+
"system-monitor": { icon: "π", color: "#6366f1", badge: "Telemetry" },
|
|
55
|
+
"operator-alerts": { icon: "π", color: "#ef4444", badge: "Alerts" },
|
|
56
|
+
dsh: { icon: "π", color: "#64748b", badge: "Remote Shell" },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolvePluginBranding(p: { id: string; branding?: { icon?: string | null; color?: string | null; badge?: string | null } }) {
|
|
60
|
+
const custom = p.branding || {}
|
|
61
|
+
const shortId = p.id.replace(/^tr-plugin-/, "")
|
|
62
|
+
const fallback = DEFAULT_BRANDING[shortId] || DEFAULT_BRANDING[p.id] || { icon: "π§©", color: "#3b82f6", badge: "Capability" }
|
|
63
|
+
return {
|
|
64
|
+
icon: custom.icon || fallback.icon,
|
|
65
|
+
color: custom.color || fallback.color,
|
|
66
|
+
badge: custom.badge || fallback.badge,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
function shortHash(sha: string | null | undefined) {
|
|
40
71
|
return sha ? `${sha.slice(0, 12)}β¦` : "β"
|
|
41
72
|
}
|
|
@@ -106,12 +137,19 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
106
137
|
const [installGlobal, setInstallGlobal] = useState(false)
|
|
107
138
|
|
|
108
139
|
const [detail, setDetail] = useState<PluginInfo | null>(null)
|
|
109
|
-
const [detailTab, setDetailTab] = useState<DetailTab>("
|
|
140
|
+
const [detailTab, setDetailTab] = useState<DetailTab>("preview")
|
|
110
141
|
const [subcommand, setSubcommand] = useState("")
|
|
111
142
|
const [args, setArgs] = useState("")
|
|
112
143
|
const [output, setOutput] = useState<{ text: string; ok: boolean } | null>(null)
|
|
113
144
|
const [readme, setReadme] = useState("")
|
|
114
145
|
|
|
146
|
+
// Branding Customization Modal state
|
|
147
|
+
const [brandingOpen, setBrandingOpen] = useState(false)
|
|
148
|
+
const [brandingPlugin, setBrandingPlugin] = useState<PluginInfo | null>(null)
|
|
149
|
+
const [brandingIcon, setBrandingIcon] = useState("")
|
|
150
|
+
const [brandingColor, setBrandingColor] = useState("#3b82f6")
|
|
151
|
+
const [brandingBadge, setBrandingBadge] = useState("")
|
|
152
|
+
|
|
115
153
|
const loadLocal = useCallback(async () => {
|
|
116
154
|
setLoading(true)
|
|
117
155
|
try {
|
|
@@ -195,9 +233,78 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
195
233
|
loadLocal()
|
|
196
234
|
}
|
|
197
235
|
|
|
198
|
-
const
|
|
236
|
+
const doToggleLock = async (p: PluginInfo) => {
|
|
237
|
+
setBusy(p.id)
|
|
238
|
+
const nextLocked = !p.locked
|
|
239
|
+
const res = await setPluginLocked(p.id, nextLocked)
|
|
240
|
+
setBusy(null)
|
|
241
|
+
if (!res.success) {
|
|
242
|
+
setAlert({ type: "error", message: res.error || "Could not update lock status" })
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
if (res.plugin && detail?.id === p.id) setDetail(res.plugin)
|
|
246
|
+
setAlert({
|
|
247
|
+
type: "success",
|
|
248
|
+
message: nextLocked ? `${p.name} is locked against further editing.` : `${p.name} is unlocked for editing.`
|
|
249
|
+
})
|
|
250
|
+
loadLocal()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const doDeployStore = async (p: PluginInfo) => {
|
|
254
|
+
setBusy(p.id)
|
|
255
|
+
const res = await deployPluginToStore(p.id, true)
|
|
256
|
+
setBusy(null)
|
|
257
|
+
if (!res.success) {
|
|
258
|
+
setAlert({ type: "error", message: res.error || "Could not deploy plugin to store" })
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
if (res.plugin && detail?.id === p.id) setDetail(res.plugin)
|
|
262
|
+
setAlert({
|
|
263
|
+
type: "success",
|
|
264
|
+
message: `${p.name} deployed to the plugin store and locked as complete.`
|
|
265
|
+
})
|
|
266
|
+
loadLocal()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const openBrandingModal = (p: PluginInfo) => {
|
|
270
|
+
const branding = resolvePluginBranding(p)
|
|
271
|
+
setBrandingPlugin(p)
|
|
272
|
+
setBrandingIcon(branding.icon)
|
|
273
|
+
setBrandingColor(branding.color)
|
|
274
|
+
setBrandingBadge(branding.badge)
|
|
275
|
+
setBrandingOpen(true)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const saveBranding = async () => {
|
|
279
|
+
if (!brandingPlugin) return
|
|
280
|
+
if (brandingPlugin.locked) {
|
|
281
|
+
setAlert({ type: "error", message: "Plugin is locked. Unlock it before editing branding." })
|
|
282
|
+
setBrandingOpen(false)
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
setBusy(brandingPlugin.id)
|
|
286
|
+
const res = await updatePluginBranding(brandingPlugin.id, {
|
|
287
|
+
icon: brandingIcon.trim() || undefined,
|
|
288
|
+
color: brandingColor.trim() || undefined,
|
|
289
|
+
badge: brandingBadge.trim() || undefined
|
|
290
|
+
})
|
|
291
|
+
setBusy(null)
|
|
292
|
+
setBrandingOpen(false)
|
|
293
|
+
if (!res.success) {
|
|
294
|
+
setAlert({ type: "error", message: res.error || "Could not save branding" })
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
setAlert({ type: "success", message: `Updated branding for ${brandingPlugin.name}` })
|
|
298
|
+
await loadLocal()
|
|
299
|
+
if (detail?.id === brandingPlugin.id && res.plugin) {
|
|
300
|
+
setDetail(res.plugin)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const openDetail = async (p: PluginInfo, targetTab: DetailTab = "preview") => {
|
|
199
305
|
setDetail(p)
|
|
200
|
-
|
|
306
|
+
const hasPreview = !!getPreviewComponent(p.id)
|
|
307
|
+
setDetailTab(hasPreview ? targetTab : (p.cli ? "run" : "details"))
|
|
201
308
|
setSubcommand(p.cli?.subcommands?.[0]?.name || "")
|
|
202
309
|
setArgs("")
|
|
203
310
|
setOutput(null)
|
|
@@ -222,7 +329,6 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
222
329
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
223
330
|
}, [searchParams, installed])
|
|
224
331
|
|
|
225
|
-
// Keep an open detail panel in sync after share/remove/reload.
|
|
226
332
|
useEffect(() => {
|
|
227
333
|
if (!detail) return
|
|
228
334
|
const fresh = installed.find((p) => p.id === detail.id)
|
|
@@ -261,8 +367,8 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
261
367
|
<div>
|
|
262
368
|
<h1>Plugins</h1>
|
|
263
369
|
<p>
|
|
264
|
-
Plugins
|
|
265
|
-
|
|
370
|
+
Plugins deliver modular capabilities into Total Recall and SSSS apps β memory categories, agent context, commands, and design-token UI.
|
|
371
|
+
Customizable branding, locked release status, and direct store deployment are managed on each card.
|
|
266
372
|
</p>
|
|
267
373
|
</div>
|
|
268
374
|
<button className="btn btn-primary" onClick={() => { setInstallOpen(true); setAlert(null) }}>
|
|
@@ -302,33 +408,160 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
302
408
|
</Empty>
|
|
303
409
|
) : (
|
|
304
410
|
<div className="card-grid">
|
|
305
|
-
{filteredInstalled.map((p) =>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
411
|
+
{filteredInstalled.map((p) => {
|
|
412
|
+
const branding = resolvePluginBranding(p)
|
|
413
|
+
return (
|
|
414
|
+
<div
|
|
415
|
+
key={`${p.scope}:${p.id}`}
|
|
416
|
+
className="card"
|
|
417
|
+
style={{
|
|
418
|
+
display: "flex",
|
|
419
|
+
flexDirection: "column",
|
|
420
|
+
gap: "12px",
|
|
421
|
+
borderTop: `3px solid ${branding.color}`,
|
|
422
|
+
position: "relative"
|
|
423
|
+
}}
|
|
424
|
+
>
|
|
425
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "8px", alignItems: "flex-start" }}>
|
|
426
|
+
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
|
|
427
|
+
<div
|
|
428
|
+
style={{
|
|
429
|
+
width: "36px",
|
|
430
|
+
height: "36px",
|
|
431
|
+
borderRadius: "8px",
|
|
432
|
+
background: `${branding.color}20`,
|
|
433
|
+
border: `1px solid ${branding.color}50`,
|
|
434
|
+
display: "flex",
|
|
435
|
+
alignItems: "center",
|
|
436
|
+
justifyContent: "center",
|
|
437
|
+
fontSize: "18px",
|
|
438
|
+
flexShrink: 0
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
{branding.icon}
|
|
442
|
+
</div>
|
|
443
|
+
<div>
|
|
444
|
+
<button
|
|
445
|
+
onClick={() => openDetail(p, "preview")}
|
|
446
|
+
style={{
|
|
447
|
+
background: "none",
|
|
448
|
+
border: "none",
|
|
449
|
+
padding: 0,
|
|
450
|
+
color: "var(--text-primary)",
|
|
451
|
+
fontWeight: 600,
|
|
452
|
+
fontSize: "15px",
|
|
453
|
+
cursor: "pointer",
|
|
454
|
+
textAlign: "left"
|
|
455
|
+
}}
|
|
456
|
+
>
|
|
457
|
+
{p.name}
|
|
458
|
+
</button>
|
|
459
|
+
<div style={{ display: "flex", gap: "6px", alignItems: "center", marginTop: "2px" }}>
|
|
460
|
+
<span style={{ ...mono, color: "var(--text-tertiary)" }}>v{p.version}</span>
|
|
461
|
+
<span
|
|
462
|
+
className="badge"
|
|
463
|
+
style={{
|
|
464
|
+
fontSize: "10px",
|
|
465
|
+
background: `${branding.color}15`,
|
|
466
|
+
color: branding.color,
|
|
467
|
+
borderColor: `${branding.color}40`,
|
|
468
|
+
padding: "1px 6px"
|
|
469
|
+
}}
|
|
470
|
+
>
|
|
471
|
+
{branding.badge}
|
|
472
|
+
</span>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
<div style={{ display: "flex", gap: "4px" }}>
|
|
477
|
+
{p.locked ? (
|
|
478
|
+
<span className="badge" style={{ background: "#2e1065", color: "#c084fc", borderColor: "#7e22ce" }} title="Editing locked">
|
|
479
|
+
π Locked
|
|
480
|
+
</span>
|
|
481
|
+
) : (
|
|
482
|
+
<span className="badge" style={{ background: "#064e3b", color: "#6ee7b7", borderColor: "#059669" }} title="Editing enabled">
|
|
483
|
+
π Editable
|
|
484
|
+
</span>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<div style={muted}>{p.description}</div>
|
|
490
|
+
<UseCaseChips useCases={p.use_cases} />
|
|
491
|
+
|
|
492
|
+
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap", fontSize: "11px" }}>
|
|
493
|
+
<span className="badge">{p.scope}</span>
|
|
494
|
+
<span className="badge">{sourceLabel(p)}</span>
|
|
495
|
+
{p.shared && <span className="badge badge-accent">shared</span>}
|
|
496
|
+
{p.store_deployed && <span className="badge badge-success">πͺ in store</span>}
|
|
497
|
+
{!p.valid && <span className="badge badge-error">invalid manifest</span>}
|
|
498
|
+
{p.modified_since_install && <span className="badge badge-warning">changed since install</span>}
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<div style={{ ...mono, color: "var(--text-tertiary)", fontSize: "11px" }} title={p.sha256 || undefined}>
|
|
502
|
+
sha256 {shortHash(p.sha256)}
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
{/* Common Config Controls & Action Toolbar */}
|
|
506
|
+
<div style={{ display: "flex", gap: "6px", marginTop: "auto", flexWrap: "wrap", paddingTop: "8px", borderTop: "1px solid var(--border)" }}>
|
|
507
|
+
<button
|
|
508
|
+
className="btn btn-primary btn-sm"
|
|
509
|
+
onClick={() => openDetail(p, "preview")}
|
|
510
|
+
title="Open interactive component preview"
|
|
511
|
+
>
|
|
512
|
+
Preview
|
|
513
|
+
</button>
|
|
514
|
+
<button
|
|
515
|
+
className="btn btn-ghost btn-sm"
|
|
516
|
+
onClick={() => openDetail(p, "run")}
|
|
517
|
+
title="Run plugin CLI handler"
|
|
518
|
+
>
|
|
519
|
+
Run CLI
|
|
520
|
+
</button>
|
|
521
|
+
<button
|
|
522
|
+
className="btn btn-ghost btn-sm"
|
|
523
|
+
onClick={() => openBrandingModal(p)}
|
|
524
|
+
disabled={p.locked}
|
|
525
|
+
title={p.locked ? "Plugin is locked" : "Customize icon and branding"}
|
|
526
|
+
>
|
|
527
|
+
π¨ Brand
|
|
528
|
+
</button>
|
|
529
|
+
<button
|
|
530
|
+
className="btn btn-ghost btn-sm"
|
|
531
|
+
onClick={() => doToggleLock(p)}
|
|
532
|
+
disabled={busy === p.id}
|
|
533
|
+
title={p.locked ? "Unlock editing" : "Lock editing when complete"}
|
|
534
|
+
>
|
|
535
|
+
{p.locked ? "π Unlock" : "π Lock"}
|
|
536
|
+
</button>
|
|
537
|
+
<button
|
|
538
|
+
className="btn btn-ghost btn-sm"
|
|
539
|
+
onClick={() => doDeployStore(p)}
|
|
540
|
+
disabled={busy === p.id || !p.valid}
|
|
541
|
+
title="Publish and deploy to the plugin store"
|
|
542
|
+
>
|
|
543
|
+
{p.store_deployed ? "β Store" : "π Store"}
|
|
544
|
+
</button>
|
|
545
|
+
<button
|
|
546
|
+
className="btn btn-ghost btn-sm"
|
|
547
|
+
disabled={busy === p.id || (!p.valid && !p.shared)}
|
|
548
|
+
onClick={() => doShare(p, !p.shared)}
|
|
549
|
+
title="Share over mesh or direct link"
|
|
550
|
+
>
|
|
551
|
+
{p.shared ? "Unshare" : "Share"}
|
|
552
|
+
</button>
|
|
553
|
+
<button
|
|
554
|
+
className="btn btn-ghost btn-sm"
|
|
555
|
+
disabled={busy === p.id}
|
|
556
|
+
onClick={() => doRemove(p)}
|
|
557
|
+
title="Uninstall plugin"
|
|
558
|
+
>
|
|
559
|
+
Remove
|
|
560
|
+
</button>
|
|
561
|
+
</div>
|
|
329
562
|
</div>
|
|
330
|
-
|
|
331
|
-
)
|
|
563
|
+
)
|
|
564
|
+
})}
|
|
332
565
|
</div>
|
|
333
566
|
)
|
|
334
567
|
)}
|
|
@@ -337,32 +570,56 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
337
570
|
loading ? <div style={muted}>Loadingβ¦</div> :
|
|
338
571
|
filteredBundled.length === 0 ? <Empty title="No bundled plugins match" /> : (
|
|
339
572
|
<>
|
|
340
|
-
<p style={{ ...muted, marginBottom: "16px" }}>Shipped with this version of Total Recall,
|
|
573
|
+
<p style={{ ...muted, marginBottom: "16px" }}>Shipped with this version of Total Recall, installable on any node.</p>
|
|
341
574
|
<div className="card-grid">
|
|
342
|
-
{filteredBundled.map((p) =>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
575
|
+
{filteredBundled.map((p) => {
|
|
576
|
+
const branding = resolvePluginBranding(p)
|
|
577
|
+
return (
|
|
578
|
+
<div key={p.id} className="card" style={{ display: "flex", flexDirection: "column", gap: "10px", borderTop: `3px solid ${branding.color}` }}>
|
|
579
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "8px", alignItems: "flex-start" }}>
|
|
580
|
+
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
|
|
581
|
+
<div
|
|
582
|
+
style={{
|
|
583
|
+
width: "32px",
|
|
584
|
+
height: "32px",
|
|
585
|
+
borderRadius: "6px",
|
|
586
|
+
background: `${branding.color}20`,
|
|
587
|
+
display: "flex",
|
|
588
|
+
alignItems: "center",
|
|
589
|
+
justifyContent: "center",
|
|
590
|
+
fontSize: "16px"
|
|
591
|
+
}}
|
|
592
|
+
>
|
|
593
|
+
{branding.icon}
|
|
594
|
+
</div>
|
|
595
|
+
<div>
|
|
596
|
+
<span style={{ fontWeight: 600, fontSize: "15px" }}>{p.name}</span>
|
|
597
|
+
<div style={{ ...mono, color: "var(--text-tertiary)" }}>v{p.version}</div>
|
|
598
|
+
</div>
|
|
599
|
+
</div>
|
|
600
|
+
<span className="badge" style={{ background: `${branding.color}15`, color: branding.color }}>{branding.badge}</span>
|
|
601
|
+
</div>
|
|
602
|
+
<div style={muted}>{p.description}</div>
|
|
603
|
+
<UseCaseChips useCases={p.use_cases} />
|
|
604
|
+
<div style={{ ...muted, fontSize: "12px" }}>
|
|
605
|
+
{[
|
|
606
|
+
p.cli && `command: total-recall ${p.cli.command}`,
|
|
607
|
+
p.tasks.length > 0 && `${p.tasks.length} scheduled task${p.tasks.length > 1 ? "s" : ""}`,
|
|
608
|
+
p.categories.length > 0 && `${p.categories.length} memory categor${p.categories.length > 1 ? "ies" : "y"}`
|
|
609
|
+
].filter(Boolean).join(" Β· ")}
|
|
610
|
+
</div>
|
|
611
|
+
<div style={{ marginTop: "auto", display: "flex", gap: "8px" }}>
|
|
612
|
+
{p.installed ? (
|
|
613
|
+
<span className="badge badge-success">installed</span>
|
|
614
|
+
) : (
|
|
615
|
+
<button className="btn btn-primary btn-sm" disabled={busy === p.id} onClick={() => doInstall(p.id)}>
|
|
361
616
|
{busy === p.id ? "Installingβ¦" : "Install"}
|
|
362
|
-
</button>
|
|
617
|
+
</button>
|
|
618
|
+
)}
|
|
619
|
+
</div>
|
|
363
620
|
</div>
|
|
364
|
-
|
|
365
|
-
)
|
|
621
|
+
)
|
|
622
|
+
})}
|
|
366
623
|
</div>
|
|
367
624
|
</>
|
|
368
625
|
)
|
|
@@ -468,97 +725,307 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
468
725
|
</div>
|
|
469
726
|
)}
|
|
470
727
|
|
|
471
|
-
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
<div style={{ display: "flex", justifyContent: "space-between",
|
|
477
|
-
<div>
|
|
478
|
-
|
|
479
|
-
<div style={{ ...mono, color: "var(--text-tertiary)" }}>{detail.id} Β· v{detail.version}</div>
|
|
480
|
-
</div>
|
|
481
|
-
<button className="btn btn-ghost btn-sm" onClick={closeDetail} aria-label="Close">Close</button>
|
|
728
|
+
{/* Branding Customization Modal */}
|
|
729
|
+
{brandingOpen && brandingPlugin && (
|
|
730
|
+
<div role="dialog" aria-modal="true" aria-label="Customize Plugin Branding" onClick={() => setBrandingOpen(false)}
|
|
731
|
+
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: "16px" }}>
|
|
732
|
+
<div className="card" onClick={(e) => e.stopPropagation()} style={{ width: "100%", maxWidth: "480px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
733
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
734
|
+
<div style={{ fontWeight: 700, fontSize: "16px" }}>Customize Branding: {brandingPlugin.name}</div>
|
|
735
|
+
<button className="btn btn-ghost btn-sm" onClick={() => setBrandingOpen(false)}>β</button>
|
|
482
736
|
</div>
|
|
483
|
-
|
|
484
|
-
{
|
|
485
|
-
<div className="alert alert-
|
|
486
|
-
|
|
737
|
+
|
|
738
|
+
{brandingPlugin.locked && (
|
|
739
|
+
<div className="alert alert-warning" style={{ fontSize: "12px" }}>
|
|
740
|
+
π This plugin is marked complete and locked against editing. Unlock it to change branding.
|
|
487
741
|
</div>
|
|
488
742
|
)}
|
|
489
|
-
<div role="tablist" style={{ display: "flex", gap: "8px" }}>
|
|
490
|
-
{detail.cli && <button role="tab" aria-selected={detailTab === "run"} className={`btn btn-sm ${detailTab === "run" ? "btn-primary" : "btn-ghost"}`} onClick={() => setDetailTab("run")}>Run</button>}
|
|
491
|
-
<button role="tab" aria-selected={detailTab === "details"} className={`btn btn-sm ${detailTab === "details" ? "btn-primary" : "btn-ghost"}`} onClick={() => setDetailTab("details")}>Details</button>
|
|
492
|
-
<button role="tab" aria-selected={detailTab === "readme"} className={`btn btn-sm ${detailTab === "readme" ? "btn-primary" : "btn-ghost"}`} onClick={() => setDetailTab("readme")}>README</button>
|
|
493
|
-
</div>
|
|
494
743
|
|
|
495
|
-
{
|
|
496
|
-
<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
744
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
745
|
+
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Icon (Emoji or Symbol)</label>
|
|
746
|
+
<div style={{ display: "flex", gap: "8px" }}>
|
|
747
|
+
<input
|
|
748
|
+
className="input"
|
|
749
|
+
value={brandingIcon}
|
|
750
|
+
onChange={(e) => setBrandingIcon(e.target.value)}
|
|
751
|
+
style={{ width: "80px", textAlign: "center", fontSize: "18px" }}
|
|
752
|
+
disabled={brandingPlugin.locked}
|
|
753
|
+
/>
|
|
754
|
+
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap", alignItems: "center" }}>
|
|
755
|
+
{["π", "π", "βοΈ", "π‘οΈ", "π»", "βοΈ", "π¨", "π¬", "π", "πΏ", "π", "π", "π"].map((emoji) => (
|
|
756
|
+
<button
|
|
757
|
+
key={emoji}
|
|
758
|
+
type="button"
|
|
759
|
+
className="btn btn-ghost btn-sm"
|
|
760
|
+
style={{ padding: "4px 8px", fontSize: "14px" }}
|
|
761
|
+
disabled={brandingPlugin.locked}
|
|
762
|
+
onClick={() => setBrandingIcon(emoji)}
|
|
763
|
+
>
|
|
764
|
+
{emoji}
|
|
765
|
+
</button>
|
|
766
|
+
))}
|
|
507
767
|
</div>
|
|
508
|
-
<div style={{ ...muted, fontSize: "12px" }}>
|
|
509
|
-
{detail.cli.subcommands.find((s) => s.name === subcommand)?.description}
|
|
510
|
-
{" "}Same as <code style={mono}>npx total-recall {detail.cli.command} {subcommand} {args}</code>. Runs in a separate process.
|
|
511
|
-
</div>
|
|
512
|
-
{output && (
|
|
513
|
-
<pre style={{ ...mono, margin: 0, padding: "14px", borderRadius: "var(--radius-sm)", background: "var(--bg-primary)", border: `1px solid ${output.ok ? "var(--border)" : "var(--error)"}`, whiteSpace: "pre-wrap", maxHeight: "420px", overflow: "auto" }}>
|
|
514
|
-
{output.text}
|
|
515
|
-
</pre>
|
|
516
|
-
)}
|
|
517
768
|
</div>
|
|
518
|
-
|
|
769
|
+
</div>
|
|
770
|
+
|
|
771
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
772
|
+
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Theme Color (Hex)</label>
|
|
773
|
+
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
|
774
|
+
<input
|
|
775
|
+
type="color"
|
|
776
|
+
value={brandingColor}
|
|
777
|
+
onChange={(e) => setBrandingColor(e.target.value)}
|
|
778
|
+
style={{ width: "42px", height: "38px", border: "none", cursor: "pointer", background: "none" }}
|
|
779
|
+
disabled={brandingPlugin.locked}
|
|
780
|
+
/>
|
|
781
|
+
<input
|
|
782
|
+
className="input"
|
|
783
|
+
value={brandingColor}
|
|
784
|
+
onChange={(e) => setBrandingColor(e.target.value)}
|
|
785
|
+
style={{ flex: 1, fontFamily: "var(--font-mono)" }}
|
|
786
|
+
disabled={brandingPlugin.locked}
|
|
787
|
+
/>
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
|
|
791
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
792
|
+
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Domain Badge Text</label>
|
|
793
|
+
<input
|
|
794
|
+
className="input"
|
|
795
|
+
value={brandingBadge}
|
|
796
|
+
onChange={(e) => setBrandingBadge(e.target.value)}
|
|
797
|
+
placeholder="e.g. Telephony, Quality, DNS"
|
|
798
|
+
disabled={brandingPlugin.locked}
|
|
799
|
+
/>
|
|
800
|
+
</div>
|
|
519
801
|
|
|
520
|
-
{
|
|
521
|
-
<
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
{
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
802
|
+
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "8px" }}>
|
|
803
|
+
<button type="button" className="btn btn-ghost" onClick={() => setBrandingOpen(false)}>Cancel</button>
|
|
804
|
+
<button
|
|
805
|
+
type="button"
|
|
806
|
+
className="btn btn-primary"
|
|
807
|
+
onClick={saveBranding}
|
|
808
|
+
disabled={brandingPlugin.locked || busy !== null}
|
|
809
|
+
>
|
|
810
|
+
Save Branding
|
|
811
|
+
</button>
|
|
812
|
+
</div>
|
|
813
|
+
</div>
|
|
814
|
+
</div>
|
|
815
|
+
)}
|
|
816
|
+
|
|
817
|
+
{/* Plugin Detail & Interactive Preview Drawer */}
|
|
818
|
+
{detail && (
|
|
819
|
+
<div role="dialog" aria-modal="true" aria-label={detail.name} onClick={closeDetail}
|
|
820
|
+
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", justifyContent: "flex-end", zIndex: 1000 }}>
|
|
821
|
+
<div onClick={(e) => e.stopPropagation()}
|
|
822
|
+
style={{ width: "100%", maxWidth: "680px", height: "100%", overflowY: "auto", background: "var(--bg-secondary)", borderLeft: "1px solid var(--border)", padding: "24px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
823
|
+
|
|
824
|
+
{/* Drawer Header */}
|
|
825
|
+
{(() => {
|
|
826
|
+
const branding = resolvePluginBranding(detail)
|
|
827
|
+
const PreviewComponent = getPreviewComponent(detail.id)
|
|
828
|
+
return (
|
|
829
|
+
<>
|
|
830
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px" }}>
|
|
831
|
+
<div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
|
|
832
|
+
<div
|
|
833
|
+
style={{
|
|
834
|
+
width: "44px",
|
|
835
|
+
height: "44px",
|
|
836
|
+
borderRadius: "10px",
|
|
837
|
+
background: `${branding.color}20`,
|
|
838
|
+
border: `1px solid ${branding.color}50`,
|
|
839
|
+
display: "flex",
|
|
840
|
+
alignItems: "center",
|
|
841
|
+
justifyContent: "center",
|
|
842
|
+
fontSize: "22px",
|
|
843
|
+
flexShrink: 0
|
|
844
|
+
}}
|
|
845
|
+
>
|
|
846
|
+
{branding.icon}
|
|
847
|
+
</div>
|
|
848
|
+
<div>
|
|
849
|
+
<div style={{ fontSize: "18px", fontWeight: 700 }}>{detail.name}</div>
|
|
850
|
+
<div style={{ display: "flex", gap: "8px", alignItems: "center", marginTop: "2px" }}>
|
|
851
|
+
<span style={{ ...mono, color: "var(--text-tertiary)" }}>{detail.id} Β· v{detail.version}</span>
|
|
852
|
+
<span className="badge" style={{ background: `${branding.color}15`, color: branding.color, borderColor: `${branding.color}40`, fontSize: "11px" }}>
|
|
853
|
+
{branding.badge}
|
|
854
|
+
</span>
|
|
855
|
+
{detail.locked && <span className="badge" style={{ background: "#2e1065", color: "#c084fc" }}>π Locked</span>}
|
|
545
856
|
</div>
|
|
546
|
-
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
<button className="btn btn-ghost btn-sm" onClick={closeDetail} aria-label="Close">Close</button>
|
|
860
|
+
</div>
|
|
861
|
+
|
|
862
|
+
<div style={muted}>{detail.description}</div>
|
|
863
|
+
|
|
864
|
+
{!detail.valid && (
|
|
865
|
+
<div className="alert alert-error">
|
|
866
|
+
{detail.errors.map((e) => <div key={e}>{e}</div>)}
|
|
547
867
|
</div>
|
|
548
868
|
)}
|
|
549
|
-
</Fact>
|
|
550
|
-
<div style={{ display: "flex", gap: "8px", marginTop: "16px" }}>
|
|
551
|
-
<button className="btn btn-ghost btn-sm" disabled={busy === detail.id || (!detail.valid && !detail.shared)} onClick={() => doShare(detail, !detail.shared)}>
|
|
552
|
-
{detail.shared ? "Stop sharing" : "Share"}
|
|
553
|
-
</button>
|
|
554
|
-
<button className="btn btn-ghost btn-sm" disabled={busy === detail.id} onClick={() => doRemove(detail)}>Remove</button>
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
)}
|
|
558
869
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
870
|
+
{/* Drawer Navigation Tabs */}
|
|
871
|
+
<div role="tablist" style={{ display: "flex", gap: "8px", borderBottom: "1px solid var(--border)", paddingBottom: "10px" }}>
|
|
872
|
+
{PreviewComponent && (
|
|
873
|
+
<button
|
|
874
|
+
role="tab"
|
|
875
|
+
aria-selected={detailTab === "preview"}
|
|
876
|
+
className={`btn btn-sm ${detailTab === "preview" ? "btn-primary" : "btn-ghost"}`}
|
|
877
|
+
onClick={() => setDetailTab("preview")}
|
|
878
|
+
>
|
|
879
|
+
Interactive Preview
|
|
880
|
+
</button>
|
|
881
|
+
)}
|
|
882
|
+
{detail.cli && (
|
|
883
|
+
<button
|
|
884
|
+
role="tab"
|
|
885
|
+
aria-selected={detailTab === "run"}
|
|
886
|
+
className={`btn btn-sm ${detailTab === "run" ? "btn-primary" : "btn-ghost"}`}
|
|
887
|
+
onClick={() => setDetailTab("run")}
|
|
888
|
+
>
|
|
889
|
+
Run CLI
|
|
890
|
+
</button>
|
|
891
|
+
)}
|
|
892
|
+
<button
|
|
893
|
+
role="tab"
|
|
894
|
+
aria-selected={detailTab === "details"}
|
|
895
|
+
className={`btn btn-sm ${detailTab === "details" ? "btn-primary" : "btn-ghost"}`}
|
|
896
|
+
onClick={() => setDetailTab("details")}
|
|
897
|
+
>
|
|
898
|
+
Details & Config
|
|
899
|
+
</button>
|
|
900
|
+
<button
|
|
901
|
+
role="tab"
|
|
902
|
+
aria-selected={detailTab === "readme"}
|
|
903
|
+
className={`btn btn-sm ${detailTab === "readme" ? "btn-primary" : "btn-ghost"}`}
|
|
904
|
+
onClick={() => setDetailTab("readme")}
|
|
905
|
+
>
|
|
906
|
+
README
|
|
907
|
+
</button>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
910
|
+
{/* Tab 1: 100% Functional Interactive Preview */}
|
|
911
|
+
{detailTab === "preview" && (
|
|
912
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
|
913
|
+
{PreviewComponent ? (
|
|
914
|
+
<PreviewComponent />
|
|
915
|
+
) : (
|
|
916
|
+
<PreviewFallback pluginId={detail.id} />
|
|
917
|
+
)}
|
|
918
|
+
</div>
|
|
919
|
+
)}
|
|
920
|
+
|
|
921
|
+
{/* Tab 2: CLI Command Runner */}
|
|
922
|
+
{detailTab === "run" && detail.cli && (
|
|
923
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
|
924
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
925
|
+
<select className="select" value={subcommand} onChange={(e) => setSubcommand(e.target.value)} aria-label="Subcommand" style={{ paddingRight: "32px" }}>
|
|
926
|
+
{(detail.cli.subcommands.length ? detail.cli.subcommands : [{ name: "" }]).map((s) => (
|
|
927
|
+
<option key={s.name} value={s.name}>{s.name || "(default)"}</option>
|
|
928
|
+
))}
|
|
929
|
+
</select>
|
|
930
|
+
<input className="input" value={args} onChange={(e) => setArgs(e.target.value)} placeholder="extra arguments" aria-label="Arguments" style={{ flex: 1, minWidth: "160px" }} />
|
|
931
|
+
<button className="btn btn-primary" onClick={runCommand} disabled={busy === `run:${detail.id}` || !detail.valid}>
|
|
932
|
+
{busy === `run:${detail.id}` ? "Runningβ¦" : "Run"}
|
|
933
|
+
</button>
|
|
934
|
+
</div>
|
|
935
|
+
<div style={{ ...muted, fontSize: "12px" }}>
|
|
936
|
+
{detail.cli.subcommands.find((s) => s.name === subcommand)?.description}
|
|
937
|
+
{" "}Same as <code style={mono}>npx total-recall {detail.cli.command} {subcommand} {args}</code>. Runs in a separate process.
|
|
938
|
+
</div>
|
|
939
|
+
{output && (
|
|
940
|
+
<pre style={{ ...mono, margin: 0, padding: "14px", borderRadius: "var(--radius-sm)", background: "var(--bg-primary)", border: `1px solid ${output.ok ? "var(--border)" : "var(--error)"}`, whiteSpace: "pre-wrap", maxHeight: "420px", overflow: "auto" }}>
|
|
941
|
+
{output.text}
|
|
942
|
+
</pre>
|
|
943
|
+
)}
|
|
944
|
+
</div>
|
|
945
|
+
)}
|
|
946
|
+
|
|
947
|
+
{/* Tab 3: Plugin Facts & Config Controls */}
|
|
948
|
+
{detailTab === "details" && (
|
|
949
|
+
<div>
|
|
950
|
+
<Fact label="Scope">{detail.scope}{detail.linked ? " (linked folder)" : ""}</Fact>
|
|
951
|
+
<Fact label="Source">{sourceLabel(detail)} β <span style={mono}>{detail.source.ref}</span></Fact>
|
|
952
|
+
<Fact label="Installed">{detail.installed_at ? new Date(detail.installed_at).toLocaleString() : "no install record"}</Fact>
|
|
953
|
+
<Fact label="Content sha256"><span style={mono}>{detail.sha256 || "β"}</span></Fact>
|
|
954
|
+
{detail.modified_since_install && <Fact label="At install"><span style={mono}>{detail.installed_sha256}</span></Fact>}
|
|
955
|
+
<Fact label="Files">{detail.file_count ?? "β"} Β· {formatBytes(detail.size_bytes)}</Fact>
|
|
956
|
+
<Fact label="Shared">{detail.shared ? "yes" : "no"}</Fact>
|
|
957
|
+
<Fact label="Lock Status">{detail.locked ? "π Locked (Completed)" : "π Unlocked (Editable)"}</Fact>
|
|
958
|
+
<Fact label="Store Status">{detail.store_deployed ? "πͺ Published in Plugin Store" : "Not yet deployed to store"}</Fact>
|
|
959
|
+
{detail.shared && <Fact label="Public link">{detail.share_url ? <input className="input" aria-label="Public plugin share link" readOnly value={detail.share_url} onFocus={(e) => e.currentTarget.select()} /> : "Set TR_PUBLIC_BASE_URL to your public HTTPS origin to create a link."}</Fact>}
|
|
960
|
+
<Fact label="Use cases">{detail.use_cases.join(", ") || "β"}</Fact>
|
|
961
|
+
{detail.author && <Fact label="Author">{detail.author}</Fact>}
|
|
962
|
+
{detail.license && <Fact label="License">{detail.license}</Fact>}
|
|
963
|
+
<Fact label="Location"><span style={mono}>{detail.dir}</span></Fact>
|
|
964
|
+
<Fact label="Agent context">{detail.has_generator ? "adds a block to compiled instructions" : "β"}</Fact>
|
|
965
|
+
<Fact label="Memory categories">
|
|
966
|
+
{detail.categories.length ? detail.categories.map((c) => c.name).join(", ") : "β"}
|
|
967
|
+
</Fact>
|
|
968
|
+
<Fact label="Scheduled tasks">
|
|
969
|
+
{detail.tasks.length === 0 ? "β" : (
|
|
970
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
971
|
+
{detail.tasks.map((t) => (
|
|
972
|
+
<div key={t.command}>
|
|
973
|
+
<span style={mono}>{t.schedule}</span> {t.command} β {t.intent}
|
|
974
|
+
<div style={{ color: "var(--text-tertiary)", fontSize: "12px" }}>last run: {t.last_run || "not yet"}</div>
|
|
975
|
+
</div>
|
|
976
|
+
))}
|
|
977
|
+
</div>
|
|
978
|
+
)}
|
|
979
|
+
</Fact>
|
|
980
|
+
|
|
981
|
+
{/* Config Actions Toolbar inside drawer */}
|
|
982
|
+
<div style={{ display: "flex", gap: "8px", marginTop: "16px", flexWrap: "wrap" }}>
|
|
983
|
+
<button
|
|
984
|
+
className="btn btn-ghost btn-sm"
|
|
985
|
+
onClick={() => openBrandingModal(detail)}
|
|
986
|
+
disabled={detail.locked}
|
|
987
|
+
>
|
|
988
|
+
π¨ Customize Branding
|
|
989
|
+
</button>
|
|
990
|
+
<button
|
|
991
|
+
className="btn btn-ghost btn-sm"
|
|
992
|
+
onClick={() => doToggleLock(detail)}
|
|
993
|
+
disabled={busy === detail.id}
|
|
994
|
+
>
|
|
995
|
+
{detail.locked ? "π Unlock Editing" : "π Lock When Complete"}
|
|
996
|
+
</button>
|
|
997
|
+
<button
|
|
998
|
+
className="btn btn-primary btn-sm"
|
|
999
|
+
onClick={() => doDeployStore(detail)}
|
|
1000
|
+
disabled={busy === detail.id || !detail.valid}
|
|
1001
|
+
>
|
|
1002
|
+
{detail.store_deployed ? "β Deployed to Store" : "π Deploy to Plugin Store"}
|
|
1003
|
+
</button>
|
|
1004
|
+
<button
|
|
1005
|
+
className="btn btn-ghost btn-sm"
|
|
1006
|
+
disabled={busy === detail.id || (!detail.valid && !detail.shared)}
|
|
1007
|
+
onClick={() => doShare(detail, !detail.shared)}
|
|
1008
|
+
>
|
|
1009
|
+
{detail.shared ? "Stop sharing" : "Share"}
|
|
1010
|
+
</button>
|
|
1011
|
+
<button
|
|
1012
|
+
className="btn btn-ghost btn-sm"
|
|
1013
|
+
disabled={busy === detail.id}
|
|
1014
|
+
onClick={() => doRemove(detail)}
|
|
1015
|
+
>
|
|
1016
|
+
Remove
|
|
1017
|
+
</button>
|
|
1018
|
+
</div>
|
|
1019
|
+
</div>
|
|
1020
|
+
)}
|
|
1021
|
+
|
|
1022
|
+
{/* Tab 4: Markdown Readme */}
|
|
1023
|
+
{detailTab === "readme" && (
|
|
1024
|
+
<div style={{ fontSize: "14px", lineHeight: 1.6 }}>{readme ? renderMarkdown(readme) : <span style={muted}>Loadingβ¦</span>}</div>
|
|
1025
|
+
)}
|
|
1026
|
+
</>
|
|
1027
|
+
)
|
|
1028
|
+
})()}
|
|
562
1029
|
</div>
|
|
563
1030
|
</div>
|
|
564
1031
|
)}
|