@try-works/dsh-model-as-tool 0.1.2 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dsh-tool-mode-credentials.mjs +20 -0
- package/bin/dsh-tool-mode.mjs +202 -0
- package/catalog/desert-ant-models.json +693 -693
- package/client/client.model-tools.js +592 -0
- package/client/index.js +1679 -0
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +4 -4
- package/lib/bindings.d.ts.map +1 -1
- package/lib/bindings.js +26 -25
- package/lib/capability.d.ts.map +1 -1
- package/lib/capability.js +17 -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/diagnostics.d.ts +166 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +135 -0
- package/lib/extension-dependencies.d.ts +197 -0
- package/lib/extension-dependencies.d.ts.map +1 -0
- package/lib/extension-dependencies.js +369 -0
- package/lib/extension-dispatch.d.ts +117 -0
- package/lib/extension-dispatch.d.ts.map +1 -0
- package/lib/extension-dispatch.js +124 -0
- package/lib/extension-registrar.d.ts +100 -0
- package/lib/extension-registrar.d.ts.map +1 -0
- package/lib/extension-registrar.js +134 -0
- package/lib/extensions-api.d.ts +217 -0
- package/lib/extensions-api.d.ts.map +1 -0
- package/lib/extensions-api.js +17 -0
- package/lib/extensions-remote.d.ts +54 -0
- package/lib/extensions-remote.d.ts.map +1 -0
- package/lib/extensions-remote.js +66 -0
- package/lib/extensions-service.d.ts +334 -0
- package/lib/extensions-service.d.ts.map +1 -0
- package/lib/extensions-service.js +1351 -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 +95 -0
- package/lib/harness-serving.d.ts.map +1 -0
- package/lib/harness-serving.js +157 -0
- package/lib/headless.d.ts +97 -0
- package/lib/headless.d.ts.map +1 -0
- package/lib/headless.js +349 -0
- package/lib/http-acquisition.d.ts +27 -0
- package/lib/http-acquisition.d.ts.map +1 -0
- package/lib/http-acquisition.js +136 -0
- package/lib/index.d.ts +250 -7
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2625 -70
- 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/legacy-migration.d.ts +95 -0
- package/lib/legacy-migration.d.ts.map +1 -0
- package/lib/legacy-migration.js +429 -0
- package/lib/managed-workers.d.ts +151 -0
- package/lib/managed-workers.d.ts.map +1 -0
- package/lib/managed-workers.js +249 -0
- package/lib/mcp/client-grant.d.ts +74 -0
- package/lib/mcp/client-grant.d.ts.map +1 -0
- package/lib/mcp/client-grant.js +60 -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 +454 -0
- package/lib/mcp/host-binding.d.ts +25 -0
- package/lib/mcp/host-binding.d.ts.map +1 -1
- package/lib/mcp/host-binding.js +107 -14
- package/lib/mcp/http.d.ts +68 -18
- package/lib/mcp/http.d.ts.map +1 -1
- package/lib/mcp/http.js +390 -62
- 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 +46 -7
- package/lib/mcp/protocol.d.ts.map +1 -1
- package/lib/mcp/protocol.js +63 -16
- 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 +181 -0
- package/lib/mcp/server.d.ts +11 -1
- package/lib/mcp/server.d.ts.map +1 -1
- package/lib/mcp/server.js +80 -13
- package/lib/mcp/stdio.d.ts.map +1 -1
- package/lib/mcp/stdio.js +9 -1
- package/lib/mcp/subscriptions.d.ts +43 -1
- package/lib/mcp/subscriptions.d.ts.map +1 -1
- package/lib/mcp/subscriptions.js +90 -9
- 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 +246 -0
- package/lib/planning.d.ts.map +1 -0
- package/lib/planning.js +457 -0
- package/lib/prepared-assets.d.ts +178 -0
- package/lib/prepared-assets.d.ts.map +1 -0
- package/lib/prepared-assets.js +286 -0
- package/lib/provider-bridge.d.ts +37 -0
- package/lib/provider-bridge.d.ts.map +1 -1
- package/lib/provider-bridge.js +189 -12
- package/lib/ptc-authority.d.ts +140 -0
- package/lib/ptc-authority.d.ts.map +1 -0
- package/lib/ptc-authority.js +216 -0
- package/lib/ptc-runtime.d.ts +156 -0
- package/lib/ptc-runtime.d.ts.map +1 -0
- package/lib/ptc-runtime.js +191 -0
- package/lib/publication.d.ts +2 -1
- package/lib/publication.d.ts.map +1 -1
- package/lib/publication.js +13 -5
- package/lib/receipt.d.ts.map +1 -1
- package/lib/receipt.js +8 -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-mode-run.d.ts +235 -0
- package/lib/tool-mode-run.d.ts.map +1 -0
- package/lib/tool-mode-run.js +283 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- 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/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 +85 -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 +48 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -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 +55 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +107 -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 +34 -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 +83 -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 +125 -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 +31 -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 +31 -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 +185 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -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 +297 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -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 +800 -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 +235 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -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 +807 -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/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 +118 -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/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 +150 -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 +670 -0
- package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -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 +427 -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 +592 -0
- package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -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 +936 -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 +200 -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 +105 -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 +129 -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 +325 -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 +1310 -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 +227 -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 +407 -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 +50 -17
- package/remote/spec.mjs +439 -0
- package/remote/typert.host.mjs +14 -0
- package/remote/typert.remote-client.mjs +16 -0
- package/lib/adapters/tongue.d.ts +0 -31
- package/lib/adapters/tongue.d.ts.map +0 -1
- package/lib/adapters/tongue.js +0 -93
- package/lib/adapters/voz.d.ts +0 -28
- package/lib/adapters/voz.d.ts.map +0 -1
- package/lib/adapters/voz.js +0 -102
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
- /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
package/client/index.js
ADDED
|
@@ -0,0 +1,1679 @@
|
|
|
1
|
+
// dsh-model-as-tool 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-model-as-tool',
|
|
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
|
+
/**
|
|
28
|
+
* The catalog, embedded at build time by scripts/gen-client-catalog.mjs.
|
|
29
|
+
*
|
|
30
|
+
* Each entry: { id, name, summary, package, capability, sizeMB, license,
|
|
31
|
+
* runnable, reason }. `runnable` is this host's capability verdict, so the
|
|
32
|
+
* page can only offer toggles for models that can actually run and can say
|
|
33
|
+
* why the rest cannot.
|
|
34
|
+
*/
|
|
35
|
+
const CATALOG = /*__CATALOG_JSON__*/
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
"id": "align",
|
|
39
|
+
"name": "Align",
|
|
40
|
+
"summary": "Word-timestamp refinement for any transcript, on device, from 0.7MB.",
|
|
41
|
+
"package": "@desert-ant-labs/align",
|
|
42
|
+
"extension": null,
|
|
43
|
+
"capability": "audio.word-alignment",
|
|
44
|
+
"sizeMB": 0.7,
|
|
45
|
+
"license": "source-available",
|
|
46
|
+
"runnable": false,
|
|
47
|
+
"reason": "Align has no prebuilt native build for win32-x64: @desert-ant-labs/align/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "clear",
|
|
51
|
+
"name": "Clear",
|
|
52
|
+
"summary": "On-device speech enhancement: denoise, dereverb and loudness-normalize to 48 kHz.",
|
|
53
|
+
"package": "@desert-ant-labs/clear",
|
|
54
|
+
"extension": null,
|
|
55
|
+
"capability": "audio.speech-enhancement",
|
|
56
|
+
"sizeMB": null,
|
|
57
|
+
"license": "source-available",
|
|
58
|
+
"runnable": false,
|
|
59
|
+
"reason": "Clear has no prebuilt native build for win32-x64: @desert-ant-labs/clear/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "clips",
|
|
63
|
+
"name": "Clips",
|
|
64
|
+
"summary": "Clip selection that turns a long recording into ranked shorts and highlights from a transcript.",
|
|
65
|
+
"package": null,
|
|
66
|
+
"extension": null,
|
|
67
|
+
"capability": "video.clip-selection",
|
|
68
|
+
"sizeMB": null,
|
|
69
|
+
"license": "source-available",
|
|
70
|
+
"runnable": false,
|
|
71
|
+
"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`."
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "ear",
|
|
75
|
+
"name": "Ear",
|
|
76
|
+
"summary": "On-device spoken language identification across 102 languages from a short audio stretch.",
|
|
77
|
+
"package": "@desert-ant-labs/ear",
|
|
78
|
+
"extension": null,
|
|
79
|
+
"capability": "audio.spoken-language-identification",
|
|
80
|
+
"sizeMB": null,
|
|
81
|
+
"license": "source-available",
|
|
82
|
+
"runnable": false,
|
|
83
|
+
"reason": "Ear has no prebuilt native build for win32-x64: @desert-ant-labs/ear/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "emo",
|
|
87
|
+
"name": "Emo",
|
|
88
|
+
"summary": "Type a task or message and get the emoji that fit, in 22 languages, on the device.",
|
|
89
|
+
"package": "@desert-ant-labs/emo",
|
|
90
|
+
"extension": null,
|
|
91
|
+
"capability": "text.emoji-suggestion",
|
|
92
|
+
"sizeMB": 10.2,
|
|
93
|
+
"license": "source-available",
|
|
94
|
+
"runnable": false,
|
|
95
|
+
"reason": "Emo has no prebuilt native build for win32-x64: @desert-ant-labs/emo/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"id": "eye",
|
|
99
|
+
"name": "Eye",
|
|
100
|
+
"summary": "On-device frame scoring that tells you which shot to keep, what is in it, and which shots repeat.",
|
|
101
|
+
"package": null,
|
|
102
|
+
"extension": null,
|
|
103
|
+
"capability": "image.frame-scoring",
|
|
104
|
+
"sizeMB": null,
|
|
105
|
+
"license": "source-available",
|
|
106
|
+
"runnable": false,
|
|
107
|
+
"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."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "face",
|
|
111
|
+
"name": "Face",
|
|
112
|
+
"summary": "On-device face matching that finds the same person across a photo library or through a video.",
|
|
113
|
+
"package": null,
|
|
114
|
+
"extension": null,
|
|
115
|
+
"capability": "image.face-matching",
|
|
116
|
+
"sizeMB": null,
|
|
117
|
+
"license": "source-available",
|
|
118
|
+
"runnable": false,
|
|
119
|
+
"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."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "gist",
|
|
123
|
+
"name": "Gist",
|
|
124
|
+
"summary": "On-device topic tagging across a 36-topic taxonomy, in 101 languages, with no server call.",
|
|
125
|
+
"package": "@desert-ant-labs/gist",
|
|
126
|
+
"extension": null,
|
|
127
|
+
"capability": "text.topic-tagging",
|
|
128
|
+
"sizeMB": null,
|
|
129
|
+
"license": "source-available",
|
|
130
|
+
"runnable": false,
|
|
131
|
+
"reason": "Gist has no prebuilt native build for win32-x64: @desert-ant-labs/gist/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"id": "moderator",
|
|
135
|
+
"name": "Moderator",
|
|
136
|
+
"summary": "On-device NSFW detection that flags nudity before upload, trained only on licensed and synthetic data.",
|
|
137
|
+
"package": "@desert-ant-labs/moderator",
|
|
138
|
+
"extension": null,
|
|
139
|
+
"capability": "image.nsfw-detection",
|
|
140
|
+
"sizeMB": 9.2,
|
|
141
|
+
"license": "source-available",
|
|
142
|
+
"runnable": false,
|
|
143
|
+
"reason": "Moderator has no prebuilt native build for win32-x64: @desert-ant-labs/moderator/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"id": "redact",
|
|
147
|
+
"name": "Redact",
|
|
148
|
+
"summary": "On-device redaction that strips personal data as you type, in 27 languages, with restore support.",
|
|
149
|
+
"package": "@desert-ant-labs/redact",
|
|
150
|
+
"extension": null,
|
|
151
|
+
"capability": "text.pii-redaction",
|
|
152
|
+
"sizeMB": null,
|
|
153
|
+
"license": "source-available",
|
|
154
|
+
"runnable": false,
|
|
155
|
+
"reason": "Redact has no prebuilt native build for win32-x64: @desert-ant-labs/redact/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "schemer",
|
|
159
|
+
"name": "Schemer",
|
|
160
|
+
"summary": "Extract typed fields from any text against your own schema, on device, with empty fields left empty.",
|
|
161
|
+
"package": null,
|
|
162
|
+
"extension": null,
|
|
163
|
+
"capability": "text.structured-extraction",
|
|
164
|
+
"sizeMB": null,
|
|
165
|
+
"license": "source-available",
|
|
166
|
+
"runnable": false,
|
|
167
|
+
"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."
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"id": "shapes",
|
|
171
|
+
"name": "Shapes",
|
|
172
|
+
"summary": "On-device shape recognition that turns one hand-drawn stroke into a rectangle, ellipse, triangle, star or line in under 10ms.",
|
|
173
|
+
"package": "@desert-ant-labs/shapes",
|
|
174
|
+
"extension": null,
|
|
175
|
+
"capability": "stroke.shape-recognition",
|
|
176
|
+
"sizeMB": null,
|
|
177
|
+
"license": "source-available",
|
|
178
|
+
"runnable": false,
|
|
179
|
+
"reason": "Shapes has no prebuilt native build for win32-x64: @desert-ant-labs/shapes/native supports linux-x64, linux-arm64 and darwin-arm64 only."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "title",
|
|
183
|
+
"name": "Title",
|
|
184
|
+
"summary": "On-device titles and descriptions: a short factual title and a one- or two-sentence description for any passage.",
|
|
185
|
+
"package": null,
|
|
186
|
+
"extension": null,
|
|
187
|
+
"capability": "text.title-generation",
|
|
188
|
+
"sizeMB": null,
|
|
189
|
+
"license": "source-available",
|
|
190
|
+
"runnable": false,
|
|
191
|
+
"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."
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
"id": "tongue",
|
|
195
|
+
"name": "Tongue",
|
|
196
|
+
"summary": "On-device language identification for short text across 84 languages, from just a few words.",
|
|
197
|
+
"package": "@desert-ant-labs/tongue",
|
|
198
|
+
"extension": "@try-works/extension-desert-ant-tongue",
|
|
199
|
+
"capability": "text.language-identification",
|
|
200
|
+
"sizeMB": 2,
|
|
201
|
+
"license": "source-available",
|
|
202
|
+
"runnable": true
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "toxic",
|
|
206
|
+
"name": "Toxic",
|
|
207
|
+
"summary": "On-device moderation that flags hateful, abusive and threatening text in 23 European languages.",
|
|
208
|
+
"package": null,
|
|
209
|
+
"extension": null,
|
|
210
|
+
"capability": "text.toxicity-detection",
|
|
211
|
+
"sizeMB": null,
|
|
212
|
+
"license": "source-available",
|
|
213
|
+
"runnable": false,
|
|
214
|
+
"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."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"id": "uhm",
|
|
218
|
+
"name": "Uhm",
|
|
219
|
+
"summary": "On-device filler detection that marks every um, uh and hmm to within 20ms, an hour of audio in 12s.",
|
|
220
|
+
"package": null,
|
|
221
|
+
"extension": null,
|
|
222
|
+
"capability": "audio.filler-detection",
|
|
223
|
+
"sizeMB": null,
|
|
224
|
+
"license": "source-available",
|
|
225
|
+
"runnable": false,
|
|
226
|
+
"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."
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"id": "voz",
|
|
230
|
+
"name": "Voz",
|
|
231
|
+
"summary": "Transcribe 10 minutes of audio in 2s on an iPhone, 14x faster than Whisper, with word timestamps.",
|
|
232
|
+
"package": "@desert-ant-labs/voz",
|
|
233
|
+
"extension": "@try-works/extension-desert-ant-voz",
|
|
234
|
+
"capability": "audio.speech-recognition",
|
|
235
|
+
"sizeMB": 349,
|
|
236
|
+
"license": "source-available",
|
|
237
|
+
"runnable": true
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": "who",
|
|
241
|
+
"name": "Who",
|
|
242
|
+
"summary": "On-device speaker labeling that turns a recording into per-person turns with face boxes and timestamps.",
|
|
243
|
+
"package": null,
|
|
244
|
+
"extension": null,
|
|
245
|
+
"capability": "audio.speaker-labeling",
|
|
246
|
+
"sizeMB": null,
|
|
247
|
+
"license": "source-available",
|
|
248
|
+
"runnable": false,
|
|
249
|
+
"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."
|
|
250
|
+
}
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The Tool Mode extensions this repository ships, embedded at build time.
|
|
255
|
+
*
|
|
256
|
+
* Each entry: { id, name, package, version, license, summary, models, tools,
|
|
257
|
+
* contracts }. The host's extension store is the authority for
|
|
258
|
+
* installed/active state, and it is reachable as `ctx.remote.modelTools`
|
|
259
|
+
* when this client assembly mounts that Remote. When it does not, the page
|
|
260
|
+
* degrades to the config-only `disabledExtensions` preference and says so:
|
|
261
|
+
* it never invents an installed state.
|
|
262
|
+
*/
|
|
263
|
+
const EXTENSIONS = /*__EXTENSIONS_JSON__*/
|
|
264
|
+
[
|
|
265
|
+
{
|
|
266
|
+
"id": "@try-works/extension-declarative-recipes",
|
|
267
|
+
"name": "declarative recipes",
|
|
268
|
+
"package": "@try-works/extension-declarative-recipes",
|
|
269
|
+
"version": "0.1.0",
|
|
270
|
+
"license": "MIT",
|
|
271
|
+
"summary": "Ships NO CODE.",
|
|
272
|
+
"models": [],
|
|
273
|
+
"tools": [],
|
|
274
|
+
"contracts": [
|
|
275
|
+
"text.title",
|
|
276
|
+
"image.describe"
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"id": "@try-works/extension-embeddinggemma",
|
|
281
|
+
"name": "EmbeddingGemma 2",
|
|
282
|
+
"package": "@try-works/extension-embeddinggemma",
|
|
283
|
+
"version": "0.1.0",
|
|
284
|
+
"license": "MIT",
|
|
285
|
+
"summary": "Multimodal embedding and retrieval tools over EmbeddingGemma 2 (google/embeddinggemma-2 through a pinned ONNX conversion): multimodal_embed produces 768-, 51…",
|
|
286
|
+
"models": [
|
|
287
|
+
"embeddinggemma-2"
|
|
288
|
+
],
|
|
289
|
+
"tools": [
|
|
290
|
+
"multimodal_embed",
|
|
291
|
+
"semantic_similarity",
|
|
292
|
+
"semantic_rank",
|
|
293
|
+
"semantic_classify",
|
|
294
|
+
"semantic_deduplicate",
|
|
295
|
+
"media_find_moments"
|
|
296
|
+
],
|
|
297
|
+
"contracts": [
|
|
298
|
+
"@try-works/extension-embeddinggemma/multimodal_embed",
|
|
299
|
+
"@try-works/extension-embeddinggemma/semantic_similarity",
|
|
300
|
+
"@try-works/extension-embeddinggemma/semantic_rank",
|
|
301
|
+
"@try-works/extension-embeddinggemma/semantic_classify",
|
|
302
|
+
"@try-works/extension-embeddinggemma/semantic_deduplicate",
|
|
303
|
+
"@try-works/extension-embeddinggemma/media_find_moments"
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
"id": "@try-works/extension-laya",
|
|
308
|
+
"name": "Laya",
|
|
309
|
+
"package": "@try-works/extension-laya",
|
|
310
|
+
"version": "1.0.0",
|
|
311
|
+
"license": "Apache-2.0",
|
|
312
|
+
"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…",
|
|
313
|
+
"models": [
|
|
314
|
+
"laya"
|
|
315
|
+
],
|
|
316
|
+
"tools": [
|
|
317
|
+
"decision_choose",
|
|
318
|
+
"decision_test",
|
|
319
|
+
"decision_score"
|
|
320
|
+
],
|
|
321
|
+
"contracts": [
|
|
322
|
+
"@try-works/extension-laya/decision_choose",
|
|
323
|
+
"@try-works/extension-laya/decision_test",
|
|
324
|
+
"@try-works/extension-laya/decision_score"
|
|
325
|
+
]
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"id": "@try-works/extension-minilm-rerank",
|
|
329
|
+
"name": "MiniLM Rerank",
|
|
330
|
+
"package": "@try-works/extension-minilm-rerank",
|
|
331
|
+
"version": "1.0.0",
|
|
332
|
+
"license": "Apache-2.0",
|
|
333
|
+
"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…",
|
|
334
|
+
"models": [
|
|
335
|
+
"minilm-rerank"
|
|
336
|
+
],
|
|
337
|
+
"tools": [],
|
|
338
|
+
"contracts": [
|
|
339
|
+
"text.rerank"
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"id": "@try-works/extension-minilm-embed",
|
|
344
|
+
"name": "MiniLM-L6",
|
|
345
|
+
"package": "@try-works/extension-minilm-embed",
|
|
346
|
+
"version": "1.0.0",
|
|
347
|
+
"license": "Apache-2.0",
|
|
348
|
+
"summary": "The legacy 384-dimensional MiniLM-L6 text embedding space (TOOL-04, BASE-12): mean pooling, L2 normalization, input order preserved.",
|
|
349
|
+
"models": [
|
|
350
|
+
"minilm-l6"
|
|
351
|
+
],
|
|
352
|
+
"tools": [],
|
|
353
|
+
"contracts": [
|
|
354
|
+
"text.embed"
|
|
355
|
+
]
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"id": "@try-works/extension-needle",
|
|
359
|
+
"name": "Needle 3",
|
|
360
|
+
"package": "@try-works/extension-needle",
|
|
361
|
+
"version": "0.1.0",
|
|
362
|
+
"license": "MIT",
|
|
363
|
+
"summary": "Tool proposal and structured extraction over the pinned Cactus Needle 3 native engine (Cactus-Compute/needle3, windows-x86_64/needle.exe at revision 93e88ede…",
|
|
364
|
+
"models": [
|
|
365
|
+
"needle3"
|
|
366
|
+
],
|
|
367
|
+
"tools": [
|
|
368
|
+
"tools_plan",
|
|
369
|
+
"structured_extract"
|
|
370
|
+
],
|
|
371
|
+
"contracts": [
|
|
372
|
+
"@try-works/extension-needle/tools_plan",
|
|
373
|
+
"@try-works/extension-needle/structured_extract"
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
/** Page styles, prefixed to avoid colliding with host styles. */
|
|
379
|
+
const CSS = `
|
|
380
|
+
.mtt-page {
|
|
381
|
+
display: flex;
|
|
382
|
+
flex-direction: column;
|
|
383
|
+
gap: 12px;
|
|
384
|
+
max-width: 720px;
|
|
385
|
+
color: var(--dsw-alias-label-primary);
|
|
386
|
+
}
|
|
387
|
+
.mtt-title {
|
|
388
|
+
margin: 0;
|
|
389
|
+
font-size: 16px;
|
|
390
|
+
line-height: 24px;
|
|
391
|
+
font-weight: 500;
|
|
392
|
+
color: var(--dsw-alias-label-primary);
|
|
393
|
+
}
|
|
394
|
+
.mtt-intro {
|
|
395
|
+
margin: 0;
|
|
396
|
+
font-size: 14px;
|
|
397
|
+
line-height: 22px;
|
|
398
|
+
color: var(--dsw-alias-label-tertiary);
|
|
399
|
+
}
|
|
400
|
+
.mtt-card {
|
|
401
|
+
border: 0.5px solid var(--dsw-alias-settings-card-stroke);
|
|
402
|
+
background: var(--dsw-alias-settings-card-fill);
|
|
403
|
+
border-radius: var(--dsw-radius-xl);
|
|
404
|
+
padding: 12px 14px;
|
|
405
|
+
display: flex;
|
|
406
|
+
flex-direction: column;
|
|
407
|
+
gap: 2px;
|
|
408
|
+
}
|
|
409
|
+
.mtt-row {
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: flex-start;
|
|
412
|
+
justify-content: space-between;
|
|
413
|
+
gap: 24px;
|
|
414
|
+
padding: 12px 0;
|
|
415
|
+
border-top: 0.5px solid var(--dsw-alias-settings-card-stroke);
|
|
416
|
+
}
|
|
417
|
+
.mtt-row:first-of-type { border-top: none; }
|
|
418
|
+
.mtt-row-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
419
|
+
.mtt-label {
|
|
420
|
+
font-size: 13px;
|
|
421
|
+
line-height: 20px;
|
|
422
|
+
color: var(--dsw-alias-label-primary);
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: 8px;
|
|
426
|
+
}
|
|
427
|
+
.mtt-name { font-weight: 500; }
|
|
428
|
+
.mtt-meta {
|
|
429
|
+
font-family: var(--ds-font-family-code);
|
|
430
|
+
font-size: 11px;
|
|
431
|
+
line-height: 16px;
|
|
432
|
+
color: var(--dsw-alias-label-tertiary);
|
|
433
|
+
word-break: break-all;
|
|
434
|
+
}
|
|
435
|
+
.mtt-help {
|
|
436
|
+
margin: 0;
|
|
437
|
+
font-size: 12px;
|
|
438
|
+
line-height: 18px;
|
|
439
|
+
color: var(--dsw-alias-label-tertiary);
|
|
440
|
+
}
|
|
441
|
+
.mtt-control { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; }
|
|
442
|
+
.mtt-input {
|
|
443
|
+
box-sizing: border-box;
|
|
444
|
+
width: 300px;
|
|
445
|
+
height: 32px;
|
|
446
|
+
padding: 0 10px;
|
|
447
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
448
|
+
border-radius: var(--dsw-radius-sm);
|
|
449
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
450
|
+
color: var(--dsw-alias-label-primary);
|
|
451
|
+
font-family: var(--ds-font-family-code);
|
|
452
|
+
font-size: 12px;
|
|
453
|
+
}
|
|
454
|
+
.mtt-input:disabled { opacity: 0.6; }
|
|
455
|
+
.mtt-toggle {
|
|
456
|
+
width: 40px;
|
|
457
|
+
height: 22px;
|
|
458
|
+
border-radius: 999px;
|
|
459
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
460
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
461
|
+
position: relative;
|
|
462
|
+
cursor: pointer;
|
|
463
|
+
padding: 0;
|
|
464
|
+
flex: 0 0 auto;
|
|
465
|
+
}
|
|
466
|
+
.mtt-toggle[data-on='true'] { background: var(--dsw-alias-button-primary-fill); border-color: transparent; }
|
|
467
|
+
.mtt-toggle:disabled {
|
|
468
|
+
cursor: not-allowed;
|
|
469
|
+
/* A disabled control must still READ as a control. The previous opacity-only
|
|
470
|
+
treatment left it white on the card fill, which users read as an empty
|
|
471
|
+
placeholder rather than an unavailable switch. */
|
|
472
|
+
opacity: 1;
|
|
473
|
+
background: var(--dsw-alias-bg-mask-2);
|
|
474
|
+
border-color: transparent;
|
|
475
|
+
}
|
|
476
|
+
.mtt-toggle:disabled .mtt-knob {
|
|
477
|
+
background: var(--dsw-alias-label-tertiary);
|
|
478
|
+
opacity: 0.9;
|
|
479
|
+
}
|
|
480
|
+
.mtt-knob {
|
|
481
|
+
position: absolute;
|
|
482
|
+
top: 2px;
|
|
483
|
+
left: 2px;
|
|
484
|
+
width: 16px;
|
|
485
|
+
height: 16px;
|
|
486
|
+
border-radius: 50%;
|
|
487
|
+
background: var(--dsw-alias-label-primary-inverted);
|
|
488
|
+
transition: transform 120ms ease;
|
|
489
|
+
}
|
|
490
|
+
.mtt-toggle[data-on='true'] .mtt-knob { transform: translateX(18px); }
|
|
491
|
+
.mtt-badge {
|
|
492
|
+
font-size: 11px;
|
|
493
|
+
line-height: 16px;
|
|
494
|
+
padding: 1px 6px;
|
|
495
|
+
border-radius: 999px;
|
|
496
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
497
|
+
color: var(--dsw-alias-label-tertiary);
|
|
498
|
+
}
|
|
499
|
+
.mtt-badge-on { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-tertiary); }
|
|
500
|
+
.mtt-row-note {
|
|
501
|
+
margin: 0;
|
|
502
|
+
font-size: 12px;
|
|
503
|
+
line-height: 18px;
|
|
504
|
+
color: var(--dsw-alias-label-tertiary);
|
|
505
|
+
}
|
|
506
|
+
.mtt-row-error {
|
|
507
|
+
margin: 0;
|
|
508
|
+
font-size: 12px;
|
|
509
|
+
line-height: 18px;
|
|
510
|
+
color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-primary));
|
|
511
|
+
}
|
|
512
|
+
.mtt-row-blocker {
|
|
513
|
+
margin: 0;
|
|
514
|
+
font-size: 12px;
|
|
515
|
+
line-height: 18px;
|
|
516
|
+
font-weight: 500;
|
|
517
|
+
color: var(--dsw-alias-state-error-primary, var(--dsw-alias-label-primary));
|
|
518
|
+
}
|
|
519
|
+
.mtt-actions {
|
|
520
|
+
display: flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
gap: 12px;
|
|
523
|
+
padding-top: 8px;
|
|
524
|
+
}
|
|
525
|
+
.mtt-button {
|
|
526
|
+
height: 32px;
|
|
527
|
+
padding: 0 14px;
|
|
528
|
+
border: none;
|
|
529
|
+
border-radius: var(--dsw-radius-sm);
|
|
530
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
531
|
+
color: var(--dsw-alias-label-primary-inverted);
|
|
532
|
+
font-family: inherit;
|
|
533
|
+
font-size: 13px;
|
|
534
|
+
cursor: pointer;
|
|
535
|
+
}
|
|
536
|
+
.mtt-button:disabled { opacity: 0.5; cursor: default; }
|
|
537
|
+
.mtt-button-small {
|
|
538
|
+
height: 28px;
|
|
539
|
+
padding: 0 12px;
|
|
540
|
+
font-size: 12px;
|
|
541
|
+
}
|
|
542
|
+
.mtt-status { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
543
|
+
.mtt-status-ok { color: var(--dsw-alias-label-secondary); }
|
|
544
|
+
.mtt-status-error { color: var(--dsw-alias-label-primary); }
|
|
545
|
+
`
|
|
546
|
+
|
|
547
|
+
// The settings namespace is the plugin's ENTRY ID (the bundle patch row id);
|
|
548
|
+
// a hand-written row may use the package name, so both are tried in order.
|
|
549
|
+
const NAMESPACES = ['model-tools', '@try-works/dsh-model-as-tool']
|
|
550
|
+
|
|
551
|
+
/** Global fields (per-model toggles are rendered from CATALOG). */
|
|
552
|
+
const FIELDS = [
|
|
553
|
+
{
|
|
554
|
+
key: 'exposeUnavailable',
|
|
555
|
+
kind: 'toggle',
|
|
556
|
+
label: 'Expose unavailable models',
|
|
557
|
+
help: 'Register a tool even when its runtime is missing, so a model turn can discover why it cannot run.',
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
key: 'registerSkills',
|
|
561
|
+
kind: 'toggle',
|
|
562
|
+
label: 'Register skills',
|
|
563
|
+
help: 'Publish one skill per registered tool, teaching when to use it.',
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
key: 'registerControlTool',
|
|
567
|
+
kind: 'toggle',
|
|
568
|
+
label: 'Register the inspection tool',
|
|
569
|
+
help: 'Expose the model_tools inspection surface to authorised native callers.',
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
key: 'allowNpm',
|
|
573
|
+
kind: 'toggle',
|
|
574
|
+
label: 'Allow in-process packages',
|
|
575
|
+
help: 'Resolve model packages from this installation. Turn off to force every call through the CLI or the bridge.',
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
key: 'managedWorkers',
|
|
579
|
+
kind: 'toggle',
|
|
580
|
+
label: 'Managed workers',
|
|
581
|
+
help: 'Run extension entries in a managed child process with a scrubbed environment.',
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
key: 'repairAttempts',
|
|
585
|
+
kind: 'number',
|
|
586
|
+
label: 'Repair attempts',
|
|
587
|
+
help: 'Bounded title-repair budget per call (0-8). Out-of-range values are refused, never clamped.',
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
key: 'approvedRoots',
|
|
591
|
+
kind: 'list',
|
|
592
|
+
label: 'Approved roots',
|
|
593
|
+
help: 'Comma-separated absolute paths file-reading tools may reach. Empty approves nothing.',
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
key: 'models',
|
|
597
|
+
kind: 'list',
|
|
598
|
+
label: 'Allowlist override',
|
|
599
|
+
help: 'Advanced: comma-separated model ids. Empty means every enabled model below; set only to pin an exact set.',
|
|
600
|
+
},
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
/** Read the value a field should display: the draft, else the saved value. */
|
|
604
|
+
function fieldValue(field, value, draft) {
|
|
605
|
+
if (draft[field.key] !== undefined) return draft[field.key]
|
|
606
|
+
const saved = value[field.key]
|
|
607
|
+
if (field.kind === 'list') return Array.isArray(saved) ? saved.join(', ') : ''
|
|
608
|
+
if (field.kind === 'number') return saved === undefined || saved === null ? '' : String(saved)
|
|
609
|
+
return saved === true
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Parse one edited field back into its config representation. */
|
|
613
|
+
function fieldPatch(field, text) {
|
|
614
|
+
if (field.kind === 'list') {
|
|
615
|
+
return text.split(',').map(part => part.trim()).filter(part => part.length > 0)
|
|
616
|
+
}
|
|
617
|
+
if (field.kind === 'number') return Number(text)
|
|
618
|
+
return text
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** One global row: label + help on the left, the control on the right. */
|
|
622
|
+
function fieldRow(field, value, draft, edit, disabled) {
|
|
623
|
+
const current = fieldValue(field, value, draft)
|
|
624
|
+
let control
|
|
625
|
+
if (field.kind === 'toggle') {
|
|
626
|
+
control = h('button', {
|
|
627
|
+
type: 'button',
|
|
628
|
+
className: 'mtt-toggle',
|
|
629
|
+
'data-on': current === true ? 'true' : 'false',
|
|
630
|
+
disabled,
|
|
631
|
+
'aria-pressed': current === true,
|
|
632
|
+
onClick: () => edit(field.key, !(current === true)),
|
|
633
|
+
}, h('span', { className: 'mtt-knob' }))
|
|
634
|
+
} else {
|
|
635
|
+
control = h('input', {
|
|
636
|
+
className: 'mtt-input',
|
|
637
|
+
type: field.kind === 'number' ? 'number' : 'text',
|
|
638
|
+
min: field.kind === 'number' ? 0 : undefined,
|
|
639
|
+
max: field.kind === 'number' ? 8 : undefined,
|
|
640
|
+
value: current,
|
|
641
|
+
disabled,
|
|
642
|
+
onChange: event => edit(field.key, event.target.value),
|
|
643
|
+
})
|
|
644
|
+
}
|
|
645
|
+
return h('div', { className: 'mtt-row', key: field.key },
|
|
646
|
+
h('div', { className: 'mtt-row-text' },
|
|
647
|
+
h('span', { className: 'mtt-label' }, field.label),
|
|
648
|
+
h('p', { className: 'mtt-help' }, field.help)),
|
|
649
|
+
h('div', { className: 'mtt-control' }, control))
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** One model row: identity, package, size and status, with the toggle. */
|
|
653
|
+
function modelRow(entry, enabled, disabled, onToggle) {
|
|
654
|
+
// An extension-backed model is provided by another package; only the HOST
|
|
655
|
+
// can tell whether it is installed, so the row says "extension" instead of
|
|
656
|
+
// promising it runs here.
|
|
657
|
+
const badge = entry.extension !== null && entry.extension !== undefined
|
|
658
|
+
? h('span', { className: 'mtt-badge' }, 'extension')
|
|
659
|
+
: entry.runnable === true
|
|
660
|
+
? h('span', { className: 'mtt-badge mtt-badge-on' }, 'runnable here')
|
|
661
|
+
: h('span', { className: 'mtt-badge' }, 'unavailable here')
|
|
662
|
+
const source = entry.extension ?? (entry.package === null ? 'no package' : entry.package)
|
|
663
|
+
return h('div', { className: 'mtt-row', key: entry.id },
|
|
664
|
+
h('div', { className: 'mtt-row-text' },
|
|
665
|
+
h('span', { className: 'mtt-label' },
|
|
666
|
+
h('span', { className: 'mtt-name' }, entry.name),
|
|
667
|
+
badge),
|
|
668
|
+
h('p', { className: 'mtt-help' }, entry.summary),
|
|
669
|
+
h('span', { className: 'mtt-meta' },
|
|
670
|
+
entry.id,
|
|
671
|
+
' · ',
|
|
672
|
+
source,
|
|
673
|
+
entry.sizeMB === null ? '' : ' · ' + entry.sizeMB + ' MB',
|
|
674
|
+
' · ',
|
|
675
|
+
entry.license,
|
|
676
|
+
entry.extension !== null && entry.extension !== undefined
|
|
677
|
+
? ' · provided by the Tool Mode extension; install and activate it to run'
|
|
678
|
+
: entry.runnable !== true && entry.reason ? ' · ' + entry.reason : '')),
|
|
679
|
+
h('div', { className: 'mtt-control' },
|
|
680
|
+
h('button', {
|
|
681
|
+
type: 'button',
|
|
682
|
+
className: 'mtt-toggle',
|
|
683
|
+
'data-on': enabled ? 'true' : 'false',
|
|
684
|
+
disabled: disabled,
|
|
685
|
+
'aria-pressed': enabled,
|
|
686
|
+
title: enabled ? 'Disable this model' : 'Enable this model',
|
|
687
|
+
onClick: () => onToggle(entry.id, !enabled),
|
|
688
|
+
}, h('span', { className: 'mtt-knob' }))))
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** How an in-flight Install/Enable/Disable is spelled in the card. */
|
|
692
|
+
const EXTENSION_ACTION_LABELS = { install: 'Installing', prepare: 'Preparing', enable: 'Enabling', disable: 'Disabling' }
|
|
693
|
+
|
|
694
|
+
// The extension Remote is a package-local lazy chunk (`client.model-tools.js`)
|
|
695
|
+
// mounted by the wire half: it installs the generated Typert namespace on
|
|
696
|
+
// ctx.remote as `modelTools`. It is optional on purpose — a host without the
|
|
697
|
+
// Remote leaves the page on its config-only fallback.
|
|
698
|
+
const REMOTE_CHUNK = './client.model-tools.js'
|
|
699
|
+
|
|
700
|
+
// The namespace may arrive a moment after the page mounts (the chunk is
|
|
701
|
+
// fetched, then the Remote contribution mounts). The first read retries
|
|
702
|
+
// briefly, and the slot subscription below re-reads whenever the chunk's
|
|
703
|
+
// mount settles, so a slow mount upgrades the card instead of stranding it.
|
|
704
|
+
const REMOTE_RETRY_ATTEMPTS = 3
|
|
705
|
+
const REMOTE_RETRY_DELAY_MS = 400
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* A tiny observable for the optional Remote mount.
|
|
709
|
+
*
|
|
710
|
+
* `apply()` cannot await the chunk before registering the Settings section:
|
|
711
|
+
* on a host without the Remote the page must still render. So the mount runs
|
|
712
|
+
* in the background and publishes its status here; the card subscribes and
|
|
713
|
+
* re-reads when it changes.
|
|
714
|
+
*/
|
|
715
|
+
function createRemoteSlot() {
|
|
716
|
+
const listeners = new Set()
|
|
717
|
+
let status = null
|
|
718
|
+
return {
|
|
719
|
+
get: () => status,
|
|
720
|
+
set: next => {
|
|
721
|
+
if (next === status) return
|
|
722
|
+
status = next
|
|
723
|
+
for (const listener of [...listeners]) {
|
|
724
|
+
try {
|
|
725
|
+
listener()
|
|
726
|
+
} catch (error) {
|
|
727
|
+
// A broken listener must not abort the mount or another listener.
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
subscribe: listener => {
|
|
732
|
+
listeners.add(listener)
|
|
733
|
+
return () => { listeners.delete(listener) }
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Warn without letting a logger failure blank the page. */
|
|
739
|
+
function warnClient(ctx, message) {
|
|
740
|
+
try {
|
|
741
|
+
if (ctx.logger && typeof ctx.logger.warn === 'function') ctx.logger.warn(message)
|
|
742
|
+
else if (typeof console !== 'undefined' && typeof console.warn === 'function') console.warn(message)
|
|
743
|
+
} catch (error) {
|
|
744
|
+
// A warning is diagnostics, never control flow.
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Load the wire chunk and mount the optional `modelTools` namespace.
|
|
750
|
+
*
|
|
751
|
+
* Never throws into `apply`: a missing chunk (older host), a chunk that fails
|
|
752
|
+
* to mount, or a host without `remote` all leave the slot unset, which is the
|
|
753
|
+
* config-only fallback. The disposer is registered through ctx.effect so the
|
|
754
|
+
* namespace unmounts with this plugin.
|
|
755
|
+
*/
|
|
756
|
+
function mountExtensionRemote(ctx, slot) {
|
|
757
|
+
const load = async () => {
|
|
758
|
+
if (typeof require.async !== 'function') return
|
|
759
|
+
const chunk = await require.async(REMOTE_CHUNK)
|
|
760
|
+
if (chunk === null || chunk === undefined || typeof chunk.mount !== 'function') {
|
|
761
|
+
throw new Error('the extension Remote chunk exposes no mount()')
|
|
762
|
+
}
|
|
763
|
+
const mounted = await chunk.mount(ctx)
|
|
764
|
+
if (mounted === null || mounted === undefined) return
|
|
765
|
+
if (typeof mounted.dispose === 'function') {
|
|
766
|
+
ctx.effect(() => () => mounted.dispose(), 'model-tools: extension Remote')
|
|
767
|
+
}
|
|
768
|
+
slot.set(mounted)
|
|
769
|
+
}
|
|
770
|
+
load().catch(error => warnClient(ctx, 'model-tools: extension Remote unavailable: '
|
|
771
|
+
+ ((error && error.message) || String(error))))
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* The extension Remote, when this client has one.
|
|
776
|
+
*
|
|
777
|
+
* The generated namespace is registered as the context service
|
|
778
|
+
* `remote.modelTools`, so a direct service read is tried FIRST: the `remote`
|
|
779
|
+
* service's associated-property lookup would resolve against this plugin's
|
|
780
|
+
* own fiber and throw without an `inject`, while `ctx.get('remote.modelTools')`
|
|
781
|
+
* reads the provided service without that requirement. The mount status is
|
|
782
|
+
* still tried as a fallback (a future chunk could hand back the namespace
|
|
783
|
+
* directly), and the associated `remote.modelTools` property is the last
|
|
784
|
+
* route for an assembly that provides it that way. Every route is guarded so
|
|
785
|
+
* absence is a value, not an exception.
|
|
786
|
+
*
|
|
787
|
+
* @param ctx - the client plugin context.
|
|
788
|
+
* @param remoteStatus - the mounted status published by the wire chunk, or null.
|
|
789
|
+
* @returns the namespace surface, or null when it is not available.
|
|
790
|
+
*/
|
|
791
|
+
function modelToolsRemote(ctx, remoteStatus) {
|
|
792
|
+
const usable = namespace => namespace !== null && namespace !== undefined
|
|
793
|
+
&& typeof namespace.describe === 'function'
|
|
794
|
+
const routes = [() => ctx.get('remote.modelTools')]
|
|
795
|
+
if (remoteStatus !== null && remoteStatus !== undefined) {
|
|
796
|
+
routes.push(() => remoteStatus.remote.modelTools)
|
|
797
|
+
routes.push(() => remoteStatus.modelTools)
|
|
798
|
+
}
|
|
799
|
+
routes.push(() => {
|
|
800
|
+
const remote = ctx.get('remote')
|
|
801
|
+
return remote === null || remote === undefined ? null : remote.modelTools
|
|
802
|
+
})
|
|
803
|
+
for (const route of routes) {
|
|
804
|
+
try {
|
|
805
|
+
const namespace = route()
|
|
806
|
+
if (usable(namespace)) return namespace
|
|
807
|
+
} catch (error) {
|
|
808
|
+
// Try the next route: an undeclared associated namespace throws.
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return null
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* The install operation on the wire namespace.
|
|
816
|
+
*
|
|
817
|
+
* The verb is `installExtension`, not `install`: the harness gateway's
|
|
818
|
+
* RemoteNamespaceService owns a private `install`, and a colliding descriptor
|
|
819
|
+
* is refused at mount. The host-side service keeps the raw name `install`;
|
|
820
|
+
* this helper tolerates either spelling so a wire rename cannot blank the UI.
|
|
821
|
+
*
|
|
822
|
+
* @param namespace - the modelTools namespace.
|
|
823
|
+
* @returns the method to call, or null when the host exposes neither.
|
|
824
|
+
*/
|
|
825
|
+
function extensionInstallMethod(namespace) {
|
|
826
|
+
if (namespace === null || namespace === undefined) return null
|
|
827
|
+
if (typeof namespace.installExtension === 'function') return namespace.installExtension
|
|
828
|
+
if (typeof namespace.install === 'function') return namespace.install
|
|
829
|
+
return null
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* The host's error text, without inventing one.
|
|
834
|
+
*
|
|
835
|
+
* @param error - a RemoteResult error, an Error, or a string.
|
|
836
|
+
* @returns the message to show; never empty.
|
|
837
|
+
*/
|
|
838
|
+
function remoteErrorText(error) {
|
|
839
|
+
if (typeof error === 'string' && error.length > 0) return error
|
|
840
|
+
if (error !== null && typeof error === 'object'
|
|
841
|
+
&& typeof error.message === 'string' && error.message.length > 0) return error.message
|
|
842
|
+
return 'The host did not explain the failure.'
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Read a generated Remote method's answer.
|
|
847
|
+
*
|
|
848
|
+
* A generated Typert Remote method resolves a RemoteResult
|
|
849
|
+
* (`{ ok: true, value }` / `{ ok: false, error }`); the host service itself
|
|
850
|
+
* resolves the plain value. Both shapes are accepted so a hand-authored wire
|
|
851
|
+
* artifact and a generated one behave the same here, and a refusal becomes a
|
|
852
|
+
* thrown error carrying the host's own message.
|
|
853
|
+
*
|
|
854
|
+
* @param result - the method's resolved value.
|
|
855
|
+
* @returns the value.
|
|
856
|
+
*/
|
|
857
|
+
function unwrapRemoteResult(result) {
|
|
858
|
+
if (result !== null && typeof result === 'object' && typeof result.ok === 'boolean') {
|
|
859
|
+
if (result.ok !== true) throw new Error(remoteErrorText(result.error))
|
|
860
|
+
return result.value
|
|
861
|
+
}
|
|
862
|
+
return result
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Narrow an ExtensionsDescribeResult. A malformed answer is an error, not an
|
|
867
|
+
* empty list: rendering "not installed" from a broken state read would be a
|
|
868
|
+
* guess, and a guess about installed state is exactly what this page must not
|
|
869
|
+
* make.
|
|
870
|
+
*
|
|
871
|
+
* @param value - the value describe() resolved.
|
|
872
|
+
* @returns { store, writable, extensions }.
|
|
873
|
+
*/
|
|
874
|
+
function normalizeDescribe(value) {
|
|
875
|
+
if (value === null || typeof value !== 'object') {
|
|
876
|
+
throw new Error('The host returned no extension state.')
|
|
877
|
+
}
|
|
878
|
+
if (!Array.isArray(value.extensions)) {
|
|
879
|
+
throw new Error('The host returned an extension state without its extension list.')
|
|
880
|
+
}
|
|
881
|
+
const extensions = value.extensions.filter(row => row !== null && typeof row === 'object'
|
|
882
|
+
&& typeof row.id === 'string' && row.id.length > 0)
|
|
883
|
+
return {
|
|
884
|
+
store: typeof value.store === 'string' && value.store.length > 0 ? value.store : null,
|
|
885
|
+
writable: value.writable === true,
|
|
886
|
+
extensions,
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/** Read one extension state snapshot, unwrapping the Remote result. */
|
|
891
|
+
async function describeExtensions(namespace) {
|
|
892
|
+
return normalizeDescribe(unwrapRemoteResult(await namespace.describe()))
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Find one shipped extension in a describe() snapshot. The host may key a row
|
|
897
|
+
* by the extension id or by its package name, so both are compared.
|
|
898
|
+
*
|
|
899
|
+
* @param extension - the generated EXTENSIONS entry.
|
|
900
|
+
* @param states - the describe() rows.
|
|
901
|
+
* @returns the matching state, or null when the host did not report it.
|
|
902
|
+
*/
|
|
903
|
+
function findExtensionState(extension, states) {
|
|
904
|
+
for (let index = 0; index < states.length; index += 1) {
|
|
905
|
+
const state = states[index]
|
|
906
|
+
if (state.id === extension.id) return state
|
|
907
|
+
if (typeof extension.package === 'string' && state.id === extension.package) return state
|
|
908
|
+
if (typeof state.package === 'string' && state.package === extension.id) return state
|
|
909
|
+
if (typeof state.package === 'string' && typeof extension.package === 'string'
|
|
910
|
+
&& state.package === extension.package) return state
|
|
911
|
+
}
|
|
912
|
+
return null
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* The Config.disabledExtensions list after one Enable/Disable action.
|
|
917
|
+
*
|
|
918
|
+
* Two durable sources describe "disabled": the SDK lifecycle that
|
|
919
|
+
* enable/disable change, and this config list, which the plugin's restore
|
|
920
|
+
* filter checks on the next load. The Remote UI writes the lifecycle, so it
|
|
921
|
+
* must dual-write this list too — otherwise a restart can undo the user's
|
|
922
|
+
* choice in the other direction (the example that motivated this: Enable
|
|
923
|
+
* succeeds, the list still names the id, and the next restore re-disables it).
|
|
924
|
+
*
|
|
925
|
+
* @param list - the saved disabledExtensions list (any value, tolerated).
|
|
926
|
+
* @param id - the extension id being enabled/disabled.
|
|
927
|
+
* @param action - 'enable' removes the id; anything else ('disable') adds it.
|
|
928
|
+
* @returns the sorted next list, with non-string entries dropped.
|
|
929
|
+
*/
|
|
930
|
+
function nextDisabledExtensions(list, id, action) {
|
|
931
|
+
const next = new Set(Array.isArray(list)
|
|
932
|
+
? list.filter(entry => typeof entry === 'string' && entry.length > 0)
|
|
933
|
+
: [])
|
|
934
|
+
if (action === 'disable') next.add(id)
|
|
935
|
+
else next.delete(id)
|
|
936
|
+
return [...next].sort()
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** The state badge: installed/active are the host's words, not a guess. */
|
|
940
|
+
function extensionBadge(state) {
|
|
941
|
+
if (state === null) return h('span', { className: 'mtt-badge' }, 'not reported')
|
|
942
|
+
if (state.installed !== true) return h('span', { className: 'mtt-badge' }, 'not installed')
|
|
943
|
+
return state.active === true
|
|
944
|
+
? h('span', { className: 'mtt-badge mtt-badge-on' }, 'active')
|
|
945
|
+
: h('span', { className: 'mtt-badge' }, 'inactive')
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/** The parts of one extension row shared by both flows: name, tools, models. */
|
|
949
|
+
function extensionParts(entry) {
|
|
950
|
+
const parts = []
|
|
951
|
+
if (entry.tools.length > 0) parts.push('tools: ' + entry.tools.join(', '))
|
|
952
|
+
if (entry.contracts.length > 0) parts.push('contracts: ' + entry.contracts.join(', '))
|
|
953
|
+
if (entry.models.length > 0) parts.push('models: ' + entry.models.join(', '))
|
|
954
|
+
return parts
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* The requirements a describe() row reports, when the host checked them.
|
|
959
|
+
*
|
|
960
|
+
* A legacy host (older wire/host) has no `requirements` field, and [] then
|
|
961
|
+
* means "the host did not check" — never "everything is satisfied". Malformed
|
|
962
|
+
* entries are dropped rather than rendered as facts.
|
|
963
|
+
*/
|
|
964
|
+
function extensionRequirements(state) {
|
|
965
|
+
if (state === null || !Array.isArray(state.requirements)) return []
|
|
966
|
+
return state.requirements.filter(requirement => requirement !== null && typeof requirement === 'object'
|
|
967
|
+
&& typeof requirement.kind === 'string' && typeof requirement.satisfied === 'boolean')
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/** Unmet requirements, preserving the host's order. */
|
|
971
|
+
function unmetExtensionRequirements(state) {
|
|
972
|
+
return extensionRequirements(state).filter(requirement => requirement.satisfied !== true)
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** A compact disclosure of what the host checked and satisfied. */
|
|
976
|
+
function extensionRequirementSummary(state) {
|
|
977
|
+
const satisfied = extensionRequirements(state).filter(requirement => requirement.satisfied === true)
|
|
978
|
+
if (satisfied.length === 0) return null
|
|
979
|
+
return 'Requirements checked: ' + satisfied.map(requirement => requirement.kind + ' ✓').join(' · ')
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* The one hint an unmet requirement carries, derived from `action` — never
|
|
984
|
+
* from the message text, because the host may reword its reasons. Assets and
|
|
985
|
+
* executable requirements get no hint: the Prepare control is their fix.
|
|
986
|
+
*
|
|
987
|
+
* @param requirement - one unmet ExtensionRequirement.
|
|
988
|
+
* @returns a hint line, or null when the row's controls say it.
|
|
989
|
+
*/
|
|
990
|
+
function requirementHint(requirement) {
|
|
991
|
+
if (requirement.kind === 'assets' || requirement.kind === 'executable') return null
|
|
992
|
+
if (requirement.action === 'config') return 'Configure the deployment to satisfy this requirement.'
|
|
993
|
+
if (requirement.action === 'none') return 'This requirement cannot be fixed from this page.'
|
|
994
|
+
if (requirement.action === 'install') return 'Install the extension to satisfy this requirement.'
|
|
995
|
+
return null
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* What a row's controls may do, keyed ONLY on requirement kind/action/satisfied
|
|
1000
|
+
* and the host's `blocked` flag.
|
|
1001
|
+
*
|
|
1002
|
+
* Absent fields mean a legacy host: no requirements, `blocked` false, actions
|
|
1003
|
+
* enabled exactly as before. Harness-model/platform/integrity/activation
|
|
1004
|
+
* blockers disable Enable; assets/executable disable Enable but keep Prepare
|
|
1005
|
+
* (the fix); store/source disable Install.
|
|
1006
|
+
*
|
|
1007
|
+
* @param state - the describe() row, or null.
|
|
1008
|
+
* @returns { blocked, unmet, enableBlocked, installBlocked, prepareNeeded }.
|
|
1009
|
+
*/
|
|
1010
|
+
function extensionRequirementControls(state) {
|
|
1011
|
+
const installed = state !== null && state.installed === true
|
|
1012
|
+
const blocked = state !== null && state.blocked === true
|
|
1013
|
+
const unmet = unmetExtensionRequirements(state)
|
|
1014
|
+
return {
|
|
1015
|
+
blocked,
|
|
1016
|
+
unmet,
|
|
1017
|
+
enableBlocked: installed && blocked,
|
|
1018
|
+
installBlocked: unmet.some(requirement => requirement.kind === 'store' || requirement.kind === 'source'),
|
|
1019
|
+
prepareNeeded: installed && unmet.some(requirement => requirement.kind === 'assets'
|
|
1020
|
+
|| requirement.kind === 'executable'),
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Whether a describe() row says the extension's model assets must be
|
|
1026
|
+
* prepared before activation.
|
|
1027
|
+
*
|
|
1028
|
+
* On a current host this is decided ONLY by the requirements array (an unmet
|
|
1029
|
+
* assets/executable entry). A legacy host carries no requirements, so its
|
|
1030
|
+
* reason text is the only signal — and it only decides whether the Prepare
|
|
1031
|
+
* control is offered; the host's text is never rewritten.
|
|
1032
|
+
*
|
|
1033
|
+
* @param state - the describe() row, or null.
|
|
1034
|
+
* @returns true when the host names preparation as the blocker.
|
|
1035
|
+
*/
|
|
1036
|
+
function extensionNeedsPreparation(state) {
|
|
1037
|
+
if (state === null || state.installed !== true) return false
|
|
1038
|
+
if (Array.isArray(state.requirements)) return extensionRequirementControls(state).prepareNeeded
|
|
1039
|
+
if (typeof state.reason !== 'string' || state.reason.length === 0) return false
|
|
1040
|
+
const reason = state.reason.toLowerCase()
|
|
1041
|
+
return reason.includes('needs its model assets prepared')
|
|
1042
|
+
|| reason.includes('needs preparation')
|
|
1043
|
+
|| reason.includes('run prepare for this extension')
|
|
1044
|
+
|| reason.includes('then prepare again')
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* What to show after a prepare() call.
|
|
1049
|
+
*
|
|
1050
|
+
* `prepare` reports acquisition failures IN BAND (`ok: true` with
|
|
1051
|
+
* `prepared: false` and a reason), and RES-02 executable approval refusals
|
|
1052
|
+
* arrive the same way, so the refusal text is returned verbatim and never
|
|
1053
|
+
* replaced with a success claim. A successful call renders the disclosure the
|
|
1054
|
+
* host returned (plan bytes/license) rather than a bare "done".
|
|
1055
|
+
*
|
|
1056
|
+
* @param result - the ExtensionPrepareResult.
|
|
1057
|
+
* @returns one operator-facing note.
|
|
1058
|
+
*/
|
|
1059
|
+
function prepareResultNote(result) {
|
|
1060
|
+
if (result === null || result === undefined || typeof result !== 'object') {
|
|
1061
|
+
return 'The host returned no preparation state.'
|
|
1062
|
+
}
|
|
1063
|
+
if (result.prepared !== true) {
|
|
1064
|
+
return typeof result.reason === 'string' && result.reason.length > 0
|
|
1065
|
+
? result.reason
|
|
1066
|
+
: 'The host reported the preparation did not complete.'
|
|
1067
|
+
}
|
|
1068
|
+
const plan = result.plan !== null && typeof result.plan === 'object' ? result.plan : null
|
|
1069
|
+
let text
|
|
1070
|
+
if (plan === null) {
|
|
1071
|
+
text = 'The host reported the model assets prepared.'
|
|
1072
|
+
} else {
|
|
1073
|
+
text = 'Prepared ' + (typeof plan.modelId === 'string' ? plan.modelId : 'the model assets')
|
|
1074
|
+
if (typeof plan.totalBytes === 'number' && typeof plan.storageBytes === 'number') {
|
|
1075
|
+
text += ' — ' + plan.totalBytes + ' bytes download, ' + plan.storageBytes + ' retained'
|
|
1076
|
+
}
|
|
1077
|
+
if (plan.license !== null && typeof plan.license === 'object'
|
|
1078
|
+
&& typeof plan.license.name === 'string' && plan.license.name.length > 0) {
|
|
1079
|
+
text += ' · license: ' + plan.license.name
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (result.activationReady === false) {
|
|
1083
|
+
text += ' (activation still has remaining blockers; see the row reason)'
|
|
1084
|
+
}
|
|
1085
|
+
return text
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/**
|
|
1089
|
+
* The config-only row: the switch records a `disabledExtensions` preference
|
|
1090
|
+
* and cannot observe or change the store. Used when the Remote is absent.
|
|
1091
|
+
*/
|
|
1092
|
+
function extensionFallbackRow(entry, enabled, disabled, onToggle) {
|
|
1093
|
+
const parts = extensionParts(entry)
|
|
1094
|
+
return h('div', { className: 'mtt-row', key: entry.id },
|
|
1095
|
+
h('div', { className: 'mtt-row-text' },
|
|
1096
|
+
h('span', { className: 'mtt-label' },
|
|
1097
|
+
h('span', { className: 'mtt-name' }, entry.name),
|
|
1098
|
+
h('span', { className: 'mtt-badge' }, 'extension')),
|
|
1099
|
+
h('p', { className: 'mtt-help' }, entry.summary),
|
|
1100
|
+
h('span', { className: 'mtt-meta' },
|
|
1101
|
+
entry.id,
|
|
1102
|
+
parts.length > 0 ? ' · ' + parts.join(' · ') : '',
|
|
1103
|
+
' · ',
|
|
1104
|
+
entry.license ?? 'no license')),
|
|
1105
|
+
h('div', { className: 'mtt-control' },
|
|
1106
|
+
h('button', {
|
|
1107
|
+
type: 'button',
|
|
1108
|
+
className: 'mtt-toggle',
|
|
1109
|
+
'data-on': enabled ? 'true' : 'false',
|
|
1110
|
+
disabled: disabled,
|
|
1111
|
+
'aria-pressed': enabled,
|
|
1112
|
+
title: enabled ? 'Disable this extension' : 'Enable this extension',
|
|
1113
|
+
onClick: () => onToggle(entry.id, !enabled),
|
|
1114
|
+
}, h('span', { className: 'mtt-knob' }))))
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* The store-backed row: Install when absent, Enable/Disable when installed.
|
|
1119
|
+
*
|
|
1120
|
+
* Nothing here is optimistic. `state` comes from the last describe(), the
|
|
1121
|
+
* toggle reflects `active` from that snapshot, and a failed action leaves the
|
|
1122
|
+
* host's error text on the row rather than flipping a switch that did not move.
|
|
1123
|
+
*
|
|
1124
|
+
* @param entry - the generated extension entry.
|
|
1125
|
+
* @param state - the describe() row for it, or null when not reported.
|
|
1126
|
+
* @param ui - { writable, canInstall, listed, busy, pending, error, prepareNote, onAction }.
|
|
1127
|
+
*/
|
|
1128
|
+
function extensionRemoteRow(entry, state, ui) {
|
|
1129
|
+
const installed = state !== null && state.installed === true
|
|
1130
|
+
const active = installed && state.active === true
|
|
1131
|
+
const pending = ui.pending !== null && ui.pending.id === entry.id ? ui.pending.action : null
|
|
1132
|
+
const requirementControls = extensionRequirementControls(state)
|
|
1133
|
+
const needsPreparation = extensionNeedsPreparation(state)
|
|
1134
|
+
const parts = extensionParts(entry)
|
|
1135
|
+
if (state !== null && typeof state.version === 'string' && state.version.length > 0) {
|
|
1136
|
+
parts.push('installed v' + state.version)
|
|
1137
|
+
}
|
|
1138
|
+
if (installed && state.disabled === true) parts.push('disabled in config')
|
|
1139
|
+
|
|
1140
|
+
// Note kinds: 'note' is muted disclosure, 'blocker' is an unmet requirement
|
|
1141
|
+
// (prominent), 'error' is the last action's failure. Keeping the kind per
|
|
1142
|
+
// note means the class never depends on an array position.
|
|
1143
|
+
const notes = []
|
|
1144
|
+
const addNote = (text, kind) => {
|
|
1145
|
+
if (typeof text === 'string' && text.length > 0) notes.push({ text, kind: kind ?? 'note' })
|
|
1146
|
+
}
|
|
1147
|
+
const summary = extensionRequirementSummary(state)
|
|
1148
|
+
if (summary !== null) addNote(summary)
|
|
1149
|
+
if (state === null) {
|
|
1150
|
+
addNote('The extension store did not report this package. Installing asks the host for it, and any refusal is shown here.')
|
|
1151
|
+
}
|
|
1152
|
+
// The host's reason is rendered VERBATIM: it is the authoritative
|
|
1153
|
+
// explanation. On a blocked row it is that row's headline, so it is
|
|
1154
|
+
// prominent instead of muted.
|
|
1155
|
+
if (state !== null && typeof state.reason === 'string' && state.reason.length > 0) {
|
|
1156
|
+
addNote(state.reason, requirementControls.blocked ? 'blocker' : 'note')
|
|
1157
|
+
}
|
|
1158
|
+
if (ui.prepareNote !== null && ui.prepareNote !== undefined && ui.prepareNote.id === entry.id) {
|
|
1159
|
+
addNote(ui.prepareNote.text)
|
|
1160
|
+
}
|
|
1161
|
+
// Every unmet requirement gets its exact reason on the row. A requirement
|
|
1162
|
+
// whose reason is the row reason already is not repeated; the hint comes
|
|
1163
|
+
// from `action` only, never from the wording.
|
|
1164
|
+
if (state !== null && Array.isArray(state.requirements)) {
|
|
1165
|
+
for (const requirement of requirementControls.unmet) {
|
|
1166
|
+
if (requirement.reason !== state.reason) addNote(requirement.reason, 'blocker')
|
|
1167
|
+
addNote(requirementHint(requirement))
|
|
1168
|
+
}
|
|
1169
|
+
if (requirementControls.blocked && requirementControls.unmet.length === 0 && state.reason === null) {
|
|
1170
|
+
addNote('The host reports this extension blocked but named no requirement; see the dsh logs.', 'blocker')
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (state !== null && state.integrity === 'changed') {
|
|
1174
|
+
addNote('The installed content changed since it was recorded; reinstall before enabling it.')
|
|
1175
|
+
}
|
|
1176
|
+
// The store and the config list must agree across a restart. Rendering both
|
|
1177
|
+
// is what lets the operator fix a disagreement with one toggle instead of
|
|
1178
|
+
// discovering it after the next restore.
|
|
1179
|
+
if (installed && active && ui.listed) {
|
|
1180
|
+
addNote('The config list still names this extension; it will be disabled on restart.')
|
|
1181
|
+
}
|
|
1182
|
+
if (installed && !active && !ui.listed) {
|
|
1183
|
+
addNote('The config list does not name this extension; it will not block a restore.')
|
|
1184
|
+
}
|
|
1185
|
+
if (!installed && !ui.canInstall) {
|
|
1186
|
+
addNote('This host exposes extension state but not the install action; install it through the extension store.')
|
|
1187
|
+
}
|
|
1188
|
+
if (ui.error !== null && ui.error.id === entry.id) addNote(ui.error.text, 'error')
|
|
1189
|
+
|
|
1190
|
+
const canInstall = ui.canInstall
|
|
1191
|
+
const control = installed
|
|
1192
|
+
? h('button', {
|
|
1193
|
+
type: 'button',
|
|
1194
|
+
className: 'mtt-toggle',
|
|
1195
|
+
'data-on': active ? 'true' : 'false',
|
|
1196
|
+
disabled: ui.busy || !ui.writable || requirementControls.enableBlocked,
|
|
1197
|
+
'aria-pressed': active,
|
|
1198
|
+
'aria-busy': pending !== null,
|
|
1199
|
+
title: requirementControls.enableBlocked
|
|
1200
|
+
? 'Enable is blocked by a host requirement; see the row reason'
|
|
1201
|
+
: active ? 'Disable this extension' : 'Enable this extension',
|
|
1202
|
+
onClick: () => ui.onAction(entry.id, active ? 'disable' : 'enable'),
|
|
1203
|
+
}, h('span', { className: 'mtt-knob' }))
|
|
1204
|
+
: h('button', {
|
|
1205
|
+
type: 'button',
|
|
1206
|
+
className: 'mtt-button mtt-button-small',
|
|
1207
|
+
disabled: ui.busy || !ui.writable || !canInstall || requirementControls.installBlocked,
|
|
1208
|
+
'aria-busy': pending === 'install',
|
|
1209
|
+
title: !canInstall
|
|
1210
|
+
? 'This host does not expose the extension install action'
|
|
1211
|
+
: requirementControls.installBlocked
|
|
1212
|
+
? 'Install is blocked by a host requirement; see the row reason'
|
|
1213
|
+
: state === null ? 'Ask the host to install this extension' : 'Install this extension',
|
|
1214
|
+
onClick: () => ui.onAction(entry.id, 'install'),
|
|
1215
|
+
}, pending === 'install' ? 'Installing…' : 'Install')
|
|
1216
|
+
|
|
1217
|
+
return h('div', { className: 'mtt-row', key: entry.id },
|
|
1218
|
+
h('div', { className: 'mtt-row-text' },
|
|
1219
|
+
h('span', { className: 'mtt-label' },
|
|
1220
|
+
h('span', { className: 'mtt-name' }, entry.name),
|
|
1221
|
+
extensionBadge(state)),
|
|
1222
|
+
h('p', { className: 'mtt-help' }, entry.summary),
|
|
1223
|
+
h('span', { className: 'mtt-meta' },
|
|
1224
|
+
entry.id,
|
|
1225
|
+
parts.length > 0 ? ' · ' + parts.join(' · ') : '',
|
|
1226
|
+
' · ',
|
|
1227
|
+
entry.license ?? 'no license'),
|
|
1228
|
+
notes.map((note, index) => h('p', {
|
|
1229
|
+
className: note.kind === 'error'
|
|
1230
|
+
? 'mtt-row-error'
|
|
1231
|
+
: note.kind === 'blocker' ? 'mtt-row-blocker' : 'mtt-row-note',
|
|
1232
|
+
key: 'note-' + index,
|
|
1233
|
+
}, note.text))),
|
|
1234
|
+
h('div', { className: 'mtt-control' },
|
|
1235
|
+
control,
|
|
1236
|
+
// Preparation is offered only when the HOST names it as the blocker (or
|
|
1237
|
+
// an unmet assets/executable requirement names it); it is not a second
|
|
1238
|
+
// way to force activation. After a successful prepare the row's
|
|
1239
|
+
// requirements change and this control disappears.
|
|
1240
|
+
needsPreparation
|
|
1241
|
+
? h('button', {
|
|
1242
|
+
type: 'button',
|
|
1243
|
+
className: 'mtt-button mtt-button-small',
|
|
1244
|
+
disabled: ui.busy || !ui.writable,
|
|
1245
|
+
'aria-busy': pending === 'prepare',
|
|
1246
|
+
title: 'Prepare the model assets this extension needs before it can be enabled',
|
|
1247
|
+
onClick: () => ui.onAction(entry.id, 'prepare'),
|
|
1248
|
+
}, pending === 'prepare' ? 'Preparing…' : 'Prepare')
|
|
1249
|
+
: null))
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function ModelToolsSettings({ form, ctx, remoteSlot }) {
|
|
1253
|
+
const [snap, setSnap] = React.useState(() => form.getSnapshot())
|
|
1254
|
+
const [draft, setDraft] = React.useState({})
|
|
1255
|
+
const [exclude, setExclude] = React.useState(null)
|
|
1256
|
+
const [disabledExt, setDisabledExt] = React.useState(null)
|
|
1257
|
+
const [status, setStatus] = React.useState(null)
|
|
1258
|
+
// Extension state is the HOST's answer, read from ctx.remote.modelTools.
|
|
1259
|
+
// `kind` is loading | ready | absent | error; it starts loading so the card
|
|
1260
|
+
// never renders a guessed installed/active state before the first read.
|
|
1261
|
+
const [extRemote, setExtRemote] = React.useState({ kind: 'loading' })
|
|
1262
|
+
const [extPending, setExtPending] = React.useState(null)
|
|
1263
|
+
const [extRowError, setExtRowError] = React.useState(null)
|
|
1264
|
+
// Non-fatal: activation succeeded but the config-list dual-write did not.
|
|
1265
|
+
// The action is never rolled back for this; the note tells the operator the
|
|
1266
|
+
// two stores can disagree until the list is saved manually.
|
|
1267
|
+
const [extConfigNote, setExtConfigNote] = React.useState(null)
|
|
1268
|
+
// The last prepare() disclosure/refusal, per row. It is rendered beside the
|
|
1269
|
+
// host's reason so a RES-02 refusal or plan is shown verbatim.
|
|
1270
|
+
const [extPrepareNote, setExtPrepareNote] = React.useState(null)
|
|
1271
|
+
|
|
1272
|
+
/** This mount's namespace status, or null when the wire chunk is absent. */
|
|
1273
|
+
const mounted = () => (remoteSlot === null || remoteSlot === undefined ? null : remoteSlot.get())
|
|
1274
|
+
|
|
1275
|
+
React.useEffect(() => form.subscribe(() => setSnap(form.getSnapshot())), [form])
|
|
1276
|
+
|
|
1277
|
+
// Read the extension state on mount. The namespace may arrive after this
|
|
1278
|
+
// page (the wire chunk is fetched first), so an absent namespace is retried
|
|
1279
|
+
// briefly AND the chunk's slot subscription re-reads when the mount settles.
|
|
1280
|
+
// `alive` keeps a late answer from writing into an unmounted page.
|
|
1281
|
+
React.useEffect(() => {
|
|
1282
|
+
let alive = true
|
|
1283
|
+
let timer = null
|
|
1284
|
+
let attempts = 0
|
|
1285
|
+
const adopt = namespace => {
|
|
1286
|
+
describeExtensions(namespace).then(described => {
|
|
1287
|
+
if (alive) setExtRemote({ kind: 'ready', namespace, ...described })
|
|
1288
|
+
}, error => {
|
|
1289
|
+
if (alive) setExtRemote({ kind: 'error', text: remoteErrorText(error) })
|
|
1290
|
+
})
|
|
1291
|
+
}
|
|
1292
|
+
const read = () => {
|
|
1293
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
1294
|
+
if (namespace !== null) {
|
|
1295
|
+
adopt(namespace)
|
|
1296
|
+
return
|
|
1297
|
+
}
|
|
1298
|
+
if (attempts >= REMOTE_RETRY_ATTEMPTS) {
|
|
1299
|
+
if (alive) setExtRemote({ kind: 'absent' })
|
|
1300
|
+
return
|
|
1301
|
+
}
|
|
1302
|
+
attempts += 1
|
|
1303
|
+
timer = setTimeout(read, REMOTE_RETRY_DELAY_MS)
|
|
1304
|
+
}
|
|
1305
|
+
read()
|
|
1306
|
+
const unsubscribe = remoteSlot !== null && remoteSlot !== undefined
|
|
1307
|
+
&& typeof remoteSlot.subscribe === 'function'
|
|
1308
|
+
? remoteSlot.subscribe(() => {
|
|
1309
|
+
if (!alive) return
|
|
1310
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
1311
|
+
if (namespace !== null) adopt(namespace)
|
|
1312
|
+
})
|
|
1313
|
+
: null
|
|
1314
|
+
return () => {
|
|
1315
|
+
alive = false
|
|
1316
|
+
if (unsubscribe !== null) unsubscribe()
|
|
1317
|
+
if (timer !== null) clearTimeout(timer)
|
|
1318
|
+
}
|
|
1319
|
+
}, [ctx])
|
|
1320
|
+
|
|
1321
|
+
if (snap.status === 'unavailable') {
|
|
1322
|
+
return h('div', { className: 'mtt-page' },
|
|
1323
|
+
h('style', null, CSS),
|
|
1324
|
+
h('h2', { className: 'mtt-title' }, 'Model as Tool'),
|
|
1325
|
+
h('p', { className: 'mtt-intro' },
|
|
1326
|
+
'The model-tools configuration namespace is not exposed to this client. '
|
|
1327
|
+
+ 'Edit the profile cordis.patch.yml entry with id "model-tools" instead.'))
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const value = snap.value || {}
|
|
1331
|
+
const disabled = snap.writable === false
|
|
1332
|
+
const edit = (key, next) => setDraft(previous => {
|
|
1333
|
+
const updated = { ...previous }
|
|
1334
|
+
updated[key] = next
|
|
1335
|
+
return updated
|
|
1336
|
+
})
|
|
1337
|
+
|
|
1338
|
+
const savedExclude = Array.isArray(value.exclude) ? value.exclude : []
|
|
1339
|
+
const currentExclude = exclude ?? savedExclude
|
|
1340
|
+
const allowlist = Array.isArray(value.models) ? value.models : []
|
|
1341
|
+
|
|
1342
|
+
/** Whether a model is exposed: not excluded (and inside the allowlist when one is set). */
|
|
1343
|
+
function isEnabled(id) {
|
|
1344
|
+
if (currentExclude.includes(id)) return false
|
|
1345
|
+
return allowlist.length === 0 || allowlist.includes(id)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const dirty = FIELDS.some(field => draft[field.key] !== undefined) || exclude !== null || disabledExt !== null
|
|
1349
|
+
|
|
1350
|
+
const save = async () => {
|
|
1351
|
+
setStatus({ kind: 'busy', text: 'Saving…' })
|
|
1352
|
+
try {
|
|
1353
|
+
let refused = false
|
|
1354
|
+
for (const field of FIELDS) {
|
|
1355
|
+
if (draft[field.key] === undefined) continue
|
|
1356
|
+
const accepted = await form.set(field.key, fieldPatch(field, draft[field.key]))
|
|
1357
|
+
if (accepted === false) refused = true
|
|
1358
|
+
}
|
|
1359
|
+
if (exclude !== null) {
|
|
1360
|
+
const accepted = await form.set('exclude', exclude)
|
|
1361
|
+
if (accepted === false) refused = true
|
|
1362
|
+
}
|
|
1363
|
+
if (disabledExt !== null) {
|
|
1364
|
+
const accepted = await form.set('disabledExtensions', disabledExt)
|
|
1365
|
+
if (accepted === false) refused = true
|
|
1366
|
+
}
|
|
1367
|
+
setDraft({})
|
|
1368
|
+
setExclude(null)
|
|
1369
|
+
setDisabledExt(null)
|
|
1370
|
+
setStatus(refused
|
|
1371
|
+
? { kind: 'error', text: 'Refused by the host — see the dsh logs.' }
|
|
1372
|
+
: { kind: 'ok', text: 'Saved. Changes apply to new sessions.' })
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
setStatus({ kind: 'error', text: String((error && error.message) || error) })
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const savedDisabledExt = Array.isArray(value.disabledExtensions) ? value.disabledExtensions : []
|
|
1379
|
+
const currentDisabledExt = disabledExt ?? savedDisabledExt
|
|
1380
|
+
const isExtensionEnabled = id => !currentDisabledExt.includes(id)
|
|
1381
|
+
const toggleExtension = (id, on) => setDisabledExt(() => {
|
|
1382
|
+
const next = new Set(currentDisabledExt)
|
|
1383
|
+
if (on) next.delete(id)
|
|
1384
|
+
else next.add(id)
|
|
1385
|
+
return [...next].sort()
|
|
1386
|
+
})
|
|
1387
|
+
|
|
1388
|
+
const toggleModel = (id, on) => setExclude(() => {
|
|
1389
|
+
const next = new Set(currentExclude)
|
|
1390
|
+
if (on) next.delete(id)
|
|
1391
|
+
else next.add(id)
|
|
1392
|
+
return [...next].sort()
|
|
1393
|
+
})
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Best-effort write of Config.disabledExtensions after a lifecycle
|
|
1397
|
+
* enable/disable. Activation is the authority: a refusal here is reported
|
|
1398
|
+
* as a NON-FATAL note and never undoes or hides the activation.
|
|
1399
|
+
*
|
|
1400
|
+
* The list is read from the form's CURRENT snapshot (another tab or the
|
|
1401
|
+
* Save button may have changed it since this render), falling back to the
|
|
1402
|
+
* render's saved value.
|
|
1403
|
+
*/
|
|
1404
|
+
const dualWriteDisabledExtensions = async (id, action) => {
|
|
1405
|
+
const snapshot = form.getSnapshot()
|
|
1406
|
+
const snapshotValue = snapshot && snapshot.value !== null && typeof snapshot.value === 'object'
|
|
1407
|
+
? snapshot.value
|
|
1408
|
+
: {}
|
|
1409
|
+
const saved = Array.isArray(snapshotValue.disabledExtensions)
|
|
1410
|
+
? snapshotValue.disabledExtensions
|
|
1411
|
+
: savedDisabledExt
|
|
1412
|
+
const nextList = nextDisabledExtensions(saved, id, action)
|
|
1413
|
+
const unchanged = Array.isArray(saved) && nextList.length === saved.length
|
|
1414
|
+
&& nextList.every((entry, index) => entry === saved[index])
|
|
1415
|
+
if (unchanged) return
|
|
1416
|
+
const what = action === 'disable' ? 'disabled' : 'enabled'
|
|
1417
|
+
try {
|
|
1418
|
+
const accepted = await form.set('disabledExtensions', nextList)
|
|
1419
|
+
if (accepted === false) {
|
|
1420
|
+
setExtConfigNote('The extension was ' + what + ', but the host refused the disabledExtensions update; '
|
|
1421
|
+
+ 'the config list can disagree with the store until it is saved manually.')
|
|
1422
|
+
}
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
setExtConfigNote('The extension was ' + what + ', but the config list update failed: '
|
|
1425
|
+
+ remoteErrorText(error))
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
/** Read the host state once, after a manual "Try again". */
|
|
1430
|
+
const recheckExtensions = async () => {
|
|
1431
|
+
setExtRowError(null)
|
|
1432
|
+
setExtConfigNote(null)
|
|
1433
|
+
setExtPrepareNote(null)
|
|
1434
|
+
setExtRemote({ kind: 'loading' })
|
|
1435
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
1436
|
+
if (namespace === null) {
|
|
1437
|
+
setExtRemote({ kind: 'absent' })
|
|
1438
|
+
return
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
setExtRemote({ kind: 'ready', namespace, ...(await describeExtensions(namespace)) })
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
setExtRemote({ kind: 'error', text: remoteErrorText(error) })
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Run one extension action, then RE-READ the host state.
|
|
1449
|
+
*
|
|
1450
|
+
* The host is the authority: a successful install is not activation, and
|
|
1451
|
+
* enable/disable may be refused for reasons only describe() can explain.
|
|
1452
|
+
* The re-read is what moves the controls; the action result is never
|
|
1453
|
+
* painted as a state change by itself.
|
|
1454
|
+
*/
|
|
1455
|
+
const runExtensionAction = async (id, action) => {
|
|
1456
|
+
setExtRowError(null)
|
|
1457
|
+
setExtConfigNote(null)
|
|
1458
|
+
setExtPrepareNote(null)
|
|
1459
|
+
setExtPending({ id, action })
|
|
1460
|
+
try {
|
|
1461
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
1462
|
+
if (namespace === null) {
|
|
1463
|
+
setExtRemote({ kind: 'absent' })
|
|
1464
|
+
return
|
|
1465
|
+
}
|
|
1466
|
+
if (action === 'install') {
|
|
1467
|
+
const install = extensionInstallMethod(namespace)
|
|
1468
|
+
if (install === null) throw new Error('This host does not expose the extension install action.')
|
|
1469
|
+
// The wire descriptor is installExtension(id, source): Typert counts
|
|
1470
|
+
// business arguments exactly, and an omitted `source` still occupies
|
|
1471
|
+
// its positional slot. Passing only the id is refused by the gateway
|
|
1472
|
+
// ("expected 2 business argument(s) ... got 1"), so pass undefined to
|
|
1473
|
+
// mean "use the configured first-party source".
|
|
1474
|
+
unwrapRemoteResult(await install.call(namespace, id, undefined))
|
|
1475
|
+
} else if (action === 'prepare') {
|
|
1476
|
+
if (typeof namespace.prepare !== 'function') {
|
|
1477
|
+
throw new Error('This host does not expose the extension prepare action.')
|
|
1478
|
+
}
|
|
1479
|
+
// prepare(id) has no optional data argument; an AbortSignal may ride
|
|
1480
|
+
// as the second wire slot, which this page does not need.
|
|
1481
|
+
const prepared = unwrapRemoteResult(await namespace.prepare(id))
|
|
1482
|
+
setExtPrepareNote({ id, text: prepareResultNote(prepared) })
|
|
1483
|
+
} else if (action === 'enable') {
|
|
1484
|
+
unwrapRemoteResult(await namespace.enable(id))
|
|
1485
|
+
// Dual-write Config.disabledExtensions so a restart cannot re-disable
|
|
1486
|
+
// what the operator just enabled. Best-effort and non-fatal.
|
|
1487
|
+
await dualWriteDisabledExtensions(id, 'enable')
|
|
1488
|
+
} else {
|
|
1489
|
+
unwrapRemoteResult(await namespace.disable(id))
|
|
1490
|
+
await dualWriteDisabledExtensions(id, 'disable')
|
|
1491
|
+
}
|
|
1492
|
+
setExtRemote({ kind: 'ready', namespace, ...(await describeExtensions(namespace)) })
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
setExtRowError({ id, text: remoteErrorText(error) })
|
|
1495
|
+
// The action may have changed host state before failing. Read it back,
|
|
1496
|
+
// but keep the action's error text if the re-read fails as well.
|
|
1497
|
+
try {
|
|
1498
|
+
const namespace = modelToolsRemote(ctx, mounted())
|
|
1499
|
+
if (namespace !== null) {
|
|
1500
|
+
setExtRemote({ kind: 'ready', namespace, ...(await describeExtensions(namespace)) })
|
|
1501
|
+
}
|
|
1502
|
+
} catch (readError) {
|
|
1503
|
+
// The row keeps the host's action error; the state stays the last
|
|
1504
|
+
// snapshot describe() gave us, which is still the last known truth.
|
|
1505
|
+
}
|
|
1506
|
+
} finally {
|
|
1507
|
+
setExtPending(null)
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
const runnable = CATALOG.filter(entry => entry.runnable === true)
|
|
1512
|
+
const unavailable = CATALOG.filter(entry => entry.runnable !== true)
|
|
1513
|
+
|
|
1514
|
+
const extPendingEntry = extPending === null
|
|
1515
|
+
? null
|
|
1516
|
+
: EXTENSIONS.find(entry => entry.id === extPending.id) ?? { name: extPending.id }
|
|
1517
|
+
const extPendingText = extPending === null
|
|
1518
|
+
? null
|
|
1519
|
+
: (EXTENSION_ACTION_LABELS[extPending.action] ?? 'Working on') + ' ' + extPendingEntry.name + '…'
|
|
1520
|
+
const extWritable = extRemote.kind === 'ready' && extRemote.writable === true
|
|
1521
|
+
const extBusy = extPending !== null
|
|
1522
|
+
|
|
1523
|
+
let extensionCard
|
|
1524
|
+
if (extRemote.kind === 'ready') {
|
|
1525
|
+
const notes = []
|
|
1526
|
+
if (extRemote.store === null) {
|
|
1527
|
+
notes.push('No extension store is configured on this host (extensionStore), so an extension cannot be installed or activated here.')
|
|
1528
|
+
} else if (!extRemote.writable) {
|
|
1529
|
+
notes.push('The host reports its extension store as read-only right now; the extension controls are disabled.')
|
|
1530
|
+
}
|
|
1531
|
+
const canInstall = extensionInstallMethod(extRemote.namespace) !== null
|
|
1532
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
1533
|
+
h('p', { className: 'mtt-help' },
|
|
1534
|
+
'Tool Mode extensions shipped with this plugin. The host\'s extension store is the authority: '
|
|
1535
|
+
+ 'Install copies a package into the store, then the switch activates it. Installation is not activation.'),
|
|
1536
|
+
notes.map((note, index) => h('p', { className: 'mtt-row-note', key: 'store-note-' + index }, note)),
|
|
1537
|
+
extPendingText === null ? null : h('p', { className: 'mtt-status' }, extPendingText),
|
|
1538
|
+
extConfigNote === null ? null : h('p', { className: 'mtt-row-note' }, extConfigNote),
|
|
1539
|
+
EXTENSIONS.map(entry => extensionRemoteRow(
|
|
1540
|
+
entry,
|
|
1541
|
+
findExtensionState(entry, extRemote.extensions),
|
|
1542
|
+
{
|
|
1543
|
+
writable: extWritable,
|
|
1544
|
+
canInstall,
|
|
1545
|
+
// The SAVED config list, not the fallback draft: the row describes
|
|
1546
|
+
// what a restart will actually read, and the dual-write compares
|
|
1547
|
+
// against the same saved value.
|
|
1548
|
+
listed: savedDisabledExt.includes(entry.id),
|
|
1549
|
+
busy: extBusy,
|
|
1550
|
+
pending: extPending,
|
|
1551
|
+
error: extRowError,
|
|
1552
|
+
prepareNote: extPrepareNote,
|
|
1553
|
+
onAction: runExtensionAction,
|
|
1554
|
+
})))
|
|
1555
|
+
} else if (extRemote.kind === 'error') {
|
|
1556
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
1557
|
+
h('p', { className: 'mtt-help' },
|
|
1558
|
+
'The host exposes extension controls, but reading their state failed.'),
|
|
1559
|
+
h('p', { className: 'mtt-row-error' }, extRemote.text),
|
|
1560
|
+
h('div', { className: 'mtt-actions' },
|
|
1561
|
+
h('button', {
|
|
1562
|
+
type: 'button',
|
|
1563
|
+
className: 'mtt-button mtt-button-small',
|
|
1564
|
+
onClick: recheckExtensions,
|
|
1565
|
+
}, 'Try again')))
|
|
1566
|
+
} else if (extRemote.kind === 'absent') {
|
|
1567
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
1568
|
+
h('p', { className: 'mtt-help' },
|
|
1569
|
+
'Tool Mode extensions shipped with this plugin. The host\'s extension controls are not available to '
|
|
1570
|
+
+ 'this page (an older host build, or the extension Remote is not mounted), so this page cannot install, '
|
|
1571
|
+
+ 'verify, or observe them.'),
|
|
1572
|
+
h('p', { className: 'mtt-row-note' },
|
|
1573
|
+
'The switches below only record a disabledExtensions preference for the next load: they neither install '
|
|
1574
|
+
+ 'nor verify the package.'),
|
|
1575
|
+
EXTENSIONS.map(entry => extensionFallbackRow(entry, isExtensionEnabled(entry.id), disabled, toggleExtension)),
|
|
1576
|
+
h('div', { className: 'mtt-actions' },
|
|
1577
|
+
h('button', {
|
|
1578
|
+
type: 'button',
|
|
1579
|
+
className: 'mtt-button mtt-button-small',
|
|
1580
|
+
onClick: recheckExtensions,
|
|
1581
|
+
}, 'Recheck')))
|
|
1582
|
+
} else {
|
|
1583
|
+
extensionCard = h('div', { className: 'mtt-card' },
|
|
1584
|
+
h('p', { className: 'mtt-help' }, 'Reading the extension state from the host…'))
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
return h('div', { className: 'mtt-page' },
|
|
1588
|
+
h('style', null, CSS),
|
|
1589
|
+
h('h2', { className: 'mtt-title' }, 'Model as Tool'),
|
|
1590
|
+
h('p', { className: 'mtt-intro' },
|
|
1591
|
+
'On-device models from the Desert Ant catalog, exposed as tools and skills. A model becomes a tool only when this '
|
|
1592
|
+
+ 'process can actually run it; extension-backed models are listed with the extension that provides them.'),
|
|
1593
|
+
h('div', { className: 'mtt-card' },
|
|
1594
|
+
h('p', { className: 'mtt-help' },
|
|
1595
|
+
runnable.length + ' of ' + CATALOG.length + ' catalog models are runnable in this process. '
|
|
1596
|
+
+ 'Unavailable models are listed with the reason they cannot run here.'),
|
|
1597
|
+
runnable.map(entry => modelRow(entry, isEnabled(entry.id), disabled, toggleModel)),
|
|
1598
|
+
unavailable.map(entry => modelRow(entry, false, disabled, toggleModel))),
|
|
1599
|
+
EXTENSIONS.length === 0 ? null : extensionCard,
|
|
1600
|
+
h('div', { className: 'mtt-card' }, FIELDS.map(field => fieldRow(field, value, draft, edit, disabled))),
|
|
1601
|
+
h('div', { className: 'mtt-actions' },
|
|
1602
|
+
h('button', {
|
|
1603
|
+
type: 'button', className: 'mtt-button', disabled: disabled || !dirty || status?.kind === 'busy', onClick: save,
|
|
1604
|
+
}, 'Save'),
|
|
1605
|
+
status ? h('span', { className: 'mtt-status mtt-status-' + status.kind }, status.text) : null,
|
|
1606
|
+
disabled
|
|
1607
|
+
? h('span', { className: 'mtt-status' }, 'Read-only: connected from a remote browser.')
|
|
1608
|
+
: null),
|
|
1609
|
+
h('p', { className: 'mtt-help' },
|
|
1610
|
+
'Per-model toggles write the exclude list; the allowlist override pins an exact set. Credentials, the '
|
|
1611
|
+
+ 'bridge endpoint, the extension store and the headless MCP listener live in the profile cordis.patch.yml '
|
|
1612
|
+
+ 'row (id "model-tools"), where the launcher and doctor read them.'))
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function apply(ctx) {
|
|
1616
|
+
// The extension Remote is optional. Mount it in the BACKGROUND (never await
|
|
1617
|
+
// it before registering this section): on a host without it the page must
|
|
1618
|
+
// still render its config-only fallback. The slot's subscription upgrades
|
|
1619
|
+
// the card when the mount settles.
|
|
1620
|
+
const remoteSlot = createRemoteSlot()
|
|
1621
|
+
mountExtensionRemote(ctx, remoteSlot)
|
|
1622
|
+
|
|
1623
|
+
const forms = NAMESPACES.map(namespace => ctx.configForms.get(namespace))
|
|
1624
|
+
const served = () => forms.find(candidate => {
|
|
1625
|
+
const snap = candidate.getSnapshot()
|
|
1626
|
+
return snap.status === 'ready' || snap.value !== undefined
|
|
1627
|
+
})
|
|
1628
|
+
// Fall back to the first candidate so the page still renders its explanatory copy.
|
|
1629
|
+
const form = {
|
|
1630
|
+
getSnapshot: () => (served() ?? forms[0]).getSnapshot(),
|
|
1631
|
+
subscribe: listener => {
|
|
1632
|
+
const disposers = forms.map(candidate => candidate.subscribe(listener))
|
|
1633
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
1634
|
+
},
|
|
1635
|
+
set: (field, value) => (served() ?? forms[0]).set(field, value),
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
ctx.effect(() => ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
1639
|
+
name: 'settings.section',
|
|
1640
|
+
id: 'model-tools',
|
|
1641
|
+
// After the shipped sections (general 0, models 10, plugins 15, agent-presets 20).
|
|
1642
|
+
order: 65,
|
|
1643
|
+
label: 'Model as Tool',
|
|
1644
|
+
}, () => h(ModelToolsSettings, { form, ctx, remoteSlot }))))
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
exports.name = 'model-tools'
|
|
1648
|
+
// 'remote' is deliberately NOT injected: the mount above is optional and
|
|
1649
|
+
// reaches the namespace through a guarded context read, so a host that does
|
|
1650
|
+
// not mount it can only fall back to the config-only switches. A required
|
|
1651
|
+
// 'remote' / 'remote.modelTools' would park this Settings page instead.
|
|
1652
|
+
exports.inject = ['slots', 'configForms']
|
|
1653
|
+
exports.apply = apply
|
|
1654
|
+
exports.__internals = {
|
|
1655
|
+
NAMESPACES,
|
|
1656
|
+
FIELDS,
|
|
1657
|
+
CATALOG,
|
|
1658
|
+
fieldValue,
|
|
1659
|
+
fieldPatch,
|
|
1660
|
+
createRemoteSlot,
|
|
1661
|
+
modelToolsRemote,
|
|
1662
|
+
extensionInstallMethod,
|
|
1663
|
+
remoteErrorText,
|
|
1664
|
+
unwrapRemoteResult,
|
|
1665
|
+
normalizeDescribe,
|
|
1666
|
+
describeExtensions,
|
|
1667
|
+
findExtensionState,
|
|
1668
|
+
nextDisabledExtensions,
|
|
1669
|
+
extensionRequirements,
|
|
1670
|
+
unmetExtensionRequirements,
|
|
1671
|
+
extensionRequirementSummary,
|
|
1672
|
+
requirementHint,
|
|
1673
|
+
extensionRequirementControls,
|
|
1674
|
+
extensionNeedsPreparation,
|
|
1675
|
+
prepareResultNote,
|
|
1676
|
+
}
|
|
1677
|
+
return module.exports
|
|
1678
|
+
},
|
|
1679
|
+
})
|