@try-works/dsh-tool-mode 0.0.0-stage → 0.5.44
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/README.md +299 -2
- package/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
- package/bin/dsh-tool-mode.mjs +211 -0
- package/builtins.lock.json +26 -0
- package/catalog/desert-ant-models.json +693 -0
- package/catalog/playbooks.json +531 -0
- package/client/client.model-tools.js +1910 -0
- package/client/index.js +4039 -0
- package/cordis.patch.yml +39 -0
- package/lib/adapters/index.d.ts +61 -0
- package/lib/adapters/index.d.ts.map +1 -0
- package/lib/adapters/index.js +82 -0
- package/lib/adapters/types.d.ts +106 -0
- package/lib/adapters/types.d.ts.map +1 -0
- package/lib/adapters/types.js +85 -0
- package/lib/approval.d.ts +189 -0
- package/lib/approval.d.ts.map +1 -0
- package/lib/approval.js +510 -0
- package/lib/asset-acquisition.d.ts +149 -0
- package/lib/asset-acquisition.d.ts.map +1 -0
- package/lib/asset-acquisition.js +175 -0
- package/lib/authority.d.ts +185 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +223 -0
- package/lib/bindings.d.ts +144 -0
- package/lib/bindings.d.ts.map +1 -0
- package/lib/bindings.js +314 -0
- package/lib/builtins.d.ts +27 -0
- package/lib/builtins.d.ts.map +1 -0
- package/lib/builtins.js +37 -0
- package/lib/capability.d.ts +58 -0
- package/lib/capability.d.ts.map +1 -0
- package/lib/capability.js +192 -0
- package/lib/capture-path.d.ts +97 -0
- package/lib/capture-path.d.ts.map +1 -0
- package/lib/capture-path.js +152 -0
- package/lib/catalog.d.ts +37 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +274 -0
- package/lib/contract-payload.d.ts +39 -0
- package/lib/contract-payload.d.ts.map +1 -0
- package/lib/contract-payload.js +56 -0
- package/lib/custom-tools/compile.d.ts +25 -0
- package/lib/custom-tools/compile.d.ts.map +1 -0
- package/lib/custom-tools/compile.js +61 -0
- package/lib/custom-tools/composite-executor.d.ts +42 -0
- package/lib/custom-tools/composite-executor.d.ts.map +1 -0
- package/lib/custom-tools/composite-executor.js +113 -0
- package/lib/custom-tools/dispatch.d.ts +28 -0
- package/lib/custom-tools/dispatch.d.ts.map +1 -0
- package/lib/custom-tools/dispatch.js +34 -0
- package/lib/custom-tools/http-executor.d.ts +48 -0
- package/lib/custom-tools/http-executor.d.ts.map +1 -0
- package/lib/custom-tools/http-executor.js +241 -0
- package/lib/custom-tools/index.d.ts +12 -0
- package/lib/custom-tools/index.d.ts.map +1 -0
- package/lib/custom-tools/index.js +12 -0
- package/lib/custom-tools/mcp-surface.d.ts +14 -0
- package/lib/custom-tools/mcp-surface.d.ts.map +1 -0
- package/lib/custom-tools/mcp-surface.js +41 -0
- package/lib/custom-tools/prompt-executor.d.ts +39 -0
- package/lib/custom-tools/prompt-executor.d.ts.map +1 -0
- package/lib/custom-tools/prompt-executor.js +59 -0
- package/lib/custom-tools/registry.d.ts +69 -0
- package/lib/custom-tools/registry.d.ts.map +1 -0
- package/lib/custom-tools/registry.js +130 -0
- package/lib/custom-tools/schema.d.ts +26 -0
- package/lib/custom-tools/schema.d.ts.map +1 -0
- package/lib/custom-tools/schema.js +375 -0
- package/lib/custom-tools/service.d.ts +90 -0
- package/lib/custom-tools/service.d.ts.map +1 -0
- package/lib/custom-tools/service.js +203 -0
- package/lib/custom-tools/store.d.ts +61 -0
- package/lib/custom-tools/store.d.ts.map +1 -0
- package/lib/custom-tools/store.js +240 -0
- package/lib/custom-tools/types.d.ts +131 -0
- package/lib/custom-tools/types.d.ts.map +1 -0
- package/lib/custom-tools/types.js +11 -0
- package/lib/decision-combine.d.ts +113 -0
- package/lib/decision-combine.d.ts.map +1 -0
- package/lib/decision-combine.js +156 -0
- package/lib/decision-evaluate.d.ts +185 -0
- package/lib/decision-evaluate.d.ts.map +1 -0
- package/lib/decision-evaluate.js +227 -0
- package/lib/deployment-coordinator.d.ts +213 -0
- package/lib/deployment-coordinator.d.ts.map +1 -0
- package/lib/deployment-coordinator.js +274 -0
- package/lib/diagnostics.d.ts +169 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +136 -0
- package/lib/discovery-contracts.d.ts +14 -0
- package/lib/discovery-contracts.d.ts.map +1 -0
- package/lib/discovery-contracts.js +90 -0
- package/lib/discovery-describe.d.ts +35 -0
- package/lib/discovery-describe.d.ts.map +1 -0
- package/lib/discovery-describe.js +99 -0
- package/lib/embedding-index.d.ts +55 -0
- package/lib/embedding-index.d.ts.map +1 -0
- package/lib/embedding-index.js +126 -0
- package/lib/extension-dependencies.d.ts +355 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +987 -0
- package/lib/extension-dispatch.d.ts +150 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +178 -0
- package/lib/extension-page-state.d.ts +69 -0
- package/lib/extension-page-state.d.ts.map +1 -0
- package/lib/extension-page-state.js +82 -0
- package/lib/extension-registrar.d.ts +116 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +144 -0
- package/lib/extensions-api.d.ts +346 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +57 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +69 -0
- package/lib/extensions-service.d.ts +568 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +2507 -0
- package/lib/harness-budgets.d.ts +158 -0
- package/lib/harness-budgets.d.ts.map +1 -0
- package/lib/harness-budgets.js +152 -0
- package/lib/harness-llm.d.ts +138 -0
- package/lib/harness-llm.d.ts.map +1 -0
- package/lib/harness-llm.js +218 -0
- package/lib/harness-serving.d.ts +115 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +176 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +407 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +140 -0
- package/lib/index.d.ts +546 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +5197 -0
- package/lib/invocation-accounting.d.ts +169 -0
- package/lib/invocation-accounting.d.ts.map +1 -0
- package/lib/invocation-accounting.js +148 -0
- package/lib/jev.d.ts +218 -0
- package/lib/jev.d.ts.map +1 -0
- package/lib/jev.js +366 -0
- package/lib/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/license-basis.d.ts +29 -0
- package/lib/license-basis.d.ts.map +1 -0
- package/lib/license-basis.js +58 -0
- package/lib/managed-workers.d.ts +158 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +269 -0
- package/lib/mcp/auth-selection.d.ts +35 -0
- package/lib/mcp/auth-selection.d.ts.map +1 -0
- package/lib/mcp/auth-selection.js +64 -0
- package/lib/mcp/client-grant.d.ts +89 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +63 -0
- package/lib/mcp/credential-cli.d.ts +38 -0
- package/lib/mcp/credential-cli.d.ts.map +1 -0
- package/lib/mcp/credential-cli.js +148 -0
- package/lib/mcp/credential-store.d.ts +143 -0
- package/lib/mcp/credential-store.d.ts.map +1 -0
- package/lib/mcp/credential-store.js +461 -0
- package/lib/mcp/export-policy.d.ts +25 -0
- package/lib/mcp/export-policy.d.ts.map +1 -0
- package/lib/mcp/export-policy.js +31 -0
- package/lib/mcp/host-binding.d.ts +144 -0
- package/lib/mcp/host-binding.d.ts.map +1 -0
- package/lib/mcp/host-binding.js +280 -0
- package/lib/mcp/http.d.ts +180 -0
- package/lib/mcp/http.d.ts.map +1 -0
- package/lib/mcp/http.js +602 -0
- package/lib/mcp/oauth.d.ts +149 -0
- package/lib/mcp/oauth.d.ts.map +1 -0
- package/lib/mcp/oauth.js +396 -0
- package/lib/mcp/protocol.d.ts +329 -0
- package/lib/mcp/protocol.d.ts.map +1 -0
- package/lib/mcp/protocol.js +459 -0
- package/lib/mcp/rate-limit.d.ts +68 -0
- package/lib/mcp/rate-limit.d.ts.map +1 -0
- package/lib/mcp/rate-limit.js +114 -0
- package/lib/mcp/sdk-v2.d.ts +17 -0
- package/lib/mcp/sdk-v2.d.ts.map +1 -0
- package/lib/mcp/sdk-v2.js +204 -0
- package/lib/mcp/server.d.ts +149 -0
- package/lib/mcp/server.d.ts.map +1 -0
- package/lib/mcp/server.js +302 -0
- package/lib/mcp/stdio.d.ts +80 -0
- package/lib/mcp/stdio.d.ts.map +1 -0
- package/lib/mcp/stdio.js +184 -0
- package/lib/mcp/subscriptions.d.ts +177 -0
- package/lib/mcp/subscriptions.d.ts.map +1 -0
- package/lib/mcp/subscriptions.js +288 -0
- package/lib/mcp-status.d.ts +96 -0
- package/lib/mcp-status.d.ts.map +1 -0
- package/lib/mcp-status.js +70 -0
- package/lib/mcp-usage.d.ts +110 -0
- package/lib/mcp-usage.d.ts.map +1 -0
- package/lib/mcp-usage.js +189 -0
- package/lib/model-qualification.d.ts +192 -0
- package/lib/model-qualification.d.ts.map +1 -0
- package/lib/model-qualification.js +178 -0
- package/lib/planning.d.ts +256 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +471 -0
- package/lib/playbook.d.ts +42 -0
- package/lib/playbook.d.ts.map +1 -0
- package/lib/playbook.js +65 -0
- package/lib/prepared-assets.d.ts +269 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +471 -0
- package/lib/provider-bridge.d.ts +107 -0
- package/lib/provider-bridge.d.ts.map +1 -0
- package/lib/provider-bridge.js +423 -0
- package/lib/ptc-authority.d.ts +154 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +242 -0
- package/lib/ptc-confinement.d.ts +60 -0
- package/lib/ptc-confinement.d.ts.map +1 -0
- package/lib/ptc-confinement.js +212 -0
- package/lib/ptc-runtime.d.ts +166 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +231 -0
- package/lib/publication.d.ts +161 -0
- package/lib/publication.d.ts.map +1 -0
- package/lib/publication.js +181 -0
- package/lib/receipt-store.d.ts +57 -0
- package/lib/receipt-store.d.ts.map +1 -0
- package/lib/receipt-store.js +143 -0
- package/lib/receipt.d.ts +231 -0
- package/lib/receipt.d.ts.map +1 -0
- package/lib/receipt.js +165 -0
- package/lib/registrar.d.ts +75 -0
- package/lib/registrar.d.ts.map +1 -0
- package/lib/registrar.js +152 -0
- package/lib/resource-governance.d.ts +204 -0
- package/lib/resource-governance.d.ts.map +1 -0
- package/lib/resource-governance.js +213 -0
- package/lib/server-lifecycle.d.ts +175 -0
- package/lib/server-lifecycle.d.ts.map +1 -0
- package/lib/server-lifecycle.js +269 -0
- package/lib/skills.d.ts +31 -0
- package/lib/skills.d.ts.map +1 -0
- package/lib/skills.js +170 -0
- package/lib/speech-bridge.d.ts +74 -0
- package/lib/speech-bridge.d.ts.map +1 -0
- package/lib/speech-bridge.js +115 -0
- package/lib/state.d.ts +100 -0
- package/lib/state.d.ts.map +1 -0
- package/lib/state.js +160 -0
- package/lib/title-proposal.d.ts +194 -0
- package/lib/title-proposal.d.ts.map +1 -0
- package/lib/title-proposal.js +244 -0
- package/lib/tool-inventory.d.ts +90 -0
- package/lib/tool-inventory.d.ts.map +1 -0
- package/lib/tool-inventory.js +102 -0
- package/lib/tool-mode-run.d.ts +310 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +411 -0
- package/lib/tool-sources.d.ts +80 -0
- package/lib/tool-sources.d.ts.map +1 -0
- package/lib/tool-sources.js +123 -0
- package/lib/tool-usage-projection.d.ts +390 -0
- package/lib/tool-usage-projection.d.ts.map +1 -0
- package/lib/tool-usage-projection.js +1038 -0
- package/lib/tool-usage-report.d.ts +153 -0
- package/lib/tool-usage-report.d.ts.map +1 -0
- package/lib/tool-usage-report.js +274 -0
- package/lib/transport.d.ts +158 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +332 -0
- package/lib/types.d.ts +134 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +8 -0
- package/lib/update-surface.d.ts +79 -0
- package/lib/update-surface.d.ts.map +1 -0
- package/lib/update-surface.js +224 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +27 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.d.ts +13 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contract-metadata.js +85 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +87 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +53 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +133 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +64 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +121 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +145 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +33 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +33 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +191 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +137 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +349 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +274 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +932 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +299 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +395 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +839 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts +44 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/tar-gzip-source.js +255 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.d.ts +221 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/licensing.js +188 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +128 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.d.ts +41 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/test-host.js +52 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +157 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +688 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +54 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
- package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +460 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +647 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +61 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +1106 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +205 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +119 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/images.js +201 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +345 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1465 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +231 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +435 -0
- package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
- package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
- package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
- package/package.json +127 -4
- package/remote/spec.mjs +993 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
package/client/index.js
ADDED
|
@@ -0,0 +1,4039 @@
|
|
|
1
|
+
// dsh-tool-mode browser half: the Settings -> Model as Tool page.
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript by necessity: DSH's client module system serves this file to
|
|
4
|
+
// the page as a lazy-CJS bundle, so it IS the shipped artifact. The loader id is
|
|
5
|
+
// the PACKAGE name (the web client looks that up); the Cordis entry id
|
|
6
|
+
// (`model-tools`) is the settings namespace this page configures.
|
|
7
|
+
//
|
|
8
|
+
// Only 'react' is requested. Controls and styles are written here, using only
|
|
9
|
+
// theme tokens that exist on the shipped settings pages: a token that does not
|
|
10
|
+
// exist is dropped by the browser and the page degrades into undifferentiated
|
|
11
|
+
// prose.
|
|
12
|
+
//
|
|
13
|
+
// The model catalog is GENERATED INTO THIS FILE at build time (see
|
|
14
|
+
// client/index.template.js and scripts/gen-client-catalog.mjs): it carries the
|
|
15
|
+
// same entries model_tools reports, with the platform verdict resolved on the
|
|
16
|
+
// host that builds the package.
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: '@try-works/dsh-tool-mode',
|
|
19
|
+
factory(require) {
|
|
20
|
+
var module = { exports: {} }
|
|
21
|
+
var exports = module.exports
|
|
22
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
23
|
+
|
|
24
|
+
const React = require('react')
|
|
25
|
+
const h = React.createElement
|
|
26
|
+
|
|
27
|
+
// The chat cards, INLINED at build time from client/toolmode-toolview.js. A plugin's
|
|
28
|
+
// client module service serves only the declared bundle file, so a sibling chunk is a
|
|
29
|
+
// 404 and the cards would silently fall back to the generic tool card.
|
|
30
|
+
/**
|
|
31
|
+
* The chat tool view for every Tool Mode model call.
|
|
32
|
+
*
|
|
33
|
+
* A generic tool card already lists name, arguments and the JSON result, but it
|
|
34
|
+
* cannot say WHICH extension and model answered, whether the call ran on the
|
|
35
|
+
* device or a remote harness, or what the receipt was. Those are the fields a
|
|
36
|
+
* reader of this plugin needs, and they are all present in the result envelope.
|
|
37
|
+
*
|
|
38
|
+
* The harness dispatches `tool.call.toolview` by TOOL NAME (see the slot
|
|
39
|
+
* declaration in `@deepseek-ai/dsh-client-ui-tool`), so this module registers one
|
|
40
|
+
* key per tool the plugin can expose. A tool name the plugin does not know falls
|
|
41
|
+
* back to the generic card, which is the correct outcome: this card must never
|
|
42
|
+
* pretend to recognize a call it does not.
|
|
43
|
+
*
|
|
44
|
+
* THIS FILE IS INLINED INTO client/index.js by `scripts/gen-client-catalog.mjs`.
|
|
45
|
+
* It is not a separate chunk: the client module service serves only the bundle file
|
|
46
|
+
* a plugin declares as its client entry, so an unreferenced sibling chunk answers
|
|
47
|
+
* 404 and the view silently falls back to the generic card. Keeping the source
|
|
48
|
+
* separate here and injecting it at build time gives one testable module and one
|
|
49
|
+
* served file.
|
|
50
|
+
*
|
|
51
|
+
* Renders with plain `React.createElement` and inline styles, so it needs no CSS
|
|
52
|
+
* asset and cannot break the page stylesheet.
|
|
53
|
+
* @param createElement - React's createElement, supplied by the client bundle.
|
|
54
|
+
* @returns the registrations and the pure helpers (also used by tests).
|
|
55
|
+
*/
|
|
56
|
+
function createToolviews(createElement) {
|
|
57
|
+
const h = createElement
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Every tool name this plugin can register.
|
|
61
|
+
*
|
|
62
|
+
* Extension tools are dynamic in principle, but a keyed slot can only be
|
|
63
|
+
* claimed by name. The list is the union of the shipped extensions' tools
|
|
64
|
+
* plus the plugin's own control tools; a future extension's tool shows the
|
|
65
|
+
* generic card until its name is added here.
|
|
66
|
+
*/
|
|
67
|
+
const TOOL_NAMES = Object.freeze([
|
|
68
|
+
// The plugin's own control tools.
|
|
69
|
+
'tool_mode_list', 'tool_mode_describe', 'tool_mode_run',
|
|
70
|
+
// embeddinggemma
|
|
71
|
+
'multimodal_embed', 'semantic_similarity', 'semantic_rank', 'semantic_classify',
|
|
72
|
+
'semantic_deduplicate', 'media_find_moments', 'dedupe_and_rank',
|
|
73
|
+
// laya
|
|
74
|
+
'decision_choose', 'decision_test', 'decision_score',
|
|
75
|
+
// needle
|
|
76
|
+
'tools_plan', 'structured_extract', 'validate_record', 'extract_with_repair',
|
|
77
|
+
// tongue, voz, minilm, declarative recipes
|
|
78
|
+
'lang_detect', 'stt_transcribe', 'text_embed', 'text_rerank', 'text_title', 'describe_image',
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
/** The first text block of a tool result, or null. */
|
|
82
|
+
function resultText(block) {
|
|
83
|
+
const content = block && Array.isArray(block.content) ? block.content : []
|
|
84
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
85
|
+
const entry = content[index]
|
|
86
|
+
if (entry && entry.type === 'text' && typeof entry.text === 'string') return entry.text
|
|
87
|
+
}
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The raw arguments text of any phase, or ''. */
|
|
92
|
+
function argsText(block) {
|
|
93
|
+
if (!block || typeof block !== 'object') return ''
|
|
94
|
+
if (typeof block.argsRaw === 'string' && block.argsRaw.length > 0) return block.argsRaw
|
|
95
|
+
if (block.args && typeof block.args.raw === 'string') return block.args.raw
|
|
96
|
+
return ''
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Parse JSON without throwing: a half-streamed argument block is normal. */
|
|
100
|
+
function safeJson(text) {
|
|
101
|
+
if (typeof text !== 'string' || text.length === 0) return null
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(text)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return null
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The host envelope, from the payload or the result record, or null. */
|
|
110
|
+
function envelopeOf(payload, block) {
|
|
111
|
+
if (payload && typeof payload === 'object' && payload.meta && typeof payload.meta === 'object') {
|
|
112
|
+
return payload.meta
|
|
113
|
+
}
|
|
114
|
+
if (block && block.meta && typeof block.meta === 'object') return block.meta
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Round a measured number for display, keeping enough digits to compare. */
|
|
119
|
+
function number(value) {
|
|
120
|
+
return typeof value === 'number' && Number.isFinite(value) ? String(value) : null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Numbers inside a vector, truncated to the first three. */
|
|
124
|
+
function firstValues(vector, count) {
|
|
125
|
+
if (!Array.isArray(vector)) return []
|
|
126
|
+
return vector.slice(0, count).map(value => (typeof value === 'number' ? String(value) : String(value)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* One line per tool family, describing what the call actually produced.
|
|
131
|
+
*
|
|
132
|
+
* Every branch reads a field the tool's own contract declares; a shape the
|
|
133
|
+
* branch does not recognize falls through to the raw JSON disclosure rather
|
|
134
|
+
* than a guessed sentence.
|
|
135
|
+
* @param toolName - the wire tool name.
|
|
136
|
+
* @param payload - the parsed result payload, or null.
|
|
137
|
+
* @returns display lines (never null).
|
|
138
|
+
*/
|
|
139
|
+
function summaryFor(toolName, payload) {
|
|
140
|
+
if (payload === null || typeof payload !== 'object') return []
|
|
141
|
+
const lines = []
|
|
142
|
+
switch (toolName) {
|
|
143
|
+
case 'multimodal_embed': {
|
|
144
|
+
const vector = Array.isArray(payload.vectors) ? payload.vectors[0] : null
|
|
145
|
+
if (vector) {
|
|
146
|
+
lines.push('dimensions ' + String(vector.dimensions) + ' · normalized ' + String(vector.normalized === true))
|
|
147
|
+
lines.push('first values ' + firstValues(vector.values, 3).join(', '))
|
|
148
|
+
if (typeof vector.embeddingSpaceId === 'string') lines.push('space ' + vector.embeddingSpaceId)
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
case 'semantic_similarity': {
|
|
153
|
+
if (typeof payload.score === 'number') {
|
|
154
|
+
lines.push('score ' + (number(payload.score) ?? '?') + ' · ' + String(payload.scoreKind))
|
|
155
|
+
}
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
case 'semantic_rank':
|
|
159
|
+
if (Array.isArray(payload.results)) {
|
|
160
|
+
lines.push(payload.results.length + ' ranked · ' + String(payload.scoreKind))
|
|
161
|
+
}
|
|
162
|
+
break
|
|
163
|
+
case 'semantic_classify':
|
|
164
|
+
if (typeof payload.label === 'string' || typeof payload.status === 'string') {
|
|
165
|
+
lines.push('label ' + String(payload.label ?? payload.status))
|
|
166
|
+
}
|
|
167
|
+
break
|
|
168
|
+
case 'semantic_deduplicate':
|
|
169
|
+
if (Array.isArray(payload.groups)) lines.push(payload.groups.length + ' group(s)')
|
|
170
|
+
break
|
|
171
|
+
case 'dedupe_and_rank': {
|
|
172
|
+
if (Array.isArray(payload.groups) && Array.isArray(payload.ranking)) {
|
|
173
|
+
lines.push(payload.groups.length + ' group(s) · ' + payload.ranking.length
|
|
174
|
+
+ ' ranked at threshold ' + String(payload.threshold))
|
|
175
|
+
}
|
|
176
|
+
break
|
|
177
|
+
}
|
|
178
|
+
case 'media_find_moments': {
|
|
179
|
+
const match = Array.isArray(payload.matches) ? payload.matches[0] : null
|
|
180
|
+
if (match) {
|
|
181
|
+
lines.push('top window ' + String(match.startSeconds) + '–' + String(match.endSeconds) + ' s · score '
|
|
182
|
+
+ (number(match.score) ?? '?'))
|
|
183
|
+
}
|
|
184
|
+
lines.push(String(payload.windowsEvaluated) + ' window(s) evaluated')
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
case 'decision_choose':
|
|
188
|
+
if (typeof payload.chosen !== 'string') break
|
|
189
|
+
lines.push('chosen ' + String(payload.chosen))
|
|
190
|
+
lines.push('confidence ' + String(payload.confidence) + ' · calibrated ' + String(payload.calibrated))
|
|
191
|
+
break
|
|
192
|
+
case 'decision_test':
|
|
193
|
+
if (typeof payload.decision !== 'string') break
|
|
194
|
+
lines.push('decision ' + String(payload.decision))
|
|
195
|
+
lines.push('confidence ' + String(payload.confidence) + ' · calibrated ' + String(payload.calibrated))
|
|
196
|
+
break
|
|
197
|
+
case 'decision_score':
|
|
198
|
+
if (typeof payload.score !== 'string') break
|
|
199
|
+
lines.push('score ' + String(payload.score))
|
|
200
|
+
lines.push('confidence ' + String(payload.confidence) + ' · calibrated ' + String(payload.calibrated))
|
|
201
|
+
break
|
|
202
|
+
case 'validate_record':
|
|
203
|
+
if (typeof payload.valid !== 'boolean') break
|
|
204
|
+
lines.push('valid ' + String(payload.valid) + ' · ' + (Array.isArray(payload.issues) ? payload.issues.length : 0) + ' issue(s)')
|
|
205
|
+
break
|
|
206
|
+
case 'extract_with_repair':
|
|
207
|
+
if (typeof payload.valid !== 'boolean') break
|
|
208
|
+
lines.push('valid ' + String(payload.valid) + ' · repaired ' + String(payload.repaired === true)
|
|
209
|
+
+ ' · ' + (Array.isArray(payload.attempts) ? payload.attempts.length : 0) + ' attempt(s)')
|
|
210
|
+
break
|
|
211
|
+
case 'structured_extract':
|
|
212
|
+
if (typeof payload.valid !== 'boolean') break
|
|
213
|
+
lines.push('proposed ' + String(payload.proposed === true) + ' · valid ' + String(payload.valid))
|
|
214
|
+
break
|
|
215
|
+
case 'tools_plan':
|
|
216
|
+
if (!Array.isArray(payload.proposals)) break
|
|
217
|
+
lines.push(payload.proposals.length + ' proposal(s) · '
|
|
218
|
+
+ (Array.isArray(payload.suppressed) ? payload.suppressed.length : 0) + ' suppressed')
|
|
219
|
+
break
|
|
220
|
+
case 'stt_transcribe':
|
|
221
|
+
if (typeof payload.text === 'string') lines.push('"' + payload.text.slice(0, 120) + '"')
|
|
222
|
+
break
|
|
223
|
+
case 'lang_detect':
|
|
224
|
+
lines.push('language ' + String(payload.language ?? '?'))
|
|
225
|
+
break
|
|
226
|
+
case 'text_title':
|
|
227
|
+
if (typeof payload.title === 'string') lines.push(payload.title)
|
|
228
|
+
break
|
|
229
|
+
case 'text_embed':
|
|
230
|
+
lines.push('dimensions ' + String(Array.isArray(payload.vectors) && payload.vectors[0]
|
|
231
|
+
? payload.vectors[0].dimensions : '?'))
|
|
232
|
+
break
|
|
233
|
+
case 'text_rerank':
|
|
234
|
+
lines.push((Array.isArray(payload.ranking) || Array.isArray(payload.results)
|
|
235
|
+
? (payload.ranking ?? payload.results).length : 0) + ' ranked')
|
|
236
|
+
break
|
|
237
|
+
default:
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
return lines
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** A small inline badge. */
|
|
244
|
+
function badge(text, tone) {
|
|
245
|
+
return h('span', {
|
|
246
|
+
style: {
|
|
247
|
+
display: 'inline-block', padding: '1px 6px', marginLeft: 6, borderRadius: 9,
|
|
248
|
+
fontSize: 11, lineHeight: '16px',
|
|
249
|
+
background: tone === 'device' ? '#e8f5ec' : tone === 'remote' ? '#fff4e5' : '#eef1f5',
|
|
250
|
+
color: tone === 'device' ? '#1f6b3a' : tone === 'remote' ? '#8a5a00' : '#4a5563',
|
|
251
|
+
border: '1px solid ' + (tone === 'device' ? '#b7e0c4' : tone === 'remote' ? '#f0d29b' : '#d7dce3'),
|
|
252
|
+
},
|
|
253
|
+
}, text)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The chat row for one Tool Mode call.
|
|
258
|
+
*
|
|
259
|
+
* Shows the tool, the extension/model that answered, the device/remote route,
|
|
260
|
+
* the receipt, and a family-specific summary; the full JSON is behind a
|
|
261
|
+
* disclosure so a long embedding payload never floods the transcript.
|
|
262
|
+
*/
|
|
263
|
+
function ToolModeToolCard(props) {
|
|
264
|
+
const block = props.block
|
|
265
|
+
const toolName = props.toolName
|
|
266
|
+
const payload = safeJson(resultText(block))
|
|
267
|
+
const args = safeJson(argsText(block))
|
|
268
|
+
const envelope = envelopeOf(payload, block)
|
|
269
|
+
const failed = block && block.isError === true
|
|
270
|
+
const phase = block && typeof block.phase === 'string' ? block.phase : 'result'
|
|
271
|
+
|
|
272
|
+
const metaBits = []
|
|
273
|
+
if (envelope && typeof envelope.extension === 'string') metaBits.push(envelope.extension)
|
|
274
|
+
if (envelope && typeof envelope.implementationId === 'string') metaBits.push(envelope.implementationId)
|
|
275
|
+
if (envelope && typeof envelope.extensionVersion === 'string') metaBits.push('v' + envelope.extensionVersion)
|
|
276
|
+
if (envelope && typeof envelope.receiptId === 'string') metaBits.push(envelope.receiptId)
|
|
277
|
+
else if (phase === 'result' && !failed) metaBits.push('no receipt')
|
|
278
|
+
|
|
279
|
+
const lines = summaryFor(toolName, payload)
|
|
280
|
+
const failureText = failed
|
|
281
|
+
? (block.error && (block.error.reason ?? block.error.code)) ?? resultText(block) ?? 'the call failed'
|
|
282
|
+
: null
|
|
283
|
+
|
|
284
|
+
const rawArgs = argsText(block)
|
|
285
|
+
const rawResult = resultText(block)
|
|
286
|
+
|
|
287
|
+
return h('div', {
|
|
288
|
+
style: {
|
|
289
|
+
border: '1px solid #d7dce3', borderRadius: 8, padding: '8px 10px', margin: '4px 0',
|
|
290
|
+
background: '#fbfcfd', fontSize: 13,
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
h('div', { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 4 } },
|
|
294
|
+
h('span', { style: { fontWeight: 600 } }, toolName),
|
|
295
|
+
envelope && typeof envelope.executionLocation === 'string'
|
|
296
|
+
? badge(envelope.executionLocation, envelope.executionLocation)
|
|
297
|
+
: null,
|
|
298
|
+
phase !== 'result' ? badge(phase) : null,
|
|
299
|
+
failureText !== null ? badge('error', 'remote') : null),
|
|
300
|
+
metaBits.length > 0
|
|
301
|
+
? h('div', { style: { color: '#5b6675', fontSize: 12, marginTop: 2 } }, metaBits.join(' · '))
|
|
302
|
+
: null,
|
|
303
|
+
failureText !== null
|
|
304
|
+
? h('div', { style: { color: '#8a1f1f', marginTop: 4 } }, String(failureText))
|
|
305
|
+
: null,
|
|
306
|
+
lines.length > 0
|
|
307
|
+
? h('div', { style: { marginTop: 4 } }, lines.map((line, index) => h('div', { key: 'line-' + index }, line)))
|
|
308
|
+
: null,
|
|
309
|
+
h('details', { style: { marginTop: 6 } },
|
|
310
|
+
h('summary', { style: { cursor: 'pointer', color: '#5b6675', fontSize: 12 } }, 'arguments and result'),
|
|
311
|
+
h('div', { style: { marginTop: 4 } },
|
|
312
|
+
h('div', { style: { fontSize: 11, color: '#5b6675' } }, 'arguments'),
|
|
313
|
+
h('pre', { style: { margin: 0, maxHeight: 160, overflow: 'auto', fontSize: 11 } }, pretty(rawArgs)),
|
|
314
|
+
h('div', { style: { fontSize: 11, color: '#5b6675', marginTop: 4 } }, 'result'),
|
|
315
|
+
h('pre', { style: { margin: 0, maxHeight: 240, overflow: 'auto', fontSize: 11 } }, pretty(rawResult)))))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The trace a `tool_mode_run` result carries, or [].
|
|
320
|
+
*
|
|
321
|
+
* The host reports `meta.calls` as a BOUNDED list (name, ok, elapsedMs, previews);
|
|
322
|
+
* this function is the one place that shape is read, so the card and its tests agree.
|
|
323
|
+
* @param payload - the parsed run report.
|
|
324
|
+
* @returns the trace entries.
|
|
325
|
+
*/
|
|
326
|
+
function runTrace(payload) {
|
|
327
|
+
if (payload === null || typeof payload !== 'object') return []
|
|
328
|
+
const meta = payload.meta
|
|
329
|
+
if (meta === null || typeof meta !== 'object' || !Array.isArray(meta.calls)) return []
|
|
330
|
+
return meta.calls.filter(call => call !== null && typeof call === 'object' && typeof call.name === 'string')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The chat row for a Code Mode program.
|
|
335
|
+
*
|
|
336
|
+
* Shows the program's description and requested tool set, then the ORDERED sub-call
|
|
337
|
+
* trace (which tool, whether it succeeded, how long, argument/result previews), the
|
|
338
|
+
* program's own logs and result, and the run's receipt. This is the only way a reader
|
|
339
|
+
* can see how a program combined tools: the tool result itself reports a count.
|
|
340
|
+
*/
|
|
341
|
+
function ToolModeRunCard(props) {
|
|
342
|
+
const block = props.block
|
|
343
|
+
const payload = safeJson(resultText(block))
|
|
344
|
+
const args = safeJson(argsText(block))
|
|
345
|
+
const trace = runTrace(payload)
|
|
346
|
+
const meta = payload && typeof payload === 'object' && payload.meta && typeof payload.meta === 'object'
|
|
347
|
+
? payload.meta : null
|
|
348
|
+
const failed = block && block.isError === true
|
|
349
|
+
const requested = args && Array.isArray(args.tools) ? args.tools : []
|
|
350
|
+
const code = args && typeof args.code === 'string' ? args.code : ''
|
|
351
|
+
const logs = payload && Array.isArray(payload.logs) ? payload.logs : []
|
|
352
|
+
|
|
353
|
+
return h('div', {
|
|
354
|
+
style: {
|
|
355
|
+
border: '1px solid #cfd8e3', borderRadius: 8, padding: '8px 10px', margin: '4px 0',
|
|
356
|
+
background: '#f7fafc', fontSize: 13,
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
h('div', { style: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 4 } },
|
|
360
|
+
h('span', { style: { fontWeight: 600 } }, 'tool_mode_run'),
|
|
361
|
+
badge('code mode', 'device'),
|
|
362
|
+
meta && typeof meta.toolCalls === 'number' ? badge(meta.toolCalls + ' tool call(s)') : null,
|
|
363
|
+
failed ? badge('error', 'remote') : null),
|
|
364
|
+
args && typeof args.description === 'string'
|
|
365
|
+
? h('div', { style: { marginTop: 2 } }, args.description)
|
|
366
|
+
: null,
|
|
367
|
+
meta !== null
|
|
368
|
+
? h('div', { style: { color: '#5b6675', fontSize: 12, marginTop: 2 } },
|
|
369
|
+
[typeof meta.receiptId === 'string' ? meta.receiptId : null,
|
|
370
|
+
typeof meta.elapsedMs === 'number' ? meta.elapsedMs + ' ms' : null,
|
|
371
|
+
typeof meta.catalogRevision === 'string' ? 'catalog ' + meta.catalogRevision : null]
|
|
372
|
+
.filter(part => part !== null).join(' · '))
|
|
373
|
+
: null,
|
|
374
|
+
requested.length > 0
|
|
375
|
+
? h('div', { style: { marginTop: 4, fontSize: 12 } },
|
|
376
|
+
h('span', { style: { color: '#5b6675' } }, 'tools available: '),
|
|
377
|
+
requested.map(name => badge(String(name), 'plain')))
|
|
378
|
+
: null,
|
|
379
|
+
trace.length > 0
|
|
380
|
+
? h('div', { style: { marginTop: 6 } },
|
|
381
|
+
h('div', { style: { fontSize: 11, color: '#5b6675' } }, 'sub-calls, in dispatch order'),
|
|
382
|
+
trace.map((call, index) => h('div', {
|
|
383
|
+
key: 'trace-' + index,
|
|
384
|
+
style: {
|
|
385
|
+
marginTop: 3, paddingLeft: 6,
|
|
386
|
+
borderLeft: '2px solid ' + (call.ok === true ? '#b7e0c4' : '#e7b4b4'),
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
h('div', null,
|
|
390
|
+
h('span', { style: { fontWeight: 600 } }, String(index + 1) + '. ' + call.name),
|
|
391
|
+
' ',
|
|
392
|
+
badge(call.ok === true ? 'ok' : 'failed', call.ok === true ? 'device' : 'remote'),
|
|
393
|
+
typeof call.elapsedMs === 'number'
|
|
394
|
+
? h('span', { style: { color: '#5b6675', fontSize: 11 } }, ' ' + call.elapsedMs + ' ms')
|
|
395
|
+
: null),
|
|
396
|
+
h('div', { style: { fontSize: 11, color: '#4a5563', whiteSpace: 'pre-wrap' } },
|
|
397
|
+
typeof call.argumentsPreview === 'string' ? 'args ' + call.argumentsPreview : ''),
|
|
398
|
+
typeof call.resultPreview === 'string'
|
|
399
|
+
? h('div', { style: { fontSize: 11, color: '#4a5563', whiteSpace: 'pre-wrap' } },
|
|
400
|
+
'result ' + call.resultPreview)
|
|
401
|
+
: null,
|
|
402
|
+
typeof call.error === 'string'
|
|
403
|
+
? h('div', { style: { fontSize: 11, color: '#8a1f1f' } }, 'error ' + call.error)
|
|
404
|
+
: null)))
|
|
405
|
+
: h('div', { style: { fontSize: 12, color: '#5b6675', marginTop: 4 } },
|
|
406
|
+
'No sub-call trace was reported for this run.'),
|
|
407
|
+
logs.length > 0
|
|
408
|
+
? h('details', { style: { marginTop: 6 } },
|
|
409
|
+
h('summary', { style: { cursor: 'pointer', fontSize: 12 } }, 'logs (' + logs.length + ')'),
|
|
410
|
+
h('pre', { style: { margin: 0, maxHeight: 140, overflow: 'auto', fontSize: 11 } }, logs.join('\n')))
|
|
411
|
+
: null,
|
|
412
|
+
h('details', { style: { marginTop: 6 } },
|
|
413
|
+
h('summary', { style: { cursor: 'pointer', fontSize: 12 } }, 'program, arguments and result'),
|
|
414
|
+
h('div', { style: { marginTop: 4 } },
|
|
415
|
+
h('div', { style: { fontSize: 11, color: '#5b6675' } }, 'program'),
|
|
416
|
+
h('pre', { style: { margin: 0, maxHeight: 220, overflow: 'auto', fontSize: 11 } },
|
|
417
|
+
code.length > 0 ? code : '(none)'),
|
|
418
|
+
h('div', { style: { fontSize: 11, color: '#5b6675', marginTop: 4 } }, 'result'),
|
|
419
|
+
h('pre', { style: { margin: 0, maxHeight: 240, overflow: 'auto', fontSize: 11 } },
|
|
420
|
+
payload !== null && typeof payload === 'object' && 'result' in payload
|
|
421
|
+
? pretty(JSON.stringify(payload.result))
|
|
422
|
+
: pretty(resultText(block))))))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Pretty-print JSON when it parses; otherwise show the raw text unchanged. */
|
|
426
|
+
function pretty(text) {
|
|
427
|
+
const parsed = safeJson(text)
|
|
428
|
+
if (parsed === null) return typeof text === 'string' && text.length > 0 ? text : '(none)'
|
|
429
|
+
try {
|
|
430
|
+
return JSON.stringify(parsed, null, 2)
|
|
431
|
+
} catch (error) {
|
|
432
|
+
return String(text)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Register the card for every known tool name.
|
|
438
|
+
*
|
|
439
|
+
* One registration per key: the slot is keyed by tool name, and a missing
|
|
440
|
+
* key falls back to the generic card. Disposers are registered through
|
|
441
|
+
* `ctx.effect` so a disabled/reloaded plugin leaves no view behind.
|
|
442
|
+
* @param ctx - the client plugin context, with the `slots` service.
|
|
443
|
+
*/
|
|
444
|
+
function register(ctx) {
|
|
445
|
+
if (!ctx || !ctx.slots) return
|
|
446
|
+
for (const toolName of TOOL_NAMES) {
|
|
447
|
+
ctx.effect(() => ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
448
|
+
name: 'tool.call.toolview',
|
|
449
|
+
key: toolName,
|
|
450
|
+
// Code Mode needs its own view: the program, the tools it could call, and the
|
|
451
|
+
// ordered sub-call trace are not fields the generic family summary reads.
|
|
452
|
+
}, toolName === 'tool_mode_run' ? ToolModeRunCard : ToolModeToolCard)))
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return {
|
|
457
|
+
TOOL_NAMES,
|
|
458
|
+
ToolModeToolCard,
|
|
459
|
+
ToolModeRunCard,
|
|
460
|
+
runTrace,
|
|
461
|
+
summaryFor,
|
|
462
|
+
register,
|
|
463
|
+
internals: { safeJson, resultText, argsText, envelopeOf, pretty, firstValues, runTrace },
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const toolviews = createToolviews(h)
|
|
468
|
+
|
|
469
|
+
// The Tool Insights page, INLINED at build time from client/tool-insights.js
|
|
470
|
+
// for the same reason: a sibling chunk is a 404 and the page would vanish.
|
|
471
|
+
/**
|
|
472
|
+
* Tool Insights — the three-tab first-level page (Tool Insights / Tool Inventory /
|
|
473
|
+
* MCP) in the approved blue system.
|
|
474
|
+
*
|
|
475
|
+
* THIS FILE IS INLINED INTO client/index.js by `scripts/gen-client-catalog.mjs`.
|
|
476
|
+
* It is deliberately not a sibling chunk: the client module service serves only the
|
|
477
|
+
* bundle file a plugin declares, so a chunk would answer 404 and the page would
|
|
478
|
+
* silently disappear (the same reason the chat cards are inlined).
|
|
479
|
+
*
|
|
480
|
+
* Data sources, all guarded:
|
|
481
|
+
* - the compact `toolModeUsage` projection carried by each session row in
|
|
482
|
+
* `projectionValues` (free with `session.list`);
|
|
483
|
+
* - the host's additive Remote methods `toolUsageReport`, `toolInventory`,
|
|
484
|
+
* `mcpStatus` and `mcpCalls`, reached through the injected `call(method, args)`
|
|
485
|
+
* (which already unwraps RemoteResult and never throws).
|
|
486
|
+
*
|
|
487
|
+
* Nothing here invents data: a missing projection is an empty state, a failed
|
|
488
|
+
* report is a named degraded mode that says what is missing, and an older host
|
|
489
|
+
* without the methods still renders the page from the compact projection.
|
|
490
|
+
*
|
|
491
|
+
* @param createElement - React.createElement, supplied by the client bundle.
|
|
492
|
+
* @param reactLib - React itself (hooks); the bundle passes it, tests may stub it.
|
|
493
|
+
* @returns the page components and the pure helpers (also used by tests).
|
|
494
|
+
*/
|
|
495
|
+
function createToolInsights(createElement, reactLib) {
|
|
496
|
+
const h = createElement
|
|
497
|
+
const React = reactLib
|
|
498
|
+
|
|
499
|
+
/** The blue system from the approved cover card. */
|
|
500
|
+
const PALETTE = Object.freeze({
|
|
501
|
+
primary: '#4F63FF',
|
|
502
|
+
tint: '#E9EDFF',
|
|
503
|
+
page: '#F7F8FC',
|
|
504
|
+
surface: '#F5F8FF',
|
|
505
|
+
border: '#E5E8F2',
|
|
506
|
+
ink: '#0B1015',
|
|
507
|
+
muted: '#5F6B80',
|
|
508
|
+
failure: '#B7791F',
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
const TABS = Object.freeze([
|
|
512
|
+
{ id: 'insights', label: 'Tool Insights' },
|
|
513
|
+
{ id: 'inventory', label: 'Tool Inventory' },
|
|
514
|
+
{ id: 'mcp', label: 'MCP' },
|
|
515
|
+
{ id: 'create', label: 'Create tool' },
|
|
516
|
+
{ id: 'settings', label: 'Settings' },
|
|
517
|
+
])
|
|
518
|
+
|
|
519
|
+
const RANGES = Object.freeze(['24h', '7d', '30d', 'all'])
|
|
520
|
+
const TOOL_PAGE_SIZES = Object.freeze([25, 50, 100])
|
|
521
|
+
const CALL_PAGE_SIZES = Object.freeze([10, 25, 50])
|
|
522
|
+
const DAY_MILLISECONDS = 24 * 60 * 60 * 1000
|
|
523
|
+
|
|
524
|
+
// ── small value helpers ────────────────────────────────────────────────────
|
|
525
|
+
|
|
526
|
+
/** A plain object (arrays and null excluded). */
|
|
527
|
+
function isObject(value) {
|
|
528
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** A finite number, else null. */
|
|
532
|
+
function finite(value) {
|
|
533
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** A non-negative finite number, else 0. */
|
|
537
|
+
function count(value) {
|
|
538
|
+
const number = finite(value)
|
|
539
|
+
return number === null || number < 0 ? 0 : number
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** A non-empty string, else undefined. */
|
|
543
|
+
function text(value) {
|
|
544
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** Assign an own data property, so `__proto__` and `constructor` stay data. */
|
|
548
|
+
function own(target, key, value) {
|
|
549
|
+
Object.defineProperty(target, key, { value, enumerable: true, configurable: true, writable: true })
|
|
550
|
+
return target
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Iterate the own entries of a plain object, tolerating any junk. */
|
|
554
|
+
function entries(value) {
|
|
555
|
+
if (!isObject(value)) return []
|
|
556
|
+
return Object.keys(value).map(key => [key, value[key]])
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ── tabs and deep links ────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Resolve a location.hash to a tab id.
|
|
563
|
+
* @param hash - `location.hash` (any value; junk falls back to insights).
|
|
564
|
+
* @returns the matching tab id from {@link TABS}, else 'insights'.
|
|
565
|
+
*/
|
|
566
|
+
function readTabFromHash(hash) {
|
|
567
|
+
if (typeof hash !== 'string') return 'insights'
|
|
568
|
+
const id = hash.replace(/^#/, '').trim().toLowerCase()
|
|
569
|
+
return TABS.some(entry => entry.id === id) ? id : 'insights'
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** The hash to write for a tab id (invalid ids fall back to insights). */
|
|
573
|
+
function tabHash(id) {
|
|
574
|
+
return '#' + readTabFromHash('#' + String(id))
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── formatting ─────────────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
/** Group the integer part with commas; non-numbers are an em dash. */
|
|
580
|
+
function formatNumber(value) {
|
|
581
|
+
const number = finite(value)
|
|
582
|
+
if (number === null) return '—'
|
|
583
|
+
return Math.round(number).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** A fraction as a percentage; a missing fraction is an em dash. */
|
|
587
|
+
function formatPercent(value) {
|
|
588
|
+
const number = finite(value)
|
|
589
|
+
if (number === null) return '—'
|
|
590
|
+
const percent = number * 100
|
|
591
|
+
return (Number.isInteger(percent) ? String(percent) : percent.toFixed(1)) + '%'
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Milliseconds in a compact human duration. */
|
|
595
|
+
function formatDuration(value) {
|
|
596
|
+
const number = finite(value)
|
|
597
|
+
if (number === null) return '—'
|
|
598
|
+
if (number < 1000) return Math.max(0, Math.round(number)) + 'ms'
|
|
599
|
+
if (number < 60_000) return (number / 1000).toFixed(1) + 's'
|
|
600
|
+
const minutes = Math.floor(number / 60_000)
|
|
601
|
+
const seconds = Math.round((number % 60_000) / 1000)
|
|
602
|
+
return minutes + 'm' + (seconds === 0 ? '' : ' ' + seconds + 's')
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Bytes in KB/MB, bounded to one decimal. */
|
|
606
|
+
function formatBytes(value) {
|
|
607
|
+
const number = finite(value)
|
|
608
|
+
if (number === null) return '—'
|
|
609
|
+
if (number < 1024) return Math.round(number) + ' B'
|
|
610
|
+
if (number < 1024 * 1024) return (number / 1024).toFixed(1) + ' KB'
|
|
611
|
+
return (number / (1024 * 1024)).toFixed(1) + ' MB'
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** A relative "n ago" label; a missing time is an em dash. */
|
|
615
|
+
function formatAgo(value, now) {
|
|
616
|
+
const time = finite(value)
|
|
617
|
+
const current = finite(now) ?? Date.now()
|
|
618
|
+
if (time === null) return '—'
|
|
619
|
+
const delta = Math.max(0, current - time)
|
|
620
|
+
if (delta < 10_000) return 'just now'
|
|
621
|
+
if (delta < 60_000) return Math.round(delta / 1000) + 's ago'
|
|
622
|
+
if (delta < 3_600_000) return Math.round(delta / 60_000) + 'm ago'
|
|
623
|
+
if (delta < DAY_MILLISECONDS) return Math.round(delta / 3_600_000) + 'h ago'
|
|
624
|
+
return Math.round(delta / DAY_MILLISECONDS) + 'd ago'
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ── pagination ─────────────────────────────────────────────────────────────
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Page math for a list.
|
|
631
|
+
* @param total - number of rows.
|
|
632
|
+
* @param pageSize - rows per page (positive).
|
|
633
|
+
* @param page - requested page (clamped into range).
|
|
634
|
+
* @returns { page, pages, from, to, total, pageSize } (1-based, inclusive).
|
|
635
|
+
*/
|
|
636
|
+
function pageInfo(total, pageSize, page) {
|
|
637
|
+
const size = finite(pageSize) !== null && pageSize > 0 ? Math.floor(pageSize) : 10
|
|
638
|
+
const rows = Math.max(0, Math.floor(finite(total) ?? 0))
|
|
639
|
+
const pages = Math.max(1, Math.ceil(rows / size))
|
|
640
|
+
const requested = Math.floor(finite(page) ?? 1)
|
|
641
|
+
const current = Math.min(Math.max(requested, 1), pages)
|
|
642
|
+
return {
|
|
643
|
+
page: current,
|
|
644
|
+
pages,
|
|
645
|
+
from: rows === 0 ? 0 : (current - 1) * size + 1,
|
|
646
|
+
to: Math.min(current * size, rows),
|
|
647
|
+
total: rows,
|
|
648
|
+
pageSize: size,
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Slice rows for a page and return both slices and page math. */
|
|
653
|
+
function paginate(rows, page, pageSize) {
|
|
654
|
+
const list = Array.isArray(rows) ? rows : []
|
|
655
|
+
const info = pageInfo(list.length, pageSize, page)
|
|
656
|
+
return { rows: list.slice(info.from === 0 ? 0 : info.from - 1, info.to), info }
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ── compact projection ────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
/** A normalized per-owner/entry usage triple. */
|
|
662
|
+
function usageTriple(value) {
|
|
663
|
+
if (!isObject(value)) return null
|
|
664
|
+
return { calls: count(value.calls), errors: count(value.errors), ms: count(value.ms) }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Normalize the compact wire one session row carries.
|
|
669
|
+
* @param value - `projectionValues.toolModeUsage` (any shape).
|
|
670
|
+
* @returns the normalized compact value, or null when it is not an object.
|
|
671
|
+
*/
|
|
672
|
+
function normalizeProjection(value) {
|
|
673
|
+
if (!isObject(value)) return null
|
|
674
|
+
const projection = {
|
|
675
|
+
calls: count(value.calls),
|
|
676
|
+
errors: Math.min(count(value.errors), count(value.calls)),
|
|
677
|
+
toolMs: count(value.toolMs),
|
|
678
|
+
uniqueTools: count(value.uniqueTools),
|
|
679
|
+
byOwner: {},
|
|
680
|
+
byModel: {},
|
|
681
|
+
byDay: {},
|
|
682
|
+
recent: [],
|
|
683
|
+
topTools: [],
|
|
684
|
+
}
|
|
685
|
+
for (const [owner, usage] of entries(value.byOwner)) {
|
|
686
|
+
const triple = usageTriple(usage)
|
|
687
|
+
if (triple !== null && owner.length > 0) own(projection.byOwner, owner, triple)
|
|
688
|
+
}
|
|
689
|
+
for (const [model, usage] of entries(value.byModel)) {
|
|
690
|
+
const triple = usageTriple(usage)
|
|
691
|
+
if (triple === null || model.length === 0) continue
|
|
692
|
+
own(projection.byModel, model, { ...triple, tools: count(usage.tools) })
|
|
693
|
+
}
|
|
694
|
+
for (const [day, usage] of entries(value.byDay)) {
|
|
695
|
+
const triple = usageTriple(usage)
|
|
696
|
+
if (triple === null || !/^\d{4}-\d{2}-\d{2}$/.test(day)) continue
|
|
697
|
+
own(projection.byDay, day, triple)
|
|
698
|
+
}
|
|
699
|
+
if (Array.isArray(value.recent)) {
|
|
700
|
+
for (const candidate of value.recent) {
|
|
701
|
+
if (!isObject(candidate) || typeof candidate.name !== 'string' || candidate.name.length === 0) continue
|
|
702
|
+
const recent = {
|
|
703
|
+
time: finite(candidate.time),
|
|
704
|
+
name: candidate.name,
|
|
705
|
+
model: text(candidate.model),
|
|
706
|
+
ms: finite(candidate.ms),
|
|
707
|
+
ok: candidate.ok !== false,
|
|
708
|
+
kind: candidate.kind === 'code' ? 'code' : 'direct',
|
|
709
|
+
}
|
|
710
|
+
if (projection.recent.length < 20) projection.recent.push(recent)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (Array.isArray(value.topTools)) {
|
|
714
|
+
for (const candidate of value.topTools) {
|
|
715
|
+
if (!isObject(candidate) || typeof candidate.name !== 'string' || candidate.name.length === 0) continue
|
|
716
|
+
if (projection.topTools.length >= 5) break
|
|
717
|
+
projection.topTools.push({ name: candidate.name, calls: count(candidate.calls), ms: count(candidate.ms) })
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return projection
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** Merge one normalized compact value into an aggregate. */
|
|
724
|
+
function mergeProjection(target, projection, top) {
|
|
725
|
+
target.calls += projection.calls
|
|
726
|
+
target.errors += projection.errors
|
|
727
|
+
target.toolMs += projection.toolMs
|
|
728
|
+
target.uniqueTools = Math.max(target.uniqueTools, projection.uniqueTools)
|
|
729
|
+
for (const [owner, usage] of entries(projection.byOwner)) {
|
|
730
|
+
const previous = Object.hasOwn(target.byOwner, owner) && isObject(target.byOwner[owner])
|
|
731
|
+
? target.byOwner[owner]
|
|
732
|
+
: { calls: 0, errors: 0, ms: 0 }
|
|
733
|
+
own(target.byOwner, owner, { calls: previous.calls + usage.calls, errors: previous.errors + usage.errors, ms: previous.ms + usage.ms })
|
|
734
|
+
}
|
|
735
|
+
for (const [model, usage] of entries(projection.byModel)) {
|
|
736
|
+
const previous = Object.hasOwn(target.byModel, model) && isObject(target.byModel[model])
|
|
737
|
+
? target.byModel[model]
|
|
738
|
+
: { calls: 0, errors: 0, tools: 0, ms: 0 }
|
|
739
|
+
own(target.byModel, model, {
|
|
740
|
+
calls: previous.calls + usage.calls,
|
|
741
|
+
errors: previous.errors + usage.errors,
|
|
742
|
+
tools: Math.max(previous.tools, usage.tools),
|
|
743
|
+
ms: previous.ms + usage.ms,
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
for (const [day, usage] of entries(projection.byDay)) {
|
|
747
|
+
const previous = Object.hasOwn(target.byDay, day) && isObject(target.byDay[day])
|
|
748
|
+
? target.byDay[day]
|
|
749
|
+
: { calls: 0, errors: 0, ms: 0 }
|
|
750
|
+
own(target.byDay, day, { calls: previous.calls + usage.calls, errors: previous.errors + usage.errors, ms: previous.ms + usage.ms })
|
|
751
|
+
}
|
|
752
|
+
for (const recent of projection.recent) {
|
|
753
|
+
if (target.recent.length >= 200) break
|
|
754
|
+
target.recent.push(recent)
|
|
755
|
+
}
|
|
756
|
+
for (const tool of projection.topTools) {
|
|
757
|
+
const previous = top.get(tool.name) ?? { name: tool.name, calls: 0, ms: 0 }
|
|
758
|
+
previous.calls += tool.calls
|
|
759
|
+
previous.ms += tool.ms
|
|
760
|
+
top.set(tool.name, previous)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Aggregate the compact projection across session rows.
|
|
766
|
+
* @param rows - session rows carrying `projectionValues.toolModeUsage`.
|
|
767
|
+
* @returns totals, rollups, bounded recent/top lists and coverage counts.
|
|
768
|
+
*/
|
|
769
|
+
function aggregateProjections(rows) {
|
|
770
|
+
const aggregate = {
|
|
771
|
+
calls: 0, errors: 0, toolMs: 0, uniqueTools: 0,
|
|
772
|
+
byOwner: {}, byModel: {}, byDay: {},
|
|
773
|
+
recent: [], topTools: [],
|
|
774
|
+
coverage: { sessions: 0, covered: 0 },
|
|
775
|
+
}
|
|
776
|
+
const top = new Map()
|
|
777
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
778
|
+
if (!isObject(row)) continue
|
|
779
|
+
aggregate.coverage.sessions += 1
|
|
780
|
+
const projectionValues = row.projectionValues
|
|
781
|
+
if (!isObject(projectionValues)) continue
|
|
782
|
+
const projection = normalizeProjection(projectionValues.toolModeUsage)
|
|
783
|
+
if (projection === null) continue
|
|
784
|
+
aggregate.coverage.covered += 1
|
|
785
|
+
mergeProjection(aggregate, projection, top)
|
|
786
|
+
}
|
|
787
|
+
aggregate.topTools = [...top.values()].sort((left, right) => right.calls - left.calls).slice(0, 5)
|
|
788
|
+
aggregate.sessions = aggregate.coverage.covered
|
|
789
|
+
aggregate.recent.sort((left, right) => (finite(right.time) ?? 0) - (finite(left.time) ?? 0))
|
|
790
|
+
return aggregate
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── host report ───────────────────────────────────────────────────────────
|
|
794
|
+
|
|
795
|
+
/** Normalize one report tool row; null when it carries no name. */
|
|
796
|
+
function reportTool(value) {
|
|
797
|
+
if (!isObject(value) || typeof value.name !== 'string' || value.name.length === 0) return null
|
|
798
|
+
return {
|
|
799
|
+
name: value.name,
|
|
800
|
+
owner: text(value.owner),
|
|
801
|
+
calls: count(value.calls),
|
|
802
|
+
errors: Math.min(count(value.errors), count(value.calls)),
|
|
803
|
+
totalMs: count(value.totalMs),
|
|
804
|
+
maxMs: finite(value.maxMs),
|
|
805
|
+
p50: finite(value.p50),
|
|
806
|
+
p95: finite(value.p95),
|
|
807
|
+
firstUsed: finite(value.firstUsed),
|
|
808
|
+
lastUsed: finite(value.lastUsed),
|
|
809
|
+
codeCalls: count(value.codeCalls),
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** Normalize one report model row; null when it carries no model. */
|
|
814
|
+
function reportModel(value) {
|
|
815
|
+
if (!isObject(value) || typeof value.model !== 'string' || value.model.length === 0) return null
|
|
816
|
+
return {
|
|
817
|
+
model: value.model,
|
|
818
|
+
calls: count(value.calls),
|
|
819
|
+
errors: Math.min(count(value.errors), count(value.calls)),
|
|
820
|
+
tools: count(value.tools),
|
|
821
|
+
ms: count(value.ms),
|
|
822
|
+
p50: finite(value.p50),
|
|
823
|
+
p95: finite(value.p95),
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** Normalize a call row for the All calls table. */
|
|
828
|
+
function callRow(value) {
|
|
829
|
+
if (!isObject(value) || typeof value.name !== 'string' || value.name.length === 0) return null
|
|
830
|
+
const callId = text(value.callId)
|
|
831
|
+
const time = finite(value.time)
|
|
832
|
+
if (callId === undefined && time === null) return null
|
|
833
|
+
return {
|
|
834
|
+
time,
|
|
835
|
+
name: value.name,
|
|
836
|
+
owner: text(value.owner),
|
|
837
|
+
model: text(value.model),
|
|
838
|
+
ms: finite(value.ms),
|
|
839
|
+
ok: value.ok !== false && value.isError !== true,
|
|
840
|
+
errorCode: text(value.errorCode) ?? text(value.error?.code),
|
|
841
|
+
kind: value.kind === 'code' ? 'code' : 'direct',
|
|
842
|
+
callId,
|
|
843
|
+
parentCallId: text(value.parentCallId),
|
|
844
|
+
sessionId: text(value.sessionId),
|
|
845
|
+
workspace: text(value.workspace),
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Normalize the host report, dropping malformed rows and preserving only facts.
|
|
851
|
+
* @param value - the `toolUsageReport` value.
|
|
852
|
+
* @returns the normalized report, or null when it is not a usable object.
|
|
853
|
+
*/
|
|
854
|
+
function normalizeReport(value) {
|
|
855
|
+
if (!isObject(value)) return null
|
|
856
|
+
const tools = []
|
|
857
|
+
for (const candidate of Array.isArray(value.tools) ? value.tools : []) {
|
|
858
|
+
const row = reportTool(candidate)
|
|
859
|
+
if (row !== null) tools.push(row)
|
|
860
|
+
}
|
|
861
|
+
const models = []
|
|
862
|
+
for (const candidate of Array.isArray(value.models) ? value.models : []) {
|
|
863
|
+
const row = reportModel(candidate)
|
|
864
|
+
if (row !== null) models.push(row)
|
|
865
|
+
}
|
|
866
|
+
const days = []
|
|
867
|
+
for (const candidate of Array.isArray(value.days) ? value.days : []) {
|
|
868
|
+
if (!isObject(candidate) || typeof candidate.day !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(candidate.day)) continue
|
|
869
|
+
days.push({ day: candidate.day, calls: count(candidate.calls), errors: count(candidate.errors), ms: count(candidate.ms) })
|
|
870
|
+
}
|
|
871
|
+
const calls = []
|
|
872
|
+
const callList = Array.isArray(value.calls)
|
|
873
|
+
? value.calls
|
|
874
|
+
: Array.isArray(value.callRows) ? value.callRows : Array.isArray(value.recentCalls) ? value.recentCalls : []
|
|
875
|
+
for (const candidate of callList) {
|
|
876
|
+
const row = callRow(candidate)
|
|
877
|
+
if (row !== null) calls.push(row)
|
|
878
|
+
}
|
|
879
|
+
calls.sort((left, right) => (finite(right.time) ?? 0) - (finite(left.time) ?? 0))
|
|
880
|
+
// A host may send `calls` as the count or as the list itself; when it is the
|
|
881
|
+
// list the count comes from `callCount`, else from the list length.
|
|
882
|
+
const callCount = Array.isArray(value.calls)
|
|
883
|
+
? (finite(value.callCount) !== null ? count(value.callCount) : calls.length)
|
|
884
|
+
: count(value.calls)
|
|
885
|
+
return {
|
|
886
|
+
calls: callCount,
|
|
887
|
+
errors: Math.min(count(value.errors), callCount),
|
|
888
|
+
unsettled: count(value.unsettled),
|
|
889
|
+
toolMs: count(value.toolMs),
|
|
890
|
+
codeCalls: count(value.codeCalls),
|
|
891
|
+
sessions: count(value.sessions),
|
|
892
|
+
p50: finite(value.p50),
|
|
893
|
+
p95: finite(value.p95),
|
|
894
|
+
tools,
|
|
895
|
+
models,
|
|
896
|
+
days,
|
|
897
|
+
callRows: calls,
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// ── derived view model ────────────────────────────────────────────────────
|
|
902
|
+
|
|
903
|
+
/** Milliseconds covered by a range id. */
|
|
904
|
+
function rangeWindow(range, now) {
|
|
905
|
+
const current = finite(now) ?? Date.now()
|
|
906
|
+
const days = range === '24h' ? 1 : range === '7d' ? 7 : range === '30d' ? 30 : null
|
|
907
|
+
return { from: days === null ? null : current - days * DAY_MILLISECONDS, to: current }
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* The query value for `toolUsageReport`. `all` is range-only: a numeric window
|
|
912
|
+
* requires both bounds and null is not a number. The value is passed
|
|
913
|
+
* POSITIONALLY to the generated method (see requestUsageReport).
|
|
914
|
+
*/
|
|
915
|
+
function reportQuery(range, now) {
|
|
916
|
+
const window = rangeWindow(range, now)
|
|
917
|
+
return window.from === null ? { range } : { range, from: window.from, to: window.to }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Call the positional Client Remote method. The generated namespace takes the
|
|
922
|
+
* descriptor's parameters positionally and the gateway builds the wire
|
|
923
|
+
* `args` object; passing `{ query: … }` here would nest it one level too deep
|
|
924
|
+
* and the host would fall back to its default 7d window.
|
|
925
|
+
*/
|
|
926
|
+
function requestUsageReport(call, range, now) {
|
|
927
|
+
return call('toolUsageReport', reportQuery(range, now))
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** The same positional convention for the MCP calls page. */
|
|
931
|
+
function requestMcpCalls(call, page, pageSize) {
|
|
932
|
+
return call('mcpCalls', { page, pageSize })
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Success rate for a calls/errors pair (null when there were no calls). */
|
|
936
|
+
function successRate(calls, errors) {
|
|
937
|
+
return calls > 0 ? (calls - Math.min(errors, calls)) / calls : null
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/** Success rate of a tool row; null when the row carries no error facts. */
|
|
941
|
+
function toolSuccessRate(tool) {
|
|
942
|
+
return typeof tool.errors === 'number' && typeof tool.calls === 'number' ? successRate(tool.calls, tool.errors) : null
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Success ratio for an inventory row. The host's `success` column is a COUNT
|
|
947
|
+
* of successful calls (not a ratio); normalize it here so every renderer
|
|
948
|
+
* formats the same value.
|
|
949
|
+
*/
|
|
950
|
+
function inventorySuccessRate(row) {
|
|
951
|
+
const calls = finite(row.calls)
|
|
952
|
+
const success = finite(row.success)
|
|
953
|
+
if (calls === null || success === null || calls <= 0) return null
|
|
954
|
+
return Math.max(0, Math.min(success, calls)) / calls
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Build the workbench view model from the compact projection and/or the report.
|
|
959
|
+
* The report wins when present; otherwise the page degrades to the compact
|
|
960
|
+
* projection and says so.
|
|
961
|
+
* @param input - { projection, report, range, now, toolsAvailable }.
|
|
962
|
+
* @returns { degraded, banner, kpis, days, models, tools, calls, range }.
|
|
963
|
+
*/
|
|
964
|
+
function deriveInsights(input) {
|
|
965
|
+
const projection = isObject(input.projection) ? input.projection : aggregateProjections([])
|
|
966
|
+
const report = normalizeReport(input.report)
|
|
967
|
+
const range = typeof input.range === 'string' ? input.range : '7d'
|
|
968
|
+
const toolsAvailable = finite(input.toolsAvailable)
|
|
969
|
+
|
|
970
|
+
if (report !== null) {
|
|
971
|
+
const tools = report.tools.slice().sort((left, right) => right.calls - left.calls)
|
|
972
|
+
return {
|
|
973
|
+
degraded: false,
|
|
974
|
+
banner: null,
|
|
975
|
+
range,
|
|
976
|
+
kpis: {
|
|
977
|
+
calls: report.calls,
|
|
978
|
+
errors: report.errors,
|
|
979
|
+
unsettled: report.unsettled,
|
|
980
|
+
successRate: successRate(report.calls, report.errors),
|
|
981
|
+
toolMs: report.toolMs,
|
|
982
|
+
sessions: report.sessions,
|
|
983
|
+
callsPerSession: report.sessions > 0 ? report.calls / report.sessions : null,
|
|
984
|
+
p50: report.p50,
|
|
985
|
+
p95: report.p95,
|
|
986
|
+
codeModeShare: report.calls > 0 ? report.codeCalls / report.calls : null,
|
|
987
|
+
toolsUsed: report.tools.length,
|
|
988
|
+
toolsAvailable,
|
|
989
|
+
},
|
|
990
|
+
days: report.days.slice().sort((left, right) => left.day.localeCompare(right.day)),
|
|
991
|
+
models: report.models.slice().sort((left, right) => right.calls - left.calls),
|
|
992
|
+
tools,
|
|
993
|
+
calls: report.callRows.slice(),
|
|
994
|
+
coverage: projection.coverage,
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const byDay = entries(projection.byDay)
|
|
999
|
+
.map(([day, usage]) => ({ day, calls: usage.calls, errors: usage.errors, ms: usage.ms }))
|
|
1000
|
+
.sort((left, right) => left.day.localeCompare(right.day))
|
|
1001
|
+
const models = entries(projection.byModel)
|
|
1002
|
+
.map(([model, usage]) => ({ model, calls: usage.calls, errors: usage.errors, tools: usage.tools, ms: usage.ms, p50: null, p95: null }))
|
|
1003
|
+
.sort((left, right) => right.calls - left.calls)
|
|
1004
|
+
const tools = projection.topTools
|
|
1005
|
+
.slice()
|
|
1006
|
+
.sort((left, right) => right.calls - left.calls)
|
|
1007
|
+
.map(tool => ({ name: tool.name, owner: null, calls: tool.calls, errors: null, totalMs: tool.ms, maxMs: null, p50: null, p95: null, firstUsed: null, lastUsed: null, codeCalls: null }))
|
|
1008
|
+
const codeCalls = projection.recent.filter(recent => recent.kind === 'code').length
|
|
1009
|
+
return {
|
|
1010
|
+
degraded: true,
|
|
1011
|
+
banner: 'The exact host report is not available on this build; these figures come from session projections '
|
|
1012
|
+
+ '(' + projection.coverage.covered + ' of ' + projection.coverage.sessions + ' sessions carry the usage projection).',
|
|
1013
|
+
range,
|
|
1014
|
+
kpis: {
|
|
1015
|
+
calls: projection.calls,
|
|
1016
|
+
errors: projection.errors,
|
|
1017
|
+
unsettled: null,
|
|
1018
|
+
successRate: successRate(projection.calls, projection.errors),
|
|
1019
|
+
toolMs: projection.toolMs,
|
|
1020
|
+
sessions: projection.sessions,
|
|
1021
|
+
callsPerSession: projection.sessions > 0 ? projection.calls / projection.sessions : null,
|
|
1022
|
+
p50: null,
|
|
1023
|
+
p95: null,
|
|
1024
|
+
codeModeShare: projection.calls > 0 ? codeCalls / projection.calls : null,
|
|
1025
|
+
toolsUsed: projection.uniqueTools,
|
|
1026
|
+
toolsAvailable,
|
|
1027
|
+
},
|
|
1028
|
+
days: byDay,
|
|
1029
|
+
models,
|
|
1030
|
+
tools,
|
|
1031
|
+
calls: projection.recent.slice(),
|
|
1032
|
+
coverage: projection.coverage,
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ── Code Mode grouping ────────────────────────────────────────────────────
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Group call rows: a `tool_mode_run` parent carries its component sub-calls.
|
|
1040
|
+
* A sub-call whose parent is absent is shown as its own row, never hidden.
|
|
1041
|
+
* @param calls - call rows (report or projection shape).
|
|
1042
|
+
* @returns top-level rows, each with a `children` array.
|
|
1043
|
+
*/
|
|
1044
|
+
function groupCallRows(calls) {
|
|
1045
|
+
const rows = []
|
|
1046
|
+
for (const candidate of Array.isArray(calls) ? calls : []) {
|
|
1047
|
+
const row = callRow(candidate)
|
|
1048
|
+
if (row !== null) rows.push({ ...row, children: [] })
|
|
1049
|
+
}
|
|
1050
|
+
const byId = new Map()
|
|
1051
|
+
for (const row of rows) {
|
|
1052
|
+
if (row.callId !== undefined && !byId.has(row.callId)) byId.set(row.callId, row)
|
|
1053
|
+
}
|
|
1054
|
+
const top = []
|
|
1055
|
+
for (const row of rows) {
|
|
1056
|
+
const parent = row.parentCallId === undefined ? undefined : byId.get(row.parentCallId)
|
|
1057
|
+
if (parent !== undefined && parent !== row) parent.children.push(row)
|
|
1058
|
+
else top.push(row)
|
|
1059
|
+
}
|
|
1060
|
+
for (const row of top) {
|
|
1061
|
+
row.children.sort((left, right) => (finite(left.time) ?? 0) - (finite(right.time) ?? 0))
|
|
1062
|
+
}
|
|
1063
|
+
return top
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// ── inventory ─────────────────────────────────────────────────────────────
|
|
1067
|
+
|
|
1068
|
+
/** The origin group a tool belongs to. */
|
|
1069
|
+
function originOf(tool) {
|
|
1070
|
+
if (tool.kind === 'built-in' || tool.kind === 'core') return 'core'
|
|
1071
|
+
if (tool.kind === 'extension' || tool.kind === 'tool-mode') return 'tool-mode'
|
|
1072
|
+
if (tool.kind === 'custom') return 'custom'
|
|
1073
|
+
if (typeof tool.owner === 'string') {
|
|
1074
|
+
if (tool.owner.startsWith('@deepseek-ai/dsh')) return 'core'
|
|
1075
|
+
if (tool.owner.startsWith('@try-works/')) return 'tool-mode'
|
|
1076
|
+
}
|
|
1077
|
+
return 'other'
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const ORIGIN_LABELS = Object.freeze({ core: 'DSH core', 'tool-mode': 'Tool Mode', custom: 'Custom tools', other: 'Other plugins' })
|
|
1081
|
+
|
|
1082
|
+
/** Normalize one inventory row; null when it carries no usable name. */
|
|
1083
|
+
function inventoryRow(value) {
|
|
1084
|
+
if (!isObject(value) || typeof value.name !== 'string' || value.name.length === 0) return null
|
|
1085
|
+
const row = {
|
|
1086
|
+
name: value.name,
|
|
1087
|
+
description: text(value.description),
|
|
1088
|
+
owner: text(value.owner),
|
|
1089
|
+
kind: typeof value.kind === 'string' && value.kind.length > 0 ? value.kind : 'unknown',
|
|
1090
|
+
state: typeof value.state === 'string' && value.state.length > 0 ? value.state : 'unknown',
|
|
1091
|
+
extension: text(value.extension),
|
|
1092
|
+
extensionVersion: text(value.extensionVersion),
|
|
1093
|
+
contractVersion: text(value.contractVersion),
|
|
1094
|
+
argumentCount: finite(value.argumentCount),
|
|
1095
|
+
lastRegistered: finite(value.lastRegistered),
|
|
1096
|
+
calls: finite(value.calls),
|
|
1097
|
+
success: finite(value.success),
|
|
1098
|
+
lastUsed: finite(value.lastUsed),
|
|
1099
|
+
mcpExposed: value.mcpExposed === true ? true : value.mcpExposed === false ? false : undefined,
|
|
1100
|
+
}
|
|
1101
|
+
row.origin = originOf(row)
|
|
1102
|
+
return row
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Filter, group and paginate the inventory.
|
|
1107
|
+
* @param tools - raw inventory rows.
|
|
1108
|
+
* @param query - { query, origin, state, page, pageSize }.
|
|
1109
|
+
* @returns { available, groups, total, info, filtered }.
|
|
1110
|
+
*/
|
|
1111
|
+
function filterAndGroupInventory(tools, query) {
|
|
1112
|
+
const options = isObject(query) ? query : {}
|
|
1113
|
+
const search = typeof options.query === 'string' ? options.query.trim().toLowerCase() : ''
|
|
1114
|
+
const origin = typeof options.origin === 'string' && options.origin !== 'all' ? options.origin : 'all'
|
|
1115
|
+
const state = typeof options.state === 'string' && options.state !== 'all' ? options.state : 'all'
|
|
1116
|
+
const rows = []
|
|
1117
|
+
for (const candidate of Array.isArray(tools) ? tools : []) {
|
|
1118
|
+
const row = inventoryRow(candidate)
|
|
1119
|
+
if (row === null) continue
|
|
1120
|
+
if (origin !== 'all' && row.origin !== origin) continue
|
|
1121
|
+
if (state !== 'all' && row.state !== state) continue
|
|
1122
|
+
if (search.length > 0) {
|
|
1123
|
+
const haystack = (row.name + ' ' + (row.owner ?? '') + ' ' + (row.description ?? '') + ' ' + row.kind).toLowerCase()
|
|
1124
|
+
if (!haystack.includes(search)) continue
|
|
1125
|
+
}
|
|
1126
|
+
rows.push(row)
|
|
1127
|
+
}
|
|
1128
|
+
const page = paginate(rows, options.page, options.pageSize)
|
|
1129
|
+
const order = ['core', 'tool-mode', 'custom', 'other']
|
|
1130
|
+
const groups = order
|
|
1131
|
+
.map(id => ({ origin: id, label: ORIGIN_LABELS[id], rows: page.rows.filter(row => row.origin === id) }))
|
|
1132
|
+
.filter(group => group.rows.length > 0)
|
|
1133
|
+
return {
|
|
1134
|
+
available: true,
|
|
1135
|
+
groups,
|
|
1136
|
+
filtered: rows,
|
|
1137
|
+
total: rows.length,
|
|
1138
|
+
info: page.info,
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// ── MCP ───────────────────────────────────────────────────────────────────
|
|
1143
|
+
|
|
1144
|
+
/** Normalize a names list or a {count,tools} object. */
|
|
1145
|
+
function namesOf(value) {
|
|
1146
|
+
if (Array.isArray(value)) return value.filter(name => typeof name === 'string' && name.length > 0)
|
|
1147
|
+
if (isObject(value) && Array.isArray(value.tools)) return value.tools.filter(name => typeof name === 'string' && name.length > 0)
|
|
1148
|
+
return []
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/** Normalize the MCP status document; null when it is not usable. */
|
|
1152
|
+
function normalizeMcpStatus(value) {
|
|
1153
|
+
if (!isObject(value) || typeof value.enabled !== 'boolean') return null
|
|
1154
|
+
const endpoint = isObject(value.endpoint) ? value.endpoint : null
|
|
1155
|
+
const host = endpoint === null ? undefined : text(endpoint.host)
|
|
1156
|
+
const port = endpoint === null ? null : finite(endpoint.port)
|
|
1157
|
+
const path = endpoint === null ? undefined : text(endpoint.path)
|
|
1158
|
+
const url = host !== undefined && port !== null ? 'http://' + host + ':' + String(port) + (path ?? '') : null
|
|
1159
|
+
const clients = []
|
|
1160
|
+
for (const candidate of Array.isArray(value.clients) ? value.clients : []) {
|
|
1161
|
+
if (!isObject(candidate) || typeof candidate.id !== 'string' || candidate.id.length === 0) continue
|
|
1162
|
+
clients.push({
|
|
1163
|
+
id: candidate.id,
|
|
1164
|
+
label: text(candidate.label),
|
|
1165
|
+
calls: count(candidate.calls),
|
|
1166
|
+
errors: Math.min(count(candidate.errors), count(candidate.calls)),
|
|
1167
|
+
lastSeen: finite(candidate.lastSeen),
|
|
1168
|
+
})
|
|
1169
|
+
}
|
|
1170
|
+
const exposed = namesOf(value.exposed)
|
|
1171
|
+
const withheld = namesOf(value.withheld)
|
|
1172
|
+
return {
|
|
1173
|
+
enabled: value.enabled,
|
|
1174
|
+
endpoint: url,
|
|
1175
|
+
host: host ?? null,
|
|
1176
|
+
port,
|
|
1177
|
+
path: path ?? null,
|
|
1178
|
+
transport: text(value.transport) ?? null,
|
|
1179
|
+
scheme: isObject(value.auth) ? text(value.auth.scheme) ?? null : null,
|
|
1180
|
+
tokenFile: isObject(value.auth) ? text(value.auth.tokenFile) ?? null : null,
|
|
1181
|
+
uptimeMs: finite(value.uptimeMs),
|
|
1182
|
+
clients,
|
|
1183
|
+
exposed,
|
|
1184
|
+
withheld,
|
|
1185
|
+
exposedCount: exposed.length,
|
|
1186
|
+
withheldCount: withheld.length,
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/** Normalize one external call row. */
|
|
1191
|
+
function mcpCallRow(value) {
|
|
1192
|
+
if (!isObject(value) || typeof value.tool !== 'string' || value.tool.length === 0) return null
|
|
1193
|
+
return {
|
|
1194
|
+
time: finite(value.time),
|
|
1195
|
+
tool: value.tool,
|
|
1196
|
+
clientId: text(value.clientId) ?? 'unknown client',
|
|
1197
|
+
totalMs: finite(value.totalMs),
|
|
1198
|
+
failureClass: text(value.failureClass),
|
|
1199
|
+
inputBytes: finite(value.inputBytes),
|
|
1200
|
+
outputBytes: finite(value.outputBytes),
|
|
1201
|
+
receiptId: text(value.receiptId),
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* Build the MCP view model: endpoint, clients, exposure and a page of calls.
|
|
1207
|
+
* @param status - the `mcpStatus` value (or null when the host lacks it).
|
|
1208
|
+
* @param calls - the `mcpCalls` value (or null).
|
|
1209
|
+
* @param options - { page, pageSize }.
|
|
1210
|
+
*/
|
|
1211
|
+
function mcpView(status, calls, options) {
|
|
1212
|
+
const normalized = normalizeMcpStatus(status)
|
|
1213
|
+
const optionsValue = isObject(options) ? options : {}
|
|
1214
|
+
if (normalized === null) {
|
|
1215
|
+
return {
|
|
1216
|
+
available: false,
|
|
1217
|
+
banner: 'The MCP status is not available on this host build (the mcpStatus remote method is absent).',
|
|
1218
|
+
enabled: false, endpoint: null, host: null, port: null, path: null, transport: null,
|
|
1219
|
+
scheme: null, tokenFile: null, uptimeMs: null, clients: [], exposed: [], withheld: [],
|
|
1220
|
+
exposedCount: 0, withheldCount: 0, calls: [], callsInfo: pageInfo(0, 10, 1),
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
const rawCalls = Array.isArray(calls) ? calls : isObject(calls) && Array.isArray(calls.calls) ? calls.calls : []
|
|
1224
|
+
const rows = []
|
|
1225
|
+
for (const candidate of rawCalls) {
|
|
1226
|
+
const row = mcpCallRow(candidate)
|
|
1227
|
+
if (row !== null) rows.push(row)
|
|
1228
|
+
}
|
|
1229
|
+
rows.sort((left, right) => (finite(right.time) ?? 0) - (finite(left.time) ?? 0))
|
|
1230
|
+
const page = paginate(rows, optionsValue.page, optionsValue.pageSize)
|
|
1231
|
+
return {
|
|
1232
|
+
available: true,
|
|
1233
|
+
banner: null,
|
|
1234
|
+
...normalized,
|
|
1235
|
+
calls: page.rows,
|
|
1236
|
+
callsInfo: page.info,
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// ── components ────────────────────────────────────────────────────────────
|
|
1241
|
+
|
|
1242
|
+
const CSS = `
|
|
1243
|
+
.mti-page { box-sizing: border-box; height: 100%; min-height: 100%; overflow-y: auto; overflow-x: hidden; padding: 18px 24px 44px; background: ${PALETTE.page}; color: ${PALETTE.ink}; font-size: 13px; line-height: 20px; }
|
|
1244
|
+
.mti-page *, .mti-page *::before, .mti-page *::after { box-sizing: border-box; }
|
|
1245
|
+
.mti-page:focus-visible, .mti-page button:focus-visible, .mti-page input:focus-visible, .mti-page select:focus-visible {
|
|
1246
|
+
outline: 2px solid ${PALETTE.primary}; outline-offset: 2px;
|
|
1247
|
+
}
|
|
1248
|
+
.mti-tabs { display: flex; gap: 2px; border-bottom: 1px solid ${PALETTE.border}; margin-bottom: 16px; }
|
|
1249
|
+
.mti-tab { appearance: none; border: 0; background: transparent; color: ${PALETTE.muted}; font: inherit; font-size: 13px; line-height: 20px;
|
|
1250
|
+
padding: 8px 12px 10px; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; border-radius: 6px 6px 0 0; }
|
|
1251
|
+
.mti-tab:hover { color: ${PALETTE.ink}; background: ${PALETTE.tint}; }
|
|
1252
|
+
.mti-tab[data-active='true'] { color: ${PALETTE.primary}; border-bottom-color: ${PALETTE.primary}; font-weight: 600; }
|
|
1253
|
+
.mti-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
1254
|
+
.mti-title { margin: 0; font-size: 22px; line-height: 28px; font-weight: 650; letter-spacing: -0.01em; }
|
|
1255
|
+
.mti-sub { margin: 4px 0 0; color: ${PALETTE.muted}; font-size: 12.5px; line-height: 18px; max-width: 720px; }
|
|
1256
|
+
.mti-range { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
1257
|
+
.mti-range-label { color: ${PALETTE.muted}; font-size: 11.5px; }
|
|
1258
|
+
.mti-seg { display: inline-flex; border: 1px solid ${PALETTE.border}; border-radius: 7px; overflow: hidden; background: #FFFFFF; }
|
|
1259
|
+
.mti-seg button { appearance: none; border: 0; background: transparent; color: ${PALETTE.muted}; font: inherit; font-size: 12px;
|
|
1260
|
+
padding: 5px 10px; cursor: pointer; border-left: 1px solid ${PALETTE.border}; }
|
|
1261
|
+
.mti-seg button:first-child { border-left: 0; }
|
|
1262
|
+
.mti-seg button[data-active='true'] { background: ${PALETTE.tint}; color: ${PALETTE.primary}; font-weight: 600; }
|
|
1263
|
+
.mti-banner { display: flex; align-items: flex-start; gap: 8px; border: 1px solid ${PALETTE.border}; border-left: 3px solid ${PALETTE.primary};
|
|
1264
|
+
background: ${PALETTE.tint}; color: ${PALETTE.ink}; border-radius: 8px; padding: 9px 12px; margin: 0 0 14px; font-size: 12.5px; }
|
|
1265
|
+
.mti-banner[data-kind='warn'] { border-left-color: ${PALETTE.failure}; background: #FDF6EA; }
|
|
1266
|
+
.mti-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 14px; }
|
|
1267
|
+
.mti-kpi { background: #FFFFFF; border: 1px solid ${PALETTE.border}; border-radius: 10px; padding: 11px 13px; min-width: 0; }
|
|
1268
|
+
.mti-kpi-label { color: ${PALETTE.muted}; font-size: 11px; line-height: 15px; letter-spacing: 0.03em; text-transform: uppercase; }
|
|
1269
|
+
.mti-kpi-value { margin-top: 3px; font-size: 23px; line-height: 28px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
1270
|
+
.mti-kpi-hint { color: ${PALETTE.muted}; font-size: 11px; line-height: 15px; margin-top: 2px; }
|
|
1271
|
+
.mti-grid { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); gap: 12px; margin-bottom: 14px; }
|
|
1272
|
+
.mti-card { background: #FFFFFF; border: 1px solid ${PALETTE.border}; border-radius: 10px; padding: 13px 15px; min-width: 0; margin-bottom: 14px; }
|
|
1273
|
+
.mti-grid .mti-card { margin-bottom: 0; }
|
|
1274
|
+
.mti-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; }
|
|
1275
|
+
.mti-card-title { margin: 0; font-size: 13px; line-height: 18px; font-weight: 600; }
|
|
1276
|
+
.mti-card-note { color: ${PALETTE.muted}; font-size: 11px; }
|
|
1277
|
+
.mti-bars { display: flex; align-items: flex-end; gap: 6px; min-height: 96px; border-bottom: 1px solid ${PALETTE.border}; padding-top: 6px; }
|
|
1278
|
+
.mti-bar { flex: 1 1 0; min-width: 6px; display: flex; flex-direction: column; justify-content: flex-end; height: 96px; }
|
|
1279
|
+
.mti-bar-stack { display: flex; flex-direction: column-reverse; gap: 1px; }
|
|
1280
|
+
.mti-bar-ok { background: ${PALETTE.primary}; border-radius: 2px 2px 0 0; min-height: 1px; }
|
|
1281
|
+
.mti-bar-err { background: ${PALETTE.failure}; border-radius: 2px 2px 0 0; min-height: 1px; }
|
|
1282
|
+
.mti-bar-label { color: ${PALETTE.muted}; font-size: 10px; text-align: center; margin-top: 5px; white-space: nowrap; }
|
|
1283
|
+
.mti-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
|
1284
|
+
.mti-table th { text-align: left; color: ${PALETTE.muted}; font-size: 10.5px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase;
|
|
1285
|
+
padding: 6px 8px; border-bottom: 1px solid ${PALETTE.border}; white-space: nowrap; }
|
|
1286
|
+
.mti-table th.mti-num { text-align: right; }
|
|
1287
|
+
.mti-table td { padding: 7px 8px; border-bottom: 1px solid ${PALETTE.border}; font-size: 12.5px; vertical-align: top; }
|
|
1288
|
+
.mti-table tbody tr:last-child td { border-bottom: 0; }
|
|
1289
|
+
.mti-table tbody tr:hover td { background: ${PALETTE.surface}; }
|
|
1290
|
+
.mti-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
1291
|
+
.mti-name { font-weight: 550; color: ${PALETTE.ink}; }
|
|
1292
|
+
.mti-tool-link { appearance: none; border: 0; background: transparent; padding: 0; font: inherit; color: ${PALETTE.primary}; cursor: pointer; text-align: left; }
|
|
1293
|
+
.mti-tool-link:hover { text-decoration: underline; }
|
|
1294
|
+
.mti-muted { color: ${PALETTE.muted}; }
|
|
1295
|
+
.mti-pill { display: inline-block; border: 1px solid ${PALETTE.border}; border-radius: 999px; padding: 0 7px; font-size: 10.5px; line-height: 16px;
|
|
1296
|
+
color: ${PALETTE.muted}; background: #FFFFFF; white-space: nowrap; }
|
|
1297
|
+
.mti-pill[data-ok='true'] { color: #1F7A4D; border-color: #BFDCCB; background: #F1FAF5; }
|
|
1298
|
+
.mti-pill[data-ok='false'] { color: ${PALETTE.failure}; border-color: #EAD3B3; background: #FDF6EA; }
|
|
1299
|
+
.mti-pill[data-state='active'] { color: ${PALETTE.primary}; border-color: #C9D2FF; background: ${PALETTE.tint}; }
|
|
1300
|
+
.mti-pager { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding-top: 10px; color: ${PALETTE.muted}; font-size: 11.5px; flex-wrap: wrap; }
|
|
1301
|
+
.mti-pager button { appearance: none; border: 1px solid ${PALETTE.border}; background: #FFFFFF; color: ${PALETTE.ink}; border-radius: 6px;
|
|
1302
|
+
font: inherit; font-size: 11.5px; padding: 3px 9px; cursor: pointer; }
|
|
1303
|
+
.mti-pager button:disabled { color: ${PALETTE.muted}; opacity: 0.55; cursor: default; }
|
|
1304
|
+
.mti-pager .mti-page-count { white-space: nowrap; }
|
|
1305
|
+
.mti-empty { border: 1px dashed ${PALETTE.border}; border-radius: 10px; padding: 26px 18px; text-align: center; color: ${PALETTE.muted};
|
|
1306
|
+
background: ${PALETTE.surface}; font-size: 12.5px; }
|
|
1307
|
+
.mti-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
1308
|
+
.mti-btn { appearance: none; border: 1px solid ${PALETTE.border}; background: #FFFFFF; color: ${PALETTE.ink}; border-radius: 7px;
|
|
1309
|
+
font: inherit; font-size: 12px; line-height: 18px; padding: 5px 11px; cursor: pointer; }
|
|
1310
|
+
.mti-btn:hover { border-color: ${PALETTE.primary}; color: ${PALETTE.primary}; }
|
|
1311
|
+
.mti-btn-primary { background: ${PALETTE.primary}; border-color: ${PALETTE.primary}; color: #FFFFFF; }
|
|
1312
|
+
.mti-btn-primary:hover { color: #FFFFFF; opacity: 0.92; }
|
|
1313
|
+
.mti-filters { display: flex; align-items: flex-end; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
1314
|
+
.mti-field { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
|
1315
|
+
.mti-field label { color: ${PALETTE.muted}; font-size: 10.5px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 600; }
|
|
1316
|
+
.mti-input, .mti-select { height: 30px; border: 1px solid ${PALETTE.border}; border-radius: 7px; background: #FFFFFF; color: ${PALETTE.ink};
|
|
1317
|
+
font: inherit; font-size: 12px; padding: 0 9px; min-width: 140px; }
|
|
1318
|
+
.mti-input { min-width: 220px; }
|
|
1319
|
+
.mti-group-title { display: flex; align-items: center; gap: 8px; margin: 14px 0 6px; color: ${PALETTE.muted};
|
|
1320
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; }
|
|
1321
|
+
.mti-group-title::after { content: ''; flex: 1 1 auto; height: 1px; background: ${PALETTE.border}; }
|
|
1322
|
+
.mti-detail { border: 1px solid ${PALETTE.border}; border-left: 3px solid ${PALETTE.primary}; background: ${PALETTE.surface};
|
|
1323
|
+
border-radius: 8px; padding: 11px 13px; margin: 10px 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px 16px; }
|
|
1324
|
+
.mti-detail dt { color: ${PALETTE.muted}; font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
1325
|
+
.mti-detail dd { margin: 1px 0 0; font-size: 12.5px; word-break: break-word; }
|
|
1326
|
+
.mti-mono { font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); font-size: 11.5px; }
|
|
1327
|
+
.mti-subcall td { background: ${PALETTE.surface}; }
|
|
1328
|
+
.mti-indent { padding-left: 22px; }
|
|
1329
|
+
.mti-expand { appearance: none; border: 0; background: transparent; color: ${PALETTE.muted}; cursor: pointer; font: inherit; font-size: 11px;
|
|
1330
|
+
width: 16px; padding: 0; text-align: left; }
|
|
1331
|
+
.mti-split { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; }
|
|
1332
|
+
.mti-kv { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 5px 12px; margin: 0; }
|
|
1333
|
+
.mti-kv dt { color: ${PALETTE.muted}; font-size: 11.5px; }
|
|
1334
|
+
.mti-kv dd { margin: 0; font-size: 12.5px; word-break: break-word; }
|
|
1335
|
+
@media (max-width: 980px) { .mti-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } .mti-grid, .mti-split { grid-template-columns: minmax(0, 1fr); } }
|
|
1336
|
+
@media (max-width: 720px) {
|
|
1337
|
+
.mti-page { padding: 12px 12px 36px; }
|
|
1338
|
+
.mti-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1339
|
+
.mti-title { font-size: 19px; }
|
|
1340
|
+
.mti-col-optional { display: none; }
|
|
1341
|
+
.mti-detail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1342
|
+
}
|
|
1343
|
+
.mti-create-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr); gap: 20px; align-items: start; }
|
|
1344
|
+
.mti-create-heading { margin: 18px 0 10px; font-size: 13px; font-weight: 650; letter-spacing: 0.01em; }
|
|
1345
|
+
.mti-create-heading:first-child { margin-top: 0; }
|
|
1346
|
+
.mti-create-row { display: flex; gap: 10px; align-items: flex-end; }
|
|
1347
|
+
.mti-create-checks { display: flex; gap: 16px; flex-wrap: wrap; margin: 12px 0 4px; color: ${PALETTE.ink}; font-size: 12.5px; }
|
|
1348
|
+
.mti-create-checks label { display: flex; align-items: center; gap: 6px; }
|
|
1349
|
+
.mti-create-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
|
1350
|
+
.mti-create-list { border: 1px solid ${PALETTE.border}; border-radius: 9px; overflow: hidden; }
|
|
1351
|
+
.mti-create-list-row { display: flex; justify-content: space-between; gap: 12px; align-items: center; padding: 10px 12px; border-bottom: 1px solid ${PALETTE.border}; }
|
|
1352
|
+
.mti-create-list-row:last-child { border-bottom: 0; }
|
|
1353
|
+
.mti-create-list-title { font-size: 12.5px; font-weight: 600; color: ${PALETTE.ink}; }
|
|
1354
|
+
.mti-create-list-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
1355
|
+
.mti-create-side { border-left: 1px solid ${PALETTE.border}; padding-left: 20px; }
|
|
1356
|
+
.mti-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 10px; min-width: 0; }
|
|
1357
|
+
.mti-field-wide { width: 100%; }
|
|
1358
|
+
.mti-field-grow { flex: 1 1 auto; }
|
|
1359
|
+
.mti-field label { color: ${PALETTE.muted}; font-size: 11px; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase; }
|
|
1360
|
+
.mti-input, .mti-select, .mti-textarea { box-sizing: border-box; width: 100%; border: 1px solid ${PALETTE.border}; border-radius: 7px;
|
|
1361
|
+
background: #FFFFFF; color: ${PALETTE.ink}; font: inherit; font-size: 12.5px; padding: 6px 9px; }
|
|
1362
|
+
.mti-textarea { font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); font-size: 11.5px; line-height: 1.45; resize: vertical; }
|
|
1363
|
+
.mti-input:focus-visible, .mti-select:focus-visible, .mti-textarea:focus-visible { outline: 2px solid ${PALETTE.primary}; outline-offset: 1px; }
|
|
1364
|
+
.mti-button { appearance: none; border: 1px solid ${PALETTE.border}; background: #FFFFFF; color: ${PALETTE.ink}; border-radius: 7px;
|
|
1365
|
+
font: inherit; font-size: 12px; padding: 5px 11px; cursor: pointer; }
|
|
1366
|
+
.mti-button:hover:not(:disabled) { border-color: ${PALETTE.primary}; color: ${PALETTE.primary}; }
|
|
1367
|
+
.mti-button:disabled { opacity: 0.55; cursor: default; }
|
|
1368
|
+
.mti-button-primary { background: ${PALETTE.primary}; border-color: ${PALETTE.primary}; color: #FFFFFF; }
|
|
1369
|
+
.mti-button-small { font-size: 11px; padding: 3px 8px; }
|
|
1370
|
+
.mti-help { margin: 0 0 10px; color: ${PALETTE.muted}; font-size: 11.5px; line-height: 16px; }
|
|
1371
|
+
.mti-note { margin: 10px 0; border-radius: 7px; padding: 8px 10px; font-size: 12px; line-height: 17px; }
|
|
1372
|
+
.mti-note-ok { background: #F1FAF5; border: 1px solid #BFDCCB; color: #1F7A4D; }
|
|
1373
|
+
.mti-note-error { background: #FDF6EA; border: 1px solid #EAD3B3; color: ${PALETTE.failure}; }
|
|
1374
|
+
.mti-issues { margin: 8px 0; padding-left: 18px; color: ${PALETTE.failure}; font-size: 11.5px; line-height: 17px; }
|
|
1375
|
+
.mti-pre { max-height: 220px; overflow: auto; margin: 10px 0; padding: 9px 10px; border: 1px solid ${PALETTE.border}; border-radius: 7px;
|
|
1376
|
+
background: ${PALETTE.surface}; font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace); font-size: 11px; line-height: 1.45; }
|
|
1377
|
+
@media (max-width: 980px) { .mti-create-grid { grid-template-columns: minmax(0, 1fr); } .mti-create-side { border-left: 0; padding-left: 0; } }
|
|
1378
|
+
@media (max-width: 720px) { .mti-create-row { flex-direction: column; align-items: stretch; } }
|
|
1379
|
+
@media (prefers-reduced-motion: reduce) { .mti-page * { transition: none !important; } }
|
|
1380
|
+
`
|
|
1381
|
+
|
|
1382
|
+
/** A section card with a title row. */
|
|
1383
|
+
function Card(title, note, children) {
|
|
1384
|
+
return h('section', { className: 'mti-card' },
|
|
1385
|
+
h('div', { className: 'mti-card-head' },
|
|
1386
|
+
h('h3', { className: 'mti-card-title' }, title),
|
|
1387
|
+
note === null || note === undefined ? null : h('span', { className: 'mti-card-note' }, note)),
|
|
1388
|
+
children)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/** One KPI tile. */
|
|
1392
|
+
function Kpi(label, value, hint) {
|
|
1393
|
+
return h('div', { className: 'mti-kpi', key: label },
|
|
1394
|
+
h('div', { className: 'mti-kpi-label' }, label),
|
|
1395
|
+
h('div', { className: 'mti-kpi-value' }, value),
|
|
1396
|
+
hint === null || hint === undefined ? null : h('div', { className: 'mti-kpi-hint' }, hint))
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/** A pager with a showing range and previous/next controls. */
|
|
1400
|
+
function Pager(info, onPage, label) {
|
|
1401
|
+
const name = typeof label === 'string' ? label : 'rows'
|
|
1402
|
+
return h('div', { className: 'mti-pager', role: 'navigation', 'aria-label': 'Pagination' },
|
|
1403
|
+
h('span', { className: 'mti-page-count' }, 'Showing ' + String(info.from) + '-' + String(info.to) + ' of ' + String(info.total) + ' ' + name),
|
|
1404
|
+
h('span', { className: 'mti-page-count' }, 'Page ' + String(info.page) + ' of ' + String(info.pages)),
|
|
1405
|
+
h('button', {
|
|
1406
|
+
type: 'button',
|
|
1407
|
+
disabled: info.page <= 1,
|
|
1408
|
+
onClick: () => onPage(info.page - 1),
|
|
1409
|
+
'aria-label': 'Previous page',
|
|
1410
|
+
}, 'Previous'),
|
|
1411
|
+
h('button', {
|
|
1412
|
+
type: 'button',
|
|
1413
|
+
disabled: info.page >= info.pages,
|
|
1414
|
+
onClick: () => onPage(info.page + 1),
|
|
1415
|
+
'aria-label': 'Next page',
|
|
1416
|
+
}, 'Next'))
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/** The segmented range control. */
|
|
1420
|
+
function RangeControl(range, onSelectRange) {
|
|
1421
|
+
return h('div', { className: 'mti-range' },
|
|
1422
|
+
h('span', { className: 'mti-range-label', id: 'mti-range-label' }, 'Range'),
|
|
1423
|
+
h('div', { className: 'mti-seg', role: 'group', 'aria-labelledby': 'mti-range-label' },
|
|
1424
|
+
RANGES.map(id => h('button', {
|
|
1425
|
+
type: 'button',
|
|
1426
|
+
key: id,
|
|
1427
|
+
'data-active': id === range ? 'true' : 'false',
|
|
1428
|
+
'aria-pressed': id === range,
|
|
1429
|
+
onClick: () => onSelectRange(id),
|
|
1430
|
+
}, id === 'all' ? 'All' : id))))
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** The three-tab strip. */
|
|
1434
|
+
function TabBar(props) {
|
|
1435
|
+
const tab = readTabFromHash('#' + String(props.tab))
|
|
1436
|
+
return h('div', { className: 'mti-tabs', role: 'tablist', 'aria-label': 'Tool insights sections' },
|
|
1437
|
+
TABS.map(entry => h('button', {
|
|
1438
|
+
key: entry.id,
|
|
1439
|
+
type: 'button',
|
|
1440
|
+
role: 'tab',
|
|
1441
|
+
className: 'mti-tab',
|
|
1442
|
+
id: 'mti-tab-' + entry.id,
|
|
1443
|
+
'data-tab': entry.id,
|
|
1444
|
+
'data-active': entry.id === tab ? 'true' : 'false',
|
|
1445
|
+
'aria-selected': entry.id === tab,
|
|
1446
|
+
'aria-controls': 'mti-panel-' + entry.id,
|
|
1447
|
+
tabIndex: 0,
|
|
1448
|
+
onClick: () => props.onSelect(entry.id),
|
|
1449
|
+
}, entry.label)))
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/** An explicit empty state. */
|
|
1453
|
+
function EmptyState(message) {
|
|
1454
|
+
return h('div', { className: 'mti-empty' }, message)
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/** A degraded-mode banner. */
|
|
1458
|
+
function Banner(message, kind) {
|
|
1459
|
+
if (typeof message !== 'string' || message.length === 0) return null
|
|
1460
|
+
return h('div', { className: 'mti-banner', role: 'status', 'data-kind': kind === 'warn' ? 'warn' : 'info' }, message)
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/** A status pill. */
|
|
1464
|
+
function Pill(label, props) {
|
|
1465
|
+
const extra = isObject(props) ? props : {}
|
|
1466
|
+
return h('span', { className: 'mti-pill', ...extra }, label)
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/** Calls-per-day bars (success + errors). */
|
|
1470
|
+
function CallsPerDay(days) {
|
|
1471
|
+
const rows = Array.isArray(days) ? days.filter(day => isObject(day) && typeof day.day === 'string') : []
|
|
1472
|
+
if (rows.length === 0) return EmptyState('No calls recorded in this range.')
|
|
1473
|
+
const peak = Math.max(1, ...rows.map(day => count(day.calls)))
|
|
1474
|
+
return h('div', { className: 'mti-bars', role: 'img', 'aria-label': 'Calls per day, success versus errors' },
|
|
1475
|
+
rows.slice(-30).map(day => {
|
|
1476
|
+
const calls = count(day.calls)
|
|
1477
|
+
const errors = Math.min(count(day.errors), calls)
|
|
1478
|
+
const height = Math.max(2, Math.round((calls / peak) * 80))
|
|
1479
|
+
const errorHeight = calls === 0 ? 0 : Math.round((errors / calls) * height)
|
|
1480
|
+
return h('div', { className: 'mti-bar', key: day.day, title: day.day + ': ' + String(calls) + ' calls, ' + String(errors) + ' errors' },
|
|
1481
|
+
h('div', { className: 'mti-bar-stack', style: { height: String(height) + 'px' } },
|
|
1482
|
+
h('div', { className: 'mti-bar-ok', style: { height: String(Math.max(0, height - errorHeight)) + 'px' } }),
|
|
1483
|
+
errorHeight > 0 ? h('div', { className: 'mti-bar-err', style: { height: String(errorHeight) + 'px' } }) : null),
|
|
1484
|
+
h('div', { className: 'mti-bar-label' }, day.day.slice(5)))
|
|
1485
|
+
}))
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/** The model leaderboard. */
|
|
1489
|
+
function ModelLeaderboard(models) {
|
|
1490
|
+
const rows = Array.isArray(models) ? models : []
|
|
1491
|
+
if (rows.length === 0) return EmptyState('No model attribution recorded in this range.')
|
|
1492
|
+
return h('table', { className: 'mti-table' },
|
|
1493
|
+
h('thead', null, h('tr', null,
|
|
1494
|
+
h('th', { scope: 'col' }, 'Model'),
|
|
1495
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Calls'),
|
|
1496
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Tools'),
|
|
1497
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Success'),
|
|
1498
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'p50'))),
|
|
1499
|
+
h('tbody', null, rows.slice(0, 8).map(row => h('tr', { key: row.model },
|
|
1500
|
+
h('td', null, h('span', { className: 'mti-name' }, row.model)),
|
|
1501
|
+
h('td', { className: 'mti-num' }, formatNumber(row.calls)),
|
|
1502
|
+
h('td', { className: 'mti-num' }, formatNumber(row.tools)),
|
|
1503
|
+
h('td', { className: 'mti-num' }, formatPercent(successRate(row.calls, row.errors))),
|
|
1504
|
+
h('td', { className: 'mti-num mti-col-optional' }, formatDuration(row.p50))))))
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/** The per-tool performance table with pagination. */
|
|
1508
|
+
function ToolPerformanceTable(props) {
|
|
1509
|
+
const rows = Array.isArray(props.rows) ? props.rows : []
|
|
1510
|
+
const page = paginate(rows, props.page, props.pageSize)
|
|
1511
|
+
if (rows.length === 0) return EmptyState('No tool performance recorded in this range.')
|
|
1512
|
+
return h('div', null,
|
|
1513
|
+
h('table', { className: 'mti-table' },
|
|
1514
|
+
h('thead', null, h('tr', null,
|
|
1515
|
+
h('th', { scope: 'col' }, 'Tool'),
|
|
1516
|
+
h('th', { scope: 'col', className: 'mti-col-optional' }, 'Owner'),
|
|
1517
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Calls'),
|
|
1518
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Success'),
|
|
1519
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'p50'),
|
|
1520
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'p95'),
|
|
1521
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'Max'),
|
|
1522
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'Total'),
|
|
1523
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'Code'),
|
|
1524
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'Last used'))),
|
|
1525
|
+
h('tbody', null, page.rows.map(row => h('tr', { key: row.name },
|
|
1526
|
+
h('td', null, h('button', {
|
|
1527
|
+
type: 'button', className: 'mti-tool-link', onClick: () => props.onSelectTool(row.name),
|
|
1528
|
+
title: 'Show this tool in the inventory',
|
|
1529
|
+
}, row.name)),
|
|
1530
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted mti-mono' }, row.owner ?? '—')),
|
|
1531
|
+
h('td', { className: 'mti-num' }, formatNumber(row.calls)),
|
|
1532
|
+
h('td', { className: 'mti-num' }, formatPercent(toolSuccessRate(row))),
|
|
1533
|
+
h('td', { className: 'mti-num' }, formatDuration(row.p50)),
|
|
1534
|
+
h('td', { className: 'mti-num mti-col-optional' }, formatDuration(row.p95)),
|
|
1535
|
+
h('td', { className: 'mti-num mti-col-optional' }, formatDuration(row.maxMs)),
|
|
1536
|
+
h('td', { className: 'mti-num mti-col-optional' }, formatDuration(row.totalMs)),
|
|
1537
|
+
h('td', { className: 'mti-num mti-col-optional' }, row.codeCalls === null ? '—' : formatNumber(row.codeCalls)),
|
|
1538
|
+
h('td', { className: 'mti-num mti-col-optional' }, formatAgo(row.lastUsed, props.now)))))),
|
|
1539
|
+
Pager(page.info, props.onPage, 'tools'))
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/** The All calls table with expandable Code Mode parents. */
|
|
1543
|
+
function AllCallsTable(props) {
|
|
1544
|
+
const rows = groupCallRows(props.calls)
|
|
1545
|
+
const page = paginate(rows, props.page, props.pageSize)
|
|
1546
|
+
if (rows.length === 0) return EmptyState('No tool calls in this range.')
|
|
1547
|
+
const expanded = isObject(props.expanded) ? props.expanded : {}
|
|
1548
|
+
return h('div', null,
|
|
1549
|
+
h('table', { className: 'mti-table' },
|
|
1550
|
+
h('thead', null, h('tr', null,
|
|
1551
|
+
h('th', { scope: 'col' }, ''),
|
|
1552
|
+
h('th', { scope: 'col' }, 'Time'),
|
|
1553
|
+
h('th', { scope: 'col' }, 'Tool'),
|
|
1554
|
+
h('th', { scope: 'col', className: 'mti-col-optional' }, 'Owner'),
|
|
1555
|
+
h('th', { scope: 'col', className: 'mti-col-optional' }, 'Session'),
|
|
1556
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'ms'),
|
|
1557
|
+
h('th', { scope: 'col' }, 'Status'))),
|
|
1558
|
+
h('tbody', null, page.rows.flatMap(row => {
|
|
1559
|
+
const open = expanded[row.callId ?? row.name + String(row.time)] === true
|
|
1560
|
+
const expandable = row.children.length > 0
|
|
1561
|
+
const key = row.callId ?? row.name + String(row.time)
|
|
1562
|
+
const self = h('tr', { key },
|
|
1563
|
+
h('td', null, expandable
|
|
1564
|
+
? h('button', {
|
|
1565
|
+
type: 'button', className: 'mti-expand', 'aria-expanded': open,
|
|
1566
|
+
onClick: () => props.onToggleCall(key),
|
|
1567
|
+
title: open ? 'Hide Code Mode sub-calls' : 'Show Code Mode sub-calls',
|
|
1568
|
+
}, open ? '▾' : '▸')
|
|
1569
|
+
: null),
|
|
1570
|
+
h('td', null, h('span', { className: 'mti-muted' }, formatAgo(row.time, props.now))),
|
|
1571
|
+
h('td', null, h('span', { className: 'mti-name' }, row.name), row.kind === 'code' ? ' ' : null,
|
|
1572
|
+
row.kind === 'code' && row.parentCallId === undefined ? Pill('CODE MODE') : null),
|
|
1573
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted mti-mono' }, row.owner ?? '—')),
|
|
1574
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted' }, row.sessionId ?? '—')),
|
|
1575
|
+
h('td', { className: 'mti-num' }, formatDuration(row.ms)),
|
|
1576
|
+
h('td', null, Pill(row.ok ? 'ok' : (row.errorCode ?? 'error'), { 'data-ok': row.ok ? 'true' : 'false' })))
|
|
1577
|
+
if (!expandable || !open) return [self]
|
|
1578
|
+
const children = row.children.map(child => h('tr', { key: child.callId ?? child.name, className: 'mti-subcall' },
|
|
1579
|
+
h('td', null),
|
|
1580
|
+
h('td', null, h('span', { className: 'mti-muted' }, formatAgo(child.time, props.now))),
|
|
1581
|
+
h('td', null, h('span', { className: 'mti-indent mti-name' }, child.name), ' ', Pill('sub-call')),
|
|
1582
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted mti-mono' }, child.owner ?? '—')),
|
|
1583
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted' }, child.sessionId ?? '—')),
|
|
1584
|
+
h('td', { className: 'mti-num' }, formatDuration(child.ms)),
|
|
1585
|
+
h('td', null, Pill(child.ok ? 'ok' : (child.errorCode ?? 'error'), { 'data-ok': child.ok ? 'true' : 'false' }))))
|
|
1586
|
+
return [self, ...children]
|
|
1587
|
+
}))),
|
|
1588
|
+
Pager(page.info, props.onPage, 'calls'))
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** The workbench (Tool Insights tab). */
|
|
1592
|
+
function InsightsView(props) {
|
|
1593
|
+
const model = isObject(props.model) ? props.model : deriveInsights({})
|
|
1594
|
+
const kpis = isObject(model.kpis) ? model.kpis : {}
|
|
1595
|
+
const hasCalls = count(kpis.calls) > 0
|
|
1596
|
+
return h('div', { role: 'tabpanel', id: 'mti-panel-insights', 'aria-labelledby': 'mti-tab-insights' },
|
|
1597
|
+
h('div', { className: 'mti-head' },
|
|
1598
|
+
h('div', null,
|
|
1599
|
+
h('h2', { className: 'mti-title' }, 'Tool Insights'),
|
|
1600
|
+
h('p', { className: 'mti-sub' },
|
|
1601
|
+
'What the agent actually did with tools: volume, health, latency, Code Mode nesting and model attribution. '
|
|
1602
|
+
+ 'Figures are host-reported; nothing on this page is estimated.')),
|
|
1603
|
+
RangeControl(props.range, props.onSelectRange)),
|
|
1604
|
+
Banner(model.banner, 'warn'),
|
|
1605
|
+
h('div', { className: 'mti-kpis' },
|
|
1606
|
+
Kpi('Calls', formatNumber(kpis.calls), model.range === 'all' ? 'all time' : 'in ' + String(model.range)),
|
|
1607
|
+
Kpi('Tools used', formatNumber(kpis.toolsUsed) + (finite(kpis.toolsAvailable) === null ? '' : ' / ' + formatNumber(kpis.toolsAvailable)),
|
|
1608
|
+
'distinct tools with activity'),
|
|
1609
|
+
Kpi('Success', formatPercent(kpis.successRate), formatNumber(kpis.errors) + ' failed calls'),
|
|
1610
|
+
Kpi('Tool time', formatDuration(kpis.toolMs), 'sum of settled calls'),
|
|
1611
|
+
Kpi('Sessions', formatNumber(kpis.sessions), 'sessions carrying usage'),
|
|
1612
|
+
Kpi('Calls / session', finite(kpis.callsPerSession) === null ? '—' : (kpis.callsPerSession).toFixed(1), 'mean across sessions'),
|
|
1613
|
+
Kpi('p50 latency', formatDuration(kpis.p50), finite(kpis.p95) === null ? '' : 'p95 ' + formatDuration(kpis.p95)),
|
|
1614
|
+
Kpi('Code Mode', formatPercent(kpis.codeModeShare), 'share of sub-calls')),
|
|
1615
|
+
h('div', { className: 'mti-grid' },
|
|
1616
|
+
Card('Calls per day', 'success versus errors, host-local days',
|
|
1617
|
+
CallsPerDay(model.days)),
|
|
1618
|
+
Card('Model leaderboard', 'who used tools most',
|
|
1619
|
+
ModelLeaderboard(model.models))),
|
|
1620
|
+
Card('Tool performance', 'settled calls; percentiles from the host histogram',
|
|
1621
|
+
ToolPerformanceTable({
|
|
1622
|
+
rows: model.tools, page: props.toolPage, pageSize: props.toolPageSize, now: props.now,
|
|
1623
|
+
onPage: props.onToolPage, onSelectTool: props.onSelectTool,
|
|
1624
|
+
})),
|
|
1625
|
+
Card('All calls', 'newest first; Code Mode parents expand into their sub-calls',
|
|
1626
|
+
h('div', null,
|
|
1627
|
+
h('div', { className: 'mti-actions', style: { marginBottom: '8px' } },
|
|
1628
|
+
h('button', { type: 'button', className: 'mti-btn', onClick: () => props.onOpenInventory() }, 'Inventory →')),
|
|
1629
|
+
hasCalls
|
|
1630
|
+
? AllCallsTable({
|
|
1631
|
+
calls: model.calls, page: props.callPage, pageSize: props.callPageSize, now: props.now,
|
|
1632
|
+
expanded: props.expanded, onToggleCall: props.onToggleCall, onPage: props.onCallPage,
|
|
1633
|
+
})
|
|
1634
|
+
: EmptyState('No tool calls in this range.'))))
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/** One inventory row's inline detail. */
|
|
1638
|
+
function ToolDetail(row, onClose) {
|
|
1639
|
+
return h('dl', { className: 'mti-detail' },
|
|
1640
|
+
h('div', null, h('dt', null, 'Owner'), h('dd', { className: 'mti-mono' }, row.owner ?? '—')),
|
|
1641
|
+
h('div', null, h('dt', null, 'Kind'), h('dd', null, row.kind)),
|
|
1642
|
+
h('div', null, h('dt', null, 'State'), h('dd', null, Pill(row.state, { 'data-state': row.state }))),
|
|
1643
|
+
h('div', null, h('dt', null, 'Calls'), h('dd', null, row.calls === null ? 'never called' : formatNumber(row.calls))),
|
|
1644
|
+
h('div', null, h('dt', null, 'Success'), h('dd', null, formatPercent(inventorySuccessRate(row)))),
|
|
1645
|
+
h('div', null, h('dt', null, 'Last used'), h('dd', null, formatAgo(row.lastUsed))),
|
|
1646
|
+
h('div', null, h('dt', null, 'Extension'), h('dd', { className: 'mti-mono' }, row.extension ?? '—')),
|
|
1647
|
+
h('div', null, h('dt', null, 'Version'), h('dd', null, row.extensionVersion ?? row.contractVersion ?? '—')),
|
|
1648
|
+
h('div', null, h('dt', null, 'Arguments'), h('dd', null, row.argumentCount === null ? '—' : String(row.argumentCount))),
|
|
1649
|
+
h('div', null, h('dt', null, 'MCP'), h('dd', null, row.mcpExposed === false ? 'withheld from MCP' : row.mcpExposed === true ? 'exposed' : '—')),
|
|
1650
|
+
h('div', { style: { gridColumn: '1 / -1' } },
|
|
1651
|
+
typeof row.description === 'string' && row.description.length > 0
|
|
1652
|
+
? h('dd', { className: 'mti-muted' }, row.description) : h('dd', { className: 'mti-muted' }, 'No description reported.' )),
|
|
1653
|
+
h('div', { style: { gridColumn: '1 / -1' } },
|
|
1654
|
+
h('button', { type: 'button', className: 'mti-btn', onClick: onClose }, 'Close detail')))
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/** The Tool Inventory tab. */
|
|
1658
|
+
function InventoryView(props) {
|
|
1659
|
+
const model = isObject(props.model) ? props.model : filterAndGroupInventory([], {})
|
|
1660
|
+
const filters = isObject(props.filters) ? props.filters : {}
|
|
1661
|
+
const rows = Array.isArray(model.filtered) ? model.filtered : []
|
|
1662
|
+
return h('div', { role: 'tabpanel', id: 'mti-panel-inventory', 'aria-labelledby': 'mti-tab-inventory' },
|
|
1663
|
+
h('div', { className: 'mti-head' },
|
|
1664
|
+
h('div', null,
|
|
1665
|
+
h('h2', { className: 'mti-title' }, 'Tool Inventory'),
|
|
1666
|
+
h('p', { className: 'mti-sub' },
|
|
1667
|
+
'Every registered tool, grouped by origin. State and provenance come from the host; usage columns join the '
|
|
1668
|
+
+ 'same report as the workbench. A tool with no calls is kept and labelled.')),
|
|
1669
|
+
h('div', { className: 'mti-actions' },
|
|
1670
|
+
h('button', { type: 'button', className: 'mti-btn', onClick: () => props.onBack() }, 'Back to workbench'))),
|
|
1671
|
+
model.available === false ? Banner(model.banner, 'warn') : null,
|
|
1672
|
+
h('div', { className: 'mti-filters' },
|
|
1673
|
+
h('div', { className: 'mti-field' },
|
|
1674
|
+
h('label', { htmlFor: 'mti-inv-search' }, 'Search'),
|
|
1675
|
+
h('input', {
|
|
1676
|
+
id: 'mti-inv-search', className: 'mti-input', type: 'search', value: filters.query ?? '',
|
|
1677
|
+
placeholder: 'Tool, owner or description',
|
|
1678
|
+
onChange: event => props.onFilter({ query: event.target.value }),
|
|
1679
|
+
})),
|
|
1680
|
+
h('div', { className: 'mti-field' },
|
|
1681
|
+
h('label', { htmlFor: 'mti-inv-origin' }, 'Origin'),
|
|
1682
|
+
h('select', {
|
|
1683
|
+
id: 'mti-inv-origin', className: 'mti-select', value: filters.origin ?? 'all',
|
|
1684
|
+
onChange: event => props.onFilter({ origin: event.target.value }),
|
|
1685
|
+
}, h('option', { value: 'all' }, 'All origins'), h('option', { value: 'core' }, 'DSH core'),
|
|
1686
|
+
h('option', { value: 'tool-mode' }, 'Tool Mode'), h('option', { value: 'custom' }, 'Custom tools'), h('option', { value: 'other' }, 'Other plugins'))),
|
|
1687
|
+
h('div', { className: 'mti-field' },
|
|
1688
|
+
h('label', { htmlFor: 'mti-inv-state' }, 'State'),
|
|
1689
|
+
h('select', {
|
|
1690
|
+
id: 'mti-inv-state', className: 'mti-select', value: filters.state ?? 'all',
|
|
1691
|
+
onChange: event => props.onFilter({ state: event.target.value }),
|
|
1692
|
+
}, h('option', { value: 'all' }, 'All states'), h('option', { value: 'active' }, 'active'),
|
|
1693
|
+
h('option', { value: 'prepared-cold' }, 'prepared-cold'), h('option', { value: 'failed' }, 'failed'))),
|
|
1694
|
+
h('div', { className: 'mti-field' },
|
|
1695
|
+
h('label', { htmlFor: 'mti-inv-size' }, 'Per page'),
|
|
1696
|
+
h('select', {
|
|
1697
|
+
id: 'mti-inv-size', className: 'mti-select', value: String(props.pageSize ?? 25),
|
|
1698
|
+
onChange: event => props.onPageSize(Number(event.target.value)),
|
|
1699
|
+
}, TOOL_PAGE_SIZES.map(size => h('option', { key: size, value: String(size) }, String(size)))))),
|
|
1700
|
+
rows.length === 0
|
|
1701
|
+
? EmptyState('No tools match these filters.')
|
|
1702
|
+
: h('div', null,
|
|
1703
|
+
model.groups.map(group => h('div', { key: group.origin },
|
|
1704
|
+
h('div', { className: 'mti-group-title' }, group.label + ' · ' + String(group.rows.length)),
|
|
1705
|
+
h('table', { className: 'mti-table' },
|
|
1706
|
+
h('thead', null, h('tr', null,
|
|
1707
|
+
h('th', { scope: 'col' }, 'Tool'),
|
|
1708
|
+
h('th', { scope: 'col', className: 'mti-col-optional' }, 'Owner'),
|
|
1709
|
+
h('th', { scope: 'col' }, 'Kind'),
|
|
1710
|
+
h('th', { scope: 'col' }, 'State'),
|
|
1711
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Calls'),
|
|
1712
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Success'),
|
|
1713
|
+
h('th', { scope: 'col', className: 'mti-num mti-col-optional' }, 'Last used'),
|
|
1714
|
+
h('th', { scope: 'col' }, 'MCP'))),
|
|
1715
|
+
h('tbody', null, group.rows.map(row => h('tr', { key: row.name },
|
|
1716
|
+
h('td', null, h('button', {
|
|
1717
|
+
type: 'button', className: 'mti-tool-link', onClick: () => props.onSelect(row.name),
|
|
1718
|
+
}, row.name)),
|
|
1719
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted mti-mono' }, row.owner ?? '—')),
|
|
1720
|
+
h('td', null, row.kind),
|
|
1721
|
+
h('td', null, Pill(row.state, { 'data-state': row.state })),
|
|
1722
|
+
h('td', { className: 'mti-num' }, row.calls === null ? h('span', { className: 'mti-muted' }, 'never called') : formatNumber(row.calls)),
|
|
1723
|
+
h('td', { className: 'mti-num' }, formatPercent(inventorySuccessRate(row))),
|
|
1724
|
+
h('td', { className: 'mti-num mti-col-optional' }, formatAgo(row.lastUsed)),
|
|
1725
|
+
h('td', null, row.mcpExposed === false
|
|
1726
|
+
? Pill('withheld from MCP', { 'data-ok': 'false' })
|
|
1727
|
+
: row.mcpExposed === true ? Pill('exposed', { 'data-ok': 'true' }) : h('span', { className: 'mti-muted' }, '—')))))))),
|
|
1728
|
+
Pager(model.info, props.onPage, 'tools')),
|
|
1729
|
+
props.selected === null || props.selected === undefined
|
|
1730
|
+
? null
|
|
1731
|
+
: (() => {
|
|
1732
|
+
const row = rows.find(candidate => candidate.name === props.selected)
|
|
1733
|
+
return row === undefined ? null : h('div', null, ToolDetail(row, () => props.onSelect(null)))
|
|
1734
|
+
})())
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/** Dedicated Model as Tool configuration tab. */
|
|
1738
|
+
function SettingsView(props) {
|
|
1739
|
+
return h('div', { role: 'tabpanel', id: 'mti-panel-settings', 'aria-labelledby': 'mti-tab-settings' },
|
|
1740
|
+
typeof props.settings === 'function'
|
|
1741
|
+
? props.settings()
|
|
1742
|
+
: h('p', { className: 'mti-sub' }, 'Model as Tool configuration is not available on this host.'))
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/** The MCP tab. */
|
|
1746
|
+
function McpView(props) {
|
|
1747
|
+
const model = isObject(props.model) ? props.model : mcpView(null, null, {})
|
|
1748
|
+
const clients = Array.isArray(model.clients) ? model.clients : []
|
|
1749
|
+
const withheld = Array.isArray(model.withheld) ? model.withheld : []
|
|
1750
|
+
const exposed = Array.isArray(model.exposed) ? model.exposed : []
|
|
1751
|
+
const header = h('div', { className: 'mti-head' },
|
|
1752
|
+
h('div', null,
|
|
1753
|
+
h('h2', { className: 'mti-title' }, 'MCP'),
|
|
1754
|
+
h('p', { className: 'mti-sub' },
|
|
1755
|
+
'The external MCP surface: the configured endpoint, the clients that actually hold a grant, which tools are '
|
|
1756
|
+
+ 'exposed, and the persisted external call log. Tokens, arguments and results are never shown.')),
|
|
1757
|
+
h('div', { className: 'mti-actions' },
|
|
1758
|
+
h('button', { type: 'button', className: 'mti-btn', onClick: () => props.onBack() }, 'Back to workbench')))
|
|
1759
|
+
const endpointCard = Card('Endpoint', model.enabled ? null : 'disabled',
|
|
1760
|
+
model.enabled === false
|
|
1761
|
+
? EmptyState('MCP endpoint is disabled on this host.')
|
|
1762
|
+
: h('dl', { className: 'mti-kv' },
|
|
1763
|
+
h('dt', null, 'Endpoint'), h('dd', { className: 'mti-mono' }, model.endpoint ?? '—'),
|
|
1764
|
+
h('dt', null, 'Transport'), h('dd', null, model.transport ?? '—'),
|
|
1765
|
+
h('dt', null, 'Auth'), h('dd', null, model.scheme ?? '—'),
|
|
1766
|
+
h('dt', null, 'Token file'), h('dd', { className: 'mti-mono' }, model.tokenFile ?? 'configured outside this page'),
|
|
1767
|
+
h('dt', null, 'Uptime'), h('dd', null, formatDuration(model.uptimeMs))))
|
|
1768
|
+
const exposureCard = Card('Exposure', 'model_tools is host-local and withheld by design',
|
|
1769
|
+
h('dl', { className: 'mti-kv' },
|
|
1770
|
+
h('dt', null, 'Exposed'),
|
|
1771
|
+
h('dd', null, 'Exposed ' + String(model.exposedCount) + ' of ' + String(model.exposedCount + model.withheldCount) + ' tools'),
|
|
1772
|
+
h('dt', null, 'Withheld'),
|
|
1773
|
+
h('dd', null, withheld.length === 0
|
|
1774
|
+
? h('span', { className: 'mti-muted' }, 'None reported')
|
|
1775
|
+
: withheld.map(name => Pill(name, { 'data-ok': 'false', key: name }))),
|
|
1776
|
+
h('dt', null, 'Tools'),
|
|
1777
|
+
h('dd', { className: 'mti-muted mti-mono' }, exposed.length === 0 ? '—' : exposed.slice(0, 12).join(', '))))
|
|
1778
|
+
const clientsTable = h('table', { className: 'mti-table' },
|
|
1779
|
+
h('thead', null, h('tr', null,
|
|
1780
|
+
h('th', { scope: 'col' }, 'Client'),
|
|
1781
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Calls'),
|
|
1782
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Success'),
|
|
1783
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'Last seen'))),
|
|
1784
|
+
h('tbody', null, clients.map(client => h('tr', { key: client.id },
|
|
1785
|
+
h('td', null, h('span', { className: 'mti-name mti-mono' }, client.id)),
|
|
1786
|
+
h('td', { className: 'mti-num' }, formatNumber(client.calls)),
|
|
1787
|
+
h('td', { className: 'mti-num' }, formatPercent(successRate(client.calls, client.errors))),
|
|
1788
|
+
h('td', { className: 'mti-num' }, formatAgo(client.lastSeen))))))
|
|
1789
|
+
const clientsCard = Card('Clients', 'identities come from grants, never invented',
|
|
1790
|
+
clients.length === 0 ? EmptyState('No MCP client has called this host yet.') : clientsTable)
|
|
1791
|
+
const callsTable = h('table', { className: 'mti-table' },
|
|
1792
|
+
h('thead', null, h('tr', null,
|
|
1793
|
+
h('th', { scope: 'col' }, 'Time'),
|
|
1794
|
+
h('th', { scope: 'col' }, 'Tool'),
|
|
1795
|
+
h('th', { scope: 'col' }, 'Client'),
|
|
1796
|
+
h('th', { scope: 'col', className: 'mti-num' }, 'ms'),
|
|
1797
|
+
h('th', { scope: 'col' }, 'Status'),
|
|
1798
|
+
h('th', { scope: 'col', className: 'mti-col-optional' }, 'Receipt'))),
|
|
1799
|
+
h('tbody', null, model.calls.map((call, index) => h('tr', { key: (call.receiptId ?? call.tool) + String(index) },
|
|
1800
|
+
h('td', null, h('span', { className: 'mti-muted' }, formatAgo(call.time, props.now))),
|
|
1801
|
+
h('td', null, h('span', { className: 'mti-name' }, call.tool)),
|
|
1802
|
+
h('td', null, h('span', { className: 'mti-muted mti-mono' }, call.clientId)),
|
|
1803
|
+
h('td', { className: 'mti-num' }, formatDuration(call.totalMs)),
|
|
1804
|
+
h('td', null, call.failureClass === undefined
|
|
1805
|
+
? Pill('ok', { 'data-ok': 'true' })
|
|
1806
|
+
: Pill(call.failureClass, { 'data-ok': 'false' })),
|
|
1807
|
+
h('td', { className: 'mti-col-optional' }, h('span', { className: 'mti-muted mti-mono' }, call.receiptId ?? '—'))))))
|
|
1808
|
+
const logCard = Card('External call log', 'persisted ledger; no payloads',
|
|
1809
|
+
h('div', null,
|
|
1810
|
+
(Array.isArray(model.calls) ? model.calls : []).length === 0
|
|
1811
|
+
? EmptyState('No external MCP calls recorded yet.')
|
|
1812
|
+
: callsTable,
|
|
1813
|
+
Pager(model.callsInfo, props.onPage, 'calls')))
|
|
1814
|
+
return h('div', { role: 'tabpanel', id: 'mti-panel-mcp', 'aria-labelledby': 'mti-tab-mcp' },
|
|
1815
|
+
header,
|
|
1816
|
+
model.available === false ? Banner(model.banner, 'warn') : null,
|
|
1817
|
+
h('div', { className: 'mti-split' }, endpointCard, exposureCard),
|
|
1818
|
+
clientsCard,
|
|
1819
|
+
logCard)
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/** The sidebar icon for the Tool Insights entry. */
|
|
1823
|
+
function ToolInsightsIcon() {
|
|
1824
|
+
return h('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': 'true' },
|
|
1825
|
+
h('path', { d: 'M2 13V7M6 13V3M10 13V9M14 13V5', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round' }),
|
|
1826
|
+
h('path', { d: 'M2 13h12', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', opacity: 0.55 }))
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
/** Read one injected Remote answer into a page state. */
|
|
1830
|
+
function remoteState(result) {
|
|
1831
|
+
if (result === null || result === undefined) return { status: 'unavailable', value: null }
|
|
1832
|
+
if (isObject(result) && typeof result.ok === 'boolean') {
|
|
1833
|
+
return result.ok === true
|
|
1834
|
+
? { status: 'ready', value: result.value }
|
|
1835
|
+
: { status: 'error', value: null, text: typeof result.error === 'object' && result.error !== null && typeof result.error.message === 'string' ? result.error.message : 'The host refused the request.' }
|
|
1836
|
+
}
|
|
1837
|
+
return { status: 'ready', value: result }
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
/**
|
|
1841
|
+
* Page-lifetime caches. Opening the page again reconstructs the component,
|
|
1842
|
+
* so component state alone would refetch every tab; these survive the
|
|
1843
|
+
* remount and make Inventory/MCP instant until their TTL expires.
|
|
1844
|
+
*/
|
|
1845
|
+
const REPORT_TTL_MS = 30_000
|
|
1846
|
+
const INVENTORY_TTL_MS = 5 * 60_000
|
|
1847
|
+
const MCP_TTL_MS = 30_000
|
|
1848
|
+
const reportCache = new Map()
|
|
1849
|
+
const inventoryCache = { value: null, at: 0 }
|
|
1850
|
+
const mcpCache = { value: null, calls: null, at: 0 }
|
|
1851
|
+
|
|
1852
|
+
/**
|
|
1853
|
+
* The page. Root props come from the host (`useSessions`, `useWorkspaces`);
|
|
1854
|
+
* the injected `call`, `openSession` and `now` come from the registration.
|
|
1855
|
+
*/
|
|
1856
|
+
function ToolInsightsPage(props) {
|
|
1857
|
+
const options = isObject(props) ? props : {}
|
|
1858
|
+
const sessionState = typeof options.useSessions === 'function' ? options.useSessions(state => state) : null
|
|
1859
|
+
const rows = isObject(sessionState) && Array.isArray(sessionState.ids)
|
|
1860
|
+
? sessionState.ids.map(id => (isObject(sessionState.byId) ? sessionState.byId[id] : undefined)).filter(row => isObject(row))
|
|
1861
|
+
: []
|
|
1862
|
+
|
|
1863
|
+
const initialHash = typeof location !== 'undefined' && location !== null ? location.hash : ''
|
|
1864
|
+
const [tab, setTab] = React.useState(() => readTabFromHash(initialHash))
|
|
1865
|
+
const [range, setRange] = React.useState('7d')
|
|
1866
|
+
const cachedReport = reportCache.get('7d')
|
|
1867
|
+
const [report, setReport] = React.useState(() => cachedReport === undefined
|
|
1868
|
+
? { status: 'loading', value: null }
|
|
1869
|
+
: { status: 'ready', value: cachedReport.value })
|
|
1870
|
+
const [inventory, setInventory] = React.useState(() => inventoryCache.at === 0
|
|
1871
|
+
? { status: 'loading', value: null }
|
|
1872
|
+
: { status: 'ready', value: inventoryCache.value })
|
|
1873
|
+
const [mcp, setMcp] = React.useState(() => mcpCache.at === 0
|
|
1874
|
+
? { status: 'loading', value: null, calls: null }
|
|
1875
|
+
: { status: 'ready', value: mcpCache.value, calls: mcpCache.calls })
|
|
1876
|
+
const [toolPage, setToolPage] = React.useState(1)
|
|
1877
|
+
const [toolPageSize, setToolPageSize] = React.useState(7)
|
|
1878
|
+
const [callPage, setCallPage] = React.useState(1)
|
|
1879
|
+
const [callPageSize, setCallPageSize] = React.useState(10)
|
|
1880
|
+
const [inventoryPage, setInventoryPage] = React.useState(1)
|
|
1881
|
+
const [inventoryPageSize, setInventoryPageSize] = React.useState(25)
|
|
1882
|
+
const [inventoryFilters, setInventoryFilters] = React.useState({ query: '', origin: 'all', state: 'all' })
|
|
1883
|
+
const [mcpPage, setMcpPage] = React.useState(1)
|
|
1884
|
+
const [expanded, setExpanded] = React.useState({})
|
|
1885
|
+
const [selectedTool, setSelectedTool] = React.useState(null)
|
|
1886
|
+
const reportRequest = React.useRef(0)
|
|
1887
|
+
|
|
1888
|
+
const call = typeof options.call === 'function' ? options.call : null
|
|
1889
|
+
const now = typeof options.now === 'function' ? options.now() : Date.now()
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Load the report for one range DIRECTLY from the range control as well as
|
|
1893
|
+
* from the mount effect. The gateway is not the only place this can fail:
|
|
1894
|
+
* an effect that updates its dependency array without executing leaves the
|
|
1895
|
+
* previous range's report on screen under the new label. A request counter
|
|
1896
|
+
* makes a slow answer for the old range unable to overwrite the new one.
|
|
1897
|
+
*/
|
|
1898
|
+
const loadReport = nextRange => {
|
|
1899
|
+
const request = ++reportRequest.current
|
|
1900
|
+
if (call === null) {
|
|
1901
|
+
setReport({ status: 'unavailable', value: null })
|
|
1902
|
+
return
|
|
1903
|
+
}
|
|
1904
|
+
setReport({ status: 'loading', value: null })
|
|
1905
|
+
Promise.resolve(requestUsageReport(call, nextRange, now)).then(result => {
|
|
1906
|
+
if (request !== reportRequest.current) return
|
|
1907
|
+
const state = remoteState(result)
|
|
1908
|
+
if (state.status === 'ready') reportCache.set(nextRange, { value: state.value, at: Date.now() })
|
|
1909
|
+
setReport(state)
|
|
1910
|
+
}, error => {
|
|
1911
|
+
if (request === reportRequest.current) {
|
|
1912
|
+
setReport({ status: 'error', value: null, text: String((error && error.message) || error) })
|
|
1913
|
+
}
|
|
1914
|
+
})
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
const selectTab = next => {
|
|
1918
|
+
const id = readTabFromHash('#' + String(next))
|
|
1919
|
+
setTab(id)
|
|
1920
|
+
try {
|
|
1921
|
+
if (typeof location !== 'undefined' && location !== null && typeof history !== 'undefined' && history !== null
|
|
1922
|
+
&& typeof history.replaceState === 'function') history.replaceState(null, '', tabHash(id))
|
|
1923
|
+
} catch (error) {
|
|
1924
|
+
// A host without history keeps the local tab state; the hash is a convenience.
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
React.useEffect(() => {
|
|
1929
|
+
const onHash = () => setTab(readTabFromHash(typeof location !== 'undefined' && location !== null ? location.hash : ''))
|
|
1930
|
+
if (typeof window !== 'undefined' && window !== null && typeof window.addEventListener === 'function') {
|
|
1931
|
+
window.addEventListener('hashchange', onHash)
|
|
1932
|
+
return () => window.removeEventListener('hashchange', onHash)
|
|
1933
|
+
}
|
|
1934
|
+
return undefined
|
|
1935
|
+
}, [])
|
|
1936
|
+
|
|
1937
|
+
React.useEffect(() => {
|
|
1938
|
+
if (call === null) {
|
|
1939
|
+
setReport({ status: 'unavailable', value: null })
|
|
1940
|
+
return
|
|
1941
|
+
}
|
|
1942
|
+
const cached = reportCache.get(range)
|
|
1943
|
+
if (cached !== undefined && Date.now() - cached.at < REPORT_TTL_MS) {
|
|
1944
|
+
setReport({ status: 'ready', value: cached.value })
|
|
1945
|
+
return
|
|
1946
|
+
}
|
|
1947
|
+
loadReport(range)
|
|
1948
|
+
}, [call, range])
|
|
1949
|
+
|
|
1950
|
+
React.useEffect(() => {
|
|
1951
|
+
if (tab !== 'inventory') return
|
|
1952
|
+
if (call === null) {
|
|
1953
|
+
setInventory({ status: 'unavailable', value: null })
|
|
1954
|
+
return
|
|
1955
|
+
}
|
|
1956
|
+
if (inventoryCache.at !== 0 && Date.now() - inventoryCache.at < INVENTORY_TTL_MS) {
|
|
1957
|
+
setInventory({ status: 'ready', value: inventoryCache.value })
|
|
1958
|
+
return
|
|
1959
|
+
}
|
|
1960
|
+
let alive = true
|
|
1961
|
+
setInventory({ status: 'loading', value: null })
|
|
1962
|
+
Promise.resolve(call('toolInventory')).then(result => {
|
|
1963
|
+
const state = remoteState(result)
|
|
1964
|
+
if (!alive) return
|
|
1965
|
+
if (state.status === 'ready') {
|
|
1966
|
+
inventoryCache.value = state.value
|
|
1967
|
+
inventoryCache.at = Date.now()
|
|
1968
|
+
}
|
|
1969
|
+
setInventory(state)
|
|
1970
|
+
}, error => {
|
|
1971
|
+
if (alive) setInventory({ status: 'error', value: null, text: String((error && error.message) || error) })
|
|
1972
|
+
})
|
|
1973
|
+
return () => { alive = false }
|
|
1974
|
+
}, [call, tab])
|
|
1975
|
+
|
|
1976
|
+
React.useEffect(() => {
|
|
1977
|
+
if (tab !== 'mcp') return
|
|
1978
|
+
if (call === null) {
|
|
1979
|
+
setMcp({ status: 'unavailable', value: null, calls: null })
|
|
1980
|
+
return
|
|
1981
|
+
}
|
|
1982
|
+
if (mcpCache.at !== 0 && Date.now() - mcpCache.at < MCP_TTL_MS) {
|
|
1983
|
+
setMcp({ status: 'ready', value: mcpCache.value, calls: mcpCache.calls })
|
|
1984
|
+
return
|
|
1985
|
+
}
|
|
1986
|
+
let alive = true
|
|
1987
|
+
setMcp({ status: 'loading', value: null, calls: null })
|
|
1988
|
+
Promise.resolve(call('mcpStatus')).then(result => {
|
|
1989
|
+
const state = remoteState(result)
|
|
1990
|
+
if (!alive) return
|
|
1991
|
+
if (state.status !== 'ready') {
|
|
1992
|
+
setMcp(state.status === 'unavailable'
|
|
1993
|
+
? { status: 'unavailable', value: null, calls: null }
|
|
1994
|
+
: { status: 'error', value: null, calls: null, text: state.text })
|
|
1995
|
+
return
|
|
1996
|
+
}
|
|
1997
|
+
setMcp({ status: 'ready', value: state.value, calls: null })
|
|
1998
|
+
Promise.resolve(requestMcpCalls(call, 1, 50)).then(calls => {
|
|
1999
|
+
if (!alive) return
|
|
2000
|
+
const callState = remoteState(calls)
|
|
2001
|
+
mcpCache.value = state.value
|
|
2002
|
+
mcpCache.calls = callState.value
|
|
2003
|
+
mcpCache.at = Date.now()
|
|
2004
|
+
setMcp({ status: 'ready', value: state.value, calls: callState.value })
|
|
2005
|
+
}, () => {
|
|
2006
|
+
if (!alive) return
|
|
2007
|
+
mcpCache.value = state.value
|
|
2008
|
+
mcpCache.calls = null
|
|
2009
|
+
mcpCache.at = Date.now()
|
|
2010
|
+
setMcp({ status: 'ready', value: state.value, calls: null })
|
|
2011
|
+
})
|
|
2012
|
+
}, error => {
|
|
2013
|
+
if (alive) setMcp({ status: 'error', value: null, calls: null, text: String((error && error.message) || error) })
|
|
2014
|
+
})
|
|
2015
|
+
return () => { alive = false }
|
|
2016
|
+
}, [call, tab])
|
|
2017
|
+
|
|
2018
|
+
const projection = aggregateProjections(rows)
|
|
2019
|
+
const inventoryValue = inventory.status === 'ready' && isObject(inventory.value) ? inventory.value : null
|
|
2020
|
+
const inventoryTools = inventoryValue !== null && Array.isArray(inventoryValue.tools) ? inventoryValue.tools : []
|
|
2021
|
+
const toolsAvailable = inventoryTools.length > 0 ? inventoryTools.length : null
|
|
2022
|
+
const model = deriveInsights({
|
|
2023
|
+
projection,
|
|
2024
|
+
report: report.status === 'ready' ? report.value : null,
|
|
2025
|
+
range,
|
|
2026
|
+
now,
|
|
2027
|
+
toolsAvailable,
|
|
2028
|
+
})
|
|
2029
|
+
const inventoryModel = filterAndGroupInventory(inventoryTools, {
|
|
2030
|
+
...inventoryFilters,
|
|
2031
|
+
page: inventoryPage,
|
|
2032
|
+
pageSize: inventoryPageSize,
|
|
2033
|
+
})
|
|
2034
|
+
const mcpModel = mcpView(mcp.value, mcp.calls, { page: mcpPage, pageSize: 10 })
|
|
2035
|
+
|
|
2036
|
+
let body
|
|
2037
|
+
if (tab === 'inventory') {
|
|
2038
|
+
body = InventoryView({
|
|
2039
|
+
model: inventory.status === 'ready' ? inventoryModel : {
|
|
2040
|
+
available: false,
|
|
2041
|
+
banner: inventory.status === 'loading'
|
|
2042
|
+
? 'Reading the tool inventory from the host…'
|
|
2043
|
+
: inventory.status === 'error'
|
|
2044
|
+
? (inventory.text || 'The host refused the tool inventory request.')
|
|
2045
|
+
: 'The tool inventory is not available on this host build (the toolInventory remote method is absent).',
|
|
2046
|
+
groups: [], filtered: [], total: 0, info: pageInfo(0, inventoryPageSize, 1),
|
|
2047
|
+
},
|
|
2048
|
+
filters: inventoryFilters,
|
|
2049
|
+
page: inventoryPage,
|
|
2050
|
+
pageSize: inventoryPageSize,
|
|
2051
|
+
selected: selectedTool,
|
|
2052
|
+
onFilter: patch => {
|
|
2053
|
+
setInventoryFilters(previous => ({ ...previous, ...patch }))
|
|
2054
|
+
setInventoryPage(1)
|
|
2055
|
+
},
|
|
2056
|
+
onPage: page => setInventoryPage(page),
|
|
2057
|
+
onPageSize: size => { setInventoryPageSize(size); setInventoryPage(1) },
|
|
2058
|
+
onSelect: name => setSelectedTool(name ?? null),
|
|
2059
|
+
onBack: () => selectTab('insights'),
|
|
2060
|
+
})
|
|
2061
|
+
} else if (tab === 'mcp') {
|
|
2062
|
+
body = McpView({ model: mcpModel, page: mcpPage, onPage: page => setMcpPage(page), onBack: () => selectTab('insights') })
|
|
2063
|
+
} else if (tab === 'create') {
|
|
2064
|
+
body = typeof CreateToolView === 'function'
|
|
2065
|
+
? CreateToolView({ call: options.call })
|
|
2066
|
+
: Banner('The Create tool editor is unavailable in this build.', 'mti-banner mti-banner-warn')
|
|
2067
|
+
} else if (tab === 'settings') {
|
|
2068
|
+
body = SettingsView({ settings: options.settings })
|
|
2069
|
+
} else {
|
|
2070
|
+
body = InsightsView({
|
|
2071
|
+
model,
|
|
2072
|
+
range,
|
|
2073
|
+
now,
|
|
2074
|
+
toolPage,
|
|
2075
|
+
toolPageSize,
|
|
2076
|
+
callPage,
|
|
2077
|
+
callPageSize,
|
|
2078
|
+
expanded,
|
|
2079
|
+
onSelectRange: next => { setRange(next); setToolPage(1); setCallPage(1); loadReport(next) },
|
|
2080
|
+
onOpenInventory: () => selectTab('inventory'),
|
|
2081
|
+
onToolPage: page => setToolPage(page),
|
|
2082
|
+
onCallPage: page => setCallPage(page),
|
|
2083
|
+
onToggleCall: key => setExpanded(previous => ({ ...previous, [key]: previous[key] !== true })),
|
|
2084
|
+
onSelectTool: name => { setSelectedTool(name); selectTab('inventory') },
|
|
2085
|
+
onCallPageSize: size => { setCallPageSize(size); setCallPage(1) },
|
|
2086
|
+
})
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
return h('div', { className: 'mti-page', 'data-page': 'tool-insights' },
|
|
2090
|
+
h('style', null, CSS),
|
|
2091
|
+
TabBar({ tab, onSelect: selectTab }),
|
|
2092
|
+
body)
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
return {
|
|
2096
|
+
// pure helpers (tests and the registration both use them)
|
|
2097
|
+
PALETTE,
|
|
2098
|
+
TABS,
|
|
2099
|
+
RANGES,
|
|
2100
|
+
TOOL_PAGE_SIZES,
|
|
2101
|
+
readTabFromHash,
|
|
2102
|
+
tabHash,
|
|
2103
|
+
formatNumber,
|
|
2104
|
+
formatPercent,
|
|
2105
|
+
formatDuration,
|
|
2106
|
+
formatBytes,
|
|
2107
|
+
formatAgo,
|
|
2108
|
+
pageInfo,
|
|
2109
|
+
paginate,
|
|
2110
|
+
normalizeProjection,
|
|
2111
|
+
aggregateProjections,
|
|
2112
|
+
normalizeReport,
|
|
2113
|
+
deriveInsights,
|
|
2114
|
+
rangeWindow,
|
|
2115
|
+
reportQuery,
|
|
2116
|
+
requestUsageReport,
|
|
2117
|
+
requestMcpCalls,
|
|
2118
|
+
groupCallRows,
|
|
2119
|
+
filterAndGroupInventory,
|
|
2120
|
+
inventorySuccessRate,
|
|
2121
|
+
originOf,
|
|
2122
|
+
normalizeMcpStatus,
|
|
2123
|
+
mcpView,
|
|
2124
|
+
// components
|
|
2125
|
+
Card,
|
|
2126
|
+
Kpi,
|
|
2127
|
+
Pager,
|
|
2128
|
+
TabBar,
|
|
2129
|
+
Banner,
|
|
2130
|
+
EmptyState,
|
|
2131
|
+
CallsPerDay,
|
|
2132
|
+
ModelLeaderboard,
|
|
2133
|
+
ToolPerformanceTable,
|
|
2134
|
+
AllCallsTable,
|
|
2135
|
+
InsightsView,
|
|
2136
|
+
InventoryView,
|
|
2137
|
+
McpView,
|
|
2138
|
+
SettingsView,
|
|
2139
|
+
...(typeof CreateToolView === 'function' ? { CreateToolView } : {}),
|
|
2140
|
+
ToolInsightsPage,
|
|
2141
|
+
ToolInsightsIcon,
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
const toolInsights = createToolInsights(h, React)
|
|
2146
|
+
|
|
2147
|
+
/**
|
|
2148
|
+
* The catalog, embedded at build time by scripts/gen-client-catalog.mjs.
|
|
2149
|
+
*
|
|
2150
|
+
* Each entry: { id, name, summary, package, capability, sizeMB, license,
|
|
2151
|
+
* runnable, reason }. `runnable` is this host's capability verdict, so the
|
|
2152
|
+
* page can only offer toggles for models that can actually run and can say
|
|
2153
|
+
* why the rest cannot.
|
|
2154
|
+
*/
|
|
2155
|
+
const CATALOG = /*__CATALOG_JSON__*/
|
|
2156
|
+
[
|
|
2157
|
+
{
|
|
2158
|
+
"id": "align",
|
|
2159
|
+
"name": "Align",
|
|
2160
|
+
"summary": "Word-timestamp refinement for any transcript, on device, from 0.7MB.",
|
|
2161
|
+
"package": "@desert-ant-labs/align",
|
|
2162
|
+
"extension": null,
|
|
2163
|
+
"capability": "audio.word-alignment",
|
|
2164
|
+
"sizeMB": 0.7,
|
|
2165
|
+
"license": "source-available",
|
|
2166
|
+
"runnable": false,
|
|
2167
|
+
"reason": "Align has no prebuilt native build for win32-x64: @desert-ant-labs/align/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2168
|
+
},
|
|
2169
|
+
{
|
|
2170
|
+
"id": "clear",
|
|
2171
|
+
"name": "Clear",
|
|
2172
|
+
"summary": "On-device speech enhancement: denoise, dereverb and loudness-normalize to 48 kHz.",
|
|
2173
|
+
"package": "@desert-ant-labs/clear",
|
|
2174
|
+
"extension": null,
|
|
2175
|
+
"capability": "audio.speech-enhancement",
|
|
2176
|
+
"sizeMB": null,
|
|
2177
|
+
"license": "source-available",
|
|
2178
|
+
"runnable": false,
|
|
2179
|
+
"reason": "Clear has no prebuilt native build for win32-x64: @desert-ant-labs/clear/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2180
|
+
},
|
|
2181
|
+
{
|
|
2182
|
+
"id": "clips",
|
|
2183
|
+
"name": "Clips",
|
|
2184
|
+
"summary": "Clip selection that turns a long recording into ranked shorts and highlights from a transcript.",
|
|
2185
|
+
"package": null,
|
|
2186
|
+
"extension": null,
|
|
2187
|
+
"capability": "video.clip-selection",
|
|
2188
|
+
"sizeMB": null,
|
|
2189
|
+
"license": "source-available",
|
|
2190
|
+
"runnable": false,
|
|
2191
|
+
"reason": "Clips has no npm package; run it through the Desert Ant CLI (macOS/Linux) with `cli.enabled`, or through a bridge with `remote.endpoint`."
|
|
2192
|
+
},
|
|
2193
|
+
{
|
|
2194
|
+
"id": "ear",
|
|
2195
|
+
"name": "Ear",
|
|
2196
|
+
"summary": "On-device spoken language identification across 102 languages from a short audio stretch.",
|
|
2197
|
+
"package": "@desert-ant-labs/ear",
|
|
2198
|
+
"extension": null,
|
|
2199
|
+
"capability": "audio.spoken-language-identification",
|
|
2200
|
+
"sizeMB": null,
|
|
2201
|
+
"license": "source-available",
|
|
2202
|
+
"runnable": false,
|
|
2203
|
+
"reason": "Ear has no prebuilt native build for win32-x64: @desert-ant-labs/ear/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
"id": "emo",
|
|
2207
|
+
"name": "Emo",
|
|
2208
|
+
"summary": "Type a task or message and get the emoji that fit, in 22 languages, on the device.",
|
|
2209
|
+
"package": "@desert-ant-labs/emo",
|
|
2210
|
+
"extension": null,
|
|
2211
|
+
"capability": "text.emoji-suggestion",
|
|
2212
|
+
"sizeMB": 10.2,
|
|
2213
|
+
"license": "source-available",
|
|
2214
|
+
"runnable": false,
|
|
2215
|
+
"reason": "Emo has no prebuilt native build for win32-x64: @desert-ant-labs/emo/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2216
|
+
},
|
|
2217
|
+
{
|
|
2218
|
+
"id": "eye",
|
|
2219
|
+
"name": "Eye",
|
|
2220
|
+
"summary": "On-device frame scoring that tells you which shot to keep, what is in it, and which shots repeat.",
|
|
2221
|
+
"package": null,
|
|
2222
|
+
"extension": null,
|
|
2223
|
+
"capability": "image.frame-scoring",
|
|
2224
|
+
"sizeMB": null,
|
|
2225
|
+
"license": "source-available",
|
|
2226
|
+
"runnable": false,
|
|
2227
|
+
"reason": "Eye is in closed beta: the weights are published at https://huggingface.co/desert-ant-labs/eye but no SDK ships them yet, so it cannot be installed as a tool."
|
|
2228
|
+
},
|
|
2229
|
+
{
|
|
2230
|
+
"id": "face",
|
|
2231
|
+
"name": "Face",
|
|
2232
|
+
"summary": "On-device face matching that finds the same person across a photo library or through a video.",
|
|
2233
|
+
"package": null,
|
|
2234
|
+
"extension": null,
|
|
2235
|
+
"capability": "image.face-matching",
|
|
2236
|
+
"sizeMB": null,
|
|
2237
|
+
"license": "source-available",
|
|
2238
|
+
"runnable": false,
|
|
2239
|
+
"reason": "Face is in closed beta: the weights are published at https://huggingface.co/desert-ant-labs/face but no SDK ships them yet, so it cannot be installed as a tool."
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
"id": "gist",
|
|
2243
|
+
"name": "Gist",
|
|
2244
|
+
"summary": "On-device topic tagging across a 36-topic taxonomy, in 101 languages, with no server call.",
|
|
2245
|
+
"package": "@desert-ant-labs/gist",
|
|
2246
|
+
"extension": null,
|
|
2247
|
+
"capability": "text.topic-tagging",
|
|
2248
|
+
"sizeMB": null,
|
|
2249
|
+
"license": "source-available",
|
|
2250
|
+
"runnable": false,
|
|
2251
|
+
"reason": "Gist has no prebuilt native build for win32-x64: @desert-ant-labs/gist/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2252
|
+
},
|
|
2253
|
+
{
|
|
2254
|
+
"id": "moderator",
|
|
2255
|
+
"name": "Moderator",
|
|
2256
|
+
"summary": "On-device NSFW detection that flags nudity before upload, trained only on licensed and synthetic data.",
|
|
2257
|
+
"package": "@desert-ant-labs/moderator",
|
|
2258
|
+
"extension": null,
|
|
2259
|
+
"capability": "image.nsfw-detection",
|
|
2260
|
+
"sizeMB": 9.2,
|
|
2261
|
+
"license": "source-available",
|
|
2262
|
+
"runnable": false,
|
|
2263
|
+
"reason": "Moderator has no prebuilt native build for win32-x64: @desert-ant-labs/moderator/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
"id": "redact",
|
|
2267
|
+
"name": "Redact",
|
|
2268
|
+
"summary": "On-device redaction that strips personal data as you type, in 27 languages, with restore support.",
|
|
2269
|
+
"package": "@desert-ant-labs/redact",
|
|
2270
|
+
"extension": null,
|
|
2271
|
+
"capability": "text.pii-redaction",
|
|
2272
|
+
"sizeMB": null,
|
|
2273
|
+
"license": "source-available",
|
|
2274
|
+
"runnable": false,
|
|
2275
|
+
"reason": "Redact has no prebuilt native build for win32-x64: @desert-ant-labs/redact/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
"id": "schemer",
|
|
2279
|
+
"name": "Schemer",
|
|
2280
|
+
"summary": "Extract typed fields from any text against your own schema, on device, with empty fields left empty.",
|
|
2281
|
+
"package": null,
|
|
2282
|
+
"extension": null,
|
|
2283
|
+
"capability": "text.structured-extraction",
|
|
2284
|
+
"sizeMB": null,
|
|
2285
|
+
"license": "source-available",
|
|
2286
|
+
"runnable": false,
|
|
2287
|
+
"reason": "Schemer is in closed beta: the weights are published at https://huggingface.co/desert-ant-labs/schemer but no SDK ships them yet, so it cannot be installed as a tool."
|
|
2288
|
+
},
|
|
2289
|
+
{
|
|
2290
|
+
"id": "shapes",
|
|
2291
|
+
"name": "Shapes",
|
|
2292
|
+
"summary": "On-device shape recognition that turns one hand-drawn stroke into a rectangle, ellipse, triangle, star or line in under 10ms.",
|
|
2293
|
+
"package": "@desert-ant-labs/shapes",
|
|
2294
|
+
"extension": null,
|
|
2295
|
+
"capability": "stroke.shape-recognition",
|
|
2296
|
+
"sizeMB": null,
|
|
2297
|
+
"license": "source-available",
|
|
2298
|
+
"runnable": false,
|
|
2299
|
+
"reason": "Shapes has no prebuilt native build for win32-x64: @desert-ant-labs/shapes/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
"id": "title",
|
|
2303
|
+
"name": "Title",
|
|
2304
|
+
"summary": "On-device titles and descriptions: a short factual title and a one- or two-sentence description for any passage.",
|
|
2305
|
+
"package": null,
|
|
2306
|
+
"extension": null,
|
|
2307
|
+
"capability": "text.title-generation",
|
|
2308
|
+
"sizeMB": null,
|
|
2309
|
+
"license": "source-available",
|
|
2310
|
+
"runnable": false,
|
|
2311
|
+
"reason": "Title is Apple-only (Core ML/MLX through the Swift package); there is no JavaScript SDK, so it cannot run in a Node process. Point `remote.endpoint` at a bridge on an Apple machine to reach it."
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
"id": "tongue",
|
|
2315
|
+
"name": "Tongue",
|
|
2316
|
+
"summary": "On-device language identification for short text across 84 languages, from just a few words.",
|
|
2317
|
+
"package": "@desert-ant-labs/tongue",
|
|
2318
|
+
"extension": "@try-works/extension-desert-ant-tongue",
|
|
2319
|
+
"capability": "text.language-identification",
|
|
2320
|
+
"sizeMB": 2,
|
|
2321
|
+
"license": "source-available",
|
|
2322
|
+
"runnable": true
|
|
2323
|
+
},
|
|
2324
|
+
{
|
|
2325
|
+
"id": "toxic",
|
|
2326
|
+
"name": "Toxic",
|
|
2327
|
+
"summary": "On-device moderation that flags hateful, abusive and threatening text in 23 European languages.",
|
|
2328
|
+
"package": null,
|
|
2329
|
+
"extension": null,
|
|
2330
|
+
"capability": "text.toxicity-detection",
|
|
2331
|
+
"sizeMB": null,
|
|
2332
|
+
"license": "source-available",
|
|
2333
|
+
"runnable": false,
|
|
2334
|
+
"reason": "Toxic is in closed beta: the weights are published at https://huggingface.co/desert-ant-labs/toxic but no SDK ships them yet, so it cannot be installed as a tool."
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
"id": "uhm",
|
|
2338
|
+
"name": "Uhm",
|
|
2339
|
+
"summary": "On-device filler detection that marks every um, uh and hmm to within 20ms, an hour of audio in 12s.",
|
|
2340
|
+
"package": null,
|
|
2341
|
+
"extension": null,
|
|
2342
|
+
"capability": "audio.filler-detection",
|
|
2343
|
+
"sizeMB": null,
|
|
2344
|
+
"license": "source-available",
|
|
2345
|
+
"runnable": false,
|
|
2346
|
+
"reason": "Uhm is Apple-only (Core ML/MLX through the Swift package); there is no JavaScript SDK, so it cannot run in a Node process. Point `remote.endpoint` at a bridge on an Apple machine to reach it."
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
"id": "voz",
|
|
2350
|
+
"name": "Voz",
|
|
2351
|
+
"summary": "Transcribe 10 minutes of audio in 2s on an iPhone, 14x faster than Whisper, with word timestamps.",
|
|
2352
|
+
"package": "@desert-ant-labs/voz",
|
|
2353
|
+
"extension": "@try-works/extension-desert-ant-voz",
|
|
2354
|
+
"capability": "audio.speech-recognition",
|
|
2355
|
+
"sizeMB": 349,
|
|
2356
|
+
"license": "source-available",
|
|
2357
|
+
"runnable": true
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
"id": "who",
|
|
2361
|
+
"name": "Who",
|
|
2362
|
+
"summary": "On-device speaker labeling that turns a recording into per-person turns with face boxes and timestamps.",
|
|
2363
|
+
"package": null,
|
|
2364
|
+
"extension": null,
|
|
2365
|
+
"capability": "audio.speaker-labeling",
|
|
2366
|
+
"sizeMB": null,
|
|
2367
|
+
"license": "source-available",
|
|
2368
|
+
"runnable": false,
|
|
2369
|
+
"reason": "Who is in closed beta: the weights are published at https://huggingface.co/desert-ant-labs/who but no SDK ships them yet, so it cannot be installed as a tool."
|
|
2370
|
+
}
|
|
2371
|
+
]
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* The Tool Mode extensions this repository ships, embedded at build time.
|
|
2375
|
+
*
|
|
2376
|
+
* Each entry: { id, name, package, version, license, summary, models, tools,
|
|
2377
|
+
* contracts }. The host's extension store is the authority for
|
|
2378
|
+
* installed/active state, and it is reachable as `ctx.remote.modelTools`
|
|
2379
|
+
* when this client assembly mounts that Remote. When it does not, the page
|
|
2380
|
+
* degrades to the config-only `disabledExtensions` preference and says so:
|
|
2381
|
+
* it never invents an installed state.
|
|
2382
|
+
*/
|
|
2383
|
+
const EXTENSIONS = /*__EXTENSIONS_JSON__*/
|
|
2384
|
+
[
|
|
2385
|
+
{
|
|
2386
|
+
"id": "@try-works/extension-declarative-recipes",
|
|
2387
|
+
"name": "declarative recipes",
|
|
2388
|
+
"package": "@try-works/extension-declarative-recipes",
|
|
2389
|
+
"version": "0.1.0",
|
|
2390
|
+
"license": "MIT",
|
|
2391
|
+
"summary": "Ships NO CODE.",
|
|
2392
|
+
"models": [],
|
|
2393
|
+
"tools": [],
|
|
2394
|
+
"contracts": [
|
|
2395
|
+
"text.title",
|
|
2396
|
+
"image.describe"
|
|
2397
|
+
]
|
|
2398
|
+
},
|
|
2399
|
+
{
|
|
2400
|
+
"id": "@try-works/extension-desert-ant-tongue",
|
|
2401
|
+
"name": "desert ant tongue",
|
|
2402
|
+
"package": "@try-works/extension-desert-ant-tongue",
|
|
2403
|
+
"version": "0.1.3",
|
|
2404
|
+
"license": "MIT",
|
|
2405
|
+
"summary": "Language identification over the Desert Ant Labs Tongue model, exposed as the standard tool-mode/lang_detect contract.",
|
|
2406
|
+
"models": [
|
|
2407
|
+
"tongue"
|
|
2408
|
+
],
|
|
2409
|
+
"tools": [
|
|
2410
|
+
"lang_detect"
|
|
2411
|
+
],
|
|
2412
|
+
"contracts": [
|
|
2413
|
+
"tool-mode/lang_detect"
|
|
2414
|
+
]
|
|
2415
|
+
},
|
|
2416
|
+
{
|
|
2417
|
+
"id": "@try-works/extension-desert-ant-voz",
|
|
2418
|
+
"name": "desert ant voz",
|
|
2419
|
+
"package": "@try-works/extension-desert-ant-voz",
|
|
2420
|
+
"version": "0.1.5",
|
|
2421
|
+
"license": "MIT",
|
|
2422
|
+
"summary": "On-device speech recognition over the Desert Ant Labs Voz model, exposed as the standard tool-mode/stt_transcribe contract.",
|
|
2423
|
+
"models": [
|
|
2424
|
+
"voz"
|
|
2425
|
+
],
|
|
2426
|
+
"tools": [
|
|
2427
|
+
"stt_transcribe"
|
|
2428
|
+
],
|
|
2429
|
+
"contracts": [
|
|
2430
|
+
"tool-mode/stt_transcribe"
|
|
2431
|
+
]
|
|
2432
|
+
},
|
|
2433
|
+
{
|
|
2434
|
+
"id": "@try-works/extension-embeddinggemma",
|
|
2435
|
+
"name": "EmbeddingGemma 2",
|
|
2436
|
+
"package": "@try-works/extension-embeddinggemma",
|
|
2437
|
+
"version": "0.1.2",
|
|
2438
|
+
"license": "MIT",
|
|
2439
|
+
"summary": "Multimodal embedding and retrieval tools over EmbeddingGemma 2 (google/embeddinggemma-2 through a pinned ONNX conversion): multimodal_embed produces 768-, 51…",
|
|
2440
|
+
"models": [
|
|
2441
|
+
"embeddinggemma-2"
|
|
2442
|
+
],
|
|
2443
|
+
"tools": [
|
|
2444
|
+
"multimodal_embed",
|
|
2445
|
+
"semantic_similarity",
|
|
2446
|
+
"semantic_rank",
|
|
2447
|
+
"semantic_classify",
|
|
2448
|
+
"semantic_deduplicate",
|
|
2449
|
+
"dedupe_and_rank",
|
|
2450
|
+
"media_find_moments"
|
|
2451
|
+
],
|
|
2452
|
+
"contracts": [
|
|
2453
|
+
"@try-works/extension-embeddinggemma/multimodal_embed",
|
|
2454
|
+
"@try-works/extension-embeddinggemma/semantic_similarity",
|
|
2455
|
+
"@try-works/extension-embeddinggemma/semantic_rank",
|
|
2456
|
+
"@try-works/extension-embeddinggemma/semantic_classify",
|
|
2457
|
+
"@try-works/extension-embeddinggemma/semantic_deduplicate",
|
|
2458
|
+
"@try-works/extension-embeddinggemma/dedupe_and_rank",
|
|
2459
|
+
"@try-works/extension-embeddinggemma/media_find_moments"
|
|
2460
|
+
]
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
"id": "@try-works/extension-laya",
|
|
2464
|
+
"name": "Laya",
|
|
2465
|
+
"package": "@try-works/extension-laya",
|
|
2466
|
+
"version": "1.0.4",
|
|
2467
|
+
"license": "Apache-2.0",
|
|
2468
|
+
"summary": "The Laya decision family (H5): decision_choose (a choice over the caller's options), decision_test (yes/no) and decision_score (an ordinal level) over the pi…",
|
|
2469
|
+
"models": [
|
|
2470
|
+
"laya"
|
|
2471
|
+
],
|
|
2472
|
+
"tools": [
|
|
2473
|
+
"decision_choose_local",
|
|
2474
|
+
"decision_choose",
|
|
2475
|
+
"decision_test",
|
|
2476
|
+
"decision_score",
|
|
2477
|
+
"decision_test_typed",
|
|
2478
|
+
"decision_score_typed"
|
|
2479
|
+
],
|
|
2480
|
+
"contracts": [
|
|
2481
|
+
"@try-works/extension-laya/decision_choose_local",
|
|
2482
|
+
"decision.choose",
|
|
2483
|
+
"@try-works/extension-laya/decision_test",
|
|
2484
|
+
"@try-works/extension-laya/decision_score",
|
|
2485
|
+
"@try-works/extension-laya/decision_test_typed",
|
|
2486
|
+
"@try-works/extension-laya/decision_score_typed"
|
|
2487
|
+
]
|
|
2488
|
+
},
|
|
2489
|
+
{
|
|
2490
|
+
"id": "@try-works/extension-minilm-rerank",
|
|
2491
|
+
"name": "MiniLM Rerank",
|
|
2492
|
+
"package": "@try-works/extension-minilm-rerank",
|
|
2493
|
+
"version": "1.0.0",
|
|
2494
|
+
"license": "Apache-2.0",
|
|
2495
|
+
"summary": "The ms-marco MiniLM-L-6 cross-encoder reranking extension (TOOL-05): query/document pairs cross-encoded to ranking logits, returned with the caller's own ids…",
|
|
2496
|
+
"models": [
|
|
2497
|
+
"minilm-rerank"
|
|
2498
|
+
],
|
|
2499
|
+
"tools": [],
|
|
2500
|
+
"contracts": [
|
|
2501
|
+
"text.rerank"
|
|
2502
|
+
]
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
"id": "@try-works/extension-minilm-embed",
|
|
2506
|
+
"name": "MiniLM-L6",
|
|
2507
|
+
"package": "@try-works/extension-minilm-embed",
|
|
2508
|
+
"version": "1.0.0",
|
|
2509
|
+
"license": "Apache-2.0",
|
|
2510
|
+
"summary": "The legacy 384-dimensional MiniLM-L6 text embedding space (TOOL-04, BASE-12): mean pooling, L2 normalization, input order preserved.",
|
|
2511
|
+
"models": [
|
|
2512
|
+
"minilm-l6"
|
|
2513
|
+
],
|
|
2514
|
+
"tools": [],
|
|
2515
|
+
"contracts": [
|
|
2516
|
+
"text.embed"
|
|
2517
|
+
]
|
|
2518
|
+
},
|
|
2519
|
+
{
|
|
2520
|
+
"id": "@try-works/extension-mirage",
|
|
2521
|
+
"name": "mirage",
|
|
2522
|
+
"package": "@try-works/extension-mirage",
|
|
2523
|
+
"version": "0.1.2",
|
|
2524
|
+
"license": "Apache-2.0",
|
|
2525
|
+
"summary": "Mirage is a 51.3M-parameter, 2-bit Cactus vision model (about 21.6 MB) that describes scenes, finds objects, reads text, produces masks and fills constrained…",
|
|
2526
|
+
"models": [
|
|
2527
|
+
"mirage"
|
|
2528
|
+
],
|
|
2529
|
+
"tools": [
|
|
2530
|
+
"mirage_describe",
|
|
2531
|
+
"mirage_detect",
|
|
2532
|
+
"mirage_ocr",
|
|
2533
|
+
"mirage_extract"
|
|
2534
|
+
],
|
|
2535
|
+
"contracts": [
|
|
2536
|
+
"@try-works/extension-mirage/mirage_describe",
|
|
2537
|
+
"@try-works/extension-mirage/mirage_detect",
|
|
2538
|
+
"@try-works/extension-mirage/mirage_ocr",
|
|
2539
|
+
"@try-works/extension-mirage/mirage_extract"
|
|
2540
|
+
]
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
"id": "@try-works/extension-needle",
|
|
2544
|
+
"name": "Needle 3",
|
|
2545
|
+
"package": "@try-works/extension-needle",
|
|
2546
|
+
"version": "0.1.3",
|
|
2547
|
+
"license": "MIT",
|
|
2548
|
+
"summary": "Tool proposal, structured extraction, record validation and one repair retry over the pinned Cactus Needle 3 native engine (Cactus-Compute/needle3, windows-x…",
|
|
2549
|
+
"models": [
|
|
2550
|
+
"needle3"
|
|
2551
|
+
],
|
|
2552
|
+
"tools": [
|
|
2553
|
+
"tools_plan",
|
|
2554
|
+
"structured_extract",
|
|
2555
|
+
"validate_record",
|
|
2556
|
+
"extract_with_repair"
|
|
2557
|
+
],
|
|
2558
|
+
"contracts": [
|
|
2559
|
+
"@try-works/extension-needle/tools_plan",
|
|
2560
|
+
"@try-works/extension-needle/structured_extract",
|
|
2561
|
+
"@try-works/extension-needle/validate_record",
|
|
2562
|
+
"@try-works/extension-needle/extract_with_repair"
|
|
2563
|
+
]
|
|
2564
|
+
}
|
|
2565
|
+
]
|
|
2566
|
+
|
|
2567
|
+
/** Page styles, prefixed to avoid colliding with host styles. */
|
|
2568
|
+
const CSS = `
|
|
2569
|
+
.mtt-page {
|
|
2570
|
+
display: flex;
|
|
2571
|
+
flex-direction: column;
|
|
2572
|
+
gap: 12px;
|
|
2573
|
+
max-width: 720px;
|
|
2574
|
+
color: var(--dsw-alias-label-primary);
|
|
2575
|
+
}
|
|
2576
|
+
.mtt-title {
|
|
2577
|
+
margin: 0;
|
|
2578
|
+
font-size: 16px;
|
|
2579
|
+
line-height: 24px;
|
|
2580
|
+
font-weight: 500;
|
|
2581
|
+
color: var(--dsw-alias-label-primary);
|
|
2582
|
+
}
|
|
2583
|
+
.mtt-intro {
|
|
2584
|
+
margin: 0;
|
|
2585
|
+
font-size: 14px;
|
|
2586
|
+
line-height: 22px;
|
|
2587
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2588
|
+
}
|
|
2589
|
+
.mtt-card {
|
|
2590
|
+
border: 0.5px solid var(--dsw-alias-settings-card-stroke);
|
|
2591
|
+
background: var(--dsw-alias-settings-card-fill);
|
|
2592
|
+
border-radius: var(--dsw-radius-xl);
|
|
2593
|
+
padding: 12px 14px;
|
|
2594
|
+
display: flex;
|
|
2595
|
+
flex-direction: column;
|
|
2596
|
+
gap: 2px;
|
|
2597
|
+
}
|
|
2598
|
+
.mtt-row {
|
|
2599
|
+
display: flex;
|
|
2600
|
+
align-items: flex-start;
|
|
2601
|
+
justify-content: space-between;
|
|
2602
|
+
gap: 24px;
|
|
2603
|
+
padding: 12px 0;
|
|
2604
|
+
border-top: 0.5px solid var(--dsw-alias-settings-card-stroke);
|
|
2605
|
+
}
|
|
2606
|
+
.mtt-row:first-of-type { border-top: none; }
|
|
2607
|
+
.mtt-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
2608
|
+
.mtt-label {
|
|
2609
|
+
font-size: 13px;
|
|
2610
|
+
line-height: 20px;
|
|
2611
|
+
color: var(--dsw-alias-label-primary);
|
|
2612
|
+
display: flex;
|
|
2613
|
+
align-items: center;
|
|
2614
|
+
gap: 8px;
|
|
2615
|
+
}
|
|
2616
|
+
.mtt-name { font-weight: 500; }
|
|
2617
|
+
.mtt-meta {
|
|
2618
|
+
font-family: var(--ds-font-family-code);
|
|
2619
|
+
font-size: 11px;
|
|
2620
|
+
line-height: 16px;
|
|
2621
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2622
|
+
word-break: break-all;
|
|
2623
|
+
}
|
|
2624
|
+
.mtt-help {
|
|
2625
|
+
margin: 0;
|
|
2626
|
+
font-size: 12px;
|
|
2627
|
+
line-height: 18px;
|
|
2628
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2629
|
+
}
|
|
2630
|
+
.mtt-control { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
|
|
2631
|
+
.mtt-input {
|
|
2632
|
+
box-sizing: border-box;
|
|
2633
|
+
width: 300px;
|
|
2634
|
+
height: 32px;
|
|
2635
|
+
padding: 0 10px;
|
|
2636
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2637
|
+
border-radius: var(--dsw-radius-sm);
|
|
2638
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2639
|
+
color: var(--dsw-alias-label-primary);
|
|
2640
|
+
font-family: var(--ds-font-family-code);
|
|
2641
|
+
font-size: 12px;
|
|
2642
|
+
}
|
|
2643
|
+
.mtt-input:disabled { opacity: 0.6; }
|
|
2644
|
+
.mtt-toggle {
|
|
2645
|
+
width: 40px;
|
|
2646
|
+
height: 22px;
|
|
2647
|
+
border-radius: 999px;
|
|
2648
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2649
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
2650
|
+
position: relative;
|
|
2651
|
+
cursor: pointer;
|
|
2652
|
+
padding: 0;
|
|
2653
|
+
flex: 0 0 auto;
|
|
2654
|
+
}
|
|
2655
|
+
.mtt-toggle[data-on='true'] { background: var(--dsw-alias-button-primary-fill); border-color: transparent; }
|
|
2656
|
+
.mtt-toggle:disabled {
|
|
2657
|
+
cursor: not-allowed;
|
|
2658
|
+
/* A disabled control must still READ as a control on every host theme.
|
|
2659
|
+
The theme mask variable resolved transparent here; explicit neutral
|
|
2660
|
+
greys keep the switch visible when the model is unavailable. */
|
|
2661
|
+
opacity: 1;
|
|
2662
|
+
background: #E5E7EB;
|
|
2663
|
+
border-color: #C9CEDA;
|
|
2664
|
+
}
|
|
2665
|
+
.mtt-toggle:disabled .mtt-knob {
|
|
2666
|
+
background: #8B93A1;
|
|
2667
|
+
opacity: 1;
|
|
2668
|
+
}
|
|
2669
|
+
.mtt-knob {
|
|
2670
|
+
position: absolute;
|
|
2671
|
+
top: 2px;
|
|
2672
|
+
left: 2px;
|
|
2673
|
+
width: 16px;
|
|
2674
|
+
height: 16px;
|
|
2675
|
+
border-radius: 50%;
|
|
2676
|
+
background: var(--dsw-alias-label-primary-inverted);
|
|
2677
|
+
transition: transform 120ms ease;
|
|
2678
|
+
}
|
|
2679
|
+
.mtt-toggle[data-on='true'] .mtt-knob { transform: translateX(18px); }
|
|
2680
|
+
.mtt-badge {
|
|
2681
|
+
font-size: 11px;
|
|
2682
|
+
line-height: 16px;
|
|
2683
|
+
padding: 1px 6px;
|
|
2684
|
+
border-radius: 999px;
|
|
2685
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
2686
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2687
|
+
}
|
|
2688
|
+
.mtt-badge-on { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-tertiary); }
|
|
2689
|
+
.mtt-row-note {
|
|
2690
|
+
margin: 0;
|
|
2691
|
+
font-size: 12px;
|
|
2692
|
+
line-height: 18px;
|
|
2693
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2694
|
+
}
|
|
2695
|
+
.mtt-row-error {
|
|
2696
|
+
margin: 0;
|
|
2697
|
+
font-size: 12px;
|
|
2698
|
+
line-height: 18px;
|
|
2699
|
+
color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-primary));
|
|
2700
|
+
}
|
|
2701
|
+
.mtt-row-blocker {
|
|
2702
|
+
margin: 0;
|
|
2703
|
+
font-size: 12px;
|
|
2704
|
+
line-height: 18px;
|
|
2705
|
+
font-weight: 500;
|
|
2706
|
+
color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-primary));
|
|
2707
|
+
}
|
|
2708
|
+
.mtt-actions {
|
|
2709
|
+
display: flex;
|
|
2710
|
+
align-items: center;
|
|
2711
|
+
gap: 12px;
|
|
2712
|
+
padding-top: 8px;
|
|
2713
|
+
}
|
|
2714
|
+
.mtt-button {
|
|
2715
|
+
height: 32px;
|
|
2716
|
+
padding: 0 14px;
|
|
2717
|
+
border: none;
|
|
2718
|
+
border-radius: var(--dsw-radius-sm);
|
|
2719
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
2720
|
+
color: var(--dsw-alias-label-primary-inverted);
|
|
2721
|
+
font-family: inherit;
|
|
2722
|
+
font-size: 13px;
|
|
2723
|
+
cursor: pointer;
|
|
2724
|
+
}
|
|
2725
|
+
.mtt-button:disabled { opacity: 0.5; cursor: default; }
|
|
2726
|
+
.mtt-button-small {
|
|
2727
|
+
height: 28px;
|
|
2728
|
+
padding: 0 12px;
|
|
2729
|
+
font-size: 12px;
|
|
2730
|
+
}
|
|
2731
|
+
.mtt-status { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
2732
|
+
.mtt-status-ok { color: var(--dsw-alias-label-secondary); }
|
|
2733
|
+
.mtt-status-error { color: var(--dsw-alias-label-primary); }
|
|
2734
|
+
`
|
|
2735
|
+
|
|
2736
|
+
// The settings namespace is the plugin's ENTRY ID (the bundle patch row id);
|
|
2737
|
+
// a hand-written row may use the package name, so both are tried in order.
|
|
2738
|
+
const NAMESPACES = ['model-tools', '@try-works/dsh-tool-mode']
|
|
2739
|
+
|
|
2740
|
+
/** Global fields (per-model toggles are rendered from CATALOG). */
|
|
2741
|
+
const FIELDS = [
|
|
2742
|
+
{
|
|
2743
|
+
key: 'exposeUnavailable',
|
|
2744
|
+
kind: 'toggle',
|
|
2745
|
+
label: 'Expose unavailable models',
|
|
2746
|
+
help: 'Register a tool even when its runtime is missing, so a model turn can discover why it cannot run.',
|
|
2747
|
+
},
|
|
2748
|
+
{
|
|
2749
|
+
key: 'registerSkills',
|
|
2750
|
+
kind: 'toggle',
|
|
2751
|
+
label: 'Register skills',
|
|
2752
|
+
help: 'Publish one skill per registered tool, teaching when to use it.',
|
|
2753
|
+
},
|
|
2754
|
+
{
|
|
2755
|
+
key: 'registerControlTool',
|
|
2756
|
+
kind: 'toggle',
|
|
2757
|
+
label: 'Register the inspection tool',
|
|
2758
|
+
help: 'Expose the model_tools inspection surface to authorised native callers.',
|
|
2759
|
+
},
|
|
2760
|
+
{
|
|
2761
|
+
key: 'allowNpm',
|
|
2762
|
+
kind: 'toggle',
|
|
2763
|
+
label: 'Allow in-process packages',
|
|
2764
|
+
help: 'Resolve model packages from this installation. Turn off to force every call through the CLI or the bridge.',
|
|
2765
|
+
},
|
|
2766
|
+
{
|
|
2767
|
+
key: 'managedWorkers',
|
|
2768
|
+
kind: 'toggle',
|
|
2769
|
+
label: 'Managed workers',
|
|
2770
|
+
help: 'Run extension entries in a managed child process with a scrubbed environment.',
|
|
2771
|
+
},
|
|
2772
|
+
{
|
|
2773
|
+
key: 'repairAttempts',
|
|
2774
|
+
kind: 'number',
|
|
2775
|
+
label: 'Repair attempts',
|
|
2776
|
+
help: 'Bounded title-repair budget per call (0-8). Out-of-range values are refused, never clamped.',
|
|
2777
|
+
},
|
|
2778
|
+
{
|
|
2779
|
+
key: 'approvedRoots',
|
|
2780
|
+
kind: 'list',
|
|
2781
|
+
label: 'Approved roots',
|
|
2782
|
+
help: 'Comma-separated absolute paths file-reading tools may reach. Empty approves nothing.',
|
|
2783
|
+
},
|
|
2784
|
+
{
|
|
2785
|
+
key: 'models',
|
|
2786
|
+
kind: 'list',
|
|
2787
|
+
label: 'Allowlist override',
|
|
2788
|
+
help: 'Advanced: comma-separated model ids. Empty means every enabled model below; set only to pin an exact set.',
|
|
2789
|
+
},
|
|
2790
|
+
]
|
|
2791
|
+
|
|
2792
|
+
/** Read the value a field should display: the draft, else the saved value. */
|
|
2793
|
+
function fieldValue(field, value, draft) {
|
|
2794
|
+
if (draft[field.key] !== undefined) return draft[field.key]
|
|
2795
|
+
const saved = value[field.key]
|
|
2796
|
+
if (field.kind === 'list') return Array.isArray(saved) ? saved.join(', ') : ''
|
|
2797
|
+
if (field.kind === 'number') return saved === undefined || saved === null ? '' : String(saved)
|
|
2798
|
+
return saved === true
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
/** Parse one edited field back into its config representation. */
|
|
2802
|
+
function fieldPatch(field, text) {
|
|
2803
|
+
if (field.kind === 'list') {
|
|
2804
|
+
return text.split(',').map(part => part.trim()).filter(part => part.length > 0)
|
|
2805
|
+
}
|
|
2806
|
+
if (field.kind === 'number') return Number(text)
|
|
2807
|
+
return text
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
/** One global row: label + help on the left, the control on the right. */
|
|
2811
|
+
function fieldRow(field, value, draft, edit, disabled) {
|
|
2812
|
+
const current = fieldValue(field, value, draft)
|
|
2813
|
+
let control
|
|
2814
|
+
if (field.kind === 'toggle') {
|
|
2815
|
+
control = h('button', {
|
|
2816
|
+
type: 'button',
|
|
2817
|
+
className: 'mtt-toggle',
|
|
2818
|
+
'data-on': current === true ? 'true' : 'false',
|
|
2819
|
+
disabled,
|
|
2820
|
+
'aria-pressed': current === true,
|
|
2821
|
+
onClick: () => edit(field.key, !(current === true)),
|
|
2822
|
+
}, h('span', { className: 'mtt-knob' }))
|
|
2823
|
+
} else {
|
|
2824
|
+
control = h('input', {
|
|
2825
|
+
className: 'mtt-input',
|
|
2826
|
+
type: field.kind === 'number' ? 'number' : 'text',
|
|
2827
|
+
min: field.kind === 'number' ? 0 : undefined,
|
|
2828
|
+
max: field.kind === 'number' ? 8 : undefined,
|
|
2829
|
+
value: current,
|
|
2830
|
+
disabled,
|
|
2831
|
+
onChange: event => edit(field.key, event.target.value),
|
|
2832
|
+
})
|
|
2833
|
+
}
|
|
2834
|
+
return h('div', { className: 'mtt-row', key: field.key },
|
|
2835
|
+
h('div', { className: 'mtt-row-text' },
|
|
2836
|
+
h('span', { className: 'mtt-label' }, field.label),
|
|
2837
|
+
h('p', { className: 'mtt-help' }, field.help)),
|
|
2838
|
+
h('div', { className: 'mtt-control' }, control))
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
/** One model row: identity, package, size and status, with the toggle. */
|
|
2842
|
+
function modelRow(entry, enabled, disabled, onToggle) {
|
|
2843
|
+
// An extension-backed model is provided by another package; only the HOST
|
|
2844
|
+
// can tell whether it is installed, so the row says "extension" instead of
|
|
2845
|
+
// promising it runs here.
|
|
2846
|
+
const badge = entry.extension !== null && entry.extension !== undefined
|
|
2847
|
+
? h('span', { className: 'mtt-badge' }, 'extension')
|
|
2848
|
+
: entry.runnable === true
|
|
2849
|
+
? h('span', { className: 'mtt-badge mtt-badge-on' }, 'runnable here')
|
|
2850
|
+
: h('span', { className: 'mtt-badge' }, 'unavailable here')
|
|
2851
|
+
const source = entry.extension ?? (entry.package === null ? 'no package' : entry.package)
|
|
2852
|
+
return h('div', { className: 'mtt-row', key: entry.id },
|
|
2853
|
+
h('div', { className: 'mtt-row-text' },
|
|
2854
|
+
h('span', { className: 'mtt-label' },
|
|
2855
|
+
h('span', { className: 'mtt-name' }, entry.name),
|
|
2856
|
+
badge),
|
|
2857
|
+
h('p', { className: 'mtt-help' }, entry.summary),
|
|
2858
|
+
h('span', { className: 'mtt-meta' },
|
|
2859
|
+
entry.id,
|
|
2860
|
+
' · ',
|
|
2861
|
+
source,
|
|
2862
|
+
entry.sizeMB === null ? '' : ' · ' + entry.sizeMB + ' MB',
|
|
2863
|
+
' · ',
|
|
2864
|
+
entry.license,
|
|
2865
|
+
entry.extension !== null && entry.extension !== undefined
|
|
2866
|
+
? ' · provided by the Tool Mode extension; install and activate it to run'
|
|
2867
|
+
: entry.runnable !== true && entry.reason ? ' · ' + entry.reason : '')),
|
|
2868
|
+
h('div', { className: 'mtt-control' },
|
|
2869
|
+
h('button', {
|
|
2870
|
+
type: 'button',
|
|
2871
|
+
className: 'mtt-toggle',
|
|
2872
|
+
'data-on': enabled ? 'true' : 'false',
|
|
2873
|
+
disabled: disabled,
|
|
2874
|
+
'aria-pressed': enabled,
|
|
2875
|
+
title: enabled ? 'Disable this model' : 'Enable this model',
|
|
2876
|
+
onClick: () => onToggle(entry.id, !enabled),
|
|
2877
|
+
}, h('span', { className: 'mtt-knob' }))))
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
/** How an in-flight Install/Enable/Disable is spelled in the card. */
|
|
2881
|
+
const EXTENSION_ACTION_LABELS = { install: 'Installing', prepare: 'Preparing', enable: 'Enabling', disable: 'Disabling' }
|
|
2882
|
+
|
|
2883
|
+
// The extension Remote is a package-local lazy chunk (`client.model-tools.js`)
|
|
2884
|
+
// mounted by the wire half: it installs the generated Typert namespace on
|
|
2885
|
+
// ctx.remote as `modelTools`. It is optional on purpose — a host without the
|
|
2886
|
+
// Remote leaves the page on its config-only fallback.
|
|
2887
|
+
const REMOTE_CHUNK = './client.model-tools.js'
|
|
2888
|
+
|
|
2889
|
+
// The namespace may arrive a moment after the page mounts (the chunk is
|
|
2890
|
+
// fetched, then the Remote contribution mounts). The first read retries
|
|
2891
|
+
// briefly, and the slot subscription below re-reads whenever the chunk's
|
|
2892
|
+
// mount settles, so a slow mount upgrades the card instead of stranding it.
|
|
2893
|
+
const REMOTE_RETRY_ATTEMPTS = 3
|
|
2894
|
+
const REMOTE_RETRY_DELAY_MS = 400
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* A tiny observable for the optional Remote mount.
|
|
2898
|
+
*
|
|
2899
|
+
* `apply()` cannot await the chunk before registering the Settings section:
|
|
2900
|
+
* on a host without the Remote the page must still render. So the mount runs
|
|
2901
|
+
* in the background and publishes its status here; the card subscribes and
|
|
2902
|
+
* re-reads when it changes.
|
|
2903
|
+
*/
|
|
2904
|
+
function createRemoteSlot() {
|
|
2905
|
+
const listeners = new Set()
|
|
2906
|
+
let status = null
|
|
2907
|
+
return {
|
|
2908
|
+
get: () => status,
|
|
2909
|
+
set: next => {
|
|
2910
|
+
if (next === status) return
|
|
2911
|
+
status = next
|
|
2912
|
+
for (const listener of [...listeners]) {
|
|
2913
|
+
try {
|
|
2914
|
+
listener()
|
|
2915
|
+
} catch (error) {
|
|
2916
|
+
// A broken listener must not abort the mount or another listener.
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
},
|
|
2920
|
+
subscribe: listener => {
|
|
2921
|
+
listeners.add(listener)
|
|
2922
|
+
return () => { listeners.delete(listener) }
|
|
2923
|
+
},
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
/** Warn without letting a logger failure blank the page. */
|
|
2928
|
+
function warnClient(ctx, message) {
|
|
2929
|
+
try {
|
|
2930
|
+
if (ctx.logger && typeof ctx.logger.warn === 'function') ctx.logger.warn(message)
|
|
2931
|
+
else if (typeof console !== 'undefined' && typeof console.warn === 'function') console.warn(message)
|
|
2932
|
+
} catch (error) {
|
|
2933
|
+
// A warning is diagnostics, never control flow.
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
/**
|
|
2938
|
+
* Load the wire chunk and mount the optional `modelTools` namespace.
|
|
2939
|
+
*
|
|
2940
|
+
* Never throws into `apply`: a missing chunk (older host), a chunk that fails
|
|
2941
|
+
* to mount, or a host without `remote` all leave the slot unset, which is the
|
|
2942
|
+
* config-only fallback. The disposer is registered through ctx.effect so the
|
|
2943
|
+
* namespace unmounts with this plugin.
|
|
2944
|
+
*/
|
|
2945
|
+
function mountExtensionRemote(ctx, slot) {
|
|
2946
|
+
const load = async () => {
|
|
2947
|
+
if (typeof require.async !== 'function') return
|
|
2948
|
+
const chunk = await require.async(REMOTE_CHUNK)
|
|
2949
|
+
if (chunk === null || chunk === undefined || typeof chunk.mount !== 'function') {
|
|
2950
|
+
throw new Error('the extension Remote chunk exposes no mount()')
|
|
2951
|
+
}
|
|
2952
|
+
const mounted = await chunk.mount(ctx)
|
|
2953
|
+
if (mounted === null || mounted === undefined) return
|
|
2954
|
+
if (typeof mounted.dispose === 'function') {
|
|
2955
|
+
ctx.effect(() => () => mounted.dispose(), 'model-tools: extension Remote')
|
|
2956
|
+
}
|
|
2957
|
+
slot.set(mounted)
|
|
2958
|
+
}
|
|
2959
|
+
load().catch(error => warnClient(ctx, 'model-tools: extension Remote unavailable: '
|
|
2960
|
+
+ ((error && error.message) || String(error))))
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
/**
|
|
2964
|
+
* The extension Remote, when this client has one.
|
|
2965
|
+
*
|
|
2966
|
+
* The generated namespace is registered as the context service
|
|
2967
|
+
* `remote.modelTools`, so a direct service read is tried FIRST: the `remote`
|
|
2968
|
+
* service's associated-property lookup would resolve against this plugin's
|
|
2969
|
+
* own fiber and throw without an `inject`, while `ctx.get('remote.modelTools')`
|
|
2970
|
+
* reads the provided service without that requirement. The mount status is
|
|
2971
|
+
* still tried as a fallback (a future chunk could hand back the namespace
|
|
2972
|
+
* directly), and the associated `remote.modelTools` property is the last
|
|
2973
|
+
* route for an assembly that provides it that way. Every route is guarded so
|
|
2974
|
+
* absence is a value, not an exception.
|
|
2975
|
+
*
|
|
2976
|
+
* @param ctx - the client plugin context.
|
|
2977
|
+
* @param remoteStatus - the mounted status published by the wire chunk, or null.
|
|
2978
|
+
* @returns the namespace surface, or null when it is not available.
|
|
2979
|
+
*/
|
|
2980
|
+
function modelToolsRemote(ctx, remoteStatus) {
|
|
2981
|
+
const usable = namespace => namespace !== null && namespace !== undefined
|
|
2982
|
+
&& typeof namespace.describe === 'function'
|
|
2983
|
+
const routes = [() => ctx.get('remote.modelTools')]
|
|
2984
|
+
if (remoteStatus !== null && remoteStatus !== undefined) {
|
|
2985
|
+
routes.push(() => remoteStatus.remote.modelTools)
|
|
2986
|
+
routes.push(() => remoteStatus.modelTools)
|
|
2987
|
+
}
|
|
2988
|
+
routes.push(() => {
|
|
2989
|
+
const remote = ctx.get('remote')
|
|
2990
|
+
return remote === null || remote === undefined ? null : remote.modelTools
|
|
2991
|
+
})
|
|
2992
|
+
for (const route of routes) {
|
|
2993
|
+
try {
|
|
2994
|
+
const namespace = route()
|
|
2995
|
+
if (usable(namespace)) return namespace
|
|
2996
|
+
} catch (error) {
|
|
2997
|
+
// Try the next route: an undeclared associated namespace throws.
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
return null
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
/**
|
|
3004
|
+
* Read one additive Tool Insights method from the optional Remote.
|
|
3005
|
+
*
|
|
3006
|
+
* The page's data methods (`toolUsageReport`, `toolInventory`, `mcpStatus`,
|
|
3007
|
+
* `mcpCalls`) are ADDITIVE: an older host does not expose them, so an
|
|
3008
|
+
* absent method is `null` (the page renders its named unavailable state),
|
|
3009
|
+
* never an exception. A refusal is returned in the RemoteResult shape the
|
|
3010
|
+
* page already understands, so its host text is shown verbatim.
|
|
3011
|
+
*
|
|
3012
|
+
* @param ctx - the client plugin context.
|
|
3013
|
+
* @param remoteSlot - the optional mounted Remote status.
|
|
3014
|
+
* @param method - the namespace method name.
|
|
3015
|
+
* @param args - its single arguments object.
|
|
3016
|
+
* @returns the value, a `{ok:false,error}` refusal, or null when absent.
|
|
3017
|
+
*/
|
|
3018
|
+
async function callModelTools(ctx, remoteSlot, method, args) {
|
|
3019
|
+
const namespace = modelToolsRemote(ctx,
|
|
3020
|
+
remoteSlot === null || remoteSlot === undefined ? null : remoteSlot.get())
|
|
3021
|
+
if (namespace === null) return null
|
|
3022
|
+
const fn = namespace[method]
|
|
3023
|
+
if (typeof fn !== 'function') return null
|
|
3024
|
+
try {
|
|
3025
|
+
return unwrapRemoteResult(await fn.call(namespace, args))
|
|
3026
|
+
} catch (error) {
|
|
3027
|
+
return { ok: false, error: { message: remoteErrorText(error) } }
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* The install operation on the wire namespace.
|
|
3033
|
+
*
|
|
3034
|
+
* The verb is `installExtension`, not `install`: the harness gateway's
|
|
3035
|
+
* RemoteNamespaceService owns a private `install`, and a colliding descriptor
|
|
3036
|
+
* is refused at mount. The host-side service keeps the raw name `install`;
|
|
3037
|
+
* this helper tolerates either spelling so a wire rename cannot blank the UI.
|
|
3038
|
+
*
|
|
3039
|
+
* @param namespace - the modelTools namespace.
|
|
3040
|
+
* @returns the method to call, or null when the host exposes neither.
|
|
3041
|
+
*/
|
|
3042
|
+
function extensionInstallMethod(namespace) {
|
|
3043
|
+
if (namespace === null || namespace === undefined) return null
|
|
3044
|
+
if (typeof namespace.installExtension === 'function') return namespace.installExtension
|
|
3045
|
+
if (typeof namespace.install === 'function') return namespace.install
|
|
3046
|
+
return null
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* The host's error text, without inventing one.
|
|
3051
|
+
*
|
|
3052
|
+
* @param error - a RemoteResult error, an Error, or a string.
|
|
3053
|
+
* @returns the message to show; never empty.
|
|
3054
|
+
*/
|
|
3055
|
+
function remoteErrorText(error) {
|
|
3056
|
+
if (typeof error === 'string' && error.length > 0) return error
|
|
3057
|
+
if (error !== null && typeof error === 'object'
|
|
3058
|
+
&& typeof error.message === 'string' && error.message.length > 0) return error.message
|
|
3059
|
+
return 'The host did not explain the failure.'
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
/**
|
|
3063
|
+
* Read a generated Remote method's answer.
|
|
3064
|
+
*
|
|
3065
|
+
* A generated Typert Remote method resolves a RemoteResult
|
|
3066
|
+
* (`{ ok: true, value }` / `{ ok: false, error }`); the host service itself
|
|
3067
|
+
* resolves the plain value. Both shapes are accepted so a hand-authored wire
|
|
3068
|
+
* artifact and a generated one behave the same here, and a refusal becomes a
|
|
3069
|
+
* thrown error carrying the host's own message.
|
|
3070
|
+
*
|
|
3071
|
+
* @param result - the method's resolved value.
|
|
3072
|
+
* @returns the value.
|
|
3073
|
+
*/
|
|
3074
|
+
function unwrapRemoteResult(result) {
|
|
3075
|
+
if (result !== null && typeof result === 'object' && typeof result.ok === 'boolean') {
|
|
3076
|
+
if (result.ok !== true) throw new Error(remoteErrorText(result.error))
|
|
3077
|
+
return result.value
|
|
3078
|
+
}
|
|
3079
|
+
return result
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
/**
|
|
3083
|
+
* Narrow an ExtensionsDescribeResult. A malformed answer is an error, not an
|
|
3084
|
+
* empty list: rendering "not installed" from a broken state read would be a
|
|
3085
|
+
* guess, and a guess about installed state is exactly what this page must not
|
|
3086
|
+
* make.
|
|
3087
|
+
*
|
|
3088
|
+
* @param value - the value describe() resolved.
|
|
3089
|
+
* @returns { store, writable, extensions }.
|
|
3090
|
+
*/
|
|
3091
|
+
function normalizeDescribe(value) {
|
|
3092
|
+
if (value === null || typeof value !== 'object') {
|
|
3093
|
+
throw new Error('The host returned no extension state.')
|
|
3094
|
+
}
|
|
3095
|
+
if (!Array.isArray(value.extensions)) {
|
|
3096
|
+
throw new Error('The host returned an extension state without its extension list.')
|
|
3097
|
+
}
|
|
3098
|
+
const extensions = value.extensions.filter(row => row !== null && typeof row === 'object'
|
|
3099
|
+
&& typeof row.id === 'string' && row.id.length > 0)
|
|
3100
|
+
return {
|
|
3101
|
+
store: typeof value.store === 'string' && value.store.length > 0 ? value.store : null,
|
|
3102
|
+
writable: value.writable === true,
|
|
3103
|
+
extensions,
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/** Read one extension state snapshot, unwrapping the Remote result. */
|
|
3108
|
+
async function describeExtensions(namespace) {
|
|
3109
|
+
return normalizeDescribe(unwrapRemoteResult(await namespace.describe()))
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
/**
|
|
3113
|
+
* Find one shipped extension in a describe() snapshot. The host may key a row
|
|
3114
|
+
* by the extension id or by its package name, so both are compared.
|
|
3115
|
+
*
|
|
3116
|
+
* @param extension - the generated EXTENSIONS entry.
|
|
3117
|
+
* @param states - the describe() rows.
|
|
3118
|
+
* @returns the matching state, or null when the host did not report it.
|
|
3119
|
+
*/
|
|
3120
|
+
function findExtensionState(extension, states) {
|
|
3121
|
+
for (let index = 0; index < states.length; index += 1) {
|
|
3122
|
+
const state = states[index]
|
|
3123
|
+
if (state.id === extension.id) return state
|
|
3124
|
+
if (typeof extension.package === 'string' && state.id === extension.package) return state
|
|
3125
|
+
if (typeof state.package === 'string' && state.package === extension.id) return state
|
|
3126
|
+
if (typeof state.package === 'string' && typeof extension.package === 'string'
|
|
3127
|
+
&& state.package === extension.package) return state
|
|
3128
|
+
}
|
|
3129
|
+
return null
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
/**
|
|
3133
|
+
* The Config.disabledExtensions list after one Enable/Disable action.
|
|
3134
|
+
*
|
|
3135
|
+
* Two durable sources describe "disabled": the SDK lifecycle that
|
|
3136
|
+
* enable/disable change, and this config list, which the plugin's restore
|
|
3137
|
+
* filter checks on the next load. The Remote UI writes the lifecycle, so it
|
|
3138
|
+
* must dual-write this list too — otherwise a restart can undo the user's
|
|
3139
|
+
* choice in the other direction (the example that motivated this: Enable
|
|
3140
|
+
* succeeds, the list still names the id, and the next restore re-disables it).
|
|
3141
|
+
*
|
|
3142
|
+
* @param list - the saved disabledExtensions list (any value, tolerated).
|
|
3143
|
+
* @param id - the extension id being enabled/disabled.
|
|
3144
|
+
* @param action - 'enable' removes the id; anything else ('disable') adds it.
|
|
3145
|
+
* @returns the sorted next list, with non-string entries dropped.
|
|
3146
|
+
*/
|
|
3147
|
+
function nextDisabledExtensions(list, id, action) {
|
|
3148
|
+
const next = new Set(Array.isArray(list)
|
|
3149
|
+
? list.filter(entry => typeof entry === 'string' && entry.length > 0)
|
|
3150
|
+
: [])
|
|
3151
|
+
if (action === 'disable') next.add(id)
|
|
3152
|
+
else next.delete(id)
|
|
3153
|
+
return [...next].sort()
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
/**
|
|
3157
|
+
* The state badge: installed/active are the host's words, not a guess.
|
|
3158
|
+
*
|
|
3159
|
+
* RC3 (one source of truth): a row the host reports `blocked` — or that
|
|
3160
|
+
* carries an unmet requirement which blocks this row — is never rendered
|
|
3161
|
+
* as plainly "active". The badge reads "active · blocked" so the
|
|
3162
|
+
* lifecycle fact and the refusal are both visible (the toggle visual
|
|
3163
|
+
* follows the same rule in extensionRemoteRow).
|
|
3164
|
+
*/
|
|
3165
|
+
function extensionBadge(state) {
|
|
3166
|
+
if (state === null) return h('span', { className: 'mtt-badge' }, 'not reported')
|
|
3167
|
+
if (state.installed !== true) return h('span', { className: 'mtt-badge' }, 'not installed')
|
|
3168
|
+
const blocked = state.blocked === true
|
|
3169
|
+
if (state.active === true && !blocked) return h('span', { className: 'mtt-badge mtt-badge-on' }, 'active')
|
|
3170
|
+
if (state.active === true && blocked) return h('span', { className: 'mtt-badge' }, 'active · blocked')
|
|
3171
|
+
return h('span', { className: 'mtt-badge' }, 'inactive')
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
/**
|
|
3175
|
+
* Whether one unmet requirement blocks THIS row (RC3; mirrors the host's
|
|
3176
|
+
* `blocksRow` classification and src/extension-page-state.ts verbatim).
|
|
3177
|
+
* Pre-install, only store/source/dependencies/platform/harness-model/
|
|
3178
|
+
* runtime unmet requirements block; assets and the other post-install
|
|
3179
|
+
* kinds are notes, never the headline blocker.
|
|
3180
|
+
*/
|
|
3181
|
+
function requirementBlocksThisRow(installed, requirement) {
|
|
3182
|
+
if (requirement.satisfied === true) return false
|
|
3183
|
+
if (installed) return true
|
|
3184
|
+
return requirement.kind === 'store' || requirement.kind === 'source'
|
|
3185
|
+
|| requirement.kind === 'dependencies' || requirement.kind === 'platform'
|
|
3186
|
+
|| requirement.kind === 'harness-model' || requirement.kind === 'runtime'
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
/** The parts of one extension row shared by both flows: name, tools, models. */
|
|
3190
|
+
function extensionParts(entry) {
|
|
3191
|
+
const parts = []
|
|
3192
|
+
if (entry.tools.length > 0) parts.push('tools: ' + entry.tools.join(', '))
|
|
3193
|
+
if (entry.contracts.length > 0) parts.push('contracts: ' + entry.contracts.join(', '))
|
|
3194
|
+
if (entry.models.length > 0) parts.push('models: ' + entry.models.join(', '))
|
|
3195
|
+
return parts
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
/**
|
|
3199
|
+
* The requirements a describe() row reports, when the host checked them.
|
|
3200
|
+
*
|
|
3201
|
+
* A legacy host (older wire/host) has no `requirements` field, and [] then
|
|
3202
|
+
* means "the host did not check" — never "everything is satisfied". Malformed
|
|
3203
|
+
* entries are dropped rather than rendered as facts.
|
|
3204
|
+
*/
|
|
3205
|
+
function extensionRequirements(state) {
|
|
3206
|
+
if (state === null || !Array.isArray(state.requirements)) return []
|
|
3207
|
+
return state.requirements.filter(requirement => requirement !== null && typeof requirement === 'object'
|
|
3208
|
+
&& typeof requirement.kind === 'string' && typeof requirement.satisfied === 'boolean')
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
/** Unmet requirements, preserving the host's order. */
|
|
3212
|
+
function unmetExtensionRequirements(state) {
|
|
3213
|
+
return extensionRequirements(state).filter(requirement => requirement.satisfied !== true)
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
/** A compact disclosure of what the host checked and satisfied. */
|
|
3217
|
+
function extensionRequirementSummary(state) {
|
|
3218
|
+
const satisfied = extensionRequirements(state).filter(requirement => requirement.satisfied === true)
|
|
3219
|
+
if (satisfied.length === 0) return null
|
|
3220
|
+
return 'Requirements checked: ' + satisfied.map(requirement => requirement.kind + ' ✓').join(' · ')
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
/**
|
|
3224
|
+
* The one hint an unmet requirement carries, derived from `action` — never
|
|
3225
|
+
* from the message text, because the host may reword its reasons. Assets and
|
|
3226
|
+
* executable requirements get no hint: the Prepare control is their fix.
|
|
3227
|
+
*
|
|
3228
|
+
* @param requirement - one unmet ExtensionRequirement.
|
|
3229
|
+
* @returns a hint line, or null when the row's controls say it.
|
|
3230
|
+
*/
|
|
3231
|
+
function requirementHint(requirement) {
|
|
3232
|
+
if (requirement.kind === 'assets' || requirement.kind === 'executable') return null
|
|
3233
|
+
if (requirement.action === 'config') return 'Configure the deployment to satisfy this requirement.'
|
|
3234
|
+
if (requirement.action === 'none') return 'This requirement cannot be fixed from this page.'
|
|
3235
|
+
if (requirement.action === 'install') return 'Install the extension to satisfy this requirement.'
|
|
3236
|
+
return null
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
/**
|
|
3240
|
+
* What a row's controls may do, keyed ONLY on requirement kind/action/satisfied
|
|
3241
|
+
* and the host's `blocked` flag.
|
|
3242
|
+
*
|
|
3243
|
+
* Absent fields mean a legacy host: no requirements, `blocked` false, actions
|
|
3244
|
+
* enabled exactly as before. Harness-model/platform/integrity/activation
|
|
3245
|
+
* blockers disable Enable; assets/executable disable Enable but keep Prepare
|
|
3246
|
+
* (the fix); store/source disable Install.
|
|
3247
|
+
*
|
|
3248
|
+
* @param state - the describe() row, or null.
|
|
3249
|
+
* @returns { blocked, unmet, enableBlocked, installBlocked, prepareNeeded }.
|
|
3250
|
+
*/
|
|
3251
|
+
function extensionRequirementControls(state) {
|
|
3252
|
+
const installed = state !== null && state.installed === true
|
|
3253
|
+
const blocked = state !== null && state.blocked === true
|
|
3254
|
+
const unmet = unmetExtensionRequirements(state)
|
|
3255
|
+
return {
|
|
3256
|
+
blocked,
|
|
3257
|
+
unmet,
|
|
3258
|
+
enableBlocked: installed && blocked,
|
|
3259
|
+
installBlocked: unmet.some(requirement => requirement.kind === 'store' || requirement.kind === 'source'),
|
|
3260
|
+
prepareNeeded: installed && unmet.some(requirement => requirement.kind === 'assets'
|
|
3261
|
+
|| requirement.kind === 'executable'),
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3265
|
+
/**
|
|
3266
|
+
* Whether a describe() row says the extension's model assets must be
|
|
3267
|
+
* prepared before activation.
|
|
3268
|
+
*
|
|
3269
|
+
* On a current host this is decided ONLY by the requirements array (an unmet
|
|
3270
|
+
* assets/executable entry). A legacy host carries no requirements, so its
|
|
3271
|
+
* reason text is the only signal — and it only decides whether the Prepare
|
|
3272
|
+
* control is offered; the host's text is never rewritten.
|
|
3273
|
+
*
|
|
3274
|
+
* @param state - the describe() row, or null.
|
|
3275
|
+
* @returns true when the host names preparation as the blocker.
|
|
3276
|
+
*/
|
|
3277
|
+
function extensionNeedsPreparation(state) {
|
|
3278
|
+
if (state === null || state.installed !== true) return false
|
|
3279
|
+
if (Array.isArray(state.requirements)) return extensionRequirementControls(state).prepareNeeded
|
|
3280
|
+
if (typeof state.reason !== 'string' || state.reason.length === 0) return false
|
|
3281
|
+
const reason = state.reason.toLowerCase()
|
|
3282
|
+
return reason.includes('needs its model assets prepared')
|
|
3283
|
+
|| reason.includes('needs preparation')
|
|
3284
|
+
|| reason.includes('run prepare for this extension')
|
|
3285
|
+
|| reason.includes('then prepare again')
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
/**
|
|
3289
|
+
* What to show after a prepare() call.
|
|
3290
|
+
*
|
|
3291
|
+
* `prepare` reports acquisition failures IN BAND (`ok: true` with
|
|
3292
|
+
* `prepared: false` and a reason), and RES-02 executable approval refusals
|
|
3293
|
+
* arrive the same way, so the refusal text is returned verbatim and never
|
|
3294
|
+
* replaced with a success claim. A successful call renders the disclosure the
|
|
3295
|
+
* host returned (plan bytes/license) rather than a bare "done".
|
|
3296
|
+
*
|
|
3297
|
+
* @param result - the ExtensionPrepareResult.
|
|
3298
|
+
* @returns one operator-facing note.
|
|
3299
|
+
*/
|
|
3300
|
+
function prepareResultNote(result) {
|
|
3301
|
+
if (result === null || result === undefined || typeof result !== 'object') {
|
|
3302
|
+
return 'The host returned no preparation state.'
|
|
3303
|
+
}
|
|
3304
|
+
if (result.prepared !== true) {
|
|
3305
|
+
return typeof result.reason === 'string' && result.reason.length > 0
|
|
3306
|
+
? result.reason
|
|
3307
|
+
: 'The host reported the preparation did not complete.'
|
|
3308
|
+
}
|
|
3309
|
+
const plan = result.plan !== null && typeof result.plan === 'object' ? result.plan : null
|
|
3310
|
+
let text
|
|
3311
|
+
if (plan === null) {
|
|
3312
|
+
text = 'The host reported the model assets prepared.'
|
|
3313
|
+
} else {
|
|
3314
|
+
text = 'Prepared ' + (typeof plan.modelId === 'string' ? plan.modelId : 'the model assets')
|
|
3315
|
+
if (typeof plan.totalBytes === 'number' && typeof plan.storageBytes === 'number') {
|
|
3316
|
+
text += ' — ' + plan.totalBytes + ' bytes download, ' + plan.storageBytes + ' retained'
|
|
3317
|
+
}
|
|
3318
|
+
if (plan.license !== null && typeof plan.license === 'object'
|
|
3319
|
+
&& typeof plan.license.name === 'string' && plan.license.name.length > 0) {
|
|
3320
|
+
text += ' · license: ' + plan.license.name
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
if (result.activationReady === false) {
|
|
3324
|
+
text += ' (activation still has remaining blockers; see the row reason)'
|
|
3325
|
+
}
|
|
3326
|
+
return text
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* The row note for a successful install.
|
|
3331
|
+
*
|
|
3332
|
+
* The wire result may carry `source` ('registry' | 'directory') and the exact
|
|
3333
|
+
* `resolvedVersion`. Both are OPTIONAL: an older host returns only
|
|
3334
|
+
* {id, version, active}, in which case this returns null and the describe()
|
|
3335
|
+
* row keeps showing today's "installed v<version>" text — provenance is never
|
|
3336
|
+
* invented from an absent or unknown source.
|
|
3337
|
+
*
|
|
3338
|
+
* @param result - the ExtensionInstallResult (any host version).
|
|
3339
|
+
* @returns the note, or null when the result does not name a known source.
|
|
3340
|
+
*/
|
|
3341
|
+
function installResultNote(result) {
|
|
3342
|
+
if (result === null || result === undefined || typeof result !== 'object') return null
|
|
3343
|
+
const version = typeof result.resolvedVersion === 'string' && result.resolvedVersion.length > 0
|
|
3344
|
+
? result.resolvedVersion
|
|
3345
|
+
: typeof result.version === 'string' && result.version.length > 0 ? result.version : null
|
|
3346
|
+
if (version === null) return null
|
|
3347
|
+
if (result.source === 'registry') return 'installed v' + version + ' from npm'
|
|
3348
|
+
if (result.source === 'directory') return 'installed v' + version + ' from the local package directory'
|
|
3349
|
+
return null
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
/**
|
|
3353
|
+
* The config-only row: the switch records a `disabledExtensions` preference
|
|
3354
|
+
* and cannot observe or change the store. Used when the Remote is absent.
|
|
3355
|
+
*/
|
|
3356
|
+
function extensionFallbackRow(entry, enabled, disabled, onToggle) {
|
|
3357
|
+
const parts = extensionParts(entry)
|
|
3358
|
+
return h('div', { className: 'mtt-row', key: entry.id },
|
|
3359
|
+
h('div', { className: 'mtt-row-text' },
|
|
3360
|
+
h('span', { className: 'mtt-label' },
|
|
3361
|
+
h('span', { className: 'mtt-name' }, entry.name),
|
|
3362
|
+
h('span', { className: 'mtt-badge' }, 'extension')),
|
|
3363
|
+
h('p', { className: 'mtt-help' }, entry.summary),
|
|
3364
|
+
h('span', { className: 'mtt-meta' },
|
|
3365
|
+
entry.id,
|
|
3366
|
+
parts.length > 0 ? ' · ' + parts.join(' · ') : '',
|
|
3367
|
+
' · ',
|
|
3368
|
+
entry.license ?? 'no license')),
|
|
3369
|
+
h('div', { className: 'mtt-control' },
|
|
3370
|
+
h('button', {
|
|
3371
|
+
type: 'button',
|
|
3372
|
+
className: 'mtt-toggle',
|
|
3373
|
+
'data-on': enabled ? 'true' : 'false',
|
|
3374
|
+
disabled: disabled,
|
|
3375
|
+
'aria-pressed': enabled,
|
|
3376
|
+
title: enabled ? 'Disable this extension' : 'Enable this extension',
|
|
3377
|
+
onClick: () => onToggle(entry.id, !enabled),
|
|
3378
|
+
}, h('span', { className: 'mtt-knob' }))))
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
/**
|
|
3382
|
+
* The store-backed row: Install when absent, Enable/Disable when installed.
|
|
3383
|
+
*
|
|
3384
|
+
* Nothing here is optimistic. `state` comes from the last describe(), the
|
|
3385
|
+
* toggle reflects `active` from that snapshot, and a failed action leaves the
|
|
3386
|
+
* host's error text on the row rather than flipping a switch that did not move.
|
|
3387
|
+
*
|
|
3388
|
+
* @param entry - the generated extension entry.
|
|
3389
|
+
* @param state - the describe() row for it, or null when not reported.
|
|
3390
|
+
* @param ui - { writable, canInstall, listed, busy, pending, error, prepareNote, installNote, onAction }.
|
|
3391
|
+
*/
|
|
3392
|
+
function extensionRemoteRow(entry, state, ui) {
|
|
3393
|
+
const installed = state !== null && state.installed === true
|
|
3394
|
+
const active = installed && state.active === true
|
|
3395
|
+
const pending = ui.pending !== null && ui.pending.id === entry.id ? ui.pending.action : null
|
|
3396
|
+
const requirementControls = extensionRequirementControls(state)
|
|
3397
|
+
const needsPreparation = extensionNeedsPreparation(state)
|
|
3398
|
+
const parts = extensionParts(entry)
|
|
3399
|
+
if (state !== null && typeof state.version === 'string' && state.version.length > 0) {
|
|
3400
|
+
parts.push('installed v' + state.version)
|
|
3401
|
+
}
|
|
3402
|
+
if (installed && state.disabled === true) parts.push('disabled in config')
|
|
3403
|
+
|
|
3404
|
+
// Note kinds: 'note' is muted disclosure, 'blocker' is an unmet requirement
|
|
3405
|
+
// (prominent), 'error' is the last action's failure. Keeping the kind per
|
|
3406
|
+
// note means the class never depends on an array position.
|
|
3407
|
+
const notes = []
|
|
3408
|
+
const addNote = (text, kind) => {
|
|
3409
|
+
if (typeof text === 'string' && text.length > 0) notes.push({ text, kind: kind ?? 'note' })
|
|
3410
|
+
}
|
|
3411
|
+
const summary = extensionRequirementSummary(state)
|
|
3412
|
+
if (summary !== null) addNote(summary)
|
|
3413
|
+
if (state === null) {
|
|
3414
|
+
addNote('The extension store did not report this package. Installing asks the host for it, and any refusal is shown here.')
|
|
3415
|
+
}
|
|
3416
|
+
// The host's reason is rendered VERBATIM: it is the authoritative
|
|
3417
|
+
// explanation. On a blocked row it is that row's headline, so it is
|
|
3418
|
+
// prominent instead of muted.
|
|
3419
|
+
if (state !== null && typeof state.reason === 'string' && state.reason.length > 0) {
|
|
3420
|
+
addNote(state.reason, requirementControls.blocked ? 'blocker' : 'note')
|
|
3421
|
+
}
|
|
3422
|
+
if (ui.prepareNote !== null && ui.prepareNote !== undefined && ui.prepareNote.id === entry.id) {
|
|
3423
|
+
addNote(ui.prepareNote.text)
|
|
3424
|
+
}
|
|
3425
|
+
if (ui.installNote !== null && ui.installNote !== undefined && ui.installNote.id === entry.id) {
|
|
3426
|
+
addNote(ui.installNote.text)
|
|
3427
|
+
}
|
|
3428
|
+
// Every unmet requirement gets its exact reason on the row. A requirement
|
|
3429
|
+
// whose reason is the row reason already is not repeated; the hint comes
|
|
3430
|
+
// from `action` only, never from the wording.
|
|
3431
|
+
//
|
|
3432
|
+
// RC3: the NOTE KIND follows the row classification — an unmet
|
|
3433
|
+
// requirement that blocks THIS row is a 'blocker'; a post-install fact
|
|
3434
|
+
// (assets/executable/readiness on a not-installed row) is a muted note,
|
|
3435
|
+
// so a pre-install assets hint can never read as the install blocker.
|
|
3436
|
+
if (state !== null && Array.isArray(state.requirements)) {
|
|
3437
|
+
for (const requirement of requirementControls.unmet) {
|
|
3438
|
+
if (requirement.reason !== state.reason) {
|
|
3439
|
+
addNote(requirement.reason, requirementBlocksThisRow(installed, requirement) ? 'blocker' : 'note')
|
|
3440
|
+
}
|
|
3441
|
+
addNote(requirementHint(requirement))
|
|
3442
|
+
}
|
|
3443
|
+
if (requirementControls.blocked && requirementControls.unmet.length === 0 && state.reason === null) {
|
|
3444
|
+
addNote('The host reports this extension blocked but named no requirement; see the dsh logs.', 'blocker')
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
if (state !== null && state.integrity === 'changed') {
|
|
3448
|
+
addNote('The installed content changed since it was recorded; reinstall before enabling it.')
|
|
3449
|
+
}
|
|
3450
|
+
// The store and the config list must agree across a restart. Rendering both
|
|
3451
|
+
// is what lets the operator fix a disagreement with one toggle instead of
|
|
3452
|
+
// discovering it after the next restore.
|
|
3453
|
+
if (installed && active && ui.listed) {
|
|
3454
|
+
addNote('The config list still names this extension; it will be disabled on restart.')
|
|
3455
|
+
}
|
|
3456
|
+
if (installed && !active && !ui.listed) {
|
|
3457
|
+
addNote('The config list does not name this extension; it will not block a restore.')
|
|
3458
|
+
}
|
|
3459
|
+
if (!installed && !ui.canInstall) {
|
|
3460
|
+
addNote('This host exposes extension state but not the install action; install it through the extension store.')
|
|
3461
|
+
}
|
|
3462
|
+
if (ui.error !== null && ui.error.id === entry.id) addNote(ui.error.text, 'error')
|
|
3463
|
+
|
|
3464
|
+
const canInstall = ui.canInstall
|
|
3465
|
+
// RC3: the toggle VISUAL is OFF whenever the row is blocked, so a
|
|
3466
|
+
// blocked row never renders as plainly ON; the aria-pressed state and
|
|
3467
|
+
// the action title keep the lifecycle fact (the host's own words), and
|
|
3468
|
+
// the disabled/enabled semantics are unchanged.
|
|
3469
|
+
const blockedRow = installed && requirementControls.blocked
|
|
3470
|
+
const control = installed
|
|
3471
|
+
? h('button', {
|
|
3472
|
+
type: 'button',
|
|
3473
|
+
className: 'mtt-toggle',
|
|
3474
|
+
'data-on': active && !blockedRow ? 'true' : 'false',
|
|
3475
|
+
disabled: ui.busy || !ui.writable || requirementControls.enableBlocked,
|
|
3476
|
+
'aria-pressed': active,
|
|
3477
|
+
'aria-busy': pending !== null,
|
|
3478
|
+
title: requirementControls.enableBlocked
|
|
3479
|
+
? 'Enable is blocked by a host requirement; see the row reason'
|
|
3480
|
+
: active ? 'Disable this extension' : 'Enable this extension',
|
|
3481
|
+
onClick: () => ui.onAction(entry.id, active ? 'disable' : 'enable'),
|
|
3482
|
+
}, h('span', { className: 'mtt-knob' }))
|
|
3483
|
+
: h('button', {
|
|
3484
|
+
type: 'button',
|
|
3485
|
+
className: 'mtt-button mtt-button-small',
|
|
3486
|
+
disabled: ui.busy || !ui.writable || !canInstall || requirementControls.installBlocked,
|
|
3487
|
+
'aria-busy': pending === 'install',
|
|
3488
|
+
title: !canInstall
|
|
3489
|
+
? 'This host does not expose the extension install action'
|
|
3490
|
+
: requirementControls.installBlocked
|
|
3491
|
+
? 'Install is blocked by a host requirement; see the row reason'
|
|
3492
|
+
: state === null ? 'Ask the host to install this extension' : 'Install this extension',
|
|
3493
|
+
onClick: () => ui.onAction(entry.id, 'install'),
|
|
3494
|
+
}, pending === 'install' ? 'Installing…' : 'Install')
|
|
3495
|
+
|
|
3496
|
+
return h('div', { className: 'mtt-row', key: entry.id },
|
|
3497
|
+
h('div', { className: 'mtt-row-text' },
|
|
3498
|
+
h('span', { className: 'mtt-label' },
|
|
3499
|
+
h('span', { className: 'mtt-name' }, entry.name),
|
|
3500
|
+
extensionBadge(state)),
|
|
3501
|
+
h('p', { className: 'mtt-help' }, entry.summary),
|
|
3502
|
+
h('span', { className: 'mtt-meta' },
|
|
3503
|
+
entry.id,
|
|
3504
|
+
parts.length > 0 ? ' · ' + parts.join(' · ') : '',
|
|
3505
|
+
' · ',
|
|
3506
|
+
entry.license ?? 'no license'),
|
|
3507
|
+
notes.map((note, index) => h('p', {
|
|
3508
|
+
className: note.kind === 'error'
|
|
3509
|
+
? 'mtt-row-error'
|
|
3510
|
+
: note.kind === 'blocker' ? 'mtt-row-blocker' : 'mtt-row-note',
|
|
3511
|
+
key: 'note-' + index,
|
|
3512
|
+
}, note.text))),
|
|
3513
|
+
h('div', { className: 'mtt-control' },
|
|
3514
|
+
control,
|
|
3515
|
+
// Preparation is offered only when the HOST names it as the blocker (or
|
|
3516
|
+
// an unmet assets/executable requirement names it); it is not a second
|
|
3517
|
+
// way to force activation. After a successful prepare the row's
|
|
3518
|
+
// requirements change and this control disappears.
|
|
3519
|
+
needsPreparation
|
|
3520
|
+
? h('button', {
|
|
3521
|
+
type: 'button',
|
|
3522
|
+
className: 'mtt-button mtt-button-small',
|
|
3523
|
+
disabled: ui.busy || !ui.writable,
|
|
3524
|
+
'aria-busy': pending === 'prepare',
|
|
3525
|
+
title: 'Prepare the model assets this extension needs before it can be enabled',
|
|
3526
|
+
onClick: () => ui.onAction(entry.id, 'prepare'),
|
|
3527
|
+
}, pending === 'prepare' ? 'Preparing…' : 'Prepare')
|
|
3528
|
+
: null))
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
function ModelToolsSettings({ form, ctx, remoteSlot }) {
|
|
3532
|
+
const [snap, setSnap] = React.useState(() => form.getSnapshot())
|
|
3533
|
+
const [draft, setDraft] = React.useState({})
|
|
3534
|
+
const [exclude, setExclude] = React.useState(null)
|
|
3535
|
+
const [disabledExt, setDisabledExt] = React.useState(null)
|
|
3536
|
+
const [status, setStatus] = React.useState(null)
|
|
3537
|
+
// Extension state is the HOST's answer, read from ctx.remote.modelTools.
|
|
3538
|
+
// `kind` is loading | ready | absent | error; it starts loading so the card
|
|
3539
|
+
// never renders a guessed installed/active state before the first read.
|
|
3540
|
+
const [extRemote, setExtRemote] = React.useState({ kind: 'loading' })
|
|
3541
|
+
const [extPending, setExtPending] = React.useState(null)
|
|
3542
|
+
const [extRowError, setExtRowError] = React.useState(null)
|
|
3543
|
+
// Non-fatal: activation succeeded but the config-list dual-write did not.
|
|
3544
|
+
// The action is never rolled back for this; the note tells the operator the
|
|
3545
|
+
// two stores can disagree until the list is saved manually.
|
|
3546
|
+
const [extConfigNote, setExtConfigNote] = React.useState(null)
|
|
3547
|
+
// The last prepare() disclosure/refusal, per row. It is rendered beside the
|
|
3548
|
+
// host's reason so a RES-02 refusal or plan is shown verbatim.
|
|
3549
|
+
const [extPrepareNote, setExtPrepareNote] = React.useState(null)
|
|
3550
|
+
// The provenance note from the last successful install, per row ("installed
|
|
3551
|
+
// v<resolvedVersion> from npm / from the local package directory"). Null for
|
|
3552
|
+
// a legacy host result that carries neither source nor resolvedVersion.
|
|
3553
|
+
const [extInstallNote, setExtInstallNote] = React.useState(null)
|
|
3554
|
+
|
|
3555
|
+
/** This mount's namespace status, or null when the wire chunk is absent. */
|
|
3556
|
+
const mounted = () => (remoteSlot === null || remoteSlot === undefined ? null : remoteSlot.get())
|
|
3557
|
+
|
|
3558
|
+
React.useEffect(() => form.subscribe(() => setSnap(form.getSnapshot())), [form])
|
|
3559
|
+
|
|
3560
|
+
// Read the extension state on mount. The namespace may arrive after this
|
|
3561
|
+
// page (the wire chunk is fetched first), so an absent namespace is retried
|
|
3562
|
+
// briefly AND the chunk's slot subscription re-reads when the mount settles.
|
|
3563
|
+
// `alive` keeps a late answer from writing into an unmounted page.
|
|
3564
|
+
React.useEffect(() => {
|
|
3565
|
+
let alive = true
|
|
3566
|
+
let timer = null
|
|
3567
|
+
let attempts = 0
|
|
3568
|
+
const adopt = namespace => {
|
|
3569
|
+
describeExtensions(namespace).then(described => {
|
|
3570
|
+
if (alive) setExtRemote({ kind: 'ready', namespace, ...described })
|
|
3571
|
+
}, error => {
|
|
3572
|
+
if (alive) setExtRemote({ kind: 'error', text: remoteErrorText(error) })
|
|
3573
|
+
})
|
|
3574
|
+
}
|
|
3575
|
+
const read = () => {
|
|
3576
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
3577
|
+
if (namespace !== null) {
|
|
3578
|
+
adopt(namespace)
|
|
3579
|
+
return
|
|
3580
|
+
}
|
|
3581
|
+
if (attempts >= REMOTE_RETRY_ATTEMPTS) {
|
|
3582
|
+
if (alive) setExtRemote({ kind: 'absent' })
|
|
3583
|
+
return
|
|
3584
|
+
}
|
|
3585
|
+
attempts += 1
|
|
3586
|
+
timer = setTimeout(read, REMOTE_RETRY_DELAY_MS)
|
|
3587
|
+
}
|
|
3588
|
+
read()
|
|
3589
|
+
const unsubscribe = remoteSlot !== null && remoteSlot !== undefined
|
|
3590
|
+
&& typeof remoteSlot.subscribe === 'function'
|
|
3591
|
+
? remoteSlot.subscribe(() => {
|
|
3592
|
+
if (!alive) return
|
|
3593
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
3594
|
+
if (namespace !== null) adopt(namespace)
|
|
3595
|
+
})
|
|
3596
|
+
: null
|
|
3597
|
+
return () => {
|
|
3598
|
+
alive = false
|
|
3599
|
+
if (unsubscribe !== null) unsubscribe()
|
|
3600
|
+
if (timer !== null) clearTimeout(timer)
|
|
3601
|
+
}
|
|
3602
|
+
}, [ctx])
|
|
3603
|
+
|
|
3604
|
+
if (snap.status === 'unavailable') {
|
|
3605
|
+
return h('div', { className: 'mtt-page' },
|
|
3606
|
+
h('style', null, CSS),
|
|
3607
|
+
h('h2', { className: 'mtt-title' }, 'Model as Tool'),
|
|
3608
|
+
h('p', { className: 'mtt-intro' },
|
|
3609
|
+
'The model-tools configuration namespace is not exposed to this client. '
|
|
3610
|
+
+ 'Edit the profile cordis.patch.yml entry with id "model-tools" instead.'))
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
const value = snap.value || {}
|
|
3614
|
+
const disabled = snap.writable === false
|
|
3615
|
+
const edit = (key, next) => setDraft(previous => {
|
|
3616
|
+
const updated = { ...previous }
|
|
3617
|
+
updated[key] = next
|
|
3618
|
+
return updated
|
|
3619
|
+
})
|
|
3620
|
+
|
|
3621
|
+
const savedExclude = Array.isArray(value.exclude) ? value.exclude : []
|
|
3622
|
+
const currentExclude = exclude ?? savedExclude
|
|
3623
|
+
const allowlist = Array.isArray(value.models) ? value.models : []
|
|
3624
|
+
|
|
3625
|
+
/** Whether a model is exposed: not excluded (and inside the allowlist when one is set). */
|
|
3626
|
+
function isEnabled(id) {
|
|
3627
|
+
if (currentExclude.includes(id)) return false
|
|
3628
|
+
return allowlist.length === 0 || allowlist.includes(id)
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
const dirty = FIELDS.some(field => draft[field.key] !== undefined) || exclude !== null || disabledExt !== null
|
|
3632
|
+
|
|
3633
|
+
const save = async () => {
|
|
3634
|
+
setStatus({ kind: 'busy', text: 'Saving…' })
|
|
3635
|
+
try {
|
|
3636
|
+
let refused = false
|
|
3637
|
+
for (const field of FIELDS) {
|
|
3638
|
+
if (draft[field.key] === undefined) continue
|
|
3639
|
+
const accepted = await form.set(field.key, fieldPatch(field, draft[field.key]))
|
|
3640
|
+
if (accepted === false) refused = true
|
|
3641
|
+
}
|
|
3642
|
+
if (exclude !== null) {
|
|
3643
|
+
const accepted = await form.set('exclude', exclude)
|
|
3644
|
+
if (accepted === false) refused = true
|
|
3645
|
+
}
|
|
3646
|
+
if (disabledExt !== null) {
|
|
3647
|
+
const accepted = await form.set('disabledExtensions', disabledExt)
|
|
3648
|
+
if (accepted === false) refused = true
|
|
3649
|
+
}
|
|
3650
|
+
setDraft({})
|
|
3651
|
+
setExclude(null)
|
|
3652
|
+
setDisabledExt(null)
|
|
3653
|
+
setStatus(refused
|
|
3654
|
+
? { kind: 'error', text: 'Refused by the host — see the dsh logs.' }
|
|
3655
|
+
: { kind: 'ok', text: 'Saved. Changes apply to new sessions.' })
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
setStatus({ kind: 'error', text: String((error && error.message) || error) })
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
const savedDisabledExt = Array.isArray(value.disabledExtensions) ? value.disabledExtensions : []
|
|
3662
|
+
const currentDisabledExt = disabledExt ?? savedDisabledExt
|
|
3663
|
+
const isExtensionEnabled = id => !currentDisabledExt.includes(id)
|
|
3664
|
+
const toggleExtension = (id, on) => setDisabledExt(() => {
|
|
3665
|
+
const next = new Set(currentDisabledExt)
|
|
3666
|
+
if (on) next.delete(id)
|
|
3667
|
+
else next.add(id)
|
|
3668
|
+
return [...next].sort()
|
|
3669
|
+
})
|
|
3670
|
+
|
|
3671
|
+
const toggleModel = (id, on) => setExclude(() => {
|
|
3672
|
+
const next = new Set(currentExclude)
|
|
3673
|
+
if (on) next.delete(id)
|
|
3674
|
+
else next.add(id)
|
|
3675
|
+
return [...next].sort()
|
|
3676
|
+
})
|
|
3677
|
+
|
|
3678
|
+
/**
|
|
3679
|
+
* Best-effort write of Config.disabledExtensions after a lifecycle
|
|
3680
|
+
* enable/disable. Activation is the authority: a refusal here is reported
|
|
3681
|
+
* as a NON-FATAL note and never undoes or hides the activation.
|
|
3682
|
+
*
|
|
3683
|
+
* The list is read from the form's CURRENT snapshot (another tab or the
|
|
3684
|
+
* Save button may have changed it since this render), falling back to the
|
|
3685
|
+
* render's saved value.
|
|
3686
|
+
*/
|
|
3687
|
+
const dualWriteDisabledExtensions = async (id, action) => {
|
|
3688
|
+
const snapshot = form.getSnapshot()
|
|
3689
|
+
const snapshotValue = snapshot && snapshot.value !== null && typeof snapshot.value === 'object'
|
|
3690
|
+
? snapshot.value
|
|
3691
|
+
: {}
|
|
3692
|
+
const saved = Array.isArray(snapshotValue.disabledExtensions)
|
|
3693
|
+
? snapshotValue.disabledExtensions
|
|
3694
|
+
: savedDisabledExt
|
|
3695
|
+
const nextList = nextDisabledExtensions(saved, id, action)
|
|
3696
|
+
const unchanged = Array.isArray(saved) && nextList.length === saved.length
|
|
3697
|
+
&& nextList.every((entry, index) => entry === saved[index])
|
|
3698
|
+
if (unchanged) return
|
|
3699
|
+
const what = action === 'disable' ? 'disabled' : 'enabled'
|
|
3700
|
+
try {
|
|
3701
|
+
const accepted = await form.set('disabledExtensions', nextList)
|
|
3702
|
+
if (accepted === false) {
|
|
3703
|
+
setExtConfigNote('The extension was ' + what + ', but the host refused the disabledExtensions update; '
|
|
3704
|
+
+ 'the config list can disagree with the store until it is saved manually.')
|
|
3705
|
+
}
|
|
3706
|
+
} catch (error) {
|
|
3707
|
+
setExtConfigNote('The extension was ' + what + ', but the config list update failed: '
|
|
3708
|
+
+ remoteErrorText(error))
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
/** Read the host state once, after a manual "Try again". */
|
|
3713
|
+
const recheckExtensions = async () => {
|
|
3714
|
+
setExtRowError(null)
|
|
3715
|
+
setExtConfigNote(null)
|
|
3716
|
+
setExtPrepareNote(null)
|
|
3717
|
+
setExtInstallNote(null)
|
|
3718
|
+
setExtRemote({ kind: 'loading' })
|
|
3719
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
3720
|
+
if (namespace === null) {
|
|
3721
|
+
setExtRemote({ kind: 'absent' })
|
|
3722
|
+
return
|
|
3723
|
+
}
|
|
3724
|
+
try {
|
|
3725
|
+
setExtRemote({ kind: 'ready', namespace, ...(await describeExtensions(namespace)) })
|
|
3726
|
+
} catch (error) {
|
|
3727
|
+
setExtRemote({ kind: 'error', text: remoteErrorText(error) })
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
/**
|
|
3732
|
+
* Run one extension action, then RE-READ the host state.
|
|
3733
|
+
*
|
|
3734
|
+
* The host is the authority: a successful install is not activation, and
|
|
3735
|
+
* enable/disable may be refused for reasons only describe() can explain.
|
|
3736
|
+
* The re-read is what moves the controls; the action result is never
|
|
3737
|
+
* painted as a state change by itself.
|
|
3738
|
+
*/
|
|
3739
|
+
const runExtensionAction = async (id, action) => {
|
|
3740
|
+
setExtRowError(null)
|
|
3741
|
+
setExtConfigNote(null)
|
|
3742
|
+
setExtPrepareNote(null)
|
|
3743
|
+
setExtInstallNote(null)
|
|
3744
|
+
setExtPending({ id, action })
|
|
3745
|
+
try {
|
|
3746
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
3747
|
+
if (namespace === null) {
|
|
3748
|
+
setExtRemote({ kind: 'absent' })
|
|
3749
|
+
return
|
|
3750
|
+
}
|
|
3751
|
+
if (action === 'install') {
|
|
3752
|
+
const install = extensionInstallMethod(namespace)
|
|
3753
|
+
if (install === null) throw new Error('This host does not expose the extension install action.')
|
|
3754
|
+
// The wire descriptor is installExtension(id, source): Typert counts
|
|
3755
|
+
// business arguments exactly, and an omitted `source` still occupies
|
|
3756
|
+
// its positional slot. Passing only the id is refused by the gateway
|
|
3757
|
+
// ("expected 2 business argument(s) ... got 1"), so pass undefined to
|
|
3758
|
+
// mean "use the configured first-party source".
|
|
3759
|
+
const installed = unwrapRemoteResult(await install.call(namespace, id, undefined))
|
|
3760
|
+
// Provenance note when the host names the route; null on a legacy
|
|
3761
|
+
// result, in which case describe()'s "installed v<version>" stands.
|
|
3762
|
+
const note = installResultNote(installed)
|
|
3763
|
+
if (note !== null) setExtInstallNote({ id, text: note })
|
|
3764
|
+
} else if (action === 'prepare') {
|
|
3765
|
+
if (typeof namespace.prepare !== 'function') {
|
|
3766
|
+
throw new Error('This host does not expose the extension prepare action.')
|
|
3767
|
+
}
|
|
3768
|
+
// prepare(id) has no optional data argument; an AbortSignal may ride
|
|
3769
|
+
// as the second wire slot, which this page does not need.
|
|
3770
|
+
const prepared = unwrapRemoteResult(await namespace.prepare(id))
|
|
3771
|
+
setExtPrepareNote({ id, text: prepareResultNote(prepared) })
|
|
3772
|
+
} else if (action === 'enable') {
|
|
3773
|
+
unwrapRemoteResult(await namespace.enable(id))
|
|
3774
|
+
// Dual-write Config.disabledExtensions so a restart cannot re-disable
|
|
3775
|
+
// what the operator just enabled. Best-effort and non-fatal.
|
|
3776
|
+
await dualWriteDisabledExtensions(id, 'enable')
|
|
3777
|
+
} else {
|
|
3778
|
+
unwrapRemoteResult(await namespace.disable(id))
|
|
3779
|
+
await dualWriteDisabledExtensions(id, 'disable')
|
|
3780
|
+
}
|
|
3781
|
+
setExtRemote({ kind: 'ready', namespace, ...(await describeExtensions(namespace)) })
|
|
3782
|
+
// A store write and the following describe can land in either order
|
|
3783
|
+
// under load. One bounded re-read after a successful action settles
|
|
3784
|
+
// the row's controls (the Enable switch appears on an install) without
|
|
3785
|
+
// asking the operator to reopen the page. mounted() gates a stale
|
|
3786
|
+
// update after the dialog closes.
|
|
3787
|
+
setTimeout(() => {
|
|
3788
|
+
void (async () => {
|
|
3789
|
+
try {
|
|
3790
|
+
const settled = modelToolsRemote(ctx, mounted())
|
|
3791
|
+
if (settled !== null) {
|
|
3792
|
+
setExtRemote({ kind: 'ready', namespace: settled, ...(await describeExtensions(settled)) })
|
|
3793
|
+
}
|
|
3794
|
+
} catch (settleError) {
|
|
3795
|
+
// The snapshot already rendered is the last known host truth.
|
|
3796
|
+
}
|
|
3797
|
+
})()
|
|
3798
|
+
}, 500)
|
|
3799
|
+
} catch (error) {
|
|
3800
|
+
setExtRowError({ id, text: remoteErrorText(error) })
|
|
3801
|
+
// The action may have changed host state before failing. Read it back,
|
|
3802
|
+
// but keep the action's error text if the re-read fails as well.
|
|
3803
|
+
try {
|
|
3804
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
3805
|
+
if (namespace !== null) {
|
|
3806
|
+
setExtRemote({ kind: 'ready', namespace, ...(await describeExtensions(namespace)) })
|
|
3807
|
+
}
|
|
3808
|
+
} catch (readError) {
|
|
3809
|
+
// The row keeps the host's action error; the state stays the last
|
|
3810
|
+
// snapshot describe() gave us, which is still the last known truth.
|
|
3811
|
+
}
|
|
3812
|
+
} finally {
|
|
3813
|
+
setExtPending(null)
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
const runnable = CATALOG.filter(entry => entry.runnable === true)
|
|
3818
|
+
const unavailable = CATALOG.filter(entry => entry.runnable !== true)
|
|
3819
|
+
|
|
3820
|
+
const extPendingEntry = extPending === null
|
|
3821
|
+
? null
|
|
3822
|
+
: EXTENSIONS.find(entry => entry.id === extPending.id) ?? { name: extPending.id }
|
|
3823
|
+
const extPendingText = extPending === null
|
|
3824
|
+
? null
|
|
3825
|
+
: (EXTENSION_ACTION_LABELS[extPending.action] ?? 'Working on') + ' ' + extPendingEntry.name + '…'
|
|
3826
|
+
const extWritable = extRemote.kind === 'ready' && extRemote.writable === true
|
|
3827
|
+
const extBusy = extPending !== null
|
|
3828
|
+
|
|
3829
|
+
let extensionCard
|
|
3830
|
+
if (extRemote.kind === 'ready') {
|
|
3831
|
+
const notes = []
|
|
3832
|
+
if (extRemote.store === null) {
|
|
3833
|
+
notes.push('No extension store is configured on this host (extensionStore), so an extension cannot be installed or activated here.')
|
|
3834
|
+
} else if (!extRemote.writable) {
|
|
3835
|
+
notes.push('The host reports its extension store as read-only right now; the extension controls are disabled.')
|
|
3836
|
+
}
|
|
3837
|
+
const canInstall = extensionInstallMethod(extRemote.namespace) !== null
|
|
3838
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
3839
|
+
h('p', { className: 'mtt-help' },
|
|
3840
|
+
'Tool Mode extensions shipped with this plugin. The host\'s extension store is the authority: '
|
|
3841
|
+
+ 'Install copies a package into the store, then the switch activates it. Installation is not activation.'),
|
|
3842
|
+
notes.map((note, index) => h('p', { className: 'mtt-row-note', key: 'store-note-' + index }, note)),
|
|
3843
|
+
extPendingText === null ? null : h('p', { className: 'mtt-status' }, extPendingText),
|
|
3844
|
+
extConfigNote === null ? null : h('p', { className: 'mtt-row-note' }, extConfigNote),
|
|
3845
|
+
EXTENSIONS.map(entry => extensionRemoteRow(
|
|
3846
|
+
entry,
|
|
3847
|
+
findExtensionState(entry, extRemote.extensions),
|
|
3848
|
+
{
|
|
3849
|
+
writable: extWritable,
|
|
3850
|
+
canInstall,
|
|
3851
|
+
// The SAVED config list, not the fallback draft: the row describes
|
|
3852
|
+
// what a restart will actually read, and the dual-write compares
|
|
3853
|
+
// against the same saved value.
|
|
3854
|
+
listed: savedDisabledExt.includes(entry.id),
|
|
3855
|
+
busy: extBusy,
|
|
3856
|
+
pending: extPending,
|
|
3857
|
+
error: extRowError,
|
|
3858
|
+
prepareNote: extPrepareNote,
|
|
3859
|
+
installNote: extInstallNote,
|
|
3860
|
+
onAction: runExtensionAction,
|
|
3861
|
+
})))
|
|
3862
|
+
} else if (extRemote.kind === 'error') {
|
|
3863
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
3864
|
+
h('p', { className: 'mtt-help' },
|
|
3865
|
+
'The host exposes extension controls, but reading their state failed.'),
|
|
3866
|
+
h('p', { className: 'mtt-row-error' }, extRemote.text),
|
|
3867
|
+
h('div', { className: 'mtt-actions' },
|
|
3868
|
+
h('button', {
|
|
3869
|
+
type: 'button',
|
|
3870
|
+
className: 'mtt-button mtt-button-small',
|
|
3871
|
+
onClick: recheckExtensions,
|
|
3872
|
+
}, 'Try again')))
|
|
3873
|
+
} else if (extRemote.kind === 'absent') {
|
|
3874
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
3875
|
+
h('p', { className: 'mtt-help' },
|
|
3876
|
+
'Tool Mode extensions shipped with this plugin. The host\'s extension controls are not available to '
|
|
3877
|
+
+ 'this page (an older host build, or the extension Remote is not mounted), so this page cannot install, '
|
|
3878
|
+
+ 'verify, or observe them.'),
|
|
3879
|
+
h('p', { className: 'mtt-row-note' },
|
|
3880
|
+
'The switches below only record a disabledExtensions preference for the next load: they neither install '
|
|
3881
|
+
+ 'nor verify the package.'),
|
|
3882
|
+
EXTENSIONS.map(entry => extensionFallbackRow(entry, isExtensionEnabled(entry.id), disabled, toggleExtension)),
|
|
3883
|
+
h('div', { className: 'mtt-actions' },
|
|
3884
|
+
h('button', {
|
|
3885
|
+
type: 'button',
|
|
3886
|
+
className: 'mtt-button mtt-button-small',
|
|
3887
|
+
onClick: recheckExtensions,
|
|
3888
|
+
}, 'Recheck')))
|
|
3889
|
+
} else {
|
|
3890
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
3891
|
+
h('p', { className: 'mtt-help' }, 'Reading the extension state from the host…'))
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
return h('div', { className: 'mtt-page' },
|
|
3895
|
+
h('style', null, CSS),
|
|
3896
|
+
h('h2', { className: 'mtt-title' }, 'Model as Tool'),
|
|
3897
|
+
h('p', { className: 'mtt-intro' },
|
|
3898
|
+
'On-device models from the Desert Ant catalog, exposed as tools and skills. A model becomes a tool only when this '
|
|
3899
|
+
+ 'process can actually run it; extension-backed models are listed with the extension that provides them.'),
|
|
3900
|
+
h('p', { className: 'mtt-help mtt-restart-notice', role: 'note' }, 'Model and tool registration changes apply after restart.'),
|
|
3901
|
+
h('div', { className: 'mtt-card' },
|
|
3902
|
+
h('p', { className: 'mtt-help' },
|
|
3903
|
+
runnable.length + ' of ' + CATALOG.length + ' catalog models are runnable in this process. '
|
|
3904
|
+
+ 'Unavailable models are listed with the reason they cannot run here.'),
|
|
3905
|
+
runnable.map(entry => modelRow(entry, isEnabled(entry.id), disabled, toggleModel)),
|
|
3906
|
+
// Unavailable rows are not operable switches: render them as disabled
|
|
3907
|
+
// controls so the grey off state matches their real state.
|
|
3908
|
+
unavailable.map(entry => modelRow(entry, false, true, toggleModel))),
|
|
3909
|
+
EXTENSIONS.length === 0 ? null : extensionCard,
|
|
3910
|
+
h('div', { className: 'mtt-card' }, FIELDS.map(field => fieldRow(field, value, draft, edit, disabled))),
|
|
3911
|
+
h('div', { className: 'mtt-actions' },
|
|
3912
|
+
h('button', {
|
|
3913
|
+
type: 'button', className: 'mtt-button', disabled: disabled || !dirty || status?.kind === 'busy', onClick: save,
|
|
3914
|
+
}, 'Save'),
|
|
3915
|
+
status ? h('span', { className: 'mtt-status mtt-status-' + status.kind }, status.text) : null,
|
|
3916
|
+
disabled
|
|
3917
|
+
? h('span', { className: 'mtt-status' }, 'Read-only: connected from a remote browser.')
|
|
3918
|
+
: null),
|
|
3919
|
+
h('p', { className: 'mtt-help' },
|
|
3920
|
+
'Per-model toggles write the exclude list; the allowlist override pins an exact set. Credentials, the '
|
|
3921
|
+
+ 'bridge endpoint, the extension store and the headless MCP listener live in the profile cordis.patch.yml '
|
|
3922
|
+
+ 'row (id "model-tools"), where the launcher and doctor read them.'))
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
function apply(ctx) {
|
|
3926
|
+
// The extension Remote is optional. Mount it in the BACKGROUND (never await
|
|
3927
|
+
// it before registering this section): on a host without it the page must
|
|
3928
|
+
// still render its config-only fallback. The slot's subscription upgrades
|
|
3929
|
+
// the card when the mount settles.
|
|
3930
|
+
const remoteSlot = createRemoteSlot()
|
|
3931
|
+
mountExtensionRemote(ctx, remoteSlot)
|
|
3932
|
+
// The chat cards were INLINED above, so registration cannot depend on a chunk
|
|
3933
|
+
// fetch that the client module service would refuse.
|
|
3934
|
+
toolviews.register(ctx)
|
|
3935
|
+
|
|
3936
|
+
// The Tool Insights page is OPTIONAL: an older host may not declare the root
|
|
3937
|
+
// `main` / `sidebar.panellist` slots, and this plugin's Settings page must keep
|
|
3938
|
+
// working there. Do NOT probe `ctx.slots.spec()` here: this plugin can apply
|
|
3939
|
+
// before ui-sidebar declares its slots, and `inject` is the lazy registration
|
|
3940
|
+
// path that resolves once that declaration exists.
|
|
3941
|
+
const registerPageSlot = (name, factory) => {
|
|
3942
|
+
if (ctx.slots === null || ctx.slots === undefined || typeof ctx.slots.inject !== 'function') return
|
|
3943
|
+
try {
|
|
3944
|
+
ctx.slots.inject(name, factory)
|
|
3945
|
+
} catch {
|
|
3946
|
+
// Older host without this root slot: the Settings page still works.
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
registerPageSlot('main', () => ctx.slots.register({
|
|
3950
|
+
name: 'main',
|
|
3951
|
+
key: 'tool-insights',
|
|
3952
|
+
inject: () => ({
|
|
3953
|
+
// Additive methods only; the page degrades when a host lacks them.
|
|
3954
|
+
call: (method, args) => callModelTools(ctx, remoteSlot, method, args),
|
|
3955
|
+
openSession: id => {
|
|
3956
|
+
const workspace = ctx.get('uiWorkspace')
|
|
3957
|
+
if (workspace !== null && workspace !== undefined
|
|
3958
|
+
&& typeof workspace.openSession === 'function') workspace.openSession(id)
|
|
3959
|
+
},
|
|
3960
|
+
// The Inventory tab owns the detailed extension/model configuration.
|
|
3961
|
+
// Resolve the config form lazily: inject runs at render time, after
|
|
3962
|
+
// apply() has registered configForms.
|
|
3963
|
+
settings: () => {
|
|
3964
|
+
const candidates = NAMESPACES.map(namespace => ctx.configForms.get(namespace))
|
|
3965
|
+
const form = candidates.find(candidate => {
|
|
3966
|
+
const snap = candidate.getSnapshot()
|
|
3967
|
+
return snap.status === 'ready' || snap.value !== undefined
|
|
3968
|
+
}) ?? candidates[0]
|
|
3969
|
+
return h(ModelToolsSettings, { form, ctx, remoteSlot })
|
|
3970
|
+
},
|
|
3971
|
+
}),
|
|
3972
|
+
}, toolInsights.ToolInsightsPage))
|
|
3973
|
+
registerPageSlot('sidebar.panellist', () => ctx.slots.register({
|
|
3974
|
+
name: 'sidebar.panellist',
|
|
3975
|
+
id: 'tool-insights',
|
|
3976
|
+
order: 15,
|
|
3977
|
+
label: 'Tool Insights',
|
|
3978
|
+
}, toolInsights.ToolInsightsIcon))
|
|
3979
|
+
|
|
3980
|
+
const forms = NAMESPACES.map(namespace => ctx.configForms.get(namespace))
|
|
3981
|
+
const served = () => forms.find(candidate => {
|
|
3982
|
+
const snap = candidate.getSnapshot()
|
|
3983
|
+
return snap.status === 'ready' || snap.value !== undefined
|
|
3984
|
+
})
|
|
3985
|
+
// Fall back to the first candidate so the page still renders its explanatory copy.
|
|
3986
|
+
const form = {
|
|
3987
|
+
getSnapshot: () => (served() ?? forms[0]).getSnapshot(),
|
|
3988
|
+
subscribe: listener => {
|
|
3989
|
+
const disposers = forms.map(candidate => candidate.subscribe(listener))
|
|
3990
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
3991
|
+
},
|
|
3992
|
+
set: (field, value) => (served() ?? forms[0]).set(field, value),
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
ctx.effect(() => ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
3996
|
+
name: 'settings.section',
|
|
3997
|
+
id: 'model-tools',
|
|
3998
|
+
// After the shipped sections (general 0, models 10, plugins 15, agent-presets 20).
|
|
3999
|
+
order: 65,
|
|
4000
|
+
label: 'Model as Tool',
|
|
4001
|
+
}, () => h(ModelToolsSettings, { form, ctx, remoteSlot }))))
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
exports.name = 'model-tools'
|
|
4005
|
+
// 'remote' is deliberately NOT injected: the mount above is optional and
|
|
4006
|
+
// reaches the namespace through a guarded context read, so a host that does
|
|
4007
|
+
// not mount it can only fall back to the config-only switches. A required
|
|
4008
|
+
// 'remote' / 'remote.modelTools' would park this Settings page instead.
|
|
4009
|
+
exports.inject = ['slots', 'configForms']
|
|
4010
|
+
exports.apply = apply
|
|
4011
|
+
exports.__internals = {
|
|
4012
|
+
NAMESPACES,
|
|
4013
|
+
FIELDS,
|
|
4014
|
+
CATALOG,
|
|
4015
|
+
fieldValue,
|
|
4016
|
+
fieldPatch,
|
|
4017
|
+
createRemoteSlot,
|
|
4018
|
+
modelToolsRemote,
|
|
4019
|
+
extensionInstallMethod,
|
|
4020
|
+
remoteErrorText,
|
|
4021
|
+
unwrapRemoteResult,
|
|
4022
|
+
normalizeDescribe,
|
|
4023
|
+
describeExtensions,
|
|
4024
|
+
findExtensionState,
|
|
4025
|
+
nextDisabledExtensions,
|
|
4026
|
+
extensionRequirements,
|
|
4027
|
+
unmetExtensionRequirements,
|
|
4028
|
+
extensionRequirementSummary,
|
|
4029
|
+
requirementHint,
|
|
4030
|
+
extensionRequirementControls,
|
|
4031
|
+
extensionNeedsPreparation,
|
|
4032
|
+
prepareResultNote,
|
|
4033
|
+
installResultNote,
|
|
4034
|
+
callModelTools,
|
|
4035
|
+
toolInsights,
|
|
4036
|
+
}
|
|
4037
|
+
return module.exports
|
|
4038
|
+
},
|
|
4039
|
+
})
|