@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,291 @@
1
+ /**
2
+ * CMD-004 Phase 2 Stage C (TC-04): the TUI is a pure renderer — the four UI screens open from the
3
+ * requester-routed `ui_intent` session event (never from legacy `result.effects` branches), the
4
+ * session title updates from the broadcast `session_renamed` event (never by the TUI mutating the
5
+ * session), and the statusline re-reads its persisted settings when a command result arrives
6
+ * (refresh-on-result — the HOST applies the patch; the TUI no longer writes settings).
7
+ *
8
+ * Red-first evidence: written against the pre-Stage-C hook (which still consumed legacy effects and
9
+ * had no session subscription) — every behavioral case below failed there.
10
+ */
11
+
12
+ import { Text } from 'ink';
13
+ import { render } from 'ink-testing-library';
14
+ import React from 'react';
15
+ import { describe, expect, it, vi } from 'vitest';
16
+
17
+ import { useSideEffects } from '../hooks/useSideEffects.js';
18
+ import { useStatusLineSettings } from '../hooks/useStatusLineSettings.js';
19
+ import { TuiCliAdapterProvider } from '../tui-cli-adapter-context.js';
20
+ import { TUI_SESSION_EVENT_CLASSIFICATION } from '../tui-session-events.js';
21
+
22
+ import type { IUseSideEffectsOptions, IUseSideEffectsResult } from '../hooks/side-effects-types.js';
23
+ import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
24
+ import type { TUniversalValue } from '@robota-sdk/agent-core';
25
+
26
+ const FRAME_DEADLINE_MS = 3000;
27
+
28
+ async function waitUntil(predicate: () => boolean): Promise<void> {
29
+ const deadline = Date.now() + FRAME_DEADLINE_MS;
30
+ while (Date.now() < deadline) {
31
+ if (predicate()) return;
32
+ await new Promise((resolve) => setTimeout(resolve, 10));
33
+ }
34
+ expect(predicate()).toBe(true);
35
+ }
36
+
37
+ type TSessionListener = (payload: unknown) => void;
38
+
39
+ /** Minimal InteractiveSession stand-in: a typed event emitter (`on`/`off`/`emit`). */
40
+ class FakeSessionEmitter {
41
+ private readonly listeners = new Map<string, Set<TSessionListener>>();
42
+
43
+ on(event: string, handler: TSessionListener): void {
44
+ const set = this.listeners.get(event) ?? new Set<TSessionListener>();
45
+ set.add(handler);
46
+ this.listeners.set(event, set);
47
+ }
48
+
49
+ off(event: string, handler: TSessionListener): void {
50
+ this.listeners.get(event)?.delete(handler);
51
+ }
52
+
53
+ emit(event: string, payload: unknown): void {
54
+ for (const handler of [...(this.listeners.get(event) ?? [])]) handler(payload);
55
+ }
56
+
57
+ listenerCount(event: string): number {
58
+ return this.listeners.get(event)?.size ?? 0;
59
+ }
60
+
61
+ subscribedEvents(): string[] {
62
+ return [...this.listeners].filter(([, handlers]) => handlers.size > 0).map(([event]) => event);
63
+ }
64
+
65
+ setName = vi.fn();
66
+ getName = (): string | undefined => undefined;
67
+ }
68
+
69
+ function fakeCliAdapter(store: () => Record<string, TUniversalValue>): ITuiCliAdapter {
70
+ return {
71
+ getUserSettingsPath: () => '/tmp/fake-settings.json',
72
+ readSettings: () => store(),
73
+ reloadPluginCommandSource: vi.fn(),
74
+ applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
75
+ getGitBranch: vi.fn().mockReturnValue(undefined),
76
+ getProviderDisplayName: vi.fn((type: string) => type),
77
+ } as unknown as ITuiCliAdapter;
78
+ }
79
+
80
+ interface IHarness {
81
+ session: FakeSessionEmitter;
82
+ result: () => IUseSideEffectsResult;
83
+ lastFrame: () => string | undefined;
84
+ unmount: () => void;
85
+ spies: {
86
+ setSessionName: ReturnType<typeof vi.fn>;
87
+ refreshStatusLineSettings: ReturnType<typeof vi.fn>;
88
+ refreshAppearanceSettings: ReturnType<typeof vi.fn>;
89
+ openAgentSwitcher: ReturnType<typeof vi.fn>;
90
+ baseHandleSubmit: ReturnType<typeof vi.fn>;
91
+ };
92
+ }
93
+
94
+ function mountHarness(): IHarness {
95
+ const session = new FakeSessionEmitter();
96
+ const spies = {
97
+ setSessionName: vi.fn(),
98
+ refreshStatusLineSettings: vi.fn(),
99
+ refreshAppearanceSettings: vi.fn(),
100
+ openAgentSwitcher: vi.fn(),
101
+ baseHandleSubmit: vi.fn(async () => {}),
102
+ };
103
+ const resultRef: { current: IUseSideEffectsResult | null } = { current: null };
104
+
105
+ // Superset options object: carries both the pre-Stage-C fields (queue, addEntry, …) and the
106
+ // Stage-C fields so the SAME test file compiled red (pre) and green (post).
107
+ const options = {
108
+ cwd: '/tmp',
109
+ uiEventPort: session,
110
+ commandEffectQueue: {
111
+ enqueueEffects: () => undefined,
112
+ drain: () => undefined,
113
+ clear: () => undefined,
114
+ },
115
+ addEntry: vi.fn(),
116
+ baseHandleSubmit: spies.baseHandleSubmit,
117
+ setSessionName: spies.setSessionName,
118
+ setStatusLineSettings: vi.fn(),
119
+ refreshStatusLineSettings: spies.refreshStatusLineSettings,
120
+ refreshAppearanceSettings: spies.refreshAppearanceSettings,
121
+ showSessionPickerOnStart: false,
122
+ openAgentSwitcher: spies.openAgentSwitcher,
123
+ } as unknown as IUseSideEffectsOptions;
124
+
125
+ function Probe(): React.ReactElement {
126
+ const state = useSideEffects(options);
127
+ resultRef.current = state;
128
+ return (
129
+ <Text>
130
+ {`plugin:${state.showPluginTUI ? 1 : 0} settings:${state.showTransportTUI ? 1 : 0} picker:${state.showSessionPicker ? 1 : 0}`}
131
+ </Text>
132
+ );
133
+ }
134
+
135
+ const instance = render(
136
+ <TuiCliAdapterProvider value={fakeCliAdapter(() => ({}))}>
137
+ <Probe />
138
+ </TuiCliAdapterProvider>,
139
+ );
140
+
141
+ return {
142
+ session,
143
+ result: () => {
144
+ if (!resultRef.current) throw new Error('useSideEffects result not captured');
145
+ return resultRef.current;
146
+ },
147
+ lastFrame: instance.lastFrame,
148
+ unmount: instance.unmount,
149
+ spies,
150
+ };
151
+ }
152
+
153
+ describe('ui_intent → TUI screens (requester-routed, CMD-004 Stage C)', () => {
154
+ it('show-settings with the owner requester opens the settings screen', async () => {
155
+ const h = mountHarness();
156
+ h.session.emit('ui_intent', { intent: { type: 'show-settings' }, requesterDriverId: 'owner' });
157
+ await waitUntil(() => (h.lastFrame() ?? '').includes('settings:1'));
158
+ h.unmount();
159
+ });
160
+
161
+ it('show-plugin-manager with the owner requester opens the plugin screen', async () => {
162
+ const h = mountHarness();
163
+ h.session.emit('ui_intent', {
164
+ intent: { type: 'show-plugin-manager' },
165
+ requesterDriverId: 'owner',
166
+ });
167
+ await waitUntil(() => (h.lastFrame() ?? '').includes('plugin:1'));
168
+ h.unmount();
169
+ });
170
+
171
+ it('show-session-picker with the owner requester opens the session picker', async () => {
172
+ const h = mountHarness();
173
+ h.session.emit('ui_intent', {
174
+ intent: { type: 'show-session-picker' },
175
+ requesterDriverId: 'owner',
176
+ });
177
+ await waitUntil(() => (h.lastFrame() ?? '').includes('picker:1'));
178
+ h.unmount();
179
+ });
180
+
181
+ it('show-agent-switcher with the owner requester invokes the switcher callback', async () => {
182
+ const h = mountHarness();
183
+ h.session.emit('ui_intent', {
184
+ intent: { type: 'show-agent-switcher' },
185
+ requesterDriverId: 'owner',
186
+ });
187
+ await waitUntil(() => h.spies.openAgentSwitcher.mock.calls.length === 1);
188
+ h.unmount();
189
+ });
190
+
191
+ it('an intent stamped with ANOTHER surface driver id is ignored (requester-routed)', async () => {
192
+ const h = mountHarness();
193
+ h.session.emit('ui_intent', {
194
+ intent: { type: 'show-settings' },
195
+ requesterDriverId: 'device-remote-1',
196
+ });
197
+ h.session.emit('ui_intent', {
198
+ intent: { type: 'show-agent-switcher' },
199
+ requesterDriverId: 'device-remote-1',
200
+ });
201
+ // Give the (wrong) render a chance to happen, then assert it did not.
202
+ await new Promise((resolve) => setTimeout(resolve, 50));
203
+ expect(h.lastFrame()).toContain('settings:0');
204
+ expect(h.spies.openAgentSwitcher).not.toHaveBeenCalled();
205
+ h.unmount();
206
+ });
207
+
208
+ it('an unattributed intent (no requester id) is ignored by this surface', async () => {
209
+ const h = mountHarness();
210
+ h.session.emit('ui_intent', { intent: { type: 'show-settings' } });
211
+ await new Promise((resolve) => setTimeout(resolve, 50));
212
+ expect(h.lastFrame()).toContain('settings:0');
213
+ h.unmount();
214
+ });
215
+
216
+ it('unmounting unsubscribes the ui_intent and session_renamed listeners', async () => {
217
+ const h = mountHarness();
218
+ const expected = Object.entries(TUI_SESSION_EVENT_CLASSIFICATION)
219
+ .filter(([, classification]) => classification === 'react-side-effect')
220
+ .map(([event]) => event)
221
+ .sort();
222
+ await waitUntil(() => h.session.subscribedEvents().sort().join() === expected.join());
223
+ h.unmount();
224
+ await waitUntil(() => h.session.subscribedEvents().length === 0);
225
+ });
226
+ });
227
+
228
+ describe('session_renamed broadcast → TUI title (CMD-004 Stage C / TC-10)', () => {
229
+ it('updates the session name from the broadcast without mutating the session itself', async () => {
230
+ const h = mountHarness();
231
+ h.session.emit('session_renamed', { name: 'Renamed via broadcast' });
232
+ await waitUntil(() => h.spies.setSessionName.mock.calls.length === 1);
233
+ expect(h.spies.setSessionName).toHaveBeenCalledWith('Renamed via broadcast');
234
+ // Pure renderer: the TUI reflects the host-executed rename; it never performs it.
235
+ expect(h.session.setName).not.toHaveBeenCalled();
236
+ h.unmount();
237
+ });
238
+ });
239
+
240
+ describe('statusline refresh-on-result (CMD-004 Stage C)', () => {
241
+ it('re-reads statusline settings after a slash-command result arrives', async () => {
242
+ const h = mountHarness();
243
+ await h.result().handleSubmit('/statusline off');
244
+ expect(h.spies.baseHandleSubmit).toHaveBeenCalledWith('/statusline off');
245
+ expect(h.spies.refreshStatusLineSettings).toHaveBeenCalledTimes(1);
246
+ // SCREEN-2002: the appearance is re-read on the same signal — a `/theme` result has already
247
+ // been written to the settings document by the host when this fires.
248
+ expect(h.spies.refreshAppearanceSettings).toHaveBeenCalledTimes(1);
249
+ h.unmount();
250
+ });
251
+
252
+ it('does not re-read for a plain (non-command) submit', async () => {
253
+ const h = mountHarness();
254
+ await h.result().handleSubmit('hello there');
255
+ expect(h.spies.refreshStatusLineSettings).not.toHaveBeenCalled();
256
+ expect(h.spies.refreshAppearanceSettings).not.toHaveBeenCalled();
257
+ h.unmount();
258
+ });
259
+ });
260
+
261
+ describe('useStatusLineSettings exposes a from-disk refresh', () => {
262
+ it('refresh() re-reads the persisted settings document', async () => {
263
+ let doc: Record<string, TUniversalValue> = { statusline: { enabled: true, gitBranch: true } };
264
+ const adapter = fakeCliAdapter(() => doc);
265
+ const captured: {
266
+ settings: { enabled: boolean; gitBranch: boolean } | null;
267
+ refresh: (() => void) | null;
268
+ } = { settings: null, refresh: null };
269
+
270
+ function Probe(): React.ReactElement {
271
+ const [settings, refresh] = useStatusLineSettings();
272
+ captured.settings = settings;
273
+ captured.refresh = refresh as unknown as () => void;
274
+ return <Text>{`enabled:${settings.enabled ? 1 : 0}`}</Text>;
275
+ }
276
+
277
+ const instance = render(
278
+ <TuiCliAdapterProvider value={adapter}>
279
+ <Probe />
280
+ </TuiCliAdapterProvider>,
281
+ );
282
+ await waitUntil(() => (instance.lastFrame() ?? '').includes('enabled:1'));
283
+
284
+ // The HOST persisted a patch (Stage B applies it via the settings adapter); the TUI refreshes.
285
+ doc = { statusline: { enabled: false, gitBranch: true } };
286
+ captured.refresh?.();
287
+ await waitUntil(() => (instance.lastFrame() ?? '').includes('enabled:0'));
288
+ expect(captured.settings?.enabled).toBe(false);
289
+ instance.unmount();
290
+ });
291
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * SCREEN-006 WaveText token-sourcing tests.
3
+ *
4
+ * WaveText's color ramp comes from the resolved theme's motion tokens (its cadence is its own) in
5
+ * `src/theme/built-in-themes.ts` (not component-private literals), and the gated/static
6
+ * frame must use the canonical muted token. Reduced-motion behavior is
7
+ * re-asserted against the token source: when `isInteractiveColorTerminal()` is
8
+ * false there is no interval and no color churn.
9
+ */
10
+ import chalk from 'chalk';
11
+ import { render } from 'ink-testing-library';
12
+ import React from 'react';
13
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
14
+
15
+ import WaveText from '../WaveText.js';
16
+ import { ScreenReaderProvider } from '../screen-reader-context.js';
17
+ import { DARK_THEME } from '../theme/built-in-themes.js';
18
+ import { foreground } from '../theme/index.js';
19
+
20
+ /** Cadence is not themed; the component owns it (SCREEN-2002). */
21
+ const WAVE_INTERVAL_MS = 400;
22
+
23
+ const gateMock = vi.hoisted(() => ({ value: true }));
24
+
25
+ vi.mock('../terminal-capabilities.js', () => ({
26
+ isInteractiveColorTerminal: (): boolean => gateMock.value,
27
+ }));
28
+
29
+ /** Extract every SGR open-sequence used in a frame (e.g. "38;2;85;85;85"). */
30
+ function sgrCodes(frame: string): string[] {
31
+ // eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
32
+ return [...frame.matchAll(/\x1b\[([0-9;]+)m/g)].map((m) => m[1]);
33
+ }
34
+
35
+ /** The SGR open code chalk emits for a color, at the level ink uses in this env. */
36
+ function chalkOpenCodes(colorize: (s: string) => string): string[] {
37
+ return sgrCodes(colorize('x'));
38
+ }
39
+
40
+ const ORIGINAL_CHALK_LEVEL = chalk.level;
41
+
42
+ beforeAll(() => {
43
+ // Pin color bytes regardless of the test process's TTY detection (cjk-fallback precedent).
44
+ chalk.level = 3;
45
+ });
46
+
47
+ afterAll(() => {
48
+ chalk.level = ORIGINAL_CHALK_LEVEL;
49
+ });
50
+
51
+ afterEach(() => {
52
+ gateMock.value = true;
53
+ vi.useRealTimers();
54
+ });
55
+
56
+ describe('WaveText motion tokens (SCREEN-006)', () => {
57
+ it('animated frames draw only from the theme’s wave ramp', () => {
58
+ gateMock.value = true;
59
+ const allowed = new Set<string>(['39', '0']);
60
+ for (const stop of DARK_THEME.motion.wave) {
61
+ for (const code of chalkOpenCodes((s) => chalk.hex(stop)(s))) {
62
+ allowed.add(code);
63
+ }
64
+ }
65
+
66
+ const { lastFrame, unmount } = render(<WaveText text="Thinking" />);
67
+ const frame = lastFrame() ?? '';
68
+ unmount();
69
+
70
+ const used = sgrCodes(frame);
71
+ expect(used.length).toBeGreaterThan(0);
72
+ const outside = used.filter((code) => !allowed.has(code));
73
+ expect(outside).toEqual([]);
74
+ });
75
+
76
+ it('gated/static frame renders with the theme’s muted token and no color churn', () => {
77
+ gateMock.value = false;
78
+ vi.useFakeTimers();
79
+
80
+ const { lastFrame, unmount } = render(<WaveText text="Thinking" />);
81
+ const first = lastFrame() ?? '';
82
+
83
+ // The muted token (not a wave hex stop) styles the static frame.
84
+ const mutedCodes = chalkOpenCodes((s) => foreground(DARK_THEME.colors.text.muted)(s));
85
+ for (const code of mutedCodes) {
86
+ expect(first).toContain(`\u001b[${code}m`);
87
+ }
88
+
89
+ // No interval ⇒ no motion: advancing time produces the identical frame.
90
+ vi.advanceTimersByTime(WAVE_INTERVAL_MS * 5);
91
+ const later = lastFrame() ?? '';
92
+ unmount();
93
+ expect(later).toBe(first);
94
+ });
95
+ });
96
+
97
+ /**
98
+ * CLI-2004 TC-19 — a spinner is a repaint per frame, and a repaint is an announcement. In the mode
99
+ * the text is written once and no interval is ever scheduled.
100
+ */
101
+ describe('CLI-2004 TC-19: WaveText in screen-reader mode', () => {
102
+ it('renders its text once and schedules no interval', () => {
103
+ gateMock.value = true; // the colour gate is OPEN — the mode alone must stop the animation
104
+ vi.useFakeTimers();
105
+
106
+ const { lastFrame, unmount } = render(
107
+ <ScreenReaderProvider enabled={true}>
108
+ <WaveText text="Thinking" />
109
+ </ScreenReaderProvider>,
110
+ );
111
+ const first = lastFrame() ?? '';
112
+
113
+ // No pending timer at all — not merely a frame that happens to look the same.
114
+ expect(vi.getTimerCount()).toBe(0);
115
+
116
+ vi.advanceTimersByTime(WAVE_INTERVAL_MS * 5);
117
+ const later = lastFrame() ?? '';
118
+ unmount();
119
+
120
+ expect(first).toContain('Thinking');
121
+ expect(later).toBe(first);
122
+ expect(sgrCodes(first).filter((code) => code !== '0' && code !== '39')).toEqual([]);
123
+ });
124
+
125
+ it('leaves the 400 ms ramp exactly as it is outside the mode', () => {
126
+ gateMock.value = true;
127
+ vi.useFakeTimers();
128
+
129
+ const { unmount } = render(<WaveText text="Thinking" />);
130
+ expect(vi.getTimerCount()).toBeGreaterThan(0);
131
+ unmount();
132
+ });
133
+ });