@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,330 @@
1
+ /**
2
+ * SCREEN-006 anti-drift consistency floor — the mechanical floor for the color-token SSOT.
3
+ *
4
+ * Every component must consume the resolved theme (`usePalette()` / `useMotionTokens()`, whose data
5
+ * lives in `src/theme/`) instead of spelling Ink color names or hex values inline. This scan reads
6
+ * every source file in the package (excluding `src/theme/` itself and tests) and fails on any
7
+ * `color="…"` / `borderColor="…"` / `backgroundColor="…"` JSX string literal and on any
8
+ * `#rrggbb` hex literal. Precedent: `key-hint-consistency.test.tsx` (the SCREEN-005 floor).
9
+ *
10
+ * SCREEN-2002 added the two ratchets the SCREEN-006 limit predicted. The deferred case — "a future
11
+ * TS helper returning a bare color-name string" — recurred twice (`getContextColor`, the status
12
+ * glyph's colour), so the floor now also refuses any file outside `src/theme/` that names the
13
+ * built-in theme data or calls a chalk COLOUR function. `chalk.inverse` (a modifier, the
14
+ * drawn cursor) and `chalk.level` (the colour gate) are deliberately untouched: they choose no colour.
15
+ *
16
+ * What the floor still cannot see: a colour-name string returned from a helper that neither imports
17
+ * the theme data nor calls chalk — e.g. a literal `'cyan'` returned by a pure function. The type
18
+ * system covers the tokens it flows into; this limit is recorded rather than claimed away.
19
+ */
20
+ import { readdirSync, readFileSync } from 'node:fs';
21
+ import { join, relative } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ import { describe, expect, it } from 'vitest';
25
+
26
+ const SRC_ROOT = fileURLToPath(new URL('..', import.meta.url));
27
+
28
+ /** `src/theme/` holds the token data and the one chalk boundary; nothing else may. */
29
+ const THEME_DIR = 'theme';
30
+ const EXCLUDED_DIRS = new Set(['__tests__']);
31
+ const SOURCE_EXTENSIONS = ['.ts', '.tsx'];
32
+
33
+ const COLOR_ATTR_LITERAL = /\b(?:color|borderColor|backgroundColor)="[^"]*"/g;
34
+ const HEX_LITERAL = /#[0-9a-fA-F]{6}\b/g;
35
+ /**
36
+ * SCREEN-2002: the built-in colour DATA may be imported only from inside `src/theme/`. The SYMBOL is
37
+ * what the ratchet matches, not the module path — matching the path alone let the `src/theme/index.js`
38
+ * barrel re-export the data straight past the floor, which is exactly the route every migrated
39
+ * component already imports through.
40
+ *
41
+ * Recorded limit (widened by SCREEN-2002 work unit 3): the package now EXPORTS `listBuiltInThemes`,
42
+ * so a caller can obtain a built-in's colour values through the accessor without naming a constant
43
+ * or the data module — the floor does not see that, exactly as it does not see a colour-name string
44
+ * returned by a pure helper. The accessor exists for one composition root, which puts the built-ins
45
+ * in a registry; reading a colour off it is the case this floor cannot catch, and it is written down
46
+ * rather than claimed away.
47
+ *
48
+ * Recorded limit: a bare word matches anywhere, so PROSE naming a theme constant in a comment
49
+ * outside `src/theme/` fails the floor too. That is the strict direction — it fails loudly and is
50
+ * rewritten in a sentence, rather than passing silently — so it is left as is.
51
+ */
52
+ const BUILT_IN_THEME_IMPORT =
53
+ /\b(?:BUILT_IN_THEMES|DARK_THEME|LIGHT_THEME|DARK_DALTONIZED_THEME|LIGHT_DALTONIZED_THEME)\b|from\s+'[^']*built-in-themes\.js'/g;
54
+ /** SCREEN-2002: a chalk COLOUR call outside `src/theme/` decides a colour the theme should own. */
55
+ const CHALK_COLOR_CALL = /\bchalk\.(?!inverse\b|level\b)[A-Za-z]+[.(]/g;
56
+ /** SCREEN-2002: the three inputs `<ThemeProvider>` must be fed from the resolved view model. */
57
+ const THEME_PROVIDER_PROPS = ['theme', 'reducedMotion', 'syntaxHighlighting'] as const;
58
+
59
+ interface IFinding {
60
+ file: string;
61
+ line: number;
62
+ match: string;
63
+ }
64
+
65
+ function collectSourceFiles(dir: string): string[] {
66
+ const files: string[] = [];
67
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
68
+ if (entry.isDirectory()) {
69
+ if (EXCLUDED_DIRS.has(entry.name)) continue;
70
+ files.push(...collectSourceFiles(join(dir, entry.name)));
71
+ continue;
72
+ }
73
+ if (!SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) continue;
74
+ files.push(join(dir, entry.name));
75
+ }
76
+ return files;
77
+ }
78
+
79
+ /**
80
+ * Blank out comment CONTENT before scanning, keeping every newline and every character position so
81
+ * a finding's line number still points at the real line.
82
+ *
83
+ * The floor is about CODE. Matching a bare symbol anywhere was recorded as a known limit when these
84
+ * ratchets were written — "it fails loudly rather than passing silently, so leave it" — and then it
85
+ * failed on a comment that correctly EXPLAINS the rule it was named in (`useMotion()` in a docblock
86
+ * about the motion gate). A guard that fires on correct work is how a guard stops being read, so
87
+ * the limit is closed rather than re-recorded.
88
+ */
89
+ function blankComments(source: string): string {
90
+ const blank = (match: string): string => match.replace(/[^\n]/gu, ' ');
91
+ return source
92
+ .replace(/\/\*[\s\S]*?\*\//gu, blank)
93
+ .replace(
94
+ /(^|[^:])\/\/[^\n]*/gu,
95
+ (match, prefix: string) => prefix + blank(match.slice(prefix.length)),
96
+ );
97
+ }
98
+
99
+ function scanFile(filePath: string, pattern: RegExp): IFinding[] {
100
+ const findings: IFinding[] = [];
101
+ const lines = blankComments(readFileSync(filePath, 'utf8')).split('\n');
102
+ lines.forEach((line, index) => {
103
+ for (const match of line.matchAll(pattern)) {
104
+ findings.push({
105
+ file: relative(SRC_ROOT, filePath),
106
+ line: index + 1,
107
+ match: match[0],
108
+ });
109
+ }
110
+ });
111
+ return findings;
112
+ }
113
+
114
+ function formatFindings(findings: IFinding[]): string {
115
+ return findings.map((f) => ` ${f.file}:${f.line} ${f.match}`).join('\n');
116
+ }
117
+
118
+ describe('SCREEN-006 palette consistency floor', () => {
119
+ const allFiles = collectSourceFiles(SRC_ROOT);
120
+ // `src/theme/` is the one place a colour value or a chalk colour call may appear.
121
+ const sourceFiles = allFiles.filter(
122
+ (file) => !relative(SRC_ROOT, file).split('/').includes(THEME_DIR),
123
+ );
124
+
125
+ it('scans a non-empty source inventory', () => {
126
+ expect(sourceFiles.length).toBeGreaterThan(0);
127
+ });
128
+
129
+ it('has zero color/borderColor/backgroundColor JSX string literals outside the token modules', () => {
130
+ const findings = sourceFiles.flatMap((file) => scanFile(file, COLOR_ATTR_LITERAL));
131
+ expect(
132
+ findings,
133
+ `Inline color attribute literals found — read the theme with usePalette():\n${formatFindings(findings)}`,
134
+ ).toEqual([]);
135
+ });
136
+
137
+ it('has zero #rrggbb hex literals outside the token modules', () => {
138
+ const findings = sourceFiles.flatMap((file) => scanFile(file, HEX_LITERAL));
139
+ expect(
140
+ findings,
141
+ `Inline hex color literals found — put the value in src/theme/built-in-themes.ts:\n${formatFindings(findings)}`,
142
+ ).toEqual([]);
143
+ });
144
+
145
+ it('SCREEN-2002: only src/theme/ imports the built-in theme data', () => {
146
+ const findings = sourceFiles.flatMap((file) => scanFile(file, BUILT_IN_THEME_IMPORT));
147
+ expect(
148
+ findings,
149
+ `The dark theme's values are data, not a palette — read the live theme instead:\n${formatFindings(findings)}`,
150
+ ).toEqual([]);
151
+ });
152
+
153
+ it('SCREEN-2002: only src/theme/ calls a chalk colour', () => {
154
+ const findings = sourceFiles.flatMap((file) => scanFile(file, CHALK_COLOR_CALL));
155
+ expect(
156
+ findings,
157
+ `A chalk colour call decides a colour the theme owns — go through src/theme/:\n${formatFindings(findings)}`,
158
+ ).toEqual([]);
159
+ });
160
+
161
+ /**
162
+ * SCREEN-2002 TC-06 carve-out, made mechanical. The motion gate has exactly ONE consumer, and the
163
+ * carve-outs depend on that: the sleeping-task countdown still advances under reduced motion
164
+ * (it reports a fact, it is not decoration) and `StreamingIndicator`'s collapse follows
165
+ * screen-reader mode alone. A second consumer would quietly widen what the setting turns off.
166
+ */
167
+ it('SCREEN-2002: the motion gate has one consumer', () => {
168
+ const consumers = sourceFiles.filter((file) =>
169
+ /\buseMotion\(\)/u.test(blankComments(readFileSync(file, 'utf8'))),
170
+ );
171
+ expect(consumers.map((file) => relative(SRC_ROOT, file)).sort()).toEqual(['WaveText.tsx']);
172
+ });
173
+
174
+ /**
175
+ * SCREEN-2002: the provider's three inputs must all be WIRED, and every markdown render site must
176
+ * READ the one that is not a colour.
177
+ *
178
+ * `/theme syntax off` shipped in this package's first draft persisting a setting that reached no
179
+ * render site — the command reported success for a no-op, and every context-level test still
180
+ * passed, because the context was fine; it was the one line feeding it that was missing.
181
+ *
182
+ * The first version of this floor was itself half-real: it matched `prop={viewModel.theme.` on the
183
+ * RAW file, so a commented-out provider stayed green and so did moving all three props onto a
184
+ * different element. Both halves below are anchored and comment-blind, and the fixture test at the
185
+ * end proves each one fires.
186
+ */
187
+ const themeProviderElement = (source: string): string =>
188
+ /<ThemeProvider\b[\s\S]*?>/u.exec(blankComments(source))?.[0] ?? '';
189
+
190
+ /**
191
+ * The consumer end, as a PROPERTY rather than a proxy for one.
192
+ *
193
+ * `/theme syntax off` shipped persisting a setting no render site passed, so the command reported
194
+ * success and changed nothing. Four guards were written for it, and each measured something
195
+ * standing in for "every call site passes the setting" — the prop appears in the file, the file
196
+ * matched the regex, the identifier occurs N times. Each held for exactly one review round,
197
+ * because a closer proxy is still a proxy.
198
+ *
199
+ * So the obligation is gone instead: `useRenderMarkdown()` binds the theme, the setting and
200
+ * screen-reader mode, and there is no per-call-site argument left to forget. What remains to
201
+ * check is that nothing goes around it — the single-consumer shape the motion gate already uses
202
+ * below.
203
+ */
204
+ const RENDER_MARKDOWN_OWNER = 'hooks/useRenderMarkdown.ts';
205
+ const RAW_RENDERER_SPECIFIER = /(?:from|import\()\s*'[^']*render-markdown\.js'/u;
206
+ /**
207
+ * `from` AND `import(` — a dynamic import reaches the same function, and a check whose whole job
208
+ * is that there is no way around it must see one. A TYPE-only import is exempt: it reaches no
209
+ * call, and a guard that fires on correct work is how a guard stops being read.
210
+ */
211
+ const importsRawRenderer = (source: string): boolean =>
212
+ source
213
+ .split('\n')
214
+ .some(
215
+ (line) =>
216
+ RAW_RENDERER_SPECIFIER.test(line) && !/^\s*(?:import|export)\s+type\b/u.test(line),
217
+ );
218
+ const RAW_RENDERER_IMPORT = /(?<!import type )(?:from|import\()\s*'[^']*render-markdown\.js'/u;
219
+
220
+ it('SCREEN-2002: AppView feeds the theme provider all three resolved inputs', () => {
221
+ const element = themeProviderElement(readFileSync(join(SRC_ROOT, 'AppView.tsx'), 'utf8'));
222
+
223
+ expect(element, 'AppView must render a <ThemeProvider> — it is the only live one').not.toBe('');
224
+ for (const prop of THEME_PROVIDER_PROPS) {
225
+ expect(
226
+ new RegExp(`${prop}=\\{viewModel\\.theme\\.`, 'u').test(element),
227
+ `<ThemeProvider> must be passed ${prop} — the resolved value reaches components only here`,
228
+ ).toBe(true);
229
+ }
230
+ });
231
+
232
+ it('SCREEN-2002: the markdown renderer has one consumer, which binds the appearance', () => {
233
+ const importers = sourceFiles
234
+ .map((file) => ({
235
+ file: relative(SRC_ROOT, file),
236
+ source: blankComments(readFileSync(file, 'utf8')),
237
+ }))
238
+ .filter(({ file }) => file !== 'render-markdown.ts')
239
+ // `from` AND `import(` — a dynamic import is a way around a check whose whole job is that
240
+ // there is no way around it. A TYPE-only import is exempt: it reaches no call, and a guard
241
+ // that fires on correct work is how a guard stops being read (the reason `blankComments`
242
+ // exists a few lines up).
243
+ .filter(({ source }) => importsRawRenderer(source))
244
+ .map(({ file }) => file)
245
+ .sort();
246
+
247
+ expect(
248
+ importers,
249
+ `Only ${RENDER_MARKDOWN_OWNER} may import the raw renderer — everything else goes through ` +
250
+ `\`useRenderMarkdown()\`, which binds the theme, the syntax-highlighting setting and ` +
251
+ `screen-reader mode. A direct import is a call site that can forget one of them.`,
252
+ ).toEqual([RENDER_MARKDOWN_OWNER]);
253
+ });
254
+
255
+ it('the wiring floors fire on a violating fixture', () => {
256
+ // (a) A commented-out provider is not a provider.
257
+ expect(themeProviderElement('// <ThemeProvider theme={viewModel.theme.resolved}>')).toBe('');
258
+ expect(themeProviderElement('/* <ThemeProvider theme={viewModel.theme.resolved}> */')).toBe('');
259
+ // (b) The props must be on the PROVIDER, not on whatever element happens to follow it.
260
+ const moved = [
261
+ '<ThemeProvider>',
262
+ ' <AppPresentation theme={viewModel.theme.resolved} />',
263
+ '</ThemeProvider>',
264
+ ].join('\n');
265
+ expect(/theme=\{viewModel\.theme\./u.test(themeProviderElement(moved))).toBe(false);
266
+ // (c) The real shape passes.
267
+ const wired = [
268
+ '<ThemeProvider',
269
+ ' theme={viewModel.theme.resolved}',
270
+ ' reducedMotion={viewModel.theme.reducedMotion}',
271
+ ' syntaxHighlighting={viewModel.theme.syntaxHighlighting}',
272
+ '>',
273
+ ].join('\n');
274
+ const element = themeProviderElement(wired);
275
+ for (const prop of THEME_PROVIDER_PROPS) {
276
+ expect(new RegExp(`${prop}=\\{viewModel\\.theme\\.`, 'u').test(element)).toBe(true);
277
+ }
278
+ // (d) The consumer half is an IMPORT check, so it cannot be satisfied by an identifier that
279
+ // merely occurs — a dependency array, a destructure, a sibling component's props.
280
+ const shapes: [string, boolean][] = [
281
+ ["import { renderMarkdown } from './render-markdown.js';", true],
282
+ ["import * as md from './render-markdown.js';", true],
283
+ ["import { renderMarkdown } from '../../render-markdown.js';", true],
284
+ ["export { renderMarkdown } from './render-markdown.js';", true],
285
+ // A dynamic import reaches the same function; the check must see it.
286
+ ["const { renderMarkdown } = await import('./render-markdown.js');", true],
287
+ // A TYPE-only import reaches no call, so flagging it would be a false red.
288
+ ["import type { IRenderMarkdownOptions } from './render-markdown.js';", false],
289
+ ["import { useRenderMarkdown } from './hooks/useRenderMarkdown.js';", false],
290
+ ];
291
+ for (const [shape, flagged] of shapes) {
292
+ expect(importsRawRenderer(shape), shape).toBe(flagged);
293
+ }
294
+ });
295
+
296
+ it('both ratchets actually fire on a violating fixture', () => {
297
+ const violation = [
298
+ "import { DARK_THEME } from './theme/built-in-themes.js';",
299
+ "const label = chalk.cyan('x');",
300
+ ].join('\n');
301
+ // Two matches: the symbol and the path. Either alone is a violation.
302
+ expect([...violation.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(2);
303
+ expect([...violation.matchAll(CHALK_COLOR_CALL)]).toHaveLength(1);
304
+ // The route the path-only ratchet could not see: the barrel re-exporting the same data.
305
+ const throughTheBarrel = "import { DARK_THEME } from './theme/index.js';";
306
+ expect([...throughTheBarrel.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(1);
307
+ // The two deliberate exemptions — and only those two — stay green.
308
+ expect([...'chalk.inverse(c) + (chalk.level = 0)'.matchAll(CHALK_COLOR_CALL)]).toHaveLength(0);
309
+ // `chalk.reset` is NOT among them: it clears whatever style the caller established, which is a
310
+ // rendering decision, not a colour-free modifier.
311
+ expect([...'chalk.reset(row)'.matchAll(CHALK_COLOR_CALL)]).toHaveLength(1);
312
+ // `resolveTheme` is the sanctioned way to ask for a theme one did not receive.
313
+ expect([...'resolveTheme(options.theme)'.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(0);
314
+ });
315
+
316
+ it('reads CODE, not the comments that explain it', () => {
317
+ const explained = [
318
+ '// `useMotion()` gates on the colour gate above this.',
319
+ '/* DARK_THEME is the default; see built-in-themes.ts. */',
320
+ 'const label = value; // chalk.cyan(x) would be a violation here',
321
+ ].join('\n');
322
+ const code = blankComments(explained);
323
+
324
+ expect([...code.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(0);
325
+ expect([...code.matchAll(CHALK_COLOR_CALL)]).toHaveLength(0);
326
+ expect(/\buseMotion\(\)/u.test(code)).toBe(false);
327
+ // Line positions survive, so a finding still points at the line it came from.
328
+ expect(code.split('\n')).toHaveLength(3);
329
+ });
330
+ });
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import PermissionPrompt from '../PermissionPrompt.js';
6
+
7
+ describe('project permission availability in the terminal', () => {
8
+ it('labels the project choice unavailable before selection', () => {
9
+ const frame = render(
10
+ <PermissionPrompt
11
+ request={{
12
+ toolName: 'Bash',
13
+ toolArgs: { command: 'git status' },
14
+ canPersistProjectPermission: false,
15
+ resolve: () => {},
16
+ }}
17
+ />,
18
+ ).lastFrame();
19
+
20
+ expect(frame).toContain('Project-wide approval unavailable');
21
+ expect(frame).not.toContain('always (this project)');
22
+ });
23
+ });
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import PermissionPrompt from '../PermissionPrompt.js';
6
+
7
+ describe('a permission asked for in a peer turn', () => {
8
+ it('names the session whose message the call is serving, and shows the full reply', () => {
9
+ const text = 'The project is called Lumen and its token budget is 4096.';
10
+ const frame = render(
11
+ <PermissionPrompt
12
+ request={{
13
+ toolName: 'peer_reply',
14
+ toolArgs: { text },
15
+ requestedByPeer: 'peer:session-a',
16
+ resolve: () => {},
17
+ }}
18
+ />,
19
+ ).lastFrame();
20
+
21
+ expect(frame).toContain('peer:session-a');
22
+ expect(frame?.replace(/\s+/g, ' ')).toContain(text);
23
+ });
24
+
25
+ it('says nothing about a peer for the operator’s own call', () => {
26
+ const frame = render(
27
+ <PermissionPrompt
28
+ request={{ toolName: 'Bash', toolArgs: { command: 'ls' }, resolve: () => {} }}
29
+ />,
30
+ ).lastFrame();
31
+
32
+ expect(frame).not.toContain('peer');
33
+ });
34
+ });
@@ -0,0 +1,79 @@
1
+ import React from 'react';
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ import { ProductDisplayNameProvider } from '../product-display-name-context.js';
6
+ import { RoleLabel } from '../RoleLabel.js';
7
+ import StreamingIndicator from '../StreamingIndicator.js';
8
+ import TransportTUI from '../TransportTUI.js';
9
+ import { useTerminalTitle } from '../use-terminal-title.js';
10
+
11
+ describe('host-selected terminal display name', () => {
12
+ it('uses the host name for history and streaming labels', () => {
13
+ const history = render(
14
+ <ProductDisplayNameProvider name="Atlas">
15
+ <RoleLabel role="assistant" />
16
+ </ProductDisplayNameProvider>,
17
+ );
18
+ const streaming = render(
19
+ <ProductDisplayNameProvider name="Atlas">
20
+ <StreamingIndicator text="Hello" activeTools={[]} />
21
+ </ProductDisplayNameProvider>,
22
+ );
23
+
24
+ expect(history.lastFrame()).toContain('Atlas:');
25
+ expect(streaming.lastFrame()).toContain('Atlas:');
26
+ expect(history.lastFrame()).not.toContain('Robota:');
27
+ expect(streaming.lastFrame()).not.toContain('Robota:');
28
+ });
29
+
30
+ it('uses a neutral name when the host does not choose one', () => {
31
+ const { lastFrame } = render(<RoleLabel role="assistant" />);
32
+ expect(lastFrame()).toContain('Assistant:');
33
+ });
34
+
35
+ it('renders a projected command using the host-selected prefix', () => {
36
+ const { lastFrame } = render(
37
+ <ProductDisplayNameProvider name="Atlas" modelCommandToolPrefix="robota_command_">
38
+ <StreamingIndicator
39
+ text=""
40
+ activeTools={[{ toolName: 'robota_command_echo', firstArg: '', isRunning: true }]}
41
+ />
42
+ </ProductDisplayNameProvider>,
43
+ );
44
+ expect(lastFrame()).toContain('echo(');
45
+ expect(lastFrame()).not.toContain('robota_command_echo');
46
+ });
47
+
48
+ it('uses the host name in transport restart copy', () => {
49
+ const { lastFrame } = render(
50
+ <ProductDisplayNameProvider name="Atlas">
51
+ <TransportTUI
52
+ registry={{ getAll: () => [], setEnabled: vi.fn(), setOptions: vi.fn() }}
53
+ onClose={vi.fn()}
54
+ />
55
+ </ProductDisplayNameProvider>,
56
+ );
57
+
58
+ expect(lastFrame()).toContain('applies the next time Atlas starts');
59
+ });
60
+
61
+ it('sanitizes the host name before writing the terminal title', () => {
62
+ const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
63
+ function Title(): null {
64
+ useTerminalTitle('session');
65
+ return null;
66
+ }
67
+ try {
68
+ const mounted = render(
69
+ <ProductDisplayNameProvider name={'Atlas\x07\x1b]52;c;evil\x07'}>
70
+ <Title />
71
+ </ProductDisplayNameProvider>,
72
+ );
73
+ expect(write).toHaveBeenCalledWith('\x1b]0;Atlas — session\x07');
74
+ mounted.unmount();
75
+ } finally {
76
+ write.mockRestore();
77
+ }
78
+ });
79
+ });