total-recall-brain 3.40.7 → 3.41.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-memory.mjs +0 -0
- package/bin/total-recall.mjs +9 -40
- package/docs/reference/cli-reference.md +65 -1
- package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
- package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
- package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
- package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
- package/frontend/src/components/plugins/card/card.css +2 -5
- package/frontend/src/components/plugins/card/contract.ts +13 -19
- package/frontend/src/components/plugins/card/format.ts +8 -1
- package/frontend/src/components/plugins/card/widgets.tsx +4 -35
- package/frontend/src/components/plugins/plugin-frame.ts +30 -3
- package/frontend/src/pages/PluginsPage.tsx +3 -2
- package/package.json +1 -1
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
- package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
- package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
- package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
- package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
- package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
- package/scaffold/.agent/skills/plugin/references/card.md +90 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
- package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
- package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
- package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
- package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
- package/src/cli/brain.mjs +72 -2
- package/src/cli/connect.mjs +10 -39
- package/src/cli/context-stats.mjs +104 -0
- package/src/cli/context.mjs +8 -0
- package/src/cli/daemon.mjs +10 -1
- package/src/cli/export.mjs +3 -2
- package/src/cli/ingest-okf.mjs +12 -15
- package/src/cli/init.mjs +0 -85
- package/src/cli/lint.mjs +3 -2
- package/src/cli/plugin/copies.mjs +68 -0
- package/src/cli/plugin/create.mjs +22 -24
- package/src/cli/plugin/index.mjs +49 -3
- package/src/cli/plugin/search.mjs +19 -2
- package/src/cli/rebuild.mjs +2 -0
- package/src/cli/remember.mjs +23 -0
- package/src/cli/status.mjs +10 -3
- package/src/cli/update.mjs +3 -2
- package/src/core/action-predictor.mjs +161 -0
- package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
- package/src/core/app-deploy/source.mjs +16 -9
- package/src/core/automatic-brain-updates.mjs +6 -3
- package/src/core/brain-registry.mjs +19 -2
- package/src/core/brain-state.mjs +23 -0
- package/src/core/cli-commands.mjs +1 -0
- package/src/core/cli-maintenance.mjs +1 -1
- package/src/core/cli-semantic-index.mjs +1 -1
- package/src/core/command-surface.mjs +3 -3
- package/src/core/composed-command-resolution.mjs +21 -0
- package/src/core/config.mjs +84 -0
- package/src/core/confined-memory.mjs +93 -0
- package/src/core/context-telemetry.mjs +214 -0
- package/src/core/daemon-control.mjs +14 -43
- package/src/core/fast-recall.mjs +1 -1
- package/src/core/memory-brain.mjs +2 -0
- package/src/core/plugin-activation.mjs +38 -6
- package/src/core/plugin-bundle.mjs +54 -0
- package/src/core/plugin-catalog.mjs +1 -1
- package/src/core/plugin-context.mjs +1 -1
- package/src/core/plugin-contracts/card.mjs +69 -37
- package/src/core/plugin-loader.mjs +5 -6
- package/src/core/plugin-runner.mjs +13 -6
- package/src/core/plugin-runtime.mjs +5 -4
- package/src/core/plugin-setup.mjs +5 -3
- package/src/core/plugin-state-store.mjs +12 -1
- package/src/core/plugin-state.mjs +46 -5
- package/src/core/plugin-store.mjs +78 -3
- package/src/core/plugin-tasks.mjs +3 -1
- package/src/core/plugin-updates.mjs +117 -0
- package/src/core/plugin-vendor.mjs +10 -4
- package/src/core/project-brain.mjs +14 -41
- package/src/core/project-id.mjs +4 -1
- package/src/core/required-plugins.mjs +151 -13
- package/src/core/skills-registry.mjs +27 -3
- package/src/core/ssss-kernel-bridge.mjs +3 -1
- package/src/core/startup-health.mjs +66 -16
- package/src/core/surface.mjs +53 -46
- package/src/core/testing/plugin-fixture.mjs +69 -0
- package/src/core/vault-files.mjs +13 -0
- package/src/core/vault.mjs +4 -30
- package/src/server/index.mjs +13 -1
- package/src/server/invoke-decision-child.mjs +49 -0
- package/src/server/invoke-execution.mjs +87 -0
- package/src/server/invoke-policy.mjs +64 -0
- package/src/server/invoke-read-child.mjs +26 -0
- package/src/server/invoke-targets.mjs +49 -0
- package/src/server/invoke-write-child.mjs +19 -0
- package/src/server/routes/invoke.mjs +314 -0
- package/src/server/routes/plugins.mjs +1 -1
- package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
- package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
- package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
- package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
- package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
- package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
- package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
- package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
- package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
- package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
- package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
- package/scaffold/openwiki/README.md +0 -10
- package/scaffold/openwiki/architecture.md +0 -5
- package/scaffold/openwiki/ide.md +0 -10
- package/scaffold/openwiki/memory.md +0 -11
- package/scaffold/openwiki/secrets.md +0 -7
- package/scaffold/openwiki/skills.md +0 -7
- package/src/core/okf-adapter.mjs +0 -559
- package/templates/openwiki/README.md +0 -10
- package/templates/openwiki/architecture.md +0 -5
- package/templates/openwiki/ide.md +0 -10
- package/templates/openwiki/memory.md +0 -11
- package/templates/openwiki/secrets.md +0 -7
- package/templates/openwiki/skills.md +0 -7
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-BtuiZqwP.js"></script>
|
|
21
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BG9NkVUy.css">
|
|
22
22
|
</head>
|
|
23
23
|
<body>
|
|
24
24
|
<div id="root"></div>
|
|
@@ -2,7 +2,8 @@ import { useEffect, useRef, useState } from 'react'
|
|
|
2
2
|
import type { PluginInfo } from '../../api/plugins'
|
|
3
3
|
import { runPluginCommand } from '../../api/plugins'
|
|
4
4
|
import { apiFetch, API_BASE } from '../../api/_base'
|
|
5
|
-
import { pluginFrameDocument, allowedFrameCommand, pluginCommandTimeouts } from './plugin-frame'
|
|
5
|
+
import { pluginFrameDocument, allowedFrameCommand, pluginCommandTimeouts, sanitizePluginScreens } from './plugin-frame'
|
|
6
|
+
import type { PluginScreen } from './plugin-frame'
|
|
6
7
|
|
|
7
8
|
export interface PluginElement {
|
|
8
9
|
id: string
|
|
@@ -37,10 +38,29 @@ export function PluginPanel({ plugin, elementId, readOnly = false }: { plugin: P
|
|
|
37
38
|
const secrets = command.args?.filter(arg => arg.secret === true) || []
|
|
38
39
|
return secrets.length === 1 && secrets[0].type === 'string' && secrets[0].flag === undefined
|
|
39
40
|
}).map(command => command.name))
|
|
41
|
+
const element = elements.find(item => item.id === selected) || elements[0]
|
|
42
|
+
let ownerScreens: PluginScreen[] = []
|
|
43
|
+
const showScreen = (event: Event) => {
|
|
44
|
+
const detail = (event as CustomEvent).detail
|
|
45
|
+
if (disposed || element?.kind !== 'card' || detail?.pluginId !== plugin.id || !ownerScreens.some(screen => screen.id === detail.screen)) return
|
|
46
|
+
frame.current?.contentWindow?.postMessage({ channel, type: 'show-screen', screen: detail.screen }, '*')
|
|
47
|
+
}
|
|
40
48
|
let active = 0
|
|
41
49
|
const message = async (event: MessageEvent) => {
|
|
42
50
|
if (disposed || event.source !== frame.current?.contentWindow || event.origin !== 'null' || event.data?.channel !== channel) return
|
|
43
51
|
const data = event.data
|
|
52
|
+
if (data.type === 'screens') {
|
|
53
|
+
if (element?.kind === 'card') {
|
|
54
|
+
ownerScreens = sanitizePluginScreens(data.screens)
|
|
55
|
+
window.dispatchEvent(new CustomEvent('plugin-screens', { detail: { pluginId: plugin.id, screens: ownerScreens } }))
|
|
56
|
+
}
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (data.type === 'screen-changed') {
|
|
60
|
+
if (element?.kind === 'card' && ownerScreens.some(screen => screen.id === data.screen))
|
|
61
|
+
window.dispatchEvent(new CustomEvent('plugin-screen-changed', { detail: { pluginId: plugin.id, screen: data.screen } }))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
44
64
|
if (data.type === 'contextmenu') window.dispatchEvent(new CustomEvent('plugin-context-menu', { detail: { pluginId: plugin.id } }))
|
|
45
65
|
if (data.type === 'height' && Number.isFinite(data.height)) setHeight(Math.max(128, Math.min(1000, data.height)))
|
|
46
66
|
if (data.type === 'error') setError('Could not open the plugin interface')
|
|
@@ -63,8 +83,14 @@ export function PluginPanel({ plugin, elementId, readOnly = false }: { plugin: P
|
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
window.addEventListener('message', message)
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
window.addEventListener('plugin-show-screen', showScreen)
|
|
87
|
+
const dispose = () => {
|
|
88
|
+
disposed = true
|
|
89
|
+
window.removeEventListener('message', message)
|
|
90
|
+
window.removeEventListener('plugin-show-screen', showScreen)
|
|
91
|
+
if (element?.kind === 'card') window.dispatchEvent(new CustomEvent('plugin-screens', { detail: { pluginId: plugin.id, screens: [] } }))
|
|
92
|
+
}
|
|
93
|
+
if (!element) return dispose
|
|
68
94
|
const load = async () => {
|
|
69
95
|
const response = await apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(plugin.id)}/ui/${encodeURIComponent(element.id)}`)
|
|
70
96
|
if (!response.ok) throw new Error(`Could not open ${plugin.name} (${response.status})`)
|
|
@@ -74,10 +100,10 @@ export function PluginPanel({ plugin, elementId, readOnly = false }: { plugin: P
|
|
|
74
100
|
const styles = getComputedStyle(window.document.documentElement)
|
|
75
101
|
const theme: Record<string, string> = {}
|
|
76
102
|
for (const name of ['--text-primary', '--text-secondary', '--text-tertiary', '--bg-primary', '--bg-secondary', '--border', '--accent', '--font-sans', '--color-text', '--color-surface', '--color-border', '--spacing-md', '--radius-md']) theme[name] = styles.getPropertyValue(name)
|
|
77
|
-
setDocument(pluginFrameDocument(source, element.tag, channel, theme, plugin.id, pluginCommandTimeouts(plugin.manifest)))
|
|
103
|
+
setDocument(pluginFrameDocument(source, element.tag, channel, theme, plugin.id, pluginCommandTimeouts(plugin.manifest), [...allowed]))
|
|
78
104
|
}
|
|
79
105
|
load().catch(err => { if (!disposed) setError(err.message) })
|
|
80
|
-
return
|
|
106
|
+
return dispose
|
|
81
107
|
}, [plugin.id, selected, plugin.sha256, elementId, readOnly])
|
|
82
108
|
|
|
83
109
|
return <section>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import type { PluginInfo } from '../../../api/plugins'
|
|
3
3
|
import { apiFetch, API_BASE } from '../../../api/_base'
|
|
4
|
-
import { normalizeCard, type Card
|
|
4
|
+
import { normalizeCard, type Card } from './contract'
|
|
5
5
|
import { CardRunner } from './runner'
|
|
6
|
-
import { ActivityWidget, CommandsWidget,
|
|
6
|
+
import { ActivityWidget, CommandsWidget, SettingsWidget } from './widgets'
|
|
7
7
|
import { brandStyle } from './theme'
|
|
8
8
|
import { WorkspaceHost } from '../workspace/WorkspaceHost'
|
|
9
|
-
import { PluginPanel
|
|
9
|
+
import { PluginPanel } from '../PluginPanel'
|
|
10
10
|
import './card.css'
|
|
11
11
|
import { CardScenes } from './CardScenes'
|
|
12
12
|
|
|
@@ -45,17 +45,6 @@ function Header({ card, onOpenDrawer }: { card: Card; onOpenDrawer: () => void }
|
|
|
45
45
|
</header>
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function Region({ region, plugin }: { region: CardRegion; plugin: PluginInfo }) {
|
|
49
|
-
switch (region.widget) {
|
|
50
|
-
case 'status': return <StatusWidget />
|
|
51
|
-
case 'live': return <PluginPanel plugin={plugin} elementId={pluginElements(plugin).find(element => element.kind === 'card')?.id} readOnly />
|
|
52
|
-
case 'actions': case 'form': case 'keypad': return <CommandsWidget region={region.command ? { ...region, commands: [region.command] } : region} />
|
|
53
|
-
case 'table': case 'detail': case 'list': case 'gauge': case 'log': return <DataWidget region={region} />
|
|
54
|
-
case 'settings': case 'credentials': return <SettingsWidget plugin={plugin} />
|
|
55
|
-
case 'activity': return <ActivityWidget />
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
48
|
function Drawer({ plugin, card, onClose }: { plugin: PluginInfo; card: Card; onClose: () => void }) {
|
|
60
49
|
const hasWorkspace = Boolean(card.workspace)
|
|
61
50
|
const [tab, setTab] = useState<'workspace' | 'settings' | 'commands' | 'usage'>(hasWorkspace ? 'workspace' : 'settings')
|
|
@@ -89,26 +78,29 @@ function Drawer({ plugin, card, onClose }: { plugin: PluginInfo; card: Card; onC
|
|
|
89
78
|
<div className="card-drawer-body">
|
|
90
79
|
{tab === 'workspace' && <WorkspaceHost plugin={plugin} card={card} />}
|
|
91
80
|
{tab === 'settings' && <SettingsWidget plugin={plugin} />}
|
|
92
|
-
{tab === 'commands' && <CommandsWidget
|
|
81
|
+
{tab === 'commands' && <CommandsWidget />}
|
|
93
82
|
{tab === 'usage' && <ActivityWidget />}
|
|
94
83
|
</div>
|
|
95
84
|
</aside>
|
|
96
85
|
}
|
|
97
86
|
|
|
98
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* The host gives a plugin its frame, its brand tokens and its command
|
|
89
|
+
* plumbing. The layout is the plugin's own face: CardEngine never arranges
|
|
90
|
+
* a plugin's interface and there is no shared template to fall back to.
|
|
91
|
+
*/
|
|
99
92
|
export function CardEngine({ plugin }: { plugin: PluginInfo }) {
|
|
100
93
|
const card = useMemo(() => normalizeCard(plugin), [plugin])
|
|
101
|
-
const owned = pluginElements(plugin).some(element => element.kind === 'card' || element.kind === 'panel')
|
|
102
94
|
const [drawer, setDrawer] = useState(false)
|
|
103
95
|
return <CardRunner card={card}>
|
|
104
|
-
<section className={`card card
|
|
96
|
+
<section className={`card card-bespoke${card.face ? ' card-owned' : ''}`} style={brandStyle(card.branding)} data-density={card.branding.density} aria-label={`${card.title} card`}>
|
|
105
97
|
<Header card={card} onOpenDrawer={() => setDrawer(true)} />
|
|
106
98
|
{card.description && <p className="card-muted card-description">{card.description}</p>}
|
|
107
|
-
<CardScenes plugin={plugin}
|
|
108
|
-
{card.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</
|
|
99
|
+
<CardScenes plugin={plugin}>
|
|
100
|
+
{card.face
|
|
101
|
+
? <PluginPanel plugin={plugin} elementId={card.face.id} readOnly />
|
|
102
|
+
: <p className="card-error" role="alert">{plugin.name} has not supplied its card face.</p>}
|
|
103
|
+
</CardScenes>
|
|
112
104
|
{card.branding.provider && <footer className="card-footer">{card.branding.provider.url ? <a href={card.branding.provider.url} target="_blank" rel="noreferrer noopener">{card.branding.provider.name}</a> : card.branding.provider.name} is a trademark of its owner. This plugin connects to the service with your own account.</footer>}
|
|
113
105
|
{drawer && <Drawer plugin={plugin} card={card} onClose={() => setDrawer(false)} />}
|
|
114
106
|
</section>
|
|
@@ -3,10 +3,18 @@ import type { ReactNode } from 'react'
|
|
|
3
3
|
import type { PluginInfo } from '../../../api/plugins'
|
|
4
4
|
import { API_BASE, apiFetch } from '../../../api/_base'
|
|
5
5
|
import { useRunner } from './runner-context'
|
|
6
|
-
import { CommandsWidget } from './widgets'
|
|
7
6
|
import { ExecutionPicker } from './ExecutionPicker'
|
|
7
|
+
import { sanitizePluginScreens } from '../plugin-frame'
|
|
8
|
+
import type { PluginScreen } from '../plugin-frame'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The host gives a plugin a place to put its screens and a way to move between
|
|
12
|
+
* them. Which screens exist, what they are called and what is on them is the
|
|
13
|
+
* plugin's design: the face declares them and reports the active one. The host
|
|
14
|
+
* never derives a screen from the command list.
|
|
15
|
+
*/
|
|
16
|
+
export type { PluginScreen } from '../plugin-frame'
|
|
8
17
|
|
|
9
|
-
/** Generic navigation and activation; the overview stays the owner's bespoke interface. */
|
|
10
18
|
export function CardScenes({ plugin, children }: { plugin: PluginInfo; children: ReactNode }) {
|
|
11
19
|
const { card, busy } = useRunner()
|
|
12
20
|
const [enabled, setEnabled] = useState(plugin.activation?.enabled !== false)
|
|
@@ -15,31 +23,53 @@ export function CardScenes({ plugin, children }: { plugin: PluginInfo; children:
|
|
|
15
23
|
const [error, setError] = useState('')
|
|
16
24
|
const [ownerBusy, setOwnerBusy] = useState<Record<string, string>>({})
|
|
17
25
|
const [menu, setMenu] = useState(false)
|
|
26
|
+
const [screens, setScreens] = useState<PluginScreen[]>([])
|
|
27
|
+
const [activeScreen, setActiveScreen] = useState('')
|
|
28
|
+
const screensRef = useRef<PluginScreen[]>([])
|
|
18
29
|
useEffect(() => {
|
|
30
|
+
screensRef.current = []
|
|
31
|
+
setScreens([]); setActiveScreen(''); setMenu(false)
|
|
19
32
|
const activity = (event: Event) => {
|
|
20
33
|
const detail = (event as CustomEvent).detail
|
|
21
34
|
if (detail?.pluginId !== plugin.id) return
|
|
22
35
|
setOwnerBusy(previous => { const next = { ...previous }; if (detail.running) next[detail.requestId] = detail.command; else delete next[detail.requestId]; return next })
|
|
23
36
|
}
|
|
24
37
|
const contextMenu = (event: Event) => { if ((event as CustomEvent).detail?.pluginId === plugin.id) setMenu(true) }
|
|
38
|
+
const declared = (event: Event) => {
|
|
39
|
+
const detail = (event as CustomEvent).detail
|
|
40
|
+
if (detail?.pluginId !== plugin.id) return
|
|
41
|
+
const next = sanitizePluginScreens(detail.screens)
|
|
42
|
+
screensRef.current = next
|
|
43
|
+
setScreens(next)
|
|
44
|
+
setActiveScreen(previous => next.some(screen => screen.id === previous) ? previous : next[0]?.id || '')
|
|
45
|
+
}
|
|
46
|
+
const changed = (event: Event) => {
|
|
47
|
+
const detail = (event as CustomEvent).detail
|
|
48
|
+
if (detail?.pluginId === plugin.id && screensRef.current.some(screen => screen.id === detail.screen)) setActiveScreen(detail.screen)
|
|
49
|
+
}
|
|
25
50
|
window.addEventListener('plugin-command-activity', activity)
|
|
26
51
|
window.addEventListener('plugin-context-menu', contextMenu)
|
|
27
|
-
|
|
52
|
+
window.addEventListener('plugin-screens', declared)
|
|
53
|
+
window.addEventListener('plugin-screen-changed', changed)
|
|
54
|
+
return () => {
|
|
55
|
+
window.removeEventListener('plugin-command-activity', activity); window.removeEventListener('plugin-context-menu', contextMenu)
|
|
56
|
+
window.removeEventListener('plugin-screens', declared); window.removeEventListener('plugin-screen-changed', changed)
|
|
57
|
+
}
|
|
28
58
|
}, [plugin.id])
|
|
29
59
|
const running = [...busy, ...Object.values(ownerBusy)]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
const common = validActions.length ? validActions.slice(0, 6) : names.filter(name => !['on', 'off'].includes(name)).slice(0, 6).map(subcommand => ({ subcommand, label: subcommand }))
|
|
36
|
-
const behavior = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ? 'auto' as const : 'smooth' as const
|
|
37
|
-
const openCommand = (name: string) => {
|
|
60
|
+
// The face's own screens collapse into a single mount point: the plugin keeps
|
|
61
|
+
// every screen inside its own component and reports which one is active.
|
|
62
|
+
/** Ask the face to show a screen it declared; it may ignore a name it does not own. */
|
|
63
|
+
const openScreen = (name: string) => {
|
|
64
|
+
if (!screens.some(screen => screen.id === name)) return
|
|
38
65
|
setMenu(false)
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
window.dispatchEvent(new CustomEvent('plugin-show-screen', { detail: { pluginId: plugin.id, screen: name } }))
|
|
67
|
+
}
|
|
68
|
+
const move = (delta: number) => {
|
|
69
|
+
if (screens.length < 2) return
|
|
70
|
+
const current = Math.max(0, screens.findIndex(screen => screen.id === activeScreen))
|
|
71
|
+
openScreen(screens[(current + delta + screens.length) % screens.length].id)
|
|
41
72
|
}
|
|
42
|
-
const move = (delta: number) => track.current?.scrollBy({ left: delta * track.current.clientWidth, behavior: behavior() })
|
|
43
73
|
const toggle = async () => {
|
|
44
74
|
setChanging(true); setError('')
|
|
45
75
|
try {
|
|
@@ -56,19 +86,18 @@ export function CardScenes({ plugin, children }: { plugin: PluginInfo; children:
|
|
|
56
86
|
<div className="card-row card-scene-controls">
|
|
57
87
|
<span role="status" className="card-pill">{running.length ? `Running ${running.join(', ')}` : enabled ? 'Active' : 'Inactive'}</span>
|
|
58
88
|
<button type="button" className="card-button" role="switch" aria-checked={enabled} aria-label={`${card.title} on or off`} onClick={() => void toggle()} disabled={changing || running.length > 0}>{changing ? 'Updating…' : enabled ? 'Turn off' : 'Turn on'}</button>
|
|
59
|
-
<button type="button" className="card-link" aria-label="Previous
|
|
60
|
-
<button type="button" className="card-link" aria-label="Next
|
|
61
|
-
<
|
|
89
|
+
<button type="button" className="card-link" aria-label="Previous screen" disabled={screens.length < 2} onClick={() => move(-1)}>←</button>
|
|
90
|
+
<button type="button" className="card-link" aria-label="Next screen" disabled={screens.length < 2} onClick={() => move(1)}>→</button>
|
|
91
|
+
{screens.length > 0 && <span className="card-pill" aria-label="Current screen">{screens.find(screen => screen.id === activeScreen)?.label}</span>}
|
|
92
|
+
{screens.length > 0 && <button type="button" className="card-link" aria-haspopup="menu" aria-expanded={menu} onClick={() => setMenu(!menu)}>Screens</button>}
|
|
62
93
|
</div>
|
|
63
|
-
{menu && <div role="menu" aria-label={`${card.title}
|
|
64
|
-
{
|
|
65
|
-
{!common.length && <span>No commands declared.</span>}
|
|
94
|
+
{menu && screens.length > 0 && <div role="menu" aria-label={`${card.title} declared screens`} className="card-context-menu">
|
|
95
|
+
{screens.map(screen => <button role="menuitem" type="button" className="card-link" key={screen.id} aria-current={screen.id === activeScreen ? 'page' : undefined} onClick={() => openScreen(screen.id)}>{screen.label}</button>)}
|
|
66
96
|
</div>}
|
|
67
97
|
{error && <p role="alert" className="card-error">{error}</p>}
|
|
68
98
|
<ExecutionPicker />
|
|
69
|
-
<div className="card-scene-track"
|
|
70
|
-
<section className="card-scene" aria-label="
|
|
71
|
-
{names.map(name => <section className="card-scene" key={name} aria-label={`${name} command scene`}><CommandsWidget region={{ widget: 'actions', commands: [name] }} /></section>)}
|
|
99
|
+
<div className="card-scene-track" tabIndex={0} aria-label={`${card.title} interface`}>
|
|
100
|
+
<section className="card-scene" aria-label="Interface">{children}</section>
|
|
72
101
|
</div>
|
|
73
102
|
</div>
|
|
74
103
|
}
|
|
@@ -9,11 +9,8 @@
|
|
|
9
9
|
.card-badge{display:inline-block;margin:4px 6px 0 0;padding:1px 8px;border-radius:var(--card-radius);background:var(--card-accent);color:var(--card-on-accent);font-size:.7rem;font-weight:600}
|
|
10
10
|
.card-badge-quiet{background:transparent;color:var(--card-muted);border:1px solid var(--card-line)}
|
|
11
11
|
.card-description{margin:0}.card-muted{color:var(--card-muted);font-size:.85rem;margin:2px 0}
|
|
12
|
-
|
|
13
|
-
.card-
|
|
14
|
-
.card-workbench .card-regions,.card-inbox .card-regions{grid-template-columns:minmax(0,1fr) minmax(0,2fr)}.card-workbench .card-region,.card-inbox .card-region{grid-column:auto}
|
|
15
|
-
.card-ledger .card-region-table{grid-column:span 12}.card-vault .card-region{grid-column:span 12;border-left:3px solid var(--card-accent);padding-left:var(--card-gap)}
|
|
16
|
-
@media (max-width:720px){.card-regions{grid-template-columns:1fr}.card-regions>.card-region{grid-column:auto!important}}
|
|
12
|
+
/* The face supplies its own layout; the host only gives it a surface. */
|
|
13
|
+
.card-bespoke .card-face{display:block;min-width:0}
|
|
17
14
|
.card-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.card-kv{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin:8px 0 0}.card-kv dt{font-size:.7rem;color:var(--card-muted);text-transform:uppercase}.card-kv dd{margin:0;font-weight:600;overflow-wrap:anywhere}
|
|
18
15
|
.card-pill{padding:1px 8px;border-radius:999px;font-size:.75rem;font-weight:600;border:1px solid currentColor}.card-pill-ok{color:#047857}.card-pill-warn{color:#b45309}.card-pill-error{color:#b91c1c}.card-pill-neutral{color:var(--card-muted)}
|
|
19
16
|
.card-button{background:var(--card-color);color:var(--card-on-color);border:0;border-radius:var(--card-radius);padding:6px 12px;font:inherit;cursor:pointer}.card-button:disabled{opacity:.5;cursor:not-allowed}.card-button-quiet{background:transparent;color:var(--card-text);border:1px solid var(--card-line)}
|
|
@@ -2,14 +2,9 @@ import type { PluginInfo } from '../../../api/plugins'
|
|
|
2
2
|
|
|
3
3
|
export type ArgType = 'string' | 'number' | 'boolean' | 'choice'
|
|
4
4
|
export type Effect = 'read' | 'write' | 'destructive' | 'send'
|
|
5
|
-
export type Template = 'console' | 'ledger' | 'monitor' | 'inbox' | 'dashboard' | 'workbench' | 'vault'
|
|
6
|
-
export type WidgetKind = 'status' | 'actions' | 'table' | 'detail' | 'log' | 'form' | 'keypad' | 'gauge' | 'list' | 'settings' | 'credentials' | 'activity' | 'live'
|
|
7
|
-
export type CellFormat = 'text' | 'number' | 'money' | 'date' | 'datetime' | 'bytes' | 'percent' | 'status'
|
|
8
5
|
|
|
9
6
|
export interface CardArg { name: string; flag?: string; positional?: boolean; label: string; type: ArgType; options?: Array<string | number>; required?: boolean; secret?: boolean }
|
|
10
7
|
export interface CardCommand { name: string; description?: string; args: CardArg[]; effect: Effect; output: 'json' | 'text'; timeoutMs?: number }
|
|
11
|
-
export interface CardColumn { key: string; label: string; format?: CellFormat }
|
|
12
|
-
export interface CardRegion { widget: WidgetKind; title?: string; source?: { command: string; args?: string[] }; columns?: CardColumn[]; command?: string; commands?: string[]; span?: number }
|
|
13
8
|
export interface CardBranding {
|
|
14
9
|
name: string; color: string; accent: string; font: string; density: 'compact' | 'comfortable' | 'spacious'
|
|
15
10
|
radius: 'none' | 'sm' | 'md' | 'lg' | 'pill'; header: 'plain' | 'ledger-rule' | 'band' | 'tab-strip' | 'badge-left'
|
|
@@ -20,14 +15,15 @@ export interface CardWorkspace {
|
|
|
20
15
|
tabs?: string[];
|
|
21
16
|
permissions?: string[];
|
|
22
17
|
}
|
|
18
|
+
/** The plugin's own card face. Layout belongs to the plugin, never to a shared template. */
|
|
19
|
+
export interface CardFace { id: string; tag: string; module: string; description?: string }
|
|
23
20
|
export interface Card {
|
|
24
|
-
id: string; title: string; description: string;
|
|
21
|
+
id: string; title: string; description: string; face?: CardFace
|
|
25
22
|
commands: Map<string, CardCommand>; branding: CardBranding; status?: { command: string; refreshSeconds: number }
|
|
26
23
|
distribution?: { tier: string; license: string }
|
|
27
24
|
workspace?: CardWorkspace
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
const TEMPLATES: Template[] = ['console', 'ledger', 'monitor', 'inbox', 'dashboard', 'workbench', 'vault']
|
|
31
27
|
const FONTS = ['system', 'inter', 'ibm-plex', 'space-grotesk', 'jetbrains-mono', 'dm-sans', 'outfit', 'fraunces', 'newsreader', 'chivo-mono']
|
|
32
28
|
const HEX = /^#[0-9a-fA-F]{6}$/
|
|
33
29
|
/** Manifest JSON is untyped input; every field is checked before it is used. */
|
|
@@ -71,17 +67,15 @@ export function normalizeCard(plugin: PluginInfo): Card {
|
|
|
71
67
|
}
|
|
72
68
|
const status = obj(manifest.status)
|
|
73
69
|
const statusCommand = typeof status.command === 'string' && commands.has(status.command) ? status.command : undefined
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
regions.push({ widget: 'actions', title: 'Commands' }, { widget: 'activity', title: 'Recent activity' })
|
|
84
|
-
}
|
|
70
|
+
// The face is the plugin's own component; there is no shared layout to fall back to.
|
|
71
|
+
// Only `kind: 'card'` is the face. A plugin may declare any number of
|
|
72
|
+
// `kind: 'panel'` screens, and one declared before the card is still a
|
|
73
|
+
// secondary screen, never the face.
|
|
74
|
+
const elements: Loose[] = Array.isArray(manifest.ui?.elements) ? manifest.ui.elements : (Array.isArray(plugin.ui?.elements) ? plugin.ui.elements : [])
|
|
75
|
+
const declared = elements.find((element: Loose) => element && typeof element === 'object' && element.kind === 'card')
|
|
76
|
+
const face: CardFace | undefined = declared && typeof declared.id === 'string' && typeof declared.tag === 'string' && typeof declared.module === 'string'
|
|
77
|
+
? { id: declared.id, tag: declared.tag, module: declared.module, description: typeof declared.description === 'string' ? declared.description : undefined }
|
|
78
|
+
: undefined
|
|
85
79
|
const ws = obj(manifest.workspace)
|
|
86
80
|
const workspace: CardWorkspace | undefined = ws && (ws.component || Array.isArray(ws.tabs)) ? {
|
|
87
81
|
component: typeof ws.component === 'string' ? ws.component : undefined,
|
|
@@ -89,7 +83,7 @@ export function normalizeCard(plugin: PluginInfo): Card {
|
|
|
89
83
|
permissions: Array.isArray(ws.permissions) ? ws.permissions.filter((p: unknown) => typeof p === 'string') : undefined,
|
|
90
84
|
} : undefined
|
|
91
85
|
return {
|
|
92
|
-
id: plugin.id, title: branding.name, description: plugin.description,
|
|
86
|
+
id: plugin.id, title: branding.name, description: plugin.description, face, commands, branding,
|
|
93
87
|
status: statusCommand ? { command: statusCommand, refreshSeconds: Number.isInteger(status.refresh_seconds) ? Math.min(86400, Math.max(5, status.refresh_seconds)) : 30 } : undefined,
|
|
94
88
|
distribution: obj(manifest.distribution).tier ? { tier: manifest.distribution.tier, license: manifest.distribution.license } : undefined,
|
|
95
89
|
workspace,
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Loose } from './contract'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a scalar is rendered. This is presentation owned by the card face that
|
|
5
|
+
* asks for it — not a template primitive. The layout vocabulary (`CardRegion`,
|
|
6
|
+
* `CardColumn`, `CellFormat`) that once shared these names is deleted.
|
|
7
|
+
*/
|
|
8
|
+
export type CellFormat = 'text' | 'number' | 'money' | 'percent' | 'bytes' | 'date' | 'datetime'
|
|
2
9
|
|
|
3
10
|
export type Tone = 'ok' | 'warn' | 'error' | 'neutral'
|
|
4
11
|
const GOOD = /^(ok|healthy|active|connected|ready|running|verified|delivered|sent|passed|pass|up|online|true|complete|completed)$/i
|
|
@@ -3,8 +3,8 @@ import type { PluginInfo } from '../../../api/plugins'
|
|
|
3
3
|
import { PluginSettings } from '../PluginSettings'
|
|
4
4
|
import { defaultValue, missingRequired, visibleArgs, type ArgValues } from './args'
|
|
5
5
|
import { cliLine } from './cli-line'
|
|
6
|
-
import type {
|
|
7
|
-
import {
|
|
6
|
+
import type { CardCommand } from './contract'
|
|
7
|
+
import { scalarsOf, toneOf } from './format'
|
|
8
8
|
import { useRunner, type RunResult } from './runner-context'
|
|
9
9
|
import { useSource } from './use-source'
|
|
10
10
|
|
|
@@ -58,10 +58,10 @@ function ReadForm({ command }: { command: CardCommand }) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** Every declared operation, with arguments, explicit mutation confirmation and secure stdin. */
|
|
61
|
-
export function CommandsWidget(
|
|
61
|
+
export function CommandsWidget() {
|
|
62
62
|
const { card } = useRunner()
|
|
63
63
|
const cliName = card.id
|
|
64
|
-
const names =
|
|
64
|
+
const names = [...card.commands.keys()]
|
|
65
65
|
const [open, setOpen] = useState('')
|
|
66
66
|
const [copied, setCopied] = useState('')
|
|
67
67
|
return <ul className="card-commands">
|
|
@@ -83,37 +83,6 @@ export function CommandsWidget({ region }: { region: CardRegion }) {
|
|
|
83
83
|
</ul>
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function Cell({ value, column }: { value: unknown; column: CardColumn }) {
|
|
87
|
-
return column.format === 'status' ? <Pill tone={toneOf(value)}>{formatCell(value)}</Pill> : <>{formatCell(value, column.format)}</>
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** table, list, detail, gauge and log all read one declared command and show it. */
|
|
91
|
-
export function DataWidget({ region }: { region: CardRegion }) {
|
|
92
|
-
const { result, loading } = useSource(region.source?.command, region.source?.args)
|
|
93
|
-
if (!result) return <p className="card-muted">{loading ? 'Loading…' : ''}</p>
|
|
94
|
-
if (!result.ok) return <Failure result={result} />
|
|
95
|
-
const { widget } = region
|
|
96
|
-
if (widget === 'log') return <pre className="card-pre">{result.text || JSON.stringify(result.data, null, 2)}</pre>
|
|
97
|
-
if (widget === 'gauge') {
|
|
98
|
-
const d = result.data || {}
|
|
99
|
-
const observedValue = d.value ?? d.used ?? d.percent
|
|
100
|
-
const observedMax = d.max ?? d.total ?? (d.percent !== undefined ? 100 : undefined)
|
|
101
|
-
const value = Number(observedValue), max = Number(observedMax)
|
|
102
|
-
if (observedValue == null || observedMax == null || !Number.isFinite(value) || !Number.isFinite(max) || max <= 0 || value < 0 || value > max) {
|
|
103
|
-
return <p role="status" className="card-muted">No valid usage measurement is available.</p>
|
|
104
|
-
}
|
|
105
|
-
const pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0
|
|
106
|
-
return <div><div className="card-gauge" role="meter" aria-valuenow={value} aria-valuemin={0} aria-valuemax={max}><span style={{ width: `${pct}%` }} /></div><p className="card-muted">{formatCell(value, 'number')} of {formatCell(max, 'number')}</p></div>
|
|
107
|
-
}
|
|
108
|
-
if (widget === 'detail') return <dl className="card-kv">{scalarsOf(result.data).map(([k, v]) => <div key={k}><dt>{k.replace(/_/g, ' ')}</dt><dd>{v}</dd></div>)}</dl>
|
|
109
|
-
const rows = rowsOf(result.data)
|
|
110
|
-
if (rows.length === 0) return <p className="card-muted">Nothing to show yet.</p>
|
|
111
|
-
if (widget === 'list') return <ul className="card-list">{rows.map((row, i) => <li key={i}>{typeof row === 'object' && row ? String(row.title ?? row.name ?? row.id ?? JSON.stringify(row)) : String(row)}</li>)}</ul>
|
|
112
|
-
const columns = region.columns || Object.keys(rows[0] || {}).slice(0, 5).map(key => ({ key, label: key }))
|
|
113
|
-
return <div className="card-scroll"><table className="card-table"><thead><tr>{columns.map(c => <th key={c.key} scope="col">{c.label}</th>)}</tr></thead>
|
|
114
|
-
<tbody>{rows.map((row, i) => <tr key={i}>{columns.map(c => <td key={c.key}><Cell value={row?.[c.key]} column={c} /></td>)}</tr>)}</tbody></table></div>
|
|
115
|
-
}
|
|
116
|
-
|
|
117
86
|
export function SettingsWidget({ plugin }: { plugin: PluginInfo }) {
|
|
118
87
|
return <PluginSettings plugin={plugin} />
|
|
119
88
|
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
const inlineJson = (value: unknown) => JSON.stringify(value).replace(/</g, '\\u003c')
|
|
2
2
|
|
|
3
|
+
export interface PluginScreen { id: string; label: string }
|
|
4
|
+
|
|
5
|
+
/** Bound untrusted screen metadata without deriving any screen from commands. */
|
|
6
|
+
export function sanitizePluginScreens(value: unknown): PluginScreen[] {
|
|
7
|
+
if (!Array.isArray(value)) return []
|
|
8
|
+
const seen = new Set<string>()
|
|
9
|
+
return value.slice(0, 128).flatMap(item => {
|
|
10
|
+
if (!item || typeof item !== 'object' || typeof item.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(item.id)
|
|
11
|
+
|| typeof item.label !== 'string' || !item.label.trim() || item.label.length > 160 || /[\u0000-\u001f\u007f]/.test(item.label) || seen.has(item.id)) return []
|
|
12
|
+
seen.add(item.id)
|
|
13
|
+
return [{ id: item.id, label: item.label.trim() }]
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
/** Owner code runs in an opaque origin, with no dashboard DOM/storage or direct network. */
|
|
4
|
-
export function pluginFrameDocument(source: string, tag: string, channel: string, theme: Record<string, string> = {}, pluginId = '', commandTimeouts: Record<string, number> = {}) {
|
|
18
|
+
export function pluginFrameDocument(source: string, tag: string, channel: string, theme: Record<string, string> = {}, pluginId = '', commandTimeouts: Record<string, number> = {}, operations: readonly string[] = []) {
|
|
5
19
|
return `<!doctype html><html><head>
|
|
6
20
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${channel}' blob:; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com data:; img-src data: https:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
|
7
21
|
<meta name="referrer" content="no-referrer">
|
|
@@ -13,15 +27,27 @@ export function pluginFrameDocument(source: string, tag: string, channel: string
|
|
|
13
27
|
const channel=${inlineJson(channel)};
|
|
14
28
|
const pending=new Map();let sequence=0;
|
|
15
29
|
const send=(type,extra={})=>parent.postMessage({channel,type,...extra},'*');
|
|
30
|
+
let ownerScreens=[];
|
|
31
|
+
const sanitizeScreens=${sanitizePluginScreens.toString()};
|
|
32
|
+
function publishScreens(element){
|
|
33
|
+
ownerScreens=sanitizeScreens(element.screens);
|
|
34
|
+
send('screens',{screens:ownerScreens});
|
|
35
|
+
if(ownerScreens.some(screen=>screen.id===element.screen))host.screenChanged(element.screen);
|
|
36
|
+
}
|
|
16
37
|
window.addEventListener('contextmenu',event=>{event.preventDefault();send('contextmenu');});
|
|
17
38
|
for(const [name,value] of Object.entries(${inlineJson(theme)}))document.documentElement.style.setProperty(name,value);
|
|
18
39
|
window.addEventListener('message',event=>{
|
|
19
|
-
if(event.source!==parent||event.data?.channel!==channel
|
|
40
|
+
if(event.source!==parent||event.data?.channel!==channel)return;
|
|
41
|
+
if(event.data.type==='show-screen'){
|
|
42
|
+
if(ownerScreens.some(screen=>screen.id===event.data.screen))window.dispatchEvent(new CustomEvent('plugin-show-screen',{detail:{pluginId:${inlineJson(pluginId)},screen:event.data.screen}}));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if(event.data.type!=='result')return;
|
|
20
46
|
const request=pending.get(event.data.requestId);if(!request)return;
|
|
21
47
|
pending.delete(event.data.requestId);clearTimeout(request.timer);
|
|
22
48
|
if(event.data.error)request.reject(new Error(event.data.error));else request.resolve(event.data.result);
|
|
23
49
|
});
|
|
24
|
-
const host=Object.freeze({pluginId:${inlineJson(pluginId)},run(subcommand,args=[],options={}){return new Promise((resolve,reject)=>{
|
|
50
|
+
const host=Object.freeze({pluginId:${inlineJson(pluginId)},operations:Object.freeze(${inlineJson(operations)}),screenChanged(screen){if(ownerScreens.some(item=>item.id===screen))send('screen-changed',{screen});},run(subcommand,args=[],options={}){return new Promise((resolve,reject)=>{
|
|
25
51
|
const requestId=String(++sequence);
|
|
26
52
|
const declared=${inlineJson(commandTimeouts)}[subcommand];
|
|
27
53
|
const timeout=Number.isInteger(declared)&&declared>=1&&declared<=600000?declared:30000;
|
|
@@ -34,6 +60,7 @@ try{
|
|
|
34
60
|
const tag=${inlineJson(tag)};
|
|
35
61
|
if(!customElements.get(tag))throw new Error('The plugin did not register its declared interface');
|
|
36
62
|
const element=document.createElement(tag);element.host=host;document.body.append(element);
|
|
63
|
+
publishScreens(element);
|
|
37
64
|
if(typeof element.load==='function'&&!element.hasAttribute('data-loaded')){try{element.load();}catch(_){}}
|
|
38
65
|
if('view' in element||tag==='domains-console'||tag==='stripe-panel'){
|
|
39
66
|
host.run('view').then(v=>{element.view=v;}).catch(()=>{
|
|
@@ -828,7 +828,8 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
828
828
|
)}
|
|
829
829
|
</Empty>
|
|
830
830
|
) : (
|
|
831
|
-
|
|
831
|
+
// Plugin faces are wide surfaces with screens to flip through, never narrow columns.
|
|
832
|
+
<div className="card-grid" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 720px), 1fr))", gap: "22px" }}>
|
|
832
833
|
{shownInstalled.map((p) => {
|
|
833
834
|
return (
|
|
834
835
|
<div
|
|
@@ -836,7 +837,7 @@ export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
|
836
837
|
onContextMenu={(e) => handleContextMenu(p, e)}
|
|
837
838
|
onTouchStart={(e) => handleTouchStart(p, e)}
|
|
838
839
|
onTouchEnd={cancelLongPress}
|
|
839
|
-
style={{ display: "flex", flexDirection: "column", gap: "8px" }}
|
|
840
|
+
style={{ display: "flex", flexDirection: "column", gap: "8px", minWidth: 0 }}
|
|
840
841
|
>
|
|
841
842
|
<CardEngine plugin={p} />
|
|
842
843
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0 4px", fontSize: "11px", color: "var(--text-tertiary)", flexWrap: "wrap", gap: "6px" }}>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "total-recall-brain",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.41.1",
|
|
4
4
|
"description": "The Vibe Coding Brain & Autonomous Software Factory: memory, rules, secrets, and 33 visual plugins automatically managed across all your devices.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@ Total Recall connects to every major IDE, CLI harness, and local neural runtime:
|
|
|
23
23
|
- **Lovable**: Repository root `AGENTS.md` and `CLAUDE.md`.
|
|
24
24
|
- **DeepSeek Harness (`dsh`)**: `.dsh/memory/MEMORY.md` and `AGENTS.md`.
|
|
25
25
|
- **OpenClaw**: `SOUL.md`, `AGENTS.md`, `USER.md`, and `MEMORY.md`.
|
|
26
|
-
- **Hermes Agent**: `.hermes/memories
|
|
26
|
+
- **Hermes Agent**: `AGENTS.md` (loaded as a context file from the git root to cwd) and `.hermes/skills/`. `~/.hermes/memories/` is Hermes' own notes store and is never written.
|
|
27
27
|
- **Cline**: `.clinerules/total-recall.md` and `.cline/skills/`.
|
|
28
28
|
- **VS Code**: `.github/copilot-instructions.md` and `.vscode/copilot-instructions.md`.
|
|
29
29
|
|