@try-works/dsh-tool-mode 0.0.0-stage → 0.5.45

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