@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,150 @@
1
+ /**
2
+ * CLI-B11 TC-02: real-store channel factory integration.
3
+ *
4
+ * The official CI equivalent of real-resume-verify-v3.mjs: build the channel
5
+ * exactly the way render.tsx does (toChannelOptions + TuiInteractionChannel)
6
+ * over a REAL project session store with a persisted conversation, and assert
7
+ * the restored model context is non-empty. No store/session mocks.
8
+ */
9
+
10
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { join } from 'node:path';
13
+
14
+ import { createNodeHostSessionStore } from '@robota-sdk/agent-framework';
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
16
+
17
+ import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
18
+ import { toChannelOptions, type IRenderOptions } from '../render.js';
19
+ import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
20
+
21
+ import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
22
+ import type { TUniversalMessage } from '@robota-sdk/agent-core';
23
+ import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
24
+
25
+ const RESTORE_DEADLINE_MS = 10_000;
26
+ const POLL_MS = 50;
27
+
28
+ function fakeCliAdapter(settingsPath: string): ITuiCliAdapter {
29
+ return {
30
+ getUserSettingsPath: () => settingsPath,
31
+ readSettings: () => ({}),
32
+ reloadPluginCommandSource: vi.fn(),
33
+ applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
34
+ getGitBranch: vi.fn().mockReturnValue(undefined),
35
+ getProviderDisplayName: vi.fn((type: string) => type),
36
+ };
37
+ }
38
+
39
+ function persistConversation(store: IInteractiveSessionStore, id: string, cwd: string): void {
40
+ // TRANS-007: the store decodes on load, so a message must satisfy the contract. These were cast
41
+ // stubs with only `role` and `content`; the store accepted them because it never looked.
42
+ const at = new Date('2026-06-13T00:00:00.000Z');
43
+ const messages: TUniversalMessage[] = [
44
+ {
45
+ id: 'm-0',
46
+ role: 'user',
47
+ content: 'Remember the number 42.',
48
+ timestamp: at,
49
+ state: 'complete',
50
+ },
51
+ { id: 'm-1', role: 'assistant', content: 'Noted: 42.', timestamp: at, state: 'complete' },
52
+ {
53
+ id: 'm-2',
54
+ role: 'user',
55
+ content: 'And the city is Busan.',
56
+ timestamp: at,
57
+ state: 'complete',
58
+ },
59
+ { id: 'm-3', role: 'assistant', content: 'Noted: Busan.', timestamp: at, state: 'complete' },
60
+ ];
61
+ store.save({
62
+ id,
63
+ cwd,
64
+ createdAt: '2026-06-13T00:00:00.000Z',
65
+ updatedAt: '2026-06-13T00:00:00.000Z',
66
+ messages,
67
+ });
68
+ }
69
+
70
+ describe('channel factory restores persisted context (CLI-B11 TC-02)', () => {
71
+ let cwd: string;
72
+ let channel: TuiInteractionChannel | undefined;
73
+
74
+ beforeEach(() => {
75
+ cwd = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-b11-int-')));
76
+ });
77
+
78
+ afterEach(async () => {
79
+ await channel?.stop();
80
+ channel = undefined;
81
+ rmSync(cwd, { recursive: true, force: true });
82
+ });
83
+
84
+ it('createChannel(sessionId) over a real FileSessionStore yields usedTokens > 0', async () => {
85
+ const store = createNodeHostSessionStore(join(cwd, 'state', 'sessions'));
86
+ const sessionId = 'b11-restore-session';
87
+ persistConversation(store, sessionId, cwd);
88
+
89
+ // Exactly the render.tsx factory: toChannelOptions(options, resumeSessionId).
90
+ const scripted = createScriptedProvider([{ text: 'unused in this test' }]);
91
+ const options: IRenderOptions = {
92
+ cwd,
93
+ provider: scripted.provider,
94
+ sessionStore: store,
95
+ cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
96
+ };
97
+ channel = new TuiInteractionChannel(toChannelOptions(options, sessionId));
98
+ await channel.start();
99
+
100
+ // Restoration is asynchronous (pendingRestoreMessages inject after init).
101
+ const deadline = Date.now() + RESTORE_DEADLINE_MS;
102
+ let usedTokens = 0;
103
+ while (Date.now() < deadline) {
104
+ try {
105
+ // allow-fallback: session init is asynchronous; poll until it is ready
106
+ usedTokens = channel.getSession().getContextState().usedTokens;
107
+ if (usedTokens > 0) break;
108
+ } catch {
109
+ // allow-fallback: session init is asynchronous; poll until it is ready
110
+ }
111
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
112
+ }
113
+
114
+ // The persisted messages were injected into the model context — the exact
115
+ // signal that was 0 in the 2026-05-31 bug. (getFullHistory() is the display
116
+ // log restored from record.history, which this record intentionally omits.)
117
+ expect(usedTokens).toBeGreaterThan(0);
118
+ });
119
+
120
+ it('a channel created WITHOUT resumeSessionId starts with an empty context (control)', async () => {
121
+ const store = createNodeHostSessionStore(join(cwd, 'state', 'sessions'));
122
+ persistConversation(store, 'b11-other-session', cwd);
123
+
124
+ const scripted = createScriptedProvider([{ text: 'unused' }]);
125
+ const options: IRenderOptions = {
126
+ cwd,
127
+ provider: scripted.provider,
128
+ sessionStore: store,
129
+ cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
130
+ };
131
+ channel = new TuiInteractionChannel(toChannelOptions(options, undefined));
132
+ await channel.start();
133
+
134
+ const deadline = Date.now() + RESTORE_DEADLINE_MS;
135
+ let ready = false;
136
+ while (Date.now() < deadline && !ready) {
137
+ try {
138
+ // allow-fallback: session init is asynchronous; poll until it is ready
139
+ channel.getSession().getContextState();
140
+ ready = true;
141
+ } catch {
142
+ // allow-fallback: session init is asynchronous; poll until it is ready
143
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
144
+ }
145
+ }
146
+
147
+ expect(ready).toBe(true);
148
+ expect(channel.getSession().getFullHistory()).toHaveLength(0);
149
+ });
150
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * #3231: the TUI builds a new channel on every session switch, and it may do so before the old
3
+ * session's turn has ended. A checkpoint store holds one turn in progress, so a store shared across
4
+ * channels would let the old turn close the new session's turn. Each channel gets its own.
5
+ */
6
+
7
+ import { mkdtempSync, realpathSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+
11
+ import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
12
+ import { afterEach, describe, expect, it, vi } from 'vitest';
13
+
14
+ import { createInProcessChannelFactory } from '../render.js';
15
+ import { TerminalHandoffController } from '../terminal-handoff-controller.js';
16
+
17
+ import type { IChannelServices, IRenderOptions } from '../render.js';
18
+ import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
19
+ import type { ITuiAppChannelPort } from '../tui-app-channel-port.js';
20
+ import type { EditCheckpointStore } from '@robota-sdk/agent-framework';
21
+
22
+ const sessionStores = vi.hoisted(() => [] as unknown[]);
23
+
24
+ vi.mock('../tui-session-options.js', async (importOriginal) => {
25
+ const original = await importOriginal<typeof import('../tui-session-options.js')>();
26
+ return {
27
+ buildTuiSessionOptions: (
28
+ ...args: Parameters<typeof original.buildTuiSessionOptions>
29
+ ): ReturnType<typeof original.buildTuiSessionOptions> => {
30
+ const built = original.buildTuiSessionOptions(...args);
31
+ sessionStores.push(built.editCheckpointStore);
32
+ return built;
33
+ },
34
+ };
35
+ });
36
+
37
+ function fakeCliAdapter(settingsPath: string): ITuiCliAdapter {
38
+ return {
39
+ getUserSettingsPath: () => settingsPath,
40
+ readSettings: () => ({}),
41
+ reloadPluginCommandSource: vi.fn(),
42
+ applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
43
+ getGitBranch: vi.fn().mockReturnValue(undefined),
44
+ getProviderDisplayName: vi.fn((type: string) => type),
45
+ };
46
+ }
47
+
48
+ describe('#3231: the in-process channel factory', () => {
49
+ const channels: ITuiAppChannelPort[] = [];
50
+ let cwd: string | undefined;
51
+
52
+ afterEach(async () => {
53
+ for (const channel of channels.splice(0)) await channel.stop();
54
+ sessionStores.splice(0);
55
+ if (cwd !== undefined) rmSync(cwd, { recursive: true, force: true });
56
+ cwd = undefined;
57
+ });
58
+
59
+ it('gives each channel, including a switched-to one, its own checkpoint store', () => {
60
+ cwd = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-store-per-channel-')));
61
+ const createEditCheckpointStore = vi.fn(() => ({}) as EditCheckpointStore);
62
+ const options: IRenderOptions = {
63
+ cwd,
64
+ provider: createScriptedProvider([{ text: 'unused' }]).provider,
65
+ cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
66
+ createEditCheckpointStore,
67
+ };
68
+ const services = {
69
+ terminalHandoff: new TerminalHandoffController(),
70
+ } as unknown as IChannelServices;
71
+ const createChannel = createInProcessChannelFactory(options, services);
72
+
73
+ channels.push(createChannel(), createChannel('switched-to-session'));
74
+
75
+ expect(createEditCheckpointStore).toHaveBeenCalledTimes(2);
76
+ expect(sessionStores).toHaveLength(2);
77
+ expect(sessionStores[0]).toBeDefined();
78
+ expect(sessionStores[1]).toBeDefined();
79
+ expect(sessionStores[0]).not.toBe(sessionStores[1]);
80
+ });
81
+ });
@@ -0,0 +1,68 @@
1
+ import { render } from 'ink-testing-library';
2
+ import React, { useState } from 'react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import CjkTextInput from '../CjkTextInput.js';
6
+ import { IME_SUBMIT_DEFER_MS } from '../flows/defer-submit.js';
7
+ import korean from './__fixtures__/korean-text.json';
8
+
9
+ /**
10
+ * CLI-061 — integration proof that the CjkTextInput defer-submit includes a trailing IME character whose stdin
11
+ * event arrives JUST AFTER Enter. This exercises the real handler + stateRef + defer wiring (not the pure
12
+ * reducer, which by construction only sees the value captured at Enter and would be accidental-green).
13
+ *
14
+ * Real timers + a real wait > IME_SUBMIT_DEFER_MS so the deferred submit actually fires (fake timers conflict
15
+ * with Ink's own render scheduling).
16
+ *
17
+ * That wait is DERIVED from the constant rather than written next to a comment quoting it. Before, this file
18
+ * waited a hard-coded 90ms and said `// > 50ms defer` in prose — so raising IME_SUBMIT_DEFER_MS past 90 would
19
+ * have left the test waiting less than the defer it exists to outlast, and the comment asserting otherwise.
20
+ * The constant was also, by that arrangement, exported and named by nothing but a comment.
21
+ */
22
+ const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 0));
23
+ const DEFER_WAIT_MARGIN_MS = 40;
24
+ const waitDefer = (): Promise<void> =>
25
+ new Promise((r) => setTimeout(r, IME_SUBMIT_DEFER_MS + DEFER_WAIT_MARGIN_MS));
26
+
27
+ function Harness({ onSubmit }: { onSubmit: (value: string) => void }): React.ReactElement {
28
+ const [value, setValue] = useState('');
29
+ return <CjkTextInput value={value} onChange={setValue} onSubmit={onSubmit} focus />;
30
+ }
31
+
32
+ describe('CLI-061 — CjkTextInput defer-submit (integration)', () => {
33
+ it('includes a trailing syllable that arrives after Enter (greeting without its last syllable + Enter + last syllable → full greeting)', async () => {
34
+ const onSubmit = vi.fn();
35
+ const { stdin } = render(<Harness onSubmit={onSubmit} />);
36
+ await tick();
37
+
38
+ stdin.write(korean.greetingWithoutLastSyllable); // composed so far
39
+ await tick();
40
+ stdin.write('\r'); // Enter — schedules the deferred submit, does NOT submit synchronously
41
+ await tick();
42
+ expect(onSubmit).not.toHaveBeenCalled(); // deferred, not immediate
43
+
44
+ stdin.write(korean.greetingLastSyllable); // the final syllable's stdin event lands after Enter; the pipeline is still live
45
+ await tick();
46
+
47
+ await waitDefer(); // the defer window elapses → the deferred submit fires, re-reading the LATEST value
48
+ expect(onSubmit).toHaveBeenCalledTimes(1);
49
+ expect(onSubmit).toHaveBeenCalledWith(korean.greeting);
50
+ }, 10_000);
51
+
52
+ it('a second Enter within the window does not double-submit', async () => {
53
+ const onSubmit = vi.fn();
54
+ const { stdin } = render(<Harness onSubmit={onSubmit} />);
55
+ await tick();
56
+
57
+ stdin.write('hi');
58
+ await tick();
59
+ stdin.write('\r');
60
+ await tick();
61
+ stdin.write('\r'); // second Enter within the defer window
62
+ await tick();
63
+
64
+ await waitDefer();
65
+ expect(onSubmit).toHaveBeenCalledTimes(1);
66
+ expect(onSubmit).toHaveBeenCalledWith('hi');
67
+ }, 10_000);
68
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * CLI-062 — fallback-path pinning guard.
3
+ *
4
+ * When real-cursor positioning is NOT active (capability off — the default in this test process:
5
+ * stdout is not a TTY and PRODUCT_IME_CURSOR is unset), CjkTextInput's rendering must stay
6
+ * byte-identical to the pre-CLI-062 behavior (invariant I4: guard fail → today's visuals, drawn
7
+ * inverse cursor included). The expected strings below are literal captures of the component's
8
+ * output BEFORE the CLI-062 change (chalk.level forced to 3 so the inverse-cursor bytes are pinned,
9
+ * not stripped). If this test breaks, the `<Box ref>` wrapper or the drawn-cursor gating changed
10
+ * the fallback rendering — that is a regression, not a snapshot to refresh.
11
+ */
12
+
13
+ import chalk from 'chalk';
14
+ import { render } from 'ink-testing-library';
15
+ import React from 'react';
16
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
17
+
18
+ import CjkTextInput from '../CjkTextInput.js';
19
+ import korean from './__fixtures__/korean-text.json';
20
+
21
+ const noop = (): void => {};
22
+ const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 30));
23
+
24
+ const ORIGINAL_CHALK_LEVEL = chalk.level;
25
+
26
+ describe('CLI-062 — fallback rendering is byte-identical to pre-change output', () => {
27
+ beforeAll(() => {
28
+ chalk.level = 3;
29
+ });
30
+ afterAll(() => {
31
+ chalk.level = ORIGINAL_CHALK_LEVEL;
32
+ });
33
+
34
+ it('sanity: capability is off in this test process (fallback path is the one being pinned)', () => {
35
+ expect(process.env['PRODUCT_IME_CURSOR']).toBeUndefined();
36
+ expect(Boolean(process.stdout.isTTY)).toBe(false);
37
+ });
38
+
39
+ it('ASCII value with drawn end-of-line cursor', async () => {
40
+ const { lastFrame, unmount } = render(<CjkTextInput value="hello" onChange={noop} />);
41
+ await tick();
42
+ expect(lastFrame()).toBe('hello\u001b[7m \u001b[27m');
43
+ unmount();
44
+ });
45
+
46
+ it('CJK value with drawn end-of-line cursor', async () => {
47
+ const { lastFrame, unmount } = render(<CjkTextInput value={korean.greeting} onChange={noop} />);
48
+ await tick();
49
+ expect(lastFrame()).toBe(`${korean.greeting}\u001b[7m \u001b[27m`);
50
+ unmount();
51
+ });
52
+
53
+ it('empty value renders placeholder with inverse first character', async () => {
54
+ const { lastFrame, unmount } = render(
55
+ <CjkTextInput value="" onChange={noop} placeholder="Type a message or /help" />,
56
+ );
57
+ await tick();
58
+ expect(lastFrame()).toBe('\u001b[7mT\u001b[27m\u001b[90mype a message or /help\u001b[39m');
59
+ unmount();
60
+ });
61
+
62
+ it('unfocused input renders plain text without a drawn cursor', async () => {
63
+ const { lastFrame, unmount } = render(
64
+ <CjkTextInput value="hello" onChange={noop} focus={false} />,
65
+ );
66
+ await tick();
67
+ expect(lastFrame()).toBe('hello');
68
+ unmount();
69
+ });
70
+
71
+ it('long value wraps identically (the <Box ref> wrapper must not change layout)', async () => {
72
+ const { lastFrame, unmount } = render(
73
+ <CjkTextInput
74
+ value={'a'.repeat(150) + korean.greetingFirstTwoSyllables}
75
+ onChange={noop}
76
+ availableWidth={58}
77
+ />,
78
+ );
79
+ await tick();
80
+ expect(lastFrame()).toBe(
81
+ 'a'.repeat(100) +
82
+ '\n' +
83
+ 'a'.repeat(50) +
84
+ korean.greetingFirstTwoSyllables +
85
+ '\u001b[7m \u001b[27m',
86
+ );
87
+ unmount();
88
+ });
89
+ });
@@ -0,0 +1,187 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import korean from './__fixtures__/korean-text.json';
3
+ import {
4
+ applyCjkTextInput,
5
+ applyCjkTextPaste,
6
+ createCjkTextInputFlowState,
7
+ syncCjkTextInputFlowState,
8
+ } from '../flows/cjk-text-input-flow.js';
9
+
10
+ describe('cjk text input flow', () => {
11
+ it('Given printable input When applied Then value changes at cursor', () => {
12
+ const result = applyCjkTextInput(
13
+ createCjkTextInputFlowState('ab'),
14
+ 'c',
15
+ {},
16
+ { canPaste: true },
17
+ );
18
+
19
+ expect(result.state).toMatchObject({ value: 'abc', cursor: 3 });
20
+ expect(result.effect).toEqual({ type: 'change', value: 'abc' });
21
+ });
22
+
23
+ it('Given cursor in middle When backspace is applied Then previous char is removed', () => {
24
+ const state = { ...createCjkTextInputFlowState('abc'), cursor: 2 };
25
+
26
+ const result = applyCjkTextInput(state, '', { backspace: true }, { canPaste: true });
27
+
28
+ expect(result.state).toMatchObject({ value: 'ac', cursor: 1 });
29
+ expect(result.effect).toEqual({ type: 'change', value: 'ac' });
30
+ });
31
+
32
+ it('Given return key When applied Then submit effect contains current value', () => {
33
+ const result = applyCjkTextInput(
34
+ createCjkTextInputFlowState('hello'),
35
+ '',
36
+ { return: true },
37
+ { canPaste: true },
38
+ );
39
+
40
+ expect(result.effect).toEqual({ type: 'submit', value: 'hello' });
41
+ });
42
+
43
+ it('Given multiline fallback paste When applied Then paste effect is emitted', () => {
44
+ const result = applyCjkTextInput(
45
+ createCjkTextInputFlowState(''),
46
+ 'a\nb',
47
+ {},
48
+ { canPaste: true },
49
+ );
50
+
51
+ expect(result.effect).toEqual({ type: 'paste', text: 'a\nb', cursor: 0 });
52
+ });
53
+
54
+ it('Given Ink usePaste single-line text When applied Then text is inserted at cursor', () => {
55
+ const state = { ...createCjkTextInputFlowState('ab'), cursor: 1 };
56
+
57
+ const result = applyCjkTextPaste(state, korean.hangulWord, { canPaste: true });
58
+
59
+ expect(result.state).toMatchObject({ value: `a${korean.hangulWord}b`, cursor: 3 });
60
+ expect(result.effect).toEqual({ type: 'change', value: `a${korean.hangulWord}b` });
61
+ });
62
+
63
+ it('Given Ink usePaste multiline text When applied Then normalized paste effect is emitted', () => {
64
+ const state = { ...createCjkTextInputFlowState('ab'), cursor: 1 };
65
+
66
+ const result = applyCjkTextPaste(state, 'x\r\ny\rz', { canPaste: true });
67
+
68
+ expect(result.state).toBe(state);
69
+ expect(result.effect).toEqual({ type: 'paste', text: 'x\ny\nz', cursor: 1 });
70
+ });
71
+
72
+ it('Given bracketed multiline paste When end marker arrives Then buffered paste is emitted', () => {
73
+ const started = applyCjkTextInput(
74
+ createCjkTextInputFlowState('x'),
75
+ '[200~hello',
76
+ {},
77
+ { canPaste: true },
78
+ ).state;
79
+ const result = applyCjkTextInput(started, '\nworld[201~', {}, { canPaste: true });
80
+
81
+ expect(result.effect).toEqual({ type: 'paste', text: 'hello\nworld', cursor: 1 });
82
+ expect(result.state.isPasting).toBe(false);
83
+ });
84
+
85
+ it('Given external value update When synced Then cursor hint is honored', () => {
86
+ const state = createCjkTextInputFlowState('abc');
87
+
88
+ const result = syncCjkTextInputFlowState(state, 'a[Pasted]bc', 9);
89
+
90
+ expect(result).toMatchObject({ value: 'a[Pasted]bc', cursor: 9 });
91
+ });
92
+
93
+ it('Given vertical navigation disabled When up arrow is applied Then cursor is unchanged', () => {
94
+ const state = { ...createCjkTextInputFlowState('abcdef'), cursor: 5 };
95
+
96
+ const result = applyCjkTextInput(
97
+ state,
98
+ '',
99
+ { upArrow: true },
100
+ {
101
+ canPaste: true,
102
+ enableVerticalNavigation: false,
103
+ availableWidth: 2,
104
+ },
105
+ );
106
+
107
+ expect(result.state).toBe(state);
108
+ expect(result.effect).toEqual({ type: 'none' });
109
+ });
110
+ });
111
+
112
+ /**
113
+ * CLI-2004 — `Ctrl+W` and `Ctrl+U` are NEW BINDINGS, and they are unconditional.
114
+ *
115
+ * Ink reports `Ctrl+W` as `input: 'w'` with `ctrl: true`, so before this change the keystroke fell
116
+ * through to the printable path and typed a literal `w` into the buffer. That is a defect on the
117
+ * default path, not only in screen-reader mode, so the fix is not gated on the mode — which makes it
118
+ * a change to today's behaviour, and these cases are where that change is pinned. What the mode adds
119
+ * is only the announcement, which is `InputArea`'s to render from `deleted` / `deletedScope`.
120
+ */
121
+ describe('CLI-2004 — word and line deletion are default bindings, not mode-gated ones', () => {
122
+ it('Given Ctrl+W When applied Then the word before the cursor is deleted, not a "w" typed', () => {
123
+ const result = applyCjkTextInput(
124
+ createCjkTextInputFlowState('hello world'),
125
+ 'w',
126
+ { ctrl: true },
127
+ { canPaste: true },
128
+ );
129
+
130
+ expect(result.state).toMatchObject({ value: 'hello ', cursor: 6 });
131
+ expect(result.effect).toEqual({
132
+ type: 'change',
133
+ value: 'hello ',
134
+ deleted: 'world',
135
+ deletedScope: 'word',
136
+ });
137
+ });
138
+
139
+ it('Given Ctrl+W with trailing spaces Then the spaces go with the word, as every shell does', () => {
140
+ const result = applyCjkTextInput(
141
+ createCjkTextInputFlowState('one two '),
142
+ 'w',
143
+ { ctrl: true },
144
+ { canPaste: true },
145
+ );
146
+
147
+ expect(result.state.value).toBe('one ');
148
+ expect(result.effect).toMatchObject({ deleted: 'two ', deletedScope: 'word' });
149
+ });
150
+
151
+ it('Given Ctrl+U When applied Then everything before the cursor is deleted', () => {
152
+ const state = { ...createCjkTextInputFlowState('abc def'), cursor: 4 };
153
+
154
+ const result = applyCjkTextInput(state, 'u', { ctrl: true }, { canPaste: true });
155
+
156
+ expect(result.state).toMatchObject({ value: 'def', cursor: 0 });
157
+ expect(result.effect).toEqual({
158
+ type: 'change',
159
+ value: 'def',
160
+ deleted: 'abc ',
161
+ deletedScope: 'line',
162
+ });
163
+ });
164
+
165
+ it('Given a plain "w" with no ctrl Then it is still typed', () => {
166
+ const result = applyCjkTextInput(
167
+ createCjkTextInputFlowState('ne'),
168
+ 'w',
169
+ {},
170
+ { canPaste: true },
171
+ );
172
+
173
+ expect(result.state.value).toBe('new');
174
+ expect(result.effect).toEqual({ type: 'change', value: 'new' });
175
+ });
176
+
177
+ it('Given the cursor at the start Then either deletion is a no-op rather than an empty change', () => {
178
+ const state = createCjkTextInputFlowState('');
179
+
180
+ expect(applyCjkTextInput(state, 'w', { ctrl: true }, { canPaste: true }).effect).toEqual({
181
+ type: 'none',
182
+ });
183
+ expect(applyCjkTextInput(state, 'u', { ctrl: true }, { canPaste: true }).effect).toEqual({
184
+ type: 'none',
185
+ });
186
+ });
187
+ });