@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,18 @@
1
+ import { Box } from 'ink';
2
+ import React from 'react';
3
+
4
+ import { Text } from './SafeText.js';
5
+ import { usePalette } from './theme/index.js';
6
+
7
+ interface IProps {
8
+ message: string;
9
+ }
10
+
11
+ export default function UpdateNotice({ message }: IProps): React.ReactElement {
12
+ const palette = usePalette();
13
+ return (
14
+ <Box paddingX={1} marginBottom={1}>
15
+ <Text color={palette.text.warning}>{message}</Text>
16
+ </Box>
17
+ );
18
+ }
@@ -0,0 +1,49 @@
1
+ import { formatTokenCount } from '@robota-sdk/agent-core';
2
+ import { Box } from 'ink';
3
+ import React from 'react';
4
+
5
+ import { Text } from './SafeText.js';
6
+ import { usePalette } from './theme/index.js';
7
+
8
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
9
+ import type { IUsageSnapshot } from '@robota-sdk/agent-interface-analytics';
10
+
11
+ const TOKEN_COMPACT_THRESHOLD = 1000;
12
+
13
+ export default function UsageSummaryEntry({ entry }: { entry: IHistoryEntry }): React.ReactElement {
14
+ const palette = usePalette();
15
+ const usage = entry.data as IUsageSnapshot | undefined;
16
+ if (!usage) return <></>;
17
+ const prompt = usage.promptTokens !== undefined ? formatUsageTokenCount(usage.promptTokens) : '?';
18
+ const completion =
19
+ usage.completionTokens !== undefined ? formatUsageTokenCount(usage.completionTokens) : '?';
20
+ const total = formatUsageTokenCount(usage.totalTokens);
21
+ // Usage another unit spent (the advisor, a background task) is named, and has no context window
22
+ // of the main thread to report.
23
+ const source =
24
+ usage.source !== undefined && usage.source.scope !== 'main' ? usage.source : undefined;
25
+ const sourceLabel = source ? (source.label ?? source.id ?? source.scope) : undefined;
26
+ const context = source
27
+ ? ''
28
+ : ` · Context ${Math.round(usage.contextUsedPercentage)}% (${formatTokenCount(
29
+ usage.contextUsedTokens,
30
+ )}/${formatTokenCount(usage.contextMaxTokens)})`;
31
+ const costLabel = usage.costStatus === 'unknown' ? 'cost unknown' : `cost ${usage.costStatus}`;
32
+
33
+ return (
34
+ <Box flexDirection="column" marginBottom={1}>
35
+ <Box>
36
+ <Text color={palette.text.emphasis} bold>
37
+ {sourceLabel ? `Usage (${sourceLabel}):` : 'Usage:'}{' '}
38
+ </Text>
39
+ <Text dimColor>
40
+ {usage.kind} {total} tokens (in {prompt} / out {completion}){context} · {costLabel}
41
+ </Text>
42
+ </Box>
43
+ </Box>
44
+ );
45
+ }
46
+
47
+ function formatUsageTokenCount(tokens: number): string {
48
+ return tokens < TOKEN_COMPACT_THRESHOLD ? tokens.toLocaleString() : formatTokenCount(tokens);
49
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WaveText — renders text with a subtle wave color animation.
3
+ * Groups of 3-4 characters share the same color, creating a soft flowing effect.
4
+ * The ramp comes from the resolved theme's motion tokens (SCREEN-006, SCREEN-2002): a calm span
5
+ * wide enough to be perceptible (#555→#bbb) at an unhurried 400ms cadence.
6
+ */
7
+
8
+ import React, { useState, useEffect } from 'react';
9
+
10
+ import { Text } from './SafeText.js';
11
+ import { useScreenReader } from './screen-reader-context.js';
12
+ import { useMotion, useMotionTokens, usePalette } from './theme/index.js';
13
+
14
+ /** Cadence is not themed: a theme changes colour, not how often the terminal repaints. */
15
+ const MOTION_INTERVAL_MS = 400;
16
+ const MOTION_CHARS_PER_GROUP = 4;
17
+
18
+ interface IProps {
19
+ text: string;
20
+ }
21
+
22
+ export default function WaveText({ text }: IProps): React.ReactElement {
23
+ // Animate only on an interactive color terminal (shared with the markdown color
24
+ // gate via terminal-capabilities). Non-TTY / NO_COLOR / FORCE_COLOR=0 → static,
25
+ // no interval, no motion (SCREEN-006/008).
26
+ //
27
+ // CLI-2004: screen-reader mode is a gate on the same interval, not a second mechanism. A colour
28
+ // ramp is silent to a reader, but the repaint it causes is not — every tick re-emits the line, and
29
+ // the reader announces it again. SCREEN-2002 added `reducedMotion` as a third input to that rule.
30
+ const screenReader = useScreenReader();
31
+ const palette = usePalette();
32
+ const motionTokens = useMotionTokens();
33
+ // SCREEN-2002: the colour gate, screen-reader mode and the reduced-motion setting are ONE rule
34
+ // with one owner. This is its only consumer — the package's only animation.
35
+ const animate = useMotion();
36
+ const [tick, setTick] = useState(0);
37
+
38
+ useEffect(() => {
39
+ if (!animate) return undefined;
40
+ const timer = setInterval(() => {
41
+ setTick((prev) => prev + 1);
42
+ }, MOTION_INTERVAL_MS);
43
+ return () => clearInterval(timer);
44
+ }, [animate]);
45
+
46
+ if (screenReader) {
47
+ // No colour prop at all: the mode drops the cue rather than substituting a value.
48
+ return <Text>{text}</Text>;
49
+ }
50
+
51
+ if (!animate) {
52
+ return <Text color={palette.text.muted}>{text}</Text>;
53
+ }
54
+
55
+ const chars = [...text];
56
+
57
+ return (
58
+ <Text>
59
+ {chars.map((char, i) => {
60
+ const group = Math.floor(i / MOTION_CHARS_PER_GROUP);
61
+ const colorIndex = (tick + group) % motionTokens.wave.length;
62
+ return (
63
+ <Text key={i} color={motionTokens.wave[colorIndex]}>
64
+ {char}
65
+ </Text>
66
+ );
67
+ })}
68
+ </Text>
69
+ );
70
+ }
@@ -0,0 +1,188 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { Text } from 'ink';
4
+ import { describe, it, expect } from 'vitest';
5
+ import ListPicker from '../ListPicker.js';
6
+ import {
7
+ formatKeyHints,
8
+ SELECTION_INDICATOR,
9
+ SELECTION_INDICATOR_NONE,
10
+ } from '../key-hint-footer.js';
11
+
12
+ describe('ListPicker', () => {
13
+ it('renders all items with first selected by default', () => {
14
+ const items = ['Alpha', 'Beta', 'Gamma'];
15
+ const { lastFrame } = render(
16
+ <ListPicker
17
+ items={items}
18
+ renderItem={(item, isSelected) => (
19
+ <Text>
20
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
21
+ {item}
22
+ </Text>
23
+ )}
24
+ onSelect={() => {}}
25
+ onCancel={() => {}}
26
+ />,
27
+ );
28
+ const frame = lastFrame()!;
29
+ expect(frame).toContain('> Alpha');
30
+ expect(frame).toContain(' Beta');
31
+ expect(frame).toContain(' Gamma');
32
+ });
33
+
34
+ it('renders empty state when no items', () => {
35
+ const { lastFrame } = render(
36
+ <ListPicker
37
+ items={[]}
38
+ renderItem={(item, isSelected) => (
39
+ <Text>
40
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
41
+ {String(item)}
42
+ </Text>
43
+ )}
44
+ onSelect={() => {}}
45
+ onCancel={() => {}}
46
+ />,
47
+ );
48
+ // Should render without crashing
49
+ expect(lastFrame()).toBeDefined();
50
+ });
51
+
52
+ it('calls onSelect with item on Enter', () => {
53
+ let selected = '';
54
+ const items = ['Alpha', 'Beta'];
55
+ const { stdin } = render(
56
+ <ListPicker
57
+ items={items}
58
+ renderItem={(item, isSelected) => (
59
+ <Text>
60
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
61
+ {item}
62
+ </Text>
63
+ )}
64
+ onSelect={(item) => {
65
+ selected = item;
66
+ }}
67
+ onCancel={() => {}}
68
+ />,
69
+ );
70
+ stdin.write('\r');
71
+ expect(selected).toBe('Alpha');
72
+ });
73
+
74
+ it('navigates down with arrow key and selects', () => {
75
+ let selected = '';
76
+ const items = ['Alpha', 'Beta', 'Gamma'];
77
+ const { stdin } = render(
78
+ <ListPicker
79
+ items={items}
80
+ renderItem={(item, isSelected) => (
81
+ <Text>
82
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
83
+ {item}
84
+ </Text>
85
+ )}
86
+ onSelect={(item) => {
87
+ selected = item;
88
+ }}
89
+ onCancel={() => {}}
90
+ />,
91
+ );
92
+ stdin.write('\x1B[B'); // Down arrow
93
+ stdin.write('\r');
94
+ expect(selected).toBe('Beta');
95
+ });
96
+
97
+ it('calls onCancel on Escape', async () => {
98
+ const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
99
+ let cancelled = false;
100
+ const { stdin } = render(
101
+ <ListPicker
102
+ items={['Alpha']}
103
+ renderItem={(item, isSelected) => (
104
+ <Text>
105
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
106
+ {item}
107
+ </Text>
108
+ )}
109
+ onSelect={() => {}}
110
+ onCancel={() => {
111
+ cancelled = true;
112
+ }}
113
+ />,
114
+ );
115
+ stdin.write('\x1B');
116
+ await delay(50);
117
+ expect(cancelled).toBe(true);
118
+ });
119
+
120
+ it('does not go above first item', () => {
121
+ const items = ['Alpha', 'Beta'];
122
+ const { stdin, lastFrame } = render(
123
+ <ListPicker
124
+ items={items}
125
+ renderItem={(item, isSelected) => (
126
+ <Text>
127
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
128
+ {item}
129
+ </Text>
130
+ )}
131
+ onSelect={() => {}}
132
+ onCancel={() => {}}
133
+ />,
134
+ );
135
+ stdin.write('\x1B[A'); // Up arrow (already at 0)
136
+ const frame = lastFrame()!;
137
+ expect(frame).toContain('> Alpha');
138
+ });
139
+
140
+ it('does not go below last item', () => {
141
+ let selected = '';
142
+ const items = ['Alpha', 'Beta'];
143
+ const { stdin } = render(
144
+ <ListPicker
145
+ items={items}
146
+ renderItem={(item, isSelected) => (
147
+ <Text>
148
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
149
+ {item}
150
+ </Text>
151
+ )}
152
+ onSelect={(item) => {
153
+ selected = item;
154
+ }}
155
+ onCancel={() => {}}
156
+ />,
157
+ );
158
+ stdin.write('\x1B[B'); // Down
159
+ stdin.write('\x1B[B'); // Down (should stay at Beta)
160
+ stdin.write('\x1B[B'); // Down (should stay at Beta)
161
+ stdin.write('\r');
162
+ expect(selected).toBe('Beta');
163
+ });
164
+
165
+ // SCREEN-005: default footer renders through the key-hint SSOT grammar.
166
+ it('renders the default footer in the shared key-hint grammar', () => {
167
+ const { lastFrame } = render(
168
+ <ListPicker
169
+ items={['Alpha']}
170
+ renderItem={(item, isSelected) => (
171
+ <Text>
172
+ {isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
173
+ {item}
174
+ </Text>
175
+ )}
176
+ onSelect={() => {}}
177
+ onCancel={() => {}}
178
+ />,
179
+ );
180
+ expect(lastFrame()!).toContain(
181
+ formatKeyHints([
182
+ { keys: '↑↓', label: 'Navigate' },
183
+ { keys: 'Enter', label: 'Select' },
184
+ { keys: 'Esc', label: 'Cancel' },
185
+ ]),
186
+ );
187
+ });
188
+ });
@@ -0,0 +1,127 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect } from 'vitest';
4
+ import MenuSelect from '../MenuSelect.js';
5
+ import { formatKeyHints } from '../key-hint-footer.js';
6
+
7
+ const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
8
+
9
+ describe('MenuSelect', () => {
10
+ const items = [
11
+ { label: 'Option A', value: 'a' },
12
+ { label: 'Option B', value: 'b', hint: 'some hint' },
13
+ { label: 'Option C', value: 'c' },
14
+ ];
15
+
16
+ it('renders title and all items', () => {
17
+ const { lastFrame } = render(
18
+ <MenuSelect title="Test Menu" items={items} onSelect={() => {}} onBack={() => {}} />,
19
+ );
20
+ const frame = lastFrame()!;
21
+ expect(frame).toContain('Test Menu');
22
+ expect(frame).toContain('Option A');
23
+ expect(frame).toContain('Option B');
24
+ expect(frame).toContain('Option C');
25
+ });
26
+
27
+ it('renders hint text when provided', () => {
28
+ const { lastFrame } = render(
29
+ <MenuSelect title="Test" items={items} onSelect={() => {}} onBack={() => {}} />,
30
+ );
31
+ expect(lastFrame()!).toContain('some hint');
32
+ });
33
+
34
+ it('highlights first item by default with > prefix', () => {
35
+ const { lastFrame } = render(
36
+ <MenuSelect title="Test" items={items} onSelect={() => {}} onBack={() => {}} />,
37
+ );
38
+ expect(lastFrame()!).toContain('>');
39
+ });
40
+
41
+ it('shows loading state', () => {
42
+ const { lastFrame } = render(
43
+ <MenuSelect title="Test" items={[]} onSelect={() => {}} onBack={() => {}} loading />,
44
+ );
45
+ expect(lastFrame()!).toContain('Loading');
46
+ });
47
+
48
+ it('shows error state', () => {
49
+ const { lastFrame } = render(
50
+ <MenuSelect title="Test" items={[]} onSelect={() => {}} onBack={() => {}} error="Failed" />,
51
+ );
52
+ expect(lastFrame()!).toContain('Failed');
53
+ });
54
+
55
+ it('calls onSelect with value on Enter', () => {
56
+ let selected = '';
57
+ const { stdin } = render(
58
+ <MenuSelect
59
+ title="Test"
60
+ items={items}
61
+ onSelect={(v) => {
62
+ selected = v;
63
+ }}
64
+ onBack={() => {}}
65
+ />,
66
+ );
67
+ stdin.write('\r');
68
+ expect(selected).toBe('a');
69
+ });
70
+
71
+ it('calls onBack on Escape', async () => {
72
+ let backed = false;
73
+ const { stdin } = render(
74
+ <MenuSelect
75
+ title="Test"
76
+ items={items}
77
+ onSelect={() => {}}
78
+ onBack={() => {
79
+ backed = true;
80
+ }}
81
+ />,
82
+ );
83
+ stdin.write('\x1B');
84
+ await delay(50);
85
+ expect(backed).toBe(true);
86
+ });
87
+
88
+ it('navigates down with arrow key', () => {
89
+ let selected = '';
90
+ const { stdin } = render(
91
+ <MenuSelect
92
+ title="Test"
93
+ items={items}
94
+ onSelect={(v) => {
95
+ selected = v;
96
+ }}
97
+ onBack={() => {}}
98
+ />,
99
+ );
100
+ stdin.write('\x1B[B'); // Down arrow
101
+ stdin.write('\r');
102
+ expect(selected).toBe('b');
103
+ });
104
+
105
+ // SCREEN-005: footer + error-state hint render through the key-hint SSOT grammar.
106
+ it('renders the footer in the shared key-hint grammar', () => {
107
+ const { lastFrame } = render(
108
+ <MenuSelect title="Test" items={items} onSelect={() => {}} onBack={() => {}} />,
109
+ );
110
+ expect(lastFrame()!).toContain(
111
+ formatKeyHints([
112
+ { keys: '↑↓', label: 'Navigate' },
113
+ { keys: 'Enter', label: 'Select' },
114
+ { keys: 'Esc', label: 'Back' },
115
+ ]),
116
+ );
117
+ });
118
+
119
+ it('renders the error-state hint via the shared key-hint grammar', () => {
120
+ const { lastFrame } = render(
121
+ <MenuSelect title="Test" items={[]} onSelect={() => {}} onBack={() => {}} error="Failed" />,
122
+ );
123
+ const frame = lastFrame()!;
124
+ expect(frame).toContain(formatKeyHints([{ keys: 'Esc', label: 'Back' }]));
125
+ expect(frame).not.toContain('Press Esc to go back');
126
+ });
127
+ });
@@ -0,0 +1,193 @@
1
+ import { render } from 'ink-testing-library';
2
+ import React from 'react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import PendingActionPrompt from '../PendingActionPrompt.js';
6
+ import { formatKeyHints } from '../key-hint-footer.js';
7
+
8
+ import type { IActionRequest } from '@robota-sdk/agent-core';
9
+
10
+ const delay = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 20));
11
+ const ESC = String.fromCharCode(27);
12
+ const DOWN = `${ESC}[B`;
13
+ const ENTER = '\r';
14
+
15
+ describe('PendingActionPrompt (CMD-004 unified action renderer)', () => {
16
+ it('single-select: renders options and answers with the chosen value', async () => {
17
+ const onAnswer = vi.fn();
18
+ const request: IActionRequest = {
19
+ id: 'mode',
20
+ title: 'Select mode',
21
+ options: [
22
+ { value: 'plan', label: 'Plan' },
23
+ { value: 'default', label: 'Default' },
24
+ ],
25
+ maxSelect: 1,
26
+ };
27
+ const { stdin, lastFrame } = render(
28
+ <PendingActionPrompt request={request} onAnswer={onAnswer} />,
29
+ );
30
+ expect(lastFrame()).toContain('Select mode');
31
+ expect(lastFrame()).toContain('Plan');
32
+
33
+ stdin.write(DOWN);
34
+ await delay();
35
+ stdin.write(ENTER);
36
+ await delay();
37
+ expect(onAnswer).toHaveBeenCalledWith({ type: 'answer', values: ['default'] });
38
+ });
39
+
40
+ it('single-select: the highlight starts on the request default — a No-default confirmation answers No on Enter (BEHAVIOR-2437)', async () => {
41
+ const onAnswer = vi.fn();
42
+ const request: IActionRequest = {
43
+ id: 'git-commit',
44
+ title: 'Commit 1 staged file(s)?',
45
+ options: [
46
+ { value: 'yes', label: 'Yes' },
47
+ { value: 'no', label: 'No' },
48
+ ],
49
+ maxSelect: 1,
50
+ default: { values: ['no'] },
51
+ };
52
+ const { stdin, lastFrame } = render(
53
+ <PendingActionPrompt request={request} onAnswer={onAnswer} />,
54
+ );
55
+ expect(lastFrame()).toContain('> No');
56
+ expect(lastFrame()).not.toContain('> Yes');
57
+
58
+ stdin.write(ENTER);
59
+ await delay();
60
+ expect(onAnswer).toHaveBeenCalledWith({ type: 'answer', values: ['no'] });
61
+ });
62
+
63
+ it('multi-select (maxSelect>1): toggles with Space and confirms with Enter', async () => {
64
+ const onAnswer = vi.fn();
65
+ const request: IActionRequest = {
66
+ id: 'tags',
67
+ title: 'Pick tags',
68
+ options: [
69
+ { value: 'a', label: 'Alpha' },
70
+ { value: 'b', label: 'Beta' },
71
+ { value: 'c', label: 'Gamma' },
72
+ ],
73
+ minSelect: 1,
74
+ maxSelect: 3,
75
+ };
76
+ const { stdin, lastFrame } = render(
77
+ <PendingActionPrompt request={request} onAnswer={onAnswer} />,
78
+ );
79
+ expect(lastFrame()).toContain('[ ] Alpha');
80
+
81
+ stdin.write(' ');
82
+ await delay();
83
+ expect(lastFrame()).toContain('[x] Alpha');
84
+ stdin.write(DOWN);
85
+ await delay();
86
+ stdin.write(' ');
87
+ await delay();
88
+ stdin.write(ENTER);
89
+ await delay();
90
+ expect(onAnswer).toHaveBeenCalledWith({ type: 'answer', values: ['a', 'b'] });
91
+ });
92
+
93
+ it('free-text with masked: hides input and answers with typed text', async () => {
94
+ const onAnswer = vi.fn();
95
+ const request: IActionRequest = {
96
+ id: 'apiKey',
97
+ title: 'Enter API key',
98
+ allowFreeText: true,
99
+ masked: true,
100
+ };
101
+ const { stdin, lastFrame } = render(
102
+ <PendingActionPrompt request={request} onAnswer={onAnswer} />,
103
+ );
104
+ expect(lastFrame()).toContain('Enter API key');
105
+
106
+ stdin.write('sk-secret');
107
+ await delay();
108
+ expect(lastFrame()).not.toContain('sk-secret');
109
+ expect(lastFrame()).toContain('*'.repeat('sk-secret'.length));
110
+ stdin.write(ENTER);
111
+ await delay();
112
+ expect(onAnswer).toHaveBeenCalledWith({ type: 'answer', values: [], text: 'sk-secret' });
113
+ });
114
+
115
+ it('Esc cancels the request (resolves cancelled)', async () => {
116
+ const onAnswer = vi.fn();
117
+ const request: IActionRequest = {
118
+ id: 'exit',
119
+ title: 'Exit?',
120
+ options: [
121
+ { value: 'yes', label: 'Yes' },
122
+ { value: 'no', label: 'No' },
123
+ ],
124
+ maxSelect: 1,
125
+ };
126
+ const { stdin } = render(<PendingActionPrompt request={request} onAnswer={onAnswer} />);
127
+ stdin.write(ESC);
128
+ await delay();
129
+ expect(onAnswer).toHaveBeenCalledWith({ type: 'cancelled' });
130
+ });
131
+
132
+ it('single-select + allowFreeText: "type a custom answer" opens text; Esc returns to picker (not cancel)', async () => {
133
+ const onAnswer = vi.fn();
134
+ const request: IActionRequest = {
135
+ id: 'provider',
136
+ title: 'Select provider',
137
+ options: [{ value: 'openai', label: 'OpenAI' }],
138
+ maxSelect: 1,
139
+ allowFreeText: true,
140
+ };
141
+ const { stdin, lastFrame } = render(
142
+ <PendingActionPrompt request={request} onAnswer={onAnswer} />,
143
+ );
144
+ expect(lastFrame()).toContain('Type a custom answer');
145
+
146
+ stdin.write(DOWN); // move to the synthetic "type a custom answer" entry
147
+ await delay();
148
+ stdin.write(ENTER); // enter free-text mode
149
+ await delay();
150
+ stdin.write(ESC); // Esc here returns to the picker, does NOT cancel the request
151
+ await delay();
152
+ expect(onAnswer).not.toHaveBeenCalled();
153
+ expect(lastFrame()).toContain('OpenAI'); // back at the picker
154
+ });
155
+
156
+ // SCREEN-005: multi-select footer goes through the key-hint SSOT, keeping the dynamic (min N)
157
+ // segment on the Enter hint until the selection is confirmable.
158
+ it('multi-select footer shows the (min N) segment until satisfiable, in the shared grammar', async () => {
159
+ const onAnswer = vi.fn();
160
+ const request: IActionRequest = {
161
+ id: 'tags',
162
+ title: 'Pick tags',
163
+ options: [
164
+ { value: 'a', label: 'Alpha' },
165
+ { value: 'b', label: 'Beta' },
166
+ ],
167
+ minSelect: 1,
168
+ maxSelect: 2,
169
+ };
170
+ const { stdin, lastFrame } = render(
171
+ <PendingActionPrompt request={request} onAnswer={onAnswer} />,
172
+ );
173
+ expect(lastFrame()).toContain(
174
+ formatKeyHints([
175
+ { keys: '↑↓', label: 'Navigate' },
176
+ { keys: 'Space', label: 'Toggle' },
177
+ { keys: 'Enter', label: 'Confirm (min 1)' },
178
+ { keys: 'Esc', label: 'Cancel' },
179
+ ]),
180
+ );
181
+
182
+ stdin.write(' '); // toggle Alpha → selection satisfiable → min segment drops
183
+ await delay();
184
+ expect(lastFrame()).toContain(
185
+ formatKeyHints([
186
+ { keys: '↑↓', label: 'Navigate' },
187
+ { keys: 'Space', label: 'Toggle' },
188
+ { keys: 'Enter', label: 'Confirm' },
189
+ { keys: 'Esc', label: 'Cancel' },
190
+ ]),
191
+ );
192
+ });
193
+ });