@quickchatai/quickreview 1.1.0 → 1.3.1

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