@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,160 @@
1
+ /**
2
+ * Test: ESC abort after permission prompt was shown and dismissed.
3
+ * Verifies that the global ESC handler remains available after overlays close.
4
+ */
5
+
6
+ import React, { useState } from 'react';
7
+ import { render } from 'ink-testing-library';
8
+ import { Box, Text, useInput } from 'ink';
9
+ import { describe, it, expect } from 'vitest';
10
+
11
+ /**
12
+ * Simulates App's global ESC handler with permission prompt overlay guard.
13
+ * 1. Start with "thinking" active
14
+ * 2. Permission prompt appears (App-level ESC ignores while overlay is active)
15
+ * 3. Permission resolved (App-level ESC handles abort again)
16
+ * 4. ESC should trigger abort
17
+ */
18
+ function AbortAfterPermissionApp({
19
+ onAbort,
20
+ onPermissionReady,
21
+ }: {
22
+ onAbort: () => void;
23
+ onPermissionReady: (grantPermission: () => void) => void;
24
+ }): React.ReactElement {
25
+ const [isThinking] = useState(true);
26
+ const [permissionRequest, setPermissionRequest] = useState<{
27
+ resolve: () => void;
28
+ } | null>(null);
29
+ const [aborted, setAborted] = useState(false);
30
+
31
+ // Give parent a way to grant permission
32
+ React.useEffect(() => {
33
+ // Show permission prompt immediately
34
+ const pr = {
35
+ resolve: () => setPermissionRequest(null),
36
+ };
37
+ setPermissionRequest(pr);
38
+ onPermissionReady(() => pr.resolve());
39
+ }, [onPermissionReady]);
40
+
41
+ // App's ESC handler — same pattern as real App.tsx
42
+ useInput((_input: string, key: { escape: boolean }) => {
43
+ if (!key.escape || !isThinking) return;
44
+ if (permissionRequest) return;
45
+ setAborted(true);
46
+ onAbort();
47
+ });
48
+
49
+ // Permission prompt's own useInput (when active)
50
+ useInput(
51
+ (_input: string, key: { return: boolean }) => {
52
+ if (key.return && permissionRequest) {
53
+ permissionRequest.resolve();
54
+ }
55
+ },
56
+ { isActive: !!permissionRequest },
57
+ );
58
+
59
+ return (
60
+ <Box flexDirection="column">
61
+ {permissionRequest && <Text color="yellow">[Permission Required]</Text>}
62
+ {!permissionRequest && isThinking && <Text color="cyan">Streaming...</Text>}
63
+ {aborted && <Text color="red">Aborted!</Text>}
64
+ <Text dimColor>
65
+ thinking={String(isThinking)} permission={String(!!permissionRequest)} aborted=
66
+ {String(aborted)}
67
+ </Text>
68
+ </Box>
69
+ );
70
+ }
71
+
72
+ describe('ESC abort after permission prompt', () => {
73
+ it('ESC works when no permission prompt was shown', async () => {
74
+ let abortCalled = false;
75
+ const grantHolder: { fn: (() => void) | null } = { fn: null };
76
+
77
+ const { stdin, lastFrame } = render(
78
+ <AbortAfterPermissionApp
79
+ onAbort={() => {
80
+ abortCalled = true;
81
+ }}
82
+ onPermissionReady={(fn) => {
83
+ grantHolder.fn = fn;
84
+ }}
85
+ />,
86
+ );
87
+
88
+ // Wait for mount
89
+ await new Promise((r) => setTimeout(r, 20));
90
+
91
+ // Grant permission immediately
92
+ grantHolder.fn?.();
93
+ await new Promise((r) => setTimeout(r, 50));
94
+
95
+ // Now ESC should work
96
+ stdin.write('\x1B');
97
+ await new Promise((r) => setTimeout(r, 50));
98
+
99
+ expect(abortCalled).toBe(true);
100
+ expect(lastFrame()!).toContain('Aborted!');
101
+ });
102
+
103
+ it('ESC works AFTER permission prompt was shown and dismissed', async () => {
104
+ let abortCalled = false;
105
+ const grantHolder: { fn: (() => void) | null } = { fn: null };
106
+
107
+ const { stdin, lastFrame } = render(
108
+ <AbortAfterPermissionApp
109
+ onAbort={() => {
110
+ abortCalled = true;
111
+ }}
112
+ onPermissionReady={(fn) => {
113
+ grantHolder.fn = fn;
114
+ }}
115
+ />,
116
+ );
117
+
118
+ // Wait for permission prompt to appear
119
+ await new Promise((r) => setTimeout(r, 20));
120
+ expect(lastFrame()!).toContain('[Permission Required]');
121
+
122
+ // Grant permission (dismiss prompt)
123
+ grantHolder.fn?.();
124
+ await new Promise((r) => setTimeout(r, 50));
125
+
126
+ // Permission dismissed, streaming should show
127
+ expect(lastFrame()!).toContain('Streaming...');
128
+ expect(lastFrame()!).not.toContain('[Permission Required]');
129
+
130
+ // Now press ESC — should trigger abort
131
+ stdin.write('\x1B');
132
+ await new Promise((r) => setTimeout(r, 50));
133
+
134
+ expect(abortCalled).toBe(true);
135
+ expect(lastFrame()!).toContain('Aborted!');
136
+ });
137
+
138
+ it('ESC does NOT work during permission prompt', async () => {
139
+ let abortCalled = false;
140
+
141
+ const { stdin, lastFrame } = render(
142
+ <AbortAfterPermissionApp
143
+ onAbort={() => {
144
+ abortCalled = true;
145
+ }}
146
+ onPermissionReady={() => {}}
147
+ />,
148
+ );
149
+
150
+ await new Promise((r) => setTimeout(r, 20));
151
+ expect(lastFrame()!).toContain('[Permission Required]');
152
+
153
+ // ESC during permission prompt should NOT trigger abort
154
+ stdin.write('\x1B');
155
+ await new Promise((r) => setTimeout(r, 50));
156
+
157
+ expect(abortCalled).toBe(false);
158
+ expect(lastFrame()!).not.toContain('Aborted!');
159
+ });
160
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * E2E-style test for abort during streaming.
3
+ * Uses ink-testing-library to verify that:
4
+ * 1. Streaming text debounce works (renders batched, not per-delta)
5
+ * 2. ESC during streaming triggers abort
6
+ * 3. Partial text is preserved after abort
7
+ */
8
+
9
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
10
+ import { render } from 'ink-testing-library';
11
+ import { Box, Text, useInput } from 'ink';
12
+ import { describe, it, expect } from 'vitest';
13
+
14
+ /**
15
+ * Minimal streaming component that simulates the debounced onTextDelta pattern.
16
+ * Accepts deltas via a callback ref, renders accumulated text.
17
+ */
18
+ function StreamingTestApp({
19
+ onReady,
20
+ onAbort,
21
+ }: {
22
+ onReady: (appendDelta: (text: string) => void) => void;
23
+ onAbort: () => void;
24
+ }): React.ReactElement {
25
+ const [text, setText] = useState('');
26
+ const textRef = useRef('');
27
+ const [aborted, setAborted] = useState(false);
28
+ const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
29
+ const renderCountRef = useRef(0);
30
+
31
+ // Debounced delta handler (same pattern as InteractiveSession)
32
+ const appendDelta = useCallback((delta: string) => {
33
+ textRef.current += delta;
34
+ if (!flushTimerRef.current) {
35
+ flushTimerRef.current = setTimeout(() => {
36
+ setText(textRef.current);
37
+ flushTimerRef.current = null;
38
+ }, 16);
39
+ }
40
+ }, []);
41
+
42
+ // ESC handler
43
+ useInput((_input, key) => {
44
+ if (key.escape) {
45
+ setAborted(true);
46
+ onAbort();
47
+ // Force flush any pending text
48
+ if (flushTimerRef.current) {
49
+ clearTimeout(flushTimerRef.current);
50
+ flushTimerRef.current = null;
51
+ }
52
+ setText(textRef.current);
53
+ }
54
+ });
55
+
56
+ // Notify parent that we're ready
57
+ useEffect(() => {
58
+ onReady(appendDelta);
59
+ }, [onReady, appendDelta]);
60
+
61
+ renderCountRef.current++;
62
+
63
+ return (
64
+ <Box flexDirection="column">
65
+ <Text>{text}</Text>
66
+ {aborted && <Text color="yellow">Interrupted by user.</Text>}
67
+ <Text dimColor>renders: {renderCountRef.current}</Text>
68
+ </Box>
69
+ );
70
+ }
71
+
72
+ describe('Streaming abort E2E', () => {
73
+ it('debounced streaming renders fewer times than delta count', async () => {
74
+ let appendDelta: ((text: string) => void) | null = null;
75
+
76
+ const { lastFrame } = render(
77
+ React.createElement(StreamingTestApp, {
78
+ onReady: (fn: (text: string) => void) => {
79
+ appendDelta = fn;
80
+ },
81
+ onAbort: () => {},
82
+ }),
83
+ );
84
+
85
+ // Wait for component to mount
86
+ await new Promise((r) => setTimeout(r, 20));
87
+ expect(appendDelta).not.toBeNull();
88
+
89
+ // Send 20 rapid deltas
90
+ for (let i = 0; i < 20; i++) {
91
+ appendDelta!(`chunk${i} `);
92
+ }
93
+
94
+ // Wait for debounce flush
95
+ await new Promise((r) => setTimeout(r, 50));
96
+
97
+ const frame = lastFrame()!;
98
+ // All text should be present
99
+ expect(frame).toContain('chunk0');
100
+ expect(frame).toContain('chunk19');
101
+
102
+ // Render count should be MUCH less than 20 (debounced)
103
+ const renderMatch = frame.match(/renders: (\d+)/);
104
+ expect(renderMatch).not.toBeNull();
105
+ const renderCount = parseInt(renderMatch![1], 10);
106
+ // With 16ms debounce and ~50ms total time, expect 3-5 renders, not 20+
107
+ expect(renderCount).toBeLessThan(10);
108
+ });
109
+
110
+ it('ESC during rapid streaming triggers abort and shows text', async () => {
111
+ let appendDelta: ((text: string) => void) | null = null;
112
+ let abortCalled = false;
113
+
114
+ const { stdin, lastFrame } = render(
115
+ React.createElement(StreamingTestApp, {
116
+ onReady: (fn: (text: string) => void) => {
117
+ appendDelta = fn;
118
+ },
119
+ onAbort: () => {
120
+ abortCalled = true;
121
+ },
122
+ }),
123
+ );
124
+
125
+ await new Promise((r) => setTimeout(r, 20));
126
+
127
+ // Send some deltas
128
+ for (let i = 0; i < 5; i++) {
129
+ appendDelta!(`line${i} `);
130
+ }
131
+
132
+ // Press ESC
133
+ stdin.write('\x1B');
134
+ await new Promise((r) => setTimeout(r, 50));
135
+
136
+ expect(abortCalled).toBe(true);
137
+
138
+ const frame = lastFrame()!;
139
+ // Text should be visible (flush on abort)
140
+ expect(frame).toContain('line0');
141
+ expect(frame).toContain('line4');
142
+ // Cancelled indicator
143
+ expect(frame).toContain('Interrupted by user.');
144
+ });
145
+
146
+ it('ESC during ongoing streaming stops further rendering', async () => {
147
+ let appendDelta: ((text: string) => void) | null = null;
148
+ let abortCalled = false;
149
+
150
+ const { stdin, lastFrame } = render(
151
+ React.createElement(StreamingTestApp, {
152
+ onReady: (fn: (text: string) => void) => {
153
+ appendDelta = fn;
154
+ },
155
+ onAbort: () => {
156
+ abortCalled = true;
157
+ },
158
+ }),
159
+ );
160
+
161
+ await new Promise((r) => setTimeout(r, 20));
162
+
163
+ // Send first batch
164
+ appendDelta!('before_abort ');
165
+
166
+ // Wait for flush
167
+ await new Promise((r) => setTimeout(r, 20));
168
+
169
+ // Press ESC
170
+ stdin.write('\x1B');
171
+ await new Promise((r) => setTimeout(r, 30));
172
+
173
+ expect(abortCalled).toBe(true);
174
+
175
+ // Send more deltas AFTER abort (should still accumulate in ref but component should show Cancelled)
176
+ appendDelta!('after_abort ');
177
+ await new Promise((r) => setTimeout(r, 50));
178
+
179
+ const frame = lastFrame()!;
180
+ expect(frame).toContain('before_abort');
181
+ expect(frame).toContain('Interrupted by user.');
182
+ });
183
+ });
@@ -0,0 +1,317 @@
1
+ /**
2
+ * #3189: the full App on a wire channel. The session picker lists the host's sessions, and choosing
3
+ * one asks the host to switch instead of building a new channel.
4
+ */
5
+
6
+ import { mkdtempSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ import { render } from 'ink-testing-library';
11
+ import React from 'react';
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
+
14
+ import App from '../App.js';
15
+ import { WireTuiChannel } from '../wire-tui-channel.js';
16
+
17
+ import type { IAttachedSessionConnection } from '../attached-session-connection.js';
18
+ import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
19
+ import type { TClientMessage, TServerMessage } from '@robota-sdk/agent-transport/client';
20
+
21
+ const TICK_MS = 30;
22
+ const FRAME_DEADLINE_MS = 3000;
23
+
24
+ function tick(ms = TICK_MS): Promise<void> {
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
27
+
28
+ async function waitFor(predicate: () => boolean, describeFailure: () => string): Promise<void> {
29
+ const deadline = Date.now() + FRAME_DEADLINE_MS;
30
+ while (Date.now() < deadline) {
31
+ if (predicate()) return;
32
+ await tick(10);
33
+ }
34
+ throw new Error(`waitFor timeout\n${describeFailure()}`);
35
+ }
36
+
37
+ function scriptedConnection(): IAttachedSessionConnection & {
38
+ sent: TClientMessage[];
39
+ push: (message: TServerMessage) => void;
40
+ } {
41
+ const listeners = new Set<(message: TServerMessage) => void>();
42
+ const sent: TClientMessage[] = [];
43
+ return {
44
+ sent,
45
+ send: (message) => {
46
+ sent.push(message);
47
+ },
48
+ subscribe: (listener) => {
49
+ listeners.add(listener);
50
+ return () => listeners.delete(listener);
51
+ },
52
+ onClose: () => () => undefined,
53
+ push: (message) => {
54
+ for (const listener of [...listeners]) listener(message);
55
+ },
56
+ };
57
+ }
58
+
59
+ function cliAdapter(settingsPath: string): ITuiCliAdapter {
60
+ return {
61
+ getUserSettingsPath: () => settingsPath,
62
+ readSettings: () => ({}),
63
+ reloadPluginCommandSource: vi.fn(),
64
+ applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
65
+ getGitBranch: vi.fn().mockReturnValue(undefined),
66
+ getProviderDisplayName: vi.fn((type: string) => type),
67
+ };
68
+ }
69
+
70
+ function lastListRequestId(sent: readonly TClientMessage[]): string {
71
+ const requests = sent.filter(
72
+ (message): message is Extract<TClientMessage, { type: 'list-sessions' }> =>
73
+ message.type === 'list-sessions',
74
+ );
75
+ return requests.at(-1)?.requestId ?? '';
76
+ }
77
+
78
+ describe('the full App attached to a host over the wire', () => {
79
+ let cwd: string;
80
+
81
+ beforeEach(() => {
82
+ cwd = mkdtempSync(join(tmpdir(), 'agent-attached-app-'));
83
+ });
84
+
85
+ afterEach(() => {
86
+ rmSync(cwd, { recursive: true, force: true });
87
+ });
88
+
89
+ it("lists the host's sessions in the picker and asks the host to switch", async () => {
90
+ const link = scriptedConnection();
91
+ const channel = new WireTuiChannel({ connection: link, driverId: 'attach:1' });
92
+ const createChannel = vi.fn(() => channel);
93
+ const view = render(
94
+ <App
95
+ cwd={cwd}
96
+ createChannel={createChannel}
97
+ requestSessionSwitch={(sessionId) => channel.requestSessionSwitch(sessionId)}
98
+ cliAdapter={cliAdapter(join(cwd, 'settings.json'))}
99
+ />,
100
+ );
101
+ try {
102
+ await waitFor(
103
+ () => link.sent.some((message) => message.type === 'list-sessions'),
104
+ () => JSON.stringify(link.sent),
105
+ );
106
+ // `/resume` ran on the host, which routes the picker back to this terminal only.
107
+ link.push({
108
+ type: 'ui_intent',
109
+ event: { intent: { type: 'show-session-picker' }, requesterDriverId: 'attach:1' },
110
+ });
111
+ link.push({
112
+ type: 'sessions',
113
+ requestId: lastListRequestId(link.sent),
114
+ listing: {
115
+ currentSessionId: 'host-current',
116
+ sessions: [
117
+ {
118
+ id: 'host-other-session',
119
+ name: 'Host session',
120
+ cwd,
121
+ updatedAt: '2026-09-26T10:00:00.000Z',
122
+ messageCount: 4,
123
+ preview: 'kept by the daemon',
124
+ },
125
+ ],
126
+ unreadableSessionIds: [],
127
+ },
128
+ });
129
+ await waitFor(
130
+ () => (view.lastFrame() ?? '').includes('Host session'),
131
+ () => view.lastFrame() ?? '<none>',
132
+ );
133
+ expect(view.lastFrame()).toContain('Select a session to resume');
134
+ await tick();
135
+
136
+ link.sent.length = 0;
137
+ view.stdin.write('\r');
138
+ await waitFor(
139
+ () => link.sent.some((message) => message.type === 'switch-session'),
140
+ () => JSON.stringify(link.sent),
141
+ );
142
+ expect(link.sent).toContainEqual({
143
+ type: 'switch-session',
144
+ sessionId: 'host-other-session',
145
+ requestId: expect.any(String),
146
+ });
147
+ // The App keeps its channel; the host's `session_switched` is what moves it.
148
+ expect(createChannel).toHaveBeenCalledTimes(1);
149
+ // A refusal is shown where the terminal shows what happened, and the switch is over.
150
+ const request = link.sent.find((message) => message.type === 'switch-session');
151
+ link.push({
152
+ type: 'session_change_failed',
153
+ code: 'limit',
154
+ message: 'Four sessions are live; none can be closed.',
155
+ ...(request?.type === 'switch-session' && request.requestId !== undefined
156
+ ? { requestId: request.requestId }
157
+ : {}),
158
+ });
159
+ await waitFor(
160
+ () => (view.lastFrame() ?? '').includes('Four sessions are live; none can be closed.'),
161
+ () => view.lastFrame() ?? '<none>',
162
+ );
163
+ } finally {
164
+ view.unmount();
165
+ await channel.stop();
166
+ }
167
+ });
168
+
169
+ async function resumeAgainst(
170
+ answer: (requestId: string) => TServerMessage,
171
+ notice: string,
172
+ ): Promise<string> {
173
+ const link = scriptedConnection();
174
+ const channel = new WireTuiChannel({ connection: link, driverId: 'attach:1' });
175
+ const view = render(
176
+ <App
177
+ cwd={cwd}
178
+ createChannel={() => channel}
179
+ requestSessionSwitch={(sessionId) => channel.requestSessionSwitch(sessionId)}
180
+ cliAdapter={cliAdapter(join(cwd, 'settings.json'))}
181
+ />,
182
+ );
183
+ try {
184
+ await waitFor(
185
+ () => (view.lastFrame() ?? '').includes('Type a message'),
186
+ () => view.lastFrame() ?? '<none>',
187
+ );
188
+ link.sent.length = 0;
189
+ link.push({
190
+ type: 'ui_intent',
191
+ event: { intent: { type: 'show-session-picker' }, requesterDriverId: 'attach:1' },
192
+ });
193
+ link.push(answer(lastListRequestId(link.sent)));
194
+ await waitFor(
195
+ () => (view.lastFrame() ?? '').includes(notice),
196
+ () => view.lastFrame() ?? '<none>',
197
+ );
198
+ await tick();
199
+ return view.lastFrame() ?? '';
200
+ } finally {
201
+ view.unmount();
202
+ await channel.stop();
203
+ }
204
+ }
205
+
206
+ it('gives the prompt back with the reason when the host cannot list its sessions', async () => {
207
+ const frame = await resumeAgainst(
208
+ (requestId) => ({
209
+ type: 'sessions_error',
210
+ requestId,
211
+ code: 'not_available',
212
+ message: 'Session listing is not available on this host.',
213
+ }),
214
+ 'Session listing is not available on this host.',
215
+ );
216
+ expect(frame).not.toContain('Select a session to resume');
217
+ expect(frame).not.toContain('Waiting for response');
218
+ expect(frame).toContain('Type a message');
219
+ });
220
+
221
+ it("prints the new session's transcript when the switch and its history arrive together", async () => {
222
+ const link = scriptedConnection();
223
+ const channel = new WireTuiChannel({ connection: link, driverId: 'attach:1' });
224
+ const view = render(
225
+ <App
226
+ cwd={cwd}
227
+ createChannel={() => channel}
228
+ requestSessionSwitch={(sessionId) => channel.requestSessionSwitch(sessionId)}
229
+ cliAdapter={cliAdapter(join(cwd, 'settings.json'))}
230
+ />,
231
+ );
232
+ const entry = (id: string, content: string) => ({
233
+ id,
234
+ timestamp: '2026-09-26T10:00:00.000Z',
235
+ category: 'chat',
236
+ type: 'user',
237
+ data: { id: `m-${id}`, role: 'user', content, state: 'complete' },
238
+ });
239
+ try {
240
+ await waitFor(
241
+ () => link.sent.some((message) => message.type === 'get-history'),
242
+ () => JSON.stringify(link.sent),
243
+ );
244
+ link.push({
245
+ type: 'history',
246
+ startIndex: 0,
247
+ total: 3,
248
+ entries: [entry('o1', 'old one'), entry('o2', 'old two'), entry('o3', 'old three')],
249
+ });
250
+ await waitFor(
251
+ () => (view.lastFrame() ?? '').includes('old three'),
252
+ () => view.lastFrame() ?? '<none>',
253
+ );
254
+ // One chunk from the host: the switch and the new session's history, handled in one pass.
255
+ link.push({ type: 'session_switched', event: { sessionId: 'session-2' } });
256
+ link.push({
257
+ type: 'history',
258
+ startIndex: 0,
259
+ total: 1,
260
+ entries: [entry('n1', 'new session prompt')],
261
+ });
262
+ await waitFor(
263
+ () => (view.lastFrame() ?? '').includes('new session prompt'),
264
+ () => view.lastFrame() ?? '<none>',
265
+ );
266
+ expect(view.lastFrame()).toContain('Switched to session session-2.');
267
+ } finally {
268
+ view.unmount();
269
+ await channel.stop();
270
+ }
271
+ });
272
+
273
+ it('detaches on Ctrl-] the way it does on Ctrl-C and /exit', async () => {
274
+ const link = scriptedConnection();
275
+ const channel = new WireTuiChannel({ connection: link, driverId: 'attach:1' });
276
+ const view = render(
277
+ <App
278
+ cwd={cwd}
279
+ createChannel={() => channel}
280
+ requestSessionSwitch={(sessionId) => channel.requestSessionSwitch(sessionId)}
281
+ cliAdapter={cliAdapter(join(cwd, 'settings.json'))}
282
+ />,
283
+ );
284
+ try {
285
+ await waitFor(
286
+ () => (view.lastFrame() ?? '').includes('Type a message'),
287
+ () => view.lastFrame() ?? '<none>',
288
+ );
289
+ view.stdin.write('\x1d');
290
+ await waitFor(
291
+ () => channel.getSnapshot().isShuttingDown,
292
+ () => view.lastFrame() ?? '<none>',
293
+ );
294
+ expect(JSON.stringify(channel.getSnapshot().history)).toContain(
295
+ 'Detached. The session keeps running.',
296
+ );
297
+ expect(link.sent.some((message) => message.type === 'submit')).toBe(false);
298
+ } finally {
299
+ view.unmount();
300
+ await channel.stop();
301
+ }
302
+ });
303
+
304
+ it('opens no empty picker when the host has no sessions to resume', async () => {
305
+ const frame = await resumeAgainst(
306
+ (requestId) => ({
307
+ type: 'sessions',
308
+ requestId,
309
+ listing: { currentSessionId: 'host-current', sessions: [], unreadableSessionIds: [] },
310
+ }),
311
+ 'No saved sessions to resume',
312
+ );
313
+ expect(frame).not.toContain('Select a session to resume');
314
+ expect(frame).not.toContain('Waiting for response');
315
+ expect(frame).toContain('Type a message');
316
+ });
317
+ });