@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.
Files changed (267) hide show
  1. package/bin/dsh-tool-mode-credentials.mjs +20 -0
  2. package/bin/dsh-tool-mode.mjs +202 -0
  3. package/catalog/desert-ant-models.json +693 -693
  4. package/client/client.model-tools.js +592 -0
  5. package/client/index.js +1679 -0
  6. package/lib/adapters/index.d.ts.map +1 -1
  7. package/lib/adapters/index.js +4 -4
  8. package/lib/bindings.d.ts.map +1 -1
  9. package/lib/bindings.js +26 -25
  10. package/lib/capability.d.ts.map +1 -1
  11. package/lib/capability.js +17 -0
  12. package/lib/capture-path.d.ts +97 -0
  13. package/lib/capture-path.d.ts.map +1 -0
  14. package/lib/capture-path.js +152 -0
  15. package/lib/diagnostics.d.ts +166 -0
  16. package/lib/diagnostics.d.ts.map +1 -0
  17. package/lib/diagnostics.js +135 -0
  18. package/lib/extension-dependencies.d.ts +197 -0
  19. package/lib/extension-dependencies.d.ts.map +1 -0
  20. package/lib/extension-dependencies.js +369 -0
  21. package/lib/extension-dispatch.d.ts +117 -0
  22. package/lib/extension-dispatch.d.ts.map +1 -0
  23. package/lib/extension-dispatch.js +124 -0
  24. package/lib/extension-registrar.d.ts +100 -0
  25. package/lib/extension-registrar.d.ts.map +1 -0
  26. package/lib/extension-registrar.js +134 -0
  27. package/lib/extensions-api.d.ts +217 -0
  28. package/lib/extensions-api.d.ts.map +1 -0
  29. package/lib/extensions-api.js +17 -0
  30. package/lib/extensions-remote.d.ts +54 -0
  31. package/lib/extensions-remote.d.ts.map +1 -0
  32. package/lib/extensions-remote.js +66 -0
  33. package/lib/extensions-service.d.ts +334 -0
  34. package/lib/extensions-service.d.ts.map +1 -0
  35. package/lib/extensions-service.js +1351 -0
  36. package/lib/harness-llm.d.ts +138 -0
  37. package/lib/harness-llm.d.ts.map +1 -0
  38. package/lib/harness-llm.js +218 -0
  39. package/lib/harness-serving.d.ts +95 -0
  40. package/lib/harness-serving.d.ts.map +1 -0
  41. package/lib/harness-serving.js +157 -0
  42. package/lib/headless.d.ts +97 -0
  43. package/lib/headless.d.ts.map +1 -0
  44. package/lib/headless.js +349 -0
  45. package/lib/http-acquisition.d.ts +27 -0
  46. package/lib/http-acquisition.d.ts.map +1 -0
  47. package/lib/http-acquisition.js +136 -0
  48. package/lib/index.d.ts +250 -7
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +2625 -70
  51. package/lib/invocation-accounting.d.ts +169 -0
  52. package/lib/invocation-accounting.d.ts.map +1 -0
  53. package/lib/invocation-accounting.js +148 -0
  54. package/lib/legacy-migration.d.ts +95 -0
  55. package/lib/legacy-migration.d.ts.map +1 -0
  56. package/lib/legacy-migration.js +429 -0
  57. package/lib/managed-workers.d.ts +151 -0
  58. package/lib/managed-workers.d.ts.map +1 -0
  59. package/lib/managed-workers.js +249 -0
  60. package/lib/mcp/client-grant.d.ts +74 -0
  61. package/lib/mcp/client-grant.d.ts.map +1 -0
  62. package/lib/mcp/client-grant.js +60 -0
  63. package/lib/mcp/credential-cli.d.ts +38 -0
  64. package/lib/mcp/credential-cli.d.ts.map +1 -0
  65. package/lib/mcp/credential-cli.js +148 -0
  66. package/lib/mcp/credential-store.d.ts +143 -0
  67. package/lib/mcp/credential-store.d.ts.map +1 -0
  68. package/lib/mcp/credential-store.js +454 -0
  69. package/lib/mcp/host-binding.d.ts +25 -0
  70. package/lib/mcp/host-binding.d.ts.map +1 -1
  71. package/lib/mcp/host-binding.js +107 -14
  72. package/lib/mcp/http.d.ts +68 -18
  73. package/lib/mcp/http.d.ts.map +1 -1
  74. package/lib/mcp/http.js +390 -62
  75. package/lib/mcp/oauth.d.ts +149 -0
  76. package/lib/mcp/oauth.d.ts.map +1 -0
  77. package/lib/mcp/oauth.js +396 -0
  78. package/lib/mcp/protocol.d.ts +46 -7
  79. package/lib/mcp/protocol.d.ts.map +1 -1
  80. package/lib/mcp/protocol.js +63 -16
  81. package/lib/mcp/rate-limit.d.ts +68 -0
  82. package/lib/mcp/rate-limit.d.ts.map +1 -0
  83. package/lib/mcp/rate-limit.js +114 -0
  84. package/lib/mcp/sdk-v2.d.ts +17 -0
  85. package/lib/mcp/sdk-v2.d.ts.map +1 -0
  86. package/lib/mcp/sdk-v2.js +181 -0
  87. package/lib/mcp/server.d.ts +11 -1
  88. package/lib/mcp/server.d.ts.map +1 -1
  89. package/lib/mcp/server.js +80 -13
  90. package/lib/mcp/stdio.d.ts.map +1 -1
  91. package/lib/mcp/stdio.js +9 -1
  92. package/lib/mcp/subscriptions.d.ts +43 -1
  93. package/lib/mcp/subscriptions.d.ts.map +1 -1
  94. package/lib/mcp/subscriptions.js +90 -9
  95. package/lib/model-qualification.d.ts +192 -0
  96. package/lib/model-qualification.d.ts.map +1 -0
  97. package/lib/model-qualification.js +178 -0
  98. package/lib/planning.d.ts +246 -0
  99. package/lib/planning.d.ts.map +1 -0
  100. package/lib/planning.js +457 -0
  101. package/lib/prepared-assets.d.ts +178 -0
  102. package/lib/prepared-assets.d.ts.map +1 -0
  103. package/lib/prepared-assets.js +286 -0
  104. package/lib/provider-bridge.d.ts +37 -0
  105. package/lib/provider-bridge.d.ts.map +1 -1
  106. package/lib/provider-bridge.js +189 -12
  107. package/lib/ptc-authority.d.ts +140 -0
  108. package/lib/ptc-authority.d.ts.map +1 -0
  109. package/lib/ptc-authority.js +216 -0
  110. package/lib/ptc-runtime.d.ts +156 -0
  111. package/lib/ptc-runtime.d.ts.map +1 -0
  112. package/lib/ptc-runtime.js +191 -0
  113. package/lib/publication.d.ts +2 -1
  114. package/lib/publication.d.ts.map +1 -1
  115. package/lib/publication.js +13 -5
  116. package/lib/receipt.d.ts.map +1 -1
  117. package/lib/receipt.js +8 -0
  118. package/lib/title-proposal.d.ts +194 -0
  119. package/lib/title-proposal.d.ts.map +1 -0
  120. package/lib/title-proposal.js +244 -0
  121. package/lib/tool-mode-run.d.ts +235 -0
  122. package/lib/tool-mode-run.d.ts.map +1 -0
  123. package/lib/tool-mode-run.js +283 -0
  124. package/lib/types.d.ts +3 -1
  125. package/lib/types.d.ts.map +1 -1
  126. package/lib/update-surface.d.ts +79 -0
  127. package/lib/update-surface.d.ts.map +1 -0
  128. package/lib/update-surface.js +224 -0
  129. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
  130. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
  131. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
  132. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
  133. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  134. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  135. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
  136. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
  137. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
  138. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  139. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  140. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  141. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  142. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  143. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  144. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
  145. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
  146. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +274 -0
  147. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +26 -0
  148. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
  149. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +48 -0
  150. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
  151. package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
  152. package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
  153. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +107 -0
  154. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
  155. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +246 -0
  156. package/node_modules/@try-works/tool-mode-contracts/package.json +34 -0
  157. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  158. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  159. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  160. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  161. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
  162. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  163. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
  164. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  165. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  166. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  167. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  168. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  169. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  170. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
  171. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  172. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
  173. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  174. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  175. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
  176. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
  177. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  178. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
  179. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
  180. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  181. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
  182. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  183. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  184. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  185. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  186. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  187. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  188. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  189. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  190. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  191. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  192. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  193. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
  194. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
  195. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  196. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
  197. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  198. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  199. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  200. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  201. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  202. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  203. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  204. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  205. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  206. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  207. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  208. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  209. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  210. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  211. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  212. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
  213. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  214. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  215. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  216. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  217. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  218. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
  219. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  220. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
  221. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
  222. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  223. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  224. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
  225. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  226. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
  227. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
  228. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  229. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
  230. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  231. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  232. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
  233. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  234. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  235. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  236. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
  237. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  238. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
  239. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  240. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  241. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  242. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
  243. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  244. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
  245. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  246. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  247. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  248. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
  249. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  250. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
  251. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  252. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  253. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  254. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  255. package/package.json +50 -17
  256. package/remote/spec.mjs +439 -0
  257. package/remote/typert.host.mjs +14 -0
  258. package/remote/typert.remote-client.mjs +16 -0
  259. package/lib/adapters/tongue.d.ts +0 -31
  260. package/lib/adapters/tongue.d.ts.map +0 -1
  261. package/lib/adapters/tongue.js +0 -93
  262. package/lib/adapters/voz.d.ts +0 -28
  263. package/lib/adapters/voz.d.ts.map +0 -1
  264. package/lib/adapters/voz.js +0 -102
  265. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
  266. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
  267. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
@@ -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
+ })