total-recall-brain 3.45.0 → 3.46.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 +7 -2
- package/frontend/dist/assets/index-BRYE1jmv.css +1 -0
- package/frontend/dist/assets/index-D1goIPjC.js +5598 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/sw.js +5 -4
- package/frontend/src/App.tsx +183 -236
- package/frontend/src/api/auth-inventory.ts +62 -0
- package/frontend/src/api/memory.ts +13 -8
- package/frontend/src/api/plugin-credentials.ts +23 -0
- package/frontend/src/api/plugins.ts +37 -53
- package/frontend/src/api/workspace-drawers.ts +97 -0
- package/frontend/src/components/BrainSelector.tsx +9 -3
- package/frontend/src/components/plugins/PluginPanel.tsx +83 -42
- package/frontend/src/components/plugins/PluginSettings.tsx +10 -20
- package/frontend/src/components/plugins/PluginWorkspace.tsx +367 -0
- package/frontend/src/components/plugins/ScopedPluginCommands.tsx +142 -0
- package/frontend/src/components/plugins/plugin-frame.ts +44 -33
- package/frontend/src/components/plugins/scoped-plugin-operations.ts +28 -0
- package/frontend/src/components/workspace/DrawerChat.tsx +110 -0
- package/frontend/src/components/workspace/DrawerLayout.tsx +196 -0
- package/frontend/src/components/workspace/DrawerMarkup.tsx +49 -0
- package/frontend/src/components/workspace/PluginChunkLibrary.tsx +92 -0
- package/frontend/src/components/workspace/PluginHome.tsx +76 -0
- package/frontend/src/components/workspace/WorkspaceDrawers.tsx +96 -0
- package/frontend/src/components/workspace/WorkspacePage.tsx +95 -0
- package/frontend/src/components/workspace/usePluginDesign.ts +38 -0
- package/frontend/src/index.css +409 -136
- package/frontend/src/pages/HelpPage.tsx +15 -46
- package/frontend/src/pages/MemoryPage.tsx +19 -36
- package/frontend/src/pages/SettingsPage.tsx +1 -1
- package/metadata.plugin.schema.json +53 -227
- package/package.json +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +13 -11
- package/scaffold/.agent/skills/plugin/evals/README.md +14 -21
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +15 -18
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +19 -24
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +12 -20
- package/scaffold/.agent/skills/plugin/evals/cases/04-home-collects-credential.mjs +35 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +8 -28
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +85 -47
- package/scaffold/.agent/skills/plugin/evals/run.mjs +17 -18
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +4 -4
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +2 -2
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +4 -4
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +2 -2
- package/scaffold/.agent/skills/plugin/references/generative-home.md +38 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +5 -5
- package/scaffold/.agent/skills/plugin/references/surfaces.md +13 -4
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +75 -153
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/DESIGN.md +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +3 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +20 -15
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +13 -51
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/access-overview.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/agent-login.md +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/baseline-audit.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-repository-binding.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-store-maintenance.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-password.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-sessions.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/endpoint-probe.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/google-sign-in.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/mesh-machines.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/passkey-enrollment.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/registered-brains.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/scoped-access-keys.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/secret-inventory.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/signing-key-rotation.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/{ui/DESIGN.md → DESIGN.md} +32 -21
- package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +17 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +10 -1
- package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +4 -3
- package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +11 -27
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/control-ledger.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/local-inventory-triage.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/pending-control.md +39 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/run-evidence.md +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/schedule-vs-run-evidence.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/source-coverage.md +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/automations/ui/{card.js → home.js} +32 -27
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/DESIGN.md +53 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +40 -40
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/config.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/detect.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/gate.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/init.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/report.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/run.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/snapshot.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +55 -23
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/home.mjs +214 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/DESIGN.md +146 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +51 -34
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/create.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/history.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/library.md +49 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/list.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/promote.md +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/read.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/remove.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/rollback.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/show.md +53 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/status.md +40 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test-path.md +38 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test.md +46 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/update.md +32 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/home.js +533 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/DESIGN.md +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +13 -10
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/benchmark.md +49 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/coverage.md +53 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/models.md +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/pull.md +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/status.md +59 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/home.js +353 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/DESIGN.md +31 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/README.md +25 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/cli.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +17 -27
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +24 -6
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +33 -7
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +29 -5
- package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/unmerged.mjs +182 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/change-comparison.md +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/commit-identity.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/create-worktree.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/registered-worktrees.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/remove-worktree.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/repository-ledger.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/unmerged-work.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/upstream-divergence.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-conflict-triage.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-terminal.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/home.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/DESIGN.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +4 -5
- package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +37 -39
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/active-device-dispatch.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-probe.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-server.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/credential-readiness.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/elected-leader.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/enrollment-key.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/file-receipt.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-execution.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-presence.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/interactive-ssh.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/local-enrollment.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/mesh-policy.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/network-atlas.md +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-roster.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-selection-proof.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/owner-observations.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/peer-path.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/pinned-leader.md +24 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/reachability-evidence-ladder.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/recorded-login.md +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/remote-execution.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/secret-grants.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/{card.js → home.js} +93 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/DESIGN.md +86 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +43 -22
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/chat.mjs +80 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/adapter-docs-freshness.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/chat-handoff.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/fleet-maintenance.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/harness-inventory.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-access.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-usage-verification.md +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/independent-review.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/managed-execution.md +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/official-documentation.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/quota-recovery.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/review-routing-decision.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/route-work.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/routing-evidence.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/run-ledger.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/home.js +198 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/DESIGN.md +75 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +17 -14
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +10 -44
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +5 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +3 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +23 -18
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +8 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +6 -3
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +3 -5
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +21 -47
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +17 -6
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/auto-refresh.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/bootstrap-pages.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generate-wiki.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generator-settings.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/host-probe.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/ingest-preview.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-hygiene.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-topology.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/passage-search.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/propagation-plan.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/provider-credentials.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/refresh-report.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/task-evidence.md +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/upstream-record.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/watch-upstream.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/wiki-atlas.md +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +261 -513
- package/scaffold/.agent/skills/total-recall/plugins/security/DESIGN.md +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/README.md +4 -2
- package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -19
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +2 -3
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +19 -52
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/device-evidence.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/evidence-desk.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/guided-device-review.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/host-and-network.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/https-headers.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/mesh-coverage.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/private-security-tools.md +16 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/single-origin-header-check.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/source-review.md +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/ssh-permission-plan.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/home.js +283 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/DESIGN.md +93 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -1
- package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +5 -3
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/conformance.md +26 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/registry.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/repair.md +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/scaffold.md +33 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/status.md +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validate.md +34 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validation-diagnostics.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/workspace-registry-preflight.md +17 -0
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/home.js +348 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +2 -0
- package/src/cli/api.mjs +3 -3
- package/src/cli/brief.mjs +3 -0
- package/src/cli/config.mjs +16 -0
- package/src/cli/connect.mjs +2 -7
- package/src/cli/plugin/chunks.mjs +145 -0
- package/src/cli/plugin/create.mjs +24 -52
- package/src/cli/plugin/index.mjs +20 -9
- package/src/cli/setup.mjs +2 -9
- package/src/cli/start.mjs +1 -0
- package/src/cli/startup.mjs +18 -0
- package/src/core/app-deploy/design-tokens.mjs +14 -9
- package/src/core/app-deploy/ui-elements.mjs +1 -1
- package/src/core/brain-registry.mjs +5 -0
- package/src/core/brain-updates.mjs +5 -1
- package/src/core/daemon-control.mjs +5 -1
- package/src/core/dashboard-bundle.mjs +261 -0
- package/src/core/default-plugin-refresh.mjs +39 -0
- package/src/core/ensure-dependencies.mjs +19 -15
- package/src/core/memory-brain.mjs +30 -8
- package/src/core/plugin-activation.mjs +63 -20
- package/src/core/plugin-contracts/{card.mjs → manifest.mjs} +23 -57
- package/src/core/plugin-contracts/startup-check.mjs +49 -0
- package/src/core/plugin-contracts/ui.mjs +9 -3
- package/src/core/plugin-distribution.mjs +6 -6
- package/src/core/plugin-lifecycle.mjs +58 -0
- package/src/core/plugin-loader.mjs +9 -5
- package/src/core/plugin-runner.mjs +21 -10
- package/src/core/plugin-startup-checks.mjs +44 -0
- package/src/core/plugin-state.mjs +19 -5
- package/src/core/plugin-store.mjs +30 -11
- package/src/core/project-id.mjs +15 -5
- package/src/core/required-plugins.mjs +31 -6
- package/src/core/schema.mjs +2 -0
- package/src/core/ssss-operation-service.mjs +1 -0
- package/src/core/startup-health.mjs +28 -3
- package/src/core/testing/plugin-fixture.mjs +22 -21
- package/src/core/throttled-fetch.mjs +25 -18
- package/src/core/ui-chunk-choice.mjs +77 -0
- package/src/core/ui-chunk.registry.json +23 -0
- package/src/core/ui-chunks.mjs +434 -0
- package/src/core/vault-cache.mjs +58 -59
- package/src/core/workspace-compositions.mjs +151 -0
- package/src/core/workspace-drawers.mjs +79 -0
- package/src/server/api.mjs +4 -27
- package/src/server/index.mjs +28 -21
- package/src/server/memory-events.mjs +31 -0
- package/src/server/plugin-brain-targets.mjs +89 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-inventory.mjs +3 -2
- package/src/server/route-manifest.json +52 -4
- package/src/server/routes/_shared.mjs +7 -14
- package/src/server/routes/brains.mjs +29 -44
- package/src/server/routes/dashboard.mjs +26 -0
- package/src/server/routes/plugins.mjs +154 -45
- package/src/server/routes/ui-chunks.mjs +192 -0
- package/frontend/dist/assets/index-DACh1dhx.js +0 -5613
- package/frontend/dist/assets/index-DtwsDyFM.css +0 -1
- package/frontend/src/components/plugins/card/CardEngine.tsx +0 -108
- package/frontend/src/components/plugins/card/CardScenes.tsx +0 -103
- package/frontend/src/components/plugins/card/ExecutionPicker.tsx +0 -64
- package/frontend/src/components/plugins/card/args.ts +0 -28
- package/frontend/src/components/plugins/card/card.css +0 -32
- package/frontend/src/components/plugins/card/cli-line.ts +0 -13
- package/frontend/src/components/plugins/card/contract.ts +0 -91
- package/frontend/src/components/plugins/card/format.ts +0 -49
- package/frontend/src/components/plugins/card/runner-context.ts +0 -11
- package/frontend/src/components/plugins/card/runner.tsx +0 -43
- package/frontend/src/components/plugins/card/theme.ts +0 -35
- package/frontend/src/components/plugins/card/use-source.ts +0 -22
- package/frontend/src/components/plugins/card/widgets.tsx +0 -94
- package/frontend/src/components/plugins/workspace/WorkspaceHost.tsx +0 -23
- package/frontend/src/pages/PluginsPage.tsx +0 -1719
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +0 -47
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +0 -46
- package/scaffold/.agent/skills/plugin/references/card.md +0 -90
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +0 -60
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +0 -67
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +0 -1179
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +0 -588
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +0 -575
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +0 -482
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/DESIGN.md +0 -100
- package/scaffold/.agent/skills/total-recall/plugins/git/ui/card.js +0 -637
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +0 -217
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +0 -1276
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +0 -45
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +0 -1401
- package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +0 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +0 -41
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +0 -396
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +0 -98
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/DESIGN.md +0 -15
- package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/card.js +0 -182
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import type { PluginInfo } from '../../api/plugins'
|
|
3
3
|
import { apiFetch, API_BASE } from '../../api/_base'
|
|
4
|
+
import { readSavedPluginCredentials, savePluginCredential } from '../../api/plugin-credentials'
|
|
4
5
|
|
|
5
6
|
interface Setting { name: string; label: string; type: 'string' | 'number' | 'boolean'; options?: Array<string | number>; required?: boolean; default?: string | number | boolean }
|
|
6
7
|
|
|
7
8
|
/** Credential values are write-only. Existing values never enter browser state. */
|
|
8
9
|
export function PluginSettings({ plugin }: { plugin: PluginInfo }) {
|
|
9
|
-
const [stored, setStored] = useState<Set<string
|
|
10
|
+
const [stored, setStored] = useState<Set<string> | null>(null)
|
|
10
11
|
const [values, setValues] = useState<Record<string, string>>({})
|
|
11
12
|
const [saving, setSaving] = useState('')
|
|
12
13
|
const [notice, setNotice] = useState('')
|
|
@@ -16,13 +17,10 @@ export function PluginSettings({ plugin }: { plugin: PluginInfo }) {
|
|
|
16
17
|
useEffect(() => {
|
|
17
18
|
let disposed = false
|
|
18
19
|
setValues({}); setNotice(''); setError('')
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
setStored(new Set(entries.map((entry: any) => entry.key)))
|
|
24
|
-
}
|
|
25
|
-
}).catch((err: any) => { if (!disposed) setError(err.message) })
|
|
20
|
+
setStored(null)
|
|
21
|
+
readSavedPluginCredentials().then(keys => {
|
|
22
|
+
if (!disposed) setStored(keys)
|
|
23
|
+
}).catch((err: Error) => { if (!disposed) setError(err.message) })
|
|
26
24
|
apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(plugin.id)}/configuration`).then(async response => {
|
|
27
25
|
if (!response.ok) throw new Error('Could not load plugin settings')
|
|
28
26
|
const data = await response.json()
|
|
@@ -36,17 +34,9 @@ export function PluginSettings({ plugin }: { plugin: PluginInfo }) {
|
|
|
36
34
|
async function save(key: string) {
|
|
37
35
|
setSaving(key); setError(''); setNotice('')
|
|
38
36
|
try {
|
|
39
|
-
|
|
40
|
-
method: 'POST',
|
|
41
|
-
headers: { 'Content-Type': 'application/json' },
|
|
42
|
-
body: JSON.stringify({ key, value: values[key], tags: [`plugin:${plugin.id}`] })
|
|
43
|
-
})
|
|
44
|
-
if (!res.ok) {
|
|
45
|
-
const errData = await res.json().catch(() => ({}))
|
|
46
|
-
throw new Error(errData.error || 'Failed to save secret')
|
|
47
|
-
}
|
|
37
|
+
await savePluginCredential(plugin, key, values[key])
|
|
48
38
|
setValues(previous => ({ ...previous, [key]: '' }))
|
|
49
|
-
setStored(
|
|
39
|
+
setStored(await readSavedPluginCredentials().catch(() => null))
|
|
50
40
|
setNotice('Credential saved. Open the plugin to verify the connection.')
|
|
51
41
|
} catch (err: any) { setError(err.message) }
|
|
52
42
|
finally { setSaving('') }
|
|
@@ -83,9 +73,9 @@ export function PluginSettings({ plugin }: { plugin: PluginInfo }) {
|
|
|
83
73
|
{(plugin.secrets || []).map((secret, index) => {
|
|
84
74
|
const label = `Credential ${index + 1}${secret.required ? ' (required)' : ' (optional)'}`
|
|
85
75
|
return <form key={secret.key} onSubmit={event => { event.preventDefault(); void save(secret.key) }} style={{ marginBottom: 16 }}>
|
|
86
|
-
<label style={{ display: 'block', marginBottom: 6 }}>{label} · {stored
|
|
76
|
+
<label style={{ display: 'block', marginBottom: 6 }}>{label} · {stored?.has(secret.key) ? 'Saved' : stored ? 'Not saved' : 'Status unavailable'}
|
|
87
77
|
<input className="input" type="password" autoComplete="new-password" value={values[secret.key] || ''}
|
|
88
|
-
placeholder={stored
|
|
78
|
+
placeholder={stored?.has(secret.key) ? 'Paste a replacement' : 'Paste credential'}
|
|
89
79
|
onChange={event => setValues(previous => ({ ...previous, [secret.key]: event.target.value }))} />
|
|
90
80
|
</label>
|
|
91
81
|
<button className="btn btn-sm btn-primary" disabled={!values[secret.key]?.trim() || !!saving}>{saving === secret.key ? 'Saving…' : 'Save credential'}</button>
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
2
|
+
import { useLocation, useSearchParams } from 'react-router-dom'
|
|
3
|
+
import {
|
|
4
|
+
fetchBundledPlugins, fetchPeerPlugins, fetchPlugins, installPlugin, removePlugin,
|
|
5
|
+
setPluginActivation, setPluginShared, type BrainPluginActivation, type BundledPlugin,
|
|
6
|
+
type PeersResponse, type PluginInfo
|
|
7
|
+
} from '../../api/plugins'
|
|
8
|
+
import { PluginPanel, pluginElements } from './PluginPanel'
|
|
9
|
+
import { PluginSettings } from './PluginSettings'
|
|
10
|
+
import { PluginChunkLibrary } from '../workspace/PluginChunkLibrary'
|
|
11
|
+
import { PluginHome } from '../workspace/PluginHome'
|
|
12
|
+
import { readSavedPluginCredentials } from '../../api/plugin-credentials'
|
|
13
|
+
import { ScopedPluginCommands } from './ScopedPluginCommands'
|
|
14
|
+
|
|
15
|
+
type Notice = { text: string; error?: boolean }
|
|
16
|
+
type OpenAttempt = { pluginId: string; brainId: string; error?: string }
|
|
17
|
+
|
|
18
|
+
interface WorkspaceProps {
|
|
19
|
+
activeBrainId: string
|
|
20
|
+
renderShell: (folder: ReactNode, page: ReactNode, plugins: PluginInfo[] | null) => ReactNode
|
|
21
|
+
onOpenPlugin: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function pluginOrigin(plugin: PluginInfo) {
|
|
25
|
+
if (plugin.core) return 'Core'
|
|
26
|
+
if (plugin.scope === 'scaffold' || plugin.source.kind === 'bundled') return 'Bundled'
|
|
27
|
+
if (plugin.source.kind === 'peer' || plugin.source.kind === 'public') return 'Shared'
|
|
28
|
+
return 'User'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function PluginWorkspace({ activeBrainId, renderShell, onOpenPlugin }: WorkspaceProps) {
|
|
32
|
+
const [params, setParams] = useSearchParams()
|
|
33
|
+
const { pathname } = useLocation()
|
|
34
|
+
const [expanded, setExpanded] = useState<boolean>()
|
|
35
|
+
const [inventory, setInventory] = useState<{ selection: string; plugins: PluginInfo[] }>()
|
|
36
|
+
const [failure, setFailure] = useState<{ selection: string; message: string }>()
|
|
37
|
+
const [openAttempt, setOpenAttempt] = useState<OpenAttempt>()
|
|
38
|
+
const [browse, setBrowse] = useState(false)
|
|
39
|
+
const request = useRef(0)
|
|
40
|
+
const currentSelection = useRef(activeBrainId)
|
|
41
|
+
const plugins = inventory?.selection === activeBrainId ? inventory.plugins : null
|
|
42
|
+
const loadError = failure?.selection === activeBrainId ? failure.message : undefined
|
|
43
|
+
const brainIds = activeBrainId.split(',').filter(Boolean)
|
|
44
|
+
const brainId = brainIds.length === 1 ? brainIds[0] : undefined
|
|
45
|
+
const id = pathname === '/plugins' || (pathname === '/memory' && params.get('page')) ? null : params.get('plugin')
|
|
46
|
+
const open = expanded ?? Boolean(id || params.has('plugins'))
|
|
47
|
+
const selected = id ? plugins?.find(plugin => plugin.id === id || plugin.id === `tr-plugin-${id}` || plugin.name === id) : undefined
|
|
48
|
+
|
|
49
|
+
const refresh = useCallback(async () => {
|
|
50
|
+
if (currentSelection.current !== activeBrainId) return
|
|
51
|
+
const current = ++request.current
|
|
52
|
+
try {
|
|
53
|
+
const installed = await fetchPlugins(activeBrainId)
|
|
54
|
+
if (request.current !== current) return
|
|
55
|
+
setInventory({ selection: activeBrainId, plugins: [...installed].sort((a, b) => a.name.localeCompare(b.name)) })
|
|
56
|
+
setFailure(undefined)
|
|
57
|
+
setOpenAttempt(attempt => attempt?.error ? undefined : attempt)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (request.current !== current) return
|
|
60
|
+
setFailure({ selection: activeBrainId, message: error instanceof Error ? error.message : 'Could not load plugins.' })
|
|
61
|
+
}
|
|
62
|
+
}, [activeBrainId])
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
currentSelection.current = activeBrainId
|
|
66
|
+
void refresh()
|
|
67
|
+
return () => { request.current++ }
|
|
68
|
+
}, [activeBrainId, refresh])
|
|
69
|
+
|
|
70
|
+
function select(pluginId: string | null) {
|
|
71
|
+
const next = new URLSearchParams(params)
|
|
72
|
+
if (pluginId) {
|
|
73
|
+
next.set('plugin', pluginId)
|
|
74
|
+
next.delete('page')
|
|
75
|
+
} else next.delete('plugin')
|
|
76
|
+
setParams(next, { replace: !pluginId })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function openArmed(plugin: PluginInfo) {
|
|
80
|
+
if (!brainId?.startsWith('project:') || plugin.activation?.mode !== 'on-use') return
|
|
81
|
+
if (openAttempt?.brainId === brainId && openAttempt.pluginId === plugin.id && !openAttempt.error) return
|
|
82
|
+
const attempt = { pluginId: plugin.id, brainId }
|
|
83
|
+
setOpenAttempt(attempt)
|
|
84
|
+
const result = await setPluginActivation(plugin.id, 'use', [brainId])
|
|
85
|
+
const error = result.success ? undefined : result.results?.[0]?.error || result.error || 'Could not open this plugin.'
|
|
86
|
+
await refresh()
|
|
87
|
+
setOpenAttempt(current => current === attempt ? (error ? { ...attempt, error } : undefined) : current)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const folder = <>
|
|
91
|
+
<button type="button" className="nav-link plugin-folder-toggle" id="nav-plugins"
|
|
92
|
+
aria-expanded={open} aria-controls="plugin-folder" onClick={() => setExpanded(!open)}>
|
|
93
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden="true">
|
|
94
|
+
<path d="M3 7a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />
|
|
95
|
+
</svg>
|
|
96
|
+
Plugins <span className="plugin-folder-chevron" aria-hidden="true">{open ? '▾' : '›'}</span>
|
|
97
|
+
</button>
|
|
98
|
+
<div id="plugin-folder" hidden={!open}>
|
|
99
|
+
{loadError ? <div className="plugin-folder-message" role="alert">
|
|
100
|
+
{loadError} <button className="btn btn-ghost btn-sm" onClick={() => void refresh()}>Retry</button>
|
|
101
|
+
</div> : plugins === null ? <p className="plugin-folder-message" role="status">Loading plugins…</p> :
|
|
102
|
+
<ul className="plugin-folder-list" aria-label="Installed plugins">
|
|
103
|
+
{plugins.map(plugin => <li key={plugin.id}>
|
|
104
|
+
<button type="button" className={`nav-link plugin-folder-item ${selected?.id === plugin.id ? 'active' : ''}`}
|
|
105
|
+
aria-current={selected?.id === plugin.id || undefined}
|
|
106
|
+
onClick={() => { select(plugin.id); onOpenPlugin(); void openArmed(plugin) }}>
|
|
107
|
+
<span>{plugin.name}</span><small className="plugin-origin" aria-hidden="true">{pluginOrigin(plugin)}</small>
|
|
108
|
+
</button>
|
|
109
|
+
</li>)}
|
|
110
|
+
{!plugins.length && <li className="plugin-folder-message">No plugins installed</li>}
|
|
111
|
+
</ul>}
|
|
112
|
+
<button type="button" className="nav-link plugin-folder-item" onClick={() => { setBrowse(true); onOpenPlugin() }}>
|
|
113
|
+
Browse and install
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</>
|
|
117
|
+
let page: ReactNode = null
|
|
118
|
+
if (selected) page = <PluginDetail key={`${activeBrainId}:${selected.id}`} plugin={selected} plugins={plugins} stale={!!loadError} brainId={brainId}
|
|
119
|
+
opening={openAttempt?.pluginId === selected.id && openAttempt.brainId === brainId ? openAttempt : undefined}
|
|
120
|
+
onUse={() => void openArmed(selected)} onClose={() => select(null)} onChange={refresh} />
|
|
121
|
+
else if (id && (loadError || plugins)) page = <section className="plugin-workspace-page" aria-label="Plugin unavailable">
|
|
122
|
+
<p role="alert">{loadError || `No installed plugin matches ${id}.`}</p>
|
|
123
|
+
<button type="button" className="btn btn-ghost" onClick={() => select(null)}>Close</button>
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
return <>
|
|
127
|
+
{renderShell(folder, page, loadError ? null : plugins)}
|
|
128
|
+
{browse && <PluginInstaller onClose={() => setBrowse(false)} onChange={refresh} />}
|
|
129
|
+
</>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function PluginDialog({ title, children, onClose }: { title: string; children: ReactNode; onClose: () => void }) {
|
|
133
|
+
return <div className="plugin-drawer-backdrop" onClick={onClose}>
|
|
134
|
+
<section className="plugin-drawer" role="dialog" aria-modal="true" aria-label={title}
|
|
135
|
+
onClick={event => event.stopPropagation()} onKeyDown={event => {
|
|
136
|
+
if (event.key === 'Escape') { event.stopPropagation(); onClose() }
|
|
137
|
+
}}>
|
|
138
|
+
<header className="plugin-drawer-header">
|
|
139
|
+
<h2>{title}</h2>
|
|
140
|
+
<button type="button" className="btn btn-ghost btn-sm" autoFocus onClick={onClose}>Close</button>
|
|
141
|
+
</header>
|
|
142
|
+
{children}
|
|
143
|
+
</section>
|
|
144
|
+
</div>
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function PluginDetail({ plugin, plugins, brainId, stale, opening, onUse, onChange, onClose }: {
|
|
148
|
+
plugin: PluginInfo; plugins: PluginInfo[] | null; brainId?: string; stale: boolean; opening?: OpenAttempt;
|
|
149
|
+
onUse: () => void; onChange: () => Promise<void>; onClose: () => void
|
|
150
|
+
}) {
|
|
151
|
+
const panels = pluginElements(plugin).filter(element => element.kind !== 'settings')
|
|
152
|
+
const activation = plugin.activation
|
|
153
|
+
const error = !plugin.valid || activation?.state === 'error'
|
|
154
|
+
const enabled = activation?.enabled === true && !error
|
|
155
|
+
const rows = plugin.activationByBrain ?? (brainId && activation && activation.state !== 'mixed'
|
|
156
|
+
? [{ ...activation, state: activation.state, brainId, name: brainId === 'global' ? 'Global Brain' : brainId.slice('project:'.length) }]
|
|
157
|
+
: [])
|
|
158
|
+
const armed = rows.find(row => row.brainId === brainId && row.mode === 'on-use')
|
|
159
|
+
const canManage = plugin.servingAvailable !== false
|
|
160
|
+
const [tab, setTab] = useState<'home' | 'interface' | 'settings' | 'commands' | 'blocks'>('home')
|
|
161
|
+
const [initialCommand, setInitialCommand] = useState<string>()
|
|
162
|
+
const [panelId, setPanelId] = useState(panels[0]?.id)
|
|
163
|
+
const [changing, setChanging] = useState(false)
|
|
164
|
+
const busy = changing || Boolean(opening && !opening.error)
|
|
165
|
+
const [notice, setNotice] = useState<Notice>()
|
|
166
|
+
const [cleanupRetries, setCleanupRetries] = useState<string[]>([])
|
|
167
|
+
const [uninstall, setUninstall] = useState(false)
|
|
168
|
+
const [savedCredentials, setSavedCredentials] = useState<Set<string> | null>(null)
|
|
169
|
+
const panel = panels.find(item => item.id === panelId) ?? panels[0]
|
|
170
|
+
function openCommand(name: string) {
|
|
171
|
+
setInitialCommand(name)
|
|
172
|
+
setTab('commands')
|
|
173
|
+
}
|
|
174
|
+
const declaredCredentials = plugin.secrets || []
|
|
175
|
+
const credentialCount = declaredCredentials.length
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (!credentialCount) return
|
|
178
|
+
let disposed = false
|
|
179
|
+
void readSavedPluginCredentials().then(keys => {
|
|
180
|
+
if (!disposed) setSavedCredentials(keys)
|
|
181
|
+
}).catch(() => { if (!disposed) setSavedCredentials(null) })
|
|
182
|
+
return () => { disposed = true }
|
|
183
|
+
}, [plugin.id, credentialCount, tab])
|
|
184
|
+
const credentialLabel = !credentialCount ? 'Dashboard session'
|
|
185
|
+
: !savedCredentials ? 'Credential status unavailable'
|
|
186
|
+
: declaredCredentials.some(secret => secret.required && !savedCredentials.has(secret.key)) ? 'Credential required'
|
|
187
|
+
: declaredCredentials.some(secret => savedCredentials.has(secret.key)) ? 'Saved in Auth' : 'Optional credential'
|
|
188
|
+
|
|
189
|
+
async function change(action: BrainPluginActivation | 'share' | 'uninstall', value?: boolean | 'on-use') {
|
|
190
|
+
setChanging(true)
|
|
191
|
+
setNotice(undefined)
|
|
192
|
+
try {
|
|
193
|
+
if (typeof action === 'object') {
|
|
194
|
+
const result = await setPluginActivation(plugin.id, value ?? !action.enabled, [action.brainId])
|
|
195
|
+
const target = result.results?.find(item => item.brainId === action.brainId)
|
|
196
|
+
if (target?.cleanup?.ok === false) {
|
|
197
|
+
setCleanupRetries(current => current.includes(action.brainId) ? current : [...current, action.brainId])
|
|
198
|
+
} else if (result.success || target?.cleanup?.ok === true) {
|
|
199
|
+
setCleanupRetries(current => current.filter(id => id !== action.brainId))
|
|
200
|
+
}
|
|
201
|
+
if (!result.success) {
|
|
202
|
+
setNotice({ text: target?.error ? `${target.name}. ${target.error}` : result.error || 'Could not update activation.', error: true })
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
const result = action === 'share' ? await setPluginShared(plugin.id, !plugin.shared) : await removePlugin(plugin.id, plugin.scope === 'global')
|
|
206
|
+
if (!result.success) setNotice({ text: result.error || 'Could not update the plugin.', error: true })
|
|
207
|
+
if (action === 'uninstall' && result.success) onClose()
|
|
208
|
+
}
|
|
209
|
+
// The record can persist even when recompilation reports a failure.
|
|
210
|
+
await onChange()
|
|
211
|
+
} finally { setChanging(false) }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return <section className="plugin-workspace-page" aria-label={plugin.name}>
|
|
215
|
+
<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>
|
|
222
|
+
<span className={`badge ${error ? 'badge-error' : enabled ? 'badge-success' : ''}`}>
|
|
223
|
+
{stale ? 'Unknown' : activation?.state === 'mixed' ? 'Mixed' : error ? 'Error' : activation?.mode === 'on-use' ? 'On use' : enabled ? 'Active' : activation ? 'Off' : 'Unknown'}
|
|
224
|
+
</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>}
|
|
257
|
+
{stale && <p role="alert">The installed state could not be refreshed.</p>}
|
|
258
|
+
{error && <p role="alert">{plugin.errors[0] || 'The activation policy could not be read.'}</p>}
|
|
259
|
+
{opening?.error && !enabled && !notice && <p role="alert">{opening.error}</p>}
|
|
260
|
+
{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>
|
|
274
|
+
{tab === 'home' && (enabled && !stale
|
|
275
|
+
? <PluginHome plugin={plugin} plugins={plugins} brainId={brainId} onCommand={openCommand} />
|
|
276
|
+
: armed && !stale ? <div className="plugin-drawer-actions">
|
|
277
|
+
<span className="plugin-muted">This plugin activates when you open it.</span>
|
|
278
|
+
<button type="button" className="btn btn-primary" disabled={busy}
|
|
279
|
+
onClick={onUse}>{busy ? 'Opening…' : 'Open plugin'}</button>
|
|
280
|
+
</div> : <p className="plugin-muted">An active plugin is needed to open its Home tools.</p>)}
|
|
281
|
+
{tab === 'interface' && enabled && panel && <>
|
|
282
|
+
{panels.length > 1 && <label>Panel
|
|
283
|
+
<select className="select" value={panel.id} onChange={event => setPanelId(event.target.value)}>
|
|
284
|
+
{panels.map(item => <option key={item.id} value={item.id}>{item.description || item.id}</option>)}
|
|
285
|
+
</select>
|
|
286
|
+
</label>}
|
|
287
|
+
<PluginPanel key={panel.id} plugin={plugin} elementId={panel.id} brainId={brainId} readOnly={stale} />
|
|
288
|
+
</>}
|
|
289
|
+
{tab === 'settings' && canManage && <>
|
|
290
|
+
{!panel && <p className="plugin-muted">This package has no installed workspace panel.</p>}
|
|
291
|
+
{plugin.valid && <PluginSettings plugin={plugin} />}
|
|
292
|
+
<details>
|
|
293
|
+
<summary>Manage installation</summary>
|
|
294
|
+
<div className="plugin-drawer-actions">
|
|
295
|
+
<button className="btn btn-ghost" disabled={busy || stale || !plugin.valid} onClick={() => void change('share')}>
|
|
296
|
+
{plugin.shared ? 'Stop sharing' : 'Share on mesh'}
|
|
297
|
+
</button>
|
|
298
|
+
{!uninstall ? <button className="btn btn-ghost" disabled={busy || stale || activation?.required}
|
|
299
|
+
onClick={() => setUninstall(true)}>Uninstall…</button>
|
|
300
|
+
: <button className="btn btn-ghost" disabled={busy || stale} onClick={() => void change('uninstall')}>
|
|
301
|
+
Confirm uninstall
|
|
302
|
+
</button>}
|
|
303
|
+
</div>
|
|
304
|
+
</details>
|
|
305
|
+
</>}
|
|
306
|
+
{tab === 'commands' && plugin.cli && <ScopedPluginCommands key={plugin.sha256} plugin={plugin} brainId={brainId}
|
|
307
|
+
enabled={enabled && !stale} initialCommand={initialCommand} />}
|
|
308
|
+
{tab === 'blocks' && enabled && !stale && <PluginChunkLibrary key={`${plugin.id}:${brainId}`}
|
|
309
|
+
plugin={plugin} brainId={brainId} />}
|
|
310
|
+
</>}
|
|
311
|
+
</section>
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function PluginInstaller({ onChange, onClose }: { onChange: () => Promise<void>; onClose: () => void }) {
|
|
315
|
+
const [source, setSource] = useState('')
|
|
316
|
+
const [bundled, setBundled] = useState<BundledPlugin[]>([])
|
|
317
|
+
const [peers, setPeers] = useState<PeersResponse | null>()
|
|
318
|
+
const [busy, setBusy] = useState(false)
|
|
319
|
+
const [notice, setNotice] = useState<Notice>()
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
let disposed = false
|
|
322
|
+
void fetchBundledPlugins().then(result => { if (!disposed) setBundled(result) })
|
|
323
|
+
return () => { disposed = true }
|
|
324
|
+
}, [])
|
|
325
|
+
|
|
326
|
+
async function install() {
|
|
327
|
+
setBusy(true)
|
|
328
|
+
setNotice(undefined)
|
|
329
|
+
try {
|
|
330
|
+
const result = await installPlugin({ source: source.trim(), global: true })
|
|
331
|
+
setNotice({ text: result.message || result.error || (result.success ? 'Plugin installed.' : 'Install failed.'), error: !result.success })
|
|
332
|
+
if (result.success) {
|
|
333
|
+
setSource('')
|
|
334
|
+
setBundled(await fetchBundledPlugins())
|
|
335
|
+
}
|
|
336
|
+
await onChange()
|
|
337
|
+
} finally { setBusy(false) }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return <PluginDialog title="Browse and install plugins" onClose={onClose}>
|
|
341
|
+
<p className="plugin-muted">Installs into the shared plugin folder on this host.</p>
|
|
342
|
+
<form className="plugin-drawer-form" onSubmit={event => { event.preventDefault(); void install() }}>
|
|
343
|
+
<label>Plugin source<input className="input" value={source} onChange={event => setSource(event.target.value)}
|
|
344
|
+
placeholder="Plugin ID, folder, Git URL, or peer link" required /></label>
|
|
345
|
+
<button type="submit" className="btn btn-primary" disabled={busy || !source.trim()}>Install</button>
|
|
346
|
+
</form>
|
|
347
|
+
{notice && <p role={notice.error ? 'alert' : 'status'}>{notice.text}</p>}
|
|
348
|
+
<details><summary>Bundled plugins</summary>
|
|
349
|
+
<ul className="plugin-folder-list">{bundled.map(plugin => <li key={plugin.id}>
|
|
350
|
+
<button className="btn btn-ghost" disabled={busy || plugin.installed} onClick={() => setSource(plugin.id)}>
|
|
351
|
+
{plugin.name}{plugin.installed ? ' (installed)' : ''}
|
|
352
|
+
</button>
|
|
353
|
+
</li>)}</ul>
|
|
354
|
+
</details>
|
|
355
|
+
<details><summary>Mesh peers</summary>
|
|
356
|
+
<button className="btn btn-ghost" onClick={async () => setPeers(await fetchPeerPlugins())}>Load shared plugins</button>
|
|
357
|
+
{peers === null && <p role="status">Could not load mesh peers.</p>}
|
|
358
|
+
{peers?.peers.map(peer => <div key={peer.hostname}>
|
|
359
|
+
<p>{peer.hostname} · {peer.status === 'ok' ? 'online' : peer.error || peer.status}</p>
|
|
360
|
+
<ul className="plugin-folder-list">{peer.plugins.map(plugin => <li key={plugin.id}>
|
|
361
|
+
<button className="btn btn-ghost" disabled={busy || plugin.same_as_installed}
|
|
362
|
+
onClick={() => setSource(`peer:${peer.hostname}/${plugin.id}`)}>{plugin.name}</button>
|
|
363
|
+
</li>)}</ul>
|
|
364
|
+
</div>)}
|
|
365
|
+
</details>
|
|
366
|
+
</PluginDialog>
|
|
367
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { runPluginCommand, type PluginInfo } from '../../api/plugins'
|
|
3
|
+
import { scopedPluginOperations, type Argument, type Command } from './scoped-plugin-operations'
|
|
4
|
+
|
|
5
|
+
type InputValues = Record<string, string | boolean>
|
|
6
|
+
type Run = { command: string; args: string[] }
|
|
7
|
+
|
|
8
|
+
function browserArguments(command: Command) {
|
|
9
|
+
return (command.args || []).filter(arg => !command.terminal_only_args?.includes(arg.flag || ''))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildArguments(command: Command, values: InputValues, extra: string): string[] {
|
|
13
|
+
const args: string[] = []
|
|
14
|
+
let omittedPosition: Argument | undefined
|
|
15
|
+
for (const arg of browserArguments(command)) {
|
|
16
|
+
const input = values[arg.name]
|
|
17
|
+
const value = arg.type === 'boolean' ? (input === true ? 'true' : '') : typeof input === 'string' ? input : ''
|
|
18
|
+
if (!value.trim()) {
|
|
19
|
+
if (arg.required) throw new Error(`Enter ${arg.label}.`)
|
|
20
|
+
if (!arg.flag) omittedPosition ??= arg
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
if (arg.flag) args.push(arg.flag)
|
|
24
|
+
else if (omittedPosition) throw new Error(`Enter ${omittedPosition.label} before ${arg.label}.`)
|
|
25
|
+
if (arg.type !== 'boolean' || !arg.flag) args.push(value)
|
|
26
|
+
}
|
|
27
|
+
if (extra.trim()) {
|
|
28
|
+
let parsed: unknown
|
|
29
|
+
try { parsed = JSON.parse(extra) }
|
|
30
|
+
catch { throw new Error('Arguments must be a JSON array of strings.') }
|
|
31
|
+
if (!Array.isArray(parsed) || parsed.some(value => typeof value !== 'string')) {
|
|
32
|
+
throw new Error('Arguments must be a JSON array of strings.')
|
|
33
|
+
}
|
|
34
|
+
args.push(...parsed)
|
|
35
|
+
}
|
|
36
|
+
if (args.some(arg => arg.includes('\0'))) throw new Error('An argument contains an invalid character.')
|
|
37
|
+
if (args.some(arg => command.terminal_only_args?.includes(arg.split('=', 1)[0]))) {
|
|
38
|
+
throw new Error('Use the owning CLI for terminal-only arguments.')
|
|
39
|
+
}
|
|
40
|
+
return args
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function ScopedPluginCommands({ plugin, brainId, enabled = true, operationIds, initialCommand, label }: {
|
|
44
|
+
plugin: PluginInfo
|
|
45
|
+
brainId: string
|
|
46
|
+
enabled?: boolean
|
|
47
|
+
operationIds?: readonly string[]
|
|
48
|
+
initialCommand?: string
|
|
49
|
+
label?: string
|
|
50
|
+
}) {
|
|
51
|
+
const commands = scopedPluginOperations(plugin, operationIds)
|
|
52
|
+
const [selected, setSelected] = useState(initialCommand)
|
|
53
|
+
const command = commands.find(item => item.name === selected) ?? commands[0]
|
|
54
|
+
const [values, setValues] = useState<InputValues>({})
|
|
55
|
+
const [extra, setExtra] = useState('')
|
|
56
|
+
const [pending, setPending] = useState<Run>()
|
|
57
|
+
const [output, setOutput] = useState<{ text: string; error: boolean }>()
|
|
58
|
+
const [busy, setBusy] = useState(false)
|
|
59
|
+
const running = useRef(false)
|
|
60
|
+
const canRun = enabled && plugin.valid && plugin.activation?.enabled === true &&
|
|
61
|
+
plugin.activation.state !== 'error' && Boolean(brainId) && !brainId.includes(',')
|
|
62
|
+
if (!command) return null
|
|
63
|
+
const fields = browserArguments(command)
|
|
64
|
+
|
|
65
|
+
function clearResult() {
|
|
66
|
+
setPending(undefined)
|
|
67
|
+
setOutput(undefined)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function changeValue(name: string, value: string | boolean) {
|
|
71
|
+
setValues(current => ({ ...current, [name]: value }))
|
|
72
|
+
clearResult()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function run(request: Run) {
|
|
76
|
+
if (!canRun || running.current || !commands.some(item => item.name === request.command)) return
|
|
77
|
+
running.current = true
|
|
78
|
+
setBusy(true)
|
|
79
|
+
clearResult()
|
|
80
|
+
try {
|
|
81
|
+
const result = await runPluginCommand(plugin.id, request.command, request.args, { brainId })
|
|
82
|
+
setOutput({
|
|
83
|
+
text: result.output || result.error || (result.success ? 'Command completed.' : 'Command failed.'),
|
|
84
|
+
error: !result.success,
|
|
85
|
+
})
|
|
86
|
+
} catch (error) {
|
|
87
|
+
setOutput({ text: error instanceof Error ? error.message : 'Command failed.', error: true })
|
|
88
|
+
} finally {
|
|
89
|
+
running.current = false
|
|
90
|
+
setBusy(false)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const argumentInput = <label>Arguments (JSON array)
|
|
95
|
+
<textarea className="input" rows={2} value={extra} placeholder='["--name", "Value with spaces"]'
|
|
96
|
+
onChange={event => { setExtra(event.target.value); clearResult() }} />
|
|
97
|
+
</label>
|
|
98
|
+
|
|
99
|
+
return <form className="plugin-drawer-form" aria-label={label || `${plugin.name} commands`}
|
|
100
|
+
onSubmit={event => {
|
|
101
|
+
event.preventDefault()
|
|
102
|
+
if (!canRun || running.current || pending) return
|
|
103
|
+
try {
|
|
104
|
+
const request = { command: command.name, args: buildArguments(command, values, extra) }
|
|
105
|
+
if (command.effect === 'read') void run(request)
|
|
106
|
+
else { setOutput(undefined); setPending(request) }
|
|
107
|
+
} catch (error) {
|
|
108
|
+
setOutput({ text: error instanceof Error ? error.message : 'Invalid arguments.', error: true })
|
|
109
|
+
}
|
|
110
|
+
}}>
|
|
111
|
+
<fieldset className="plugin-drawer-form" disabled={!canRun || busy} style={{ border: 0, margin: 0, padding: 0 }}>
|
|
112
|
+
<label>Command
|
|
113
|
+
<select className="select" value={command.name} onChange={event => {
|
|
114
|
+
setSelected(event.target.value); setValues({}); setExtra(''); clearResult()
|
|
115
|
+
}}>
|
|
116
|
+
{commands.map(item => <option key={item.name} value={item.name}>{item.name}</option>)}
|
|
117
|
+
</select>
|
|
118
|
+
</label>
|
|
119
|
+
{command.description && <p className="plugin-muted">{command.description}</p>}
|
|
120
|
+
{fields.map(arg => <label key={arg.name}>{arg.label}
|
|
121
|
+
{arg.type === 'choice' ? <select className="select" required={arg.required} value={String(values[arg.name] ?? '')}
|
|
122
|
+
onChange={event => changeValue(arg.name, event.target.value)}>
|
|
123
|
+
<option value="">Choose…</option>
|
|
124
|
+
{arg.options?.map(option => <option key={option} value={option}>{option}</option>)}
|
|
125
|
+
</select> : arg.type === 'boolean' ? <input type="checkbox" required={arg.required} checked={values[arg.name] === true}
|
|
126
|
+
onChange={event => changeValue(arg.name, event.target.checked)} /> :
|
|
127
|
+
<input className="input" type={arg.type === 'number' ? 'number' : 'text'} step={arg.type === 'number' ? 'any' : undefined}
|
|
128
|
+
required={arg.required} value={String(values[arg.name] ?? '')} onChange={event => changeValue(arg.name, event.target.value)} />}
|
|
129
|
+
</label>)}
|
|
130
|
+
{fields.length ? <details><summary>Additional arguments</summary>{argumentInput}</details> : argumentInput}
|
|
131
|
+
{pending ? <div className="drawer-stack" role="group" aria-label="Confirm command">
|
|
132
|
+
<p>Confirm <strong>{pending.command}</strong> in the selected brain.</p>
|
|
133
|
+
{pending.args.length > 0 && <pre className="plugin-command-output">{JSON.stringify(pending.args, null, 2)}</pre>}
|
|
134
|
+
<div className="drawer-actions">
|
|
135
|
+
<button type="button" className="btn btn-primary" onClick={() => void run(pending)}>Confirm and run</button>
|
|
136
|
+
<button type="button" className="btn btn-ghost" onClick={() => setPending(undefined)}>Cancel</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div> : <button type="submit" className="btn btn-primary">{busy ? 'Running…' : 'Run command'}</button>}
|
|
139
|
+
</fieldset>
|
|
140
|
+
{output && <pre className="plugin-command-output" role={output.error ? 'alert' : 'status'}>{output.text}</pre>}
|
|
141
|
+
</form>
|
|
142
|
+
}
|