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

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 (328) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +27 -0
  3. package/dist/node/index.cjs +32 -0
  4. package/dist/node/index.d.cts +1066 -0
  5. package/dist/node/index.d.cts.map +1 -0
  6. package/dist/node/index.d.ts +1066 -0
  7. package/dist/node/index.d.ts.map +1 -0
  8. package/dist/node/index.js +33 -0
  9. package/dist/node/index.js.map +1 -0
  10. package/package.json +93 -0
  11. package/src/App.tsx +146 -0
  12. package/src/AppPresentation.tsx +190 -0
  13. package/src/AppView.tsx +26 -0
  14. package/src/BackgroundTaskPanel.tsx +73 -0
  15. package/src/CjkTextInput.tsx +336 -0
  16. package/src/ConfirmPrompt.tsx +140 -0
  17. package/src/ContextWarningBanner.tsx +45 -0
  18. package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
  19. package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
  20. package/src/HistorySearchOverlay.tsx +150 -0
  21. package/src/InputArea.tsx +338 -0
  22. package/src/ListPicker.tsx +158 -0
  23. package/src/MenuSelect.tsx +159 -0
  24. package/src/MessageList.tsx +308 -0
  25. package/src/MultiSelectList.tsx +294 -0
  26. package/src/PendingActionPrompt.tsx +121 -0
  27. package/src/PermissionPrompt.tsx +186 -0
  28. package/src/PluginTUI.tsx +258 -0
  29. package/src/RoleLabel.tsx +72 -0
  30. package/src/SafeText.tsx +66 -0
  31. package/src/SessionEventNotices.tsx +27 -0
  32. package/src/SessionPicker.tsx +72 -0
  33. package/src/SessionStatusBar.tsx +76 -0
  34. package/src/SlashAutocomplete.tsx +154 -0
  35. package/src/StatusBar.tsx +284 -0
  36. package/src/StreamingIndicator.tsx +145 -0
  37. package/src/SupervisedSessionView.tsx +455 -0
  38. package/src/TextPrompt.tsx +112 -0
  39. package/src/ThemePicker.tsx +366 -0
  40. package/src/ToolCommandOutput.tsx +48 -0
  41. package/src/ToolDiffBlock.tsx +44 -0
  42. package/src/TransportTUI.tsx +166 -0
  43. package/src/TuiInteractionChannel.ts +567 -0
  44. package/src/UpdateNotice.tsx +18 -0
  45. package/src/UsageSummaryEntry.tsx +49 -0
  46. package/src/WaveText.tsx +70 -0
  47. package/src/__tests__/ListPicker.test.tsx +188 -0
  48. package/src/__tests__/MenuSelect.test.tsx +127 -0
  49. package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
  50. package/src/__tests__/PluginTUI.test.tsx +167 -0
  51. package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
  52. package/src/__tests__/SupervisedSessionView-render.test.tsx +24 -0
  53. package/src/__tests__/SupervisedSessionView.test.tsx +629 -0
  54. package/src/__tests__/TextPrompt.test.tsx +112 -0
  55. package/src/__tests__/TransportTUI.test.tsx +124 -0
  56. package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
  57. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +284 -0
  58. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +642 -0
  59. package/src/__tests__/UpdateNotice.test.tsx +15 -0
  60. package/src/__tests__/abort-after-permission.test.tsx +160 -0
  61. package/src/__tests__/abort-streaming-e2e.test.tsx +183 -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__/cjk-defer-submit.test.tsx +67 -0
  69. package/src/__tests__/cjk-fallback-render.test.tsx +78 -0
  70. package/src/__tests__/cjk-text-input-flow.test.ts +186 -0
  71. package/src/__tests__/cjk-text-input.test.ts +191 -0
  72. package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
  73. package/src/__tests__/command-output-summary.test.ts +95 -0
  74. package/src/__tests__/command-result-handler.test.ts +171 -0
  75. package/src/__tests__/compact-event-bridge.test.ts +20 -0
  76. package/src/__tests__/confirm-permission-flow.test.ts +149 -0
  77. package/src/__tests__/confirm-prompt.test.tsx +103 -0
  78. package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
  79. package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +45 -0
  80. package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
  81. package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
  82. package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
  83. package/src/__tests__/external-prompt-notice.test.tsx +100 -0
  84. package/src/__tests__/fixtures/command-handoff-driver.tsx +111 -0
  85. package/src/__tests__/fixtures/fake-editor.sh +7 -0
  86. package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
  87. package/src/__tests__/fork-attach.test.tsx +235 -0
  88. package/src/__tests__/helpers/terminal-profiles.ts +152 -0
  89. package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
  90. package/src/__tests__/history-clear-broadcast.test.ts +66 -0
  91. package/src/__tests__/history-search-overlay.test.tsx +329 -0
  92. package/src/__tests__/humanize-tool-name.test.ts +30 -0
  93. package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
  94. package/src/__tests__/input-area-flow.test.ts +199 -0
  95. package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
  96. package/src/__tests__/interaction-channel-charter.test.ts +13 -0
  97. package/src/__tests__/key-hint-consistency.test.tsx +249 -0
  98. package/src/__tests__/key-hint-footer.test.tsx +69 -0
  99. package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
  100. package/src/__tests__/message-list-rendering.test.tsx +473 -0
  101. package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
  102. package/src/__tests__/output-style-reaches-session.test.ts +35 -0
  103. package/src/__tests__/palette-consistency.test.ts +330 -0
  104. package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
  105. package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
  106. package/src/__tests__/product-display-name.test.tsx +79 -0
  107. package/src/__tests__/prompt-queue.test.tsx +260 -0
  108. package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
  109. package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
  110. package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
  111. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +342 -0
  112. package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
  113. package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
  114. package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
  115. package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
  116. package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
  117. package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
  118. package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
  119. package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +291 -0
  120. package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +165 -0
  121. package/src/__tests__/pty/ime-cursor.ptytest.ts +186 -0
  122. package/src/__tests__/pty/isolated-home.ts +64 -0
  123. package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
  124. package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
  125. package/src/__tests__/pty/pty-driver.ts +153 -0
  126. package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
  127. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +134 -0
  128. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +138 -0
  129. package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
  130. package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
  131. package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
  132. package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
  133. package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
  134. package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
  135. package/src/__tests__/pty/spawn-pty.test.ts +120 -0
  136. package/src/__tests__/pty/spawn-pty.ts +229 -0
  137. package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
  138. package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
  139. package/src/__tests__/pty-driver-dispose.test.ts +43 -0
  140. package/src/__tests__/real-cursor-positioning.test.tsx +263 -0
  141. package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
  142. package/src/__tests__/render-channel-options.test.ts +201 -0
  143. package/src/__tests__/render-lifecycle.test.ts +36 -0
  144. package/src/__tests__/render-markdown.test.ts +164 -0
  145. package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
  146. package/src/__tests__/safe-text-boundary.test.tsx +75 -0
  147. package/src/__tests__/screen-reader-input.test.tsx +106 -0
  148. package/src/__tests__/screen-reader-labels.test.ts +91 -0
  149. package/src/__tests__/screen-reader-menus.test.tsx +200 -0
  150. package/src/__tests__/screen-reader-pacing.test.ts +230 -0
  151. package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
  152. package/src/__tests__/screen-reader-render-options.test.ts +262 -0
  153. package/src/__tests__/screen-reader-stdout.test.ts +322 -0
  154. package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
  155. package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
  156. package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
  157. package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
  158. package/src/__tests__/selection-flow.test.ts +61 -0
  159. package/src/__tests__/session-event-notices.test.tsx +31 -0
  160. package/src/__tests__/session-init-poller.test.ts +102 -0
  161. package/src/__tests__/session-switch-channel.test.tsx +470 -0
  162. package/src/__tests__/shutdown-signal.test.ts +34 -0
  163. package/src/__tests__/status-activity.test.ts +71 -0
  164. package/src/__tests__/status-bar.test.tsx +282 -0
  165. package/src/__tests__/status-glyph.test.ts +30 -0
  166. package/src/__tests__/streaming-indicator.test.tsx +177 -0
  167. package/src/__tests__/terminal-capabilities.test.ts +153 -0
  168. package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
  169. package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
  170. package/src/__tests__/text-prompt-flow.test.ts +77 -0
  171. package/src/__tests__/theme-picker.test.tsx +345 -0
  172. package/src/__tests__/transport-boundary.test.ts +11 -0
  173. package/src/__tests__/tui-app-channel-port.test.ts +31 -0
  174. package/src/__tests__/tui-channel-init-failure.test.ts +62 -0
  175. package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
  176. package/src/__tests__/tui-interaction-queues.test.ts +139 -0
  177. package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
  178. package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
  179. package/src/__tests__/tui-state-manager.test.ts +451 -0
  180. package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
  181. package/src/__tests__/wave-text.test.tsx +133 -0
  182. package/src/app-banner.tsx +42 -0
  183. package/src/app-static-items.ts +35 -0
  184. package/src/app-view-model.ts +136 -0
  185. package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
  186. package/src/attention/__tests__/attention-tracker.test.ts +103 -0
  187. package/src/attention/__tests__/countdown.test.ts +15 -0
  188. package/src/attention/__tests__/focus-input-filter.test.ts +92 -0
  189. package/src/attention/__tests__/interval-recap.test.ts +49 -0
  190. package/src/attention/attention-coordinator.ts +71 -0
  191. package/src/attention/attention-tracker.ts +124 -0
  192. package/src/attention/countdown.ts +24 -0
  193. package/src/attention/focus-input-filter.ts +131 -0
  194. package/src/attention/interval-recap.ts +148 -0
  195. package/src/attention/time-units.ts +6 -0
  196. package/src/attention-bell.ts +79 -0
  197. package/src/attributed-user-echo.ts +30 -0
  198. package/src/background-task-row-format.ts +99 -0
  199. package/src/command-interaction.ts +8 -0
  200. package/src/command-output-summary.ts +129 -0
  201. package/src/create-default-tui-cli-adapter.ts +45 -0
  202. package/src/execution-workspace-switcher-selection.ts +118 -0
  203. package/src/execution-workspace-view-model.ts +139 -0
  204. package/src/external-prompt-notice.tsx +38 -0
  205. package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
  206. package/src/flows/__tests__/defer-submit.test.ts +87 -0
  207. package/src/flows/__tests__/real-cursor-flow.test.ts +124 -0
  208. package/src/flows/background-focus-flow.ts +44 -0
  209. package/src/flows/cjk-cursor-motion.ts +75 -0
  210. package/src/flows/cjk-text-deletion.ts +53 -0
  211. package/src/flows/cjk-text-input-flow.ts +248 -0
  212. package/src/flows/cjk-text-input-types.ts +24 -0
  213. package/src/flows/confirm-prompt-flow.ts +90 -0
  214. package/src/flows/defer-submit.ts +67 -0
  215. package/src/flows/fork-attach-flow.ts +62 -0
  216. package/src/flows/input-area-flow.ts +188 -0
  217. package/src/flows/permission-prompt-flow.ts +108 -0
  218. package/src/flows/real-cursor-flow.ts +83 -0
  219. package/src/flows/selection-flow.ts +205 -0
  220. package/src/flows/session-init-poller.ts +76 -0
  221. package/src/flows/text-prompt-flow.ts +93 -0
  222. package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
  223. package/src/history-search/history-search-flow.ts +94 -0
  224. package/src/history-search/useHistorySearch.ts +336 -0
  225. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +74 -0
  226. package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
  227. package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
  228. package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
  229. package/src/hooks/command-result-handler.ts +44 -0
  230. package/src/hooks/side-effects-types.ts +45 -0
  231. package/src/hooks/useAppController.ts +267 -0
  232. package/src/hooks/useAppInputBindings.ts +145 -0
  233. package/src/hooks/useAppInteractionState.ts +81 -0
  234. package/src/hooks/useAppLifecycleState.ts +81 -0
  235. package/src/hooks/useAppOverlays.ts +74 -0
  236. package/src/hooks/useAppScreenState.ts +81 -0
  237. package/src/hooks/useAppSubmissionState.ts +45 -0
  238. package/src/hooks/useAppThemeState.ts +157 -0
  239. package/src/hooks/useAppWorkspaceState.ts +149 -0
  240. package/src/hooks/useAppearanceSettings.ts +30 -0
  241. package/src/hooks/useAutocomplete.ts +101 -0
  242. package/src/hooks/useBackgroundPanel.ts +58 -0
  243. package/src/hooks/useCountdownTick.ts +24 -0
  244. package/src/hooks/useExecutionDetailPage.ts +69 -0
  245. package/src/hooks/useForkAttach.ts +60 -0
  246. package/src/hooks/useInputAreaHistorySearch.ts +72 -0
  247. package/src/hooks/useInputAreaKeys.ts +114 -0
  248. package/src/hooks/useNumberedSelection.ts +76 -0
  249. package/src/hooks/usePluginCallbacks.ts +38 -0
  250. package/src/hooks/usePluginScreenData.ts +85 -0
  251. package/src/hooks/useRealCursorPosition.ts +125 -0
  252. package/src/hooks/useRenderMarkdown.ts +38 -0
  253. package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
  254. package/src/hooks/useSideEffects.ts +144 -0
  255. package/src/hooks/useStatusLineSettings.ts +50 -0
  256. package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
  257. package/src/hooks/useTuiChannel.ts +129 -0
  258. package/src/humanize-tool-name.ts +51 -0
  259. package/src/index.ts +56 -0
  260. package/src/input-area-rules.tsx +71 -0
  261. package/src/json-value.ts +18 -0
  262. package/src/key-hint-footer.tsx +54 -0
  263. package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
  264. package/src/keybindings/__tests__/node-keybindings-source.test.ts +111 -0
  265. package/src/keybindings/keybinding-catalogue.ts +171 -0
  266. package/src/keybindings/keybinding-registry.ts +303 -0
  267. package/src/keybindings/keybinding-syntax.ts +155 -0
  268. package/src/keybindings/keybindings-context.tsx +124 -0
  269. package/src/keybindings/node-keybindings-source.ts +189 -0
  270. package/src/numbered-list.tsx +113 -0
  271. package/src/plugin-tui-handlers.ts +163 -0
  272. package/src/product-display-name-context.tsx +30 -0
  273. package/src/render-markdown.ts +209 -0
  274. package/src/render.tsx +544 -0
  275. package/src/sanitize-terminal-text.ts +171 -0
  276. package/src/screen-reader-announcement.ts +52 -0
  277. package/src/screen-reader-context.tsx +32 -0
  278. package/src/screen-reader-labels.ts +64 -0
  279. package/src/screen-reader-pacing-context.tsx +47 -0
  280. package/src/screen-reader-pacing.ts +178 -0
  281. package/src/screen-reader-stdout.ts +328 -0
  282. package/src/shutdown-signal.ts +34 -0
  283. package/src/status-activity.ts +70 -0
  284. package/src/status-glyph.ts +78 -0
  285. package/src/terminal-capabilities-context.tsx +27 -0
  286. package/src/terminal-capabilities.ts +77 -0
  287. package/src/terminal-focus-reporting.ts +50 -0
  288. package/src/terminal-handoff-controller.ts +98 -0
  289. package/src/terminal-marks.ts +63 -0
  290. package/src/theme/__tests__/built-in-themes.test.ts +117 -0
  291. package/src/theme/__tests__/color-vision.test.ts +82 -0
  292. package/src/theme/__tests__/theme-context.test.tsx +146 -0
  293. package/src/theme/__tests__/theme-document.test.ts +222 -0
  294. package/src/theme/__tests__/theme-registry.test.ts +138 -0
  295. package/src/theme/__tests__/theme-styles.test.ts +233 -0
  296. package/src/theme/built-in-themes.ts +342 -0
  297. package/src/theme/color-vision.ts +171 -0
  298. package/src/theme/index.ts +45 -0
  299. package/src/theme/theme-context.tsx +81 -0
  300. package/src/theme/theme-contracts.ts +139 -0
  301. package/src/theme/theme-document.ts +278 -0
  302. package/src/theme/theme-registry.ts +127 -0
  303. package/src/theme/theme-styles.ts +177 -0
  304. package/src/tool-summary-status.ts +59 -0
  305. package/src/tui-app-channel-port.ts +83 -0
  306. package/src/tui-channel-lifecycle-coordinator.ts +149 -0
  307. package/src/tui-channel-options.ts +172 -0
  308. package/src/tui-cli-adapter-context.tsx +13 -0
  309. package/src/tui-cli-adapter.ts +24 -0
  310. package/src/tui-interaction-queues.ts +163 -0
  311. package/src/tui-session-binding.ts +52 -0
  312. package/src/tui-session-event-projector.ts +140 -0
  313. package/src/tui-session-events.ts +82 -0
  314. package/src/tui-session-notice-store.ts +43 -0
  315. package/src/tui-session-options.ts +118 -0
  316. package/src/tui-state-manager.ts +327 -0
  317. package/src/types.ts +19 -0
  318. package/src/use-terminal-title.ts +29 -0
  319. package/src/utils/__tests__/edit-diff.test.ts +426 -0
  320. package/src/utils/__tests__/paste-detection.test.ts +116 -0
  321. package/src/utils/__tests__/paste-labels.test.ts +46 -0
  322. package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
  323. package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
  324. package/src/utils/edit-diff.ts +151 -0
  325. package/src/utils/input-top-border.ts +19 -0
  326. package/src/utils/paste-labels.ts +9 -0
  327. package/src/utils/tool-call-extractor.ts +92 -0
  328. package/src/utils/tool-diff-summary.ts +75 -0
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Tests for prompt queue — submit during execution queues prompt,
3
+ * auto-executes after completion, backspace cancels queue, ESC aborts.
4
+ */
5
+
6
+ import React, { useState, useCallback, useEffect, useRef } from 'react';
7
+ import { cleanup, render } from 'ink-testing-library';
8
+ import { Box, Text, useInput } from 'ink';
9
+ import { afterEach, describe, it, expect, vi } from 'vitest';
10
+
11
+ interface IQueueTestController {
12
+ completeCurrent?: () => void;
13
+ }
14
+
15
+ const WAIT_FOR_ASSERTION_TIMEOUT_MS = 10000;
16
+ const WAIT_FOR_ASSERTION_INTERVAL_MS = 20;
17
+
18
+ async function waitForAssertion(
19
+ assertion: () => void,
20
+ timeoutMs = WAIT_FOR_ASSERTION_TIMEOUT_MS,
21
+ ): Promise<void> {
22
+ const startedAt = Date.now();
23
+ let lastError: Error | undefined;
24
+
25
+ while (Date.now() - startedAt < timeoutMs) {
26
+ try {
27
+ assertion();
28
+ return;
29
+ } catch (error) {
30
+ lastError = error instanceof Error ? error : new Error(String(error));
31
+ await new Promise((resolve) => setTimeout(resolve, WAIT_FOR_ASSERTION_INTERVAL_MS));
32
+ }
33
+ }
34
+
35
+ if (lastError) throw lastError;
36
+ throw new Error('Timed out waiting for assertion');
37
+ }
38
+
39
+ /**
40
+ * Minimal App that simulates the prompt queue behavior.
41
+ */
42
+ function QueueTestApp({
43
+ onExecute,
44
+ onAbort,
45
+ controller,
46
+ }: {
47
+ onExecute: (prompt: string) => void;
48
+ onAbort?: () => void;
49
+ controller?: IQueueTestController;
50
+ }): React.ReactElement {
51
+ const [isThinking, setIsThinking] = useState(false);
52
+ const thinkingRef = useRef(false);
53
+ const [pendingPrompt, setPendingPrompt] = useState<string | null>(null);
54
+ const pendingRef = useRef<string | null>(null);
55
+ const [isAborting, setIsAborting] = useState(false);
56
+ const [log, setLog] = useState<string[]>([]);
57
+
58
+ const executePrompt = useCallback(
59
+ async (input: string) => {
60
+ thinkingRef.current = true;
61
+ setIsThinking(true);
62
+ setLog((prev) => [...prev, `exec:${input}`]);
63
+ onExecute(input);
64
+ await new Promise<void>((resolve) => {
65
+ if (controller) {
66
+ controller.completeCurrent = () => {
67
+ controller.completeCurrent = undefined;
68
+ resolve();
69
+ };
70
+ return;
71
+ }
72
+ setTimeout(resolve, 300);
73
+ });
74
+ thinkingRef.current = false;
75
+ setIsThinking(false);
76
+ },
77
+ [controller, onExecute],
78
+ );
79
+
80
+ const handleSubmit = useCallback(
81
+ async (input: string) => {
82
+ if (thinkingRef.current) {
83
+ setPendingPrompt(input);
84
+ pendingRef.current = input;
85
+ setLog((prev) => [...prev, `queued:${input}`]);
86
+ return;
87
+ }
88
+ await executePrompt(input);
89
+ },
90
+ [executePrompt],
91
+ );
92
+
93
+ useInput((_input, key) => {
94
+ // ESC always aborts + clears queue
95
+ if (key.escape && thinkingRef.current) {
96
+ setIsAborting(true);
97
+ setPendingPrompt(null);
98
+ pendingRef.current = null;
99
+ onAbort?.();
100
+ }
101
+ // Backspace cancels queue only
102
+ if ((key.backspace || key.delete) && pendingRef.current) {
103
+ setPendingPrompt(null);
104
+ pendingRef.current = null;
105
+ setLog((prev) => [...prev, 'queue-cleared']);
106
+ }
107
+ });
108
+
109
+ // Auto-execute queued prompt when thinking ends
110
+ useEffect(() => {
111
+ if (!isThinking) {
112
+ setIsAborting(false);
113
+ if (pendingRef.current) {
114
+ const prompt = pendingRef.current;
115
+ setPendingPrompt(null);
116
+ pendingRef.current = null;
117
+ setTimeout(() => executePrompt(prompt), 0);
118
+ }
119
+ }
120
+ }, [isThinking, executePrompt]);
121
+
122
+ return (
123
+ <Box flexDirection="column">
124
+ <Text>thinking={String(isThinking)}</Text>
125
+ <Text>pending={pendingPrompt ?? 'none'}</Text>
126
+ <Text>aborting={String(isAborting)}</Text>
127
+ <Text>log={log.join(',')}</Text>
128
+ <SubmitTrigger onSubmit={handleSubmit} />
129
+ </Box>
130
+ );
131
+ }
132
+
133
+ function SubmitTrigger({
134
+ onSubmit,
135
+ }: {
136
+ onSubmit: (input: string) => Promise<void>;
137
+ }): React.ReactElement {
138
+ useInput((input) => {
139
+ if (input.startsWith('s:')) {
140
+ // INFRA-040: `useInput` takes a SYNCHRONOUS handler, so there is no caller to await this. The
141
+ // rejection is routed to the test runner rather than dropped — a submit that throws would
142
+ // otherwise leave the case green while the behaviour it drives never happened.
143
+ onSubmit(input.slice(2)).catch((error: unknown) => {
144
+ throw error instanceof Error ? error : new Error(String(error));
145
+ });
146
+ }
147
+ });
148
+ return <></>;
149
+ }
150
+
151
+ describe('Prompt Queue', () => {
152
+ afterEach(() => {
153
+ cleanup();
154
+ vi.clearAllMocks();
155
+ });
156
+
157
+ it('executes prompt normally when not thinking', async () => {
158
+ const onExecute = vi.fn();
159
+ const { stdin } = render(<QueueTestApp onExecute={onExecute} />);
160
+
161
+ stdin.write('s:hello');
162
+ await waitForAssertion(() => expect(onExecute).toHaveBeenCalledWith('hello'));
163
+
164
+ expect(onExecute).toHaveBeenCalledWith('hello');
165
+ });
166
+
167
+ it('queues prompt when thinking, auto-executes after completion', async () => {
168
+ const onExecute = vi.fn();
169
+ const controller: IQueueTestController = {};
170
+ const { stdin, lastFrame } = render(
171
+ <QueueTestApp onExecute={onExecute} controller={controller} />,
172
+ );
173
+
174
+ stdin.write('s:first');
175
+ await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
176
+
177
+ stdin.write('s:second');
178
+ await waitForAssertion(() => expect(lastFrame()!).toContain('pending=second'));
179
+
180
+ controller.completeCurrent?.();
181
+ await waitForAssertion(() => expect(onExecute).toHaveBeenCalledWith('second'));
182
+
183
+ expect(onExecute).toHaveBeenCalledWith('first');
184
+ expect(onExecute).toHaveBeenCalledWith('second');
185
+ });
186
+
187
+ it('only queues 1 prompt — last one wins', async () => {
188
+ const onExecute = vi.fn();
189
+ const controller: IQueueTestController = {};
190
+ const { stdin, lastFrame } = render(
191
+ <QueueTestApp onExecute={onExecute} controller={controller} />,
192
+ );
193
+
194
+ stdin.write('s:first');
195
+ await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
196
+
197
+ stdin.write('s:second');
198
+ await new Promise((r) => setTimeout(r, 5));
199
+ stdin.write('s:third');
200
+ await waitForAssertion(() => expect(lastFrame()!).toContain('pending=third'));
201
+
202
+ controller.completeCurrent?.();
203
+ await waitForAssertion(() => expect(onExecute).toHaveBeenCalledWith('third'));
204
+
205
+ expect(onExecute).toHaveBeenCalledWith('first');
206
+ expect(onExecute).toHaveBeenCalledWith('third');
207
+ expect(onExecute).not.toHaveBeenCalledWith('second');
208
+ });
209
+
210
+ it('ESC aborts execution and clears queue', async () => {
211
+ const onExecute = vi.fn();
212
+ const onAbort = vi.fn();
213
+ const controller: IQueueTestController = {};
214
+ const { stdin, lastFrame } = render(
215
+ <QueueTestApp onExecute={onExecute} onAbort={onAbort} controller={controller} />,
216
+ );
217
+
218
+ stdin.write('s:first');
219
+ await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
220
+
221
+ stdin.write('s:queued');
222
+ await waitForAssertion(() => expect(lastFrame()!).toContain('pending=queued'));
223
+
224
+ stdin.write('\x1B');
225
+ await waitForAssertion(() => expect(lastFrame()!).toContain('pending=none'));
226
+
227
+ expect(onAbort).toHaveBeenCalled();
228
+
229
+ controller.completeCurrent?.();
230
+ await new Promise((r) => setTimeout(r, 50));
231
+ expect(onExecute).not.toHaveBeenCalledWith('queued');
232
+ });
233
+
234
+ it('Backspace cancels queue without aborting', async () => {
235
+ const onExecute = vi.fn();
236
+ const onAbort = vi.fn();
237
+ const controller: IQueueTestController = {};
238
+ const { stdin, lastFrame } = render(
239
+ <QueueTestApp onExecute={onExecute} onAbort={onAbort} controller={controller} />,
240
+ );
241
+
242
+ stdin.write('s:first');
243
+ await waitForAssertion(() => expect(lastFrame()!).toContain('thinking=true'));
244
+
245
+ stdin.write('s:queued');
246
+ await waitForAssertion(() => expect(lastFrame()!).toContain('pending=queued'));
247
+
248
+ stdin.write('\x7F'); // backspace
249
+ await waitForAssertion(() => expect(lastFrame()!).toContain('pending=none'));
250
+
251
+ expect(lastFrame()!).toContain('queue-cleared');
252
+ expect(onAbort).not.toHaveBeenCalled();
253
+
254
+ // Execution continues normally
255
+ controller.completeCurrent?.();
256
+ await new Promise((r) => setTimeout(r, 50));
257
+ expect(onExecute).toHaveBeenCalledWith('first');
258
+ expect(onExecute).not.toHaveBeenCalledWith('queued');
259
+ });
260
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { toChannelOptions } from '../render.js';
4
+ import { buildTuiSessionOptions } from '../tui-session-options.js';
5
+
6
+ import type { IRenderOptions } from '../render.js';
7
+ import type { ITuiInteractionChannelOptions } from '../TuiInteractionChannel.js';
8
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
9
+
10
+ /**
11
+ * #1844 — the provider definitions must survive the whole mapping chain.
12
+ *
13
+ * `providerDefinitions` was declared on the session options and read by `/provider switch`, and no
14
+ * production code set it. The consequence is not a dormant field: with an empty list the hot-swap
15
+ * throws `Unknown provider: <name>. Currently supported: ` — an empty supported-list, which is both
16
+ * wrong and unactionable. Measured, not inferred.
17
+ *
18
+ * Each hop is asserted separately because a forward that stops at any one of them restores exactly
19
+ * the bug, and an end-to-end assertion alone would not say WHERE it stopped.
20
+ */
21
+ const DEFINITIONS = [
22
+ { type: 'anthropic', displayName: 'Anthropic' },
23
+ ] as unknown as readonly IProviderDefinition[];
24
+
25
+ const renderOptions = (over: Partial<IRenderOptions> = {}): IRenderOptions =>
26
+ ({ cwd: '/w', provider: {}, ...over }) as IRenderOptions;
27
+
28
+ describe('#1844 — provider definitions reach the session', () => {
29
+ it('render options carry them into the channel options', () => {
30
+ const channel = toChannelOptions(renderOptions({ providerDefinitions: DEFINITIONS }));
31
+
32
+ expect(channel.providerDefinitions).toBe(DEFINITIONS);
33
+ });
34
+
35
+ it('channel options carry them into the session options', () => {
36
+ const session = buildTuiSessionOptions({
37
+ cwd: '/w',
38
+ provider: {},
39
+ providerDefinitions: DEFINITIONS,
40
+ } as unknown as ITuiInteractionChannelOptions);
41
+
42
+ expect(session).toHaveProperty('providerDefinitions', DEFINITIONS);
43
+ });
44
+
45
+ it('the whole chain preserves them', () => {
46
+ const session = buildTuiSessionOptions(
47
+ toChannelOptions(
48
+ renderOptions({ providerDefinitions: DEFINITIONS }),
49
+ ) as unknown as ITuiInteractionChannelOptions,
50
+ );
51
+
52
+ expect(session).toHaveProperty('providerDefinitions', DEFINITIONS);
53
+ });
54
+
55
+ it('omits the key entirely when none were supplied, rather than setting an empty list', () => {
56
+ // An explicit `[]` would look like "the caller supplied none" and read as configured. Absent is
57
+ // the honest shape for "this surface did not provide them", and it also keeps
58
+ // `exactOptionalPropertyTypes` callers from having to pass undefined.
59
+ const session = buildTuiSessionOptions({
60
+ cwd: '/w',
61
+ provider: {},
62
+ } as unknown as ITuiInteractionChannelOptions);
63
+
64
+ expect('providerDefinitions' in session).toBe(false);
65
+ });
66
+ });
67
+
68
+ it('preserves product provider recovery guidance from render options through the TUI session', () => {
69
+ const providerErrorGuidance = { authentication: 'Configure product A.' };
70
+ const session = buildTuiSessionOptions(
71
+ toChannelOptions(renderOptions({ providerErrorGuidance })),
72
+ );
73
+
74
+ expect(session.providerErrorGuidance).toBe(providerErrorGuidance);
75
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CMD-005 TC-03: a model-issued AskUserQuestion renders as the Ink dialog mid-turn and the picked
3
+ * answer returns to the model — end to end against the BUILT robota binary.
4
+ *
5
+ * Boots with `--session-log` (INFRA-018 replay, no model key): the replayed assistant turn calls the
6
+ * AskUserQuestion tool; the real TUI renders the CMD-004 PendingActionPrompt while the turn is
7
+ * executing; pressing Enter picks the highlighted option; the tool result resolves and the replayed
8
+ * follow-up assistant text renders — proving ask → dialog → answer → turn-continue on the real binary.
9
+ */
10
+
11
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { dirname, join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+
18
+ import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
19
+
20
+ import type { IPtySession } from './pty-driver.js';
21
+
22
+ const FIXTURE = join(
23
+ dirname(fileURLToPath(import.meta.url)),
24
+ 'fixtures',
25
+ 'ask-user-question.jsonl',
26
+ );
27
+
28
+ describe('AskUserQuestion dialog through the real binary (CMD-005)', () => {
29
+ let projectDir: string;
30
+ let session: IPtySession | undefined;
31
+
32
+ beforeEach(() => {
33
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-ask-pty-')));
34
+ writeTuiProviderSettings(projectDir); // dummy provider profile; replay answers every call
35
+ });
36
+
37
+ afterEach(async () => {
38
+ await session?.disposeAsync();
39
+ session = undefined;
40
+ rmSync(projectDir, { recursive: true, force: true });
41
+ });
42
+
43
+ it('renders the model question mid-turn, Enter answers it, and the turn continues', async () => {
44
+ session = spawnTui({
45
+ projectDir,
46
+ homeDir: join(projectDir, 'home'),
47
+ // An explicit --name suppresses LLM auto-naming, which would otherwise consume the first
48
+ // replayed response (the tool-call turn) as a title.
49
+ args: ['--session-log', FIXTURE, '--name', 'ask-fixture'],
50
+ });
51
+
52
+ await session.waitFor(/Type a message or \/help/);
53
+
54
+ // Trigger the replayed turn: assistant calls AskUserQuestion.
55
+ await session.sendKeys('go');
56
+ await session.pressEnter();
57
+
58
+ // The CMD-004 dialog renders the model's question with its options while the turn is running.
59
+ await session.waitFor(/PICK_A_COLOR/, 20_000);
60
+ const dialog = session.snapshot();
61
+ expect(dialog).toContain('Red');
62
+ expect(dialog).toContain('Blue');
63
+
64
+ // Enter picks the highlighted first option (Red) → the tool result resolves → the replayed
65
+ // follow-up assistant text renders (the turn continued with the user's answer).
66
+ await session.pressEnter();
67
+ await session.waitFor(/FINAL_AFTER_ANSWER/, 20_000);
68
+
69
+ // Back to the idle prompt after the turn.
70
+ await session.waitFor(/Type a message or \/help/, 20_000);
71
+ }, 60_000);
72
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * TEST-010 / SCREEN-013: the background-work drill-in entry point, observed through a real PTY.
3
+ *
4
+ * Component tests cover the switcher selection + detail pane in isolation; this asserts the
5
+ * App-level wiring end to end against the BUILT robota binary: pressing `Ctrl+B` in the live TUI
6
+ * opens the execution-workspace switcher (the only way into a background task), and `Esc` returns to
7
+ * the prompt. The switcher always lists the main thread, so this is assertable without seeding
8
+ * background work. Runs in the dedicated PTY project (`pnpm --filter @robota-sdk/agent-ui-terminal
9
+ * test:pty`) against `packages/agent-cli/bin/robota.cjs`.
10
+ */
11
+
12
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+
18
+ import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
19
+
20
+ import type { IPtySession } from './pty-driver.js';
21
+
22
+ const CTRL_B = '\x02';
23
+
24
+ describe('Background-work drill-in entry point through a real PTY (TEST-010 / SCREEN-013)', () => {
25
+ let projectDir: string;
26
+ let session: IPtySession | undefined;
27
+
28
+ beforeEach(() => {
29
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'robota-bgwork-')));
30
+ writeTuiProviderSettings(projectDir);
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await session?.disposeAsync();
35
+ session = undefined;
36
+ rmSync(projectDir, { recursive: true, force: true });
37
+ });
38
+
39
+ it('Ctrl+B opens the execution-workspace switcher, Esc returns to the prompt', async () => {
40
+ session = spawnTui({ projectDir, homeDir: join(projectDir, 'home') });
41
+ await session.waitFor(/Type a message or \/help/);
42
+
43
+ await session.sendKeys(CTRL_B);
44
+ await session.waitFor(/Execution workspace/, 15_000);
45
+ const open = session.snapshot();
46
+ expect(open).toContain('Execution workspace');
47
+ // The interactive switcher (the only entry into a background task) is reachable and usable.
48
+ expect(open).toMatch(/Navigate.*Switch|Enter Switch/);
49
+
50
+ session.pressEscape();
51
+ await session.waitFor(/Type a message or \/help/, 15_000);
52
+ }, 60_000);
53
+ });