@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,328 @@
1
+ /**
2
+ * SCREEN-2670 — the pre-write park: an owned write path between Ink and the terminal.
3
+ *
4
+ * A diff-based screen reader computes what to speak from successive terminal snapshots. In
5
+ * screen-reader mode Ink runs UNTHROTTLED (`ink.js:193`), so commits land back to back and a reader
6
+ * that samples between two of them sees a torn or already-overwritten region. The park separates
7
+ * commits IN TIME so a snapshot can fall between them. It does not move the cursor: every frame Ink
8
+ * writes already begins with `eraseLines`, which ends in `cursorLeft`, so column zero is where the
9
+ * frame starts anyway (CLI-062 invariant I3 — no cursor sequence is written here).
10
+ *
11
+ * THE UNIT IS A COMMIT, NOT A CHUNK. One commit in the mode is up to four `write()` calls in one
12
+ * synchronous run (`ink.js:371-412`): synchronized-output begin, `erase + staticOutput`, the frame,
13
+ * synchronized-output end. Parking each would pay the interval three times and leave the erased
14
+ * region on screen between the erase and the frame — the torn state the park exists to prevent. So
15
+ * the chunks of one synchronous run form ONE batch (closed on `process.nextTick`), parked once in
16
+ * front and released contiguously.
17
+ *
18
+ * WHAT IS NEVER PARKED:
19
+ * - the first batch of a session (there is no earlier commit to separate it from);
20
+ * - a batch with no printable content — Ink's empty-string barrier (`ink.js:588`, `:647`), an OSC
21
+ * 133 mark, a bare control sequence — which still queues BEHIND anything pending so order holds;
22
+ * - a batch stamped with the echo flag: the composer's own key handler arms it for text-mutating
23
+ * keys only, it is read when the batch OPENS and carried on it (so a keystroke queued behind a
24
+ * parked reply keeps its exemption), and it expires on `setImmediate` whether or not a batch took
25
+ * it, so a keystroke that commits nothing cannot un-park a later transcript batch.
26
+ *
27
+ * NOTHING IS EVER DROPPED. Ink keeps its own erase bookkeeping (`lastOutputHeight`) from the frame
28
+ * it believes it wrote, and writes each `<Static>` item exactly once — so a batch that never reaches
29
+ * the terminal corrupts the next erase and loses a transcript line for good. When a newer printable
30
+ * batch closes while an older one is still parked, the OLDER one is released at once, contiguously
31
+ * and without its park, so at most one batch ever waits and memory stays bounded on a fast stream;
32
+ * the park then applies to the newest commit only.
33
+ */
34
+
35
+ import { sanitizeTerminalText } from './sanitize-terminal-text.js';
36
+
37
+ import type { IScreenReaderPacingPort } from './screen-reader-pacing-context.js';
38
+
39
+ /** Ink's own fallbacks when the stream reports no size. */
40
+ const DEFAULT_COLUMNS = 80;
41
+ const DEFAULT_ROWS = 24;
42
+
43
+ type TWriteCallback = (error?: Error | null) => void;
44
+
45
+ /**
46
+ * What Ink 7.1.1 touches on the stream it is given (`ink.js`, `log-update.js`): the write, the
47
+ * dimensions, the TTY flag, the resize listener pair, and the writable-state probes.
48
+ */
49
+ export type TParkedStdoutSource = Pick<
50
+ NodeJS.WriteStream,
51
+ | 'write'
52
+ | 'columns'
53
+ | 'rows'
54
+ | 'isTTY'
55
+ | 'destroyed'
56
+ | 'writable'
57
+ | 'writableEnded'
58
+ | 'writableLength'
59
+ | 'writableNeedDrain'
60
+ | 'on'
61
+ | 'off'
62
+ >;
63
+
64
+ export interface ICreateParkedStdoutOptions {
65
+ stdout: TParkedStdoutSource;
66
+ /** Milliseconds between the previous printable release and the next parked one. */
67
+ preparkMs: number;
68
+ /** Injected clock for tests. Defaults to `Date.now`. */
69
+ now?: () => number;
70
+ }
71
+
72
+ interface IChunk {
73
+ text: string;
74
+ callback: TWriteCallback | undefined;
75
+ }
76
+
77
+ interface IBatch {
78
+ chunks: IChunk[];
79
+ /** Read at formation, never at release. */
80
+ echo: boolean;
81
+ printable: boolean;
82
+ /** Ink's `write('', cb)` — the exit barrier, which must reach the stream in order. */
83
+ barrier: boolean;
84
+ }
85
+
86
+ export interface IParkedStdout {
87
+ /** The stream to hand Ink as `stdout`. One stable object: Ink keys its instance map by it. */
88
+ asInkStdout(): NodeJS.WriteStream;
89
+ /** The composer's key handler arms this for a text-mutating key. Expires on `setImmediate`. */
90
+ armEchoRelease(): void;
91
+ /** Write through the same ordered path — for the OSC 133 marks, which are positional. */
92
+ write(chunk: string | Uint8Array, callback?: TWriteCallback): boolean;
93
+ /**
94
+ * Release everything queued NOW, in order and without waiting out a park, and keep parking what
95
+ * comes later — for a terminal handoff, so nothing lands on top of the child's output.
96
+ */
97
+ drain(): Promise<void>;
98
+ /** Release everything still queued, in order and without further parking; resolves when written. */
99
+ flush(): Promise<void>;
100
+ }
101
+
102
+ /** The members Ink reads from `options.stdout`, narrowed as `TInkStdinSurface` narrows stdin. */
103
+ type TInkStdoutSurface = TParkedStdoutSource;
104
+
105
+ function toText(chunk: string | Uint8Array): string {
106
+ return typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
107
+ }
108
+
109
+ /** Printable = something a reader could speak: visible text after every escape is stripped. */
110
+ function isPrintable(text: string): boolean {
111
+ return sanitizeTerminalText(text).trim().length > 0;
112
+ }
113
+
114
+ class ParkedStdout implements TInkStdoutSurface {
115
+ private readonly source: TParkedStdoutSource;
116
+ private readonly preparkMs: number;
117
+ private readonly now: () => number;
118
+ private open: IBatch | undefined;
119
+ private readonly queue: IBatch[] = [];
120
+ private timer: NodeJS.Timeout | undefined;
121
+ private lastPrintableReleaseAt: number | undefined;
122
+ private echoArmed = false;
123
+ private echoExpiryScheduled = false;
124
+ private flushing = false;
125
+ private drainWaiters: Array<() => void> = [];
126
+
127
+ constructor(options: ICreateParkedStdoutOptions) {
128
+ this.source = options.stdout;
129
+ this.preparkMs = options.preparkMs;
130
+ this.now = options.now ?? Date.now;
131
+ this.on = ((...args: Parameters<NodeJS.WriteStream['on']>) =>
132
+ this.source.on(...args)) as NodeJS.WriteStream['on'];
133
+ this.off = ((...args: Parameters<NodeJS.WriteStream['off']>) =>
134
+ this.source.off(...args)) as NodeJS.WriteStream['off'];
135
+ }
136
+
137
+ // ── the surface Ink reads ──────────────────────────────────────────────────────────────────
138
+
139
+ get columns(): number {
140
+ return this.source.columns ?? DEFAULT_COLUMNS;
141
+ }
142
+
143
+ get rows(): number {
144
+ return this.source.rows ?? DEFAULT_ROWS;
145
+ }
146
+
147
+ get isTTY(): boolean {
148
+ return this.source.isTTY ?? false;
149
+ }
150
+
151
+ get destroyed(): boolean {
152
+ return this.source.destroyed ?? false;
153
+ }
154
+
155
+ get writable(): boolean {
156
+ return this.source.writable ?? true;
157
+ }
158
+
159
+ get writableEnded(): boolean {
160
+ return this.source.writableEnded ?? false;
161
+ }
162
+
163
+ get writableLength(): number {
164
+ return this.source.writableLength ?? 0;
165
+ }
166
+
167
+ get writableNeedDrain(): boolean {
168
+ return this.source.writableNeedDrain ?? false;
169
+ }
170
+
171
+ // Ink registers and removes its `resize` listener through these and discards the return value.
172
+ readonly on: NodeJS.WriteStream['on'];
173
+ readonly off: NodeJS.WriteStream['off'];
174
+
175
+ /**
176
+ * The write is deferred, so the boolean is the source's CURRENT backpressure state — what the
177
+ * same call would have returned a moment ago. Ink does not read it; a caller that does is told
178
+ * the truth about the stream it is ultimately writing to.
179
+ */
180
+ write(
181
+ chunk: string | Uint8Array,
182
+ encodingOrCallback?: BufferEncoding | TWriteCallback,
183
+ maybeCallback?: TWriteCallback,
184
+ ): boolean {
185
+ const callback = typeof encodingOrCallback === 'function' ? encodingOrCallback : maybeCallback;
186
+ const text = toText(chunk);
187
+ let batch = this.open;
188
+ if (batch === undefined) {
189
+ const opened: IBatch = {
190
+ chunks: [],
191
+ echo: this.echoArmed,
192
+ printable: false,
193
+ barrier: false,
194
+ };
195
+ batch = opened;
196
+ this.open = opened;
197
+ // One synchronous run = one batch. nextTick drains before the check phase, so a batch opened
198
+ // in the turn that armed the echo flag always closes before the flag's setImmediate expiry.
199
+ process.nextTick(() => this.close(opened));
200
+ }
201
+ batch.chunks.push({ text, callback });
202
+ if (text.length === 0) batch.barrier = true;
203
+ else if (!batch.printable && isPrintable(text)) batch.printable = true;
204
+ return !this.writableNeedDrain;
205
+ }
206
+
207
+ // ── the owned-writer surface ──────────────────────────────────────────────────────────────
208
+
209
+ asInkStdout(): NodeJS.WriteStream {
210
+ const surface: TInkStdoutSurface = this;
211
+ return surface as NodeJS.WriteStream;
212
+ }
213
+
214
+ armEchoRelease(): void {
215
+ this.echoArmed = true;
216
+ if (this.echoExpiryScheduled) return;
217
+ this.echoExpiryScheduled = true;
218
+ setImmediate(() => {
219
+ this.echoArmed = false;
220
+ this.echoExpiryScheduled = false;
221
+ });
222
+ }
223
+
224
+ drain(): Promise<void> {
225
+ if (this.timer !== undefined) {
226
+ clearTimeout(this.timer);
227
+ this.timer = undefined;
228
+ }
229
+ for (const waiting of this.queue.splice(0)) this.release(waiting);
230
+ return this.settled();
231
+ }
232
+
233
+ flush(): Promise<void> {
234
+ this.flushing = true;
235
+ return this.drain();
236
+ }
237
+
238
+ /** Resolves once no batch is open or queued. */
239
+ private settled(): Promise<void> {
240
+ if (this.open === undefined && this.queue.length === 0) return Promise.resolve();
241
+ return new Promise<void>((resolve) => {
242
+ this.drainWaiters.push(resolve);
243
+ });
244
+ }
245
+
246
+ // ── batching and release ──────────────────────────────────────────────────────────────────
247
+
248
+ private close(batch: IBatch): void {
249
+ if (this.open !== batch) return;
250
+ this.open = undefined;
251
+ // A newer printable commit closing behind a parked one: release the older one NOW rather than
252
+ // drop it — Ink's erase bookkeeping and its once-only Static writes both assume it landed.
253
+ if (this.isParked(batch)) {
254
+ if (this.timer !== undefined) {
255
+ clearTimeout(this.timer);
256
+ this.timer = undefined;
257
+ }
258
+ for (const waiting of this.queue.splice(0)) this.release(waiting);
259
+ }
260
+ this.queue.push(batch);
261
+ this.pump();
262
+ }
263
+
264
+ /** The first printable release of a session is never parked: nothing precedes it to separate. */
265
+ private isParked(batch: IBatch): boolean {
266
+ return (
267
+ this.preparkMs > 0 &&
268
+ this.lastPrintableReleaseAt !== undefined &&
269
+ batch.printable &&
270
+ !batch.echo &&
271
+ !this.flushing
272
+ );
273
+ }
274
+
275
+ private pump(): void {
276
+ // Always recompute from the head: a superseded head leaves a stale timer behind, and the batch
277
+ // now at the head may be a control-only one that must not wait on it.
278
+ if (this.timer !== undefined) {
279
+ clearTimeout(this.timer);
280
+ this.timer = undefined;
281
+ }
282
+ while (this.queue.length > 0) {
283
+ const head = this.queue[0] as IBatch;
284
+ if (this.isParked(head)) {
285
+ const due = (this.lastPrintableReleaseAt ?? 0) + this.preparkMs;
286
+ const wait = due - this.now();
287
+ if (wait > 0) {
288
+ // Kept referenced: this timer guards queued output and Ink's exit barrier, whose callback
289
+ // resolves `waitUntilExit()`; the process must not exit with a frame still parked.
290
+ this.timer = setTimeout(() => {
291
+ this.timer = undefined;
292
+ this.pump();
293
+ }, wait);
294
+ return;
295
+ }
296
+ }
297
+ this.queue.shift();
298
+ this.release(head);
299
+ }
300
+ if (this.open === undefined) {
301
+ const waiters = this.drainWaiters;
302
+ this.drainWaiters = [];
303
+ for (const resolve of waiters) resolve();
304
+ }
305
+ }
306
+
307
+ private release(batch: IBatch): void {
308
+ for (const chunk of batch.chunks) {
309
+ this.source.write(chunk.text, chunk.callback);
310
+ }
311
+ if (batch.printable) this.lastPrintableReleaseAt = this.now();
312
+ }
313
+ }
314
+
315
+ /** Build the owned write path. Constructed only when the mode is on and `preparkMs > 0`. */
316
+ export function createParkedStdout(options: ICreateParkedStdoutOptions): IParkedStdout {
317
+ return new ParkedStdout(options);
318
+ }
319
+
320
+ /** The port the render tree publishes: arming and the positional write, bound to one proxy. */
321
+ export function toPacingPort(parked: IParkedStdout): IScreenReaderPacingPort {
322
+ return {
323
+ armEchoRelease: () => parked.armEchoRelease(),
324
+ write: (text) => {
325
+ parked.write(text);
326
+ },
327
+ };
328
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Interrupt-signal policy for the interactive TUI (CLI-075 / RUNTIME-33).
3
+ *
4
+ * The first Ctrl+C or termination signal runs the graceful shutdown (persist session, cancel
5
+ * background work, drain queues). A second interrupt arriving while a shutdown is already in flight
6
+ * is a force-quit: the user is telling us the graceful path is wedged, so we exit immediately rather
7
+ * than no-op. Exit code 130 = 128 + SIGINT(2), the conventional "terminated by Ctrl+C" code.
8
+ */
9
+
10
+ /** Conventional "terminated by SIGINT" exit code (128 + 2). */
11
+ export const FORCE_QUIT_EXIT_CODE = 130;
12
+
13
+ export interface IHandleInterruptParams {
14
+ /** Whether a graceful shutdown is already in progress. */
15
+ isShuttingDown: boolean;
16
+ /** Runs the graceful shutdown (only on the first interrupt). */
17
+ graceful: () => void;
18
+ /** Force-quits the process (defaults to `process.exit`); injectable for tests. */
19
+ forceExit?: (code: number) => void;
20
+ }
21
+
22
+ /**
23
+ * Route an interrupt (Ctrl+C / SIGINT / SIGTERM) to either the graceful path or a force-quit.
24
+ * Keeping this pure and side-effect-injectable makes the second-signal contract unit-testable
25
+ * without spawning a process.
26
+ */
27
+ export function handleInterrupt(params: IHandleInterruptParams): void {
28
+ const forceExit = params.forceExit ?? ((code: number): void => process.exit(code));
29
+ if (params.isShuttingDown) {
30
+ forceExit(FORCE_QUIT_EXIT_CODE);
31
+ return;
32
+ }
33
+ params.graceful();
34
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SCREEN-2002: this module is pure, so it names a colour TOKEN, not a colour. `StatusBar` resolves
3
+ * the token against the live theme — a pure function that returned a chalk name could not follow one.
4
+ */
5
+ export type TStatusActivityKind = 'tools' | 'thinking' | 'background' | 'queued' | 'idle';
6
+
7
+ export interface IStatusActivityInput {
8
+ isThinking: boolean;
9
+ activeToolCount: number;
10
+ activeBackgroundTaskCount: number;
11
+ hasPendingPrompt: boolean;
12
+ }
13
+
14
+ /** The `IThemeColors['text']` key a status activity is rendered in. */
15
+ export type TStatusActivityTone = 'accent' | 'warning' | 'muted';
16
+
17
+ export interface IStatusActivity {
18
+ kind: TStatusActivityKind;
19
+ label: string;
20
+ tone: TStatusActivityTone;
21
+ segments: string[];
22
+ text: string;
23
+ }
24
+
25
+ const NO_ACTIVE_ITEMS = 0;
26
+
27
+ export function formatStatusActivity(input: IStatusActivityInput): IStatusActivity {
28
+ const base = getPrimaryActivity(input);
29
+ const segments = input.hasPendingPrompt && base.kind !== 'queued' ? ['queued'] : [];
30
+ const text = [base.label, ...segments].join(' · ');
31
+ return { ...base, segments, text };
32
+ }
33
+
34
+ function getPrimaryActivity(
35
+ input: IStatusActivityInput,
36
+ ): Omit<IStatusActivity, 'segments' | 'text'> {
37
+ if (input.activeToolCount > NO_ACTIVE_ITEMS) {
38
+ return {
39
+ kind: 'tools',
40
+ label: `Tools (${input.activeToolCount})`,
41
+ tone: 'accent',
42
+ };
43
+ }
44
+ if (input.isThinking) {
45
+ return {
46
+ kind: 'thinking',
47
+ label: 'Thinking',
48
+ tone: 'warning',
49
+ };
50
+ }
51
+ if (input.activeBackgroundTaskCount > NO_ACTIVE_ITEMS) {
52
+ return {
53
+ kind: 'background',
54
+ label: `Background (${input.activeBackgroundTaskCount})`,
55
+ tone: 'accent',
56
+ };
57
+ }
58
+ if (input.hasPendingPrompt) {
59
+ return {
60
+ kind: 'queued',
61
+ label: 'Queued',
62
+ tone: 'warning',
63
+ };
64
+ }
65
+ return {
66
+ kind: 'idle',
67
+ label: 'Idle',
68
+ tone: 'muted',
69
+ };
70
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Single source of truth for status symbol + color across the TUI.
3
+ *
4
+ * Before SCREEN-005 the same conceptual status (e.g. "running") was rendered
5
+ * three different ways — icons (⟳/✓/✗) in the streaming indicator, markers
6
+ * (□/■) in the background-task panel, and color-only in the workspace detail
7
+ * pane. This module gives every component one semantic status vocabulary and
8
+ * one glyph map, so a "running" task looks the same everywhere and every status
9
+ * pairs a SYMBOL with a color (never color alone — required for no-color
10
+ * terminals and colorblind users).
11
+ */
12
+
13
+ import type { IThemeColors } from './theme/index.js';
14
+ import type {
15
+ TExecutionAttention,
16
+ TExecutionWorkspaceStatus,
17
+ } from '@robota-sdk/agent-interface-execution';
18
+ import type { IToolState } from '@robota-sdk/agent-interface-session';
19
+
20
+ export type TUiStatusKind =
21
+ 'running' | 'success' | 'error' | 'denied' | 'waiting' | 'cancelled' | 'idle';
22
+
23
+ /**
24
+ * Canonical status → SYMBOL map. Used by every status-rendering component.
25
+ *
26
+ * SCREEN-2002 split the colour out: a symbol is the same in every theme, a colour is not, and a
27
+ * module-level constant cannot follow a theme resolved at render time. The symbol stays here (this
28
+ * module remains the SSOT for status kinds and their glyphs); the colour comes from the live theme
29
+ * through {@link statusGlyphColor}, which every call site reaches with `usePalette()`, so the pairing
30
+ * survives — no caller gets one without the other.
31
+ */
32
+ export const STATUS_SYMBOL: Record<TUiStatusKind, string> = {
33
+ running: '⟳',
34
+ success: '✓',
35
+ error: '✗',
36
+ denied: '⊘',
37
+ waiting: '◴',
38
+ cancelled: '⊗',
39
+ idle: '·',
40
+ };
41
+
42
+ /** The live theme's colour for a status kind. Always rendered beside {@link STATUS_SYMBOL}. */
43
+ export function statusGlyphColor(colors: IThemeColors, kind: TUiStatusKind): string {
44
+ return colors.status[kind];
45
+ }
46
+
47
+ /** Map a tool-execution state to a semantic status kind. */
48
+ export function toolStateStatusKind(tool: IToolState): TUiStatusKind {
49
+ if (tool.isRunning) return 'running';
50
+ if (tool.result === 'error') return 'error';
51
+ if (tool.result === 'denied') return 'denied';
52
+ return 'success';
53
+ }
54
+
55
+ const ACTIVE_WORKSPACE_STATUSES: readonly TExecutionWorkspaceStatus[] = [
56
+ 'active',
57
+ 'queued',
58
+ 'running',
59
+ 'sleeping',
60
+ ];
61
+
62
+ /**
63
+ * Map an execution-workspace entry's status (+ optional attention) to a semantic
64
+ * status kind. `attention` takes precedence so an entry flagged failed/permission
65
+ * classifies correctly even when its raw status hasn't caught up — this mirrors the
66
+ * precedence the workspace color logic uses, so colour + glyph come from one rule.
67
+ */
68
+ export function workspaceStatusKind(
69
+ status: TExecutionWorkspaceStatus,
70
+ attention?: TExecutionAttention,
71
+ ): TUiStatusKind {
72
+ if (attention === 'failed' || status === 'failed') return 'error';
73
+ if (attention === 'permission' || status === 'waiting_permission') return 'waiting';
74
+ if (status === 'completed') return 'success';
75
+ if (status === 'cancelled') return 'cancelled';
76
+ if (ACTIVE_WORKSPACE_STATUSES.includes(status)) return 'running';
77
+ return 'idle';
78
+ }
@@ -0,0 +1,27 @@
1
+ import React, { createContext, useContext } from 'react';
2
+
3
+ /** Host-selected overrides; absent values leave terminal detection in charge. */
4
+ export interface ITerminalCapabilityOverrides {
5
+ readonly imeCursorPositioning?: boolean | undefined;
6
+ readonly turnMarks?: boolean | undefined;
7
+ }
8
+
9
+ const TerminalCapabilitiesContext = createContext<ITerminalCapabilityOverrides>({});
10
+
11
+ export function TerminalCapabilitiesProvider({
12
+ overrides,
13
+ children,
14
+ }: {
15
+ overrides: ITerminalCapabilityOverrides | undefined;
16
+ children: React.ReactNode;
17
+ }): React.ReactElement {
18
+ return (
19
+ <TerminalCapabilitiesContext.Provider value={overrides ?? {}}>
20
+ {children}
21
+ </TerminalCapabilitiesContext.Provider>
22
+ );
23
+ }
24
+
25
+ export function useTerminalCapabilityOverrides(): ITerminalCapabilityOverrides {
26
+ return useContext(TerminalCapabilitiesContext);
27
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Single source of truth for "should this process emit color + motion to the
3
+ * terminal?" — used by both the markdown renderer (color) and WaveText (motion),
4
+ * so the two never disagree (SCREEN-008).
5
+ *
6
+ * Precedence:
7
+ * - `NO_COLOR` present (any value, including empty) → off (per the NO_COLOR convention).
8
+ * - `FORCE_COLOR=0` → off; `FORCE_COLOR=<non-empty, non-0>` → on.
9
+ * - otherwise → on only when stdout is an interactive TTY.
10
+ */
11
+ export function isInteractiveColorTerminal(): boolean {
12
+ if ('NO_COLOR' in process.env) return false;
13
+ const force = process.env.FORCE_COLOR;
14
+ if (force === '0') return false;
15
+ if (force !== undefined && force !== '') return true;
16
+ return Boolean(process.stdout.isTTY);
17
+ }
18
+
19
+ /**
20
+ * CLI-062 — may this process position the REAL terminal cursor for IME composition?
21
+ *
22
+ * Precedence:
23
+ * - Host override true → on (explicit opt-in for Terminal.app); false → off.
24
+ * - `TERM_PROGRAM=Apple_Terminal` → off by default (invariant I5): the historical Korean-IME
25
+ * SIGSEGV is an Apple-side `attributedSubstringFromRange:` bug; its trigger geometry is gone
26
+ * (measured y + post-<Static> tiny frame), but Terminal.app stays opt-in until the manual
27
+ * matrix passes on real hardware. The CLI-052 startup warning remains in place.
28
+ * - otherwise → on only when stdout is an interactive TTY (ink writes no cursor sequences at
29
+ * all on a non-TTY, so this is defense in depth).
30
+ */
31
+ export function supportsImeCursorPositioning(
32
+ options: { override?: boolean | undefined } = {},
33
+ ): boolean {
34
+ if (options.override === true) return true;
35
+ if (options.override === false) return false;
36
+ if (process.env.TERM_PROGRAM === 'Apple_Terminal') return false; // I5
37
+ return Boolean(process.stdout.isTTY);
38
+ }
39
+
40
+ /**
41
+ * CLI-2004 — may this process write OSC 133 shell-integration marks at agent-turn boundaries?
42
+ *
43
+ * The marks are otherwise harmless: every emulator surveyed discards an unknown OSC sequence, so
44
+ * this gate carries DOCUMENTED negatives, not a capability probe. Robota's support table (the TUI
45
+ * package's docs/SPEC.md) is the user-facing half of the same list.
46
+ *
47
+ * Precedence:
48
+ * - Host override true → on (opt back in on a terminal listed below); false → off.
49
+ * - `TERM_PROGRAM=WezTerm` → off: WezTerm treats OSC 133 as shell-owned and a non-shell emitter
50
+ * corrupts its own prompt tracking, so the marks are withheld rather than emitted uselessly.
51
+ * - otherwise → on only when stdout is an interactive TTY. Nothing consumes the marks in a pipe,
52
+ * and they would land in a captured transcript as noise.
53
+ */
54
+ export function supportsTurnMarks(options: { override?: boolean | undefined } = {}): boolean {
55
+ if (options.override === true) return true;
56
+ if (options.override === false) return false;
57
+ if (process.env.TERM_PROGRAM === 'WezTerm') return false;
58
+ return Boolean(process.stdout.isTTY);
59
+ }
60
+
61
+ /**
62
+ * SCREEN-1992 — may this process ask the terminal to report focus changes (DECSET 1004)?
63
+ *
64
+ * The mode is harmless where unimplemented (the request is discarded and no `CSI I`/`CSI O` ever
65
+ * arrives), so the gate is the interactive-TTY check plus an override the product shell injects from
66
+ * its own environment — this package reads no product-named literal for it.
67
+ *
68
+ * Precedence:
69
+ * - `override === true` → on; `override === false` → off (kill switch).
70
+ * - otherwise → on only when BOTH stdin and stdout are interactive TTYs: the sequences arrive on
71
+ * stdin, and nothing sends them in a pipe.
72
+ */
73
+ export function supportsFocusReporting(options: { override?: boolean | undefined } = {}): boolean {
74
+ if (options.override === true) return true;
75
+ if (options.override === false) return false;
76
+ return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
77
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SCREEN-1992 — DECSET/DECRST 1004: ask the terminal to report focus changes (`CSI I` / `CSI O`).
3
+ *
4
+ * Written OUTSIDE Ink, following the `terminal-marks.ts` carve-out: the two sequences are constants,
5
+ * so nothing needs sanitizing, and no component may emit an escape sequence. A terminal that does not
6
+ * implement the mode discards the request; `supportsFocusReporting()` (terminal-capabilities.ts)
7
+ * carries the gate, and `negotiated` is what the stdin filter and the attention tracker consult.
8
+ */
9
+ const FOCUS_REPORTING_ENABLE = '\x1b[?1004h';
10
+ const FOCUS_REPORTING_DISABLE = '\x1b[?1004l';
11
+
12
+ export interface IFocusReportingWriterOptions {
13
+ /** The capability gate, read at enable time. */
14
+ supported: () => boolean;
15
+ /** Injected sink; defaults to the process's own stdout. */
16
+ write?: (text: string) => void;
17
+ }
18
+
19
+ export interface IFocusReportingWriter {
20
+ /** True between a successful `enable()` and the next `disable()`. */
21
+ readonly negotiated: boolean;
22
+ enable(): void;
23
+ disable(): void;
24
+ }
25
+
26
+ export function createFocusReportingWriter(
27
+ options: IFocusReportingWriterOptions,
28
+ ): IFocusReportingWriter {
29
+ const write =
30
+ options.write ??
31
+ ((text: string): void => {
32
+ process.stdout.write(text);
33
+ });
34
+ let negotiated = false;
35
+ return {
36
+ get negotiated(): boolean {
37
+ return negotiated;
38
+ },
39
+ enable(): void {
40
+ if (negotiated || !options.supported()) return;
41
+ negotiated = true;
42
+ write(FOCUS_REPORTING_ENABLE);
43
+ },
44
+ disable(): void {
45
+ if (!negotiated) return;
46
+ negotiated = false;
47
+ write(FOCUS_REPORTING_DISABLE);
48
+ },
49
+ };
50
+ }