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,331 +0,0 @@
1
- import { isDeepStrictEqual } from "node:util";
2
- import { errorMessage, isCatherdError } from "../../../domain/errors.ts";
3
- import { PROFILE_NAME, patchBetween, resolveProfile, type ProfileDoc } from "../../../domain/profile.ts";
4
- import { CHANGED_ON_DISK } from "../effects.ts";
5
- import { type AppApi, useApp, useDialogHandler } from "../providers/app.tsx";
6
- import { type Data, useData } from "../providers/data.tsx";
7
- import { errorToast } from "../providers/toast.tsx";
8
- import { currentDraft, dirtyCount, isSaving } from "../state.ts";
9
- import { plural } from "../text.ts";
10
-
11
- const fail = (app: AppApi, e: unknown) => app.toast(errorToast(e));
12
-
13
- /**
14
- * Shows a profile in the Profiles tab, starting its draft from disk unless one is staged. A read that
15
- * fails is a toast, or with `quiet` only returned (the caller shows it); null when it was shown.
16
- */
17
- export function showProfile(app: AppApi, name: string, o: { quiet?: boolean } = {}): unknown {
18
- try {
19
- const doc = app.effects.readProfile(name);
20
- resolveProfile(doc, name, app.getState().host);
21
- app.dispatch({ type: "show", name, doc });
22
- return null;
23
- } catch (e) {
24
- if (!o.quiet) fail(app, e);
25
- return e;
26
- }
27
- }
28
-
29
- /**
30
- * The profiles as they are now: the poll's value while its last read was good, else a read of them now
31
- * (which throws as the poll's did, until the file is fixed); a good one also brings the poll up to date.
32
- */
33
- function fresh(app: AppApi, data: Data): NonNullable<Data["profiles"]["value"]> {
34
- if (data.profiles.value && data.profiles.error === null) return data.profiles.value;
35
- const p = app.effects.profiles();
36
- if (data.profiles.error !== null) data.profiles.refresh();
37
- return p;
38
- }
39
-
40
- /** The profile list (`<leader>l`): enter shows one, ctrl+d twice deletes one. */
41
- export function openProfileList(app: AppApi, data: Data): void {
42
- let p: NonNullable<Data["profiles"]["value"]>;
43
- try {
44
- // the poll has not read them yet, or its last read failed (a kept value would be stale): read them now;
45
- // a key handler's throw would end the TUI
46
- p = fresh(app, data);
47
- } catch (e) {
48
- return fail(app, e);
49
- }
50
- app.dispatch({
51
- type: "open",
52
- dialog: {
53
- kind: "select",
54
- purpose: { type: "profiles" },
55
- title: "Profiles",
56
- deletable: true,
57
- empty: "no profiles",
58
- options: p.names.map((n) => {
59
- const d = app.getState().drafts[n];
60
- const unsaved = d ? dirtyCount(d) : 0;
61
- return {
62
- value: n,
63
- title: n,
64
- current: n === p.here,
65
- detail: [
66
- n === p.active ? "active" : "",
67
- p.repo && n === p.here ? "this repo" : "",
68
- unsaved ? `${unsaved} unsaved` : "",
69
- ]
70
- .filter(Boolean)
71
- .join(" · "),
72
- };
73
- }),
74
- },
75
- });
76
- }
77
-
78
- export const openNewProfile = (app: AppApi, from: string | null = null): void =>
79
- app.dispatch({
80
- type: "open",
81
- dialog: {
82
- kind: "prompt",
83
- purpose: from ? { type: "copy", from } : { type: "new" },
84
- title: from ? `Copy profile ${from}` : "New profile",
85
- label: "name: lowercase letters, digits and -, up to 32",
86
- value: "",
87
- error: null,
88
- },
89
- });
90
-
91
- /** Opens the save dialog, or says there is nothing to save. */
92
- export function openSave(app: AppApi): void {
93
- const d = currentDraft(app.getState());
94
- if (!d || dirtyCount(d) === 0) return app.toast({ variant: "info", message: "No unsaved changes" });
95
- app.dispatch({
96
- type: "open",
97
- dialog: { kind: "save", purpose: { type: "save", name: d.name }, error: null },
98
- });
99
- }
100
-
101
- export function openActivate(app: AppApi, data: Data): void {
102
- const d = currentDraft(app.getState());
103
- if (!d) return;
104
- let p: Data["profiles"]["value"];
105
- try {
106
- // a failed poll is read again here, so the prompt never names a stale active profile or binding
107
- p = data.profiles.error === null ? data.profiles.value : fresh(app, data);
108
- } catch (e) {
109
- return fail(app, e);
110
- }
111
- askActivate(app, d.name, p);
112
- }
113
-
114
- /**
115
- * The activate confirmation for `name`, worded for the scope `p` shows (its bound repo, or the global
116
- * profile), which the answer then applies; `notice` says why it asks again.
117
- */
118
- function askActivate(app: AppApi, name: string, p: Data["profiles"]["value"], notice?: string): void {
119
- if (p?.here === name)
120
- return app.toast({
121
- variant: "info",
122
- message: p.repo ? `${name} is already this repo's profile` : `${name} is already active`,
123
- });
124
- const d = app.getState().drafts[name];
125
- const unsaved = d ? dirtyCount(d) : 0;
126
- app.dispatch({
127
- type: "open",
128
- dialog: {
129
- kind: "confirm",
130
- purpose: { type: "activate", name, repo: p?.repo ?? null },
131
- title: p?.repo ? `Use ${name} in this repo?` : `Make ${name} active?`,
132
- message: [
133
- ...(notice ? [notice] : []),
134
- p?.repo
135
- ? `Binds ${p.repo} to it; the active profile (${p.active}) and other repos keep theirs.`
136
- : "New Claude Code sessions and catherd's next dispatch use it; repos bound to another profile keep theirs.",
137
- ...(unsaved
138
- ? [`Its ${unsaved} unsaved change${unsaved === 1 ? " is" : "s are"} not part of it until you save.`]
139
- : []),
140
- ],
141
- yes: p?.repo ? "Bind to this repo" : "Make active",
142
- no: "Cancel",
143
- destructive: false,
144
- },
145
- });
146
- }
147
-
148
- export function openRevert(app: AppApi): void {
149
- const d = currentDraft(app.getState());
150
- if (!d || dirtyCount(d) === 0) return app.toast({ variant: "info", message: "No unsaved changes" });
151
- app.dispatch({
152
- type: "open",
153
- dialog: {
154
- kind: "confirm",
155
- purpose: { type: "revert", name: d.name },
156
- title: "Discard unsaved changes?",
157
- message: [
158
- `${plural(dirtyCount(d), "change")} to ${d.name} go back to what is saved. ctrl+x u undoes this.`,
159
- ],
160
- yes: "Discard",
161
- no: "Keep them",
162
- destructive: true,
163
- },
164
- });
165
- }
166
-
167
- /** Keeps a line for after exit, and says it now. */
168
- function sessionsNeeded(app: AppApi, agents: string[]): void {
169
- if (agents.length === 0) return;
170
- app.keep([`catherd: start a new Claude Code session to use: ${agents.join(", ")}`]);
171
- app.toast({
172
- variant: "info",
173
- message: `New Claude Code session needed for ${plural(agents.length, "agent")}`,
174
- });
175
- }
176
-
177
- /**
178
- * Applies a confirmed activation to the scope the user was shown (`shown`: the bound repo, or null for the
179
- * global profile). The binding is read again first: when another process bound or unbound this repo while
180
- * the question was open, nothing is applied and the question comes back in the words for the scope now.
181
- */
182
- function activateNow(app: AppApi, data: Data, name: string, shown: string | null): void {
183
- try {
184
- const now = app.effects.profiles();
185
- if (now.repo !== shown) {
186
- data.profiles.refresh();
187
- return askActivate(
188
- app,
189
- name,
190
- now,
191
- `Changed since this was first asked: ${now.repo ? `${now.repo} is now bound to ${now.here}` : `${shown} is no longer bound`}.`,
192
- );
193
- }
194
- const r = app.effects.activate(name, shown);
195
- data.profiles.refresh();
196
- app.toast({ variant: "success", message: shown ? `${name} is bound to ${shown}` : `${name} is active` });
197
- sessionsNeeded(app, r.newSessionNeededFor);
198
- } catch (e) {
199
- fail(app, e);
200
- }
201
- }
202
-
203
- /** The handlers of the profile dialogs that any tab can open; the App registers them once. */
204
- export function useProfileDialogs(): void {
205
- const app = useApp();
206
- const data = useData();
207
- const nameError = (name: string): string | null => {
208
- if (!PROFILE_NAME.test(name))
209
- return "use lowercase letters, digits and -, up to 32, starting with a letter or digit";
210
- if ((data.profiles.value?.names ?? []).includes(name)) return `"${name}" already exists`;
211
- return null;
212
- };
213
- const create = (value: string, from?: string) => {
214
- const name = value.trim();
215
- const err = nameError(name);
216
- if (err) return app.dispatch({ type: "invalid", error: err });
217
- try {
218
- const r = app.effects.create(name, from);
219
- if (!r.saved)
220
- return app.dispatch({
221
- type: "invalid",
222
- error: r.errors.map((e) => `${e.path}: ${e.message}`).join("; "),
223
- });
224
- data.profiles.refresh();
225
- app.dispatch({ type: "close" });
226
- showProfile(app, name);
227
- app.toast({ variant: "success", message: `Created profile ${name}` });
228
- sessionsNeeded(app, r.newSessionNeededFor);
229
- } catch (e) {
230
- app.dispatch({
231
- type: "invalid",
232
- error: isCatherdError(e) && e.fix ? `${e.message}. ${e.fix}` : errorMessage(e),
233
- });
234
- }
235
- };
236
- useDialogHandler("new", (_p, value) => create(value));
237
- useDialogHandler("copy", (p, value) => create(value, p.type === "copy" ? p.from : undefined));
238
- useDialogHandler("profiles", (_p, value) => {
239
- if (!value.startsWith("delete:")) {
240
- app.dispatch({ type: "close" });
241
- showProfile(app, value);
242
- return;
243
- }
244
- const name = value.slice("delete:".length);
245
- try {
246
- app.effects.remove(name);
247
- app.dispatch({ type: "forget", name });
248
- data.profiles.refresh();
249
- app.dispatch({ type: "close" });
250
- app.toast({ variant: "success", message: `Deleted profile ${name}` });
251
- } catch (e) {
252
- fail(app, e);
253
- }
254
- });
255
- useDialogHandler("activate", (p) => {
256
- app.dispatch({ type: "close" });
257
- if (p.type === "activate") activateNow(app, data, p.name, p.repo);
258
- });
259
- useDialogHandler("revert", (p) => {
260
- app.dispatch({ type: "close" });
261
- if (p.type !== "revert") return;
262
- const d = app.getState().drafts[p.name];
263
- let doc: ProfileDoc;
264
- try {
265
- // "what is saved" is the file now: another process may have saved it since the draft opened
266
- doc = app.effects.readProfile(p.name);
267
- } catch (e) {
268
- return fail(app, e);
269
- }
270
- app.dispatch({ type: "revert", name: p.name, doc });
271
- if (d && !isDeepStrictEqual(d.base, doc))
272
- app.toast({
273
- variant: "info",
274
- message: `${p.name} changed on disk: showing what is saved now`,
275
- });
276
- });
277
- useDialogHandler("save", async (p, value, extra) => {
278
- if (p.type !== "save") return;
279
- // the scope "Save & make active" was worded for as it was answered
280
- const shown = data.profiles.value?.repo ?? null;
281
- const d = app.getState().drafts[p.name];
282
- if (!d) return app.dispatch({ type: "close" });
283
- // a second enter while the first save is writing is not a second save
284
- if (isSaving(app.getState())) return;
285
- // while it writes the dialog stays open and takes no answer, and the TUI does not quit
286
- app.dispatch({ type: "saving", name: p.name, on: true });
287
- let r: Awaited<ReturnType<AppApi["effects"]["save"]>>;
288
- try {
289
- r = await app.effects.save(p.name, patchBetween(d.base, d.doc), d.treatLikes, extra.shown);
290
- } catch (e) {
291
- app.dispatch({ type: "saving", name: p.name, on: false });
292
- return app.dispatch({
293
- type: "invalid",
294
- error: isCatherdError(e) && e.fix ? `${e.message}. ${e.fix}` : errorMessage(e),
295
- });
296
- }
297
- app.dispatch({ type: "saving", name: p.name, on: false });
298
- // another process changed the profile while this one saved: nothing is written, and the dialog shows
299
- // what saving would do now and asks again
300
- if (!r.saved && r.errors.some((e) => e.path === CHANGED_ON_DISK) && extra.changed) {
301
- app.dispatch({ type: "invalid", error: null });
302
- return extra.changed();
303
- }
304
- if (!r.saved)
305
- return app.dispatch({
306
- type: "invalid",
307
- error: r.errors.map((e) => `${e.path}: ${e.message}`).join("; "),
308
- });
309
- // saved: this dialog closes (only if it is still the top one), so what fails from here on is a toast
310
- const top = app.getState().dialogs.at(-1);
311
- if (top?.kind === "save" && top.purpose.name === p.name) app.dispatch({ type: "close" });
312
- try {
313
- // the draft as the save began: an edit made since stays staged over what was saved
314
- const from = { doc: d.doc, treatLikes: d.treatLikes };
315
- app.dispatch({ type: "saved", name: p.name, doc: app.effects.readProfile(p.name), from });
316
- } catch (e) {
317
- fail(app, e);
318
- }
319
- // spec 1.2 §6.2: a save that repaired part of an invalid profile says what is still open
320
- app.toast(
321
- r.errors.length
322
- ? {
323
- variant: "warning",
324
- message: `Saved profile ${p.name}; ${r.errors.length} ${r.errors.length === 1 ? "error" : "errors"} still open`,
325
- }
326
- : { variant: "success", message: `Saved profile ${p.name}` },
327
- );
328
- sessionsNeeded(app, r.newSessionNeededFor);
329
- if (value === "activate") activateNow(app, data, p.name, shown);
330
- });
331
- }