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.
Files changed (89) hide show
  1. package/bin/total-recall.mjs +5 -0
  2. package/frontend/dist/assets/index-CpsWeV4X.css +1 -0
  3. package/frontend/dist/assets/{index-D1goIPjC.js → index-poecbHSl.js} +100 -100
  4. package/frontend/dist/brand/total-recall-lockup-transparent.png +0 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/frontend/dist/sw.js +1 -1
  7. package/frontend/src/App.tsx +15 -81
  8. package/frontend/src/components/DashboardOverview.tsx +164 -0
  9. package/frontend/src/components/brand/BrandMark.tsx +8 -6
  10. package/frontend/src/components/plugins/PluginWorkspace.tsx +112 -57
  11. package/frontend/src/components/workspace/PluginHome.tsx +3 -3
  12. package/frontend/src/index.css +43 -11
  13. package/package.json +1 -1
  14. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +4 -4
  15. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +3 -2
  16. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +30 -5
  17. package/scaffold/.agent/skills/total-recall/plugins/auth/skills/auth/SKILL.md +35 -0
  18. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +494 -79
  19. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +4 -4
  20. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +3 -2
  21. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +33 -8
  22. package/scaffold/.agent/skills/total-recall/plugins/automations/skills/automations/SKILL.md +29 -0
  23. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/dream-consolidation.md +17 -0
  24. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
  25. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +3 -2
  26. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +31 -4
  27. package/scaffold/.agent/skills/total-recall/plugins/code-quality/skills/code-quality/SKILL.md +27 -0
  28. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
  29. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +1 -1
  30. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +17 -3
  31. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
  32. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +3 -2
  33. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +24 -4
  34. package/scaffold/.agent/skills/total-recall/plugins/embeddings/skills/embeddings/SKILL.md +25 -0
  35. package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
  36. package/scaffold/.agent/skills/total-recall/plugins/git/package.json +3 -2
  37. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +300 -58
  38. package/scaffold/.agent/skills/total-recall/plugins/git/skills/git/SKILL.md +36 -0
  39. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +5 -33
  40. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-fleet-ops.md +16 -0
  41. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/git-publish.md +16 -0
  42. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4 -4
  43. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -2
  44. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +47 -14
  45. package/scaffold/.agent/skills/total-recall/plugins/mesh/skills/mesh/SKILL.md +36 -0
  46. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +6 -3
  47. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +1 -1
  48. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +5 -2
  49. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/cascade.mjs +4 -2
  50. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/coordinator.mjs +28 -9
  51. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/dispatch.mjs +7 -1
  52. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/model-selection.mjs +59 -0
  53. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/remote-operation.mjs +3 -3
  54. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/router.mjs +9 -3
  55. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/worker.mjs +50 -6
  56. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/session-controls.md +16 -0
  57. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +175 -17
  58. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  59. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +3 -2
  60. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +242 -31
  61. package/scaffold/.agent/skills/total-recall/plugins/openwiki/skills/openwiki/SKILL.md +32 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/config.mjs +41 -15
  63. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +29 -22
  64. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +20 -33
  65. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/lint.mjs +42 -49
  66. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/markdown-links.mjs +112 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +3 -2
  68. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +219 -65
  69. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +2 -1
  70. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +21 -3
  71. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +51 -2
  72. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
  73. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +1 -1
  74. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +22 -5
  75. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +3 -2
  76. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +169 -37
  77. package/scaffold/.agent/skills/total-recall/plugins/ssss/skills/ssss/SKILL.md +30 -0
  78. package/src/cli/context.mjs +6 -2
  79. package/src/cli/plugin/create.mjs +2 -1
  80. package/src/cli/plugin/monetization.mjs +66 -32
  81. package/src/cli/plugin/search.mjs +25 -3
  82. package/src/cli/preflight.mjs +40 -6
  83. package/src/core/context-compactor.mjs +2 -0
  84. package/src/core/gate-mode.mjs +13 -24
  85. package/src/core/plugin-distribution.mjs +35 -1
  86. package/src/core/preflight-gate.mjs +28 -8
  87. package/src/core/preflight-receipt.mjs +11 -0
  88. package/src/server/routes/vault.mjs +1 -0
  89. package/frontend/dist/assets/index-BRYE1jmv.css +0 -1
@@ -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-D1goIPjC.js"></script>
21
- <link rel="stylesheet" crossorigin href="/assets/index-BRYE1jmv.css">
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>
@@ -14,7 +14,7 @@
14
14
  * skip the fetch entirely (handled client-side via the hash endpoint).
15
15
  */
16
16
 
17
- const CACHE_NAME = 'total-recall-v1';
17
+ const CACHE_NAME = 'total-recall-v4';
18
18
  const API_CACHE = 'total-recall-api-v3';
19
19
 
20
20
  // Pre-cache app shell
@@ -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 { getApiBase, setApiBase, checkSession, logout, registerUnauthedCallback, fetchHealth } from './api'
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({ health, onLogout }: { health: HealthData | null; onLogout: () => void }) {
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={<Navigate to={isOnboardingComplete() ? '/memory' : '/onboarding'} replace />} />
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
- <BrandMark height={64} className="workspace-logo" />
378
- <AccountMenu health={health} onLogout={handleLogout} />
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 &amp; 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 &amp; 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: variant === 'lockup' ? '10px 14px' : 8,
67
- borderRadius: variant === 'lockup' ? 14 : 16,
68
- background: 'linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%)',
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
- setFailure({ selection: activeBrainId, message: error instanceof Error ? error.message : 'Could not load plugins.' })
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
- <h1>{plugin.name}</h1>
217
- <button type="button" className="btn btn-ghost btn-sm" onClick={onClose}>Close</button>
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
- </dd></div>
226
- <div><dt>Authentication</dt><dd>{credentialLabel}</dd></div>
227
- <div><dt>Provider connection</dt><dd>Unreported</dd></div>
228
- <div><dt>Installed</dt><dd>v{plugin.version} · {pluginOrigin(plugin)} · {plugin.scope}</dd></div>
229
- </dl>
230
- {!!rows.length && <ul className="plugin-folder-list" aria-label="Activation by brain">
231
- {rows.map(row => <li className="plugin-drawer-actions" key={row.brainId}>
232
- <span>{row.name}{row.brainId === 'global' && <small className="plugin-muted"> · Shared default</small>}</span>
233
- {row.state === 'unavailable' || row.state === 'error' ? <span>{row.state === 'error' ? 'Error' : 'Unavailable'}</span> :
234
- row.brainId.startsWith('project:') ? <select className="select"
235
- style={{ minHeight: 44, width: 'auto' }}
236
- aria-label={`${plugin.name} activation for ${row.name}`} aria-busy={busy || undefined}
237
- disabled={stale || busy || row.required} value={row.mode || (row.enabled ? 'on' : 'off')}
238
- onChange={event => {
239
- const mode = event.currentTarget.value
240
- void change(row, mode === 'on-use' ? mode : mode === 'on')
241
- }}>
242
- <option value="off">Off</option>
243
- <option value="on-use">On use</option>
244
- <option value="on">On</option>
245
- </select> :
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 its Home tools.</p>)}
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
  }