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,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
- }