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
package/src/tui/app.ts ADDED
@@ -0,0 +1,608 @@
1
+ /** TUI chat app (port #1): wires the ONE agentLoop (ADR-003) into a Renderer.
2
+ * All vendor contact lives behind Renderer (renderer.ts) — swap-friendly. Slash handlers live
3
+ * beside it (ADR-002 cap): info-cmd.ts (/help /status /cost /skills /memory /export /todos
4
+ * /tasks), session-cmd.ts (/new /rewind /sessions /resume), checkpoints-cmd.ts, modes-cmd.ts. */
5
+
6
+ import { agentLoop, outstandingClause, outstandingTone } from "../core/loop.ts";
7
+ import { resetTurnFailureCount } from "../memory/tools.ts";
8
+ import { createRuntime } from "../cli/runtime.ts";
9
+ import { SandboxConfigError } from "../core/sandbox-config.ts";
10
+ import type { SpawnRunner } from "../core/executor.ts";
11
+ import { SessionStore, listSessions } from "../core/session.ts";
12
+ import { BlockStore } from "../memory/blocks.ts";
13
+ import { ModelCatalog } from "../providers/catalog.ts";
14
+ import { ModeManager, loadModesConfig, modeFromEntries, type AgentMode } from "../core/modes.ts";
15
+ import { isTerminal, taskNote } from "../core/tasks.ts";
16
+ import { togglePlanAct, applyModeToRun, flushModeSwitch } from "./modes-cmd.ts";
17
+ import { cmdCheckpoints, cmdRestore, type CheckpointCmdCtx } from "./checkpoints-cmd.ts";
18
+ import type { SessionCmdCtx } from "./session-cmd.ts";
19
+ import type { InfoCmdCtx } from "./info-cmd.ts";
20
+ import { todoLabel } from "./todo-label.ts";
21
+ import { cmdAttach, cmdPasteImage, carryOverAttachments, queuedAttachNote, userTurnLine, ATTACH_COMMAND, PASTE_COMMAND, type AttachCtx } from "./attach.ts";
22
+ import { cmdConnect, cmdModel as cmdModelSwitch, cmdModels, cmdProvider, cmdSetup, listModelIds, watchProviders, CONNECT_COMMAND, MODEL_COMMAND, PROVIDER_COMMANDS, SETUP_COMMAND, type ProviderCmdCtx } from "./providers-cmd.ts";
23
+ import { cmdMcp, MCP_COMMAND } from "./mcp-cmd.ts";
24
+ import { summarizePlugins } from "../plugins/index.ts";
25
+ import { acceptEditsNote, effortNote, modeSwitchNote, noModelHint, resumedLine, welcomeCard } from "../core/voice.ts";
26
+ import { checkForUpdate, updateLine } from "../core/update-check.ts";
27
+ import pkg from "../../package.json";
28
+ import { compactionNote } from "./replay-marker.ts";
29
+ import { previewDiff } from "../coding/diff.ts";
30
+ import { discoverCommands, commandsForPalette, dispatchCustomCommand, type CustomCommandCtx } from "./commands.ts";
31
+ import type { Renderer, AssistantView, SlashCommand, StatusInfo } from "./renderer.ts";
32
+ // pi-renderer.ts (and the vendored pi-tui under it) costs ~27 MB resident; a sextant session never
33
+ // constructs it, so it is required where it is constructed, not imported here (tests: pi-renderer-lazy)
34
+ type PiRendererMod = typeof import("./pi-renderer.ts");
35
+ import { buildSextantAttach, SEXTANT_LOCAL_NAMES } from "./sextant-attach.ts";
36
+ import type { ModelRef, PermissionLevel, RunEvent, StreamFn } from "../core/types.ts";
37
+ import { thinkingLine } from "../providers/thinking.ts";
38
+ import { anthropicShapeFor } from "../providers/stream.ts";
39
+ import { parseEffort, THINKING_EFFORTS } from "../core/types.ts";
40
+ import { resolvePermission, saveSetting } from "../core/settings.ts";
41
+ import type { ThinkingEffort } from "../core/types.ts";
42
+ import { join } from "node:path";
43
+
44
+ export { buildCostNote } from "./cost.ts"; // moved for the ADR-002 cap; re-exported for tests
45
+
46
+ // lazy loaders — info-cmd and session-cmd are deferred until the first slash command
47
+ type InfoCmdMod = typeof import("./info-cmd.ts");
48
+ let _infoCmdMod: InfoCmdMod | null = null;
49
+ function lazyInfoCmd(): InfoCmdMod {
50
+ if (_infoCmdMod === null) {
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ _infoCmdMod = require("./info-cmd.ts") as InfoCmdMod;
53
+ }
54
+ return _infoCmdMod;
55
+ }
56
+
57
+ type SessionCmdMod = typeof import("./session-cmd.ts");
58
+ let _sessionCmdMod: SessionCmdMod | null = null;
59
+ function lazySessionCmd(): SessionCmdMod {
60
+ if (_sessionCmdMod === null) {
61
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
62
+ _sessionCmdMod = require("./session-cmd.ts") as SessionCmdMod;
63
+ }
64
+ return _sessionCmdMod;
65
+ }
66
+
67
+ /** resolveBootSession inlined from session-cmd.ts so the module loads lazily.
68
+ * `rovecode --resume <id>` boot resolution: exact/new ids pass, unique prefix resolves,
69
+ * ambiguous prefix starts fresh and warns. */
70
+ function resolveBootSession(sessionsDir: string, id: string | undefined): { id: string | undefined; warn?: string } {
71
+ if (id === undefined) return { id };
72
+ const known = listSessions(sessionsDir);
73
+ if (known.some((s) => s.id === id)) return { id };
74
+ const pre = known.filter((s) => s.id.startsWith(id));
75
+ if (pre.length === 1) return { id: pre[0]!.id };
76
+ if (pre.length > 1) return { id: undefined, warn: `"${id}" matches ${pre.length} sessions — started fresh; use /resume to pick one` };
77
+ return { id };
78
+ }
79
+
80
+ export interface TuiAppOptions {
81
+ yolo?: boolean;
82
+ model?: string;
83
+ cwd?: string;
84
+ /** resume an existing session id instead of starting a fresh one */
85
+ sessionId?: string;
86
+ /** injected by tests/smoke (VirtualTerminal-backed renderer, mock stream) */
87
+ renderer?: Renderer;
88
+ stream?: StreamFn | null;
89
+ /** default true: process.exit(0) when the user quits */
90
+ exitOnClose?: boolean;
91
+ /** port #27 test seams, threaded into createRuntime: the process runner behind the rung
92
+ * probe (never a real wsl.exe/docker in tests) and the platform the probe assumes */
93
+ spawnRunner?: SpawnRunner;
94
+ platform?: NodeJS.Platform;
95
+ /** port #44: the sextant pet's name (`--pet <name>`); the classic renderer ignores it */
96
+ pet?: string;
97
+ /** start in the middle permission tier (`--accept-edits`, ROVECODE_ACCEPT_EDITS=1) */
98
+ acceptEdits?: boolean;
99
+ /** the permission level as an explicit ASK from an in-process caller — the same rung as a CLI flag, so it
100
+ * beats ROVECODE_PERMISSION and both settings files. `yolo`/`acceptEdits` can only demand a WIDER
101
+ * level; `yolo: false` is "no flag", and a user settings file saying "auto" then wins. A smoke whose
102
+ * assertion is "an approval card appears" needs this to say "ask" and mean it. Leaving it undefined
103
+ * changes nothing: the env var and the files keep their say. */
104
+ permission?: PermissionLevel;
105
+ /** `--effort <level>`; overrides ROVECODE_EFFORT for this session */
106
+ effort?: ThinkingEffort;
107
+ }
108
+
109
+ /** The built-in slash commands, worded in rovecode's voice (core/voice.ts) and tagged with the /help topic
110
+ * they are listed under (info-cmd.ts cmdHelp groups by `group`; the palette shows name + description). */
111
+ export const TUI_COMMANDS: SlashCommand[] = [
112
+ { name: "help", description: "This list, by topic", group: "start here" },
113
+ CONNECT_COMMAND, // providers-cmd.ts: /connect — bare it is /setup; with an id it takes the answers on the line
114
+ SETUP_COMMAND, // providers-cmd.ts: /setup — pick a provider, name the model, hand over the key, one test call
115
+ { name: "exit", description: "Quit (alias /quit; Ctrl+C does the same)", group: "start here" },
116
+ { name: "new", description: "Start over in this session (branch back to the beginning)", group: "session" },
117
+ { name: "sessions", description: "Pick an earlier session to continue", group: "session" },
118
+ { name: "resume", description: "Continue a session by id: /resume <id>", group: "session" },
119
+ { name: "rewind", description: "Go back to an earlier turn and edit it (alias: /tree)", group: "session" },
120
+ { name: "tree", description: "Alias of /rewind", group: "session" },
121
+ { name: "export", description: "Save this session as markdown: /export [--json] [path] [--force]", group: "session" },
122
+ MODEL_COMMAND, // providers-cmd.ts: /model <provider/model | model> [--save]
123
+ ...PROVIDER_COMMANDS, // /models · /provider — providers-cmd.ts (live registry: no restart after add/key/use)
124
+ { name: "yolo", description: "Toggle ask first / auto (never asks)", group: "modes & safety" },
125
+ { name: "accept-edits", description: "Stop asking for writes inside this folder; shell, subagents and writes outside it still ask", group: "modes & safety" },
126
+ { name: "effort", description: "How hard I think before answering: /effort auto | off | low | medium | high — the note says what the current model actually receives", group: "model & provider", choices: THINKING_EFFORTS },
127
+ { name: "plan", description: "Plan mode: I only read and plan, nothing changes", group: "modes & safety" },
128
+ { name: "act", description: "Act mode: I can edit and run again", group: "modes & safety" },
129
+ { name: "checkpoints", description: "Snapshots I took before each change (shadow git)", group: "files & history" },
130
+ { name: "restore", description: "Go back to a snapshot: /restore <ref> [files|conversation|both]", group: "files & history" },
131
+ { ...ATTACH_COMMAND, group: "files & history" }, // port #34: /attach <path> · /attach (list) · /attach clear — attach.ts
132
+ { ...PASTE_COMMAND, group: "files & history" }, // /paste: the clipboard image as an attachment (⌃v in sextant) — attach.ts
133
+ MCP_COMMAND, // mcp-cmd.ts: /mcp [query] — pick a server in the palette, approve the exact plan on the card, it lands in mcp.json
134
+ { name: "status", description: "Provider, model, turns, tokens, sandbox", group: "info" },
135
+ { name: "cost", description: "Tokens, cache hits and the USD estimate (/cost refresh updates prices)", group: "info" },
136
+ { name: "todos", description: "My step list for the current task", group: "info" },
137
+ { name: "tasks", description: "Background subagents: /tasks [cancel <id>|cancel all]", group: "info" },
138
+ { name: "skills", description: "Installed skills", group: "info" },
139
+ { name: "memory", description: "What I remember across turns (memory blocks)", group: "info" },
140
+ ];
141
+
142
+ interface TuiState {
143
+ yolo: boolean;
144
+ /** the middle tier (port: Claude Code's acceptEdits): writes inside the workspace stop asking,
145
+ * shell/spawn/network and writes outside it still do. Ignored while `yolo` is on — auto already
146
+ * covers everything. Turned on by `/accept-edits`, `--accept-edits`, or the `all edits` button on
147
+ * a write approval card. */
148
+ acceptEdits: boolean;
149
+ provider: string; model: string; mode: AgentMode;
150
+ turns: number; tokensIn: number; tokensOut: number;
151
+ busy: boolean;
152
+ }
153
+
154
+ export async function runTui(opts: TuiAppOptions = {}): Promise<void> {
155
+ const _t0 = process.env.ROVECODE_TRACE_BOOT === "1" ? (Number(process.env._ROVECODE_BOOT_T0) || Date.now()) : -1;
156
+ const _trace = _t0 >= 0 ? (label: string) => process.stderr.write(`[boot] +${Date.now() - _t0}ms ${label}\n`) : (_: string) => {};
157
+ _trace("runTui entered");
158
+ // opts.sessionId may be a unique id prefix (rovecode --resume <id>): resolved by the /resume rule
159
+ // (session-cmd.ts) — exact/new ids pass, a unique prefix resolves, an ambiguous one starts fresh + warns
160
+ const boot = resolveBootSession(join(opts.cwd ?? process.cwd(), ".rovecode", "sessions"), opts.sessionId);
161
+ // opts.stream passes through verbatim: a StreamFn overrides, explicit null forces
162
+ // "no provider", undefined defers to the runtime's env-resolved provider
163
+ // port #27: a sandbox MISCONFIG throws synchronously here (before any side effect) — a clean
164
+ // one-line startup error (exit 2), never a stack, never a silent direct fallback. The rung
165
+ // PROBE verdict (rt.sandbox.ready) is awaited at the end of boot: runTui must stay
166
+ // synchronous until the renderer's input handlers are wired (tests/smoke send input right
167
+ // after calling runTui), so no await may sit above that point.
168
+ const rt = (() => {
169
+ try {
170
+ _trace("createRuntime start");
171
+ const r = createRuntime({ cwd: opts.cwd, stream: opts.stream, sessionId: boot.id, spawnRunner: opts.spawnRunner, platform: opts.platform });
172
+ _trace("createRuntime done");
173
+ return r;
174
+ }
175
+ catch (e) {
176
+ if (e instanceof SandboxConfigError && opts.exitOnClose !== false) { console.error(`error: ${e.message}`); process.exit(2); }
177
+ throw e;
178
+ }
179
+ })();
180
+ const renderer: Renderer = opts.renderer ?? new (require("./pi-renderer.ts") as PiRendererMod).PiTuiRenderer({ cwd: rt.cwd }); // eslint-disable-line @typescript-eslint/no-require-imports
181
+ _trace("renderer created");
182
+ rt.setAskUser((q, signal) => renderer.askQuestion(q, signal)); // port #33: ask_user → the question overlay (Esc/abort dismisses it via signal)
183
+ const sessionsDir = join(rt.cwd, ".rovecode", "sessions");
184
+ // /cost pricing + context window. Boots from the offline snapshot; the live models.dev
185
+ // half is user-invoked only (/cost refresh), cached to .rovecode/cache with a 24h TTL —
186
+ // lookup() itself never fetches, so the TUI stays network-free unless asked.
187
+ const catalog = new ModelCatalog({ fetchFn: fetch, cacheDir: join(rt.cwd, ".rovecode", "cache") });
188
+ // session-scoped stores are swappable at runtime (/sessions, /rewind-to-root)
189
+ let store = rt.store;
190
+ let blocks = rt.blockStore;
191
+ // port #26: the runtime's ONE steering queue — background-task completion notes land on the
192
+ // next model turn; settled tasks also show in the transcript as they happen (failed → warn)
193
+ const steering = rt.steering;
194
+ rt.tasks.subscribe((t) => { if (isTerminal(t.status)) renderer.addSystemNote(taskNote(t), t.status === "failed" ? "warn" : "info"); });
195
+ // port #20: per-mode model slots from .rovecode/modes.json, restored from session entries
196
+ const modesCfg = loadModesConfig(rt.cwd);
197
+ const modes = new ModeManager(modesCfg, {
198
+ provider: rt.provider?.id ?? "mock",
199
+ model: opts.model ?? process.env.ROVECODE_MODEL ?? rt.defaultModel ?? "",
200
+ });
201
+ modes.restore(modeFromEntries(store.messages()) ?? modes.mode);
202
+ // port #30: custom slash commands — .rovecode/commands/*.md, project shadows ~/.rovecode/commands (commands.ts);
203
+ // LOW-1: /quit is a `case` alias of /exit below, not a TUI_COMMANDS entry — reserve it explicitly;
204
+ // port #44: the sextant surface's own /theme /open /diff /focus /agents never reach handleSlash — reserved too
205
+ // plugins (src/plugins): an ACTIVE plugin's commands folder is one more root, after the folder of its own
206
+ // scope — the same path setCommands feeds the palette and suggestions from, so nothing else changes
207
+ const pluginCommandDirs = rt.plugins.found.flatMap((p) => (p.status === "active" && p.commandsDir ? [{ dir: p.commandsDir, scope: p.scope }] : []));
208
+ const custom = discoverCommands(rt.cwd, { reserved: [...TUI_COMMANDS.map((c) => c.name), "quit", ...SEXTANT_LOCAL_NAMES], extraDirs: pluginCommandDirs });
209
+ if (opts.effort !== undefined) rt.setEffort(opts.effort);
210
+ // one resolved answer instead of two independent booleans: flag → env → project file → user file →
211
+ // "ask" (core/settings.ts). This is what makes `/yolo --save` survive the terminal closing.
212
+ // opts.permission is the flag rung for in-process callers; yolo/acceptEdits stay the boolean flags they were
213
+ // (true = demand, false = say nothing) — see TuiAppOptions.permission for why false cannot mean "ask"
214
+ const flagLevel: PermissionLevel | undefined = opts.permission ?? (opts.yolo === true ? "auto" : opts.acceptEdits === true ? "accept-edits" : undefined);
215
+ const startLevel = resolvePermission(rt.cwd, flagLevel, { ROVECODE_PERMISSION: process.env.ROVECODE_PERMISSION, ROVECODE_YOLO: process.env.ROVECODE_YOLO, ROVECODE_ACCEPT_EDITS: process.env.ROVECODE_ACCEPT_EDITS });
216
+ const state: TuiState = {
217
+ yolo: startLevel === "auto",
218
+ acceptEdits: startLevel === "accept-edits",
219
+ provider: modes.modelFor().provider,
220
+ model: modes.modelFor().model,
221
+ mode: modes.mode,
222
+ turns: 0, tokensIn: 0, tokensOut: 0, busy: false,
223
+ };
224
+ let run: AsyncGenerator<RunEvent> | null = null; let runAbort: AbortController | null = null; // port #21: one controller per run
225
+ let closed = false;
226
+ let resolveClosed: () => void = () => {};
227
+ const closedP = new Promise<void>((r) => { resolveClosed = r; });
228
+
229
+ const status = (): StatusInfo => {
230
+ const todos = todoLabel(join(sessionsDir, store.id)); // port #32: "todos done/total"; key omitted while the list is empty
231
+ return {
232
+ provider: state.provider, model: state.model, yolo: state.yolo, mode: state.mode,
233
+ permission: state.yolo ? "auto" : state.acceptEdits ? "accept-edits" : "ask",
234
+ effort: rt.effort,
235
+ turns: state.turns, tokensIn: state.tokensIn, tokensOut: state.tokensOut,
236
+ ...(todos !== undefined ? { todos } : {}),
237
+ };
238
+ };
239
+ const pushStatus = () => renderer.setStatus(status());
240
+
241
+ /** set once the surface owns the screen (below renderer.start); puts Node's warning printer back */
242
+ let restoreWarnings: (() => void) | undefined;
243
+ const close = () => {
244
+ if (closed) return;
245
+ closed = true;
246
+ // port #20 MED-2: /plan then quit resumes in plan (append is sync — lands pre-exit)
247
+ flushModeSwitch(modes, store);
248
+ runAbort?.abort(); // abort kills in-flight fetch/tools; return() settles the generator — kept, so the exit below waits for it
249
+ const settled = run?.return(undefined as never).then(() => undefined, () => undefined) ?? Promise.resolve();
250
+ rt.tasks.cancelAll(); // port #26: background children die with the surface, never after it
251
+ void rt.mcp?.close().catch(() => {}); // stop MCP child processes/connections
252
+ restoreWarnings?.(); // the screen is going away; Node's own printer is the right one again
253
+ renderer.stop();
254
+ // port #29: session_close fires ONCE, after the aborted run settled and its in-flight on_event
255
+ // taps drained (hooks.close() waits for those) — cmdRun's exit() order; the app promise
256
+ // resolves (and the process exits) only after it, so a quit never outruns the hook
257
+ void (async () => {
258
+ await settled;
259
+ await rt.hooks.close().catch(() => {});
260
+ // the sextant renderer's git children (repo watcher) must be GONE before the process exits — on
261
+ // Windows a live child holds its cwd, so a scratch repo removed at quit throws EBUSY (fee2e8c root
262
+ // cause). Optional: the Renderer seam stays untouched; FakeRenderer and pi-tui have no drain()
263
+ await (renderer as { drain?: () => Promise<void> }).drain?.()?.catch(() => {});
264
+ resolveClosed();
265
+ if (opts.exitOnClose !== false) process.exit(0);
266
+ })();
267
+ };
268
+
269
+ // both read the ACTIVE store live — /sessions and a root /rewind swap it (session-cmd.ts helpers)
270
+ const refreshUsage = () => { const u = lazySessionCmd().usageOf(store); state.tokensIn = u.tokensIn; state.tokensOut = u.tokensOut; };
271
+ const replayHistory = () => lazySessionCmd().replayTranscript(renderer, store);
272
+ // port #34: /attach context — the stage lives on the ACTIVE store (read live); the vision check uses the current mode's model
273
+ const attachCtx: AttachCtx = { renderer, cwd: rt.cwd, store: () => store, modelRef: () => modes.modelFor() };
274
+
275
+ const switchSession = (id: string, announce = true) => {
276
+ flushModeSwitch(modes, store); // port #20 MED-2: don't discard a pending switch on /sessions away
277
+ const pending = store.stagedAttachments; // port #34: the stage lives on the instance — re-staged on the new one below
278
+ store = new SessionStore(sessionsDir, id);
279
+ blocks = new BlockStore(join(sessionsDir, id, "memory"));
280
+ // rebind BOTH consumers: the memory tool AND the system prompt's memory block
281
+ // (critic finding: prompt kept reading the boot session's memory after /resume)
282
+ rt.setBlockStore(blocks);
283
+ rt.setSessionStore(store); // port #11: checkpoint entryId capture follows the active session
284
+ // port #20: the switched-to session resumes ITS last recorded mode
285
+ modes.restore(modeFromEntries(store.messages()) ?? modesCfg.defaultMode ?? "act");
286
+ const cur = modes.modelFor();
287
+ state.mode = modes.mode; state.model = cur.model; state.provider = cur.provider;
288
+ state.turns = 0;
289
+ replayHistory();
290
+ refreshUsage();
291
+ pushStatus();
292
+ if (announce) renderer.addSystemNote(`session ${id.slice(0, 8)} (${store.messages().length} messages)`);
293
+ carryOverAttachments(attachCtx, pending); // port #34: a swap must never lose staged images silently
294
+ };
295
+
296
+ // port #11: checkpoint command context (store/busy read live via closures)
297
+ const cpCtx: CheckpointCmdCtx = {
298
+ renderer,
299
+ busy: () => state.busy,
300
+ sessionId: () => store.id,
301
+ checkpointsFor: (sid) => rt.checkpointsFor(sid),
302
+ branchTo: (entryId) => store.branch(entryId),
303
+ replayAndRefresh: () => { replayHistory(); refreshUsage(); pushStatus(); },
304
+ };
305
+
306
+ // port #2: session navigation context (store read live via closure — /sessions and a root /rewind swap it)
307
+ const sessCtx: SessionCmdCtx = {
308
+ renderer,
309
+ sessionsDir,
310
+ busy: () => state.busy,
311
+ store: () => store,
312
+ switchSession,
313
+ replayHistory,
314
+ refreshUsage,
315
+ pushStatus,
316
+ };
317
+
318
+ // read-only info commands (info-cmd.ts) — store/blocks read live, the status slice is `state` itself
319
+ const infoCtx: InfoCmdCtx = {
320
+ renderer, rt, sessionsDir, catalog, state,
321
+ store: () => store, blocks: () => blocks,
322
+ commands: { builtin: TUI_COMMANDS, custom: custom.commands },
323
+ };
324
+
325
+ // port #30: custom command dispatch context (submit = the plain user-turn path, defined below)
326
+ const cmdCtx: CustomCommandCtx = { renderer, modes, state, pushStatus, submit: (t) => submit(t) };
327
+
328
+ // /model /models /provider (providers-cmd.ts) read the live registry; built lazily so pushStatus is bound
329
+ const provCtx = (): ProviderCmdCtx => ({ rt, modes, state, renderer, pushStatus });
330
+ /** `--save` writes the level the toggles just produced, so the next launch starts there;
331
+ * `--project` pins it to this checkout instead of to you. Without --save nothing is written —
332
+ * a toggle you meant for one run must not follow you into the next. */
333
+ const persistLevel = (arg: string): string => {
334
+ const words = arg.split(/\s+/).filter((w) => w.length > 0);
335
+ if (!words.includes("--save")) return "this session only — add --save to make it the default (--project pins it to this repo)";
336
+ const scope = words.includes("--project") ? "project" : "user";
337
+ const level: PermissionLevel = state.yolo ? "auto" : state.acceptEdits ? "accept-edits" : "ask";
338
+ try {
339
+ const path = saveSetting("permission", level, scope, rt.cwd);
340
+ return `saved: ${level} is the default now (${path})`;
341
+ } catch (e) {
342
+ return `could not save it: ${e instanceof Error ? e.message : String(e)}`;
343
+ }
344
+ };
345
+
346
+ /** what the CURRENT model's endpoint receives for a level — the /effort note's second line (providers/thinking.ts).
347
+ * Built like buildDef builds a ref (catalog reasoning flag), without touching the runtime's active model. */
348
+ const receives = (level: ThinkingEffort): string => {
349
+ const info = catalog.lookup(state.provider, state.model);
350
+ const ref: ModelRef = { provider: state.provider, model: state.model, effort: level, ...(info?.supportsReasoning !== undefined ? { reasoning: info.supportsReasoning } : {}) };
351
+ return thinkingLine(ref, rt.providers.get(state.provider)?.protocol ?? "openai", { shape: anthropicShapeFor(ref) });
352
+ };
353
+
354
+ const handleSlash = (text: string): boolean => {
355
+ const [cmd, ...rest] = text.slice(1).split(/\s+/);
356
+ const arg = rest.join(" ").trim();
357
+ switch (cmd) {
358
+ case "exit": case "quit": close(); return true;
359
+ case "help": lazyInfoCmd().cmdHelp(infoCtx); return true;
360
+ case "effort": {
361
+ const want = arg.trim();
362
+ if (want.length === 0) { renderer.addSystemNote(effortNote(rt.effort, receives(rt.effort))); return true; }
363
+ const level = parseEffort(want);
364
+ if (level === undefined) { renderer.addSystemNote(`"${want}" is not a level — ${THINKING_EFFORTS.join(" · ")}`, "warn"); return true; }
365
+ rt.setEffort(level);
366
+ renderer.addSystemNote(effortNote(level, receives(level)));
367
+ pushStatus(); return true;
368
+ }
369
+ case "accept-edits":
370
+ state.acceptEdits = !state.acceptEdits;
371
+ renderer.addSystemNote(state.yolo
372
+ ? `${acceptEditsNote(state.acceptEdits)} (auto mode is on, so nothing asks either way — /yolo turns it off)`
373
+ : acceptEditsNote(state.acceptEdits));
374
+ renderer.addSystemNote(persistLevel(arg));
375
+ pushStatus(); return true;
376
+ case "yolo":
377
+ state.yolo = !state.yolo;
378
+ renderer.addSystemNote(modeSwitchNote(state.yolo)); // "ask first" / "auto (never asks)" — the flag keeps its name
379
+ renderer.addSystemNote(persistLevel(arg));
380
+ pushStatus(); return true;
381
+ // port #20: model writes land in the CURRENT mode's slot (mirrored to both when
382
+ // planActSeparateModels is off); the selector may name another provider — the registry's
383
+ // dispatcher routes per call, so the switch needs no restart. --save persists the default.
384
+ case "model": cmdModelSwitch(provCtx(), arg); return true;
385
+ case "models": void cmdModels(provCtx(), arg); return true;
386
+ case "provider": void cmdProvider(provCtx(), arg); return true;
387
+ case "setup": void cmdSetup(provCtx()); return true; // guided connect: picker → model → key hand-off → test → default
388
+ // the same job on one line (cli/connect.ts through the live registry); bare, it hands over to /setup
389
+ case "connect": void cmdConnect(provCtx(), arg); return true;
390
+ case "plan": case "act":
391
+ togglePlanAct(modes, cmd as AgentMode, state, renderer, pushStatus);
392
+ return true;
393
+ case "checkpoints": void cmdCheckpoints(cpCtx); return true;
394
+ case "restore": void cmdRestore(cpCtx, arg); return true;
395
+ case "status": lazyInfoCmd().cmdStatus(infoCtx); return true;
396
+ case "cost": lazyInfoCmd().cmdCost(infoCtx, arg); return true;
397
+ case "skills": lazyInfoCmd().cmdSkills(infoCtx); return true;
398
+ case "memory": lazyInfoCmd().cmdMemory(infoCtx); return true;
399
+ case "todos": lazyInfoCmd().cmdTodos(infoCtx); return true; // port #32
400
+ case "tasks": lazyInfoCmd().cmdTasks(infoCtx, arg); return true; // port #26
401
+ case "new": lazySessionCmd().cmdNew(sessCtx); return true;
402
+ case "rewind": case "tree": void lazySessionCmd().cmdRewind(sessCtx); return true;
403
+ case "sessions": void lazySessionCmd().cmdSessions(sessCtx); return true;
404
+ case "resume":
405
+ if (arg) void lazySessionCmd().cmdSessions(sessCtx, arg); else void lazySessionCmd().cmdSessions(sessCtx);
406
+ return true;
407
+ case "export": lazyInfoCmd().cmdExport(infoCtx, arg); return true;
408
+ case "attach": cmdAttach(attachCtx, arg); return true; // port #34
409
+ case "paste": cmdPasteImage(attachCtx); return true; // clipboard image → attachment (⌃v)
410
+ case "mcp": void cmdMcp({ renderer, cwd: rt.cwd }, arg); return true; // the MCP market (mcp-cmd.ts): palette → approval card → mcp.json
411
+ default:
412
+ // port #30: a discovered custom command renders its template and submits it as a user turn.
413
+ // MED-2: it gets the RAW remainder of the line (whitespace runs and pasted newlines intact —
414
+ // renderCommand trims the ends itself); built-ins keep the collapsed `arg` above.
415
+ if (!dispatchCustomCommand(cmdCtx, custom.commands, cmd ?? "", text.slice(1 + (cmd ?? "").length))) renderer.addSystemNote(`unknown command: /${cmd} (try /help)`, "warn");
416
+ return true;
417
+ }
418
+ };
419
+
420
+ const startRun = async (goal: string) => {
421
+ const stream = rt.stream; // runtime already applied any opts.stream override
422
+ // live check: /provider add + /provider key (or `rovecode provider add` in another terminal) clears
423
+ // it for the next prompt — no restart
424
+ const reason = rt.noProviderReason();
425
+ if (!stream || reason !== null) {
426
+ renderer.addSystemNote(reason !== null ? noModelHint("tui") : "no provider stream", "error");
427
+ return;
428
+ }
429
+ state.busy = true;
430
+ renderer.setBusy(true, "thinking…");
431
+ pushStatus();
432
+ const level: PermissionLevel = state.yolo ? "auto" : state.acceptEdits ? "accept-edits" : "ask";
433
+ const cfg = rt.buildCfg(level, state.yolo ? undefined : async (req) => {
434
+ // port #24: edit/write approvals carry a bounded unified diff of the pending change
435
+ // (in-memory preview; any failure degrades to the plain overlay, never blocks the ask)
436
+ const isEdit = req.tool === "edit" || req.tool === "write";
437
+ // `all edits` pressed DURING this run: the rules were built before it, so the switch is honored
438
+ // here too — otherwise the mode would only start at the next prompt, which is not what the
439
+ // button says. The rules still gate the call; this only skips the card.
440
+ if (isEdit && state.acceptEdits) return "once";
441
+ let detail: string | undefined;
442
+ if (isEdit) {
443
+ try { detail = previewDiff(req.tool as "edit" | "write", req.revisedArgs, rt.cwd).text || undefined; } catch { detail = undefined; }
444
+ }
445
+ const answer = await renderer.askApproval(req.tool, JSON.stringify(req.revisedArgs).slice(0, 140), detail);
446
+ if (answer !== "all-edits") return answer;
447
+ // the surface-level door: flip the session and let THIS call through once. The core approval
448
+ // engine stays a three-verdict system — "all-edits" never crosses into it.
449
+ state.acceptEdits = true;
450
+ renderer.addSystemNote(acceptEditsNote(true));
451
+ pushStatus();
452
+ return "once";
453
+ });
454
+ // port #20: per-mode model resolution + plan-mode rule/prompt enforcement
455
+ const cur = modes.modelFor();
456
+ const def = rt.buildDef({ provider: cur.provider, model: cur.model });
457
+ applyModeToRun(modes, cfg, def);
458
+ const views = new Map<string, AssistantView>();
459
+ let lastView: AssistantView | null = null;
460
+ runAbort = new AbortController();
461
+ rt.tasks.bindRun(runAbort.signal); // port #26: Esc/quit cancel the background tasks THIS run starts; a normal end leaves them running
462
+ run = agentLoop(def, goal, {}, cfg, {
463
+ stream, registry: rt.registry, store,
464
+ tools: rt.registry.list().map((t) => t.schema),
465
+ guard: rt.guard, planReminder: rt.planReminder, signal: runAbort.signal, // port #21: Esc aborts this run's controller
466
+ cwd: rt.cwd, // cwd must be threaded — tools resolve relative paths against it, same as checkpoints/LSP/preview
467
+ hooks: rt.hooks, // port #29: pre_tool/approval/post_tool at dispatch, pre_run/compaction/post_run/on_event via the loop observer
468
+ }, steering);
469
+ try {
470
+ for await (const ev of run) {
471
+ renderer.onEvent?.(ev); // port #44: FIRST — the sextant reducer is its rows' source of truth; the calls below are duplicates it ignores while busy
472
+ if (ev.type === "turn_start") { resetTurnFailureCount(); state.turns++; pushStatus(); } // pushStatus here + in the finally also refreshes the port #32 todo label after a todo_write
473
+ else if (ev.type === "message_update") {
474
+ let v = views.get(ev.messageId);
475
+ if (!v) { v = renderer.beginAssistant(); views.set(ev.messageId, v); lastView?.done(); lastView = v; }
476
+ v.append(ev.delta);
477
+ } else if (ev.type === "tool_execution_start") {
478
+ renderer.toolStart(ev.callId, ev.tool, JSON.stringify(ev.args).slice(0, 120));
479
+ } else if (ev.type === "tool_execution_update") {
480
+ renderer.toolUpdate(ev.callId, ev.note);
481
+ } else if (ev.type === "tool_execution_end") {
482
+ renderer.toolEnd(ev.callId, ev.ok, ev.output.slice(0, 160).replace(/\n/g, " ⏎ "), ev.durationMs);
483
+ } else if (ev.type === "tool_call_failed") {
484
+ renderer.toolEnd(ev.callId, false, `${ev.reason}: ${ev.detail}`.slice(0, 160), 0);
485
+ } else if (ev.type === "compaction") {
486
+ renderer.addSystemNote(compactionNote(ev)); // one wording with the replayed marker (port #25 LOW-4)
487
+ } else if (ev.type === "steer") {
488
+ renderer.addSystemNote("↪ steering applied");
489
+ } else if (ev.type === "verify") {
490
+ // the verify gate (core/verify-gate.ts): say that the check is running — a silent two minutes reads as a hang
491
+ renderer.addSystemNote(ev.state === "running" ? `⧗ verify: ${ev.command.slice(0, 120)}` : `verify ${ev.state}: ${ev.detail ?? ""}`, ev.state === "running" || ev.state === "passed" ? "info" : "warn");
492
+ } else if (ev.type === "run_end") {
493
+ lastView?.done();
494
+ if (ev.status === "error") renderer.addSystemNote(ev.summary, "error");
495
+ else if (ev.status !== "done") renderer.addSystemNote(`run ${ev.status}: ${ev.summary}`, "warn");
496
+ else if (ev.outstanding) { const c = outstandingClause(ev.outstanding); if (c !== null) renderer.addSystemNote(`done · ${c}`, outstandingTone(ev.outstanding)); } // "done" ≠ finished: say what was left (core/loop.ts)
497
+ // if the model produced no streaming deltas, surface the final text
498
+ if (views.size === 0 && ev.status === "done" && ev.summary) {
499
+ const v = renderer.beginAssistant(); v.append(ev.summary); v.done();
500
+ }
501
+ }
502
+ }
503
+ } finally {
504
+ run = null; runAbort = null;
505
+ state.busy = false;
506
+ refreshUsage();
507
+ // port #14: surface any fallback-chain advances the router made during the run
508
+ for (const n of rt.drainRouterNotes()) renderer.addSystemNote(n, "warn");
509
+ renderer.setBusy(false);
510
+ pushStatus();
511
+ }
512
+ };
513
+
514
+ /** A plain user turn — also the path custom commands submit their rendered prompt through (port #30). */
515
+ const submit = (text: string): Promise<void> => {
516
+ // port #34: text is required. The editor drops an empty Enter before onSubmit (pi-renderer.ts),
517
+ // but a custom command whose template renders to "" (`/ask` on a bare `$ARGUMENTS`) lands here:
518
+ // an empty goal never starts a run, is never queued as a steer, and leaves the stage (and any
519
+ // pending mode switch) for the next real message
520
+ if (!text.trim()) {
521
+ const n = store.stagedAttachments.length;
522
+ renderer.addSystemNote(n === 0 ? "nothing to send — the message is empty" : `type a message to send with the attached image${n === 1 ? "" : "s"}`, "warn");
523
+ return Promise.resolve();
524
+ }
525
+ renderer.addUser(userTurnLine(text, store.stagedAttachments)); // port #34: image chips under the text — the stage folds into this message
526
+ // port #20: a pending mode switch becomes a durable session entry on the next
527
+ // submit (round-trip cancellation: toggling back before submitting records nothing)
528
+ flushModeSwitch(modes, store);
529
+ if (state.busy) { steering.push(text); renderer.addSystemNote(`queued as steering (applies before the next model turn)${queuedAttachNote(store)}`); return Promise.resolve(); }
530
+ return startRun(text);
531
+ };
532
+ // port #44: a renderer with panels (sextant) reads the runtime through this handle — once, before start()
533
+ _trace("renderer.attach");
534
+ renderer.attach?.(buildSextantAttach({ cwd: rt.cwd, sessionsDir, store: () => store, tasks: rt.tasks, model: () => modes.modelFor(), catalog, runtime: () => rt, petName: opts.pet }));
535
+ // /model suggestions: the ids of every configured provider's models, fetched off the boot path and again
536
+ // whenever the registry changes; the sextant reads the list at suggestion time (SlashCommand.choices)
537
+ const modelChoices: string[] = [];
538
+ const refreshModelChoices = (): void => { void listModelIds(rt.providers).then((ids) => { modelChoices.splice(0, modelChoices.length, ...ids); }).catch(() => {}); };
539
+ renderer.setCommands([...TUI_COMMANDS.map((c) => (c.name === MODEL_COMMAND.name ? { ...c, choices: () => modelChoices } : c)), ...commandsForPalette(custom.commands)]);
540
+ setTimeout(refreshModelChoices, 0);
541
+ // Node prints warnings on stderr, and stderr goes straight onto the alternate screen. A
542
+ // MaxListenersExceededWarning does not just say its sentence — it dumps the emitter it is complaining
543
+ // about, which for a stream is pages of `[Function: …]`, over the panels. Berkay hit exactly that
544
+ // during a Playwright MCP session (the leak itself is fixed in mcp/client.ts; this is the other half:
545
+ // no warning from anywhere should be able to garble the screen). Node's own printer is removed and the
546
+ // warning becomes a note — still said, never drawn over anything — and put back on the way out.
547
+ const nodeWarnListeners = process.listeners("warning");
548
+ // assigned here, read by `close` above (declared before this point, called only after start)
549
+ process.removeAllListeners("warning");
550
+ const onWarning = (w: Error): void => {
551
+ // the first line only: a MaxListenersExceededWarning's body is the emitter it is complaining about
552
+ const first = w.message.split("\n")[0] ?? w.message;
553
+ renderer.addSystemNote(`node: ${w.name === "Warning" ? "" : `${w.name}: `}${first}`, "warn");
554
+ };
555
+ process.on("warning", onWarning);
556
+ restoreWarnings = (): void => {
557
+ process.off("warning", onWarning);
558
+ for (const l of nodeWarnListeners) process.on("warning", l as (w: Error) => void);
559
+ };
560
+ _trace("renderer.start");
561
+ renderer.start({
562
+ onSubmit: (text) => { if (text.startsWith("/")) handleSlash(text); else void submit(text); },
563
+ // port #21: abort FIRST (kills in-flight fetch/subprocesses), then return() settles the generator
564
+ onInterrupt: () => { runAbort?.abort(); void run?.return(undefined as never); renderer.addSystemNote("run interrupted", "warn"); },
565
+ onExit: close,
566
+ });
567
+ rt.warmRepoMap(); // the repo map builds on the next tick, behind this first frame, not inside the first submit (runtime.ts)
568
+ // resumed boot: restore the transcript and usage counters (a bare session open left both blank)
569
+ if (boot.id !== undefined) { replayHistory(); refreshUsage(); }
570
+ // the welcome card (core/voice.ts): a fresh session opens with rovecode's card — connected, or the /setup
571
+ // pointer when no model is configured; a resumed session keeps its transcript and gets one line
572
+ const connected = rt.stream && rt.noProviderReason() === null ? { provider: state.provider, model: state.model } : null;
573
+ if (boot.id !== undefined) renderer.addSystemNote(resumedLine(store.id, rt.cwd, state.yolo));
574
+ else {
575
+ // What the card says about this session is counted, not assumed: skills and plugins are already
576
+ // loaded by now, and MCP servers are the entries the runtime actually accepted (an unfilled or
577
+ // untrusted one is not in this number, which is the point — the card must not claim it).
578
+ const loaded = { skills: rt.skillStore.list().length, plugins: rt.plugins.found.filter((p) => p.status === "active").length, mcp: rt.mcp?.serverNames().length ?? 0 };
579
+ renderer.addSystemNote(welcomeCard({ connected, cwd: rt.cwd, yolo: state.yolo, mode: state.mode, version: pkg.version, loaded, width: process.stdout.columns ?? 80 }));
580
+ // The update check is fire-and-forget on purpose: it never blocks the card, never throws, and says
581
+ // nothing at all unless there is genuinely a newer release (core/update-check.ts). A startup screen
582
+ // that reports its own plumbing every time teaches people to stop reading it.
583
+ void checkForUpdate(pkg.version).then((s) => { const l = updateLine(s); if (l !== null) renderer.addSystemNote(l); }).catch(() => {});
584
+ }
585
+ if (boot.warn) renderer.addSystemNote(boot.warn, "warn");
586
+ for (const w of custom.warnings) renderer.addSystemNote(w, "warn"); // port #30: skipped/shadowed command files
587
+ for (const w of rt.providers.warnings()) renderer.addSystemNote(`providers: ${w}`, "warn"); // malformed providers.json entries
588
+ rt.hooks.onWarning((w) => renderer.addSystemNote(`hooks: ${w}`, "warn")); // port #29: hook load/runtime notes (buffered ones replay first)
589
+ rt.plugins.onWarning((w) => renderer.addSystemNote(`plugins: ${w}`, "warn")); // plugin discovery/activation notes, the same way
590
+ const pluginLine = summarizePlugins(rt.plugins.found); // one line when there is at least one plugin: what loaded, what stayed off
591
+ if (pluginLine !== null) renderer.addSystemNote(pluginLine);
592
+ // a retry notice while the backoff waits ("anthropic: overloaded — retrying in 4 s (2/4)"), not after the run: the
593
+ // drain in the run's finally still runs and finds nothing once this listener exists
594
+ rt.onRouterNote((n) => renderer.addSystemNote(n, "warn"));
595
+ pushStatus();
596
+ watchProviders(provCtx()); // follow a default-model change made elsewhere; announce the first provider
597
+ // port #27: an unavailable configured rung (probe failed) is a clean one-line startup
598
+ // error — stop the renderer first so the terminal is restored, reap the MCP children
599
+ // construction spawned (LOW-3, as bootRuntime does), then exit 2 (embedders: rethrow)
600
+ await rt.sandbox.ready.catch(async (e: unknown) => {
601
+ renderer.stop();
602
+ await rt.hooks.close().catch(() => {}); // port #29: a runtime exists (session_open fired) — session_close before this exit too
603
+ await rt.mcp?.close().catch(() => {});
604
+ if (e instanceof SandboxConfigError && opts.exitOnClose !== false) { console.error(`error: ${e.message}`); process.exit(2); }
605
+ throw e;
606
+ });
607
+ await closedP;
608
+ }