@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,282 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import StatusBar from '../StatusBar.js';
5
+ import SessionStatusBar from '../SessionStatusBar.js';
6
+ import { ScreenReaderProvider } from '../screen-reader-context.js';
7
+ import { TuiCliAdapterProvider } from '../tui-cli-adapter-context.js';
8
+ import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
9
+
10
+ describe('StatusBar', () => {
11
+ const baseProps = {
12
+ permissionMode: 'default' as const,
13
+ modelName: 'test-model',
14
+ sessionId: 'sess-1',
15
+ isThinking: false,
16
+ activeToolCount: 0,
17
+ activeBackgroundTaskCount: 0,
18
+ hasPendingPrompt: false,
19
+ contextPercentage: 10,
20
+ contextUsedTokens: 1000,
21
+ contextMaxTokens: 200000,
22
+ };
23
+
24
+ it('renders without session name', () => {
25
+ const { lastFrame } = render(<StatusBar {...baseProps} />);
26
+ const frame = lastFrame()!;
27
+ expect(frame).toContain('test-model');
28
+ expect(frame).not.toContain('Mode: default');
29
+ });
30
+
31
+ it('hides default permission mode', () => {
32
+ const { lastFrame } = render(<StatusBar {...baseProps} permissionMode="default" />);
33
+ const frame = lastFrame()!;
34
+ expect(frame).not.toContain('Mode:');
35
+ expect(frame).not.toContain('default');
36
+ });
37
+
38
+ it('shows non-default permission modes', () => {
39
+ for (const permissionMode of ['plan', 'acceptEdits', 'bypassPermissions'] as const) {
40
+ const { lastFrame, unmount } = render(
41
+ <StatusBar {...baseProps} permissionMode={permissionMode} />,
42
+ );
43
+ const frame = lastFrame()!;
44
+ expect(frame).toContain('Mode:');
45
+ expect(frame).toContain(permissionMode);
46
+ unmount();
47
+ }
48
+ });
49
+
50
+ it('renders session name when provided', () => {
51
+ const { lastFrame } = render(<StatusBar {...baseProps} sessionName="my-feature" />);
52
+ const frame = lastFrame()!;
53
+ expect(frame).toContain('my-feature');
54
+ });
55
+
56
+ it('does not show session name when undefined', () => {
57
+ const { lastFrame } = render(<StatusBar {...baseProps} sessionName={undefined} />);
58
+ const frame = lastFrame()!;
59
+ // Should not have extra separator for missing name
60
+ expect(frame).not.toContain('my-feature');
61
+ });
62
+
63
+ it('renders model name', () => {
64
+ const { lastFrame } = render(<StatusBar {...baseProps} />);
65
+ const frame = lastFrame()!;
66
+ expect(frame).toContain('test-model');
67
+ });
68
+
69
+ it('renders provider display name and model when provided', () => {
70
+ const { lastFrame } = render(<StatusBar {...baseProps} providerDisplayName="Anthropic" />);
71
+ const frame = lastFrame()!;
72
+ expect(frame).toContain('Anthropic');
73
+ expect(frame).toContain('test-model');
74
+ });
75
+
76
+ it('renders the active model effort when provided', () => {
77
+ const { lastFrame } = render(<StatusBar {...baseProps} effort="high" />);
78
+ expect(lastFrame()).toContain('Effort: high');
79
+ });
80
+
81
+ it('renders auto as the provider-default effort selection', () => {
82
+ const { lastFrame } = render(<StatusBar {...baseProps} effort="auto" />);
83
+ expect(lastFrame()).toContain('Effort: auto');
84
+ });
85
+
86
+ it('does not render message count in the status bar', () => {
87
+ const { lastFrame } = render(<StatusBar {...baseProps} />);
88
+ const frame = lastFrame()!;
89
+ expect(frame).not.toContain('msgs:');
90
+ });
91
+
92
+ it('shows thinking indicator when isThinking is true', () => {
93
+ const { lastFrame } = render(<StatusBar {...baseProps} isThinking={true} />);
94
+ const frame = lastFrame()!;
95
+ expect(frame).not.toContain('Activity:');
96
+ expect(frame).toContain('Thinking');
97
+ expect(frame.indexOf('Thinking')).toBeLessThan(frame.indexOf('test-model'));
98
+ });
99
+
100
+ it('does not duplicate thinking state in secondary status text', () => {
101
+ const { lastFrame } = render(<StatusBar {...baseProps} isThinking={true} />);
102
+ const frame = lastFrame()!;
103
+ expect(frame).toContain('Thinking');
104
+ expect(frame).not.toContain('thinking...');
105
+ expect(frame).not.toContain('msgs:');
106
+ });
107
+
108
+ it('hides the lower-right prompt-processing indicator while idle', () => {
109
+ const { lastFrame } = render(<StatusBar {...baseProps} isThinking={false} />);
110
+ const frame = lastFrame()!;
111
+ expect(frame).not.toContain('thinking...');
112
+ });
113
+
114
+ it('prioritizes tool activity in the primary scan path', () => {
115
+ const { lastFrame } = render(
116
+ <StatusBar
117
+ {...baseProps}
118
+ isThinking={true}
119
+ activeToolCount={2}
120
+ activeBackgroundTaskCount={1}
121
+ hasPendingPrompt={true}
122
+ />,
123
+ );
124
+ const frame = lastFrame()!;
125
+ expect(frame).not.toContain('Activity:');
126
+ expect(frame).toContain('Tools (2)');
127
+ expect(frame).not.toContain('Tools x2');
128
+ expect(frame).toContain('queued');
129
+ expect(frame).not.toContain('thinking...');
130
+ expect(frame.indexOf('Tools (2)')).toBeLessThan(frame.indexOf('test-model'));
131
+ expect(frame).not.toContain('Thinking...');
132
+ });
133
+
134
+ it('shows background activity when no foreground execution is active', () => {
135
+ const { lastFrame } = render(<StatusBar {...baseProps} activeBackgroundTaskCount={3} />);
136
+ const frame = lastFrame()!;
137
+ expect(frame).toContain('Background (3)');
138
+ expect(frame.indexOf('Background (3)')).toBeLessThan(frame.indexOf('test-model'));
139
+ });
140
+
141
+ it('keeps the activity segment compact for narrow terminals', () => {
142
+ const { lastFrame } = render(
143
+ <StatusBar
144
+ {...baseProps}
145
+ isThinking={true}
146
+ activeToolCount={12}
147
+ activeBackgroundTaskCount={9}
148
+ hasPendingPrompt={true}
149
+ />,
150
+ );
151
+ const frame = lastFrame()!;
152
+ const firstLine = frame.split('\n')[0] ?? '';
153
+ const activityEnd = firstLine.indexOf('test-model');
154
+ const activitySegment = firstLine.slice(0, activityEnd);
155
+ expect(activitySegment).toContain('Tools (12)');
156
+ expect(activitySegment.length).toBeLessThanOrEqual(40);
157
+ });
158
+
159
+ it('renders git branch when provided', () => {
160
+ const { lastFrame } = render(<StatusBar {...baseProps} gitBranch="feat/status-line" />);
161
+ const frame = lastFrame()!;
162
+ expect(frame).toContain('feat/status-line');
163
+ });
164
+
165
+ it('does not render git branch when visibility is disabled', () => {
166
+ const { lastFrame } = render(
167
+ <StatusBar {...baseProps} gitBranch="feat/status-line" showGitBranch={false} />,
168
+ );
169
+ const frame = lastFrame()!;
170
+ expect(frame).not.toContain('feat/status-line');
171
+ });
172
+
173
+ it('TC-03: shows the active preset id when set and non-default', () => {
174
+ const { lastFrame } = render(<StatusBar {...baseProps} activePresetId="autonomous-builder" />);
175
+ const frame = lastFrame()!;
176
+ expect(frame).toContain('Preset:');
177
+ expect(frame).toContain('autonomous-builder');
178
+ });
179
+
180
+ it('TC-03: hides the default active preset', () => {
181
+ const { lastFrame } = render(<StatusBar {...baseProps} activePresetId="default" />);
182
+ const frame = lastFrame()!;
183
+ expect(frame).not.toContain('Preset:');
184
+ });
185
+
186
+ it('TC-03: hides the preset label when no active preset is provided', () => {
187
+ const { lastFrame } = render(<StatusBar {...baseProps} activePresetId={undefined} />);
188
+ const frame = lastFrame()!;
189
+ expect(frame).not.toContain('Preset:');
190
+ });
191
+ });
192
+
193
+ /**
194
+ * CLI-2004 TC-16 / TC-21 — § Solution 13, asserted through `SessionStatusBar` (the way the App
195
+ * reaches `StatusBar`) so the threading is covered too.
196
+ *
197
+ * The two halves are asserted TOGETHER on purpose: they pull in opposite directions — one field is
198
+ * rendered MORE in the mode, two are rendered LESS — and stating them apart is how they drift into
199
+ * contradicting each other.
200
+ */
201
+ describe('CLI-2004: the status line in screen-reader mode', () => {
202
+ const sessionProps = {
203
+ cwd: '/tmp/project',
204
+ permissionMode: 'default' as const,
205
+ modelId: 'test-model',
206
+ sessionId: 'sess-1',
207
+ isThinking: false,
208
+ activeToolCount: 0,
209
+ activeBackgroundTaskCount: 0,
210
+ hasPendingPrompt: false,
211
+ contextState: { percentage: 42, usedTokens: 1000, maxTokens: 200000 },
212
+ settings: { enabled: true, gitBranch: false } as never,
213
+ activePresetId: 'reviewer',
214
+ };
215
+
216
+ function cliAdapter(): ITuiCliAdapter {
217
+ return {
218
+ getUserSettingsPath: () => '/tmp/fake-settings.json',
219
+ readSettings: () => ({}),
220
+ reloadPluginCommandSource: vi.fn(),
221
+ applyActiveModelChange: vi.fn(),
222
+ getGitBranch: vi.fn().mockReturnValue(undefined),
223
+ getProviderDisplayName: vi.fn((type: string) => type),
224
+ } as unknown as ITuiCliAdapter;
225
+ }
226
+
227
+ function renderStatus(enabled: boolean, overrides: Record<string, unknown> = {}): string {
228
+ const { lastFrame, unmount } = render(
229
+ <ScreenReaderProvider enabled={enabled}>
230
+ <TuiCliAdapterProvider value={cliAdapter()}>
231
+ <SessionStatusBar {...sessionProps} {...overrides} />
232
+ </TuiCliAdapterProvider>
233
+ </ScreenReaderProvider>,
234
+ );
235
+ const frame = lastFrame() ?? '';
236
+ unmount();
237
+ return frame;
238
+ }
239
+
240
+ it('TC-16: renders the permission mode even when it is `default`', () => {
241
+ const frame = renderStatus(true);
242
+ expect(frame).toContain('Mode:');
243
+ expect(frame).toContain('default');
244
+ });
245
+
246
+ it('TC-16: still hides the `default` permission mode outside the mode', () => {
247
+ const frame = renderStatus(false);
248
+ expect(frame).not.toContain('Mode:');
249
+ });
250
+
251
+ it('TC-21: suppresses the volatile fields and keeps the stable ones', () => {
252
+ const frame = renderStatus(true, { isThinking: true, activeToolCount: 2 });
253
+
254
+ // Volatile — re-rendered on their own cadence and on every token.
255
+ expect(frame).not.toContain('Context:');
256
+ expect(frame).not.toContain('42%');
257
+ expect(frame).not.toContain('Idle');
258
+ expect(frame).not.toContain('Thinking');
259
+
260
+ // Stable — anchors a reader can go back to.
261
+ expect(frame).toContain('Mode:');
262
+ expect(frame).toContain('default');
263
+ expect(frame).toContain('Preset:');
264
+ expect(frame).toContain('reviewer');
265
+ expect(frame).toContain('test-model');
266
+ });
267
+
268
+ it('TC-21: renders all four exactly as today outside the mode', () => {
269
+ const frame = renderStatus(false, {
270
+ isThinking: true,
271
+ activeToolCount: 2,
272
+ permissionMode: 'plan',
273
+ });
274
+
275
+ expect(frame).toContain('Context:');
276
+ expect(frame).toContain('42%');
277
+ expect(frame).toContain('Mode:');
278
+ expect(frame).toContain('plan');
279
+ expect(frame).toContain('Preset:');
280
+ expect(frame).toContain('test-model');
281
+ });
282
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * SCREEN-009: lock in the status-legibility invariant — every status kind pairs a non-empty
3
+ * SYMBOL with a color (never color alone), so the TUI stays legible on no-color terminals and
4
+ * for colorblind users. A new status kind that omits the symbol fails here.
5
+ */
6
+ import { describe, it, expect } from 'vitest';
7
+ import { STATUS_SYMBOL, statusGlyphColor, workspaceStatusKind } from '../status-glyph';
8
+
9
+ import type { TUiStatusKind } from '../status-glyph';
10
+ import { DARK_THEME } from '../theme/built-in-themes.js';
11
+
12
+ describe('status-glyph SSOT (SCREEN-009)', () => {
13
+ it('every status kind has a non-empty symbol AND color (no color-only state)', () => {
14
+ const entries = Object.entries(STATUS_SYMBOL) as [TUiStatusKind, string][];
15
+ expect(entries.length).toBeGreaterThan(0);
16
+ for (const [kind, symbol] of entries) {
17
+ expect(symbol, `${kind} must have a symbol`).toBeTruthy();
18
+ expect(symbol.length, `${kind} symbol must be non-empty`).toBeGreaterThan(0);
19
+ // SCREEN-2002: the colour comes from the live theme, so the pairing is checked against one.
20
+ expect(statusGlyphColor(DARK_THEME.colors, kind), `${kind} must have a color`).toBeTruthy();
21
+ }
22
+ });
23
+
24
+ it('workspaceStatusKind maps to a glyph that exists in the SSOT', () => {
25
+ const kind = workspaceStatusKind('completed');
26
+ expect(kind).toBe('success');
27
+ expect(STATUS_SYMBOL[kind]).toBeTruthy();
28
+ expect(statusGlyphColor(DARK_THEME.colors, kind)).toBeTruthy();
29
+ });
30
+ });
@@ -0,0 +1,177 @@
1
+ import React from 'react';
2
+ import { describe, it, expect } from 'vitest';
3
+ import { render } from 'ink-testing-library';
4
+ import StreamingIndicator from '../StreamingIndicator.js';
5
+ import { ScreenReaderProvider } from '../screen-reader-context.js';
6
+
7
+ describe('StreamingIndicator', () => {
8
+ it('renders empty when no tools and no text', () => {
9
+ const { lastFrame } = render(<StreamingIndicator text="" activeTools={[]} />);
10
+ expect(lastFrame()).not.toContain('Thinking');
11
+ });
12
+
13
+ it('shows a generic thinking state when foreground work has no text or tools yet', () => {
14
+ const { lastFrame } = render(<StreamingIndicator text="" activeTools={[]} isThinking={true} />);
15
+
16
+ expect(lastFrame()).toContain('Thinking...');
17
+ });
18
+
19
+ it('shows Tools: section with running tool', () => {
20
+ const { lastFrame } = render(
21
+ <StreamingIndicator
22
+ text=""
23
+ activeTools={[{ toolName: 'Bash', firstArg: 'ls -la', isRunning: true }]}
24
+ />,
25
+ );
26
+ const frame = lastFrame()!;
27
+ expect(frame).toContain('Tools:');
28
+ expect(frame).toContain('Bash(ls -la)');
29
+ });
30
+
31
+ it('shows ⟳ for running and ✓ for completed tools', () => {
32
+ const { lastFrame } = render(
33
+ <StreamingIndicator
34
+ text=""
35
+ activeTools={[
36
+ { toolName: 'Read', firstArg: '/src/index.ts', isRunning: false },
37
+ { toolName: 'Bash', firstArg: 'ls', isRunning: true },
38
+ ]}
39
+ />,
40
+ );
41
+ const frame = lastFrame()!;
42
+ expect(frame).toContain('✓ Read(/src/index.ts)');
43
+ expect(frame).toContain('⟳ Bash(ls)');
44
+ });
45
+
46
+ it('shows Assistant: section with streaming text', () => {
47
+ const { lastFrame } = render(<StreamingIndicator text="Hello world" activeTools={[]} />);
48
+ const frame = lastFrame()!;
49
+ expect(frame).toContain('Assistant:');
50
+ expect(frame).toContain('Hello world');
51
+ expect(frame).not.toContain('Tools:');
52
+ });
53
+
54
+ it('preserves CJK and emoji text in streaming output', () => {
55
+ const text = '긴 한국어 응답과 emoji 🎉 를 스트리밍합니다';
56
+ const { lastFrame } = render(<StreamingIndicator text={text} activeTools={[]} />);
57
+
58
+ expect(lastFrame()).toContain(text);
59
+ });
60
+
61
+ it('shows Tools: before Assistant: when both present', () => {
62
+ const { lastFrame } = render(
63
+ <StreamingIndicator
64
+ text="Analyzing..."
65
+ activeTools={[{ toolName: 'Read', firstArg: 'file.ts', isRunning: false }]}
66
+ />,
67
+ );
68
+ const frame = lastFrame()!;
69
+ const toolsIndex = frame.indexOf('Tools:');
70
+ const assistantIndex = frame.indexOf('Assistant:');
71
+ expect(toolsIndex).toBeGreaterThanOrEqual(0);
72
+ expect(assistantIndex).toBeGreaterThanOrEqual(0);
73
+ expect(toolsIndex).toBeLessThan(assistantIndex);
74
+ });
75
+
76
+ it('does not show Thinking... when tools are active', () => {
77
+ const { lastFrame } = render(
78
+ <StreamingIndicator
79
+ text=""
80
+ activeTools={[{ toolName: 'Glob', firstArg: '**/*.md', isRunning: true }]}
81
+ />,
82
+ );
83
+ expect(lastFrame()).not.toContain('Thinking...');
84
+ });
85
+
86
+ it('does not show Thinking... when text is present', () => {
87
+ const { lastFrame } = render(
88
+ <StreamingIndicator text="Some response" activeTools={[]} isThinking={true} />,
89
+ );
90
+ expect(lastFrame()).not.toContain('Thinking...');
91
+ });
92
+
93
+ it('shows multiple tools in order', () => {
94
+ const { lastFrame } = render(
95
+ <StreamingIndicator
96
+ text=""
97
+ activeTools={[
98
+ { toolName: 'Read', firstArg: 'a.ts', isRunning: false },
99
+ { toolName: 'Bash', firstArg: 'echo hi', isRunning: false },
100
+ { toolName: 'Glob', firstArg: '**/*.md', isRunning: true },
101
+ ]}
102
+ />,
103
+ );
104
+ const frame = lastFrame()!;
105
+ const readIdx = frame.indexOf('Read(a.ts)');
106
+ const bashIdx = frame.indexOf('Bash(echo hi)');
107
+ const globIdx = frame.indexOf('Glob(**/*.md)');
108
+ expect(readIdx).toBeLessThan(bashIdx);
109
+ expect(bashIdx).toBeLessThan(globIdx);
110
+ });
111
+
112
+ it('renders tool diffs through markdown diff body format', () => {
113
+ const { lastFrame } = render(
114
+ <StreamingIndicator
115
+ text=""
116
+ activeTools={[
117
+ {
118
+ toolName: 'Edit',
119
+ firstArg: '/src/index.ts',
120
+ isRunning: false,
121
+ result: 'success',
122
+ diffFile: '/src/index.ts',
123
+ diffLines: [
124
+ { type: 'remove', lineNumber: 1, text: 'const oldValue = true;' },
125
+ { type: 'add', lineNumber: 1, text: 'const newValue = true;' },
126
+ ],
127
+ },
128
+ ]}
129
+ />,
130
+ );
131
+
132
+ const frame = lastFrame() ?? '';
133
+ expect(frame).toContain('/src/index.ts');
134
+ expect(frame).toContain('- 1 | const oldValue = true;');
135
+ expect(frame).toContain('+ 1 | const newValue = true;');
136
+ expect(frame).not.toContain('│ 1 - const oldValue = true;');
137
+ });
138
+ });
139
+
140
+ /** CLI-2004 TC-19 — the progress indicator collapses to one static line in the mode. */
141
+ describe('CLI-2004 TC-19: StreamingIndicator in screen-reader mode', () => {
142
+ function renderInMode(element: React.ReactElement): string {
143
+ const { lastFrame, unmount } = render(
144
+ <ScreenReaderProvider enabled={true}>{element}</ScreenReaderProvider>,
145
+ );
146
+ const frame = lastFrame() ?? '';
147
+ unmount();
148
+ return frame;
149
+ }
150
+
151
+ it('renders a single static line for a thinking turn', () => {
152
+ const frame = renderInMode(<StreamingIndicator text="" activeTools={[]} isThinking={true} />);
153
+ expect(frame.split('\n').filter((line) => line.trim().length > 0)).toHaveLength(1);
154
+ expect(frame).toContain('thinking:');
155
+ expect(frame).not.toContain('Thinking...');
156
+ });
157
+
158
+ it('folds running tools and streamed text into that one line', () => {
159
+ const frame = renderInMode(
160
+ <StreamingIndicator
161
+ text="partial answer"
162
+ activeTools={[{ toolName: 'Bash', firstArg: 'ls', isRunning: true }]}
163
+ isThinking={true}
164
+ />,
165
+ );
166
+ expect(frame.split('\n').filter((line) => line.trim().length > 0)).toHaveLength(1);
167
+ expect(frame).toContain('tool:');
168
+ expect(frame).toContain('partial answer');
169
+ });
170
+
171
+ it('leaves the multi-line indicator unchanged outside the mode', () => {
172
+ const { lastFrame } = render(
173
+ <StreamingIndicator text="Hello" activeTools={[]} isThinking={true} />,
174
+ );
175
+ expect(lastFrame()).toContain('Assistant:');
176
+ });
177
+ });
@@ -0,0 +1,153 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ IME_CURSOR_SETTINGS,
4
+ TERMINAL_PROFILES,
5
+ REPRESENTATIVE_IME_PTY_CELLS,
6
+ cellLabel,
7
+ expectedImeCursorEnabled,
8
+ } from './helpers/terminal-profiles.js';
9
+ import {
10
+ isInteractiveColorTerminal,
11
+ supportsImeCursorPositioning,
12
+ } from '../terminal-capabilities.js';
13
+
14
+ const ORIG_TTY = process.stdout.isTTY;
15
+
16
+ function setTty(value: boolean): void {
17
+ Object.defineProperty(process.stdout, 'isTTY', { value, configurable: true });
18
+ }
19
+
20
+ describe('isInteractiveColorTerminal', () => {
21
+ afterEach(() => {
22
+ vi.unstubAllEnvs();
23
+ Object.defineProperty(process.stdout, 'isTTY', { value: ORIG_TTY, configurable: true });
24
+ });
25
+
26
+ it('NO_COLOR present disables even when empty (the SCREEN-008 regression)', () => {
27
+ vi.stubEnv('FORCE_COLOR', undefined);
28
+ setTty(true);
29
+ vi.stubEnv('NO_COLOR', '');
30
+ expect(isInteractiveColorTerminal()).toBe(false);
31
+ vi.stubEnv('NO_COLOR', '1');
32
+ expect(isInteractiveColorTerminal()).toBe(false);
33
+ });
34
+
35
+ it('honors FORCE_COLOR (0 → off, non-zero → on)', () => {
36
+ vi.stubEnv('NO_COLOR', undefined);
37
+ setTty(false);
38
+ vi.stubEnv('FORCE_COLOR', '0');
39
+ expect(isInteractiveColorTerminal()).toBe(false);
40
+ vi.stubEnv('FORCE_COLOR', '1');
41
+ expect(isInteractiveColorTerminal()).toBe(true);
42
+ });
43
+
44
+ it('falls back to stdout.isTTY when no env override', () => {
45
+ vi.stubEnv('NO_COLOR', undefined);
46
+ vi.stubEnv('FORCE_COLOR', undefined);
47
+ setTty(true);
48
+ expect(isInteractiveColorTerminal()).toBe(true);
49
+ setTty(false);
50
+ expect(isInteractiveColorTerminal()).toBe(false);
51
+ });
52
+ });
53
+
54
+ describe('supportsImeCursorPositioning (CLI-062)', () => {
55
+ afterEach(() => {
56
+ vi.unstubAllEnvs();
57
+ Object.defineProperty(process.stdout, 'isTTY', { value: ORIG_TTY, configurable: true });
58
+ });
59
+
60
+ it('on for a plain interactive TTY, off for a non-TTY', () => {
61
+ vi.stubEnv('ROBOTA_IME_CURSOR', undefined);
62
+ vi.stubEnv('TERM_PROGRAM', undefined);
63
+ setTty(true);
64
+ expect(supportsImeCursorPositioning()).toBe(true);
65
+ setTty(false);
66
+ expect(supportsImeCursorPositioning()).toBe(false);
67
+ });
68
+
69
+ it('I5: Apple_Terminal is OFF by default (Korean-IME SIGSEGV is an Apple-side bug)', () => {
70
+ vi.stubEnv('ROBOTA_IME_CURSOR', undefined);
71
+ setTty(true);
72
+ vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
73
+ expect(supportsImeCursorPositioning()).toBe(false);
74
+ });
75
+
76
+ it('I5: ROBOTA_IME_CURSOR=1 opts Apple_Terminal in explicitly', () => {
77
+ setTty(true);
78
+ vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
79
+ vi.stubEnv('ROBOTA_IME_CURSOR', '1');
80
+ expect(supportsImeCursorPositioning({ override: true })).toBe(true);
81
+ });
82
+
83
+ it('ignores an ambient Robota override without a host choice', () => {
84
+ setTty(true);
85
+ vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal');
86
+ vi.stubEnv('ROBOTA_IME_CURSOR', '1');
87
+ expect(supportsImeCursorPositioning()).toBe(false);
88
+ });
89
+
90
+ it('ROBOTA_IME_CURSOR=0 is a kill switch even on a capable terminal', () => {
91
+ setTty(true);
92
+ vi.stubEnv('TERM_PROGRAM', undefined);
93
+ vi.stubEnv('ROBOTA_IME_CURSOR', '0');
94
+ expect(supportsImeCursorPositioning({ override: false })).toBe(false);
95
+ });
96
+ });
97
+
98
+ /**
99
+ * CLI-062 terminal matrix — capability half.
100
+ *
101
+ * Terminal detection is ASSERTED here, never assumed: every emulator in the matrix is applied as
102
+ * the environment handshake it really exports (see helpers/terminal-profiles.ts for how each row's
103
+ * values were obtained), and the gate's verdict is compared against the shared expectation the pty
104
+ * matrix asserts behaviourally. The macOS rows (Terminal.app, iTerm2) cannot run their emulator on
105
+ * a non-macOS machine, but their env branch — including Apple_Terminal's default-off I5 branch —
106
+ * is fully exercised by stubbing the variables those terminals set.
107
+ */
108
+ describe('CLI-062 terminal matrix — supportsImeCursorPositioning per terminal', () => {
109
+ afterEach(() => {
110
+ vi.unstubAllEnvs();
111
+ Object.defineProperty(process.stdout, 'isTTY', { value: ORIG_TTY, configurable: true });
112
+ });
113
+
114
+ for (const profile of TERMINAL_PROFILES) {
115
+ for (const override of IME_CURSOR_SETTINGS) {
116
+ const expected = expectedImeCursorEnabled(profile, override);
117
+ it(`${cellLabel(profile, override)} → ${expected ? 'ENABLED' : 'disabled'}`, () => {
118
+ setTty(true);
119
+ // A real terminal exports only its own variables; anything the matrix does not set must
120
+ // be genuinely absent, or a leaked TERM_PROGRAM would silently decide the verdict.
121
+ for (const key of ['TERM', 'TERM_PROGRAM', 'TERM_PROGRAM_VERSION', 'COLORTERM']) {
122
+ vi.stubEnv(key, undefined);
123
+ }
124
+ for (const [key, value] of Object.entries(profile.env)) vi.stubEnv(key, value);
125
+ vi.stubEnv('ROBOTA_IME_CURSOR', override);
126
+
127
+ expect(
128
+ supportsImeCursorPositioning({
129
+ override: override === '1' ? true : override === '0' ? false : undefined,
130
+ }),
131
+ ).toBe(expected);
132
+ });
133
+ }
134
+ }
135
+
136
+ it('every matrix row keeps its provenance honest (measured rows name a real capture)', () => {
137
+ for (const profile of TERMINAL_PROFILES) {
138
+ expect(profile.note.length, profile.id).toBeGreaterThan(0);
139
+ if (profile.provenance === 'measured') {
140
+ expect(profile.note, profile.id).toMatch(/Captured|pty harness/);
141
+ }
142
+ }
143
+ });
144
+
145
+ it('keeps the process-spawning PTY tier to four boundary representatives', () => {
146
+ expect(REPRESENTATIVE_IME_PTY_CELLS).toEqual([
147
+ { profileId: 'ghostty', override: undefined },
148
+ { profileId: 'apple-terminal', override: undefined },
149
+ { profileId: 'apple-terminal', override: '1' },
150
+ { profileId: 'ghostty', override: '0' },
151
+ ]);
152
+ });
153
+ });