total-recall-brain 3.28.2 → 3.30.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 +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +32 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
|
@@ -1,1328 +1,564 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo, useCallback } from "react"
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback, type CSSProperties, type ReactNode } from "react"
|
|
2
|
+
import { useSearchParams } from "react-router-dom"
|
|
3
|
+
import {
|
|
4
|
+
fetchPlugins,
|
|
5
|
+
fetchBundledPlugins,
|
|
6
|
+
fetchPeerPlugins,
|
|
7
|
+
installPlugin,
|
|
8
|
+
removePlugin,
|
|
9
|
+
setPluginShared,
|
|
8
10
|
runPluginCommand,
|
|
9
11
|
fetchPluginReadme,
|
|
10
|
-
type PluginInfo,
|
|
11
|
-
type
|
|
12
|
+
type PluginInfo,
|
|
13
|
+
type BundledPlugin,
|
|
14
|
+
type PeersResponse,
|
|
15
|
+
type PeerNode
|
|
12
16
|
} from "../api"
|
|
13
|
-
import {
|
|
17
|
+
import { renderMarkdown } from "../components/MarkdownUtils"
|
|
14
18
|
|
|
15
19
|
interface PluginsPageProps {
|
|
16
20
|
activeBrainId?: string | null
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<span style={{ fontWeight: "700", color: "var(--text-primary)" }}>{rating.toFixed(1)}</span>
|
|
31
|
-
{reviewCount !== undefined && (
|
|
32
|
-
<span style={{ color: "var(--text-tertiary)" }}>({reviewCount})</span>
|
|
33
|
-
)}
|
|
34
|
-
{installCount && (
|
|
35
|
-
<>
|
|
36
|
-
<span style={{ color: "var(--text-tertiary)" }}>•</span>
|
|
37
|
-
<span style={{ color: "var(--text-secondary)" }}>{installCount} installs</span>
|
|
38
|
-
</>
|
|
39
|
-
)}
|
|
40
|
-
</div>
|
|
41
|
-
)
|
|
23
|
+
type Tab = "installed" | "bundled" | "mesh"
|
|
24
|
+
type DetailTab = "run" | "readme" | "details"
|
|
25
|
+
type Alert = { type: "success" | "error"; message: string } | null
|
|
26
|
+
|
|
27
|
+
const PEER_STATUS_LABEL: Record<PeerNode["status"], string> = {
|
|
28
|
+
ok: "online",
|
|
29
|
+
offline: "offline",
|
|
30
|
+
unreachable: "unreachable",
|
|
31
|
+
not_configured: "mesh sync not configured",
|
|
32
|
+
unsupported: "no plugin sharing (older version)",
|
|
33
|
+
error: "error"
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
currentRating,
|
|
47
|
-
onRated
|
|
48
|
-
}: {
|
|
49
|
-
pluginId: string;
|
|
50
|
-
currentRating?: number | null;
|
|
51
|
-
onRated: (score: number) => void
|
|
52
|
-
}) {
|
|
53
|
-
const [hoverRating, setHoverRating] = useState<number | null>(null)
|
|
54
|
-
const [submitting, setSubmitting] = useState(false)
|
|
36
|
+
const mono: CSSProperties = { fontFamily: "var(--font-mono)", fontSize: "12px" }
|
|
37
|
+
const muted: CSSProperties = { color: "var(--text-secondary)", fontSize: "13px", lineHeight: 1.5 }
|
|
55
38
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
39
|
+
function shortHash(sha: string | null | undefined) {
|
|
40
|
+
return sha ? `${sha.slice(0, 12)}…` : "—"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatBytes(n: number | null | undefined) {
|
|
44
|
+
if (n === null || n === undefined) return "—"
|
|
45
|
+
if (n < 1024) return `${n} B`
|
|
46
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
47
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sourceLabel(p: PluginInfo) {
|
|
51
|
+
switch (p.source.kind) {
|
|
52
|
+
case "peer": return `from ${p.source.peer_hostname || "a peer"}`
|
|
53
|
+
case "bundled": return "bundled"
|
|
54
|
+
case "git": return "git"
|
|
55
|
+
case "public": return "direct link"
|
|
56
|
+
case "link": return "linked folder"
|
|
57
|
+
default: return "local folder"
|
|
63
58
|
}
|
|
59
|
+
}
|
|
64
60
|
|
|
65
|
-
|
|
61
|
+
function UseCaseChips({ useCases }: { useCases: string[] }) {
|
|
62
|
+
if (!useCases.length) return null
|
|
63
|
+
return (
|
|
64
|
+
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
|
|
65
|
+
{useCases.map((u) => (
|
|
66
|
+
<span key={u} className="badge" style={{ fontSize: "11px" }}>{u}</span>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
66
71
|
|
|
72
|
+
function Fact({ label, children }: { label: string; children: ReactNode }) {
|
|
67
73
|
return (
|
|
68
|
-
<div style={{ display: "flex",
|
|
69
|
-
<span style={{ color: "var(--text-tertiary)"
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
transition: "transform 0.1s ease",
|
|
81
|
-
transform: hoverRating === star ? "scale(1.2)" : "scale(1)"
|
|
82
|
-
}}
|
|
83
|
-
title={`Rate ${star} star${star > 1 ? "s" : ""}`}
|
|
84
|
-
>
|
|
85
|
-
★
|
|
86
|
-
</span>
|
|
87
|
-
))}
|
|
88
|
-
</div>
|
|
89
|
-
{currentRating && (
|
|
90
|
-
<span style={{ color: "var(--success)", fontSize: "11px", fontWeight: "600" }}>
|
|
91
|
-
(You rated {currentRating}★)
|
|
92
|
-
</span>
|
|
93
|
-
)}
|
|
74
|
+
<div style={{ display: "flex", gap: "12px", fontSize: "13px", padding: "6px 0", borderBottom: "1px solid var(--border)" }}>
|
|
75
|
+
<span style={{ width: "132px", flexShrink: 0, color: "var(--text-tertiary)" }}>{label}</span>
|
|
76
|
+
<span style={{ color: "var(--text-primary)", minWidth: 0, overflowWrap: "anywhere" }}>{children}</span>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Empty({ title, children }: { title: string; children?: ReactNode }) {
|
|
82
|
+
return (
|
|
83
|
+
<div className="card" style={{ textAlign: "center", padding: "40px 24px" }}>
|
|
84
|
+
<div style={{ fontWeight: 600, marginBottom: "6px" }}>{title}</div>
|
|
85
|
+
{children && <div style={muted}>{children}</div>}
|
|
94
86
|
</div>
|
|
95
87
|
)
|
|
96
88
|
}
|
|
97
89
|
|
|
98
90
|
export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
99
91
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
100
|
-
const [
|
|
101
|
-
const [
|
|
92
|
+
const [tab, setTab] = useState<Tab>("installed")
|
|
93
|
+
const [installed, setInstalled] = useState<PluginInfo[]>([])
|
|
94
|
+
const [bundled, setBundled] = useState<BundledPlugin[]>([])
|
|
95
|
+
const [peers, setPeers] = useState<PeersResponse | null>(null)
|
|
96
|
+
const [peersLoading, setPeersLoading] = useState(false)
|
|
102
97
|
const [loading, setLoading] = useState(true)
|
|
103
|
-
const [
|
|
104
|
-
const [
|
|
105
|
-
const [
|
|
106
|
-
const [
|
|
107
|
-
|
|
108
|
-
// Plugin Detail & Runner Modal State
|
|
109
|
-
const [detailModalPlugin, setDetailModalPlugin] = useState<PluginInfo | null>(null)
|
|
110
|
-
const [detailTab, setDetailTab] = useState<"runner" | "readme" | "schemas" | "tasks" | "manifest">("runner")
|
|
111
|
-
const [subcommand, setSubcommand] = useState("")
|
|
112
|
-
const [customArgs, setCustomArgs] = useState("")
|
|
113
|
-
const [executing, setExecuting] = useState(false)
|
|
114
|
-
const [consoleOutput, setConsoleOutput] = useState("")
|
|
115
|
-
const [readmeText, setReadmeText] = useState("")
|
|
116
|
-
const [readmeLoading, setReadmeLoading] = useState(false)
|
|
98
|
+
const [query, setQuery] = useState("")
|
|
99
|
+
const [useCase, setUseCase] = useState("")
|
|
100
|
+
const [busy, setBusy] = useState<string | null>(null)
|
|
101
|
+
const [alert, setAlert] = useState<Alert>(null)
|
|
117
102
|
|
|
118
|
-
|
|
103
|
+
const [installOpen, setInstallOpen] = useState(false)
|
|
119
104
|
const [installSource, setInstallSource] = useState("")
|
|
120
105
|
const [installLink, setInstallLink] = useState(false)
|
|
121
106
|
const [installGlobal, setInstallGlobal] = useState(false)
|
|
122
|
-
const [installing, setInstalling] = useState(false)
|
|
123
|
-
const [alert, setAlert] = useState<{ type: "success" | "error"; message: string } | null>(null)
|
|
124
107
|
|
|
125
|
-
const
|
|
108
|
+
const [detail, setDetail] = useState<PluginInfo | null>(null)
|
|
109
|
+
const [detailTab, setDetailTab] = useState<DetailTab>("run")
|
|
110
|
+
const [subcommand, setSubcommand] = useState("")
|
|
111
|
+
const [args, setArgs] = useState("")
|
|
112
|
+
const [output, setOutput] = useState<{ text: string; ok: boolean } | null>(null)
|
|
113
|
+
const [readme, setReadme] = useState("")
|
|
114
|
+
|
|
115
|
+
const loadLocal = useCallback(async () => {
|
|
126
116
|
setLoading(true)
|
|
127
117
|
try {
|
|
128
|
-
const [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
])
|
|
132
|
-
setPlugins(installedList)
|
|
133
|
-
setCatalog(catalogList)
|
|
118
|
+
const [i, b] = await Promise.all([fetchPlugins(), fetchBundledPlugins()])
|
|
119
|
+
setInstalled(i)
|
|
120
|
+
setBundled(b)
|
|
134
121
|
} finally {
|
|
135
122
|
setLoading(false)
|
|
136
123
|
}
|
|
137
124
|
}, [])
|
|
138
125
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}, [loadData, activeBrainId])
|
|
142
|
-
|
|
143
|
-
const openDetail = async (p: PluginInfo, tab: "runner" | "readme" | "schemas" | "tasks" | "manifest" = "runner") => {
|
|
144
|
-
setDetailModalPlugin(p)
|
|
145
|
-
const subcommands = (p.manifest as any)?.cli?.subcommands as Array<{ name: string; description: string }> | undefined
|
|
146
|
-
const defaultSub = subcommands?.[0]?.name || (p.cli?.command === "git-sentinel" ? "audit" : "status")
|
|
147
|
-
setSubcommand(defaultSub)
|
|
148
|
-
setCustomArgs("")
|
|
149
|
-
setConsoleOutput("")
|
|
150
|
-
setDetailTab(tab)
|
|
151
|
-
setReadmeLoading(true)
|
|
126
|
+
const loadPeers = useCallback(async () => {
|
|
127
|
+
setPeersLoading(true)
|
|
152
128
|
try {
|
|
153
|
-
|
|
154
|
-
setReadmeText(rm)
|
|
129
|
+
setPeers(await fetchPeerPlugins())
|
|
155
130
|
} finally {
|
|
156
|
-
|
|
131
|
+
setPeersLoading(false)
|
|
157
132
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const closeDetail = () => {
|
|
161
|
-
setDetailModalPlugin(null)
|
|
162
|
-
if (searchParams.has("id")) {
|
|
163
|
-
const next = new URLSearchParams(searchParams)
|
|
164
|
-
next.delete("id")
|
|
165
|
-
setSearchParams(next)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const handleExecute = async () => {
|
|
170
|
-
if (!detailModalPlugin) return
|
|
171
|
-
setExecuting(true)
|
|
172
|
-
const args = customArgs.trim() ? customArgs.trim().split(/\s+/) : []
|
|
173
|
-
const res = await runPluginCommand(detailModalPlugin.id, subcommand, args)
|
|
174
|
-
setExecuting(false)
|
|
175
|
-
if (res.success) {
|
|
176
|
-
setConsoleOutput(res.output || "(Command completed with no output)")
|
|
177
|
-
} else {
|
|
178
|
-
setConsoleOutput(`❌ Error: ${res.error || "Failed to execute command"}`)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
133
|
+
}, [])
|
|
181
134
|
|
|
182
135
|
useEffect(() => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const match = plugins.find(p => p.id === idParam)
|
|
186
|
-
if (match && (!detailModalPlugin || detailModalPlugin.id !== idParam)) {
|
|
187
|
-
openDetail(match, "runner")
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}, [searchParams, plugins])
|
|
136
|
+
loadLocal()
|
|
137
|
+
}, [loadLocal, activeBrainId])
|
|
191
138
|
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (tab === "mesh" && peers === null && !peersLoading) loadPeers()
|
|
141
|
+
}, [tab, peers, peersLoading, loadPeers])
|
|
142
|
+
|
|
143
|
+
const allUseCases = useMemo(() => {
|
|
144
|
+
const set = new Set<string>()
|
|
145
|
+
for (const p of installed) p.use_cases.forEach((u) => set.add(u))
|
|
146
|
+
for (const p of bundled) p.use_cases.forEach((u) => set.add(u))
|
|
147
|
+
for (const n of peers?.peers || []) n.plugins.forEach((p) => p.use_cases.forEach((u) => set.add(u)))
|
|
148
|
+
return Array.from(set).sort()
|
|
149
|
+
}, [installed, bundled, peers])
|
|
150
|
+
|
|
151
|
+
const matches = useCallback((p: { id: string; name: string; description: string; use_cases: string[] }) => {
|
|
152
|
+
if (useCase && !p.use_cases.includes(useCase)) return false
|
|
153
|
+
const q = query.trim().toLowerCase()
|
|
154
|
+
if (!q) return true
|
|
155
|
+
return [p.id, p.name, p.description, ...p.use_cases].some((v) => v.toLowerCase().includes(q))
|
|
156
|
+
}, [query, useCase])
|
|
157
|
+
|
|
158
|
+
const doInstall = async (source: string, opts: { link?: boolean; global?: boolean } = {}) => {
|
|
159
|
+
setBusy(source)
|
|
197
160
|
setAlert(null)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
source: installSource.trim(),
|
|
201
|
-
link: installLink,
|
|
202
|
-
global: installGlobal
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
setInstalling(false)
|
|
206
|
-
|
|
161
|
+
const res = await installPlugin({ source, ...opts })
|
|
162
|
+
setBusy(null)
|
|
207
163
|
if (res.success) {
|
|
208
|
-
setAlert({ type: "success", message: res.message ||
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
} else {
|
|
213
|
-
setAlert({ type: "error", message: res.error || "Failed to install plugin" })
|
|
164
|
+
setAlert({ type: "success", message: res.message || `Installed ${source}` })
|
|
165
|
+
await loadLocal()
|
|
166
|
+
if (peers) loadPeers()
|
|
167
|
+
return true
|
|
214
168
|
}
|
|
169
|
+
setAlert({ type: "error", message: res.error || `Could not install ${source}` })
|
|
170
|
+
return false
|
|
215
171
|
}
|
|
216
172
|
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const res = await
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
global: false
|
|
224
|
-
})
|
|
225
|
-
setInstalling(false)
|
|
226
|
-
|
|
173
|
+
const doRemove = async (p: PluginInfo) => {
|
|
174
|
+
if (!window.confirm(`Remove "${p.name}" (${p.scope})?`)) return
|
|
175
|
+
setBusy(p.id)
|
|
176
|
+
const res = await removePlugin(p.id, p.scope === "global")
|
|
177
|
+
setBusy(null)
|
|
178
|
+
setAlert(res.success ? { type: "success", message: `Removed ${p.name}` } : { type: "error", message: res.error || "Remove failed" })
|
|
227
179
|
if (res.success) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
} else {
|
|
231
|
-
setAlert({ type: "error", message: res.error || `Failed to install ${item.name}` })
|
|
180
|
+
if (detail?.id === p.id) closeDetail()
|
|
181
|
+
loadLocal()
|
|
232
182
|
}
|
|
233
183
|
}
|
|
234
184
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (res.success) {
|
|
240
|
-
setAlert({ type: "
|
|
241
|
-
|
|
242
|
-
} else {
|
|
243
|
-
setAlert({ type: "error", message: res.error || "Failed to uninstall plugin" })
|
|
185
|
+
const doShare = async (p: PluginInfo, shared: boolean) => {
|
|
186
|
+
setBusy(p.id)
|
|
187
|
+
const res = await setPluginShared(p.id, shared)
|
|
188
|
+
setBusy(null)
|
|
189
|
+
if (!res.success) {
|
|
190
|
+
setAlert({ type: "error", message: res.error || "Could not change sharing" })
|
|
191
|
+
return
|
|
244
192
|
}
|
|
193
|
+
if (res.plugin && detail?.id === p.id) setDetail(res.plugin)
|
|
194
|
+
setAlert({ type: "success", message: shared ? `${p.name} is shared. Its public link is in the details.` : `${p.name} is no longer shared` })
|
|
195
|
+
loadLocal()
|
|
245
196
|
}
|
|
246
197
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
198
|
+
const openDetail = async (p: PluginInfo) => {
|
|
199
|
+
setDetail(p)
|
|
200
|
+
setDetailTab(p.cli ? "run" : "details")
|
|
201
|
+
setSubcommand(p.cli?.subcommands?.[0]?.name || "")
|
|
202
|
+
setArgs("")
|
|
203
|
+
setOutput(null)
|
|
204
|
+
setReadme("")
|
|
205
|
+
setReadme(await fetchPluginReadme(p.id))
|
|
250
206
|
}
|
|
251
207
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
(p.cli?.command && p.cli.command.toLowerCase().includes(q))
|
|
259
|
-
)
|
|
260
|
-
|
|
261
|
-
if (sortBy === "rating") {
|
|
262
|
-
list.sort((a, b) => (b.rating || 0) - (a.rating || 0))
|
|
263
|
-
} else if (sortBy === "reviews") {
|
|
264
|
-
list.sort((a, b) => (b.reviewCount || 0) - (a.reviewCount || 0))
|
|
265
|
-
} else {
|
|
266
|
-
list.sort((a, b) => a.name.localeCompare(b.name))
|
|
208
|
+
const closeDetail = () => {
|
|
209
|
+
setDetail(null)
|
|
210
|
+
if (searchParams.has("id")) {
|
|
211
|
+
const next = new URLSearchParams(searchParams)
|
|
212
|
+
next.delete("id")
|
|
213
|
+
setSearchParams(next)
|
|
267
214
|
}
|
|
268
|
-
|
|
269
|
-
}, [plugins, searchQuery, sortBy])
|
|
215
|
+
}
|
|
270
216
|
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
const id = searchParams.get("id")
|
|
219
|
+
if (!id || detail?.id === id) return
|
|
220
|
+
const match = installed.find((p) => p.id === id)
|
|
221
|
+
if (match) openDetail(match)
|
|
222
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
223
|
+
}, [searchParams, installed])
|
|
224
|
+
|
|
225
|
+
// Keep an open detail panel in sync after share/remove/reload.
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (!detail) return
|
|
228
|
+
const fresh = installed.find((p) => p.id === detail.id)
|
|
229
|
+
if (fresh && fresh !== detail) setDetail(fresh)
|
|
230
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
231
|
+
}, [installed])
|
|
232
|
+
|
|
233
|
+
const runCommand = async () => {
|
|
234
|
+
if (!detail) return
|
|
235
|
+
setBusy(`run:${detail.id}`)
|
|
236
|
+
const res = await runPluginCommand(detail.id, subcommand, args.trim() ? args.trim().split(/\s+/) : [])
|
|
237
|
+
setBusy(null)
|
|
238
|
+
if (res.error) setOutput({ text: res.error, ok: false })
|
|
239
|
+
else setOutput({ text: (res.output || "").trim() || "(no output)", ok: res.success })
|
|
240
|
+
}
|
|
279
241
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
242
|
+
const filteredInstalled = installed.filter(matches)
|
|
243
|
+
const filteredBundled = bundled.filter(matches)
|
|
244
|
+
|
|
245
|
+
const tabButton = (id: Tab, label: string, count?: number) => (
|
|
246
|
+
<button
|
|
247
|
+
key={id}
|
|
248
|
+
role="tab"
|
|
249
|
+
aria-selected={tab === id}
|
|
250
|
+
className={`btn btn-sm ${tab === id ? "btn-primary" : "btn-ghost"}`}
|
|
251
|
+
onClick={() => setTab(id)}
|
|
252
|
+
>
|
|
253
|
+
{label}
|
|
254
|
+
{count !== undefined && <span style={{ opacity: 0.75, fontWeight: 500 }}>{count}</span>}
|
|
255
|
+
</button>
|
|
256
|
+
)
|
|
289
257
|
|
|
290
258
|
return (
|
|
291
|
-
<div style={{
|
|
292
|
-
{
|
|
293
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "28px" }}>
|
|
259
|
+
<div className="page" style={{ maxWidth: "1200px", margin: "0 auto", width: "100%" }}>
|
|
260
|
+
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "16px", flexWrap: "wrap" }}>
|
|
294
261
|
<div>
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
<span style={{
|
|
300
|
-
background: "var(--accent-muted)",
|
|
301
|
-
color: "var(--accent-hover)",
|
|
302
|
-
fontSize: "12px",
|
|
303
|
-
fontWeight: "600",
|
|
304
|
-
padding: "3px 10px",
|
|
305
|
-
borderRadius: "20px",
|
|
306
|
-
border: "1px solid var(--border-accent)"
|
|
307
|
-
}}>
|
|
308
|
-
{plugins.length} Installed
|
|
309
|
-
</span>
|
|
310
|
-
</div>
|
|
311
|
-
<p style={{ margin: 0, color: "var(--text-secondary)", fontSize: "14px", lineHeight: "1.5" }}>
|
|
312
|
-
Modular autonomous engines, scholarly daemons, and custom SSSS graph schemas extending your Total Recall brain.
|
|
262
|
+
<h1>Plugins</h1>
|
|
263
|
+
<p>
|
|
264
|
+
Plugins shape Total Recall for a particular use — memory categories, agent context, commands and scheduled jobs.
|
|
265
|
+
Share them directly with other users through a hash-pinned HTTPS link. Your mesh peers can use the same shared plugins.
|
|
313
266
|
</p>
|
|
314
267
|
</div>
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
onClick={() => { setInstallModalOpen(true); setAlert(null); }}
|
|
318
|
-
style={{
|
|
319
|
-
display: "flex",
|
|
320
|
-
alignItems: "center",
|
|
321
|
-
gap: "8px",
|
|
322
|
-
background: "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)",
|
|
323
|
-
color: "#ffffff",
|
|
324
|
-
border: "none",
|
|
325
|
-
borderRadius: "var(--radius-md)",
|
|
326
|
-
padding: "10px 18px",
|
|
327
|
-
fontSize: "14px",
|
|
328
|
-
fontWeight: "600",
|
|
329
|
-
cursor: "pointer",
|
|
330
|
-
boxShadow: "0 4px 14px var(--accent-glow)",
|
|
331
|
-
transition: "all 0.15s ease"
|
|
332
|
-
}}
|
|
333
|
-
onMouseOver={(e) => (e.currentTarget.style.filter = "brightness(1.1)")}
|
|
334
|
-
onMouseOut={(e) => (e.currentTarget.style.filter = "brightness(1.0)")}
|
|
335
|
-
>
|
|
336
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
|
|
337
|
-
<line x1="12" y1="5" x2="12" y2="19" />
|
|
338
|
-
<line x1="5" y1="12" x2="19" y2="12" />
|
|
339
|
-
</svg>
|
|
340
|
-
Install Plugin
|
|
268
|
+
<button className="btn btn-primary" onClick={() => { setInstallOpen(true); setAlert(null) }}>
|
|
269
|
+
Install from source…
|
|
341
270
|
</button>
|
|
342
271
|
</div>
|
|
343
272
|
|
|
344
|
-
{/* Alert Banner */}
|
|
345
273
|
{alert && (
|
|
346
|
-
<div
|
|
347
|
-
|
|
348
|
-
borderRadius: "var(--radius-md)",
|
|
349
|
-
marginBottom: "24px",
|
|
350
|
-
fontSize: "14px",
|
|
351
|
-
display: "flex",
|
|
352
|
-
justifyContent: "space-between",
|
|
353
|
-
alignItems: "center",
|
|
354
|
-
background: alert.type === "success" ? "var(--success-muted)" : "var(--error-muted)",
|
|
355
|
-
border: `1px solid ${alert.type === "success" ? "var(--success)" : "var(--error)"}`,
|
|
356
|
-
color: alert.type === "success" ? "var(--success)" : "var(--error)"
|
|
357
|
-
}}>
|
|
274
|
+
<div className={`alert ${alert.type === "success" ? "alert-success" : "alert-error"}`} role="status"
|
|
275
|
+
style={{ display: "flex", justifyContent: "space-between", gap: "12px", marginBottom: "20px" }}>
|
|
358
276
|
<span>{alert.message}</span>
|
|
359
|
-
<button
|
|
360
|
-
onClick={() => setAlert(null)}
|
|
361
|
-
style={{ background: "none", border: "none", color: "inherit", cursor: "pointer", fontSize: "16px" }}
|
|
362
|
-
>
|
|
363
|
-
×
|
|
364
|
-
</button>
|
|
277
|
+
<button onClick={() => setAlert(null)} aria-label="Dismiss" style={{ background: "none", border: "none", color: "inherit", cursor: "pointer" }}>×</button>
|
|
365
278
|
</div>
|
|
366
279
|
)}
|
|
367
280
|
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
borderBottom: "1px solid var(--border)",
|
|
374
|
-
paddingBottom: "16px",
|
|
375
|
-
marginBottom: "24px",
|
|
376
|
-
gap: "16px",
|
|
377
|
-
flexWrap: "wrap"
|
|
378
|
-
}}>
|
|
379
|
-
<div style={{ display: "flex", gap: "8px" }}>
|
|
380
|
-
<button
|
|
381
|
-
onClick={() => setActiveTab("installed")}
|
|
382
|
-
style={{
|
|
383
|
-
background: activeTab === "installed" ? "var(--bg-elevated)" : "transparent",
|
|
384
|
-
color: activeTab === "installed" ? "var(--text-primary)" : "var(--text-secondary)",
|
|
385
|
-
border: activeTab === "installed" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
386
|
-
borderRadius: "var(--radius-sm)",
|
|
387
|
-
padding: "8px 16px",
|
|
388
|
-
fontSize: "14px",
|
|
389
|
-
fontWeight: "600",
|
|
390
|
-
cursor: "pointer",
|
|
391
|
-
display: "flex",
|
|
392
|
-
alignItems: "center",
|
|
393
|
-
gap: "8px"
|
|
394
|
-
}}
|
|
395
|
-
>
|
|
396
|
-
<span>Installed</span>
|
|
397
|
-
<span style={{
|
|
398
|
-
background: "var(--bg-tertiary)",
|
|
399
|
-
padding: "2px 7px",
|
|
400
|
-
borderRadius: "10px",
|
|
401
|
-
fontSize: "12px",
|
|
402
|
-
color: "var(--text-tertiary)"
|
|
403
|
-
}}>
|
|
404
|
-
{plugins.length}
|
|
405
|
-
</span>
|
|
406
|
-
</button>
|
|
407
|
-
|
|
408
|
-
<button
|
|
409
|
-
onClick={() => setActiveTab("catalog")}
|
|
410
|
-
style={{
|
|
411
|
-
background: activeTab === "catalog" ? "var(--bg-elevated)" : "transparent",
|
|
412
|
-
color: activeTab === "catalog" ? "var(--text-primary)" : "var(--text-secondary)",
|
|
413
|
-
border: activeTab === "catalog" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
414
|
-
borderRadius: "var(--radius-sm)",
|
|
415
|
-
padding: "8px 16px",
|
|
416
|
-
fontSize: "14px",
|
|
417
|
-
fontWeight: "600",
|
|
418
|
-
cursor: "pointer",
|
|
419
|
-
display: "flex",
|
|
420
|
-
alignItems: "center",
|
|
421
|
-
gap: "8px"
|
|
422
|
-
}}
|
|
423
|
-
>
|
|
424
|
-
<span>Discover Catalog</span>
|
|
425
|
-
<span style={{
|
|
426
|
-
background: "var(--accent-muted)",
|
|
427
|
-
color: "var(--accent-hover)",
|
|
428
|
-
padding: "2px 7px",
|
|
429
|
-
borderRadius: "10px",
|
|
430
|
-
fontSize: "12px"
|
|
431
|
-
}}>
|
|
432
|
-
{catalog.length}
|
|
433
|
-
</span>
|
|
434
|
-
</button>
|
|
281
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px", flexWrap: "wrap", marginBottom: "20px" }}>
|
|
282
|
+
<div role="tablist" style={{ display: "flex", gap: "8px" }}>
|
|
283
|
+
{tabButton("installed", "Installed", installed.length)}
|
|
284
|
+
{tabButton("bundled", "Bundled", bundled.length)}
|
|
285
|
+
{tabButton("mesh", "On the mesh")}
|
|
435
286
|
</div>
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
style={{
|
|
444
|
-
background: "var(--bg-secondary)",
|
|
445
|
-
border: "1px solid var(--border)",
|
|
446
|
-
borderRadius: "var(--radius-sm)",
|
|
447
|
-
color: "var(--text-primary)",
|
|
448
|
-
padding: "6px 10px",
|
|
449
|
-
fontSize: "13px",
|
|
450
|
-
outline: "none",
|
|
451
|
-
cursor: "pointer"
|
|
452
|
-
}}
|
|
453
|
-
>
|
|
454
|
-
<option value="rating">Highest Rated</option>
|
|
455
|
-
<option value="reviews">Most Reviews</option>
|
|
456
|
-
<option value="name">Alphabetical</option>
|
|
457
|
-
</select>
|
|
458
|
-
</div>
|
|
459
|
-
|
|
460
|
-
<div style={{ position: "relative", minWidth: "260px" }}>
|
|
461
|
-
<input
|
|
462
|
-
type="text"
|
|
463
|
-
placeholder={activeTab === "installed" ? "Filter installed..." : "Search catalog..."}
|
|
464
|
-
value={searchQuery}
|
|
465
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
466
|
-
style={{
|
|
467
|
-
width: "100%",
|
|
468
|
-
background: "var(--bg-secondary)",
|
|
469
|
-
border: "1px solid var(--border)",
|
|
470
|
-
borderRadius: "var(--radius-md)",
|
|
471
|
-
padding: "8px 14px 8px 36px",
|
|
472
|
-
color: "var(--text-primary)",
|
|
473
|
-
fontSize: "13px",
|
|
474
|
-
outline: "none"
|
|
475
|
-
}}
|
|
476
|
-
/>
|
|
477
|
-
<svg
|
|
478
|
-
width="15"
|
|
479
|
-
height="15"
|
|
480
|
-
viewBox="0 0 24 24"
|
|
481
|
-
fill="none"
|
|
482
|
-
stroke="var(--text-tertiary)"
|
|
483
|
-
strokeWidth="2"
|
|
484
|
-
style={{ position: "absolute", left: "12px", top: "50%", transform: "translateY(-50%)" }}
|
|
485
|
-
>
|
|
486
|
-
<circle cx="11" cy="11" r="8" />
|
|
487
|
-
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
488
|
-
</svg>
|
|
489
|
-
</div>
|
|
287
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
288
|
+
<input className="input" placeholder="Search plugins" value={query} onChange={(e) => setQuery(e.target.value)}
|
|
289
|
+
aria-label="Search plugins" style={{ width: "220px" }} />
|
|
290
|
+
<select className="select" value={useCase} onChange={(e) => setUseCase(e.target.value)} aria-label="Filter by use case" style={{ paddingRight: "32px" }}>
|
|
291
|
+
<option value="">All use cases</option>
|
|
292
|
+
{allUseCases.map((u) => <option key={u} value={u}>{u}</option>)}
|
|
293
|
+
</select>
|
|
490
294
|
</div>
|
|
491
295
|
</div>
|
|
492
296
|
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
<div style={{ padding: "60px 0", textAlign: "center", color: "var(--text-tertiary)" }}>
|
|
496
|
-
Loading plugins...
|
|
497
|
-
</div>
|
|
498
|
-
) : activeTab === "installed" ? (
|
|
297
|
+
{tab === "installed" && (
|
|
298
|
+
loading ? <div style={muted}>Loading…</div> :
|
|
499
299
|
filteredInstalled.length === 0 ? (
|
|
500
|
-
<
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
background: "var(--bg-secondary)",
|
|
504
|
-
borderRadius: "var(--radius-lg)",
|
|
505
|
-
border: "1px dashed var(--border)"
|
|
506
|
-
}}>
|
|
507
|
-
<div style={{ fontSize: "36px", marginBottom: "12px" }}>🧩</div>
|
|
508
|
-
<h3 style={{ margin: "0 0 8px 0", fontSize: "18px" }}>No Plugins Found</h3>
|
|
509
|
-
<p style={{ margin: "0 0 20px 0", color: "var(--text-secondary)", fontSize: "14px" }}>
|
|
510
|
-
{searchQuery ? "No installed plugins match your search filter." : "You do not have any plugins installed in this brain."}
|
|
511
|
-
</p>
|
|
512
|
-
<button
|
|
513
|
-
onClick={() => setActiveTab("catalog")}
|
|
514
|
-
style={{
|
|
515
|
-
background: "var(--bg-elevated)",
|
|
516
|
-
border: "1px solid var(--border-accent)",
|
|
517
|
-
color: "var(--accent-hover)",
|
|
518
|
-
padding: "8px 18px",
|
|
519
|
-
borderRadius: "var(--radius-sm)",
|
|
520
|
-
cursor: "pointer",
|
|
521
|
-
fontWeight: "600",
|
|
522
|
-
fontSize: "14px"
|
|
523
|
-
}}
|
|
524
|
-
>
|
|
525
|
-
Browse Discovery Catalog
|
|
526
|
-
</button>
|
|
527
|
-
</div>
|
|
300
|
+
<Empty title={installed.length ? "No installed plugins match" : "No plugins installed"}>
|
|
301
|
+
{!installed.length && <>Start with a <button className="btn btn-ghost btn-sm" onClick={() => setTab("bundled")}>bundled plugin</button> or see what your peers share.</>}
|
|
302
|
+
</Empty>
|
|
528
303
|
) : (
|
|
529
|
-
<div
|
|
304
|
+
<div className="card-grid">
|
|
530
305
|
{filteredInstalled.map((p) => (
|
|
531
|
-
<div
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
padding: "24px",
|
|
538
|
-
display: "flex",
|
|
539
|
-
flexDirection: "column",
|
|
540
|
-
justifyContent: "space-between",
|
|
541
|
-
transition: "all 0.2s ease"
|
|
542
|
-
}}
|
|
543
|
-
>
|
|
544
|
-
<div>
|
|
545
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "10px" }}>
|
|
546
|
-
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
|
|
547
|
-
<div style={{
|
|
548
|
-
width: "38px",
|
|
549
|
-
height: "38px",
|
|
550
|
-
borderRadius: "8px",
|
|
551
|
-
background: "var(--accent-muted)",
|
|
552
|
-
color: "var(--accent-hover)",
|
|
553
|
-
display: "flex",
|
|
554
|
-
alignItems: "center",
|
|
555
|
-
justifyContent: "center",
|
|
556
|
-
fontSize: "20px"
|
|
557
|
-
}}>
|
|
558
|
-
🧩
|
|
559
|
-
</div>
|
|
560
|
-
<div>
|
|
561
|
-
<h3 style={{ margin: 0, fontSize: "16px", fontWeight: "700" }}>{p.name}</h3>
|
|
562
|
-
<span style={{ fontSize: "12px", color: "var(--text-tertiary)", fontFamily: "var(--font-mono)" }}>
|
|
563
|
-
{p.id}
|
|
564
|
-
</span>
|
|
565
|
-
</div>
|
|
566
|
-
</div>
|
|
567
|
-
|
|
568
|
-
<span style={{
|
|
569
|
-
fontSize: "12px",
|
|
570
|
-
fontWeight: "600",
|
|
571
|
-
padding: "2px 8px",
|
|
572
|
-
borderRadius: "6px",
|
|
573
|
-
background: p.valid ? "var(--success-muted)" : "var(--error-muted)",
|
|
574
|
-
color: p.valid ? "var(--success)" : "var(--error)",
|
|
575
|
-
border: `1px solid ${p.valid ? "rgba(52, 211, 153, 0.2)" : "rgba(248, 113, 113, 0.2)"}`
|
|
576
|
-
}}>
|
|
577
|
-
v{p.version}
|
|
578
|
-
</span>
|
|
579
|
-
</div>
|
|
580
|
-
|
|
581
|
-
{/* Rating & Installs */}
|
|
582
|
-
<div style={{ marginBottom: "12px" }}>
|
|
583
|
-
<StarRatingDisplay
|
|
584
|
-
rating={p.rating || 4.9}
|
|
585
|
-
reviewCount={p.reviewCount || 1}
|
|
586
|
-
installCount={p.installCount || "1k"}
|
|
587
|
-
/>
|
|
588
|
-
</div>
|
|
589
|
-
|
|
590
|
-
<p style={{ fontSize: "13px", color: "var(--text-secondary)", lineHeight: "1.5", margin: "0 0 16px 0" }}>
|
|
591
|
-
{p.description}
|
|
592
|
-
</p>
|
|
593
|
-
|
|
594
|
-
{/* Badges / Features */}
|
|
595
|
-
<div style={{ display: "flex", flexWrap: "wrap", gap: "6px", marginBottom: "16px" }}>
|
|
596
|
-
{p.cli?.command && (
|
|
597
|
-
<span style={{
|
|
598
|
-
background: "var(--bg-elevated)",
|
|
599
|
-
border: "1px solid var(--border)",
|
|
600
|
-
color: "var(--text-primary)",
|
|
601
|
-
fontFamily: "var(--font-mono)",
|
|
602
|
-
fontSize: "11px",
|
|
603
|
-
padding: "3px 8px",
|
|
604
|
-
borderRadius: "4px"
|
|
605
|
-
}}>
|
|
606
|
-
cli: total-recall {p.cli.command}
|
|
607
|
-
</span>
|
|
608
|
-
)}
|
|
609
|
-
{p.categories?.length > 0 && (
|
|
610
|
-
<span style={{
|
|
611
|
-
background: "var(--accent-muted)",
|
|
612
|
-
color: "var(--accent-hover)",
|
|
613
|
-
fontSize: "11px",
|
|
614
|
-
padding: "3px 8px",
|
|
615
|
-
borderRadius: "4px"
|
|
616
|
-
}}>
|
|
617
|
-
{p.categories.length} SSSS schemas
|
|
618
|
-
</span>
|
|
619
|
-
)}
|
|
620
|
-
{p.tasks?.length > 0 && (
|
|
621
|
-
<span style={{
|
|
622
|
-
background: "var(--bg-elevated)",
|
|
623
|
-
color: "var(--text-secondary)",
|
|
624
|
-
fontSize: "11px",
|
|
625
|
-
padding: "3px 8px",
|
|
626
|
-
borderRadius: "4px"
|
|
627
|
-
}}>
|
|
628
|
-
{p.tasks.length} tasks
|
|
629
|
-
</span>
|
|
630
|
-
)}
|
|
631
|
-
</div>
|
|
306
|
+
<div key={`${p.scope}:${p.id}`} className="card" style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
|
307
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "8px", alignItems: "baseline" }}>
|
|
308
|
+
<button onClick={() => openDetail(p)} style={{ background: "none", border: "none", padding: 0, color: "var(--text-primary)", fontWeight: 600, fontSize: "15px", cursor: "pointer", textAlign: "left" }}>
|
|
309
|
+
{p.name}
|
|
310
|
+
</button>
|
|
311
|
+
<span style={{ ...mono, color: "var(--text-tertiary)" }}>v{p.version}</span>
|
|
632
312
|
</div>
|
|
633
|
-
|
|
634
|
-
<
|
|
635
|
-
|
|
636
|
-
<
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
/>
|
|
650
|
-
</div>
|
|
651
|
-
|
|
652
|
-
<div style={{
|
|
653
|
-
display: "flex",
|
|
654
|
-
justifyContent: "space-between",
|
|
655
|
-
alignItems: "center",
|
|
656
|
-
borderTop: "1px solid var(--border)",
|
|
657
|
-
paddingTop: "14px"
|
|
658
|
-
}}>
|
|
659
|
-
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
|
660
|
-
{p.cli?.command && (
|
|
661
|
-
<button
|
|
662
|
-
onClick={() => openDetail(p, "runner")}
|
|
663
|
-
style={{
|
|
664
|
-
background: "var(--accent-muted)",
|
|
665
|
-
border: "1px solid var(--border-accent)",
|
|
666
|
-
color: "var(--accent-hover)",
|
|
667
|
-
borderRadius: "var(--radius-sm)",
|
|
668
|
-
padding: "5px 12px",
|
|
669
|
-
fontSize: "12px",
|
|
670
|
-
fontWeight: "600",
|
|
671
|
-
cursor: "pointer",
|
|
672
|
-
display: "flex",
|
|
673
|
-
alignItems: "center",
|
|
674
|
-
gap: "5px"
|
|
675
|
-
}}
|
|
676
|
-
>
|
|
677
|
-
▶ Run
|
|
678
|
-
</button>
|
|
679
|
-
)}
|
|
680
|
-
<button
|
|
681
|
-
onClick={() => openDetail(p, "readme")}
|
|
682
|
-
style={{
|
|
683
|
-
background: "none",
|
|
684
|
-
border: "1px solid var(--border)",
|
|
685
|
-
color: "var(--text-secondary)",
|
|
686
|
-
borderRadius: "var(--radius-sm)",
|
|
687
|
-
padding: "5px 10px",
|
|
688
|
-
fontSize: "12px",
|
|
689
|
-
cursor: "pointer"
|
|
690
|
-
}}
|
|
691
|
-
>
|
|
692
|
-
Inspect
|
|
693
|
-
</button>
|
|
694
|
-
</div>
|
|
695
|
-
|
|
696
|
-
<button
|
|
697
|
-
onClick={() => handleUninstall(p)}
|
|
698
|
-
style={{
|
|
699
|
-
background: "none",
|
|
700
|
-
border: "none",
|
|
701
|
-
color: "var(--error)",
|
|
702
|
-
fontSize: "12px",
|
|
703
|
-
cursor: "pointer",
|
|
704
|
-
padding: "5px 8px",
|
|
705
|
-
borderRadius: "var(--radius-sm)"
|
|
706
|
-
}}
|
|
707
|
-
>
|
|
708
|
-
Uninstall
|
|
709
|
-
</button>
|
|
710
|
-
</div>
|
|
313
|
+
<div style={muted}>{p.description}</div>
|
|
314
|
+
<UseCaseChips useCases={p.use_cases} />
|
|
315
|
+
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
|
|
316
|
+
<span className="badge">{p.scope}</span>
|
|
317
|
+
<span className="badge">{sourceLabel(p)}</span>
|
|
318
|
+
{p.shared && <span className="badge badge-accent">shared</span>}
|
|
319
|
+
{!p.valid && <span className="badge badge-error">invalid manifest</span>}
|
|
320
|
+
{p.modified_since_install && <span className="badge badge-warning">changed since install</span>}
|
|
321
|
+
</div>
|
|
322
|
+
<div style={{ ...mono, color: "var(--text-tertiary)" }} title={p.sha256 || undefined}>sha256 {shortHash(p.sha256)}</div>
|
|
323
|
+
<div style={{ display: "flex", gap: "8px", marginTop: "auto", flexWrap: "wrap" }}>
|
|
324
|
+
<button className="btn btn-ghost btn-sm" onClick={() => openDetail(p)}>Open</button>
|
|
325
|
+
<button className="btn btn-ghost btn-sm" disabled={busy === p.id || (!p.valid && !p.shared)} onClick={() => doShare(p, !p.shared)}>
|
|
326
|
+
{p.shared ? "Stop sharing" : "Share"}
|
|
327
|
+
</button>
|
|
328
|
+
<button className="btn btn-ghost btn-sm" disabled={busy === p.id} onClick={() => doRemove(p)}>Remove</button>
|
|
711
329
|
</div>
|
|
712
330
|
</div>
|
|
713
331
|
))}
|
|
714
332
|
</div>
|
|
715
333
|
)
|
|
716
|
-
)
|
|
717
|
-
/* Catalog View */
|
|
718
|
-
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(360px, 1fr))", gap: "20px" }}>
|
|
719
|
-
{filteredCatalog.map((item) => (
|
|
720
|
-
<div
|
|
721
|
-
key={item.id}
|
|
722
|
-
style={{
|
|
723
|
-
background: "var(--bg-secondary)",
|
|
724
|
-
border: "1px solid var(--border)",
|
|
725
|
-
borderRadius: "var(--radius-lg)",
|
|
726
|
-
padding: "24px",
|
|
727
|
-
display: "flex",
|
|
728
|
-
flexDirection: "column",
|
|
729
|
-
justifyContent: "space-between"
|
|
730
|
-
}}
|
|
731
|
-
>
|
|
732
|
-
<div>
|
|
733
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "8px" }}>
|
|
734
|
-
<div>
|
|
735
|
-
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
736
|
-
<h3 style={{ margin: 0, fontSize: "16px", fontWeight: "700" }}>{item.name}</h3>
|
|
737
|
-
{item.verified && (
|
|
738
|
-
<span title="Verified Plugin" style={{ color: "var(--accent)", fontSize: "14px" }}>
|
|
739
|
-
✓
|
|
740
|
-
</span>
|
|
741
|
-
)}
|
|
742
|
-
</div>
|
|
743
|
-
<span style={{ fontSize: "12px", color: "var(--text-tertiary)", fontFamily: "var(--font-mono)" }}>
|
|
744
|
-
{item.id} • by {item.author}
|
|
745
|
-
</span>
|
|
746
|
-
</div>
|
|
747
|
-
<span style={{ fontSize: "12px", color: "var(--text-secondary)", fontWeight: "600" }}>
|
|
748
|
-
v{item.version}
|
|
749
|
-
</span>
|
|
750
|
-
</div>
|
|
334
|
+
)}
|
|
751
335
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
336
|
+
{tab === "bundled" && (
|
|
337
|
+
loading ? <div style={muted}>Loading…</div> :
|
|
338
|
+
filteredBundled.length === 0 ? <Empty title="No bundled plugins match" /> : (
|
|
339
|
+
<>
|
|
340
|
+
<p style={{ ...muted, marginBottom: "16px" }}>Shipped with this version of Total Recall, so they install on any node.</p>
|
|
341
|
+
<div className="card-grid">
|
|
342
|
+
{filteredBundled.map((p) => (
|
|
343
|
+
<div key={p.id} className="card" style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
|
344
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "8px", alignItems: "baseline" }}>
|
|
345
|
+
<span style={{ fontWeight: 600, fontSize: "15px" }}>{p.name}</span>
|
|
346
|
+
<span style={{ ...mono, color: "var(--text-tertiary)" }}>v{p.version}</span>
|
|
347
|
+
</div>
|
|
348
|
+
<div style={muted}>{p.description}</div>
|
|
349
|
+
<UseCaseChips useCases={p.use_cases} />
|
|
350
|
+
<div style={{ ...muted, fontSize: "12px" }}>
|
|
351
|
+
{[
|
|
352
|
+
p.cli && `command: total-recall ${p.cli.command}`,
|
|
353
|
+
p.tasks.length > 0 && `${p.tasks.length} scheduled task${p.tasks.length > 1 ? "s" : ""}`,
|
|
354
|
+
p.categories.length > 0 && `${p.categories.length} memory categor${p.categories.length > 1 ? "ies" : "y"}`
|
|
355
|
+
].filter(Boolean).join(" · ")}
|
|
356
|
+
</div>
|
|
357
|
+
<div style={{ marginTop: "auto" }}>
|
|
358
|
+
{p.installed
|
|
359
|
+
? <span className="badge badge-success">installed</span>
|
|
360
|
+
: <button className="btn btn-primary btn-sm" disabled={busy === p.id} onClick={() => doInstall(p.id)}>
|
|
361
|
+
{busy === p.id ? "Installing…" : "Install"}
|
|
362
|
+
</button>}
|
|
363
|
+
</div>
|
|
759
364
|
</div>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
</>
|
|
368
|
+
)
|
|
369
|
+
)}
|
|
760
370
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
<span key={tag} style={{
|
|
768
|
-
background: "var(--bg-elevated)",
|
|
769
|
-
color: "var(--text-tertiary)",
|
|
770
|
-
fontSize: "11px",
|
|
771
|
-
padding: "2px 8px",
|
|
772
|
-
borderRadius: "4px"
|
|
773
|
-
}}>
|
|
774
|
-
#{tag}
|
|
775
|
-
</span>
|
|
776
|
-
))}
|
|
777
|
-
</div>
|
|
778
|
-
</div>
|
|
371
|
+
{tab === "mesh" && (
|
|
372
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
373
|
+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "12px", flexWrap: "wrap" }}>
|
|
374
|
+
<p style={{ ...muted, margin: 0 }}>Asked live from each node. Only plugins their owner chose to share appear here; content is hash-checked on install.</p>
|
|
375
|
+
<button className="btn btn-ghost btn-sm" onClick={loadPeers} disabled={peersLoading}>{peersLoading ? "Asking peers…" : "Refresh"}</button>
|
|
376
|
+
</div>
|
|
779
377
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
gap: "6px"
|
|
789
|
-
}}>
|
|
790
|
-
✓ Installed
|
|
791
|
-
</span>
|
|
792
|
-
) : (
|
|
793
|
-
<button
|
|
794
|
-
disabled={installing}
|
|
795
|
-
onClick={() => handleCatalogInstall(item)}
|
|
796
|
-
style={{
|
|
797
|
-
background: "var(--accent)",
|
|
798
|
-
color: "#ffffff",
|
|
799
|
-
border: "none",
|
|
800
|
-
borderRadius: "var(--radius-sm)",
|
|
801
|
-
padding: "7px 16px",
|
|
802
|
-
fontSize: "13px",
|
|
803
|
-
fontWeight: "600",
|
|
804
|
-
cursor: installing ? "not-allowed" : "pointer"
|
|
805
|
-
}}
|
|
806
|
-
>
|
|
807
|
-
{installing ? "Installing..." : "Install"}
|
|
808
|
-
</button>
|
|
378
|
+
{peersLoading && !peers ? <div style={muted}>Asking peers…</div> :
|
|
379
|
+
peers === null ? <Empty title="Could not ask the mesh">The brain server did not answer the peer query.</Empty> :
|
|
380
|
+
!peers.mesh.available ? (
|
|
381
|
+
<Empty title="This node is not on a mesh">Enroll it with <code style={mono}>npx total-recall mesh enroll</code> to share plugins between your nodes.</Empty>
|
|
382
|
+
) : peers.peers.length === 0 ? <Empty title="No other nodes on the mesh" /> : (
|
|
383
|
+
<>
|
|
384
|
+
{!peers.mesh.configured && (
|
|
385
|
+
<div className="alert alert-warning">This node has no <code style={mono}>TR_MESH_SYNC_TOKEN</code>, so it cannot query peers. Set the same token on each node.</div>
|
|
809
386
|
)}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
387
|
+
{peers.peers.map((node) => {
|
|
388
|
+
const shown = node.plugins.filter(matches)
|
|
389
|
+
return (
|
|
390
|
+
<div key={node.hostname} className="card">
|
|
391
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px", alignItems: "center", marginBottom: node.plugins.length ? "12px" : 0, flexWrap: "wrap" }}>
|
|
392
|
+
<div>
|
|
393
|
+
<span style={{ fontWeight: 600 }}>{node.hostname}</span>{" "}
|
|
394
|
+
<span style={{ ...mono, color: "var(--text-tertiary)" }}>{node.ip}</span>
|
|
395
|
+
</div>
|
|
396
|
+
<span className={`badge ${node.status === "ok" ? "badge-online" : node.status === "offline" ? "badge-offline" : "badge-warning"}`}>
|
|
397
|
+
{PEER_STATUS_LABEL[node.status]}{node.status === "ok" ? ` · ${node.plugins.length} shared` : ""}
|
|
398
|
+
</span>
|
|
399
|
+
</div>
|
|
400
|
+
{node.error && node.status !== "ok" && <div style={{ ...muted, fontSize: "12px" }}>{node.error}</div>}
|
|
401
|
+
{node.status === "ok" && node.plugins.length === 0 && <div style={muted}>Shares no plugins.</div>}
|
|
402
|
+
{node.status === "ok" && node.plugins.length > 0 && shown.length === 0 && <div style={muted}>No shared plugins match the filter.</div>}
|
|
403
|
+
{shown.map((p) => {
|
|
404
|
+
const source = `peer:${node.hostname}/${p.id}`
|
|
405
|
+
return (
|
|
406
|
+
<div key={p.id} style={{ display: "flex", justifyContent: "space-between", gap: "16px", padding: "12px 0", borderTop: "1px solid var(--border)", flexWrap: "wrap" }}>
|
|
407
|
+
<div style={{ minWidth: 0, flex: "1 1 320px", display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
408
|
+
<div><span style={{ fontWeight: 600 }}>{p.name}</span> <span style={{ ...mono, color: "var(--text-tertiary)" }}>v{p.version}</span></div>
|
|
409
|
+
<div style={muted}>{p.description}</div>
|
|
410
|
+
<UseCaseChips useCases={p.use_cases} />
|
|
411
|
+
<div style={{ ...mono, color: "var(--text-tertiary)" }} title={p.sha256}>
|
|
412
|
+
sha256 {shortHash(p.sha256)} · {p.file_count} files · {formatBytes(p.size_bytes)}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
<div style={{ alignSelf: "center" }}>
|
|
416
|
+
{p.installed
|
|
417
|
+
? <span className={`badge ${p.same_as_installed ? "badge-success" : "badge-warning"}`}>
|
|
418
|
+
{p.same_as_installed ? "installed (same content)" : "installed (different content)"}
|
|
419
|
+
</span>
|
|
420
|
+
: <button className="btn btn-primary btn-sm" disabled={busy === source} onClick={() => doInstall(source)}>
|
|
421
|
+
{busy === source ? "Installing…" : "Install"}
|
|
422
|
+
</button>}
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)
|
|
426
|
+
})}
|
|
427
|
+
</div>
|
|
428
|
+
)
|
|
429
|
+
})}
|
|
430
|
+
</>
|
|
431
|
+
)}
|
|
813
432
|
</div>
|
|
814
433
|
)}
|
|
815
434
|
|
|
816
|
-
{
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
<
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
<
|
|
843
|
-
<
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
required
|
|
850
|
-
placeholder="e.g. ./.agent/plugins/my-plugin or https://github.com/..."
|
|
851
|
-
value={installSource}
|
|
852
|
-
onChange={(e) => setInstallSource(e.target.value)}
|
|
853
|
-
style={{
|
|
854
|
-
width: "100%",
|
|
855
|
-
background: "var(--bg-primary)",
|
|
856
|
-
border: "1px solid var(--border)",
|
|
857
|
-
borderRadius: "var(--radius-md)",
|
|
858
|
-
padding: "10px 14px",
|
|
859
|
-
color: "var(--text-primary)",
|
|
860
|
-
fontSize: "14px",
|
|
861
|
-
outline: "none"
|
|
862
|
-
}}
|
|
863
|
-
/>
|
|
864
|
-
</div>
|
|
865
|
-
|
|
866
|
-
<div style={{ marginBottom: "14px" }}>
|
|
867
|
-
<label style={{ display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" }}>
|
|
868
|
-
<input
|
|
869
|
-
type="checkbox"
|
|
870
|
-
checked={installLink}
|
|
871
|
-
onChange={(e) => setInstallLink(e.target.checked)}
|
|
872
|
-
/>
|
|
873
|
-
<span>Link as Symlink (development mode — live edits sync without reinstalling)</span>
|
|
874
|
-
</label>
|
|
875
|
-
</div>
|
|
876
|
-
|
|
877
|
-
<div style={{ marginBottom: "24px" }}>
|
|
878
|
-
<label style={{ display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" }}>
|
|
879
|
-
<input
|
|
880
|
-
type="checkbox"
|
|
881
|
-
checked={installGlobal}
|
|
882
|
-
onChange={(e) => setInstallGlobal(e.target.checked)}
|
|
883
|
-
/>
|
|
884
|
-
<span>Install Globally (~/.agent/plugins across all projects)</span>
|
|
885
|
-
</label>
|
|
886
|
-
</div>
|
|
887
|
-
|
|
888
|
-
<div style={{ display: "flex", justifyContent: "flex-end", gap: "12px" }}>
|
|
889
|
-
<button
|
|
890
|
-
type="button"
|
|
891
|
-
onClick={() => setInstallModalOpen(false)}
|
|
892
|
-
style={{
|
|
893
|
-
background: "transparent",
|
|
894
|
-
border: "1px solid var(--border)",
|
|
895
|
-
color: "var(--text-secondary)",
|
|
896
|
-
padding: "8px 16px",
|
|
897
|
-
borderRadius: "var(--radius-md)",
|
|
898
|
-
fontSize: "14px",
|
|
899
|
-
cursor: "pointer"
|
|
900
|
-
}}
|
|
901
|
-
>
|
|
902
|
-
Cancel
|
|
903
|
-
</button>
|
|
904
|
-
<button
|
|
905
|
-
type="submit"
|
|
906
|
-
disabled={installing || !installSource.trim()}
|
|
907
|
-
style={{
|
|
908
|
-
background: "var(--accent)",
|
|
909
|
-
border: "none",
|
|
910
|
-
color: "#ffffff",
|
|
911
|
-
padding: "8px 20px",
|
|
912
|
-
borderRadius: "var(--radius-md)",
|
|
913
|
-
fontSize: "14px",
|
|
914
|
-
fontWeight: "600",
|
|
915
|
-
cursor: installing ? "not-allowed" : "pointer"
|
|
916
|
-
}}
|
|
917
|
-
>
|
|
918
|
-
{installing ? "Installing..." : "Install Plugin"}
|
|
919
|
-
</button>
|
|
920
|
-
</div>
|
|
921
|
-
</form>
|
|
922
|
-
</div>
|
|
435
|
+
{installOpen && (
|
|
436
|
+
<div role="dialog" aria-modal="true" aria-label="Install plugin from source" onClick={() => setInstallOpen(false)}
|
|
437
|
+
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: "16px" }}>
|
|
438
|
+
<form className="card" onClick={(e) => e.stopPropagation()} style={{ width: "100%", maxWidth: "520px", display: "flex", flexDirection: "column", gap: "14px" }}
|
|
439
|
+
onSubmit={async (e) => {
|
|
440
|
+
e.preventDefault()
|
|
441
|
+
if (!installSource.trim()) return
|
|
442
|
+
if (await doInstall(installSource.trim(), { link: installLink, global: installGlobal })) {
|
|
443
|
+
setInstallOpen(false)
|
|
444
|
+
setInstallSource("")
|
|
445
|
+
}
|
|
446
|
+
}}>
|
|
447
|
+
<div style={{ fontWeight: 600, fontSize: "16px" }}>Install from source</div>
|
|
448
|
+
<div style={muted}>
|
|
449
|
+
A bundled plugin id, <code style={mono}>peer:<host>/<id></code>, a git URL, or a folder on this machine containing <code style={mono}>plugin.json</code>.
|
|
450
|
+
</div>
|
|
451
|
+
<input className="input" autoFocus value={installSource} onChange={(e) => setInstallSource(e.target.value)}
|
|
452
|
+
placeholder="https://github.com/you/my-plugin.git" aria-label="Plugin source" />
|
|
453
|
+
<label style={{ ...muted, display: "flex", gap: "8px", alignItems: "center" }}>
|
|
454
|
+
<input type="checkbox" checked={installLink} onChange={(e) => setInstallLink(e.target.checked)} />
|
|
455
|
+
Link a local folder instead of copying it (for plugin development)
|
|
456
|
+
</label>
|
|
457
|
+
<label style={{ ...muted, display: "flex", gap: "8px", alignItems: "center" }}>
|
|
458
|
+
<input type="checkbox" checked={installGlobal} onChange={(e) => setInstallGlobal(e.target.checked)} />
|
|
459
|
+
Install for every project on this machine
|
|
460
|
+
</label>
|
|
461
|
+
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
|
|
462
|
+
<button type="button" className="btn btn-ghost" onClick={() => setInstallOpen(false)}>Cancel</button>
|
|
463
|
+
<button type="submit" className="btn btn-primary" disabled={!installSource.trim() || busy !== null}>
|
|
464
|
+
{busy ? "Installing…" : "Install"}
|
|
465
|
+
</button>
|
|
466
|
+
</div>
|
|
467
|
+
</form>
|
|
923
468
|
</div>
|
|
924
469
|
)}
|
|
925
470
|
|
|
926
|
-
{
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
zIndex: 1000,
|
|
936
|
-
backdropFilter: "blur(6px)",
|
|
937
|
-
padding: "20px"
|
|
938
|
-
}}>
|
|
939
|
-
<div style={{
|
|
940
|
-
background: "var(--bg-secondary)",
|
|
941
|
-
border: "1px solid var(--border-accent)",
|
|
942
|
-
borderRadius: "var(--radius-xl)",
|
|
943
|
-
padding: "24px 28px",
|
|
944
|
-
width: "100%",
|
|
945
|
-
maxWidth: "760px",
|
|
946
|
-
maxHeight: "88vh",
|
|
947
|
-
display: "flex",
|
|
948
|
-
flexDirection: "column",
|
|
949
|
-
boxShadow: "0 20px 40px rgba(0, 0, 0, 0.5)"
|
|
950
|
-
}}>
|
|
951
|
-
{/* Modal Header */}
|
|
952
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "18px" }}>
|
|
953
|
-
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
|
954
|
-
<div style={{
|
|
955
|
-
width: "42px",
|
|
956
|
-
height: "42px",
|
|
957
|
-
borderRadius: "10px",
|
|
958
|
-
background: "var(--accent-muted)",
|
|
959
|
-
color: "var(--accent-hover)",
|
|
960
|
-
display: "flex",
|
|
961
|
-
alignItems: "center",
|
|
962
|
-
justifyContent: "center",
|
|
963
|
-
fontSize: "22px"
|
|
964
|
-
}}>
|
|
965
|
-
🧩
|
|
966
|
-
</div>
|
|
967
|
-
<div>
|
|
968
|
-
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
|
969
|
-
<h2 style={{ margin: 0, fontSize: "18px", fontWeight: "700" }}>{detailModalPlugin.name}</h2>
|
|
970
|
-
<span style={{
|
|
971
|
-
fontSize: "11px",
|
|
972
|
-
fontWeight: "600",
|
|
973
|
-
padding: "2px 7px",
|
|
974
|
-
borderRadius: "6px",
|
|
975
|
-
background: detailModalPlugin.valid ? "var(--success-muted)" : "var(--error-muted)",
|
|
976
|
-
color: detailModalPlugin.valid ? "var(--success)" : "var(--error)",
|
|
977
|
-
border: `1px solid ${detailModalPlugin.valid ? "rgba(52, 211, 153, 0.2)" : "rgba(248, 113, 113, 0.2)"}`
|
|
978
|
-
}}>
|
|
979
|
-
v{detailModalPlugin.version}
|
|
980
|
-
</span>
|
|
981
|
-
</div>
|
|
982
|
-
<span style={{ fontSize: "12px", color: "var(--text-tertiary)", fontFamily: "var(--font-mono)" }}>
|
|
983
|
-
id: {detailModalPlugin.id}
|
|
984
|
-
</span>
|
|
985
|
-
</div>
|
|
471
|
+
{detail && (
|
|
472
|
+
<div role="dialog" aria-modal="true" aria-label={detail.name} onClick={closeDetail}
|
|
473
|
+
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", justifyContent: "flex-end", zIndex: 1000 }}>
|
|
474
|
+
<div onClick={(e) => e.stopPropagation()}
|
|
475
|
+
style={{ width: "100%", maxWidth: "640px", height: "100%", overflowY: "auto", background: "var(--bg-secondary)", borderLeft: "1px solid var(--border)", padding: "24px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
476
|
+
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px" }}>
|
|
477
|
+
<div>
|
|
478
|
+
<div style={{ fontSize: "18px", fontWeight: 700 }}>{detail.name}</div>
|
|
479
|
+
<div style={{ ...mono, color: "var(--text-tertiary)" }}>{detail.id} · v{detail.version}</div>
|
|
986
480
|
</div>
|
|
987
|
-
|
|
988
|
-
<button
|
|
989
|
-
onClick={closeDetail}
|
|
990
|
-
style={{
|
|
991
|
-
background: "none",
|
|
992
|
-
border: "none",
|
|
993
|
-
color: "var(--text-secondary)",
|
|
994
|
-
fontSize: "24px",
|
|
995
|
-
cursor: "pointer",
|
|
996
|
-
lineHeight: 1
|
|
997
|
-
}}
|
|
998
|
-
>
|
|
999
|
-
×
|
|
1000
|
-
</button>
|
|
481
|
+
<button className="btn btn-ghost btn-sm" onClick={closeDetail} aria-label="Close">Close</button>
|
|
1001
482
|
</div>
|
|
1002
|
-
|
|
1003
|
-
{
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
<button
|
|
1013
|
-
onClick={() => setDetailTab("runner")}
|
|
1014
|
-
style={{
|
|
1015
|
-
background: detailTab === "runner" ? "var(--bg-elevated)" : "transparent",
|
|
1016
|
-
color: detailTab === "runner" ? "var(--accent-hover)" : "var(--text-secondary)",
|
|
1017
|
-
border: detailTab === "runner" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
1018
|
-
borderRadius: "var(--radius-sm)",
|
|
1019
|
-
padding: "6px 12px",
|
|
1020
|
-
fontSize: "13px",
|
|
1021
|
-
fontWeight: "600",
|
|
1022
|
-
cursor: "pointer",
|
|
1023
|
-
display: "flex",
|
|
1024
|
-
alignItems: "center",
|
|
1025
|
-
gap: "6px"
|
|
1026
|
-
}}
|
|
1027
|
-
>
|
|
1028
|
-
<span>⚡ Interactive Runner</span>
|
|
1029
|
-
</button>
|
|
1030
|
-
|
|
1031
|
-
<button
|
|
1032
|
-
onClick={() => setDetailTab("readme")}
|
|
1033
|
-
style={{
|
|
1034
|
-
background: detailTab === "readme" ? "var(--bg-elevated)" : "transparent",
|
|
1035
|
-
color: detailTab === "readme" ? "var(--accent-hover)" : "var(--text-secondary)",
|
|
1036
|
-
border: detailTab === "readme" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
1037
|
-
borderRadius: "var(--radius-sm)",
|
|
1038
|
-
padding: "6px 12px",
|
|
1039
|
-
fontSize: "13px",
|
|
1040
|
-
fontWeight: "600",
|
|
1041
|
-
cursor: "pointer"
|
|
1042
|
-
}}
|
|
1043
|
-
>
|
|
1044
|
-
📖 Documentation
|
|
1045
|
-
</button>
|
|
1046
|
-
|
|
1047
|
-
<button
|
|
1048
|
-
onClick={() => setDetailTab("schemas")}
|
|
1049
|
-
style={{
|
|
1050
|
-
background: detailTab === "schemas" ? "var(--bg-elevated)" : "transparent",
|
|
1051
|
-
color: detailTab === "schemas" ? "var(--accent-hover)" : "var(--text-secondary)",
|
|
1052
|
-
border: detailTab === "schemas" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
1053
|
-
borderRadius: "var(--radius-sm)",
|
|
1054
|
-
padding: "6px 12px",
|
|
1055
|
-
fontSize: "13px",
|
|
1056
|
-
fontWeight: "600",
|
|
1057
|
-
cursor: "pointer"
|
|
1058
|
-
}}
|
|
1059
|
-
>
|
|
1060
|
-
🧩 SSSS Schemas ({detailModalPlugin.categories?.length || 0})
|
|
1061
|
-
</button>
|
|
1062
|
-
|
|
1063
|
-
<button
|
|
1064
|
-
onClick={() => setDetailTab("tasks")}
|
|
1065
|
-
style={{
|
|
1066
|
-
background: detailTab === "tasks" ? "var(--bg-elevated)" : "transparent",
|
|
1067
|
-
color: detailTab === "tasks" ? "var(--accent-hover)" : "var(--text-secondary)",
|
|
1068
|
-
border: detailTab === "tasks" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
1069
|
-
borderRadius: "var(--radius-sm)",
|
|
1070
|
-
padding: "6px 12px",
|
|
1071
|
-
fontSize: "13px",
|
|
1072
|
-
fontWeight: "600",
|
|
1073
|
-
cursor: "pointer"
|
|
1074
|
-
}}
|
|
1075
|
-
>
|
|
1076
|
-
⏱️ Tasks ({detailModalPlugin.tasks?.length || 0})
|
|
1077
|
-
</button>
|
|
1078
|
-
|
|
1079
|
-
<button
|
|
1080
|
-
onClick={() => setDetailTab("manifest")}
|
|
1081
|
-
style={{
|
|
1082
|
-
background: detailTab === "manifest" ? "var(--bg-elevated)" : "transparent",
|
|
1083
|
-
color: detailTab === "manifest" ? "var(--accent-hover)" : "var(--text-secondary)",
|
|
1084
|
-
border: detailTab === "manifest" ? "1px solid var(--border-accent)" : "1px solid transparent",
|
|
1085
|
-
borderRadius: "var(--radius-sm)",
|
|
1086
|
-
padding: "6px 12px",
|
|
1087
|
-
fontSize: "13px",
|
|
1088
|
-
fontWeight: "600",
|
|
1089
|
-
cursor: "pointer"
|
|
1090
|
-
}}
|
|
1091
|
-
>
|
|
1092
|
-
📋 Manifest
|
|
1093
|
-
</button>
|
|
483
|
+
<div style={muted}>{detail.description}</div>
|
|
484
|
+
{!detail.valid && (
|
|
485
|
+
<div className="alert alert-error">
|
|
486
|
+
{detail.errors.map((e) => <div key={e}>{e}</div>)}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
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>
|
|
1094
493
|
</div>
|
|
1095
494
|
|
|
1096
|
-
{
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
value={subcommand}
|
|
1109
|
-
onChange={(e) => setSubcommand(e.target.value)}
|
|
1110
|
-
style={{
|
|
1111
|
-
background: "var(--bg-primary)",
|
|
1112
|
-
border: "1px solid var(--border)",
|
|
1113
|
-
borderRadius: "var(--radius-sm)",
|
|
1114
|
-
color: "var(--text-primary)",
|
|
1115
|
-
padding: "6px 10px",
|
|
1116
|
-
fontSize: "13px",
|
|
1117
|
-
width: "160px"
|
|
1118
|
-
}}
|
|
1119
|
-
/>
|
|
1120
|
-
|
|
1121
|
-
<input
|
|
1122
|
-
type="text"
|
|
1123
|
-
placeholder="extra args (e.g. --json)"
|
|
1124
|
-
value={customArgs}
|
|
1125
|
-
onChange={(e) => setCustomArgs(e.target.value)}
|
|
1126
|
-
style={{
|
|
1127
|
-
background: "var(--bg-primary)",
|
|
1128
|
-
border: "1px solid var(--border)",
|
|
1129
|
-
borderRadius: "var(--radius-sm)",
|
|
1130
|
-
color: "var(--text-primary)",
|
|
1131
|
-
padding: "6px 10px",
|
|
1132
|
-
fontSize: "13px",
|
|
1133
|
-
flex: 1,
|
|
1134
|
-
minWidth: "140px"
|
|
1135
|
-
}}
|
|
1136
|
-
/>
|
|
1137
|
-
|
|
1138
|
-
<button
|
|
1139
|
-
onClick={handleExecute}
|
|
1140
|
-
disabled={executing}
|
|
1141
|
-
style={{
|
|
1142
|
-
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
|
|
1143
|
-
color: "#ffffff",
|
|
1144
|
-
border: "none",
|
|
1145
|
-
borderRadius: "var(--radius-sm)",
|
|
1146
|
-
padding: "7px 18px",
|
|
1147
|
-
fontSize: "13px",
|
|
1148
|
-
fontWeight: "600",
|
|
1149
|
-
cursor: executing ? "wait" : "pointer",
|
|
1150
|
-
display: "flex",
|
|
1151
|
-
alignItems: "center",
|
|
1152
|
-
gap: "6px"
|
|
1153
|
-
}}
|
|
1154
|
-
>
|
|
1155
|
-
{executing ? "Running..." : "▶ Execute"}
|
|
1156
|
-
</button>
|
|
1157
|
-
</div>
|
|
1158
|
-
|
|
1159
|
-
{/* Terminal Console View */}
|
|
1160
|
-
<div style={{
|
|
1161
|
-
background: "#090d16",
|
|
1162
|
-
border: "1px solid var(--border)",
|
|
1163
|
-
borderRadius: "var(--radius-md)",
|
|
1164
|
-
padding: "16px 20px",
|
|
1165
|
-
minHeight: "260px",
|
|
1166
|
-
maxHeight: "360px",
|
|
1167
|
-
overflowY: "auto",
|
|
1168
|
-
fontFamily: "var(--font-mono)",
|
|
1169
|
-
fontSize: "12px",
|
|
1170
|
-
lineHeight: "1.6",
|
|
1171
|
-
color: "#e2e8f0",
|
|
1172
|
-
position: "relative"
|
|
1173
|
-
}}>
|
|
1174
|
-
{consoleOutput ? (
|
|
1175
|
-
<pre style={{ margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
|
1176
|
-
{consoleOutput}
|
|
1177
|
-
</pre>
|
|
1178
|
-
) : (
|
|
1179
|
-
<div style={{ color: "var(--text-tertiary)", fontStyle: "italic", paddingTop: "50px", textAlign: "center" }}>
|
|
1180
|
-
Select a subcommand and click "Execute" to run the plugin CLI and capture live output.
|
|
1181
|
-
</div>
|
|
1182
|
-
)}
|
|
1183
|
-
|
|
1184
|
-
{consoleOutput && (
|
|
1185
|
-
<div style={{ position: "absolute", top: "10px", right: "12px", display: "flex", gap: "6px" }}>
|
|
1186
|
-
<button
|
|
1187
|
-
onClick={() => navigator.clipboard.writeText(consoleOutput)}
|
|
1188
|
-
style={{
|
|
1189
|
-
background: "rgba(255, 255, 255, 0.08)",
|
|
1190
|
-
border: "none",
|
|
1191
|
-
color: "var(--text-secondary)",
|
|
1192
|
-
borderRadius: "4px",
|
|
1193
|
-
padding: "3px 8px",
|
|
1194
|
-
fontSize: "11px",
|
|
1195
|
-
cursor: "pointer"
|
|
1196
|
-
}}
|
|
1197
|
-
>
|
|
1198
|
-
Copy
|
|
1199
|
-
</button>
|
|
1200
|
-
<button
|
|
1201
|
-
onClick={() => setConsoleOutput("")}
|
|
1202
|
-
style={{
|
|
1203
|
-
background: "rgba(255, 255, 255, 0.08)",
|
|
1204
|
-
border: "none",
|
|
1205
|
-
color: "var(--text-secondary)",
|
|
1206
|
-
borderRadius: "4px",
|
|
1207
|
-
padding: "3px 8px",
|
|
1208
|
-
fontSize: "11px",
|
|
1209
|
-
cursor: "pointer"
|
|
1210
|
-
}}
|
|
1211
|
-
>
|
|
1212
|
-
Clear
|
|
1213
|
-
</button>
|
|
1214
|
-
</div>
|
|
1215
|
-
)}
|
|
1216
|
-
</div>
|
|
495
|
+
{detailTab === "run" && detail.cli && (
|
|
496
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
|
497
|
+
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
498
|
+
<select className="select" value={subcommand} onChange={(e) => setSubcommand(e.target.value)} aria-label="Subcommand" style={{ paddingRight: "32px" }}>
|
|
499
|
+
{(detail.cli.subcommands.length ? detail.cli.subcommands : [{ name: "" }]).map((s) => (
|
|
500
|
+
<option key={s.name} value={s.name}>{s.name || "(default)"}</option>
|
|
501
|
+
))}
|
|
502
|
+
</select>
|
|
503
|
+
<input className="input" value={args} onChange={(e) => setArgs(e.target.value)} placeholder="extra arguments" aria-label="Arguments" style={{ flex: 1, minWidth: "160px" }} />
|
|
504
|
+
<button className="btn btn-primary" onClick={runCommand} disabled={busy === `run:${detail.id}` || !detail.valid}>
|
|
505
|
+
{busy === `run:${detail.id}` ? "Running…" : "Run"}
|
|
506
|
+
</button>
|
|
1217
507
|
</div>
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
<div style={{
|
|
1222
|
-
background: "var(--bg-primary)",
|
|
1223
|
-
border: "1px solid var(--border)",
|
|
1224
|
-
borderRadius: "var(--radius-md)",
|
|
1225
|
-
padding: "20px 24px",
|
|
1226
|
-
maxHeight: "420px",
|
|
1227
|
-
overflowY: "auto",
|
|
1228
|
-
lineHeight: "1.6"
|
|
1229
|
-
}}>
|
|
1230
|
-
{readmeLoading ? (
|
|
1231
|
-
<div style={{ color: "var(--text-tertiary)", textAlign: "center", padding: "40px" }}>Loading documentation...</div>
|
|
1232
|
-
) : (
|
|
1233
|
-
<pre style={{ margin: 0, whiteSpace: "pre-wrap", wordBreak: "break-word", fontFamily: "inherit", fontSize: "14px", color: "var(--text-primary)" }}>
|
|
1234
|
-
{readmeText || "No README.md documentation found for this plugin."}
|
|
1235
|
-
</pre>
|
|
1236
|
-
)}
|
|
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.
|
|
1237
511
|
</div>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
<div key={cat.name} style={{
|
|
1246
|
-
background: "var(--bg-primary)",
|
|
1247
|
-
border: "1px solid var(--border)",
|
|
1248
|
-
borderRadius: "var(--radius-md)",
|
|
1249
|
-
padding: "14px 18px"
|
|
1250
|
-
}}>
|
|
1251
|
-
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
|
|
1252
|
-
<span style={{ fontWeight: "700", color: "var(--accent-hover)", fontFamily: "var(--font-mono)" }}>
|
|
1253
|
-
{cat.name}
|
|
1254
|
-
</span>
|
|
1255
|
-
<span style={{ fontSize: "11px", background: "var(--bg-elevated)", padding: "2px 6px", borderRadius: "4px", color: "var(--text-tertiary)" }}>
|
|
1256
|
-
type: memory
|
|
1257
|
-
</span>
|
|
1258
|
-
</div>
|
|
1259
|
-
<p style={{ margin: 0, fontSize: "13px", color: "var(--text-secondary)" }}>
|
|
1260
|
-
{cat.description || "Plugin declared SSSS category schema."}
|
|
1261
|
-
</p>
|
|
1262
|
-
</div>
|
|
1263
|
-
))}
|
|
1264
|
-
</div>
|
|
1265
|
-
) : (
|
|
1266
|
-
<div style={{ padding: "40px", textAlign: "center", color: "var(--text-tertiary)" }}>
|
|
1267
|
-
This plugin does not declare custom SSSS category schemas.
|
|
1268
|
-
</div>
|
|
1269
|
-
)}
|
|
1270
|
-
</div>
|
|
1271
|
-
)}
|
|
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
|
+
</div>
|
|
518
|
+
)}
|
|
1272
519
|
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
</
|
|
520
|
+
{detailTab === "details" && (
|
|
521
|
+
<div>
|
|
522
|
+
<Fact label="Scope">{detail.scope}{detail.linked ? " (linked folder)" : ""}</Fact>
|
|
523
|
+
<Fact label="Source">{sourceLabel(detail)} — <span style={mono}>{detail.source.ref}</span></Fact>
|
|
524
|
+
<Fact label="Installed">{detail.installed_at ? new Date(detail.installed_at).toLocaleString() : "no install record"}</Fact>
|
|
525
|
+
<Fact label="Content sha256"><span style={mono}>{detail.sha256 || "—"}</span></Fact>
|
|
526
|
+
{detail.modified_since_install && <Fact label="At install"><span style={mono}>{detail.installed_sha256}</span></Fact>}
|
|
527
|
+
<Fact label="Files">{detail.file_count ?? "—"} · {formatBytes(detail.size_bytes)}</Fact>
|
|
528
|
+
<Fact label="Shared">{detail.shared ? "yes" : "no"}</Fact>
|
|
529
|
+
{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>}
|
|
530
|
+
<Fact label="Use cases">{detail.use_cases.join(", ") || "—"}</Fact>
|
|
531
|
+
{detail.author && <Fact label="Author">{detail.author}</Fact>}
|
|
532
|
+
{detail.license && <Fact label="License">{detail.license}</Fact>}
|
|
533
|
+
<Fact label="Location"><span style={mono}>{detail.dir}</span></Fact>
|
|
534
|
+
<Fact label="Agent context">{detail.has_generator ? "adds a block to compiled instructions" : "—"}</Fact>
|
|
535
|
+
<Fact label="Memory categories">
|
|
536
|
+
{detail.categories.length ? detail.categories.map((c) => c.name).join(", ") : "—"}
|
|
537
|
+
</Fact>
|
|
538
|
+
<Fact label="Scheduled tasks">
|
|
539
|
+
{detail.tasks.length === 0 ? "—" : (
|
|
540
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
541
|
+
{detail.tasks.map((t) => (
|
|
542
|
+
<div key={t.command}>
|
|
543
|
+
<span style={mono}>{t.schedule}</span> {t.command} — {t.intent}
|
|
544
|
+
<div style={{ color: "var(--text-tertiary)", fontSize: "12px" }}>last run: {t.last_run || "not yet"}</div>
|
|
1298
545
|
</div>
|
|
1299
546
|
))}
|
|
1300
547
|
</div>
|
|
1301
|
-
) : (
|
|
1302
|
-
<div style={{ padding: "40px", textAlign: "center", color: "var(--text-tertiary)" }}>
|
|
1303
|
-
No background daemon tasks registered for this plugin.
|
|
1304
|
-
</div>
|
|
1305
548
|
)}
|
|
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>
|
|
1306
555
|
</div>
|
|
1307
|
-
|
|
556
|
+
</div>
|
|
557
|
+
)}
|
|
1308
558
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
background: "var(--bg-primary)",
|
|
1313
|
-
border: "1px solid var(--border)",
|
|
1314
|
-
borderRadius: "var(--radius-md)",
|
|
1315
|
-
padding: "16px",
|
|
1316
|
-
fontFamily: "var(--font-mono)",
|
|
1317
|
-
fontSize: "12px",
|
|
1318
|
-
color: "var(--accent-hover)",
|
|
1319
|
-
maxHeight: "420px",
|
|
1320
|
-
overflow: "auto"
|
|
1321
|
-
}}>
|
|
1322
|
-
{JSON.stringify(detailModalPlugin.manifest || detailModalPlugin, null, 2)}
|
|
1323
|
-
</pre>
|
|
1324
|
-
)}
|
|
1325
|
-
</div>
|
|
559
|
+
{detailTab === "readme" && (
|
|
560
|
+
<div style={{ fontSize: "14px", lineHeight: 1.6 }}>{readme ? renderMarkdown(readme) : <span style={muted}>Loading…</span>}</div>
|
|
561
|
+
)}
|
|
1326
562
|
</div>
|
|
1327
563
|
</div>
|
|
1328
564
|
)}
|