@quickchatai/quickreview 1.2.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 (322) hide show
  1. package/README.md +114 -50
  2. package/bin/qcr.js +1 -1
  3. package/config.example.toml +20 -6
  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/src/archive/restore.js +18 -15
  25. package/dist/src/archive/restore.js.map +1 -1
  26. package/dist/src/botworker.js +13 -2
  27. package/dist/src/botworker.js.map +1 -1
  28. package/dist/src/channelfs.js +3 -26
  29. package/dist/src/channelfs.js.map +1 -1
  30. package/dist/src/cli.js +12 -50
  31. package/dist/src/cli.js.map +1 -1
  32. package/dist/src/config.js +16 -0
  33. package/dist/src/config.js.map +1 -1
  34. package/dist/src/daemon.js +26 -0
  35. package/dist/src/daemon.js.map +1 -1
  36. package/dist/src/debuground.js +1 -0
  37. package/dist/src/debuground.js.map +1 -1
  38. package/dist/src/debugroundpi.js +1 -0
  39. package/dist/src/debugroundpi.js.map +1 -1
  40. package/dist/src/engine/caserunner-sdk.js +15 -251
  41. package/dist/src/engine/caserunner-sdk.js.map +1 -1
  42. package/dist/src/home/adapters.js +349 -0
  43. package/dist/src/home/adapters.js.map +1 -0
  44. package/dist/src/home/advanced.js +318 -0
  45. package/dist/src/home/advanced.js.map +1 -0
  46. package/dist/src/home/advancedtools.js +57 -0
  47. package/dist/src/home/advancedtools.js.map +1 -0
  48. package/dist/src/home/autoreview.js +325 -0
  49. package/dist/src/home/autoreview.js.map +1 -0
  50. package/dist/src/home/daemonstatus.js +32 -0
  51. package/dist/src/home/daemonstatus.js.map +1 -0
  52. package/dist/src/home/devicelogin.js +61 -0
  53. package/dist/src/home/devicelogin.js.map +1 -0
  54. package/dist/src/home/launch.js +202 -0
  55. package/dist/src/home/launch.js.map +1 -0
  56. package/dist/src/home/loginowner.js +205 -0
  57. package/dist/src/home/loginowner.js.map +1 -0
  58. package/dist/src/home/memory.js +122 -0
  59. package/dist/src/home/memory.js.map +1 -0
  60. package/dist/src/home/model.js +811 -0
  61. package/dist/src/home/model.js.map +1 -0
  62. package/dist/src/home/panel.js +520 -0
  63. package/dist/src/home/panel.js.map +1 -0
  64. package/dist/src/home/ports.js +19 -0
  65. package/dist/src/home/ports.js.map +1 -0
  66. package/dist/src/home/review.js +163 -0
  67. package/dist/src/home/review.js.map +1 -0
  68. package/dist/src/home/reviews.js +478 -0
  69. package/dist/src/home/reviews.js.map +1 -0
  70. package/dist/src/home/setup.js +507 -0
  71. package/dist/src/home/setup.js.map +1 -0
  72. package/dist/src/home/setupadapters.js +214 -0
  73. package/dist/src/home/setupadapters.js.map +1 -0
  74. package/dist/src/home/setupcard.js +199 -0
  75. package/dist/src/home/setupcard.js.map +1 -0
  76. package/dist/src/home/setuptools.js +45 -0
  77. package/dist/src/home/setuptools.js.map +1 -0
  78. package/dist/src/home/spend.js +160 -0
  79. package/dist/src/home/spend.js.map +1 -0
  80. package/dist/src/home/switches.js +209 -0
  81. package/dist/src/home/switches.js.map +1 -0
  82. package/dist/src/home/switchon.js +439 -0
  83. package/dist/src/home/switchon.js.map +1 -0
  84. package/dist/src/home/toolnames.js +41 -0
  85. package/dist/src/home/toolnames.js.map +1 -0
  86. package/dist/src/home/tune.js +341 -0
  87. package/dist/src/home/tune.js.map +1 -0
  88. package/dist/src/home/words.js +7 -0
  89. package/dist/src/home/words.js.map +1 -0
  90. package/dist/src/homes.js +14 -0
  91. package/dist/src/homes.js.map +1 -1
  92. package/dist/src/install.js +1 -1
  93. package/dist/src/install.js.map +1 -1
  94. package/dist/src/ipc/workerspec.js +8 -0
  95. package/dist/src/ipc/workerspec.js.map +1 -1
  96. package/dist/src/live/publish.js +1 -1
  97. package/dist/src/live/publish.js.map +1 -1
  98. package/dist/src/livecli.js +1 -1
  99. package/dist/src/livecli.js.map +1 -1
  100. package/dist/src/loginrefresh/handover.js +121 -0
  101. package/dist/src/loginrefresh/handover.js.map +1 -0
  102. package/dist/src/loginrefresh/lock.js +54 -3
  103. package/dist/src/loginrefresh/lock.js.map +1 -1
  104. package/dist/src/loginrefresh/refresher.js +33 -5
  105. package/dist/src/loginrefresh/refresher.js.map +1 -1
  106. package/dist/src/loginrefresh/supervisor.js +18 -1
  107. package/dist/src/loginrefresh/supervisor.js.map +1 -1
  108. package/dist/src/npmverbs.js +42 -12
  109. package/dist/src/npmverbs.js.map +1 -1
  110. package/dist/src/onboard/choices.js +6 -3
  111. package/dist/src/onboard/choices.js.map +1 -1
  112. package/dist/src/onboard/engine.js +33 -292
  113. package/dist/src/onboard/engine.js.map +1 -1
  114. package/dist/src/onboard/steps/bringup.js +3 -2
  115. package/dist/src/onboard/steps/bringup.js.map +1 -1
  116. package/dist/src/onboard/steps/configfill.js +18 -7
  117. package/dist/src/onboard/steps/configfill.js.map +1 -1
  118. package/dist/src/onboard/steps/loginshare.js +213 -0
  119. package/dist/src/onboard/steps/loginshare.js.map +1 -0
  120. package/dist/src/onboard/steps/memory.js +14 -2
  121. package/dist/src/onboard/steps/memory.js.map +1 -1
  122. package/dist/src/onboard/steps/repoconfig.js +72 -0
  123. package/dist/src/onboard/steps/repoconfig.js.map +1 -0
  124. package/dist/src/onboard/toolnames.js +2 -2
  125. package/dist/src/onboard/toolnames.js.map +1 -1
  126. package/dist/src/piagentdir.js +13 -0
  127. package/dist/src/piagentdir.js.map +1 -0
  128. package/dist/src/pilogin.js +31 -16
  129. package/dist/src/pilogin.js.map +1 -1
  130. package/dist/src/qcrbin.js +16 -11
  131. package/dist/src/qcrbin.js.map +1 -1
  132. package/dist/src/reviewargs.js +122 -0
  133. package/dist/src/reviewargs.js.map +1 -0
  134. package/dist/src/reviewcli.js +7 -161
  135. package/dist/src/reviewcli.js.map +1 -1
  136. package/dist/src/reviewdetach.js +76 -11
  137. package/dist/src/reviewdetach.js.map +1 -1
  138. package/dist/src/scancli.js +39 -691
  139. package/dist/src/scancli.js.map +1 -1
  140. package/dist/src/selfimprove.js +66 -0
  141. package/dist/src/selfimprove.js.map +1 -0
  142. package/dist/src/setup/adapters.js +5 -223
  143. package/dist/src/setup/adapters.js.map +1 -1
  144. package/dist/src/setup/caserunner.js +15 -249
  145. package/dist/src/setup/caserunner.js.map +1 -1
  146. package/dist/src/setup/engine.js +32 -341
  147. package/dist/src/setup/engine.js.map +1 -1
  148. package/dist/src/setup/facts.js +3 -592
  149. package/dist/src/setup/facts.js.map +1 -1
  150. package/dist/src/setup/findings.js +3 -947
  151. package/dist/src/setup/findings.js.map +1 -1
  152. package/dist/src/setup/presentation.js +3 -8
  153. package/dist/src/setup/presentation.js.map +1 -1
  154. package/dist/src/setup/scan.js +3 -724
  155. package/dist/src/setup/scan.js.map +1 -1
  156. package/dist/src/setup/scanbuild.js +9 -92
  157. package/dist/src/setup/scanbuild.js.map +1 -1
  158. package/dist/src/setup/scanlease.js +3 -250
  159. package/dist/src/setup/scanlease.js.map +1 -1
  160. package/dist/src/setup/scanreport.js +3 -288
  161. package/dist/src/setup/scanreport.js.map +1 -1
  162. package/dist/src/setup/scanrun.js +3 -1732
  163. package/dist/src/setup/scanrun.js.map +1 -1
  164. package/dist/src/setup/scantree.js +3 -256
  165. package/dist/src/setup/scantree.js.map +1 -1
  166. package/dist/src/setup/stage2request.js +3 -258
  167. package/dist/src/setup/stage2request.js.map +1 -1
  168. package/dist/src/setup/unitcut.js +3 -436
  169. package/dist/src/setup/unitcut.js.map +1 -1
  170. package/dist/src/state.js +2 -0
  171. package/dist/src/state.js.map +1 -1
  172. package/dist/src/takeover.js +1 -0
  173. package/dist/src/takeover.js.map +1 -1
  174. package/dist/src/takeoverpi.js +2 -1
  175. package/dist/src/takeoverpi.js.map +1 -1
  176. package/dist/src/tui/board.js +1 -1
  177. package/dist/src/tui/board.js.map +1 -1
  178. package/dist/src/tui/companion.js +2 -1
  179. package/dist/src/tui/companion.js.map +1 -1
  180. package/dist/src/tui/deploytab.js +2 -2
  181. package/dist/src/tui/deploytab.js.map +1 -1
  182. package/dist/src/tui/index.js +9 -6
  183. package/dist/src/tui/index.js.map +1 -1
  184. package/dist/src/tui/memory.js +3 -16
  185. package/dist/src/tui/memory.js.map +1 -1
  186. package/dist/src/tui/memoryurl.js +22 -0
  187. package/dist/src/tui/memoryurl.js.map +1 -0
  188. package/dist/src/tui/remote.js +1 -1
  189. package/dist/src/tui/remote.js.map +1 -1
  190. package/dist/src/tui/route.js +2 -2
  191. package/dist/src/tui/route.js.map +1 -1
  192. package/dist/src/tui/setupstate.js +8 -5
  193. package/dist/src/tui/setupstate.js.map +1 -1
  194. package/dist/src/tui/sshtriage.js +1 -1
  195. package/dist/src/tui/sshtriage.js.map +1 -1
  196. package/dist/src/wire.js +6 -2
  197. package/dist/src/wire.js.map +1 -1
  198. package/dist/src/workerworld.js +4 -0
  199. package/dist/src/workerworld.js.map +1 -1
  200. package/docker/README.md +1 -1
  201. package/docker/entrypoint.sh +40 -6
  202. package/docker-compose.yml +16 -1
  203. package/extensions/cli/home.ts +14 -0
  204. package/extensions/cli/selfimprove.ts +18 -0
  205. package/extensions/home.ts +484 -0
  206. package/extensions/login-route.ts +1 -1
  207. package/extensions/onboarding.ts +8 -7
  208. package/extensions/selfimprove.ts +65 -0
  209. package/extensions/setup-scan.ts +79 -766
  210. package/extensions/setup.ts +14 -70
  211. package/package.json +6 -4
  212. package/prompts/onboarding.md +8 -7
  213. package/schemas/review-result.v1.json +60 -0
  214. package/scripts/deploy.sh +8 -6
  215. package/scripts/qcr-host.sh +28 -5
  216. package/scripts/vm-ghcr-login.sh +7 -4
  217. package/src/archive/restore.ts +18 -21
  218. package/src/botworker.ts +13 -2
  219. package/src/channelfs.ts +3 -26
  220. package/src/cli.ts +11 -54
  221. package/src/config.ts +38 -0
  222. package/src/daemon.ts +26 -0
  223. package/src/debuground.ts +3 -0
  224. package/src/debugroundpi.ts +1 -0
  225. package/src/engine/caserunner-sdk.ts +29 -314
  226. package/src/home/adapters.ts +418 -0
  227. package/src/home/advanced.ts +421 -0
  228. package/src/home/advancedtools.ts +96 -0
  229. package/src/home/autoreview.ts +385 -0
  230. package/src/home/daemonstatus.ts +55 -0
  231. package/src/home/devicelogin.ts +107 -0
  232. package/src/home/launch.ts +257 -0
  233. package/src/home/loginowner.ts +248 -0
  234. package/src/home/memory.ts +176 -0
  235. package/src/home/model.ts +965 -0
  236. package/src/home/panel.ts +666 -0
  237. package/src/home/ports.ts +338 -0
  238. package/src/home/review.ts +206 -0
  239. package/src/home/reviews.ts +565 -0
  240. package/src/home/setup.ts +689 -0
  241. package/src/home/setupadapters.ts +217 -0
  242. package/src/home/setupcard.ts +288 -0
  243. package/src/home/setuptools.ts +57 -0
  244. package/src/home/spend.ts +194 -0
  245. package/src/home/switches.ts +293 -0
  246. package/src/home/switchon.ts +554 -0
  247. package/src/home/toolnames.ts +46 -0
  248. package/src/home/tune.ts +405 -0
  249. package/src/home/words.ts +8 -0
  250. package/src/homes.ts +17 -0
  251. package/src/install.ts +1 -1
  252. package/src/ipc/workerspec.ts +15 -0
  253. package/src/live/publish.ts +4 -1
  254. package/src/livecli.ts +6 -1
  255. package/src/loginrefresh/handover.ts +138 -0
  256. package/src/loginrefresh/lock.ts +72 -3
  257. package/src/loginrefresh/refresher.ts +34 -5
  258. package/src/loginrefresh/supervisor.ts +25 -2
  259. package/src/npmverbs.ts +51 -11
  260. package/src/onboard/choices.ts +6 -3
  261. package/src/onboard/engine.ts +64 -434
  262. package/src/onboard/steps/bringup.ts +3 -2
  263. package/src/onboard/steps/configfill.ts +23 -7
  264. package/src/onboard/steps/loginshare.ts +258 -0
  265. package/src/onboard/steps/memory.ts +18 -2
  266. package/src/onboard/steps/repoconfig.ts +85 -0
  267. package/src/onboard/toolnames.ts +2 -2
  268. package/src/piagentdir.ts +12 -0
  269. package/src/pilogin.ts +32 -16
  270. package/src/qcrbin.ts +16 -11
  271. package/src/reviewargs.ts +119 -0
  272. package/src/reviewcli.ts +6 -167
  273. package/src/reviewdetach.ts +107 -13
  274. package/src/scancli.ts +75 -826
  275. package/src/selfimprove.ts +90 -0
  276. package/src/setup/adapters.ts +13 -221
  277. package/src/setup/caserunner.ts +34 -273
  278. package/src/setup/engine.ts +62 -496
  279. package/src/setup/facts.ts +3 -627
  280. package/src/setup/findings.ts +3 -1264
  281. package/src/setup/presentation.ts +3 -18
  282. package/src/setup/scan.ts +3 -1033
  283. package/src/setup/scanbuild.ts +20 -114
  284. package/src/setup/scanlease.ts +3 -297
  285. package/src/setup/scanreport.ts +3 -373
  286. package/src/setup/scanrun.ts +3 -2268
  287. package/src/setup/scantree.ts +3 -259
  288. package/src/setup/stage2request.ts +3 -347
  289. package/src/setup/unitcut.ts +3 -523
  290. package/src/state.ts +25 -0
  291. package/src/takeover.ts +5 -0
  292. package/src/takeoverpi.ts +3 -2
  293. package/src/tui/board.ts +1 -1
  294. package/src/tui/companion.ts +2 -1
  295. package/src/tui/deploytab.ts +2 -2
  296. package/src/tui/index.ts +9 -6
  297. package/src/tui/memory.ts +3 -15
  298. package/src/tui/memoryurl.ts +22 -0
  299. package/src/tui/remote.ts +1 -1
  300. package/src/tui/route.ts +2 -2
  301. package/src/tui/setupstate.ts +20 -16
  302. package/src/tui/sshtriage.ts +1 -1
  303. package/src/wire.ts +6 -2
  304. package/src/workerworld.ts +9 -1
  305. package/dist/src/live/projection.js +0 -450
  306. package/dist/src/live/projection.js.map +0 -1
  307. package/dist/src/live/read.js +0 -60
  308. package/dist/src/live/read.js.map +0 -1
  309. package/dist/src/onboard/launch.js +0 -75
  310. package/dist/src/onboard/launch.js.map +0 -1
  311. package/dist/src/setup/ledger.js +0 -207
  312. package/dist/src/setup/ledger.js.map +0 -1
  313. package/dist/src/setup/stage2.js +0 -617
  314. package/dist/src/setup/stage2.js.map +0 -1
  315. package/dist/src/setup/textsniff.js +0 -89
  316. package/dist/src/setup/textsniff.js.map +0 -1
  317. package/src/live/projection.ts +0 -491
  318. package/src/live/read.ts +0 -86
  319. package/src/onboard/launch.ts +0 -103
  320. package/src/setup/ledger.ts +0 -324
  321. package/src/setup/stage2.ts +0 -926
  322. package/src/setup/textsniff.ts +0 -95
@@ -0,0 +1,484 @@
1
+ // home — the app's home: the panel at the top right of the app's own pi, and
2
+ // the `home_*` tools. Loaded by a bare `qcr` (src/home/launch.ts) through its
3
+ // env shim, extensions/cli/home.ts, beside the plugin; a factory over explicit
4
+ // options, so it reads no process state.
5
+ //
6
+ // Every action is one HomeModel method (src/home/model.ts), called by a key
7
+ // and by a tool alike:
8
+ //
9
+ // action key, while typing key, panel focused tool
10
+ // move the cursor alt+j / alt+k j / k, ↓ / ↑ home_move
11
+ // select a PR (hover, click) (hover, click) home_select
12
+ // open it on GitHub alt+o o home_open_pr
13
+ // read it all again alt+u u home_refresh
14
+ // review it alt+r ⏎, r home_review_pr
15
+ // show its review alt+r (reviewed) ⏎ (reviewed) home_show_review
16
+ // stop its review alt+s s home_stop_review
17
+ // what it shows — — home_status
18
+ // auto-review on/off alt+a a home_auto_review
19
+ // memory on/off alt+m m home_memory
20
+ // sign in again alt+l l home_relogin
21
+ //
22
+ // Signing in again is for a login that expired or was revoked: the login
23
+ // line says "login expired" (src/home/loginowner.ts reads the refresher's
24
+ // record, pi's own error in the chat and auto-review's pause). It is pi's
25
+ // device login, with auto-review's login refresher paused around it when it
26
+ // runs, so the box keeps one login that one process refreshes.
27
+ // tune to this repo alt+t t home_tune,
28
+ // home_tune_write
29
+ // advanced alt+x x home_advanced
30
+ //
31
+ // The two switches ask the person every question in the home (pi's select):
32
+ // a tool starts one but cannot answer it, so the confirm that turns
33
+ // auto-review on (in dry-run) and going live's own, after its proof round,
34
+ // are the person's (invariant 6).
35
+ //
36
+ // The review runs detached (the app's `qcr review start`, on the person's own
37
+ // pi login), writes its report to a file and never posts by itself; once it
38
+ // is done the person's posting rule (pi's settings, `quickreview.posting`)
39
+ // applies, asking with "No, keep it here" first; a rule pi's settings spell
40
+ // wrong is said, as the plugin says it. A review's findings go to the chat as
41
+ // the plugin's review message: shown, never a turn of its own, so a review
42
+ // that finishes while the agent works never steers it.
43
+ // next setup step alt+n ⏎ (on the card) home_setup_next
44
+ // each setup step — — home_sign_in,
45
+ // home_check_github,
46
+ // home_choose_repo
47
+ //
48
+ // On a machine that is not set up, the setup checklist (src/home/setup.ts)
49
+ // shows as a centred card (src/home/setupcard.ts) instead of the panel; when
50
+ // its last step is done the card gives way to the panel, and back again if a
51
+ // read shows a step missing once more.
52
+ //
53
+ // f2 or a click focuses the panel for its plain keys, and Esc gives the
54
+ // editor back: the panel never takes the keyboard by itself (nonCapturing).
55
+ // The first frame is drawn before any port is read; each read lands into it.
56
+ //
57
+ // The session's end (a quit, or /new replacing it) makes pi's context for it
58
+ // stale: the home lets go of it there, and a read still in flight lands into
59
+ // nothing.
60
+ import type {
61
+ ExtensionAPI,
62
+ ExtensionContext,
63
+ ExtensionFactory,
64
+ } from "@earendil-works/pi-coding-agent";
65
+ import type { KeyId, OverlayHandle, TUI } from "@earendil-works/pi-tui";
66
+ import {
67
+ registerQcrTool,
68
+ textResult,
69
+ type QcrTool,
70
+ } from "@quickchatai/pi-quickreview/extensions/pi-tool.js";
71
+ import { REVIEW_MESSAGE } from "@quickchatai/pi-quickreview/src/plugin/messages.js";
72
+ import { readQuickReviewSettings } from "@quickchatai/pi-quickreview/src/plugin/settings.js";
73
+ import type { HomeToolSet } from "../src/config.js";
74
+ import { piDeviceLogin } from "../src/home/devicelogin.js";
75
+ import { Relogin, RELOGIN_KEY, isLoginError } from "../src/home/loginowner.js";
76
+ import { HomeAdvanced, type AdvancedAsk, type AdvancedPort } from "../src/home/advanced.js";
77
+ import { advancedTools } from "../src/home/advancedtools.js";
78
+ import { HomeModel } from "../src/home/model.js";
79
+ import { HOME_OVERLAY, HomePanel } from "../src/home/panel.js";
80
+ import type { HomeAdapters } from "../src/home/ports.js";
81
+ import type { Ask } from "../src/home/switches.js";
82
+ import { HomeSetup, type HomeSetupPorts, type SetupUi } from "../src/home/setup.js";
83
+ import { SETUP_OVERLAY, SetupCard } from "../src/home/setupcard.js";
84
+ import { setupTools } from "../src/home/setuptools.js";
85
+ import { HOME_TOOL_NAMES } from "../src/home/toolnames.js";
86
+ import { HomeTune, type TunePort } from "../src/home/tune.js";
87
+
88
+ type HomeToolName = (typeof HOME_TOOL_NAMES)[number];
89
+
90
+ export interface HomeOptions {
91
+ adapters: HomeAdapters;
92
+ /** The model the home was launched on, `provider/id`. */
93
+ model: string;
94
+ /** The tool set it was launched with, shown on the login line. */
95
+ tools: HomeToolSet;
96
+ /** Run `fn` every `ms` until the returned stop is called. */
97
+ every?: (ms: number, fn: () => void) => () => void;
98
+ /** Run `fn` once after `ms`; the returned function cancels it. */
99
+ after?: (ms: number, fn: () => void) => () => void;
100
+ /** The setup checklist's ports beyond the home's own. */
101
+ setup?: HomeSetupPorts;
102
+ /** "Tune to this repo": Setup over a checkout (src/home/tune.ts). */
103
+ tune?: TunePort;
104
+ /** Advanced: the wizard's steps beyond switch-on (src/home/advanced.ts). */
105
+ advanced?: AdvancedPort;
106
+ }
107
+
108
+ /** How often the home reads its sources again by itself. */
109
+ export const REFRESH_MS = 60_000;
110
+ const SPIN_MS = 100;
111
+
112
+ const realEvery = (ms: number, fn: () => void): (() => void) => {
113
+ const t = setInterval(fn, ms);
114
+ t.unref?.();
115
+ return () => clearInterval(t);
116
+ };
117
+
118
+ const realAfter = (ms: number, fn: () => void): (() => void) => {
119
+ const t = setTimeout(fn, ms);
120
+ t.unref?.();
121
+ return () => clearTimeout(t);
122
+ };
123
+
124
+ const NO_PARAMS = { type: "object", properties: {}, additionalProperties: false } as const;
125
+ const PR_PARAM = {
126
+ type: "integer",
127
+ minimum: 1,
128
+ description: "The pull request's number.",
129
+ } as const;
130
+
131
+ export function createHome(o: HomeOptions): ExtensionFactory {
132
+ return (pi: ExtensionAPI) => {
133
+ const every = o.every ?? realEvery;
134
+ let tui: TUI | undefined;
135
+ let handle: OverlayHandle | undefined;
136
+ let close: (() => void) | undefined;
137
+ let mounted: "setup" | "home" | undefined;
138
+ let session: ExtensionContext | undefined;
139
+ let frame = 0;
140
+ /** The session's UI, for the switches' questions. */
141
+ let sessionUi: Ask | undefined;
142
+ const askOf = (ctx: ExtensionContext | undefined): Ask | undefined =>
143
+ ctx?.hasUI ? ctx.ui : sessionUi;
144
+ const stops: Array<() => void> = [];
145
+ const redraw = (): void => tui?.requestRender();
146
+ // the session's UI, for the post question; none outside the TUI
147
+ let ui: ExtensionContext["ui"] | undefined;
148
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
149
+ // a read nobody awaits: what went wrong is said, while the session lasts
150
+ const refresh = (): void => {
151
+ model.refresh().catch((err: unknown) => {
152
+ if (session) model.say(`Couldn't read everything again: ${message(err)}`, "warn");
153
+ });
154
+ };
155
+ const model: HomeModel = new HomeModel({
156
+ adapters: o.adapters,
157
+ model: o.model,
158
+ tools: o.tools,
159
+ onChange: redraw,
160
+ after: o.after ?? realAfter,
161
+ ask: async (question, options) => ui?.select(question, options),
162
+ // shown, never a turn of its own: while the agent streams, the message
163
+ // waits for the turn to end instead of steering it
164
+ send: (m) =>
165
+ pi.sendMessage(
166
+ { customType: REVIEW_MESSAGE, content: m.content, display: true, details: m.details },
167
+ { triggerTurn: false },
168
+ ),
169
+ postingRule: () => {
170
+ let all: unknown;
171
+ try {
172
+ all = pi.getSettings();
173
+ } catch {
174
+ all = undefined;
175
+ }
176
+ const read = readQuickReviewSettings(all);
177
+ for (const w of read.warnings) warn(w);
178
+ return read.settings.posting;
179
+ },
180
+ gaps: (): string[] => setup.gaps(),
181
+ onRead: (): Promise<void> => setup.sync(),
182
+ });
183
+ // a word to the person: pi's notice, else the panel's line
184
+ const warn = (text: string): void => {
185
+ if (session?.hasUI) session.ui.notify(text, "warning");
186
+ else model.say(text, "warn");
187
+ };
188
+ const setup: HomeSetup = new HomeSetup({
189
+ model,
190
+ github: o.adapters.github,
191
+ ports: o.setup ?? {},
192
+ deviceLogin: () => deviceLogin(),
193
+ onChange: redraw,
194
+ onPhase: () => {
195
+ if (session?.mode === "tui") mount(session);
196
+ },
197
+ interactive: () => session?.mode === "tui",
198
+ });
199
+ const deviceLogin = () =>
200
+ o.setup?.deviceLogin ?? (session ? piDeviceLogin(session.modelRegistry) : undefined);
201
+ const relogin = new Relogin({
202
+ model,
203
+ owner: o.adapters.loginOwner,
204
+ deviceLogin,
205
+ onChange: redraw,
206
+ });
207
+ const tune = new HomeTune({
208
+ ...(o.tune ? { port: o.tune } : {}),
209
+ repo: () => model.repo(),
210
+ say: (text, kind) => model.say(text, kind),
211
+ after: o.after ?? realAfter,
212
+ onChange: redraw,
213
+ });
214
+ const advanced = new HomeAdvanced({
215
+ ...(o.advanced ? { port: o.advanced } : {}),
216
+ ...(o.adapters.autoReview.control ? { autoReview: o.adapters.autoReview.control } : {}),
217
+ say: (text, kind) => model.say(text, kind),
218
+ refresh: () => model.refresh(),
219
+ selected: () => model.selected,
220
+ });
221
+ // Setup's brief goes to the chat as the person's message, after the
222
+ // agent's turn when one is running
223
+ const tellAgent = (text: string): void => pi.sendUserMessage(text, { deliverAs: "followUp" });
224
+ const advancedAsk = (ctx: ExtensionContext | undefined): AdvancedAsk | undefined =>
225
+ ctx?.hasUI ? ctx.ui : session?.hasUI ? session.ui : undefined;
226
+ const dialogs = (ctx: ExtensionContext | undefined): SetupUi | undefined =>
227
+ ctx?.hasUI
228
+ ? {
229
+ select: (title, options) => ctx.ui.select(title, options),
230
+ input: (title, placeholder) => ctx.ui.input(title, placeholder),
231
+ }
232
+ : undefined;
233
+ const nextStep = (ctx: ExtensionContext | undefined): Promise<string> =>
234
+ setup.next(dialogs(ctx));
235
+
236
+ // The card while setup has a gap, the panel otherwise; a change of phase
237
+ // closes the one and opens the other.
238
+ const mount = (ctx: ExtensionContext): void => {
239
+ const want = setup.phase();
240
+ if (close && mounted === want) return;
241
+ close?.();
242
+ close = undefined;
243
+ handle = undefined;
244
+ mounted = want;
245
+ const focused = (): boolean => handle?.isFocused() ?? false;
246
+ void ctx.ui.custom<void>(
247
+ (t, theme, _keys, done) => {
248
+ tui = t;
249
+ close = () => done();
250
+ return want === "setup"
251
+ ? new SetupCard(theme, {
252
+ setup,
253
+ model,
254
+ focused,
255
+ rows: () => t.terminal.rows,
256
+ unfocus: () => handle?.unfocus(),
257
+ frame: () => frame,
258
+ next: () => void nextStep(session),
259
+ })
260
+ : new HomePanel(theme, {
261
+ model,
262
+ focused,
263
+ rows: () => t.terminal.rows,
264
+ unfocus: () => handle?.unfocus(),
265
+ frame: () => frame,
266
+ ask: () => sessionUi,
267
+ relogin: () => void relogin.start(),
268
+ signingAgain: () => relogin.running() !== null,
269
+ tune: () => void tune.tune(tellAgent),
270
+ advanced: () => void advanced.open(advancedAsk(session)),
271
+ });
272
+ },
273
+ {
274
+ overlay: true,
275
+ overlayOptions:
276
+ want === "setup"
277
+ ? { ...SETUP_OVERLAY }
278
+ : { ...HOME_OVERLAY, margin: { ...HOME_OVERLAY.margin } },
279
+ onHandle: (h) => (handle = h),
280
+ },
281
+ );
282
+ };
283
+
284
+ pi.on("session_start", (_e, ctx) => {
285
+ session = ctx;
286
+ model.setChatModel(ctx.model?.provider);
287
+ ui = ctx.mode === "tui" ? ctx.ui : undefined;
288
+ sessionUi = ctx.hasUI ? ctx.ui : undefined;
289
+ if (ctx.mode === "tui") {
290
+ mount(ctx);
291
+ // the spinner turns only while a read or a setup step is in flight
292
+ stops.push(
293
+ every(SPIN_MS, () => {
294
+ if (!model.busy() && !setup.busy() && relogin.running() === null) return;
295
+ frame++;
296
+ redraw();
297
+ }),
298
+ );
299
+ }
300
+ stops.push(every(REFRESH_MS, refresh));
301
+ refresh();
302
+ });
303
+ pi.on("model_select", (e) => model.setChatModel(e.model.provider));
304
+ // pi's own word on the login: a reply refused for it, or one that came back
305
+ pi.on("message_end", (e) => {
306
+ const m = e.message as { role?: unknown; stopReason?: unknown; errorMessage?: unknown };
307
+ if (m.role !== "assistant") return;
308
+ if (m.stopReason === "error") {
309
+ if (typeof m.errorMessage === "string" && isLoginError(m.errorMessage))
310
+ model.loginFailed(m.errorMessage);
311
+ } else if (m.stopReason !== "aborted") model.chatAnswered();
312
+ });
313
+ pi.on("session_shutdown", () => {
314
+ // the session's timers and its pricing stop; a running review goes on
315
+ model.dispose();
316
+ relogin.close();
317
+ tune.dispose();
318
+ ui = undefined;
319
+ for (const stop of stops.splice(0)) stop();
320
+ setup.close();
321
+ close?.();
322
+ close = undefined;
323
+ mounted = undefined;
324
+ // pi's context for this session is stale now: nothing may reach it
325
+ session = undefined;
326
+ sessionUi = undefined;
327
+ tui = undefined;
328
+ });
329
+
330
+ const key = (
331
+ id: string,
332
+ description: string,
333
+ run: (ctx: ExtensionContext | undefined) => unknown,
334
+ ): void =>
335
+ pi.registerShortcut(id as KeyId, {
336
+ description,
337
+ handler: (ctx) => {
338
+ void new Promise((resolve) => resolve(run(ctx))).catch((err: unknown) => {
339
+ if (session) model.say(message(err), "warn");
340
+ });
341
+ },
342
+ });
343
+ key("alt+j", "quickreview home: next pull request", () => model.move(1));
344
+ key("alt+k", "quickreview home: previous pull request", () => model.move(-1));
345
+ key("alt+o", "quickreview home: open the pull request on GitHub", () => model.openPr());
346
+ key("alt+u", "quickreview home: read everything again", () => refresh());
347
+ key("alt+r", "quickreview home: review the pull request, or show its review", () =>
348
+ model.enter(),
349
+ );
350
+ key("alt+s", "quickreview home: stop the pull request's review", () => model.stopReview());
351
+ key("f2", "quickreview home: focus the panel", () => handle?.focus());
352
+ key("alt+a", "quickreview home: switch auto-review on or off", (ctx) =>
353
+ model.switchAutoReview(askOf(ctx)),
354
+ );
355
+ key("alt+m", "quickreview home: switch memory on or off", (ctx) =>
356
+ model.switchMemory(askOf(ctx)),
357
+ );
358
+ key(RELOGIN_KEY, "quickreview home: sign in again (an expired login)", () => relogin.start());
359
+ key("alt+t", "quickreview home: tune the review to this repo (Setup)", () =>
360
+ tune.tune(tellAgent),
361
+ );
362
+ key(
363
+ "alt+x",
364
+ "quickreview home: advanced (a GitHub App, webhooks, a server, Slack, live)",
365
+ (ctx) => advanced.open(advancedAsk(ctx)),
366
+ );
367
+ pi.registerShortcut("alt+n", {
368
+ description: "quickreview home: run the next setup step",
369
+ handler: (ctx) => {
370
+ void nextStep(ctx);
371
+ },
372
+ });
373
+
374
+ // one definition per HOME_TOOL_NAMES entry: a name without one does not compile
375
+ const tools: { [N in HomeToolName]: QcrTool & { name: N } } = {
376
+ home_status: {
377
+ name: "home_status",
378
+ label: "Home status",
379
+ description:
380
+ "Read what the quickreview home shows: the repository, the logins, auto-review, memory, spend and the open pull requests (To review, Reviewed). A value that could not be read is null.",
381
+ parameters: NO_PARAMS,
382
+ execute: () => textResult(JSON.stringify(model.status(), null, 1)),
383
+ },
384
+ home_refresh: {
385
+ name: "home_refresh",
386
+ label: "Home refresh",
387
+ description:
388
+ "Read the home's sources again: the logins, the repository and its open pull requests.",
389
+ parameters: NO_PARAMS,
390
+ execute: async () => textResult(await model.refresh()),
391
+ },
392
+ home_move: {
393
+ name: "home_move",
394
+ label: "Home move",
395
+ description:
396
+ "Move the home's selection through its pull request lists: 1 is the next row, -1 the previous.",
397
+ parameters: {
398
+ type: "object",
399
+ properties: { by: { type: "integer", description: "Rows to move; negative moves up." } },
400
+ required: ["by"],
401
+ additionalProperties: false,
402
+ },
403
+ execute: (_id, p) => textResult(model.move(Number(p["by"]))),
404
+ },
405
+ home_select: {
406
+ name: "home_select",
407
+ label: "Home select",
408
+ description: "Select one open pull request in the home's lists, by number.",
409
+ parameters: {
410
+ type: "object",
411
+ properties: { pr: PR_PARAM },
412
+ required: ["pr"],
413
+ additionalProperties: false,
414
+ },
415
+ execute: (_id, p) => textResult(model.select(Number(p["pr"]))),
416
+ },
417
+ home_open_pr: {
418
+ name: "home_open_pr",
419
+ label: "Home open PR",
420
+ description:
421
+ "Open a pull request on GitHub in the person's browser: the one given, or the home's selected one.",
422
+ parameters: { type: "object", properties: { pr: PR_PARAM }, additionalProperties: false },
423
+ execute: async (_id, p) =>
424
+ textResult(await model.openPr(typeof p["pr"] === "number" ? p["pr"] : undefined)),
425
+ },
426
+ home_review_pr: {
427
+ name: "home_review_pr",
428
+ label: "Home review PR",
429
+ description:
430
+ "Review a pull request with QuickReview in the background: the one given, or the home's selected one. It previews the price, starts the review and returns at once; the home shows its progress and findings, and nothing is posted to GitHub unless the person's posting rule says so.",
431
+ parameters: { type: "object", properties: { pr: PR_PARAM }, additionalProperties: false },
432
+ execute: async (_id, p) =>
433
+ textResult(await model.review(typeof p["pr"] === "number" ? p["pr"] : undefined)),
434
+ },
435
+ home_show_review: {
436
+ name: "home_show_review",
437
+ label: "Home show review",
438
+ description:
439
+ "Put a reviewed pull request's findings in the chat: the one given, or the home's selected one.",
440
+ parameters: { type: "object", properties: { pr: PR_PARAM }, additionalProperties: false },
441
+ execute: (_id, p) =>
442
+ textResult(model.showReview(typeof p["pr"] === "number" ? p["pr"] : undefined)),
443
+ },
444
+ home_stop_review: {
445
+ name: "home_stop_review",
446
+ label: "Home stop review",
447
+ description:
448
+ "Stop the running QuickReview review of a pull request: the one given, or the home's selected one.",
449
+ parameters: { type: "object", properties: { pr: PR_PARAM }, additionalProperties: false },
450
+ execute: async (_id, p) =>
451
+ textResult(await model.stopReview(typeof p["pr"] === "number" ? p["pr"] : undefined)),
452
+ },
453
+ home_auto_review: {
454
+ name: "home_auto_review",
455
+ label: "Home auto-review",
456
+ description:
457
+ "Switch auto-review on or off. Switching on checks this machine, then asks the person where it runs, whether to turn memory on too, and for their one confirm, and it comes up in dry-run; switching off warns them which review is running, and from dry-run offers going live (a proof round on the selected pull request, then one more confirm). Every question is the person's to answer in the home: this tool cannot answer or confirm for them.",
458
+ parameters: NO_PARAMS,
459
+ execute: async (_id, _p, _signal, _onUpdate, ctx) =>
460
+ textResult(await model.switchAutoReview(askOf(ctx))),
461
+ },
462
+ home_memory: {
463
+ name: "home_memory",
464
+ label: "Home memory",
465
+ description:
466
+ "Switch reviewer memory on or off, on its own: auto-review is not touched. The person confirms in the home, after seeing its RAM cost.",
467
+ parameters: NO_PARAMS,
468
+ execute: async (_id, _p, _signal, _onUpdate, ctx) =>
469
+ textResult(await model.switchMemory(askOf(ctx))),
470
+ },
471
+ home_relogin: {
472
+ name: "home_relogin",
473
+ label: "Home sign in again",
474
+ description:
475
+ "Sign in again with pi's own device-code login, for the provider the chat runs on, when the login has expired or was revoked (the home's login line says \"login expired\"). When auto-review runs on this machine its login refresher is paused during the sign-in and resumes on the new login. Answers with the URL and the code to enter once pi has them; the sign-in finishes in the background when the person approves it.",
476
+ parameters: NO_PARAMS,
477
+ execute: async () => textResult(await relogin.start()),
478
+ },
479
+ ...setupTools(setup),
480
+ ...advancedTools({ tune, advanced, tellAgent, ask: (ctx) => advancedAsk(ctx) }),
481
+ };
482
+ for (const name of HOME_TOOL_NAMES) registerQcrTool(pi, tools[name]);
483
+ };
484
+ }
@@ -190,7 +190,7 @@ export function openLogin(ui: HintUi, open: LoginOpen): "submitted" | "prefilled
190
190
 
191
191
  /** Register the model route and the login hint on `pi`. Exported so a host that
192
192
  * builds its own onboarding extension (over `toolDefinitions`) gets the same
193
- * first-run login as `qcr onboard`. */
193
+ * first-run login as the onboarding wizard. */
194
194
  export function registerLoginRoute(pi: ExtensionAPI, config: LoginRouteConfig): void {
195
195
  if (typeof pi.on !== "function") return;
196
196
  let shown = false;
@@ -1,6 +1,8 @@
1
- // onboarding — the wizard's tool belt. Loaded ONLY by `qcr onboard`
2
- // (per spawn, `-e`, alongside --no-extensions — no ambient extension rides the
3
- // onboarding session), never registered globally. pi jiti-loads this TS source
1
+ // onboarding — the wizard's tool belt. Loaded ONLY by a pi started with it
2
+ // named by path (its env shim, extensions/cli/onboarding.ts, as `-e` alongside
3
+ // --no-extensions, so no ambient extension rides that session), never
4
+ // registered globally. No qcr verb starts such a session any more: the home
5
+ // runs the wizard's steps itself (src/home/switchon.ts, Advanced). pi jiti-loads this TS source
4
6
  // on the HOST, so it imports the OnboardingEngine source directly and runs it
5
7
  // in-process: pi talks, these deterministic tools act, and the LLM never
6
8
  // freestyles a shell command (the built-in belt is read-only by argv).
@@ -11,8 +13,7 @@
11
13
  // - finish_and_exit: re-runs the FULL verification; anything red comes back
12
14
  // as the tool result (no exit, ever, while red); all green marks the
13
15
  // checklist complete, writes the companion's first-run marker
14
- // (state/companion/first-run.json), and triggers pi's deferred shutdown —
15
- // the `qcr onboard` process then opens the TUI.
16
+ // (state/companion/first-run.json), and triggers pi's deferred shutdown.
16
17
  //
17
18
  // Convention notes (same shape as companion-guard/tui-view): degrade, never
18
19
  // wedge — a pi without registerTool registers nothing and says so; a missing
@@ -228,8 +229,8 @@ export function createOnboarding(config: OnboardingConfig): ExtensionFactory {
228
229
  * The onboarding session's first-run login: when pi has no usable model
229
230
  * login, pi's own /login menu opens with a widget that says which logins work
230
231
  * (any OpenAI one), and an OpenAI model whose provider has no login runs on
231
- * the twin login that exists (extensions/login-route.ts). `qcr onboard`
232
- * registers it with the tools; a host that builds its own onboarding
232
+ * the twin login that exists (extensions/login-route.ts). The wizard's
233
+ * session registers it with the tools; a host that builds its own onboarding
233
234
  * extension over `toolDefinitions` + `registerQcrTool` calls it too.
234
235
  */
235
236
  export function registerOnboardingSession(pi: ExtensionAPI): void {
@@ -0,0 +1,65 @@
1
+ // /selfimprove in the app's own pi (`qcr pi`, src/npmverbs.ts): scan this
2
+ // repo, then have this session's agent fix the findings (src/selfimprove.ts).
3
+ // The plugin, loaded beside it, carries /qcr-review, /qcr-scan and /qcr-setup;
4
+ // this command is the app's (ADR 0001). A factory over its config: the env
5
+ // shim extensions/cli/selfimprove.ts reads the process.
6
+ import {
7
+ getAgentDir,
8
+ type ExtensionAPI,
9
+ type ExtensionCommandContext,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import { hostAuth, sessionUi } from "@quickchatai/pi-quickreview/extensions/quickreview.js";
12
+ import type { PluginHost, Runner } from "@quickchatai/pi-quickreview/src/plugin/session.js";
13
+ import { runSelfImproveCommand } from "../src/selfimprove.js";
14
+
15
+ export interface SelfImproveConfig {
16
+ /** The process environment, read once by the shim. */
17
+ env: NodeJS.ProcessEnv;
18
+ /** The user's home. */
19
+ home: string;
20
+ runner: Runner;
21
+ /** Standard output (pi's print mode shows a command's result there). */
22
+ print: (text: string) => void;
23
+ /** Test seams: the running pi's version and its agent dir. */
24
+ piVersion?: string;
25
+ agentDir?: string;
26
+ }
27
+
28
+ /** The extension factory: registers /selfimprove, one run at a time. */
29
+ export function createSelfImprove(config: SelfImproveConfig): (pi: ExtensionAPI) => void {
30
+ return (pi) => {
31
+ // The current session's end: a scan in flight stops with it.
32
+ let shutdown = new AbortController();
33
+ pi.on("session_start", () => {
34
+ if (shutdown.signal.aborted) shutdown = new AbortController();
35
+ });
36
+ pi.on("session_shutdown", () => shutdown.abort());
37
+ let busy = false;
38
+ pi.registerCommand("selfimprove", {
39
+ description: "QuickReview: scan this repo, then have this session's agent fix the findings",
40
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
41
+ if (busy) {
42
+ ctx.ui.notify("/selfimprove is already running in this session", "warning");
43
+ return;
44
+ }
45
+ busy = true;
46
+ try {
47
+ const agentDir = config.agentDir ?? getAgentDir();
48
+ const host: PluginHost = {
49
+ cwd: ctx.cwd,
50
+ env: config.env,
51
+ home: config.home,
52
+ piVersion: config.piVersion ?? "",
53
+ agentDir,
54
+ auth: hostAuth(ctx, agentDir),
55
+ runner: config.runner,
56
+ shutdown: shutdown.signal,
57
+ };
58
+ await runSelfImproveCommand(args.trim(), host, sessionUi(pi, ctx, config.print));
59
+ } finally {
60
+ busy = false;
61
+ }
62
+ },
63
+ });
64
+ };
65
+ }