catherd-cli 1.4.0 → 2.0.0-beta.20261022113309

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 (320) hide show
  1. package/README.md +188 -169
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -239
  5. package/package.json +6 -8
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/bin/catherd-mcp +1 -1
  9. package/plugin/commands/catherd.md +2 -2
  10. package/plugin/skills/catherd/SKILL.md +97 -356
  11. package/plugin/skills/catherd-setup/SKILL.md +21 -20
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +19 -4
  14. package/src/adapters/claude-code/index.ts +126 -14
  15. package/src/adapters/codex/events.ts +10 -0
  16. package/src/adapters/codex/index.ts +17 -5
  17. package/src/adapters/cursor/home.ts +1 -1
  18. package/src/adapters/cursor/index.ts +10 -4
  19. package/src/adapters/opencode/index.ts +38 -1
  20. package/src/cli.ts +27 -27
  21. package/src/domain/agents.ts +3 -1
  22. package/src/domain/bars.ts +33 -107
  23. package/src/domain/blocked.ts +72 -0
  24. package/src/domain/branch.ts +51 -0
  25. package/src/domain/brief.ts +106 -0
  26. package/src/domain/catalog.ts +69 -313
  27. package/src/domain/changes.ts +23 -1
  28. package/src/domain/cost.ts +19 -29
  29. package/src/domain/errors.ts +6 -2
  30. package/src/domain/fail-lines.ts +26 -0
  31. package/src/domain/failover.ts +37 -75
  32. package/src/domain/findings.ts +47 -0
  33. package/src/domain/gate-brief.ts +53 -0
  34. package/src/domain/hints.ts +13 -2
  35. package/src/domain/ids.ts +13 -2
  36. package/src/domain/jev.ts +47 -2
  37. package/src/domain/judge.ts +124 -0
  38. package/src/domain/lane.ts +118 -3
  39. package/src/domain/milestone-memory.ts +53 -0
  40. package/src/domain/notice.ts +32 -8
  41. package/src/domain/notify.ts +32 -0
  42. package/src/domain/plan-doc.ts +198 -0
  43. package/src/domain/plan-lint.ts +141 -0
  44. package/src/domain/profile-rules.ts +175 -138
  45. package/src/domain/profile.ts +140 -63
  46. package/src/domain/record.ts +68 -3
  47. package/src/domain/reply.ts +29 -0
  48. package/src/domain/role-prompts.ts +182 -56
  49. package/src/domain/role-scope.ts +76 -0
  50. package/src/domain/role-tools.ts +34 -0
  51. package/src/domain/roles.ts +2 -0
  52. package/src/domain/route.ts +90 -0
  53. package/src/domain/rules.ts +33 -0
  54. package/src/domain/run-events.ts +298 -0
  55. package/src/domain/run-machine.ts +668 -0
  56. package/src/domain/run-state.ts +1310 -0
  57. package/src/domain/scorecard.ts +197 -0
  58. package/src/domain/select.ts +229 -72
  59. package/src/domain/slopalytics.ts +72 -0
  60. package/src/domain/state.ts +5 -0
  61. package/src/domain/util.ts +22 -0
  62. package/src/domain/workspace.ts +130 -0
  63. package/src/entry/answer-command.ts +39 -0
  64. package/src/entry/catalog-command.ts +24 -165
  65. package/src/entry/check-bin.ts +17 -0
  66. package/src/entry/cli-kit.ts +18 -2
  67. package/src/entry/daemon-bin.ts +22 -0
  68. package/src/entry/deps.ts +2 -0
  69. package/src/entry/doctor-command.ts +20 -2
  70. package/src/entry/glyphs.ts +15 -2
  71. package/src/entry/host-arg.ts +28 -0
  72. package/src/entry/init-command.ts +20 -69
  73. package/src/entry/knowledge-command.ts +121 -5
  74. package/src/entry/lock-command.ts +103 -8
  75. package/src/entry/mcp/handshake.ts +6 -2
  76. package/src/entry/mcp/role-bin.ts +20 -0
  77. package/src/entry/mcp/role-server.ts +134 -0
  78. package/src/entry/mcp/server.ts +85 -21
  79. package/src/entry/mcp/session-tools.ts +149 -0
  80. package/src/entry/panel/openapi.ts +189 -0
  81. package/src/entry/panel/server.ts +410 -0
  82. package/src/entry/panel-command.ts +68 -0
  83. package/src/entry/pause-command.ts +67 -0
  84. package/src/entry/profile-command.ts +33 -18
  85. package/src/entry/role-cli-command.ts +159 -0
  86. package/src/entry/runs-command.ts +81 -27
  87. package/src/entry/scorecard-command.ts +187 -0
  88. package/src/entry/supervise-bin.ts +1 -1
  89. package/src/entry/supervise-command.ts +30 -3
  90. package/src/entry/workspace-command.ts +44 -0
  91. package/src/infra/boot-lock.ts +25 -0
  92. package/src/infra/delivery.ts +3 -0
  93. package/src/infra/dispatch-dir.ts +2 -0
  94. package/src/infra/env.ts +11 -3
  95. package/src/infra/git.ts +70 -0
  96. package/src/infra/heavy-lock.ts +133 -6
  97. package/src/infra/host-probe.ts +16 -0
  98. package/src/infra/launch.ts +63 -7
  99. package/src/infra/lock-activity.ts +88 -0
  100. package/src/infra/login-env.ts +48 -0
  101. package/src/infra/memory.ts +45 -0
  102. package/src/infra/ntfy.ts +40 -0
  103. package/src/infra/open-url.ts +42 -0
  104. package/src/infra/paths.ts +37 -2
  105. package/src/infra/role-mcp.ts +69 -0
  106. package/src/infra/slopalytics.ts +30 -0
  107. package/src/infra/store.ts +6 -1
  108. package/src/infra/supervisor.ts +107 -16
  109. package/src/services/admission.ts +133 -31
  110. package/src/services/backends.ts +4 -6
  111. package/src/services/budget.ts +26 -6
  112. package/src/services/capture.ts +1 -22
  113. package/src/services/catalog-service.ts +155 -254
  114. package/src/services/credentials.ts +15 -26
  115. package/src/services/daemon.ts +181 -0
  116. package/src/services/dispatch-service.ts +209 -17
  117. package/src/services/dispatches.ts +30 -2
  118. package/src/services/doctor-access.ts +8 -3
  119. package/src/services/doctor-backends.ts +1 -1
  120. package/src/services/doctor-checks.ts +47 -0
  121. package/src/services/doctor-docker.ts +221 -0
  122. package/src/services/doctor-role-mcp.ts +65 -0
  123. package/src/services/doctor.ts +72 -15
  124. package/src/services/end-push.ts +157 -0
  125. package/src/services/failover-rungs.ts +109 -0
  126. package/src/services/finalize.ts +24 -2
  127. package/src/services/gate-env.ts +217 -0
  128. package/src/services/gate-service.ts +231 -45
  129. package/src/services/global-install.ts +46 -13
  130. package/src/services/jev-service.ts +2 -4
  131. package/src/services/lane-edit.ts +154 -0
  132. package/src/services/lane-service.ts +291 -55
  133. package/src/services/milestone-check.ts +433 -0
  134. package/src/services/milestone-tree.ts +170 -0
  135. package/src/services/milestones.ts +167 -29
  136. package/src/services/notifier.ts +124 -4
  137. package/src/services/notify-service.ts +26 -0
  138. package/src/services/orchestrator-wait.ts +82 -0
  139. package/src/services/panel-setup.ts +116 -0
  140. package/src/services/pause.ts +161 -0
  141. package/src/services/peek.ts +79 -17
  142. package/src/services/plan-input.ts +151 -0
  143. package/src/services/ports.ts +60 -5
  144. package/src/services/preflight.ts +232 -24
  145. package/src/services/profile-service.ts +31 -3
  146. package/src/services/profile-store.ts +18 -6
  147. package/src/services/protocol.ts +123 -25
  148. package/src/services/provenance.ts +34 -89
  149. package/src/services/questions.ts +24 -0
  150. package/src/services/reconcile.ts +33 -1
  151. package/src/services/reentry.ts +7 -6
  152. package/src/services/role-access.ts +27 -0
  153. package/src/services/routing-service.ts +161 -42
  154. package/src/services/run-briefs.ts +477 -0
  155. package/src/services/run-client.ts +217 -0
  156. package/src/services/run-effects.ts +837 -0
  157. package/src/services/run-events-store.ts +174 -0
  158. package/src/services/run-evidence.ts +1 -1
  159. package/src/services/run-pin.ts +125 -0
  160. package/src/services/run-read.ts +221 -0
  161. package/src/services/run-report.ts +176 -0
  162. package/src/services/run-service.ts +204 -23
  163. package/src/services/run-store.ts +159 -18
  164. package/src/services/scorecard-metrics.ts +86 -0
  165. package/src/services/scorecard.ts +382 -0
  166. package/src/services/scratch.ts +60 -0
  167. package/src/services/sessions.ts +3 -1
  168. package/src/services/state.ts +21 -0
  169. package/src/services/summary.ts +76 -12
  170. package/src/services/tree-path.ts +27 -0
  171. package/src/services/verifier-brief.ts +23 -0
  172. package/src/services/verifier-step.ts +48 -0
  173. package/src/services/workspace-admission.ts +259 -0
  174. package/src/services/workspace-service.ts +259 -0
  175. package/src/services/workspace-store.ts +95 -0
  176. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  177. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  178. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  179. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  180. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  181. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  182. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  183. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  184. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  185. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  186. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  187. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  188. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  189. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  190. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  191. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  192. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  193. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  194. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  195. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  196. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  197. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  198. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  199. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  200. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  201. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  202. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  203. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  204. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  205. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  206. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  207. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  208. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  209. package/web/dist/assets/index-C5IbylvM.css +2 -0
  210. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  211. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  212. package/web/dist/assets/label-DFSYBc62.js +1 -0
  213. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  214. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  215. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  216. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  217. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  218. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  219. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  220. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  221. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  222. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  223. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  224. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  225. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  226. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  227. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  228. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  229. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  230. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  231. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  232. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  233. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  234. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  235. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  236. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  237. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  238. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  239. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  240. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  241. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  242. package/web/dist/assets/time--9kSQbbp.js +1 -0
  243. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  244. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  245. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  246. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  247. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  248. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  249. package/web/dist/index.html +15 -0
  250. package/catalog/ATTRIBUTION.md +0 -36
  251. package/catalog/scores.json +0 -3202
  252. package/catalog/sources.json +0 -97
  253. package/src/adapters/antigravity/events.ts +0 -131
  254. package/src/adapters/antigravity/home.ts +0 -46
  255. package/src/adapters/antigravity/index.ts +0 -320
  256. package/src/adapters/antigravity/models.ts +0 -37
  257. package/src/adapters/grok/events.ts +0 -112
  258. package/src/adapters/grok/home.ts +0 -132
  259. package/src/adapters/grok/index.ts +0 -339
  260. package/src/adapters/grok/models.ts +0 -48
  261. package/src/domain/calibration.ts +0 -125
  262. package/src/domain/sources.ts +0 -144
  263. package/src/entry/mcp/dispatch-tools.ts +0 -52
  264. package/src/entry/mcp/lane-tools.ts +0 -84
  265. package/src/entry/mcp/protocol-tools.ts +0 -57
  266. package/src/entry/mcp/run-tools.ts +0 -148
  267. package/src/entry/mcp/setup-tools.ts +0 -108
  268. package/src/entry/tui/commands.ts +0 -563
  269. package/src/entry/tui/effects.ts +0 -381
  270. package/src/entry/tui/fixtures.ts +0 -637
  271. package/src/entry/tui/profile-edits.ts +0 -146
  272. package/src/entry/tui/profile-tree.ts +0 -429
  273. package/src/entry/tui/providers/app.tsx +0 -162
  274. package/src/entry/tui/providers/boundary.tsx +0 -57
  275. package/src/entry/tui/providers/data.tsx +0 -160
  276. package/src/entry/tui/providers/keymap.tsx +0 -166
  277. package/src/entry/tui/providers/root.tsx +0 -45
  278. package/src/entry/tui/providers/theme.tsx +0 -17
  279. package/src/entry/tui/providers/toast.tsx +0 -77
  280. package/src/entry/tui/run.tsx +0 -114
  281. package/src/entry/tui/state.ts +0 -373
  282. package/src/entry/tui/stories.ts +0 -114
  283. package/src/entry/tui/text.ts +0 -130
  284. package/src/entry/tui/theme.ts +0 -140
  285. package/src/entry/tui/views/app.tsx +0 -208
  286. package/src/entry/tui/views/dialogs.tsx +0 -41
  287. package/src/entry/tui/views/profile-actions.ts +0 -331
  288. package/src/entry/tui/views/profiles.tsx +0 -520
  289. package/src/entry/tui/views/runs.tsx +0 -573
  290. package/src/entry/tui/views/save-dialog.tsx +0 -340
  291. package/src/entry/tui/views/status.tsx +0 -209
  292. package/src/entry/tui/widgets/chrome.tsx +0 -114
  293. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  294. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  295. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  296. package/src/entry/tui/widgets/dialog.tsx +0 -68
  297. package/src/entry/tui/widgets/line.tsx +0 -92
  298. package/src/entry/tui/widgets/list.tsx +0 -197
  299. package/src/entry/tui/widgets/spinner.tsx +0 -49
  300. package/src/entry/tui/widgets/toast.tsx +0 -62
  301. package/src/infra/sources/arena.ts +0 -87
  302. package/src/infra/sources/artificial-analysis.ts +0 -112
  303. package/src/infra/sources/cache.ts +0 -124
  304. package/src/infra/sources/csv.ts +0 -38
  305. package/src/infra/sources/epoch.ts +0 -82
  306. package/src/infra/sources/http.ts +0 -75
  307. package/src/infra/sources/litellm.ts +0 -58
  308. package/src/infra/sources/models-dev.ts +0 -59
  309. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  310. package/src/infra/sources/openrouter-models.ts +0 -60
  311. package/src/infra/sources/rows.ts +0 -38
  312. package/src/infra/sources/vectara.ts +0 -69
  313. package/src/infra/sources/zip.ts +0 -48
  314. package/src/services/catalog-refresh.ts +0 -201
  315. package/src/services/doctor-sources.ts +0 -76
  316. package/src/services/runs-page.ts +0 -255
  317. package/src/services/source-derive.ts +0 -374
  318. package/src/services/source-sync.ts +0 -305
  319. package/src/services/standins.ts +0 -204
  320. package/src/services/treat-likes.ts +0 -97
@@ -1,520 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
2
- import { type Profile, resolveProfile } from "../../../domain/profile.ts";
3
- import type { Role } from "../../../domain/roles.ts";
4
- import { errorMessage, isCatherdError } from "../../../domain/errors.ts";
5
- import { hereWord } from "../effects.ts";
6
- import { useApp, useDialogHandler } from "../providers/app.tsx";
7
- import { type Data, useData, useLoad } from "../providers/data.tsx";
8
- import { useCommandLayer } from "../providers/keymap.tsx";
9
- import { errorToast } from "../providers/toast.tsx";
10
- import { useUi } from "../providers/theme.tsx";
11
- import {
12
- failoverOptions,
13
- numberPatch,
14
- numberValue,
15
- parseNumber,
16
- patchFor,
17
- startOptions,
18
- treatLikeOptions,
19
- } from "../profile-edits.ts";
20
- import { buildRows, filterRows, firstMatch, type Row, type RowAction, withStaged } from "../profile-tree.ts";
21
- import { currentDraft, dirtyCount } from "../state.ts";
22
- import { wrap } from "../text.ts";
23
- import { glyph, STATE_TOKEN } from "../theme.ts";
24
- import { Line, type Part } from "../widgets/line.tsx";
25
- import { List, type ListItem, useSelected } from "../widgets/list.tsx";
26
- import { openActivate, openNewProfile, openRevert, openSave, showProfile } from "./profile-actions.ts";
27
-
28
- /** What a row does, for the line under the tree when it has no issue. */
29
- const ABOUT: Partial<Record<RowAction["type"], string>> = {
30
- role: "space turns the role on or off; enter opens its access, default rung and models",
31
- access:
32
- "enter cycles read-only → workspace-write → full; enforced: the backend stops a write, advisory: it is only asked",
33
- start: "enter picks where a lane starts without a kind and difficulty",
34
- model: "enter opens its efforts; space never changes a rung",
35
- rung: "space ticks this rung onto the role's ladder; routing orders the ladder by cost",
36
- objective: "enter switches between cost and speed",
37
- jev: "enter turns Jev on (auto: when a key exists) or off",
38
- isolated: "space runs the backend with catherd's own config instead of yours",
39
- number: "enter edits the value",
40
- failover: "enter picks the stand-in on a usage limit; it runs on a fresh thread",
41
- notify: "space turns this notification on or off",
42
- };
43
-
44
- /** Spec 1.3 §8: a harness row's line: what isolating it needs, and what its native mode loads. */
45
- export const isolationAbout = (i: { note?: string; key?: string }): string =>
46
- [`${ABOUT.isolated}${i.key ? ` (it needs ${i.key})` : ""}`, i.note].filter(Boolean).join("; ");
47
-
48
- /**
49
- * The profiles poll failed after a good read (its error is newer than the value it keeps): one error line
50
- * and, when the failure says, its fix; none once a read succeeds again.
51
- */
52
- export function staleLines(data: Data, plain: boolean, o: { fix?: boolean } = {}): Part[] {
53
- const p = data.profiles;
54
- if (p.error === null || p.value === null) return [];
55
- const said: Part[] = [
56
- {
57
- text: ` ${glyph("fail", plain)} could not read the profiles again: ${p.error} · showing the last good read`,
58
- tone: "error",
59
- },
60
- ];
61
- if (p.fix && o.fix !== false) said.push({ text: ` fix: ${p.fix}`, tone: "muted" });
62
- return said;
63
- }
64
-
65
- /** Only ticking an unscored effort asks for a treat-like; unticking a rung always just unticks it. */
66
- const needsTreatLike = (p: Profile, a: RowAction): boolean =>
67
- a.type === "rung" && !a.scored && !p.roles[a.role].rungs.includes(a.rung);
68
-
69
- /**
70
- * Spec §9.1 tab 2: the profile's line, then one tree (roles → access + backend → model → efforts, then
71
- * routing, harness, budget, failover, timeouts, notify), then a line about the selected row. Every edit
72
- * is staged in the draft; nothing is written before ctrl+s.
73
- */
74
- export function ProfilesView(props: { width: number; height: number }) {
75
- const app = useApp();
76
- const data = useData();
77
- const ui = useUi();
78
- const { selected, select: setSelected, current: selectedNow } = useSelected();
79
- const [expanded, setExpanded] = useState<Set<string>>(() => new Set(["role:worker"]));
80
- const [filter, setFilter] = useState<string | null>(null);
81
- const draft = currentDraft(app.state);
82
- const here = data.profiles.value?.here ?? null;
83
- // the profile the tab opens on; a read of it that fails is shown in the tab (below), and read again
84
- // on r and after every good poll of the profiles
85
- const [openFailed, setOpenFailed] = useState<{ name: string; error: string; fix: string | null } | null>(
86
- null,
87
- );
88
- const [openTries, setOpenTries] = useState(0);
89
- const polledAt = data.profiles.at;
90
- useEffect(() => {
91
- if (app.state.profile || !here) return;
92
- const e = showProfile(app, here, { quiet: true });
93
- setOpenFailed(
94
- e === null
95
- ? null
96
- : {
97
- name: here,
98
- error: errorMessage(e),
99
- fix: isCatherdError(e) && e.fix ? e.fix : null,
100
- },
101
- );
102
- }, [app.state.profile, here, app, polledAt, openTries]);
103
- const profile = useMemo(() => (draft ? resolveProfile(draft.doc, draft.name, draft.host) : null), [draft]);
104
- const billingKey = JSON.stringify(profile?.billing ?? {});
105
- // read again after every save too, by the identity of the base it reads back (the same bytes when only
106
- // treat-likes changed): a save writes the staged treat-likes to the catalog override and clears them
107
- // from the draft, so the catalog read before it would lack them
108
- const reads = useRef(0);
109
- const savedKey = useMemo(() => ++reads.current, [draft?.base]);
110
- const loaded = useLoad(
111
- () => (profile ? app.effects.catalog(profile.billing) : null),
112
- `${billingKey}|${savedKey}`,
113
- );
114
- const catalog = useMemo(
115
- () => (loaded.value && draft ? withStaged(loaded.value.catalog, draft.treatLikes) : null),
116
- [loaded.value, draft],
117
- );
118
- const validation = useMemo(
119
- () => (profile && catalog ? app.effects.validate(profile, catalog) : { errors: [], warnings: [] }),
120
- [profile, catalog, app.effects],
121
- );
122
- const rows: Row[] = useMemo(() => {
123
- if (!profile || !catalog || !loaded.value || !draft) return [];
124
- const all = buildRows({
125
- profile,
126
- models: loaded.value.models,
127
- catalog,
128
- staged: draft.treatLikes,
129
- expanded,
130
- harnesses: app.effects.harnesses,
131
- enforcement: app.effects.enforcement,
132
- validation,
133
- expandAll: Boolean(filter),
134
- });
135
- return filter ? filterRows(all, filter) : all;
136
- }, [profile, catalog, loaded.value, draft, expanded, filter, validation, app.effects]);
137
- // a read that failed is shown with its fix and a retry, not as one still pending: the profiles (no
138
- // draft can open without them; the 2 s poll also reads them again) or, once a profile shows, the catalog
139
- const failure =
140
- !draft && data.profiles.error !== null
141
- ? {
142
- what: "the profiles",
143
- error: data.profiles.error,
144
- fix: data.profiles.fix,
145
- retry: data.profiles.refresh,
146
- polled: true,
147
- }
148
- : !draft && openFailed
149
- ? {
150
- what: `the profile ${openFailed.name}`,
151
- error: openFailed.error,
152
- fix: openFailed.fix,
153
- retry: () => setOpenTries((n) => n + 1),
154
- polled: true,
155
- }
156
- : profile && loaded.error !== null && !loaded.value
157
- ? {
158
- what: "the catalog",
159
- error: loaded.error,
160
- fix: loaded.fix,
161
- retry: loaded.refresh,
162
- polled: false,
163
- }
164
- : null;
165
- // a later read that failed over a kept good one: said under the header until a read succeeds, like the
166
- // Runs tab over its kept rows; the draft stays open and editable
167
- const stale = draft ? staleLines(data, ui.plain) : [];
168
- const row = rows.find((r) => r.key === selected) ?? null;
169
- /** the row the cursor is on now, which a key earlier in the same tick may have moved */
170
- const rowNow = () => rows.find((r) => r.key === selectedNow()) ?? null;
171
- // typing a filter puts the cursor on the first row that matches it; an edit under a kept filter
172
- // (new rows, same text) leaves the cursor where it is
173
- const placedFor = useRef<string | null>(null);
174
- useEffect(() => {
175
- if (!filter) {
176
- placedFor.current = null;
177
- return;
178
- }
179
- if (filter === placedFor.current) return;
180
- const m = firstMatch(rows, filter);
181
- if (!m) return;
182
- placedFor.current = filter;
183
- setSelected(m.key);
184
- }, [filter, rows]);
185
- /** the draft as it is now: two keys in one tick must not both edit the profile this render drew */
186
- const current = () => {
187
- const d = currentDraft(app.getState());
188
- return d ? resolveProfile(d.doc, d.name, d.host) : null;
189
- };
190
- const edit = (patch: ReturnType<typeof patchFor>) => patch && app.dispatch({ type: "edit", patch });
191
- const toggleOpen = (r: Row, open?: boolean) =>
192
- setExpanded((e) => {
193
- const next = new Set(e);
194
- if (open ?? !next.has(r.key)) next.add(r.key);
195
- else next.delete(r.key);
196
- return next;
197
- });
198
- const pick = (r: Row) => {
199
- if (!profile || !loaded.value || !catalog) return;
200
- const a = r.action;
201
- if (a.type === "start")
202
- app.dispatch({
203
- type: "open",
204
- dialog: {
205
- kind: "select",
206
- purpose: { type: "start", role: a.role },
207
- title: `Default rung for ${a.role}`,
208
- options: startOptions(profile, a.role),
209
- empty: "no rungs",
210
- },
211
- });
212
- else if (a.type === "failover")
213
- app.dispatch({
214
- type: "open",
215
- dialog: {
216
- kind: "select",
217
- purpose: { type: "failover", rung: a.rung },
218
- title: `Stand-in for ${a.rung}`,
219
- options: failoverOptions(profile, loaded.value.models, catalog, a.rung),
220
- empty: "no scored rung on another quota",
221
- },
222
- });
223
- else if (a.type === "rung" && !a.scored) openTreatLike(a.rung, a.role);
224
- else if (a.type === "number") {
225
- const v = numberValue(profile, a.path);
226
- app.dispatch({
227
- type: "open",
228
- dialog: {
229
- kind: "prompt",
230
- purpose: { type: "number", path: a.path },
231
- title: `Set ${a.path}`,
232
- label: a.path.startsWith("budget.") ? "a number above 0; empty for no cap" : "minutes, above 0",
233
- value: v === undefined ? "" : String(v),
234
- error: null,
235
- },
236
- });
237
- }
238
- };
239
- /** spec 1.2 §6.4: the treat-like picker, its three nearest stand-ins first */
240
- const openTreatLike = (rung: string, role: Role | null) => {
241
- if (!catalog) return;
242
- const suggestions = app.effects.suggest(catalog, rung);
243
- app.dispatch({
244
- type: "open",
245
- dialog: {
246
- kind: "select",
247
- purpose: { type: "treatLike", rung, role },
248
- title: `Treat ${rung} like…`,
249
- options: treatLikeOptions(catalog, suggestions),
250
- suggested: suggestions.map((s) => s.like),
251
- empty: "no scored rung",
252
- },
253
- });
254
- };
255
- /** spec 1.2 §9: the rung's values with confidence and source, and catherd's runs on it */
256
- const openDetail = (rung: string) => {
257
- if (!catalog) return;
258
- const d = app.effects.rungDetail(catalog, rung);
259
- app.dispatch({
260
- type: "open",
261
- dialog: {
262
- kind: "select",
263
- purpose: { type: "rung", rung },
264
- title: rung,
265
- options: [
266
- ...d.values.map((v) => ({
267
- value: v.dim,
268
- title: `${v.dim} ${v.value}`,
269
- group: "values",
270
- detail: `${v.inferred ? `inferred from ${v.from}` : v.confidence} · ${v.source} ${v.benchmark} · ${v.date}`,
271
- })),
272
- {
273
- value: "runs",
274
- title: d.evidence ?? "no runs yet",
275
- group: "catherd's runs (never used to route)",
276
- },
277
- ],
278
- empty: "no values",
279
- },
280
- });
281
- };
282
- const primary = (r: Row | null) => {
283
- const p = current();
284
- if (!r || !p) return;
285
- const a = r.action;
286
- if (a.type === "role" || a.type === "model") return toggleOpen(r);
287
- if (a.type === "start" || a.type === "failover" || a.type === "number" || needsTreatLike(p, a))
288
- return pick(r);
289
- edit(patchFor(p, a));
290
- };
291
- const toggle = (r: Row | null) => {
292
- const p = current();
293
- if (!r || !p) return;
294
- const a = r.action;
295
- if (a.type === "model") return toggleOpen(r);
296
- if (needsTreatLike(p, a)) return pick(r);
297
- if (a.type === "role" || a.type === "rung" || a.type === "isolated" || a.type === "notify")
298
- edit(patchFor(p, a));
299
- };
300
- useCommandLayer("tab.profiles", {
301
- "profile.save": () => openSave(app),
302
- "profile.activate": () => openActivate(app, data),
303
- "profile.copy": () => draft && openNewProfile(app, draft.name),
304
- "profile.revert": () => openRevert(app),
305
- "catalog.refresh": () => {
306
- // after a failed read, r reads it again (and only that)
307
- if (failure) return failure.retry();
308
- // spec 1.2 §9: r lists every backend's models and syncs the public sources, then shows each source's
309
- // age and last error
310
- void Promise.all([app.effects.refreshCatalog(), app.effects.syncSources()]).then(
311
- ([, sync]) => {
312
- loaded.refresh();
313
- app.toast({
314
- variant: sync.failed.length ? "warning" : "success",
315
- message: sync.busy
316
- ? "Catalog refreshed; another sync is running"
317
- : `Catalog refreshed${sync.newlyScored.length ? `; ${sync.newlyScored.length} rungs newly scored` : ""}`,
318
- });
319
- app.dispatch({
320
- type: "open",
321
- dialog: {
322
- kind: "select",
323
- purpose: { type: "sources" },
324
- title: "Sources",
325
- options: app.effects.sources().map((s) => ({
326
- value: s.source,
327
- title: s.name,
328
- detail: s.error ? `${s.age} · ${s.error}` : s.age,
329
- })),
330
- empty: "no sources",
331
- },
332
- });
333
- },
334
- (e: unknown) => app.toast(errorToast(e)),
335
- );
336
- },
337
- "edit.undo": () => app.dispatch({ type: "undo" }),
338
- "edit.redo": () => app.dispatch({ type: "redo" }),
339
- });
340
- useCommandLayer("row.profiles", {
341
- "tree.detail": () => {
342
- const a = rowNow()?.action;
343
- if (a?.type === "rung") openDetail(a.rung);
344
- else app.toast({ variant: "info", message: "Pick a rung (an effort row) to see its values" });
345
- },
346
- "tree.treatLike": () => {
347
- const a = rowNow()?.action;
348
- if (a?.type === "rung") openTreatLike(a.rung, a.role);
349
- else app.toast({ variant: "info", message: "Pick a rung (an effort row) to map it" });
350
- },
351
- "tree.toggle": () => toggle(rowNow()),
352
- "tree.open": () => primary(rowNow()),
353
- "tree.expand": () => {
354
- const row = rowNow();
355
- if (row?.expandable) toggleOpen(row, true);
356
- },
357
- "tree.collapse": () => {
358
- const row = rowNow();
359
- if (!row) return;
360
- if (row.expandable && row.expanded) return toggleOpen(row, false);
361
- let p = rows.find((r) => r.key === row.parent);
362
- while (p && !p.selectable) p = rows.find((r) => r.key === p?.parent);
363
- if (p) setSelected(p.key);
364
- },
365
- });
366
- useDialogHandler("start", (p, value) => {
367
- app.dispatch({ type: "close" });
368
- if (p.type === "start") edit({ roles: { [p.role]: { defaultRung: value || null } } });
369
- });
370
- useDialogHandler("failover", (p, value) => {
371
- app.dispatch({ type: "close" });
372
- if (p.type === "failover") edit({ failover: { [p.rung]: value || null } });
373
- });
374
- useDialogHandler("treatLike", (p, value) => {
375
- app.dispatch({ type: "close" });
376
- const prof = current();
377
- if (p.type !== "treatLike" || !prof) return;
378
- // one undo step: the treat-like and the tick it brings
379
- const tick =
380
- p.role && !prof.roles[p.role].rungs.includes(p.rung)
381
- ? patchFor(prof, { type: "rung", role: p.role, rung: p.rung, scored: true })
382
- : null;
383
- app.dispatch({ type: "treatLike", rung: p.rung, like: value, ...(tick ? { patch: tick } : {}) });
384
- });
385
- useDialogHandler("sources", () => app.dispatch({ type: "close" }));
386
- useDialogHandler("rung", () => app.dispatch({ type: "close" }));
387
- useDialogHandler("number", (p, value) => {
388
- if (p.type !== "number") return;
389
- const r = parseNumber(p.path, value);
390
- if ("error" in r) return app.dispatch({ type: "invalid", error: r.error });
391
- app.dispatch({ type: "close" });
392
- edit(numberPatch(p.path, r.value));
393
- });
394
-
395
- const unsaved = draft ? dirtyCount(draft) : 0;
396
- const top: Part[] = draft
397
- ? [
398
- { text: " PROFILE ", bold: true },
399
- { text: draft.name, bold: true },
400
- {
401
- text: draft.name === here ? ` (${hereWord(data.profiles.value ?? { repo: null })})` : "",
402
- tone: "muted",
403
- },
404
- { text: unsaved ? ` · ${unsaved} unsaved` : "", tone: "warning" },
405
- { text: ` ${glyph("dot", ui.plain)} ctrl+x l switch · ctrl+x n new`, tone: "muted" },
406
- ]
407
- : failure
408
- ? [{ text: " PROFILE", bold: true }]
409
- : [{ text: " reading the profile…", tone: "muted" }];
410
- const about = row?.issue
411
- ? [
412
- {
413
- text: ` ${glyph(row.issue.level === "error" ? "fail" : "warn", ui.plain)} ${row.issue.message}`,
414
- tone: STATE_TOKEN[row.issue.level === "error" ? "fail" : "warn"],
415
- },
416
- ...(row.issue.fix ? [{ text: ` fix: ${row.issue.fix}`, tone: "muted" as const }] : []),
417
- ]
418
- : [
419
- {
420
- text: ` ${
421
- row?.action.type === "isolated"
422
- ? isolationAbout(app.effects.isolation(row.action.harness))
423
- : ((row && ABOUT[row.action.type]) ?? "")
424
- }`,
425
- tone: "muted" as const,
426
- },
427
- ];
428
- const aboutLines = about
429
- .flatMap((p) => wrap(p.text, props.width).map((text) => ({ ...p, text })))
430
- .slice(0, 2);
431
- // the value column of a group of rows starts after its longest label, so long model names line up
432
- const widest = new Map<string | null, number>();
433
- for (const r of rows)
434
- if (r.selectable) widest.set(r.parent, Math.max(widest.get(r.parent) ?? 0, Bun.stringWidth(r.label)));
435
- const items: ListItem[] = rows.map((r) => ({
436
- key: r.key,
437
- selectable: r.selectable,
438
- render: (sel, w) => {
439
- const indent = " ".repeat(r.depth);
440
- const marker = r.expandable ? `${glyph(r.expanded ? "open" : "shut", ui.plain)} ` : " ";
441
- const check = r.check === null ? "" : `${glyph(r.check ? "on" : "off", ui.plain)} `;
442
- const issue = r.issue ? ` ${glyph(r.issue.level === "error" ? "fail" : "warn", ui.plain)}` : "";
443
- if (!r.selectable)
444
- return (
445
- <Line
446
- width={w}
447
- parts={[
448
- {
449
- text: ` ${indent}${r.label}`,
450
- bold: r.depth === 0,
451
- tone: r.depth === 0 ? undefined : "muted",
452
- },
453
- { text: r.value ? ` ${r.value}` : "", tone: "muted" },
454
- ]}
455
- />
456
- );
457
- const labelWidth = Math.max(14, 30 - r.depth * 2, (widest.get(r.parent) ?? 0) + 2);
458
- return (
459
- <Line
460
- width={w}
461
- selected={sel}
462
- dim={r.dim}
463
- parts={[
464
- { text: ` ${indent}${marker}${check}` },
465
- {
466
- text: r.label.length + 2 > labelWidth ? `${r.label} ` : r.label.padEnd(labelWidth),
467
- bold: r.depth === 1,
468
- },
469
- { text: r.value, tone: "muted" },
470
- { text: issue, tone: r.issue?.level === "error" ? "error" : "warning" },
471
- ]}
472
- />
473
- );
474
- },
475
- }));
476
- return (
477
- <box flexDirection="column" width={props.width} height={props.height}>
478
- <Line width={props.width} parts={top} />
479
- {stale.map((p, i) => (
480
- <Line key={`stale${i}`} width={props.width} parts={[p]} />
481
- ))}
482
- {failure ? (
483
- <box flexDirection="column" width={props.width} height={props.height - 3}>
484
- {[
485
- ...wrap(
486
- ` ${glyph("fail", ui.plain)} could not read ${failure.what}: ${failure.error}`,
487
- props.width,
488
- ).map((text): Part => ({ text, tone: "error" })),
489
- ...(failure.fix ? wrap(` fix: ${failure.fix}`, props.width) : []).map((text): Part => ({
490
- text,
491
- tone: "muted",
492
- })),
493
- {
494
- text: ` r retry${failure.polled ? " · also read again every 2 s" : ""}`,
495
- tone: "muted",
496
- } as Part,
497
- ]
498
- .slice(0, Math.max(0, props.height - 3))
499
- .map((p, i) => (
500
- <Line key={i} width={props.width} parts={[p]} />
501
- ))}
502
- </box>
503
- ) : (
504
- <List
505
- items={items}
506
- selected={selected}
507
- onSelect={setSelected}
508
- width={props.width}
509
- height={props.height - 3 - stale.length}
510
- filter={filter}
511
- onFilter={setFilter}
512
- empty="reading the catalog…"
513
- />
514
- )}
515
- {[0, 1].map((i) => (
516
- <Line key={i} width={props.width} parts={aboutLines[i] ? [aboutLines[i]] : []} />
517
- ))}
518
- </box>
519
- );
520
- }