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,573 +0,0 @@
1
- import { type MutableRefObject, useEffect, useRef, useState } from "react";
2
- import { useApp, useBack, useNow } from "../providers/app.tsx";
3
- import { useData, usePoll } from "../providers/data.tsx";
4
- import { useCommandLayer } from "../providers/keymap.tsx";
5
- import { errorToast } from "../providers/toast.tsx";
6
- import { useUi } from "../providers/theme.tsx";
7
- import { isArmed } from "../state.ts";
8
- import { ago, clock, plural, shortRung, wrap, wrapHanging } from "../text.ts";
9
- import { glyph, mascot, type Token } from "../theme.ts";
10
- import type { RoleRow, SessionRow, SessionRun } from "../effects.ts";
11
- import { Line, type Part } from "../widgets/line.tsx";
12
- import { List, type ListItem, useSelected } from "../widgets/list.tsx";
13
-
14
- /** How often an open screen is read again when its run folders cannot be watched, unless paused (spec §4). */
15
- export const RUN_EVERY_MS = 1_000;
16
- /** How often a watched screen is read again all the same: a run that joins the session has no watch yet. */
17
- export const WATCHED_EVERY_MS = 5_000;
18
-
19
- /**
20
- * The selected row, where moving the cursor takes back a first ctrl+d (Ruling 3: moving or esc disarms),
21
- * as the dialog list does.
22
- */
23
- function useSelection(
24
- initial: string | null = null,
25
- ): [string | null, (key: string) => void, () => string | null] {
26
- const app = useApp();
27
- const sel = useSelected(initial);
28
- const select = (key: string) => {
29
- if (key !== sel.current() && app.getState().armed) app.dispatch({ type: "disarm" });
30
- sel.select(key);
31
- };
32
- return [sel.selected, select, sel.current];
33
- }
34
-
35
- /** One session of the top level: `● live <name> 2 runs · 3 live roles · 1 landed 4m ago` (spec §4). */
36
- export function sessionParts(s: SessionRow, now: number, plain: boolean): Part[] {
37
- return [
38
- s.live
39
- ? { text: `${glyph("live", plain)} live `, tone: "info" }
40
- : { text: `${glyph("dot", plain)} idle `, tone: "muted" },
41
- { text: `${s.name} `, bold: true },
42
- {
43
- text: `${plural(s.runs, "run")} · ${plural(s.liveRoles, "live role")} · ${s.landed} landed `,
44
- tone: "muted",
45
- },
46
- { text: ago(now - Date.parse(s.lastActivity)), tone: "muted" },
47
- ];
48
- }
49
-
50
- const keyOf = (k: string | null) => (k === null ? "earlier" : `s:${k}`);
51
- const fromKey = (k: string) => (k === "earlier" ? null : k.slice("s:".length));
52
- const roleKey = (run: string, dispatchId: string) => `role:${run}:${dispatchId}`;
53
- const milestoneKey = (run: string, name: string) => `ms:${run}:${name}`;
54
-
55
- /** What `r` re-reads: the open screen registers its read here (the command lives on the tab). */
56
- type Refresher = MutableRefObject<(() => void) | null>;
57
-
58
- /** Registers `refresh` as the open screen's while it is mounted. */
59
- function useRefresher(refresher: Refresher, refresh: () => void) {
60
- // after every commit, and cleared only while still its own: the screen that replaces this one registers
61
- // after this one's cleanup has run
62
- useEffect(() => {
63
- refresher.current = refresh;
64
- return () => {
65
- if (refresher.current === refresh) refresher.current = null;
66
- };
67
- });
68
- }
69
-
70
- function SessionList(props: { width: number; height: number; initial: string | null }) {
71
- const app = useApp();
72
- const data = useData();
73
- const ui = useUi();
74
- const now = useNow(1_000);
75
- const [selected, setSelected, selectedNow] = useSelection(props.initial);
76
- const rows = data.sessions.value?.rows ?? [];
77
- useCommandLayer("row.runs", {
78
- "runs.open": () => {
79
- const k = selectedNow();
80
- if (k) app.dispatch({ type: "session", key: fromKey(k) });
81
- },
82
- });
83
- const updated = app.state.paused
84
- ? "paused"
85
- : data.sessions.at !== null
86
- ? `updated ${ago(now - data.sessions.at)}`
87
- : "";
88
- const items: ListItem[] = rows.map((s) => ({
89
- key: keyOf(s.key),
90
- selectable: true,
91
- render: (sel, w) => (
92
- <Line width={w} selected={sel} parts={[{ text: " " }, ...sessionParts(s, now, ui.plain)]} />
93
- ),
94
- }));
95
- const warnings = data.sessions.value?.warnings ?? [];
96
- // a failed read says so, whether rows from an earlier good read are shown, none were, or none exist
97
- const failed = data.sessions.error;
98
- const failure = failed ? (
99
- <Line
100
- width={props.width}
101
- parts={[{ text: ` ${glyph("fail", ui.plain)} could not read the runs: ${failed}`, tone: "error" }]}
102
- />
103
- ) : null;
104
- if (rows.length === 0 && data.sessions.value) {
105
- const art = mascot("waiting");
106
- return (
107
- <box flexDirection="column" width={props.width} height={props.height} paddingTop={failed ? 1 : 2}>
108
- {failure}
109
- {art.map((l) => (
110
- <Line key={l} width={props.width} parts={[{ text: ` ${l}`, tone: "muted" }]} />
111
- ))}
112
- <Line
113
- width={props.width}
114
- parts={[
115
- { text: " No runs yet. Start one in Claude Code: /catherd <what to build>", tone: "muted" },
116
- ]}
117
- />
118
- </box>
119
- );
120
- }
121
- return (
122
- <box flexDirection="column" width={props.width} height={props.height}>
123
- <Line
124
- width={props.width}
125
- parts={[
126
- { text: " SESSIONS", bold: true },
127
- { text: ` ${updated}`, tone: app.state.paused ? "warning" : "muted" },
128
- ]}
129
- />
130
- {failure}
131
- <List
132
- items={items}
133
- selected={selected}
134
- onSelect={setSelected}
135
- width={props.width}
136
- height={props.height - 1 - (failed ? 1 : 0) - Math.min(2, warnings.length)}
137
- filter={null}
138
- empty={failed ? "no runs read yet" : "reading runs…"}
139
- />
140
- {warnings.slice(0, 2).map((w) => (
141
- <Line
142
- key={w}
143
- width={props.width}
144
- parts={[{ text: ` ${glyph("warn", ui.plain)} ${w}`, tone: "warning" }]}
145
- />
146
- ))}
147
- </box>
148
- );
149
- }
150
-
151
- /** A finished role's glyph and colour by its record's status; a live one's by running or starting. */
152
- function roleMark(r: RoleRow, plain: boolean): Part {
153
- if (r.live) return { text: glyph(r.status === "running" ? "live" : "waiting", plain), tone: "info" };
154
- const tone: Token =
155
- r.status === "ok"
156
- ? "success"
157
- : r.status === "cancelled"
158
- ? "muted"
159
- : r.status === "limit"
160
- ? "warning"
161
- : "error";
162
- return { text: glyph(r.status === "ok" ? "ok" : r.status === "cancelled" ? "skip" : "fail", plain), tone };
163
- }
164
-
165
- /** A run's heading on its session's screen: title, repo, when it started, its budget, where it moved. */
166
- function runHeading(r: SessionRun, now: number, plain: boolean): Part[] {
167
- const bits = [r.repo, `started ${ago(now - Date.parse(r.createdAt))}`];
168
- if (r.budget !== null) bits.push(`budget ${Math.round(r.budget * 100)}%`);
169
- const moved =
170
- r.continued === "here" ? "continued here" : r.continuedIn ? `continued in ${r.continuedIn}` : null;
171
- return [
172
- { text: ` ${glyph("shut", plain)} ` },
173
- { text: r.title, bold: true },
174
- { text: ` ${bits.join(" · ")}`, tone: "muted" },
175
- ...(moved ? [{ text: ` · ${moved}`, tone: "info" as Token }] : []),
176
- ];
177
- }
178
-
179
- /** Spec §4: the open screen redraws when a run file changes, else every second; `p` stops both. */
180
- function useLiveRead<T extends { dirs: string[] }>(read: () => T, key: string) {
181
- const app = useApp();
182
- const [watching, setWatching] = useState(false);
183
- const polled = usePoll(read, watching ? WATCHED_EVERY_MS : RUN_EVERY_MS, { paused: app.state.paused, key });
184
- const refresh = useRef(polled.refresh);
185
- refresh.current = polled.refresh;
186
- const dirs = polled.value?.dirs.join("\n") ?? "";
187
- useEffect(() => {
188
- if (app.state.paused || dirs === "") {
189
- setWatching(false);
190
- return;
191
- }
192
- const stop = app.effects.watch(dirs.split("\n"), () => refresh.current());
193
- setWatching(stop !== null);
194
- return () => stop?.();
195
- }, [dirs, app.state.paused, app.effects]);
196
- return { ...polled, watching };
197
- }
198
-
199
- function SessionView(props: {
200
- sessionKey: string | null;
201
- width: number;
202
- height: number;
203
- initial: string | null;
204
- refresher: Refresher;
205
- }) {
206
- const app = useApp();
207
- const ui = useUi();
208
- const now = useNow(1_000);
209
- const [selected, setSelected, selectedNow] = useSelection(props.initial);
210
- const polled = useLiveRead(() => app.effects.session(props.sessionKey), String(props.sessionKey));
211
- useRefresher(props.refresher, polled.refresh);
212
- useBack(true, "view", () => app.dispatch({ type: "up" }));
213
- const d = polled.value;
214
- const roleAt = (key: string | null) =>
215
- d?.runs.flatMap((r) => r.roles).find((x) => key === roleKey(x.run, x.dispatchId)) ?? null;
216
- const milestoneAt = (key: string | null) =>
217
- d?.runs
218
- .flatMap((r) => r.milestones.map((m) => ({ run: r.id, name: m.name })))
219
- .find((x) => key === milestoneKey(x.run, x.name)) ?? null;
220
- useCommandLayer("row.runs", {
221
- "runs.open": () => {
222
- const k = selectedNow();
223
- const m = milestoneAt(k);
224
- if (m) return app.dispatch({ type: "milestone", run: m.run, name: m.name });
225
- const r = roleAt(k);
226
- if (r) app.dispatch({ type: "role", run: r.run, dispatchId: r.dispatchId });
227
- },
228
- "runs.cancel": () => {
229
- const r = roleAt(selectedNow());
230
- if (!r?.live) return;
231
- const target = `${r.run}/${r.name}`;
232
- const t = app.clock.now();
233
- if (!isArmed(app.getState(), "cancel", target, t))
234
- return app.dispatch({ type: "arm", what: "cancel", target, at: t });
235
- app.dispatch({ type: "disarm" });
236
- void app.effects.cancel(r.run, r.name).then(
237
- (msg) => {
238
- app.toast({ variant: "success", message: msg });
239
- polled.refresh();
240
- },
241
- (e: unknown) => app.toast(errorToast(e)),
242
- );
243
- },
244
- });
245
- if (!d)
246
- return (
247
- <Line
248
- width={props.width}
249
- parts={[
250
- {
251
- text: polled.error ? ` ${polled.error}` : " reading the session…",
252
- tone: polled.error ? "error" : "muted",
253
- },
254
- ]}
255
- />
256
- );
257
- const items: ListItem[] = [];
258
- const text = (key: string, parts: Part[]) =>
259
- items.push({ key, selectable: false, render: (_sel, w) => <Line width={w} parts={parts} /> });
260
- for (const r of d.runs) {
261
- text(`run:${r.id}`, runHeading(r, now, ui.plain));
262
- // spec 1.1 §10: each milestone is a row; enter opens it on its digest
263
- for (const m of r.milestones)
264
- items.push({
265
- key: milestoneKey(r.id, m.name),
266
- selectable: true,
267
- render: (sel, w) => (
268
- <Line
269
- width={w}
270
- selected={sel}
271
- parts={[
272
- { text: " " },
273
- m.landed
274
- ? { text: `${glyph("ok", ui.plain)} ${m.name}`, tone: "success" }
275
- : { text: `${glyph("waiting", ui.plain)} ${m.name}`, tone: "muted" },
276
- m.landed
277
- ? { text: m.what ? ` ${m.what}` : "", tone: "muted" }
278
- : { text: " not landed", tone: "muted" },
279
- ]}
280
- />
281
- ),
282
- });
283
- if (r.roles.length === 0) text(`none:${r.id}`, [{ text: " no role has run yet", tone: "muted" }]);
284
- for (const x of r.roles) {
285
- const secs = x.live ? (now - Date.parse(x.since)) / 1000 : (x.secs ?? 0);
286
- const armed = isArmed(app.state, "cancel", `${x.run}/${x.name}`, app.clock.now());
287
- items.push({
288
- key: roleKey(x.run, x.dispatchId),
289
- selectable: true,
290
- render: (sel, w) => (
291
- <Line
292
- width={w}
293
- selected={sel}
294
- parts={[
295
- { text: " " },
296
- roleMark(x, ui.plain),
297
- { text: ` ${x.name.padEnd(15)} `, bold: true },
298
- { text: shortRung(x.rung).padEnd(19) },
299
- { text: x.status.padEnd(9), tone: x.live ? "info" : "muted" },
300
- { text: clock(secs).padEnd(7) },
301
- armed
302
- ? { text: "press ctrl+d again to cancel", tone: "warning" }
303
- : { text: x.lastEvent ?? "", tone: "muted" },
304
- ]}
305
- />
306
- ),
307
- });
308
- }
309
- }
310
- const s = d.session;
311
- const updated = app.state.paused ? "paused" : polled.at !== null ? `updated ${ago(now - polled.at)}` : "";
312
- return (
313
- <box flexDirection="column" width={props.width} height={props.height}>
314
- <Line
315
- width={props.width}
316
- parts={[
317
- { text: ` ${s.name} `, bold: true },
318
- s.live
319
- ? { text: `${glyph("live", ui.plain)} live`, tone: "info" }
320
- : { text: `${glyph("dot", ui.plain)} idle`, tone: "muted" },
321
- { text: ` · ${plural(s.runs, "run")} · ${plural(s.liveRoles, "live role")} · `, tone: "muted" },
322
- { text: updated, tone: app.state.paused ? "warning" : "muted" },
323
- {
324
- text: polled.error ? ` · ${glyph("fail", ui.plain)} could not read it: ${polled.error}` : "",
325
- tone: "error",
326
- },
327
- ]}
328
- />
329
- <List
330
- items={items}
331
- selected={selected}
332
- onSelect={setSelected}
333
- width={props.width}
334
- height={props.height - 1}
335
- filter={null}
336
- empty=""
337
- />
338
- </box>
339
- );
340
- }
341
-
342
- function RoleView(props: {
343
- run: string;
344
- dispatchId: string;
345
- width: number;
346
- height: number;
347
- refresher: Refresher;
348
- }) {
349
- const app = useApp();
350
- const ui = useUi();
351
- const [selected, setSelected] = useSelection();
352
- const polled = usePoll(() => app.effects.role(props.run, props.dispatchId), RUN_EVERY_MS, {
353
- paused: app.state.paused,
354
- key: `${props.run}/${props.dispatchId}`,
355
- });
356
- useRefresher(props.refresher, polled.refresh);
357
- useBack(true, "view", () => app.dispatch({ type: "up" }));
358
- const d = polled.value;
359
- if (!d)
360
- return (
361
- <Line
362
- width={props.width}
363
- parts={[
364
- {
365
- text: polled.error ? ` ${polled.error}` : " reading the role…",
366
- tone: polled.error ? "error" : "muted",
367
- },
368
- ]}
369
- />
370
- );
371
- const items: ListItem[] = [];
372
- const room = Math.max(10, props.width - 4);
373
- let n = 0;
374
- const line = (parts: Part[]) =>
375
- items.push({
376
- key: `l${n++}`,
377
- selectable: true,
378
- render: (sel, w) => <Line width={w} selected={sel} parts={parts} />,
379
- });
380
- const section = (title: string, body: string, none: string) => {
381
- line([{ text: ` ${title}`, bold: true }]);
382
- const lines = body.trimEnd() ? body.trimEnd().split("\n") : [];
383
- if (lines.length === 0) line([{ text: ` ${none}`, tone: "muted" }]);
384
- for (const l of lines) for (const w of wrap(l, room)) line([{ text: ` ${w}` }]);
385
- };
386
- section("BRIEF", d.brief, "no brief");
387
- section("REPLY", d.reply, d.state === "finished" ? "no reply" : "no reply yet: the role is running");
388
- line([{ text: " RECORD", bold: true }]);
389
- const r = d.record;
390
- if (!r) line([{ text: " none yet: the role is running", tone: "muted" }]);
391
- else {
392
- const k = (x: number) => `${Math.round(x / 1000)}k`;
393
- line([
394
- { text: ` ${r.status}`, tone: r.status === "ok" ? "success" : "error" },
395
- {
396
- text: r.replyStatus
397
- ? ` · STATUS ${r.replyStatus}${r.replyWhy ? ` — ${r.replyWhy}` : ""}`
398
- : " · no STATUS",
399
- },
400
- ]);
401
- line([
402
- {
403
- text: ` ${clock(r.secs)} · ${k(r.tokens.input)} in (${k(r.tokens.cached)} cached) · ${k(r.tokens.output)} out`,
404
- tone: "muted",
405
- },
406
- ]);
407
- line([{ text: ` changed ${r.changedOwned.join(", ") || "nothing it owns"}`, tone: "muted" }]);
408
- if (r.violations.length)
409
- line([{ text: ` outside its lane: ${r.violations.join(", ")}`, tone: "warning" }]);
410
- if (r.thread) line([{ text: ` thread ${r.thread}`, tone: "muted" }]);
411
- }
412
- return (
413
- <box flexDirection="column" width={props.width} height={props.height}>
414
- <Line
415
- width={props.width}
416
- parts={[
417
- { text: ` ${d.name}`, bold: true },
418
- {
419
- text: ` ${d.role} · ${shortRung(d.rung)} · ${r?.status ?? d.state} · ${d.runTitle}`,
420
- tone: "muted",
421
- },
422
- {
423
- text: polled.error ? ` · ${glyph("fail", ui.plain)} could not read it: ${polled.error}` : "",
424
- tone: "error",
425
- },
426
- ]}
427
- />
428
- <List
429
- items={items}
430
- selected={selected}
431
- onSelect={setSelected}
432
- width={props.width}
433
- height={props.height - 1}
434
- filter={null}
435
- empty=""
436
- />
437
- </box>
438
- );
439
- }
440
-
441
- /** Spec 1.1 §10: a milestone's screen, the digest `land` wrote for it. */
442
- function MilestoneView(props: {
443
- run: string;
444
- name: string;
445
- width: number;
446
- height: number;
447
- refresher: Refresher;
448
- }) {
449
- const app = useApp();
450
- const ui = useUi();
451
- const [selected, setSelected] = useSelection();
452
- const polled = usePoll(() => app.effects.milestone(props.run, props.name), RUN_EVERY_MS, {
453
- paused: app.state.paused,
454
- key: `${props.run}/${props.name}`,
455
- });
456
- useRefresher(props.refresher, polled.refresh);
457
- useBack(true, "view", () => app.dispatch({ type: "up" }));
458
- const d = polled.value;
459
- if (!d)
460
- return (
461
- <Line
462
- width={props.width}
463
- parts={[
464
- {
465
- text: polled.error ? ` ${polled.error}` : " reading the milestone…",
466
- tone: polled.error ? "error" : "muted",
467
- },
468
- ]}
469
- />
470
- );
471
- const items: ListItem[] = [];
472
- const room = Math.max(10, props.width - 4);
473
- let n = 0;
474
- const line = (parts: Part[]) =>
475
- items.push({
476
- key: `l${n++}`,
477
- selectable: true,
478
- render: (sel, w) => <Line width={w} selected={sel} parts={parts} />,
479
- });
480
- line([{ text: " DIGEST", bold: true }]);
481
- const body = d.digest?.trimEnd() ?? "";
482
- if (!body)
483
- line([
484
- {
485
- text: ` no digest yet${d.landed ? "" : ": the milestone has not landed"}`,
486
- tone: "muted",
487
- },
488
- ]);
489
- else for (const l of body.split("\n")) for (const w of wrapHanging(l, room)) line([{ text: ` ${w}` }]);
490
- return (
491
- <box flexDirection="column" width={props.width} height={props.height}>
492
- <Line
493
- width={props.width}
494
- parts={[
495
- { text: ` ${d.name}`, bold: true },
496
- {
497
- text: ` ${d.what ? `${d.what} · ` : ""}${d.landed ? "landed" : "not landed"} · ${d.runTitle}`,
498
- tone: "muted",
499
- },
500
- {
501
- text: polled.error ? ` · ${glyph("fail", ui.plain)} could not read it: ${polled.error}` : "",
502
- tone: "error",
503
- },
504
- ]}
505
- />
506
- <List
507
- items={items}
508
- selected={selected}
509
- onSelect={setSelected}
510
- width={props.width}
511
- height={props.height - 1}
512
- filter={null}
513
- empty=""
514
- />
515
- </box>
516
- );
517
- }
518
-
519
- /**
520
- * Spec §4 (tab 3): the sessions, newest activity first; enter opens a session (its runs, milestones and roles,
521
- * live ones first), enter on a milestone opens its digest (spec 1.1 §10), enter on a role opens it (brief,
522
- * reply, record); esc goes back one level; p pauses.
523
- */
524
- export function RunsView(props: { width: number; height: number }) {
525
- const app = useApp();
526
- const data = useData();
527
- const refresher: Refresher = useRef(null);
528
- useCommandLayer("tab.runs", {
529
- "runs.refresh": () => (refresher.current ?? data.sessions.refresh)(),
530
- "runs.pause": () => {
531
- app.dispatch({ type: "pause" });
532
- app.toast({ variant: "info", message: app.getState().paused ? "Updates paused" : "Updates resumed" });
533
- },
534
- });
535
- const { milestone, role, session } = app.state;
536
- // esc lands on the row it came from: the last session opened, and the last milestone or role opened in it
537
- const back = useRef<{ session: string | null; row: string | null }>({ session: null, row: null });
538
- if (session && back.current.session !== keyOf(session.key))
539
- back.current = { session: keyOf(session.key), row: null };
540
- if (milestone) back.current.row = milestoneKey(milestone.run, milestone.name);
541
- if (role) back.current.row = roleKey(role.run, role.dispatchId);
542
- if (milestone)
543
- return (
544
- <MilestoneView
545
- run={milestone.run}
546
- name={milestone.name}
547
- width={props.width}
548
- height={props.height}
549
- refresher={refresher}
550
- />
551
- );
552
- if (role)
553
- return (
554
- <RoleView
555
- run={role.run}
556
- dispatchId={role.dispatchId}
557
- width={props.width}
558
- height={props.height}
559
- refresher={refresher}
560
- />
561
- );
562
- if (session)
563
- return (
564
- <SessionView
565
- sessionKey={session.key}
566
- width={props.width}
567
- height={props.height}
568
- initial={back.current.row}
569
- refresher={refresher}
570
- />
571
- );
572
- return <SessionList width={props.width} height={props.height} initial={back.current.session} />;
573
- }