total-recall-brain 3.46.2 → 3.47.1
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 +5 -0
- package/frontend/dist/assets/index-CpsWeV4X.css +1 -0
- package/frontend/dist/assets/{index-D1goIPjC.js → index-poecbHSl.js} +100 -100
- package/frontend/dist/brand/total-recall-lockup-transparent.png +0 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +1 -1
- package/frontend/src/App.tsx +15 -81
- package/frontend/src/components/DashboardOverview.tsx +164 -0
- package/frontend/src/components/brand/BrandMark.tsx +8 -6
- package/frontend/src/components/plugins/PluginWorkspace.tsx +112 -57
- package/frontend/src/components/workspace/PluginHome.tsx +3 -3
- package/frontend/src/index.css +43 -11
- package/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
- package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +300 -58
- package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +5 -33
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-fleet-ops.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-publish.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
- package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +7 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +175 -17
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +242 -31
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +41 -15
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +29 -22
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +20 -33
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +42 -49
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/markdown-links.mjs +112 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +219 -65
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +21 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +51 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
- package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
- package/src/cli/context.mjs +6 -2
- package/src/cli/plugin/create.mjs +2 -1
- package/src/cli/plugin/monetization.mjs +66 -32
- package/src/cli/plugin/search.mjs +25 -3
- package/src/cli/preflight.mjs +40 -6
- package/src/core/context-compactor.mjs +2 -0
- package/src/core/gate-mode.mjs +13 -24
- package/src/core/plugin-distribution.mjs +35 -1
- package/src/core/preflight-gate.mjs +28 -8
- package/src/core/preflight-receipt.mjs +11 -0
- package/src/server/routes/vault.mjs +1 -0
- package/frontend/dist/assets/index-BRYE1jmv.css +0 -1
|
Binary file
|
package/frontend/dist/index.html
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
18
18
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Cabinet+Grotesk:wght@700;800&family=Chivo+Mono:wght@500;700&family=Cinzel:wght@600;700;900&family=Cormorant+Garamond:ital,wght@0,600;0,700;1,600;1,700&family=DM+Mono:wght@500&family=DM+Sans:wght@500;700&family=Fraunces:opsz,wght@9..144,600;9..144,800&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&family=Lexend:wght@600;800&family=Newsreader:ital,opsz,wght@0,6..72,600;1,6..72,600&family=Orbitron:wght@700;900&family=Outfit:wght@500;700;800&family=Plus+Jakarta+Sans:wght@600;700;800&family=Space+Grotesk:wght@600;700&family=Syne:wght@700;800&display=swap" rel="stylesheet" />
|
|
19
19
|
<title>Total Recall — Portable Memory</title>
|
|
20
|
-
<script type="module" crossorigin src="/assets/index-
|
|
21
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-poecbHSl.js"></script>
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CpsWeV4X.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
24
24
|
<div id="root"></div>
|
package/frontend/dist/sw.js
CHANGED
package/frontend/src/App.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback, type ReactNode } from 'react'
|
|
2
|
-
import { BrowserRouter, Routes, Route, NavLink, Navigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
2
|
+
import { BrowserRouter, Routes, Route, NavLink, Link, Navigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
3
3
|
import './App.css'
|
|
4
|
-
import {
|
|
4
|
+
import { checkSession, logout, registerUnauthedCallback, fetchHealth } from './api'
|
|
5
5
|
import type { HealthData } from './types'
|
|
6
|
+
import DashboardOverview from './components/DashboardOverview'
|
|
6
7
|
import LoginPage from './pages/LoginPage'
|
|
7
8
|
import MemoryPage from './pages/MemoryPage'
|
|
8
9
|
import SettingsPage from './pages/SettingsPage'
|
|
@@ -22,54 +23,6 @@ import { SavedPages, WorkspacePage } from './components/workspace/WorkspacePage'
|
|
|
22
23
|
// ─── Auth state type ──────────────────────────────────────────────────────────
|
|
23
24
|
type AuthState = 'loading' | 'authed' | 'unauthed'
|
|
24
25
|
|
|
25
|
-
// ─── Sidebar ─────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
interface DotProps {
|
|
28
|
-
ok: boolean
|
|
29
|
-
label: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function StatusDot({ ok, label }: DotProps) {
|
|
33
|
-
return (
|
|
34
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 5, color: 'var(--text-tertiary)' }}>
|
|
35
|
-
<span style={{
|
|
36
|
-
width: 7, height: 7, borderRadius: '50%',
|
|
37
|
-
background: ok ? '#22c55e' : '#ef4444',
|
|
38
|
-
boxShadow: ok ? '0 0 6px rgba(34,197,94,0.5)' : '0 0 6px rgba(239,68,68,0.5)',
|
|
39
|
-
display: 'inline-block',
|
|
40
|
-
animation: ok ? undefined : 'blink 1s ease-in-out infinite',
|
|
41
|
-
}} />
|
|
42
|
-
{label}
|
|
43
|
-
</div>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Green / amber / red gate health for Network top-bar indicator. */
|
|
48
|
-
function networkGateTone(gate: HealthData['fetch_gate'] | undefined): {
|
|
49
|
-
color: string
|
|
50
|
-
label: string
|
|
51
|
-
title: string
|
|
52
|
-
} {
|
|
53
|
-
if (!gate) {
|
|
54
|
-
return { color: '#94a3b8', label: 'Gate n/a', title: 'Fetch gate stats not available' }
|
|
55
|
-
}
|
|
56
|
-
const errors = gate.total_errors || 0
|
|
57
|
-
const blocked = gate.total_blocked || 0
|
|
58
|
-
const queue = gate.current_queue_depth || 0
|
|
59
|
-
const inFlight = gate.current_in_flight || 0
|
|
60
|
-
const title = `dispatched ${gate.total_dispatched ?? 0} · errors ${errors} · blocked ${blocked} · in-flight ${inFlight} · queue ${queue}`
|
|
61
|
-
if (errors > 0 && (gate.total_completed || 0) === 0 && (gate.total_dispatched || 0) > 0) {
|
|
62
|
-
return { color: '#ef4444', label: 'Gate errors', title }
|
|
63
|
-
}
|
|
64
|
-
if (errors > 5 || queue > 10 || blocked > 20) {
|
|
65
|
-
return { color: '#ef4444', label: 'Gate hot', title }
|
|
66
|
-
}
|
|
67
|
-
if (errors > 0 || queue > 0 || blocked > 0 || inFlight > (gate.max_global_concurrency || 6) * 0.75) {
|
|
68
|
-
return { color: '#f59e0b', label: 'Gate busy', title }
|
|
69
|
-
}
|
|
70
|
-
return { color: '#22c55e', label: 'Gate ok', title }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
26
|
interface SidebarProps {
|
|
74
27
|
activeBrainId: string
|
|
75
28
|
onBrainChange: (id: string) => void
|
|
@@ -109,20 +62,12 @@ function Sidebar({ activeBrainId, onBrainChange, open, onClose, onActivity, plug
|
|
|
109
62
|
}
|
|
110
63
|
|
|
111
64
|
const ACCOUNT_LINKS = [
|
|
112
|
-
['network', 'Network'],
|
|
113
|
-
['mesh', 'Mesh'],
|
|
114
|
-
['webhooks', 'Webhooks'],
|
|
115
|
-
['secrets', 'Secrets & Keys'],
|
|
116
|
-
['notifications', 'Notifications'],
|
|
117
65
|
['settings', 'Settings'],
|
|
118
66
|
] as const
|
|
119
67
|
|
|
120
|
-
function AccountMenu({
|
|
68
|
+
function AccountMenu({ onLogout }: { health?: HealthData | null; onLogout: () => void }) {
|
|
121
69
|
const menu = useRef<HTMLDetailsElement>(null)
|
|
122
70
|
const trigger = useRef<HTMLElement>(null)
|
|
123
|
-
const gate = networkGateTone(health?.fetch_gate)
|
|
124
|
-
const serverOk = health?.status !== 'unreachable'
|
|
125
|
-
const daemonOk = health?.daemon === 'running'
|
|
126
71
|
|
|
127
72
|
function close() {
|
|
128
73
|
if (menu.current) menu.current.open = false
|
|
@@ -135,25 +80,6 @@ function AccountMenu({ health, onLogout }: { health: HealthData | null; onLogout
|
|
|
135
80
|
<nav className="account-menu-body" aria-label="Account">
|
|
136
81
|
{ACCOUNT_LINKS.map(([path, label]) => <NavLink key={path} to={`/${path}`} className={getNavLinkClass}
|
|
137
82
|
id={`nav-${path}`} onClick={close}>{label}</NavLink>)}
|
|
138
|
-
<NavLink to="/onboarding" className={getNavLinkClass} id="nav-onboarding-replay" onClick={close}>Setup guide</NavLink>
|
|
139
|
-
<label className="account-menu-host">API host
|
|
140
|
-
<select className="select" value={getApiBase()} onChange={event => setApiBase(event.target.value)}>
|
|
141
|
-
<option value="">Local API</option>
|
|
142
|
-
{import.meta.env.VITE_BRAIN_URL && <option value={import.meta.env.VITE_BRAIN_URL}>
|
|
143
|
-
{new URL(import.meta.env.VITE_BRAIN_URL).host}
|
|
144
|
-
</option>}
|
|
145
|
-
</select>
|
|
146
|
-
</label>
|
|
147
|
-
<div className="account-menu-health">
|
|
148
|
-
{health ? <>
|
|
149
|
-
{health.version && <span>v{health.version}</span>}
|
|
150
|
-
<StatusDot ok={serverOk} label={serverOk ? 'Server online' : 'Server offline'} />
|
|
151
|
-
<StatusDot ok={daemonOk} label={daemonOk ? 'Daemon running' : `Daemon ${health.daemon ?? 'unknown'}`} />
|
|
152
|
-
<NavLink to="/network" title={gate.title} data-testid="network-gate-indicator" onClick={close}>
|
|
153
|
-
<span aria-hidden="true" style={{ color: gate.color }}>● </span>{gate.label}
|
|
154
|
-
</NavLink>
|
|
155
|
-
</> : <span>Checking status…</span>}
|
|
156
|
-
</div>
|
|
157
83
|
<button type="button" className="btn btn-ghost btn-sm" id="btn-logout" onClick={onLogout}>Sign out</button>
|
|
158
84
|
</nav>
|
|
159
85
|
</details>
|
|
@@ -184,7 +110,9 @@ function MainContent({ activeBrainId }: { activeBrainId: string }) {
|
|
|
184
110
|
<OnboardingRedirect />
|
|
185
111
|
<div style={{ flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
186
112
|
<Routes>
|
|
187
|
-
<Route path="/" element={
|
|
113
|
+
<Route path="/" element={isOnboardingComplete()
|
|
114
|
+
? <DashboardOverview activeBrainId={activeBrainId} />
|
|
115
|
+
: <Navigate to="/onboarding" replace />} />
|
|
188
116
|
<Route path="/chat" element={<Navigate to="/memory" replace />} />
|
|
189
117
|
<Route path="/onboarding" element={<OnboardingPage />} />
|
|
190
118
|
<Route path="/memory" element={<MemoryPage activeBrainId={activeBrainId} />} />
|
|
@@ -357,6 +285,7 @@ function Dashboard() {
|
|
|
357
285
|
// Authed — render full dashboard
|
|
358
286
|
return (
|
|
359
287
|
<PluginWorkspace activeBrainId={activeBrainId} onOpenPlugin={closeSidebar}
|
|
288
|
+
onStaleBrain={!linkedBrainId && storedBrainId !== 'global' ? () => setStoredBrainId('global') : undefined}
|
|
360
289
|
renderShell={(pluginFolder, pluginPage, plugins) => {
|
|
361
290
|
const page = pageId ? <WorkspacePage id={pageId} brainId={pageBrainId} plugins={plugins} /> : pluginPage
|
|
362
291
|
return <div className="app-layout">
|
|
@@ -374,8 +303,13 @@ function Dashboard() {
|
|
|
374
303
|
</button>
|
|
375
304
|
<div className="workspace-main" inert={edge !== null}>
|
|
376
305
|
<header className="workspace-toolbar" aria-label="Dashboard">
|
|
377
|
-
<
|
|
378
|
-
<
|
|
306
|
+
<div className="workspace-toolbar-spacer" aria-hidden="true" />
|
|
307
|
+
<BrandMark height={46} className="workspace-logo" />
|
|
308
|
+
<div className="workspace-toolbar-actions">
|
|
309
|
+
<Link to="/" className="btn btn-ghost btn-sm">Dashboard</Link>
|
|
310
|
+
<Link to="/memory?plugins=open" className="btn btn-ghost btn-sm">Plugins</Link>
|
|
311
|
+
<AccountMenu health={health} onLogout={handleLogout} />
|
|
312
|
+
</div>
|
|
379
313
|
</header>
|
|
380
314
|
<div className="workspace-body" hidden={!!page}>
|
|
381
315
|
<MainContent activeBrainId={activeBrainId} />
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
import { fetchHealth } from '../api'
|
|
4
|
+
import { fetchMemoryStats, fetchVaultStatus } from '../api/memory'
|
|
5
|
+
import { fetchLeader, fetchNodes, type LeaderInfo, type MeshNode } from '../api/mesh'
|
|
6
|
+
import type { HealthData } from '../types'
|
|
7
|
+
|
|
8
|
+
interface DashboardOverviewProps {
|
|
9
|
+
activeBrainId: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatUptime(seconds?: number): string {
|
|
13
|
+
if (!seconds || seconds < 0) return '—'
|
|
14
|
+
const h = Math.floor(seconds / 3600)
|
|
15
|
+
const m = Math.floor((seconds % 3600) / 60)
|
|
16
|
+
if (h > 0) return `${h}h ${m}m`
|
|
17
|
+
return `${m}m`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatDate(iso?: string): string {
|
|
21
|
+
if (!iso) return '—'
|
|
22
|
+
const parsed = Date.parse(iso)
|
|
23
|
+
if (Number.isNaN(parsed)) return iso
|
|
24
|
+
return new Date(parsed).toLocaleString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default function DashboardOverview({ activeBrainId }: DashboardOverviewProps) {
|
|
28
|
+
const [health, setHealth] = useState<HealthData | null>(null)
|
|
29
|
+
const [vault, setVault] = useState<{ totalNodes?: number; node_count?: number; categories?: Record<string, number>; embeddings?: number | { vault_nodes?: number; session_chunks?: number }; lastCompiled?: string; last_compile?: string } | null>(null)
|
|
30
|
+
const [stats, setStats] = useState<{ total: number; byCategory: Record<string, number> } | null>(null)
|
|
31
|
+
const [nodes, setNodes] = useState<MeshNode[] | null>(null)
|
|
32
|
+
const [leader, setLeader] = useState<LeaderInfo | null>(null)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
let active = true
|
|
36
|
+
void Promise.resolve(fetchHealth()).then(data => { if (active && data) setHealth(data) }).catch(() => {})
|
|
37
|
+
void Promise.resolve(fetchVaultStatus()).then(data => { if (active && data) setVault(data) }).catch(() => {})
|
|
38
|
+
void Promise.resolve(fetchMemoryStats()).then(data => { if (active && data) setStats(data) }).catch(() => {})
|
|
39
|
+
void Promise.resolve(fetchNodes()).then(data => { if (active && data) setNodes(data) }).catch(() => {})
|
|
40
|
+
void Promise.resolve(fetchLeader()).then(data => { if (active && data) setLeader(data) }).catch(() => {})
|
|
41
|
+
return () => { active = false }
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
const serverOk = health ? health.status !== 'unreachable' : false
|
|
45
|
+
const daemonOk = health?.daemon === 'running'
|
|
46
|
+
const brainLabel = activeBrainId === 'global'
|
|
47
|
+
? 'Global Brain'
|
|
48
|
+
: activeBrainId.startsWith('project:')
|
|
49
|
+
? activeBrainId.slice('project:'.length)
|
|
50
|
+
: activeBrainId
|
|
51
|
+
|
|
52
|
+
const totalNodes = vault?.totalNodes ?? vault?.node_count ?? stats?.total ?? 0
|
|
53
|
+
const embeddings = typeof vault?.embeddings === 'object' && vault.embeddings !== null
|
|
54
|
+
? (vault.embeddings.vault_nodes ?? 0)
|
|
55
|
+
: (vault?.embeddings ?? 0)
|
|
56
|
+
const lastCompiled = vault?.lastCompiled || vault?.last_compile
|
|
57
|
+
const byCategory = stats?.byCategory ?? vault?.categories ?? {}
|
|
58
|
+
const onlineMesh = nodes?.filter(n => n.online).length ?? 0
|
|
59
|
+
const totalMesh = nodes?.length ?? 0
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<section
|
|
63
|
+
aria-label="Main dashboard"
|
|
64
|
+
style={{ padding: 'clamp(12px, 3vw, 24px)', maxWidth: '1120px', width: '100%', boxSizing: 'border-box', minWidth: 0, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '24px' }}
|
|
65
|
+
>
|
|
66
|
+
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '12px' }}>
|
|
67
|
+
<div>
|
|
68
|
+
<h1 style={{ margin: 0, fontSize: '1.6rem' }}>System Dashboard</h1>
|
|
69
|
+
<p style={{ margin: '4px 0 0', color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
|
|
70
|
+
Live runtime, memory vault, rules, and mesh status across Total Recall.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
|
74
|
+
<Link to="/memory" className="btn btn-primary btn-sm">Open Memory</Link>
|
|
75
|
+
<Link to="/rules" className="btn btn-ghost btn-sm">Rules</Link>
|
|
76
|
+
<Link to="/graph" className="btn btn-ghost btn-sm">Graph</Link>
|
|
77
|
+
<Link to="/settings" className="btn btn-ghost btn-sm">Settings</Link>
|
|
78
|
+
</div>
|
|
79
|
+
</header>
|
|
80
|
+
|
|
81
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(200px, 100%), 1fr))', gap: '12px', minWidth: 0 }}>
|
|
82
|
+
<div style={{ padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-secondary)', border: '1px solid var(--border)' }}>
|
|
83
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Server</div>
|
|
84
|
+
<div style={{ fontSize: '1.15rem', fontWeight: 600, marginTop: '4px', color: serverOk ? '#22c55e' : '#ef4444' }}>
|
|
85
|
+
{!health ? 'Checking…' : serverOk ? 'Online' : 'Unreachable'}
|
|
86
|
+
</div>
|
|
87
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-tertiary)', marginTop: '4px' }}>
|
|
88
|
+
{health?.version ? `v${health.version}` : '—'} · Uptime {formatUptime(health?.uptime_seconds)}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div style={{ padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-secondary)', border: '1px solid var(--border)' }}>
|
|
93
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Daemon</div>
|
|
94
|
+
<div style={{ fontSize: '1.15rem', fontWeight: 600, marginTop: '4px', color: daemonOk ? '#22c55e' : 'var(--text-primary)' }}>
|
|
95
|
+
{!health ? 'Checking…' : daemonOk ? 'Running' : (health.daemon ?? 'Unknown')}
|
|
96
|
+
</div>
|
|
97
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-tertiary)', marginTop: '4px' }}>
|
|
98
|
+
Background indexing & sync
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<Link to="/memory" style={{ textDecoration: 'none', color: 'inherit', padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-secondary)', border: '1px solid var(--border)' }}>
|
|
103
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Active Brain</div>
|
|
104
|
+
<div style={{ fontSize: '1.15rem', fontWeight: 600, marginTop: '4px', overflowWrap: 'anywhere' }}>
|
|
105
|
+
{brainLabel}
|
|
106
|
+
</div>
|
|
107
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-tertiary)', marginTop: '4px' }}>
|
|
108
|
+
{totalNodes} nodes · {embeddings} vectors
|
|
109
|
+
</div>
|
|
110
|
+
</Link>
|
|
111
|
+
|
|
112
|
+
<div style={{ padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-secondary)', border: '1px solid var(--border)' }}>
|
|
113
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>Mesh Fleet</div>
|
|
114
|
+
<div style={{ fontSize: '1.15rem', fontWeight: 600, marginTop: '4px' }}>
|
|
115
|
+
{nodes === null ? 'Checking…' : `${onlineMesh} / ${totalMesh} Online`}
|
|
116
|
+
</div>
|
|
117
|
+
<div style={{ fontSize: '0.8rem', color: 'var(--text-tertiary)', marginTop: '4px', overflowWrap: 'anywhere' }}>
|
|
118
|
+
{leader?.hostname ? `Leader: ${leader.hostname}` : 'Tailscale / Headscale mesh'}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<section aria-label="Vault breakdown" style={{ padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-secondary)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
124
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '8px' }}>
|
|
125
|
+
<h2 style={{ margin: 0, fontSize: '1rem' }}>Memory & Rules Breakdown</h2>
|
|
126
|
+
<span style={{ fontSize: '0.8rem', color: 'var(--text-tertiary)' }}>
|
|
127
|
+
Last compiled: {formatDate(lastCompiled)}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(130px, 100%), 1fr))', gap: '10px', minWidth: 0 }}>
|
|
131
|
+
{Object.entries(byCategory).length === 0 ? (
|
|
132
|
+
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>No category stats available yet.</span>
|
|
133
|
+
) : (
|
|
134
|
+
Object.entries(byCategory).map(([category, count]) => (
|
|
135
|
+
<div key={category} style={{ padding: '10px 12px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-primary)' }}>
|
|
136
|
+
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', textTransform: 'capitalize' }}>{category}</div>
|
|
137
|
+
<div style={{ fontSize: '1.1rem', fontWeight: 600, marginTop: '2px' }}>{count}</div>
|
|
138
|
+
</div>
|
|
139
|
+
))
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
{nodes && nodes.length > 0 && (
|
|
145
|
+
<section aria-label="Mesh nodes" style={{ padding: '16px', borderRadius: 'var(--radius-md)', background: 'var(--bg-secondary)', border: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
146
|
+
<h2 style={{ margin: 0, fontSize: '1rem' }}>Mesh Nodes</h2>
|
|
147
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(210px, 100%), 1fr))', gap: '10px', minWidth: 0 }}>
|
|
148
|
+
{nodes.map(node => (
|
|
149
|
+
<div key={node.hostname} style={{ padding: '10px 12px', borderRadius: 'var(--radius-sm)', border: '1px solid var(--border)', background: 'var(--bg-primary)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', minWidth: 0 }}>
|
|
150
|
+
<div style={{ minWidth: 0 }}>
|
|
151
|
+
<div style={{ fontSize: '0.88rem', fontWeight: 600, overflowWrap: 'anywhere' }}>{node.hostname}</div>
|
|
152
|
+
<div style={{ fontSize: '0.76rem', color: 'var(--text-tertiary)' }}>{node.ip || node.os || '—'}</div>
|
|
153
|
+
</div>
|
|
154
|
+
<span className={`badge ${node.online ? 'badge-success' : ''}`}>
|
|
155
|
+
{node.online ? 'Online' : 'Offline'}
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
)}
|
|
162
|
+
</section>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
@@ -15,7 +15,7 @@ interface BrandMarkProps {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const SRC: Record<Exclude<BrandVariant, 'animated'>, string> = {
|
|
18
|
-
lockup: '/brand/total-recall-lockup.png',
|
|
18
|
+
lockup: '/brand/total-recall-lockup-transparent.png',
|
|
19
19
|
icon: '/brand/total-recall-icon.svg',
|
|
20
20
|
badge: '/brand/total-recall-badge.jpg',
|
|
21
21
|
mark: '/brand/total-recall-mark.svg',
|
|
@@ -49,6 +49,9 @@ export default function BrandMark({
|
|
|
49
49
|
maxWidth: '100%',
|
|
50
50
|
objectFit: 'contain',
|
|
51
51
|
display: 'block',
|
|
52
|
+
filter: variant === 'lockup'
|
|
53
|
+
? 'drop-shadow(0 0 14px rgba(56, 189, 248, 0.52)) drop-shadow(0 0 28px rgba(37, 99, 235, 0.28))'
|
|
54
|
+
: undefined,
|
|
52
55
|
}}
|
|
53
56
|
draggable={false}
|
|
54
57
|
/>
|
|
@@ -63,11 +66,10 @@ export default function BrandMark({
|
|
|
63
66
|
display: 'inline-flex',
|
|
64
67
|
alignItems: 'center',
|
|
65
68
|
justifyContent: 'center',
|
|
66
|
-
padding:
|
|
67
|
-
borderRadius:
|
|
68
|
-
background: '
|
|
69
|
-
boxShadow:
|
|
70
|
-
'0 1px 0 rgba(255,255,255,0.6) inset, 0 12px 32px rgba(15, 23, 42, 0.35), 0 0 0 1px rgba(148,163,184,0.25)',
|
|
69
|
+
padding: 8,
|
|
70
|
+
borderRadius: 16,
|
|
71
|
+
background: 'rgba(15, 23, 42, 0.55)',
|
|
72
|
+
boxShadow: '0 0 24px rgba(56, 189, 248, 0.22), 0 0 0 1px rgba(56, 189, 248, 0.22)',
|
|
71
73
|
}}
|
|
72
74
|
>
|
|
73
75
|
{img}
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
} from '../../api/plugins'
|
|
8
8
|
import { PluginPanel, pluginElements } from './PluginPanel'
|
|
9
9
|
import { PluginSettings } from './PluginSettings'
|
|
10
|
-
import { PluginChunkLibrary } from '../workspace/PluginChunkLibrary'
|
|
11
10
|
import { PluginHome } from '../workspace/PluginHome'
|
|
12
11
|
import { readSavedPluginCredentials } from '../../api/plugin-credentials'
|
|
13
12
|
import { ScopedPluginCommands } from './ScopedPluginCommands'
|
|
@@ -19,6 +18,7 @@ interface WorkspaceProps {
|
|
|
19
18
|
activeBrainId: string
|
|
20
19
|
renderShell: (folder: ReactNode, page: ReactNode, plugins: PluginInfo[] | null) => ReactNode
|
|
21
20
|
onOpenPlugin: () => void
|
|
21
|
+
onStaleBrain?: () => void
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function pluginOrigin(plugin: PluginInfo) {
|
|
@@ -28,7 +28,7 @@ function pluginOrigin(plugin: PluginInfo) {
|
|
|
28
28
|
return 'User'
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export default function PluginWorkspace({ activeBrainId, renderShell, onOpenPlugin }: WorkspaceProps) {
|
|
31
|
+
export default function PluginWorkspace({ activeBrainId, renderShell, onOpenPlugin, onStaleBrain }: WorkspaceProps) {
|
|
32
32
|
const [params, setParams] = useSearchParams()
|
|
33
33
|
const { pathname } = useLocation()
|
|
34
34
|
const [expanded, setExpanded] = useState<boolean>()
|
|
@@ -57,7 +57,12 @@ export default function PluginWorkspace({ activeBrainId, renderShell, onOpenPlug
|
|
|
57
57
|
setOpenAttempt(attempt => attempt?.error ? undefined : attempt)
|
|
58
58
|
} catch (error) {
|
|
59
59
|
if (request.current !== current) return
|
|
60
|
-
|
|
60
|
+
const message = error instanceof Error ? error.message : 'Could not load plugins.'
|
|
61
|
+
if (activeBrainId !== 'global' && onStaleBrain && message.includes('unavailable')) {
|
|
62
|
+
onStaleBrain()
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
setFailure({ selection: activeBrainId, message })
|
|
61
66
|
}
|
|
62
67
|
}, [activeBrainId])
|
|
63
68
|
|
|
@@ -122,6 +127,39 @@ export default function PluginWorkspace({ activeBrainId, renderShell, onOpenPlug
|
|
|
122
127
|
<p role="alert">{loadError || `No installed plugin matches ${id}.`}</p>
|
|
123
128
|
<button type="button" className="btn btn-ghost" onClick={() => select(null)}>Close</button>
|
|
124
129
|
</section>
|
|
130
|
+
else if (params.has('plugins') && pathname === '/memory' && !params.get('page')) page = (
|
|
131
|
+
<section className="plugin-workspace-page" aria-label="Installed plugins overview" style={{ padding: '24px', maxWidth: '1120px', width: '100%', margin: '0 auto' }}>
|
|
132
|
+
<header style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '12px', marginBottom: '20px' }}>
|
|
133
|
+
<div>
|
|
134
|
+
<h1 style={{ margin: 0, fontSize: '1.5rem' }}>Plugins</h1>
|
|
135
|
+
<p style={{ margin: '4px 0 0', color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
|
|
136
|
+
{plugins ? `${plugins.length} installed plugins` : 'Loading installed plugins…'}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<button type="button" className="btn btn-primary btn-sm" onClick={() => setBrowse(true)}>Browse and install</button>
|
|
140
|
+
</header>
|
|
141
|
+
{loadError ? <div className="plugin-folder-message" role="alert">
|
|
142
|
+
{loadError} <button className="btn btn-ghost btn-sm" onClick={() => void refresh()}>Retry</button>
|
|
143
|
+
</div> : plugins === null ? <p role="status">Loading plugins…</p> : (
|
|
144
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))', gap: '12px' }}>
|
|
145
|
+
{plugins.map(plugin => (
|
|
146
|
+
<button key={plugin.id} type="button" onClick={() => { select(plugin.id); void openArmed(plugin) }}
|
|
147
|
+
style={{
|
|
148
|
+
textAlign: 'left', padding: '16px', borderRadius: 'var(--radius-md)',
|
|
149
|
+
background: 'var(--bg-secondary)', border: '1px solid var(--border)',
|
|
150
|
+
color: 'var(--text-primary)', cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: '6px'
|
|
151
|
+
}}>
|
|
152
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px' }}>
|
|
153
|
+
<strong style={{ fontSize: '1rem' }}>{plugin.name}</strong>
|
|
154
|
+
<small style={{ color: 'var(--text-secondary)' }}>{pluginOrigin(plugin)}</small>
|
|
155
|
+
</div>
|
|
156
|
+
{plugin.description && <span style={{ color: 'var(--text-secondary)', fontSize: '0.85rem', lineHeight: 1.4 }}>{plugin.description}</span>}
|
|
157
|
+
</button>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</section>
|
|
162
|
+
)
|
|
125
163
|
|
|
126
164
|
return <>
|
|
127
165
|
{renderShell(folder, page, loadError ? null : plugins)}
|
|
@@ -211,73 +249,74 @@ function PluginDetail({ plugin, plugins, brainId, stale, opening, onUse, onChang
|
|
|
211
249
|
} finally { setChanging(false) }
|
|
212
250
|
}
|
|
213
251
|
|
|
252
|
+
const activationList = !!rows.length && <ul className="plugin-folder-list" aria-label="Activation by brain">
|
|
253
|
+
{rows.map(row => <li className="plugin-drawer-actions" key={row.brainId}>
|
|
254
|
+
<span>{row.name}{row.brainId === 'global' && <small className="plugin-muted"> · Shared default</small>}</span>
|
|
255
|
+
{row.state === 'unavailable' || row.state === 'error' ? <span>{row.state === 'error' ? 'Error' : 'Unavailable'}</span> :
|
|
256
|
+
row.brainId.startsWith('project:') ? <select className="select"
|
|
257
|
+
style={{ minHeight: 44, width: 'auto' }}
|
|
258
|
+
aria-label={`${plugin.name} activation for ${row.name}`} aria-busy={busy || undefined}
|
|
259
|
+
disabled={stale || busy || row.required} value={row.mode || (row.enabled ? 'on' : 'off')}
|
|
260
|
+
onChange={event => {
|
|
261
|
+
const mode = event.currentTarget.value
|
|
262
|
+
void change(row, mode === 'on-use' ? mode : mode === 'on')
|
|
263
|
+
}}>
|
|
264
|
+
<option value="off">Off</option>
|
|
265
|
+
<option value="on-use">On use</option>
|
|
266
|
+
<option value="on">On</option>
|
|
267
|
+
</select> :
|
|
268
|
+
<button type="button" role="switch" aria-checked={row.enabled}
|
|
269
|
+
aria-label={rows.length === 1 ? `${plugin.name} activation` : `${plugin.name} activation for ${row.name}`}
|
|
270
|
+
title={row.required ? 'Required plugin' : undefined} className="btn btn-ghost btn-sm"
|
|
271
|
+
disabled={stale || busy || row.required} onClick={() => void change(row)}>
|
|
272
|
+
{stale ? 'Unknown' : busy ? 'Updating…' : row.enabled ? 'On' : 'Off'}
|
|
273
|
+
</button>}
|
|
274
|
+
{!row.enabled && cleanupRetries.includes(row.brainId) && <button type="button"
|
|
275
|
+
className="btn btn-ghost btn-sm" disabled={stale || busy}
|
|
276
|
+
aria-label={`Retry cleanup for ${row.name}`} onClick={() => void change(row, false)}>Retry cleanup</button>}
|
|
277
|
+
</li>)}
|
|
278
|
+
</ul>
|
|
279
|
+
|
|
214
280
|
return <section className="plugin-workspace-page" aria-label={plugin.name}>
|
|
215
281
|
<header className="plugin-drawer-header">
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
</header>
|
|
219
|
-
<p className="plugin-muted">{plugin.description}</p>
|
|
220
|
-
<dl className="plugin-detail-status">
|
|
221
|
-
<div><dt>Brain activation</dt><dd>
|
|
282
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', flexWrap: 'wrap', minWidth: 0 }}>
|
|
283
|
+
<h1>{plugin.name}</h1>
|
|
222
284
|
<span className={`badge ${error ? 'badge-error' : enabled ? 'badge-success' : ''}`}>
|
|
223
285
|
{stale ? 'Unknown' : activation?.state === 'mixed' ? 'Mixed' : error ? 'Error' : activation?.mode === 'on-use' ? 'On use' : enabled ? 'Active' : activation ? 'Off' : 'Unknown'}
|
|
224
286
|
</span>
|
|
225
|
-
</
|
|
226
|
-
<div
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<button type="button" role="switch" aria-checked={row.enabled}
|
|
247
|
-
aria-label={rows.length === 1 ? `${plugin.name} activation` : `${plugin.name} activation for ${row.name}`}
|
|
248
|
-
title={row.required ? 'Required plugin' : undefined} className="btn btn-ghost btn-sm"
|
|
249
|
-
disabled={stale || busy || row.required} onClick={() => void change(row)}>
|
|
250
|
-
{stale ? 'Unknown' : busy ? 'Updating…' : row.enabled ? 'On' : 'Off'}
|
|
251
|
-
</button>}
|
|
252
|
-
{!row.enabled && cleanupRetries.includes(row.brainId) && <button type="button"
|
|
253
|
-
className="btn btn-ghost btn-sm" disabled={stale || busy}
|
|
254
|
-
aria-label={`Retry cleanup for ${row.name}`} onClick={() => void change(row, false)}>Retry cleanup</button>}
|
|
255
|
-
</li>)}
|
|
256
|
-
</ul>}
|
|
287
|
+
</div>
|
|
288
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>
|
|
289
|
+
{brainId && canManage && (
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
className={`btn btn-sm ${tab === 'settings' ? 'btn-primary' : 'btn-ghost'}`}
|
|
293
|
+
aria-pressed={tab === 'settings'}
|
|
294
|
+
onClick={() => setTab(tab === 'settings' ? 'home' : 'settings')}
|
|
295
|
+
>
|
|
296
|
+
Settings
|
|
297
|
+
</button>
|
|
298
|
+
)}
|
|
299
|
+
{brainId && tab === 'commands' && (
|
|
300
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={() => setTab('home')}>
|
|
301
|
+
Back
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
<button type="button" className="btn btn-ghost btn-sm" onClick={onClose}>Close</button>
|
|
305
|
+
</div>
|
|
306
|
+
</header>
|
|
307
|
+
{plugin.description && <p className="plugin-muted" style={{ marginBottom: '12px' }}>{plugin.description}</p>}
|
|
257
308
|
{stale && <p role="alert">The installed state could not be refreshed.</p>}
|
|
258
309
|
{error && <p role="alert">{plugin.errors[0] || 'The activation policy could not be read.'}</p>}
|
|
259
310
|
{opening?.error && !enabled && !notice && <p role="alert">{opening.error}</p>}
|
|
260
311
|
{notice && <p role={notice.error ? 'alert' : 'status'}>{notice.text}</p>}
|
|
261
|
-
{!brainId ? <p className="plugin-muted">Select one brain to open this plugin’s tools.</p
|
|
262
|
-
<nav className="plugin-drawer-actions" aria-label={`${plugin.name} views`}>
|
|
263
|
-
<button className={`btn ${tab === 'home' ? 'btn-primary' : 'btn-ghost'}`} aria-pressed={tab === 'home'}
|
|
264
|
-
onClick={() => setTab('home')}>Home</button>
|
|
265
|
-
{panel && enabled && <button className={`btn ${tab === 'interface' ? 'btn-primary' : 'btn-ghost'}`} aria-pressed={tab === 'interface'}
|
|
266
|
-
onClick={() => setTab('interface')}>Interface</button>}
|
|
267
|
-
{enabled && !stale && <button className={`btn ${tab === 'blocks' ? 'btn-primary' : 'btn-ghost'}`} aria-pressed={tab === 'blocks'}
|
|
268
|
-
onClick={() => setTab('blocks')}>Blocks</button>}
|
|
269
|
-
{canManage && <button className={`btn ${tab === 'settings' ? 'btn-primary' : 'btn-ghost'}`} aria-pressed={tab === 'settings'}
|
|
270
|
-
onClick={() => setTab('settings')}>Settings</button>}
|
|
271
|
-
{plugin.cli && <button className={`btn ${tab === 'commands' ? 'btn-primary' : 'btn-ghost'}`} aria-pressed={tab === 'commands'}
|
|
272
|
-
onClick={() => setTab('commands')}>Commands</button>}
|
|
273
|
-
</nav>
|
|
312
|
+
{!brainId ? <>{activationList}<p className="plugin-muted">Select one brain to open this plugin’s tools.</p></> : <>
|
|
274
313
|
{tab === 'home' && (enabled && !stale
|
|
275
314
|
? <PluginHome plugin={plugin} plugins={plugins} brainId={brainId} onCommand={openCommand} />
|
|
276
315
|
: armed && !stale ? <div className="plugin-drawer-actions">
|
|
277
316
|
<span className="plugin-muted">This plugin activates when you open it.</span>
|
|
278
317
|
<button type="button" className="btn btn-primary" disabled={busy}
|
|
279
318
|
onClick={onUse}>{busy ? 'Opening…' : 'Open plugin'}</button>
|
|
280
|
-
</div> : <p className="plugin-muted">An active plugin is needed to open
|
|
319
|
+
</div> : <p className="plugin-muted">An active plugin is needed to open this plugin.</p>)}
|
|
281
320
|
{tab === 'interface' && enabled && panel && <>
|
|
282
321
|
{panels.length > 1 && <label>Panel
|
|
283
322
|
<select className="select" value={panel.id} onChange={event => setPanelId(event.target.value)}>
|
|
@@ -287,6 +326,17 @@ function PluginDetail({ plugin, plugins, brainId, stale, opening, onUse, onChang
|
|
|
287
326
|
<PluginPanel key={panel.id} plugin={plugin} elementId={panel.id} brainId={brainId} readOnly={stale} />
|
|
288
327
|
</>}
|
|
289
328
|
{tab === 'settings' && canManage && <>
|
|
329
|
+
<dl className="plugin-detail-status">
|
|
330
|
+
<div><dt>Brain activation</dt><dd>
|
|
331
|
+
<span className={`badge ${error ? 'badge-error' : enabled ? 'badge-success' : ''}`}>
|
|
332
|
+
{stale ? 'Unknown' : activation?.state === 'mixed' ? 'Mixed' : error ? 'Error' : activation?.mode === 'on-use' ? 'On use' : enabled ? 'Active' : activation ? 'Off' : 'Unknown'}
|
|
333
|
+
</span>
|
|
334
|
+
</dd></div>
|
|
335
|
+
<div><dt>Authentication</dt><dd>{credentialLabel}</dd></div>
|
|
336
|
+
<div><dt>Provider connection</dt><dd>Unreported</dd></div>
|
|
337
|
+
<div><dt>Installed</dt><dd>v{plugin.version} · {pluginOrigin(plugin)} · {plugin.scope}</dd></div>
|
|
338
|
+
</dl>
|
|
339
|
+
{activationList}
|
|
290
340
|
{!panel && <p className="plugin-muted">This package has no installed workspace panel.</p>}
|
|
291
341
|
{plugin.valid && <PluginSettings plugin={plugin} />}
|
|
292
342
|
<details>
|
|
@@ -302,11 +352,16 @@ function PluginDetail({ plugin, plugins, brainId, stale, opening, onUse, onChang
|
|
|
302
352
|
</button>}
|
|
303
353
|
</div>
|
|
304
354
|
</details>
|
|
355
|
+
{plugin.cli && (
|
|
356
|
+
<details style={{ marginBottom: '12px' }}>
|
|
357
|
+
<summary>Developer CLI commands</summary>
|
|
358
|
+
<ScopedPluginCommands key={plugin.sha256} plugin={plugin} brainId={brainId}
|
|
359
|
+
enabled={enabled && !stale} initialCommand={initialCommand} />
|
|
360
|
+
</details>
|
|
361
|
+
)}
|
|
305
362
|
</>}
|
|
306
363
|
{tab === 'commands' && plugin.cli && <ScopedPluginCommands key={plugin.sha256} plugin={plugin} brainId={brainId}
|
|
307
364
|
enabled={enabled && !stale} initialCommand={initialCommand} />}
|
|
308
|
-
{tab === 'blocks' && enabled && !stale && <PluginChunkLibrary key={`${plugin.id}:${brainId}`}
|
|
309
|
-
plugin={plugin} brainId={brainId} />}
|
|
310
365
|
</>}
|
|
311
366
|
</section>
|
|
312
367
|
}
|