@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,154 @@
1
+ import { Box, useStdout } from 'ink';
2
+ import React, { useState, useEffect } from 'react';
3
+
4
+ import {
5
+ KeyHintFooter,
6
+ SELECTION_INDICATOR,
7
+ SELECTION_INDICATOR_NONE,
8
+ type IKeyHint,
9
+ } from './key-hint-footer.js';
10
+ import { useKeybindingHints } from './keybindings/keybindings-context.js';
11
+ import { numberedRowPrefix } from './numbered-list.js';
12
+ import { Text } from './SafeText.js';
13
+ import { useScreenReader } from './screen-reader-context.js';
14
+ import { usePalette } from './theme/index.js';
15
+
16
+ import type { ICommand } from '@robota-sdk/agent-interface-command';
17
+
18
+ /** Footer for the autocomplete popup. */
19
+ export const SLASH_AUTOCOMPLETE_FOOTER_HINTS: readonly IKeyHint[] = [
20
+ { keys: '↑↓', label: 'Navigate' },
21
+ { keys: 'Tab', label: 'Complete' },
22
+ { keys: 'Enter', label: 'Select' },
23
+ { keys: 'Esc', label: 'Close' },
24
+ ];
25
+
26
+ interface IProps {
27
+ /** Filtered list of commands to display */
28
+ commands: ICommand[];
29
+ /** Currently highlighted item index */
30
+ selectedIndex: number;
31
+ /** Whether to show the popup */
32
+ visible: boolean;
33
+ /** Whether showing subcommands (no slash prefix) */
34
+ isSubcommandMode?: boolean;
35
+ }
36
+
37
+ const MAX_VISIBLE = 8;
38
+ // border(1×2) + paddingX(1×2) consumed by outer box
39
+ const OUTER_CHROME = 4;
40
+ const MIN_ROW_WIDTH = 40;
41
+ const NAME_COL_MAX = 20;
42
+
43
+ function useRowWidth(): number {
44
+ const { stdout } = useStdout();
45
+ const measure = (): number => Math.max(MIN_ROW_WIDTH, (stdout.columns ?? 80) - OUTER_CHROME);
46
+ const [width, setWidth] = useState(measure);
47
+
48
+ useEffect(() => {
49
+ const onResize = (): void => setWidth(measure());
50
+ stdout.on('resize', onResize);
51
+ return () => {
52
+ stdout.off('resize', onResize);
53
+ };
54
+ }, [stdout]);
55
+
56
+ return width;
57
+ }
58
+
59
+ function capName(name: string, colWidth: number): string {
60
+ return name.length > colWidth ? `${name.slice(0, colWidth - 1)}…` : name.padEnd(colWidth);
61
+ }
62
+
63
+ /** Render a single command row */
64
+ function CommandRow(props: {
65
+ cmd: ICommand;
66
+ isSelected: boolean;
67
+ showSlash: boolean;
68
+ rowWidth: number;
69
+ nameColWidth: number;
70
+ /** CLI-2004: row number in the mode, replacing the `> ` cursor. */
71
+ rowNumber?: number;
72
+ }): React.ReactElement {
73
+ const palette = usePalette();
74
+ const { cmd, isSelected, showSlash, rowWidth, nameColWidth, rowNumber } = props;
75
+ const indicator =
76
+ rowNumber !== undefined
77
+ ? numberedRowPrefix(rowNumber)
78
+ : isSelected
79
+ ? SELECTION_INDICATOR
80
+ : SELECTION_INDICATOR_NONE;
81
+ const nameColor = isSelected ? palette.text.accent : undefined;
82
+ const dimmed = !isSelected;
83
+ const namePart = capName(cmd.name, nameColWidth);
84
+ const text = showSlash
85
+ ? `${indicator}/${namePart} ${cmd.description ?? ''}`
86
+ : `${indicator}${namePart} ${cmd.description ?? ''}`;
87
+
88
+ return (
89
+ <Box width={rowWidth}>
90
+ <Text color={nameColor} dimColor={dimmed} wrap="truncate-end">
91
+ {text}
92
+ </Text>
93
+ </Box>
94
+ );
95
+ }
96
+
97
+ /** Autocomplete popup showing matching slash commands */
98
+ export default function SlashAutocomplete({
99
+ commands,
100
+ selectedIndex,
101
+ visible,
102
+ isSubcommandMode,
103
+ }: IProps): React.ReactElement | null {
104
+ const palette = usePalette();
105
+ const rowWidth = useRowWidth();
106
+ const screenReader = useScreenReader();
107
+ const footerHints = useKeybindingHints('autocomplete-menu', [
108
+ [['previous', 'next'], 'Navigate'],
109
+ ['accept', 'Complete'],
110
+ ['execute', 'Select'],
111
+ ['close', 'Close'],
112
+ ]);
113
+
114
+ if (!visible || commands.length === 0) return null;
115
+
116
+ const scrollOffset = computeScrollOffset(selectedIndex, commands.length);
117
+ const visibleCommands = commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
118
+
119
+ const nameColWidth = Math.min(
120
+ NAME_COL_MAX,
121
+ Math.max(...visibleCommands.map((c) => c.name.length)),
122
+ );
123
+
124
+ return (
125
+ <Box
126
+ flexDirection="column"
127
+ {...(screenReader
128
+ ? {}
129
+ : { borderStyle: 'round' as const, borderColor: palette.border.muted })}
130
+ paddingX={1}
131
+ >
132
+ {visibleCommands.map((cmd, i) => (
133
+ <CommandRow
134
+ key={cmd.name}
135
+ cmd={cmd}
136
+ isSelected={scrollOffset + i === selectedIndex}
137
+ showSlash={!isSubcommandMode}
138
+ rowWidth={rowWidth}
139
+ nameColWidth={nameColWidth}
140
+ {...(screenReader ? { rowNumber: scrollOffset + i } : {})}
141
+ />
142
+ ))}
143
+ <KeyHintFooter hints={footerHints} />
144
+ </Box>
145
+ );
146
+ }
147
+
148
+ /** Compute scroll offset to keep selectedIndex visible */
149
+ function computeScrollOffset(selectedIndex: number, total: number): number {
150
+ if (total <= MAX_VISIBLE) return 0;
151
+ if (selectedIndex < MAX_VISIBLE) return 0;
152
+ const maxOffset = total - MAX_VISIBLE;
153
+ return Math.min(selectedIndex - MAX_VISIBLE + 1, maxOffset);
154
+ }
@@ -0,0 +1,284 @@
1
+ import { formatTokenCount } from '@robota-sdk/agent-core';
2
+ import { Box } from 'ink';
3
+ import React from 'react';
4
+
5
+ import { Text } from './SafeText.js';
6
+ import { useScreenReader } from './screen-reader-context.js';
7
+ import { formatStatusActivity } from './status-activity.js';
8
+ import { usePalette } from './theme/index.js';
9
+
10
+ import type { IThemeColors } from './theme/index.js';
11
+ import type { TModelEffortSelection, TPermissionMode } from '@robota-sdk/agent-core';
12
+
13
+ /** Threshold boundaries for context percentage color coding */
14
+ const CONTEXT_YELLOW_THRESHOLD = 70;
15
+ const CONTEXT_RED_THRESHOLD = 90;
16
+
17
+ /** Segment separator between status-bar fields (SCREEN-006: single SSOT, was repeated inline 6x). */
18
+ const SEP = ' | ';
19
+
20
+ interface IProps {
21
+ permissionMode: TPermissionMode;
22
+ modelName: string;
23
+ providerDisplayName?: string | undefined;
24
+ sessionId: string;
25
+ isThinking: boolean;
26
+ activeToolCount?: number;
27
+ activeBackgroundTaskCount?: number;
28
+ hasPendingPrompt?: boolean;
29
+ contextPercentage: number;
30
+ contextUsedTokens: number;
31
+ contextMaxTokens: number;
32
+ sessionName?: string;
33
+ gitBranch?: string;
34
+ showGitBranch?: boolean;
35
+ activeAgentLabel?: string;
36
+ activePresetId?: string;
37
+ effort?: TModelEffortSelection;
38
+ }
39
+
40
+ interface IStatusLeftProps {
41
+ permissionMode: TPermissionMode;
42
+ modelName: string;
43
+ providerDisplayName?: string | undefined;
44
+ isThinking: boolean;
45
+ activeToolCount: number;
46
+ activeBackgroundTaskCount: number;
47
+ hasPendingPrompt: boolean;
48
+ contextPercentage: number;
49
+ contextUsedTokens: number;
50
+ contextMaxTokens: number;
51
+ sessionName?: string;
52
+ gitBranch?: string;
53
+ showGitBranch: boolean;
54
+ activePresetId?: string;
55
+ effort?: TModelEffortSelection;
56
+ }
57
+
58
+ /** Return the color for the context percentage indicator */
59
+ function getContextColor(percentage: number, palette: IThemeColors): string {
60
+ if (percentage >= CONTEXT_RED_THRESHOLD) return palette.text.error;
61
+ if (percentage >= CONTEXT_YELLOW_THRESHOLD) return palette.text.warning;
62
+ return palette.text.success;
63
+ }
64
+
65
+ function StatusActivityText({
66
+ isThinking,
67
+ activeToolCount,
68
+ activeBackgroundTaskCount,
69
+ hasPendingPrompt,
70
+ }: Pick<
71
+ IStatusLeftProps,
72
+ 'isThinking' | 'activeToolCount' | 'activeBackgroundTaskCount' | 'hasPendingPrompt'
73
+ >): React.ReactElement {
74
+ const palette = usePalette();
75
+ const activity = formatStatusActivity({
76
+ isThinking,
77
+ activeToolCount,
78
+ activeBackgroundTaskCount,
79
+ hasPendingPrompt,
80
+ });
81
+
82
+ return (
83
+ <Text color={palette.text[activity.tone]} bold={activity.kind !== 'idle'}>
84
+ {activity.text}
85
+ </Text>
86
+ );
87
+ }
88
+
89
+ function ContextText({
90
+ percentage,
91
+ usedTokens,
92
+ maxTokens,
93
+ }: {
94
+ percentage: number;
95
+ usedTokens: number;
96
+ maxTokens: number;
97
+ }): React.ReactElement {
98
+ const palette = usePalette();
99
+ return (
100
+ <Text color={getContextColor(percentage, palette)}>
101
+ Context: {Math.round(percentage)}% ({formatTokenCount(usedTokens)}/
102
+ {formatTokenCount(maxTokens)} tokens)
103
+ </Text>
104
+ );
105
+ }
106
+
107
+ function ModeText({ permissionMode }: { permissionMode: TPermissionMode }): React.ReactElement {
108
+ const palette = usePalette();
109
+ return (
110
+ <>
111
+ <Text color={palette.text.accent} bold>
112
+ Mode:
113
+ </Text>{' '}
114
+ <Text>{permissionMode}</Text>
115
+ </>
116
+ );
117
+ }
118
+
119
+ /**
120
+ * CLI-2004 verdict (l): outside the mode the `default` mode is hidden as noise. In the mode it is
121
+ * ALWAYS rendered — with no key-based cycling to announce, the status line is the only place a
122
+ * reader can find which permission mode it is in, and a hidden `default` leaves nothing to find.
123
+ */
124
+ function shouldShowPermissionMode(permissionMode: TPermissionMode, screenReader: boolean): boolean {
125
+ return screenReader || permissionMode !== 'default';
126
+ }
127
+
128
+ function PresetText({ activePresetId }: { activePresetId: string }): React.ReactElement {
129
+ const palette = usePalette();
130
+ return (
131
+ <>
132
+ <Text color={palette.text.accent} bold>
133
+ Preset:
134
+ </Text>{' '}
135
+ <Text>{activePresetId}</Text>
136
+ </>
137
+ );
138
+ }
139
+
140
+ function shouldShowActivePreset(activePresetId: string | undefined): activePresetId is string {
141
+ return activePresetId !== undefined && activePresetId !== 'default';
142
+ }
143
+
144
+ function ProviderText({
145
+ modelName,
146
+ providerDisplayName,
147
+ effort,
148
+ }: {
149
+ modelName: string;
150
+ providerDisplayName?: string | undefined;
151
+ effort?: TModelEffortSelection;
152
+ }): React.ReactElement {
153
+ const provider =
154
+ providerDisplayName !== undefined ? `${providerDisplayName} ${modelName}` : modelName;
155
+ if (providerDisplayName !== undefined) {
156
+ return (
157
+ <Text dimColor>
158
+ {provider}
159
+ {effort !== undefined ? ` Effort: ${effort}` : ''}
160
+ </Text>
161
+ );
162
+ }
163
+ return (
164
+ <Text dimColor>
165
+ {provider}
166
+ {effort !== undefined ? ` Effort: ${effort}` : ''}
167
+ </Text>
168
+ );
169
+ }
170
+
171
+ function StatusLeft(props: IStatusLeftProps): React.ReactElement {
172
+ const palette = usePalette();
173
+ const shouldShowGitBranch =
174
+ props.showGitBranch && props.gitBranch !== undefined && props.gitBranch.length > 0;
175
+ // CLI-2004 § Solution 13: one rule applied to fields of different VOLATILITY. The activity text
176
+ // and the context percentage re-render on their own cadence and on every token, so a reader
177
+ // reviewing the status line would re-announce them continuously while the operator types — they
178
+ // are suppressed. The permission mode, the preset and the model change only when the operator
179
+ // changes them: those are anchors, and they stay.
180
+ const screenReader = useScreenReader();
181
+ const showPermissionMode = shouldShowPermissionMode(props.permissionMode, screenReader);
182
+ const activePresetId = props.activePresetId;
183
+ const showActivePreset = shouldShowActivePreset(activePresetId);
184
+ return (
185
+ <Text>
186
+ {!screenReader && (
187
+ <StatusActivityText
188
+ isThinking={props.isThinking}
189
+ activeToolCount={props.activeToolCount}
190
+ activeBackgroundTaskCount={props.activeBackgroundTaskCount}
191
+ hasPendingPrompt={props.hasPendingPrompt}
192
+ />
193
+ )}
194
+ {showPermissionMode && (
195
+ <>
196
+ {SEP}
197
+ <ModeText permissionMode={props.permissionMode} />
198
+ </>
199
+ )}
200
+ {showActivePreset && (
201
+ <>
202
+ {SEP}
203
+ <PresetText activePresetId={activePresetId} />
204
+ </>
205
+ )}
206
+ {props.sessionName && (
207
+ <>
208
+ {SEP}
209
+ <Text color={palette.text.session}>{props.sessionName}</Text>
210
+ </>
211
+ )}
212
+ {shouldShowGitBranch && (
213
+ <>
214
+ {SEP}
215
+ <Text dimColor>git: {props.gitBranch}</Text>
216
+ </>
217
+ )}
218
+ {SEP}
219
+ <ProviderText
220
+ modelName={props.modelName}
221
+ providerDisplayName={props.providerDisplayName}
222
+ effort={props.effort}
223
+ />
224
+ {!screenReader && (
225
+ <>
226
+ {SEP}
227
+ <ContextText
228
+ percentage={props.contextPercentage}
229
+ usedTokens={props.contextUsedTokens}
230
+ maxTokens={props.contextMaxTokens}
231
+ />
232
+ </>
233
+ )}
234
+ </Text>
235
+ );
236
+ }
237
+
238
+ export default function StatusBar({
239
+ permissionMode,
240
+ modelName,
241
+ providerDisplayName,
242
+ sessionId: _sessionId,
243
+ isThinking,
244
+ activeToolCount = 0,
245
+ activeBackgroundTaskCount = 0,
246
+ hasPendingPrompt = false,
247
+ contextPercentage,
248
+ contextUsedTokens,
249
+ contextMaxTokens,
250
+ sessionName,
251
+ gitBranch,
252
+ showGitBranch = true,
253
+ activeAgentLabel,
254
+ activePresetId,
255
+ effort,
256
+ }: IProps): React.ReactElement {
257
+ const palette = usePalette();
258
+ return (
259
+ <Box paddingLeft={1} paddingRight={1} justifyContent="space-between">
260
+ <StatusLeft
261
+ permissionMode={permissionMode}
262
+ modelName={modelName}
263
+ providerDisplayName={providerDisplayName}
264
+ isThinking={isThinking}
265
+ activeToolCount={activeToolCount}
266
+ activeBackgroundTaskCount={activeBackgroundTaskCount}
267
+ hasPendingPrompt={hasPendingPrompt}
268
+ contextPercentage={contextPercentage}
269
+ contextUsedTokens={contextUsedTokens}
270
+ contextMaxTokens={contextMaxTokens}
271
+ sessionName={sessionName}
272
+ gitBranch={gitBranch}
273
+ showGitBranch={showGitBranch}
274
+ activePresetId={activePresetId}
275
+ effort={effort}
276
+ />
277
+ {activeAgentLabel !== undefined && (
278
+ <Text color={palette.text.warning} bold>
279
+ [{activeAgentLabel}]
280
+ </Text>
281
+ )}
282
+ </Box>
283
+ );
284
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Streaming indicator — shows real-time tool execution and AI response text.
3
+ * Displayed during session.run() execution.
4
+ */
5
+
6
+ import { Box } from 'ink';
7
+ import React from 'react';
8
+
9
+ import { useRenderMarkdown } from './hooks/useRenderMarkdown.js';
10
+ import { humanizeToolArgument, humanizeToolName } from './humanize-tool-name.js';
11
+ import { RenderedText, Text } from './SafeText.js';
12
+ import {
13
+ useModelCommandToolPrefix,
14
+ useProductDisplayName,
15
+ } from './product-display-name-context.js';
16
+ import { useScreenReader } from './screen-reader-context.js';
17
+ import { SCREEN_READER_LABELS } from './screen-reader-labels.js';
18
+ import { STATUS_SYMBOL, statusGlyphColor, toolStateStatusKind } from './status-glyph.js';
19
+ import { usePalette } from './theme/index.js';
20
+ import ToolDiffBlock from './ToolDiffBlock.js';
21
+
22
+ import type { IThemeColors } from './theme/index.js';
23
+ import type { IToolState } from '@robota-sdk/agent-interface-session';
24
+
25
+ function getToolStyle(
26
+ t: IToolState,
27
+ palette: IThemeColors,
28
+ ): {
29
+ color: string;
30
+ icon: string;
31
+ strikethrough: boolean;
32
+ } {
33
+ const kind = toolStateStatusKind(t);
34
+ return {
35
+ color: statusGlyphColor(palette, kind),
36
+ icon: STATUS_SYMBOL[kind],
37
+ strikethrough: kind === 'error' || kind === 'denied',
38
+ };
39
+ }
40
+
41
+ interface IProps {
42
+ text: string;
43
+ activeTools: readonly IToolState[];
44
+ isThinking?: boolean;
45
+ }
46
+
47
+ function ThinkingFallback({ isThinking }: { isThinking: boolean }): React.ReactElement {
48
+ const palette = usePalette();
49
+ if (!isThinking) return <></>;
50
+ return (
51
+ <Box marginBottom={1}>
52
+ <Text color={palette.text.warning}>Thinking...</Text>
53
+ </Box>
54
+ );
55
+ }
56
+
57
+ function ActiveTools({ activeTools }: { activeTools: readonly IToolState[] }): React.ReactElement {
58
+ const palette = usePalette();
59
+ const modelCommandToolPrefix = useModelCommandToolPrefix();
60
+ return (
61
+ <Box flexDirection="column" marginBottom={1}>
62
+ <Text color={palette.text.emphasis} bold>
63
+ Tools:
64
+ </Text>
65
+ <Text> </Text>
66
+ {activeTools.map((t, i) => {
67
+ const { color, icon, strikethrough } = getToolStyle(t, palette);
68
+ return (
69
+ <Box key={`${t.toolName}-${i}`} flexDirection="column">
70
+ <Text color={color} strikethrough={strikethrough}>
71
+ {' '}
72
+ {icon} {humanizeToolName(t.toolName, modelCommandToolPrefix)}({humanizeToolArgument(t.firstArg)})
73
+ </Text>
74
+ {t.diffLines && t.diffLines.length > 0 && (
75
+ <ToolDiffBlock file={t.diffFile} lines={t.diffLines} />
76
+ )}
77
+ </Box>
78
+ );
79
+ })}
80
+ </Box>
81
+ );
82
+ }
83
+
84
+ /**
85
+ * CLI-2004: the whole progress indicator collapses to ONE static line. A spinner is a repaint per
86
+ * frame, and a repaint is an announcement — the reader would hear the same line forever. The line
87
+ * still carries what changed (which tools are running, how far the reply has got), just once per
88
+ * actual change instead of once per frame.
89
+ */
90
+ function renderScreenReaderStatus(
91
+ text: string,
92
+ activeTools: readonly IToolState[],
93
+ isThinking: boolean,
94
+ modelCommandToolPrefix?: string,
95
+ ): React.ReactElement {
96
+ const running = activeTools
97
+ .map((tool) => humanizeToolName(tool.toolName, modelCommandToolPrefix))
98
+ .join(', ');
99
+ const parts = [
100
+ running.length > 0 ? `${SCREEN_READER_LABELS.tool} ${running}` : undefined,
101
+ text.length > 0 ? text : isThinking ? SCREEN_READER_LABELS.thinking : undefined,
102
+ ].filter((part): part is string => part !== undefined);
103
+ if (parts.length === 0) return <></>;
104
+ return <Text>{parts.join(' — ')}</Text>;
105
+ }
106
+
107
+ export default function StreamingIndicator({
108
+ text,
109
+ activeTools,
110
+ isThinking = false,
111
+ }: IProps): React.ReactElement {
112
+ const palette = usePalette();
113
+ const renderMarkdown = useRenderMarkdown();
114
+ const hasTools = activeTools.length > 0;
115
+ const hasText = text.length > 0;
116
+ const screenReader = useScreenReader();
117
+ const productDisplayName = useProductDisplayName();
118
+ const modelCommandToolPrefix = useModelCommandToolPrefix();
119
+
120
+ if (screenReader) {
121
+ return renderScreenReaderStatus(text, activeTools, isThinking, modelCommandToolPrefix);
122
+ }
123
+
124
+ if (!hasTools && !hasText) {
125
+ return <ThinkingFallback isThinking={isThinking} />;
126
+ }
127
+
128
+ return (
129
+ <Box flexDirection="column">
130
+ {hasTools && <ActiveTools activeTools={activeTools} />}
131
+ {hasText && (
132
+ <Box flexDirection="column" marginBottom={1}>
133
+ <Text color={palette.text.accent} bold>
134
+ {productDisplayName}:
135
+ </Text>
136
+ <Text> </Text>
137
+ <Box marginLeft={2}>
138
+ {/* `renderMarkdown` sanitizes its input and then styles it; the SGR in its output is ours. */}
139
+ <RenderedText wrap="wrap">{renderMarkdown(text)}</RenderedText>
140
+ </Box>
141
+ </Box>
142
+ )}
143
+ </Box>
144
+ );
145
+ }