catherd-cli 1.5.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 (275) hide show
  1. package/README.md +164 -208
  2. package/THIRD_PARTY_NOTICES.md +217 -35
  3. package/catalog/jev.json +27 -1
  4. package/catalog/models.json +56 -247
  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 -421
  11. package/plugin/skills/catherd-setup/SKILL.md +20 -19
  12. package/src/adapters/all.ts +0 -4
  13. package/src/adapters/backend.ts +1 -2
  14. package/src/adapters/cursor/home.ts +1 -1
  15. package/src/adapters/cursor/index.ts +4 -4
  16. package/src/cli.ts +22 -27
  17. package/src/domain/bars.ts +33 -107
  18. package/src/domain/blocked.ts +72 -0
  19. package/src/domain/branch.ts +51 -0
  20. package/src/domain/brief.ts +21 -6
  21. package/src/domain/catalog.ts +69 -324
  22. package/src/domain/cost.ts +15 -30
  23. package/src/domain/errors.ts +2 -2
  24. package/src/domain/fail-lines.ts +26 -0
  25. package/src/domain/failover.ts +37 -87
  26. package/src/domain/findings.ts +47 -0
  27. package/src/domain/gate-brief.ts +1 -0
  28. package/src/domain/ids.ts +13 -2
  29. package/src/domain/jev.ts +47 -2
  30. package/src/domain/judge.ts +124 -0
  31. package/src/domain/lane.ts +22 -3
  32. package/src/domain/milestone-memory.ts +53 -0
  33. package/src/domain/notice.ts +25 -8
  34. package/src/domain/notify.ts +32 -0
  35. package/src/domain/plan-doc.ts +198 -0
  36. package/src/domain/plan-lint.ts +141 -0
  37. package/src/domain/profile-rules.ts +101 -160
  38. package/src/domain/profile.ts +116 -63
  39. package/src/domain/record.ts +53 -2
  40. package/src/domain/role-prompts.ts +131 -45
  41. package/src/domain/role-scope.ts +4 -34
  42. package/src/domain/role-tools.ts +2 -2
  43. package/src/domain/roles.ts +2 -0
  44. package/src/domain/route.ts +17 -0
  45. package/src/domain/rules.ts +33 -0
  46. package/src/domain/run-events.ts +298 -0
  47. package/src/domain/run-machine.ts +668 -0
  48. package/src/domain/run-state.ts +1310 -0
  49. package/src/domain/scorecard.ts +197 -0
  50. package/src/domain/select.ts +151 -146
  51. package/src/domain/slopalytics.ts +72 -0
  52. package/src/entry/answer-command.ts +39 -0
  53. package/src/entry/catalog-command.ts +25 -174
  54. package/src/entry/check-bin.ts +17 -0
  55. package/src/entry/cli-kit.ts +1 -1
  56. package/src/entry/daemon-bin.ts +22 -0
  57. package/src/entry/glyphs.ts +15 -2
  58. package/src/entry/init-command.ts +18 -68
  59. package/src/entry/knowledge-command.ts +45 -4
  60. package/src/entry/lock-command.ts +6 -7
  61. package/src/entry/mcp/role-server.ts +18 -0
  62. package/src/entry/mcp/server.ts +13 -40
  63. package/src/entry/mcp/session-tools.ts +149 -0
  64. package/src/entry/panel/openapi.ts +189 -0
  65. package/src/entry/panel/server.ts +410 -0
  66. package/src/entry/panel-command.ts +68 -0
  67. package/src/entry/profile-command.ts +10 -14
  68. package/src/entry/role-cli-command.ts +47 -3
  69. package/src/entry/runs-command.ts +3 -19
  70. package/src/entry/scorecard-command.ts +187 -0
  71. package/src/entry/supervise-bin.ts +1 -1
  72. package/src/infra/env.ts +0 -2
  73. package/src/infra/git.ts +64 -0
  74. package/src/infra/heavy-lock.ts +45 -7
  75. package/src/infra/launch.ts +63 -7
  76. package/src/infra/memory.ts +45 -0
  77. package/src/infra/ntfy.ts +40 -0
  78. package/src/infra/open-url.ts +42 -0
  79. package/src/infra/paths.ts +2 -2
  80. package/src/infra/slopalytics.ts +30 -0
  81. package/src/infra/store.ts +6 -1
  82. package/src/services/admission.ts +24 -9
  83. package/src/services/backends.ts +4 -6
  84. package/src/services/capture.ts +1 -22
  85. package/src/services/catalog-service.ts +155 -262
  86. package/src/services/credentials.ts +4 -20
  87. package/src/services/daemon.ts +181 -0
  88. package/src/services/dispatch-service.ts +5 -1
  89. package/src/services/doctor-backends.ts +1 -1
  90. package/src/services/doctor-checks.ts +47 -0
  91. package/src/services/doctor.ts +4 -9
  92. package/src/services/failover-rungs.ts +109 -0
  93. package/src/services/finalize.ts +12 -0
  94. package/src/services/gate-env.ts +1 -2
  95. package/src/services/gate-service.ts +21 -6
  96. package/src/services/jev-service.ts +2 -4
  97. package/src/services/lane-service.ts +34 -3
  98. package/src/services/milestone-check.ts +433 -0
  99. package/src/services/milestone-tree.ts +170 -0
  100. package/src/services/milestones.ts +35 -8
  101. package/src/services/notifier.ts +69 -2
  102. package/src/services/notify-service.ts +26 -0
  103. package/src/services/panel-setup.ts +116 -0
  104. package/src/services/plan-input.ts +151 -0
  105. package/src/services/ports.ts +30 -4
  106. package/src/services/preflight.ts +82 -8
  107. package/src/services/profile-service.ts +30 -3
  108. package/src/services/profile-store.ts +8 -3
  109. package/src/services/protocol.ts +4 -0
  110. package/src/services/provenance.ts +31 -90
  111. package/src/services/questions.ts +9 -0
  112. package/src/services/reconcile.ts +11 -0
  113. package/src/services/routing-service.ts +26 -15
  114. package/src/services/run-briefs.ts +477 -0
  115. package/src/services/run-client.ts +217 -0
  116. package/src/services/run-effects.ts +837 -0
  117. package/src/services/run-events-store.ts +174 -0
  118. package/src/services/run-evidence.ts +1 -1
  119. package/src/services/run-read.ts +221 -0
  120. package/src/services/run-report.ts +176 -0
  121. package/src/services/run-service.ts +55 -6
  122. package/src/services/run-store.ts +54 -7
  123. package/src/services/scorecard-metrics.ts +86 -0
  124. package/src/services/scorecard.ts +382 -0
  125. package/src/services/summary.ts +27 -3
  126. package/src/services/tree-path.ts +27 -0
  127. package/src/services/workspace-admission.ts +5 -13
  128. package/src/services/workspace-service.ts +5 -0
  129. package/web/dist/assets/Brand-PKlUX5TE.js +1 -0
  130. package/web/dist/assets/Brand-PKlUX5TE.js.br +0 -0
  131. package/web/dist/assets/Brand-PKlUX5TE.js.gz +0 -0
  132. package/web/dist/assets/Modal-Dxc5so53.js +1 -0
  133. package/web/dist/assets/Modal-Dxc5so53.js.br +0 -0
  134. package/web/dist/assets/Modal-Dxc5so53.js.gz +0 -0
  135. package/web/dist/assets/Pressable-C8-T6Ed_.js +1 -0
  136. package/web/dist/assets/StatusChip-B5xfI_Mn.js +153 -0
  137. package/web/dist/assets/StatusChip-B5xfI_Mn.js.br +0 -0
  138. package/web/dist/assets/StatusChip-B5xfI_Mn.js.gz +0 -0
  139. package/web/dist/assets/_app-Ea5cpyKe.js +1 -0
  140. package/web/dist/assets/_app-Ea5cpyKe.js.br +0 -0
  141. package/web/dist/assets/_app-Ea5cpyKe.js.gz +0 -0
  142. package/web/dist/assets/_runId-pTG0fXVo.js +1 -0
  143. package/web/dist/assets/_runId-pTG0fXVo.js.br +0 -0
  144. package/web/dist/assets/_runId-pTG0fXVo.js.gz +0 -0
  145. package/web/dist/assets/drawer-Du3cdlok.js +1 -0
  146. package/web/dist/assets/drawer-Du3cdlok.js.br +0 -0
  147. package/web/dist/assets/drawer-Du3cdlok.js.gz +0 -0
  148. package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  149. package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  150. package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  151. package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  152. package/web/dist/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
  153. package/web/dist/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
  154. package/web/dist/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
  155. package/web/dist/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
  156. package/web/dist/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
  157. package/web/dist/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
  158. package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  159. package/web/dist/assets/index-B6GiwDkh.js +11 -0
  160. package/web/dist/assets/index-B6GiwDkh.js.br +0 -0
  161. package/web/dist/assets/index-B6GiwDkh.js.gz +0 -0
  162. package/web/dist/assets/index-C5IbylvM.css +2 -0
  163. package/web/dist/assets/index-C5IbylvM.css.br +0 -0
  164. package/web/dist/assets/index-C5IbylvM.css.gz +0 -0
  165. package/web/dist/assets/label-DFSYBc62.js +1 -0
  166. package/web/dist/assets/label-DFSYBc62.js.br +0 -0
  167. package/web/dist/assets/label-DFSYBc62.js.gz +0 -0
  168. package/web/dist/assets/link-D8EcKZUt.js +1 -0
  169. package/web/dist/assets/link-D8EcKZUt.js.br +0 -0
  170. package/web/dist/assets/link-D8EcKZUt.js.gz +0 -0
  171. package/web/dist/assets/login-DaN5HPPW.js +1 -0
  172. package/web/dist/assets/login-DaN5HPPW.js.br +0 -0
  173. package/web/dist/assets/login-DaN5HPPW.js.gz +0 -0
  174. package/web/dist/assets/query-Cu--ccT3.js +1 -0
  175. package/web/dist/assets/questions-shoO9OVj.js +1 -0
  176. package/web/dist/assets/questions-shoO9OVj.js.br +0 -0
  177. package/web/dist/assets/questions-shoO9OVj.js.gz +0 -0
  178. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js +12 -0
  179. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.br +0 -0
  180. package/web/dist/assets/react-query.gen-Bu2yV-qJ.js.gz +0 -0
  181. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/web/dist/assets/run-status-C0DRuV7U.js +1 -0
  183. package/web/dist/assets/runs-C3rg2zZc.js +1 -0
  184. package/web/dist/assets/runs-C3rg2zZc.js.br +0 -0
  185. package/web/dist/assets/runs-C3rg2zZc.js.gz +0 -0
  186. package/web/dist/assets/setup-5I4Vl9fR.js +1 -0
  187. package/web/dist/assets/setup-5I4Vl9fR.js.br +0 -0
  188. package/web/dist/assets/setup-5I4Vl9fR.js.gz +0 -0
  189. package/web/dist/assets/table-BOxXhqS4.js +1 -0
  190. package/web/dist/assets/table-BOxXhqS4.js.br +0 -0
  191. package/web/dist/assets/table-BOxXhqS4.js.gz +0 -0
  192. package/web/dist/assets/textfield-BzRFkxBF.js +1 -0
  193. package/web/dist/assets/textfield-BzRFkxBF.js.br +0 -0
  194. package/web/dist/assets/textfield-BzRFkxBF.js.gz +0 -0
  195. package/web/dist/assets/time--9kSQbbp.js +1 -0
  196. package/web/dist/assets/use-css-variable-CpnN9WtA.js +5 -0
  197. package/web/dist/assets/use-css-variable-CpnN9WtA.js.br +0 -0
  198. package/web/dist/assets/use-css-variable-CpnN9WtA.js.gz +0 -0
  199. package/web/dist/assets/useMutation-CVPKlR34.js +1 -0
  200. package/web/dist/assets/useMutation-CVPKlR34.js.br +0 -0
  201. package/web/dist/assets/useMutation-CVPKlR34.js.gz +0 -0
  202. package/web/dist/index.html +15 -0
  203. package/catalog/ATTRIBUTION.md +0 -44
  204. package/catalog/scores.json +0 -3202
  205. package/catalog/sources.json +0 -97
  206. package/src/adapters/antigravity/events.ts +0 -131
  207. package/src/adapters/antigravity/home.ts +0 -46
  208. package/src/adapters/antigravity/index.ts +0 -335
  209. package/src/adapters/antigravity/models.ts +0 -37
  210. package/src/adapters/grok/events.ts +0 -112
  211. package/src/adapters/grok/home.ts +0 -187
  212. package/src/adapters/grok/index.ts +0 -358
  213. package/src/adapters/grok/models.ts +0 -48
  214. package/src/domain/calibration.ts +0 -132
  215. package/src/domain/sources.ts +0 -144
  216. package/src/entry/mcp/dispatch-tools.ts +0 -52
  217. package/src/entry/mcp/lane-tools.ts +0 -131
  218. package/src/entry/mcp/protocol-tools.ts +0 -63
  219. package/src/entry/mcp/push-tools.ts +0 -16
  220. package/src/entry/mcp/run-tools.ts +0 -164
  221. package/src/entry/mcp/setup-tools.ts +0 -109
  222. package/src/entry/mcp/workspace-tools.ts +0 -142
  223. package/src/entry/tui/commands.ts +0 -563
  224. package/src/entry/tui/effects.ts +0 -385
  225. package/src/entry/tui/fixtures.ts +0 -637
  226. package/src/entry/tui/profile-edits.ts +0 -146
  227. package/src/entry/tui/profile-tree.ts +0 -429
  228. package/src/entry/tui/providers/app.tsx +0 -162
  229. package/src/entry/tui/providers/boundary.tsx +0 -57
  230. package/src/entry/tui/providers/data.tsx +0 -160
  231. package/src/entry/tui/providers/keymap.tsx +0 -166
  232. package/src/entry/tui/providers/root.tsx +0 -45
  233. package/src/entry/tui/providers/theme.tsx +0 -17
  234. package/src/entry/tui/providers/toast.tsx +0 -77
  235. package/src/entry/tui/run.tsx +0 -114
  236. package/src/entry/tui/state.ts +0 -400
  237. package/src/entry/tui/stories.ts +0 -114
  238. package/src/entry/tui/text.ts +0 -130
  239. package/src/entry/tui/theme.ts +0 -140
  240. package/src/entry/tui/views/app.tsx +0 -208
  241. package/src/entry/tui/views/dialogs.tsx +0 -41
  242. package/src/entry/tui/views/profile-actions.ts +0 -331
  243. package/src/entry/tui/views/profiles.tsx +0 -520
  244. package/src/entry/tui/views/runs.tsx +0 -619
  245. package/src/entry/tui/views/save-dialog.tsx +0 -344
  246. package/src/entry/tui/views/status.tsx +0 -213
  247. package/src/entry/tui/widgets/chrome.tsx +0 -114
  248. package/src/entry/tui/widgets/dialog-confirm.tsx +0 -58
  249. package/src/entry/tui/widgets/dialog-prompt.tsx +0 -52
  250. package/src/entry/tui/widgets/dialog-select.tsx +0 -212
  251. package/src/entry/tui/widgets/dialog.tsx +0 -68
  252. package/src/entry/tui/widgets/line.tsx +0 -92
  253. package/src/entry/tui/widgets/list.tsx +0 -197
  254. package/src/entry/tui/widgets/spinner.tsx +0 -49
  255. package/src/entry/tui/widgets/toast.tsx +0 -62
  256. package/src/infra/sources/arena.ts +0 -95
  257. package/src/infra/sources/artificial-analysis.ts +0 -119
  258. package/src/infra/sources/cache.ts +0 -126
  259. package/src/infra/sources/csv.ts +0 -38
  260. package/src/infra/sources/epoch.ts +0 -82
  261. package/src/infra/sources/http.ts +0 -75
  262. package/src/infra/sources/litellm.ts +0 -58
  263. package/src/infra/sources/models-dev.ts +0 -59
  264. package/src/infra/sources/openrouter-endpoints.ts +0 -52
  265. package/src/infra/sources/openrouter-models.ts +0 -60
  266. package/src/infra/sources/rows.ts +0 -38
  267. package/src/infra/sources/vectara.ts +0 -69
  268. package/src/infra/sources/zip.ts +0 -48
  269. package/src/services/catalog-refresh.ts +0 -201
  270. package/src/services/doctor-sources.ts +0 -76
  271. package/src/services/runs-page.ts +0 -290
  272. package/src/services/source-derive.ts +0 -397
  273. package/src/services/source-sync.ts +0 -312
  274. package/src/services/standins.ts +0 -238
  275. package/src/services/treat-likes.ts +0 -113
@@ -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.lent === "treat-like" ? `like ${v.from}` : 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
- }