@azure-id/orc 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (449) hide show
  1. package/CHANGELOG.md +2017 -0
  2. package/README-id.md +521 -0
  3. package/README.md +485 -0
  4. package/bin/build-agents.js +86 -0
  5. package/bin/cli.js +29050 -0
  6. package/bin/mockrun-catalog.js +267 -0
  7. package/bin/onboarding-content.js +172 -0
  8. package/bin/pricing.json +200 -0
  9. package/bin/providers.json +437 -0
  10. package/bin/ui.js +95 -0
  11. package/bin/verify-contracts.js +3277 -0
  12. package/bin/verify-package.js +498 -0
  13. package/bin/webui/api.js +1163 -0
  14. package/bin/webui/app.html +198 -0
  15. package/bin/webui/css/00-tokens.css +92 -0
  16. package/bin/webui/css/01-base.css +42 -0
  17. package/bin/webui/css/02-shell.css +103 -0
  18. package/bin/webui/css/03-components.css +462 -0
  19. package/bin/webui/css/04-motion.css +91 -0
  20. package/bin/webui/css/05-tour.css +100 -0
  21. package/bin/webui/css/06-responsive.css +108 -0
  22. package/bin/webui/css/panels/boundary.css +26 -0
  23. package/bin/webui/css/panels/challenge.css +151 -0
  24. package/bin/webui/css/panels/crosslink.css +194 -0
  25. package/bin/webui/css/panels/docs.css +254 -0
  26. package/bin/webui/css/panels/experiment.css +23 -0
  27. package/bin/webui/css/panels/extra.css +536 -0
  28. package/bin/webui/css/panels/flow.css +177 -0
  29. package/bin/webui/css/panels/handoff.css +67 -0
  30. package/bin/webui/css/panels/knowledge.css +86 -0
  31. package/bin/webui/css/panels/learn.css +154 -0
  32. package/bin/webui/css/panels/maintenance.css +55 -0
  33. package/bin/webui/css/panels/mockrun.css +268 -0
  34. package/bin/webui/css/panels/overview.css +100 -0
  35. package/bin/webui/css/panels/pact.css +43 -0
  36. package/bin/webui/css/panels/runs.css +105 -0
  37. package/bin/webui/css/panels/settings.css +227 -0
  38. package/bin/webui/css/panels/stats.css +87 -0
  39. package/bin/webui/fixtures/boundary.js +92 -0
  40. package/bin/webui/fixtures/challenge.js +825 -0
  41. package/bin/webui/fixtures/crosslink.js +31 -0
  42. package/bin/webui/fixtures/docs.js +982 -0
  43. package/bin/webui/fixtures/extra.js +1934 -0
  44. package/bin/webui/fixtures/flow.js +81 -0
  45. package/bin/webui/fixtures/handoff.js +35 -0
  46. package/bin/webui/fixtures/index.js +486 -0
  47. package/bin/webui/fixtures/knowledge.js +291 -0
  48. package/bin/webui/fixtures/maintenance.js +42 -0
  49. package/bin/webui/fixtures/mockrun.js +36 -0
  50. package/bin/webui/fixtures/pact.js +111 -0
  51. package/bin/webui/fixtures/runs.js +109 -0
  52. package/bin/webui/fixtures/settings.js +78 -0
  53. package/bin/webui/fixtures/shell.js +48 -0
  54. package/bin/webui/fixtures/stats.js +104 -0
  55. package/bin/webui/i18n/TERMS.md +134 -0
  56. package/bin/webui/i18n/en/banner.json +29 -0
  57. package/bin/webui/i18n/en/boundary.json +20 -0
  58. package/bin/webui/i18n/en/challenge.json +106 -0
  59. package/bin/webui/i18n/en/common.json +69 -0
  60. package/bin/webui/i18n/en/crosslink.json +69 -0
  61. package/bin/webui/i18n/en/docs.json +175 -0
  62. package/bin/webui/i18n/en/experiment.json +17 -0
  63. package/bin/webui/i18n/en/extra.json +313 -0
  64. package/bin/webui/i18n/en/flow.json +30 -0
  65. package/bin/webui/i18n/en/handoff.json +37 -0
  66. package/bin/webui/i18n/en/knowledge.json +141 -0
  67. package/bin/webui/i18n/en/learn.json +16 -0
  68. package/bin/webui/i18n/en/maintenance.json +52 -0
  69. package/bin/webui/i18n/en/mockrun.json +19 -0
  70. package/bin/webui/i18n/en/nav.json +19 -0
  71. package/bin/webui/i18n/en/overview.json +89 -0
  72. package/bin/webui/i18n/en/pact.json +36 -0
  73. package/bin/webui/i18n/en/runs.json +51 -0
  74. package/bin/webui/i18n/en/settings.json +49 -0
  75. package/bin/webui/i18n/en/stats.json +69 -0
  76. package/bin/webui/i18n/en/tour.json +33 -0
  77. package/bin/webui/i18n/id/banner.json +29 -0
  78. package/bin/webui/i18n/id/boundary.json +20 -0
  79. package/bin/webui/i18n/id/challenge.json +106 -0
  80. package/bin/webui/i18n/id/common.json +70 -0
  81. package/bin/webui/i18n/id/crosslink.json +69 -0
  82. package/bin/webui/i18n/id/docs.json +175 -0
  83. package/bin/webui/i18n/id/experiment.json +17 -0
  84. package/bin/webui/i18n/id/extra.json +313 -0
  85. package/bin/webui/i18n/id/flow.json +30 -0
  86. package/bin/webui/i18n/id/handoff.json +37 -0
  87. package/bin/webui/i18n/id/knowledge.json +141 -0
  88. package/bin/webui/i18n/id/learn.json +16 -0
  89. package/bin/webui/i18n/id/maintenance.json +52 -0
  90. package/bin/webui/i18n/id/mockrun.json +19 -0
  91. package/bin/webui/i18n/id/nav.json +19 -0
  92. package/bin/webui/i18n/id/overview.json +89 -0
  93. package/bin/webui/i18n/id/pact.json +36 -0
  94. package/bin/webui/i18n/id/runs.json +51 -0
  95. package/bin/webui/i18n/id/settings.json +49 -0
  96. package/bin/webui/i18n/id/stats.json +69 -0
  97. package/bin/webui/i18n/id/tour.json +33 -0
  98. package/bin/webui/js/00-core.js +115 -0
  99. package/bin/webui/js/01-i18n.js +149 -0
  100. package/bin/webui/js/02-ui.js +211 -0
  101. package/bin/webui/js/03-md.js +286 -0
  102. package/bin/webui/js/04-router.js +51 -0
  103. package/bin/webui/js/05-banners.js +172 -0
  104. package/bin/webui/js/06-edit.js +183 -0
  105. package/bin/webui/js/90-tour.js +488 -0
  106. package/bin/webui/js/91-shortcuts.js +126 -0
  107. package/bin/webui/js/99-boot.js +118 -0
  108. package/bin/webui/js/panels/boundary.js +104 -0
  109. package/bin/webui/js/panels/challenge.js +781 -0
  110. package/bin/webui/js/panels/crosslink.js +654 -0
  111. package/bin/webui/js/panels/docs.js +1338 -0
  112. package/bin/webui/js/panels/experiment.js +98 -0
  113. package/bin/webui/js/panels/extra.js +3011 -0
  114. package/bin/webui/js/panels/flow.js +297 -0
  115. package/bin/webui/js/panels/handoff.js +169 -0
  116. package/bin/webui/js/panels/knowledge.js +890 -0
  117. package/bin/webui/js/panels/learn.js +220 -0
  118. package/bin/webui/js/panels/maintenance.js +235 -0
  119. package/bin/webui/js/panels/mockrun.js +227 -0
  120. package/bin/webui/js/panels/overview.js +447 -0
  121. package/bin/webui/js/panels/pact.js +189 -0
  122. package/bin/webui/js/panels/runs.js +416 -0
  123. package/bin/webui/js/panels/settings.js +573 -0
  124. package/bin/webui/js/panels/stats.js +378 -0
  125. package/bin/webui/serve.js +557 -0
  126. package/mock-run/INDEX.md +106 -0
  127. package/mock-run/a-normal-day.md +587 -0
  128. package/mock-run/context-combiner.md +100 -0
  129. package/mock-run/extra-recovery.md +330 -0
  130. package/mock-run/extra-slots.md +177 -0
  131. package/mock-run/media/README.md +43 -0
  132. package/mock-run/orc-aftermath.md +392 -0
  133. package/mock-run/orc-boundary.md +356 -0
  134. package/mock-run/orc-brainstorm.md +177 -0
  135. package/mock-run/orc-budget.md +534 -0
  136. package/mock-run/orc-challenge-council.md +262 -0
  137. package/mock-run/orc-challenge.md +416 -0
  138. package/mock-run/orc-cli.md +200 -0
  139. package/mock-run/orc-diy.md +129 -0
  140. package/mock-run/orc-doc.md +448 -0
  141. package/mock-run/orc-explain.md +86 -0
  142. package/mock-run/orc-export.md +423 -0
  143. package/mock-run/orc-extra.md +392 -0
  144. package/mock-run/orc-fast.md +106 -0
  145. package/mock-run/orc-grill.md +151 -0
  146. package/mock-run/orc-handoff.md +480 -0
  147. package/mock-run/orc-pact.md +421 -0
  148. package/mock-run/orc-pattern.md +112 -0
  149. package/mock-run/orc-plan.md +108 -0
  150. package/mock-run/orc-pr-setup.md +126 -0
  151. package/mock-run/orc-quick.md +113 -0
  152. package/mock-run/orc-route.md +93 -0
  153. package/mock-run/orc-ui.md +125 -0
  154. package/mock-run/orc-ultra.md +103 -0
  155. package/mock-run/orc.md +157 -0
  156. package/mock-run/the-example-project.md +55 -0
  157. package/package.json +39 -0
  158. package/templates/agents/MODEL-MAPPING.md +168 -0
  159. package/templates/agents/orc-advisor-fable-5.md +50 -0
  160. package/templates/agents/orc-advisor-opus-5-xhigh.md +56 -0
  161. package/templates/agents/orc-analyst-fable-5.md +115 -0
  162. package/templates/agents/orc-analyze-mini-opus-5-med.md +60 -0
  163. package/templates/agents/orc-analyze-mini-sonnet-5-high.md +58 -0
  164. package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -0
  165. package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -0
  166. package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -0
  167. package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -0
  168. package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -0
  169. package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -0
  170. package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -0
  171. package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -0
  172. package/templates/agents/orc-claude-writer-opus-4-8-high.md +53 -0
  173. package/templates/agents/orc-claude-writer-opus-5-med.md +55 -0
  174. package/templates/agents/orc-context-combiner-opus-5-high.md +88 -0
  175. package/templates/agents/orc-doc-checker-opus-5-low.md +108 -0
  176. package/templates/agents/orc-doc-writer-opus-5-med.md +134 -0
  177. package/templates/agents/orc-executor-haiku-4-5.md +113 -0
  178. package/templates/agents/orc-executor-opus-4-7-high.md +114 -0
  179. package/templates/agents/orc-executor-opus-4-7-med.md +114 -0
  180. package/templates/agents/orc-executor-opus-4-8-high.md +114 -0
  181. package/templates/agents/orc-executor-opus-5-high.md +114 -0
  182. package/templates/agents/orc-executor-opus-5-low.md +114 -0
  183. package/templates/agents/orc-executor-opus-5-med.md +114 -0
  184. package/templates/agents/orc-executor-sonnet-4-6-high.md +114 -0
  185. package/templates/agents/orc-executor-sonnet-4-6-med.md +114 -0
  186. package/templates/agents/orc-executor-sonnet-5-high.md +114 -0
  187. package/templates/agents/orc-judge-fable-5.md +79 -0
  188. package/templates/agents/orc-judge-opus-5-xhigh.md +85 -0
  189. package/templates/agents/orc-learn-writer-opus-5-low.md +73 -0
  190. package/templates/agents/orc-pattern-codifier-opus-5-med.md +65 -0
  191. package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +63 -0
  192. package/templates/agents/orc-planner-fable-5.md +152 -0
  193. package/templates/agents/orc-planner-mini-opus-5-med.md +69 -0
  194. package/templates/agents/orc-planner-mini-sonnet-5-high.md +67 -0
  195. package/templates/agents/orc-planner-opus-5-med.md +160 -0
  196. package/templates/agents/orc-retro-opus-5-med.md +73 -0
  197. package/templates/agents/orc-retro-sonnet-5-high.md +72 -0
  198. package/templates/agents/orc-reviewer-fable-5.md +57 -0
  199. package/templates/agents/orc-reviewer-opus-5-med.md +60 -0
  200. package/templates/agents/orc-scout-opus-5-low.md +40 -0
  201. package/templates/agents/orc-scout-sonnet-4-6-high.md +39 -0
  202. package/templates/agents/orc-system-analyst-opus-5-high.md +120 -0
  203. package/templates/agents/orc-test-author-opus-5-med.md +71 -0
  204. package/templates/agents/orc-trace-writer-haiku-4-5.md +107 -0
  205. package/templates/agents/orc-verifier-opus-5-med.md +69 -0
  206. package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +79 -0
  207. package/templates/agents/orc-wiki-scanner-opus-5-med.md +81 -0
  208. package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +106 -0
  209. package/templates/commands/orc-aftermath.md +49 -0
  210. package/templates/commands/orc-analyze.md +21 -0
  211. package/templates/commands/orc-boundary.md +46 -0
  212. package/templates/commands/orc-brainstorm.md +51 -0
  213. package/templates/commands/orc-budget.md +54 -0
  214. package/templates/commands/orc-challenge.md +58 -0
  215. package/templates/commands/orc-claude.md +14 -0
  216. package/templates/commands/orc-diy.md +13 -0
  217. package/templates/commands/orc-doc.md +128 -0
  218. package/templates/commands/orc-explain.md +20 -0
  219. package/templates/commands/orc-export.md +46 -0
  220. package/templates/commands/orc-fast.md +15 -0
  221. package/templates/commands/orc-grill.md +40 -0
  222. package/templates/commands/orc-handoff.md +52 -0
  223. package/templates/commands/orc-learn.md +17 -0
  224. package/templates/commands/orc-mini.md +12 -0
  225. package/templates/commands/orc-pact.md +44 -0
  226. package/templates/commands/orc-pattern.md +15 -0
  227. package/templates/commands/orc-plan.md +22 -0
  228. package/templates/commands/orc-poly.md +21 -0
  229. package/templates/commands/orc-pr-driver.md +30 -0
  230. package/templates/commands/orc-pr-setup.md +31 -0
  231. package/templates/commands/orc-quick.md +33 -0
  232. package/templates/commands/orc-retro.md +17 -0
  233. package/templates/commands/orc-route.md +41 -0
  234. package/templates/commands/orc-ultra.md +17 -0
  235. package/templates/commands/orc-verify.md +11 -0
  236. package/templates/commands/orc-wiki.md +42 -0
  237. package/templates/commands/orc.md +16 -0
  238. package/templates/hooks/orc-effort-guard.js +178 -0
  239. package/templates/hooks/orc-statusline.js +305 -0
  240. package/templates/hooks/orc-trace.js +471 -0
  241. package/templates/hooks/orc-update-lib.js +125 -0
  242. package/templates/skills/_shared/README.md +50 -0
  243. package/templates/skills/_shared/detecting-artifacts.md +90 -0
  244. package/templates/skills/_shared/drift-recovery.md +98 -0
  245. package/templates/skills/_shared/extra-dispatch.md +1051 -0
  246. package/templates/skills/_shared/fable5-override.md +56 -0
  247. package/templates/skills/_shared/fallback-handoff.md +42 -0
  248. package/templates/skills/_shared/gh-stack-commands.md +65 -0
  249. package/templates/skills/_shared/gotchas.md +177 -0
  250. package/templates/skills/_shared/interview.md +144 -0
  251. package/templates/skills/_shared/lane-suspend.md +82 -0
  252. package/templates/skills/_shared/opus5-only.md +131 -0
  253. package/templates/skills/_shared/pr-templates.md +106 -0
  254. package/templates/skills/_shared/read-ladder.md +54 -0
  255. package/templates/skills/_shared/return-validation.md +145 -0
  256. package/templates/skills/_shared/smoke-gate.md +28 -0
  257. package/templates/skills/_shared/stack-plan.md +135 -0
  258. package/templates/skills/_shared/untrusted-input.md +63 -0
  259. package/templates/skills/context-combiner/SKILL.md +187 -0
  260. package/templates/skills/context-combiner/schemas/combined-report.md +78 -0
  261. package/templates/skills/context-combiner/schemas/combined-requirement-spec.md +87 -0
  262. package/templates/skills/orc/README.md +150 -0
  263. package/templates/skills/orc/SKILL.md +526 -0
  264. package/templates/skills/orc/config.md +389 -0
  265. package/templates/skills/orc/examples/full-run-mock.md +73 -0
  266. package/templates/skills/orc/references/analyst-gates.md +124 -0
  267. package/templates/skills/orc/references/effort-and-mode.md +224 -0
  268. package/templates/skills/orc/references/house-rules.md +20 -0
  269. package/templates/skills/orc/references/intake.md +120 -0
  270. package/templates/skills/orc/references/log-protocol.md +39 -0
  271. package/templates/skills/orc/references/pattern-gate.md +89 -0
  272. package/templates/skills/orc/references/plan-handoff.md +84 -0
  273. package/templates/skills/orc/references/preflight-report.md +220 -0
  274. package/templates/skills/orc/references/security-checklist.md +39 -0
  275. package/templates/skills/orc/references/stop-and-resume.md +160 -0
  276. package/templates/skills/orc/references/trace-protocol.md +314 -0
  277. package/templates/skills/orc/references/ultra-mode.md +123 -0
  278. package/templates/skills/orc/references/wave-grouping.md +121 -0
  279. package/templates/skills/orc/references/wiki-consult.md +177 -0
  280. package/templates/skills/orc/schemas/checkpoint.md +122 -0
  281. package/templates/skills/orc/schemas/intent-spec.md +42 -0
  282. package/templates/skills/orc/schemas/planning-output.md +279 -0
  283. package/templates/skills/orc/subskills/orc-checkpoint/SKILL.md +55 -0
  284. package/templates/skills/orc/subskills/orc-execution/SKILL.md +72 -0
  285. package/templates/skills/orc/subskills/orc-execution/core.md +151 -0
  286. package/templates/skills/orc/subskills/orc-execution/subagent.md +13 -0
  287. package/templates/skills/orc/subskills/orc-planner/SKILL.md +264 -0
  288. package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -0
  289. package/templates/skills/orc/subskills/orc-pr/SKILL.md +51 -0
  290. package/templates/skills/orc/subskills/orc-pr/pr.md +22 -0
  291. package/templates/skills/orc/subskills/orc-pr/stack-gate.md +108 -0
  292. package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +76 -0
  293. package/templates/skills/orc/subskills/orc-review-verify/core.md +135 -0
  294. package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -0
  295. package/templates/skills/orc/subskills/orc-testgen/SKILL.md +45 -0
  296. package/templates/skills/orc/subskills/orc-testgen/core.md +59 -0
  297. package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -0
  298. package/templates/skills/orc-advisor/SKILL.md +60 -0
  299. package/templates/skills/orc-aftermath/SKILL.md +136 -0
  300. package/templates/skills/orc-aftermath/references/report.md +95 -0
  301. package/templates/skills/orc-analyze/SKILL.md +220 -0
  302. package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -0
  303. package/templates/skills/orc-analyze/references/branching.md +51 -0
  304. package/templates/skills/orc-analyze/references/deep-mode.md +27 -0
  305. package/templates/skills/orc-analyze/references/thin-input.md +77 -0
  306. package/templates/skills/orc-analyze/schemas/report-audit.md +83 -0
  307. package/templates/skills/orc-analyze/schemas/report-prose.md +63 -0
  308. package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -0
  309. package/templates/skills/orc-analyze/schemas/requirement-spec.md +77 -0
  310. package/templates/skills/orc-analyze-mini/SKILL.md +112 -0
  311. package/templates/skills/orc-analyze-mini/examples/quick-analysis-mock.md +31 -0
  312. package/templates/skills/orc-boundary/SKILL.md +208 -0
  313. package/templates/skills/orc-boundary/references/card.md +78 -0
  314. package/templates/skills/orc-boundary/references/gate.md +113 -0
  315. package/templates/skills/orc-brainstorm/SKILL.md +339 -0
  316. package/templates/skills/orc-brainstorm/references/brainstorm-doc.md +133 -0
  317. package/templates/skills/orc-brainstorm/references/lenses.md +152 -0
  318. package/templates/skills/orc-budget/SKILL.md +225 -0
  319. package/templates/skills/orc-budget/references/corpus.md +87 -0
  320. package/templates/skills/orc-challenge/README.md +142 -0
  321. package/templates/skills/orc-challenge/SKILL.md +255 -0
  322. package/templates/skills/orc-challenge/examples/code-module.md +110 -0
  323. package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -0
  324. package/templates/skills/orc-challenge/examples/tsd-two-iterations.md +213 -0
  325. package/templates/skills/orc-challenge/references/conservation.md +107 -0
  326. package/templates/skills/orc-challenge/references/council.md +315 -0
  327. package/templates/skills/orc-challenge/references/cycle-state.md +185 -0
  328. package/templates/skills/orc-challenge/references/dimensions.md +121 -0
  329. package/templates/skills/orc-challenge/references/fix-brief.md +130 -0
  330. package/templates/skills/orc-challenge/references/intake.md +171 -0
  331. package/templates/skills/orc-challenge/references/kinds.md +44 -0
  332. package/templates/skills/orc-challenge/references/plain-english.md +98 -0
  333. package/templates/skills/orc-challenge/references/rubric.md +109 -0
  334. package/templates/skills/orc-challenge/references/sealed-slice.md +97 -0
  335. package/templates/skills/orc-challenge/references/verdict-doc.md +154 -0
  336. package/templates/skills/orc-claude/SKILL.md +199 -0
  337. package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -0
  338. package/templates/skills/orc-claude/references/refresh.md +76 -0
  339. package/templates/skills/orc-claude/references/template.md +109 -0
  340. package/templates/skills/orc-diy/README.md +142 -0
  341. package/templates/skills/orc-diy/SKILL.md +70 -0
  342. package/templates/skills/orc-diy/references/blocks/analyze.md +24 -0
  343. package/templates/skills/orc-diy/references/blocks/execution.md +42 -0
  344. package/templates/skills/orc-diy/references/blocks/extra.md +55 -0
  345. package/templates/skills/orc-diy/references/blocks/header.md +55 -0
  346. package/templates/skills/orc-diy/references/blocks/mock-example.md +22 -0
  347. package/templates/skills/orc-diy/references/blocks/pattern.md +18 -0
  348. package/templates/skills/orc-diy/references/blocks/planning.md +34 -0
  349. package/templates/skills/orc-diy/references/blocks/review.md +18 -0
  350. package/templates/skills/orc-diy/references/blocks/scoring.md +16 -0
  351. package/templates/skills/orc-diy/references/blocks/security.md +25 -0
  352. package/templates/skills/orc-diy/references/blocks/ship.md +25 -0
  353. package/templates/skills/orc-diy/references/blocks/summary.md +18 -0
  354. package/templates/skills/orc-diy/references/blocks/testgen.md +17 -0
  355. package/templates/skills/orc-diy/references/blocks/trace.md +28 -0
  356. package/templates/skills/orc-diy/references/blocks/verify.md +25 -0
  357. package/templates/skills/orc-diy/references/blocks/wiki.md +26 -0
  358. package/templates/skills/orc-diy/references/compile.md +59 -0
  359. package/templates/skills/orc-diy/references/flow-schema.md +100 -0
  360. package/templates/skills/orc-diy/references/locked-blocks.md +37 -0
  361. package/templates/skills/orc-doc/README.md +229 -0
  362. package/templates/skills/orc-doc/SKILL.md +444 -0
  363. package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -0
  364. package/templates/skills/orc-doc/references/chunking.md +527 -0
  365. package/templates/skills/orc-doc/references/gates.md +311 -0
  366. package/templates/skills/orc-doc/references/generation-rules.md +122 -0
  367. package/templates/skills/orc-doc/references/house-rules.md +170 -0
  368. package/templates/skills/orc-doc/references/import-targets.md +40 -0
  369. package/templates/skills/orc-doc/references/plain-language.md +66 -0
  370. package/templates/skills/orc-doc/references/portable-markdown.md +74 -0
  371. package/templates/skills/orc-doc/references/resume-protocol.md +228 -0
  372. package/templates/skills/orc-doc/references/templates/collaboration.md +118 -0
  373. package/templates/skills/orc-doc/references/templates/prd.md +133 -0
  374. package/templates/skills/orc-doc/references/templates/report.md +102 -0
  375. package/templates/skills/orc-doc/references/templates/tsd.md +122 -0
  376. package/templates/skills/orc-doc/references/templates/workflow.md +112 -0
  377. package/templates/skills/orc-explain/SKILL.md +57 -0
  378. package/templates/skills/orc-export/SKILL.md +154 -0
  379. package/templates/skills/orc-fast/SKILL.md +188 -0
  380. package/templates/skills/orc-grill/SKILL.md +207 -0
  381. package/templates/skills/orc-grill/references/grill-doc.md +105 -0
  382. package/templates/skills/orc-handoff/SKILL.md +204 -0
  383. package/templates/skills/orc-handoff/references/handoff-log.md +67 -0
  384. package/templates/skills/orc-handoff/references/surfaces.md +88 -0
  385. package/templates/skills/orc-judge/SKILL.md +82 -0
  386. package/templates/skills/orc-learn/SKILL.md +156 -0
  387. package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -0
  388. package/templates/skills/orc-learn/references/deepen.md +70 -0
  389. package/templates/skills/orc-learn/references/refresh.md +65 -0
  390. package/templates/skills/orc-learn/references/template-knowledge.md +83 -0
  391. package/templates/skills/orc-learn/references/template-learning.md +67 -0
  392. package/templates/skills/orc-mini/SKILL.md +226 -0
  393. package/templates/skills/orc-mini/examples/mini-run-mock.md +50 -0
  394. package/templates/skills/orc-pact/SKILL.md +218 -0
  395. package/templates/skills/orc-pact/references/gate.md +70 -0
  396. package/templates/skills/orc-pact/references/ledger.md +99 -0
  397. package/templates/skills/orc-pattern/SKILL.md +124 -0
  398. package/templates/skills/orc-pattern/references/INDEX.md +73 -0
  399. package/templates/skills/orc-pattern/references/be-django.md +80 -0
  400. package/templates/skills/orc-pattern/references/be-express.md +82 -0
  401. package/templates/skills/orc-pattern/references/be-fastapi.md +107 -0
  402. package/templates/skills/orc-pattern/references/be-go.md +96 -0
  403. package/templates/skills/orc-pattern/references/be-nestjs.md +86 -0
  404. package/templates/skills/orc-pattern/references/be-postgres.md +98 -0
  405. package/templates/skills/orc-pattern/references/fe-a11y.md +36 -0
  406. package/templates/skills/orc-pattern/references/fe-angular.md +87 -0
  407. package/templates/skills/orc-pattern/references/fe-nextjs.md +65 -0
  408. package/templates/skills/orc-pattern/references/fe-perf.md +39 -0
  409. package/templates/skills/orc-pattern/references/fe-react.md +83 -0
  410. package/templates/skills/orc-pattern/references/fe-vue.md +75 -0
  411. package/templates/skills/orc-pattern/schemas/pattern-doc.md +62 -0
  412. package/templates/skills/orc-poly/SKILL.md +216 -0
  413. package/templates/skills/orc-poly/examples/poly-run-mock.md +51 -0
  414. package/templates/skills/orc-poly/references/gather.md +70 -0
  415. package/templates/skills/orc-poly/references/poly-spec.md +84 -0
  416. package/templates/skills/orc-pr-driver/README.md +349 -0
  417. package/templates/skills/orc-pr-driver/SKILL.md +167 -0
  418. package/templates/skills/orc-pr-driver/references/conflict-playbook.md +78 -0
  419. package/templates/skills/orc-pr-driver/references/green-gate.md +105 -0
  420. package/templates/skills/orc-pr-driver/references/orc-run-split.md +99 -0
  421. package/templates/skills/orc-pr-setup/README.md +436 -0
  422. package/templates/skills/orc-pr-setup/SKILL.md +184 -0
  423. package/templates/skills/orc-pr-setup/references/certainty-gate.md +66 -0
  424. package/templates/skills/orc-pr-setup/references/layer-taxonomy.md +67 -0
  425. package/templates/skills/orc-quick/README.md +423 -0
  426. package/templates/skills/orc-quick/SKILL.md +306 -0
  427. package/templates/skills/orc-quick/references/context-doc.md +114 -0
  428. package/templates/skills/orc-quick/references/dispatch-gate.md +163 -0
  429. package/templates/skills/orc-quick/references/gh-mode.md +127 -0
  430. package/templates/skills/orc-retro/SKILL.md +222 -0
  431. package/templates/skills/orc-retro/examples/retro-mock.md +171 -0
  432. package/templates/skills/orc-route/SKILL.md +165 -0
  433. package/templates/skills/orc-verify/SKILL.md +86 -0
  434. package/templates/skills/orc-verify/examples/verify-mock.md +33 -0
  435. package/templates/skills/orc-wiki/README.md +325 -0
  436. package/templates/skills/orc-wiki/SKILL.md +322 -0
  437. package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -0
  438. package/templates/skills/orc-wiki/references/claude-md-injection.md +61 -0
  439. package/templates/skills/orc-wiki/references/crosslink-compile.md +39 -0
  440. package/templates/skills/orc-wiki/references/crosslink-kinds.md +57 -0
  441. package/templates/skills/orc-wiki/references/crosslink.md +284 -0
  442. package/templates/skills/orc-wiki/references/extra.md +79 -0
  443. package/templates/skills/orc-wiki/references/integrity-check.md +86 -0
  444. package/templates/skills/orc-wiki/references/orientation.md +51 -0
  445. package/templates/skills/orc-wiki/references/partial-refresh.md +203 -0
  446. package/templates/skills/orc-wiki/references/pattern-prewarm.md +19 -0
  447. package/templates/skills/orc-wiki/references/staleness.md +288 -0
  448. package/templates/skills/orc-wiki/schemas/crosslink-tag.md +166 -0
  449. package/templates/skills/orc-wiki/schemas/wiki-doc.md +120 -0
@@ -0,0 +1,3011 @@
1
+ "use strict";
2
+ /* panels/extra.js — orc ui client
3
+ Integrate with other AI model. The panel for `orc extra`: which providers ORC
4
+ can reach, which connections you have, and whether each one has ever answered.
5
+
6
+ THE PANEL NAMES NO PROVIDER, NO MODEL AND NO PRICE. Every tile below is a row
7
+ of `orc extra providers --json`, every chip is a field of `orc extra list
8
+ --json`, and every state word is one the CLI computed — the Flow-stepper rule.
9
+ Model ids are deliberately not shipped at all (they rot within a quarter), so
10
+ the only model names that ever appear here are the ones a real connection test
11
+ read back from the provider.
12
+
13
+ THE ONE THING THIS PANEL DERIVES IS NOTHING. Not a freshness tier, not a
14
+ verification state, not an attempt ceiling: STALE and UNVERIFIED arrive as
15
+ `orc extra doctor` findings keyed by profile, and the attempt ceiling is the
16
+ config key's own value.
17
+
18
+ THE BOUNDARY CARD IS PANEL PROSE, and that is deliberate rather than an
19
+ oversight. It makes no claim about this repo's state — it explains what the
20
+ subsystem does — so it is a sentence a human reads and therefore a sentence
21
+ that must translate. Everything beside it that IS a fact about your setup (the
22
+ engine, the base URL, whether a key was found) comes from the CLI verbatim.
23
+
24
+ Loaded by app.html in the order its numeric prefix names. Classic
25
+ script, no import/export: an ES module import carries no query string,
26
+ and every static request here needs the per-launch session token. */
27
+
28
+ /* ================================================================== EXTRA == */
29
+
30
+ PANELS.extra = function (host) {
31
+ head(host, t("extra.title"), t("extra.sub"));
32
+ const body = el("div", "stack");
33
+ host.append(body);
34
+ renderExtra(body);
35
+ };
36
+
37
+ async function renderExtra(body) {
38
+ body.replaceChildren(skeleton(6));
39
+ // Four reads, in parallel. A read that FAILED is not a read that came back
40
+ // empty (v0.49.4): the error is kept so the card can say which half is
41
+ // missing instead of rendering as "you have nothing configured".
42
+ const [listRes, provRes, docRes, cfgRes, routeRes, roleRes, laneRes, statRes, rateRes, toolRes, jourRes] = await Promise.all([
43
+ read("/api/extra").catch((e) => ({ data: null, error: e })),
44
+ read("/api/extra/providers").catch((e) => ({ data: null, error: e })),
45
+ read("/api/extra/doctor").catch((e) => ({ data: null, error: e })),
46
+ read("/api/config").catch((e) => ({ data: null, error: e })),
47
+ read("/api/extra/route").catch((e) => ({ data: null, error: e })),
48
+ // v0.55.0 — THE POSITIONS. It exits 1 when nothing routes, which is
49
+ // exit-code-as-DATA like every other gate command on this panel.
50
+ read("/api/extra/role").catch((e) => ({ data: null, error: e })),
51
+ read("/api/extra/lanes").catch((e) => ({ data: null, error: e })),
52
+ read("/api/extra/stats").catch((e) => ({ data: null, error: e })),
53
+ read("/api/extra/rates").catch((e) => ({ data: null, error: e })),
54
+ // v0.51.0 — the LOCAL TOOLS read. It exits 1 when nothing is ready, which is
55
+ // exit-code-as-DATA like every other gate command on this panel.
56
+ read("/api/extra/tools").catch((e) => ({ data: null, error: e })),
57
+ // v0.54.0 — the journal listing. It is a FREE read and its `orphans` count
58
+ // is the one thing the Recovery tab exists to surface.
59
+ read("/api/extra/journal").catch((e) => ({ data: null, error: e })),
60
+ ]);
61
+ const d = {
62
+ list: listRes.data,
63
+ providers: provRes.data,
64
+ doctor: docRes.data,
65
+ config: cfgRes.data,
66
+ route: routeRes.data,
67
+ role: roleRes.data,
68
+ lanes: laneRes.data,
69
+ // `orc extra stats` exits 1 with a real object when nothing has been
70
+ // dispatched yet — an ANSWER, not an error, so it is read like every other
71
+ // exit-code-as-data command on this panel.
72
+ stats: statRes.data,
73
+ rates: rateRes.data,
74
+ tools: toolRes.data,
75
+ journal: jourRes.data,
76
+ errors: {
77
+ list: listRes.error || null,
78
+ providers: provRes.error || null,
79
+ route: routeRes.error || null,
80
+ role: roleRes.error || null,
81
+ lanes: laneRes.error || null,
82
+ tools: toolRes.error || null,
83
+ },
84
+ };
85
+
86
+ // ONE staged-edit set for the whole panel, and it OUTLIVES a re-render on
87
+ // purpose: a connection test in the middle of planning a routing change must
88
+ // not silently throw the plan away.
89
+ // NOTHING RE-RENDERS UNTIL APPLY (v0.44.1), so staging only repaints the bar
90
+ // and each control repaints its OWN state. A full re-render on every click
91
+ // would re-fetch five endpoints and scroll the list out from under the person
92
+ // using it — the exact fight that release was written to end.
93
+ if (!EX_EDITS) EX_EDITS = editSet(() => EX_BAR && EX_BAR.paint());
94
+ // A render that gated the bar away must not leave the previous one behind as
95
+ // a live reference: paint() on a detached node is a write to nothing.
96
+ EX_BAR = null;
97
+ const edits = EX_EDITS;
98
+ // Which config keys this panel owns is the CLI's answer, read fresh every
99
+ // render. A hard-coded list here would be a second registry.
100
+ EX_CONFIG_KEYS = ((d.config && d.config.keys) || []).filter((k) => k.key.indexOf("extra_") === 0).map((k) => k.key);
101
+
102
+ // W8 (v0.51.0) — THE SETUP GATE. `connected` is the CLI's answer (it computes
103
+ // it once, and the config gate and the doctor finding read the same one), so
104
+ // there is no second idea here of what "has anything ever answered" means.
105
+ const connected = !!(d.list && d.list.gate && d.list.gate.connected);
106
+ const out = frag();
107
+
108
+ // THE HEADER STRIP AND "WHAT NEEDS YOUR ATTENTION" ARE ON EVERY TAB. The
109
+ // strip is the Knowledge precedent (v0.49.1), and the findings card is the
110
+ // one card that must not be behind a tab: a caution you have to go looking
111
+ // for is a caution nobody reads. Everything else is tabbed.
112
+ out.append(exStrip(d));
113
+ out.append(exFindingsCard(d));
114
+
115
+ const tabs = el("div", "tabs");
116
+ // `stack` as well as `tab-pane`: every tab holds several cards, and the
117
+ // container is what spaces panel blocks.
118
+ const pane = el("div", "tab-pane stack");
119
+ // THE GATE STILL DECIDES WHAT EXISTS. With nothing connected there is no
120
+ // routing to draw, no limits worth setting and nothing spent — so those tabs
121
+ // are NOT RENDERED AS EMPTY SHELLS. Setup is the only tab there is, and its
122
+ // own tab is spotlighted (the Crosslink "nothing linked" rule).
123
+ const views = connected
124
+ ? {
125
+ setup: () => exSetupTab(d, body),
126
+ routing: () => exRoutingTab(d, body, edits),
127
+ limits: () => exLimitsTab(d, body, edits),
128
+ spending: () => exSpendingTab(d),
129
+ recovery: () => exRecoveryTab(d, body),
130
+ providers: () => exProvidersTab(d),
131
+ }
132
+ : { setup: () => exSetupTab(d, body) };
133
+ const select = (which) => {
134
+ EX_TAB = which;
135
+ for (const b of tabs.children) b.setAttribute("aria-selected", String(b.dataset.tab === which));
136
+ pane.replaceChildren(views[which]());
137
+ };
138
+ // Keys are written out in full, never assembled from the tab id — a key built
139
+ // from a fragment is invisible to every check that looks for one.
140
+ for (const [which, label] of [
141
+ ["setup", t("extra.tab.setup")],
142
+ ["routing", t("extra.tab.routing")],
143
+ ["limits", t("extra.tab.limits")],
144
+ ["spending", t("extra.tab.spending")],
145
+ ["recovery", t("extra.tab.recovery")],
146
+ ["providers", t("extra.tab.providers")],
147
+ ]) {
148
+ if (!views[which]) continue;
149
+ const b = el("button", null, label);
150
+ b.type = "button";
151
+ b.dataset.tab = which;
152
+ if (!connected && which === "setup") b.classList.add("tab-spot");
153
+ b.addEventListener("click", () => select(which));
154
+ tabs.append(b);
155
+ }
156
+ out.append(tabs, pane);
157
+ body.replaceChildren(out);
158
+ select(views[EX_TAB] ? EX_TAB : "setup");
159
+ // The bar sticks only while dirty, and Discard renders only while dirty —
160
+ // both are editBar's own rules, unchanged. It is GATED with the two cards it
161
+ // belongs to (v0.51.0): "Reset the guardrails" beside a panel that is not
162
+ // showing any guardrails is the same mistake as a Connect button on a tool
163
+ // that is not installed.
164
+ EX_BAR = connected ? exEditBar(edits, body) : null;
165
+ if (EX_BAR) body.append(EX_BAR);
166
+ }
167
+
168
+ /* THE FIVE TABS (v0.53.0).
169
+
170
+ This panel was nine cards in one 8,800px scroll — no first step, no last
171
+ step, and no way to be DONE with a section. Knowledge (five tabs, v0.49.1)
172
+ and Crosslink (two, v0.43.7) are the precedent, down to the shared `.tabs` /
173
+ `.tab-pane` in runs.css; Extra was the biggest panel in the app and the only
174
+ big one that never adopted it.
175
+
176
+ The grouping is what you DO, in the order you do it: connect something,
177
+ decide where work goes, set the rules, read what it cost, look a provider up.
178
+ Routing carries the band ladder AND the lane table together because a band is
179
+ arithmetic and the lane is the decision — they were two cards you scrolled
180
+ between. */
181
+ function exSetupTab(d, body) {
182
+ const out = frag();
183
+ // THE BOUNDARY CARD RENDERS ALWAYS. It is the one thing a first-time reader
184
+ // has to see before they connect anything.
185
+ out.append(exBoundaryCard());
186
+ if (!(d.list && d.list.gate && d.list.gate.connected)) out.append(exGateNotice(d));
187
+ out.append(exToolsCard(d, body));
188
+ out.append(exProfilesCard(d, body));
189
+ return out;
190
+ }
191
+ function exRoutingTab(d, body, edits) {
192
+ const out = frag();
193
+ out.append(exRoutingCard(d, body, edits));
194
+ // v0.55.0 — the second ladder, directly below the bands. One tab, two halves,
195
+ // no seventh tab: a band and a position are two answers to the same question
196
+ // and reading them apart from each other is how the doc checker ended up
197
+ // resolving the writer's band for a release.
198
+ out.append(exSlotCard(d, body, edits));
199
+ out.append(exLanesCard(d));
200
+ return out;
201
+ }
202
+ function exLimitsTab(d, body, edits) {
203
+ const out = frag();
204
+ out.append(exGuardrailsCard(d, body, edits));
205
+ return out;
206
+ }
207
+ function exSpendingTab(d) {
208
+ const out = frag();
209
+ out.append(exCostCard(d));
210
+ return out;
211
+ }
212
+ function exProvidersTab(d) {
213
+ const out = frag();
214
+ out.append(exProvidersCard(d));
215
+ return out;
216
+ }
217
+
218
+ /* ── TAB 6 — RECOVERY (v0.54.0) ──────────────────────────────────────────────
219
+
220
+ A failed foreign dispatch is a POSITION, not a blank page. This tab renders
221
+ what the position IS and hands over the two commands that act on it.
222
+
223
+ THE FREE/PAID LINE IS VISIBLE HERE, not hidden. `orc extra journal list` and
224
+ `orc extra reconcile` are deterministic and cost no model tokens, so both are
225
+ real BUTTONS. `orc extra resume-slice` composes a slice for a dispatch that
226
+ WILL cost money, and the dispatch itself is a lane action — so Recovery ships
227
+ copy-able commands and NO Run button. The panel never runs a lane; that is the
228
+ founding boundary, narrowed in v0.50.0 and still binding.
229
+
230
+ EVERY STATE WORD IS THE CLI'S — `resumable`, `nothing-to-resume`,
231
+ `no-journal`, `complete`, `in-flight`. Never a friendlier synonym and never a
232
+ translated one: a simplified state word is a state that does not exist.
233
+
234
+ A ROW WITH NOTHING TO SHOW KEEPS ITS SLOT. `nothing-to-resume` and `complete`
235
+ render as rows, not as filtered-out entries — the `used 0/20` and `TOO_RECENT`
236
+ rule. Filtering them makes "there was nothing to resume" and "ORC did not
237
+ look" identical. */
238
+ function exRecoveryTab(d, body) {
239
+ const out = frag();
240
+ out.append(exJournalCard(d, body));
241
+ out.append(exPruneCard(body));
242
+ return out;
243
+ }
244
+
245
+ // Which row is open, so a prune or a re-render does not close it — the Runs-row
246
+ // rule. One row open at a time, detail fetched on first open, EXPANDED IN PLACE:
247
+ // there is no detail box below the list.
248
+ let EX_JOURNAL_OPEN = null;
249
+
250
+ function exJournalCard(d, body) {
251
+ const c = card(t("extra.recovery.title"));
252
+ c.append(el("div", "note", t("extra.recovery.note")));
253
+ c.append(exWhy(t("extra.recovery.noteWhy")));
254
+
255
+ const j = d.journal;
256
+ if (!j) {
257
+ c.append(empty(t("extra.recovery.none"), t("extra.recovery.noneHint")));
258
+ return c;
259
+ }
260
+ const rows = j.journals || [];
261
+ const head = el("div", "row-actions");
262
+ head.append(chip(t("extra.recovery.count", { n: j.entries }), j.entries ? "info" : "idle"));
263
+ // The ONE state this listing exists to make visible. It renders even at zero,
264
+ // because an absent count and a healthy one must never look the same.
265
+ head.append(chip(t("extra.recovery.orphans", { n: j.orphans }), j.orphans ? "bad" : "ok"));
266
+ if (j.in_flight) head.append(chip(t("extra.recovery.inFlight", { n: j.in_flight }), "warn"));
267
+ head.append(el("span", "note", t("extra.recovery.retention", { days: j.retention_days })));
268
+ c.append(head);
269
+
270
+ if (!rows.length) {
271
+ c.append(empty(t("extra.recovery.none"), t("extra.recovery.noneHint")));
272
+ return c;
273
+ }
274
+
275
+ const list = el("div", "ex-rec-list");
276
+ const entries = [];
277
+ for (const r of rows) entries.push(exJournalRow(r, list, entries, body));
278
+ c.append(list);
279
+ return c;
280
+ }
281
+
282
+ function exJournalRow(r, list, entries, body) {
283
+ const row = el("div", "ex-rec-row");
284
+ const headBtn = el("button", "ex-rec-head");
285
+ headBtn.type = "button";
286
+ const pane = el("div", "ex-rec-pane");
287
+ const entry = { row, headBtn, pane, task: r.task_id, loaded: false };
288
+
289
+ headBtn.append(el("span", "mono ex-rec-task", r.task_id));
290
+ const who = el("span", "note ex-rec-who", `${r.profile || "—"}/${r.model_requested || "?"} · ${r.engine || "?"}`);
291
+ headBtn.append(who);
292
+ // The CLI computed these three; the panel repeats them.
293
+ const chips = el("span", "ex-rec-chips");
294
+ if (r.live) chips.append(chip(t("extra.recovery.chipLive"), "warn"));
295
+ else if (r.orphan) chips.append(chip(t("extra.recovery.chipOrphan"), "bad"));
296
+ else if (r.outcome) chips.append(chip(r.outcome, r.outcome === "done" ? "ok" : r.outcome === "partial" ? "warn" : "bad"));
297
+ if (r.attempts > 1) chips.append(chip(t("extra.recovery.chipAttempts", { n: r.attempts }), "info"));
298
+ // FIDELITY IS NEVER RENDERED STRONGER THAN IT IS. A `streamed-opaque` journal
299
+ // has no per-turn tool attribution, and a reader must be able to see that
300
+ // before they trust `last action`.
301
+ if (r.journal_fidelity) chips.append(chip(r.journal_fidelity, r.journal_fidelity === "per-turn" ? "info" : "idle"));
302
+ headBtn.append(chips);
303
+ headBtn.append(el("span", "ex-rec-caret", "▸"));
304
+
305
+ headBtn.addEventListener("click", () => {
306
+ const open = !row.classList.contains("open");
307
+ for (const e of entries) exJournalSetOpen(e, false);
308
+ exJournalSetOpen(entry, open, body);
309
+ EX_JOURNAL_OPEN = open ? r.task_id : null;
310
+ });
311
+
312
+ row.append(headBtn, pane);
313
+ list.append(row);
314
+ if (EX_JOURNAL_OPEN === r.task_id) exJournalSetOpen(entry, true, body);
315
+ return entry;
316
+ }
317
+
318
+ function exJournalSetOpen(entry, open, body) {
319
+ entry.row.classList.toggle("open", open);
320
+ entry.headBtn.setAttribute("aria-expanded", String(open));
321
+ if (!open || entry.loaded) return;
322
+ entry.loaded = true;
323
+ entry.pane.replaceChildren(skeleton(2));
324
+ read("/api/extra/reconcile?task=" + encodeURIComponent(entry.task))
325
+ .then((r) => entry.pane.replaceChildren(exReconcileView(r.data, entry.task, body)))
326
+ .catch((e) => entry.pane.replaceChildren(failBox(e)));
327
+ }
328
+
329
+ /* THE POSITION, rendered. Every line here is a field of `orc extra reconcile
330
+ --json`; the panel computes nothing — not the state, not the line counts, not
331
+ the verdict, and above all not the prose beside the verdict. Writing
332
+ "probably your wifi" next to `network` would be the panel deciding what a
333
+ verdict means, which is the Flow-stepper rule. */
334
+ function exReconcileView(v, task, body) {
335
+ const out = el("div", "stack stack-sm");
336
+ if (!v) return failBox(new Error(t("extra.recovery.readFailed")));
337
+
338
+ // The state word, verbatim, with the CLI's own sentence under it.
339
+ const st = el("div", "row-actions");
340
+ st.append(chip(v.state, v.state === "resumable" ? "ok" : v.state === "complete" ? "info" : v.state === "in-flight" ? "bad" : "idle"));
341
+ if (v.attempt) st.append(el("span", "note", t("extra.recovery.attempt", { n: v.attempt, of: v.attempts_total })));
342
+ if (v.reason) st.append(el("span", "mono note", v.reason));
343
+ out.append(st);
344
+
345
+ if (!v.ok) {
346
+ out.append(el("div", "note", v.error || ""));
347
+ return out;
348
+ }
349
+
350
+ if (!v.reported_back) out.append(el("div", "note bad", t("extra.recovery.neverReported")));
351
+
352
+ // WHOSE FAULT IT WAS, in the CLI's words, with its evidence.
353
+ if (v.attribution && v.attribution.verdict) {
354
+ const a = el("div", "ex-rec-attr");
355
+ const h = el("div", "row-actions");
356
+ h.append(chip(v.attribution.verdict, v.attribution.verdict === "worker" ? "warn" : v.attribution.verdict === "orc" ? "bad" : "info"));
357
+ h.append(el("span", "ex-probe-head", t("extra.recovery.attribution")));
358
+ a.append(h);
359
+ a.append(el("div", "note", v.attribution.why));
360
+ for (const e of v.attribution.evidence || []) a.append(el("div", "note mono", e));
361
+ // THE FIELD THAT CHANGES THE RECOVERY.
362
+ if (v.attribution.fallback_would_also_fail) a.append(el("div", "note bad", t("extra.recovery.holdWave")));
363
+ out.append(a);
364
+ }
365
+
366
+ // THE PER-FILE POSITION. An untouched file keeps its row: it is the strongest
367
+ // signal there is, and hiding it would make "unchanged" and "not looked at"
368
+ // the same picture.
369
+ if ((v.files || []).length) {
370
+ const box = el("div", "ex-rec-files");
371
+ box.append(el("div", "ex-probe-head", t("extra.recovery.position")));
372
+ for (const f of v.files) {
373
+ const line = el("div", "row-actions ex-rec-file");
374
+ line.append(el("span", "mono", f.path));
375
+ line.append(chip(f.state, f.state === "created" || f.state === "modified" ? "warn" : f.state === "reverted" || f.state === "deleted" ? "bad" : "idle"));
376
+ // UNKNOWN IS NOT ZERO. A line count that would mix two people's changes
377
+ // reads as an em dash and never as `+0 −0`.
378
+ line.append(
379
+ el("span", "note mono", f.numstat.added === null ? "—" : `+${f.numstat.added} −${f.numstat.removed}`)
380
+ );
381
+ box.append(line);
382
+ }
383
+ out.append(box);
384
+ }
385
+
386
+ // A FENCE BREACH, surfaced here as well as at the worktree delta — because a
387
+ // crashed dispatch is exactly when that check never ran.
388
+ for (const u of v.touched_undeclared || [])
389
+ out.append(el("div", "note bad", t("extra.recovery.undeclared", { path: u.path })));
390
+
391
+ if (v.last_action) out.append(el("div", "note", t("extra.recovery.lastAction", { action: v.last_action })));
392
+ if (v.journal_fidelity_note) out.append(el("div", "note", v.journal_fidelity_note));
393
+ if (v.unreadable_progress_lines)
394
+ out.append(el("div", "note bad", t("extra.recovery.torn", { n: v.unreadable_progress_lines })));
395
+
396
+ // A RECOVERED VECTOR IS A FLOOR AND SAYS SO. It is drawn distinctly and its
397
+ // note is not optional chrome.
398
+ if (v.partial_usage) {
399
+ const u = el("div", "ex-rec-floor");
400
+ u.append(docTokenBar(v.partial_usage));
401
+ u.append(el("div", "note bad", v.partial_usage_note));
402
+ out.append(u);
403
+ }
404
+
405
+ // CARRIED FORWARD, UNEVALUATED.
406
+ if ((v.acceptance || []).length) {
407
+ const acc = el("div", "ex-rec-acc");
408
+ acc.append(el("div", "ex-probe-head", t("extra.recovery.acceptance")));
409
+ for (const a of v.acceptance) acc.append(el("div", "note mono", a));
410
+ acc.append(el("div", "note", v.acceptance_note));
411
+ out.append(acc);
412
+ }
413
+
414
+ // A REFUSAL IS RENDERED AS A REFUSAL WITH ITS REASON, never as a disabled
415
+ // control with no explanation.
416
+ if (v.blocked_by) out.append(el("div", "note bad", t("extra.recovery.blocked", { why: v.blocked_by })));
417
+
418
+ if (v.resume_target) {
419
+ const rt = el("div", "row-actions");
420
+ rt.append(chip(v.resume_target.kind, v.resume_target.kind === "extra" || v.resume_target.kind === "claude" ? "ok" : "warn"));
421
+ rt.append(el("span", "ex-probe-head", t("extra.recovery.target")));
422
+ out.append(rt);
423
+ out.append(el("div", "note", v.resume_target.why));
424
+ }
425
+
426
+ // THE TWO COMMANDS. `reconcile` is free and is the button that opened this
427
+ // row; the paid half is a command you copy and run where lanes run.
428
+ if (v.next && v.state === "resumable" && !(v.reverted || []).length)
429
+ out.append(laneCommand("orc extra resume-slice " + task + " --out .orc/" + task + ".resume.json", t("extra.recovery.resumeWhy")));
430
+ out.append(laneCommand("orc extra reconcile " + task, t("extra.recovery.reconcileWhy")));
431
+ return out;
432
+ }
433
+
434
+ /* Preview-then-apply, and the preview NAMES EVERY DIRECTORY. A count is not
435
+ consent, one mutation at a time, and nothing ever runs automatically. Only a
436
+ journal whose EVERY attempt closed `done` 30+ days ago is ever a candidate, so
437
+ the record of a dispatch that never reported back can never be swept. */
438
+ function exPruneCard(body) {
439
+ const c = card(t("extra.recovery.prune.title"));
440
+ c.append(el("div", "note", t("extra.recovery.prune.note")));
441
+ const acts = el("div", "row-actions");
442
+ const preview = el("button", "btn btn-ghost btn-sm", t("extra.recovery.prune.preview"));
443
+ const apply = el("button", "btn btn-sm", t("extra.recovery.prune.apply"));
444
+ preview.type = "button";
445
+ apply.type = "button";
446
+ // The apply button stays disabled until a preview was fetched — the
447
+ // maintenance rule, and the reason the list below is not decoration.
448
+ apply.disabled = true;
449
+ const list = el("div", "stack stack-sm");
450
+ preview.addEventListener("click", async () => {
451
+ const r = await read("/api/extra/journal/prune/preview").catch((e) => ({ data: null, error: e }));
452
+ const d = r.data;
453
+ list.replaceChildren();
454
+ if (!d || !(d.candidates || []).length) {
455
+ list.append(el("div", "note ok", t("extra.recovery.prune.none")));
456
+ apply.disabled = true;
457
+ return;
458
+ }
459
+ list.append(el("div", "note warn", t("extra.recovery.prune.would", { n: d.candidates.length })));
460
+ for (const x of d.candidates) list.append(el("div", "note mono", `${x.task_id} · ${x.dir}`));
461
+ // WHY EACH ONE IS KEPT is as much of the answer as why one goes.
462
+ for (const k of d.kept || []) list.append(el("div", "note", `${k.task_id} — ${k.why}`));
463
+ apply.disabled = false;
464
+ });
465
+ apply.addEventListener("click", async () => {
466
+ const r = await post("/api/extra/journal/prune", {});
467
+ toast(r.command, r.ok ? "ok" : "bad", r.output);
468
+ renderExtra(body);
469
+ });
470
+ acts.append(preview, apply);
471
+ c.append(acts, list);
472
+ return c;
473
+ }
474
+
475
+ /* THE RELIABILITY STRIP (Spending tab). A provider that drops one dispatch in
476
+ three is a fact that belongs where the user CHOOSES a profile, not in
477
+ folklore.
478
+
479
+ BELOW THE SAMPLE FLOOR THERE IS NO PERCENTAGE — the counts render and the
480
+ panel says `sample too small`, the identical restraint to the doctor finding:
481
+ a rate computed from three dispatches is noise with a percent sign on it. The
482
+ floor is the CLI's number, never one written here. */
483
+ function exReliabilityStrip(rel) {
484
+ const box = el("div", "ex-rel");
485
+ box.append(el("div", "ex-probe-head", t("extra.rel.title")));
486
+ if (!rel || !(rel.profiles || []).length) {
487
+ box.append(el("div", "note", t("extra.rel.none")));
488
+ return box;
489
+ }
490
+ for (const g of rel.profiles) {
491
+ const row = el("div", "ex-rel-row");
492
+ const h = el("div", "row-actions");
493
+ h.append(el("span", "mono ex-name", g.profile));
494
+ h.append(
495
+ el("span", "note", t("extra.rel.counts", { n: g.dispatches_total, failed: g.failed, resumed: g.resumed, orphaned: g.orphaned }))
496
+ );
497
+ // NO PERCENTAGE BELOW THE FLOOR.
498
+ if (g.sample_too_small) h.append(chip(t("extra.rel.tooSmall", { floor: rel.sample_floor }), "idle"));
499
+ else h.append(chip(t("extra.rel.rate", { pct: Math.round(g.failure_rate * 100) }), g.failure_rate > 0.3 ? "bad" : "ok"));
500
+ row.append(h);
501
+ const a = g.attribution || {};
502
+ // `unattributed` is ALWAYS printed, including when zero.
503
+ row.append(
504
+ el(
505
+ "div",
506
+ "note mono",
507
+ t("extra.rel.attribution", {
508
+ provider: a.provider || 0,
509
+ network: a.network || 0,
510
+ local: a.local || 0,
511
+ worker: a.worker || 0,
512
+ orc: a.orc || 0,
513
+ unattributed: g.unattributed || 0,
514
+ })
515
+ )
516
+ );
517
+ if (g.mean_time_to_failure_ms !== null && g.mean_time_to_failure_ms !== undefined)
518
+ row.append(el("div", "note", t("extra.rel.mttf", { s: Math.round(g.mean_time_to_failure_ms / 1000) })));
519
+ box.append(row);
520
+ }
521
+ // The two ABSENT counts, named rather than absorbed — the same rule the cost
522
+ // card's torn-line and undated-row warnings follow.
523
+ if (rel.unreadable_journals)
524
+ box.append(el("div", "note bad", t("extra.rel.unreadable", { n: rel.unreadable_journals })));
525
+ if (rel.journals_without_result)
526
+ box.append(el("div", "note bad", t("extra.rel.noResult", { n: rel.journals_without_result })));
527
+ return box;
528
+ }
529
+
530
+ // Which tab was open, so a write that re-renders the panel does not throw you
531
+ // back to Setup — the KN_TAB rule.
532
+ let EX_TAB = "setup";
533
+
534
+ // The panel's staged writes. EVERY entry here is an ACTION — a route with a
535
+ // body — including the config ones, which stage as `/api/config/set` rather than
536
+ // through `applyEdits`. That is why one bar can carry a routing change and a
537
+ // guardrail change together, and why `applyActions` needed no new parameter: a
538
+ // panel with two Apply buttons is a panel where you forget to press one.
539
+ let EX_EDITS = null;
540
+ let EX_BAR = null;
541
+
542
+ function exEditBar(edits, body) {
543
+ return editBar(edits, {
544
+ resetLabel: t("extra.reset.label"),
545
+ onApply: async (btn) => {
546
+ await applyActions(edits, btn);
547
+ edits.clear();
548
+ renderExtra(body);
549
+ },
550
+ onReset: () => exResetModal(edits, body),
551
+ onCancel: () => edits.clear(),
552
+ });
553
+ }
554
+
555
+ /* THE BOUNDARY CARD. It renders ALWAYS and it is never behind a hover or a
556
+ fold — the house-rules precedent. A person about to send their source code to
557
+ a third party should not have to go looking for the paragraph that says so. */
558
+ function exBoundaryCard() {
559
+ const c = card(t("extra.boundary.title"));
560
+ c.classList.add("ex-boundary");
561
+ for (const line of [
562
+ t("extra.boundary.leaves"),
563
+ t("extra.boundary.whom"),
564
+ t("extra.boundary.cannot"),
565
+ t("extra.boundary.fence"),
566
+ ])
567
+ c.append(el("p", "ex-boundary-line", line));
568
+ c.append(el("div", "note", t("extra.boundary.probe")));
569
+ return c;
570
+ }
571
+
572
+ /* The header strip. Every number is the CLI's own count; one it could not
573
+ compute renders as an em dash, never as a zero (the Knowledge rule). */
574
+ function exStrip(d) {
575
+ const strip = el("div", "ex-strip");
576
+ const counts = (d.list && d.list.counts) || null;
577
+ const item = (label, value) => {
578
+ const box = el("div", "ex-strip-item");
579
+ box.append(el("span", "ex-strip-value", value));
580
+ box.append(el("span", "ex-strip-label", label));
581
+ strip.append(box);
582
+ };
583
+ item(t("extra.strip.profiles"), counts ? String(counts.profiles) : "—");
584
+ // The CLI already counts this. Recounting the array here would be a second
585
+ // idea of what "verified" means, in the panel whose whole gate it is.
586
+ item(t("extra.strip.verified"), counts ? String(counts.verified) : "—");
587
+ item(t("extra.strip.routed"), d.list ? String((d.list.routes || []).length) : "—");
588
+ item(t("extra.strip.findings"), d.doctor ? String((d.doctor.findings || []).length) : "—");
589
+ // The SOONEST deadline across every saved passphrase. An uncomputable value is
590
+ // an em dash, never a guess, and the date is the CLI's — this panel does no
591
+ // arithmetic on it.
592
+ const soon = ((d.list && d.list.profiles) || [])
593
+ .map((x) => x.session)
594
+ .filter((x) => x && x.expires_at)
595
+ .sort((a, b) => String(a.expires_at).localeCompare(String(b.expires_at)))[0];
596
+ item(t("extra.strip.passphrase"), soon ? String(soon.expires_at).slice(0, 10) : "—");
597
+ item(t("extra.strip.catalog"), (d.list && d.list.catalog_as_of) || "—");
598
+ return strip;
599
+ }
600
+
601
+ /* ---------------------------------------------------------------- profiles */
602
+
603
+ function exProfilesCard(d, body) {
604
+ // "Add a connection" is a WRITE, so it lives in the card head where every
605
+ // other write on this panel will live (W12's rail Apply bar sits below).
606
+ const add = el("button", "btn btn-sm btn-primary", t("extra.add.button"));
607
+ add.type = "button";
608
+ add.disabled = !d.providers;
609
+ add.addEventListener("click", () => exAddModal(d.providers, body, null, d.config));
610
+ const c = card(t("extra.profiles.title"), add);
611
+ if (d.errors.list) {
612
+ c.append(failBox(d.errors.list));
613
+ return c;
614
+ }
615
+ const rows = (d.list && d.list.profiles) || [];
616
+ if (!rows.length) {
617
+ c.append(empty(t("extra.profiles.none"), t("extra.profiles.noneHint")));
618
+ // A connection is added at the terminal until the connect modal lands.
619
+ c.append(el("div", "note", t("extra.profiles.addWhy")));
620
+ return c;
621
+ }
622
+ const max = exConfigValue(d.config, "extra_vault_max_attempts");
623
+ const byProfile = exFindingsByProfile(d.doctor);
624
+ for (const p of rows) c.append(exProfileRow(p, byProfile.get(p.name) || [], max, body, d.config));
625
+ return c;
626
+ }
627
+
628
+ function exProfileRow(p, findings, maxAttempts, body, cfg) {
629
+ const row = el("div", "ex-profile");
630
+
631
+ const top = el("div", "row-actions");
632
+ top.append(el("span", "mono ex-name", p.name));
633
+ // `provider/engine` is the CLI's own composite label everywhere else, so it
634
+ // is written the same way here rather than split into two friendlier words.
635
+ top.append(el("span", "note", p.provider + "/" + p.engine));
636
+ top.append(exVerifyChip(p, findings));
637
+ const vault = exVaultChip(p, maxAttempts);
638
+ if (vault) top.append(vault);
639
+ const sess = exSessionChip(p);
640
+ if (sess) top.append(sess);
641
+ row.append(top);
642
+
643
+ const kv = [
644
+ [t("extra.profile.engine"), p.engine],
645
+ [t("extra.profile.base"), p.anthropic_base_url || p.base_url || "—"],
646
+ [t("extra.profile.region"), p.region === "default" ? "—" : p.region],
647
+ [t("extra.profile.credential"), exCredentialLine(p)],
648
+ [
649
+ t("extra.profile.modelsLabel"),
650
+ p.models_seen.length ? t("extra.profile.models", { n: p.models_seen.length }) : t("extra.profile.modelsNone"),
651
+ ],
652
+ ];
653
+ row.append(kvList(kv));
654
+
655
+ // The model names themselves are DATA the provider returned. They are shown
656
+ // verbatim and never abbreviated into a family name — a family name is a
657
+ // model id that does not exist.
658
+ if (p.models_seen.length) {
659
+ const list = el("div", "ex-models");
660
+ for (const m of p.models_seen) list.append(el("span", "mono ex-model", m));
661
+ row.append(list);
662
+ }
663
+
664
+ if (p.privacy) row.append(el("div", "note", t("extra.profile.privacy")));
665
+
666
+ // The findings that are about THIS connection, next to it — the caution and
667
+ // the thing it is a caution about on one screen.
668
+ for (const f of findings) row.append(exFindingLine(f));
669
+
670
+ const actions = el("div", "row-actions");
671
+ const test = el("button", "btn btn-sm", t("extra.test.button"));
672
+ test.type = "button";
673
+ test.addEventListener("click", () => exTestModal(p, body, cfg));
674
+ actions.append(test);
675
+ // The countdown is only actionable through the one command that can clear it:
676
+ // a correct unlock proves the passphrase and resets the counter to zero. It is
677
+ // offered only where there is a stored key to unlock — never on a tombstone.
678
+ if ((p.credential || {}).vault && p.credential.vault.state === "stored") {
679
+ const unlock = el("button", "btn btn-ghost btn-sm", t("extra.unlock.button"));
680
+ unlock.type = "button";
681
+ unlock.addEventListener("click", () => exUnlockModal(p, body));
682
+ actions.append(unlock);
683
+ }
684
+ // The passphrase's own two actions. EXTEND re-opens the picker with a new
685
+ // deadline measured from today — never an auto-extend on use, because a
686
+ // deadline that renews itself every time you use it is not a deadline.
687
+ if (p.session) {
688
+ const extend = el("button", "btn btn-ghost btn-sm", t("extra.session.extend"));
689
+ extend.type = "button";
690
+ extend.addEventListener("click", () => exSessionModal(p, body, cfg, true));
691
+ actions.append(extend);
692
+ if (p.session.state !== "ABSENT") {
693
+ const forget = el("button", "btn btn-ghost btn-sm", t("extra.session.forget"));
694
+ forget.type = "button";
695
+ forget.addEventListener("click", () => exSessionForgetModal(p, body));
696
+ actions.append(forget);
697
+ }
698
+ }
699
+ const drop = el("button", "btn btn-ghost btn-sm", t("extra.remove.button"));
700
+ drop.type = "button";
701
+ drop.addEventListener("click", () => exRemoveModal(p, body));
702
+ actions.append(drop);
703
+ row.append(actions);
704
+ return row;
705
+ }
706
+
707
+ // THE DEADLINE, beside the vault chip. Every word and every date is the CLI's:
708
+ // the state is COMPUTED there on read and never stored, so this renders four
709
+ // answers and decides none of them. `not saved` KEEPS ITS SLOT on a vaulted
710
+ // connection — that is the state a run STOPS on, and a missing chip would make
711
+ // it look like nothing was wrong.
712
+ function exSessionChip(p) {
713
+ const sn = p.session;
714
+ if (!sn) return null;
715
+ if (sn.state === "ACTIVE") return chip(t("extra.session.active", { date: String(sn.expires_at).slice(0, 10) }), "ok");
716
+ if (sn.state === "EXPIRING")
717
+ return chip(t("extra.session.expiring", { date: String(sn.expires_at).slice(0, 10), days: sn.days_left }), "warn");
718
+ if (sn.state === "EXPIRED") return chip(t("extra.session.expired"), "bad");
719
+ return chip(t("extra.session.none"), "warn");
720
+ }
721
+
722
+ // UNVERIFIED and STALE are the CLI's states, and they arrive as doctor findings
723
+ // rather than being recomputed here: this panel has no idea what
724
+ // `extra_verify_max_days` means and must not learn.
725
+ function exVerifyChip(p, findings) {
726
+ if (findings.some((f) => f.id === "extra-unverified")) return chip(t("extra.profile.never"), "warn");
727
+ if (!p.verified_at) return chip(t("extra.profile.never"), "warn");
728
+ const stale = findings.some((f) => f.id === "extra-stale-verify");
729
+ const when = String(p.verified_at).slice(0, 10);
730
+ const label =
731
+ t("extra.profile.verifiedAt", { when, how: p.verify_method || "—" }) +
732
+ (p.latency_ms ? " · " + t("extra.profile.latency", { ms: p.latency_ms }) : "");
733
+ return chip(label, stale ? "warn" : "ok");
734
+ }
735
+
736
+ function exCredentialLine(p) {
737
+ const c = p.credential || {};
738
+ // v0.51.0 — a LOCAL TOOL may hold its own credential, and then ORC has nothing
739
+ // to send and nothing is missing. `found`/`not found` are both wrong, so the
740
+ // CLI answers `present: null` and this says what is actually true.
741
+ if (c.source === "tool") return t("extra.profile.credentialTool");
742
+ const where =
743
+ c.source === "vault"
744
+ ? t("extra.profile.credentialVault")
745
+ : t("extra.profile.credentialEnv", { name: c.key_name || "—" });
746
+ return where + " · " + (c.present ? t("extra.profile.present") : t("extra.profile.missing"));
747
+ }
748
+
749
+ // The vault chip carries the CLI's own state word plus the countdown when it is
750
+ // non-zero. A zero countdown KEEPS ITS SLOT and reads as no attempts used — a
751
+ // countdown toward a destructive action that is not shown is indistinguishable
752
+ // from no countdown at all.
753
+ function exVaultChip(p, maxAttempts) {
754
+ const v = (p.credential || {}).vault;
755
+ if (!v) return null;
756
+ const used = Number(v.attempts_used) || 0;
757
+ const count =
758
+ used > 0 && maxAttempts !== null
759
+ ? t("extra.vault.attempts", { used, max: maxAttempts })
760
+ : used > 0
761
+ ? t("extra.vault.attempts", { used, max: "—" })
762
+ : t("extra.vault.attemptsNone");
763
+ if (v.state === "wiped") return chip(t("extra.vault.wiped"), "bad");
764
+ if (v.state === "none") return chip(t("extra.vault.none"), "warn");
765
+ return chip(t("extra.vault.stored") + " · " + count, used > 0 ? "warn" : "ok");
766
+ }
767
+
768
+ /* ---------------------------------------------------------------- findings */
769
+
770
+ function exFindingsByProfile(doctor) {
771
+ const map = new Map();
772
+ for (const f of (doctor && doctor.findings) || []) {
773
+ if (!f.profile) continue;
774
+ if (!map.has(f.profile)) map.set(f.profile, []);
775
+ map.get(f.profile).push(f);
776
+ }
777
+ return map;
778
+ }
779
+
780
+ function exFindingLine(f) {
781
+ const line = el("div", "note ex-finding");
782
+ // The id and the message are the CLI's words. Never softened, never rewritten.
783
+ line.append(chip(f.id, f.fixable === false ? "bad" : "warn"));
784
+ line.append(document.createTextNode(" " + f.message));
785
+ if (f.fixable === false) line.append(el("div", "note", t("extra.findings.notFixable")));
786
+ return line;
787
+ }
788
+
789
+ function exFindingsCard(d) {
790
+ const all = (d.doctor && d.doctor.findings) || [];
791
+ // Only what belongs to no profile lands here; the rest is rendered beside the
792
+ // connection it is about.
793
+ const loose = all.filter((f) => !f.profile);
794
+ const c = card(t("extra.findings.title"));
795
+ if (!all.length) {
796
+ c.append(el("div", "note", t("extra.findings.none")));
797
+ return c;
798
+ }
799
+ for (const f of loose) c.append(exFindingLine(f));
800
+ c.append(laneCommand("orc extra doctor", t("extra.findings.cmdWhy")));
801
+ return c;
802
+ }
803
+
804
+ /* --------------------------------------------------------------- providers */
805
+
806
+ function exProvidersCard(d) {
807
+ const c = card(t("extra.providers.title"));
808
+ if (d.errors.providers) {
809
+ c.append(failBox(d.errors.providers));
810
+ return c;
811
+ }
812
+ const cat = d.providers || {};
813
+ c.append(el("div", "note", t("extra.providers.sub")));
814
+ c.append(exWhy(t("extra.providers.subWhy")));
815
+ if (cat.stale) c.append(el("div", "banner banner-bad", t("extra.providers.stale")));
816
+
817
+ const grid = el("div", "ex-provider-grid");
818
+ for (const p of cat.providers || []) grid.append(exProviderTile(p));
819
+ c.append(grid);
820
+ // The one thing the catalog deliberately does NOT carry, said out loud so
821
+ // nobody goes looking for a model dropdown that will never exist.
822
+ c.append(el("div", "note", t("extra.providers.noModels")));
823
+ return c;
824
+ }
825
+
826
+ function exProviderTile(p) {
827
+ const tile = el("div", "ex-provider");
828
+ // id and label are catalog data: rendered as written, never translated.
829
+ tile.append(el("div", "mono ex-provider-id", p.id));
830
+ tile.append(el("div", "ex-provider-label", p.label));
831
+ const eng = el("div", "row-actions");
832
+ for (const e of p.engines || []) eng.append(chip(e, "info"));
833
+ tile.append(eng);
834
+ if ((p.regions || []).length) {
835
+ const r = el("div", "note");
836
+ r.append(document.createTextNode(t("extra.providers.regions") + ": " + p.regions.map((x) => x.id).join(", ")));
837
+ tile.append(r);
838
+ }
839
+ const links = el("div", "row-actions");
840
+ if (p.docs_url) links.append(exLink(p.docs_url, t("extra.providers.docs")));
841
+ if (p.terms_url) links.append(exLink(p.terms_url, t("extra.providers.terms")));
842
+ if (links.childNodes.length) tile.append(links);
843
+ return tile;
844
+ }
845
+
846
+ // A catalog URL opens in a new tab with no referrer. The shell already sets a
847
+ // no-referrer policy; `rel` says it again at the link, because this is the one
848
+ // place in the panel that points off the machine.
849
+ function exLink(href, label) {
850
+ const a = el("a", "ex-link", label);
851
+ a.href = href;
852
+ a.target = "_blank";
853
+ a.rel = "noreferrer noopener";
854
+ return a;
855
+ }
856
+
857
+ // One config key's effective value, straight from `orc config list --json`.
858
+ // `null` when the read failed — which is what makes the attempt ceiling render
859
+ // as an em dash rather than as a number this panel made up.
860
+ function exConfigValue(cfg, key) {
861
+ const row = ((cfg && cfg.keys) || []).find((k) => k.key === key);
862
+ return row && row.value !== undefined && row.value !== null ? row.value : null;
863
+ }
864
+
865
+ // The legal values of a key, from `orc config list --json`. THE PANEL NAMES NO
866
+ // NUMBER ITSELF — the same rule the flow dropdowns are built under, and the
867
+ // same grep test enforces it.
868
+ function exConfigOptions(cfg, key) {
869
+ const row = ((cfg && cfg.keys) || []).find((k) => k.key === key);
870
+ return (row && Array.isArray(row.options) ? row.options : []).map(String);
871
+ }
872
+
873
+ /* ------------------------------------------- the passphrase and its deadline
874
+
875
+ A passphrase stored on the same machine as the vault it opens is NOT A SECOND
876
+ FACTOR ANY MORE. It is a DEADLINE. Every surface that shows the countdown
877
+ repeats the CLI's own sentence saying so, because a promise the product does
878
+ not keep is worse than a feature it does not have.
879
+
880
+ At CONNECT TIME this modal is NOT DISMISSIBLE. It has exactly one other
881
+ button, and it is destructive and named — "do not save, disconnect this
882
+ connection" — because a modal with genuinely no way out is a trap the first
883
+ time a write fails, and an escape that DESTROYS the thing being configured
884
+ cannot be pressed by accident and leaves no half-configured state behind. */
885
+ function exSessionModal(p, body, cfg, dismissible, pastedKey) {
886
+ const form = el("div", "stack stack-sm");
887
+ form.append(el("div", "note", p.name + " " + "·" + " " + p.provider + "/" + p.engine));
888
+
889
+ const pass = exInput(t("extra.save.passPh"), "password");
890
+ const again = exInput(t("extra.save.againPh"), "password");
891
+ form.append(exField(t("extra.save.pass"), pass, t("extra.save.passHint")));
892
+ form.append(exField(t("extra.save.again"), again));
893
+
894
+ const opts = exConfigOptions(cfg, "extra_passphrase_ttl_days");
895
+ const current = String(exConfigValue(cfg, "extra_passphrase_ttl_days") || "");
896
+ const ttl = exSelect(opts.map((v) => ({ value: v, label: t("extra.session.days", { n: v }) })));
897
+ if (opts.indexOf(current) !== -1) ttl.value = current;
898
+ form.append(exField(t("extra.session.keepFor"), ttl, t("extra.session.keepForHint")));
899
+
900
+ // THE DEADLINE AS A DATE, live under the picker. "30 days" is not something a
901
+ // person can plan around; a date is.
902
+ const when = el("div", "note");
903
+ const paint = () => {
904
+ const d = new Date(Date.now() + Number(ttl.value || 0) * 86400000);
905
+ when.textContent = t("extra.session.until", { date: d.toISOString().slice(0, 10) });
906
+ };
907
+ ttl.addEventListener("change", paint);
908
+ paint();
909
+ form.append(when);
910
+
911
+ form.append(el("div", "banner", t("extra.session.honesty")));
912
+ const out = el("div", "note");
913
+ form.append(out);
914
+
915
+ const save = async (close) => {
916
+ out.textContent = "";
917
+ if (!pass.value || pass.value !== again.value) {
918
+ out.textContent = t("extra.save.mismatch");
919
+ return;
920
+ }
921
+ setBusy(true);
922
+ try {
923
+ // TWO CALLS, because storing the KEY and caching the PASSPHRASE are two
924
+ // different acts and only one of them has a deadline.
925
+ if (pastedKey) {
926
+ const r = await post("/api/extra/ping", { profile: p.name, key: pastedKey, passphrase: pass.value });
927
+ const d = (r && r.data) || null;
928
+ if (!d || !d.vault || !d.vault.stored) {
929
+ out.textContent = (d && d.vault && d.vault.error) || (r && r.error) || t("common.loadFail");
930
+ return;
931
+ }
932
+ }
933
+ const r2 = await post("/api/extra/session/save", { profile: p.name, ttl_days: ttl.value, passphrase: pass.value });
934
+ const d2 = (r2 && r2.data) || null;
935
+ if (!d2 || !d2.ok) {
936
+ out.textContent = (d2 && d2.error) || (r2 && r2.error) || t("common.loadFail");
937
+ return;
938
+ }
939
+ close();
940
+ exRefresh(body);
941
+ } catch (e) {
942
+ out.textContent = String(e.message);
943
+ } finally {
944
+ setBusy(false);
945
+ pass.value = "";
946
+ again.value = "";
947
+ }
948
+ };
949
+
950
+ const actions = [];
951
+ if (dismissible) actions.push({ label: t("common.cancel"), onClick: (c) => c() });
952
+ else
953
+ actions.push({
954
+ // NOT a cancel. It deletes the key and the connection just made, and it
955
+ // says so in the label.
956
+ label: t("extra.session.abandon"),
957
+ cls: "btn-ghost",
958
+ onClick: async (c) => {
959
+ setBusy(true);
960
+ try {
961
+ await post("/api/extra/remove", { name: p.name, reason: "the passphrase was not saved" });
962
+ } catch (_) {
963
+ } finally {
964
+ setBusy(false);
965
+ }
966
+ c();
967
+ exRefresh(body);
968
+ },
969
+ });
970
+ actions.push({ label: t("extra.session.save"), cls: "btn-primary", onClick: (c) => save(c) });
971
+
972
+ modal({ title: t("extra.session.title"), body: form, actions, dismissible: dismissible !== false ? true : false });
973
+ }
974
+
975
+ function exSessionForgetModal(p, body) {
976
+ const form = el("div", "stack stack-sm");
977
+ form.append(el("div", "note", t("extra.session.forgetWhat", { name: p.name })));
978
+ modal({
979
+ title: t("extra.session.forget"),
980
+ body: form,
981
+ actions: [
982
+ { label: t("common.cancel"), onClick: (c) => c() },
983
+ {
984
+ label: t("extra.session.forget"),
985
+ cls: "btn-danger",
986
+ onClick: async (c) => {
987
+ c();
988
+ setBusy(true);
989
+ try {
990
+ await post("/api/extra/session/forget", { profile: p.name });
991
+ } finally {
992
+ setBusy(false);
993
+ exRefresh(body);
994
+ }
995
+ },
996
+ },
997
+ ],
998
+ });
999
+ }
1000
+
1001
+ /* ==================================== the connect modal and the wire (W11) ==
1002
+ THE PANEL STILL NEVER TALKS TO A MODEL. Testing a connection POSTs to a route
1003
+ that subprocesses `orc extra ping <profile> --json` — the same command you
1004
+ would type — so the HTTPS call is the CLI's and every validator, redaction and
1005
+ exit code comes for free. That is the whole of what v0.50.0 narrowed in the
1006
+ `orc ui` boundary: a probe is a DIAGNOSTIC, the family `orc doctor` is in.
1007
+
1008
+ THE LIFECYCLE IS THE CLI'S: test first, then store. A pasted key is held for
1009
+ the probe and NEVER written before the test is green, and a failed test on a
1010
+ never-verified profile removes the profile the CLI just wrote — so a typo'd
1011
+ key cannot rot in a vault nobody can open. The panel reimplements none of
1012
+ that; it calls `add`, then `ping`, and renders what came back.
1013
+
1014
+ A PASTED KEY LEAVES THE BROWSER ONCE, in a POST body over loopback with the
1015
+ per-launch token, and reaches the CLI on STDIN. It is never put in a URL,
1016
+ never kept past the request, never written to localStorage and never
1017
+ re-rendered as a field value. */
1018
+
1019
+ // What the probe will do and what it costs, said BEFORE the button — the CLI
1020
+ // picks the rung, so this describes its ladder and never predicts the outcome.
1021
+ // Which rung actually answered comes back as `verify_method`, verbatim.
1022
+ function exProbeNote(engine) {
1023
+ const box = el("div", "ex-probe");
1024
+ box.append(el("div", "ex-probe-head", t("extra.probe.head")));
1025
+ // v0.51.0 — a LOCAL TOOL climbs a different ladder from an endpoint, and its
1026
+ // paid rung is not a cheap ping: the tool loads its own system prompt and tool
1027
+ // schemas before it sends anything, which is thousands of input tokens against
1028
+ // an endpoint probe's ten. The two are therefore quoted separately, and both
1029
+ // BEFORE the button.
1030
+ if (engine === "cli") {
1031
+ box.append(el("div", "note", t("extra.probe.cliFree")));
1032
+ box.append(el("div", "note", t("extra.probe.cliPaid")));
1033
+ return box;
1034
+ }
1035
+ box.append(el("div", "note", t("extra.probe.free")));
1036
+ box.append(el("div", "note", t("extra.probe.paid")));
1037
+ return box;
1038
+ }
1039
+
1040
+ /* THE WIRE. A pulse travels from the credential to the provider and lands green
1041
+ or red — the feedback that makes an invisible network call feel like an event.
1042
+ The dot's animation is INFINITE while the probe is in flight, which is why
1043
+ 04-motion.css REMOVES it under reduced motion rather than capping it: a capped
1044
+ infinite animation freezes mid-cycle. */
1045
+ function exWire(fromLabel, toLabel) {
1046
+ const wrap = el("div", "ex-wire");
1047
+ wrap.append(el("span", "ex-wire-end", fromLabel));
1048
+ const line = el("span", "ex-wire-line");
1049
+ line.append(el("span", "ex-wire-dot"));
1050
+ wrap.append(line);
1051
+ wrap.append(el("span", "ex-wire-end", toLabel));
1052
+ wrap.arm = () => {
1053
+ wrap.classList.remove("ex-wire-ok", "ex-wire-bad");
1054
+ wrap.classList.add("ex-wire-live");
1055
+ };
1056
+ wrap.settle = (ok) => {
1057
+ wrap.classList.remove("ex-wire-live");
1058
+ wrap.classList.add(ok ? "ex-wire-ok" : "ex-wire-bad");
1059
+ };
1060
+ return wrap;
1061
+ }
1062
+
1063
+ // One form row, in the shape crosslink's add form already uses.
1064
+ function exField(labelText, node, hint) {
1065
+ // `ex-form-hide` is not decoration: `.field` is a flex box, and an author
1066
+ // `display` beats the UA stylesheet's `[hidden]` rule — so a field this form
1067
+ // hides needs a rule that names it. See css/panels/extra.css.
1068
+ const f = el("label", "field ex-form-hide");
1069
+ f.append(el("span", "field-label", labelText));
1070
+ f.append(node);
1071
+ if (hint) f.append(el("span", "field-hint", hint));
1072
+ return f;
1073
+ }
1074
+
1075
+ function exInput(placeholder, type) {
1076
+ const i = el("input", "text-input");
1077
+ i.type = type || "text";
1078
+ if (type === "password") {
1079
+ // Never remembered, never autofilled, never re-rendered with a value.
1080
+ i.autocomplete = "off";
1081
+ i.spellcheck = false;
1082
+ }
1083
+ if (placeholder) i.placeholder = placeholder;
1084
+ return i;
1085
+ }
1086
+
1087
+ function exSelect(options, onChange) {
1088
+ const sel = el("select", "text-input");
1089
+ for (const o of options) {
1090
+ const opt = el("option", null, o.label);
1091
+ opt.value = o.value;
1092
+ // A value outside its own set leads the list, is labelled, and is DISABLED:
1093
+ // the state must be visible, never re-offerable (the v0.44.0 rule).
1094
+ if (o.disabled) opt.disabled = true;
1095
+ sel.append(opt);
1096
+ }
1097
+ if (onChange) sel.addEventListener("change", onChange);
1098
+ return sel;
1099
+ }
1100
+
1101
+ /* ================================ the native-tools card, and the setup gate ==
1102
+ W6/W8 (v0.51.0). Some providers are a LOCAL TOOL rather than an endpoint, and
1103
+ a local tool can simply not be installed. When it is absent this card is the
1104
+ FIRST thing on the panel and the ONLY thing it offers — no Connect box, no
1105
+ test button, no model list, because all three are buttons that cannot succeed.
1106
+
1107
+ THE PANEL SWITCHES ON `state` AND DERIVES NOTHING. Four states arrive from
1108
+ `orc extra tools --json`, each with exactly one next action, and every label,
1109
+ command, version, URL and alternative in this card came out of that JSON. The
1110
+ card names no tool: it cannot, and a test asserts it cannot.
1111
+
1112
+ THE INSTALL OPENS THE USER'S OWN TERMINAL. Not a background job (a hidden
1113
+ subprocess makes an elevation prompt, a permissions error, an 80 MB download
1114
+ and a forty-second wait all look identical: nothing happened) and not merely a
1115
+ string to copy. The exact command renders ABOVE the button — preview-then-
1116
+ apply, unchanged — the window is theirs to read and Ctrl-C, and ORC never
1117
+ elevates. A machine with no terminal to open degrades to the command, never to
1118
+ a dead button. */
1119
+
1120
+ function exToolsCard(d, body) {
1121
+ const c = card(t("extra.tools.title"));
1122
+ const tools = (d.tools && d.tools.tools) || [];
1123
+ if (d.errors.tools) {
1124
+ c.append(failBox(d.errors.tools));
1125
+ return c;
1126
+ }
1127
+ if (!tools.length) {
1128
+ c.append(el("div", "note", t("extra.tools.none")));
1129
+ return c;
1130
+ }
1131
+ c.append(el("div", "note", t("extra.tools.sub")));
1132
+ c.append(exWhy(t("extra.tools.subWhy")));
1133
+ const grid = el("div", "ex-tool-grid");
1134
+ for (const tool of tools) grid.append(exToolBox(tool, d, body));
1135
+ c.append(grid);
1136
+ // The user will install in another terminal and come back. A card that only
1137
+ // refreshed on a full page load would send them away thinking it failed.
1138
+ const re = el("button", "btn btn-sm", t("extra.tools.recheck"));
1139
+ re.type = "button";
1140
+ re.addEventListener("click", () => exRefresh(body));
1141
+ const foot = el("div", "row-actions");
1142
+ foot.append(re);
1143
+ foot.append(el("span", "note", t("extra.tools.recheckWhy")));
1144
+ c.append(foot);
1145
+ return c;
1146
+ }
1147
+
1148
+ function exToolBox(tool, d, body) {
1149
+ const box = el("div", "ex-tool ex-tool-" + tool.state);
1150
+ const top = el("div", "row-actions");
1151
+ // The label and the binary name are catalog data: written as they arrived.
1152
+ top.append(el("span", "ex-tool-name", tool.label));
1153
+ top.append(el("span", "mono note", tool.bin));
1154
+ top.append(chip(tool.state, tool.state === "ready" ? "ok" : tool.state === "absent" ? "bad" : "warn"));
1155
+ box.append(top);
1156
+
1157
+ // v0.53.0 — ONE SENTENCE AND ONE CONTROL PER STATE. Four states carry four
1158
+ // different shapes and exactly ONE action each, and the `absent` card used to
1159
+ // stack five paragraphs above its button. Everything a state does not need in
1160
+ // order to be acted on now lives behind `exToolMore`. Nothing is removed and
1161
+ // nothing about the states, their colours or their next actions changes —
1162
+ // those are all `orc extra tools`'s answers.
1163
+ if (tool.state === "absent") {
1164
+ box.append(el("div", "note", t("extra.tools.absentWhat")));
1165
+ box.append(exInstallRow(tool, body));
1166
+ const more = exToolMore();
1167
+ // `null` MEANS THERE IS NONE — never that ORC forgot to look. The two cases
1168
+ // must not render the same, and neither may render as an empty slot.
1169
+ if (tool.no_install_alternative) {
1170
+ const alt = el("div", "note");
1171
+ alt.append(document.createTextNode(t("extra.tools.altYes") + " "));
1172
+ alt.append(el("span", "mono", tool.no_install_alternative));
1173
+ more.append(alt);
1174
+ } else {
1175
+ more.append(el("div", "note", t("extra.tools.altNone")));
1176
+ }
1177
+ if (tool.docs_url) more.append(exLink(tool.docs_url, t("extra.providers.docs")));
1178
+ box.append(more);
1179
+ return box;
1180
+ }
1181
+
1182
+ const kv = [
1183
+ [t("extra.tools.version"), tool.version || t("extra.tools.versionUnknown")],
1184
+ [t("extra.tools.floor"), tool.min_version || "—"],
1185
+ [t("extra.tools.auth"), tool.auth_detail || (tool.authed === null ? "—" : tool.authed ? t("extra.tools.authYes") : t("extra.tools.authNo"))],
1186
+ [t("extra.tools.models"), tool.models_count === null ? "—" : String(tool.models_count)],
1187
+ ];
1188
+ const kvDl = kvList(kv);
1189
+ // The auth string is the one unbounded field on this card, so it is TAGGED and
1190
+ // clamped in CSS — see `.ex-tool .kv dd.ex-auth-detail`. Found by its own
1191
+ // label rather than by an index, because kvList drops empty rows.
1192
+ const authDt = Array.from(kvDl.querySelectorAll("dt")).find((x) => x.textContent === t("extra.tools.auth"));
1193
+ if (authDt && authDt.nextElementSibling) authDt.nextElementSibling.classList.add("ex-auth-detail");
1194
+ // THE DIAGNOSTICS GO BEHIND THE DISCLOSURE. A version, a floor, an auth string
1195
+ // and a model count are what you read when something is wrong; the state chip
1196
+ // already told you whether it is. The PROBE ERROR stays out here — it is the
1197
+ // reason for the state, not detail about it.
1198
+ const more = exToolMore();
1199
+ more.append(kvDl);
1200
+ if (tool.bin_path) more.append(el("div", "note mono ex-tool-path", tool.bin_path));
1201
+ if (tool.probe_error) box.append(el("div", "note bad", tool.probe_error));
1202
+
1203
+ if (tool.state === "outdated") {
1204
+ box.append(el("div", "note", t("extra.tools.outdatedWhat")));
1205
+ box.append(exInstallRow(tool, body));
1206
+ box.append(more);
1207
+ return box;
1208
+ }
1209
+ if (tool.state === "unauthenticated") {
1210
+ box.append(el("div", "note", t("extra.tools.unauthWhat")));
1211
+ box.append(exKeyhelpRow(tool, d, body));
1212
+ box.append(more);
1213
+ return box;
1214
+ }
1215
+ // ready — and READY IS NOT THE SAME AS UNCONNECTED. `connected` / `verified`
1216
+ // are computed by `orc extra tools`, never joined here.
1217
+ const actions = el("div", "row-actions");
1218
+ if (tool.verified) {
1219
+ // A state must be VISIBLE, never re-offerable. There is NO Connect button
1220
+ // here, not a disabled one: an absent control and a dead control must not
1221
+ // look the same (the `no_install_alternative: null` rule).
1222
+ const named = tool.connected_profiles.filter((x) => x.verified_at).map((x) => x.name).join(", ");
1223
+ box.append(chip(t("extra.tools.connectedAs") + " " + named, "ok"));
1224
+ actions.append(exToolTestBtn(tool, d, body));
1225
+ } else if (tool.connected) {
1226
+ // Configured and never tested — the setup gate's own state, in the setup
1227
+ // gate's own words. The action is a test, not another connection.
1228
+ box.append(el("div", "note", t("extra.tools.connectedUntested")));
1229
+ actions.append(exToolTestBtn(tool, d, body));
1230
+ } else {
1231
+ const connect = el("button", "btn btn-sm btn-primary", t("extra.tools.connect"));
1232
+ connect.type = "button";
1233
+ connect.addEventListener("click", () => exAddModal(d.providers, body, tool, d.config));
1234
+ actions.append(connect);
1235
+ }
1236
+ // A second connection to the same tool with a different model map is
1237
+ // legitimate, so it stays available — as a SECONDARY, because it is not what
1238
+ // someone looking at a connected card came for.
1239
+ if (tool.connected) {
1240
+ const again = el("button", "btn btn-sm", t("extra.tools.addAnother"));
1241
+ again.type = "button";
1242
+ again.addEventListener("click", () => exAddModal(d.providers, body, tool, d.config));
1243
+ actions.append(again);
1244
+ }
1245
+ box.append(actions);
1246
+ box.append(more);
1247
+ return box;
1248
+ }
1249
+
1250
+ /* THE "WHY" DISCLOSURE (v0.53.0).
1251
+
1252
+ The copy problem on this panel was never that it was untranslated — both
1253
+ tables are complete. It was that DESIGN RATIONALE WAS BEING SERVED AS USER
1254
+ INSTRUCTION. "A gap is not a hole … so 'I left the hardest work on Claude on
1255
+ purpose' and 'there is no top band' can never look the same" is true, and it
1256
+ belongs in knowledge.md; as the first thing under a table it tells a
1257
+ first-time reader nothing about what to do.
1258
+
1259
+ So the shape is: the INSTRUCTION first, in Simplified Technical English (one
1260
+ sentence, active, twenty words at most — bin/webui/i18n/TERMS.md), and the
1261
+ REASONING underneath, collapsed. Nothing is deleted; the rationale keeps its
1262
+ own voice, which is the one thing TERMS.md protects. It is just no longer in
1263
+ the way. */
1264
+ function exWhy(text) {
1265
+ const d = document.createElement("details");
1266
+ d.className = "ex-more ex-why";
1267
+ const s = document.createElement("summary");
1268
+ s.textContent = t("extra.why");
1269
+ d.append(s, el("div", "note", text));
1270
+ return d;
1271
+ }
1272
+
1273
+ /* THE DISCLOSURE. A native <details>, so the open/closed state, the keyboard
1274
+ handling and the toggle are the browser's rather than a fourth hand-rolled
1275
+ expander. It is CLOSED by default because a card whose whole content is
1276
+ visible has no first thing to look at — and it holds only detail: a state
1277
+ never hides the control that acts on it. */
1278
+ function exToolMore() {
1279
+ const d = document.createElement("details");
1280
+ d.className = "ex-more";
1281
+ const s = document.createElement("summary");
1282
+ s.textContent = t("extra.tools.more");
1283
+ d.append(s);
1284
+ return d;
1285
+ }
1286
+
1287
+ // The one action that can still tell you something about a connected tool. The
1288
+ // profile it opens on is the CLI's — the first one this tool's row names.
1289
+ function exToolTestBtn(tool, d, body) {
1290
+ const btn = el("button", "btn btn-sm btn-primary", t("extra.tools.test"));
1291
+ btn.type = "button";
1292
+ btn.addEventListener("click", () => {
1293
+ const first = tool.connected_profiles[0];
1294
+ const full = ((d.list && d.list.profiles) || []).find((x) => x.name === first.name);
1295
+ if (full) exTestModal(full, body, d.config);
1296
+ });
1297
+ return btn;
1298
+ }
1299
+
1300
+ // PREVIEW THEN APPLY, unchanged: the exact command is visible ABOVE the button,
1301
+ // and the button is what runs it in a terminal the user can watch.
1302
+ function exInstallRow(tool, body) {
1303
+ const wrap = el("div", "ex-install");
1304
+ const inst = tool.install || {};
1305
+ const cmds = (inst.cmds || []).length ? inst.cmds : inst.all_cmds || [];
1306
+ if (!cmds.length) {
1307
+ wrap.append(el("div", "note", t("extra.tools.noCommand")));
1308
+ if (inst.docs_url) wrap.append(exLink(inst.docs_url, t("extra.providers.docs")));
1309
+ return wrap;
1310
+ }
1311
+ // The CLI already filtered these to this platform, so the panel picks nothing.
1312
+ const pick = cmds.length > 1 ? exSelect(cmds.map((x) => ({ value: x.manager, label: x.manager }))) : null;
1313
+ const cmdLine = el("div", "action-cmd", cmds[0].cmd);
1314
+ const setCmd = () => {
1315
+ const chosen = cmds.find((x) => x.manager === (pick ? pick.value : cmds[0].manager)) || cmds[0];
1316
+ cmdLine.textContent = chosen.cmd;
1317
+ };
1318
+ if (pick) pick.addEventListener("change", setCmd);
1319
+ wrap.append(el("div", "note", t("extra.tools.willRun")));
1320
+ if (pick) wrap.append(pick);
1321
+ wrap.append(cmdLine);
1322
+ const out = el("div", "note");
1323
+ const btn = el("button", "btn btn-sm btn-primary", t("extra.tools.install"));
1324
+ btn.type = "button";
1325
+ btn.addEventListener("click", async () => {
1326
+ btn.disabled = true;
1327
+ out.textContent = "";
1328
+ try {
1329
+ const r = await post("/api/extra/install", {
1330
+ provider: tool.provider,
1331
+ manager: pick ? pick.value : cmds[0].manager,
1332
+ });
1333
+ const j = (r && r.data) || null;
1334
+ if (!j) {
1335
+ out.textContent = (r && r.error) || t("common.loadFail");
1336
+ return;
1337
+ }
1338
+ // `launched: false` is an ANSWER, not an error — the command stays on
1339
+ // screen and the user runs it themselves.
1340
+ out.textContent = j.note;
1341
+ wrap.append(el("div", "note", t("extra.tools.neverElevates")));
1342
+ } catch (e) {
1343
+ out.textContent = String(e.message);
1344
+ } finally {
1345
+ btn.disabled = false;
1346
+ }
1347
+ });
1348
+ wrap.append(btn, out);
1349
+ return wrap;
1350
+ }
1351
+
1352
+ // Whatever `orc extra keyhelp` returned, rendered. The panel does not decide
1353
+ // which of the three routes applies and never pipes a key itself.
1354
+ function exKeyhelpRow(tool, d, body) {
1355
+ const wrap = el("div", "ex-keyhelp");
1356
+ wrap.append(el("div", "note", t("extra.tools.keyhelpSub")));
1357
+ const profiles = ((d.list && d.list.profiles) || []).filter((p) => p.provider === tool.provider);
1358
+ if (!profiles.length) {
1359
+ // The credential route is a property of a PROFILE (which variable, which
1360
+ // key), so there is nothing to compute until one exists.
1361
+ wrap.append(el("div", "note", t("extra.tools.keyhelpNoProfile")));
1362
+ const connect = el("button", "btn btn-sm", t("extra.tools.connect"));
1363
+ connect.type = "button";
1364
+ connect.addEventListener("click", () => exAddModal(d.providers, body, tool, d.config));
1365
+ wrap.append(connect);
1366
+ return wrap;
1367
+ }
1368
+ const out = el("div", "stack stack-sm");
1369
+ wrap.append(out);
1370
+ read("/api/extra/keyhelp?profile=" + encodeURIComponent(profiles[0].name))
1371
+ .then((r) => {
1372
+ const k = r.data;
1373
+ if (!k) return;
1374
+ out.append(chip(k.route, k.route === "env" ? "ok" : "warn"));
1375
+ out.append(el("div", "note", k.why));
1376
+ if (k.env_var) out.append(el("div", "note mono", k.env_var));
1377
+ if (k.note) out.append(el("div", "note", k.note));
1378
+ if (k.cmd) {
1379
+ out.append(el("div", "action-cmd", k.cmd));
1380
+ out.append(el("div", "note", t("extra.tools.loginManual")));
1381
+ }
1382
+ // v0.52.0 (D3 Part 2) — HOW to set the variable, per OS. Every command is
1383
+ // the CLI's and carries a PLACEHOLDER, never a key, so nothing here can
1384
+ // leak into a screenshot or a copy button. ORC does not run it: `setx`
1385
+ // would put the key in argv and an `export` line writes it in plaintext.
1386
+ exEnvSetBlock(out, k.env_set);
1387
+ // v0.53.3 — the route WITH A DEADLINE first, then the variable. Both
1388
+ // lines are the CLI's; the panel names no command of its own.
1389
+ if (k.vault_unlock) {
1390
+ out.append(el("div", "action-cmd", k.vault_unlock.cmd));
1391
+ out.append(el("div", "note", k.vault_unlock.why));
1392
+ }
1393
+ exEnvSetBlock(out, k.key_env);
1394
+ })
1395
+ .catch((e) => out.append(failBox(e)));
1396
+ return wrap;
1397
+ }
1398
+
1399
+ // The per-OS instruction, rendered. The panel names no command of its own — the
1400
+ // two lines and the note are all computed by `orc extra keyhelp`.
1401
+ function exEnvSetBlock(out, block) {
1402
+ if (!block) return;
1403
+ out.append(el("div", "field-label", t("extra.keyhelp.session")));
1404
+ out.append(el("div", "action-cmd", block.session));
1405
+ out.append(el("div", "field-label", t("extra.keyhelp.persist")));
1406
+ out.append(el("div", "action-cmd", block.persist));
1407
+ out.append(el("div", "note", block.persist_note));
1408
+ // The honest half of the persistent form, when there is one.
1409
+ if (block.warning) out.append(el("div", "note bad", block.warning));
1410
+ }
1411
+
1412
+ /* THE SETUP GATE. Until one connection has answered, the routing table, the
1413
+ limits, the cost report and the full provider catalogue are NOT APPENDED —
1414
+ not hidden, not disabled: absent. Every one of them is a control for work that
1415
+ cannot happen yet, and a panel that offers them teaches somebody to configure
1416
+ a routing table that will never fire.
1417
+
1418
+ It has TWO FLOORS and the CLI says which one you are on, because the
1419
+ instruction differs: with nothing installed and no key the answer is an
1420
+ INSTALL, and with a connection that has never answered the answer is TEST IT.
1421
+ Someone with neither should never be shown a Connect box that cannot succeed. */
1422
+ function exGateNotice(d) {
1423
+ const gate = (d.list && d.list.gate) || null;
1424
+ const box = el("div", "banner ex-gate");
1425
+ box.append(el("div", "ex-gate-head", t("extra.gate.title")));
1426
+ box.append(
1427
+ el("div", null, gate && gate.floor === "never-tested" ? t("extra.gate.neverTested") : t("extra.gate.noConnection"))
1428
+ );
1429
+ box.append(el("div", "note", t("extra.gate.hidden")));
1430
+ box.append(exWhy(t("extra.gate.hiddenWhy")));
1431
+ // The CLI's own sentence about what to do next, in the CLI's own words.
1432
+ if (gate && gate.next) box.append(el("div", "action-cmd", gate.next));
1433
+ return box;
1434
+ }
1435
+
1436
+ /* ---------------------------------------------------------- the add modal -- */
1437
+
1438
+ function exAddModal(cat, body, tool, cfg) {
1439
+ const providers = (cat && cat.providers) || [];
1440
+ const form = el("div", "stack stack-sm");
1441
+
1442
+ const name = exInput(t("extra.add.namePh"));
1443
+ // The provider list is the catalog's, in the catalog's order, with the
1444
+ // catalog's own ids and labels. The panel names none of them.
1445
+ const provider = exSelect(
1446
+ providers.map((p) => ({ value: p.id, label: p.id + " — " + p.label })),
1447
+ () => sync()
1448
+ );
1449
+ const engine = exSelect([]);
1450
+ const region = exSelect([]);
1451
+ const baseUrl = exInput("");
1452
+ const envKey = exInput("");
1453
+ // v0.51.0 — this placeholder used to be a hard-coded tool name, which became
1454
+ // a catalog id and therefore a provider this panel named. It is the selected
1455
+ // provider's own `cli_bin` now, or empty.
1456
+ const cliBin = exInput("");
1457
+ const cliAgent = exInput(t("extra.add.cliAgentPh"));
1458
+ const key = exInput(t("extra.add.keyPh"), "password");
1459
+
1460
+ // Env-var NAME first and selected by default (D3): an environment variable
1461
+ // your OS already protects beats a passphrase you will forget. Pasting a key
1462
+ // is the second option, and it is warned rather than hidden.
1463
+ const srcEnv = el("input");
1464
+ srcEnv.type = "radio";
1465
+ srcEnv.name = "ex-cred";
1466
+ srcEnv.checked = true;
1467
+ const srcVault = el("input");
1468
+ srcVault.type = "radio";
1469
+ srcVault.name = "ex-cred";
1470
+ // v0.52.0 — THE THIRD SOURCE, and the one that was missing. A local tool that
1471
+ // already signed itself in holds its own credential, so it needs no key from
1472
+ // ORC: no env var to set, no vault, no passphrase and no deadline. The CLI has
1473
+ // had `--tool-auth` since v0.51.0; the panel offered two radios, so a
1474
+ // signed-in tool was pushed into the vault and the vault then locked the run.
1475
+ // Offered only where it can be true: engine `cli`, on a catalog row that has a
1476
+ // binary.
1477
+ const srcTool = el("input");
1478
+ srcTool.type = "radio";
1479
+ srcTool.name = "ex-cred";
1480
+ const srcRow = el("div", "ex-cred-choice");
1481
+ const lblEnv = el("label", "ex-cred-opt");
1482
+ lblEnv.append(srcEnv, el("span", null, t("extra.add.sourceEnv")));
1483
+ const lblVault = el("label", "ex-cred-opt");
1484
+ lblVault.append(srcVault, el("span", null, t("extra.add.sourceVault")));
1485
+ const lblTool = el("label", "ex-cred-opt");
1486
+ lblTool.append(srcTool, el("span", null, t("extra.add.sourceTool")));
1487
+ srcRow.append(lblEnv, lblVault, lblTool);
1488
+ srcEnv.addEventListener("change", () => sync());
1489
+ srcVault.addEventListener("change", () => sync());
1490
+ srcTool.addEventListener("change", () => sync());
1491
+
1492
+ const fName = exField(t("extra.add.name"), name, t("extra.add.nameHint"));
1493
+ const fProvider = exField(t("extra.add.provider"), provider);
1494
+ const fEngine = exField(t("extra.add.engine"), engine, t("extra.add.engineHint"));
1495
+ const fRegion = exField(t("extra.add.region"), region);
1496
+ const fBase = exField(t("extra.add.base"), baseUrl, t("extra.add.baseHint"));
1497
+ const fCliBin = exField(t("extra.add.cliBin"), cliBin, t("extra.add.cliBinHint"));
1498
+ const fCliAgent = exField(t("extra.add.cliAgent"), cliAgent);
1499
+ const fEnvKey = exField(t("extra.add.envKey"), envKey, t("extra.add.envKeyHint"));
1500
+ const fKey = exField(t("extra.add.key"), key, t("extra.add.keyHint"));
1501
+
1502
+ const grid = el("div", "linkform");
1503
+ grid.append(fName, fProvider, fEngine, fRegion, fBase, fCliBin, fCliAgent);
1504
+ form.append(grid);
1505
+ form.append(el("div", "field-label", t("extra.add.credential")));
1506
+ form.append(srcRow);
1507
+ const credGrid = el("div", "linkform");
1508
+ credGrid.append(fEnvKey, fKey);
1509
+ form.append(credGrid);
1510
+ const vaultWarn = el("div", "banner banner-bad ex-form-hide", t("extra.add.vaultWarn"));
1511
+ form.append(vaultWarn);
1512
+ const toolNote = el("div", "banner ex-form-hide", t("extra.add.sourceToolHint"));
1513
+ form.append(toolNote);
1514
+ // The rung ladder differs per engine and so does what it COSTS, so the note is
1515
+ // repainted whenever the engine changes rather than fixed at the top of a form
1516
+ // whose engine the user has not picked yet.
1517
+ const probeNote = el("div");
1518
+ form.append(probeNote);
1519
+
1520
+ const wire = exWire(t("extra.wire.you"), t("extra.wire.provider"));
1521
+ form.append(wire);
1522
+ const result = el("div", "ex-result");
1523
+ form.append(result);
1524
+
1525
+ const row = () => providers.find((p) => p.id === provider.value) || null;
1526
+
1527
+ // Everything the form offers is derived from the catalog row: the engines this
1528
+ // provider ships, the regions it has, the variable it reads by default. A
1529
+ // field the CLI would refuse is never offered here.
1530
+ function sync() {
1531
+ const r = row();
1532
+ const engines = (r && r.engines) || [];
1533
+ const keep = engine.value;
1534
+ engine.replaceChildren();
1535
+ for (const e of engines) {
1536
+ const o = el("option", null, e);
1537
+ o.value = e;
1538
+ engine.append(o);
1539
+ }
1540
+ if (engines.includes(keep)) engine.value = keep;
1541
+
1542
+ const regions = [{ value: "default", label: "default" }].concat(
1543
+ ((r && r.regions) || []).map((x) => ({ value: x.id, label: x.id + " — " + x.label }))
1544
+ );
1545
+ const keepR = region.value;
1546
+ region.replaceChildren();
1547
+ for (const x of regions) {
1548
+ const o = el("option", null, x.label);
1549
+ o.value = x.value;
1550
+ region.append(o);
1551
+ }
1552
+ if (regions.some((x) => x.value === keepR)) region.value = keepR;
1553
+ fRegion.hidden = regions.length < 2;
1554
+
1555
+ const isCli = engine.value === "cli";
1556
+ fCliBin.hidden = !isCli;
1557
+ fCliAgent.hidden = !isCli;
1558
+ // Engine `cli` has no endpoint at all, and `api` / `claude-shim` read
1559
+ // different halves of the catalog — the placeholder is whichever base the
1560
+ // CLI will actually use, so an empty field means "the catalog's".
1561
+ fBase.hidden = isCli;
1562
+ baseUrl.placeholder = (engine.value === "claude-shim" ? r && r.anthropic_base : r && r.api_base) || "";
1563
+ cliBin.placeholder = (r && r.cli_bin) || "";
1564
+ envKey.placeholder = (r && r.env_key_default) || "";
1565
+
1566
+ probeNote.replaceChildren(exProbeNote(engine.value));
1567
+
1568
+ // The tool source only exists where a tool does. If the engine moves away
1569
+ // from `cli` while it is selected, the choice falls back to the default
1570
+ // rather than staying checked on a hidden radio.
1571
+ const toolable = isCli && !!(r && r.cli_bin);
1572
+ lblTool.hidden = !toolable;
1573
+ if (!toolable && srcTool.checked) srcEnv.checked = true;
1574
+
1575
+ const vault = srcVault.checked;
1576
+ const toolAuth = srcTool.checked && toolable;
1577
+ // A tool that holds its own credential is asked for NEITHER field. Showing
1578
+ // an empty env box beside it would read as something still to fill in.
1579
+ fEnvKey.hidden = vault || toolAuth;
1580
+ fKey.hidden = !vault;
1581
+ vaultWarn.hidden = !vault;
1582
+ toolNote.hidden = !toolAuth;
1583
+ }
1584
+ engine.addEventListener("change", () => sync());
1585
+ // Connect from a READY tool box pre-selects that provider and pre-fills the
1586
+ // binary, so the one thing the user was looking at is the one thing the form
1587
+ // opens on. Both values are the CLI's — the panel names neither.
1588
+ if (tool && providers.some((p) => p.id === tool.provider)) {
1589
+ provider.value = tool.provider;
1590
+ sync();
1591
+ if (Array.from(engine.options).some((o) => o.value === "cli")) engine.value = "cli";
1592
+ cliBin.value = tool.bin || "";
1593
+ // Decision 2 — PRE-SELECT the tool source when the card the user pressed
1594
+ // Connect on says the tool is signed in. The form opens on the state they
1595
+ // were already looking at, and the hint says what the other two are for.
1596
+ if (tool.authed) srcTool.checked = true;
1597
+ }
1598
+ sync();
1599
+
1600
+ const run = async (btn) => {
1601
+ result.replaceChildren();
1602
+ if (!name.value.trim()) {
1603
+ result.append(el("div", "note bad", t("extra.add.needName")));
1604
+ return;
1605
+ }
1606
+ btn.disabled = true;
1607
+ btn.textContent = t("extra.test.running");
1608
+ wire.arm();
1609
+ setBusy(true);
1610
+ try {
1611
+ const added = await post("/api/extra/add", {
1612
+ name: name.value.trim(),
1613
+ provider: provider.value,
1614
+ engine: engine.value,
1615
+ region: region.value,
1616
+ base_url: engine.value === "claude-shim" ? "" : baseUrl.value.trim(),
1617
+ anthropic_base_url: engine.value === "claude-shim" ? baseUrl.value.trim() : "",
1618
+ cli_bin: engine.value === "cli" ? cliBin.value.trim() : "",
1619
+ cli_agent: engine.value === "cli" ? cliAgent.value.trim() : "",
1620
+ tool_auth: srcTool.checked,
1621
+ vault: srcVault.checked,
1622
+ env_key: srcVault.checked || srcTool.checked ? "" : envKey.value.trim(),
1623
+ });
1624
+ if (added.fixture) {
1625
+ wire.settle(false);
1626
+ result.append(el("div", "note", t("extra.fixture")));
1627
+ return;
1628
+ }
1629
+ if (!added.ok) {
1630
+ wire.settle(false);
1631
+ // The CLI's own refusal, in the CLI's own words. There is no second idea
1632
+ // of a valid provider, engine, region or credential source in this panel.
1633
+ result.append(exOutput(added.output || added.command));
1634
+ return;
1635
+ }
1636
+ const ping = await post("/api/extra/ping", {
1637
+ profile: name.value.trim(),
1638
+ key: srcVault.checked ? key.value : "",
1639
+ });
1640
+ exPingResult(result, wire, ping, name.value.trim(), key.value, body, cfg);
1641
+ } catch (e) {
1642
+ wire.settle(false);
1643
+ result.append(failBox(e));
1644
+ } finally {
1645
+ btn.disabled = false;
1646
+ btn.textContent = t("extra.test.button");
1647
+ setBusy(false);
1648
+ // The pasted key leaves the form the moment it has been sent.
1649
+ key.value = "";
1650
+ }
1651
+ };
1652
+
1653
+ modal({
1654
+ title: t("extra.add.title"),
1655
+ body: form,
1656
+ actions: [
1657
+ { label: t("common.cancel"), onClick: (c) => c() },
1658
+ {
1659
+ label: t("extra.test.button"),
1660
+ cls: "btn-primary",
1661
+ id: "ex-test-btn",
1662
+ onClick: () => run(document.getElementById("ex-test-btn")),
1663
+ },
1664
+ ],
1665
+ });
1666
+ }
1667
+
1668
+ /* ------------------------------------------------- test an existing profile */
1669
+
1670
+ function exTestModal(p, body, cfg) {
1671
+ const form = el("div", "stack stack-sm");
1672
+ form.append(el("div", "note", p.name + " · " + p.provider + "/" + p.engine));
1673
+ form.append(exProbeNote(p.engine));
1674
+
1675
+ // A VAULTED KEY IS RE-PROBED WITH ITS PASSPHRASE (v0.50.0, W14): the CLI
1676
+ // decrypts the stored key into memory for the probe and nothing else. Pasting
1677
+ // a key here instead REPLACES it, which is the path for a key you rotated at
1678
+ // the provider — the two are different acts and the form says which is which.
1679
+ const vaulted = (p.credential || {}).source === "vault";
1680
+ const key = exInput(t("extra.add.keyPh"), "password");
1681
+ const pass = exInput(t("extra.save.passPh"), "password");
1682
+ if (vaulted) {
1683
+ form.append(el("div", "banner", t("extra.test.vaultedNote")));
1684
+ form.append(exField(t("extra.save.pass"), pass, t("extra.test.passHint")));
1685
+ form.append(exField(t("extra.test.replaceKey"), key, t("extra.test.replaceKeyHint")));
1686
+ }
1687
+
1688
+ // v0.51.0 — the PAID rung, and it is a button of its own so it can never be
1689
+ // pressed by accident. The model comes from the same CLI-decided control the
1690
+ // routing table uses, because a live test of a model you cannot name is not a
1691
+ // test of anything.
1692
+ const box = exModelBox(t("extra.routing.modelPh"));
1693
+ box.load(p.name);
1694
+ form.append(exField(t("extra.test.model"), box.node, t("extra.test.modelHint")));
1695
+
1696
+ const wire = exWire(t("extra.wire.you"), t("extra.wire.provider"));
1697
+ form.append(wire);
1698
+ const result = el("div", "ex-result");
1699
+ form.append(result);
1700
+
1701
+ const run = async (btn, live) => {
1702
+ result.replaceChildren();
1703
+ btn.disabled = true;
1704
+ const label = btn.textContent;
1705
+ btn.textContent = t("extra.test.running");
1706
+ wire.arm();
1707
+ setBusy(true);
1708
+ try {
1709
+ const r = await post("/api/extra/ping", {
1710
+ profile: p.name,
1711
+ key: vaulted ? key.value : "",
1712
+ passphrase: vaulted ? pass.value : "",
1713
+ live: !!live,
1714
+ model: live ? box.value() : "",
1715
+ });
1716
+ exPingResult(result, wire, r, p.name, vaulted ? key.value : "", body, cfg);
1717
+ } catch (e) {
1718
+ wire.settle(false);
1719
+ result.append(failBox(e));
1720
+ } finally {
1721
+ btn.disabled = false;
1722
+ // Its OWN label back. The free rung and the paid rung are different acts
1723
+ // and one of them costs money — they may never end up reading the same.
1724
+ btn.textContent = label;
1725
+ setBusy(false);
1726
+ key.value = "";
1727
+ pass.value = "";
1728
+ }
1729
+ };
1730
+
1731
+ modal({
1732
+ title: t("extra.test.title", { name: p.name }),
1733
+ body: form,
1734
+ actions: [
1735
+ { label: t("common.close"), onClick: (c) => c() },
1736
+ {
1737
+ label: t("extra.test.live"),
1738
+ id: "ex-live-btn",
1739
+ onClick: () => run(document.getElementById("ex-live-btn"), true),
1740
+ },
1741
+ {
1742
+ label: t("extra.test.button"),
1743
+ cls: "btn-primary",
1744
+ id: "ex-test-btn",
1745
+ onClick: () => run(document.getElementById("ex-test-btn"), false),
1746
+ },
1747
+ ],
1748
+ });
1749
+ }
1750
+
1751
+ /* ------------------------------------------------------------- the result -- */
1752
+
1753
+ // Everything rendered here is the CLI's `--json` object. The panel adds no
1754
+ // verdict of its own: `ok` is the CLI's, `verify_method` is which rung actually
1755
+ // answered, and `note` is its own caveat about what that rung did NOT prove.
1756
+ function exPingResult(result, wire, payload, profile, pastedKey, body, cfg) {
1757
+ result.replaceChildren();
1758
+ if (payload && payload.fixture) {
1759
+ wire.settle(false);
1760
+ result.append(el("div", "note", t("extra.fixture")));
1761
+ return;
1762
+ }
1763
+ const d = (payload && payload.data) || null;
1764
+ if (!d) {
1765
+ wire.settle(false);
1766
+ result.append(el("div", "note bad", (payload && payload.error) || t("common.loadFail")));
1767
+ return;
1768
+ }
1769
+ wire.settle(!!d.ok);
1770
+ if (!d.ok) {
1771
+ const box = el("div", "ex-result-bad");
1772
+ box.append(chip(d.reason || "unreachable", "bad"));
1773
+ if (d.error) box.append(el("div", "note", d.error));
1774
+ if (d.base_url) box.append(el("div", "note mono", d.base_url));
1775
+ // v0.51.0 — a tool that is not installed is not an unreachable endpoint, and
1776
+ // the fix is not a retry. The CLI carries the command; the panel prints it.
1777
+ if (d.install_cmd) box.append(el("div", "action-cmd", d.install_cmd));
1778
+ if (d.reason === "not-installed")
1779
+ box.append(
1780
+ el("div", "note", d.no_install_alternative ? t("extra.tools.altYes") + " " + d.no_install_alternative : t("extra.tools.altNone"))
1781
+ );
1782
+ // The reset the CLI performed, said out loud: a failed test on a connection
1783
+ // that had never verified leaves NOTHING behind.
1784
+ if (d.profile_reverted) box.append(el("div", "note", t("extra.result.reverted")));
1785
+ result.append(box);
1786
+ exRefresh(body);
1787
+ return;
1788
+ }
1789
+
1790
+ const box = el("div", "ex-result-ok");
1791
+ box.append(
1792
+ chip(
1793
+ t("extra.result.verified", { how: d.verify_method || "—" }) +
1794
+ (d.latency_ms ? " · " + t("extra.profile.latency", { ms: d.latency_ms }) : ""),
1795
+ "ok"
1796
+ )
1797
+ );
1798
+ if (d.models_seen && d.models_seen.length) {
1799
+ const list = el("div", "ex-models");
1800
+ for (const m of d.models_seen) list.append(el("span", "mono ex-model", m));
1801
+ box.append(list);
1802
+ }
1803
+ // A rung that authenticated and then rejected an invented model name proves
1804
+ // the URL and the credential and NOTHING about a model. The CLI says so; it is
1805
+ // shown, never softened.
1806
+ if (d.note) box.append(el("div", "note", d.note));
1807
+ exLiveBlock(box, d);
1808
+
1809
+ // THE SAVE OPENS ONLY ON A GREEN TEST, and the self-destruct is named BEFORE
1810
+ // the save rather than after it.
1811
+ if (d.vault && d.vault.stored) {
1812
+ box.append(el("div", "note", t("extra.save.stored", { pepper: d.vault.pepper })));
1813
+ if (d.vault.honesty) box.append(el("div", "note", d.vault.honesty));
1814
+ } else if (d.vault && d.vault.error) {
1815
+ box.append(el("div", "note", d.vault.error));
1816
+ // v0.52.0 — THE HARD GATE. The save used to be an optional inline box the
1817
+ // user could ignore, and ignoring it is what left a vaulted key with no
1818
+ // passphrase and a run that locked at wave 1. A green test on a connection
1819
+ // that will hold its key in the vault now opens a modal with no exit but
1820
+ // Save and one destructive escape.
1821
+ if (pastedKey) box.append(exSaveRow(profile, pastedKey, body, cfg));
1822
+ }
1823
+ result.append(box);
1824
+ exRefresh(body);
1825
+ }
1826
+
1827
+ /* THE LIVE RESULT. Everything here is the CLI's `--json` object and the panel
1828
+ adds no verdict of its own.
1829
+
1830
+ TWO HONESTIES ARE NOT OPTIONAL CHROME.
1831
+
1832
+ `model_reported: null` WITH `reports_model: false` is a real pair, not a
1833
+ missing field: neither local tool reports which model answered, so a
1834
+ substitution is INVISIBLE on that engine. The renderer says that sentence and
1835
+ never leaves a blank, because a blank reads as "nothing went wrong".
1836
+
1837
+ The reply is FOREIGN INPUT (`_shared/untrusted-input.md`): a third party's
1838
+ text, capped by the CLI, rendered as DOM text and never as HTML, and never
1839
+ acted on. It is evidence that something answered — nothing more. */
1840
+ function exLiveBlock(box, d) {
1841
+ if (!d.reply_excerpt && !d.tokens && !d.model_requested) return;
1842
+ const live = el("div", "ex-live");
1843
+ const head = el("div", "row-actions");
1844
+ if (d.latency_ms) head.append(chip(t("extra.profile.latency", { ms: d.latency_ms }), "info"));
1845
+ if (d.model_requested) head.append(el("span", "mono note", d.model_requested));
1846
+ live.append(head);
1847
+
1848
+ const kv = [[t("extra.live.requested"), d.model_requested || "—"]];
1849
+ kv.push([
1850
+ t("extra.live.reported"),
1851
+ d.model_reported ? d.model_reported : d.reports_model === false ? t("extra.live.noReport") : "—",
1852
+ ]);
1853
+ live.append(kvList(kv));
1854
+
1855
+ if (d.reply_excerpt) {
1856
+ live.append(el("div", "field-label", t("extra.live.reply")));
1857
+ // DOM text, never HTML. The cap is the CLI's.
1858
+ live.append(el("pre", "block wrap ex-live-reply", d.reply_excerpt));
1859
+ if (d.reply_truncated) live.append(el("div", "note", t("extra.live.truncated")));
1860
+ if (d.foreign_input) live.append(el("div", "note", d.foreign_input));
1861
+ }
1862
+ if (d.tokens) {
1863
+ // FOUR KINDS, NEVER BLENDED (/orc-budget). A kind the tool has no concept of
1864
+ // reads an em dash, never a zero — unknown is not zero.
1865
+ const rows = [
1866
+ [t("extra.live.input"), d.tokens.input],
1867
+ [t("extra.live.cacheWrite"), d.tokens.cache_write],
1868
+ [t("extra.live.cacheRead"), d.tokens.cache_read],
1869
+ [t("extra.live.output"), d.tokens.output],
1870
+ ];
1871
+ live.append(kvList(rows.map(([k, v]) => [k, v === null || v === undefined ? "—" : String(v)])));
1872
+ }
1873
+ if (d.cost_note) live.append(el("div", "note", d.cost_note));
1874
+ box.append(live);
1875
+ }
1876
+
1877
+ // Saving re-runs the test with the passphrase attached, because that is the only
1878
+ // way the CLI ever stores a key: proved by a live connection first, every time.
1879
+ //
1880
+ // v0.52.0 — the inline form is now the LAST RESORT rather than the route. When
1881
+ // the panel knows the config (and it does, everywhere the connect flow runs) the
1882
+ // key and its deadline are asked for together, in a modal that cannot be walked
1883
+ // away from half-done.
1884
+ function exSaveRow(profile, pastedKey, body, cfg) {
1885
+ if (cfg) {
1886
+ exSessionModal({ name: profile, provider: "", engine: "", session: null }, body, cfg, false, pastedKey);
1887
+ return el("div", "note", t("extra.session.opened"));
1888
+ }
1889
+ return exSaveRowInline(profile, pastedKey, body);
1890
+ }
1891
+
1892
+ function exSaveRowInline(profile, pastedKey, body) {
1893
+ const wrap = el("div", "ex-save");
1894
+ wrap.append(el("div", "ex-probe-head", t("extra.save.head")));
1895
+ wrap.append(el("div", "banner banner-bad", t("extra.save.warning")));
1896
+ const pass = exInput(t("extra.save.passPh"), "password");
1897
+ const again = exInput(t("extra.save.againPh"), "password");
1898
+ wrap.append(exField(t("extra.save.pass"), pass, t("extra.save.passHint")));
1899
+ wrap.append(exField(t("extra.save.again"), again));
1900
+ const out = el("div", "note");
1901
+ const b = el("button", "btn btn-sm btn-primary btn-allow-busy", t("extra.save.button"));
1902
+ b.type = "button";
1903
+ b.addEventListener("click", async () => {
1904
+ out.textContent = "";
1905
+ if (!pass.value || pass.value !== again.value) {
1906
+ out.textContent = t("extra.save.mismatch");
1907
+ return;
1908
+ }
1909
+ b.disabled = true;
1910
+ try {
1911
+ const r = await post("/api/extra/ping", { profile, key: pastedKey, passphrase: pass.value });
1912
+ const d = (r && r.data) || null;
1913
+ if (d && d.vault && d.vault.stored) {
1914
+ out.textContent = t("extra.save.stored", { pepper: d.vault.pepper });
1915
+ wrap.replaceChildren(out);
1916
+ if (d.vault.honesty) wrap.append(el("div", "note", d.vault.honesty));
1917
+ exRefresh(body);
1918
+ } else {
1919
+ out.textContent = (d && d.vault && d.vault.error) || (r && r.error) || t("common.loadFail");
1920
+ }
1921
+ } catch (e) {
1922
+ out.textContent = String(e.message);
1923
+ } finally {
1924
+ b.disabled = false;
1925
+ pass.value = "";
1926
+ again.value = "";
1927
+ }
1928
+ });
1929
+ wrap.append(b, out);
1930
+ return wrap;
1931
+ }
1932
+
1933
+ /* ------------------------------------------------------------ unlock, remove */
1934
+
1935
+ // The ONE action that clears a countdown: a correct passphrase resets the
1936
+ // counter to zero. It never yields the key — `orc extra unlock` answers one
1937
+ // question, with a yes or a no.
1938
+ function exUnlockModal(p, body) {
1939
+ const form = el("div", "stack stack-sm");
1940
+ form.append(el("div", "note", t("extra.unlock.what")));
1941
+ const pass = exInput(t("extra.save.passPh"), "password");
1942
+ form.append(exField(t("extra.save.pass"), pass));
1943
+ const out = el("div", "note");
1944
+ form.append(out);
1945
+ modal({
1946
+ title: t("extra.unlock.title", { name: p.name }),
1947
+ body: form,
1948
+ actions: [
1949
+ { label: t("common.cancel"), onClick: (c) => c() },
1950
+ {
1951
+ label: t("extra.unlock.button"),
1952
+ cls: "btn-primary",
1953
+ id: "ex-unlock-btn",
1954
+ onClick: async () => {
1955
+ const b = document.getElementById("ex-unlock-btn");
1956
+ out.textContent = "";
1957
+ b.disabled = true;
1958
+ try {
1959
+ const r = await post("/api/extra/unlock", { profile: p.name, passphrase: pass.value });
1960
+ const d = (r && r.data) || null;
1961
+ out.replaceChildren();
1962
+ // The countdown is the CLI's own message, verbatim — "wrong
1963
+ // passphrase — attempt 8 of 10" is the point, and a panel that
1964
+ // summarised it would delete the one number this feature exists to
1965
+ // show. Its honesty line rides with it: the counter stops someone at
1966
+ // your keyboard and does not stop someone who copied the file.
1967
+ if (d && d.ok) out.append(el("div", null, t("extra.unlock.ok")));
1968
+ else if (d) {
1969
+ out.append(el("div", "bad", d.error || t("common.loadFail")));
1970
+ if (d.honesty) out.append(el("div", "note", d.honesty));
1971
+ } else out.append(el("div", "bad", (r && r.error) || t("common.loadFail")));
1972
+ exRefresh(body);
1973
+ } catch (e) {
1974
+ out.textContent = String(e.message);
1975
+ } finally {
1976
+ b.disabled = false;
1977
+ pass.value = "";
1978
+ }
1979
+ },
1980
+ },
1981
+ ],
1982
+ });
1983
+ }
1984
+
1985
+ // A reason is REQUIRED and the CLI is what refuses without one. The form only
1986
+ // collects it, and the CLI's answer names the route rows the removal drops.
1987
+ function exRemoveModal(p, body) {
1988
+ const form = el("div", "stack stack-sm");
1989
+ form.append(el("div", "note", t("extra.remove.what", { name: p.name })));
1990
+ const reason = exInput(t("extra.remove.reasonPh"));
1991
+ form.append(exField(t("extra.remove.reason"), reason, t("extra.remove.reasonHint")));
1992
+ const out = el("div", "note");
1993
+ form.append(out);
1994
+ modal({
1995
+ title: t("extra.remove.title", { name: p.name }),
1996
+ body: form,
1997
+ actions: [
1998
+ { label: t("common.cancel"), onClick: (c) => c() },
1999
+ {
2000
+ label: t("extra.remove.button"),
2001
+ cls: "btn-danger",
2002
+ id: "ex-remove-btn",
2003
+ onClick: async (close) => {
2004
+ const b = document.getElementById("ex-remove-btn");
2005
+ out.textContent = "";
2006
+ b.disabled = true;
2007
+ try {
2008
+ const r = await post("/api/extra/remove", { name: p.name, reason: reason.value });
2009
+ if (r && r.ok) {
2010
+ close();
2011
+ toast(t("extra.remove.done", { name: p.name }), "ok");
2012
+ exRefresh(body);
2013
+ } else {
2014
+ out.textContent = (r && (r.output || r.error)) || t("common.loadFail");
2015
+ }
2016
+ } catch (e) {
2017
+ out.textContent = String(e.message);
2018
+ } finally {
2019
+ b.disabled = false;
2020
+ }
2021
+ },
2022
+ },
2023
+ ],
2024
+ });
2025
+ }
2026
+
2027
+ // The CLI's own stdout for a refused write, shown exactly as it was printed.
2028
+ function exOutput(text) {
2029
+ return el("pre", "block wrap fail-detail", String(text || "").slice(0, 2000));
2030
+ }
2031
+
2032
+ // Re-render the panel against the new truth. The modal deliberately stays open:
2033
+ // the result of the thing you just did is inside it.
2034
+ function exRefresh(body) {
2035
+ if (body) renderExtra(body);
2036
+ }
2037
+
2038
+ /* ================================================ the routing rail (W12 T1) ==
2039
+ THE RAIL IS THE PICTURE THAT MAKES THIS CONFIGURABLE AT A GLANCE. One
2040
+ horizontal 0→100 axis, every band a segment, drawn from `orc extra route
2041
+ --json` and from nothing else — the panel computes no band edge, no agent
2042
+ name and no fall-through.
2043
+
2044
+ AN UNMAPPED RANGE KEEPS ITS SLOT and is drawn in Claude's colour with the
2045
+ agent it actually resolves to. Filtering the gaps out would make "I left the
2046
+ top band on Opus on purpose" and "there is no top band" identical, and would
2047
+ make the rail change width on every edit — the v0.43.7 OFF-phase rule.
2048
+
2049
+ GEOMETRY IS SOLVED FROM THE BOX: a segment is `var(--w)` of the axis with a
2050
+ readable MINIMUM, so a rail too narrow for its labels SCROLLS inside its own
2051
+ box and is never squeezed. (The VAULT/ringRadii lesson: a label sized by a
2052
+ fraction of the container knows nothing about how wide a word is.)
2053
+
2054
+ WRITES ARE STAGED AND BATCHED, the v0.44.1 rule: nothing is written until
2055
+ Apply, the pending edits are NAMED, a refused write never aborts the rest,
2056
+ and Discard renders only while dirty. */
2057
+
2058
+ // Which band is open in the editor, so an Apply that re-renders does not throw
2059
+ // you back to the top of the list.
2060
+ let EX_OPEN_BAND = null;
2061
+
2062
+ /* WHICH LANE DOES A BAND GOVERN (v0.52.0, D6).
2063
+
2064
+ The routing table says `[40,55) → opencode/big-pickle`. That is true for
2065
+ `/orc`, which scores every task, and it is NOT how `/orc-fast` works: that
2066
+ lane pins ONE executor and resolves the BAND its agent already encodes, at
2067
+ BOTH EDGES, requiring them to agree. The rule was implemented and written
2068
+ down and rendered nowhere, so a user reading this panel could not get from a
2069
+ band to the decision it drives.
2070
+
2071
+ Every word here is `orc extra lanes --json`: the shape, the verdict, the
2072
+ edges, whether they agreed, and the sentence underneath. The panel decides
2073
+ nothing — a second idea of the routing is drift no lint can see. */
2074
+ /* ================================================ the POSITIONS (v0.55.0) ==
2075
+
2076
+ A SCORE IS WHAT A BAND NEEDS, AND FOUR LANES DO NOT HAVE ONE. The ladder
2077
+ above this one is the SCORED half — `/orc`, `/orc-ultra`, `/orc-mini`,
2078
+ `/orc-diy`. This one is the other half: `/orc-quick`, `/orc-fast`, `/orc-doc`
2079
+ and `/orc-wiki` pin an agent to a POSITION, and a position is a point rather
2080
+ than an interval.
2081
+
2082
+ EVERY STRING IN THIS CARD COMES FROM `orc extra role list --json`. The panel
2083
+ names no slot meaning, no provider, no model and no agent of its own — the
2084
+ Flow-stepper rule. Writing "cheap work" beside a slot would be the panel
2085
+ deciding what a position means.
2086
+
2087
+ AN UNROUTED SLOT KEEPS ITS SLOT and is drawn as Claude with its pinned agent
2088
+ — the OFF-phase rule. Filtering it out makes "I left the checker on Claude on
2089
+ purpose" and "there is no checker" identical, and makes the ladder change
2090
+ length every time a row is cleared.
2091
+
2092
+ NO PROPORTIONAL BAR. A band has a width because it covers a range of scores;
2093
+ a position covers nothing, and drawing one a width would be the panel
2094
+ inventing an interval the CLI never computed. */
2095
+ function exSlotCard(d, body, edits) {
2096
+ const c = card(t("extra.slots.title"));
2097
+ if (d.errors.role) {
2098
+ c.append(failBox(d.errors.role));
2099
+ return c;
2100
+ }
2101
+ const r = d.role || {};
2102
+ const rows = r.slots || [];
2103
+ c.append(el("div", "note", t("extra.slots.sub")));
2104
+ c.append(exWhy(t("extra.slots.subWhy")));
2105
+
2106
+ const verified = ((d.list && d.list.counts) || {}).verified || 0;
2107
+ const list = el("div", "ex-ladder");
2108
+ const entries = [];
2109
+ const openOnly = (which) => {
2110
+ for (const e of entries) if (e !== which) e.close();
2111
+ };
2112
+ for (const row of rows) {
2113
+ const e = exSlotRow(row, d, edits, verified > 0, openOnly);
2114
+ entries.push(e);
2115
+ list.append(e.node);
2116
+ }
2117
+ c.append(list);
2118
+ // The CLI's own sentence: absence is not a hole.
2119
+ if (r.note) c.append(el("div", "note", r.note));
2120
+ c.append(exWhy(t("extra.slots.keepsSlotWhy")));
2121
+
2122
+ // The six `extra_roles` values nothing can resolve. REPORTED, not fixed — and
2123
+ // reported in the CLI's own words, because "nothing resolves this" is a state
2124
+ // and a state word is never the panel's to soften.
2125
+ const un = r.unreachable_roles || [];
2126
+ if (un.length) {
2127
+ const box = el("div", "ex-unreachable");
2128
+ box.append(el("div", "note", t("extra.slots.unreachable")));
2129
+ const ul = el("div", "ex-unreachable-list");
2130
+ for (const u of un) {
2131
+ const row = el("div", "row-actions");
2132
+ row.append(el("span", "mono", u.role));
2133
+ row.append(el("span", "note", u.state));
2134
+ ul.append(row);
2135
+ }
2136
+ box.append(ul);
2137
+ c.append(box);
2138
+ }
2139
+ if (!verified) c.append(empty(t("extra.routing.locked"), t("extra.routing.lockedHint")));
2140
+ return c;
2141
+ }
2142
+
2143
+ // Which position is open in the editor, so an Apply that re-renders does not
2144
+ // throw the person out of the row they were working in — the Runs-row rule.
2145
+ let EX_OPEN_SLOT = null;
2146
+
2147
+ function exSlotRow(raw, d, edits, verified, openOnly) {
2148
+ const key = "role " + raw.slot;
2149
+ const node = el("div", "ex-band ex-slot");
2150
+ const headBtn = el("button", "ex-band-head ex-slot-head");
2151
+ headBtn.type = "button";
2152
+ headBtn.setAttribute("aria-expanded", "false");
2153
+ const pane = el("div", "ex-band-pane");
2154
+ node.append(headBtn, pane);
2155
+
2156
+ const isOpen = () => node.classList.contains("open");
2157
+ const setOpen = (open) => {
2158
+ node.classList.toggle("open", open);
2159
+ headBtn.setAttribute("aria-expanded", String(open));
2160
+ if (open) EX_OPEN_SLOT = raw.slot;
2161
+ else if (EX_OPEN_SLOT === raw.slot) EX_OPEN_SLOT = null;
2162
+ pane.replaceChildren(open ? editor() : frag());
2163
+ };
2164
+
2165
+ // What a staged edit would make this row read as. `edits` holds the intent,
2166
+ // never the write — nothing reaches disk until Apply.
2167
+ const preview = () => {
2168
+ const staged = edits.map.get(key);
2169
+ if (!staged) return { routes: raw.resolved === "extra", profile: raw.profile, model: raw.model, staged: false };
2170
+ if (staged.kind === "clear" || staged.url === "/api/extra/role/rm")
2171
+ return { routes: false, profile: null, model: null, staged: true };
2172
+ return { routes: true, profile: staged.profile || null, model: staged.model || null, staged: true };
2173
+ };
2174
+
2175
+ const paint = () => {
2176
+ const p = preview();
2177
+ headBtn.replaceChildren();
2178
+ const mid = el("span", "ex-band-mid");
2179
+ mid.append(el("span", "mono ex-band-label", raw.slot));
2180
+ // The LANE, and whether this position is asked for or announced — both the
2181
+ // CLI's words. `announce_point` is where the sentence gets printed, which is
2182
+ // the thing a person actually wants to know before routing a position.
2183
+ mid.append(el("span", "note ex-band-range", raw.lane));
2184
+ headBtn.append(mid);
2185
+ headBtn.append(el("span", "note ex-slot-when", raw.asks ? t("extra.slots.asked") : raw.announce_point));
2186
+ // THE TARGET, never truncated, and never guessed: a position that is not
2187
+ // routed falls through to a NAMED agent, which the CLI hands over.
2188
+ headBtn.append(
2189
+ el("span", "mono ex-band-target", p.routes && p.profile ? p.profile + "/" + p.model : "claude · " + raw.claude.agent)
2190
+ );
2191
+ const chips = el("span", "ex-band-chips");
2192
+ if (p.staged) chips.append(chip(t("extra.routing.staged"), "info"));
2193
+ if (!p.staged && raw.resolved === "extra" && raw.engine) chips.append(chip(raw.engine, "info"));
2194
+ // The CLI's state word, verbatim.
2195
+ if (!p.staged && raw.routed && raw.verify_state)
2196
+ chips.append(chip(raw.verify_state, raw.verify_state === "VERIFIED" ? "ok" : "warn"));
2197
+ if (!p.staged && raw.routed && raw.model_known === false) chips.append(chip(t("extra.routing.modelGone"), "warn"));
2198
+ // ROUTED BUT NOT RESOLVING. The row is held back and the CLI said why — a
2199
+ // position that quietly fell back to Claude is the thing this chip exists
2200
+ // to make impossible to miss.
2201
+ if (!p.staged && raw.routed && raw.resolved !== "extra") chips.append(chip(raw.held_back || "claude", "warn"));
2202
+ headBtn.append(chips);
2203
+ headBtn.append(el("span", "ex-band-caret", "▸"));
2204
+ headBtn.title = raw.slot + " → " + (p.routes && p.profile ? p.profile + "/" + p.model : raw.claude.agent);
2205
+ if (isOpen()) pane.replaceChildren(editor());
2206
+ };
2207
+
2208
+ const editor = () => {
2209
+ const box = el("div", "stack stack-sm");
2210
+ // WHAT THIS POSITION IS, in the CLI's words.
2211
+ box.append(el("div", "note", raw.meaning));
2212
+ box.append(el("div", "note", raw.why));
2213
+ const staged = edits.map.get(key);
2214
+ if (staged) {
2215
+ const line = el("div", "note ex-route-staged");
2216
+ line.append(chip(t("extra.routing.staged"), "info"));
2217
+ line.append(document.createTextNode(" " + staged.value));
2218
+ const undo = el("button", "btn btn-ghost btn-sm", t("extra.routing.undo"));
2219
+ undo.type = "button";
2220
+ undo.addEventListener("click", () => {
2221
+ edits.drop(key);
2222
+ paint();
2223
+ });
2224
+ line.append(undo);
2225
+ box.append(line);
2226
+ return box;
2227
+ }
2228
+ if (!verified) {
2229
+ box.append(el("div", "note", t("extra.routing.lockedHint")));
2230
+ return box;
2231
+ }
2232
+ if (raw.routed) {
2233
+ box.append(
2234
+ kvList([
2235
+ [t("extra.routing.kvProfile"), raw.profile],
2236
+ [t("extra.routing.kvModel"), raw.model],
2237
+ [t("extra.routing.kvSmall"), raw.small_model || "—"],
2238
+ [t("extra.routing.kvTurns"), raw.max_turns === null || raw.max_turns === undefined ? "—" : String(raw.max_turns)],
2239
+ [t("extra.slots.kvDisplaces"), raw.claude.agents.join(" · ")],
2240
+ ])
2241
+ );
2242
+ if (raw.next) box.append(el("div", "note", raw.next));
2243
+ const clear = el("button", "btn btn-sm", t("extra.slots.clear"));
2244
+ clear.type = "button";
2245
+ clear.addEventListener("click", () => {
2246
+ edits.action(key, "/api/extra/role/rm", { slot: raw.slot }, t("extra.slots.stagedClear", { agent: raw.claude.agent }));
2247
+ paint();
2248
+ });
2249
+ const acts = el("div", "row-actions");
2250
+ acts.append(clear);
2251
+ box.append(acts);
2252
+ return box;
2253
+ }
2254
+ box.append(el("div", "note", t("extra.slots.displaces", { agents: raw.claude.agents.join(" · ") })));
2255
+ box.append(exSlotControls(raw, d, edits, key, paint));
2256
+ return box;
2257
+ };
2258
+
2259
+ headBtn.addEventListener("click", () => {
2260
+ const next = !isOpen();
2261
+ if (next) openOnly(entry);
2262
+ setOpen(next);
2263
+ });
2264
+ paint();
2265
+ const entry = { node, close: () => isOpen() && setOpen(false) };
2266
+ if (EX_OPEN_SLOT === raw.slot) setOpen(true);
2267
+ return entry;
2268
+ }
2269
+
2270
+ function exSlotControls(row, d, edits, key, paint) {
2271
+ // The same two controls the band editor uses, for the same reasons: a
2272
+ // connection that has never answered is DISABLED and labelled rather than
2273
+ // hidden, and whether the model control is a dropdown or a box is the CLI's
2274
+ // answer, never this panel's.
2275
+ const profiles = (d.list && d.list.profiles) || [];
2276
+ const sel = exSelect(
2277
+ [{ value: "", label: t("extra.routing.pickProfile") }].concat(
2278
+ profiles.map((p) => ({
2279
+ value: p.name,
2280
+ label: p.name + (p.verified_at ? "" : " " + t("extra.routing.untested")),
2281
+ disabled: !p.verified_at,
2282
+ }))
2283
+ )
2284
+ );
2285
+ const box = exModelBox(t("extra.routing.modelPh"));
2286
+ const fillModels = () => {
2287
+ const p = profiles.find((x) => x.name === sel.value);
2288
+ box.load(p ? p.name : null);
2289
+ };
2290
+ sel.addEventListener("change", fillModels);
2291
+ fillModels();
2292
+
2293
+ const add = el("button", "btn btn-sm", t("extra.slots.hold"));
2294
+ add.type = "button";
2295
+ add.addEventListener("click", () => {
2296
+ const chosen = box.value();
2297
+ if (!sel.value || !chosen) return;
2298
+ const target = sel.value + "/" + chosen;
2299
+ // REPLACING A POSITION IS A ROUTING CHANGE, so the confirmation NAMES what
2300
+ // it replaces. A count is not consent — and neither is a slot id.
2301
+ if (row.routed && !confirm(t("extra.slots.confirmReplace", { slot: row.slot, old: row.profile + "/" + row.model, next: target })))
2302
+ return;
2303
+ edits.action(
2304
+ key,
2305
+ "/api/extra/role/set",
2306
+ { slot: row.slot, target, profile: sel.value, model: chosen },
2307
+ t("extra.slots.stagedSet", { slot: row.slot, target })
2308
+ );
2309
+ paint();
2310
+ });
2311
+
2312
+ const controls = el("div", "ex-route-controls");
2313
+ controls.append(sel, box.node, add);
2314
+ return controls;
2315
+ }
2316
+
2317
+ function exLanesCard(d) {
2318
+ const c = card(t("extra.lanes.title"));
2319
+ c.append(el("div", "note", t("extra.lanes.sub")));
2320
+ c.append(exWhy(t("extra.lanes.subWhy")));
2321
+ if (d.errors.lanes) {
2322
+ c.append(failBox(d.errors.lanes));
2323
+ return c;
2324
+ }
2325
+ const lanes = (d.lanes && d.lanes.lanes) || [];
2326
+ if (!lanes.length) {
2327
+ c.append(empty(t("extra.lanes.none"), ""));
2328
+ return c;
2329
+ }
2330
+ const list = el("div", "ex-lanes");
2331
+ for (const l of lanes) {
2332
+ const row = el("div", "ex-lane");
2333
+ const top = el("div", "row-actions");
2334
+ top.append(el("span", "mono ex-lane-name", l.lane));
2335
+ top.append(el("span", "note", l.shape));
2336
+ // The verdict word is the CLI's own. A friendlier synonym would be a state
2337
+ // that does not exist.
2338
+ top.append(
2339
+ chip(l.routes, l.routes === "foreign" || l.routes === "roles" || l.routes === "per-task" ? "ok" : l.routes === "never" ? "info" : "warn")
2340
+ );
2341
+ row.append(top);
2342
+ // The disclosure `extra-dispatch.md` demands, finally rendered: both edges
2343
+ // and whether they agreed.
2344
+ if (l.band)
2345
+ row.append(
2346
+ el("div", "note mono", t("extra.lanes.edges", { band: l.band, edges: (l.edges || []).join(","), agree: String(l.agree) }))
2347
+ );
2348
+ // v0.55.0 — a SLOT lane has no band and no edges. It has one row per
2349
+ // POSITION, each carrying the agent it displaces, and an unrouted one keeps
2350
+ // its row: the whole point is that the two positions in a lane are two
2351
+ // separate decisions.
2352
+ for (const sl of l.slots || []) {
2353
+ const line = el("div", "note mono ex-lane-slot");
2354
+ line.append(el("span", "", sl.slot));
2355
+ line.append(el("span", "", " → "));
2356
+ line.append(el("span", "", sl.routes ? sl.profile + "/" + sl.model : "claude · " + sl.claude));
2357
+ row.append(line);
2358
+ }
2359
+ row.append(el("div", "note", l.detail));
2360
+ list.append(row);
2361
+ }
2362
+ c.append(list);
2363
+ // A lane not listed does not route foreign. Absence is a `no`, never an
2364
+ // omission to be interpreted.
2365
+ if (d.lanes && d.lanes.note) c.append(el("div", "note", d.lanes.note));
2366
+ return c;
2367
+ }
2368
+
2369
+ function exRoutingCard(d, body, edits) {
2370
+ const c = card(t("extra.routing.title"));
2371
+ if (d.errors.route) {
2372
+ c.append(failBox(d.errors.route));
2373
+ return c;
2374
+ }
2375
+ const r = d.route || {};
2376
+ const rows = r.rows || [];
2377
+
2378
+ // The CLI's own sentence when the master gate is off. Every row below is
2379
+ // inert and it says so — never a panel paraphrase.
2380
+ if (r.note) c.append(el("div", "banner", r.note));
2381
+ c.append(el("div", "note", t("extra.routing.sub")));
2382
+
2383
+ // R10 — THE GATE. Nothing may be routed to a connection that has never
2384
+ // answered, so the editor stays shut until one has, and the ONE line that
2385
+ // would open it is printed rather than implied. The LADDER still draws either
2386
+ // way: what your bands resolve to today is worth reading before you can
2387
+ // change it.
2388
+ const verified = ((d.list && d.list.counts) || {}).verified || 0;
2389
+
2390
+ c.append(exBandLadder(rows, d, edits, verified > 0));
2391
+ c.append(exBandLegend());
2392
+ c.append(el("div", "note", t("extra.routing.gapNote")));
2393
+ c.append(exWhy(t("extra.routing.gapWhy")));
2394
+ if (!verified) c.append(empty(t("extra.routing.locked"), t("extra.routing.lockedHint")));
2395
+ else c.append(el("div", "note", t("extra.routing.editNote")));
2396
+ return c;
2397
+ }
2398
+
2399
+ /* THE BAND LADDER (v0.53.0) — ONE PICTURE, AND THE ROW YOU READ IS THE ROW YOU
2400
+ EDIT.
2401
+
2402
+ What this replaces was a horizontal 0→100 rail ABOVE a separate list of
2403
+ editable rows: the same data twice, and only the second copy was interactive.
2404
+ Six things were wrong with the rail and every one of them is structural.
2405
+
2406
+ 1. THE TARGET WAS TRUNCATED — clipped mid-word, three rows out of seven.
2407
+ The single most important fact in the picture was the one you could not
2408
+ read.
2409
+ 2. THE WIDTHS LIED. `min-width: 128px` fought `var(--w)`, so a 10-point
2410
+ band and a 30-point band came out nearly the same width while the
2411
+ `0 … 100` axis underneath promised they were to scale.
2412
+ 3. THE LAST BAND WAS OFF-SCREEN with no affordance that the rail scrolled.
2413
+ 4. NOTHING SAID WHAT THE COLOURS MEANT. Green versus blue was never
2414
+ explained anywhere on the page, and green is "this work leaves your
2415
+ machine" — see exBandLegend.
2416
+ 5. `[0,30)` IS DEVELOPER NOTATION. The half-open bracket is load-bearing so
2417
+ it stays, but it now sits beside the CLI's plain reading of the same
2418
+ range instead of instead of one.
2419
+ 6. AND IT WAS THE SAME DATA TWICE.
2420
+
2421
+ Vertical fixes all six at once: the proportional bar is a width INSIDE the
2422
+ row, so scale is honest and nothing is ever squeezed or clipped; the target
2423
+ gets the whole row; and the editor opens IN PLACE — the Runs-row and
2424
+ Knowledge-doc rule, one row open at a time. */
2425
+ function exBandLadder(rows, d, edits, verified) {
2426
+ const list = el("div", "ex-ladder");
2427
+ // NO FILTER, and that is the assertion rather than the absence. A range with
2428
+ // no connection of yours on it is a CLAUDE range and it keeps its row — the
2429
+ // v0.43.7 OFF-phase rule. Filtering the gaps out would make "I left the top
2430
+ // band on Claude on purpose" and "there is no top band" look identical.
2431
+ const entries = [];
2432
+ const openOnly = (which) => {
2433
+ for (const e of entries) if (e !== which) e.close();
2434
+ };
2435
+ for (const row of rows) {
2436
+ const e = exBandRow(row, d, edits, verified, openOnly);
2437
+ entries.push(e);
2438
+ list.append(e.node);
2439
+ }
2440
+ return list;
2441
+ }
2442
+
2443
+ /* THE LEGEND — the fix for the single biggest comprehension gap on this panel.
2444
+ Two colours were carrying the whole meaning of the picture and neither was
2445
+ ever named. The words are the panel's own (they describe what the subsystem
2446
+ does, not this repo's state), which is the same rule the boundary card is
2447
+ written under. */
2448
+ function exBandLegend() {
2449
+ const box = el("div", "ex-legend");
2450
+ const item = (cls, label) => {
2451
+ const s = el("span", "ex-legend-item");
2452
+ s.append(el("span", "ex-legend-swatch " + cls));
2453
+ s.append(el("span", "note", label));
2454
+ box.append(s);
2455
+ };
2456
+ item("ex-band-extra", t("extra.routing.legendExtra"));
2457
+ item("ex-band-claude", t("extra.routing.legendClaude"));
2458
+ return box;
2459
+ }
2460
+
2461
+ /* WHAT YOU ARE ABOUT TO DO, drawn from the STAGED shape rather than from disk.
2462
+ Nothing re-renders until Apply (v0.44.1), so a row has to show its own staged
2463
+ state and an undo has to put the control back without the panel reloading
2464
+ underneath it.
2465
+
2466
+ IT NEVER INVENTS THE OTHER HALF. Un-routing a band hands it back to the
2467
+ Claude ladder, and which agent it lands on is `claudeGaps`'s answer — split at
2468
+ the resolving table's own edges, which this panel does not know and must not
2469
+ learn. A staged un-route therefore draws a Claude row whose target is an em
2470
+ dash, and says it is recomputed on Apply. A guessed agent name here would be a
2471
+ picture of a run that is not going to happen. */
2472
+ function exPreviewRow(r, edits) {
2473
+ const staged = edits.map.get("route " + r.band);
2474
+ if (!staged) return r;
2475
+ if (staged.route === "/api/extra/route/rm")
2476
+ return { from: r.from, to: r.to, band: r.band, range: r.range, meaning: r.meaning, via: "claude", agent: null, staged: true };
2477
+ const target = String((staged.body && staged.body.target) || "");
2478
+ const cut = target.indexOf("/");
2479
+ return {
2480
+ from: r.from,
2481
+ to: r.to,
2482
+ band: r.band,
2483
+ range: r.range,
2484
+ meaning: r.meaning,
2485
+ via: "extra",
2486
+ profile: cut > 0 ? target.slice(0, cut) : target,
2487
+ model: cut > 0 ? target.slice(cut + 1) : "",
2488
+ engine: null,
2489
+ verify_state: null,
2490
+ model_known: true,
2491
+ staged: true,
2492
+ };
2493
+ }
2494
+ function exPreviewRows(rows, edits) {
2495
+ return rows.map((r) => exPreviewRow(r, edits));
2496
+ }
2497
+
2498
+ function exBandRow(raw, d, edits, verified, openOnly) {
2499
+ const key = "route " + raw.band;
2500
+ const node = el("div", "ex-band");
2501
+ const headBtn = el("button", "ex-band-head");
2502
+ headBtn.type = "button";
2503
+ headBtn.setAttribute("aria-expanded", "false");
2504
+ const pane = el("div", "ex-band-pane");
2505
+ node.append(headBtn, pane);
2506
+
2507
+ const isOpen = () => node.classList.contains("open");
2508
+ const setOpen = (open) => {
2509
+ node.classList.toggle("open", open);
2510
+ headBtn.setAttribute("aria-expanded", String(open));
2511
+ if (open) EX_OPEN_BAND = raw.band;
2512
+ else if (EX_OPEN_BAND === raw.band) EX_OPEN_BAND = null;
2513
+ pane.replaceChildren(open ? editor() : frag());
2514
+ };
2515
+
2516
+ const target = (p) => (p.via === "extra" ? p.profile + "/" + p.model : p.agent || "—");
2517
+
2518
+ const paint = () => {
2519
+ const p = exPreviewRow(raw, edits);
2520
+ headBtn.replaceChildren();
2521
+ // The proportional bar, INSIDE the row. A CUSTOM PROPERTY and not an inline
2522
+ // width: the panel's CSP is `style-src 'self'` and a style attribute is
2523
+ // blocked outright. The track is full width, so `--w` is honest at every
2524
+ // viewport and no band is ever squeezed to a sliver to make room for text.
2525
+ const track = el("span", "ex-band-track");
2526
+ const bar = el(
2527
+ "span",
2528
+ "ex-band-bar " + (p.via === "extra" ? "ex-band-extra" : "ex-band-claude") + (p.staged ? " ex-band-staged" : "")
2529
+ );
2530
+ bar.style.setProperty("--w", Math.max(0, raw.to - raw.from).toFixed(2) + "%");
2531
+ track.append(bar);
2532
+ headBtn.append(track);
2533
+ const mid = el("span", "ex-band-mid");
2534
+ // The CLI's own `[from,to)` string, brackets included: the half-open edge is
2535
+ // the whole point of the notation. And BESIDE it the CLI's plain reading of
2536
+ // the same range — `range` is computed by `orc extra route`, never written
2537
+ // here. "simple work" beside a score would be the panel deciding what a
2538
+ // score means, which is the Flow-stepper rule.
2539
+ mid.append(el("span", "mono ex-band-label", raw.band));
2540
+ if (raw.range) mid.append(el("span", "note ex-band-range", raw.range));
2541
+ headBtn.append(mid);
2542
+ // THE FULL TARGET, never truncated. It is the most important fact in the row.
2543
+ headBtn.append(el("span", "mono ex-band-target", target(p)));
2544
+ const chips = el("span", "ex-band-chips");
2545
+ if (p.staged) chips.append(chip(t("extra.routing.staged"), "info"));
2546
+ if (p.via === "extra" && p.engine) chips.append(chip(p.engine, "info"));
2547
+ // The CLI's state word, verbatim — never a friendlier synonym. A STAGED row
2548
+ // has no state word yet, because nothing has been written for the CLI to
2549
+ // have an opinion about.
2550
+ if (p.via === "extra" && p.verify_state)
2551
+ chips.append(chip(p.verify_state, p.verify_state === "VERIFIED" ? "ok" : "warn"));
2552
+ if (p.via === "extra" && !p.model_known) chips.append(chip(t("extra.routing.modelGone"), "warn"));
2553
+ if (p.staged && p.via === "claude") chips.append(el("span", "note", t("extra.routing.recomputed")));
2554
+ headBtn.append(chips);
2555
+ headBtn.append(el("span", "ex-band-caret", "▸"));
2556
+ headBtn.title = raw.band + " → " + target(p);
2557
+ if (isOpen()) pane.replaceChildren(editor());
2558
+ };
2559
+
2560
+ const editor = () => {
2561
+ const box = el("div", "stack stack-sm");
2562
+ // WHAT A SCORE IN THIS RANGE DESCRIBES, in the CLI's words.
2563
+ if (raw.meaning) box.append(el("div", "note", raw.meaning));
2564
+ const staged = edits.map.get(key);
2565
+ if (staged) {
2566
+ const line = el("div", "note ex-route-staged");
2567
+ line.append(chip(t("extra.routing.staged"), "info"));
2568
+ line.append(document.createTextNode(" " + staged.value));
2569
+ const undo = el("button", "btn btn-ghost btn-sm", t("extra.routing.undo"));
2570
+ undo.type = "button";
2571
+ undo.addEventListener("click", () => {
2572
+ edits.drop(key);
2573
+ paint();
2574
+ });
2575
+ line.append(undo);
2576
+ box.append(line);
2577
+ return box;
2578
+ }
2579
+ if (!verified) {
2580
+ box.append(el("div", "note", t("extra.routing.lockedHint")));
2581
+ return box;
2582
+ }
2583
+ if (raw.via === "extra") {
2584
+ // The row's own detail, all of it the CLI's. It was never visible before:
2585
+ // a small model and a turn cap decide what a wave actually does.
2586
+ box.append(
2587
+ kvList([
2588
+ [t("extra.routing.kvProfile"), raw.profile],
2589
+ [t("extra.routing.kvModel"), raw.model],
2590
+ [t("extra.routing.kvSmall"), raw.small_model || "—"],
2591
+ [t("extra.routing.kvTurns"), raw.max_turns === null || raw.max_turns === undefined ? "—" : String(raw.max_turns)],
2592
+ ])
2593
+ );
2594
+ const clear = el("button", "btn btn-sm", t("extra.routing.clear"));
2595
+ clear.type = "button";
2596
+ clear.addEventListener("click", () => {
2597
+ edits.action(key, "/api/extra/route/rm", { band: raw.from + "-" + raw.to }, t("extra.routing.stagedClear"));
2598
+ paint();
2599
+ });
2600
+ const acts = el("div", "row-actions");
2601
+ acts.append(clear);
2602
+ box.append(acts);
2603
+ return box;
2604
+ }
2605
+ box.append(exRouteControls(raw, d, edits, key, paint));
2606
+ return box;
2607
+ };
2608
+
2609
+ headBtn.addEventListener("click", () => {
2610
+ const next = !isOpen();
2611
+ if (next) openOnly(entry);
2612
+ setOpen(next);
2613
+ });
2614
+ paint();
2615
+ const entry = { node, close: () => isOpen() && setOpen(false) };
2616
+ // A re-render must not close the row you were working in.
2617
+ if (EX_OPEN_BAND === raw.band) setOpen(true);
2618
+ return entry;
2619
+ }
2620
+
2621
+ function exRouteControls(row, d, edits, key, paint) {
2622
+ // A Claude band. The control offers every connection ORC knows about; one
2623
+ // that has never answered is DISABLED and labelled, never hidden — the
2624
+ // v0.44.0 rule that a value outside its own set must stay visible and must
2625
+ // not be re-offerable.
2626
+ const profiles = (d.list && d.list.profiles) || [];
2627
+ const sel = exSelect(
2628
+ [{ value: "", label: t("extra.routing.pickProfile") }].concat(
2629
+ profiles.map((p) => ({
2630
+ value: p.name,
2631
+ label: p.name + (p.verified_at ? "" : " " + t("extra.routing.untested")),
2632
+ disabled: !p.verified_at,
2633
+ }))
2634
+ )
2635
+ );
2636
+ // v0.51.0 — WHETHER THIS IS A DROPDOWN OR A TEXT BOX IS THE CLI'S ANSWER.
2637
+ // `orc extra models --json` returns `entry: "list" | "free-text"` plus a
2638
+ // `group` per row, and this renders it and derives nothing — the Flow-stepper
2639
+ // rule (`steps[]`), applied to models. A list that came back empty and the
2640
+ // escape-hatch provider are both free text, because a dropdown that cannot
2641
+ // contain the answer is worse than a box.
2642
+ const box = exModelBox(t("extra.routing.modelPh"));
2643
+ const model = box.input;
2644
+ const fillModels = () => {
2645
+ const p = profiles.find((x) => x.name === sel.value);
2646
+ box.load(p ? p.name : null);
2647
+ };
2648
+ sel.addEventListener("change", fillModels);
2649
+ fillModels();
2650
+
2651
+ const add = el("button", "btn btn-sm", t("extra.routing.route"));
2652
+ add.type = "button";
2653
+ add.addEventListener("click", () => {
2654
+ const chosen = box.value();
2655
+ if (!sel.value || !chosen) return;
2656
+ edits.action(
2657
+ key,
2658
+ "/api/extra/route/set",
2659
+ { band: row.from + "-" + row.to, target: sel.value + "/" + chosen },
2660
+ t("extra.routing.stagedSet", { target: sel.value + "/" + chosen })
2661
+ );
2662
+ paint();
2663
+ });
2664
+
2665
+ const controls = el("div", "ex-route-controls");
2666
+ controls.append(sel, box.node, add);
2667
+ return controls;
2668
+ }
2669
+
2670
+ /* ONE model control, and the CLI decides which shape it takes.
2671
+
2672
+ `entry: "list"` renders a real <select> grouped by the CLI's own `group`, each
2673
+ option labelled `label (group)` from data the panel was HANDED. `free-text`
2674
+ renders the box that was always here. The panel never decides which — and it
2675
+ never composes a group from a string it does not own.
2676
+
2677
+ IT ALSO CARRIES F5's WARNING. A model being LISTED is not a model that WORKS:
2678
+ a listed id can be dead upstream, and only a live call tells those two apart.
2679
+ The caveat is the CLI's sentence, printed beside the picker, and the per-model
2680
+ test is offered next to it. */
2681
+ function exModelBox(placeholder) {
2682
+ const node = el("div", "ex-modelbox");
2683
+ const input = exInput(placeholder);
2684
+ const sel = exSelect([]);
2685
+ const note = el("div", "note");
2686
+ const api = { node, input, profile: null, entry: "free-text", value: () => (api.entry === "list" ? sel.value : input.value.trim()) };
2687
+ const paint = () => {
2688
+ node.replaceChildren(api.entry === "list" ? sel : input, note);
2689
+ };
2690
+ api.load = (profile) => {
2691
+ api.profile = profile;
2692
+ if (!profile) {
2693
+ api.entry = "free-text";
2694
+ note.textContent = "";
2695
+ paint();
2696
+ return;
2697
+ }
2698
+ read("/api/extra/models?profile=" + encodeURIComponent(profile))
2699
+ .then((r) => {
2700
+ const j = r.data || {};
2701
+ api.entry = j.entry === "list" ? "list" : "free-text";
2702
+ sel.replaceChildren();
2703
+ const groups = new Map();
2704
+ for (const m of j.models || []) {
2705
+ const g = m.group || "";
2706
+ if (!groups.has(g)) groups.set(g, []);
2707
+ groups.get(g).push(m);
2708
+ }
2709
+ for (const [g, rows] of groups) {
2710
+ const og = el("optgroup");
2711
+ og.label = g;
2712
+ for (const m of rows) {
2713
+ const o = el("option", null, m.label + (m.group ? " (" + m.group + ")" : ""));
2714
+ o.value = m.id;
2715
+ og.append(o);
2716
+ }
2717
+ sel.append(og);
2718
+ }
2719
+ // The provider's own caveat, verbatim: OFFERED is not WORKING.
2720
+ note.textContent = j.caveat || "";
2721
+ paint();
2722
+ })
2723
+ .catch(() => {
2724
+ api.entry = "free-text";
2725
+ paint();
2726
+ });
2727
+ };
2728
+ paint();
2729
+ return api;
2730
+ }
2731
+
2732
+ // Reset here is the GUARDRAILS, not the routes: `orc config reset <key>` removes
2733
+ // a key from the file, and doing that to nine keys is a real write with a real
2734
+ // effect. It deliberately does NOT touch the routing table — dropping every
2735
+ // route row is a change to how this repo builds, and the CLI makes that an
2736
+ // explicit act with a recorded reason for exactly that reason.
2737
+ function exResetModal(edits, body) {
2738
+ const keys = EX_CONFIG_KEYS.slice();
2739
+ const box = el("div", "stack stack-sm");
2740
+ box.append(el("div", null, t("extra.reset.body")));
2741
+ const list = el("div", "file-list");
2742
+ // The keys are NAMED. A count is not consent.
2743
+ for (const k of keys) list.append(el("div", null, k));
2744
+ box.append(list);
2745
+ box.append(el("div", "action-cmd", "orc config reset <key>"));
2746
+ box.append(el("div", "note", t("extra.reset.note")));
2747
+ modal({
2748
+ title: t("extra.reset.title"),
2749
+ body: box,
2750
+ actions: [
2751
+ { label: t("common.cancel"), onClick: (c) => c() },
2752
+ {
2753
+ label: t("extra.reset.apply"),
2754
+ cls: "btn-danger",
2755
+ onClick: async (close) => {
2756
+ close();
2757
+ edits.clear();
2758
+ const failed = [];
2759
+ for (const key of keys) {
2760
+ try {
2761
+ const r = await post("/api/config/reset", { key });
2762
+ if (!r.ok) failed.push(key);
2763
+ } catch (_) {
2764
+ failed.push(key);
2765
+ }
2766
+ }
2767
+ if (failed.length) toast(t("edits.someFailed", { n: failed.length }), "bad", failed.join(String.fromCharCode(10)));
2768
+ else toast(t("extra.reset.done", { n: keys.length }), "ok");
2769
+ renderExtra(body);
2770
+ },
2771
+ },
2772
+ ],
2773
+ });
2774
+ }
2775
+
2776
+ // The keys this panel owns, filled from `orc config list --json` on every
2777
+ // render. The panel never hard-codes a key name — it reads which ones exist.
2778
+ let EX_CONFIG_KEYS = [];
2779
+
2780
+ /* ================================== the guardrails and the cost (W12 T2) ==
2781
+ ZERO PANEL-SIDE WORK FOR A CONFIG KEY. The nine `extra_*` keys render through
2782
+ the SAME `settingRow` / `controlFor` the Settings panel uses, so the control
2783
+ follows the CLI's validator, the shadow lock and its reason come for free, and
2784
+ adding a tenth key is still zero steps here. The only adapter is the edit set:
2785
+ this panel stages everything as an ACTION, so one bar carries a routing change
2786
+ and a guardrail change together.
2787
+
2788
+ THE COST CARD RENDERS `orc extra stats --json` AND COMPUTES NOTHING. Four
2789
+ token kinds, never blended (`docTokenBar`, the same renderer the Docs panel
2790
+ uses — one idea of a token vector). A band nothing joins reads `—`, never `0`,
2791
+ and KEEPS ITS SLOT: "this band cost nothing" and "nothing has run here yet"
2792
+ are different facts. `usd` is null unless ORC priced it itself. */
2793
+
2794
+ // `settingRow`/`controlFor` call `edits.set(key, value, original)` and
2795
+ // `edits.reset(key)`. This translates both into ACTIONS on the panel's one edit
2796
+ // set, which is what lets `applyActions` stay untouched.
2797
+ function exConfigEdits(edits) {
2798
+ return {
2799
+ map: edits.map,
2800
+ get size() {
2801
+ return edits.size;
2802
+ },
2803
+ set(key, value, original) {
2804
+ // Staging a value back to what it already was CLEARS the edit rather than
2805
+ // recording a no-op — the v0.44.1 rule, and it has to be re-stated here
2806
+ // because this adapter is what `controlFor` now talks to.
2807
+ if (String(value) === String(original)) edits.drop(key);
2808
+ else edits.action(key, "/api/config/set", { key, value: String(value) }, String(value));
2809
+ },
2810
+ reset(key) {
2811
+ // `orc config reset <key>` REMOVES the key from the file, which is not the
2812
+ // same write as setting it to its default value.
2813
+ edits.action(key, "/api/config/reset", { key }, t("edits.toDefault"));
2814
+ },
2815
+ drop: (key) => edits.drop(key),
2816
+ clear: () => edits.clear(),
2817
+ entries: () => edits.entries(),
2818
+ };
2819
+ }
2820
+
2821
+ function exGuardrailsCard(d, body, edits) {
2822
+ const c = card(t("extra.guard.title"));
2823
+ const keys = ((d.config && d.config.keys) || []).filter((k) => k.key.indexOf("extra_") === 0);
2824
+ if (!keys.length) {
2825
+ c.append(empty(t("extra.guard.none")));
2826
+ return c;
2827
+ }
2828
+ c.append(el("div", "note", t("extra.guard.sub")));
2829
+ c.append(exWhy(t("extra.guard.subWhy")));
2830
+ const proxy = exConfigEdits(edits);
2831
+ const rows = el("div", "settings-list");
2832
+ for (const k of keys) rows.append(settingRow(k, body, proxy));
2833
+ c.append(rows);
2834
+ return c;
2835
+ }
2836
+
2837
+ /* ------------------------------------------------------------- the cost -- */
2838
+
2839
+ function exCostCard(d) {
2840
+ const c = card(t("extra.cost.title"));
2841
+ const st = d.stats;
2842
+ if (!st) {
2843
+ c.append(empty(t("extra.cost.none"), t("extra.cost.noneHint")));
2844
+ return c;
2845
+ }
2846
+
2847
+ const head = el("div", "row-actions");
2848
+ head.append(chip(t("extra.cost.dispatches", { n: st.dispatches }), st.dispatches ? "info" : "idle"));
2849
+ head.append(el("span", "note", t("extra.cost.scanned", { n: st.files_scanned })));
2850
+ if (st.since) head.append(el("span", "note", st.since));
2851
+ c.append(head);
2852
+
2853
+ // WHERE THE NUMBERS CAME FROM, and the panel decides none of it: the three
2854
+ // counts are the CLI's own `sources` object. "ORC wrote this down itself" and
2855
+ // "a trace happened to mention it" are different levels of confidence in the
2856
+ // same total, and a reader who cannot tell them apart cannot tell a broken
2857
+ // relay from a lane that never ran — which is the exact failure that had this
2858
+ // card reading `0 tasks sent` while two dispatches had really been paid for.
2859
+ const src = st.sources;
2860
+ if (src) {
2861
+ c.append(
2862
+ el(
2863
+ "div",
2864
+ "note",
2865
+ t("extra.cost.sources", {
2866
+ log: src.spend_log,
2867
+ traces: src.traces_only,
2868
+ backfill: src.run_returns,
2869
+ })
2870
+ )
2871
+ );
2872
+ c.append(exWhy(t("extra.cost.sourcesWhy")));
2873
+ // Both of these are ABSENT counts — rows that exist and are not in the
2874
+ // totals. Neither is chrome: a silently short report is the thing this
2875
+ // whole card is being fixed for.
2876
+ if (src.unreadable_spend_lines)
2877
+ c.append(el("div", "note bad", t("extra.cost.unreadable", { n: src.unreadable_spend_lines })));
2878
+ if (src.run_returns_undated_skipped)
2879
+ c.append(el("div", "note bad", t("extra.cost.undated", { n: src.run_returns_undated_skipped })));
2880
+ }
2881
+
2882
+ // v0.54.0 — RELIABILITY BESIDE COST. Whether a profile finishes what it was
2883
+ // given is the other half of what it costs, and it belongs where the user is
2884
+ // choosing one.
2885
+ if (st.reliability) c.append(exReliabilityStrip(st.reliability));
2886
+
2887
+ // The price table's own dating, and its own staleness word. No dollar figure
2888
+ // exists without one, and a stale table says so — a cost ORC did not price is
2889
+ // never printed as a number.
2890
+ if (st.price_table)
2891
+ c.append(
2892
+ el(
2893
+ "div",
2894
+ "note" + (st.price_table.stale ? " bad" : ""),
2895
+ t("extra.cost.priceTable", { as_of: st.price_table.as_of, days: st.price_table.age_days })
2896
+ )
2897
+ );
2898
+
2899
+ if (!st.dispatches) {
2900
+ // The CLI's own hint, verbatim: it names the next command.
2901
+ c.append(empty(t("extra.cost.none"), st.hint));
2902
+ return c;
2903
+ }
2904
+
2905
+ // EVERY ROUTED BAND KEEPS ITS SLOT, joined by profile+band. One that nothing
2906
+ // has run through yet reads `—` in every column, never `0`.
2907
+ const byKey = new Map();
2908
+ for (const b of st.bands || []) byKey.set(b.profile + "|" + b.band, b);
2909
+ const slots = [];
2910
+ for (const r of ((d.route && d.route.rows) || []).filter((x) => x.via === "extra")) {
2911
+ const key = r.profile + "|" + r.band;
2912
+ slots.push({ profile: r.profile, band: r.band, stat: byKey.get(key) || null });
2913
+ byKey.delete(key);
2914
+ }
2915
+ // …and a band the traces know about that is no longer routed keeps its slot
2916
+ // too, because that history is exactly what tells you whether unrouting it was
2917
+ // the right call.
2918
+ for (const b of byKey.values()) slots.push({ profile: b.profile, band: b.band, stat: b, unrouted: true });
2919
+
2920
+ for (const s of slots) c.append(exCostRow(s));
2921
+
2922
+ exCostList(c, t("extra.cost.substitutions"), st.substitutions, (x) => `${x.task} ${x.requested} → ${x.reported}`);
2923
+ exCostList(c, t("extra.cost.reroutes"), st.reroutes, (x) => `${x.task} ${(x.providers || []).join(" → ")}`);
2924
+ exCostList(c, t("extra.cost.fallbacks"), st.fallbacks, (x) => `${x.task} ${x.reason} → ${x.agent}`);
2925
+
2926
+ if ((st.missing_rates || []).length) c.append(exRatesBox(d, st));
2927
+ return c;
2928
+ }
2929
+
2930
+ function exCostRow(s) {
2931
+ const row = el("div", "ex-cost-row");
2932
+ const head = el("div", "row-actions");
2933
+ head.append(el("span", "mono ex-name", s.profile));
2934
+ head.append(el("span", "mono ex-route-band", s.band));
2935
+ if (s.unrouted) head.append(chip(t("extra.cost.unrouted"), "idle"));
2936
+ const g = s.stat;
2937
+ head.append(el("span", "note", g ? t("extra.cost.nDispatches", { n: g.dispatches }) : "—"));
2938
+ row.append(head);
2939
+
2940
+ if (!g) {
2941
+ row.append(el("div", "note", t("extra.cost.never")));
2942
+ return row;
2943
+ }
2944
+
2945
+ const oc = el("div", "row-actions");
2946
+ // The outcome names are the CLI's own field names — never translated.
2947
+ for (const [k, n] of Object.entries(g.outcomes || {}))
2948
+ if (n) oc.append(chip(k + " " + n, k === "done" ? "ok" : k === "failed" ? "bad" : "warn"));
2949
+ for (const e of Object.keys(g.engines || {})) oc.append(chip(e, "info"));
2950
+ row.append(oc);
2951
+
2952
+ // The four kinds, never blended, through the SAME renderer the Docs panel
2953
+ // uses. `cache_read` is usually the largest count and about a tenth of the
2954
+ // price, which is exactly why one blended number would mislead.
2955
+ row.append(docTokenBar(g.usage));
2956
+
2957
+ // THE COUNT THAT KEEPS THE VECTOR HONEST. A total assembled from six of ten
2958
+ // dispatches is not this band's cost, and it says so rather than averaging
2959
+ // the gap away.
2960
+ if (g.usage_reported !== g.dispatches)
2961
+ row.append(
2962
+ el("div", "note bad", t("extra.cost.partial", { got: g.usage_reported, of: g.dispatches, missing: g.usage_missing }))
2963
+ );
2964
+
2965
+ row.append(
2966
+ el(
2967
+ "div",
2968
+ "note",
2969
+ g.usd === null
2970
+ ? t("extra.cost.noRate", { provider: g.provider || "—" })
2971
+ : t("extra.cost.usd", { usd: g.usd < 0.01 ? "<0.01" : g.usd.toFixed(2) })
2972
+ )
2973
+ );
2974
+ return row;
2975
+ }
2976
+
2977
+ function exCostList(c, title, arr, fmt) {
2978
+ if (!arr || !arr.length) return;
2979
+ const box = el("div", "ex-cost-list");
2980
+ const h = el("div", "row-actions");
2981
+ h.append(chip(String(arr.length), "warn"));
2982
+ h.append(el("span", "ex-probe-head", title));
2983
+ box.append(h);
2984
+ for (const x of arr.slice(0, 8)) box.append(el("div", "note mono", fmt(x)));
2985
+ if (arr.length > 8) box.append(el("div", "note", t("extra.cost.more", { n: arr.length - 8 })));
2986
+ c.append(box);
2987
+ }
2988
+
2989
+ // The paste path. `bin/pricing.json` ships every models map EMPTY on purpose —
2990
+ // a figure that is wrong by 2x gets believed — so this is the JSON the CLI built
2991
+ // for the pairs it actually saw, and the note about where to put it is the
2992
+ // CLI's own sentence.
2993
+ function exRatesBox(d, st) {
2994
+ const box = el("div", "ex-rates");
2995
+ box.append(el("div", "ex-probe-head", t("extra.cost.missingRates")));
2996
+ for (const m of st.missing_rates) box.append(el("div", "note mono", m.pair + " · " + t("extra.cost.nDispatches", { n: m.dispatches })));
2997
+ const rates = d.rates;
2998
+ if (rates && rates.paste) {
2999
+ const pre = el("pre", "block wrap", JSON.stringify(rates.paste, null, 2));
3000
+ box.append(pre);
3001
+ const b = el("button", "btn btn-ghost btn-sm", t("common.copy"));
3002
+ b.type = "button";
3003
+ b.addEventListener("click", () => copy(JSON.stringify(rates.paste, null, 2), t("extra.cost.missingRates")));
3004
+ box.append(b);
3005
+ if (rates.where) box.append(el("div", "note", rates.where));
3006
+ for (const c of rates.caveats || []) box.append(el("div", "note", c.provider + ": " + c.caveat));
3007
+ } else {
3008
+ box.append(laneCommand("orc extra rates", t("extra.cost.ratesWhy")));
3009
+ }
3010
+ return box;
3011
+ }