total-recall-brain 3.45.0 → 3.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/total-recall.mjs +4 -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/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,1719 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo, useCallback, useRef, type CSSProperties, type ReactNode } from "react"
|
|
2
|
-
import { useSearchParams } from "react-router-dom"
|
|
3
|
-
import {
|
|
4
|
-
fetchPlugins,
|
|
5
|
-
fetchPlugin,
|
|
6
|
-
fetchBundledPlugins,
|
|
7
|
-
fetchPeerPlugins,
|
|
8
|
-
installPlugin,
|
|
9
|
-
removePlugin,
|
|
10
|
-
setPluginShared,
|
|
11
|
-
updatePluginBranding,
|
|
12
|
-
setPluginLocked,
|
|
13
|
-
deployPluginToStore,
|
|
14
|
-
runPluginCommand,
|
|
15
|
-
fetchPluginReadme,
|
|
16
|
-
setPluginActivation,
|
|
17
|
-
type PluginInfo,
|
|
18
|
-
type BundledPlugin,
|
|
19
|
-
type PeersResponse,
|
|
20
|
-
type PeerNode
|
|
21
|
-
} from "../api"
|
|
22
|
-
import { renderMarkdown } from "../components/MarkdownUtils"
|
|
23
|
-
import { PluginPanel, pluginElements } from "../components/plugins/PluginPanel"
|
|
24
|
-
import { PluginSettings } from "../components/plugins/PluginSettings"
|
|
25
|
-
|
|
26
|
-
interface PluginsPageProps {
|
|
27
|
-
activeBrainId?: string | null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type Tab = "installed" | "bundled" | "mesh"
|
|
31
|
-
type DetailTab = "preview" | "run" | "readme" | "details"
|
|
32
|
-
type Alert = { type: "success" | "error"; message: string } | null
|
|
33
|
-
|
|
34
|
-
const PEER_STATUS_LABEL: Record<PeerNode["status"], string> = {
|
|
35
|
-
ok: "online",
|
|
36
|
-
offline: "offline",
|
|
37
|
-
unreachable: "unreachable",
|
|
38
|
-
not_configured: "mesh sync not configured",
|
|
39
|
-
unsupported: "no plugin sharing (older version)",
|
|
40
|
-
error: "error"
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const mono: CSSProperties = { fontFamily: "var(--font-mono)", fontSize: "12px" }
|
|
44
|
-
const muted: CSSProperties = { color: "var(--text-secondary)", fontSize: "13px", lineHeight: 1.5 }
|
|
45
|
-
|
|
46
|
-
const DEFAULT_CORE_PLUGINS = new Set([
|
|
47
|
-
"auth",
|
|
48
|
-
"mesh",
|
|
49
|
-
"openwiki",
|
|
50
|
-
"composable-cli",
|
|
51
|
-
"cli-agents",
|
|
52
|
-
"code-quality",
|
|
53
|
-
"security",
|
|
54
|
-
"embeddings",
|
|
55
|
-
"git"
|
|
56
|
-
])
|
|
57
|
-
|
|
58
|
-
const AGENT_RECIPE_PROMPT = `You are my coding agent. Author a bespoke Total Recall plugin for [DOMAIN] following the Total Recall Plugin Recipe:
|
|
59
|
-
|
|
60
|
-
1. Scaffold:
|
|
61
|
-
total-recall scaffold-plugin <id> --title "<Name>" --description "<Description>"
|
|
62
|
-
|
|
63
|
-
2. Manifest (plugin.json):
|
|
64
|
-
- id: lowercase kebab-case
|
|
65
|
-
- name: Clear Title
|
|
66
|
-
- branding: { name, color, badge }
|
|
67
|
-
- cli: command name and subcommands with effect (read/write)
|
|
68
|
-
- ui: elements declaring the Web Component tag <tr-plugin-<id>-card>
|
|
69
|
-
- secrets: declare required keys, never store plain text
|
|
70
|
-
|
|
71
|
-
3. Web Component (ui/card.js):
|
|
72
|
-
- Register <tr-plugin-<id>-card>
|
|
73
|
-
- Standardized layout with clean borders and >=44px touch targets
|
|
74
|
-
- Authentic purpose-built domain tools (NO fake audio equalizers or fake skins)
|
|
75
|
-
- Real interactive controls executing plugin subcommands via host.run(subcommand, args)
|
|
76
|
-
|
|
77
|
-
4. Distribution:
|
|
78
|
-
- Private: keep in .agent/plugins/<id>
|
|
79
|
-
- Mesh: share peer-to-peer via total-recall plugin share <id>`
|
|
80
|
-
|
|
81
|
-
function resolvePluginBranding(p: { id: string; branding?: { icon?: string | null; color?: string | null; badge?: string | null } }) {
|
|
82
|
-
const custom = p.branding || {}
|
|
83
|
-
return {
|
|
84
|
-
icon: custom.icon || "",
|
|
85
|
-
color: custom.color || "#3b82f6",
|
|
86
|
-
badge: custom.badge || "Plugin",
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function shortHash(sha: string | null | undefined) {
|
|
91
|
-
return sha ? `${sha.slice(0, 12)}…` : "—"
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function formatBytes(n: number | null | undefined) {
|
|
95
|
-
if (n === null || n === undefined) return "—"
|
|
96
|
-
if (n < 1024) return `${n} B`
|
|
97
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
98
|
-
return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function sourceLabel(p: PluginInfo) {
|
|
102
|
-
switch (p.source.kind) {
|
|
103
|
-
case "peer": return `from ${p.source.peer_hostname || "a peer"}`
|
|
104
|
-
case "bundled": return "bundled"
|
|
105
|
-
case "git": return "git"
|
|
106
|
-
case "public": return "direct link"
|
|
107
|
-
case "link": return "linked folder"
|
|
108
|
-
default: return "local folder"
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function UseCaseChips({ useCases }: { useCases: string[] }) {
|
|
113
|
-
if (!useCases.length) return null
|
|
114
|
-
return (
|
|
115
|
-
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }}>
|
|
116
|
-
{useCases.map((u) => (
|
|
117
|
-
<span key={u} className="badge" style={{ fontSize: "11px" }}>{u}</span>
|
|
118
|
-
))}
|
|
119
|
-
</div>
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function Fact({ label, children }: { label: string; children: ReactNode }) {
|
|
124
|
-
return (
|
|
125
|
-
<div style={{ display: "flex", gap: "12px", fontSize: "13px", padding: "6px 0", borderBottom: "1px solid var(--border)" }}>
|
|
126
|
-
<span style={{ width: "132px", flexShrink: 0, color: "var(--text-tertiary)" }}>{label}</span>
|
|
127
|
-
<span style={{ color: "var(--text-primary)", minWidth: 0, overflowWrap: "anywhere" }}>{children}</span>
|
|
128
|
-
</div>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function Empty({ title, children }: { title: string; children?: ReactNode }) {
|
|
133
|
-
return (
|
|
134
|
-
<div className="card" style={{ textAlign: "center", padding: "40px 24px" }}>
|
|
135
|
-
<div style={{ fontWeight: 600, marginBottom: "6px" }}>{title}</div>
|
|
136
|
-
{children && <div style={muted}>{children}</div>}
|
|
137
|
-
</div>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function PluginCardCapabilities({
|
|
142
|
-
p,
|
|
143
|
-
onRunSubcommand,
|
|
144
|
-
onOpenSettings,
|
|
145
|
-
onOpenPreview
|
|
146
|
-
}: {
|
|
147
|
-
p: PluginInfo;
|
|
148
|
-
onRunSubcommand: (subcommand: string) => void;
|
|
149
|
-
onOpenSettings: () => void;
|
|
150
|
-
onOpenPreview: () => void;
|
|
151
|
-
}) {
|
|
152
|
-
const [expanded, setExpanded] = useState(false)
|
|
153
|
-
const subcommands = p.cli?.subcommands || []
|
|
154
|
-
const tasks = p.tasks || []
|
|
155
|
-
const secrets = p.secrets || []
|
|
156
|
-
const uiElements = p.ui?.elements || []
|
|
157
|
-
const categories = p.categories || []
|
|
158
|
-
const deploy = (p.manifest?.deploy as Record<string, unknown> | undefined) || {}
|
|
159
|
-
const accessGrants = (deploy.access_grants as string[] | undefined) || []
|
|
160
|
-
|
|
161
|
-
return (
|
|
162
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginTop: "4px" }}>
|
|
163
|
-
{/* Quick Capability Badges */}
|
|
164
|
-
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap", fontSize: "11px" }}>
|
|
165
|
-
{p.cli && (
|
|
166
|
-
<span className="badge" style={{ background: "rgba(20, 184, 166, 0.15)", color: "#14b8a6", borderColor: "rgba(20, 184, 166, 0.3)" }}>
|
|
167
|
-
[CLI] {p.cli.command || p.id} ({subcommands.length} verbs)
|
|
168
|
-
</span>
|
|
169
|
-
)}
|
|
170
|
-
{tasks.length > 0 && (
|
|
171
|
-
<span className="badge" style={{ background: "rgba(99, 102, 241, 0.15)", color: "#818cf8", borderColor: "rgba(99, 102, 241, 0.3)" }}>
|
|
172
|
-
[CRON] {tasks.length} {tasks.length === 1 ? "task" : "tasks"}
|
|
173
|
-
</span>
|
|
174
|
-
)}
|
|
175
|
-
{secrets.length > 0 && (
|
|
176
|
-
<span className="badge" style={{ background: "rgba(245, 158, 11, 0.15)", color: "#f59e0b", borderColor: "rgba(245, 158, 11, 0.3)" }}>
|
|
177
|
-
[KEY] {secrets.length} {secrets.length === 1 ? "secret" : "secrets"}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
{uiElements.length > 0 && (
|
|
181
|
-
<span className="badge" style={{ background: "rgba(236, 72, 153, 0.15)", color: "#f472b6", borderColor: "rgba(236, 72, 153, 0.3)" }}>
|
|
182
|
-
[UI] {uiElements.length} {uiElements.length === 1 ? "component" : "components"}
|
|
183
|
-
</span>
|
|
184
|
-
)}
|
|
185
|
-
{categories.length > 0 && (
|
|
186
|
-
<span className="badge" style={{ background: "rgba(139, 92, 246, 0.15)", color: "#a78bfa", borderColor: "rgba(139, 92, 246, 0.3)" }}>
|
|
187
|
-
[CAT] {categories.length} {categories.length === 1 ? "category" : "categories"}
|
|
188
|
-
</span>
|
|
189
|
-
)}
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
{/* Clickable Subcommand Chips */}
|
|
193
|
-
{subcommands.length > 0 && (
|
|
194
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
195
|
-
<div style={{ fontSize: "11px", color: "var(--text-tertiary)", fontWeight: 600 }}>ACTIONS & CLIS:</div>
|
|
196
|
-
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
|
|
197
|
-
{subcommands.slice(0, expanded ? undefined : 4).map((sub) => (
|
|
198
|
-
<button
|
|
199
|
-
key={sub.name}
|
|
200
|
-
type="button"
|
|
201
|
-
className="btn btn-ghost btn-sm"
|
|
202
|
-
onClick={() => onRunSubcommand(sub.name)}
|
|
203
|
-
title={sub.description || `Run ${p.cli?.command} ${sub.name}`}
|
|
204
|
-
style={{
|
|
205
|
-
padding: "2px 8px",
|
|
206
|
-
fontSize: "11px",
|
|
207
|
-
fontFamily: "var(--font-mono)",
|
|
208
|
-
background: "var(--bg-tertiary)",
|
|
209
|
-
border: "1px solid var(--border)",
|
|
210
|
-
borderRadius: "4px"
|
|
211
|
-
}}
|
|
212
|
-
>
|
|
213
|
-
${p.cli?.command ? ` ${p.cli.command}` : ""} {sub.name}
|
|
214
|
-
</button>
|
|
215
|
-
))}
|
|
216
|
-
{subcommands.length > 4 && !expanded && (
|
|
217
|
-
<button
|
|
218
|
-
type="button"
|
|
219
|
-
className="btn btn-ghost btn-sm"
|
|
220
|
-
onClick={() => setExpanded(true)}
|
|
221
|
-
style={{ padding: "2px 6px", fontSize: "10px", color: "var(--text-secondary)" }}
|
|
222
|
-
>
|
|
223
|
-
+{subcommands.length - 4} more
|
|
224
|
-
</button>
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
|
|
230
|
-
{/* Expandable Deep Capabilities Drawer */}
|
|
231
|
-
{expanded && (
|
|
232
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginTop: "6px", padding: "10px", background: "rgba(0,0,0,0.2)", borderRadius: "6px", border: "1px solid var(--border)" }}>
|
|
233
|
-
{/* Workflows / Tasks */}
|
|
234
|
-
{tasks.length > 0 && (
|
|
235
|
-
<div>
|
|
236
|
-
<div style={{ fontSize: "11px", fontWeight: 600, color: "var(--text-tertiary)", marginBottom: "4px" }}>SCHEDULED WORKFLOWS:</div>
|
|
237
|
-
{tasks.map((t, idx) => (
|
|
238
|
-
<div key={idx} style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: "11px", padding: "3px 0" }}>
|
|
239
|
-
<span style={{ fontFamily: "var(--font-mono)", color: "#818cf8" }}>{t.schedule}</span>
|
|
240
|
-
<span style={{ color: "var(--text-secondary)", flex: 1, marginLeft: "8px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }} title={t.intent}>
|
|
241
|
-
{t.intent}
|
|
242
|
-
</span>
|
|
243
|
-
<button
|
|
244
|
-
type="button"
|
|
245
|
-
className="btn btn-ghost btn-sm"
|
|
246
|
-
onClick={() => onRunSubcommand(t.command)}
|
|
247
|
-
style={{ padding: "1px 6px", fontSize: "10px" }}
|
|
248
|
-
>
|
|
249
|
-
▶ Run
|
|
250
|
-
</button>
|
|
251
|
-
</div>
|
|
252
|
-
))}
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
{/* Secrets & Config */}
|
|
257
|
-
{secrets.length > 0 && (
|
|
258
|
-
<div>
|
|
259
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
|
|
260
|
-
<span style={{ fontSize: "11px", fontWeight: 600, color: "var(--text-tertiary)" }}>CONFIGS & CREDENTIALS:</span>
|
|
261
|
-
<button
|
|
262
|
-
type="button"
|
|
263
|
-
className="btn btn-ghost btn-sm"
|
|
264
|
-
onClick={onOpenSettings}
|
|
265
|
-
style={{ padding: "1px 6px", fontSize: "10px" }}
|
|
266
|
-
>
|
|
267
|
-
Configure
|
|
268
|
-
</button>
|
|
269
|
-
</div>
|
|
270
|
-
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
|
|
271
|
-
{secrets.map((s) => (
|
|
272
|
-
<span
|
|
273
|
-
key={s.key}
|
|
274
|
-
className="badge"
|
|
275
|
-
style={{
|
|
276
|
-
fontSize: "10px",
|
|
277
|
-
fontFamily: "var(--font-mono)",
|
|
278
|
-
background: s.required ? "rgba(239, 68, 68, 0.1)" : "rgba(255, 255, 255, 0.05)",
|
|
279
|
-
color: s.required ? "#f87171" : "var(--text-secondary)",
|
|
280
|
-
borderColor: s.required ? "rgba(239, 68, 68, 0.2)" : "var(--border)"
|
|
281
|
-
}}
|
|
282
|
-
title={s.description || s.key}
|
|
283
|
-
>
|
|
284
|
-
{s.key} {s.required ? "(required)" : "(optional)"}
|
|
285
|
-
</span>
|
|
286
|
-
))}
|
|
287
|
-
</div>
|
|
288
|
-
</div>
|
|
289
|
-
)}
|
|
290
|
-
|
|
291
|
-
{/* Access Grants & Workspaces */}
|
|
292
|
-
{accessGrants.length > 0 && (
|
|
293
|
-
<div>
|
|
294
|
-
<div style={{ fontSize: "11px", fontWeight: 600, color: "var(--text-tertiary)", marginBottom: "4px" }}>SSSS ACCESS GRANTS:</div>
|
|
295
|
-
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
|
|
296
|
-
{accessGrants.map((grant: string) => (
|
|
297
|
-
<span key={grant} className="badge" style={{ fontSize: "10px", fontFamily: "var(--font-mono)" }}>
|
|
298
|
-
{grant}
|
|
299
|
-
</span>
|
|
300
|
-
))}
|
|
301
|
-
</div>
|
|
302
|
-
</div>
|
|
303
|
-
)}
|
|
304
|
-
|
|
305
|
-
{/* UI Custom Elements */}
|
|
306
|
-
{uiElements.length > 0 && (
|
|
307
|
-
<div>
|
|
308
|
-
<div style={{ fontSize: "11px", fontWeight: 600, color: "var(--text-tertiary)", marginBottom: "4px" }}>UI CUSTOM ELEMENTS:</div>
|
|
309
|
-
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap" }}>
|
|
310
|
-
{uiElements.map((el) => (
|
|
311
|
-
<button
|
|
312
|
-
key={el.id}
|
|
313
|
-
type="button"
|
|
314
|
-
className="btn btn-ghost btn-sm"
|
|
315
|
-
onClick={onOpenPreview}
|
|
316
|
-
style={{ fontSize: "10px", fontFamily: "var(--font-mono)", padding: "2px 6px" }}
|
|
317
|
-
title={`Open <${el.tag}> (${el.kind})`}
|
|
318
|
-
>
|
|
319
|
-
<{el.tag}>
|
|
320
|
-
</button>
|
|
321
|
-
))}
|
|
322
|
-
</div>
|
|
323
|
-
</div>
|
|
324
|
-
)}
|
|
325
|
-
</div>
|
|
326
|
-
)}
|
|
327
|
-
|
|
328
|
-
{/* Toggle Expand / Collapse */}
|
|
329
|
-
<button
|
|
330
|
-
type="button"
|
|
331
|
-
className="btn btn-ghost btn-sm"
|
|
332
|
-
onClick={() => setExpanded(!expanded)}
|
|
333
|
-
style={{
|
|
334
|
-
alignSelf: "flex-start",
|
|
335
|
-
padding: "2px 6px",
|
|
336
|
-
fontSize: "11px",
|
|
337
|
-
color: "var(--text-secondary)",
|
|
338
|
-
marginTop: "2px"
|
|
339
|
-
}}
|
|
340
|
-
>
|
|
341
|
-
{expanded ? "▴ Collapse details" : "▾ Expand capabilities & configs"}
|
|
342
|
-
</button>
|
|
343
|
-
</div>
|
|
344
|
-
)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export default function PluginsPage({ activeBrainId }: PluginsPageProps) {
|
|
348
|
-
const [searchParams, setSearchParams] = useSearchParams()
|
|
349
|
-
const [tab, setTab] = useState<Tab>("installed")
|
|
350
|
-
const [installed, setInstalled] = useState<PluginInfo[]>([])
|
|
351
|
-
const [bundled, setBundled] = useState<BundledPlugin[]>([])
|
|
352
|
-
const [peers, setPeers] = useState<PeersResponse | null>(null)
|
|
353
|
-
const [peersLoading, setPeersLoading] = useState(false)
|
|
354
|
-
const [loading, setLoading] = useState(true)
|
|
355
|
-
const [query, setQuery] = useState("")
|
|
356
|
-
const [useCase, setUseCase] = useState("")
|
|
357
|
-
const [busy, setBusy] = useState<string | null>(null)
|
|
358
|
-
const [alert, setAlert] = useState<Alert>(null)
|
|
359
|
-
|
|
360
|
-
const [installOpen, setInstallOpen] = useState(false)
|
|
361
|
-
const [installSource, setInstallSource] = useState("")
|
|
362
|
-
const [installLink, setInstallLink] = useState(false)
|
|
363
|
-
const [installGlobal, setInstallGlobal] = useState(false)
|
|
364
|
-
|
|
365
|
-
const [detail, setDetail] = useState<PluginInfo | null>(null)
|
|
366
|
-
const [detailTab, setDetailTab] = useState<DetailTab>("preview")
|
|
367
|
-
const [subcommand, setSubcommand] = useState("")
|
|
368
|
-
const [args, setArgs] = useState("")
|
|
369
|
-
const [output, setOutput] = useState<{ text: string; ok: boolean } | null>(null)
|
|
370
|
-
const [readme, setReadme] = useState("")
|
|
371
|
-
|
|
372
|
-
// Agent Plugin Recipe & Workspace State
|
|
373
|
-
const [recipeOpen, setRecipeOpen] = useState(false)
|
|
374
|
-
const [viewMode, setViewMode] = useState<"workspace" | "all">("workspace")
|
|
375
|
-
const [workspaceActiveIds, setWorkspaceActiveIds] = useState<Set<string>>(() => {
|
|
376
|
-
try {
|
|
377
|
-
const stored = localStorage.getItem("tr_workspace_plugins")
|
|
378
|
-
if (stored) return new Set(JSON.parse(stored))
|
|
379
|
-
} catch {}
|
|
380
|
-
return new Set<string>()
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// Branding Customization Modal state
|
|
384
|
-
const [brandingOpen, setBrandingOpen] = useState(false)
|
|
385
|
-
const [brandingPlugin, setBrandingPlugin] = useState<PluginInfo | null>(null)
|
|
386
|
-
const [brandingIcon, setBrandingIcon] = useState("")
|
|
387
|
-
const [brandingColor, setBrandingColor] = useState("#3b82f6")
|
|
388
|
-
const [brandingBadge, setBrandingBadge] = useState("")
|
|
389
|
-
|
|
390
|
-
// Touch & Context Menu state (right-click & long-press)
|
|
391
|
-
const [contextMenu, setContextMenu] = useState<{ plugin: PluginInfo; x: number; y: number } | null>(null)
|
|
392
|
-
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
393
|
-
|
|
394
|
-
const handleTouchStart = (p: PluginInfo, e: React.TouchEvent) => {
|
|
395
|
-
if (e.touches.length !== 1) return
|
|
396
|
-
const touch = e.touches[0]
|
|
397
|
-
const clientX = touch.clientX
|
|
398
|
-
const clientY = touch.clientY
|
|
399
|
-
longPressTimerRef.current = setTimeout(() => {
|
|
400
|
-
if ("vibrate" in navigator) {
|
|
401
|
-
try { navigator.vibrate(50) } catch {}
|
|
402
|
-
}
|
|
403
|
-
setContextMenu({ plugin: p, x: clientX, y: clientY })
|
|
404
|
-
}, 500)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const cancelLongPress = () => {
|
|
408
|
-
if (longPressTimerRef.current) {
|
|
409
|
-
clearTimeout(longPressTimerRef.current)
|
|
410
|
-
longPressTimerRef.current = null
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const handleContextMenu = (p: PluginInfo, e: React.MouseEvent) => {
|
|
415
|
-
e.preventDefault()
|
|
416
|
-
e.stopPropagation()
|
|
417
|
-
setContextMenu({ plugin: p, x: e.clientX, y: e.clientY })
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
useEffect(() => {
|
|
421
|
-
const onPointerDown = () => setContextMenu(null)
|
|
422
|
-
window.addEventListener("pointerdown", onPointerDown)
|
|
423
|
-
return () => window.removeEventListener("pointerdown", onPointerDown)
|
|
424
|
-
}, [])
|
|
425
|
-
|
|
426
|
-
const loadLocal = useCallback(async () => {
|
|
427
|
-
setLoading(true)
|
|
428
|
-
try {
|
|
429
|
-
const [i, b] = await Promise.all([fetchPlugins(), fetchBundledPlugins()])
|
|
430
|
-
setInstalled(i)
|
|
431
|
-
setBundled(b)
|
|
432
|
-
} finally {
|
|
433
|
-
setLoading(false)
|
|
434
|
-
}
|
|
435
|
-
}, [])
|
|
436
|
-
|
|
437
|
-
const loadPeers = useCallback(async () => {
|
|
438
|
-
setPeersLoading(true)
|
|
439
|
-
try {
|
|
440
|
-
setPeers(await fetchPeerPlugins())
|
|
441
|
-
} finally {
|
|
442
|
-
setPeersLoading(false)
|
|
443
|
-
}
|
|
444
|
-
}, [])
|
|
445
|
-
|
|
446
|
-
useEffect(() => {
|
|
447
|
-
loadLocal()
|
|
448
|
-
}, [loadLocal, activeBrainId])
|
|
449
|
-
|
|
450
|
-
useEffect(() => {
|
|
451
|
-
if (tab === "mesh" && peers === null && !peersLoading) loadPeers()
|
|
452
|
-
}, [tab, peers, peersLoading, loadPeers])
|
|
453
|
-
|
|
454
|
-
const allUseCases = useMemo(() => {
|
|
455
|
-
const set = new Set<string>()
|
|
456
|
-
for (const p of installed) p.use_cases.forEach((u) => set.add(u))
|
|
457
|
-
for (const p of bundled) p.use_cases.forEach((u) => set.add(u))
|
|
458
|
-
for (const n of peers?.peers || []) n.plugins.forEach((p) => p.use_cases.forEach((u) => set.add(u)))
|
|
459
|
-
return Array.from(set).sort()
|
|
460
|
-
}, [installed, bundled, peers])
|
|
461
|
-
|
|
462
|
-
const matches = useCallback((p: { id: string; name: string; description: string; use_cases: string[] }) => {
|
|
463
|
-
if (useCase && !p.use_cases.includes(useCase)) return false
|
|
464
|
-
const q = query.trim().toLowerCase()
|
|
465
|
-
if (!q) return true
|
|
466
|
-
return [p.id, p.name, p.description, ...p.use_cases].some((v) => v.toLowerCase().includes(q))
|
|
467
|
-
}, [query, useCase])
|
|
468
|
-
|
|
469
|
-
const isWorkspaceCard = useCallback((p: { id: string; scope?: string }) => {
|
|
470
|
-
if (p.scope === "project") return true
|
|
471
|
-
if (DEFAULT_CORE_PLUGINS.has(p.id)) return true
|
|
472
|
-
if (workspaceActiveIds.has(p.id)) return true
|
|
473
|
-
return false
|
|
474
|
-
}, [workspaceActiveIds])
|
|
475
|
-
|
|
476
|
-
const addToWorkspace = async (p: { id: string; name: string }) => {
|
|
477
|
-
setBusy(p.id)
|
|
478
|
-
try {
|
|
479
|
-
const next = new Set(workspaceActiveIds)
|
|
480
|
-
next.add(p.id)
|
|
481
|
-
setWorkspaceActiveIds(next)
|
|
482
|
-
try { localStorage.setItem("tr_workspace_plugins", JSON.stringify([...next])) } catch {}
|
|
483
|
-
await setPluginActivation(p.id, true)
|
|
484
|
-
setAlert({ type: "success", message: `Added ${p.name} to workspace dashboard` })
|
|
485
|
-
await loadLocal()
|
|
486
|
-
} catch (err: any) {
|
|
487
|
-
setAlert({ type: "error", message: err?.message || `Could not activate ${p.name}` })
|
|
488
|
-
} finally {
|
|
489
|
-
setBusy(null)
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
const removeFromWorkspace = async (p: { id: string; name: string }) => {
|
|
494
|
-
setBusy(p.id)
|
|
495
|
-
try {
|
|
496
|
-
const next = new Set(workspaceActiveIds)
|
|
497
|
-
next.delete(p.id)
|
|
498
|
-
setWorkspaceActiveIds(next)
|
|
499
|
-
try { localStorage.setItem("tr_workspace_plugins", JSON.stringify([...next])) } catch {}
|
|
500
|
-
await setPluginActivation(p.id, false)
|
|
501
|
-
setAlert({ type: "success", message: `Deactivated card for ${p.name}` })
|
|
502
|
-
await loadLocal()
|
|
503
|
-
} catch (err: any) {
|
|
504
|
-
setAlert({ type: "error", message: err?.message || `Could not deactivate ${p.name}` })
|
|
505
|
-
} finally {
|
|
506
|
-
setBusy(null)
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const copyAgentPrompt = () => {
|
|
511
|
-
void navigator.clipboard?.writeText(AGENT_RECIPE_PROMPT)
|
|
512
|
-
setAlert({ type: "success", message: "Copied Agent Plugin Recipe prompt to clipboard" })
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const doInstall = async (source: string, opts: { link?: boolean; global?: boolean } = {}) => {
|
|
516
|
-
setBusy(source)
|
|
517
|
-
setAlert(null)
|
|
518
|
-
const res = await installPlugin({ source, ...opts })
|
|
519
|
-
setBusy(null)
|
|
520
|
-
if (res.success) {
|
|
521
|
-
setAlert({ type: "success", message: res.message || `Installed ${source}` })
|
|
522
|
-
await loadLocal()
|
|
523
|
-
if (peers) loadPeers()
|
|
524
|
-
return true
|
|
525
|
-
}
|
|
526
|
-
setAlert({ type: "error", message: res.error || `Could not install ${source}` })
|
|
527
|
-
return false
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
const doRemove = async (p: PluginInfo) => {
|
|
531
|
-
if (!window.confirm(`Remove "${p.name}" (${p.scope})?`)) return
|
|
532
|
-
setBusy(p.id)
|
|
533
|
-
const res = await removePlugin(p.id, p.scope === "global")
|
|
534
|
-
setBusy(null)
|
|
535
|
-
setAlert(res.success ? { type: "success", message: `Removed ${p.name}` } : { type: "error", message: res.error || "Remove failed" })
|
|
536
|
-
if (res.success) {
|
|
537
|
-
if (detail?.id === p.id) closeDetail()
|
|
538
|
-
loadLocal()
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const doShare = async (p: PluginInfo, shared: boolean) => {
|
|
543
|
-
setBusy(p.id)
|
|
544
|
-
const res = await setPluginShared(p.id, shared)
|
|
545
|
-
setBusy(null)
|
|
546
|
-
if (!res.success) {
|
|
547
|
-
setAlert({ type: "error", message: res.error || "Could not change sharing" })
|
|
548
|
-
return
|
|
549
|
-
}
|
|
550
|
-
if (res.plugin && detail?.id === p.id) setDetail(res.plugin)
|
|
551
|
-
setAlert({ type: "success", message: shared ? `${p.name} is shared. Its public link is in the details.` : `${p.name} is no longer shared` })
|
|
552
|
-
loadLocal()
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
const doToggleLock = async (p: PluginInfo) => {
|
|
556
|
-
setBusy(p.id)
|
|
557
|
-
const nextLocked = !p.locked
|
|
558
|
-
const res = await setPluginLocked(p.id, nextLocked)
|
|
559
|
-
setBusy(null)
|
|
560
|
-
if (!res.success) {
|
|
561
|
-
setAlert({ type: "error", message: res.error || "Could not update lock status" })
|
|
562
|
-
return
|
|
563
|
-
}
|
|
564
|
-
if (res.plugin && detail?.id === p.id) setDetail(res.plugin)
|
|
565
|
-
setAlert({
|
|
566
|
-
type: "success",
|
|
567
|
-
message: nextLocked ? `${p.name} is locked against further editing.` : `${p.name} is unlocked for editing.`
|
|
568
|
-
})
|
|
569
|
-
loadLocal()
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
const doDeployStore = async (p: PluginInfo) => {
|
|
573
|
-
setBusy(p.id)
|
|
574
|
-
const res = await deployPluginToStore(p.id, true)
|
|
575
|
-
setBusy(null)
|
|
576
|
-
if (!res.success) {
|
|
577
|
-
setAlert({ type: "error", message: res.error || "Could not deploy plugin to store" })
|
|
578
|
-
return
|
|
579
|
-
}
|
|
580
|
-
if (res.plugin && detail?.id === p.id) setDetail(res.plugin)
|
|
581
|
-
setAlert({
|
|
582
|
-
type: "success",
|
|
583
|
-
message: `${p.name} deployed to the plugin store and locked as complete.`
|
|
584
|
-
})
|
|
585
|
-
loadLocal()
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const openBrandingModal = (p: PluginInfo) => {
|
|
589
|
-
const branding = resolvePluginBranding(p)
|
|
590
|
-
setBrandingPlugin(p)
|
|
591
|
-
setBrandingIcon(branding.icon)
|
|
592
|
-
setBrandingColor(branding.color)
|
|
593
|
-
setBrandingBadge(branding.badge)
|
|
594
|
-
setBrandingOpen(true)
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const saveBranding = async () => {
|
|
598
|
-
if (!brandingPlugin) return
|
|
599
|
-
if (brandingPlugin.locked) {
|
|
600
|
-
setAlert({ type: "error", message: "Plugin is locked. Unlock it before editing branding." })
|
|
601
|
-
setBrandingOpen(false)
|
|
602
|
-
return
|
|
603
|
-
}
|
|
604
|
-
setBusy(brandingPlugin.id)
|
|
605
|
-
const res = await updatePluginBranding(brandingPlugin.id, {
|
|
606
|
-
icon: brandingIcon.trim() || undefined,
|
|
607
|
-
color: brandingColor.trim() || undefined,
|
|
608
|
-
badge: brandingBadge.trim() || undefined
|
|
609
|
-
})
|
|
610
|
-
setBusy(null)
|
|
611
|
-
setBrandingOpen(false)
|
|
612
|
-
if (!res.success) {
|
|
613
|
-
setAlert({ type: "error", message: res.error || "Could not save branding" })
|
|
614
|
-
return
|
|
615
|
-
}
|
|
616
|
-
setAlert({ type: "success", message: `Updated branding for ${brandingPlugin.name}` })
|
|
617
|
-
await loadLocal()
|
|
618
|
-
if (detail?.id === brandingPlugin.id && res.plugin) {
|
|
619
|
-
setDetail(res.plugin)
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const openDetail = async (p: PluginInfo, targetTab: DetailTab = "preview") => {
|
|
624
|
-
setDetail(await fetchPlugin(p.id) || p)
|
|
625
|
-
setDetailTab(targetTab)
|
|
626
|
-
setSubcommand(p.cli?.subcommands?.[0]?.name || "")
|
|
627
|
-
setArgs("")
|
|
628
|
-
setOutput(null)
|
|
629
|
-
setReadme("")
|
|
630
|
-
setReadme(await fetchPluginReadme(p.id))
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const closeDetail = () => {
|
|
634
|
-
setDetail(null)
|
|
635
|
-
if (searchParams.has("id") || searchParams.has("plugin")) {
|
|
636
|
-
const next = new URLSearchParams(searchParams)
|
|
637
|
-
next.delete("id")
|
|
638
|
-
next.delete("plugin")
|
|
639
|
-
setSearchParams(next)
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
useEffect(() => {
|
|
644
|
-
const id = searchParams.get("id") || searchParams.get("plugin")
|
|
645
|
-
if (!id || detail?.id === id) return
|
|
646
|
-
const match = installed.find((p) => p.id === id || p.id === `tr-plugin-${id}` || p.name === id)
|
|
647
|
-
if (match) openDetail(match)
|
|
648
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
649
|
-
}, [searchParams, installed])
|
|
650
|
-
|
|
651
|
-
useEffect(() => {
|
|
652
|
-
if (!detail) return
|
|
653
|
-
const fresh = installed.find((p) => p.id === detail.id)
|
|
654
|
-
if (fresh && fresh !== detail) setDetail(fresh)
|
|
655
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
656
|
-
}, [installed])
|
|
657
|
-
|
|
658
|
-
const runCommand = async () => {
|
|
659
|
-
if (!detail) return
|
|
660
|
-
setBusy(`run:${detail.id}`)
|
|
661
|
-
const res = await runPluginCommand(detail.id, subcommand, args.trim() ? args.trim().split(/\s+/) : [])
|
|
662
|
-
setBusy(null)
|
|
663
|
-
if (res.error) setOutput({ text: res.error, ok: false })
|
|
664
|
-
else setOutput({ text: (res.output || "").trim() || "(no output)", ok: res.success })
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const filteredInstalled = installed.filter(matches)
|
|
668
|
-
const myCards = filteredInstalled.filter(isWorkspaceCard)
|
|
669
|
-
const shownInstalled = viewMode === "workspace" ? myCards : filteredInstalled
|
|
670
|
-
|
|
671
|
-
const storeCatalog = useMemo(() => {
|
|
672
|
-
const map = new Map<string, {
|
|
673
|
-
id: string;
|
|
674
|
-
name: string;
|
|
675
|
-
version: string;
|
|
676
|
-
description: string;
|
|
677
|
-
use_cases: string[];
|
|
678
|
-
installed: boolean;
|
|
679
|
-
inWorkspace: boolean;
|
|
680
|
-
sourcePlugin?: PluginInfo;
|
|
681
|
-
branding?: { icon?: string | null; color?: string | null; badge?: string | null };
|
|
682
|
-
cli?: { command: string | null; subcommands?: Array<{ name: string; description?: string }> } | null;
|
|
683
|
-
tasks?: any[];
|
|
684
|
-
secrets?: any[];
|
|
685
|
-
}>()
|
|
686
|
-
|
|
687
|
-
// Add bundled plugins
|
|
688
|
-
for (const b of bundled) {
|
|
689
|
-
const inst = installed.find((i) => i.id === b.id)
|
|
690
|
-
map.set(b.id, {
|
|
691
|
-
id: b.id,
|
|
692
|
-
name: b.name,
|
|
693
|
-
version: b.version,
|
|
694
|
-
description: b.description,
|
|
695
|
-
use_cases: b.use_cases || [],
|
|
696
|
-
installed: b.installed || !!inst,
|
|
697
|
-
inWorkspace: inst ? isWorkspaceCard(inst) : false,
|
|
698
|
-
sourcePlugin: inst,
|
|
699
|
-
branding: b.branding || (inst ? inst.branding : undefined),
|
|
700
|
-
cli: b.cli || (inst ? inst.cli : null),
|
|
701
|
-
tasks: b.tasks || (inst ? inst.tasks : []),
|
|
702
|
-
secrets: inst?.secrets || []
|
|
703
|
-
})
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Add any installed plugins that aren't in bundled
|
|
707
|
-
for (const inst of installed) {
|
|
708
|
-
if (!map.has(inst.id)) {
|
|
709
|
-
map.set(inst.id, {
|
|
710
|
-
id: inst.id,
|
|
711
|
-
name: inst.name,
|
|
712
|
-
version: inst.version,
|
|
713
|
-
description: inst.description,
|
|
714
|
-
use_cases: inst.use_cases || [],
|
|
715
|
-
installed: true,
|
|
716
|
-
inWorkspace: isWorkspaceCard(inst),
|
|
717
|
-
sourcePlugin: inst,
|
|
718
|
-
branding: inst.branding,
|
|
719
|
-
cli: inst.cli,
|
|
720
|
-
tasks: inst.tasks,
|
|
721
|
-
secrets: inst.secrets || []
|
|
722
|
-
})
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return Array.from(map.values())
|
|
727
|
-
}, [bundled, installed, isWorkspaceCard])
|
|
728
|
-
|
|
729
|
-
const filteredStoreCatalog = storeCatalog.filter(matches)
|
|
730
|
-
|
|
731
|
-
const tabButton = (id: Tab, label: string, count?: number) => (
|
|
732
|
-
<button
|
|
733
|
-
key={id}
|
|
734
|
-
role="tab"
|
|
735
|
-
aria-selected={tab === id}
|
|
736
|
-
className={`btn btn-sm ${tab === id ? "btn-primary" : "btn-ghost"}`}
|
|
737
|
-
onClick={() => setTab(id)}
|
|
738
|
-
>
|
|
739
|
-
{label}
|
|
740
|
-
{count !== undefined && <span style={{ opacity: 0.75, fontWeight: 500, marginLeft: "4px" }}>{count}</span>}
|
|
741
|
-
</button>
|
|
742
|
-
)
|
|
743
|
-
|
|
744
|
-
return (
|
|
745
|
-
<div className="page" style={{ maxWidth: "100%", padding: "0 20px", margin: "0 auto", width: "100%" }}>
|
|
746
|
-
<div className="page-header" style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "16px", flexWrap: "wrap" }}>
|
|
747
|
-
<div>
|
|
748
|
-
<h1>Plugins</h1>
|
|
749
|
-
<p>
|
|
750
|
-
Modular capability system for Total Recall and SSSS apps — memory categories, agent context, commands, and design-token UI.
|
|
751
|
-
Customizable branding, locked release status, and direct store deployment are managed on each card.
|
|
752
|
-
</p>
|
|
753
|
-
</div>
|
|
754
|
-
<div style={{ display: "flex", gap: "8px" }}>
|
|
755
|
-
<button className="btn btn-ghost" onClick={() => setRecipeOpen(true)}>
|
|
756
|
-
Agent Recipe
|
|
757
|
-
</button>
|
|
758
|
-
<button className="btn btn-primary" onClick={() => { setInstallOpen(true); setAlert(null) }}>
|
|
759
|
-
Install from source…
|
|
760
|
-
</button>
|
|
761
|
-
</div>
|
|
762
|
-
</div>
|
|
763
|
-
|
|
764
|
-
{alert && (
|
|
765
|
-
<div className={`alert ${alert.type === "success" ? "alert-success" : "alert-error"}`} role="status"
|
|
766
|
-
style={{ display: "flex", justifyContent: "space-between", gap: "12px", marginBottom: "20px" }}>
|
|
767
|
-
<span>{alert.message}</span>
|
|
768
|
-
<button onClick={() => setAlert(null)} aria-label="Dismiss" style={{ background: "none", border: "none", color: "inherit", cursor: "pointer" }}>×</button>
|
|
769
|
-
</div>
|
|
770
|
-
)}
|
|
771
|
-
|
|
772
|
-
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px", flexWrap: "wrap", marginBottom: "20px" }}>
|
|
773
|
-
<div role="tablist" style={{ display: "flex", gap: "8px" }}>
|
|
774
|
-
{tabButton("installed", "My Cards", myCards.length)}
|
|
775
|
-
{tabButton("bundled", "Plugin Catalog", filteredStoreCatalog.length)}
|
|
776
|
-
{tabButton("mesh", "On the mesh")}
|
|
777
|
-
</div>
|
|
778
|
-
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
779
|
-
<input className="input" placeholder="Search plugins" value={query} onChange={(e) => setQuery(e.target.value)}
|
|
780
|
-
aria-label="Search plugins" style={{ width: "220px" }} />
|
|
781
|
-
<select className="select" value={useCase} onChange={(e) => setUseCase(e.target.value)} aria-label="Filter by use case" style={{ paddingRight: "32px" }}>
|
|
782
|
-
<option value="">All use cases</option>
|
|
783
|
-
{allUseCases.map((u) => <option key={u} value={u}>{u}</option>)}
|
|
784
|
-
</select>
|
|
785
|
-
</div>
|
|
786
|
-
</div>
|
|
787
|
-
|
|
788
|
-
{tab === "installed" && (
|
|
789
|
-
loading ? <div style={muted}>Loading…</div> : (
|
|
790
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
791
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: "12px" }}>
|
|
792
|
-
<div>
|
|
793
|
-
<div style={{ fontWeight: 600, fontSize: "15px" }}>
|
|
794
|
-
{viewMode === "workspace" ? "Active Workspace Cards" : "All Installed Plugins"}
|
|
795
|
-
</div>
|
|
796
|
-
<div style={muted}>
|
|
797
|
-
{viewMode === "workspace"
|
|
798
|
-
? "Core runtime foundation and activated repository cards. Standalone tools live in the Plugin Catalog."
|
|
799
|
-
: "All plugin packages installed on this host."}
|
|
800
|
-
</div>
|
|
801
|
-
</div>
|
|
802
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
803
|
-
<button
|
|
804
|
-
type="button"
|
|
805
|
-
className={`btn btn-xs ${viewMode === "workspace" ? "btn-primary" : "btn-ghost"}`}
|
|
806
|
-
onClick={() => setViewMode("workspace")}
|
|
807
|
-
>
|
|
808
|
-
Workspace ({myCards.length})
|
|
809
|
-
</button>
|
|
810
|
-
<button
|
|
811
|
-
type="button"
|
|
812
|
-
className={`btn btn-xs ${viewMode === "all" ? "btn-primary" : "btn-ghost"}`}
|
|
813
|
-
onClick={() => setViewMode("all")}
|
|
814
|
-
>
|
|
815
|
-
All Installed ({installed.length})
|
|
816
|
-
</button>
|
|
817
|
-
</div>
|
|
818
|
-
</div>
|
|
819
|
-
|
|
820
|
-
{shownInstalled.length === 0 ? (
|
|
821
|
-
<Empty title={viewMode === "workspace" ? "No workspace cards active" : "No plugins installed"}>
|
|
822
|
-
{viewMode === "workspace" ? (
|
|
823
|
-
<>
|
|
824
|
-
Explore the <button type="button" className="btn btn-ghost btn-sm" onClick={() => setTab("bundled")}>Plugin Store</button> to add capabilities or ask your agent to author one.
|
|
825
|
-
</>
|
|
826
|
-
) : (
|
|
827
|
-
<>Start with a <button type="button" className="btn btn-ghost btn-sm" onClick={() => setTab("bundled")}>bundled plugin</button> or see what your peers share.</>
|
|
828
|
-
)}
|
|
829
|
-
</Empty>
|
|
830
|
-
) : (
|
|
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" }}>
|
|
833
|
-
{shownInstalled.map((p) => {
|
|
834
|
-
return (
|
|
835
|
-
<div
|
|
836
|
-
key={`${p.scope}:${p.id}`}
|
|
837
|
-
onContextMenu={(e) => handleContextMenu(p, e)}
|
|
838
|
-
onTouchStart={(e) => handleTouchStart(p, e)}
|
|
839
|
-
onTouchEnd={cancelLongPress}
|
|
840
|
-
style={{ display: "flex", flexDirection: "column", gap: "8px", minWidth: 0 }}
|
|
841
|
-
>
|
|
842
|
-
<PluginPanel plugin={p} />
|
|
843
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "0 4px", fontSize: "11px", color: "var(--text-tertiary)", flexWrap: "wrap", gap: "6px" }}>
|
|
844
|
-
<div style={{ display: "flex", gap: "6px", alignItems: "center", flexWrap: "wrap" }}>
|
|
845
|
-
<span className="badge">{p.scope}</span>
|
|
846
|
-
<span className="badge">{sourceLabel(p)}</span>
|
|
847
|
-
{p.shared && <span className="badge badge-accent">shared</span>}
|
|
848
|
-
{!p.valid && <span className="badge badge-error">invalid manifest</span>}
|
|
849
|
-
{p.modified_since_install && <span className="badge badge-warning">changed since install</span>}
|
|
850
|
-
</div>
|
|
851
|
-
<button
|
|
852
|
-
type="button"
|
|
853
|
-
className="btn btn-ghost btn-xs"
|
|
854
|
-
onClick={() => openDetail(p, "details")}
|
|
855
|
-
style={{ fontSize: "11px", color: "var(--text-secondary)" }}
|
|
856
|
-
>
|
|
857
|
-
Settings & usage
|
|
858
|
-
</button>
|
|
859
|
-
{!DEFAULT_CORE_PLUGINS.has(p.id) && (
|
|
860
|
-
<button
|
|
861
|
-
type="button"
|
|
862
|
-
className="btn btn-ghost btn-xs"
|
|
863
|
-
onClick={() => removeFromWorkspace(p)}
|
|
864
|
-
style={{ fontSize: "11px", color: "var(--text-secondary)" }}
|
|
865
|
-
title="Move back to Plugin Store"
|
|
866
|
-
>
|
|
867
|
-
Remove from Workspace
|
|
868
|
-
</button>
|
|
869
|
-
)}
|
|
870
|
-
</div>
|
|
871
|
-
</div>
|
|
872
|
-
)
|
|
873
|
-
})}
|
|
874
|
-
</div>
|
|
875
|
-
)}
|
|
876
|
-
</div>
|
|
877
|
-
)
|
|
878
|
-
)}
|
|
879
|
-
|
|
880
|
-
{tab === "bundled" && (
|
|
881
|
-
loading ? <div style={muted}>Loading…</div> : (
|
|
882
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "20px" }}>
|
|
883
|
-
{/* Agent Plugin Recipe Hero Banner */}
|
|
884
|
-
<div className="card" style={{
|
|
885
|
-
background: "linear-gradient(135deg, rgba(30, 41, 59, 0.7) 0%, rgba(15, 23, 42, 0.85) 100%)",
|
|
886
|
-
border: "1px solid rgba(99, 102, 241, 0.3)",
|
|
887
|
-
padding: "20px",
|
|
888
|
-
display: "flex",
|
|
889
|
-
flexDirection: "column",
|
|
890
|
-
gap: "14px"
|
|
891
|
-
}}>
|
|
892
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: "16px", flexWrap: "wrap" }}>
|
|
893
|
-
<div>
|
|
894
|
-
<div style={{ display: "flex", alignItems: "center", gap: "8px", marginBottom: "4px" }}>
|
|
895
|
-
<span className="badge" style={{ background: "rgba(99, 102, 241, 0.2)", color: "#818cf8", borderColor: "rgba(99, 102, 241, 0.4)", fontSize: "11px" }}>
|
|
896
|
-
AGENT RECIPE ARCHITECTURE
|
|
897
|
-
</span>
|
|
898
|
-
<span style={{ fontSize: "11px", color: "var(--text-tertiary)" }}>
|
|
899
|
-
Declarative Recipes · Private or Store
|
|
900
|
-
</span>
|
|
901
|
-
</div>
|
|
902
|
-
<h2 style={{ fontSize: "18px", fontWeight: 700, margin: 0, color: "#f8fafc" }}>
|
|
903
|
-
Build Bespoke Plugins with Your Coding Agent
|
|
904
|
-
</h2>
|
|
905
|
-
<p style={{ ...muted, margin: "6px 0 0 0", maxWidth: "640px" }}>
|
|
906
|
-
Total Recall plugins are not generated by closed system templates; your contextual coding agent (Antigravity, Claude, Codex, DSH) builds them from a declarative Recipe. Run them privately in your workspace or publish them to the Plugin Store.
|
|
907
|
-
</p>
|
|
908
|
-
</div>
|
|
909
|
-
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
910
|
-
<button
|
|
911
|
-
type="button"
|
|
912
|
-
className="btn btn-primary btn-sm"
|
|
913
|
-
onClick={() => setRecipeOpen(true)}
|
|
914
|
-
>
|
|
915
|
-
View Agent Recipe
|
|
916
|
-
</button>
|
|
917
|
-
<button
|
|
918
|
-
type="button"
|
|
919
|
-
className="btn btn-ghost btn-sm"
|
|
920
|
-
onClick={copyAgentPrompt}
|
|
921
|
-
>
|
|
922
|
-
Copy Agent Prompt
|
|
923
|
-
</button>
|
|
924
|
-
<button
|
|
925
|
-
type="button"
|
|
926
|
-
className="btn btn-ghost btn-sm"
|
|
927
|
-
onClick={() => { setInstallOpen(true); setAlert(null) }}
|
|
928
|
-
>
|
|
929
|
-
Install from source…
|
|
930
|
-
</button>
|
|
931
|
-
</div>
|
|
932
|
-
</div>
|
|
933
|
-
</div>
|
|
934
|
-
|
|
935
|
-
{filteredStoreCatalog.length === 0 ? (
|
|
936
|
-
<Empty title="No plugins match in store" />
|
|
937
|
-
) : (
|
|
938
|
-
<div className="card-grid" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 340px), 1fr))", gap: "16px" }}>
|
|
939
|
-
{filteredStoreCatalog.map((item) => {
|
|
940
|
-
const branding = resolvePluginBranding(item)
|
|
941
|
-
return (
|
|
942
|
-
<div
|
|
943
|
-
key={item.id}
|
|
944
|
-
className="card"
|
|
945
|
-
style={{
|
|
946
|
-
display: "flex",
|
|
947
|
-
flexDirection: "column",
|
|
948
|
-
gap: "10px",
|
|
949
|
-
borderTop: `3px solid ${branding.color}`,
|
|
950
|
-
background: "var(--bg-secondary)"
|
|
951
|
-
}}
|
|
952
|
-
>
|
|
953
|
-
<div style={{ display: "flex", justifyContent: "space-between", gap: "8px", alignItems: "flex-start" }}>
|
|
954
|
-
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
|
|
955
|
-
<div
|
|
956
|
-
style={{
|
|
957
|
-
width: "34px",
|
|
958
|
-
height: "34px",
|
|
959
|
-
borderRadius: "8px",
|
|
960
|
-
background: `${branding.color}20`,
|
|
961
|
-
border: `1px solid ${branding.color}40`,
|
|
962
|
-
display: "flex",
|
|
963
|
-
alignItems: "center",
|
|
964
|
-
justifyContent: "center",
|
|
965
|
-
fontWeight: 700,
|
|
966
|
-
fontSize: "12px",
|
|
967
|
-
color: branding.color
|
|
968
|
-
}}
|
|
969
|
-
>
|
|
970
|
-
{branding.icon || item.name.slice(0, 2).toUpperCase()}
|
|
971
|
-
</div>
|
|
972
|
-
<div>
|
|
973
|
-
<div style={{ fontWeight: 600, fontSize: "14px", lineHeight: 1.2 }}>{item.name}</div>
|
|
974
|
-
<div style={{ ...mono, color: "var(--text-tertiary)", fontSize: "11px" }}>
|
|
975
|
-
{item.id} · v{item.version}
|
|
976
|
-
</div>
|
|
977
|
-
</div>
|
|
978
|
-
</div>
|
|
979
|
-
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", gap: "4px" }}>
|
|
980
|
-
<span className="badge" style={{ background: `${branding.color}15`, color: branding.color, borderColor: `${branding.color}35`, fontSize: "10px" }}>
|
|
981
|
-
{branding.badge}
|
|
982
|
-
</span>
|
|
983
|
-
{item.inWorkspace && (
|
|
984
|
-
<span className="badge badge-success" style={{ fontSize: "10px" }}>
|
|
985
|
-
✓ In Workspace
|
|
986
|
-
</span>
|
|
987
|
-
)}
|
|
988
|
-
</div>
|
|
989
|
-
</div>
|
|
990
|
-
|
|
991
|
-
<div style={{ ...muted, fontSize: "12px", minHeight: "36px" }}>
|
|
992
|
-
{item.description}
|
|
993
|
-
</div>
|
|
994
|
-
|
|
995
|
-
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap", fontSize: "10px" }}>
|
|
996
|
-
{item.cli && (
|
|
997
|
-
<span className="badge" style={{ fontFamily: "var(--font-mono)" }}>
|
|
998
|
-
CLI ({item.cli.subcommands?.length || 1})
|
|
999
|
-
</span>
|
|
1000
|
-
)}
|
|
1001
|
-
{item.tasks && item.tasks.length > 0 && (
|
|
1002
|
-
<span className="badge" style={{ fontFamily: "var(--font-mono)" }}>
|
|
1003
|
-
{item.tasks.length} {item.tasks.length === 1 ? "task" : "tasks"}
|
|
1004
|
-
</span>
|
|
1005
|
-
)}
|
|
1006
|
-
{item.secrets && item.secrets.length > 0 && (
|
|
1007
|
-
<span className="badge" style={{ fontFamily: "var(--font-mono)" }}>
|
|
1008
|
-
{item.secrets.length} {item.secrets.length === 1 ? "secret" : "secrets"}
|
|
1009
|
-
</span>
|
|
1010
|
-
)}
|
|
1011
|
-
{DEFAULT_CORE_PLUGINS.has(item.id) && (
|
|
1012
|
-
<span className="badge" style={{ background: "rgba(59, 130, 246, 0.15)", color: "#60a5fa" }}>
|
|
1013
|
-
Core Bundle
|
|
1014
|
-
</span>
|
|
1015
|
-
)}
|
|
1016
|
-
</div>
|
|
1017
|
-
|
|
1018
|
-
<UseCaseChips useCases={item.use_cases} />
|
|
1019
|
-
|
|
1020
|
-
<div style={{ marginTop: "auto", display: "flex", justifyContent: "space-between", alignItems: "center", gap: "8px", paddingTop: "8px", borderTop: "1px solid var(--border)" }}>
|
|
1021
|
-
<button
|
|
1022
|
-
type="button"
|
|
1023
|
-
className="btn btn-ghost btn-sm"
|
|
1024
|
-
onClick={() => {
|
|
1025
|
-
if (item.sourcePlugin) {
|
|
1026
|
-
openDetail(item.sourcePlugin, "preview")
|
|
1027
|
-
} else {
|
|
1028
|
-
openDetail({
|
|
1029
|
-
id: item.id,
|
|
1030
|
-
name: item.name,
|
|
1031
|
-
version: item.version,
|
|
1032
|
-
description: item.description,
|
|
1033
|
-
author: null,
|
|
1034
|
-
license: null,
|
|
1035
|
-
homepage: null,
|
|
1036
|
-
use_cases: item.use_cases,
|
|
1037
|
-
valid: true,
|
|
1038
|
-
errors: [],
|
|
1039
|
-
scope: "global",
|
|
1040
|
-
linked: false,
|
|
1041
|
-
dir: "",
|
|
1042
|
-
shared: false,
|
|
1043
|
-
mesh_shared: false,
|
|
1044
|
-
share_url: null,
|
|
1045
|
-
source: { kind: "bundled", ref: item.id },
|
|
1046
|
-
installed_at: null,
|
|
1047
|
-
sha256: null,
|
|
1048
|
-
installed_sha256: null,
|
|
1049
|
-
modified_since_install: false,
|
|
1050
|
-
file_count: null,
|
|
1051
|
-
size_bytes: null,
|
|
1052
|
-
categories: [],
|
|
1053
|
-
tasks: item.tasks || [],
|
|
1054
|
-
openwiki_hubs: [],
|
|
1055
|
-
cli: item.cli as any,
|
|
1056
|
-
has_generator: false,
|
|
1057
|
-
branding: item.branding
|
|
1058
|
-
}, "preview")
|
|
1059
|
-
}
|
|
1060
|
-
}}
|
|
1061
|
-
>
|
|
1062
|
-
Preview
|
|
1063
|
-
</button>
|
|
1064
|
-
|
|
1065
|
-
<div style={{ display: "flex", gap: "6px" }}>
|
|
1066
|
-
{!item.installed ? (
|
|
1067
|
-
<button
|
|
1068
|
-
type="button"
|
|
1069
|
-
className="btn btn-primary btn-sm"
|
|
1070
|
-
disabled={busy === item.id}
|
|
1071
|
-
onClick={() => doInstall(item.id)}
|
|
1072
|
-
>
|
|
1073
|
-
{busy === item.id ? "Installing…" : "Install"}
|
|
1074
|
-
</button>
|
|
1075
|
-
) : item.inWorkspace ? (
|
|
1076
|
-
<button
|
|
1077
|
-
type="button"
|
|
1078
|
-
className="btn btn-ghost btn-sm"
|
|
1079
|
-
disabled={busy === item.id}
|
|
1080
|
-
onClick={() => removeFromWorkspace(item.sourcePlugin || item)}
|
|
1081
|
-
style={{ fontSize: "11px" }}
|
|
1082
|
-
>
|
|
1083
|
-
Remove from Workspace
|
|
1084
|
-
</button>
|
|
1085
|
-
) : (
|
|
1086
|
-
<button
|
|
1087
|
-
type="button"
|
|
1088
|
-
className="btn btn-primary btn-sm"
|
|
1089
|
-
disabled={busy === item.id}
|
|
1090
|
-
onClick={() => addToWorkspace(item.sourcePlugin || item)}
|
|
1091
|
-
>
|
|
1092
|
-
{busy === item.id ? "Adding…" : "+ Add to Workspace"}
|
|
1093
|
-
</button>
|
|
1094
|
-
)}
|
|
1095
|
-
</div>
|
|
1096
|
-
</div>
|
|
1097
|
-
</div>
|
|
1098
|
-
)
|
|
1099
|
-
})}
|
|
1100
|
-
</div>
|
|
1101
|
-
)}
|
|
1102
|
-
</div>
|
|
1103
|
-
)
|
|
1104
|
-
)}
|
|
1105
|
-
|
|
1106
|
-
{tab === "mesh" && (
|
|
1107
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
1108
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: "12px", flexWrap: "wrap" }}>
|
|
1109
|
-
<p style={{ ...muted, margin: 0 }}>Asked live from each node. Only plugins their owner chose to share appear here; content is hash-checked on install.</p>
|
|
1110
|
-
<button className="btn btn-ghost btn-sm" onClick={loadPeers} disabled={peersLoading}>{peersLoading ? "Asking peers…" : "Refresh"}</button>
|
|
1111
|
-
</div>
|
|
1112
|
-
|
|
1113
|
-
{peersLoading && !peers ? <div style={muted}>Asking peers…</div> :
|
|
1114
|
-
peers === null ? <Empty title="Could not ask the mesh">The brain server did not answer the peer query.</Empty> :
|
|
1115
|
-
!peers.mesh.available ? (
|
|
1116
|
-
<Empty title="This node is not on a mesh">Enroll it with <code style={mono}>npx total-recall mesh enroll</code> to share plugins between your nodes.</Empty>
|
|
1117
|
-
) : peers.peers.length === 0 ? <Empty title="No other nodes on the mesh" /> : (
|
|
1118
|
-
<>
|
|
1119
|
-
{!peers.mesh.configured && (
|
|
1120
|
-
<div className="alert alert-warning">This node has no <code style={mono}>TR_MESH_SYNC_TOKEN</code>, so it cannot query peers. Set the same token on each node.</div>
|
|
1121
|
-
)}
|
|
1122
|
-
{peers.peers.map((node) => {
|
|
1123
|
-
const shown = node.plugins.filter(matches)
|
|
1124
|
-
return (
|
|
1125
|
-
<div key={node.hostname} className="card">
|
|
1126
|
-
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px", alignItems: "center", marginBottom: node.plugins.length ? "12px" : 0, flexWrap: "wrap" }}>
|
|
1127
|
-
<div>
|
|
1128
|
-
<span style={{ fontWeight: 600 }}>{node.hostname}</span>{" "}
|
|
1129
|
-
<span style={{ ...mono, color: "var(--text-tertiary)" }}>{node.ip}</span>
|
|
1130
|
-
</div>
|
|
1131
|
-
<span className={`badge ${node.status === "ok" ? "badge-online" : node.status === "offline" ? "badge-offline" : "badge-warning"}`}>
|
|
1132
|
-
{PEER_STATUS_LABEL[node.status]}{node.status === "ok" ? ` · ${node.plugins.length} shared` : ""}
|
|
1133
|
-
</span>
|
|
1134
|
-
</div>
|
|
1135
|
-
{node.error && node.status !== "ok" && <div style={{ ...muted, fontSize: "12px" }}>{node.error}</div>}
|
|
1136
|
-
{node.status === "ok" && node.plugins.length === 0 && <div style={muted}>Shares no plugins.</div>}
|
|
1137
|
-
{node.status === "ok" && node.plugins.length > 0 && shown.length === 0 && <div style={muted}>No shared plugins match the filter.</div>}
|
|
1138
|
-
{shown.map((p) => {
|
|
1139
|
-
const source = `peer:${node.hostname}/${p.id}`
|
|
1140
|
-
return (
|
|
1141
|
-
<div key={p.id} style={{ display: "flex", justifyContent: "space-between", gap: "16px", padding: "12px 0", borderTop: "1px solid var(--border)", flexWrap: "wrap" }}>
|
|
1142
|
-
<div style={{ minWidth: 0, flex: "1 1 320px", display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
1143
|
-
<div><span style={{ fontWeight: 600 }}>{p.name}</span> <span style={{ ...mono, color: "var(--text-tertiary)" }}>v{p.version}</span></div>
|
|
1144
|
-
<div style={muted}>{p.description}</div>
|
|
1145
|
-
<UseCaseChips useCases={p.use_cases} />
|
|
1146
|
-
<div style={{ ...mono, color: "var(--text-tertiary)" }} title={p.sha256}>
|
|
1147
|
-
sha256 {shortHash(p.sha256)} · {p.file_count} files · {formatBytes(p.size_bytes)}
|
|
1148
|
-
</div>
|
|
1149
|
-
</div>
|
|
1150
|
-
<div style={{ alignSelf: "center" }}>
|
|
1151
|
-
{p.installed
|
|
1152
|
-
? <span className={`badge ${p.same_as_installed ? "badge-success" : "badge-warning"}`}>
|
|
1153
|
-
{p.same_as_installed ? "installed (same content)" : "installed (different content)"}
|
|
1154
|
-
</span>
|
|
1155
|
-
: <button className="btn btn-primary btn-sm" disabled={busy === source} onClick={() => doInstall(source)}>
|
|
1156
|
-
{busy === source ? "Installing…" : "Install"}
|
|
1157
|
-
</button>}
|
|
1158
|
-
</div>
|
|
1159
|
-
</div>
|
|
1160
|
-
)
|
|
1161
|
-
})}
|
|
1162
|
-
</div>
|
|
1163
|
-
)
|
|
1164
|
-
})}
|
|
1165
|
-
</>
|
|
1166
|
-
)}
|
|
1167
|
-
</div>
|
|
1168
|
-
)}
|
|
1169
|
-
|
|
1170
|
-
{installOpen && (
|
|
1171
|
-
<div role="dialog" aria-modal="true" aria-label="Install plugin from source" onClick={() => setInstallOpen(false)}
|
|
1172
|
-
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: "16px" }}>
|
|
1173
|
-
<form className="card" onClick={(e) => e.stopPropagation()} style={{ width: "100%", maxWidth: "520px", display: "flex", flexDirection: "column", gap: "14px" }}
|
|
1174
|
-
onSubmit={async (e) => {
|
|
1175
|
-
e.preventDefault()
|
|
1176
|
-
if (!installSource.trim()) return
|
|
1177
|
-
if (await doInstall(installSource.trim(), { link: installLink, global: installGlobal })) {
|
|
1178
|
-
setInstallOpen(false)
|
|
1179
|
-
setInstallSource("")
|
|
1180
|
-
}
|
|
1181
|
-
}}>
|
|
1182
|
-
<div style={{ fontWeight: 600, fontSize: "16px" }}>Install from source</div>
|
|
1183
|
-
<div style={muted}>
|
|
1184
|
-
A bundled plugin id, <code style={mono}>peer:<host>/<id></code>, a git URL, or a folder on this machine containing <code style={mono}>plugin.json</code>.
|
|
1185
|
-
</div>
|
|
1186
|
-
<input className="input" autoFocus value={installSource} onChange={(e) => setInstallSource(e.target.value)}
|
|
1187
|
-
placeholder="https://github.com/you/my-plugin.git" aria-label="Plugin source" />
|
|
1188
|
-
<label style={{ ...muted, display: "flex", gap: "8px", alignItems: "center" }}>
|
|
1189
|
-
<input type="checkbox" checked={installLink} onChange={(e) => setInstallLink(e.target.checked)} />
|
|
1190
|
-
Link a local folder instead of copying it (for plugin development)
|
|
1191
|
-
</label>
|
|
1192
|
-
<label style={{ ...muted, display: "flex", gap: "8px", alignItems: "center" }}>
|
|
1193
|
-
<input type="checkbox" checked={installGlobal} onChange={(e) => setInstallGlobal(e.target.checked)} />
|
|
1194
|
-
Install for every project on this machine
|
|
1195
|
-
</label>
|
|
1196
|
-
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px" }}>
|
|
1197
|
-
<button type="button" className="btn btn-ghost" onClick={() => setInstallOpen(false)}>Cancel</button>
|
|
1198
|
-
<button type="submit" className="btn btn-primary" disabled={!installSource.trim() || busy !== null}>
|
|
1199
|
-
{busy ? "Installing…" : "Install"}
|
|
1200
|
-
</button>
|
|
1201
|
-
</div>
|
|
1202
|
-
</form>
|
|
1203
|
-
</div>
|
|
1204
|
-
)}
|
|
1205
|
-
|
|
1206
|
-
{/* Branding Customization Modal */}
|
|
1207
|
-
{brandingOpen && brandingPlugin && (
|
|
1208
|
-
<div role="dialog" aria-modal="true" aria-label="Customize Plugin Branding" onClick={() => setBrandingOpen(false)}
|
|
1209
|
-
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: "16px" }}>
|
|
1210
|
-
<div className="card" onClick={(e) => e.stopPropagation()} style={{ width: "100%", maxWidth: "480px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
1211
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
1212
|
-
<div style={{ fontWeight: 700, fontSize: "16px" }}>Customize Branding: {brandingPlugin.name}</div>
|
|
1213
|
-
<button className="btn btn-ghost btn-sm" onClick={() => setBrandingOpen(false)}>✕</button>
|
|
1214
|
-
</div>
|
|
1215
|
-
|
|
1216
|
-
{brandingPlugin.locked && (
|
|
1217
|
-
<div className="alert alert-warning" style={{ fontSize: "12px" }}>
|
|
1218
|
-
[LOCKED] This plugin is marked complete and locked against editing. Unlock it to change branding.
|
|
1219
|
-
</div>
|
|
1220
|
-
)}
|
|
1221
|
-
|
|
1222
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
1223
|
-
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Wordmark Glyph / Tag</label>
|
|
1224
|
-
<div style={{ display: "flex", gap: "8px" }}>
|
|
1225
|
-
<input
|
|
1226
|
-
className="input"
|
|
1227
|
-
value={brandingIcon}
|
|
1228
|
-
onChange={(e) => setBrandingIcon(e.target.value)}
|
|
1229
|
-
style={{ width: "90px", textAlign: "center", fontSize: "14px", fontFamily: "var(--font-mono)" }}
|
|
1230
|
-
disabled={brandingPlugin.locked}
|
|
1231
|
-
/>
|
|
1232
|
-
<div style={{ display: "flex", gap: "4px", flexWrap: "wrap", alignItems: "center" }}>
|
|
1233
|
-
{["[TEL]", "[DNS]", "[SIGN]", "[GATE]", "[CLI]", "[LAW]", "[UI]", "[SMS]", "[FIND]", "[GIT]", "[OPS]", "[WARN]", "[CORE]"].map((glyph) => (
|
|
1234
|
-
<button
|
|
1235
|
-
key={glyph}
|
|
1236
|
-
type="button"
|
|
1237
|
-
className="btn btn-ghost btn-sm"
|
|
1238
|
-
style={{ padding: "4px 6px", fontSize: "11px", fontFamily: "var(--font-mono)" }}
|
|
1239
|
-
disabled={brandingPlugin.locked}
|
|
1240
|
-
onClick={() => setBrandingIcon(glyph)}
|
|
1241
|
-
>
|
|
1242
|
-
{glyph}
|
|
1243
|
-
</button>
|
|
1244
|
-
))}
|
|
1245
|
-
</div>
|
|
1246
|
-
</div>
|
|
1247
|
-
</div>
|
|
1248
|
-
|
|
1249
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
1250
|
-
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Theme Color (Hex)</label>
|
|
1251
|
-
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
|
|
1252
|
-
<input
|
|
1253
|
-
type="color"
|
|
1254
|
-
value={brandingColor}
|
|
1255
|
-
onChange={(e) => setBrandingColor(e.target.value)}
|
|
1256
|
-
style={{ width: "42px", height: "38px", border: "none", cursor: "pointer", background: "none" }}
|
|
1257
|
-
disabled={brandingPlugin.locked}
|
|
1258
|
-
/>
|
|
1259
|
-
<input
|
|
1260
|
-
className="input"
|
|
1261
|
-
value={brandingColor}
|
|
1262
|
-
onChange={(e) => setBrandingColor(e.target.value)}
|
|
1263
|
-
style={{ flex: 1, fontFamily: "var(--font-mono)" }}
|
|
1264
|
-
disabled={brandingPlugin.locked}
|
|
1265
|
-
/>
|
|
1266
|
-
</div>
|
|
1267
|
-
</div>
|
|
1268
|
-
|
|
1269
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
1270
|
-
<label style={{ fontSize: "12px", color: "var(--text-secondary)" }}>Domain Badge Text</label>
|
|
1271
|
-
<input
|
|
1272
|
-
className="input"
|
|
1273
|
-
value={brandingBadge}
|
|
1274
|
-
onChange={(e) => setBrandingBadge(e.target.value)}
|
|
1275
|
-
placeholder="e.g. Telephony, Quality, DNS"
|
|
1276
|
-
disabled={brandingPlugin.locked}
|
|
1277
|
-
/>
|
|
1278
|
-
</div>
|
|
1279
|
-
|
|
1280
|
-
<div style={{ display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "8px" }}>
|
|
1281
|
-
<button type="button" className="btn btn-ghost" onClick={() => setBrandingOpen(false)}>Cancel</button>
|
|
1282
|
-
<button
|
|
1283
|
-
type="button"
|
|
1284
|
-
className="btn btn-primary"
|
|
1285
|
-
onClick={saveBranding}
|
|
1286
|
-
disabled={brandingPlugin.locked || busy !== null}
|
|
1287
|
-
>
|
|
1288
|
-
Save Branding
|
|
1289
|
-
</button>
|
|
1290
|
-
</div>
|
|
1291
|
-
</div>
|
|
1292
|
-
</div>
|
|
1293
|
-
)}
|
|
1294
|
-
|
|
1295
|
-
{/* Agent Plugin Recipe Modal */}
|
|
1296
|
-
{recipeOpen && (
|
|
1297
|
-
<div
|
|
1298
|
-
role="dialog"
|
|
1299
|
-
aria-modal="true"
|
|
1300
|
-
aria-label="Agent Plugin Recipe"
|
|
1301
|
-
onClick={() => setRecipeOpen(false)}
|
|
1302
|
-
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.65)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: "16px" }}
|
|
1303
|
-
>
|
|
1304
|
-
<div
|
|
1305
|
-
className="card"
|
|
1306
|
-
onClick={(e) => e.stopPropagation()}
|
|
1307
|
-
style={{ width: "100%", maxWidth: "680px", maxHeight: "90vh", overflowY: "auto", display: "flex", flexDirection: "column", gap: "16px" }}
|
|
1308
|
-
>
|
|
1309
|
-
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
1310
|
-
<div>
|
|
1311
|
-
<h3 style={{ margin: 0, fontSize: "17px", fontWeight: 700 }}>Agent Plugin Recipe</h3>
|
|
1312
|
-
<div style={{ ...muted, fontSize: "12px" }}>How your coding agent authors private or store plugins</div>
|
|
1313
|
-
</div>
|
|
1314
|
-
<button className="btn btn-ghost btn-sm" onClick={() => setRecipeOpen(false)}>✕</button>
|
|
1315
|
-
</div>
|
|
1316
|
-
|
|
1317
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "12px", fontSize: "13px" }}>
|
|
1318
|
-
<div style={{ background: "var(--bg-tertiary)", padding: "12px", borderRadius: "8px", border: "1px solid var(--border)" }}>
|
|
1319
|
-
<div style={{ fontWeight: 600, marginBottom: "4px" }}>1. Command your agent</div>
|
|
1320
|
-
<div style={muted}>
|
|
1321
|
-
Copy and send this instruction to your coding agent (Antigravity, Claude, Codex, DSH):
|
|
1322
|
-
</div>
|
|
1323
|
-
<pre style={{ ...mono, background: "rgba(0,0,0,0.4)", padding: "10px", borderRadius: "6px", marginTop: "8px", whiteSpace: "pre-wrap", overflowX: "auto", fontSize: "11px", color: "#a5b4fc" }}>
|
|
1324
|
-
{AGENT_RECIPE_PROMPT}
|
|
1325
|
-
</pre>
|
|
1326
|
-
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "8px" }}>
|
|
1327
|
-
<button type="button" className="btn btn-primary btn-sm" onClick={() => copyAgentPrompt()}>
|
|
1328
|
-
Copy Agent Prompt
|
|
1329
|
-
</button>
|
|
1330
|
-
</div>
|
|
1331
|
-
</div>
|
|
1332
|
-
|
|
1333
|
-
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "12px" }}>
|
|
1334
|
-
<div style={{ background: "var(--bg-tertiary)", padding: "12px", borderRadius: "8px", border: "1px solid var(--border)" }}>
|
|
1335
|
-
<div style={{ fontWeight: 600, color: "#38bdf8", marginBottom: "4px" }}>Private Plugins</div>
|
|
1336
|
-
<div style={{ ...muted, fontSize: "12px", lineHeight: 1.4 }}>
|
|
1337
|
-
Store directly inside <code style={mono}>.agent/skills/total-recall/plugins/<id></code> or link locally. Never shared outside this machine or project. Zero cloud exposure.
|
|
1338
|
-
</div>
|
|
1339
|
-
</div>
|
|
1340
|
-
|
|
1341
|
-
<div style={{ background: "var(--bg-tertiary)", padding: "12px", borderRadius: "8px", border: "1px solid var(--border)" }}>
|
|
1342
|
-
<div style={{ fontWeight: 600, color: "#a855f7", marginBottom: "4px" }}>Plugin Store Distribution</div>
|
|
1343
|
-
<div style={{ ...muted, fontSize: "12px", lineHeight: 1.4 }}>
|
|
1344
|
-
Deploy to the store via <code style={mono}>total-recall deploy-store <id></code>. Cryptographically signed and distributed across the mesh for team consumption.
|
|
1345
|
-
</div>
|
|
1346
|
-
</div>
|
|
1347
|
-
</div>
|
|
1348
|
-
|
|
1349
|
-
<div style={{ background: "rgba(16, 185, 129, 0.08)", padding: "12px", borderRadius: "8px", border: "1px solid rgba(16, 185, 129, 0.2)" }}>
|
|
1350
|
-
<div style={{ fontWeight: 600, color: "#10b981", marginBottom: "4px" }}>Quality Invariants Enforced</div>
|
|
1351
|
-
<ul style={{ margin: "4px 0 0 16px", padding: 0, fontSize: "12px", color: "var(--text-secondary)" }}>
|
|
1352
|
-
<li>Guaranteed Title, Brand Glyph, Version, and Settings drawer trigger on every card</li>
|
|
1353
|
-
<li>Authentic domain tools only — zero fake audio equalizers or fake skins</li>
|
|
1354
|
-
<li>Touch-first ergonomics with minimum 44px tap targets</li>
|
|
1355
|
-
</ul>
|
|
1356
|
-
</div>
|
|
1357
|
-
</div>
|
|
1358
|
-
|
|
1359
|
-
<div style={{ display: "flex", justifyContent: "flex-end", marginTop: "8px" }}>
|
|
1360
|
-
<button type="button" className="btn btn-ghost" onClick={() => setRecipeOpen(false)}>Close</button>
|
|
1361
|
-
</div>
|
|
1362
|
-
</div>
|
|
1363
|
-
</div>
|
|
1364
|
-
)}
|
|
1365
|
-
|
|
1366
|
-
{/* Plugin Detail & Interactive Preview Drawer */}
|
|
1367
|
-
{detail && (
|
|
1368
|
-
<div role="dialog" aria-modal="true" aria-label={detail.name} onClick={closeDetail}
|
|
1369
|
-
style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.6)", display: "flex", justifyContent: "flex-end", zIndex: 1000 }}>
|
|
1370
|
-
<div onClick={(e) => e.stopPropagation()}
|
|
1371
|
-
style={{ width: "100%", maxWidth: "680px", height: "100%", overflowY: "auto", background: "var(--bg-secondary)", borderLeft: "1px solid var(--border)", padding: "24px", display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
1372
|
-
|
|
1373
|
-
{/* Drawer Header */}
|
|
1374
|
-
{(() => {
|
|
1375
|
-
const branding = resolvePluginBranding(detail)
|
|
1376
|
-
const hasPanel = pluginElements(detail).length > 0
|
|
1377
|
-
return (
|
|
1378
|
-
<>
|
|
1379
|
-
<div style={{ display: "flex", justifyContent: "space-between", gap: "12px" }}>
|
|
1380
|
-
<div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
|
|
1381
|
-
<div
|
|
1382
|
-
style={{
|
|
1383
|
-
width: "44px",
|
|
1384
|
-
height: "44px",
|
|
1385
|
-
borderRadius: "10px",
|
|
1386
|
-
background: `${branding.color}20`,
|
|
1387
|
-
border: `1px solid ${branding.color}50`,
|
|
1388
|
-
display: "flex",
|
|
1389
|
-
alignItems: "center",
|
|
1390
|
-
justifyContent: "center",
|
|
1391
|
-
fontSize: "22px",
|
|
1392
|
-
flexShrink: 0
|
|
1393
|
-
}}
|
|
1394
|
-
>
|
|
1395
|
-
{branding.icon}
|
|
1396
|
-
</div>
|
|
1397
|
-
<div>
|
|
1398
|
-
<div style={{ fontSize: "18px", fontWeight: 700 }}>{detail.name}</div>
|
|
1399
|
-
<div style={{ display: "flex", gap: "8px", alignItems: "center", marginTop: "2px" }}>
|
|
1400
|
-
<span style={{ ...mono, color: "var(--text-tertiary)" }}>{detail.id} · v{detail.version}</span>
|
|
1401
|
-
<span className="badge" style={{ background: `${branding.color}15`, color: branding.color, borderColor: `${branding.color}40`, fontSize: "11px" }}>
|
|
1402
|
-
{branding.badge}
|
|
1403
|
-
</span>
|
|
1404
|
-
{detail.locked && <span className="badge" style={{ background: "#2e1065", color: "#c084fc" }}>Locked</span>}
|
|
1405
|
-
</div>
|
|
1406
|
-
</div>
|
|
1407
|
-
</div>
|
|
1408
|
-
<button className="btn btn-ghost btn-sm" onClick={closeDetail} aria-label="Close">Close</button>
|
|
1409
|
-
</div>
|
|
1410
|
-
|
|
1411
|
-
<div style={muted}>{detail.description}</div>
|
|
1412
|
-
|
|
1413
|
-
{!detail.valid && (
|
|
1414
|
-
<div className="alert alert-error">
|
|
1415
|
-
{detail.errors.map((e) => <div key={e}>{e}</div>)}
|
|
1416
|
-
</div>
|
|
1417
|
-
)}
|
|
1418
|
-
|
|
1419
|
-
{/* Drawer Navigation Tabs */}
|
|
1420
|
-
<div role="tablist" style={{ display: "flex", gap: "8px", borderBottom: "1px solid var(--border)", paddingBottom: "10px" }}>
|
|
1421
|
-
{hasPanel && (
|
|
1422
|
-
<button
|
|
1423
|
-
role="tab"
|
|
1424
|
-
aria-selected={detailTab === "preview"}
|
|
1425
|
-
className={`btn btn-sm ${detailTab === "preview" ? "btn-primary" : "btn-ghost"}`}
|
|
1426
|
-
onClick={() => setDetailTab("preview")}
|
|
1427
|
-
>
|
|
1428
|
-
Open {detail.name}
|
|
1429
|
-
</button>
|
|
1430
|
-
)}
|
|
1431
|
-
{detail.cli && (
|
|
1432
|
-
<button
|
|
1433
|
-
role="tab"
|
|
1434
|
-
aria-selected={detailTab === "run"}
|
|
1435
|
-
className={`btn btn-sm ${detailTab === "run" ? "btn-primary" : "btn-ghost"}`}
|
|
1436
|
-
onClick={() => setDetailTab("run")}
|
|
1437
|
-
>
|
|
1438
|
-
Run CLI
|
|
1439
|
-
</button>
|
|
1440
|
-
)}
|
|
1441
|
-
<button
|
|
1442
|
-
role="tab"
|
|
1443
|
-
aria-selected={detailTab === "details"}
|
|
1444
|
-
className={`btn btn-sm ${detailTab === "details" ? "btn-primary" : "btn-ghost"}`}
|
|
1445
|
-
onClick={() => setDetailTab("details")}
|
|
1446
|
-
>
|
|
1447
|
-
Settings
|
|
1448
|
-
</button>
|
|
1449
|
-
<button
|
|
1450
|
-
role="tab"
|
|
1451
|
-
aria-selected={detailTab === "readme"}
|
|
1452
|
-
className={`btn btn-sm ${detailTab === "readme" ? "btn-primary" : "btn-ghost"}`}
|
|
1453
|
-
onClick={() => setDetailTab("readme")}
|
|
1454
|
-
>
|
|
1455
|
-
README
|
|
1456
|
-
</button>
|
|
1457
|
-
</div>
|
|
1458
|
-
|
|
1459
|
-
{/* Plugin-owned interface */}
|
|
1460
|
-
{detailTab === "preview" && (
|
|
1461
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
|
|
1462
|
-
<PluginPanel plugin={detail} />
|
|
1463
|
-
</div>
|
|
1464
|
-
)}
|
|
1465
|
-
|
|
1466
|
-
{/* Tab 2: CLI Command Runner */}
|
|
1467
|
-
{detailTab === "run" && detail.cli && (
|
|
1468
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
|
1469
|
-
<div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}>
|
|
1470
|
-
<select className="select" value={subcommand} onChange={(e) => setSubcommand(e.target.value)} aria-label="Subcommand" style={{ paddingRight: "32px" }}>
|
|
1471
|
-
{(detail.cli.subcommands.length ? detail.cli.subcommands : [{ name: "" }]).map((s) => (
|
|
1472
|
-
<option key={s.name} value={s.name}>{s.name || "(default)"}</option>
|
|
1473
|
-
))}
|
|
1474
|
-
</select>
|
|
1475
|
-
<input className="input" value={args} onChange={(e) => setArgs(e.target.value)} placeholder="extra arguments" aria-label="Arguments" style={{ flex: 1, minWidth: "160px" }} />
|
|
1476
|
-
<button className="btn btn-primary" onClick={runCommand} disabled={busy === `run:${detail.id}` || !detail.valid}>
|
|
1477
|
-
{busy === `run:${detail.id}` ? "Running…" : "Run"}
|
|
1478
|
-
</button>
|
|
1479
|
-
</div>
|
|
1480
|
-
<div style={{ ...muted, fontSize: "12px" }}>
|
|
1481
|
-
{detail.cli.subcommands.find((s) => s.name === subcommand)?.description}
|
|
1482
|
-
{" "}Same as <code style={mono}>npx total-recall {detail.cli.command} {subcommand} {args}</code>. Runs in a separate process.
|
|
1483
|
-
</div>
|
|
1484
|
-
{output && (
|
|
1485
|
-
<pre style={{ ...mono, margin: 0, padding: "14px", borderRadius: "var(--radius-sm)", background: "var(--bg-primary)", border: `1px solid ${output.ok ? "var(--border)" : "var(--error)"}`, whiteSpace: "pre-wrap", maxHeight: "420px", overflow: "auto" }}>
|
|
1486
|
-
{output.text}
|
|
1487
|
-
</pre>
|
|
1488
|
-
)}
|
|
1489
|
-
</div>
|
|
1490
|
-
)}
|
|
1491
|
-
|
|
1492
|
-
{/* Tab 3: Plugin Facts & Config Controls */}
|
|
1493
|
-
{detailTab === "details" && (
|
|
1494
|
-
<div>
|
|
1495
|
-
<PluginSettings plugin={detail} />
|
|
1496
|
-
<Fact label="Scope">{detail.scope}{detail.linked ? " (linked folder)" : ""}</Fact>
|
|
1497
|
-
<Fact label="Source">{sourceLabel(detail)} — <span style={mono}>{detail.source.ref}</span></Fact>
|
|
1498
|
-
<Fact label="Installed">{detail.installed_at ? new Date(detail.installed_at).toLocaleString() : "no install record"}</Fact>
|
|
1499
|
-
<Fact label="Content sha256"><span style={mono}>{detail.sha256 || "—"}</span></Fact>
|
|
1500
|
-
{detail.modified_since_install && <Fact label="At install"><span style={mono}>{detail.installed_sha256}</span></Fact>}
|
|
1501
|
-
<Fact label="Files">{detail.file_count ?? "—"} · {formatBytes(detail.size_bytes)}</Fact>
|
|
1502
|
-
<Fact label="Shared">{detail.shared ? "yes" : "no"}</Fact>
|
|
1503
|
-
<Fact label="Lock Status">{detail.locked ? "Locked (Completed)" : "Unlocked (Editable)"}</Fact>
|
|
1504
|
-
<Fact label="Store Status">{detail.store_deployed ? "Published in Plugin Store" : "Not yet deployed to store"}</Fact>
|
|
1505
|
-
{detail.shared && <Fact label="Public link">{detail.share_url ? <input className="input" aria-label="Public plugin share link" readOnly value={detail.share_url} onFocus={(e) => e.currentTarget.select()} /> : "Set TR_PUBLIC_BASE_URL to your public HTTPS origin to create a link."}</Fact>}
|
|
1506
|
-
<Fact label="Use cases">{detail.use_cases.join(", ") || "—"}</Fact>
|
|
1507
|
-
{detail.author && <Fact label="Author">{detail.author}</Fact>}
|
|
1508
|
-
{detail.license && <Fact label="License">{detail.license}</Fact>}
|
|
1509
|
-
<Fact label="Location"><span style={mono}>{detail.dir}</span></Fact>
|
|
1510
|
-
<Fact label="Agent context">{detail.has_generator ? "adds a block to compiled instructions" : "—"}</Fact>
|
|
1511
|
-
<Fact label="Memory categories">
|
|
1512
|
-
{detail.categories.length ? detail.categories.map((c) => c.name).join(", ") : "—"}
|
|
1513
|
-
</Fact>
|
|
1514
|
-
<Fact label="Scheduled tasks">
|
|
1515
|
-
{detail.tasks.length === 0 ? "—" : (
|
|
1516
|
-
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
|
|
1517
|
-
{detail.tasks.map((t) => (
|
|
1518
|
-
<div key={t.command}>
|
|
1519
|
-
<span style={mono}>{t.schedule}</span> {t.command} — {t.intent}
|
|
1520
|
-
<div style={{ color: "var(--text-tertiary)", fontSize: "12px" }}>last run: {t.last_run || "not yet"}</div>
|
|
1521
|
-
</div>
|
|
1522
|
-
))}
|
|
1523
|
-
</div>
|
|
1524
|
-
)}
|
|
1525
|
-
</Fact>
|
|
1526
|
-
|
|
1527
|
-
{/* Config Actions Toolbar inside drawer */}
|
|
1528
|
-
<div style={{ display: "flex", gap: "8px", marginTop: "16px", flexWrap: "wrap" }}>
|
|
1529
|
-
<button
|
|
1530
|
-
className="btn btn-ghost btn-sm"
|
|
1531
|
-
onClick={() => openBrandingModal(detail)}
|
|
1532
|
-
disabled={detail.locked}
|
|
1533
|
-
>
|
|
1534
|
-
Customize Branding
|
|
1535
|
-
</button>
|
|
1536
|
-
<button
|
|
1537
|
-
className="btn btn-ghost btn-sm"
|
|
1538
|
-
onClick={() => doToggleLock(detail)}
|
|
1539
|
-
disabled={busy === detail.id}
|
|
1540
|
-
>
|
|
1541
|
-
{detail.locked ? "Unlock Editing" : "Lock When Complete"}
|
|
1542
|
-
</button>
|
|
1543
|
-
<button
|
|
1544
|
-
className="btn btn-primary btn-sm"
|
|
1545
|
-
onClick={() => doDeployStore(detail)}
|
|
1546
|
-
disabled={busy === detail.id || !detail.valid}
|
|
1547
|
-
>
|
|
1548
|
-
{detail.store_deployed ? "✓ Deployed to Store" : "Deploy to Plugin Store"}
|
|
1549
|
-
</button>
|
|
1550
|
-
<button
|
|
1551
|
-
className="btn btn-ghost btn-sm"
|
|
1552
|
-
disabled={busy === detail.id || (!detail.valid && !detail.shared)}
|
|
1553
|
-
onClick={() => doShare(detail, !detail.shared)}
|
|
1554
|
-
>
|
|
1555
|
-
{detail.shared ? "Stop sharing" : "Share"}
|
|
1556
|
-
</button>
|
|
1557
|
-
<button
|
|
1558
|
-
className="btn btn-ghost btn-sm"
|
|
1559
|
-
disabled={busy === detail.id}
|
|
1560
|
-
onClick={() => doRemove(detail)}
|
|
1561
|
-
>
|
|
1562
|
-
Remove
|
|
1563
|
-
</button>
|
|
1564
|
-
</div>
|
|
1565
|
-
</div>
|
|
1566
|
-
)}
|
|
1567
|
-
|
|
1568
|
-
{/* Tab 4: Markdown Readme */}
|
|
1569
|
-
{detailTab === "readme" && (
|
|
1570
|
-
<div style={{ fontSize: "14px", lineHeight: 1.6 }}>{readme ? renderMarkdown(readme) : <span style={muted}>Loading…</span>}</div>
|
|
1571
|
-
)}
|
|
1572
|
-
</>
|
|
1573
|
-
)
|
|
1574
|
-
})()}
|
|
1575
|
-
</div>
|
|
1576
|
-
</div>
|
|
1577
|
-
)}
|
|
1578
|
-
|
|
1579
|
-
{/* Floating Touch & Right-Click Context Menu */}
|
|
1580
|
-
{contextMenu && (
|
|
1581
|
-
<div
|
|
1582
|
-
role="menu"
|
|
1583
|
-
aria-label="Plugin actions menu"
|
|
1584
|
-
onClick={(e) => e.stopPropagation()}
|
|
1585
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
1586
|
-
style={{
|
|
1587
|
-
position: "fixed",
|
|
1588
|
-
left: Math.max(12, Math.min(contextMenu.x, window.innerWidth - 264)),
|
|
1589
|
-
top: Math.max(12, Math.min(contextMenu.y, window.innerHeight - 380)),
|
|
1590
|
-
zIndex: 2000,
|
|
1591
|
-
width: "250px",
|
|
1592
|
-
background: "var(--bg-secondary)",
|
|
1593
|
-
border: "1px solid var(--border)",
|
|
1594
|
-
borderRadius: "10px",
|
|
1595
|
-
boxShadow: "0 12px 36px rgba(0,0,0,0.55), 0 0 0 1px rgba(255,255,255,0.08)",
|
|
1596
|
-
padding: "6px",
|
|
1597
|
-
display: "flex",
|
|
1598
|
-
flexDirection: "column",
|
|
1599
|
-
gap: "2px",
|
|
1600
|
-
backdropFilter: "blur(12px)"
|
|
1601
|
-
}}
|
|
1602
|
-
>
|
|
1603
|
-
<div style={{ padding: "8px 10px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", gap: "8px" }}>
|
|
1604
|
-
<div style={{ width: "8px", height: "8px", borderRadius: "50%", background: resolvePluginBranding(contextMenu.plugin).color }} />
|
|
1605
|
-
<div style={{ minWidth: 0 }}>
|
|
1606
|
-
<div style={{ fontWeight: 600, fontSize: "13px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
|
1607
|
-
{contextMenu.plugin.name}
|
|
1608
|
-
</div>
|
|
1609
|
-
<div style={{ ...mono, fontSize: "10px", color: "var(--text-tertiary)" }}>
|
|
1610
|
-
{contextMenu.plugin.id} · v{contextMenu.plugin.version}
|
|
1611
|
-
</div>
|
|
1612
|
-
</div>
|
|
1613
|
-
</div>
|
|
1614
|
-
|
|
1615
|
-
<button
|
|
1616
|
-
className="btn btn-ghost btn-sm"
|
|
1617
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1618
|
-
onClick={() => {
|
|
1619
|
-
const p = contextMenu.plugin
|
|
1620
|
-
setContextMenu(null)
|
|
1621
|
-
openDetail(p, "preview")
|
|
1622
|
-
}}
|
|
1623
|
-
>
|
|
1624
|
-
Open UI Component
|
|
1625
|
-
</button>
|
|
1626
|
-
|
|
1627
|
-
<button
|
|
1628
|
-
className="btn btn-ghost btn-sm"
|
|
1629
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1630
|
-
onClick={() => {
|
|
1631
|
-
const p = contextMenu.plugin
|
|
1632
|
-
setContextMenu(null)
|
|
1633
|
-
openDetail(p, "run")
|
|
1634
|
-
}}
|
|
1635
|
-
>
|
|
1636
|
-
Run CLI ({contextMenu.plugin.cli?.command || contextMenu.plugin.id})
|
|
1637
|
-
</button>
|
|
1638
|
-
|
|
1639
|
-
<button
|
|
1640
|
-
className="btn btn-ghost btn-sm"
|
|
1641
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1642
|
-
onClick={() => {
|
|
1643
|
-
const p = contextMenu.plugin
|
|
1644
|
-
setContextMenu(null)
|
|
1645
|
-
openDetail(p, "details")
|
|
1646
|
-
}}
|
|
1647
|
-
>
|
|
1648
|
-
Configure & Settings
|
|
1649
|
-
</button>
|
|
1650
|
-
|
|
1651
|
-
<button
|
|
1652
|
-
className="btn btn-ghost btn-sm"
|
|
1653
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1654
|
-
onClick={() => {
|
|
1655
|
-
const p = contextMenu.plugin
|
|
1656
|
-
setContextMenu(null)
|
|
1657
|
-
openDetail(p, "readme")
|
|
1658
|
-
}}
|
|
1659
|
-
>
|
|
1660
|
-
View Documentation
|
|
1661
|
-
</button>
|
|
1662
|
-
|
|
1663
|
-
<div style={{ height: "1px", background: "var(--border)", margin: "4px 0" }} />
|
|
1664
|
-
|
|
1665
|
-
<button
|
|
1666
|
-
className="btn btn-ghost btn-sm"
|
|
1667
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1668
|
-
onClick={() => {
|
|
1669
|
-
const p = contextMenu.plugin
|
|
1670
|
-
setContextMenu(null)
|
|
1671
|
-
doShare(p, !p.shared)
|
|
1672
|
-
}}
|
|
1673
|
-
>
|
|
1674
|
-
{contextMenu.plugin.shared ? "Stop sharing on mesh" : "Share on mesh & link"}
|
|
1675
|
-
</button>
|
|
1676
|
-
|
|
1677
|
-
<button
|
|
1678
|
-
className="btn btn-ghost btn-sm"
|
|
1679
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1680
|
-
onClick={() => {
|
|
1681
|
-
const p = contextMenu.plugin
|
|
1682
|
-
setContextMenu(null)
|
|
1683
|
-
doToggleLock(p)
|
|
1684
|
-
}}
|
|
1685
|
-
>
|
|
1686
|
-
{contextMenu.plugin.locked ? "Unlock editing" : "Lock for release"}
|
|
1687
|
-
</button>
|
|
1688
|
-
|
|
1689
|
-
<button
|
|
1690
|
-
className="btn btn-ghost btn-sm"
|
|
1691
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", minHeight: "36px" }}
|
|
1692
|
-
onClick={() => {
|
|
1693
|
-
const p = contextMenu.plugin
|
|
1694
|
-
setContextMenu(null)
|
|
1695
|
-
void navigator.clipboard?.writeText(`npx total-recall ${p.cli?.command || p.id}`)
|
|
1696
|
-
setAlert({ type: "success", message: `Copied "npx total-recall ${p.cli?.command || p.id}" to clipboard` })
|
|
1697
|
-
}}
|
|
1698
|
-
>
|
|
1699
|
-
Copy CLI command
|
|
1700
|
-
</button>
|
|
1701
|
-
|
|
1702
|
-
<div style={{ height: "1px", background: "var(--border)", margin: "4px 0" }} />
|
|
1703
|
-
|
|
1704
|
-
<button
|
|
1705
|
-
className="btn btn-ghost btn-sm"
|
|
1706
|
-
style={{ justifyContent: "flex-start", width: "100%", padding: "8px 10px", fontSize: "12px", color: "var(--color-error, #f87171)", minHeight: "36px" }}
|
|
1707
|
-
onClick={() => {
|
|
1708
|
-
const p = contextMenu.plugin
|
|
1709
|
-
setContextMenu(null)
|
|
1710
|
-
doRemove(p)
|
|
1711
|
-
}}
|
|
1712
|
-
>
|
|
1713
|
-
Uninstall plugin
|
|
1714
|
-
</button>
|
|
1715
|
-
</div>
|
|
1716
|
-
)}
|
|
1717
|
-
</div>
|
|
1718
|
-
)
|
|
1719
|
-
}
|