@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,13 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const TUI_CHANNEL_SOURCE = new URL('../TuiInteractionChannel.ts', import.meta.url);
6
+ describe('ARCH-018 interaction-channel charter', () => {
7
+ it('does not retain nominal TUI conformance with a no-op write path', () => {
8
+ const source = readFileSync(TUI_CHANNEL_SOURCE, 'utf8');
9
+
10
+ expect(source).not.toContain('implements IInteractionChannel');
11
+ expect(source).not.toMatch(/write\(_event:\s*InteractionEvent\)/);
12
+ });
13
+ });
@@ -0,0 +1,249 @@
1
+ /**
2
+ * SCREEN-005 anti-drift consistency guard — the mechanical floor for the key-hint SSOT.
3
+ *
4
+ * Covers the FULL footer inventory: every component's declared hint set must (a) render exactly as
5
+ * `formatKeyHints(<declared hints>)` (round-trip through the SSOT — a fourth footer dialect cannot
6
+ * re-appear silently), and (b) obey the navigate → modify → primary → dismiss ordering. Also pins
7
+ * the Esc-suppression contract: the explicit-resolve prompts (Confirm/Permission) omit Esc from
8
+ * their footers because their flows suppress it.
9
+ */
10
+ import React from 'react';
11
+ import { render } from 'ink-testing-library';
12
+ import { describe, expect, it } from 'vitest';
13
+ import { Text } from 'ink';
14
+
15
+ import ConfirmPrompt, { CONFIRM_PROMPT_FOOTER_HINTS } from '../ConfirmPrompt.js';
16
+ import ExecutionWorkspaceSwitcher, {
17
+ EXECUTION_WORKSPACE_SWITCHER_FOOTER_HINTS,
18
+ } from '../ExecutionWorkspaceSwitcher.js';
19
+ import ListPicker, { LIST_PICKER_DEFAULT_FOOTER_HINTS } from '../ListPicker.js';
20
+ import MenuSelect, {
21
+ MENU_SELECT_ERROR_FOOTER_HINTS,
22
+ MENU_SELECT_FOOTER_HINTS,
23
+ } from '../MenuSelect.js';
24
+ import MultiSelectList, { getMultiSelectFooterHints } from '../MultiSelectList.js';
25
+ import PermissionPrompt, { PERMISSION_PROMPT_FOOTER_HINTS } from '../PermissionPrompt.js';
26
+ import SlashAutocomplete, { SLASH_AUTOCOMPLETE_FOOTER_HINTS } from '../SlashAutocomplete.js';
27
+ import TextPrompt, { TEXT_PROMPT_FOOTER_HINTS } from '../TextPrompt.js';
28
+ import HistorySearchOverlay, { HISTORY_SEARCH_FOOTER_HINTS } from '../HistorySearchOverlay.js';
29
+ import { formatKeyHints, type IKeyHint } from '../key-hint-footer.js';
30
+
31
+ import type { ICommand } from '@robota-sdk/agent-interface-command';
32
+ import type { IExecutionWorkspaceSnapshot } from '@robota-sdk/agent-interface-execution';
33
+
34
+ /**
35
+ * Footer ordering contract: navigate → modify → primary → dismiss (docs/SPEC.md "Interaction
36
+ * affordance contract"). The rank vocabulary lives here in the test — the SSOT module stays
37
+ * mechanics-only.
38
+ */
39
+ const KEY_RANK: Record<string, number> = {
40
+ '↑↓': 0, // navigate
41
+ '←→': 0, // navigate
42
+ Space: 1, // modify
43
+ Tab: 1, // modify
44
+ 'Ctrl+S': 1, // modify (SCREEN-1993: the search scope)
45
+ Enter: 2, // primary
46
+ 'Enter/Tab': 2, // primary (SCREEN-1993: insert the match)
47
+ 'Ctrl+E': 2, // primary (SCREEN-1993: run the match)
48
+ Esc: 3, // dismiss
49
+ 'Ctrl+B/Esc': 3, // dismiss
50
+ };
51
+
52
+ interface IFooterInventoryEntry {
53
+ name: string;
54
+ hints: readonly IKeyHint[];
55
+ renderFrame: () => string;
56
+ }
57
+
58
+ function makeSnapshot(): IExecutionWorkspaceSnapshot {
59
+ return {
60
+ sessionId: 's1',
61
+ selectedEntryId: 'main:s1',
62
+ updatedAt: '2026-07-24T00:00:00.000Z',
63
+ entries: [
64
+ {
65
+ id: 'main:s1',
66
+ sourceId: 's1',
67
+ kind: 'main_thread',
68
+ origin: { kind: 'user_prompt', sessionId: 's1' },
69
+ status: 'idle',
70
+ state: 'completed',
71
+ title: 'Main thread',
72
+ unread: false,
73
+ attention: 'none',
74
+ visibility: 'default',
75
+ updatedAt: '2026-07-24T00:00:00.000Z',
76
+ controls: ['select'],
77
+ },
78
+ ],
79
+ };
80
+ }
81
+
82
+ const noop = (): void => {};
83
+
84
+ /** The FULL footer inventory — every footer call site in the package. */
85
+ const FOOTER_INVENTORY: readonly IFooterInventoryEntry[] = [
86
+ {
87
+ name: 'ListPicker (default footer)',
88
+ hints: LIST_PICKER_DEFAULT_FOOTER_HINTS,
89
+ renderFrame: () =>
90
+ render(
91
+ <ListPicker
92
+ items={['a']}
93
+ renderItem={(item) => <Text>{item}</Text>}
94
+ onSelect={noop}
95
+ onCancel={noop}
96
+ />,
97
+ ).lastFrame()!,
98
+ },
99
+ {
100
+ name: 'SlashAutocomplete',
101
+ hints: SLASH_AUTOCOMPLETE_FOOTER_HINTS,
102
+ renderFrame: () =>
103
+ render(
104
+ <SlashAutocomplete
105
+ commands={[{ name: 'help', description: 'Show help' } as ICommand]}
106
+ selectedIndex={0}
107
+ visible={true}
108
+ />,
109
+ ).lastFrame()!,
110
+ },
111
+ {
112
+ name: 'TextPrompt',
113
+ hints: TEXT_PROMPT_FOOTER_HINTS,
114
+ renderFrame: () =>
115
+ render(<TextPrompt title="t" onSubmit={noop} onCancel={noop} />).lastFrame()!,
116
+ },
117
+ {
118
+ name: 'MenuSelect (normal)',
119
+ hints: MENU_SELECT_FOOTER_HINTS,
120
+ renderFrame: () =>
121
+ render(
122
+ <MenuSelect title="t" items={[{ label: 'A', value: 'a' }]} onSelect={noop} onBack={noop} />,
123
+ ).lastFrame()!,
124
+ },
125
+ {
126
+ name: 'MenuSelect (error-state hint)',
127
+ hints: MENU_SELECT_ERROR_FOOTER_HINTS,
128
+ renderFrame: () =>
129
+ render(
130
+ <MenuSelect title="t" items={[]} onSelect={noop} onBack={noop} error="Failed" />,
131
+ ).lastFrame()!,
132
+ },
133
+ {
134
+ name: 'MultiSelectList (min not yet satisfied)',
135
+ hints: getMultiSelectFooterHints({ canConfirm: false, minSelect: 2 }),
136
+ renderFrame: () =>
137
+ render(
138
+ <MultiSelectList
139
+ title="t"
140
+ options={[
141
+ { value: 'a', label: 'A' },
142
+ { value: 'b', label: 'B' },
143
+ ]}
144
+ minSelect={2}
145
+ maxSelect={2}
146
+ onConfirm={noop}
147
+ onCancel={noop}
148
+ />,
149
+ ).lastFrame()!,
150
+ },
151
+ {
152
+ name: 'MultiSelectList (confirmable)',
153
+ hints: getMultiSelectFooterHints({ canConfirm: true, minSelect: 0 }),
154
+ renderFrame: () =>
155
+ render(
156
+ <MultiSelectList
157
+ title="t"
158
+ options={[{ value: 'a', label: 'A' }]}
159
+ minSelect={0}
160
+ maxSelect={1}
161
+ onConfirm={noop}
162
+ onCancel={noop}
163
+ />,
164
+ ).lastFrame()!,
165
+ },
166
+ {
167
+ name: 'ConfirmPrompt',
168
+ hints: CONFIRM_PROMPT_FOOTER_HINTS,
169
+ renderFrame: () => render(<ConfirmPrompt message="m" onSelect={noop} />).lastFrame()!,
170
+ },
171
+ {
172
+ name: 'PermissionPrompt',
173
+ hints: PERMISSION_PROMPT_FOOTER_HINTS,
174
+ renderFrame: () =>
175
+ render(
176
+ <PermissionPrompt request={{ toolName: 'tool', toolArgs: {}, resolve: noop }} />,
177
+ ).lastFrame()!,
178
+ },
179
+ {
180
+ name: 'HistorySearchOverlay',
181
+ hints: HISTORY_SEARCH_FOOTER_HINTS,
182
+ renderFrame: () =>
183
+ render(
184
+ <HistorySearchOverlay
185
+ view={{
186
+ open: true,
187
+ query: '',
188
+ scope: 'all',
189
+ selectedIndex: 0,
190
+ matches: [],
191
+ loading: false,
192
+ skippedLines: 0,
193
+ error: undefined,
194
+ }}
195
+ />,
196
+ ).lastFrame()!,
197
+ },
198
+ {
199
+ name: 'ExecutionWorkspaceSwitcher',
200
+ hints: EXECUTION_WORKSPACE_SWITCHER_FOOTER_HINTS,
201
+ renderFrame: () =>
202
+ render(
203
+ <ExecutionWorkspaceSwitcher
204
+ snapshot={makeSnapshot()}
205
+ selectedEntryId="main:s1"
206
+ onSelect={noop}
207
+ onClose={noop}
208
+ />,
209
+ ).lastFrame()!,
210
+ },
211
+ ];
212
+
213
+ describe('key-hint consistency (SCREEN-005 anti-drift guard)', () => {
214
+ it.each(FOOTER_INVENTORY.map((entry) => [entry.name, entry] as const))(
215
+ '%s: rendered footer round-trips through formatKeyHints',
216
+ (_name, entry) => {
217
+ expect(entry.renderFrame()).toContain(formatKeyHints(entry.hints));
218
+ },
219
+ );
220
+
221
+ it.each(FOOTER_INVENTORY.map((entry) => [entry.name, entry] as const))(
222
+ '%s: hint order is navigate → modify → primary → dismiss',
223
+ (_name, entry) => {
224
+ const ranks = entry.hints.map((hint) => {
225
+ const rank = KEY_RANK[hint.keys];
226
+ if (rank === undefined) {
227
+ throw new Error(
228
+ `Unknown key '${hint.keys}' — add it to KEY_RANK with its ordering category`,
229
+ );
230
+ }
231
+ return rank;
232
+ });
233
+ const sorted = [...ranks].sort((a, b) => a - b);
234
+ expect(ranks).toEqual(sorted);
235
+ },
236
+ );
237
+
238
+ it('explicit-resolve prompts (Confirm/Permission) omit Esc from their footers', () => {
239
+ for (const hints of [CONFIRM_PROMPT_FOOTER_HINTS, PERMISSION_PROMPT_FOOTER_HINTS]) {
240
+ expect(hints.some((hint) => hint.keys.includes('Esc'))).toBe(false);
241
+ }
242
+ });
243
+
244
+ it('Confirm and Permission prompt footers are identical (same row, same reducer)', () => {
245
+ expect(formatKeyHints(CONFIRM_PROMPT_FOOTER_HINTS)).toBe(
246
+ formatKeyHints(PERMISSION_PROMPT_FOOTER_HINTS),
247
+ );
248
+ });
249
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * SCREEN-005: key-hint footer SSOT unit tests — formatter grammar, separator constant, indicator
3
+ * constants, and the KeyHintFooter render contract (dim, leading pad, nothing when empty).
4
+ */
5
+ import React from 'react';
6
+ import { render } from 'ink-testing-library';
7
+ import { describe, expect, it } from 'vitest';
8
+
9
+ import {
10
+ formatKeyHints,
11
+ KeyHintFooter,
12
+ KEY_HINT_SEPARATOR,
13
+ SELECTION_INDICATOR,
14
+ SELECTION_INDICATOR_NONE,
15
+ type IKeyHint,
16
+ } from '../key-hint-footer.js';
17
+
18
+ describe('formatKeyHints', () => {
19
+ it('joins keys+label pairs with the separator constant', () => {
20
+ const hints: IKeyHint[] = [
21
+ { keys: '↑↓', label: 'Navigate' },
22
+ { keys: 'Enter', label: 'Select' },
23
+ { keys: 'Esc', label: 'Cancel' },
24
+ ];
25
+ expect(formatKeyHints(hints)).toBe(
26
+ `↑↓ Navigate${KEY_HINT_SEPARATOR}Enter Select${KEY_HINT_SEPARATOR}Esc Cancel`,
27
+ );
28
+ });
29
+
30
+ it('renders a single hint without a separator', () => {
31
+ expect(formatKeyHints([{ keys: 'Esc', label: 'Back' }])).toBe('Esc Back');
32
+ });
33
+
34
+ it('returns the empty string for an empty list', () => {
35
+ expect(formatKeyHints([])).toBe('');
36
+ });
37
+
38
+ it('keeps the keys-before-label pair order', () => {
39
+ expect(formatKeyHints([{ keys: 'Space', label: 'Toggle' }])).toBe('Space Toggle');
40
+ });
41
+ });
42
+
43
+ describe('constants', () => {
44
+ it('separator is the middle-dot idiom', () => {
45
+ expect(KEY_HINT_SEPARATOR).toBe(' · ');
46
+ });
47
+
48
+ it('selection indicator and its blank counterpart are the same width', () => {
49
+ expect(SELECTION_INDICATOR).toBe('> ');
50
+ expect(SELECTION_INDICATOR_NONE).toBe(' ');
51
+ expect(SELECTION_INDICATOR_NONE.length).toBe(SELECTION_INDICATOR.length);
52
+ });
53
+ });
54
+
55
+ describe('KeyHintFooter', () => {
56
+ it('renders the formatted hints with a leading pad', () => {
57
+ const hints: IKeyHint[] = [
58
+ { keys: 'Enter', label: 'Submit' },
59
+ { keys: 'Esc', label: 'Cancel' },
60
+ ];
61
+ const { lastFrame } = render(<KeyHintFooter hints={hints} />);
62
+ expect(lastFrame()).toBe(` ${formatKeyHints(hints)}`);
63
+ });
64
+
65
+ it('renders nothing for an empty hint list', () => {
66
+ const { lastFrame } = render(<KeyHintFooter hints={[]} />);
67
+ expect(lastFrame()).toBe('');
68
+ });
69
+ });
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { toChannelOptions } from '../render.js';
3
+ import { buildTuiSessionOptions } from '../tui-session-options.js';
4
+ import type { IRenderOptions } from '../render.js';
5
+
6
+ describe('live trace port projection', () => {
7
+ it('passes the exact host port through the TUI to the runtime session', () => {
8
+ const livePromptTrace = { enqueue: vi.fn() };
9
+ const render = { cwd: '/w', provider: {} as never, cliAdapter: {} as never, livePromptTrace } as IRenderOptions;
10
+ const channel = toChannelOptions(render);
11
+ expect(channel.livePromptTrace).toBe(livePromptTrace);
12
+ expect(buildTuiSessionOptions(channel).livePromptTrace).toBe(livePromptTrace);
13
+ });
14
+ });