@quickchatai/quickreview 1.2.0 → 1.3.2

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 (350) hide show
  1. package/README.md +116 -53
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +25 -7
  4. package/deploy/ansible/README.md +20 -12
  5. package/deploy/ansible/inventory.example.ini +1 -1
  6. package/deploy/collect-secrets.sh +1 -1
  7. package/deploy/qcr-update.sh +1 -1
  8. package/dist/extensions/cli/home.js +14 -0
  9. package/dist/extensions/cli/home.js.map +1 -0
  10. package/dist/extensions/cli/selfimprove.js +18 -0
  11. package/dist/extensions/cli/selfimprove.js.map +1 -0
  12. package/dist/extensions/home.js +350 -0
  13. package/dist/extensions/home.js.map +1 -0
  14. package/dist/extensions/login-route.js +1 -1
  15. package/dist/extensions/login-route.js.map +1 -1
  16. package/dist/extensions/onboarding.js +8 -7
  17. package/dist/extensions/onboarding.js.map +1 -1
  18. package/dist/extensions/selfimprove.js +49 -0
  19. package/dist/extensions/selfimprove.js.map +1 -0
  20. package/dist/extensions/setup-scan.js +23 -592
  21. package/dist/extensions/setup-scan.js.map +1 -1
  22. package/dist/extensions/setup.js +2 -47
  23. package/dist/extensions/setup.js.map +1 -1
  24. package/dist/extensions/vendor/openviking-pi/index.js +4 -0
  25. package/dist/extensions/vendor/openviking-pi/index.js.map +1 -1
  26. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js +3 -1
  27. package/dist/extensions/vendor/openviking-pi/qcr-hooks.js.map +1 -1
  28. package/dist/extensions/vendor/openviking-pi/sync.js +81 -0
  29. package/dist/extensions/vendor/openviking-pi/sync.js.map +1 -1
  30. package/dist/src/archive/restore.js +18 -15
  31. package/dist/src/archive/restore.js.map +1 -1
  32. package/dist/src/botworker.js +13 -2
  33. package/dist/src/botworker.js.map +1 -1
  34. package/dist/src/channelfs.js +3 -26
  35. package/dist/src/channelfs.js.map +1 -1
  36. package/dist/src/cli.js +14 -50
  37. package/dist/src/cli.js.map +1 -1
  38. package/dist/src/config.js +16 -0
  39. package/dist/src/config.js.map +1 -1
  40. package/dist/src/daemon.js +33 -4
  41. package/dist/src/daemon.js.map +1 -1
  42. package/dist/src/debuground.js +1 -0
  43. package/dist/src/debuground.js.map +1 -1
  44. package/dist/src/debugroundpi.js +1 -0
  45. package/dist/src/debugroundpi.js.map +1 -1
  46. package/dist/src/engine/caserunner-sdk.js +15 -251
  47. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  48. package/dist/src/home/adapters.js +349 -0
  49. package/dist/src/home/adapters.js.map +1 -0
  50. package/dist/src/home/advanced.js +318 -0
  51. package/dist/src/home/advanced.js.map +1 -0
  52. package/dist/src/home/advancedtools.js +57 -0
  53. package/dist/src/home/advancedtools.js.map +1 -0
  54. package/dist/src/home/autoreview.js +325 -0
  55. package/dist/src/home/autoreview.js.map +1 -0
  56. package/dist/src/home/daemonstatus.js +32 -0
  57. package/dist/src/home/daemonstatus.js.map +1 -0
  58. package/dist/src/home/devicelogin.js +61 -0
  59. package/dist/src/home/devicelogin.js.map +1 -0
  60. package/dist/src/home/launch.js +202 -0
  61. package/dist/src/home/launch.js.map +1 -0
  62. package/dist/src/home/loginowner.js +205 -0
  63. package/dist/src/home/loginowner.js.map +1 -0
  64. package/dist/src/home/memory.js +122 -0
  65. package/dist/src/home/memory.js.map +1 -0
  66. package/dist/src/home/model.js +811 -0
  67. package/dist/src/home/model.js.map +1 -0
  68. package/dist/src/home/panel.js +520 -0
  69. package/dist/src/home/panel.js.map +1 -0
  70. package/dist/src/home/ports.js +19 -0
  71. package/dist/src/home/ports.js.map +1 -0
  72. package/dist/src/home/review.js +163 -0
  73. package/dist/src/home/review.js.map +1 -0
  74. package/dist/src/home/reviews.js +478 -0
  75. package/dist/src/home/reviews.js.map +1 -0
  76. package/dist/src/home/setup.js +507 -0
  77. package/dist/src/home/setup.js.map +1 -0
  78. package/dist/src/home/setupadapters.js +214 -0
  79. package/dist/src/home/setupadapters.js.map +1 -0
  80. package/dist/src/home/setupcard.js +199 -0
  81. package/dist/src/home/setupcard.js.map +1 -0
  82. package/dist/src/home/setuptools.js +45 -0
  83. package/dist/src/home/setuptools.js.map +1 -0
  84. package/dist/src/home/spend.js +160 -0
  85. package/dist/src/home/spend.js.map +1 -0
  86. package/dist/src/home/switches.js +209 -0
  87. package/dist/src/home/switches.js.map +1 -0
  88. package/dist/src/home/switchon.js +439 -0
  89. package/dist/src/home/switchon.js.map +1 -0
  90. package/dist/src/home/toolnames.js +41 -0
  91. package/dist/src/home/toolnames.js.map +1 -0
  92. package/dist/src/home/tune.js +341 -0
  93. package/dist/src/home/tune.js.map +1 -0
  94. package/dist/src/home/words.js +7 -0
  95. package/dist/src/home/words.js.map +1 -0
  96. package/dist/src/homes.js +14 -0
  97. package/dist/src/homes.js.map +1 -1
  98. package/dist/src/install.js +1 -1
  99. package/dist/src/install.js.map +1 -1
  100. package/dist/src/ipc/workerspec.js +8 -0
  101. package/dist/src/ipc/workerspec.js.map +1 -1
  102. package/dist/src/ledgercheck.js +39 -10
  103. package/dist/src/ledgercheck.js.map +1 -1
  104. package/dist/src/live/publish.js +1 -1
  105. package/dist/src/live/publish.js.map +1 -1
  106. package/dist/src/livecli.js +1 -1
  107. package/dist/src/livecli.js.map +1 -1
  108. package/dist/src/loginrefresh/handover.js +121 -0
  109. package/dist/src/loginrefresh/handover.js.map +1 -0
  110. package/dist/src/loginrefresh/lock.js +54 -3
  111. package/dist/src/loginrefresh/lock.js.map +1 -1
  112. package/dist/src/loginrefresh/refresher.js +33 -5
  113. package/dist/src/loginrefresh/refresher.js.map +1 -1
  114. package/dist/src/loginrefresh/supervisor.js +18 -1
  115. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  116. package/dist/src/npmverbs.js +42 -12
  117. package/dist/src/npmverbs.js.map +1 -1
  118. package/dist/src/onboard/choices.js +6 -3
  119. package/dist/src/onboard/choices.js.map +1 -1
  120. package/dist/src/onboard/engine.js +33 -292
  121. package/dist/src/onboard/engine.js.map +1 -1
  122. package/dist/src/onboard/steps/bringup.js +3 -2
  123. package/dist/src/onboard/steps/bringup.js.map +1 -1
  124. package/dist/src/onboard/steps/configfill.js +18 -7
  125. package/dist/src/onboard/steps/configfill.js.map +1 -1
  126. package/dist/src/onboard/steps/loginshare.js +213 -0
  127. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  128. package/dist/src/onboard/steps/memory.js +14 -2
  129. package/dist/src/onboard/steps/memory.js.map +1 -1
  130. package/dist/src/onboard/steps/repoconfig.js +72 -0
  131. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  132. package/dist/src/onboard/toolnames.js +2 -2
  133. package/dist/src/onboard/toolnames.js.map +1 -1
  134. package/dist/src/piagentdir.js +13 -0
  135. package/dist/src/piagentdir.js.map +1 -0
  136. package/dist/src/pilogin.js +31 -16
  137. package/dist/src/pilogin.js.map +1 -1
  138. package/dist/src/qcrbin.js +16 -11
  139. package/dist/src/qcrbin.js.map +1 -1
  140. package/dist/src/reviewargs.js +122 -0
  141. package/dist/src/reviewargs.js.map +1 -0
  142. package/dist/src/reviewcli.js +7 -161
  143. package/dist/src/reviewcli.js.map +1 -1
  144. package/dist/src/reviewdetach.js +76 -11
  145. package/dist/src/reviewdetach.js.map +1 -1
  146. package/dist/src/scancli.js +39 -691
  147. package/dist/src/scancli.js.map +1 -1
  148. package/dist/src/selfimprove.js +66 -0
  149. package/dist/src/selfimprove.js.map +1 -0
  150. package/dist/src/session-command-supervisor.js +38 -9
  151. package/dist/src/session-command-supervisor.js.map +1 -1
  152. package/dist/src/setup/adapters.js +5 -223
  153. package/dist/src/setup/adapters.js.map +1 -1
  154. package/dist/src/setup/caserunner.js +15 -249
  155. package/dist/src/setup/caserunner.js.map +1 -1
  156. package/dist/src/setup/engine.js +32 -341
  157. package/dist/src/setup/engine.js.map +1 -1
  158. package/dist/src/setup/facts.js +3 -592
  159. package/dist/src/setup/facts.js.map +1 -1
  160. package/dist/src/setup/findings.js +3 -947
  161. package/dist/src/setup/findings.js.map +1 -1
  162. package/dist/src/setup/presentation.js +3 -8
  163. package/dist/src/setup/presentation.js.map +1 -1
  164. package/dist/src/setup/scan.js +3 -724
  165. package/dist/src/setup/scan.js.map +1 -1
  166. package/dist/src/setup/scanbuild.js +9 -92
  167. package/dist/src/setup/scanbuild.js.map +1 -1
  168. package/dist/src/setup/scanlease.js +3 -250
  169. package/dist/src/setup/scanlease.js.map +1 -1
  170. package/dist/src/setup/scanreport.js +3 -288
  171. package/dist/src/setup/scanreport.js.map +1 -1
  172. package/dist/src/setup/scanrun.js +3 -1732
  173. package/dist/src/setup/scanrun.js.map +1 -1
  174. package/dist/src/setup/scantree.js +3 -256
  175. package/dist/src/setup/scantree.js.map +1 -1
  176. package/dist/src/setup/stage2request.js +3 -258
  177. package/dist/src/setup/stage2request.js.map +1 -1
  178. package/dist/src/setup/unitcut.js +3 -436
  179. package/dist/src/setup/unitcut.js.map +1 -1
  180. package/dist/src/state.js +75 -9
  181. package/dist/src/state.js.map +1 -1
  182. package/dist/src/takeover.js +1 -0
  183. package/dist/src/takeover.js.map +1 -1
  184. package/dist/src/takeoverpi.js +2 -1
  185. package/dist/src/takeoverpi.js.map +1 -1
  186. package/dist/src/tui/alerts.js +4 -1
  187. package/dist/src/tui/alerts.js.map +1 -1
  188. package/dist/src/tui/board.js +2 -2
  189. package/dist/src/tui/board.js.map +1 -1
  190. package/dist/src/tui/companion.js +2 -1
  191. package/dist/src/tui/companion.js.map +1 -1
  192. package/dist/src/tui/deploytab.js +2 -2
  193. package/dist/src/tui/deploytab.js.map +1 -1
  194. package/dist/src/tui/index.js +9 -6
  195. package/dist/src/tui/index.js.map +1 -1
  196. package/dist/src/tui/memory.js +3 -16
  197. package/dist/src/tui/memory.js.map +1 -1
  198. package/dist/src/tui/memoryurl.js +22 -0
  199. package/dist/src/tui/memoryurl.js.map +1 -0
  200. package/dist/src/tui/remote.js +1 -1
  201. package/dist/src/tui/remote.js.map +1 -1
  202. package/dist/src/tui/route.js +2 -2
  203. package/dist/src/tui/route.js.map +1 -1
  204. package/dist/src/tui/setupstate.js +8 -5
  205. package/dist/src/tui/setupstate.js.map +1 -1
  206. package/dist/src/tui/sshtriage.js +1 -1
  207. package/dist/src/tui/sshtriage.js.map +1 -1
  208. package/dist/src/verdicts.js +12 -0
  209. package/dist/src/verdicts.js.map +1 -1
  210. package/dist/src/wire.js +9 -4
  211. package/dist/src/wire.js.map +1 -1
  212. package/dist/src/workerbreaker.js +32 -5
  213. package/dist/src/workerbreaker.js.map +1 -1
  214. package/dist/src/workerworld.js +4 -0
  215. package/dist/src/workerworld.js.map +1 -1
  216. package/docker/README.md +1 -1
  217. package/docker/entrypoint.sh +40 -6
  218. package/docker-compose.yml +16 -1
  219. package/extensions/cli/home.ts +14 -0
  220. package/extensions/cli/selfimprove.ts +18 -0
  221. package/extensions/home.ts +484 -0
  222. package/extensions/login-route.ts +1 -1
  223. package/extensions/onboarding.ts +8 -7
  224. package/extensions/selfimprove.ts +65 -0
  225. package/extensions/setup-scan.ts +79 -766
  226. package/extensions/setup.ts +14 -70
  227. package/extensions/vendor/openviking-pi/NOTICE +5 -0
  228. package/extensions/vendor/openviking-pi/README.md +39 -5
  229. package/extensions/vendor/openviking-pi/index.ts +4 -0
  230. package/extensions/vendor/openviking-pi/patches/0014-one-turn-sync-in-flight.patch +239 -0
  231. package/extensions/vendor/openviking-pi/qcr-hooks.ts +3 -1
  232. package/extensions/vendor/openviking-pi/sync.ts +80 -0
  233. package/extensions/vendor/openviking-pi/upstream.json +2 -1
  234. package/package.json +6 -4
  235. package/prompts/onboarding.md +8 -7
  236. package/schemas/review-result.v1.json +60 -0
  237. package/scripts/deploy.sh +8 -6
  238. package/scripts/qcr-host.sh +28 -5
  239. package/scripts/vm-ghcr-login.sh +7 -4
  240. package/src/archive/restore.ts +18 -21
  241. package/src/botworker.ts +13 -2
  242. package/src/channelfs.ts +3 -26
  243. package/src/cli.ts +13 -54
  244. package/src/config.ts +38 -0
  245. package/src/daemon.ts +34 -4
  246. package/src/debuground.ts +3 -0
  247. package/src/debugroundpi.ts +1 -0
  248. package/src/engine/caserunner-sdk.ts +29 -314
  249. package/src/home/adapters.ts +418 -0
  250. package/src/home/advanced.ts +421 -0
  251. package/src/home/advancedtools.ts +96 -0
  252. package/src/home/autoreview.ts +385 -0
  253. package/src/home/daemonstatus.ts +55 -0
  254. package/src/home/devicelogin.ts +107 -0
  255. package/src/home/launch.ts +257 -0
  256. package/src/home/loginowner.ts +248 -0
  257. package/src/home/memory.ts +176 -0
  258. package/src/home/model.ts +965 -0
  259. package/src/home/panel.ts +666 -0
  260. package/src/home/ports.ts +338 -0
  261. package/src/home/review.ts +206 -0
  262. package/src/home/reviews.ts +565 -0
  263. package/src/home/setup.ts +689 -0
  264. package/src/home/setupadapters.ts +217 -0
  265. package/src/home/setupcard.ts +288 -0
  266. package/src/home/setuptools.ts +57 -0
  267. package/src/home/spend.ts +194 -0
  268. package/src/home/switches.ts +293 -0
  269. package/src/home/switchon.ts +554 -0
  270. package/src/home/toolnames.ts +46 -0
  271. package/src/home/tune.ts +405 -0
  272. package/src/home/words.ts +8 -0
  273. package/src/homes.ts +17 -0
  274. package/src/install.ts +1 -1
  275. package/src/ipc/workerspec.ts +15 -0
  276. package/src/ledgercheck.ts +45 -9
  277. package/src/live/publish.ts +4 -1
  278. package/src/livecli.ts +6 -1
  279. package/src/loginrefresh/handover.ts +138 -0
  280. package/src/loginrefresh/lock.ts +72 -3
  281. package/src/loginrefresh/refresher.ts +34 -5
  282. package/src/loginrefresh/supervisor.ts +25 -2
  283. package/src/npmverbs.ts +51 -11
  284. package/src/onboard/choices.ts +6 -3
  285. package/src/onboard/engine.ts +64 -434
  286. package/src/onboard/steps/bringup.ts +3 -2
  287. package/src/onboard/steps/configfill.ts +23 -7
  288. package/src/onboard/steps/loginshare.ts +258 -0
  289. package/src/onboard/steps/memory.ts +18 -2
  290. package/src/onboard/steps/repoconfig.ts +85 -0
  291. package/src/onboard/toolnames.ts +2 -2
  292. package/src/piagentdir.ts +12 -0
  293. package/src/pilogin.ts +32 -16
  294. package/src/qcrbin.ts +16 -11
  295. package/src/reviewargs.ts +119 -0
  296. package/src/reviewcli.ts +6 -167
  297. package/src/reviewdetach.ts +107 -13
  298. package/src/scancli.ts +75 -826
  299. package/src/selfimprove.ts +90 -0
  300. package/src/session-command-supervisor.ts +41 -8
  301. package/src/setup/adapters.ts +13 -221
  302. package/src/setup/caserunner.ts +34 -273
  303. package/src/setup/engine.ts +62 -496
  304. package/src/setup/facts.ts +3 -627
  305. package/src/setup/findings.ts +3 -1264
  306. package/src/setup/presentation.ts +3 -18
  307. package/src/setup/scan.ts +3 -1033
  308. package/src/setup/scanbuild.ts +20 -114
  309. package/src/setup/scanlease.ts +3 -297
  310. package/src/setup/scanreport.ts +3 -373
  311. package/src/setup/scanrun.ts +3 -2268
  312. package/src/setup/scantree.ts +3 -259
  313. package/src/setup/stage2request.ts +3 -347
  314. package/src/setup/unitcut.ts +3 -523
  315. package/src/state.ts +126 -11
  316. package/src/takeover.ts +5 -0
  317. package/src/takeoverpi.ts +3 -2
  318. package/src/tui/alerts.ts +9 -2
  319. package/src/tui/board.ts +2 -2
  320. package/src/tui/companion.ts +2 -1
  321. package/src/tui/deploytab.ts +2 -2
  322. package/src/tui/index.ts +9 -6
  323. package/src/tui/memory.ts +3 -15
  324. package/src/tui/memoryurl.ts +22 -0
  325. package/src/tui/remote.ts +1 -1
  326. package/src/tui/route.ts +2 -2
  327. package/src/tui/setupstate.ts +20 -16
  328. package/src/tui/sshtriage.ts +1 -1
  329. package/src/verdicts.ts +13 -0
  330. package/src/wire.ts +9 -4
  331. package/src/workerbreaker.ts +41 -5
  332. package/src/workerworld.ts +9 -1
  333. package/dist/src/live/projection.js +0 -450
  334. package/dist/src/live/projection.js.map +0 -1
  335. package/dist/src/live/read.js +0 -60
  336. package/dist/src/live/read.js.map +0 -1
  337. package/dist/src/onboard/launch.js +0 -75
  338. package/dist/src/onboard/launch.js.map +0 -1
  339. package/dist/src/setup/ledger.js +0 -207
  340. package/dist/src/setup/ledger.js.map +0 -1
  341. package/dist/src/setup/stage2.js +0 -617
  342. package/dist/src/setup/stage2.js.map +0 -1
  343. package/dist/src/setup/textsniff.js +0 -89
  344. package/dist/src/setup/textsniff.js.map +0 -1
  345. package/src/live/projection.ts +0 -491
  346. package/src/live/read.ts +0 -86
  347. package/src/onboard/launch.ts +0 -103
  348. package/src/setup/ledger.ts +0 -324
  349. package/src/setup/stage2.ts +0 -926
  350. package/src/setup/textsniff.ts +0 -95
@@ -0,0 +1,811 @@
1
+ import { KEEP_IT_HERE, postAnswer } from "@quickchatai/pi-quickreview/src/plugin/messages.js";
2
+ import { PENDING, known, unknown, } from "./ports.js";
3
+ import { attachedRun, foldStatus, plannedRun, quoteRun, reviewMessage, runReview, } from "./review.js";
4
+ import { switchAutoReview, switchMemory } from "./switches.js";
5
+ import { money, plural } from "./words.js";
6
+ /** After a re-login, auto-review's own "login expired" is taken as old news
7
+ * for this long: its status lags the refresher's by a poll or two. */
8
+ export const RELOGIN_GRACE_MS = 5 * 60_000;
9
+ /** How long the cursor rests on a PR before the home prices it. */
10
+ export const PRICE_REST_MS = 500;
11
+ /** How often a running review's status is read. */
12
+ export const STATUS_POLL_MS = 2000;
13
+ /** "about $1.33, stops at $2.66", or what is not known of it. */
14
+ export function priceWords(est, cap) {
15
+ if (est === null)
16
+ return "price unknown";
17
+ return `about ${money(est)}${cap === null ? ", no hard stop" : `, stops at ${money(cap)}`}`;
18
+ }
19
+ const noTimer = () => () => { };
20
+ /** The provider of a `provider/id` model. */
21
+ export function providerOf(model) {
22
+ return model.split("/")[0] ?? model;
23
+ }
24
+ /** The provider as a person names it. */
25
+ export function providerLabel(provider) {
26
+ return provider === "openai-codex" || provider === "openai" ? "OpenAI" : provider;
27
+ }
28
+ export function pullUrl(repo, pr) {
29
+ return `https://github.com/${repo}/pull/${pr}`;
30
+ }
31
+ /** A short age: 5m, 3h, 2d. */
32
+ export function ago(thenMs, nowMs) {
33
+ if (!Number.isFinite(thenMs))
34
+ return "?";
35
+ const m = Math.max(0, (nowMs - thenMs) / 60_000);
36
+ return m < 60
37
+ ? `${Math.round(m)}m`
38
+ : m < 1440
39
+ ? `${Math.round(m / 60)}h`
40
+ : `${Math.round(m / 1440)}d`;
41
+ }
42
+ const message = (err) => (err instanceof Error ? err.message : String(err));
43
+ /** A port's answer; a port that throws instead of answering is unknown. */
44
+ async function safe(read) {
45
+ try {
46
+ return await read();
47
+ }
48
+ catch (err) {
49
+ return unknown(message(err));
50
+ }
51
+ }
52
+ /** An action port's outcome; one that throws is a refusal with its message. */
53
+ async function attempt(act) {
54
+ try {
55
+ return await act();
56
+ }
57
+ catch (err) {
58
+ return { ok: false, why: message(err) };
59
+ }
60
+ }
61
+ const later = (a, b) => a === undefined ? b : b === undefined ? a : b.at > a.at ? b : a;
62
+ const newestFirst = (a, b) => b.number - a.number;
63
+ const valueOf = (s) => (s.kind === "known" ? s.value : undefined);
64
+ const nullable = (s) => (s.kind === "known" ? s.value : null);
65
+ export class HomeModel {
66
+ o;
67
+ state = {
68
+ login: PENDING,
69
+ github: PENDING,
70
+ repo: PENDING,
71
+ pulls: PENDING,
72
+ reviews: PENDING,
73
+ autoRounds: PENDING,
74
+ autoReview: PENDING,
75
+ trigger: null,
76
+ memory: PENDING,
77
+ spend: PENDING,
78
+ loginHealth: null,
79
+ };
80
+ /** Slots a switch shows its own state in while it runs (src/home/switches.ts):
81
+ * a refresh does not overwrite them until released. */
82
+ held = new Set();
83
+ /** The selected pull request's number. */
84
+ selected = null;
85
+ notice = null;
86
+ tools;
87
+ provider;
88
+ inFlight = null;
89
+ greeted = false;
90
+ pullsWarned = false;
91
+ /** The latest run per PR this home started or re-attached to. */
92
+ runs = new Map();
93
+ /** Quotes by PR, each for the head it priced. */
94
+ quotes = new Map();
95
+ /** The pending rest timer's cancel. */
96
+ pricer = null;
97
+ /** The one background preview in flight. */
98
+ pricing = null;
99
+ timers = new Set();
100
+ /** PRs whose review the person stopped before it had a run id. */
101
+ stopping = new Set();
102
+ attachedTo = null;
103
+ disposed = false;
104
+ /** pi refused the chat for its login: the error it showed. */
105
+ chatLoginError = null;
106
+ /** When the person last signed in again, ms since the epoch. */
107
+ reloggedAt = null;
108
+ after;
109
+ constructor(o) {
110
+ this.o = o;
111
+ this.tools = o.tools;
112
+ this.provider = providerOf(o.model);
113
+ this.after = o.after ?? noTimer;
114
+ if (o.adapters.autoReview.trigger)
115
+ this.state.trigger = PENDING;
116
+ if (o.adapters.loginOwner)
117
+ this.state.loginHealth = PENDING;
118
+ }
119
+ /** The session ended: no timer fires and no preview runs. Runs go on. */
120
+ dispose() {
121
+ this.disposed = true;
122
+ this.pricer?.();
123
+ this.pricer = null;
124
+ this.pricing?.stop.abort();
125
+ this.pricing = null;
126
+ for (const cancel of this.timers)
127
+ cancel();
128
+ this.timers.clear();
129
+ }
130
+ changed() {
131
+ this.o.onChange?.();
132
+ }
133
+ now() {
134
+ return this.o.adapters.clock.now();
135
+ }
136
+ /** The provider the chat's model runs on: the one login that counts. */
137
+ chatProvider() {
138
+ return this.provider;
139
+ }
140
+ /** pi chose another model (at start, or the person's /model). */
141
+ setChatModel(provider) {
142
+ if (provider === undefined || provider === this.provider)
143
+ return;
144
+ this.provider = provider;
145
+ this.changed();
146
+ }
147
+ /** Signed in for the chat's provider: true, false, or null when the auth
148
+ * file could not be read. A login under another provider is not one. */
149
+ signedIn() {
150
+ const l = this.state.login;
151
+ return l.kind === "known" ? l.value.includes(this.provider) : null;
152
+ }
153
+ // ---- one login per box (src/home/loginowner.ts) -------------------------
154
+ /** pi refused the chat for its login (extensions/home.ts reads its error). */
155
+ loginFailed(why) {
156
+ if (this.chatLoginError === why)
157
+ return;
158
+ this.chatLoginError = why;
159
+ this.changed();
160
+ }
161
+ /** pi answered the chat: the login works. */
162
+ chatAnswered() {
163
+ if (this.chatLoginError === null)
164
+ return;
165
+ this.chatLoginError = null;
166
+ this.changed();
167
+ }
168
+ /** The person signed in again: what said the old login failed is past. */
169
+ signedInAgain() {
170
+ this.chatLoginError = null;
171
+ this.reloggedAt = this.now();
172
+ this.changed();
173
+ }
174
+ /** Why the login no longer works ("login expired"), or null while nothing
175
+ * says so. */
176
+ loginExpired() {
177
+ if (this.chatLoginError !== null)
178
+ return "login expired";
179
+ const h = this.state.loginHealth;
180
+ if (h?.kind === "known" &&
181
+ h.value.expired !== undefined &&
182
+ !(this.reloggedAt !== null && (h.value.checkedAt ?? 0) < this.reloggedAt))
183
+ return h.value.expired;
184
+ const a = this.state.autoReview;
185
+ if (a.kind === "known" &&
186
+ a.value.state === "paused-login" &&
187
+ a.value.cause === "login expired" &&
188
+ !(this.reloggedAt !== null && this.now() - this.reloggedAt < RELOGIN_GRACE_MS))
189
+ return "login expired";
190
+ return null;
191
+ }
192
+ /** Who refreshes the login now; null when that is not known. */
193
+ refreshedBy() {
194
+ const h = this.state.loginHealth;
195
+ return h?.kind === "known" ? h.value.refresher : null;
196
+ }
197
+ /** The setup steps a read showed to be needed: the checklist's gaps. */
198
+ gaps() {
199
+ return this.o.gaps?.() ?? [];
200
+ }
201
+ /** Is anything still being read? */
202
+ busy() {
203
+ return (this.inFlight !== null ||
204
+ this.pricing !== null ||
205
+ [...this.runs.keys()].some((pr) => this.isRunning(pr)));
206
+ }
207
+ repo() {
208
+ const r = valueOf(this.state.repo);
209
+ return typeof r === "string" ? r : null;
210
+ }
211
+ /** The lists, or null when the open pull requests are not known. */
212
+ lists() {
213
+ const pulls = valueOf(this.state.pulls);
214
+ if (pulls === undefined)
215
+ return null;
216
+ const sorted = [...pulls].sort(newestFirst);
217
+ if (valueOf(this.state.reviews) === undefined)
218
+ return { toReview: sorted, reviewed: null };
219
+ return {
220
+ toReview: sorted.filter((p) => this.reviewOf(p.number) === undefined),
221
+ reviewed: sorted.flatMap((p) => {
222
+ const review = this.reviewOf(p.number);
223
+ return review ? [{ pull: p, review }] : [];
224
+ }),
225
+ };
226
+ }
227
+ /** Why auto-review's half of the Reviewed list is unknown; null when known. */
228
+ autoUnknown() {
229
+ const a = this.state.autoRounds;
230
+ return a.kind === "unknown" ? a.why : null;
231
+ }
232
+ /** Every listed pull request, in the panel's order: To review, then Reviewed. */
233
+ ordered() {
234
+ const l = this.lists();
235
+ return l ? [...l.toReview, ...(l.reviewed ?? []).map((r) => r.pull)] : [];
236
+ }
237
+ selectedPull() {
238
+ return this.ordered().find((p) => p.number === this.selected);
239
+ }
240
+ /** The latest review of `pr`: this home's finished run, the person's own
241
+ * reviews, or auto-review's, whichever finished last. */
242
+ reviewOf(pr) {
243
+ const run = this.runs.get(pr);
244
+ const mine = run?.stage === "done" ? runReview(run) : undefined;
245
+ return later(later(valueOf(this.state.reviews)?.get(pr), valueOf(this.state.autoRounds)?.get(pr)), mine);
246
+ }
247
+ /** Is a review of `pr` under way (pricing, starting or running)? */
248
+ isRunning(pr) {
249
+ const r = this.runs.get(pr);
250
+ return r !== undefined && r.stage !== "done" && r.stage !== "failed";
251
+ }
252
+ /** The PR's price, for its head: pending while it is being priced, null
253
+ * before it was asked for. */
254
+ quoteOf(p) {
255
+ const e = this.quotes.get(p.number);
256
+ return e !== undefined && e.sha === p.headSha ? e.q : null;
257
+ }
258
+ say(text, kind = "info") {
259
+ this.notice = { text, kind };
260
+ this.changed();
261
+ }
262
+ // ---- the actions ------------------------------------------------------
263
+ /** Read every source again. A refresh already running is joined. */
264
+ refresh() {
265
+ this.inFlight ??= this.detect().finally(() => {
266
+ this.inFlight = null;
267
+ this.changed();
268
+ });
269
+ return this.inFlight;
270
+ }
271
+ async detect() {
272
+ const a = this.o.adapters;
273
+ const land = (key) => (v) => {
274
+ if (this.held.has(key))
275
+ return;
276
+ this.state[key] = v;
277
+ this.changed();
278
+ };
279
+ const side = Promise.all([
280
+ safe(() => a.login.providers()).then(land("login")),
281
+ safe(() => a.github.user()).then(land("github")),
282
+ safe(() => a.autoReview.status()).then(land("autoReview")),
283
+ safe(() => a.memory.status()).then(land("memory")),
284
+ a.autoReview.trigger
285
+ ? safe(() => a.autoReview.trigger()).then(land("trigger"))
286
+ : Promise.resolve(),
287
+ safe(() => a.spend.totals()).then(land("spend")),
288
+ a.loginOwner
289
+ ? safe(() => a.loginOwner.health()).then(land("loginHealth"))
290
+ : Promise.resolve(),
291
+ ]);
292
+ const repo = await safe(() => a.repo.repo());
293
+ land("repo")(repo);
294
+ if (repo.kind === "unknown") {
295
+ const why = `can't read the repository from your qcr config: ${repo.why}`;
296
+ this.state.pulls = unknown(why);
297
+ this.state.reviews = unknown(why);
298
+ this.state.autoRounds = unknown(why);
299
+ }
300
+ else if (repo.value === null) {
301
+ this.state.pulls = unknown("no repository is chosen");
302
+ this.state.reviews = unknown("no repository is chosen");
303
+ this.state.autoRounds = unknown("no repository is chosen");
304
+ }
305
+ else {
306
+ const name = repo.value;
307
+ const [pulls, reviews, autoRounds] = await Promise.all([
308
+ safe(() => a.github.openPulls(name)),
309
+ safe(() => a.reviews.latest(name)),
310
+ safe(() => a.reviews.autoRounds(name)),
311
+ this.attach(name),
312
+ ]);
313
+ this.state.pulls = pulls;
314
+ this.state.reviews = reviews;
315
+ this.state.autoRounds = autoRounds;
316
+ }
317
+ this.keepSelection();
318
+ await side;
319
+ // the checklist never throws (a step's detect that does is a missing)
320
+ await this.o.onRead?.();
321
+ this.greet();
322
+ return this.summary();
323
+ }
324
+ /** The selection survives a refresh while its PR is listed; else the first row. */
325
+ keepSelection() {
326
+ const rows = this.ordered();
327
+ if (!rows.some((p) => p.number === this.selected))
328
+ this.selected = rows[0]?.number ?? null;
329
+ this.rested();
330
+ }
331
+ /** The first notice, once everything has been read. */
332
+ greet() {
333
+ const repo = this.repo();
334
+ if (this.state.pulls.kind === "unknown" && repo !== null) {
335
+ this.pullsWarned = true;
336
+ this.say(`Couldn't read ${repo}'s pull requests: ${this.state.pulls.why}.`, "warn");
337
+ return;
338
+ }
339
+ if (this.pullsWarned) {
340
+ // the list came back: the warning about it no longer holds
341
+ this.pullsWarned = false;
342
+ const open = this.ordered().length;
343
+ this.say(`${open} open pull request${open === 1 ? "" : "s"}.`, "ok");
344
+ return;
345
+ }
346
+ if (this.greeted)
347
+ return;
348
+ this.greeted = true;
349
+ const gaps = this.gaps();
350
+ if (gaps.length > 0) {
351
+ this.say("Not set up yet: the steps above say what is missing.", "warn");
352
+ return;
353
+ }
354
+ const user = nullable(this.state.github);
355
+ const found = [
356
+ this.signedIn() ? `your ${providerLabel(this.provider)} login` : null,
357
+ user ? `GitHub @${user}` : null,
358
+ repo ? `${repo} from your qcr config` : null,
359
+ ].filter((x) => x !== null);
360
+ const open = this.ordered().length;
361
+ this.say(found.length === 3
362
+ ? `Found ${found[0]}, ${found[1]} and ${found[2]}: you're set. ${open} open pull request${open === 1 ? "" : "s"}.`
363
+ : `${open} open pull request${open === 1 ? "" : "s"}.`, "ok");
364
+ }
365
+ summary() {
366
+ const l = this.lists();
367
+ if (l === null) {
368
+ const p = this.state.pulls;
369
+ return p.kind === "unknown" ? `pull requests unknown: ${p.why}` : "still reading";
370
+ }
371
+ return `${l.toReview.length} to review, ${l.reviewed === null ? "reviewed unknown" : `${l.reviewed.length} reviewed`}`;
372
+ }
373
+ /** Move the selection by `by` rows through both lists. */
374
+ move(by) {
375
+ const rows = this.ordered();
376
+ if (rows.length === 0)
377
+ return "there is no pull request to move to";
378
+ const at = rows.findIndex((p) => p.number === this.selected);
379
+ const next = rows[Math.max(0, Math.min(rows.length - 1, (at < 0 ? 0 : at) + Math.trunc(by)))];
380
+ this.selected = next.number;
381
+ this.rested();
382
+ this.changed();
383
+ return `selected #${next.number} ${next.title}`;
384
+ }
385
+ /** Select one listed pull request (a hover or a click does this). */
386
+ select(pr) {
387
+ const p = this.ordered().find((x) => x.number === pr);
388
+ if (!p) {
389
+ const repo = this.repo();
390
+ return `#${pr} is not listed: it is not an open pull request${repo ? ` in ${repo}` : ""}, or the list is not known`;
391
+ }
392
+ if (this.selected !== pr) {
393
+ this.selected = pr;
394
+ this.rested();
395
+ this.changed();
396
+ }
397
+ return `selected #${p.number} ${p.title}`;
398
+ }
399
+ /** Open a pull request (by default the selected one) in the browser. */
400
+ async openPr(pr) {
401
+ const repo = this.repo();
402
+ if (repo === null) {
403
+ const r = this.state.repo;
404
+ return r.kind === "unknown"
405
+ ? `the repository is unknown (${r.why}), so there is nothing to open`
406
+ : "no repository is chosen, so there is nothing to open";
407
+ }
408
+ const n = pr ?? this.selected;
409
+ if (n === null)
410
+ return "no pull request is selected";
411
+ const url = pullUrl(repo, n);
412
+ const r = await this.o.adapters.github
413
+ .openInBrowser(repo, n)
414
+ .catch((err) => ({ ok: false, why: message(err) }));
415
+ if (r.ok) {
416
+ this.say(`Opened #${n} in your browser.`, "ok");
417
+ return `opened ${url} in the browser`;
418
+ }
419
+ this.say(`Couldn't open a browser (${r.why}). The pull request is at ${url}`, "warn");
420
+ return `couldn't open a browser (${r.why}); the pull request is at ${url}`;
421
+ }
422
+ // ---- the review button ------------------------------------------------
423
+ /** A PR's price for the model: null until it is quoted. */
424
+ priceOf(p) {
425
+ const q = this.quoteOf(p);
426
+ if (q === null || q.kind === "pending")
427
+ return null;
428
+ return q.kind === "known"
429
+ ? { estimate_usd: q.value.estimateUsd, cap_usd: q.value.capUsd }
430
+ : { unknown_because: q.why };
431
+ }
432
+ pullOf(pr) {
433
+ return this.ordered().find((p) => p.number === pr);
434
+ }
435
+ /** The cursor came to rest on a PR: price it in a moment, unless its head
436
+ * is priced already. A rest on another PR first cancels this one's. */
437
+ rested() {
438
+ this.pricer?.();
439
+ this.pricer = null;
440
+ const p = this.selectedPull();
441
+ if (this.disposed || !p || this.quoteOf(p) !== null || this.isRunning(p.number))
442
+ return;
443
+ this.pricer = this.after(PRICE_REST_MS, () => {
444
+ this.pricer = null;
445
+ void this.priceSelected();
446
+ });
447
+ }
448
+ /** Give way: the background preview in flight stops, and its PR is left
449
+ * unpriced (the next rest on it prices it again). */
450
+ yieldPricing() {
451
+ const p = this.pricing;
452
+ if (p === null)
453
+ return;
454
+ p.stop.abort();
455
+ this.pricing = null;
456
+ if (this.quotes.get(p.pr)?.q.kind === "pending")
457
+ this.quotes.delete(p.pr);
458
+ }
459
+ /** Price the PR the cursor rests on: one preview at a time. */
460
+ async priceSelected() {
461
+ const repo = this.repo();
462
+ const p = this.selectedPull();
463
+ if (repo === null || !p || this.quoteOf(p) !== null || this.isRunning(p.number))
464
+ return;
465
+ this.yieldPricing();
466
+ const stop = new AbortController();
467
+ this.pricing = { pr: p.number, stop };
468
+ this.quotes.set(p.number, { sha: p.headSha, q: PENDING });
469
+ this.changed();
470
+ const r = await attempt(() => this.o.adapters.reviews.preview(repo, p.number, { signal: stop.signal }));
471
+ if (this.pricing?.stop === stop)
472
+ this.pricing = null;
473
+ if (stop.signal.aborted)
474
+ return;
475
+ this.quotes.set(p.number, { sha: p.headSha, q: r.ok ? known(r.quote) : unknown(r.why) });
476
+ this.changed();
477
+ }
478
+ /** ⏎ (or alt+r): review the selected PR, or show its review when it has
479
+ * one and none is running. */
480
+ enter() {
481
+ const p = this.selectedPull();
482
+ if (p && !this.isRunning(p.number) && this.reviewOf(p.number))
483
+ return Promise.resolve(this.showReview(p.number));
484
+ return this.review();
485
+ }
486
+ /** Refused before anything starts: why the review button cannot act. */
487
+ cannotAct(pr) {
488
+ const repo = this.repo();
489
+ if (repo === null) {
490
+ const r = this.state.repo;
491
+ return r.kind === "unknown"
492
+ ? `the repository is unknown (${r.why}), so there is nothing to review`
493
+ : "no repository is chosen, so there is nothing to review";
494
+ }
495
+ const gaps = this.gaps();
496
+ if (gaps.length > 0)
497
+ return `finish setup first: ${gaps.join(", ")}`;
498
+ const n = pr ?? this.selected;
499
+ if (n === null)
500
+ return "no pull request is selected";
501
+ return (this.pullOf(n) ??
502
+ `#${n} is not listed: it is not an open pull request in ${repo}, or the list is not known`);
503
+ }
504
+ /** Review a pull request (by default the selected one) in the background:
505
+ * preview it, start a detached run with the preview's key, follow it. The
506
+ * run writes its report to a file and never posts by itself. */
507
+ async review(pr) {
508
+ const target = this.cannotAct(pr);
509
+ if (typeof target === "string")
510
+ return target;
511
+ const repo = this.repo();
512
+ const n = target.number;
513
+ const running = this.runs.get(n);
514
+ if (running && this.isRunning(n))
515
+ return `#${n} is already being reviewed${running.runId ? ` (run ${running.runId})` : ""}`;
516
+ const run = plannedRun(n, this.now());
517
+ this.runs.set(n, run);
518
+ this.stopping.delete(n);
519
+ this.selected = n;
520
+ this.pricer?.();
521
+ this.pricer = null;
522
+ this.yieldPricing();
523
+ this.say(`Pricing the review of #${n}…`);
524
+ const fail = (why) => {
525
+ run.stage = "failed";
526
+ run.error = why;
527
+ run.ended = this.now();
528
+ this.say(`#${n} was not reviewed: ${why}. It can be reviewed again.`, "warn");
529
+ return `the review of #${n} did not start: ${why}`;
530
+ };
531
+ const reviews = this.o.adapters.reviews;
532
+ const pre = await attempt(() => reviews.preview(repo, n));
533
+ this.quotes.set(n, { sha: target.headSha, q: pre.ok ? known(pre.quote) : unknown(pre.why) });
534
+ if (!pre.ok)
535
+ return fail(`the preview failed: ${pre.why}`);
536
+ if (this.stopping.delete(n))
537
+ return fail("you stopped it");
538
+ quoteRun(run, pre.quote);
539
+ run.now = "starting the review";
540
+ this.changed();
541
+ const st = await attempt(() => reviews.start(repo, n, pre.quote.key));
542
+ if (!st.ok)
543
+ return fail(st.why);
544
+ run.runId = st.runId;
545
+ run.report = st.report;
546
+ run.now = "fetching the pull request";
547
+ this.say(`Reviewing #${n} in the background — ${priceWords(run.est, run.cap)}. Keep chatting.`);
548
+ if (this.stopping.delete(n))
549
+ void this.stopReview(n);
550
+ this.follow(run);
551
+ return `the review of #${n} started (run ${st.runId}, ${priceWords(run.est, run.cap)}); it writes its report to ${st.report} and posts nothing by itself`;
552
+ }
553
+ /** Poll a run's status until it is over. */
554
+ follow(run) {
555
+ if (this.disposed)
556
+ return;
557
+ const cancel = this.after(STATUS_POLL_MS, () => {
558
+ this.timers.delete(cancel);
559
+ void this.poll(run);
560
+ });
561
+ this.timers.add(cancel);
562
+ }
563
+ async poll(run) {
564
+ if (this.disposed || run.runId === undefined)
565
+ return;
566
+ const id = run.runId;
567
+ const s = await safe(() => this.o.adapters.reviews.status(id));
568
+ if (this.disposed || this.runs.get(run.pr) !== run)
569
+ return;
570
+ if (s.kind === "unknown") {
571
+ run.now = `can't read its status: ${s.why}`;
572
+ this.changed();
573
+ return this.follow(run);
574
+ }
575
+ const over = foldStatus(run, s.value, this.now());
576
+ this.changed();
577
+ if (!over)
578
+ return this.follow(run);
579
+ await this.finished(run);
580
+ }
581
+ /** A run this home followed is over: say how, then the posting rule. */
582
+ async finished(run) {
583
+ const n = run.pr;
584
+ if (run.stage === "failed") {
585
+ this.say(`#${n} was not reviewed: ${run.error ?? "unknown"}. It can be reviewed again.`, "warn");
586
+ return;
587
+ }
588
+ const high = run.findings.filter((f) => f.sev === "high").length;
589
+ this.selected = n;
590
+ this.say(`#${n} reviewed — ${plural(run.findings.length, "finding")}${high ? `, ${high} high` : ""}. ${"⏎"} shows them.`, "ok");
591
+ await this.offerPost(run);
592
+ }
593
+ /** After a finished review the person's posting rule applies: never, always,
594
+ * or ask with "No, keep it here" first. Nobody to ask is a no. */
595
+ async offerPost(run) {
596
+ const file = run.postFile;
597
+ const repo = this.repo();
598
+ if (file === undefined || repo === null)
599
+ return;
600
+ const rule = this.o.postingRule?.() ?? "ask";
601
+ const n = run.pr;
602
+ if (rule === "never")
603
+ return;
604
+ if (rule === "always") {
605
+ this.say(`Posting the review of #${n} to GitHub, as your posting rule says…`);
606
+ return this.postNow(n, file);
607
+ }
608
+ const ask = this.o.ask;
609
+ if (ask === undefined)
610
+ return;
611
+ // the review first, so the question is about something the person has read
612
+ this.showReview(n);
613
+ const dry = await safe(() => this.o.adapters.reviews.dryRun());
614
+ const yes = dry.kind === "known"
615
+ ? postAnswer(dry.value)
616
+ : "Yes, post it as a comment-only review (your install's dry_run decides whether it is sent)";
617
+ let picked;
618
+ try {
619
+ picked = await ask(`Post the review of #${n} (${repo}) on GitHub as a comment-only review?`, [
620
+ KEEP_IT_HERE,
621
+ yes,
622
+ ]);
623
+ }
624
+ catch {
625
+ picked = undefined;
626
+ }
627
+ if (picked !== yes) {
628
+ this.say(`The review of #${n} stays here: nothing was posted.`);
629
+ return;
630
+ }
631
+ await this.postNow(n, file);
632
+ }
633
+ async postNow(n, file) {
634
+ const r = await attempt(() => this.o.adapters.reviews.post(file));
635
+ this.say(r.ok ? r.line : `The review of #${n} was not posted: ${r.why}`, r.ok ? "ok" : "warn");
636
+ }
637
+ /** Put a reviewed PR's findings in the chat as a message. */
638
+ showReview(pr) {
639
+ const n = pr ?? this.selected;
640
+ if (n === null)
641
+ return "no pull request is selected";
642
+ const review = this.reviewOf(n);
643
+ if (!review)
644
+ return `#${n} has no review yet`;
645
+ const title = this.pullOf(n)?.title ?? "";
646
+ const content = reviewMessage({ pr: n, title, review });
647
+ if (!this.o.send)
648
+ return content;
649
+ this.o.send({
650
+ content,
651
+ details: { repo: this.repo(), pr: n, by: review.by, findings: review.list ?? null },
652
+ });
653
+ return `the review of #${n} is in the chat`;
654
+ }
655
+ /** Stop the review of a pull request (by default the selected one). The PR
656
+ * is reviewable again once its status says it stopped. */
657
+ async stopReview(pr) {
658
+ const n = pr ?? this.selected;
659
+ if (n === null)
660
+ return "no pull request is selected";
661
+ const run = this.runs.get(n);
662
+ if (!run || !this.isRunning(n))
663
+ return `no review of #${n} is running`;
664
+ if (run.runId === undefined) {
665
+ // still pricing or starting: it stops as soon as it has a run
666
+ this.stopping.add(n);
667
+ this.say(`Stopping the review of #${n}…`);
668
+ return `the review of #${n} stops as soon as it starts`;
669
+ }
670
+ const r = await attempt(() => this.o.adapters.reviews.stop(run.runId));
671
+ if (!r.ok) {
672
+ this.say(`Couldn't stop the review of #${n}: ${r.why}`, "warn");
673
+ return `couldn't stop the review of #${n}: ${r.why}`;
674
+ }
675
+ this.say(`Stopping the review of #${n}…`);
676
+ return `stopped the review of #${n} (run ${run.runId})`;
677
+ }
678
+ /** On opening (and on a new repository): the registry's runs of `repo`.
679
+ * A running one is followed, a finished or failed one shown; nothing is
680
+ * asked about a run that ended while the home was closed. */
681
+ async attach(repo) {
682
+ if (this.attachedTo === repo)
683
+ return;
684
+ this.attachedTo = repo;
685
+ const r = await safe(() => this.o.adapters.reviews.runs(repo));
686
+ if (r.kind === "unknown" || this.disposed)
687
+ return;
688
+ const latest = new Map();
689
+ for (const s of r.value)
690
+ if (s.pr !== null && s.repo === repo)
691
+ latest.set(s.pr, s);
692
+ for (const [pr, s] of latest) {
693
+ if (this.runs.has(pr))
694
+ continue;
695
+ const run = attachedRun(s);
696
+ foldStatus(run, s, this.now());
697
+ this.runs.set(pr, run);
698
+ if (s.state === "running")
699
+ this.follow(run);
700
+ }
701
+ this.changed();
702
+ }
703
+ // ---- the switches (src/home/switches.ts) ------------------------------
704
+ /** Can the home switch auto-review / memory, or only show them? */
705
+ canSwitch(what) {
706
+ return this.o.adapters[what].control !== undefined;
707
+ }
708
+ /** Memory's switch, when the home has one. */
709
+ memoryControl() {
710
+ return this.o.adapters.memory.control;
711
+ }
712
+ /** The trigger label, when known. */
713
+ trigger() {
714
+ const t = this.state.trigger;
715
+ return t?.kind === "known" ? t.value : null;
716
+ }
717
+ switchHost() {
718
+ const hold = (slot, v) => {
719
+ this.state[slot] = v;
720
+ this.held.add(slot);
721
+ this.changed();
722
+ };
723
+ return {
724
+ autoReview: () => this.state.autoReview,
725
+ memory: () => this.state.memory,
726
+ trigger: () => this.trigger(),
727
+ repo: () => this.repo(),
728
+ selected: () => this.selected,
729
+ say: (text, kind) => this.say(text, kind),
730
+ hold,
731
+ release: (slot) => {
732
+ this.held.delete(slot);
733
+ },
734
+ refresh: () => this.refresh(),
735
+ };
736
+ }
737
+ /** Switch auto-review on or off: every question asked through `ask`. */
738
+ switchAutoReview(ask) {
739
+ return switchAutoReview(this.switchHost(), this.o.adapters, ask);
740
+ }
741
+ /** Switch memory on or off: its own switch, auto-review untouched. */
742
+ switchMemory(ask) {
743
+ return switchMemory(this.switchHost(), this.o.adapters, ask);
744
+ }
745
+ /** The login's owner and health, for the model: null when unknown. */
746
+ loginOwnerStatus(h) {
747
+ const expired = this.loginExpired();
748
+ return {
749
+ refreshed_by: this.refreshedBy(),
750
+ expired: expired !== null ? true : h.kind === "known" ? false : null,
751
+ ...(h.kind === "unknown" ? { refresher_unknown_because: h.why } : {}),
752
+ };
753
+ }
754
+ /** What the panel shows, for the model: an unknown source is null, never
755
+ * absent and never zero. */
756
+ status() {
757
+ const l = this.lists();
758
+ const why = (s) => (s.kind === "unknown" ? s.why : null);
759
+ const auto = this.state.autoReview;
760
+ return {
761
+ repo: nullable(this.state.repo),
762
+ login: {
763
+ provider: this.provider,
764
+ signed_in: this.signedIn(),
765
+ ...(why(this.state.login) ? { unknown_because: why(this.state.login) } : {}),
766
+ ...(this.state.loginHealth ? this.loginOwnerStatus(this.state.loginHealth) : {}),
767
+ },
768
+ github: {
769
+ user: nullable(this.state.github),
770
+ signed_in: this.state.github.kind === "known" ? this.state.github.value !== null : null,
771
+ },
772
+ tools: this.tools,
773
+ setup_needed: this.gaps(),
774
+ auto_review: auto.kind === "known" ? auto.value : null,
775
+ ...(auto.kind === "unknown" ? { auto_review_unknown_because: auto.why } : {}),
776
+ ...(this.state.trigger
777
+ ? { auto_review_picks_up: { trigger_label: this.trigger(), review_requests: true } }
778
+ : {}),
779
+ memory: nullable(this.state.memory),
780
+ spend: nullable(this.state.spend),
781
+ to_review: l ? l.toReview.map((p) => ({ ...row(p), price: this.priceOf(p) })) : null,
782
+ reviewed: l?.reviewed ? l.reviewed.map((r) => ({ ...row(r.pull), review: r.review })) : null,
783
+ ...(l?.reviewed && this.autoUnknown() !== null
784
+ ? { auto_reviews_unknown_because: this.autoUnknown() }
785
+ : {}),
786
+ reviewing: [...this.runs.values()]
787
+ .filter((r) => this.isRunning(r.pr))
788
+ .map((r) => ({
789
+ number: r.pr,
790
+ run_id: r.runId ?? null,
791
+ stage: r.stage,
792
+ estimate_usd: r.est,
793
+ cap_usd: r.cap,
794
+ lenses: r.lenses.map((x) => ({ name: x.name, state: x.state, tools: x.tools })),
795
+ })),
796
+ ...(why(this.state.pulls) ? { pull_requests_unknown_because: why(this.state.pulls) } : {}),
797
+ selected: this.selected,
798
+ };
799
+ }
800
+ }
801
+ function row(p) {
802
+ return {
803
+ number: p.number,
804
+ title: p.title,
805
+ author: p.author,
806
+ updated_at: p.updatedAt,
807
+ draft: p.isDraft,
808
+ branch: p.headRefName,
809
+ };
810
+ }
811
+ //# sourceMappingURL=model.js.map