rovecode 0.4.0-beta.3 → 0.4.0

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 (432) hide show
  1. package/README.md +57 -72
  2. package/THIRD_PARTY_NOTICES.md +0 -44
  3. package/bin/rovecode.ts +21 -0
  4. package/package.json +16 -38
  5. package/src/account/keys.ts +97 -0
  6. package/src/account/login.ts +158 -0
  7. package/src/account/provision.ts +47 -0
  8. package/src/account/store.ts +63 -0
  9. package/src/acp/server.ts +373 -0
  10. package/src/cli/account-cmd.ts +116 -0
  11. package/src/cli/connect.ts +244 -0
  12. package/src/cli/context-cmd.ts +199 -0
  13. package/src/cli/dispatch.ts +109 -0
  14. package/src/cli/doctor.ts +324 -0
  15. package/src/cli/export.ts +278 -0
  16. package/src/cli/help.ts +240 -0
  17. package/src/cli/is-tui-invocation.ts +8 -0
  18. package/src/cli/main.ts +599 -0
  19. package/src/cli/market-cmd.ts +658 -0
  20. package/src/cli/mcp-market-cmd.ts +299 -0
  21. package/src/cli/output.ts +382 -0
  22. package/src/cli/repl.ts +172 -0
  23. package/src/cli/resume.ts +32 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +792 -0
  26. package/src/cli/setup.ts +187 -0
  27. package/src/cli/update-cmd.ts +78 -0
  28. package/src/cli/workflow-cmd.ts +100 -0
  29. package/src/coding/checkpoints.ts +270 -0
  30. package/src/coding/diff.ts +136 -0
  31. package/src/coding/files.ts +339 -0
  32. package/src/coding/hashline.ts +319 -0
  33. package/src/coding/lsp.ts +406 -0
  34. package/src/coding/repomap-cache.ts +99 -0
  35. package/src/coding/repomap-files.ts +110 -0
  36. package/src/coding/repomap.ts +392 -0
  37. package/src/core/compaction.ts +399 -0
  38. package/src/core/config.ts +289 -0
  39. package/src/core/context-report.ts +228 -0
  40. package/src/core/context.ts +60 -0
  41. package/src/core/count-remote.ts +107 -0
  42. package/src/core/execpolicy-rules.ts +196 -0
  43. package/src/core/execpolicy.ts +385 -0
  44. package/src/core/executor.ts +397 -0
  45. package/src/core/guardrails.ts +400 -0
  46. package/src/core/hooks.ts +398 -0
  47. package/src/core/images.ts +230 -0
  48. package/src/core/intro.ts +236 -0
  49. package/src/core/loop.ts +621 -0
  50. package/src/core/modes.ts +372 -0
  51. package/src/core/orchestrator.ts +207 -0
  52. package/src/core/reflection.ts +165 -0
  53. package/src/core/sandbox-config.ts +167 -0
  54. package/src/core/session-images.ts +73 -0
  55. package/src/core/session.ts +398 -0
  56. package/src/core/settings.ts +98 -0
  57. package/src/core/stuck-detector.ts +273 -0
  58. package/src/core/tasks.ts +374 -0
  59. package/src/core/token-scale.ts +108 -0
  60. package/src/core/tool-output-budget.ts +166 -0
  61. package/src/core/tools.ts +288 -0
  62. package/src/core/types.ts +330 -0
  63. package/src/core/update-check.ts +171 -0
  64. package/src/core/update.ts +158 -0
  65. package/src/core/usage.ts +204 -0
  66. package/src/core/validate.ts +121 -0
  67. package/src/core/verify-gate.ts +159 -0
  68. package/src/core/verify.ts +237 -0
  69. package/src/core/voice.ts +158 -0
  70. package/src/core/win-job.ts +183 -0
  71. package/src/design/audit.ts +797 -0
  72. package/src/design/direction.ts +190 -0
  73. package/src/design/rules.ts +157 -0
  74. package/src/eval/bench.ts +150 -0
  75. package/src/eval/gauntlet-runner.ts +218 -0
  76. package/src/eval/gauntlet.ts +226 -0
  77. package/src/eval/grader.ts +186 -0
  78. package/src/eval/record.ts +202 -0
  79. package/src/eval/redact.ts +141 -0
  80. package/src/eval/replay.ts +147 -0
  81. package/src/eval/trajectory.ts +373 -0
  82. package/src/index.ts +17 -0
  83. package/src/market/catalogs/mcp-docs.json +111 -0
  84. package/src/market/catalogs/plugins.json +111 -0
  85. package/src/market/catalogs/skills.json +478 -0
  86. package/src/market/clone.ts +72 -0
  87. package/src/market/context-cost.ts +121 -0
  88. package/src/market/digest.ts +106 -0
  89. package/src/market/index.ts +22 -0
  90. package/src/market/install.ts +578 -0
  91. package/src/market/manifest.ts +187 -0
  92. package/src/market/prereq.ts +145 -0
  93. package/src/market/registry.ts +363 -0
  94. package/src/market/resolve.ts +111 -0
  95. package/src/market/types.ts +236 -0
  96. package/src/market/validate.ts +227 -0
  97. package/src/mcp/client.ts +431 -0
  98. package/src/mcp/config.ts +239 -0
  99. package/src/mcp/local-package.ts +211 -0
  100. package/src/mcp/market-catalog.ts +84 -0
  101. package/src/mcp/market-install.ts +289 -0
  102. package/src/mcp/market.ts +0 -0
  103. package/src/mcp/tools.ts +131 -0
  104. package/src/mcp/trust.ts +49 -0
  105. package/src/memory/blocks.ts +175 -0
  106. package/src/memory/recall.ts +355 -0
  107. package/src/memory/store.ts +105 -0
  108. package/src/memory/tools.ts +99 -0
  109. package/src/plugins/cli.ts +123 -0
  110. package/src/plugins/discover.ts +108 -0
  111. package/src/plugins/index.ts +50 -0
  112. package/src/plugins/init.ts +140 -0
  113. package/src/plugins/install.ts +184 -0
  114. package/src/plugins/load.ts +149 -0
  115. package/src/plugins/manifest.ts +106 -0
  116. package/src/plugins/state.ts +83 -0
  117. package/src/providers/auth.ts +293 -0
  118. package/src/providers/cache.ts +223 -0
  119. package/src/providers/catalog-local.ts +160 -0
  120. package/src/providers/catalog.ts +408 -0
  121. package/src/providers/middleware-context.ts +86 -0
  122. package/src/providers/middleware.ts +373 -0
  123. package/src/providers/profile-glm53.ts +111 -0
  124. package/src/providers/profile-sonnet5-persona.ts +65 -0
  125. package/src/providers/profile-sonnet5-voice.ts +23 -0
  126. package/src/providers/profiles.ts +156 -0
  127. package/src/providers/provider-config.ts +311 -0
  128. package/src/providers/registry.ts +302 -0
  129. package/src/providers/response-validation.ts +80 -0
  130. package/src/providers/retry.ts +234 -0
  131. package/src/providers/router.ts +294 -0
  132. package/src/providers/sse.ts +26 -0
  133. package/src/providers/stream-errors.ts +117 -0
  134. package/src/providers/stream.ts +569 -0
  135. package/src/providers/thinking.ts +189 -0
  136. package/src/providers/wire-messages.ts +129 -0
  137. package/src/sdk/client.ts +225 -0
  138. package/src/sdk/index.ts +3 -0
  139. package/src/server/dashboard.ts +144 -0
  140. package/src/server/http.ts +343 -0
  141. package/src/server/openapi.ts +246 -0
  142. package/src/sextant/card-hits.ts +102 -0
  143. package/src/sextant/card-keys.ts +55 -0
  144. package/src/sextant/context-source.ts +157 -0
  145. package/src/sextant/draw-agents.ts +273 -0
  146. package/src/sextant/draw-code.ts +388 -0
  147. package/src/sextant/draw-context.ts +222 -0
  148. package/src/sextant/draw-frame.ts +164 -0
  149. package/src/sextant/draw-market.ts +573 -0
  150. package/src/sextant/draw-messages.ts +386 -0
  151. package/src/sextant/draw-pet.ts +230 -0
  152. package/src/sextant/draw-plan.ts +159 -0
  153. package/src/sextant/draw-tabs.ts +85 -0
  154. package/src/sextant/draw-util.ts +65 -0
  155. package/src/sextant/engine.ts +230 -0
  156. package/src/sextant/frame-hits.ts +25 -0
  157. package/src/sextant/frame.ts +101 -0
  158. package/src/sextant/git-status.ts +197 -0
  159. package/src/sextant/grid.ts +59 -0
  160. package/src/sextant/input.ts +119 -0
  161. package/src/sextant/keys.ts +488 -0
  162. package/src/sextant/layout.ts +86 -0
  163. package/src/sextant/local-commands.ts +156 -0
  164. package/src/sextant/market-source.ts +287 -0
  165. package/src/sextant/mentions.ts +141 -0
  166. package/src/sextant/message-hits.ts +26 -0
  167. package/src/sextant/model.ts +387 -0
  168. package/src/sextant/overlays.ts +451 -0
  169. package/src/sextant/panel-hits.ts +38 -0
  170. package/src/sextant/pet.ts +399 -0
  171. package/src/sextant/screen.ts +324 -0
  172. package/src/sextant/scroll-hits.ts +66 -0
  173. package/src/sextant/scrollbar.ts +82 -0
  174. package/src/sextant/selection.ts +123 -0
  175. package/src/sextant/sextant-bridge.ts +174 -0
  176. package/src/sextant/sextant-cards.ts +142 -0
  177. package/src/sextant/sextant-diff-base.ts +63 -0
  178. package/src/sextant/sextant-files.ts +154 -0
  179. package/src/sextant/sextant-frame-loop.ts +314 -0
  180. package/src/sextant/sextant-renderer.ts +478 -0
  181. package/src/sextant/sextant-repo.ts +131 -0
  182. package/src/sextant/theme.ts +66 -0
  183. package/src/sextant/tool-rows.ts +189 -0
  184. package/src/sextant/types.ts +473 -0
  185. package/src/skills/index.ts +306 -0
  186. package/src/skills/tools.ts +69 -0
  187. package/src/skills/versioned.ts +227 -0
  188. package/src/telemetry/otel.ts +353 -0
  189. package/src/telemetry/otlp.ts +68 -0
  190. package/src/tools/ask-user.ts +156 -0
  191. package/src/tools/design.ts +151 -0
  192. package/src/tools/evalcell.ts +338 -0
  193. package/src/tools/html-text.ts +139 -0
  194. package/src/tools/provider.ts +149 -0
  195. package/src/tools/task.ts +216 -0
  196. package/src/tools/todo.ts +320 -0
  197. package/src/tools/webfetch.ts +331 -0
  198. package/src/tui/app.ts +608 -0
  199. package/src/tui/attach.ts +127 -0
  200. package/src/tui/checkpoints-cmd.ts +70 -0
  201. package/src/tui/clipboard-image.ts +81 -0
  202. package/src/tui/commands.ts +277 -0
  203. package/src/tui/cost.ts +108 -0
  204. package/src/tui/info-cmd.ts +144 -0
  205. package/src/tui/mcp-cmd.ts +128 -0
  206. package/src/tui/modes-cmd.ts +45 -0
  207. package/src/tui/overlays.ts +97 -0
  208. package/src/tui/pi-renderer.ts +424 -0
  209. package/src/tui/providers-cmd.ts +366 -0
  210. package/src/tui/renderer.ts +101 -0
  211. package/src/tui/replay-marker.ts +29 -0
  212. package/src/tui/session-cmd.ts +146 -0
  213. package/src/tui/sextant-attach.ts +68 -0
  214. package/src/tui/sextant-io.ts +184 -0
  215. package/src/tui/sextant-smoke.ts +110 -0
  216. package/src/tui/smoke.ts +72 -0
  217. package/src/tui/theme.ts +59 -0
  218. package/src/tui/todo-label.ts +7 -0
  219. package/src/workflow/engine.ts +266 -0
  220. package/tsconfig.json +30 -0
  221. package/vendor/pi-tui/LICENSE +21 -0
  222. package/vendor/pi-tui/PATCHES.md +12 -0
  223. package/vendor/pi-tui/PROVENANCE.md +12 -0
  224. package/vendor/pi-tui/README.upstream.md +854 -0
  225. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  226. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  227. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  228. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  229. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  230. package/vendor/pi-tui/src/components/box.ts +138 -0
  231. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  232. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  233. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  234. package/vendor/pi-tui/src/components/image.ts +128 -0
  235. package/vendor/pi-tui/src/components/input.ts +448 -0
  236. package/vendor/pi-tui/src/components/loader.ts +93 -0
  237. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  238. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  239. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  240. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  241. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  242. package/vendor/pi-tui/src/components/stack.ts +155 -0
  243. package/vendor/pi-tui/src/components/text.ts +108 -0
  244. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  245. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  246. package/vendor/pi-tui/src/editor-component.ts +75 -0
  247. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  248. package/vendor/pi-tui/src/index.ts +149 -0
  249. package/vendor/pi-tui/src/keybindings.ts +321 -0
  250. package/vendor/pi-tui/src/keys.ts +1402 -0
  251. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  252. package/vendor/pi-tui/src/latex.ts +1381 -0
  253. package/vendor/pi-tui/src/layout-node.ts +52 -0
  254. package/vendor/pi-tui/src/layout.ts +411 -0
  255. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  256. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  257. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  258. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  259. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  260. package/vendor/pi-tui/src/terminal.ts +554 -0
  261. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  262. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  263. package/vendor/pi-tui/src/tui.ts +1264 -0
  264. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  265. package/vendor/pi-tui/src/utils.ts +1327 -0
  266. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  267. package/vendor/pi-tui/test/test-themes.ts +39 -0
  268. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
  269. package/CHANGELOG.md +0 -527
  270. package/bin/rovecode.js +0 -24
  271. package/dist/cli/app-j6gn14w3.js +0 -2
  272. package/dist/cli/ask-user-cwstt8fz.js +0 -2
  273. package/dist/cli/auth-login-9bbp9915.js +0 -2
  274. package/dist/cli/auth-m8p9grty.js +0 -2
  275. package/dist/cli/bench-16zqdms5.js +0 -9
  276. package/dist/cli/catalog-1xchffa4.js +0 -2
  277. package/dist/cli/cli-1n1zb64f.js +0 -2
  278. package/dist/cli/client-2t9gjkck.js +0 -2
  279. package/dist/cli/commands-exafvm2b.js +0 -2
  280. package/dist/cli/connect-6zde0kn3.js +0 -2
  281. package/dist/cli/context-cmd-5t43wgqt.js +0 -2
  282. package/dist/cli/context-report-kt01pw8y.js +0 -2
  283. package/dist/cli/count-remote-ap7x3vh6.js +0 -2
  284. package/dist/cli/design-ne5zszyh.js +0 -2
  285. package/dist/cli/dispatch-2r5myxye.js +0 -2
  286. package/dist/cli/doctor-ws4fh4tn.js +0 -3
  287. package/dist/cli/executor-bdrjn634.js +0 -2
  288. package/dist/cli/export-1mxb9g5p.js +0 -2
  289. package/dist/cli/files-g104xghh.js +0 -2
  290. package/dist/cli/gauntlet-07xrjpj7.js +0 -2
  291. package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
  292. package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
  293. package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
  294. package/dist/cli/hashline-znvrat11.js +0 -2
  295. package/dist/cli/http-xafw6fsh.js +0 -143
  296. package/dist/cli/index-1sgjm25y.js +0 -2
  297. package/dist/cli/init-g2m0tn4m.js +0 -51
  298. package/dist/cli/install-avaqjjqq.js +0 -2
  299. package/dist/cli/loop-mmpfft01.js +0 -2
  300. package/dist/cli/main-0904f6ps.js +0 -5
  301. package/dist/cli/main-0ab9fc26.js +0 -9
  302. package/dist/cli/main-0jys2ccn.js +0 -3
  303. package/dist/cli/main-0mtcdbs7.js +0 -3
  304. package/dist/cli/main-0z1w2zsg.js +0 -3
  305. package/dist/cli/main-1dchs7xv.js +0 -18
  306. package/dist/cli/main-1ereejm1.js +0 -3
  307. package/dist/cli/main-1k1kw6b5.js +0 -3
  308. package/dist/cli/main-27y4sm2k.js +0 -38
  309. package/dist/cli/main-2wwjex5j.js +0 -58
  310. package/dist/cli/main-2yeveeve.js +0 -6
  311. package/dist/cli/main-2yfck9b5.js +0 -3
  312. package/dist/cli/main-2zmzgkwh.js +0 -3
  313. package/dist/cli/main-351pz3z7.js +0 -7
  314. package/dist/cli/main-3gjqfh7a.js +0 -6
  315. package/dist/cli/main-3nf3kgve.js +0 -3
  316. package/dist/cli/main-3pjrb2hd.js +0 -3
  317. package/dist/cli/main-3rxcvgna.js +0 -19
  318. package/dist/cli/main-4b3jgy66.js +0 -19
  319. package/dist/cli/main-4wndhjdc.js +0 -7
  320. package/dist/cli/main-4xcmvxnk.js +0 -3
  321. package/dist/cli/main-5tbz0wbz.js +0 -4
  322. package/dist/cli/main-5ywnwthm.js +0 -3
  323. package/dist/cli/main-6b62vkz0.js +0 -14
  324. package/dist/cli/main-6dnk69vp.js +0 -3
  325. package/dist/cli/main-6genrmhs.js +0 -136
  326. package/dist/cli/main-73g7eff4.js +0 -15
  327. package/dist/cli/main-7c5thhjd.js +0 -5
  328. package/dist/cli/main-7rn6bqje.js +0 -3
  329. package/dist/cli/main-80haw7qk.js +0 -4
  330. package/dist/cli/main-875s60s2.js +0 -4
  331. package/dist/cli/main-8kjxbpw4.js +0 -8
  332. package/dist/cli/main-90ds1z4e.js +0 -10
  333. package/dist/cli/main-9etavkew.js +0 -3
  334. package/dist/cli/main-a9njrkk1.js +0 -3
  335. package/dist/cli/main-aecrjq2d.js +0 -12
  336. package/dist/cli/main-ck9asesq.js +0 -9
  337. package/dist/cli/main-cta9racd.js +0 -4
  338. package/dist/cli/main-ddv7j2ag.js +0 -3
  339. package/dist/cli/main-dfreez27.js +0 -10
  340. package/dist/cli/main-f7rw7des.js +0 -3
  341. package/dist/cli/main-ggcn7rd7.js +0 -5
  342. package/dist/cli/main-gzkmycnv.js +0 -3
  343. package/dist/cli/main-hq51jg8v.js +0 -18
  344. package/dist/cli/main-jft389w9.js +0 -8
  345. package/dist/cli/main-k1eqkg83.js +0 -3
  346. package/dist/cli/main-k2y8a2aw.js +0 -9
  347. package/dist/cli/main-kcpbykxz.js +0 -4
  348. package/dist/cli/main-kd488vje.js +0 -22
  349. package/dist/cli/main-kh32yvgk.js +0 -5
  350. package/dist/cli/main-kqxnqjnv.js +0 -25
  351. package/dist/cli/main-kyn0xnsg.js +0 -3
  352. package/dist/cli/main-m1kk6fp5.js +0 -21
  353. package/dist/cli/main-mv40pcr2.js +0 -4
  354. package/dist/cli/main-n0t3973w.js +0 -3
  355. package/dist/cli/main-nqveez48.js +0 -4
  356. package/dist/cli/main-pknhvrmj.js +0 -3
  357. package/dist/cli/main-pn1w7a7j.js +0 -3
  358. package/dist/cli/main-prxxs70n.js +0 -4
  359. package/dist/cli/main-q3vsesf9.js +0 -3
  360. package/dist/cli/main-qsevpgsv.js +0 -3
  361. package/dist/cli/main-rdgdw24b.js +0 -25
  362. package/dist/cli/main-rfth4tbm.js +0 -16
  363. package/dist/cli/main-rg0wn0xf.js +0 -5
  364. package/dist/cli/main-sdmxhtv8.js +0 -4
  365. package/dist/cli/main-skbp13js.js +0 -18
  366. package/dist/cli/main-t4xnd213.js +0 -7
  367. package/dist/cli/main-vqak588n.js +0 -4
  368. package/dist/cli/main-w2n1303f.js +0 -9
  369. package/dist/cli/main-wbrdspr2.js +0 -5
  370. package/dist/cli/main-wsrg79c1.js +0 -7
  371. package/dist/cli/main-x4r0fne4.js +0 -5
  372. package/dist/cli/main-xea2f3tn.js +0 -6
  373. package/dist/cli/main-xg704a3c.js +0 -3
  374. package/dist/cli/main-xvnrabfp.js +0 -16
  375. package/dist/cli/main-xy53xf0r.js +0 -4
  376. package/dist/cli/main-y1fqy60y.js +0 -3
  377. package/dist/cli/main-yn8cd281.js +0 -34
  378. package/dist/cli/main-yr0ksc0h.js +0 -4
  379. package/dist/cli/main-z2ex2vyf.js +0 -4
  380. package/dist/cli/main-z3aayzvq.js +0 -3
  381. package/dist/cli/main-zaqh35jg.js +0 -3
  382. package/dist/cli/main-zc2e8e46.js +0 -4
  383. package/dist/cli/main-zzrfw6cf.js +0 -13
  384. package/dist/cli/main.js +0 -280
  385. package/dist/cli/market-cmd-e14kmx9n.js +0 -5
  386. package/dist/cli/mcp-login-wq7ktdek.js +0 -2
  387. package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
  388. package/dist/cli/notify-b7qc0cjb.js +0 -2
  389. package/dist/cli/oauth-z8whcgfx.js +0 -2
  390. package/dist/cli/output-b3ewj3ps.js +0 -16
  391. package/dist/cli/profiles-6mr5he5e.js +0 -2
  392. package/dist/cli/provider-config-g7j42q8x.js +0 -2
  393. package/dist/cli/provider-jr1y8vvm.js +0 -2
  394. package/dist/cli/registry-s8yk86g0.js +0 -2
  395. package/dist/cli/registry-t6p8d4mn.js +0 -2
  396. package/dist/cli/repl-bajwe1mh.js +0 -11
  397. package/dist/cli/resume-rwn9nz7y.js +0 -2
  398. package/dist/cli/run-flags-nah7ndpt.js +0 -2
  399. package/dist/cli/runtime-n7gafzhb.js +0 -2
  400. package/dist/cli/sandbox-config-emdy18x4.js +0 -2
  401. package/dist/cli/server-b0nvs2bn.js +0 -5
  402. package/dist/cli/session-arg-y75wd4kj.js +0 -2
  403. package/dist/cli/session-j62evmjq.js +0 -2
  404. package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
  405. package/dist/cli/settings-df10wfez.js +0 -2
  406. package/dist/cli/setup-jzvv72fg.js +0 -2
  407. package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
  408. package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
  409. package/dist/cli/smoke-p7748apt.js +0 -8
  410. package/dist/cli/start-chat-s4st3mm0.js +0 -12
  411. package/dist/cli/stream-gmeyewds.js +0 -2
  412. package/dist/cli/task-gh0kkp3n.js +0 -2
  413. package/dist/cli/tasks-z1kfpe8e.js +0 -2
  414. package/dist/cli/thinking-0eqkrz6t.js +0 -2
  415. package/dist/cli/todo-5brcrt9m.js +0 -2
  416. package/dist/cli/tools-7pzm0vj9.js +0 -2
  417. package/dist/cli/tools-s635p6s8.js +0 -2
  418. package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
  419. package/dist/cli/update-check-pt31bm2f.js +0 -2
  420. package/dist/cli/update-cmd-tk131s9t.js +0 -2
  421. package/dist/cli/voice-56nabd8d.js +0 -2
  422. package/dist/cli/webfetch-xd8q596m.js +0 -2
  423. package/dist/cli/websearch-5hkf98k1.js +0 -2
  424. package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
  425. package/dist/cli/workspace-q10g5z3e.js +0 -2
  426. package/dist/lib/index.js +0 -62
  427. package/dist/lib/models-index.json +0 -1
  428. package/dist/lib/plugins.js +0 -55
  429. package/dist/lib/providers.js +0 -17
  430. package/dist/lib/public-api.js +0 -20
  431. package/dist/lib/sdk.js +0 -360
  432. /package/{dist/cli → src/providers}/models-index.json +0 -0
@@ -0,0 +1,478 @@
1
+ /** SextantRenderer (port #44): the sextant surface as rovecode's `Renderer` — the ONE controller stays
2
+ * runTui (ADR-003, no second loop); this class maps the Renderer seam plus the optional onEvent/attach
3
+ * members onto the SextantState (#41 model), paints through FrameLoop (#40 screen, #41-#46 painters)
4
+ * and routes keys through #43. Ported from the user's sextant v0.4.0 app.js:1599-1632 (boot: enter
5
+ * the alt screen, state, tick, input, resize, leave).
6
+ *
7
+ * ONE source of truth for transcript rows while a run is busy: the RunEvent stream (onEvent →
8
+ * applyEvent). The per-event Renderer calls app.ts makes for the same events are therefore no-ops
9
+ * while busy (beginAssistant views, toolStart/toolUpdate/toolEnd) or dropped by exact count (the
10
+ * compaction / "↪ steering applied" / run_end notes — sextant-bridge duplicateNotes); every other
11
+ * note (boot, slash commands, task settlements, "queued as steering", router notes) is a row + toast.
12
+ * When NOT busy (history replay after /resume, /rewind, /new) the same methods build rows directly.
13
+ * Renderer-local slash commands (/theme /open /diff /focus /agents, the /help card) run in keys.ts
14
+ * runLocal through the `local` hooks below — src/sextant/local-commands.ts is their future home.
15
+ * I/O: the terminal through TerminalIO; git/fs through sextant-repo.ts (RepoWatcher: scheduled OFF
16
+ * the frame loop from the tick and run async, so a slow `git status` never freezes a frame). */
17
+
18
+ import { homedir } from "node:os";
19
+ import { basename, join } from "node:path";
20
+ import pkg from "../../package.json";
21
+ import type { RunEvent } from "../core/types.ts";
22
+ import { loadTodos } from "../tools/todo.ts";
23
+ import { loadSettings } from "../core/settings.ts";
24
+ import type { ApprovalAnswer, AssistantView, PickItem, QuestionAnswer, QuestionPrompt, Renderer, RendererHooks, SlashCommand, StatusInfo } from "../tui/renderer.ts";
25
+ import { drawAgents } from "./draw-agents.ts";
26
+ import { openMarket } from "./draw-market.ts";
27
+ import { docsFor, install, loadMarket, planFor } from "./market-source.ts";
28
+ import { openContext } from "./draw-context.ts";
29
+ import { fixedFrom, loadContext } from "./context-source.ts";
30
+ import { hunksFromUnified, setAgentsPainter } from "./draw-code.ts";
31
+ import { fuzzy } from "./engine.ts";
32
+ import { spawnGitAsync, toAsync, type GitRunner, type GitRunnerAsync } from "./git-status.ts";
33
+ import { enterSequence, leaveSequence } from "./input.ts";
34
+ import { ESC_WINDOW_MS, type KeyCtx } from "./keys.ts";
35
+ import { initialState, makeApplyEvent, planCounts, pushToast, setCrew, setPlan, setUsage } from "./model.ts";
36
+ import { suggestions } from "./overlays.ts";
37
+ import { createPet, type Pet } from "./pet.ts";
38
+ import { argsFromPreview, duplicateNotes, petOnEvent, petOnTask, replayToolRow, userRow, type LiveCall } from "./sextant-bridge.ts";
39
+ import { CardHost } from "./sextant-cards.ts";
40
+ import { editOpsOf, type HashlineOp } from "./sextant-diff-base.ts";
41
+ import { readFileBounded } from "./sextant-files.ts";
42
+ import { FrameLoop } from "./sextant-frame-loop.ts";
43
+ import { RepoWatcher } from "./sextant-repo.ts";
44
+ import { buildTheme, isThemeName } from "./theme.ts";
45
+ import { clipText, relPath, summarizeEnd } from "./tool-rows.ts";
46
+ import type { ApplyEvent, InputEvent, SextantAttach, SextantState, TerminalIO, Theme, ThemeName, ToolRow } from "./types.ts";
47
+
48
+ export { IDLE_SCAN_MS } from "./sextant-repo.ts";
49
+
50
+ export interface SextantRendererOptions {
51
+ io: TerminalIO;
52
+ /** the frame clock (Date.now in production; tests inject a controllable one) */
53
+ clock?: () => number;
54
+ /** starting palette (ROVECODE_THEME / --theme); an unknown name falls back to night */
55
+ theme?: string;
56
+ /** the pet's name (`--pet <name>`); attach() may override it */
57
+ pet?: string;
58
+ /** emit truecolor SGR (default true; false quantizes to xterm-256) */
59
+ truecolor?: boolean;
60
+ /** the project dir until attach() supplies the runtime's cwd */
61
+ cwd?: string;
62
+ /** git seam for tests (a sync fake is wrapped); undefined = spawn git asynchronously */
63
+ git?: GitRunner | GitRunnerAsync;
64
+ /** false disables the repo scan entirely (pure tests) */
65
+ scan?: boolean;
66
+ /** the terminal bell on run end / a card that needs you; default = settings.json `bell` (true when unset) */
67
+ bell?: boolean;
68
+ }
69
+
70
+ const TOAST_MAX = 48;
71
+ const NOOP_VIEW: AssistantView = { append() {}, done() {} };
72
+ const NO_HOOKS: RendererHooks = { onSubmit() {}, onInterrupt() {}, onExit() {} };
73
+
74
+ export class SextantRenderer implements Renderer {
75
+ /** the surface state (keys.ts and the reducer mutate it; tests read it) */
76
+ readonly state: SextantState;
77
+ private theme: Theme;
78
+ private readonly io: TerminalIO;
79
+ private readonly clock: () => number;
80
+ private readonly pet: Pet;
81
+ private readonly loop: FrameLoop;
82
+ /** the git/fs work, off the loop and async (sextant-repo.ts) */
83
+ private readonly repo: RepoWatcher;
84
+ private readonly applyEv: ApplyEvent;
85
+ /** SGR mode the Screen emits (false = the xterm-256 quantizer; tests/pickRenderer read it) */
86
+ readonly truecolor: boolean;
87
+ private hooks: RendererHooks | null = null;
88
+ private ctx: SextantAttach | null = null;
89
+ private unsubTasks: (() => void) | null = null;
90
+ private started = false;
91
+ private stopped = false;
92
+ /** between setBusy(true) and setBusy(false): the event stream owns the rows */
93
+ private busy = false;
94
+ /** ring BEL when a run ends or a card opens — the one signal a human who tabbed away gets; settings.json `bell: false` silences it */
95
+ private readonly bell: boolean;
96
+ private dropNotes = 0;
97
+ /** the approval / question / picker promises (sextant-cards.ts) */
98
+ private readonly cards: CardHost;
99
+ private readonly liveCalls = new Map<string, LiveCall>();
100
+ /** per cwd-relative path: the content when its edit/write was APPROVED (null = did not exist) — the
101
+ * pre-edit diff base. Captured at askApproval (the one moment before the tool runs), NOT at
102
+ * tool_execution_start: that event reaches the renderer through the loop's buffered pump after
103
+ * rovecode's synchronous edit/write already wrote, so a snapshot then is the post-edit file. Ungated
104
+ * calls (yolo / always) rebuild their base from the hashline ops instead (sextant-diff-base.ts). */
105
+ private readonly before = new Map<string, string | null>();
106
+ /** per live edit callId: its hashline ops (the base of an ungated edit is rebuilt from them at the end) */
107
+ private readonly editOps = new Map<string, HashlineOp[]>();
108
+ /** the file whose content the code panel holds (null → reload on the next tick) */
109
+ private loadedFile: string | null = null;
110
+ private planLoaded = false;
111
+ private todoDone = 0;
112
+
113
+ constructor(o: SextantRendererOptions) {
114
+ this.io = o.io; this.clock = o.clock ?? Date.now; this.truecolor = o.truecolor ?? true;
115
+ const themeName: ThemeName = isThemeName(o.theme ?? "") ? (o.theme as ThemeName) : "night";
116
+ this.theme = buildTheme(themeName);
117
+ const cwd = o.cwd ?? process.cwd();
118
+ this.bell = o.bell ?? loadSettings(cwd).bell ?? true;
119
+ this.state = initialState({ cwd, repo: { name: basename(cwd) || cwd, branch: null }, version: pkg.version, theme: themeName, mode: "act", yolo: false, commands: [], now: this.clock() });
120
+ this.pet = createPet({ name: o.pet });
121
+ // the reducer's diffFor seam stays empty: the hunks arrive through the watcher's scheduleDiff (a
122
+ // setTimeout(0) from the end event, then async git — nothing runs inside the event dispatch or a painter)
123
+ this.applyEv = makeApplyEvent();
124
+ this.repo = new RepoWatcher({ state: this.state, clock: this.clock, git: o.git ? toAsync(o.git) : spawnGitAsync, dirty: () => this.loop.markDirty() }, o.scan ?? true);
125
+ this.cards = new CardHost({
126
+ state: this.state, dirty: () => this.loop.markDirty(), stopped: () => this.stopped,
127
+ onApprovalOpen: () => this.pet.event("permission", undefined, this.clock()),
128
+ onAllowed: () => this.pet.event("allowed", undefined, this.clock()),
129
+ });
130
+ setAgentsPainter(drawAgents); // #46: the crew board paints the ∷ mode
131
+ this.loop = new FrameLoop({
132
+ io: this.io, clock: this.clock, state: this.state, theme: () => this.theme, pet: this.pet, truecolor: this.truecolor,
133
+ keyCtx: () => ({ hooks: this.hooks ?? NO_HOOKS, local: this.local }),
134
+ beforeInput: (ev, now) => this.beforeInput(ev, now),
135
+ afterInput: (ev, now) => this.afterInput(ev, now),
136
+ onTick: (now) => this.onTick(now),
137
+ });
138
+ }
139
+
140
+ // ------------------------------------------------------------------ test/smoke seams
141
+ /** the last painted frame as text ("" before start) */
142
+ frameText(): string { return this.loop.frameText(); }
143
+ /** the frame interval is live */
144
+ get active(): boolean { return this.loop.active; }
145
+ /** frames painted so far (tests: "nothing paints after stop") */
146
+ get frames(): number { return this.loop.frames; }
147
+ get themeName(): ThemeName { return this.theme.name; }
148
+ /** run one frame-loop tick now (tests: deterministic instead of waiting 40 ms) */
149
+ tick(): void { this.loop.tick(); }
150
+ /** parse input held by the ESC-hold timer now (tests) */
151
+ flushInput(): void { this.loop.flushInput(); }
152
+
153
+ // ------------------------------------------------------------------ lifecycle
154
+ start(hooks: RendererHooks): void {
155
+ if (this.started) return;
156
+ this.started = true; this.hooks = hooks;
157
+ this.io.enterRaw();
158
+ this.io.write(enterSequence(true));
159
+ this.state.bootAt = this.clock();
160
+ this.loop.start();
161
+ }
162
+
163
+ /** clear the interval, settle every open card/picker (deny / null), restore the terminal */
164
+ /** settled once stop() has run AND the repo watcher's git children are gone (sextant-repo.ts stop) */
165
+ private drained: Promise<void> | null = null;
166
+ /** wait for the git children a stop() killed to be gone — call after stop(); a caller that removes
167
+ * the cwd (a test's scratch repo) must await this first, or Windows answers EBUSY */
168
+ drain(): Promise<void> { return this.drained ?? this.repo.stop(); }
169
+ stop(): void {
170
+ if (this.stopped) return;
171
+ this.stopped = true;
172
+ this.loop.stop();
173
+ this.drained = this.repo.stop(); // kills the git children in flight; drain() awaits them
174
+ this.unsubTasks?.(); this.unsubTasks = null;
175
+ this.cards.settleAll();
176
+ if (this.started) { this.io.write(leaveSequence()); this.io.leaveRaw(); }
177
+ this.hooks = null;
178
+ }
179
+
180
+ attach(ctx: SextantAttach): void {
181
+ this.ctx = ctx;
182
+ const s = this.state;
183
+ s.cwd = ctx.cwd; s.repo.name = basename(ctx.cwd) || ctx.cwd;
184
+ if (ctx.petName) this.pet.state.name = ctx.petName.slice(0, 14);
185
+ this.unsubTasks?.();
186
+ const sync = (): void => {
187
+ const list = ctx.tasks.list();
188
+ if (list.length > s.crew.length) s.code.lane = list.length - 1; // the newest lane is selected
189
+ setCrew(s, list); this.loop.markDirty();
190
+ };
191
+ sync();
192
+ this.unsubTasks = ctx.tasks.subscribe((t) => { sync(); petOnTask(this.pet, t, this.clock()); });
193
+ this.repo.afterAttach();
194
+ this.refreshPlan();
195
+ }
196
+
197
+ // ------------------------------------------------------------------ the event stream (source of truth while busy)
198
+ onEvent(ev: RunEvent): void {
199
+ const now = this.clock(), s = this.state;
200
+ this.applyEv(s, ev, now);
201
+ petOnEvent(this.pet, ev, this.liveCalls, s.cwd, now);
202
+ this.dropNotes += duplicateNotes(ev);
203
+ if (ev.type === "tool_execution_start" && ev.tool === "edit") this.editOps.set(ev.callId, editOpsOf(ev.args));
204
+ if (ev.type === "tool_execution_end" || ev.type === "tool_call_failed") {
205
+ const row = this.rowOf(ev.callId);
206
+ const before = row?.path !== undefined ? this.before.get(row.path) : undefined;
207
+ if (row?.path !== undefined) this.before.delete(row.path);
208
+ const ops = this.editOps.get(ev.callId) ?? [];
209
+ this.editOps.delete(ev.callId);
210
+ if (row && (row.verb === "edit" || row.verb === "write" || row.verb === "remove" || row.verb === "run")) {
211
+ this.repo.afterMutation();
212
+ if (row.path !== undefined && row.path === s.code.file) s.code.content = null; // reload the edited file (before the next paint: onTick)
213
+ this.loadedFile = null;
214
+ if (ev.type === "tool_execution_end" && ev.ok && row.path !== undefined && (row.verb === "edit" || row.verb === "write")) this.repo.scheduleDiff(row, row.path, before, ops);
215
+ }
216
+ }
217
+ this.loop.markDirty();
218
+ }
219
+
220
+ /** pre-approval (the approver runs BEFORE the tool executes — the one real-time moment): capture the
221
+ * file's current content as the diff base, and let the card's previewDiff text fill the code panel's
222
+ * diff view for the pending file; returns the cwd-relative file (null when the card names none) */
223
+ private previewInCodePanel(tool: string, argsPreview: string, detail: string | undefined): string | null {
224
+ if (tool !== "edit" && tool !== "write") return null;
225
+ const path = argsFromPreview(argsPreview).path;
226
+ if (typeof path !== "string" || !path) return null;
227
+ const file = relPath(this.state.cwd, path);
228
+ this.before.set(file, readFileBounded(this.state.cwd, file));
229
+ const hunks = detail ? hunksFromUnified(detail) : [];
230
+ if (!hunks.length) return file;
231
+ const rows = hunks.flatMap((h) => h.rows);
232
+ this.state.code.diff = { file, hunks, add: rows.filter((r) => r.op === "+").length, del: rows.filter((r) => r.op === "-").length };
233
+ this.state.code.mode = "diff";
234
+ return file;
235
+ }
236
+
237
+ // ------------------------------------------------------------------ Renderer → state
238
+ setCommands(cmds: SlashCommand[]): void {
239
+ this.state.commands = cmds.map((c) => ({ name: c.name, description: c.description, ...(c.choices ? { choices: c.choices } : {}), ...(c.choicesThen ? { choicesThen: c.choicesThen } : {}) }));
240
+ this.loop.markDirty();
241
+ }
242
+
243
+ addUser(text: string): void { this.push(userRow(text, this.clock())); }
244
+
245
+ addSystemNote(text: string, tone: "info" | "warn" | "error" = "info"): void {
246
+ if (this.dropNotes > 0) { this.dropNotes--; return; } // the reducer already made this row from the event
247
+ this.push({ kind: "system", text, tone });
248
+ // toasts: every warning/error, and one-line info notes; a multi-line info note (boot banner, /help,
249
+ // /cost) is a transcript row only — a clipped first line would only smear the top of the code panel
250
+ if (tone !== "info" || (!text.includes("\n") && text.length <= TOAST_MAX)) this.toast(text.split("\n")[0] ?? "", tone);
251
+ }
252
+
253
+ beginAssistant(): AssistantView {
254
+ if (this.busy) return NOOP_VIEW; // message_update events stream the row
255
+ const row: Extract<SextantState["messages"][number], { kind: "assistant" }> = { kind: "assistant", text: "", streaming: true };
256
+ this.push(row);
257
+ return { append: (delta) => { row.text += delta; this.loop.markDirty(); }, done: () => { row.streaming = false; this.loop.markDirty(); } };
258
+ }
259
+
260
+ toolStart(callId: string, tool: string, argsPreview: string): void {
261
+ if (this.busy) return;
262
+ this.push(replayToolRow(callId, tool, argsPreview, this.state.cwd));
263
+ }
264
+ toolUpdate(callId: string, note: string): void {
265
+ if (this.busy) return;
266
+ const row = this.rowOf(callId);
267
+ if (row) { row.detail = clipText(note, 80); this.loop.markDirty(); }
268
+ }
269
+ toolEnd(callId: string, ok: boolean, outputPreview: string, durationMs: number): void {
270
+ if (this.busy) return;
271
+ let row = this.rowOf(callId);
272
+ if (!row) { row = replayToolRow(callId, "tool", "{}", this.state.cwd); this.push(row); }
273
+ row.running = false; row.ok = ok; row.ms = durationMs;
274
+ const end = summarizeEnd({ verb: row.verb }, row.tool, ok, outputPreview);
275
+ if (end.detail) row.detail = end.detail;
276
+ this.loop.markDirty();
277
+ }
278
+
279
+ /** the approval card (sextant-cards.ts); an edit/write preview also fills the code panel's diff view.
280
+ * A denied call never executes (no row, no end event), so its captured base is dropped here — a later
281
+ * ungated edit of the same file must not diff against that stale snapshot */
282
+ askApproval(tool: string, argsPreview: string, detail?: string): Promise<ApprovalAnswer> {
283
+ const file = this.previewInCodePanel(tool, argsPreview, detail);
284
+ this.ring(); // the run is now waiting on a human who may have tabbed away
285
+ const answer = this.cards.approval(tool, argsPreview, detail);
286
+ if (file !== null) void answer.then((a) => { if (a === "deny") this.before.delete(file); });
287
+ return answer;
288
+ }
289
+ /** the palette as a picker (sextant-cards.ts) */
290
+ pickOne(items: PickItem[], title?: string): Promise<string | null> { return this.cards.pick(items, title); }
291
+ /** the question card (sextant-cards.ts): `signal` abort dismisses it, a second concurrent ask is rejected */
292
+ askQuestion(q: QuestionPrompt, signal?: AbortSignal): Promise<QuestionAnswer | null> { this.ring(); return this.cards.question(q, signal); }
293
+
294
+ /** BEL (\x07) to the terminal — only once the surface is live, and never after leave. The terminal decides
295
+ * what a bell is (Windows Terminal: audible/visual/taskbar flash per profile; iTerm2/kitty: a notification
296
+ * when unfocused). No OSC 9/777: support is uneven and a stray escape sequence on screen is worse than no
297
+ * sound. Off with settings.json `"bell": false` (core/settings.ts) or the renderer option. */
298
+ private ring(): void {
299
+ if (this.bell && this.started && !this.stopped) this.io.write("\x07");
300
+ }
301
+
302
+ clearTranscript(): void { this.state.messages = []; this.state.stick = true; this.pet.event("fresh", undefined, this.clock()); this.loop.markDirty(); }
303
+
304
+ prefillEditor(text: string): void {
305
+ const I = this.state.input;
306
+ I.text = text; I.cur = text.length; I.sgSel = 0; I.histIdx = -1; this.state.focus = "messages"; this.loop.markDirty();
307
+ }
308
+
309
+ /** busy = a run is in flight: the event stream owns the rows. setBusy(false) after a run the
310
+ * reducer never saw end (an interrupted generator yields no run_end) settles what run_end would have. */
311
+ setBusy(busy: boolean, label?: string): void {
312
+ const s = this.state, now = this.clock();
313
+ this.busy = busy;
314
+ if (busy) { s.running = true; if (label) s.activity.label = label.replace(/…$/, ""); }
315
+ else {
316
+ this.dropNotes = 0;
317
+ if (s.running) {
318
+ s.running = false;
319
+ if (s.activity.startedAt !== null && s.activity.endedAt === null) s.activity.endedAt = now;
320
+ if (s.activity.state !== "SUCCESS" && s.activity.state !== "ERROR") { s.activity.state = "IDLE"; s.activity.label = "stopped"; }
321
+ for (const r of s.messages) if (r.kind === "tool" && r.running) { r.running = false; r.ok = false; r.detail ??= "interrupted"; }
322
+ this.pet.event("stopped", undefined, now);
323
+ this.ring(); // a run just ended (done, failed or interrupted): the moment a human who tabbed away wants to know about
324
+ }
325
+ s.escUntil = 0; delete s.ctrlCUntil;
326
+ }
327
+ this.loop.markDirty();
328
+ }
329
+
330
+ setStatus(info: StatusInfo): void {
331
+ const s = this.state;
332
+ s.yolo = info.yolo; s.acceptEdits = info.permission === "accept-edits"; if (info.mode) s.mode = info.mode;
333
+ const u = this.ctx?.usage?.();
334
+ setUsage(s, {
335
+ provider: info.provider, model: info.model, turns: info.turns, tokensIn: info.tokensIn, tokensOut: info.tokensOut,
336
+ ...(info.effort !== undefined ? { effort: info.effort } : {}),
337
+ contextTokens: u?.contextTokens ?? 0, contextWindow: this.ctx?.contextWindow(), costUsd: u ? u.costUsd : null,
338
+ });
339
+ this.refreshPlan();
340
+ this.loop.markDirty();
341
+ }
342
+
343
+ // ------------------------------------------------------------------ internals
344
+ private push(row: SextantState["messages"][number]): void { this.state.messages.push(row); this.state.stick = true; this.loop.markDirty(); }
345
+ private rowOf(callId: string): ToolRow | undefined {
346
+ for (let i = this.state.messages.length - 1; i >= 0; i--) { const r = this.state.messages[i]!; if (r.kind === "tool" && r.callId === callId) return r; }
347
+ return undefined;
348
+ }
349
+ private toast(text: string, tone: "info" | "warn" | "error" = "info"): void { pushToast(this.state, clipText(text.trim(), TOAST_MAX), this.clock(), tone); this.loop.markDirty(); }
350
+
351
+ /** the session's todos.json → plan panel; a newly completed step is a pet quip */
352
+ private refreshPlan(): void {
353
+ if (!this.ctx) return;
354
+ setPlan(this.state, loadTodos(join(this.ctx.sessionsDir, this.ctx.store().id)));
355
+ const c = planCounts(this.state);
356
+ if (this.planLoaded && c.completed > this.todoDone) this.pet.event("todo_done", { d: c.completed, n: c.total }, this.clock());
357
+ this.planLoaded = true; this.todoDone = c.completed;
358
+ }
359
+
360
+ /** the renderer-owned effects keys.ts fires after writing the state field (theme rebuild, file/diff loading, toasts) */
361
+ private readonly local: KeyCtx["local"] = {
362
+ setTheme: (name) => { this.theme = buildTheme(name); this.state.theme = name; this.toast(`theme · ${name}`); this.pet.event("theme", undefined, this.clock()); },
363
+ setMode: (mode) => { if (mode === "diff" && this.state.code.file) this.repo.loadDiff(this.state.code.file); },
364
+ openFile: (path) => { this.state.code.content = readFileBounded(this.state.cwd, path); this.state.code.hl = null; this.loadedFile = path; },
365
+ toast: (text) => this.toast(text),
366
+ // the market: the overlay is opened at once in its loading state so the frame after ⌃m already shows
367
+ // the box, and the catalog is joined in when the module answers (it may touch the network)
368
+ openMarket: () => {
369
+ openMarket(this.state, [], { kind: "loading" });
370
+ this.loop.markDirty();
371
+ void loadMarket(this.state.cwd, homedir())
372
+ .then((load) => { if (this.state.market) { this.state.market.rows = load.rows; this.state.market.status = load.status; this.state.market.notes = load.notes; this.loop.markDirty(); } })
373
+ .catch((e: unknown) => { if (this.state.market) { this.state.market.status = { kind: "error", reason: e instanceof Error ? e.message : String(e) }; this.loop.markDirty(); } });
374
+ },
375
+ // /context (⌃g). The panel opens on the NEXT frame with what we already have — the model and an
376
+ // empty count — so the chord feels instant, and the real numbers are joined in when counting is
377
+ // done. Counting is synchronous but not free on a long transcript, so it happens off the frame.
378
+ openContext: () => {
379
+ const ref = this.ctx?.model() ?? { provider: "unknown", model: "unknown" };
380
+ const inputs = this.ctx?.contextInputs?.();
381
+ openContext(this.state, {
382
+ model: `${ref.provider}/${ref.model}`, raw: 0, estimated: 0,
383
+ scale: { factor: 1, measured: false, note: "counting…" },
384
+ slices: [], images: 0, billed: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
385
+ unpricedTurns: 0, live: false, tolerance: 0.05,
386
+ });
387
+ this.loop.markDirty();
388
+ if (!inputs) return; // the panel stays in its "not counted" state and says so
389
+ void loadContext({
390
+ messages: inputs.messages, current: ref, lookup: inputs.lookup,
391
+ fixed: fixedFrom(inputs.runtime, ref),
392
+ }).then((state) => {
393
+ // the human may have closed it while we counted; writing into a closed panel would reopen it
394
+ if (!this.state.context) return;
395
+ this.state.context = { ...state, scroll: this.state.context.scroll };
396
+ this.loop.markDirty();
397
+ }).catch(() => { /* loadContext does not reject; this is belt and braces on the dynamic import */ });
398
+ },
399
+ marketPlan: (row, local) => {
400
+ void planFor(row, { scope: "user", cwd: this.state.cwd, home: homedir() }, local)
401
+ .then((plan) => {
402
+ const m = this.state.market;
403
+ if (!m) return;
404
+ if ("error" in plan) { this.toast(plan.error); return; }
405
+ m.plan = plan;
406
+ this.loop.markDirty();
407
+ });
408
+ },
409
+ marketDocs: (row) => {
410
+ void docsFor(row).then((docs) => {
411
+ const m = this.state.market;
412
+ if (!m || !docs) return;
413
+ // write onto the row the list holds, so closing and reopening the pane costs nothing
414
+ const target = m.rows.find((r) => r.kind === row.kind && r.id === row.id);
415
+ if (target) target.docs = docs;
416
+ this.loop.markDirty();
417
+ });
418
+ },
419
+ marketInstall: (row, local) => {
420
+ // `local` is the approved card's own answer (keys.ts hands over req.plan.local): npm runs only now
421
+ void install(row, { scope: "user", cwd: this.state.cwd, home: homedir() }, local === true ? { local: true } : {}).then(async (outcome) => {
422
+ // An MCP server lands in a file this session already read, so the install used to end in
423
+ // "restart rovecode" — a poor answer to "I just installed it". Ask the runtime to re-read the
424
+ // files and connect what is new, and report what actually happened instead of what to do next.
425
+ // Only on success, only for MCP, and never fatal: a reload that throws leaves the install
426
+ // reported exactly as it was, because the write DID happen and saying otherwise would be worse.
427
+ let text = outcome.text;
428
+ if (outcome.ok && row.kind === "mcp" && this.ctx?.reloadMcp) {
429
+ try {
430
+ const r = await this.ctx.reloadMcp();
431
+ const failed = r.failed.find((f) => r.added.includes(f.name));
432
+ text = text.replace(/ · restart rovecode[^·]*/, "");
433
+ // `skipped` before `added`: an entry the loader refused never reaches `added`, so without this
434
+ // the install read "installed" and then said nothing at all about why no tools appeared.
435
+ const skipped = r.skipped.length > 0 ? ` · ${r.skipped.join(" · ")}` : "";
436
+ text += failed
437
+ ? ` · ${failed.name} did not connect: ${failed.error}`
438
+ : r.added.length > 0 ? ` · connected as ${r.added.join(", ")} — no restart needed${skipped}` : skipped;
439
+ } catch { /* the install stands on its own */ }
440
+ }
441
+ const m = this.state.market;
442
+ if (!m?.plan) return;
443
+ m.plan.running = false;
444
+ m.plan.outcome = { ...outcome, text };
445
+ this.loop.markDirty();
446
+ });
447
+ },
448
+ };
449
+
450
+ /** renderer-level intercepts: the picker's Enter/Esc, and the two-press ⌃c guarantee (a ⌃c while busy
451
+ * interrupts through keys.ts and arms ctrlCUntil; a second one inside the window quits even before the
452
+ * interrupted run settled — keys.ts alone would interrupt again while s.running is still true) */
453
+ private beforeInput(ev: InputEvent, now: number): boolean {
454
+ const s = this.state;
455
+ if (this.cards.interceptKey(ev)) return true;
456
+ if (ev.type === "key" && ev.ctrl && ev.name === "c" && s.running && s.ctrlCUntil !== undefined && now < s.ctrlCUntil) { this.hooks?.onExit(); return true; }
457
+ return false;
458
+ }
459
+ private afterInput(ev: InputEvent, now: number): void {
460
+ const s = this.state;
461
+ this.cards.afterKey();
462
+ if (ev.type === "key" && ev.ctrl && ev.name === "c" && s.running) s.ctrlCUntil = now + ESC_WINDOW_MS;
463
+ if (ev.type === "key" && !ev.ctrl && s.input.text.startsWith("/")) { const first = suggestions(s, s.files.paths, fuzzy)[0]; if (first?.kind === "slash") this.pet.suggest(first.label, now); }
464
+ }
465
+
466
+ /** off-frame work, BEFORE the tick's paint: the code panel's file content (a bounded sync read, so
467
+ * the frame that follows an edit shows the file, never `cannot read`) and the repo scan (full after
468
+ * a mutating tool, statuses while idle — async, sextant-repo.ts) */
469
+ private onTick(now: number): void {
470
+ const s = this.state;
471
+ if (s.code.file && s.code.content === null && this.loadedFile !== s.code.file) { s.code.content = readFileBounded(s.cwd, s.code.file); this.loadedFile = s.code.file; this.loop.markDirty(); }
472
+ // the staged images are owned by the store (tui/attach.ts); mirror their names so the prompt shows
473
+ // chips before the message is sent — one read per tick, never a second copy of the list
474
+ const staged = this.ctx?.staged?.() ?? [];
475
+ if (staged.length !== s.staged.length || staged.some((n, i) => n !== s.staged[i])) { s.staged = staged; this.loop.markDirty(); }
476
+ this.repo.onTick(now, s.running);
477
+ }
478
+ }
@@ -0,0 +1,131 @@
1
+ /** Sextant repo watcher (port #44 fix wave): the renderer's git + fs work — the files-panel scan (full
2
+ * after a mutating tool, porcelain-only while idle), the post-edit diff and the /diff view — scheduled
3
+ * OFF the frame loop (a setTimeout(0) from the tick, never inside a painter) and run ASYNC through the
4
+ * git-status.ts `…Async` forms (child processes; results applied on completion + a repaint), so a
5
+ * 200 ms `git status` on a large repo never freezes the spinner, the pet or the keys. One scan is in
6
+ * flight at a time: a request that arrives meanwhile runs on the first tick after it settles; stop()
7
+ * drops every pending timer and ignores results that land afterwards. Extracted from
8
+ * sextant-renderer.ts (ADR-002 line budget); the renderer owns the state, this class only writes
9
+ * s.files / s.repo.branch / s.code.diff and the tool row's landed counts. */
10
+
11
+ import type { GitRunnerAsync } from "./git-status.ts";
12
+ import { setFiles } from "./model.ts";
13
+ import type { HashlineOp } from "./sextant-diff-base.ts";
14
+ import { fileDiffAsync, headDiffAsync, scanRepoAsync, statusOnlyAsync, type HeadDiff } from "./sextant-files.ts";
15
+ import type { SextantState, ToolRow } from "./types.ts";
16
+
17
+ /** while idle, the porcelain statuses are re-read at most this often */
18
+ export const IDLE_SCAN_MS = 5000;
19
+
20
+ export interface RepoWatcherDeps {
21
+ state: SextantState;
22
+ clock: () => number;
23
+ git: GitRunnerAsync;
24
+ /** request a repaint (the frame loop's markDirty) */
25
+ dirty: () => void;
26
+ }
27
+
28
+ export class RepoWatcher {
29
+ private wanted: "full" | "status" | null;
30
+ private timer: ReturnType<typeof setTimeout> | null = null;
31
+ private readonly deferred = new Set<ReturnType<typeof setTimeout>>();
32
+ private inFlight = false;
33
+ private lastScanAt = -Infinity;
34
+ private hasGit = false;
35
+ private stopped = false;
36
+ /** aborts the git children of a scan in flight when stop() is called (git-status.ts runner signal) */
37
+ private readonly ac = new AbortController();
38
+ /** the scan in flight, so stop() can wait for its children to be GONE, not merely killed */
39
+ private live: Promise<void> | null = null;
40
+
41
+ /** `scan` false disables the repo scan entirely (pure unit tests) */
42
+ constructor(private readonly d: RepoWatcherDeps, scan: boolean) { this.wanted = scan ? "full" : null; }
43
+
44
+ /** a mutating tool (edit / write / remove / run) landed: list + statuses + branch again — repos only,
45
+ * but while the first scan is still in flight the answer is unknown, so the request is kept */
46
+ afterMutation(): void { if (this.wanted !== null || this.hasGit || this.inFlight) this.wanted = "full"; }
47
+ /** attach() re-pointed the cwd: a fresh full scan unless scanning is off */
48
+ afterAttach(): void { if (this.wanted !== null || this.lastScanAt !== -Infinity) this.wanted = "full"; }
49
+ /** a scan is running in the background (tests) */
50
+ get scanning(): boolean { return this.inFlight; }
51
+
52
+ /** from the frame loop's tick: arm the idle poll, then schedule whatever is wanted (once) */
53
+ onTick(now: number, running: boolean): void {
54
+ if (this.wanted === null && this.hasGit && !running && now - this.lastScanAt >= IDLE_SCAN_MS) this.wanted = "status";
55
+ if (this.wanted !== null && !this.inFlight && !this.timer && !this.stopped) this.timer = setTimeout(() => { this.timer = null; this.live = this.scan(); }, 0);
56
+ }
57
+
58
+ /** only reached through onTick's timer, which is armed when no scan is in flight */
59
+ private async scan(): Promise<void> {
60
+ if (this.stopped || this.wanted === null) return;
61
+ const kind = this.wanted, s = this.d.state;
62
+ this.wanted = null; this.lastScanAt = this.d.clock(); this.inFlight = true;
63
+ // every git child of this scan is bound to the watcher's abort signal: stop() kills them and the
64
+ // runner settles only when they are gone, so `live` resolving means "no child holds the cwd"
65
+ const git: GitRunnerAsync = (a, c) => this.d.git(a, c, this.ac.signal);
66
+ try {
67
+ if (kind === "full") {
68
+ const snap = await scanRepoAsync(s.cwd, git);
69
+ if (this.stopped) return;
70
+ setFiles(s, snap.paths, snap.statuses); s.repo.branch = snap.branch; this.hasGit = snap.git;
71
+ } else {
72
+ const st = await statusOnlyAsync(s.cwd, git);
73
+ if (this.stopped) return;
74
+ if (st) setFiles(s, s.files.paths, st);
75
+ }
76
+ this.d.dirty();
77
+ } finally { this.inFlight = false; }
78
+ }
79
+
80
+ /** The #41 diffFor seam, deferred and async: the hunks for the file an edit/write just changed →
81
+ * s.code.diff + the diff view. `before` = the pre-edit content captured at approval (undefined when
82
+ * the call was not gated), `ops` = the edit's hashline ops (the base is rebuilt from them when
83
+ * nothing was captured). The row's `+a −b` is overwritten only by an edit-only diff — a HEAD base
84
+ * is cumulative, so the row keeps the reducer's args-derived counts and the view says `vs HEAD`. */
85
+ scheduleDiff(row: ToolRow, file: string, before: string | null | undefined, ops: readonly HashlineOp[]): void {
86
+ this.defer(async () => {
87
+ const d = await fileDiffAsync(this.d.state.cwd, file, before, this.d.git, ops);
88
+ if (this.stopped || !d) return;
89
+ if (d.base !== "head") { row.add = d.add; row.del = d.del; }
90
+ this.show(file, d, true);
91
+ });
92
+ }
93
+
94
+ /** the /diff view (⌃d, /diff, ←/→ into diff mode): HEAD vs disk for the file, flagged as such; the
95
+ * mode is already diff (keys.ts wrote it before the hook fired) and is not forced back if the user
96
+ * moved on while git ran */
97
+ loadDiff(file: string): void {
98
+ this.defer(async () => {
99
+ const d = await headDiffAsync(this.d.state.cwd, file, this.d.git);
100
+ if (this.stopped) return;
101
+ if (d) this.show(file, d, false); else { this.d.state.code.diff = null; this.d.dirty(); }
102
+ });
103
+ }
104
+
105
+ private show(file: string, d: HeadDiff, flip: boolean): void {
106
+ const s = this.d.state;
107
+ s.code.diff = { file, hunks: d.hunks, add: d.add, del: d.del, ...(d.base ? { base: d.base } : {}) };
108
+ if (flip) s.code.mode = "diff";
109
+ this.d.dirty();
110
+ }
111
+
112
+ private defer(work: () => Promise<void>): void {
113
+ if (this.stopped) return;
114
+ const t = setTimeout(() => { this.deferred.delete(t); if (!this.stopped) void work(); }, 0);
115
+ this.deferred.add(t);
116
+ }
117
+
118
+ /** clear every pending timer, KILL the git children of a scan in flight (their results are dropped),
119
+ * and resolve once those children are gone — the caller can then remove the cwd. Idempotent: a second
120
+ * stop() returns the same settled promise. Before this, stop() only dropped results and left the child
121
+ * running; on Windows a live `git status` holds the directory and every tui-sextant test's quit()
122
+ * failed at rmSync with EBUSY, a different test each run. */
123
+ stop(): Promise<void> {
124
+ this.stopped = true;
125
+ if (this.timer) { clearTimeout(this.timer); this.timer = null; }
126
+ for (const t of this.deferred) clearTimeout(t);
127
+ this.deferred.clear();
128
+ this.ac.abort();
129
+ return this.live ?? Promise.resolve();
130
+ }
131
+ }