rovecode 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (334) hide show
  1. package/LICENSE +662 -0
  2. package/README.md +737 -0
  3. package/THIRD_PARTY_NOTICES.md +268 -0
  4. package/bin/rovecode.ts +21 -0
  5. package/package.json +56 -0
  6. package/src/acp/server.ts +374 -0
  7. package/src/cli/auth-login.ts +122 -0
  8. package/src/cli/connect.ts +244 -0
  9. package/src/cli/context-cmd.ts +199 -0
  10. package/src/cli/dispatch.ts +82 -0
  11. package/src/cli/doctor.ts +362 -0
  12. package/src/cli/export.ts +276 -0
  13. package/src/cli/help.ts +293 -0
  14. package/src/cli/is-tui-invocation.ts +8 -0
  15. package/src/cli/main.ts +583 -0
  16. package/src/cli/market-cmd.ts +658 -0
  17. package/src/cli/mcp-login.ts +141 -0
  18. package/src/cli/mcp-market-cmd.ts +302 -0
  19. package/src/cli/output.ts +382 -0
  20. package/src/cli/repl.ts +250 -0
  21. package/src/cli/repomap-root.ts +14 -0
  22. package/src/cli/resume.ts +57 -0
  23. package/src/cli/run-flags.ts +43 -0
  24. package/src/cli/run-limits.ts +78 -0
  25. package/src/cli/runtime.ts +931 -0
  26. package/src/cli/session-arg.ts +30 -0
  27. package/src/cli/sessions-cmd.ts +145 -0
  28. package/src/cli/setup.ts +153 -0
  29. package/src/cli/skills-cmd.ts +194 -0
  30. package/src/cli/start-chat.ts +65 -0
  31. package/src/cli/trust-cmd.ts +52 -0
  32. package/src/coding/bash.ts +148 -0
  33. package/src/coding/checkpoints.ts +327 -0
  34. package/src/coding/diff.ts +138 -0
  35. package/src/coding/files.ts +341 -0
  36. package/src/coding/hashline.ts +274 -0
  37. package/src/coding/lsp-gate.ts +254 -0
  38. package/src/coding/lsp-servers.ts +147 -0
  39. package/src/coding/lsp.ts +283 -0
  40. package/src/coding/repomap-cache.ts +99 -0
  41. package/src/coding/repomap-files.ts +192 -0
  42. package/src/coding/repomap.ts +481 -0
  43. package/src/core/agents.ts +255 -0
  44. package/src/core/compaction.ts +259 -0
  45. package/src/core/config.ts +289 -0
  46. package/src/core/context-report.ts +228 -0
  47. package/src/core/context.ts +60 -0
  48. package/src/core/count-remote.ts +107 -0
  49. package/src/core/execpolicy-rules.ts +196 -0
  50. package/src/core/execpolicy.ts +385 -0
  51. package/src/core/executor.ts +454 -0
  52. package/src/core/guardrails.ts +400 -0
  53. package/src/core/hooks.ts +411 -0
  54. package/src/core/images.ts +230 -0
  55. package/src/core/intro.ts +266 -0
  56. package/src/core/loop.ts +567 -0
  57. package/src/core/modes.ts +372 -0
  58. package/src/core/orchestrator.ts +245 -0
  59. package/src/core/proc-group.ts +48 -0
  60. package/src/core/project-trust.ts +98 -0
  61. package/src/core/reflection.ts +165 -0
  62. package/src/core/sandbox-config.ts +186 -0
  63. package/src/core/session-id.ts +24 -0
  64. package/src/core/session-images.ts +73 -0
  65. package/src/core/session-ops.ts +183 -0
  66. package/src/core/session-text.ts +29 -0
  67. package/src/core/session.ts +469 -0
  68. package/src/core/settings.ts +170 -0
  69. package/src/core/tasks.ts +646 -0
  70. package/src/core/token-scale.ts +108 -0
  71. package/src/core/tools.ts +309 -0
  72. package/src/core/trust.ts +104 -0
  73. package/src/core/types.ts +330 -0
  74. package/src/core/update-check.ts +171 -0
  75. package/src/core/usage.ts +204 -0
  76. package/src/core/validate.ts +121 -0
  77. package/src/core/verify-gate.ts +159 -0
  78. package/src/core/verify.ts +236 -0
  79. package/src/core/voice.ts +158 -0
  80. package/src/core/win-job.ts +183 -0
  81. package/src/core/workspace.ts +184 -0
  82. package/src/design/audit.ts +797 -0
  83. package/src/design/direction.ts +190 -0
  84. package/src/design/rules.ts +157 -0
  85. package/src/eval/bench.ts +150 -0
  86. package/src/eval/gauntlet-runner.ts +215 -0
  87. package/src/eval/gauntlet-support.ts +84 -0
  88. package/src/eval/gauntlet-wave3.ts +269 -0
  89. package/src/eval/gauntlet-wave4.ts +217 -0
  90. package/src/eval/gauntlet.ts +253 -0
  91. package/src/index.ts +17 -0
  92. package/src/lanes/agy.ts +95 -0
  93. package/src/lanes/approval.ts +24 -0
  94. package/src/lanes/claude.ts +129 -0
  95. package/src/lanes/codex.ts +127 -0
  96. package/src/lanes/events.ts +130 -0
  97. package/src/lanes/job.ts +142 -0
  98. package/src/lanes/opencode.ts +122 -0
  99. package/src/lanes/process.ts +184 -0
  100. package/src/lanes/progress.ts +183 -0
  101. package/src/lanes/registry.ts +178 -0
  102. package/src/lanes/runner.ts +124 -0
  103. package/src/lanes/types.ts +112 -0
  104. package/src/market/catalogs/mcp-docs.json +111 -0
  105. package/src/market/catalogs/plugins.json +111 -0
  106. package/src/market/catalogs/skills.json +478 -0
  107. package/src/market/clone.ts +72 -0
  108. package/src/market/context-cost.ts +121 -0
  109. package/src/market/digest.ts +106 -0
  110. package/src/market/index.ts +22 -0
  111. package/src/market/install.ts +578 -0
  112. package/src/market/manifest.ts +187 -0
  113. package/src/market/prereq.ts +145 -0
  114. package/src/market/registry.ts +363 -0
  115. package/src/market/resolve.ts +111 -0
  116. package/src/market/types.ts +236 -0
  117. package/src/market/validate.ts +227 -0
  118. package/src/mcp/client.ts +449 -0
  119. package/src/mcp/config.ts +252 -0
  120. package/src/mcp/local-package.ts +211 -0
  121. package/src/mcp/market-catalog.ts +84 -0
  122. package/src/mcp/market-install.ts +289 -0
  123. package/src/mcp/market.ts +362 -0
  124. package/src/mcp/oauth.ts +251 -0
  125. package/src/mcp/prompts-resources.ts +249 -0
  126. package/src/mcp/shared.ts +149 -0
  127. package/src/mcp/status.ts +67 -0
  128. package/src/mcp/tools.ts +275 -0
  129. package/src/mcp/transport.ts +122 -0
  130. package/src/mcp/trust.ts +25 -0
  131. package/src/memory/blocks.ts +278 -0
  132. package/src/memory/recall.ts +355 -0
  133. package/src/memory/scope.ts +182 -0
  134. package/src/memory/store.ts +105 -0
  135. package/src/memory/tools.ts +99 -0
  136. package/src/plugins/cli.ts +119 -0
  137. package/src/plugins/discover.ts +108 -0
  138. package/src/plugins/index.ts +50 -0
  139. package/src/plugins/install.ts +184 -0
  140. package/src/plugins/load.ts +124 -0
  141. package/src/plugins/manifest.ts +92 -0
  142. package/src/plugins/state.ts +83 -0
  143. package/src/providers/auth.ts +408 -0
  144. package/src/providers/cache.ts +223 -0
  145. package/src/providers/catalog-local.ts +160 -0
  146. package/src/providers/catalog.ts +421 -0
  147. package/src/providers/middleware-context.ts +86 -0
  148. package/src/providers/middleware.ts +373 -0
  149. package/src/providers/model-list.ts +23 -0
  150. package/src/providers/models-index.json +1 -0
  151. package/src/providers/oauth/common.ts +105 -0
  152. package/src/providers/oauth/device-code.ts +107 -0
  153. package/src/providers/oauth/github-copilot.ts +146 -0
  154. package/src/providers/oauth/loopback.ts +158 -0
  155. package/src/providers/oauth/openai.ts +163 -0
  156. package/src/providers/oauth/openrouter.ts +89 -0
  157. package/src/providers/oauth/pkce.ts +45 -0
  158. package/src/providers/oauth/registry.ts +39 -0
  159. package/src/providers/oauth/seam.ts +89 -0
  160. package/src/providers/profile-glm53.ts +111 -0
  161. package/src/providers/profile-sonnet5-persona.ts +65 -0
  162. package/src/providers/profile-sonnet5-voice.ts +23 -0
  163. package/src/providers/profiles.ts +156 -0
  164. package/src/providers/provider-config.ts +311 -0
  165. package/src/providers/registry.ts +333 -0
  166. package/src/providers/responses.ts +209 -0
  167. package/src/providers/retry.ts +234 -0
  168. package/src/providers/router.ts +294 -0
  169. package/src/providers/sse.ts +26 -0
  170. package/src/providers/stream-errors.ts +117 -0
  171. package/src/providers/stream.ts +566 -0
  172. package/src/providers/thinking.ts +189 -0
  173. package/src/providers/wire-messages.ts +129 -0
  174. package/src/providers/wire-responses.ts +79 -0
  175. package/src/providers/wire-select.ts +53 -0
  176. package/src/server/http.ts +291 -0
  177. package/src/server/openapi.ts +246 -0
  178. package/src/sextant/card-hits.ts +102 -0
  179. package/src/sextant/card-keys.ts +55 -0
  180. package/src/sextant/context-source.ts +157 -0
  181. package/src/sextant/crew-cards.ts +350 -0
  182. package/src/sextant/draw-agents.ts +273 -0
  183. package/src/sextant/draw-code.ts +388 -0
  184. package/src/sextant/draw-context.ts +222 -0
  185. package/src/sextant/draw-frame.ts +164 -0
  186. package/src/sextant/draw-market.ts +573 -0
  187. package/src/sextant/draw-messages.ts +386 -0
  188. package/src/sextant/draw-pet.ts +230 -0
  189. package/src/sextant/draw-plan.ts +187 -0
  190. package/src/sextant/draw-tabs.ts +85 -0
  191. package/src/sextant/draw-util.ts +65 -0
  192. package/src/sextant/draw-wizard.ts +378 -0
  193. package/src/sextant/engine.ts +230 -0
  194. package/src/sextant/frame-hits.ts +25 -0
  195. package/src/sextant/frame.ts +101 -0
  196. package/src/sextant/git-status.ts +197 -0
  197. package/src/sextant/grid.ts +59 -0
  198. package/src/sextant/input.ts +119 -0
  199. package/src/sextant/keys.ts +521 -0
  200. package/src/sextant/layout.ts +86 -0
  201. package/src/sextant/local-commands.ts +169 -0
  202. package/src/sextant/market-source.ts +287 -0
  203. package/src/sextant/mentions.ts +200 -0
  204. package/src/sextant/message-hits.ts +26 -0
  205. package/src/sextant/model.ts +387 -0
  206. package/src/sextant/overlays.ts +456 -0
  207. package/src/sextant/panel-hits.ts +38 -0
  208. package/src/sextant/pet.ts +399 -0
  209. package/src/sextant/screen.ts +324 -0
  210. package/src/sextant/scroll-hits.ts +66 -0
  211. package/src/sextant/scrollbar.ts +82 -0
  212. package/src/sextant/sextant-bridge.ts +174 -0
  213. package/src/sextant/sextant-cards.ts +142 -0
  214. package/src/sextant/sextant-diff-base.ts +63 -0
  215. package/src/sextant/sextant-files.ts +154 -0
  216. package/src/sextant/sextant-frame-loop.ts +335 -0
  217. package/src/sextant/sextant-renderer.ts +574 -0
  218. package/src/sextant/sextant-repo.ts +140 -0
  219. package/src/sextant/theme.ts +66 -0
  220. package/src/sextant/tool-rows.ts +189 -0
  221. package/src/sextant/types.ts +493 -0
  222. package/src/skills/index.ts +387 -0
  223. package/src/skills/pack.ts +220 -0
  224. package/src/skills/spec.ts +162 -0
  225. package/src/skills/tools.ts +69 -0
  226. package/src/skills/versioned.ts +227 -0
  227. package/src/telemetry/otel-export.ts +122 -0
  228. package/src/telemetry/otel-lanes.ts +89 -0
  229. package/src/telemetry/otel-logs.ts +131 -0
  230. package/src/telemetry/otel-metrics.ts +136 -0
  231. package/src/telemetry/otel.ts +397 -0
  232. package/src/telemetry/otlp.ts +76 -0
  233. package/src/tools/ask-user.ts +156 -0
  234. package/src/tools/bash-bg.ts +94 -0
  235. package/src/tools/bash-jobs.ts +237 -0
  236. package/src/tools/design.ts +151 -0
  237. package/src/tools/evalcell.ts +338 -0
  238. package/src/tools/html-text.ts +139 -0
  239. package/src/tools/provider.ts +149 -0
  240. package/src/tools/task.ts +250 -0
  241. package/src/tools/todo.ts +320 -0
  242. package/src/tools/webfetch.ts +332 -0
  243. package/src/tools/websearch.ts +359 -0
  244. package/src/tui/agents-cmd.ts +41 -0
  245. package/src/tui/app.ts +749 -0
  246. package/src/tui/attach.ts +127 -0
  247. package/src/tui/boot-notes.ts +41 -0
  248. package/src/tui/builtin-prompts.ts +59 -0
  249. package/src/tui/checkpoints-cmd.ts +70 -0
  250. package/src/tui/clipboard-image.ts +81 -0
  251. package/src/tui/clipboard.ts +78 -0
  252. package/src/tui/commands.ts +283 -0
  253. package/src/tui/config-view.ts +53 -0
  254. package/src/tui/context-cmds.ts +282 -0
  255. package/src/tui/cost.ts +108 -0
  256. package/src/tui/crash-guard.ts +173 -0
  257. package/src/tui/focus-terminal.ts +34 -0
  258. package/src/tui/git-cmds.ts +273 -0
  259. package/src/tui/git-plain.ts +58 -0
  260. package/src/tui/info-cmd.ts +150 -0
  261. package/src/tui/input-plain.ts +76 -0
  262. package/src/tui/mcp-cmd.ts +128 -0
  263. package/src/tui/memory-note.ts +77 -0
  264. package/src/tui/modes-cmd.ts +45 -0
  265. package/src/tui/notify-seq.ts +100 -0
  266. package/src/tui/notify.ts +318 -0
  267. package/src/tui/overlays.ts +97 -0
  268. package/src/tui/pi-renderer.ts +428 -0
  269. package/src/tui/providers-cmd.ts +377 -0
  270. package/src/tui/reasoning-view.ts +56 -0
  271. package/src/tui/renderer.ts +128 -0
  272. package/src/tui/replay-marker.ts +29 -0
  273. package/src/tui/session-cmd.ts +148 -0
  274. package/src/tui/session-manage.ts +95 -0
  275. package/src/tui/sextant-attach.ts +102 -0
  276. package/src/tui/sextant-io.ts +202 -0
  277. package/src/tui/sextant-smoke.ts +110 -0
  278. package/src/tui/shell-cmd.ts +158 -0
  279. package/src/tui/smoke.ts +72 -0
  280. package/src/tui/staged-terminal.ts +50 -0
  281. package/src/tui/startup.ts +12 -0
  282. package/src/tui/theme.ts +59 -0
  283. package/src/tui/todo-label.ts +7 -0
  284. package/src/tui/trust-card.ts +107 -0
  285. package/src/tui/tui-commands.ts +87 -0
  286. package/tsconfig.json +30 -0
  287. package/vendor/pi-tui/LICENSE +21 -0
  288. package/vendor/pi-tui/PATCHES.md +12 -0
  289. package/vendor/pi-tui/PROVENANCE.md +12 -0
  290. package/vendor/pi-tui/README.upstream.md +854 -0
  291. package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  292. package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  293. package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
  294. package/vendor/pi-tui/src/autocomplete.ts +827 -0
  295. package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
  296. package/vendor/pi-tui/src/components/box.ts +138 -0
  297. package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
  298. package/vendor/pi-tui/src/components/editor.ts +2364 -0
  299. package/vendor/pi-tui/src/components/h-stack.ts +45 -0
  300. package/vendor/pi-tui/src/components/image.ts +128 -0
  301. package/vendor/pi-tui/src/components/input.ts +448 -0
  302. package/vendor/pi-tui/src/components/loader.ts +93 -0
  303. package/vendor/pi-tui/src/components/markdown.ts +1016 -0
  304. package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
  305. package/vendor/pi-tui/src/components/select-list.ts +230 -0
  306. package/vendor/pi-tui/src/components/settings-list.ts +277 -0
  307. package/vendor/pi-tui/src/components/spacer.ts +29 -0
  308. package/vendor/pi-tui/src/components/stack.ts +155 -0
  309. package/vendor/pi-tui/src/components/text.ts +108 -0
  310. package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
  311. package/vendor/pi-tui/src/components/v-stack.ts +34 -0
  312. package/vendor/pi-tui/src/editor-component.ts +75 -0
  313. package/vendor/pi-tui/src/fuzzy.ts +138 -0
  314. package/vendor/pi-tui/src/index.ts +149 -0
  315. package/vendor/pi-tui/src/keybindings.ts +321 -0
  316. package/vendor/pi-tui/src/keys.ts +1402 -0
  317. package/vendor/pi-tui/src/kill-ring.ts +47 -0
  318. package/vendor/pi-tui/src/latex.ts +1381 -0
  319. package/vendor/pi-tui/src/layout-node.ts +52 -0
  320. package/vendor/pi-tui/src/layout.ts +411 -0
  321. package/vendor/pi-tui/src/native-modifiers.ts +60 -0
  322. package/vendor/pi-tui/src/native-module-path.ts +32 -0
  323. package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
  324. package/vendor/pi-tui/src/terminal-colors.ts +74 -0
  325. package/vendor/pi-tui/src/terminal-image.ts +701 -0
  326. package/vendor/pi-tui/src/terminal.ts +554 -0
  327. package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
  328. package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
  329. package/vendor/pi-tui/src/tui.ts +1264 -0
  330. package/vendor/pi-tui/src/undo-stack.ts +29 -0
  331. package/vendor/pi-tui/src/utils.ts +1327 -0
  332. package/vendor/pi-tui/src/word-navigation.ts +118 -0
  333. package/vendor/pi-tui/test/test-themes.ts +39 -0
  334. package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
@@ -0,0 +1,428 @@
1
+ /** PiTuiRenderer — the Renderer seam implemented over the vendored pi-tui library.
2
+ *
3
+ * Wiring ported from upstream pi (earendil-works/pi, pinned 853a80d2):
4
+ * - chat layout / transcript splice-before-editor / loader-while-responding:
5
+ * packages/tui/test/chat-simple.ts (splice at children.length-1, editor last & focused)
6
+ * - CancellableLoader Escape-to-abort: packages/tui/src/components/cancellable-loader.ts
7
+ * Vendor quirk: TruncatedText has no setText (its text is private), so setStatus
8
+ * swaps the status-line instance in tui.children instead of mutating it.
9
+ *
10
+ * Only this module, overlays.ts (card bodies) and theme.ts may import from vendor/pi-tui.
11
+ */
12
+
13
+ import {
14
+ CancellableLoader,
15
+ CombinedAutocompleteProvider,
16
+ type Component,
17
+ Editor,
18
+ Key,
19
+ Markdown,
20
+ matchesKey,
21
+ ProcessTerminal,
22
+ type SelectItem,
23
+ SelectList,
24
+ type Terminal,
25
+ Text,
26
+ TruncatedText,
27
+ type TUI,
28
+ TuiMainScreen,
29
+ } from "../../vendor/pi-tui/src/index.ts";
30
+ import type {
31
+ ApprovalAnswer,
32
+ AssistantView,
33
+ PickItem,
34
+ QuestionAnswer,
35
+ QuestionPrompt,
36
+ Renderer,
37
+ RendererHooks,
38
+ RendererStartOptions,
39
+ SlashCommand,
40
+ StatusInfo,
41
+ } from "./renderer.ts";
42
+ import { ApprovalCard, FREE_TEXT, QuestionCard, SKIP_QUESTION } from "./overlays.ts";
43
+ import { modeLabelShort } from "../core/voice.ts";
44
+ import { expandMentions, mentionsIn, splitAttached } from "../sextant/mentions.ts";
45
+ import { existsSync } from "node:fs";
46
+ import { join } from "node:path";
47
+ import { rovecodeEditorTheme, rovecodeMarkdownTheme, rovecodeSelectListTheme, pal, st } from "./theme.ts";
48
+
49
+ /** Tool cards stay single-line: collapse whitespace and clip to ~120 columns. */
50
+ import { StagedTerminal } from "./staged-terminal.ts";
51
+
52
+ const CARD_MAX = 120;
53
+ function oneLine(text: string, max = CARD_MAX): string {
54
+ const flat = text.replace(/\s+/g, " ").trim();
55
+ return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
56
+ }
57
+
58
+ interface ToolCard {
59
+ line: Text;
60
+ tool: string;
61
+ base: string;
62
+ }
63
+
64
+ // requestRender() from seeding must not leak a frame while the loading intro still owns stdout.
65
+ class PreparingTui extends TuiMainScreen {
66
+ private holding = true;
67
+ override requestRender(force = false): void { if (!this.holding) super.requestRender(force); }
68
+ reveal(): void { this.holding = false; this.start(); }
69
+ }
70
+
71
+ export interface PiTuiRendererOptions {
72
+ /** Defaults to ProcessTerminal; tests inject a VirtualTerminal. */
73
+ terminal?: Terminal;
74
+ /** Working directory for file autocomplete; defaults to process.cwd(). */
75
+ cwd?: string;
76
+ }
77
+
78
+ export class PiTuiRenderer implements Renderer {
79
+ private readonly terminal: Terminal;
80
+ private readonly cwd: string;
81
+ private commands: SlashCommand[] = [];
82
+ private tui: TUI | null = null;
83
+ private editor: Editor | null = null;
84
+ private statusLine: TruncatedText | null = null;
85
+ private statusText = "";
86
+ private loader: CancellableLoader | null = null;
87
+ private hooks: RendererHooks | null = null;
88
+ private unsubscribeInput: (() => void) | null = null;
89
+ private readonly toolCards = new Map<string, ToolCard>();
90
+ /** cancel thunks for overlays awaiting an answer; drained on stop() */
91
+ private readonly pendingPickers = new Set<() => void>();
92
+
93
+ constructor(opts?: PiTuiRendererOptions) {
94
+ this.terminal = opts?.terminal ?? new ProcessTerminal();
95
+ this.cwd = opts?.cwd ?? process.cwd();
96
+ }
97
+ private ui(): TUI {
98
+ if (!this.tui) throw new Error("PiTuiRenderer: start() must be called first");
99
+ return this.tui;
100
+ }
101
+
102
+ private requireEditor(): Editor {
103
+ if (!this.editor) throw new Error("PiTuiRenderer: start() must be called first");
104
+ return this.editor;
105
+ }
106
+
107
+ /** Children order: header … transcript … [loader] … editor … status line.
108
+ * New transcript items land after existing ones: before the loader when busy,
109
+ * otherwise directly before the editor (chat-simple.ts pattern). */
110
+ private insertTranscript(component: Component): void {
111
+ // after stop() (e.g. a run's finally block racing user exit) mutations are no-ops
112
+ const tui = this.tui;
113
+ const anchor = this.loader ?? this.editor;
114
+ if (!tui || !anchor) return;
115
+ const idx = tui.children.indexOf(anchor);
116
+ tui.children.splice(idx < 0 ? tui.children.length : idx, 0, component);
117
+ tui.requestRender();
118
+ }
119
+
120
+ start(hooks: RendererHooks, options?: RendererStartOptions): void | Promise<void> {
121
+ if (this.tui) return;
122
+ this.hooks = hooks;
123
+ const staged = options?.beforeReveal ? new StagedTerminal(this.terminal) : undefined;
124
+ const tui = staged ? new PreparingTui(staged) : new TuiMainScreen(this.terminal);
125
+ this.tui = tui;
126
+
127
+ // header banner
128
+ tui.addChild(new Text(st.dim("rovecode"), 1, 0));
129
+
130
+ // editor (kept just above the status line; transcript is spliced in before it)
131
+ const editor = new Editor(tui, rovecodeEditorTheme);
132
+ this.editor = editor;
133
+ editor.onSubmit = (value: string) => {
134
+ const text = value.trim();
135
+ if (!text) return;
136
+ // `@file` (sextant/mentions.ts): this editor's autocomplete offers files after `@`, so the mention must do
137
+ // something here too. No file list on this surface, so a mention is the exact cwd-relative path — what
138
+ // the autocomplete inserts. Every refusal and cap is said as a note, never swallowed.
139
+ const mentions = mentionsIn(text);
140
+ if (mentions.length === 0) { hooks.onSubmit(text); return; }
141
+ const r = expandMentions(text, { cwd: this.cwd, mentions, resolve: (m) => (existsSync(join(this.cwd, m)) ? m.replace(/\\/g, "/") : null) });
142
+ for (const n of r.notes) this.addSystemNote(n, "warn");
143
+ hooks.onSubmit(r.text);
144
+ };
145
+ editor.setAutocompleteProvider(new CombinedAutocompleteProvider(this.commands, this.cwd));
146
+ tui.addChild(editor);
147
+
148
+ // status line (always last)
149
+ const status = new TruncatedText(this.statusText, 1, 0);
150
+ this.statusLine = status;
151
+ tui.addChild(status);
152
+
153
+ tui.setFocus(editor);
154
+
155
+ // Input listeners run before the focused component (tui.ts handleTerminalInput),
156
+ // so Ctrl+C always exits and Escape reaches the loader while the editor keeps focus.
157
+ this.unsubscribeInput = tui.addInputListener((data) => {
158
+ if (matchesKey(data, Key.ctrl("c"))) {
159
+ hooks.onExit();
160
+ return { consume: true };
161
+ }
162
+ if (this.loader && !tui.hasOverlay() && matchesKey(data, Key.escape)) {
163
+ this.loader.handleInput(data); // abort → onAbort → hooks.onInterrupt()
164
+ return { consume: true };
165
+ }
166
+ return undefined;
167
+ });
168
+
169
+ options?.beforeFirstRender?.();
170
+ if (staged && tui instanceof PreparingTui && options?.beforeReveal) {
171
+ staged.prepare(() => tui.renderNow(true));
172
+ return options.beforeReveal().then(() => {
173
+ if (this.tui !== tui) return; // cancelled while the intro was visible
174
+ staged.prepare(() => tui.renderNow(true)); // latest terminal size, transcript and status
175
+ staged.reveal(() => tui.reveal(), options.onReveal);
176
+ });
177
+ }
178
+ options?.onReveal?.();
179
+ tui.start();
180
+ }
181
+
182
+ stop(): void {
183
+ const tui = this.tui;
184
+ if (!tui) return;
185
+ if (this.loader) {
186
+ this.loader.stop();
187
+ this.loader = null;
188
+ }
189
+ if (this.unsubscribeInput) {
190
+ this.unsubscribeInput();
191
+ this.unsubscribeInput = null;
192
+ }
193
+ // an overlay that never gets an answer must not pin the process (or a caller
194
+ // awaiting it) forever — a stopped UI cannot answer, so cancel them all
195
+ for (const cancel of this.pendingPickers) cancel();
196
+ this.pendingPickers.clear();
197
+ tui.stop();
198
+ this.tui = null;
199
+ this.editor = null;
200
+ this.statusLine = null;
201
+ this.hooks = null;
202
+ this.toolCards.clear();
203
+ }
204
+
205
+ setCommands(cmds: SlashCommand[]): void {
206
+ this.commands = cmds.slice();
207
+ if (this.editor) {
208
+ this.editor.setAutocompleteProvider(new CombinedAutocompleteProvider(this.commands, this.cwd));
209
+ }
210
+ }
211
+
212
+ addUser(text: string): void {
213
+ // the typed line and one row per @file attached — the read blocks stay in the session, not on screen
214
+ const { text: typed, files } = splitAttached(text);
215
+ this.insertTranscript(new Text(st.dim("> ") + typed, 1, 0));
216
+ for (const f of files) this.insertTranscript(new Text(st.dim(` ▤ attached ${f}`), 1, 0));
217
+ }
218
+
219
+ addSystemNote(text: string, tone: "info" | "warn" | "error" = "info"): void {
220
+ const paint = tone === "warn" ? pal.warn : tone === "error" ? pal.err : pal.muted;
221
+ this.insertTranscript(new Text(paint(text), 1, 0));
222
+ }
223
+
224
+ beginAssistant(): AssistantView {
225
+ const tui = this.tui;
226
+ if (!tui) return { append: () => {}, done: () => {} };
227
+ const md = new Markdown("", 1, 1, rovecodeMarkdownTheme);
228
+ this.insertTranscript(md);
229
+ let buffer = "";
230
+ return {
231
+ append: (delta: string) => {
232
+ buffer += delta;
233
+ md.setText(buffer);
234
+ tui.requestRender();
235
+ },
236
+ done: () => {
237
+ /* buffer already flushed on each append */
238
+ },
239
+ };
240
+ }
241
+
242
+ toolStart(callId: string, tool: string, argsPreview: string): void {
243
+ const base = oneLine(`→ ${tool} ${argsPreview}`);
244
+ const line = new Text(st.dim(base), 1, 0);
245
+ this.toolCards.set(callId, { line, tool, base });
246
+ this.insertTranscript(line);
247
+ }
248
+
249
+ toolUpdate(callId: string, note: string): void {
250
+ const card = this.toolCards.get(callId);
251
+ if (!card) return;
252
+ card.line.setText(st.dim(oneLine(`${card.base} · ${note}`)));
253
+ this.tui?.requestRender();
254
+ }
255
+
256
+ toolEnd(callId: string, ok: boolean, outputPreview: string, durationMs: number): void {
257
+ const card = this.toolCards.get(callId);
258
+ if (!card) return;
259
+ this.toolCards.delete(callId);
260
+ const paint = ok ? pal.ok : pal.err;
261
+ const verdict = ok ? "ok" : "FAIL";
262
+ card.line.setText(paint(oneLine(`← ${verdict} ${card.tool} (${durationMs}ms) ${outputPreview}`)));
263
+ this.tui?.requestRender();
264
+ }
265
+
266
+ /** Show `component` as a centered overlay whose keys route to `list`; settles on select,
267
+ * cancel, or stop() (a stopped UI cannot answer — pendingPickers drains it to null). */
268
+ private pickWith(component: Component, list: SelectList, width: number): Promise<string | null> {
269
+ const tui = this.tui;
270
+ const editor = this.editor;
271
+ if (!tui || !editor) return Promise.resolve(null);
272
+ return new Promise<string | null>((resolve) => {
273
+ const handle = tui.showOverlay(component, { width, anchor: "center" });
274
+ let settled = false;
275
+ const finish = (answer: string | null): void => {
276
+ if (settled) return;
277
+ settled = true;
278
+ this.pendingPickers.delete(cancel);
279
+ handle.hide();
280
+ if (this.tui) { tui.setFocus(editor); tui.requestRender(); }
281
+ resolve(answer);
282
+ };
283
+ const cancel = () => finish(null);
284
+ this.pendingPickers.add(cancel);
285
+ list.onSelect = (item: SelectItem) => finish(item.value);
286
+ list.onCancel = () => finish(null);
287
+ });
288
+ }
289
+
290
+ pickOne(items: PickItem[], title?: string): Promise<string | null> {
291
+ if (!this.tui || items.length === 0) return Promise.resolve(null);
292
+ if (title) this.addSystemNote(title);
293
+ const list = new SelectList(
294
+ items.map((i) => ({ value: i.value, label: i.label, description: i.description })),
295
+ Math.min(items.length, 8),
296
+ rovecodeSelectListTheme,
297
+ );
298
+ return this.pickWith(list, list, 64);
299
+ }
300
+
301
+ async askApproval(tool: string, argsPreview: string, detail?: string): Promise<ApprovalAnswer> {
302
+ this.addSystemNote(`approval needed: ${tool} ${argsPreview}`, "warn");
303
+ const items: PickItem[] = [
304
+ { value: "once", label: "allow once", description: "run this call only" },
305
+ { value: "always", label: "always", description: "allow this tool for the session" },
306
+ { value: "deny", label: "deny", description: "reject this call" },
307
+ ];
308
+ let picked: string | null;
309
+ if (detail) {
310
+ // port #24: the diff rides inside the overlay; same list, same keys as the plain path
311
+ const list = new SelectList(items, items.length, rovecodeSelectListTheme);
312
+ const card = new ApprovalCard(`approval needed: ${tool}`, detail.split("\n"), list, () => this.terminal.rows);
313
+ picked = await this.pickWith(card, list, Math.max(40, Math.min(this.terminal.columns - 4, 100)));
314
+ } else picked = await this.pickOne(items);
315
+ return picked === "once" || picked === "always" ? picked : "deny"; // null/cancel/stop → deny
316
+ }
317
+
318
+ /** Port #33 question overlay, modal like approval (keys go to the card). Escape while a run is
319
+ * in flight (loader showing) INTERRUPTS the run — stopping beats letting the model proceed
320
+ * unanswered — and the run's abort signal then dismisses the card; Escape with no run (idle
321
+ * caller) declines → null; inside the free-text input Escape steps back to the options. */
322
+ askQuestion(q: QuestionPrompt, signal?: AbortSignal): Promise<QuestionAnswer | null> {
323
+ const tui = this.tui;
324
+ const editor = this.editor;
325
+ if (!tui || !editor || signal?.aborted) return Promise.resolve(null); // nobody to ask / run already gone
326
+ // concurrent-ask decision: ONE modal at a time — a second ask is a caller bug surfaced loudly, not queued
327
+ if (tui.hasOverlay()) return Promise.reject(new Error("a question or approval overlay is already open"));
328
+ const options = q.options ?? [];
329
+ const freeText = q.allowFreeText !== false;
330
+ const items: SelectItem[] = options.map((label, i) => ({ value: String(i), label }));
331
+ if (freeText) items.push({ value: FREE_TEXT, label: "type an answer…" });
332
+ items.push({ value: SKIP_QUESTION, label: "skip this question" }); // non-destructive decline → null; Escape on a busy run still stops the run
333
+ const list = new SelectList(items, Math.max(1, items.length), rovecodeSelectListTheme);
334
+ const card = new QuestionCard(q.question, list, options.length, freeText, () => this.terminal.rows, () => this.loader !== null);
335
+ this.addSystemNote(`question: ${oneLine(q.question)}`, "warn"); // transcript record, like approvals
336
+ return new Promise<QuestionAnswer | null>((resolve) => {
337
+ const handle = tui.showOverlay(card, { width: Math.max(40, Math.min(this.terminal.columns - 4, 100)), anchor: "center" });
338
+ let settled = false;
339
+ const finish = (answer: QuestionAnswer | null): void => {
340
+ if (settled) return;
341
+ settled = true;
342
+ this.pendingPickers.delete(cancel);
343
+ signal?.removeEventListener("abort", cancel);
344
+ handle.hide();
345
+ if (this.tui) { tui.setFocus(editor); tui.requestRender(); }
346
+ resolve(answer);
347
+ };
348
+ const cancel = () => finish(null);
349
+ this.pendingPickers.add(cancel);
350
+ signal?.addEventListener("abort", cancel, { once: true }); // the run's abort (port #21) dismisses the card
351
+ const escape = () => { if (this.loader) this.hooks?.onInterrupt(); else finish(null); };
352
+ list.onCancel = escape;
353
+ list.onSelect = (item: SelectItem) => {
354
+ if (item.value === SKIP_QUESTION) { finish(null); return; } // declined: the tool reports "user declined to answer"
355
+ if (item.value === FREE_TEXT) { card.setTyping(true); tui.requestRender(); return; }
356
+ const choice = Number(item.value);
357
+ finish({ choice, label: options[choice] });
358
+ };
359
+ card.input.onEscape = () => { if (options.length > 0) { card.setTyping(false); tui.requestRender(); } else escape(); };
360
+ card.input.onSubmit = (value: string) => { const text = value.trim(); if (text) finish({ text }); };
361
+ });
362
+ }
363
+
364
+ /** Remove every transcript item (keep header, loader, editor, status) — history replay. */
365
+ clearTranscript(): void {
366
+ const tui = this.tui;
367
+ const anchor = this.loader ?? this.editor;
368
+ if (!tui || !anchor) return;
369
+ const end = tui.children.indexOf(anchor);
370
+ if (end > 1) tui.children.splice(1, end - 1); // index 0 = header banner
371
+ this.toolCards.clear();
372
+ tui.requestRender();
373
+ }
374
+
375
+ prefillEditor(text: string): void {
376
+ const tui = this.tui;
377
+ const editor = this.editor;
378
+ if (!tui || !editor) return;
379
+ editor.setText(text);
380
+ tui.setFocus(editor);
381
+ tui.requestRender();
382
+ }
383
+
384
+ setBusy(busy: boolean, label?: string): void {
385
+ const tui = this.tui;
386
+ if (!tui) { this.loader = null; return; }
387
+ if (busy) {
388
+ const message = label ?? "thinking…";
389
+ if (this.loader) {
390
+ this.loader.setMessage(message);
391
+ return;
392
+ }
393
+ const loader = new CancellableLoader(tui, (s) => pal.accent(s), (s) => st.dim(s), message);
394
+ loader.onAbort = () => { this.hooks?.onInterrupt(); };
395
+ this.loader = loader;
396
+ const anchor = this.editor;
397
+ const idx = anchor ? tui.children.indexOf(anchor) : -1;
398
+ tui.children.splice(idx < 0 ? tui.children.length : idx, 0, loader);
399
+ loader.start();
400
+ tui.requestRender();
401
+ } else {
402
+ const loader = this.loader;
403
+ if (!loader) return;
404
+ this.loader = null;
405
+ loader.stop();
406
+ const idx = tui.children.indexOf(loader);
407
+ if (idx >= 0) tui.children.splice(idx, 1);
408
+ tui.requestRender();
409
+ }
410
+ }
411
+
412
+ setStatus(info: StatusInfo): void {
413
+ const gate = modeLabelShort(info.yolo); // "ask first" | "auto" — the flags keep their names
414
+ const mode = info.mode ? `${info.mode} · ` : ""; // port #20 mode indicator
415
+ this.statusText = st.dim(
416
+ `${mode}${info.provider}/${info.model} · ${gate} · turns ${info.turns} · tokens ${info.tokensIn}/${info.tokensOut}${info.todos ? ` · ${info.todos}` : ""}`, // port #32 todo progress
417
+ );
418
+ const tui = this.tui;
419
+ if (!tui || !this.statusLine) return; // applied at start()
420
+ // vendor quirk: TruncatedText has no setText — swap the instance in place.
421
+ const next = new TruncatedText(this.statusText, 1, 0);
422
+ const idx = tui.children.indexOf(this.statusLine);
423
+ if (idx >= 0) tui.children.splice(idx, 1, next);
424
+ else tui.children.push(next);
425
+ this.statusLine = next;
426
+ tui.requestRender();
427
+ }
428
+ }