@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,366 @@
1
+ /**
2
+ * SCREEN-2002 — the theme picker.
3
+ *
4
+ * Preview-on-move: the highlighted row IS the live region's theme while the picker is open, because
5
+ * a colour scheme cannot be judged from a swatch beside the thing it does not apply to. Escape
6
+ * restores what was persisted; selecting submits `/theme <id>` so the HOST writes the settings
7
+ * document and the picker never becomes a second writer.
8
+ *
9
+ * The transcript above the picker keeps the theme it was written in — Ink's `<Static>` emits an
10
+ * entry once — so a preview moves the input frame, the status bar and this overlay. That is the
11
+ * region a reader is looking at while choosing, and it is the property that makes a preview honest
12
+ * rather than partial.
13
+ */
14
+ import { Box } from 'ink';
15
+ import React from 'react';
16
+
17
+ import { useNumberedSelection } from './hooks/useNumberedSelection.js';
18
+ import { KeyHintFooter, SELECTION_INDICATOR, SELECTION_INDICATOR_NONE } from './key-hint-footer.js';
19
+ import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
20
+ import { formatNumberedSelectionPrompt, numberedRowPrefix } from './numbered-list.js';
21
+ import { Text } from './SafeText.js';
22
+ import { useScreenReader } from './screen-reader-context.js';
23
+ import { isInteractiveColorTerminal } from './terminal-capabilities.js';
24
+ import { usePalette } from './theme/index.js';
25
+
26
+ import type { IAppThemePickerViewModel, IThemeToggles } from './hooks/useAppThemeState.js';
27
+ import type { ITuiTheme } from './theme/index.js';
28
+ import type { IThemeSkip } from './theme/theme-registry.js';
29
+
30
+ function describeTheme(theme: ITuiTheme, isActive: boolean): string {
31
+ const active = isActive ? ' (current)' : '';
32
+ return `${theme.name} — ${theme.appearance}, ${theme.source}${active}`;
33
+ }
34
+
35
+ function ThemeRow({
36
+ theme,
37
+ isFocused,
38
+ isActive,
39
+ rowNumber,
40
+ }: {
41
+ theme: ITuiTheme;
42
+ isFocused: boolean;
43
+ isActive: boolean;
44
+ rowNumber?: number | undefined;
45
+ }): React.ReactElement {
46
+ const palette = usePalette();
47
+ const marker =
48
+ rowNumber !== undefined
49
+ ? numberedRowPrefix(rowNumber)
50
+ : isFocused
51
+ ? SELECTION_INDICATOR
52
+ : SELECTION_INDICATOR_NONE;
53
+ return (
54
+ <Text
55
+ {...(isFocused && rowNumber === undefined ? { color: palette.text.accent, bold: true } : {})}
56
+ >
57
+ {marker}
58
+ {describeTheme(theme, isActive)}
59
+ </Text>
60
+ );
61
+ }
62
+
63
+ /** The typed-selection prompt, re-printed verbatim after an out-of-range answer. */
64
+ function ScreenReaderThemePrompt({
65
+ itemCount,
66
+ buffer,
67
+ invalid,
68
+ }: {
69
+ itemCount: number;
70
+ buffer: string;
71
+ invalid: boolean;
72
+ }): React.ReactElement {
73
+ const prompt = formatNumberedSelectionPrompt(itemCount, true);
74
+ return (
75
+ <Box flexDirection="column">
76
+ <Text>
77
+ {prompt}
78
+ {buffer.length > 0 ? ` ${buffer}` : ''}
79
+ </Text>
80
+ {invalid && <Text>{prompt}</Text>}
81
+ </Box>
82
+ );
83
+ }
84
+
85
+ function focusedIndexOf(themes: readonly ITuiTheme[], activeThemeId: string): number {
86
+ const index = themes.findIndex((theme) => theme.id === activeThemeId);
87
+ return index < 0 ? 0 : index;
88
+ }
89
+
90
+ /**
91
+ * Navigation PREVIEWS and only `select` applies, so the highlight and the live region move together
92
+ * without anything being written until the user says so.
93
+ */
94
+ /**
95
+ * Pending, not applied: the toggles travel with the selection and are submitted WITH it, so escape
96
+ * abandons them exactly as it abandons the previewed theme. Applying them live would make escape
97
+ * mean two different things for two controls in the same overlay.
98
+ */
99
+ function usePendingToggles(picker: IAppThemePickerViewModel): {
100
+ toggles: IThemeToggles;
101
+ setToggles: React.Dispatch<React.SetStateAction<IThemeToggles>>;
102
+ } {
103
+ const [toggles, setToggles] = React.useState<IThemeToggles>(() => ({
104
+ syntaxHighlighting: picker.syntaxHighlighting,
105
+ reducedMotion: picker.reducedMotion,
106
+ }));
107
+ return { toggles, setToggles };
108
+ }
109
+
110
+ /**
111
+ * The highlight, and the preview that follows it. Moving WRAPS at both ends rather than stopping —
112
+ * four themes is a short list, and a dead end at the bottom reads as a broken key.
113
+ */
114
+ function useFocusedRow(picker: IAppThemePickerViewModel): {
115
+ focusedIndex: number;
116
+ move: (delta: number) => void;
117
+ } {
118
+ const { themes, activeThemeId, preview } = picker;
119
+ const [focusedIndex, setFocusedIndex] = React.useState(() =>
120
+ focusedIndexOf(themes, activeThemeId),
121
+ );
122
+ const move = React.useCallback(
123
+ (delta: number): void => {
124
+ if (themes.length === 0) return;
125
+ setFocusedIndex((current) => {
126
+ const next = (current + delta + themes.length) % themes.length;
127
+ const theme = themes[next];
128
+ if (theme) preview(theme.id);
129
+ return next;
130
+ });
131
+ },
132
+ [preview, themes],
133
+ );
134
+ return { focusedIndex, move };
135
+ }
136
+
137
+ function useThemePickerSelection(
138
+ picker: IAppThemePickerViewModel,
139
+ screenReader: boolean,
140
+ ): {
141
+ focusedIndex: number;
142
+ toggles: IThemeToggles;
143
+ numbered: ReturnType<typeof useNumberedSelection>;
144
+ } {
145
+ const { themes, select, cancel } = picker;
146
+ const { focusedIndex, move } = useFocusedRow(picker);
147
+ const { toggles, setToggles } = usePendingToggles(picker);
148
+ const togglesRef = React.useRef(toggles);
149
+ togglesRef.current = toggles;
150
+ const applyAt = React.useCallback(
151
+ (index: number): void => {
152
+ const theme = themes[index];
153
+ if (theme) select(theme.id, togglesRef.current);
154
+ },
155
+ [select, themes],
156
+ );
157
+
158
+ // CLI-2004: an arrow-key menu gets numbers and a typed answer in the mode. It applies on SELECT
159
+ // rather than on navigation — there is no highlight to hear, so a preview per keystroke would
160
+ // just be repaints a reader has to sit through.
161
+ const numbered = useNumberedSelection({
162
+ enabled: screenReader && themes.length > 0,
163
+ itemCount: themes.length,
164
+ cancellable: true,
165
+ onSelect: applyAt,
166
+ onCancel: cancel,
167
+ });
168
+
169
+ const toggle = React.useCallback(
170
+ (key: keyof IThemeToggles): void => {
171
+ setToggles((current) => ({ ...current, [key]: !current[key] }));
172
+ },
173
+ [setToggles],
174
+ );
175
+
176
+ useKeybindingActions(
177
+ 'theme-picker',
178
+ (actions) => {
179
+ for (const action of actions) {
180
+ if (action === 'previous') move(-1);
181
+ else if (action === 'next') move(1);
182
+ else if (action === 'select') applyAt(focusedIndex);
183
+ else if (action === 'toggle-syntax') toggle('syntaxHighlighting');
184
+ else if (action === 'toggle-motion') toggle('reducedMotion');
185
+ else if (action === 'cancel') cancel();
186
+ }
187
+ },
188
+ { isActive: !screenReader },
189
+ );
190
+ return { focusedIndex, toggles, numbered };
191
+ }
192
+
193
+ function ThemeRows({
194
+ themes,
195
+ activeThemeId,
196
+ focusedIndex,
197
+ screenReader,
198
+ }: {
199
+ themes: readonly ITuiTheme[];
200
+ activeThemeId: string;
201
+ focusedIndex: number;
202
+ screenReader: boolean;
203
+ }): React.ReactElement {
204
+ if (themes.length === 0) return <Text dimColor>No themes installed</Text>;
205
+ return (
206
+ <>
207
+ {themes.map((theme, index) => (
208
+ <ThemeRow
209
+ key={theme.id}
210
+ theme={theme}
211
+ isFocused={index === focusedIndex}
212
+ isActive={theme.id === activeThemeId}
213
+ {...(screenReader ? { rowNumber: index } : {})}
214
+ />
215
+ ))}
216
+ </>
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Under the colour gate a preview shows nothing, so the picker says so in words rather than letting
222
+ * the user conclude the highlight is broken. The gate is the degradation rule this package has
223
+ * always had; this is the one place that has to ADMIT it.
224
+ */
225
+ function ColourGateNotice(): React.ReactElement {
226
+ if (isInteractiveColorTerminal()) return <></>;
227
+ return (
228
+ <Text dimColor>Colour is off for this terminal — themes apply, previews cannot show.</Text>
229
+ );
230
+ }
231
+
232
+ /**
233
+ * The theme files this run found and refused. They are shown, never offered: a row a user cannot
234
+ * pick is still the only place that says why the theme they wrote is not in the list — without it
235
+ * the file just silently is not there, and the startup line has long since scrolled away.
236
+ */
237
+ function SkippedRows({ skipped }: { skipped: readonly IThemeSkip[] }): React.ReactElement {
238
+ if (skipped.length === 0) return <></>;
239
+ return (
240
+ <Box flexDirection="column" marginTop={1}>
241
+ {/*
242
+ Keyed by POSITION, not by id. `IThemeSkip.id` is a LABEL and its contract says so: a
243
+ scope-level failure carries one id for a whole scope, and two scopes can both fail, so two
244
+ rows can share it. A duplicate React key would put a warning on stderr from inside the live
245
+ frame, corrupting the frame it is complaining about.
246
+
247
+ Recorded limit: no test pins this. Rendering two same-id rows under ink-testing-library
248
+ produced both rows and no warning on `console.error`, so a test asserting the warning's
249
+ ABSENCE is green either way — an unfalsifiable guard, which is worse than none. The reason
250
+ is written here instead of measured.
251
+ */}
252
+ {skipped.map((skip, index) => (
253
+ <Text key={index} dimColor>
254
+ {`${SELECTION_INDICATOR_NONE}Skipped "${skip.fileName}" — ${skip.reason}`}
255
+ </Text>
256
+ ))}
257
+ </Box>
258
+ );
259
+ }
260
+
261
+ function TogglesRow({
262
+ toggles,
263
+ reducedMotionPin,
264
+ screenReader,
265
+ }: {
266
+ toggles: IThemeToggles;
267
+ reducedMotionPin?: IAppThemePickerViewModel['reducedMotionPin'];
268
+ screenReader: boolean;
269
+ }): React.ReactElement {
270
+ const on = (value: boolean): string => (value ? 'on' : 'off');
271
+ // The pin is admitted here as it is by `/theme list` and `/theme motion` — and it names what THIS
272
+ // RUN does, not only who decided it. The tier alone is not enough: `--no-reduced-motion` is an
273
+ // override too and pins the opposite value, so a row carrying the chosen value beside the tier
274
+ // can read `motion on (pinned by flag)` on a visibly still run.
275
+ const pinned =
276
+ reducedMotionPin === undefined
277
+ ? ''
278
+ : ` (this run: motion ${on(!reducedMotionPin.reducedMotion)}, pinned by ${reducedMotionPin.tier})`;
279
+ return (
280
+ <Box flexDirection="column">
281
+ <Text dimColor>
282
+ {`syntax ${on(toggles.syntaxHighlighting)} · motion ${on(!toggles.reducedMotion)}${pinned}`}
283
+ </Text>
284
+ {/*
285
+ The mode takes a typed answer, so `s` and `m` are not bound in it — but the state above is
286
+ still worth reading before choosing. So the state stays and the AFFORDANCE changes: this
287
+ names a route the mode can actually take, instead of leaving two keys silently inert.
288
+ */}
289
+ {screenReader && (
290
+ <Text dimColor>Change with /theme syntax on|off or /theme motion on|off</Text>
291
+ )}
292
+ </Box>
293
+ );
294
+ }
295
+
296
+ function ThemePickerFooter({
297
+ screenReader,
298
+ itemCount,
299
+ numbered,
300
+ }: {
301
+ screenReader: boolean;
302
+ itemCount: number;
303
+ numbered: ReturnType<typeof useNumberedSelection>;
304
+ }): React.ReactElement {
305
+ // Footer order: navigate → primary → dismiss, the same as every other picker in this package.
306
+ const footerHints = useKeybindingHints('theme-picker', [
307
+ [['previous', 'next'], 'Preview'],
308
+ ['select', 'Apply'],
309
+ ['toggle-syntax', 'Syntax'],
310
+ ['toggle-motion', 'Motion'],
311
+ ['cancel', 'Cancel'],
312
+ ]);
313
+ if (!screenReader) return <KeyHintFooter hints={footerHints} />;
314
+ return (
315
+ <ScreenReaderThemePrompt
316
+ itemCount={itemCount}
317
+ buffer={numbered.buffer}
318
+ invalid={numbered.invalid}
319
+ />
320
+ );
321
+ }
322
+
323
+ export default function ThemePicker({
324
+ picker,
325
+ }: {
326
+ picker: IAppThemePickerViewModel;
327
+ }): React.ReactElement {
328
+ const palette = usePalette();
329
+ const screenReader = useScreenReader();
330
+ const { themes, activeThemeId } = picker;
331
+ const { focusedIndex, toggles, numbered } = useThemePickerSelection(picker, screenReader);
332
+
333
+ return (
334
+ <Box
335
+ flexDirection="column"
336
+ {...(screenReader
337
+ ? {}
338
+ : { borderStyle: 'round' as const, borderColor: palette.border.focused })}
339
+ paddingX={1}
340
+ >
341
+ <Text color={palette.text.accent} bold>
342
+ Theme
343
+ </Text>
344
+ <ColourGateNotice />
345
+ <TogglesRow
346
+ toggles={toggles}
347
+ reducedMotionPin={picker.reducedMotionPin}
348
+ screenReader={screenReader}
349
+ />
350
+ <Box flexDirection="column" marginTop={1}>
351
+ <ThemeRows
352
+ themes={themes}
353
+ activeThemeId={activeThemeId}
354
+ focusedIndex={focusedIndex}
355
+ screenReader={screenReader}
356
+ />
357
+ </Box>
358
+ <SkippedRows skipped={picker.skipped} />
359
+ <ThemePickerFooter
360
+ screenReader={screenReader}
361
+ itemCount={themes.length}
362
+ numbered={numbered}
363
+ />
364
+ </Box>
365
+ );
366
+ }
@@ -0,0 +1,48 @@
1
+ import { Box } from 'ink';
2
+ import React from 'react';
3
+
4
+ import { formatCommandOutputSummary } from './command-output-summary.js';
5
+ import { Text } from './SafeText.js';
6
+ import { statusGlyphColor } from './status-glyph.js';
7
+ import { usePalette } from './theme/index.js';
8
+
9
+ import type { ICommandOutputInput } from './command-output-summary.js';
10
+
11
+ interface IProps {
12
+ tool: ICommandOutputInput;
13
+ }
14
+
15
+ export default function ToolCommandOutput({ tool }: IProps): React.ReactElement | null {
16
+ const palette = usePalette();
17
+ const summary = formatCommandOutputSummary(tool);
18
+ if (!summary) return null;
19
+
20
+ // A successful command with no output renders nothing here: the parent
21
+ // (MessageList ToolSummaryEntry) already shows a "✓ tool(args)" summary line for
22
+ // every tool, so a second "✓ ok" line was a duplicate success marker (SCREEN-007).
23
+ if (
24
+ summary.statusLabel === 'ok' &&
25
+ summary.previewLines.length === 0 &&
26
+ !summary.transcriptHint
27
+ ) {
28
+ return null;
29
+ }
30
+ const color =
31
+ summary.status === 'error' ? statusGlyphColor(palette, 'error') : palette.text.emphasis;
32
+
33
+ return (
34
+ <Box flexDirection="column" marginLeft={4}>
35
+ {summary.statusLabel !== 'ok' && (
36
+ <Text color={color} dimColor>
37
+ {summary.statusLabel}
38
+ </Text>
39
+ )}
40
+ {summary.previewLines.map((line, index) => (
41
+ <Text key={`${line}-${index}`} color={color} dimColor>
42
+ {line}
43
+ </Text>
44
+ ))}
45
+ {summary.transcriptHint && <Text dimColor>{summary.transcriptHint}</Text>}
46
+ </Box>
47
+ );
48
+ }
@@ -0,0 +1,44 @@
1
+ import { Box } from 'ink';
2
+ import React from 'react';
3
+
4
+ import { useRenderMarkdown } from './hooks/useRenderMarkdown.js';
5
+ import { RenderedText, Text } from './SafeText.js';
6
+ import { sanitizeTerminalText } from './sanitize-terminal-text.js';
7
+ import { useScreenReader } from './screen-reader-context.js';
8
+ import { usePalette } from './theme/index.js';
9
+ import { buildToolDiffSummary } from './utils/tool-diff-summary.js';
10
+
11
+ import type { IDiffLine } from './utils/edit-diff.js';
12
+
13
+ interface IProps {
14
+ file?: string;
15
+ lines: readonly IDiffLine[];
16
+ }
17
+
18
+ export default function ToolDiffBlock({ file, lines }: IProps): React.ReactElement {
19
+ const palette = usePalette();
20
+ const renderMarkdown = useRenderMarkdown();
21
+ const summary = buildToolDiffSummary({ file, lines });
22
+ // CLI-2004: the `\u2502` gutter is a vertical rule a reader announces on every line it prefixes.
23
+ // The mode drops it — indentation already separates the block from the surrounding transcript.
24
+ const screenReader = useScreenReader();
25
+ const gutter = screenReader ? '' : '\u2502 ';
26
+
27
+ return (
28
+ <Box flexDirection="column" marginLeft={4}>
29
+ {summary.file && (
30
+ <Text color={palette.text.emphasis} dimColor>
31
+ {gutter}
32
+ {sanitizeTerminalText(summary.file)}
33
+ </Text>
34
+ )}
35
+ {/* `renderMarkdown` sanitizes its input and then styles it; the SGR in its output is ours. */}
36
+ <RenderedText>{renderMarkdown(summary.markdown)}</RenderedText>
37
+ {summary.truncated && (
38
+ <Text color={palette.text.emphasis} dimColor>
39
+ {gutter}... and {summary.remainingLineCount} more lines
40
+ </Text>
41
+ )}
42
+ </Box>
43
+ );
44
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * TransportTUI — interactive overlay for transport enable/disable settings.
3
+ *
4
+ * Arrow keys navigate the list, space toggles enabled/disabled, enter/esc closes.
5
+ *
6
+ * TRANS-009: a toggle SAVES a setting. It does not start or stop anything — `registry.setEnabled`
7
+ * writes the settings file and returns, and `startAll` reads `getEnabled()` when a session starts, so
8
+ * the change takes effect at the next start. This component used to render `[enabled]` on the way
9
+ * back from that write, which told the user a transport was running when nothing had been started.
10
+ *
11
+ * The badge therefore names the SAVED setting and the footer says when it applies. And a failed write
12
+ * is rendered rather than swallowed: the previous `.catch(() => setSaving(false))` took the error and
13
+ * discarded it, so a settings file that could not be written looked exactly like a successful save
14
+ * with the row unchanged.
15
+ */
16
+
17
+ import { Box } from 'ink';
18
+ import React, { useState, useCallback } from 'react';
19
+
20
+ import { KeyHintFooter } from './key-hint-footer.js';
21
+ import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
22
+ import { useProductDisplayName } from './product-display-name-context.js';
23
+ import { Text } from './SafeText.js';
24
+ import { usePalette } from './theme/index.js';
25
+
26
+ import type {
27
+ ITransportEntry,
28
+ ITransportSettingsRegistryView,
29
+ } from '@robota-sdk/agent-interface-transport';
30
+
31
+ const TRANSPORT_NAME_WIDTH = 18;
32
+
33
+ interface IEntryRowProps {
34
+ entry: ITransportEntry;
35
+ selected: boolean;
36
+ }
37
+
38
+ function TransportEntryRow({ entry, selected }: IEntryRowProps): React.ReactElement {
39
+ const palette = usePalette();
40
+ const enabled = entry.config.enabled;
41
+ const dot = enabled ? '●' : '○';
42
+ // The saved setting, not a running state — see the file header.
43
+ const badge = enabled ? '[on] ' : '[off]';
44
+ const portOpt = entry.config.options?.port;
45
+ const portHint = typeof portOpt === 'number' ? `port: ${portOpt}` : '';
46
+ return (
47
+ <Box>
48
+ <Text color={selected ? palette.text.accent : undefined} bold={selected}>
49
+ {`${dot} ${entry.transport.name.padEnd(TRANSPORT_NAME_WIDTH)} ${badge} ${portHint}`}
50
+ </Text>
51
+ </Box>
52
+ );
53
+ }
54
+
55
+ function useTransportInput(
56
+ entries: ITransportEntry[],
57
+ cursor: number,
58
+ saving: boolean,
59
+ registry: ITransportSettingsRegistryView,
60
+ setCursor: (fn: (c: number) => number) => void,
61
+ setSaving: (v: boolean) => void,
62
+ setError: (v: string | undefined) => void,
63
+ onClose: () => void,
64
+ refresh: () => void,
65
+ ): void {
66
+ useKeybindingActions(
67
+ 'transport-settings',
68
+ useCallback(
69
+ (actions: readonly ('previous' | 'next' | 'toggle' | 'close')[]) => {
70
+ if (saving) return;
71
+ if (actions.includes('previous')) {
72
+ setCursor((c) => Math.max(0, c - 1));
73
+ return;
74
+ }
75
+ if (actions.includes('next')) {
76
+ setCursor((c) => Math.min(entries.length - 1, c + 1));
77
+ return;
78
+ }
79
+ if (actions.includes('close')) {
80
+ onClose();
81
+ return;
82
+ }
83
+ if (actions.includes('toggle')) {
84
+ const entry = entries[cursor];
85
+ if (!entry) return;
86
+ setSaving(true);
87
+ setError(undefined);
88
+ registry
89
+ .setEnabled(entry.transport.name, !entry.config.enabled)
90
+ .then(() => {
91
+ refresh();
92
+ setSaving(false);
93
+ })
94
+ .catch((cause: unknown) => {
95
+ // The reason, not just the fact. A settings file that cannot be written and a
96
+ // transport that refuses configuration are different problems for the user.
97
+ setError(cause instanceof Error ? cause.message : 'could not save the setting');
98
+ setSaving(false);
99
+ });
100
+ }
101
+ },
102
+ [saving, entries, cursor, registry, onClose, refresh, setCursor, setSaving, setError],
103
+ ),
104
+ );
105
+ }
106
+
107
+ interface IProps {
108
+ registry: ITransportSettingsRegistryView;
109
+ onClose: () => void;
110
+ }
111
+
112
+ export default function TransportTUI({ registry, onClose }: IProps): React.ReactElement {
113
+ const palette = usePalette();
114
+ const productDisplayName = useProductDisplayName();
115
+ const [entries, setEntries] = useState(() => registry.getAll());
116
+ const [cursor, setCursor] = useState(0);
117
+ const [saving, setSaving] = useState(false);
118
+ const [error, setError] = useState<string | undefined>(undefined);
119
+ const refresh = useCallback((): void => {
120
+ setEntries(registry.getAll());
121
+ }, [registry]);
122
+ const footerHints = useKeybindingHints('transport-settings', [
123
+ [['previous', 'next'], 'Select'],
124
+ ['toggle', 'Toggle'],
125
+ ['close', 'Close'],
126
+ ]);
127
+
128
+ useTransportInput(
129
+ entries,
130
+ cursor,
131
+ saving,
132
+ registry,
133
+ setCursor,
134
+ setSaving,
135
+ setError,
136
+ onClose,
137
+ refresh,
138
+ );
139
+
140
+ return (
141
+ <Box flexDirection="column" paddingX={2} paddingY={1}>
142
+ <Text bold>Settings › Transports</Text>
143
+ <Box marginTop={1} flexDirection="column">
144
+ {entries.map((entry, i) => (
145
+ <TransportEntryRow key={entry.transport.name} entry={entry} selected={i === cursor} />
146
+ ))}
147
+ </Box>
148
+ <Box marginTop={1} flexDirection="column">
149
+ <KeyHintFooter hints={footerHints} />
150
+ <Text dimColor>
151
+ A toggle is saved now and applies the next time {productDisplayName} starts.
152
+ </Text>
153
+ </Box>
154
+ {saving && (
155
+ <Box marginTop={1}>
156
+ <Text color={palette.text.warning}>Saving…</Text>
157
+ </Box>
158
+ )}
159
+ {error !== undefined && (
160
+ <Box marginTop={1}>
161
+ <Text color={palette.text.error}>{`Not saved — ${error}`}</Text>
162
+ </Box>
163
+ )}
164
+ </Box>
165
+ );
166
+ }