@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,72 @@
1
+ /**
2
+ * SCREEN-1993 — the input area's side of prompt-history search: the `chat-input.history-search`
3
+ * open binding, and what insert and execute do to the composer.
4
+ *
5
+ * Insert replaces the composer text with the match (cursor at the end); execute submits the match
6
+ * through the same path Enter takes, so it is recorded in the live prompt list like any prompt. While
7
+ * the overlay is open the composer is untouched, which is what makes cancel restore the draft exactly.
8
+ */
9
+ import { useCallback, useEffect } from 'react';
10
+
11
+ import {
12
+ useHistorySearch,
13
+ type IHistorySearchSurface,
14
+ } from '../history-search/useHistorySearch.js';
15
+ import { useKeybindingActions } from '../keybindings/keybindings-context.js';
16
+
17
+ import type { IHistorySearchView } from '../history-search/useHistorySearch.js';
18
+
19
+ /** What the input area receives from the app; absent ⇒ the feature is off and `ctrl+r` is inert. */
20
+ export interface IInputAreaHistorySearch extends IHistorySearchSurface {
21
+ /** Lets the app block its own bindings (Esc abort, the switcher) while the overlay is open. */
22
+ readonly onOpenChange?: ((open: boolean) => void) | undefined;
23
+ }
24
+
25
+ export interface IUseInputAreaHistorySearchOptions {
26
+ readonly historySearch: IInputAreaHistorySearch | undefined;
27
+ readonly sessionPrompts: readonly string[];
28
+ /** The composer can take keys: no popup, not disabled, nothing queued. */
29
+ readonly composerActive: boolean;
30
+ readonly setValue: (value: string) => void;
31
+ readonly setCursorHint: (hint: number | null) => void;
32
+ readonly submitPrompt: (prompt: string) => void;
33
+ }
34
+
35
+ export function useInputAreaHistorySearch(
36
+ options: IUseInputAreaHistorySearchOptions,
37
+ ): IHistorySearchView {
38
+ const { historySearch, setValue, setCursorHint, submitPrompt, composerActive } = options;
39
+ const onInsert = useCallback(
40
+ (text: string) => {
41
+ setValue(text);
42
+ setCursorHint(null);
43
+ },
44
+ [setCursorHint, setValue],
45
+ );
46
+ const onExecute = useCallback(
47
+ (text: string) => {
48
+ setValue('');
49
+ submitPrompt(text);
50
+ },
51
+ [setValue, submitPrompt],
52
+ );
53
+ const { view, openSearch, closeSearch } = useHistorySearch({
54
+ surface: historySearch,
55
+ sessionPrompts: options.sessionPrompts,
56
+ onInsert,
57
+ onExecute,
58
+ onOpenChange: historySearch?.onOpenChange,
59
+ });
60
+ useKeybindingActions(
61
+ 'chat-input',
62
+ (actions) => {
63
+ if (actions.includes('history-search')) openSearch();
64
+ },
65
+ { isActive: composerActive && !view.open && historySearch !== undefined },
66
+ );
67
+ // Another overlay or a disabled composer takes the keys: the search cannot stay open under it.
68
+ useEffect(() => {
69
+ if (view.open && !composerActive) closeSearch();
70
+ }, [closeSearch, composerActive, view.open]);
71
+ return view;
72
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The input area's three key bindings, in one place.
3
+ *
4
+ * Extracted from `InputArea.tsx` (CLI-2004) by responsibility: the component owns the input's state
5
+ * and its rendering; these three `useInput` registrations are the routing between the two, and each
6
+ * is gated on a different combination of the same flags — which is the shape that gets copied wrong
7
+ * when it is inline among the render code.
8
+ */
9
+
10
+ import {
11
+ moveAutocompleteSelection,
12
+ navigatePromptHistory,
13
+ type IPromptHistoryNavigationState,
14
+ } from '../flows/input-area-flow.js';
15
+ import { useKeybindingActions } from '../keybindings/keybindings-context.js';
16
+
17
+ import type { ICommand } from '@robota-sdk/agent-interface-command';
18
+
19
+ export interface IUseInputAreaKeysInputs {
20
+ value: string;
21
+ isDisabled: boolean;
22
+ /** SCREEN-1993: the history-search overlay owns the keys while it is open. */
23
+ searchOpen: boolean;
24
+ isQueueCancellationDisabled: boolean;
25
+ pendingPrompt?: string | null;
26
+ showPopup: boolean;
27
+ setShowPopup: (shown: boolean) => void;
28
+ filteredCommands: ICommand[];
29
+ selectedIndex: number;
30
+ setSelectedIndex: (update: (previous: number) => number) => void;
31
+ tabCompleteCommand: (command: ICommand) => void;
32
+ promptHistory: string[];
33
+ historyState: IPromptHistoryNavigationState;
34
+ setValue: (value: string) => void;
35
+ setCursorHint: (hint: number | null) => void;
36
+ setHistoryState: (state: IPromptHistoryNavigationState) => void;
37
+ onRequestFocusBackgroundList?: (() => void) | undefined;
38
+ onCancelQueue?: (() => void) | undefined;
39
+ }
40
+
41
+ /** The autocomplete popup's own keys: move the selection, close it, or take the completion. */
42
+ function useAutocompletePopupKeys(inputs: IUseInputAreaKeysInputs): void {
43
+ const { showPopup, isDisabled, filteredCommands, selectedIndex } = inputs;
44
+ useKeybindingActions(
45
+ 'autocomplete-menu',
46
+ (actions) => {
47
+ if (!showPopup) return;
48
+ for (const action of actions) {
49
+ if (action === 'previous' || action === 'next') {
50
+ inputs.setSelectedIndex((previous) =>
51
+ moveAutocompleteSelection(previous, filteredCommands.length, action),
52
+ );
53
+ } else if (action === 'close') {
54
+ inputs.setShowPopup(false);
55
+ } else if (action === 'accept') {
56
+ const command = filteredCommands[selectedIndex];
57
+ if (command) inputs.tabCompleteCommand(command);
58
+ }
59
+ }
60
+ },
61
+ { isActive: showPopup && !isDisabled },
62
+ );
63
+ }
64
+
65
+ /** Register the autocomplete-popup, prompt-history and queued-prompt bindings. */
66
+ export function useInputAreaKeys(inputs: IUseInputAreaKeysInputs): void {
67
+ const { showPopup, isDisabled, isQueueCancellationDisabled, pendingPrompt, searchOpen } = inputs;
68
+
69
+ useAutocompletePopupKeys(inputs);
70
+
71
+ useKeybindingActions(
72
+ 'chat-input',
73
+ (actions) => {
74
+ const action = actions.find(
75
+ (candidate): candidate is 'history-previous' | 'history-next' =>
76
+ candidate === 'history-previous' || candidate === 'history-next',
77
+ );
78
+ if (action === undefined) return;
79
+ const direction = action === 'history-previous' ? 'previous' : 'next';
80
+ // SCREEN-014: ↓ on an empty input that is not browsing history falls through into the
81
+ // background-work list (where it is a no-op for the input today). The parent decides whether
82
+ // there is a list to focus.
83
+ if (
84
+ direction === 'next' &&
85
+ inputs.historyState.selectedIndex === null &&
86
+ inputs.value.length === 0
87
+ ) {
88
+ inputs.onRequestFocusBackgroundList?.();
89
+ return;
90
+ }
91
+ const result = navigatePromptHistory(
92
+ inputs.value,
93
+ inputs.promptHistory,
94
+ inputs.historyState,
95
+ direction,
96
+ );
97
+ inputs.setValue(result.value);
98
+ inputs.setCursorHint(result.cursorHint);
99
+ inputs.setHistoryState(result.state);
100
+ },
101
+ { isActive: !showPopup && !isDisabled && !pendingPrompt && !searchOpen },
102
+ );
103
+
104
+ // Backspace cancels queued prompt
105
+ useKeybindingActions(
106
+ 'queued-prompt',
107
+ (actions) => {
108
+ if (actions.includes('cancel') && pendingPrompt) {
109
+ inputs.onCancelQueue?.();
110
+ }
111
+ },
112
+ { isActive: !!pendingPrompt && !isQueueCancellationDisabled },
113
+ );
114
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * CLI-2004 — the typed-number input wiring shared by every numbered menu.
3
+ *
4
+ * The reducer lives in `flows/selection-flow.ts` (one owner of "what is selected"); this hook is the
5
+ * Ink adapter around it, so six components get the same behaviour without six copies of the same
6
+ * `useInput` block. Inactive when the mode is off, so the arrow-key handlers keep the terminal.
7
+ */
8
+
9
+ import { useInput } from 'ink';
10
+ import { useCallback, useRef, useState } from 'react';
11
+
12
+ import {
13
+ applyNumericSelection,
14
+ createNumericSelectionState,
15
+ type INumericSelectionState,
16
+ } from '../flows/selection-flow.js';
17
+
18
+ export interface IUseNumberedSelectionInputs {
19
+ /** Screen-reader mode. Off ⇒ the hook reads nothing. */
20
+ enabled: boolean;
21
+ itemCount: number;
22
+ cancellable?: boolean;
23
+ /** A checklist: a number toggles a row, the menu stays open, and an empty Enter commits. */
24
+ multi?: boolean;
25
+ /** Keep a single-row selection open so the user can select another row later. */
26
+ repeatable?: boolean;
27
+ onSelect: (index: number) => void;
28
+ onCancel?: () => void;
29
+ /** Required with `multi` — what an empty Enter commits. */
30
+ onConfirm?: () => void;
31
+ }
32
+
33
+ export interface INumberedSelection {
34
+ buffer: string;
35
+ invalid: boolean;
36
+ clear: () => void;
37
+ }
38
+
39
+ /** Read a typed selection for a numbered list. */
40
+ export function useNumberedSelection(inputs: IUseNumberedSelectionInputs): INumberedSelection {
41
+ const [state, setState] = useState<INumericSelectionState>(createNumericSelectionState);
42
+ const stateRef = useRef(state);
43
+ const { enabled, itemCount, cancellable, multi, repeatable, onSelect, onCancel, onConfirm } =
44
+ inputs;
45
+
46
+ const clear = useCallback((): void => {
47
+ const empty = createNumericSelectionState();
48
+ stateRef.current = empty;
49
+ setState(empty);
50
+ }, []);
51
+
52
+ const handle = useCallback(
53
+ (input: string, key: Parameters<typeof applyNumericSelection>[2]): boolean => {
54
+ const result = applyNumericSelection(stateRef.current, input, key, {
55
+ itemCount,
56
+ ...(cancellable === true ? { cancellable: true } : {}),
57
+ ...(multi === true ? { multi: true } : {}),
58
+ ...(repeatable === true ? { repeatable: true } : {}),
59
+ });
60
+ stateRef.current = result.state;
61
+ setState(result.state);
62
+ if (result.effect.type === 'select') onSelect(result.effect.index);
63
+ else if (result.effect.type === 'cancel') onCancel?.();
64
+ else if (result.effect.type === 'confirm') onConfirm?.();
65
+ return result.effect.type !== 'none';
66
+ },
67
+ [itemCount, cancellable, multi, repeatable, onSelect, onCancel, onConfirm],
68
+ );
69
+
70
+ useInput(
71
+ (input, key) => {
72
+ // Pasted or fast-typed input can arrive as one chunk ("2\r"): Ink then reports it as plain
73
+ // text with no return key. Replay it as the keystrokes it contains so the Enter is not lost,
74
+ // but only up to the first keystroke that acts: the menu's callbacks see the state of the
75
+ // current render, so acting twice within one chunk could decide on stale state.
76
+ if (key.return !== true && /[\r\n]/u.test(input)) {
77
+ const plainKey = { ...key, return: false };
78
+ for (const part of input.split(/(\r\n|\r|\n)/u)) {
79
+ if (part === '') continue;
80
+ const acted = /^(\r\n|\r|\n)$/u.test(part)
81
+ ? handle('', { ...plainKey, return: true })
82
+ : handle(part, plainKey);
83
+ if (acted) break;
84
+ }
85
+ return;
86
+ }
87
+ handle(input, key);
88
+ },
89
+ { isActive: enabled },
90
+ );
91
+
92
+ return { buffer: state.buffer, invalid: state.invalid, clear };
93
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Hook: uses the host plugin adapter, or an explicit unsupported adapter when none is provided.
3
+ *
4
+ * A missing host capability must fail visibly. Returning successful no-ops would make the plugin
5
+ * screen claim mutations succeeded while changing nothing.
6
+ */
7
+
8
+ import { useMemo } from 'react';
9
+
10
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
11
+
12
+ function unsupported(): Promise<never> {
13
+ return Promise.reject(new Error('Plugin management is unavailable in this host.'));
14
+ }
15
+
16
+ function createUnsupportedPluginAdapter(): ICommandPluginAdapter {
17
+ return {
18
+ listInstalled: unsupported,
19
+ listAvailablePlugins: unsupported,
20
+ install: unsupported,
21
+ uninstall: unsupported,
22
+ enable: unsupported,
23
+ disable: unsupported,
24
+ marketplaceAdd: unsupported,
25
+ marketplaceRemove: unsupported,
26
+ marketplaceUpdate: unsupported,
27
+ marketplaceList: unsupported,
28
+ reloadPlugins: unsupported,
29
+ };
30
+ }
31
+
32
+ export function resolvePluginCallbacks(adapter?: ICommandPluginAdapter): ICommandPluginAdapter {
33
+ return adapter ?? createUnsupportedPluginAdapter();
34
+ }
35
+
36
+ export function usePluginCallbacks(adapter?: ICommandPluginAdapter): ICommandPluginAdapter {
37
+ return useMemo(() => resolvePluginCallbacks(adapter), [adapter]);
38
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Hook: fetch data for PluginTUI screens.
3
+ * Extracted from PluginTUI.tsx for single-responsibility.
4
+ */
5
+
6
+ import { useState, useEffect } from 'react';
7
+
8
+ import type { IMenuSelectItem } from '../MenuSelect.js';
9
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
10
+
11
+ export function usePluginScreenData(
12
+ screen: string,
13
+ marketplace: string | undefined,
14
+ callbacks: ICommandPluginAdapter,
15
+ refreshCounter: number,
16
+ stackLength: number,
17
+ ): { items: IMenuSelectItem[]; loading: boolean; error: string | undefined } {
18
+ const [items, setItems] = useState<IMenuSelectItem[]>([]);
19
+ const [loading, setLoading] = useState(false);
20
+ const [error, setError] = useState<string | undefined>();
21
+
22
+ useEffect(() => {
23
+ setItems([]);
24
+ setError(undefined);
25
+
26
+ if (screen === 'marketplace-list') {
27
+ setLoading(true);
28
+ callbacks
29
+ .marketplaceList()
30
+ .then((sources) => {
31
+ const baseItems: IMenuSelectItem[] = [{ label: 'Add Marketplace', value: '__add__' }];
32
+ const sourceItems: IMenuSelectItem[] = sources.map((s) => ({
33
+ label: s.name,
34
+ value: s.name,
35
+ hint: s.type,
36
+ }));
37
+ setItems([...baseItems, ...sourceItems]);
38
+ setLoading(false);
39
+ })
40
+ .catch((err) => {
41
+ setError(err instanceof Error ? err.message : String(err));
42
+ setLoading(false);
43
+ });
44
+ } else if (screen === 'marketplace-browse') {
45
+ const mp = marketplace ?? '';
46
+ setLoading(true);
47
+ callbacks
48
+ .listAvailablePlugins(mp)
49
+ .then((plugins) => {
50
+ setItems(
51
+ plugins.map((p) => ({
52
+ label: p.name,
53
+ value: p.name,
54
+ hint: p.installed ? 'installed' : p.description,
55
+ })),
56
+ );
57
+ setLoading(false);
58
+ })
59
+ .catch((err) => {
60
+ setError(err instanceof Error ? err.message : String(err));
61
+ setLoading(false);
62
+ });
63
+ } else if (screen === 'installed-list') {
64
+ setLoading(true);
65
+ callbacks
66
+ .listInstalled()
67
+ .then((plugins) => {
68
+ setItems(
69
+ plugins.map((p) => ({
70
+ label: p.name,
71
+ value: p.name,
72
+ hint: p.description,
73
+ })),
74
+ );
75
+ setLoading(false);
76
+ })
77
+ .catch((err) => {
78
+ setError(err instanceof Error ? err.message : String(err));
79
+ setLoading(false);
80
+ });
81
+ }
82
+ }, [stackLength, screen, marketplace, callbacks, refreshCounter]);
83
+
84
+ return { items, loading, error };
85
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * CLI-062 — position the REAL terminal cursor at the CJK composition point.
3
+ *
4
+ * Mechanism (contract: .design/investigations/2026-07-25-cli-062-ime-cursor-design.md, POC-PASS):
5
+ * the input box's absolute frame-space origin comes from walking the yoga `parentNode` chain from
6
+ * a `<Box ref>` (the `5195e326b` approach, minus its debug logging); the cell is handed to ink's
7
+ * `useCursor().setCursorPosition`, which converts it into a relative `cursorUp` from the frame
8
+ * bottom INSIDE the same atomic synchronized frame write (invariant I3: never out-of-band — all
9
+ * cursor movement rides ink's ≤30fps frame writes; this hook itself writes NOTHING to any stream,
10
+ * the PoC row-accounting lesson).
11
+ *
12
+ * Measurement happens post-commit (layout is valid there) and is stored in state so the NEXT
13
+ * commit's insertion effect propagates a fresh position with its frame; `useBoxMetrics` provides
14
+ * the layout-change subscription so sibling-driven moves re-measure too. A stale origin self-heals
15
+ * within one frame (ink recomputes layout before onRender and notifies layout listeners).
16
+ */
17
+
18
+ import { useBoxMetrics, useCursor, useWindowSize } from 'ink';
19
+ import { useEffect, useState } from 'react';
20
+
21
+ import { computeCursorCell, shouldPositionRealCursor } from '../flows/real-cursor-flow.js';
22
+
23
+ import type { DOMElement } from 'ink';
24
+ import type { RefObject } from 'react';
25
+
26
+ export interface IUseRealCursorPositionOptions {
27
+ /** Ref attached to the `<Box>` wrapping the input's text. */
28
+ boxRef: RefObject<DOMElement | null>;
29
+ /** Capability + focus gate: supportsImeCursorPositioning() && focus && showCursor. */
30
+ enabled: boolean;
31
+ /** Live input value (from the input's state ref — fresh at render time). */
32
+ value: string;
33
+ /** Live cursor character index. */
34
+ cursor: number;
35
+ /** Wrap width in columns (matches the input's own wrap math). */
36
+ availableWidth?: number;
37
+ }
38
+
39
+ export interface IUseRealCursorPositionResult {
40
+ /** True while the hardware cursor is being positioned — the drawn cursor must be suppressed. */
41
+ realCursorActive: boolean;
42
+ }
43
+
44
+ interface IMeasuredOrigin {
45
+ x: number;
46
+ y: number;
47
+ frameHeight: number;
48
+ }
49
+
50
+ /** Walk the yoga `parentNode` chain to the absolute frame-space origin, plus the root frame height. */
51
+ function measureAbsoluteOrigin(element: DOMElement): IMeasuredOrigin | undefined {
52
+ if (element.yogaNode === undefined) return undefined;
53
+ let x = 0;
54
+ let y = 0;
55
+ let current: DOMElement | undefined = element;
56
+ let root: DOMElement = element;
57
+ while (current !== undefined) {
58
+ const layout = current.yogaNode?.getComputedLayout();
59
+ x += layout?.left ?? 0;
60
+ y += layout?.top ?? 0;
61
+ root = current;
62
+ current = current.parentNode;
63
+ }
64
+ const frameHeight = root.yogaNode?.getComputedLayout().height ?? 0;
65
+ return { x, y, frameHeight };
66
+ }
67
+
68
+ function sameOrigin(a: IMeasuredOrigin | undefined, b: IMeasuredOrigin | undefined): boolean {
69
+ if (a === undefined || b === undefined) return a === b;
70
+ return a.x === b.x && a.y === b.y && a.frameHeight === b.frameHeight;
71
+ }
72
+
73
+ export function useRealCursorPosition(
74
+ options: IUseRealCursorPositionOptions,
75
+ ): IUseRealCursorPositionResult {
76
+ const { setCursorPosition } = useCursor();
77
+ // Layout-change subscription: useBoxMetrics re-renders this component whenever the tracked
78
+ // box's metrics change on a root layout commit, and exposes hasMeasured for invariant I1.
79
+ const metrics = useBoxMetrics(options.boxRef);
80
+ const { rows: viewportRows } = useWindowSize();
81
+ const [origin, setOrigin] = useState<IMeasuredOrigin | undefined>(undefined);
82
+
83
+ // I1: never a guessed row — measure the absolute origin ONLY from a committed yoga layout
84
+ // (post-commit effect; ink ran calculateLayout before onRender, passive effects run after).
85
+ // No dependency array on purpose: any render may follow a layout change, and the setState
86
+ // bails out when the origin is unchanged, so this settles instead of looping.
87
+ useEffect(() => {
88
+ const element = options.boxRef.current;
89
+ const next = element === null ? undefined : measureAbsoluteOrigin(element);
90
+ setOrigin((previous) => (sameOrigin(previous, next) ? previous : next));
91
+ });
92
+
93
+ const cell =
94
+ origin !== undefined && options.enabled
95
+ ? computeCursorCell({
96
+ absX: origin.x,
97
+ absY: origin.y,
98
+ value: options.value,
99
+ cursor: options.cursor,
100
+ ...(options.availableWidth !== undefined
101
+ ? { availableWidth: options.availableWidth }
102
+ : {}),
103
+ })
104
+ : undefined;
105
+
106
+ const realCursorActive =
107
+ cell !== undefined &&
108
+ origin !== undefined &&
109
+ shouldPositionRealCursor({
110
+ hasMeasured: metrics.hasMeasured, // I1 — `origin` is already narrowed non-undefined above
111
+ y: cell.y,
112
+ frameHeight: origin.frameHeight, // I2 checked inside the guard
113
+ viewportRows,
114
+ capability: options.enabled, // I5 folded in via supportsImeCursorPositioning()
115
+ });
116
+
117
+ // Render-phase call is safe: useCursor only records the position in a ref; propagation happens
118
+ // in its insertion effect, inside ink's synchronized frame write (I3). I4: when the guard fails
119
+ // (blur, fullscreen frame, not measured yet) we pass `undefined` — ink hides the hardware cursor
120
+ // and the caller falls back to today's drawn-cursor rendering; on unmount, useCursor's cleanup
121
+ // propagates `undefined` by itself.
122
+ setCursorPosition(realCursorActive && cell !== undefined ? { x: cell.x, y: cell.y } : undefined);
123
+
124
+ return { realCursorActive };
125
+ }
@@ -0,0 +1,38 @@
1
+ import { renderMarkdown } from '../render-markdown.js';
2
+ import { useScreenReader } from '../screen-reader-context.js';
3
+ import { useSyntaxHighlighting, useTheme } from '../theme/index.js';
4
+
5
+ import type { IRenderMarkdownOptions } from '../render-markdown.js';
6
+
7
+ /** What a call site still decides. The three appearance inputs are no longer among them. */
8
+ export type TRenderMarkdownExtras = Omit<
9
+ IRenderMarkdownOptions,
10
+ 'theme' | 'syntaxHighlighting' | 'screenReader'
11
+ >;
12
+
13
+ /**
14
+ * SCREEN-2002 — markdown rendered with this surface's resolved appearance.
15
+ *
16
+ * The three inputs every render site needs — the theme, the syntax-highlighting setting and
17
+ * screen-reader mode — are bound here instead of at each call. That is a deliberate removal of an
18
+ * obligation rather than a convenience.
19
+ *
20
+ * The obligation was forgotten once and guarded four times. `/theme syntax off` shipped persisting
21
+ * a setting no call site passed, so the command reported success and changed nothing; each guard
22
+ * written afterwards measured a PROXY for "every call site passes it" — the prop appears in the
23
+ * file, the file matched the regex, the identifier occurs N times — and each held for exactly one
24
+ * review round. A binding removes the thing being proxied: there is no per-call-site argument left
25
+ * to forget, and the floor collapses to a single-consumer ratchet (nothing outside this module
26
+ * imports `renderMarkdown`), which is a property rather than a proxy for one.
27
+ *
28
+ * Screen-reader mode is bound too, uniformly. `StreamingIndicator` previously omitted it, which was
29
+ * correct but only incidentally: it returns its collapsed line before reaching any markdown, so the
30
+ * flag never applied. Binding it removes that coincidence as something a reader has to reconstruct.
31
+ */
32
+ export function useRenderMarkdown(): (md: string, extras?: TRenderMarkdownExtras) => string {
33
+ const theme = useTheme();
34
+ const syntaxHighlighting = useSyntaxHighlighting();
35
+ const screenReader = useScreenReader();
36
+ return (md, extras = {}) =>
37
+ renderMarkdown(md, { ...extras, theme, syntaxHighlighting, screenReader });
38
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * CLI-2004 — the non-visual half of a turn: the bell and the OSC 133 boundary marks.
3
+ *
4
+ * Both are byte-stream events, not rendering, so they live in a hook rather than a component: the
5
+ * App says WHEN a turn starts, ends, or wants an answer, and this decides what reaches the terminal.
6
+ * When the mode is off both writers are inert, so the App has exactly one code path.
7
+ *
8
+ * One turn emits `A` (prompt start) → `B` (prompt end) → `C` (turn start) → `D` (turn end): the
9
+ * prompt is marked while the user is being asked for input, and the execution pair brackets the
10
+ * agent's work. That is the boundary a reader jumps between.
11
+ */
12
+
13
+ import { useEffect, useMemo, useRef } from 'react';
14
+
15
+ import { createAttentionBell } from '../attention-bell.js';
16
+ import { useScreenReaderPacing } from '../screen-reader-pacing-context.js';
17
+ import { supportsTurnMarks } from '../terminal-capabilities.js';
18
+ import { useTerminalCapabilityOverrides } from '../terminal-capabilities-context.js';
19
+ import { createTurnMarkWriter } from '../terminal-marks.js';
20
+
21
+ import type { IAttentionBell } from '../attention-bell.js';
22
+ import type { IToolState } from '@robota-sdk/agent-interface-session';
23
+
24
+ export interface IUseScreenReaderTurnSignalsInputs {
25
+ enabled: boolean;
26
+ /** The agent is working (a turn is in flight). */
27
+ isThinking: boolean;
28
+ /** Tools currently reported as active by the session. */
29
+ activeTools: readonly IToolState[];
30
+ /** A permission ask or user-action dialog is mounted and waiting for an answer. */
31
+ awaitingAnswer: boolean;
32
+ }
33
+
34
+ /**
35
+ * Stable key for one tool run within a turn.
36
+ *
37
+ * `executionId` where the session supplies one; otherwise the position, which `onToolEnd` preserves
38
+ * (it replaces the entry at its index rather than removing it).
39
+ */
40
+ function toolKey(tool: IToolState, index: number): string {
41
+ return tool.executionId ?? `${tool.toolName}#${index}`;
42
+ }
43
+
44
+ /** The keys of the tools currently RUNNING — not merely present in the array. */
45
+ function runningKeys(activeTools: readonly IToolState[]): Set<string> {
46
+ const running = new Set<string>();
47
+ activeTools.forEach((tool, index) => {
48
+ if (tool.isRunning) running.add(toolKey(tool, index));
49
+ });
50
+ return running;
51
+ }
52
+
53
+ /** Wire the bell and the turn marks to the App's turn lifecycle. */
54
+ export function useScreenReaderTurnSignals(inputs: IUseScreenReaderTurnSignalsInputs): void {
55
+ const { enabled, isThinking, activeTools, awaitingAnswer } = inputs;
56
+ const bell = useMemo<IAttentionBell>(() => createAttentionBell({ enabled }), [enabled]);
57
+ // SCREEN-2670: the marks are POSITIONAL — the terminal records the line they arrive on — so they
58
+ // travel through the owned write path, ordered with the commit they belong to. Outside a provider
59
+ // the port writes to the process's own stdout, which is exactly today's path.
60
+ const pacing = useScreenReaderPacing();
61
+ const terminalCapabilities = useTerminalCapabilityOverrides();
62
+ const marks = useMemo(
63
+ () =>
64
+ createTurnMarkWriter({
65
+ enabled,
66
+ supported: () => supportsTurnMarks({ override: terminalCapabilities.turnMarks }),
67
+ write: (text) => pacing.write(text),
68
+ }),
69
+ [enabled, pacing, terminalCapabilities.turnMarks],
70
+ );
71
+ const wasThinking = useRef(false);
72
+ const runningTools = useRef<ReadonlySet<string>>(new Set());
73
+
74
+ // Turn boundaries: B/C when work starts, D/A when it ends and the prompt comes back.
75
+ useEffect(() => {
76
+ if (isThinking && !wasThinking.current) {
77
+ marks.emit('promptEnd');
78
+ marks.emit('turnStart');
79
+ } else if (!isThinking && wasThinking.current) {
80
+ marks.emit('turnEnd');
81
+ marks.emit('promptStart');
82
+ bell.replyCompleted();
83
+ }
84
+ wasThinking.current = isThinking;
85
+ }, [isThinking, bell, marks]);
86
+
87
+ // The very first prompt of the session is marked once, before anything has been asked.
88
+ useEffect(() => {
89
+ marks.emit('promptStart');
90
+ }, [marks]);
91
+
92
+ // A prompt or dialog that wants an answer rings once on mount.
93
+ useEffect(() => {
94
+ if (awaitingAnswer) bell.promptMounted();
95
+ }, [awaitingAnswer, bell]);
96
+
97
+ // Long-running tools: record each start, ring on the completions that outlived the threshold.
98
+ //
99
+ // Completion is the `isRunning` true→false transition, NOT the entry leaving the array. The
100
+ // session's `onToolEnd` replaces the finished entry in place (same index, same name, so the same
101
+ // key) and only `onThinking(false)` empties the array — so a membership test would report every
102
+ // tool as completing at turn end, judging its duration against the whole turn.
103
+ useEffect(() => {
104
+ const running = runningKeys(activeTools);
105
+ for (const key of running) {
106
+ if (!runningTools.current.has(key)) bell.toolStarted(key);
107
+ }
108
+ for (const key of runningTools.current) {
109
+ if (!running.has(key)) bell.toolCompleted(key);
110
+ }
111
+ runningTools.current = running;
112
+ }, [activeTools, bell]);
113
+ }