@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,167 @@
1
+ // packages/agent-cli/src/ui/__tests__/PluginTUI.test.tsx
2
+ import React from 'react';
3
+ import { render } from 'ink-testing-library';
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import PluginTUI from '../PluginTUI.js';
6
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
7
+
8
+ function mockCallbacks(): ICommandPluginAdapter {
9
+ return {
10
+ listInstalled: vi.fn().mockResolvedValue([]),
11
+ listAvailablePlugins: vi.fn().mockResolvedValue([]),
12
+ install: vi.fn().mockResolvedValue(undefined),
13
+ uninstall: vi.fn().mockResolvedValue(undefined),
14
+ enable: vi.fn().mockResolvedValue(undefined),
15
+ disable: vi.fn().mockResolvedValue(undefined),
16
+ marketplaceAdd: vi.fn().mockResolvedValue('test-marketplace'),
17
+ marketplaceRemove: vi.fn().mockResolvedValue(undefined),
18
+ marketplaceUpdate: vi.fn().mockResolvedValue(undefined),
19
+ marketplaceList: vi.fn().mockResolvedValue([]),
20
+ reloadPlugins: vi.fn().mockResolvedValue({ loadedPluginCount: 0 }),
21
+ };
22
+ }
23
+
24
+ describe('PluginTUI', () => {
25
+ it('renders main menu with Marketplace and Installed Plugins', () => {
26
+ const { lastFrame } = render(<PluginTUI callbacks={mockCallbacks()} onClose={() => {}} />);
27
+ const frame = lastFrame()!;
28
+ expect(frame).toContain('Marketplace');
29
+ expect(frame).toContain('Installed Plugins');
30
+ });
31
+
32
+ it('calls onClose when Escape on main menu', async () => {
33
+ let closed = false;
34
+ const { stdin } = render(
35
+ <PluginTUI
36
+ callbacks={mockCallbacks()}
37
+ onClose={() => {
38
+ closed = true;
39
+ }}
40
+ />,
41
+ );
42
+ stdin.write('\x1B');
43
+ await new Promise((r) => setTimeout(r, 50));
44
+ expect(closed).toBe(true);
45
+ });
46
+
47
+ it('navigates to installed plugins and shows actions', async () => {
48
+ const cbs = mockCallbacks();
49
+ cbs.listInstalled = vi
50
+ .fn()
51
+ .mockResolvedValue([{ name: 'my-plugin', description: 'A plugin', enabled: true }]);
52
+ const { stdin, lastFrame } = render(<PluginTUI callbacks={cbs} onClose={() => {}} />);
53
+ stdin.write('\x1B[B'); // Down to "Installed Plugins"
54
+ stdin.write('\r');
55
+ await new Promise((r) => setTimeout(r, 100));
56
+ expect(lastFrame()!).toContain('my-plugin');
57
+ });
58
+
59
+ it('navigates to marketplace list on Enter', async () => {
60
+ const cbs = mockCallbacks();
61
+ cbs.marketplaceList = vi.fn().mockResolvedValue([{ name: 'test-mp', type: 'github' }]);
62
+ const { stdin, lastFrame } = render(<PluginTUI callbacks={cbs} onClose={() => {}} />);
63
+ stdin.write('\r'); // Enter on "Marketplace"
64
+ await new Promise((r) => setTimeout(r, 100));
65
+ const frame = lastFrame()!;
66
+ expect(frame).toContain('Add Marketplace');
67
+ });
68
+
69
+ // Regression: arrow keys must work after navigating to a sub-screen (resolvedRef reset via key)
70
+ it('arrow keys work in marketplace list after navigating from main', async () => {
71
+ const cbs = mockCallbacks();
72
+ cbs.marketplaceList = vi.fn().mockResolvedValue([
73
+ { name: 'mp-a', type: 'github' },
74
+ { name: 'mp-b', type: 'git' },
75
+ ]);
76
+ const { stdin, lastFrame } = render(<PluginTUI callbacks={cbs} onClose={() => {}} />);
77
+ stdin.write('\r'); // Enter on "Marketplace"
78
+ await new Promise((r) => setTimeout(r, 100));
79
+ // Should be on marketplace-list with arrow keys working
80
+ stdin.write('\x1B[B'); // Down from "Add Marketplace" to "mp-a"
81
+ stdin.write('\x1B[B'); // Down to "mp-b"
82
+ await new Promise((r) => setTimeout(r, 50));
83
+ const frame = lastFrame()!;
84
+ // "mp-b" should be highlighted (has > prefix)
85
+ expect(frame).toContain('mp-b');
86
+ // Select mp-b → should navigate to marketplace-action
87
+ stdin.write('\r');
88
+ await new Promise((r) => setTimeout(r, 50));
89
+ expect(lastFrame()!).toContain('Browse plugins');
90
+ });
91
+
92
+ // Regression: installed plugin browse shows uninstall for already-installed plugins
93
+ it('selecting installed plugin in browse shows uninstall action', async () => {
94
+ const cbs = mockCallbacks();
95
+ cbs.marketplaceList = vi.fn().mockResolvedValue([{ name: 'test-mp', type: 'github' }]);
96
+ cbs.listAvailablePlugins = vi.fn().mockResolvedValue([
97
+ { name: 'my-plugin', description: 'A plugin', installed: true },
98
+ { name: 'new-plugin', description: 'Not installed', installed: false },
99
+ ]);
100
+ const { stdin, lastFrame } = render(<PluginTUI callbacks={cbs} onClose={() => {}} />);
101
+ // Main → Marketplace
102
+ stdin.write('\r');
103
+ await new Promise((r) => setTimeout(r, 100));
104
+ // Marketplace list → select test-mp
105
+ stdin.write('\x1B[B'); // Down to "test-mp"
106
+ stdin.write('\r');
107
+ await new Promise((r) => setTimeout(r, 50));
108
+ // Marketplace action → Browse plugins
109
+ stdin.write('\r');
110
+ await new Promise((r) => setTimeout(r, 100));
111
+ // Browse → select installed "my-plugin"
112
+ stdin.write('\r');
113
+ await new Promise((r) => setTimeout(r, 50));
114
+ // Should show uninstall action, not install scope
115
+ expect(lastFrame()!).toContain('Uninstall');
116
+ expect(lastFrame()!).not.toContain('User scope');
117
+ });
118
+
119
+ // Regression: uninstalled plugin in browse shows install scope selection
120
+ it('selecting uninstalled plugin in browse shows install scope', async () => {
121
+ const cbs = mockCallbacks();
122
+ cbs.marketplaceList = vi.fn().mockResolvedValue([{ name: 'test-mp', type: 'github' }]);
123
+ cbs.listAvailablePlugins = vi
124
+ .fn()
125
+ .mockResolvedValue([{ name: 'new-plugin', description: 'Not installed', installed: false }]);
126
+ const { stdin, lastFrame } = render(<PluginTUI callbacks={cbs} onClose={() => {}} />);
127
+ // Main → Marketplace
128
+ stdin.write('\r');
129
+ await new Promise((r) => setTimeout(r, 100));
130
+ // Marketplace list → select test-mp
131
+ stdin.write('\x1B[B');
132
+ stdin.write('\r');
133
+ await new Promise((r) => setTimeout(r, 50));
134
+ // Marketplace action → Browse plugins
135
+ stdin.write('\r');
136
+ await new Promise((r) => setTimeout(r, 100));
137
+ // Browse → select "new-plugin"
138
+ stdin.write('\r');
139
+ await new Promise((r) => setTimeout(r, 50));
140
+ // Should show install scope, not uninstall
141
+ expect(lastFrame()!).toContain('User scope');
142
+ expect(lastFrame()!).toContain('Project scope');
143
+ });
144
+
145
+ // Regression: install passes name@marketplace format to callback
146
+ it('install callback receives pluginId in name@marketplace format', async () => {
147
+ const cbs = mockCallbacks();
148
+ cbs.marketplaceList = vi.fn().mockResolvedValue([{ name: 'test-mp', type: 'github' }]);
149
+ cbs.listAvailablePlugins = vi
150
+ .fn()
151
+ .mockResolvedValue([{ name: 'new-plugin', description: 'A plugin', installed: false }]);
152
+ const { stdin } = render(<PluginTUI callbacks={cbs} onClose={() => {}} />);
153
+ // Main → Marketplace → test-mp → Browse → new-plugin → User scope
154
+ stdin.write('\r');
155
+ await new Promise((r) => setTimeout(r, 100));
156
+ stdin.write('\x1B[B');
157
+ stdin.write('\r');
158
+ await new Promise((r) => setTimeout(r, 50));
159
+ stdin.write('\r'); // Browse plugins
160
+ await new Promise((r) => setTimeout(r, 100));
161
+ stdin.write('\r'); // new-plugin
162
+ await new Promise((r) => setTimeout(r, 50));
163
+ stdin.write('\r'); // User scope
164
+ await new Promise((r) => setTimeout(r, 100));
165
+ expect(cbs.install).toHaveBeenCalledWith('new-plugin@test-mp', 'user');
166
+ }, 15000);
167
+ });
@@ -0,0 +1,162 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect } from 'vitest';
4
+ import SlashAutocomplete from '../SlashAutocomplete.js';
5
+ import { formatKeyHints, SELECTION_INDICATOR } from '../key-hint-footer.js';
6
+ import type { ICommand } from '@robota-sdk/agent-interface-command';
7
+
8
+ // ink-testing-library fixes stdout.columns = 100
9
+ // outer box chrome = 4 → rowWidth = 96 in tests
10
+ const NAME_COL_MAX = 20;
11
+
12
+ function makeCmd(name: string, description: string): ICommand {
13
+ return { name, description } as ICommand;
14
+ }
15
+
16
+ describe('SlashAutocomplete', () => {
17
+ it('renders nothing when not visible', () => {
18
+ const { lastFrame } = render(
19
+ <SlashAutocomplete
20
+ commands={[makeCmd('help', 'Show help')]}
21
+ selectedIndex={0}
22
+ visible={false}
23
+ />,
24
+ );
25
+ expect(lastFrame()).toBe('');
26
+ });
27
+
28
+ it('renders nothing when command list is empty', () => {
29
+ const { lastFrame } = render(
30
+ <SlashAutocomplete commands={[]} selectedIndex={0} visible={true} />,
31
+ );
32
+ expect(lastFrame()).toBe('');
33
+ });
34
+
35
+ it('aligns descriptions to the same column across all rows', () => {
36
+ const { lastFrame } = render(
37
+ <SlashAutocomplete
38
+ commands={[
39
+ makeCmd('go', 'Short'),
40
+ makeCmd('session-persistence', 'Manage sessions'),
41
+ makeCmd('help', 'Show help'),
42
+ ]}
43
+ selectedIndex={0}
44
+ visible={true}
45
+ />,
46
+ );
47
+ const frame = lastFrame()!;
48
+ const lines = frame
49
+ .split('\n')
50
+ .filter((l) => l.includes('Short') || l.includes('Manage') || l.includes('Show help'));
51
+ // All description texts should start at the same column index
52
+ const descPositions = lines.map((l) => {
53
+ // find position after the two-space separator following the name
54
+ const match = / {2}\S/.exec(l.slice(l.indexOf('/') + 1));
55
+ return match ? l.indexOf('/') + 1 + match.index + 2 : -1;
56
+ });
57
+ expect(new Set(descPositions).size).toBe(1);
58
+ });
59
+
60
+ it('pads short names to match the longest name in visible set', () => {
61
+ const { lastFrame } = render(
62
+ <SlashAutocomplete
63
+ commands={[makeCmd('go', 'Run'), makeCmd('help', 'Show help')]}
64
+ selectedIndex={0}
65
+ visible={true}
66
+ />,
67
+ );
68
+ const frame = lastFrame()!;
69
+ // 'go' should be padded to 4 chars (length of 'help')
70
+ expect(frame).toContain('/go ');
71
+ });
72
+
73
+ it('caps name column at NAME_COL_MAX and truncates with ellipsis', () => {
74
+ const longName = 'a'.repeat(NAME_COL_MAX + 5);
75
+ const { lastFrame } = render(
76
+ <SlashAutocomplete
77
+ commands={[makeCmd(longName, 'Description'), makeCmd('go', 'Short')]}
78
+ selectedIndex={0}
79
+ visible={true}
80
+ />,
81
+ );
82
+ const frame = lastFrame()!;
83
+ expect(frame).toContain('…');
84
+ expect(frame).not.toContain(longName);
85
+ });
86
+
87
+ it('does not truncate name exactly at NAME_COL_MAX', () => {
88
+ const exactName = 'b'.repeat(NAME_COL_MAX);
89
+ const { lastFrame } = render(
90
+ <SlashAutocomplete
91
+ commands={[makeCmd(exactName, 'Desc'), makeCmd('go', 'Short')]}
92
+ selectedIndex={0}
93
+ visible={true}
94
+ />,
95
+ );
96
+ const frame = lastFrame()!;
97
+ expect(frame).toContain(exactName);
98
+ });
99
+
100
+ it('truncates long row text with ellipsis via Ink wrap', () => {
101
+ const longDesc = 'X'.repeat(100);
102
+ const { lastFrame } = render(
103
+ <SlashAutocomplete commands={[makeCmd('cmd', longDesc)]} selectedIndex={0} visible={true} />,
104
+ );
105
+ expect(lastFrame()).toContain('…');
106
+ expect(lastFrame()).not.toContain(longDesc);
107
+ });
108
+
109
+ it('handles undefined description gracefully', () => {
110
+ const cmd = { name: 'cmd' } as ICommand;
111
+ const { lastFrame } = render(
112
+ <SlashAutocomplete commands={[cmd]} selectedIndex={0} visible={true} />,
113
+ );
114
+ expect(lastFrame()).toContain('cmd');
115
+ });
116
+
117
+ it('shows slash prefix in normal mode', () => {
118
+ const { lastFrame } = render(
119
+ <SlashAutocomplete
120
+ commands={[makeCmd('help', 'Show help')]}
121
+ selectedIndex={0}
122
+ visible={true}
123
+ />,
124
+ );
125
+ expect(lastFrame()).toContain('/help');
126
+ });
127
+
128
+ it('omits slash prefix in subcommand mode', () => {
129
+ const { lastFrame } = render(
130
+ <SlashAutocomplete
131
+ commands={[makeCmd('run', 'Run task')]}
132
+ selectedIndex={0}
133
+ visible={true}
134
+ isSubcommandMode={true}
135
+ />,
136
+ );
137
+ const frame = lastFrame()!;
138
+ expect(frame).not.toContain('/run');
139
+ expect(frame).toContain('Run task');
140
+ });
141
+
142
+ // SCREEN-005: footer renders through the key-hint SSOT grammar; selected row uses the indicator.
143
+ it('renders the footer in the shared key-hint grammar', () => {
144
+ const { lastFrame } = render(
145
+ <SlashAutocomplete
146
+ commands={[makeCmd('help', 'Show help')]}
147
+ selectedIndex={0}
148
+ visible={true}
149
+ />,
150
+ );
151
+ const frame = lastFrame()!;
152
+ expect(frame).toContain(
153
+ formatKeyHints([
154
+ { keys: '↑↓', label: 'Navigate' },
155
+ { keys: 'Tab', label: 'Complete' },
156
+ { keys: 'Enter', label: 'Select' },
157
+ { keys: 'Esc', label: 'Close' },
158
+ ]),
159
+ );
160
+ expect(frame).toContain(`${SELECTION_INDICATOR}/help`);
161
+ });
162
+ });
@@ -0,0 +1,52 @@
1
+ import { render } from 'ink';
2
+ import React from 'react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { renderSupervisedSessionView } from '../SupervisedSessionView.js';
6
+
7
+ vi.mock('ink', async (importOriginal) => ({
8
+ ...await importOriginal<typeof import('ink')>(),
9
+ render: vi.fn(() => ({ waitUntilExit: async () => undefined, unmount: () => undefined })),
10
+ }));
11
+
12
+ describe('production supervised view renderer', () => {
13
+ it('forwards the PR open action and filter state into the live view', async () => {
14
+ const onOpenPr = vi.fn(async () => undefined);
15
+ await renderSupervisedSessionView({
16
+ loadRows: async () => [], onOpenPr, filteredByPr: true, screenReader: false,
17
+ });
18
+ const wrapper = vi.mocked(render).mock.calls[0]?.[0] as React.ReactElement<{
19
+ children: React.ReactElement<{ onOpenPr?: typeof onOpenPr; filteredByPr?: boolean }>;
20
+ }>;
21
+ expect(wrapper.props.children.props.onOpenPr).toBe(onOpenPr);
22
+ expect(wrapper.props.children.props.filteredByPr).toBe(true);
23
+ });
24
+
25
+ it('returns the attach the user confirmed so the host can run it and come back', async () => {
26
+ vi.mocked(render).mockClear();
27
+ let exitView: () => void = () => undefined;
28
+ vi.mocked(render).mockImplementationOnce(() => ({
29
+ waitUntilExit: () => new Promise<void>((resolve) => { exitView = resolve; }),
30
+ unmount: () => undefined,
31
+ }) as never);
32
+ const ended = renderSupervisedSessionView({ loadRows: async () => [], screenReader: false });
33
+ const wrapper = vi.mocked(render).mock.calls[0]?.[0] as React.ReactElement<{
34
+ children: React.ReactElement<{ onAttach?: (request: unknown) => void }>;
35
+ }>;
36
+ wrapper.props.children.props.onAttach?.({ id: 'x', generation: 'g', mode: 'observe', groupByDirectory: true });
37
+ exitView();
38
+ expect(await ended).toEqual({ kind: 'attach', id: 'x', generation: 'g', mode: 'observe', groupByDirectory: true });
39
+ });
40
+
41
+ it('prints the screen-reader line only when asked to announce', async () => {
42
+ const out = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
43
+ try {
44
+ await renderSupervisedSessionView({ loadRows: async () => [], screenReader: true, screenReaderChannel: 'flag', announce: false });
45
+ expect(out.mock.calls.map(([text]) => String(text)).join('')).not.toContain('Screen reader mode');
46
+ await renderSupervisedSessionView({ loadRows: async () => [], screenReader: true, screenReaderChannel: 'flag' });
47
+ expect(out.mock.calls.map(([text]) => String(text)).join('')).toContain('Screen reader mode: on via flag');
48
+ } finally {
49
+ out.mockRestore();
50
+ }
51
+ });
52
+ });