@robota-sdk/agent-ui-terminal 3.0.0-beta.100

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 (348) hide show
  1. package/CHANGELOG.md +1204 -0
  2. package/LICENSE +661 -0
  3. package/README.md +50 -0
  4. package/dist/node/index.d.ts +1224 -0
  5. package/dist/node/index.d.ts.map +1 -0
  6. package/dist/node/index.js +33 -0
  7. package/dist/node/index.js.map +1 -0
  8. package/package.json +88 -0
  9. package/src/App.tsx +157 -0
  10. package/src/AppPresentation.tsx +190 -0
  11. package/src/AppView.tsx +26 -0
  12. package/src/BackgroundTaskPanel.tsx +73 -0
  13. package/src/CjkTextInput.tsx +336 -0
  14. package/src/ConfirmPrompt.tsx +140 -0
  15. package/src/ContextWarningBanner.tsx +45 -0
  16. package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
  17. package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
  18. package/src/HistorySearchOverlay.tsx +150 -0
  19. package/src/InputArea.tsx +338 -0
  20. package/src/ListPicker.tsx +158 -0
  21. package/src/MenuSelect.tsx +159 -0
  22. package/src/MessageList.tsx +308 -0
  23. package/src/MultiSelectList.tsx +294 -0
  24. package/src/PendingActionPrompt.tsx +121 -0
  25. package/src/PermissionPrompt.tsx +236 -0
  26. package/src/PluginTUI.tsx +258 -0
  27. package/src/RoleLabel.tsx +72 -0
  28. package/src/SafeText.tsx +66 -0
  29. package/src/SessionEventNotices.tsx +27 -0
  30. package/src/SessionPicker.tsx +92 -0
  31. package/src/SessionStatusBar.tsx +80 -0
  32. package/src/SlashAutocomplete.tsx +154 -0
  33. package/src/StatusBar.tsx +308 -0
  34. package/src/StreamingIndicator.tsx +145 -0
  35. package/src/SupervisedSessionView.tsx +872 -0
  36. package/src/TextPrompt.tsx +112 -0
  37. package/src/ThemePicker.tsx +366 -0
  38. package/src/ToolCommandOutput.tsx +48 -0
  39. package/src/ToolDiffBlock.tsx +44 -0
  40. package/src/TransportTUI.tsx +166 -0
  41. package/src/TuiInteractionChannel.ts +530 -0
  42. package/src/UpdateNotice.tsx +18 -0
  43. package/src/UsageSummaryEntry.tsx +49 -0
  44. package/src/WaveText.tsx +70 -0
  45. package/src/__tests__/ListPicker.test.tsx +188 -0
  46. package/src/__tests__/MenuSelect.test.tsx +127 -0
  47. package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
  48. package/src/__tests__/PluginTUI.test.tsx +167 -0
  49. package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
  50. package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
  51. package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
  52. package/src/__tests__/TextPrompt.test.tsx +112 -0
  53. package/src/__tests__/TransportTUI.test.tsx +124 -0
  54. package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
  55. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
  56. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
  57. package/src/__tests__/UpdateNotice.test.tsx +15 -0
  58. package/src/__tests__/__fixtures__/korean-text.json +17 -0
  59. package/src/__tests__/abort-after-permission.test.tsx +160 -0
  60. package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
  61. package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
  62. package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
  63. package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
  64. package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
  65. package/src/__tests__/background-task-panel.test.tsx +98 -0
  66. package/src/__tests__/background-task-row-format.test.ts +152 -0
  67. package/src/__tests__/channel-factory-integration.test.ts +150 -0
  68. package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
  69. package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
  70. package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
  71. package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
  72. package/src/__tests__/cjk-text-input.test.ts +196 -0
  73. package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
  74. package/src/__tests__/command-output-summary.test.ts +95 -0
  75. package/src/__tests__/command-result-handler.test.ts +171 -0
  76. package/src/__tests__/compact-event-bridge.test.ts +20 -0
  77. package/src/__tests__/confirm-permission-flow.test.ts +149 -0
  78. package/src/__tests__/confirm-prompt.test.tsx +103 -0
  79. package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
  80. package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
  81. package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
  82. package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
  83. package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
  84. package/src/__tests__/external-prompt-notice.test.tsx +100 -0
  85. package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
  86. package/src/__tests__/fixtures/fake-editor.sh +7 -0
  87. package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
  88. package/src/__tests__/fork-attach.test.tsx +235 -0
  89. package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
  90. package/src/__tests__/helpers/terminal-profiles.ts +152 -0
  91. package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
  92. package/src/__tests__/history-clear-broadcast.test.ts +66 -0
  93. package/src/__tests__/history-search-overlay.test.tsx +329 -0
  94. package/src/__tests__/humanize-tool-name.test.ts +30 -0
  95. package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
  96. package/src/__tests__/input-area-flow.test.ts +199 -0
  97. package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
  98. package/src/__tests__/interaction-channel-charter.test.ts +13 -0
  99. package/src/__tests__/key-hint-consistency.test.tsx +249 -0
  100. package/src/__tests__/key-hint-footer.test.tsx +69 -0
  101. package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
  102. package/src/__tests__/message-list-rendering.test.tsx +474 -0
  103. package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
  104. package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
  105. package/src/__tests__/output-style-reaches-session.test.ts +35 -0
  106. package/src/__tests__/palette-consistency.test.ts +329 -0
  107. package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
  108. package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
  109. package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
  110. package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
  111. package/src/__tests__/product-display-name.test.tsx +79 -0
  112. package/src/__tests__/prompt-queue.test.tsx +260 -0
  113. package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
  114. package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
  115. package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
  116. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
  117. package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
  118. package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
  119. package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
  120. package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
  121. package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
  122. package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
  123. package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
  124. package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
  125. package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
  126. package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
  127. package/src/__tests__/pty/isolated-home.ts +97 -0
  128. package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
  129. package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
  130. package/src/__tests__/pty/pty-driver.ts +155 -0
  131. package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
  132. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
  133. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
  134. package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
  135. package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
  136. package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
  137. package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
  138. package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
  139. package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
  140. package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
  141. package/src/__tests__/pty/spawn-pty.test.ts +120 -0
  142. package/src/__tests__/pty/spawn-pty.ts +229 -0
  143. package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
  144. package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
  145. package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
  146. package/src/__tests__/pty-driver-dispose.test.ts +43 -0
  147. package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
  148. package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
  149. package/src/__tests__/render-channel-options.test.ts +201 -0
  150. package/src/__tests__/render-lifecycle.test.ts +36 -0
  151. package/src/__tests__/render-markdown.test.ts +164 -0
  152. package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
  153. package/src/__tests__/safe-text-boundary.test.tsx +75 -0
  154. package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
  155. package/src/__tests__/screen-reader-input.test.tsx +106 -0
  156. package/src/__tests__/screen-reader-labels.test.ts +91 -0
  157. package/src/__tests__/screen-reader-menus.test.tsx +200 -0
  158. package/src/__tests__/screen-reader-pacing.test.ts +230 -0
  159. package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
  160. package/src/__tests__/screen-reader-render-options.test.ts +262 -0
  161. package/src/__tests__/screen-reader-stdout.test.ts +322 -0
  162. package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
  163. package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
  164. package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
  165. package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
  166. package/src/__tests__/selection-flow.test.ts +61 -0
  167. package/src/__tests__/session-event-notices.test.tsx +31 -0
  168. package/src/__tests__/session-init-poller.test.ts +102 -0
  169. package/src/__tests__/session-picker.test.tsx +93 -0
  170. package/src/__tests__/session-switch-channel.test.tsx +470 -0
  171. package/src/__tests__/shutdown-signal.test.ts +34 -0
  172. package/src/__tests__/status-activity.test.ts +71 -0
  173. package/src/__tests__/status-bar.test.tsx +290 -0
  174. package/src/__tests__/status-glyph.test.ts +30 -0
  175. package/src/__tests__/streaming-indicator.test.tsx +178 -0
  176. package/src/__tests__/terminal-capabilities.test.ts +153 -0
  177. package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
  178. package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
  179. package/src/__tests__/text-prompt-flow.test.ts +77 -0
  180. package/src/__tests__/theme-picker.test.tsx +345 -0
  181. package/src/__tests__/transport-boundary.test.ts +11 -0
  182. package/src/__tests__/tui-app-channel-port.test.ts +31 -0
  183. package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
  184. package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
  185. package/src/__tests__/tui-interaction-queues.test.ts +149 -0
  186. package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
  187. package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
  188. package/src/__tests__/tui-state-manager.test.ts +482 -0
  189. package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
  190. package/src/__tests__/wave-text.test.tsx +133 -0
  191. package/src/__tests__/wire-tui-channel.test.ts +1457 -0
  192. package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
  193. package/src/app-banner.tsx +42 -0
  194. package/src/app-static-items.ts +35 -0
  195. package/src/app-view-model.ts +140 -0
  196. package/src/attached-session-connection.ts +14 -0
  197. package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
  198. package/src/attention/__tests__/attention-tracker.test.ts +103 -0
  199. package/src/attention/__tests__/countdown.test.ts +15 -0
  200. package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
  201. package/src/attention/__tests__/interval-recap.test.ts +49 -0
  202. package/src/attention/attention-coordinator.ts +71 -0
  203. package/src/attention/attention-tracker.ts +124 -0
  204. package/src/attention/countdown.ts +24 -0
  205. package/src/attention/focus-input-filter.ts +131 -0
  206. package/src/attention/interval-recap.ts +148 -0
  207. package/src/attention/time-units.ts +6 -0
  208. package/src/attention-bell.ts +79 -0
  209. package/src/attributed-user-echo.ts +30 -0
  210. package/src/background-task-row-format.ts +99 -0
  211. package/src/command-interaction.ts +8 -0
  212. package/src/command-output-summary.ts +129 -0
  213. package/src/create-default-tui-cli-adapter.ts +45 -0
  214. package/src/execution-workspace-switcher-selection.ts +118 -0
  215. package/src/execution-workspace-view-model.ts +139 -0
  216. package/src/external-prompt-notice.tsx +38 -0
  217. package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
  218. package/src/flows/__tests__/defer-submit.test.ts +88 -0
  219. package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
  220. package/src/flows/background-focus-flow.ts +44 -0
  221. package/src/flows/cjk-cursor-motion.ts +75 -0
  222. package/src/flows/cjk-text-deletion.ts +53 -0
  223. package/src/flows/cjk-text-input-flow.ts +248 -0
  224. package/src/flows/cjk-text-input-types.ts +24 -0
  225. package/src/flows/confirm-prompt-flow.ts +90 -0
  226. package/src/flows/defer-submit.ts +67 -0
  227. package/src/flows/fork-attach-flow.ts +62 -0
  228. package/src/flows/input-area-flow.ts +188 -0
  229. package/src/flows/permission-prompt-flow.ts +108 -0
  230. package/src/flows/real-cursor-flow.ts +83 -0
  231. package/src/flows/selection-flow.ts +205 -0
  232. package/src/flows/session-init-poller.ts +76 -0
  233. package/src/flows/text-prompt-flow.ts +93 -0
  234. package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
  235. package/src/history-search/history-search-flow.ts +94 -0
  236. package/src/history-search/useHistorySearch.ts +336 -0
  237. package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
  238. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
  239. package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
  240. package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
  241. package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
  242. package/src/hooks/command-result-handler.ts +44 -0
  243. package/src/hooks/side-effects-types.ts +45 -0
  244. package/src/hooks/useAppController.ts +271 -0
  245. package/src/hooks/useAppInputBindings.ts +163 -0
  246. package/src/hooks/useAppInteractionState.ts +84 -0
  247. package/src/hooks/useAppLifecycleState.ts +81 -0
  248. package/src/hooks/useAppOverlays.ts +90 -0
  249. package/src/hooks/useAppScreenState.ts +81 -0
  250. package/src/hooks/useAppSubmissionState.ts +45 -0
  251. package/src/hooks/useAppThemeState.ts +157 -0
  252. package/src/hooks/useAppWorkspaceState.ts +149 -0
  253. package/src/hooks/useAppearanceSettings.ts +30 -0
  254. package/src/hooks/useAutocomplete.ts +101 -0
  255. package/src/hooks/useBackgroundPanel.ts +58 -0
  256. package/src/hooks/useCountdownTick.ts +24 -0
  257. package/src/hooks/useExecutionDetailPage.ts +69 -0
  258. package/src/hooks/useForkAttach.ts +60 -0
  259. package/src/hooks/useInputAreaHistorySearch.ts +72 -0
  260. package/src/hooks/useInputAreaKeys.ts +114 -0
  261. package/src/hooks/useNumberedSelection.ts +93 -0
  262. package/src/hooks/usePluginCallbacks.ts +38 -0
  263. package/src/hooks/usePluginScreenData.ts +85 -0
  264. package/src/hooks/useRealCursorPosition.ts +125 -0
  265. package/src/hooks/useRenderMarkdown.ts +38 -0
  266. package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
  267. package/src/hooks/useSideEffects.ts +144 -0
  268. package/src/hooks/useStatusLineSettings.ts +50 -0
  269. package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
  270. package/src/hooks/useTuiChannel.ts +141 -0
  271. package/src/humanize-tool-name.ts +51 -0
  272. package/src/index.ts +72 -0
  273. package/src/input-area-rules.tsx +71 -0
  274. package/src/json-value.ts +18 -0
  275. package/src/key-hint-footer.tsx +54 -0
  276. package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
  277. package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
  278. package/src/keybindings/keybinding-catalogue.ts +171 -0
  279. package/src/keybindings/keybinding-registry.ts +303 -0
  280. package/src/keybindings/keybinding-syntax.ts +155 -0
  281. package/src/keybindings/keybindings-context.tsx +124 -0
  282. package/src/keybindings/node-keybindings-source.ts +189 -0
  283. package/src/numbered-list.tsx +113 -0
  284. package/src/plugin-tui-handlers.ts +163 -0
  285. package/src/product-display-name-context.tsx +30 -0
  286. package/src/render-markdown.ts +209 -0
  287. package/src/render.tsx +746 -0
  288. package/src/sanitize-terminal-text.ts +171 -0
  289. package/src/screen-reader-announcement.ts +52 -0
  290. package/src/screen-reader-context.tsx +32 -0
  291. package/src/screen-reader-labels.ts +64 -0
  292. package/src/screen-reader-pacing-context.tsx +47 -0
  293. package/src/screen-reader-pacing.ts +178 -0
  294. package/src/screen-reader-stdout.ts +328 -0
  295. package/src/short-session-id.ts +9 -0
  296. package/src/shutdown-signal.ts +34 -0
  297. package/src/slash-command-input.ts +11 -0
  298. package/src/status-activity.ts +70 -0
  299. package/src/status-glyph.ts +78 -0
  300. package/src/terminal-capabilities-context.tsx +27 -0
  301. package/src/terminal-capabilities.ts +77 -0
  302. package/src/terminal-focus-reporting.ts +50 -0
  303. package/src/terminal-handoff-controller.ts +98 -0
  304. package/src/terminal-marks.ts +63 -0
  305. package/src/theme/__tests__/built-in-themes.test.ts +117 -0
  306. package/src/theme/__tests__/color-vision.test.ts +82 -0
  307. package/src/theme/__tests__/theme-context.test.tsx +146 -0
  308. package/src/theme/__tests__/theme-document.test.ts +222 -0
  309. package/src/theme/__tests__/theme-registry.test.ts +138 -0
  310. package/src/theme/__tests__/theme-styles.test.ts +233 -0
  311. package/src/theme/built-in-themes.ts +342 -0
  312. package/src/theme/color-vision.ts +171 -0
  313. package/src/theme/index.ts +45 -0
  314. package/src/theme/theme-context.tsx +81 -0
  315. package/src/theme/theme-contracts.ts +139 -0
  316. package/src/theme/theme-document.ts +278 -0
  317. package/src/theme/theme-registry.ts +127 -0
  318. package/src/theme/theme-styles.ts +177 -0
  319. package/src/tool-summary-status.ts +59 -0
  320. package/src/tui-app-channel-port.ts +111 -0
  321. package/src/tui-channel-lifecycle-coordinator.ts +149 -0
  322. package/src/tui-channel-options.ts +184 -0
  323. package/src/tui-cli-adapter-context.tsx +13 -0
  324. package/src/tui-cli-adapter.ts +24 -0
  325. package/src/tui-interaction-queues.ts +171 -0
  326. package/src/tui-session-binding.ts +52 -0
  327. package/src/tui-session-event-projector.ts +149 -0
  328. package/src/tui-session-events.ts +89 -0
  329. package/src/tui-session-notice-store.ts +43 -0
  330. package/src/tui-session-options.ts +134 -0
  331. package/src/tui-state-manager.ts +347 -0
  332. package/src/types.ts +21 -0
  333. package/src/use-terminal-title.ts +29 -0
  334. package/src/utils/__tests__/edit-diff.test.ts +426 -0
  335. package/src/utils/__tests__/paste-detection.test.ts +116 -0
  336. package/src/utils/__tests__/paste-labels.test.ts +46 -0
  337. package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
  338. package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
  339. package/src/utils/edit-diff.ts +151 -0
  340. package/src/utils/input-top-border.ts +19 -0
  341. package/src/utils/paste-labels.ts +9 -0
  342. package/src/utils/tool-call-extractor.ts +92 -0
  343. package/src/utils/tool-diff-summary.ts +75 -0
  344. package/src/waiting-loop-stop-notice.ts +23 -0
  345. package/src/wire-history-sync.ts +159 -0
  346. package/src/wire-tui-channel.ts +963 -0
  347. package/src/wire-tui-client-commands.ts +142 -0
  348. package/src/wire-tui-projection.ts +136 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * TransportTUI — interactive overlay for transport enable/disable settings.
3
+ *
4
+ * Arrow keys navigate the list, space toggles enabled/disabled, enter/esc closes.
5
+ *
6
+ * TRANS-009: a toggle SAVES a setting. It does not start or stop anything — `registry.setEnabled`
7
+ * writes the settings file and returns, and `startAll` reads `getEnabled()` when a session starts, so
8
+ * the change takes effect at the next start. This component used to render `[enabled]` on the way
9
+ * back from that write, which told the user a transport was running when nothing had been started.
10
+ *
11
+ * The badge therefore names the SAVED setting and the footer says when it applies. And a failed write
12
+ * is rendered rather than swallowed: the previous `.catch(() => setSaving(false))` took the error and
13
+ * discarded it, so a settings file that could not be written looked exactly like a successful save
14
+ * with the row unchanged.
15
+ */
16
+
17
+ import { Box } from 'ink';
18
+ import React, { useState, useCallback } from 'react';
19
+
20
+ import { KeyHintFooter } from './key-hint-footer.js';
21
+ import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
22
+ import { useProductDisplayName } from './product-display-name-context.js';
23
+ import { Text } from './SafeText.js';
24
+ import { usePalette } from './theme/index.js';
25
+
26
+ import type {
27
+ ITransportEntry,
28
+ ITransportSettingsRegistryView,
29
+ } from '@robota-sdk/agent-interface-transport';
30
+
31
+ const TRANSPORT_NAME_WIDTH = 18;
32
+
33
+ interface IEntryRowProps {
34
+ entry: ITransportEntry;
35
+ selected: boolean;
36
+ }
37
+
38
+ function TransportEntryRow({ entry, selected }: IEntryRowProps): React.ReactElement {
39
+ const palette = usePalette();
40
+ const enabled = entry.config.enabled;
41
+ const dot = enabled ? '●' : '○';
42
+ // The saved setting, not a running state — see the file header.
43
+ const badge = enabled ? '[on] ' : '[off]';
44
+ const portOpt = entry.config.options?.port;
45
+ const portHint = typeof portOpt === 'number' ? `port: ${portOpt}` : '';
46
+ return (
47
+ <Box>
48
+ <Text color={selected ? palette.text.accent : undefined} bold={selected}>
49
+ {`${dot} ${entry.transport.name.padEnd(TRANSPORT_NAME_WIDTH)} ${badge} ${portHint}`}
50
+ </Text>
51
+ </Box>
52
+ );
53
+ }
54
+
55
+ function useTransportInput(
56
+ entries: ITransportEntry[],
57
+ cursor: number,
58
+ saving: boolean,
59
+ registry: ITransportSettingsRegistryView,
60
+ setCursor: (fn: (c: number) => number) => void,
61
+ setSaving: (v: boolean) => void,
62
+ setError: (v: string | undefined) => void,
63
+ onClose: () => void,
64
+ refresh: () => void,
65
+ ): void {
66
+ useKeybindingActions(
67
+ 'transport-settings',
68
+ useCallback(
69
+ (actions: readonly ('previous' | 'next' | 'toggle' | 'close')[]) => {
70
+ if (saving) return;
71
+ if (actions.includes('previous')) {
72
+ setCursor((c) => Math.max(0, c - 1));
73
+ return;
74
+ }
75
+ if (actions.includes('next')) {
76
+ setCursor((c) => Math.min(entries.length - 1, c + 1));
77
+ return;
78
+ }
79
+ if (actions.includes('close')) {
80
+ onClose();
81
+ return;
82
+ }
83
+ if (actions.includes('toggle')) {
84
+ const entry = entries[cursor];
85
+ if (!entry) return;
86
+ setSaving(true);
87
+ setError(undefined);
88
+ registry
89
+ .setEnabled(entry.transport.name, !entry.config.enabled)
90
+ .then(() => {
91
+ refresh();
92
+ setSaving(false);
93
+ })
94
+ .catch((cause: unknown) => {
95
+ // The reason, not just the fact. A settings file that cannot be written and a
96
+ // transport that refuses configuration are different problems for the user.
97
+ setError(cause instanceof Error ? cause.message : 'could not save the setting');
98
+ setSaving(false);
99
+ });
100
+ }
101
+ },
102
+ [saving, entries, cursor, registry, onClose, refresh, setCursor, setSaving, setError],
103
+ ),
104
+ );
105
+ }
106
+
107
+ interface IProps {
108
+ registry: ITransportSettingsRegistryView;
109
+ onClose: () => void;
110
+ }
111
+
112
+ export default function TransportTUI({ registry, onClose }: IProps): React.ReactElement {
113
+ const palette = usePalette();
114
+ const productDisplayName = useProductDisplayName();
115
+ const [entries, setEntries] = useState(() => registry.getAll());
116
+ const [cursor, setCursor] = useState(0);
117
+ const [saving, setSaving] = useState(false);
118
+ const [error, setError] = useState<string | undefined>(undefined);
119
+ const refresh = useCallback((): void => {
120
+ setEntries(registry.getAll());
121
+ }, [registry]);
122
+ const footerHints = useKeybindingHints('transport-settings', [
123
+ [['previous', 'next'], 'Select'],
124
+ ['toggle', 'Toggle'],
125
+ ['close', 'Close'],
126
+ ]);
127
+
128
+ useTransportInput(
129
+ entries,
130
+ cursor,
131
+ saving,
132
+ registry,
133
+ setCursor,
134
+ setSaving,
135
+ setError,
136
+ onClose,
137
+ refresh,
138
+ );
139
+
140
+ return (
141
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
142
+ <Text bold>Settings › Transports</Text>
143
+ <Box marginTop={1} flexDirection="column">
144
+ {entries.map((entry, i) => (
145
+ <TransportEntryRow key={entry.transport.name} entry={entry} selected={i === cursor} />
146
+ ))}
147
+ </Box>
148
+ <Box marginTop={1} flexDirection="column">
149
+ <KeyHintFooter hints={footerHints} />
150
+ <Text dimColor>
151
+ A toggle is saved now and applies the next time {productDisplayName} starts.
152
+ </Text>
153
+ </Box>
154
+ {saving && (
155
+ <Box marginTop={1}>
156
+ <Text color={palette.text.warning}>Saving…</Text>
157
+ </Box>
158
+ )}
159
+ {error !== undefined && (
160
+ <Box marginTop={1}>
161
+ <Text color={palette.text.error}>{`Not saved — ${error}`}</Text>
162
+ </Box>
163
+ )}
164
+ </Box>
165
+ );
166
+ }
@@ -0,0 +1,530 @@
1
+ /**
2
+ * TuiInteractionChannel — session-owning presentation surface for the Ink TUI.
3
+ *
4
+ * Moves session lifecycle (InteractiveSession, CommandRegistry, TuiStateManager)
5
+ * out of React hooks and into a plain TypeScript class.
6
+ */
7
+
8
+ import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
9
+ import { CommandRegistry, buildRuntimeSession } from '@robota-sdk/agent-framework';
10
+
11
+ import { AttentionCoordinator } from './attention/attention-coordinator.js';
12
+ import { MS_PER_SECOND } from './attention/time-units.js';
13
+ import { createSessionInitPoller } from './flows/session-init-poller.js';
14
+ import { applySystemCommandResult } from './hooks/command-result-handler.js';
15
+ import { parseSlashCommandInput } from './slash-command-input.js';
16
+ import {
17
+ TuiChannelLifecycleCoordinator,
18
+ TuiChannelStartRollbackError,
19
+ } from './tui-channel-lifecycle-coordinator.js';
20
+ import { TuiPermissionQueue, TuiUserActionQueue } from './tui-interaction-queues.js';
21
+ import { TuiSessionEventProjector } from './tui-session-event-projector.js';
22
+ import { buildTuiSessionOptions } from './tui-session-options.js';
23
+ import { TuiStateManager } from './tui-state-manager.js';
24
+ import { WAITING_LOOP_STOP_REASON, waitingLoopStopNotice } from './waiting-loop-stop-notice.js';
25
+
26
+ import type { IAttentionSource } from './attention/attention-tracker.js';
27
+ import type { ISessionInitPoller, TSessionInitFailure } from './flows/session-init-poller.js';
28
+ import type { TerminalHandoffController } from './terminal-handoff-controller.js';
29
+ import type {
30
+ ITuiAppChannelPort,
31
+ ITuiChannelSnapshot,
32
+ ITuiCommandQueryPort,
33
+ ITuiRuntimeStatusSnapshot,
34
+ ITuiSessionUiEventPort,
35
+ } from './tui-app-channel-port.js';
36
+ import type { ITuiInteractionChannelOptions } from './tui-channel-options.js';
37
+ import type { IPendingPermissionRequest } from './types.js';
38
+ import type {
39
+ IHistoryEntry,
40
+ TPermissionMode,
41
+ TSessionEndReason,
42
+ TToolArgs,
43
+ } from '@robota-sdk/agent-core';
44
+ // CMD-004 unified action contract (SSOT in agent-core).
45
+ import type {
46
+ IActionRequest,
47
+ TActionResponse as TUserActionResponse,
48
+ } from '@robota-sdk/agent-core';
49
+ import type { InteractiveSession } from '@robota-sdk/agent-framework';
50
+ import type { IExecutionDetailPage } from '@robota-sdk/agent-interface-execution';
51
+ import type { ICommandInfo, TPermissionResultValue } from '@robota-sdk/agent-interface-session';
52
+
53
+ const SESSION_INIT_POLL_MS = 200;
54
+ const SESSION_INIT_TIMEOUT_MS = 15000;
55
+ /** Upper bound on the graceful session shutdown so a wedged subsystem cannot block process exit
56
+ * (CLI-075 / RUNTIME-33; api-boundary "safely cancelled within a configurable timeout"). */
57
+ const SHUTDOWN_TIMEOUT_MS = 5000;
58
+
59
+ export type { ITuiInteractionChannelOptions } from './tui-channel-options.js';
60
+
61
+ export class TuiInteractionChannel implements ITuiAppChannelPort {
62
+ readonly stateManager: TuiStateManager;
63
+
64
+ private readonly interactiveSession: InteractiveSession;
65
+ private readonly registry: CommandRegistry;
66
+ private readonly opts: ITuiInteractionChannelOptions;
67
+
68
+ private submitHandler: ((text: string) => Promise<void>) | null = null;
69
+
70
+ private readonly userActions: TuiUserActionQueue;
71
+ private readonly permissions: TuiPermissionQueue;
72
+ private readonly eventProjector: TuiSessionEventProjector;
73
+ /** SCREEN-1992: joins the terminal's attention to this channel's recap; absent without a source. */
74
+ private readonly attention: AttentionCoordinator | undefined;
75
+ private readonly lifecycle: TuiChannelLifecycleCoordinator;
76
+ availableCommands: ICommandInfo[] = [];
77
+ sessionName: string | undefined;
78
+
79
+ private initPoller: ISessionInitPoller | null = null;
80
+
81
+ /** TERM-002: the App registers its Ink suspend/resume hooks into this controller. */
82
+ get terminalHandoffController(): TerminalHandoffController | undefined {
83
+ return this.opts.terminalHandoff;
84
+ }
85
+ /** Set by React hook to trigger re-render on state change */
86
+ onChange: (() => void) | null = null;
87
+
88
+ constructor(opts: ITuiInteractionChannelOptions) {
89
+ this.opts = opts;
90
+ this.sessionName = opts.sessionName;
91
+ this.stateManager = new TuiStateManager();
92
+ this.stateManager.onChange = () => this.onChange?.();
93
+ this.userActions = new TuiUserActionQueue(() => this.onChange?.());
94
+ this.permissions = new TuiPermissionQueue(() => this.onChange?.());
95
+
96
+ this.interactiveSession = this.createSession();
97
+ this.registry = this.createRegistry();
98
+ this.attention = this.createAttention(opts.attention);
99
+ this.eventProjector = new TuiSessionEventProjector({
100
+ session: this.interactiveSession,
101
+ manager: this.stateManager,
102
+ ...(this.attention ? { attention: this.attention } : {}),
103
+ requestPermission: (
104
+ toolName,
105
+ toolArgs,
106
+ id,
107
+ canPersistProjectPermission,
108
+ requestedByPeer,
109
+ requester,
110
+ ) =>
111
+ this.permissions.enqueue(
112
+ toolName,
113
+ toolArgs,
114
+ id,
115
+ canPersistProjectPermission,
116
+ requestedByPeer,
117
+ requester,
118
+ ),
119
+ askUser: (request, id) => this.askUser(request, id),
120
+ dismissPrompt: (id) => this.dismissPromptById(id),
121
+ ...(opts.onSessionEventDeliveryError
122
+ ? { onDeliveryError: opts.onSessionEventDeliveryError }
123
+ : {}),
124
+ });
125
+ this.lifecycle = new TuiChannelLifecycleCoordinator(
126
+ {
127
+ start: () => this.startRuntime(),
128
+ stop: async () => {
129
+ this.eventProjector.unwire();
130
+ this.cancelAllPermissions();
131
+ this.cancelAllUserActions();
132
+ this.stopInitCheck();
133
+ this.onChange = null;
134
+ this.stateManager.dispose();
135
+ await this.stopTransports();
136
+ },
137
+ beginShutdown: () => {
138
+ this.cancelAllUserActions();
139
+ this.cancelAllPermissions();
140
+ this.stateManager.addEntry(
141
+ messageToHistoryEntry(createSystemMessage('Shutting down...')),
142
+ );
143
+ this.onChange?.();
144
+ },
145
+ shutdownSession: async ({ reason, message }) => {
146
+ await this.interactiveSession.shutdown({ reason, message });
147
+ },
148
+ },
149
+ SHUTDOWN_TIMEOUT_MS,
150
+ );
151
+ }
152
+
153
+ private async startRuntime(): Promise<void> {
154
+ let transportStartAttempted = false;
155
+ try {
156
+ this.eventProjector.wire();
157
+ this.syncRestoredHistory();
158
+ this.startInitCheck();
159
+ if (this.opts.transportRegistry) {
160
+ transportStartAttempted = true;
161
+ await this.opts.bindTransports?.(this.interactiveSession);
162
+ await this.opts.transportRegistry.startAll();
163
+ }
164
+ } catch (startError) {
165
+ this.eventProjector.unwire();
166
+ this.stopInitCheck();
167
+ if (transportStartAttempted && this.opts.transportRegistry) {
168
+ try {
169
+ await this.stopTransports();
170
+ } catch (rollbackError) {
171
+ const rollbackErrors =
172
+ rollbackError instanceof AggregateError ? rollbackError.errors : [rollbackError];
173
+ throw new TuiChannelStartRollbackError(
174
+ [startError, ...rollbackErrors],
175
+ 'TUI channel start failed and transport rollback also failed.',
176
+ );
177
+ }
178
+ }
179
+ throw startError;
180
+ }
181
+ }
182
+
183
+ private async stopTransports(): Promise<void> {
184
+ if (!this.opts.transportRegistry) return;
185
+ const result = await this.opts.transportRegistry.stopAll();
186
+ if (result.errors.length > 0) {
187
+ throw new AggregateError(result.errors, 'TUI transport teardown failed.');
188
+ }
189
+ }
190
+
191
+ get permissionRequest(): IPendingPermissionRequest | null {
192
+ return this.permissions.current;
193
+ }
194
+
195
+ /** CMD-004: the action currently awaiting a user answer, or null. */
196
+ get pendingUserAction(): IActionRequest | null {
197
+ return this.userActions.current;
198
+ }
199
+
200
+ get isShuttingDown(): boolean {
201
+ return this.lifecycle.isShuttingDown;
202
+ }
203
+
204
+ get isActiveForPeerStatus(): boolean {
205
+ return this.lifecycle.isActiveForPeerStatus;
206
+ }
207
+
208
+ private createSession(): InteractiveSession {
209
+ return buildRuntimeSession(buildTuiSessionOptions(this.opts));
210
+ }
211
+
212
+ private createRegistry(): CommandRegistry {
213
+ const registry = new CommandRegistry();
214
+ for (const module of this.opts.commandModules ?? []) {
215
+ registry.addModule(module);
216
+ }
217
+ this.opts.reloadPluginCommandSource?.(registry);
218
+ return registry;
219
+ }
220
+
221
+ onSubmit(handler: (text: string) => Promise<void>): void {
222
+ this.submitHandler = handler;
223
+ }
224
+
225
+ setAvailableCommands(commands: ICommandInfo[]): void {
226
+ this.availableCommands = commands;
227
+ this.onChange?.();
228
+ }
229
+
230
+ setBusy(busy: boolean): void {
231
+ this.stateManager.onThinking(busy);
232
+ }
233
+
234
+ async start(): Promise<void> {
235
+ await this.lifecycle.start();
236
+ }
237
+
238
+ /**
239
+ * Full, idempotent channel teardown (CLI-075). Unwires every session listener, drains both
240
+ * request queues, stops the init poller, disposes the render-state manager, stops transports, and
241
+ * — unless a graceful `shutdown()` already ran — shuts the underlying session down so a discarded
242
+ * or switched-away channel releases its background tasks, subagent processes, and timers.
243
+ */
244
+ async stop(): Promise<void> {
245
+ await this.lifecycle.stop();
246
+ }
247
+
248
+ // ── Additional methods for App.tsx ───────────────────────────
249
+
250
+ getSession(): InteractiveSession {
251
+ return this.interactiveSession;
252
+ }
253
+
254
+ getRegistry(): CommandRegistry {
255
+ return this.registry;
256
+ }
257
+
258
+ subscribe(onChange: () => void): () => void {
259
+ this.onChange = onChange;
260
+ return () => {
261
+ if (this.onChange === onChange) this.onChange = null;
262
+ };
263
+ }
264
+
265
+ getSnapshot(): ITuiChannelSnapshot {
266
+ const manager = this.stateManager;
267
+ return {
268
+ history: manager.history,
269
+ streamingText: manager.streamingText,
270
+ activeTools: manager.activeTools,
271
+ isThinking: manager.isThinking,
272
+ isAborting: manager.isAborting,
273
+ lastErrorMessage: manager.lastErrorMessage,
274
+ isStalled: manager.isStalled,
275
+ sessionEventNotices: manager.sessionEventNotices,
276
+ isShuttingDown: this.isShuttingDown,
277
+ pendingPrompt: manager.pendingPrompt,
278
+ pendingCount: this.interactiveSession.getPendingCount(),
279
+ executionWorkspaceSnapshot: manager.executionWorkspaceSnapshot,
280
+ ...(manager.selectedExecutionEntryId !== undefined
281
+ ? { selectedExecutionEntryId: manager.selectedExecutionEntryId }
282
+ : {}),
283
+ permissionRequest: this.permissionRequest,
284
+ pendingUserAction: this.pendingUserAction,
285
+ contextState: manager.contextState,
286
+ };
287
+ }
288
+
289
+ getCommandQueryPort(): ITuiCommandQueryPort {
290
+ return this.registry;
291
+ }
292
+
293
+ getSessionUiEventPort(): ITuiSessionUiEventPort {
294
+ return this.interactiveSession;
295
+ }
296
+
297
+ getRuntimeStatusSnapshot(fallbackPermissionMode: TPermissionMode): ITuiRuntimeStatusSnapshot {
298
+ try {
299
+ const session = this.interactiveSession.getSession();
300
+ const activePresetId = session.getActivePresetId?.();
301
+ const effort = session.getModelEffort();
302
+ return {
303
+ permissionMode: session.getPermissionMode(),
304
+ sessionId: session.getSessionId(),
305
+ ...(activePresetId !== undefined ? { activePresetId } : {}),
306
+ ...(effort !== undefined ? { effort } : {}),
307
+ };
308
+ } catch {
309
+ return { permissionMode: fallbackPermissionMode, sessionId: '' };
310
+ }
311
+ }
312
+
313
+ addEntry(entry: IHistoryEntry): void {
314
+ this.stateManager.addEntry(entry);
315
+ }
316
+
317
+ abort(): void {
318
+ this.stateManager.setAborting(true);
319
+ this.cancelAllUserActions();
320
+ this.cancelAllPermissions();
321
+ this.interactiveSession.abort();
322
+ }
323
+
324
+ cancelQueue(): void {
325
+ this.interactiveSession.cancelQueue();
326
+ this.cancelAllUserActions();
327
+ this.cancelAllPermissions();
328
+ this.stateManager.setPendingPrompt(null);
329
+ }
330
+
331
+ /** Esc on an idle prompt: the session decides which waiting loop, if any, stops. */
332
+ async stopWaitingSelfPacedLoop(): Promise<void> {
333
+ const outcome = await this.interactiveSession.stopWaitingSelfPacedLoop(WAITING_LOOP_STOP_REASON);
334
+ const notice = waitingLoopStopNotice(outcome);
335
+ if (notice !== undefined) this.addEntry(messageToHistoryEntry(createSystemMessage(notice)));
336
+ }
337
+
338
+ async shutdown(options?: { reason?: TSessionEndReason; timeoutMs?: number }): Promise<void> {
339
+ await this.lifecycle.shutdown(options);
340
+ }
341
+
342
+ selectExecutionWorkspaceEntry(entryId: string): void {
343
+ this.stateManager.selectExecutionWorkspaceEntry(entryId);
344
+ }
345
+
346
+ async readExecutionWorkspaceDetail(entryId: string): Promise<IExecutionDetailPage> {
347
+ return this.interactiveSession.readExecutionWorkspaceDetail(entryId);
348
+ }
349
+
350
+ async sendAgentJob(taskId: string, input: string): Promise<void> {
351
+ await this.interactiveSession.sendAgentJob(taskId, input);
352
+ }
353
+
354
+ setSessionName(name: string): void {
355
+ this.sessionName = name;
356
+ this.interactiveSession.setName(name);
357
+ this.onChange?.();
358
+ }
359
+
360
+ // ── CMD-004 unified ask path ─────────────────────────────────
361
+
362
+ /**
363
+ * Queue an ask and resolve when the user answers. Reached via the `ask_request` event handler
364
+ * (which passes the framework prompt `id` for co-drive dismissal) and by any direct
365
+ * `IInteractionChannel.askUser` caller (no id).
366
+ */
367
+ async askUser(request: IActionRequest, id?: string): Promise<TUserActionResponse> {
368
+ return this.userActions.enqueue(request, id);
369
+ }
370
+
371
+ /** Called by App's PendingActionPrompt when the user answers (or cancels) the pending action. */
372
+ resolveUserAction(request: IActionRequest, response: TUserActionResponse): void {
373
+ this.userActions.resolveCurrent(request, response);
374
+ }
375
+
376
+ /** Resolve every queued/in-flight ask as cancelled (abort, shutdown). */
377
+ private cancelAllUserActions(): void {
378
+ this.userActions.cancelAll();
379
+ }
380
+
381
+ async handleInput(input: string): Promise<void> {
382
+ if (!input.startsWith('/')) {
383
+ await this.interactiveSession.submit(input);
384
+ this.stateManager.setPendingPrompt(this.interactiveSession.getPendingPrompt());
385
+ return;
386
+ }
387
+ await this.handleSlashCommand(input);
388
+ }
389
+
390
+ private async handleSlashCommand(input: string): Promise<void> {
391
+ const { name: cmd, args } = parseSlashCommandInput(input);
392
+
393
+ const result = await this.interactiveSession.executeCommand(cmd, args);
394
+ if (result) {
395
+ // CMD-004 Stage E: `data.sessionExecution` is the requester-local "a session turn is now
396
+ // running" hint (formerly the `session-execution-started` effect).
397
+ if (result.data?.['sessionExecution'] === true) {
398
+ this.stateManager.setPendingPrompt(this.interactiveSession.getPendingPrompt());
399
+ return;
400
+ }
401
+ applySystemCommandResult(
402
+ result,
403
+ this.interactiveSession,
404
+ this.registry,
405
+ this.stateManager,
406
+ this.opts.reloadPluginCommandSource,
407
+ );
408
+ return;
409
+ }
410
+
411
+ this.stateManager.addEntry(
412
+ messageToHistoryEntry(createSystemMessage(`Unknown command "/${cmd}". Type /help for help.`)),
413
+ );
414
+ }
415
+
416
+ // ── Private helpers ──────────────────────────────────────────
417
+
418
+ private handlePermissionRequest(
419
+ toolName: string,
420
+ toolArgs: TToolArgs,
421
+ id?: string,
422
+ ): Promise<TPermissionResultValue> {
423
+ return this.permissions.enqueue(toolName, toolArgs, id);
424
+ }
425
+
426
+ /**
427
+ * REMOTE-007 co-drive: another surface answered the prompt with this framework `id`, so dismiss the
428
+ * still-showing local dialog. Resolving the local entry (cancelled) is a no-op on the framework side
429
+ * — that id is already settled — but it clears the TUI's queue + render state.
430
+ */
431
+ private dismissPromptById(id: string): void {
432
+ this.userActions.dismissById(id);
433
+ this.permissions.dismissById(id);
434
+ }
435
+
436
+ /**
437
+ * Resolve every queued/in-flight permission request as `false` (deny) — abort, cancelQueue,
438
+ * shutdown, stop. Symmetric to `cancelAllUserActions()`: tearing down must neither leave a tool's
439
+ * permission promise dangling (the tool would hang) nor silently grant it (CLI-075 RUNTIME-32).
440
+ */
441
+ private cancelAllPermissions(): void {
442
+ this.permissions.cancelAll();
443
+ }
444
+
445
+ private syncRestoredHistory(): void {
446
+ if (this.stateManager.history.length === 0) {
447
+ const restored = this.interactiveSession.getFullHistory();
448
+ if (restored.length > 0) {
449
+ this.stateManager.syncHistory(restored);
450
+ }
451
+ }
452
+ }
453
+
454
+ private startInitCheck(): void {
455
+ this.stopInitCheck();
456
+ this.initPoller = createSessionInitPoller({
457
+ check: () => this.runInitCheck(),
458
+ intervalMs: SESSION_INIT_POLL_MS,
459
+ timeoutMs: SESSION_INIT_TIMEOUT_MS,
460
+ onReady: () => undefined,
461
+ onFailure: (failure) => this.onInitFailure(failure),
462
+ });
463
+ this.initPoller.start();
464
+ }
465
+
466
+ /** Throws while the session is not ready; the init poller classifies the error. */
467
+ private runInitCheck(): void {
468
+ const ctx = this.interactiveSession.getContextState();
469
+ const restoredSessionName = this.interactiveSession.getName();
470
+ if (restoredSessionName && restoredSessionName !== this.sessionName) {
471
+ this.sessionName = restoredSessionName;
472
+ this.onChange?.();
473
+ }
474
+ this.stateManager.setContextState({
475
+ percentage: ctx.usedPercentage,
476
+ usedTokens: ctx.usedTokens,
477
+ maxTokens: ctx.maxTokens,
478
+ });
479
+ const restored = this.interactiveSession.getFullHistory();
480
+ if (restored.length > 0) {
481
+ this.stateManager.syncHistory(restored);
482
+ }
483
+ this.syncExecutionWorkspace();
484
+ }
485
+
486
+ /** SCREEN-1992: the recap lands in this channel's notice store; no source ⇒ no coordinator. */
487
+ private createAttention(source: IAttentionSource | undefined): AttentionCoordinator | undefined {
488
+ if (!source) return undefined;
489
+ return new AttentionCoordinator({
490
+ source,
491
+ onRecap: (line) => this.stateManager.addAttentionRecap(line),
492
+ });
493
+ }
494
+
495
+ private onInitFailure(failure: TSessionInitFailure): void {
496
+ const message =
497
+ failure.kind === 'timeout'
498
+ ? `Session initialization timed out after ${SESSION_INIT_TIMEOUT_MS / MS_PER_SECOND}s${
499
+ failure.lastError ? ` (last error: ${failure.lastError.message})` : ''
500
+ }`
501
+ : `Session initialization failed: ${failure.error.message}`;
502
+ this.stateManager.onError();
503
+ this.stateManager.addEntry({
504
+ id: `session-init-error-${Date.now()}`,
505
+ timestamp: new Date(),
506
+ category: 'event',
507
+ type: 'session-init-error',
508
+ data: { message },
509
+ });
510
+ }
511
+
512
+ private stopInitCheck(): void {
513
+ this.initPoller?.stop();
514
+ this.initPoller = null;
515
+ }
516
+
517
+ private syncExecutionWorkspace(): void {
518
+ try {
519
+ // allow-fallback: session may not be initialized yet; swallow until ready
520
+ this.stateManager.syncExecutionWorkspaceSnapshot(
521
+ this.interactiveSession.getExecutionWorkspaceSnapshot({
522
+ selectedEntryId: this.stateManager.selectedExecutionEntryId,
523
+ }),
524
+ );
525
+ } catch {
526
+ // allow-fallback: session may not be initialized yet; swallow until ready
527
+ /* Session not initialized yet */
528
+ }
529
+ }
530
+ }