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 +0,0 @@
|
|
|
1
|
-
:root{--bg-primary:#070b14;--bg-secondary:#0c1220;--bg-tertiary:#121a2b;--bg-elevated:#172033;--bg-hover:#1c2740;--bg-active:#243352;--accent:#3b82f6;--accent-deep:#1e3a8a;--accent-glow:#3b82f647;--accent-hover:#60a5fa;--accent-muted:#3b82f624;--accent-soft:#93c5fd1f;--purple-primary:var(--accent);--purple-border:var(--border-accent);--success:#34d399;--success-muted:#34d3991f;--warning:#fbbf24;--warning-muted:#fbbf241f;--error:#f87171;--error-muted:#f871711f;--text-primary:#f1f5f9;--text-secondary:#94a3b8;--text-tertiary:#64748b;--text-inverse:#070b14;--border:#94a3b81a;--border-hover:#94a3b838;--border-accent:#3b82f659;--glass:#0c1220b8;--glass-border:#94a3b81f;--mesh-1:#1e3a8a59;--mesh-2:#3b82f61f;--mesh-3:#0f172ae6;--font-heading:"Space Grotesk", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-sans:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", "SF Mono", ui-monospace, monospace;--sidebar-width:252px;--radius-sm:8px;--radius-md:12px;--radius-lg:18px;--radius-xl:24px;--shadow-sm:0 1px 2px #00000059;--shadow-md:0 8px 24px #00000059;--shadow-lg:0 24px 64px #00000073;--shadow-glow:0 0 40px #3b82f62e;--transition-fast:.14s cubic-bezier(.23, 1, .32, 1);--transition-normal:.22s cubic-bezier(.23, 1, .32, 1)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{background:var(--bg-primary);height:100%;color:var(--text-primary);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;font-size:14px;line-height:1.6}body{background:radial-gradient(1200px 600px at 10% -10%, var(--mesh-1), transparent 55%), radial-gradient(900px 500px at 100% 0%, var(--mesh-2), transparent 50%), radial-gradient(800px 400px at 50% 100%, #1e3a8a33, transparent 60%), var(--bg-primary)}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover)}h1,h2,h3,h4,h5,h6{font-family:var(--font-heading);letter-spacing:-.025em;color:var(--text-primary);font-weight:700}button{font-family:var(--font-sans);cursor:pointer;border:none;outline:none}code,pre{font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--bg-hover);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-tertiary)}.app-layout{height:100vh;display:flex;position:relative;overflow:hidden}.sidebar{width:var(--sidebar-width);min-width:var(--sidebar-width);background:var(--glass);-webkit-backdrop-filter:blur(20px)saturate(1.4);border-right:1px solid var(--glass-border);transition:transform var(--transition-normal);z-index:100;flex-direction:column;padding:14px 0 12px;display:flex;box-shadow:4px 0 40px #00000040}.sidebar-brand{border-bottom:1px solid var(--border);flex-direction:column;gap:10px;margin-bottom:10px;padding:4px 16px 18px;display:flex}.sidebar-brand-row{align-items:center;gap:12px;display:flex}.sidebar-brand h1{color:var(--text-primary);letter-spacing:-.03em;font-size:15px;font-weight:700;line-height:1.2}.sidebar-brand span{color:var(--text-tertiary);letter-spacing:.02em;font-size:11px;font-weight:500}.sidebar-brand-lockup{background:linear-gradient(#f8fafc 0%,#e8eef9 100%);border-radius:12px;width:100%;padding:10px 12px;overflow:hidden;box-shadow:inset 0 1px #ffffffb3,0 8px 20px #0f172a59,0 0 0 1px #94a3b833}.sidebar-brand-lockup img{object-fit:contain;width:100%;height:auto;display:block}.sidebar-nav{scrollbar-width:thin;flex-direction:column;flex:1;gap:2px;padding:0 10px;display:flex;overflow-y:auto}.nav-section-label{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);opacity:.85;padding:12px 12px 6px;font-size:10px;font-weight:600}.nav-link{color:var(--text-secondary);transition:all var(--transition-fast);border:1px solid #0000;border-radius:10px;align-items:center;gap:10px;padding:9px 12px;font-size:13px;font-weight:500;text-decoration:none;display:flex;position:relative}.nav-link:hover{color:var(--text-primary);background:#94a3b814}.nav-link.active{color:#dbeafe;border-color:var(--border-accent);background:linear-gradient(135deg,#3b82f62e,#1e3a8a38);box-shadow:0 0 0 1px #3b82f614,inset 0 1px #ffffff0a}.nav-link.active:before{content:"";background:linear-gradient(#60a5fa,#2563eb);border-radius:0 3px 3px 0;width:3px;position:absolute;top:20%;bottom:20%;left:0;box-shadow:0 0 12px #3b82f699}.nav-link svg{opacity:.72;flex-shrink:0;width:18px;height:18px}.nav-link.active svg{opacity:1;color:#93c5fd}.nav-link:hover svg{opacity:.95}.sidebar-footer{border-top:1px solid var(--border);background:linear-gradient(#0000,#0f172a66);margin-top:8px;padding:12px 14px}.main-content{background:0 0;flex-direction:column;flex:1;min-width:0;display:flex;overflow:hidden}.page{flex:1;padding:28px 32px 40px;overflow-y:auto}.page-header{margin-bottom:28px}.page-header h1{letter-spacing:-.03em;background:linear-gradient(135deg,#f8fafc 0%,#93c5fd 100%);-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text;font-size:24px;font-weight:700}.page-header p{color:var(--text-secondary);max-width:56ch;margin-top:6px;font-size:13.5px}.card{-webkit-backdrop-filter:blur(16px);border-radius:var(--radius-md);transition:all var(--transition-normal);background:#121a2ba6;border:1px solid #94a3b81f;padding:22px;box-shadow:0 4px 20px #00000040}.card:hover{background:#1c2740bf;border-color:#60a5fa59;transform:translateY(-1px);box-shadow:0 8px 30px #00000059}.card-grid{grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;display:grid}.btn{border-radius:var(--radius-md);min-height:44px;transition:all var(--transition-fast);letter-spacing:-.01em;cursor:pointer;border:1px solid #0000;justify-content:center;align-items:center;gap:8px;padding:0 18px;font-size:13.5px;font-weight:600;display:inline-flex}.btn-primary{color:#fff;background:linear-gradient(135deg,#3b82f6 0%,#2563eb 55%,#1d4ed8 100%);border-color:#ffffff1a;box-shadow:inset 0 1px #fff3,0 4px 14px #2563eb59}.btn-primary:hover{box-shadow:0 0 24px var(--accent-glow), 0 6px 20px #2563eb73;background:linear-gradient(135deg,#60a5fa 0%,#3b82f6 50%,#2563eb 100%);transform:translateY(-1px)}.btn-ghost{color:var(--text-secondary);background:#94a3b814;border:1px solid #94a3b824}.btn-ghost:hover{color:var(--text-primary);background:#94a3b829;border-color:#94a3b847}.btn-sm{border-radius:var(--radius-sm);min-height:36px;padding:0 12px;font-size:12px}.alert{border-radius:var(--radius-sm);border:1px solid #0000;padding:12px 16px;font-size:13px;font-weight:500}.alert-error{background:var(--error-muted);color:var(--error);border-color:#f8717140}.alert-success{background:var(--success-muted);color:var(--success);border-color:#34d39940}.alert-warning{background:var(--warning-muted);color:var(--warning);border-color:#fbbf2440}.input,.select{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-family:var(--font-sans);transition:border-color var(--transition-fast), box-shadow var(--transition-fast);outline:none;padding:9px 12px;font-size:13px}.input:focus,.select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-muted)}.input::placeholder{color:var(--text-tertiary)}.select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-position:right 10px center;background-repeat:no-repeat;padding-right:32px}.data-table{border-collapse:collapse;width:100%}.data-table th{text-align:left;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.04em;border-bottom:1px solid var(--border);background:var(--bg-secondary);padding:12px 16px;font-size:12px;font-weight:500}.data-table td{border-bottom:1px solid var(--border);color:var(--text-primary);padding:14px 16px;font-size:13px}.data-table tbody tr{transition:background var(--transition-fast)}.data-table tbody tr:hover{background:var(--bg-hover)}.data-table tr.is-leader td{background:var(--accent-soft)}.page-container{max-width:1200px;margin:0 auto;padding:32px}.page-header{justify-content:space-between;align-items:center;margin-bottom:32px;display:flex}.page-header h1{color:var(--text-primary);font-size:24px;font-weight:600}.badge{letter-spacing:.03em;border-radius:4px;align-items:center;padding:3px 8px;font-size:11px;font-weight:600;display:inline-flex}.badge-online,.badge-active{background:var(--success-muted);color:var(--success)}.badge-offline,.badge-inactive{background:var(--error-muted);color:var(--error)}.badge-leader{color:var(--accent-hover);background:#3b82f626}.badge-follower{background:var(--bg-hover);color:var(--text-secondary)}.surface-premium{border:1px solid var(--glass-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-md), inset 0 1px 0 #ffffff0a;background:linear-gradient(165deg,#172033eb,#0c1220f5)}.login-shell{justify-content:center;align-items:center;min-height:100vh;padding:24px;display:flex;position:relative;overflow:hidden}.login-shell:before{content:"";pointer-events:none;background:radial-gradient(50% 40% at 20% 20%,#2563eb47,#0000 55%),radial-gradient(45% 35% at 80% 70%,#1e3a8a59,#0000 50%),radial-gradient(30% 25%,#60a5fa14,#0000 60%);animation:18s ease-in-out infinite alternate meshDrift;position:absolute;inset:-20%}@keyframes meshDrift{0%{transform:translate(0,0)scale(1)}to{transform:translate(-2%,2%)scale(1.05)}}.login-card{z-index:1;border-radius:var(--radius-xl);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);background:linear-gradient(165deg,#121a2beb,#080c16f5);border:1px solid #94a3b824;flex-direction:column;gap:28px;width:min(400px,100%);padding:40px 36px;display:flex;position:relative;box-shadow:0 0 0 1px #3b82f614,0 32px 80px #0000008c,0 0 80px #2563eb1f}.onboarding-shell{max-width:680px;margin:0 auto;padding:36px 22px 72px}.onboarding-progress{background:#94a3b81a;border-radius:99px;height:6px;overflow:hidden;box-shadow:inset 0 1px 2px #0000004d}.onboarding-progress>div{background:linear-gradient(90deg,#1e40af,#3b82f6,#93c5fd);border-radius:99px;height:100%;transition:width .3s cubic-bezier(.23,1,.32,1);box-shadow:0 0 16px #3b82f680}.loop-card{border:1px solid var(--border-accent);border-radius:var(--radius-md);font-family:var(--font-mono);color:#cbd5e1;background:linear-gradient(145deg,#1e3a8a40,#0c1220cc);padding:18px 20px;font-size:12.5px;line-height:1.85;box-shadow:inset 0 1px #ffffff0a}.badge{text-transform:uppercase;letter-spacing:.04em;border-radius:99px;align-items:center;gap:4px;padding:3px 8px;font-size:11px;font-weight:600;display:inline-flex}.badge-success{background:var(--success-muted);color:var(--success)}.badge-warning{background:var(--warning-muted);color:var(--warning)}.badge-error{background:var(--error-muted);color:var(--error)}.badge-accent{background:var(--accent-muted);color:var(--accent)}.chat-container{flex-direction:column;flex:1;height:calc(100vh - 1px);display:flex;overflow:hidden}.chat-messages{flex-direction:column;flex:1;gap:16px;padding:24px 32px;display:flex;overflow-y:auto}.chat-empty{color:var(--text-tertiary);flex-direction:column;flex:1;justify-content:center;align-items:center;gap:12px;display:flex}.chat-empty svg{opacity:.3;width:48px;height:48px}.chat-empty h2{color:var(--text-secondary);font-size:18px;font-weight:600}.chat-empty p{font-size:13px}.message{border-radius:var(--radius-md);white-space:pre-wrap;word-break:break-word;max-width:760px;padding:16px 20px;font-size:14px;line-height:1.65;animation:.18s cubic-bezier(.16,1,.3,1) msgIn;position:relative}@keyframes msgIn{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.message-user{color:#f8fafc;-webkit-backdrop-filter:blur(12px);background:#1e3a8a47;border:1px solid #3b82f659;border-bottom-right-radius:4px;align-self:flex-end;box-shadow:0 4px 18px #00000040}.message-assistant{-webkit-backdrop-filter:blur(12px);color:var(--text-primary);background:#121a2bb8;border:1px solid #94a3b81f;border-bottom-left-radius:4px;align-self:flex-start;box-shadow:0 6px 24px #0000004d}.message-assistant code{background:#070b14bf;border:1px solid #94a3b81a;border-radius:4px;padding:2px 6px;font-size:12.5px}.chat-input-bar{-webkit-backdrop-filter:blur(20px);background:#0c1220d9;border-top:1px solid #94a3b81a;padding:16px 32px 20px}.chat-input-wrapper{align-items:flex-end;gap:10px;max-width:820px;margin:0 auto;display:flex}.chat-input-wrapper textarea{resize:none;border-radius:var(--radius-md);color:var(--text-primary);font-family:var(--font-sans);min-height:44px;max-height:160px;transition:all var(--transition-fast);background:#121a2ba6;border:1px solid #94a3b824;flex:1;padding:12px 16px;font-size:14px;line-height:1.5}.chat-input-wrapper textarea:focus{background:#121a2be6;border-color:#3b82f680;outline:none;box-shadow:0 0 0 3px #3b82f629}.chat-input-wrapper textarea::placeholder{color:var(--text-tertiary)}.chat-send-btn{border-radius:var(--radius-md);color:#fff;width:44px;height:44px;transition:all var(--transition-fast);background:linear-gradient(135deg,#3b82f6,#1d4ed8);flex-shrink:0;justify-content:center;align-items:center;display:flex;box-shadow:0 4px 14px #2563eb59}.chat-send-btn:hover{box-shadow:0 0 24px var(--accent-glow);background:linear-gradient(135deg,#60a5fa,#2563eb);transform:translateY(-1px)}.chat-send-btn:disabled{opacity:.35;cursor:not-allowed;box-shadow:none;transform:none}.chat-send-btn svg{width:18px;height:18px}.memory-layout{grid-template-columns:240px 1fr;gap:20px;height:calc(100vh - 120px);display:grid}.memory-sidebar{-webkit-backdrop-filter:blur(16px);border-radius:var(--radius-md);background:#0c1220b8;border:1px solid #94a3b81f;padding:14px;overflow-y:auto;box-shadow:0 4px 20px #00000040}.memory-sidebar h3{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);padding:6px 8px 10px;font-size:11px;font-weight:600}.category-btn{border-radius:var(--radius-sm);width:100%;color:var(--text-secondary);text-align:left;transition:all var(--transition-fast);background:0 0;border:1px solid #0000;justify-content:space-between;align-items:center;padding:9px 12px;font-size:13px;font-weight:500;display:flex}.category-btn:hover{color:var(--text-primary);background:#94a3b814}.category-btn.active{color:#93c5fd;background:linear-gradient(135deg,#3b82f633,#1e3a8a40);border-color:#3b82f659;font-weight:600}.category-count{color:var(--text-secondary);background:#94a3b81f;border-radius:99px;padding:2px 7px;font-size:11px;font-weight:600}.memory-main{flex-direction:column;gap:14px;display:flex;overflow:hidden}.memory-search{gap:10px;display:flex}.memory-search input{-webkit-backdrop-filter:blur(16px);border-radius:var(--radius-md);color:var(--text-primary);font-size:13.5px;font-family:var(--font-sans);transition:all var(--transition-fast);background:#0c1220b8;border:1px solid #94a3b824;flex:1;padding:12px 16px}.memory-search input:focus{background:#121a2be6;border-color:#3b82f680;outline:none;box-shadow:0 0 0 3px #3b82f629}.memory-search input::placeholder{color:var(--text-tertiary)}.memory-list{flex-direction:column;flex:1;gap:10px;display:flex;overflow-y:auto}.memory-node-card{border-radius:var(--radius-md);cursor:pointer;transition:all var(--transition-fast);background:#121a2b99;border:1px solid #94a3b81f;padding:16px 18px;box-shadow:0 2px 8px #0003}.memory-node-card:hover{background:#1c2740b3;border-color:#60a5fa66;transform:translateY(-1px);box-shadow:0 4px 16px #0000004d}.memory-node-card.selected{background:#1e3a8a40;border-color:#3b82f68c;box-shadow:0 0 0 1px #3b82f64d,0 4px 20px #00000059}.memory-node-card h4{color:#f8fafc;margin-bottom:6px;font-size:14px;font-weight:600}.memory-node-card .meta{color:var(--text-tertiary);align-items:center;gap:14px;font-size:12px;display:flex}.memory-detail{-webkit-backdrop-filter:blur(20px);border-radius:var(--radius-lg);background:#0c1220cc;border:1px solid #94a3b824;padding:24px;overflow-y:auto;box-shadow:0 8px 32px #0006}.memory-detail h2{margin-bottom:12px;font-size:18px}.memory-detail pre{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:pre-wrap;color:var(--text-secondary);padding:16px;font-size:12px;line-height:1.7;overflow-x:auto}.memory-detail .frontmatter{background:var(--bg-tertiary);border-radius:var(--radius-sm);grid-template-columns:auto 1fr;gap:4px 16px;margin-bottom:16px;padding:12px 16px;font-size:13px;display:grid}.frontmatter dt{color:var(--text-tertiary);font-weight:500}.frontmatter dd{color:var(--text-primary)}.health-grid{grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:24px;display:grid}.stat-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);text-align:center;transition:border-color var(--transition-normal);padding:20px}.stat-card:hover{border-color:var(--border-hover)}.stat-card .value{letter-spacing:-.02em;font-size:28px;font-weight:700}.stat-card .label{color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;margin-top:4px;font-size:12px}.stat-card.healthy .value{color:var(--success)}.stat-card.warning .value{color:var(--warning)}.stat-card.error .value{color:var(--error)}.health-log{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-md);padding:20px}.health-log h3{margin-bottom:12px;font-size:14px;font-weight:600}.health-log table{border-collapse:collapse;width:100%;font-size:13px}.health-log th{text-align:left;color:var(--text-tertiary);border-bottom:1px solid var(--border);padding:8px 12px;font-weight:500}.health-log td{border-bottom:1px solid var(--border);color:var(--text-secondary);padding:8px 12px}.sandbox-layout{grid-template-rows:1fr auto 1fr;gap:0;height:calc(100vh - 120px);display:grid}.sandbox-editor{flex-direction:column;display:flex;overflow:hidden}.sandbox-editor-header{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-md) var(--radius-md) 0 0;color:var(--text-tertiary);justify-content:space-between;align-items:center;padding:8px 12px;font-size:12px;display:flex}.sandbox-editor textarea{resize:none;background:var(--bg-secondary);border:1px solid var(--border);border-radius:0 0 var(--radius-md) var(--radius-md);width:100%;color:var(--text-primary);font-family:var(--font-mono);tab-size:2;border-top:none;flex:1;padding:16px;font-size:13px;line-height:1.6}.sandbox-editor textarea:focus{border-color:var(--accent);outline:none}.sandbox-toolbar{justify-content:center;align-items:center;padding:8px;display:flex}.sandbox-output{background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius-md);font-family:var(--font-mono);color:var(--success);white-space:pre-wrap;padding:16px;font-size:13px;line-height:1.6;overflow-y:auto}.sandbox-output.error{color:var(--error)}.pulse{background:var(--success);border-radius:50%;width:8px;height:8px;animation:2s infinite pulseAnim;display:inline-block}@keyframes pulseAnim{0%,to{box-shadow:0 0 #00cec966}50%{box-shadow:0 0 0 6px #00cec900}}.skeleton{background:linear-gradient(90deg, var(--bg-tertiary) 25%, var(--bg-hover) 50%, var(--bg-tertiary) 75%);border-radius:var(--radius-sm);background-size:200% 100%;animation:1.5s infinite shimmer}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@media (width<=768px){.sidebar{position:fixed;top:0;bottom:0;left:0;transform:translate(-100%);box-shadow:0 0 40px #00000080}.sidebar.open{transform:translate(0)}.memory-layout{grid-template-columns:1fr;height:auto}.page{padding:16px}.btn,.nav-link,.category-btn{touch-action:manipulation;min-height:44px}.chat-input-wrapper textarea,.memory-search input,.sandbox-editor textarea{font-size:16px}.chat-input-bar{padding:12px 16px 16px}}.chat-layout{width:100%;height:100%;display:flex;overflow:hidden}.chat-threads-sidebar{-webkit-backdrop-filter:blur(16px);background:#0c1220b3;border-right:1px solid #94a3b81a;flex-direction:column;flex-shrink:0;width:260px;height:100%;display:flex;overflow:hidden}.sidebar-header{border-bottom:1px solid #94a3b81a;padding:14px 16px}.new-chat-btn{color:#93c5fd;border-radius:var(--radius-sm);width:100%;transition:all var(--transition-fast);cursor:pointer;background:linear-gradient(135deg,#3b82f638,#1e3a8a47);border:1px solid #3b82f659;justify-content:center;align-items:center;gap:8px;padding:10px 16px;font-size:13px;font-weight:600;display:flex}.new-chat-btn:hover{color:#fff;box-shadow:0 0 16px var(--accent-glow);background:linear-gradient(135deg,#3b82f659,#1e3a8a73);border-color:#60a5fa80}.thread-list{flex-direction:column;flex:1;gap:6px;padding:16px 12px;display:flex;overflow-y:auto}.thread-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);border:1px solid #0000;justify-content:space-between;align-items:center;padding:10px 12px;display:flex;position:relative}.thread-item:hover{background:#94a3b814}.thread-item.active{background:#3b82f61f;border-color:#3b82f64d}.thread-info{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.thread-title{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}.thread-meta{color:var(--text-tertiary);align-items:center;gap:8px;font-size:11px;display:flex}.thread-delete-btn{color:var(--text-tertiary);cursor:pointer;opacity:0;transition:all var(--transition-fast);background:0 0;border:none;border-radius:4px;justify-content:center;align-items:center;padding:4px;display:flex}.thread-item:hover .thread-delete-btn{opacity:1}.thread-delete-btn:hover{color:var(--error);background:var(--error-muted)}.chat-suggestions-container{width:100%;max-width:900px;margin-top:32px;animation:.4s ease-out fadeIn}.chat-suggestions-container h3{text-transform:uppercase;letter-spacing:.08em;color:var(--text-tertiary);text-align:center;margin-bottom:16px;font-size:12px}.chat-suggestions-deck{grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;width:100%;display:grid}.suggestion-card{-webkit-backdrop-filter:blur(12px);border-radius:var(--radius-md);cursor:pointer;text-align:left;transition:all var(--transition-normal);background:#12121a73;border:1px solid #ffffff0d;gap:14px;padding:16px;display:flex;position:relative;overflow:hidden}.suggestion-card:before{content:"";opacity:.8;width:4px;height:100%;transition:width var(--transition-fast) ease;position:absolute;top:0;left:0}.suggestion-card.card-fact:before{background:var(--success)}.suggestion-card.card-concept:before{background:var(--accent)}.suggestion-card.card-question:before{background:var(--warning)}.suggestion-card:hover{background:#1a1a2699;border-color:#ffffff1f;transform:translateY(-2px);box-shadow:0 8px 24px #0003}.suggestion-card:hover:before{width:6px}.suggestion-card.card-fact:hover{border-color:#00cec940;box-shadow:0 8px 24px #00cec90f}.suggestion-card.card-concept:hover{border-color:#6c5ce740;box-shadow:0 8px 24px #6c5ce70f}.suggestion-card.card-question:hover{border-color:#fdcb6e40;box-shadow:0 8px 24px #fdcb6e0f}.suggestion-card-icon{-webkit-user-select:none;user-select:none;align-items:flex-start;padding-top:2px;font-size:22px;display:flex}.suggestion-card-content{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.suggestion-card-content h4{color:var(--text-primary);font-size:13.5px;font-weight:600;line-height:1.4}.suggestion-card-content p{color:var(--text-secondary);font-size:12.5px;line-height:1.5}.suggestion-card-nodes{flex-wrap:wrap;gap:6px;margin-top:8px;display:flex}.suggestion-node-pill{background:var(--bg-tertiary);border:1px solid var(--border);color:var(--text-secondary);white-space:nowrap;text-overflow:ellipsis;border-radius:4px;max-width:140px;padding:2px 6px;font-size:10px;font-weight:500;overflow:hidden}.grounding-pills{border:1px solid var(--border);border-radius:var(--radius-md) var(--radius-md) 0 0;background:#12121a66;border-bottom:none;flex-wrap:wrap;align-items:center;gap:8px;padding:8px 12px;display:flex}.grounding-pill{background:var(--accent-muted);color:var(--text-primary);transition:all var(--transition-fast);border:1px solid #6c5ce74d;border-radius:99px;align-items:center;gap:6px;padding:4px 10px;font-size:12px;font-weight:500;display:inline-flex}.grounding-pill:hover{background:#6c5ce733;border-color:#6c5ce780}.grounding-pill svg{color:var(--accent);flex-shrink:0}.grounding-pill-title{text-overflow:ellipsis;white-space:nowrap;max-width:150px;overflow:hidden}.grounding-pill-remove{color:var(--text-tertiary);cursor:pointer;transition:color var(--transition-fast);background:0 0;border:none;justify-content:center;align-items:center;padding:0 2px;font-size:14px;line-height:1;display:inline-flex}.grounding-pill-remove:hover{color:var(--error)}.grounding-count-badge{background:var(--accent);color:#fff;border:1.5px solid var(--bg-secondary);min-width:16px;height:16px;box-shadow:0 0 8px var(--accent-glow);border-radius:50%;justify-content:center;align-items:center;font-size:9px;font-weight:700;animation:.2s cubic-bezier(.175,.885,.32,1.275) popIn;display:flex;position:absolute;top:-4px;right:-4px}.node-selector-popover{-webkit-backdrop-filter:blur(20px);border-radius:var(--radius-lg);z-index:50;background:#12121ad9;border:1px solid #ffffff14;flex-direction:column;width:320px;max-height:380px;animation:.25s cubic-bezier(.16,1,.3,1) slideUp;display:flex;position:absolute;bottom:calc(100% + 12px);left:0;overflow:hidden;box-shadow:0 12px 32px #0006}.node-selector-header{border-bottom:1px solid var(--border);flex-direction:column;gap:8px;padding:14px 16px 10px;display:flex}.node-selector-header h4{color:var(--text-primary);letter-spacing:.02em;font-size:13px;font-weight:600}.node-search-input{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:var(--radius-sm);width:100%;color:var(--text-primary);font-size:12px;font-family:var(--font-sans);transition:all var(--transition-fast);padding:6px 10px}.node-search-input:focus{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow);outline:none}.node-selector-list{flex-direction:column;flex:1;gap:2px;padding:8px;display:flex;overflow-y:auto}.node-selector-empty{text-align:center;color:var(--text-tertiary);padding:24px;font-size:12px}.node-selector-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);align-items:center;gap:10px;padding:8px 12px;display:flex}.node-selector-item:hover{background:var(--bg-hover)}.node-selector-item.selected{background:var(--accent-muted)}.node-selector-item-checkbox{border:1px solid var(--text-tertiary);width:16px;height:16px;color:var(--accent);transition:all var(--transition-fast);background:var(--bg-tertiary);border-radius:3px;flex-shrink:0;justify-content:center;align-items:center;font-size:11px;font-weight:700;display:flex}.node-selector-item.selected .node-selector-item-checkbox{background:var(--accent);border-color:var(--accent);color:#fff}.node-selector-item-info{flex-direction:column;flex:1;min-width:0;display:flex}.node-selector-item-title{color:var(--text-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;overflow:hidden}.node-selector-item-meta{color:var(--text-tertiary);text-transform:capitalize;font-size:11px}.animate-fade-in{animation:.2s ease-out fadeIn}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideUp{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}@keyframes popIn{0%{opacity:0;transform:scale(.6)}to{opacity:1;transform:scale(1)}}.graph-3d-wrapper{-webkit-user-select:none;user-select:none;background:radial-gradient(circle,#0a0d16 0%,#020408 100%);width:100%;height:100%;position:relative;overflow:hidden}.node-detail-drawer{-webkit-backdrop-filter:blur(25px);border-radius:var(--radius-lg);z-index:100;width:350px;color:var(--text-primary);background:#0a0f1ebf;border:1px solid #ffffff14;flex-direction:column;display:flex;position:absolute;top:20px;bottom:20px;right:20px;overflow:hidden;box-shadow:0 16px 48px #00000080,inset 0 0 0 1px #ffffff0d}.animate-slide-in{animation:.4s cubic-bezier(.16,1,.3,1) forwards slideInRight}@keyframes slideInRight{0%{opacity:0;transform:translate(100%)}to{opacity:1;transform:translate(0)}}.drawer-header{border-bottom:1px solid #ffffff0f;justify-content:space-between;align-items:center;padding:16px 20px;display:flex}.drawer-badge{text-transform:uppercase;letter-spacing:.05em;border-radius:4px;padding:4px 8px;font-size:10px;font-weight:700}.drawer-badge.badge-memory{color:#c084fc;background:#8b5cf626;border:1px solid #8b5cf64d}.drawer-badge.badge-thread{color:#22d3ee;background:#06b6d426;border:1px solid #06b6d44d}.drawer-badge.badge-research{color:#fbbf24;background:#f59e0b26;border:1px solid #f59e0b4d}.drawer-close-btn{color:var(--text-tertiary);cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:4px;font-size:20px;transition:color .15s;display:flex}.drawer-close-btn:hover{color:#fff}.drawer-body{flex-direction:column;flex:1;gap:12px;padding:20px;display:flex;overflow-y:auto}.drawer-title{color:#fff;margin:0;font-size:18px;font-weight:700;line-height:1.35}.drawer-subtitle{color:var(--text-tertiary);margin-top:-4px;font-size:11px}.drawer-status{background:#ffffff08;border:1px solid #ffffff0a;border-radius:6px;align-items:center;gap:8px;width:fit-content;padding:6px 12px;font-size:12px;display:flex}.drawer-status .label{color:var(--text-tertiary)}.drawer-status .value{text-transform:capitalize;font-weight:600}.drawer-status .value.status-completed,.drawer-status .value.status-done,.drawer-status .value.status-active{color:#10b981}.drawer-status .value.status-in-progress{color:#f59e0b}.drawer-status .value.status-pending{color:#3b82f6}.drawer-excerpt{color:var(--text-secondary);white-space:pre-wrap;margin-top:8px;font-size:13.5px;line-height:1.6}.drawer-footer{background:#00000026;border-top:1px solid #ffffff0f;padding:16px 20px}.drawer-action-btn{border-radius:var(--radius-sm);cursor:pointer;width:100%;transition:all var(--transition-fast);border:none;justify-content:center;align-items:center;padding:10px 16px;font-size:13px;font-weight:600;display:flex}.drawer-action-btn.btn-cyan{color:#fff;background:#06b6d4}.drawer-action-btn.btn-cyan:hover{background:#0891b2;box-shadow:0 0 16px #06b6d466}.drawer-action-btn.btn-purple{color:#fff;background:#8b5cf6}.drawer-action-btn.btn-purple:hover{background:#7c3aed;box-shadow:0 0 16px #8b5cf666}@media (width<=768px){.node-detail-drawer{width:auto;height:300px;bottom:20px;left:20px}}.chat-messages:has(.graph-3d-wrapper){padding:0;overflow:hidden}.chat-header{-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);z-index:40;background:#12121aeb;flex-shrink:0;justify-content:space-between;align-items:center;padding:12px 24px;display:flex;position:relative;overflow:visible}.chat-header-title{align-items:center;gap:10px;display:flex}.chat-header-title h2{color:var(--text-primary);letter-spacing:-.01em;font-size:15px;font-weight:600}.chat-header-model-selector{align-items:center;gap:8px;display:flex}.chat-header-model-selector span.selector-label{color:var(--text-tertiary);font-size:12px;font-weight:500}.model-selector-dropdown-wrapper{position:relative}.model-selector-dropdown-btn{border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-secondary);transition:all var(--transition-fast);cursor:pointer;background:#ffffff0a;align-items:center;gap:8px;padding:6px 12px;font-size:13px;font-weight:500;display:flex}.model-selector-dropdown-btn:hover{border-color:var(--border-hover);color:var(--text-primary);background:#ffffff14}.model-selector-dropdown-btn.active{border-color:var(--accent);color:var(--text-primary);background:var(--accent-muted)}.model-selector-menu{-webkit-backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:var(--radius-md);z-index:200;pointer-events:auto;background:#12121afa;flex-direction:column;gap:2px;width:240px;margin-top:6px;padding:6px;animation:.15s ease-out popoverIn;display:flex;position:absolute;top:100%;right:0;box-shadow:0 12px 32px #0000008c}@keyframes popoverIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.model-selector-item{border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);text-align:left;justify-content:space-between;align-items:center;padding:8px 12px;display:flex}.model-selector-item:hover{background:var(--bg-hover)}.model-selector-item.selected{background:var(--accent-muted)}.model-selector-item-info{flex-direction:column;gap:2px;display:flex}.model-selector-item-name{color:var(--text-primary);font-size:13px;font-weight:500}.model-selector-item.selected .model-selector-item-name{color:var(--accent)}.model-selector-item-desc{color:var(--text-tertiary);font-size:11px}.model-selector-item-badge{background:var(--bg-tertiary);color:var(--text-tertiary);text-transform:uppercase;border-radius:99px;padding:2px 6px;font-size:9px;font-weight:600}.model-selector-item.selected .model-selector-item-badge{background:var(--accent-muted);color:var(--accent)}@media (width<=768px){button,a,[role=button]{min-width:44px;min-height:44px}.chat-input-area{bottom:0;bottom:env(safe-area-inset-bottom,0);position:sticky}.app-container{padding-left:env(safe-area-inset-left,0);padding-right:env(safe-area-inset-right,0);padding-bottom:env(safe-area-inset-bottom,0)}}@media (display-mode:standalone){body{padding-top:env(safe-area-inset-top,0)}}.network-page{animation:.35s ease-out page-fade-in}@keyframes page-fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.stat-grid{grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:32px;display:grid}.stat-card{text-align:center;flex-direction:column;align-items:center;padding:24px 16px;display:flex}.stat-value{color:var(--text-primary);font-variant-numeric:tabular-nums;font-size:32px;font-weight:700;line-height:1.2;transition:color .3s}.stat-value.error{color:var(--error)}.stat-value.success{color:var(--success)}.stat-value.accent{color:var(--accent)}.stat-label{color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.06em;margin-top:8px;font-size:12px;font-weight:500}.network-sections{grid-template-columns:1fr 1fr;gap:24px;margin-bottom:32px;display:grid}.network-section-full{margin-bottom:32px}.section-title{color:var(--text-primary);align-items:center;gap:8px;margin-bottom:16px;font-size:15px;font-weight:600;display:flex}.blocklist-items{max-height:240px;margin:0 0 12px;padding:0;list-style:none;overflow-y:auto}.blocklist-item{border-bottom:1px solid var(--border);font-family:var(--font-mono);color:var(--text-primary);transition:background var(--transition-fast);justify-content:space-between;align-items:center;padding:8px 12px;font-size:13px;display:flex}.blocklist-item:hover{background:var(--bg-hover)}.blocklist-item:last-child{border-bottom:none}.blocklist-remove{color:var(--error);opacity:.6;transition:opacity var(--transition-fast), background var(--transition-fast);background:0 0;border-radius:4px;padding:4px 8px;font-size:12px}.blocklist-remove:hover{opacity:1;background:var(--error-muted)}.blocklist-add{gap:8px;display:flex}.blocklist-add .input{flex:1}.limits-form{flex-direction:column;gap:12px;display:flex}.limits-row{justify-content:space-between;align-items:center;gap:12px;display:flex}.limits-label{color:var(--text-secondary);flex-shrink:0;font-size:13px}.limits-input{text-align:right;width:100px}.limits-actions{justify-content:flex-end;margin-top:8px;display:flex}.audit-filters{flex-wrap:wrap;gap:12px;margin-bottom:16px;display:flex}.audit-filters .input,.audit-filters .select{flex:1;min-width:140px}.audit-status-badge{border-radius:3px;align-items:center;padding:2px 6px;font-size:11px;font-weight:600;display:inline-flex}.audit-status-badge.success{background:var(--success-muted);color:var(--success)}.audit-status-badge.error{background:var(--error-muted);color:var(--error)}.audit-status-badge.timeout{background:var(--warning-muted);color:var(--warning)}.toggle-row{align-items:center;gap:12px;margin-bottom:16px;display:flex}.toggle-switch{background:var(--bg-hover);cursor:pointer;width:44px;height:24px;transition:background var(--transition-fast);border:1px solid var(--border);border-radius:12px;flex-shrink:0;position:relative}.toggle-switch.active{background:var(--accent);border-color:var(--accent)}.toggle-switch:after{content:"";width:18px;height:18px;transition:transform var(--transition-fast);background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;box-shadow:0 1px 3px #0003}.toggle-switch.active:after{transform:translate(20px)}.toggle-label{color:var(--text-secondary);font-size:13px}.refresh-controls{align-items:center;gap:8px;display:flex}.refresh-controls .select{width:auto;padding:6px 28px 6px 10px;font-size:12px}.empty-state{text-align:center;color:var(--text-tertiary);padding:32px 16px;font-size:13px;font-style:italic}@media (width<=768px){.stat-grid,.network-sections{grid-template-columns:1fr}}.webhooks-page{animation:.35s ease-out page-fade-in}.wizard-modal{width:100%;max-width:600px}.wizard-steps{justify-content:space-between;margin-bottom:24px;display:flex;position:relative}.wizard-steps:before{content:"";z-index:0;background:#ffffff1a;height:2px;position:absolute;top:12px;left:0;right:0}.wizard-step{background:var(--bg-surface);color:var(--text-muted);z-index:1;border:2px solid #ffffff1a;border-radius:50%;justify-content:center;align-items:center;width:28px;height:28px;font-size:14px;font-weight:600;transition:all .3s;display:flex}.wizard-step.active{background:var(--primary);color:#fff;border-color:var(--primary);box-shadow:0 0 12px #6366f180}.wizard-step.completed{background:var(--success);color:#fff;border-color:var(--success)}.json-viewer{color:#a5b4fc;background:#0003;border:1px solid #ffffff0d;border-radius:8px;margin-top:8px;padding:12px;font-family:Fira Code,monospace;font-size:13px;overflow-x:auto}.json-toggle{color:var(--primary);cursor:pointer;background:0 0;border:none;margin-top:4px;padding:0;font-size:12px}.json-toggle:hover{text-decoration:underline}.webhook-stats{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px;margin-bottom:24px;display:grid}.stat-box{text-align:center;background:#ffffff08;border:1px solid #ffffff0d;border-radius:12px;padding:16px}.stat-box .label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;font-size:12px}.stat-box .value{color:var(--text-primary);font-size:24px;font-weight:700}.rotate-btn{color:var(--text-primary);cursor:pointer;background:#ffffff0d;border:1px solid #ffffff1a;border-radius:4px;padding:4px 8px;font-size:12px;transition:all .2s}.rotate-btn:hover{background:#ffffff1a}.notifications-page{animation:.35s ease-out notif-fade-in}@keyframes notif-fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.notif-rules-card{padding:0;overflow:hidden}.notif-rules-actions{gap:6px;display:flex}.priority-badge{text-transform:uppercase;letter-spacing:.04em;border-radius:4px;align-items:center;padding:2px 8px;font-size:11px;font-weight:600;display:inline-flex}.priority-badge.critical{background:var(--error-muted);color:var(--error)}.priority-badge.high{background:var(--warning-muted);color:var(--warning)}.priority-badge.low{background:var(--success-muted);color:var(--success)}.notif-toggle{background:var(--bg-hover);cursor:pointer;width:40px;height:22px;transition:background var(--transition-fast);border:1px solid var(--border);border-radius:11px;flex-shrink:0;position:relative}.notif-toggle.active{background:var(--accent);border-color:var(--accent)}.notif-toggle:after{content:"";width:16px;height:16px;transition:transform var(--transition-fast);background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;box-shadow:0 1px 3px #0003}.notif-toggle.active:after{transform:translate(18px)}.notif-history-header{justify-content:space-between;align-items:center;margin-bottom:16px;display:flex}.notif-history-header h2{color:var(--text-primary);font-size:15px;font-weight:600}.notif-history-list{flex-direction:column;gap:10px;max-height:420px;display:flex;overflow-y:auto}.notif-history-entry{background:var(--bg-hover);border-radius:var(--radius-sm);transition:background var(--transition-fast);justify-content:space-between;align-items:flex-start;padding:14px 16px;display:flex}.notif-history-entry:hover{background:var(--bg-secondary)}.notif-entry-content{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}.notif-entry-title{color:var(--text-primary);font-size:14px;font-weight:500}.notif-entry-message{color:var(--text-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;overflow:hidden}.notif-entry-meta{flex-direction:column;flex-shrink:0;align-items:flex-end;gap:4px;margin-left:16px;display:flex}.notif-entry-time{color:var(--text-tertiary);white-space:nowrap;font-size:12px}.notif-status-badge{border-radius:3px;align-items:center;padding:2px 6px;font-size:11px;font-weight:600;display:inline-flex}.notif-status-badge.delivered{background:var(--success-muted);color:var(--success)}.notif-status-badge.failed{background:var(--error-muted);color:var(--error)}.notif-quiet-hours-row{align-items:center;gap:10px;display:flex}.notif-quiet-hours-label{color:var(--text-secondary);cursor:pointer;font-size:13px}.notif-empty{text-align:center;color:var(--text-tertiary);padding:32px 16px;font-size:13px;font-style:italic}@media (width<=768px){.notif-history-entry{flex-direction:column;gap:8px}.notif-entry-meta{flex-direction:row;align-items:center;margin-left:0}}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import type { PluginInfo } from '../../../api/plugins'
|
|
3
|
-
import { apiFetch, API_BASE } from '../../../api/_base'
|
|
4
|
-
import { normalizeCard, type Card } from './contract'
|
|
5
|
-
import { CardRunner } from './runner'
|
|
6
|
-
import { ActivityWidget, CommandsWidget, SettingsWidget } from './widgets'
|
|
7
|
-
import { brandStyle } from './theme'
|
|
8
|
-
import { WorkspaceHost } from '../workspace/WorkspaceHost'
|
|
9
|
-
import { PluginPanel } from '../PluginPanel'
|
|
10
|
-
import './card.css'
|
|
11
|
-
import { CardScenes } from './CardScenes'
|
|
12
|
-
|
|
13
|
-
/** The provider's logo comes from the plugin through a locked-down route; <img> never runs SVG script. */
|
|
14
|
-
function useLogo(card: Card) {
|
|
15
|
-
const [url, setUrl] = useState('')
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (!card.branding.logo) return
|
|
18
|
-
let objectUrl = ''
|
|
19
|
-
let disposed = false
|
|
20
|
-
apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(card.id)}/logo`)
|
|
21
|
-
.then(r => r.ok ? r.blob() : Promise.reject(new Error('no logo')))
|
|
22
|
-
.then(blob => { if (!disposed) { objectUrl = URL.createObjectURL(blob); setUrl(objectUrl) } })
|
|
23
|
-
.catch(() => { if (!disposed) setUrl('') })
|
|
24
|
-
return () => { disposed = true; if (objectUrl) URL.revokeObjectURL(objectUrl) }
|
|
25
|
-
}, [card.id, card.branding.logo])
|
|
26
|
-
return url
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function Header({ card, onOpenDrawer }: { card: Card; onOpenDrawer: () => void }) {
|
|
30
|
-
const logo = useLogo(card)
|
|
31
|
-
const { branding, distribution } = card
|
|
32
|
-
const hasWorkspace = Boolean(card.workspace)
|
|
33
|
-
return <header className={`card-header card-header-${branding.header}`}>
|
|
34
|
-
<div className="card-brand">
|
|
35
|
-
{logo ? <img className="card-logo" src={logo} alt={`${branding.provider?.name || branding.name} logo`} /> : <span className="card-logo-missing" aria-hidden="true" />}
|
|
36
|
-
<div>
|
|
37
|
-
<h2>{branding.name}</h2>
|
|
38
|
-
{branding.badge && <span className="card-badge">{branding.badge}</span>}
|
|
39
|
-
{distribution && <span className="card-badge card-badge-quiet">{distribution.tier} · {distribution.license === 'open' ? 'open source' : 'service'}</span>}
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
<button type="button" className="card-button card-button-quiet" onClick={onOpenDrawer}>
|
|
43
|
-
{hasWorkspace ? 'Open workspace' : 'Settings & usage'}
|
|
44
|
-
</button>
|
|
45
|
-
</header>
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function Drawer({ plugin, card, onClose }: { plugin: PluginInfo; card: Card; onClose: () => void }) {
|
|
49
|
-
const hasWorkspace = Boolean(card.workspace)
|
|
50
|
-
const [tab, setTab] = useState<'workspace' | 'settings' | 'commands' | 'usage'>(hasWorkspace ? 'workspace' : 'settings')
|
|
51
|
-
const dialog = useRef<HTMLElement>(null)
|
|
52
|
-
const close = useRef(onClose)
|
|
53
|
-
useEffect(() => { close.current = onClose }, [onClose])
|
|
54
|
-
useEffect(() => {
|
|
55
|
-
const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
|
56
|
-
const focusable = () => Array.from(dialog.current?.querySelectorAll<HTMLElement>('button:not(:disabled), a[href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex="0"]') || [])
|
|
57
|
-
focusable()[0]?.focus()
|
|
58
|
-
const key = (e: KeyboardEvent) => {
|
|
59
|
-
if (e.key === 'Escape') { e.preventDefault(); close.current(); return }
|
|
60
|
-
if (e.key !== 'Tab') return
|
|
61
|
-
const targets = focusable(), first = targets[0], last = targets[targets.length - 1]
|
|
62
|
-
if (!first) { e.preventDefault(); dialog.current?.focus(); return }
|
|
63
|
-
if (!dialog.current?.contains(document.activeElement) || (e.shiftKey && document.activeElement === first)) { e.preventDefault(); (e.shiftKey ? last : first).focus() }
|
|
64
|
-
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
|
|
65
|
-
}
|
|
66
|
-
window.addEventListener('keydown', key)
|
|
67
|
-
return () => { window.removeEventListener('keydown', key); if (previous?.isConnected) previous.focus() }
|
|
68
|
-
}, [])
|
|
69
|
-
const tabs = hasWorkspace
|
|
70
|
-
? (['workspace', 'settings', 'commands', 'usage'] as const)
|
|
71
|
-
: (['settings', 'commands', 'usage'] as const)
|
|
72
|
-
return <aside ref={dialog} tabIndex={-1} className="card-drawer" role="dialog" aria-modal="true" aria-label={`${card.title} settings`}>
|
|
73
|
-
<div className="card-drawer-head"><strong>{card.title}</strong><button type="button" className="card-link" onClick={onClose} aria-label="Close">Close</button></div>
|
|
74
|
-
<nav className="card-tabs" role="tablist">{tabs.map(t =>
|
|
75
|
-
<button key={t} role="tab" type="button" aria-selected={tab === t} onClick={() => setTab(t)}>
|
|
76
|
-
{t === 'workspace' ? 'Workspace' : t === 'settings' ? 'Settings' : t === 'commands' ? 'Commands' : 'Session activity'}
|
|
77
|
-
</button>)}</nav>
|
|
78
|
-
<div className="card-drawer-body">
|
|
79
|
-
{tab === 'workspace' && <WorkspaceHost plugin={plugin} card={card} />}
|
|
80
|
-
{tab === 'settings' && <SettingsWidget plugin={plugin} />}
|
|
81
|
-
{tab === 'commands' && <CommandsWidget />}
|
|
82
|
-
{tab === 'usage' && <ActivityWidget />}
|
|
83
|
-
</div>
|
|
84
|
-
</aside>
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The host gives a plugin its frame, its brand tokens and its command
|
|
89
|
-
* plumbing. The layout is the plugin's own face: CardEngine never arranges
|
|
90
|
-
* a plugin's interface and there is no shared template to fall back to.
|
|
91
|
-
*/
|
|
92
|
-
export function CardEngine({ plugin }: { plugin: PluginInfo }) {
|
|
93
|
-
const card = useMemo(() => normalizeCard(plugin), [plugin])
|
|
94
|
-
const [drawer, setDrawer] = useState(false)
|
|
95
|
-
return <CardRunner card={card}>
|
|
96
|
-
<section className={`card card-bespoke${card.face ? ' card-owned' : ''}`} style={brandStyle(card.branding)} data-density={card.branding.density} aria-label={`${card.title} card`}>
|
|
97
|
-
<Header card={card} onOpenDrawer={() => setDrawer(true)} />
|
|
98
|
-
{card.description && <p className="card-muted card-description">{card.description}</p>}
|
|
99
|
-
<CardScenes plugin={plugin}>
|
|
100
|
-
{card.face
|
|
101
|
-
? <PluginPanel plugin={plugin} elementId={card.face.id} />
|
|
102
|
-
: <p className="card-error" role="alert">{plugin.name} has not supplied its card face.</p>}
|
|
103
|
-
</CardScenes>
|
|
104
|
-
{card.branding.provider && <footer className="card-footer">{card.branding.provider.url ? <a href={card.branding.provider.url} target="_blank" rel="noreferrer noopener">{card.branding.provider.name}</a> : card.branding.provider.name} is a trademark of its owner. This plugin connects to the service with your own account.</footer>}
|
|
105
|
-
{drawer && <Drawer plugin={plugin} card={card} onClose={() => setDrawer(false)} />}
|
|
106
|
-
</section>
|
|
107
|
-
</CardRunner>
|
|
108
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import type { ReactNode } from 'react'
|
|
3
|
-
import type { PluginInfo } from '../../../api/plugins'
|
|
4
|
-
import { API_BASE, apiFetch } from '../../../api/_base'
|
|
5
|
-
import { useRunner } from './runner-context'
|
|
6
|
-
import { ExecutionPicker } from './ExecutionPicker'
|
|
7
|
-
import { sanitizePluginScreens } from '../plugin-frame'
|
|
8
|
-
import type { PluginScreen } from '../plugin-frame'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* The host gives a plugin a place to put its screens and a way to move between
|
|
12
|
-
* them. Which screens exist, what they are called and what is on them is the
|
|
13
|
-
* plugin's design: the face declares them and reports the active one. The host
|
|
14
|
-
* never derives a screen from the command list.
|
|
15
|
-
*/
|
|
16
|
-
export type { PluginScreen } from '../plugin-frame'
|
|
17
|
-
|
|
18
|
-
export function CardScenes({ plugin, children }: { plugin: PluginInfo; children: ReactNode }) {
|
|
19
|
-
const { card, busy } = useRunner()
|
|
20
|
-
const [enabled, setEnabled] = useState(plugin.activation?.enabled !== false)
|
|
21
|
-
useEffect(() => { setEnabled(plugin.activation?.enabled !== false) }, [plugin.id, plugin.activation?.enabled])
|
|
22
|
-
const [changing, setChanging] = useState(false)
|
|
23
|
-
const [error, setError] = useState('')
|
|
24
|
-
const [ownerBusy, setOwnerBusy] = useState<Record<string, string>>({})
|
|
25
|
-
const [menu, setMenu] = useState(false)
|
|
26
|
-
const [screens, setScreens] = useState<PluginScreen[]>([])
|
|
27
|
-
const [activeScreen, setActiveScreen] = useState('')
|
|
28
|
-
const screensRef = useRef<PluginScreen[]>([])
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
screensRef.current = []
|
|
31
|
-
setScreens([]); setActiveScreen(''); setMenu(false)
|
|
32
|
-
const activity = (event: Event) => {
|
|
33
|
-
const detail = (event as CustomEvent).detail
|
|
34
|
-
if (detail?.pluginId !== plugin.id) return
|
|
35
|
-
setOwnerBusy(previous => { const next = { ...previous }; if (detail.running) next[detail.requestId] = detail.command; else delete next[detail.requestId]; return next })
|
|
36
|
-
}
|
|
37
|
-
const contextMenu = (event: Event) => { if ((event as CustomEvent).detail?.pluginId === plugin.id) setMenu(true) }
|
|
38
|
-
const declared = (event: Event) => {
|
|
39
|
-
const detail = (event as CustomEvent).detail
|
|
40
|
-
if (detail?.pluginId !== plugin.id) return
|
|
41
|
-
const next = sanitizePluginScreens(detail.screens)
|
|
42
|
-
screensRef.current = next
|
|
43
|
-
setScreens(next)
|
|
44
|
-
setActiveScreen(previous => next.some(screen => screen.id === previous) ? previous : next[0]?.id || '')
|
|
45
|
-
}
|
|
46
|
-
const changed = (event: Event) => {
|
|
47
|
-
const detail = (event as CustomEvent).detail
|
|
48
|
-
if (detail?.pluginId === plugin.id && screensRef.current.some(screen => screen.id === detail.screen)) setActiveScreen(detail.screen)
|
|
49
|
-
}
|
|
50
|
-
window.addEventListener('plugin-command-activity', activity)
|
|
51
|
-
window.addEventListener('plugin-context-menu', contextMenu)
|
|
52
|
-
window.addEventListener('plugin-screens', declared)
|
|
53
|
-
window.addEventListener('plugin-screen-changed', changed)
|
|
54
|
-
return () => {
|
|
55
|
-
window.removeEventListener('plugin-command-activity', activity); window.removeEventListener('plugin-context-menu', contextMenu)
|
|
56
|
-
window.removeEventListener('plugin-screens', declared); window.removeEventListener('plugin-screen-changed', changed)
|
|
57
|
-
}
|
|
58
|
-
}, [plugin.id])
|
|
59
|
-
const running = [...busy, ...Object.values(ownerBusy)]
|
|
60
|
-
// The face's own screens collapse into a single mount point: the plugin keeps
|
|
61
|
-
// every screen inside its own component and reports which one is active.
|
|
62
|
-
/** Ask the face to show a screen it declared; it may ignore a name it does not own. */
|
|
63
|
-
const openScreen = (name: string) => {
|
|
64
|
-
if (!screens.some(screen => screen.id === name)) return
|
|
65
|
-
setMenu(false)
|
|
66
|
-
window.dispatchEvent(new CustomEvent('plugin-show-screen', { detail: { pluginId: plugin.id, screen: name } }))
|
|
67
|
-
}
|
|
68
|
-
const move = (delta: number) => {
|
|
69
|
-
if (screens.length < 2) return
|
|
70
|
-
const current = Math.max(0, screens.findIndex(screen => screen.id === activeScreen))
|
|
71
|
-
openScreen(screens[(current + delta + screens.length) % screens.length].id)
|
|
72
|
-
}
|
|
73
|
-
const toggle = async () => {
|
|
74
|
-
setChanging(true); setError('')
|
|
75
|
-
try {
|
|
76
|
-
const response = await apiFetch(`${API_BASE}/api/plugins/${encodeURIComponent(plugin.id)}/activation`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: !enabled }) })
|
|
77
|
-
const result = await response.json()
|
|
78
|
-
if (result.activation) setEnabled(result.activation.enabled)
|
|
79
|
-
if (!response.ok || !result.success) throw new Error(result.compilation?.error || result.error || 'Activation could not be changed')
|
|
80
|
-
} catch (err) { setError(err instanceof Error ? err.message : 'Activation could not be changed') }
|
|
81
|
-
finally { setChanging(false) }
|
|
82
|
-
}
|
|
83
|
-
return <div className="card-scenes" data-state={enabled ? 'active' : 'inactive'} aria-busy={running.length > 0 || changing}
|
|
84
|
-
onContextMenu={event => { event.preventDefault(); setMenu(true) }}
|
|
85
|
-
onKeyDown={event => { if (event.key === 'Escape') setMenu(false); if (event.key === 'F10' && event.shiftKey) { event.preventDefault(); setMenu(true) } }}>
|
|
86
|
-
<div className="card-row card-scene-controls">
|
|
87
|
-
<span role="status" className="card-pill">{running.length ? `Running ${running.join(', ')}` : enabled ? 'Active' : 'Inactive'}</span>
|
|
88
|
-
<button type="button" className="card-button" role="switch" aria-checked={enabled} aria-label={`${card.title} on or off`} onClick={() => void toggle()} disabled={changing || running.length > 0}>{changing ? 'Updating…' : enabled ? 'Turn off' : 'Turn on'}</button>
|
|
89
|
-
<button type="button" className="card-link" aria-label="Previous screen" disabled={screens.length < 2} onClick={() => move(-1)}>←</button>
|
|
90
|
-
<button type="button" className="card-link" aria-label="Next screen" disabled={screens.length < 2} onClick={() => move(1)}>→</button>
|
|
91
|
-
{screens.length > 0 && <span className="card-pill" aria-label="Current screen">{screens.find(screen => screen.id === activeScreen)?.label}</span>}
|
|
92
|
-
{screens.length > 0 && <button type="button" className="card-link" aria-haspopup="menu" aria-expanded={menu} onClick={() => setMenu(!menu)}>Screens</button>}
|
|
93
|
-
</div>
|
|
94
|
-
{menu && screens.length > 0 && <div role="menu" aria-label={`${card.title} declared screens`} className="card-context-menu">
|
|
95
|
-
{screens.map(screen => <button role="menuitem" type="button" className="card-link" key={screen.id} aria-current={screen.id === activeScreen ? 'page' : undefined} onClick={() => openScreen(screen.id)}>{screen.label}</button>)}
|
|
96
|
-
</div>}
|
|
97
|
-
{error && <p role="alert" className="card-error">{error}</p>}
|
|
98
|
-
<ExecutionPicker />
|
|
99
|
-
<div className="card-scene-track" tabIndex={0} aria-label={`${card.title} interface`}>
|
|
100
|
-
<section className="card-scene" aria-label="Interface">{children}</section>
|
|
101
|
-
</div>
|
|
102
|
-
</div>
|
|
103
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { useRef, useState } from 'react'
|
|
2
|
-
import { apiFetch, API_BASE } from '../../../api/_base'
|
|
3
|
-
import { useRunner } from './runner-context'
|
|
4
|
-
import type { PluginExecution } from '../../../api/plugins'
|
|
5
|
-
|
|
6
|
-
type Agent = { id: string; name: string; installed: boolean; authenticated: boolean | 'unknown'; version?: string; versionVerified?: boolean }
|
|
7
|
-
type Node = { id: string; name: string; online: boolean; self: boolean; accessConfigured: boolean }
|
|
8
|
-
const DEFAULT: PluginExecution = { agent: 'direct', node: 'local', permissions: { read: true, write: false, send: false, destructive: false } }
|
|
9
|
-
const labels = { read: 'Read commands', write: 'Write commands', send: 'Send commands', destructive: 'Delete commands' }
|
|
10
|
-
let targetCache: { at: number; data: { nodes: Node[] } } | undefined
|
|
11
|
-
|
|
12
|
-
export function ExecutionPicker() {
|
|
13
|
-
const { execution, setExecution, busy } = useRunner()
|
|
14
|
-
const selected = execution || DEFAULT
|
|
15
|
-
const [open, setOpen] = useState(false)
|
|
16
|
-
const [nodes, setNodes] = useState<Node[]>([])
|
|
17
|
-
const [agents, setAgents] = useState<Agent[]>([])
|
|
18
|
-
const [loading, setLoading] = useState(false)
|
|
19
|
-
const [error, setError] = useState('')
|
|
20
|
-
const inspection = useRef(0)
|
|
21
|
-
const loadAgents = async (node: string) => {
|
|
22
|
-
const revision = ++inspection.current
|
|
23
|
-
setLoading(true); setAgents([]); setError('')
|
|
24
|
-
try {
|
|
25
|
-
const response = await apiFetch(`${API_BASE}/api/plugins/execution-targets?node=${encodeURIComponent(node)}`)
|
|
26
|
-
const value = await response.json()
|
|
27
|
-
if (!response.ok || !value.success) throw new Error(value.error || 'Could not inspect this node')
|
|
28
|
-
if (revision === inspection.current) setAgents(value.agents || [])
|
|
29
|
-
} catch (err) { if (revision === inspection.current) setError(err instanceof Error ? err.message : 'Could not inspect this node') }
|
|
30
|
-
finally { if (revision === inspection.current) setLoading(false) }
|
|
31
|
-
}
|
|
32
|
-
const show = async () => {
|
|
33
|
-
setOpen(!open)
|
|
34
|
-
if (open) return
|
|
35
|
-
setLoading(true); setError('')
|
|
36
|
-
try {
|
|
37
|
-
if (!targetCache || Date.now() - targetCache.at > 60000) {
|
|
38
|
-
const response = await apiFetch(`${API_BASE}/api/plugins/execution-targets`)
|
|
39
|
-
const data = await response.json()
|
|
40
|
-
if (!response.ok || !data.success) throw new Error(data.error || 'CLI agents is unavailable')
|
|
41
|
-
targetCache = { at: Date.now(), data }
|
|
42
|
-
}
|
|
43
|
-
setNodes(targetCache.data.nodes || [])
|
|
44
|
-
await loadAgents(selected.node)
|
|
45
|
-
} catch (err) { setError(err instanceof Error ? err.message : 'CLI agents is unavailable'); setLoading(false) }
|
|
46
|
-
}
|
|
47
|
-
return <div className="card-execution">
|
|
48
|
-
<button type="button" className="card-link" onClick={() => void show()} aria-expanded={open}>Agent, node & permissions</button>
|
|
49
|
-
{open && <fieldset className="card-form" disabled={loading || busy.size > 0}>
|
|
50
|
-
<legend>Command execution</legend>
|
|
51
|
-
<label>Mesh node<select value={selected.node} onChange={event => { const node = event.target.value; setExecution({ ...selected, node, agent: 'direct', cwd: undefined }); void loadAgents(node) }}>
|
|
52
|
-
<option value="local">This node</option>{nodes.filter(node => !node.self).map(node => <option key={node.id} value={node.id} disabled={!node.online || !node.accessConfigured}>{node.name}{!node.online ? ' · offline' : !node.accessConfigured ? ' · access needed' : ''}</option>)}
|
|
53
|
-
</select></label>
|
|
54
|
-
<label>CLI agent<select value={selected.agent} onChange={event => setExecution({ ...selected, agent: event.target.value, allowUnknownAuth: event.target.value !== 'direct' })}>
|
|
55
|
-
<option value="direct">Direct CLI</option>{agents.map(agent => <option key={agent.id} value={agent.id} disabled={!agent.installed || !agent.versionVerified || agent.authenticated === false}>{agent.name}{!agent.installed ? ' · not installed' : agent.authenticated === false ? ' · sign in needed' : agent.authenticated === 'unknown' ? ' · account unchecked' : ''}</option>)}
|
|
56
|
-
</select></label>
|
|
57
|
-
{selected.node !== 'local' && <label>Workspace on selected node<input value={selected.cwd || ''} placeholder="/absolute/workspace" onChange={event => setExecution({ ...selected, cwd: event.target.value })} /></label>}
|
|
58
|
-
<div className="card-permissions">{(Object.keys(labels) as Array<keyof typeof labels>).map(key => <label key={key}><input type="checkbox" role="switch" checked={selected.permissions[key]} onChange={event => setExecution({ ...selected, permissions: { ...selected.permissions, [key]: event.target.checked } })} />{labels[key]}</label>)}</div>
|
|
59
|
-
<button type="button" className="card-link" onClick={() => void loadAgents(selected.node)}>Refresh installed agents</button>
|
|
60
|
-
</fieldset>}
|
|
61
|
-
{loading && <p role="status" className="card-muted">Inspecting installed agents…</p>}
|
|
62
|
-
{error && <p role="alert" className="card-error">{error}</p>}
|
|
63
|
-
</div>
|
|
64
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { CardArg, CardCommand } from './contract'
|
|
2
|
-
|
|
3
|
-
export type ArgValues = Record<string, string | number | boolean | undefined>
|
|
4
|
-
|
|
5
|
-
/** Secret arguments are never put on the command line; they are managed in the drawer's Credentials. */
|
|
6
|
-
export const visibleArgs = (command: CardCommand) => command.args.filter(arg => !arg.secret)
|
|
7
|
-
|
|
8
|
-
export function missingRequired(command: CardCommand, values: ArgValues): string[] {
|
|
9
|
-
return visibleArgs(command).filter(arg => arg.required && (values[arg.name] === undefined || values[arg.name] === '')).map(arg => arg.label)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Positionals first, in declared order, then flags. Returns argv for `--json` runs. */
|
|
13
|
-
export function buildArgs(command: CardCommand, values: ArgValues, extra: string[] = []): string[] {
|
|
14
|
-
const argv: string[] = []
|
|
15
|
-
const flagged: string[] = []
|
|
16
|
-
for (const arg of visibleArgs(command)) {
|
|
17
|
-
const value = values[arg.name]
|
|
18
|
-
if (value === undefined || value === '' || value === false) continue
|
|
19
|
-
if (arg.positional) { argv.push(String(value)); continue }
|
|
20
|
-
if (!arg.flag) continue
|
|
21
|
-
if (arg.type === 'boolean') flagged.push(arg.flag)
|
|
22
|
-
else flagged.push(arg.flag, String(value))
|
|
23
|
-
}
|
|
24
|
-
return [...argv, ...flagged, ...extra, ...(command.output === 'json' ? ['--json'] : [])]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const needsConfirmation = (command: CardCommand) => command.effect === 'destructive' || command.effect === 'send'
|
|
28
|
-
export const defaultValue = (arg: CardArg) => arg.type === 'boolean' ? false : arg.type === 'choice' && arg.options?.length && arg.required ? arg.options[0] : ''
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
.card{--card-surface:var(--bg-secondary,#fff);--card-text:var(--text-primary,#111);--card-muted:var(--text-secondary,#555);--card-line:var(--border,#d4d4d8);position:relative;background:var(--card-surface);color:var(--card-text);border:1px solid var(--card-line);border-radius:var(--card-radius);padding:var(--card-gap);display:grid;gap:var(--card-gap);overflow:hidden}
|
|
2
|
-
.card h2,.card h3{margin:0}.card h2{font-size:1.15rem}.card h3{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--card-muted);margin-bottom:6px}
|
|
3
|
-
.card-header{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
|
|
4
|
-
.card-brand{display:flex;align-items:center;gap:12px}.card-logo{height:36px;max-width:140px;object-fit:contain}.card-logo-missing{width:36px;height:36px;border-radius:var(--card-radius);background:var(--card-color)}
|
|
5
|
-
.card-header-band{background:var(--card-color);color:var(--card-on-color);margin:calc(var(--card-gap)*-1) calc(var(--card-gap)*-1) 0;padding:var(--card-gap)}
|
|
6
|
-
.card-header-ledger-rule{border-bottom:3px double var(--card-line);padding-bottom:var(--card-gap)}
|
|
7
|
-
.card-header-tab-strip{border-bottom:2px solid var(--card-accent);padding-bottom:6px}
|
|
8
|
-
.card-header-badge-left{flex-direction:row-reverse;justify-content:flex-end}
|
|
9
|
-
.card-badge{display:inline-block;margin:4px 6px 0 0;padding:1px 8px;border-radius:var(--card-radius);background:var(--card-accent);color:var(--card-on-accent);font-size:.7rem;font-weight:600}
|
|
10
|
-
.card-badge-quiet{background:transparent;color:var(--card-muted);border:1px solid var(--card-line)}
|
|
11
|
-
.card-description{margin:0}.card-muted{color:var(--card-muted);font-size:.85rem;margin:2px 0}
|
|
12
|
-
/* The face supplies its own layout; the host only gives it a surface. */
|
|
13
|
-
.card-bespoke .card-face{display:block;min-width:0}
|
|
14
|
-
.card-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.card-kv{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin:8px 0 0}.card-kv dt{font-size:.7rem;color:var(--card-muted);text-transform:uppercase}.card-kv dd{margin:0;font-weight:600;overflow-wrap:anywhere}
|
|
15
|
-
.card-pill{padding:1px 8px;border-radius:999px;font-size:.75rem;font-weight:600;border:1px solid currentColor}.card-pill-ok{color:#047857}.card-pill-warn{color:#b45309}.card-pill-error{color:#b91c1c}.card-pill-neutral{color:var(--card-muted)}
|
|
16
|
-
.card-button{background:var(--card-color);color:var(--card-on-color);border:0;border-radius:var(--card-radius);padding:6px 12px;font:inherit;cursor:pointer}.card-button:disabled{opacity:.5;cursor:not-allowed}.card-button-quiet{background:transparent;color:var(--card-text);border:1px solid var(--card-line)}
|
|
17
|
-
.card-link{background:none;border:0;color:var(--card-color);cursor:pointer;font:inherit;text-decoration:underline;padding:0}
|
|
18
|
-
.card :focus-visible{outline:2px solid var(--card-accent);outline-offset:2px}
|
|
19
|
-
.card-code,.card-pre{font-family:ui-monospace,monospace;font-size:.78rem;background:rgba(127,127,127,.12);border-radius:var(--card-radius);padding:4px 8px;overflow-x:auto}.card-pre{white-space:pre-wrap;max-height:260px;overflow:auto}
|
|
20
|
-
.card-commands,.card-list{list-style:none;margin:0;padding:0;display:grid;gap:10px}.card-commands li{border-top:1px solid var(--card-line);padding-top:8px}
|
|
21
|
-
.card-form{display:grid;gap:8px;margin-top:8px}.card-form label{display:grid;gap:2px;font-size:.8rem}.card-form input,.card-form select{font:inherit;padding:4px 6px;border:1px solid var(--card-line);border-radius:var(--card-radius);background:transparent;color:inherit}
|
|
22
|
-
.card-scroll{overflow-x:auto}.card-table{width:100%;border-collapse:collapse;font-size:.85rem}.card-table th{text-align:left;font-size:.7rem;text-transform:uppercase;color:var(--card-muted);border-bottom:2px solid var(--card-line);padding:4px 8px}.card-table td{padding:5px 8px;border-bottom:1px solid var(--card-line)}
|
|
23
|
-
.card-gauge{height:10px;background:rgba(127,127,127,.2);border-radius:var(--card-radius);overflow:hidden}.card-gauge span{display:block;height:100%;background:var(--card-accent)}
|
|
24
|
-
.card-error{color:#b91c1c;margin:6px 0}.card-footer{font-size:.72rem;color:var(--card-muted)}.card-footer a{color:inherit}
|
|
25
|
-
.card-drawer{position:absolute;top:0;right:0;bottom:0;width:min(420px,100%);background:var(--card-surface);border-left:1px solid var(--card-line);box-shadow:-8px 0 24px rgba(0,0,0,.15);display:flex;flex-direction:column;z-index:2}
|
|
26
|
-
.card-drawer-head{display:flex;justify-content:space-between;padding:var(--card-gap);border-bottom:1px solid var(--card-line)}.card-drawer-body{padding:var(--card-gap);overflow:auto;flex:1}
|
|
27
|
-
.card-tabs{display:flex;border-bottom:1px solid var(--card-line)}.card-tabs button{flex:1;background:none;border:0;border-bottom:3px solid transparent;padding:8px;font:inherit;color:var(--card-muted);cursor:pointer}.card-tabs button[aria-selected=true]{color:var(--card-text);border-bottom-color:var(--card-accent)}
|
|
28
|
-
@media (prefers-reduced-motion:no-preference){.card-drawer{animation:card-slide .18s ease-out}@keyframes card-slide{from{transform:translateX(24px);opacity:0}}}
|
|
29
|
-
.card-scene-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:thin;gap:var(--card-gap);overscroll-behavior-x:contain}.card-scene{flex:0 0 100%;min-width:0;scroll-snap-align:start;padding:4px}.card-scene-controls{margin-bottom:var(--card-gap)}.card-scenes[data-state=inactive] .card-scene-track{opacity:.65}.card-scenes[aria-busy=true] .card-pill{animation:card-running 1.1s ease-in-out infinite}@keyframes card-running{50%{box-shadow:0 0 0 3px color-mix(in srgb,var(--card-color) 28%,transparent)}}@media(prefers-reduced-motion:reduce){.card-scenes[aria-busy=true] .card-pill{animation:none}.card-scene-track{scroll-behavior:auto}}
|
|
30
|
-
.card-scene-track{position:relative}.card-context-menu{display:grid;gap:8px;padding:12px;border:1px solid var(--card-line);border-radius:var(--card-radius);background:var(--card-surface);box-shadow:0 8px 24px #0002}.card-context-menu button{text-align:left}
|
|
31
|
-
.card.card-owned{border:1px solid var(--card-line);background:var(--card-surface);border-radius:var(--card-radius);padding:var(--card-gap);box-shadow:0 1px 3px rgba(0,0,0,0.06)}.card-owned .card-link{color:var(--text-primary,#111)}.card-permissions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.card-permissions label{display:flex;align-items:center;gap:8px}.card-execution{margin-bottom:var(--card-gap)}
|
|
32
|
-
.card-scenes,.card-scene-track{min-width:0;max-width:100%}.card-scene{box-sizing:border-box}.card-execution fieldset{min-inline-size:0;box-sizing:border-box;width:100%}.card-execution select,.card-execution input:not([type=checkbox]){min-width:0;width:100%;box-sizing:border-box}.card-permissions label{white-space:normal}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Card, CardCommand } from './contract'
|
|
2
|
-
|
|
3
|
-
/** The exact line an agent runs; shown on the card so config and CLI never drift apart. */
|
|
4
|
-
export function cliLine(card: Card, command: CardCommand, cliName: string): string {
|
|
5
|
-
const parts = [`total-recall`, cliName || card.id, command.name]
|
|
6
|
-
for (const arg of command.args) {
|
|
7
|
-
if (arg.secret) continue // secrets go on stdin, never in the line an agent logs
|
|
8
|
-
const token = arg.positional ? `<${arg.name}>` : `${arg.flag}${arg.type === 'boolean' ? '' : ` <${arg.name}>`}`
|
|
9
|
-
parts.push(arg.required ? token : `[${token}]`)
|
|
10
|
-
}
|
|
11
|
-
if (command.output === 'json') parts.push('--json')
|
|
12
|
-
return parts.join(' ')
|
|
13
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type { PluginInfo } from '../../../api/plugins'
|
|
2
|
-
|
|
3
|
-
export type ArgType = 'string' | 'number' | 'boolean' | 'choice'
|
|
4
|
-
export type Effect = 'read' | 'write' | 'destructive' | 'send'
|
|
5
|
-
|
|
6
|
-
export interface CardArg { name: string; flag?: string; positional?: boolean; label: string; type: ArgType; options?: Array<string | number>; required?: boolean; secret?: boolean }
|
|
7
|
-
export interface CardCommand { name: string; description?: string; args: CardArg[]; effect: Effect; output: 'json' | 'text'; timeoutMs?: number }
|
|
8
|
-
export interface CardBranding {
|
|
9
|
-
name: string; color: string; accent: string; font: string; density: 'compact' | 'comfortable' | 'spacious'
|
|
10
|
-
radius: 'none' | 'sm' | 'md' | 'lg' | 'pill'; header: 'plain' | 'ledger-rule' | 'band' | 'tab-strip' | 'badge-left'
|
|
11
|
-
badge?: string; logo?: string; provider?: { name: string; url?: string }
|
|
12
|
-
}
|
|
13
|
-
export interface CardWorkspace {
|
|
14
|
-
component?: string;
|
|
15
|
-
tabs?: string[];
|
|
16
|
-
permissions?: string[];
|
|
17
|
-
}
|
|
18
|
-
/** The plugin's own card face. Layout belongs to the plugin, never to a shared template. */
|
|
19
|
-
export interface CardFace { id: string; tag: string; module: string; description?: string }
|
|
20
|
-
export interface Card {
|
|
21
|
-
id: string; title: string; description: string; face?: CardFace
|
|
22
|
-
commands: Map<string, CardCommand>; branding: CardBranding; status?: { command: string; refreshSeconds: number }
|
|
23
|
-
distribution?: { tier: string; license: string }
|
|
24
|
-
workspace?: CardWorkspace
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const FONTS = ['system', 'inter', 'ibm-plex', 'space-grotesk', 'jetbrains-mono', 'dm-sans', 'outfit', 'fraunces', 'newsreader', 'chivo-mono']
|
|
28
|
-
const HEX = /^#[0-9a-fA-F]{6}$/
|
|
29
|
-
/** Manifest JSON is untyped input; every field is checked before it is used. */
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
-
export type Loose = any
|
|
32
|
-
const obj = (v: unknown): Record<string, Loose> => (v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, Loose> : {})
|
|
33
|
-
|
|
34
|
-
function normalizeArg(raw: unknown): CardArg | null {
|
|
35
|
-
const a = obj(raw)
|
|
36
|
-
if (typeof a.name !== 'string' || typeof a.label !== 'string') return null
|
|
37
|
-
const type = ['string', 'number', 'boolean', 'choice'].includes(a.type) ? a.type : 'string'
|
|
38
|
-
return { name: a.name, flag: typeof a.flag === 'string' ? a.flag : undefined, positional: a.positional === true, label: a.label, type, options: Array.isArray(a.options) ? a.options : undefined, required: a.required === true, secret: a.secret === true }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function normalizeCard(plugin: PluginInfo): Card {
|
|
42
|
-
const manifest = obj(plugin.manifest)
|
|
43
|
-
const commands = new Map<string, CardCommand>()
|
|
44
|
-
for (const sub of plugin.cli?.subcommands || []) {
|
|
45
|
-
const declared = obj(manifest.cli?.subcommands?.find((item: Loose) => item?.name === sub.name))
|
|
46
|
-
commands.set(sub.name, {
|
|
47
|
-
name: sub.name, description: sub.description,
|
|
48
|
-
args: (Array.isArray(declared.args) ? declared.args : []).map(normalizeArg).filter(Boolean) as CardArg[],
|
|
49
|
-
// A grouped command such as `config set` may write. Only explicit read declarations are safe.
|
|
50
|
-
effect: ['read', 'write', 'destructive', 'send'].includes(declared.effect) ? declared.effect : 'write',
|
|
51
|
-
output: declared.output === 'text' ? 'text' : 'json',
|
|
52
|
-
timeoutMs: Number.isFinite(declared.timeout_ms) ? declared.timeout_ms : undefined,
|
|
53
|
-
})
|
|
54
|
-
}
|
|
55
|
-
const b = obj(manifest.branding)
|
|
56
|
-
const branding: CardBranding = {
|
|
57
|
-
name: typeof b.name === 'string' ? b.name : plugin.name,
|
|
58
|
-
color: HEX.test(b.color) ? b.color : '#475569',
|
|
59
|
-
accent: HEX.test(b.accent) ? b.accent : HEX.test(b.color) ? b.color : '#0ea5e9',
|
|
60
|
-
font: FONTS.includes(b.font) ? b.font : 'system',
|
|
61
|
-
density: ['compact', 'comfortable', 'spacious'].includes(b.density) ? b.density : 'comfortable',
|
|
62
|
-
radius: ['none', 'sm', 'md', 'lg', 'pill'].includes(b.radius) ? b.radius : 'md',
|
|
63
|
-
header: ['plain', 'ledger-rule', 'band', 'tab-strip', 'badge-left'].includes(b.header) ? b.header : 'plain',
|
|
64
|
-
badge: typeof b.badge === 'string' ? b.badge : undefined,
|
|
65
|
-
logo: typeof b.logo === 'string' ? b.logo : undefined,
|
|
66
|
-
provider: obj(b.provider).name ? { name: String(b.provider.name), url: typeof b.provider.url === 'string' ? b.provider.url : undefined } : undefined,
|
|
67
|
-
}
|
|
68
|
-
const status = obj(manifest.status)
|
|
69
|
-
const statusCommand = typeof status.command === 'string' && commands.has(status.command) ? status.command : undefined
|
|
70
|
-
// The face is the plugin's own component; there is no shared layout to fall back to.
|
|
71
|
-
// Only `kind: 'card'` is the face. A plugin may declare any number of
|
|
72
|
-
// `kind: 'panel'` screens, and one declared before the card is still a
|
|
73
|
-
// secondary screen, never the face.
|
|
74
|
-
const elements: Loose[] = Array.isArray(manifest.ui?.elements) ? manifest.ui.elements : (Array.isArray(plugin.ui?.elements) ? plugin.ui.elements : [])
|
|
75
|
-
const declared = elements.find((element: Loose) => element && typeof element === 'object' && element.kind === 'card')
|
|
76
|
-
const face: CardFace | undefined = declared && typeof declared.id === 'string' && typeof declared.tag === 'string' && typeof declared.module === 'string'
|
|
77
|
-
? { id: declared.id, tag: declared.tag, module: declared.module, description: typeof declared.description === 'string' ? declared.description : undefined }
|
|
78
|
-
: undefined
|
|
79
|
-
const ws = obj(manifest.workspace)
|
|
80
|
-
const workspace: CardWorkspace | undefined = ws && (ws.component || Array.isArray(ws.tabs)) ? {
|
|
81
|
-
component: typeof ws.component === 'string' ? ws.component : undefined,
|
|
82
|
-
tabs: Array.isArray(ws.tabs) ? ws.tabs.filter((t: unknown) => typeof t === 'string') : undefined,
|
|
83
|
-
permissions: Array.isArray(ws.permissions) ? ws.permissions.filter((p: unknown) => typeof p === 'string') : undefined,
|
|
84
|
-
} : undefined
|
|
85
|
-
return {
|
|
86
|
-
id: plugin.id, title: branding.name, description: plugin.description, face, commands, branding,
|
|
87
|
-
status: statusCommand ? { command: statusCommand, refreshSeconds: Number.isInteger(status.refresh_seconds) ? Math.min(86400, Math.max(5, status.refresh_seconds)) : 30 } : undefined,
|
|
88
|
-
distribution: obj(manifest.distribution).tier ? { tier: manifest.distribution.tier, license: manifest.distribution.license } : undefined,
|
|
89
|
-
workspace,
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { Loose } from './contract'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* How a scalar is rendered. This is presentation owned by the card face that
|
|
5
|
-
* asks for it — not a template primitive. The layout vocabulary (`CardRegion`,
|
|
6
|
-
* `CardColumn`, `CellFormat`) that once shared these names is deleted.
|
|
7
|
-
*/
|
|
8
|
-
export type CellFormat = 'text' | 'number' | 'money' | 'percent' | 'bytes' | 'date' | 'datetime'
|
|
9
|
-
|
|
10
|
-
export type Tone = 'ok' | 'warn' | 'error' | 'neutral'
|
|
11
|
-
const GOOD = /^(ok|healthy|active|connected|ready|running|verified|delivered|sent|passed|pass|up|online|true|complete|completed)$/i
|
|
12
|
-
const BAD = /^(error|failed|fail|down|offline|rejected|bounced|blocked|false|expired|denied)$/i
|
|
13
|
-
const WARN = /^(warn|warning|pending|queued|degraded|unknown|stale|paused|partial|throttled)$/i
|
|
14
|
-
export const toneOf = (value: unknown): Tone => { const s = String(value); return GOOD.test(s) ? 'ok' : BAD.test(s) ? 'error' : WARN.test(s) ? 'warn' : 'neutral' }
|
|
15
|
-
|
|
16
|
-
export function formatCell(value: unknown, format: CellFormat = 'text'): string {
|
|
17
|
-
if (value === null || value === undefined || value === '') return '—'
|
|
18
|
-
if (typeof value === 'object') return JSON.stringify(value)
|
|
19
|
-
const n = Number(value)
|
|
20
|
-
switch (format) {
|
|
21
|
-
case 'number': return Number.isFinite(n) ? n.toLocaleString() : String(value)
|
|
22
|
-
case 'money': return Number.isFinite(n) ? n.toLocaleString(undefined, { style: 'currency', currency: 'USD' }) : String(value)
|
|
23
|
-
case 'percent': return Number.isFinite(n) ? `${Math.round((n <= 1 ? n * 100 : n) * 10) / 10}%` : String(value)
|
|
24
|
-
case 'bytes': {
|
|
25
|
-
if (!Number.isFinite(n)) return String(value)
|
|
26
|
-
const units = ['B', 'KB', 'MB', 'GB', 'TB']; let v = n, i = 0
|
|
27
|
-
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
|
28
|
-
return `${i === 0 ? v : v.toFixed(1)} ${units[i]}`
|
|
29
|
-
}
|
|
30
|
-
case 'date': case 'datetime': {
|
|
31
|
-
const d = new Date(String(value))
|
|
32
|
-
if (Number.isNaN(d.getTime())) return String(value)
|
|
33
|
-
return format === 'date' ? d.toLocaleDateString() : d.toLocaleString()
|
|
34
|
-
}
|
|
35
|
-
default: return String(value)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Rows may arrive as a bare array or wrapped under a common key. */
|
|
40
|
-
export function rowsOf(data: Loose): Loose[] {
|
|
41
|
-
if (Array.isArray(data)) return data
|
|
42
|
-
for (const key of ['rows', 'items', 'results', 'data', 'entries', 'records']) if (Array.isArray(data?.[key])) return data[key]
|
|
43
|
-
return []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function scalarsOf(data: Loose): Array<[string, string]> {
|
|
47
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) return []
|
|
48
|
-
return Object.entries(data).filter(([, v]) => v === null || ['string', 'number', 'boolean'].includes(typeof v)).map(([k, v]) => [k, v === null ? '—' : String(v)])
|
|
49
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react'
|
|
2
|
-
import type { ArgValues } from './args'
|
|
3
|
-
import type { Card, Loose } from './contract'
|
|
4
|
-
import type { PluginExecution } from '../../../api/plugins'
|
|
5
|
-
|
|
6
|
-
export interface RunResult { ok: boolean; data?: Loose; text: string; error?: string; ms: number }
|
|
7
|
-
export interface ActivityEntry { at: string; command: string; effect: string; ok: boolean; ms: number }
|
|
8
|
-
export interface Runner { card: Card; run: (command: string, values?: ArgValues, extra?: string[], stdin?: string) => Promise<RunResult>; activity: ActivityEntry[]; busy: Set<string>; execution: PluginExecution | null; setExecution: (value: PluginExecution | null) => void }
|
|
9
|
-
|
|
10
|
-
export const RunnerContext = createContext<Runner | null>(null)
|
|
11
|
-
export const useRunner = () => { const value = useContext(RunnerContext); if (!value) throw new Error('Card widgets must render inside a CardRunner'); return value }
|