@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,131 @@
1
+ import { Readable } from 'node:stream';
2
+
3
+ /**
4
+ * SCREEN-1992: the stdin proxy handed to Ink's `render({ stdin })`.
5
+ *
6
+ * Ink tokenizes `ESC [ I` as a keypress and every `useInput` listener would receive `[I`, so the
7
+ * composer would type it. The proxy sits in front of the real stdin, strips the two focus sequences
8
+ * while mode 1004 is negotiated, reports focus and keystrokes, and forwards every other byte
9
+ * unchanged — including a split escape sequence and a bracketed paste, which are Ink's to parse.
10
+ */
11
+ const ESC = 0x1b;
12
+ const FOCUS_IN = '\x1b[I';
13
+ const FOCUS_OUT = '\x1b[O';
14
+
15
+ export interface IFocusInputFilterResult {
16
+ /** The bytes Ink receives, byte-identical to the input apart from the stripped focus sequences. */
17
+ readonly forwarded: Buffer;
18
+ readonly focusIn: number;
19
+ readonly focusOut: number;
20
+ }
21
+
22
+ /** Pure: split one chunk into focus events and the bytes that remain. */
23
+ export function filterFocusSequences(chunk: Buffer, negotiated: boolean): IFocusInputFilterResult {
24
+ if (!negotiated || !chunk.includes(ESC)) return { forwarded: chunk, focusIn: 0, focusOut: 0 };
25
+ const text = chunk.toString('latin1');
26
+ let focusIn = 0;
27
+ let focusOut = 0;
28
+ let forwarded = '';
29
+ let index = 0;
30
+ while (index < text.length) {
31
+ if (text.startsWith(FOCUS_IN, index)) {
32
+ focusIn += 1;
33
+ index += FOCUS_IN.length;
34
+ } else if (text.startsWith(FOCUS_OUT, index)) {
35
+ focusOut += 1;
36
+ index += FOCUS_OUT.length;
37
+ } else {
38
+ forwarded += text[index];
39
+ index += 1;
40
+ }
41
+ }
42
+ return { forwarded: Buffer.from(forwarded, 'latin1'), focusIn, focusOut };
43
+ }
44
+
45
+ export interface IFocusReportingStdinHooks {
46
+ /** Whether mode 1004 is currently negotiated; read per chunk so a handoff can flip it. */
47
+ readonly negotiated: () => boolean;
48
+ readonly onFocusIn: () => void;
49
+ readonly onFocusOut: () => void;
50
+ /** Any byte that was not a focus sequence. */
51
+ readonly onKeystroke: () => void;
52
+ }
53
+
54
+ type TStdinDataListener = (chunk: Buffer | string) => void;
55
+
56
+ /** What ink 7.1.1 reads from the stream it is given (`App.js` input handling, `ink.js` kitty probe). */
57
+ type TInkStdinSurface = Pick<
58
+ Readable,
59
+ 'read' | 'setEncoding' | 'unshift' | 'on' | 'addListener' | 'removeListener'
60
+ > & {
61
+ readonly isTTY: boolean;
62
+ // `tty.ReadStream` types these as returning itself; Ink discards the return value.
63
+ setRawMode(mode: boolean): TInkStdinSurface;
64
+ ref(): TInkStdinSurface;
65
+ unref(): TInkStdinSurface;
66
+ };
67
+
68
+ /** The subset of the real stdin the proxy touches (what `process.stdin` provides). */
69
+ export interface IFocusReportingStdinSource {
70
+ readonly isTTY?: boolean;
71
+ on(event: 'data', listener: TStdinDataListener): void;
72
+ off(event: 'data', listener: TStdinDataListener): void;
73
+ setRawMode?: (mode: boolean) => void;
74
+ ref?: () => void;
75
+ unref?: () => void;
76
+ }
77
+
78
+ export class FocusReportingStdin extends Readable {
79
+ private readonly onData = (data: Buffer | string): void => {
80
+ const chunk = typeof data === 'string' ? Buffer.from(data) : data;
81
+ const result = filterFocusSequences(chunk, this.hooks.negotiated());
82
+ for (let i = 0; i < result.focusOut; i += 1) this.hooks.onFocusOut();
83
+ for (let i = 0; i < result.focusIn; i += 1) this.hooks.onFocusIn();
84
+ if (result.forwarded.length === 0) return;
85
+ this.hooks.onKeystroke();
86
+ this.push(result.forwarded);
87
+ };
88
+
89
+ constructor(
90
+ private readonly source: IFocusReportingStdinSource,
91
+ private readonly hooks: IFocusReportingStdinHooks,
92
+ ) {
93
+ super({ read() {} });
94
+ source.on('data', this.onData);
95
+ }
96
+
97
+ get isTTY(): boolean {
98
+ return this.source.isTTY === true;
99
+ }
100
+
101
+ setRawMode(mode: boolean): this {
102
+ this.source.setRawMode?.(mode);
103
+ return this;
104
+ }
105
+
106
+ ref(): this {
107
+ this.source.ref?.();
108
+ return this;
109
+ }
110
+
111
+ unref(): this {
112
+ this.source.unref?.();
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * Ink declares `stdin?: NodeJS.ReadStream` (a `tty.ReadStream`, which cannot be constructed
118
+ * without an fd) but touches only the members named in `TInkStdinSurface` — verified against ink
119
+ * 7.1.1's App and Ink classes. Assigning `this` to that surface is a typed check that every one of
120
+ * them exists here; the final narrowing is from the surface Ink reads to the type it declares.
121
+ */
122
+ asInkStdin(): NodeJS.ReadStream {
123
+ const surface: TInkStdinSurface = this;
124
+ return surface as NodeJS.ReadStream;
125
+ }
126
+
127
+ /** Detach from the underlying stdin; the proxy emits nothing afterwards. */
128
+ detach(): void {
129
+ this.source.off('data', this.onData);
130
+ }
131
+ }
@@ -0,0 +1,148 @@
1
+ import { MINUTES_PER_HOUR, MS_PER_MINUTE } from './time-units.js';
2
+
3
+ import type { TExecutionNormalizedState } from '@robota-sdk/agent-interface-execution';
4
+
5
+ /**
6
+ * SCREEN-1992: one line for what happened while the user was away.
7
+ *
8
+ * Counts only while unattended; `onReturned` renders the interval and resets it. Nothing here
9
+ * inspects the session — the coordinator feeds it the channel events it already receives.
10
+ */
11
+ export const RECAP_MAX_WIDTH = 120;
12
+
13
+ /** The framework's `TTurnSource` plus whatever a future source adds; only `agent-wakeup` is special here. */
14
+ type TTurnSource = string;
15
+
16
+ const TERMINAL_STATES: ReadonlySet<TExecutionNormalizedState> = new Set([
17
+ 'completed',
18
+ 'failed',
19
+ 'stopped',
20
+ ]);
21
+
22
+ export class IntervalRecap {
23
+ private lostAt: number | undefined;
24
+ private lastTurnSource: TTurnSource = 'user';
25
+ private turnsFinished = 0;
26
+ private wakeTurnsFinished = 0;
27
+ private needsInputCount = 0;
28
+ private errorCount = 0;
29
+ /** Every entry's latest state, attended or not — the baseline an interval starts from. */
30
+ private readonly currentStates = new Map<string, TExecutionNormalizedState>();
31
+ /** Entries that were not terminal when attention was lost and are terminal now. */
32
+ private readonly reachedTerminal = new Map<string, TExecutionNormalizedState>();
33
+ private baseline = new Map<string, TExecutionNormalizedState>();
34
+ private baselinePending = false;
35
+
36
+ get away(): boolean {
37
+ return this.lostAt !== undefined;
38
+ }
39
+
40
+ onLost(atIso: string): void {
41
+ this.lostAt = Date.parse(atIso);
42
+ this.reset();
43
+ this.baseline = new Map(this.currentStates);
44
+ // Lost before any snapshot arrived (a channel wired while the user is already away): the first
45
+ // snapshot is the baseline, so a resumed session's restored terminal tasks are not "news".
46
+ this.baselinePending = this.currentStates.size === 0;
47
+ }
48
+
49
+ /** The recap line for the interval just ended, or undefined when there is nothing to say. */
50
+ onReturned(atIso: string): string | undefined {
51
+ if (this.lostAt === undefined) return undefined;
52
+ const elapsedMs = Math.max(0, Date.parse(atIso) - this.lostAt);
53
+ this.lostAt = undefined;
54
+ const parts = this.parts();
55
+ this.reset();
56
+ if (parts.length === 0) return undefined;
57
+ return bound(`While away ${formatElapsed(elapsedMs)}: ${parts.join(' · ')}`);
58
+ }
59
+
60
+ turnSource(source: TTurnSource): void {
61
+ this.lastTurnSource = source;
62
+ }
63
+
64
+ turnCompleted(): void {
65
+ if (!this.away) return;
66
+ this.turnsFinished += 1;
67
+ if (this.lastTurnSource === 'agent-wakeup') this.wakeTurnsFinished += 1;
68
+ }
69
+
70
+ needsInput(): void {
71
+ if (this.away) this.needsInputCount += 1;
72
+ }
73
+
74
+ error(): void {
75
+ if (this.away) this.errorCount += 1;
76
+ }
77
+
78
+ /**
79
+ * A workspace entry's current state. Only an entry that REACHED a terminal state during the
80
+ * interval is counted: one already terminal when attention was lost is old news, and one that
81
+ * left the terminal state again (a resumed schedule) is dropped.
82
+ */
83
+ entryState(entryId: string, state: TExecutionNormalizedState): void {
84
+ this.currentStates.set(entryId, state);
85
+ if (!this.away) return;
86
+ if (this.baselinePending) {
87
+ this.baseline.set(entryId, state);
88
+ return;
89
+ }
90
+ const before = this.baseline.get(entryId);
91
+ const wasTerminal = before !== undefined && TERMINAL_STATES.has(before);
92
+ if (!TERMINAL_STATES.has(state) || wasTerminal) {
93
+ this.reachedTerminal.delete(entryId);
94
+ return;
95
+ }
96
+ this.reachedTerminal.set(entryId, state);
97
+ }
98
+
99
+ /** The end of one snapshot: a pending baseline is now complete. */
100
+ snapshotComplete(): void {
101
+ this.baselinePending = false;
102
+ }
103
+
104
+ private parts(): string[] {
105
+ const parts: string[] = [];
106
+ if (this.turnsFinished > 0) {
107
+ const wake = this.wakeTurnsFinished > 0 ? ` (${this.wakeTurnsFinished} wake)` : '';
108
+ parts.push(`${this.turnsFinished} ${plural(this.turnsFinished, 'turn')} finished${wake}`);
109
+ }
110
+ if (this.needsInputCount > 0) parts.push(`${this.needsInputCount} needs input`);
111
+ const byState = { completed: 0, failed: 0, stopped: 0 };
112
+ for (const state of this.reachedTerminal.values()) {
113
+ if (state in byState) byState[state as keyof typeof byState] += 1;
114
+ }
115
+ for (const state of ['completed', 'failed', 'stopped'] as const) {
116
+ if (byState[state] > 0) parts.push(`${byState[state]} ${state}`);
117
+ }
118
+ if (this.errorCount > 0) {
119
+ parts.push(`${this.errorCount} ${plural(this.errorCount, 'error')}`);
120
+ }
121
+ return parts;
122
+ }
123
+
124
+ private reset(): void {
125
+ this.turnsFinished = 0;
126
+ this.wakeTurnsFinished = 0;
127
+ this.needsInputCount = 0;
128
+ this.errorCount = 0;
129
+ this.reachedTerminal.clear();
130
+ }
131
+ }
132
+
133
+ function plural(count: number, noun: string): string {
134
+ return count === 1 ? noun : `${noun}s`;
135
+ }
136
+
137
+ function formatElapsed(elapsedMs: number): string {
138
+ const totalMinutes = Math.floor(elapsedMs / MS_PER_MINUTE);
139
+ if (totalMinutes < 1) return '<1m';
140
+ const hours = Math.floor(totalMinutes / MINUTES_PER_HOUR);
141
+ const minutes = totalMinutes % MINUTES_PER_HOUR;
142
+ if (hours === 0) return `${minutes}m`;
143
+ return minutes === 0 ? `${hours}h` : `${hours}h ${minutes}m`;
144
+ }
145
+
146
+ function bound(line: string): string {
147
+ return line.length <= RECAP_MAX_WIDTH ? line : `${line.slice(0, RECAP_MAX_WIDTH - 1)}…`;
148
+ }
@@ -0,0 +1,6 @@
1
+ /** The units the attention modules convert between; named so no arithmetic carries a bare literal. */
2
+ export const MS_PER_SECOND = 1000;
3
+ export const SECONDS_PER_MINUTE = 60;
4
+ export const MINUTES_PER_HOUR = 60;
5
+ export const MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE;
6
+ export const SECONDS_PER_HOUR = SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * CLI-2004 — the audible attention signal.
3
+ *
4
+ * Three triggers, all of them "the session now needs you and you cannot see that it does": a reply
5
+ * finished, a prompt or dialog wants an answer, and a tool that ran long enough that you had stopped
6
+ * watching has finished.
7
+ *
8
+ * The long-tool threshold is a NAMED CONSTANT rather than a copied literal. It comes from a single
9
+ * reference, so it is the kind of number that has to be tunable and attributable — not folded into
10
+ * an inline `> 5000`.
11
+ *
12
+ * Written outside Ink for the same reason as `terminal-marks.ts`: BEL is terminal framing, not
13
+ * rendered content.
14
+ */
15
+
16
+ /** A tool that took longer than this rings on completion. Single-reference; tunable by design. */
17
+ export const LONG_TOOL_BELL_MS = 5000;
18
+
19
+ /** The bell byte. */
20
+ export const BELL = '\x07';
21
+
22
+ export interface IAttentionBellOptions {
23
+ /** Screen-reader mode. Off ⇒ nothing is ever written. */
24
+ enabled: boolean;
25
+ /** Injected sink; defaults to the process's own stdout. */
26
+ write?: (text: string) => void;
27
+ /** Injected clock so the threshold is testable without real time. */
28
+ now?: () => number;
29
+ /** Override the long-tool threshold. */
30
+ longToolBellMs?: number;
31
+ }
32
+
33
+ export interface IAttentionBell {
34
+ /** The assistant finished a reply. */
35
+ replyCompleted(): void;
36
+ /** A prompt or dialog mounted and is waiting for an answer. */
37
+ promptMounted(): void;
38
+ /** Record a tool's start so its duration can be judged at completion. */
39
+ toolStarted(toolId: string): void;
40
+ /** A tool finished — rings only if its recorded start is older than the threshold. */
41
+ toolCompleted(toolId: string): void;
42
+ }
43
+
44
+ /**
45
+ * Build the bell. A tool whose start was never recorded never rings: the signal is "the thing you
46
+ * stopped waiting for is done", and without a start there is no duration to judge, so silence is the
47
+ * correct answer rather than a guessed one.
48
+ */
49
+ export function createAttentionBell(options: IAttentionBellOptions): IAttentionBell {
50
+ const write =
51
+ options.write ??
52
+ ((text: string): void => {
53
+ process.stdout.write(text);
54
+ });
55
+ const now = options.now ?? ((): number => Date.now());
56
+ const threshold = options.longToolBellMs ?? LONG_TOOL_BELL_MS;
57
+ const startedAt = new Map<string, number>();
58
+
59
+ const ring = (): void => {
60
+ if (!options.enabled) return;
61
+ write(BELL);
62
+ };
63
+
64
+ return {
65
+ replyCompleted: ring,
66
+ promptMounted: ring,
67
+ toolStarted(toolId: string): void {
68
+ if (!options.enabled) return;
69
+ startedAt.set(toolId, now());
70
+ },
71
+ toolCompleted(toolId: string): void {
72
+ const started = startedAt.get(toolId);
73
+ startedAt.delete(toolId);
74
+ if (started === undefined) return;
75
+ if (now() - started <= threshold) return;
76
+ ring();
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,30 @@
1
+ import { createUserMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
2
+
3
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
4
+
5
+ /** The part of a session this needs: who is driving the turn now, if it can say. */
6
+ export interface IActiveDriverSource {
7
+ getActiveDriverId?: () => string | null;
8
+ }
9
+
10
+ /**
11
+ * PEER-007 (issue #1915): the transcript echo of a user message, attributed to the ACTIVE turn's
12
+ * driver so a peer's message is labelled as theirs the moment it appears rather than reading as the
13
+ * operator's own for the whole turn. The id is the framework's DERIVED one (`peer:<session-id>`),
14
+ * never peer-supplied text.
15
+ *
16
+ * The lookup is guarded because attribution is an ADDITION to the message, never a precondition for
17
+ * it: a session that cannot say who drove the turn must still get the message on screen. Unguarded,
18
+ * a throw here loses the message entirely and the transcript shows that nothing happened.
19
+ */
20
+ export function attributedUserEcho(content: string, session: IActiveDriverSource): IHistoryEntry {
21
+ let driverId: string | null = null;
22
+ try {
23
+ driverId = session.getActiveDriverId?.() ?? null;
24
+ } catch {
25
+ driverId = null;
26
+ }
27
+ return messageToHistoryEntry(
28
+ createUserMessage(content, driverId ? { metadata: { driverId } } : {}),
29
+ );
30
+ }
@@ -0,0 +1,99 @@
1
+ import { formatCountdown } from './attention/countdown.js';
2
+ import { formatExecutionWorkspaceEntryRow } from './execution-workspace-view-model.js';
3
+ import { STATUS_SYMBOL, workspaceStatusKind } from './status-glyph.js';
4
+
5
+ import type { TUiStatusKind } from './status-glyph.js';
6
+ import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
7
+
8
+ /**
9
+ * The row's leading connector. CLI-2004 adds the plain form: `├`/`└` draw a tree a sighted reader
10
+ * follows at a glance and a screen reader announces as two more characters per row.
11
+ */
12
+ export const BACKGROUND_TASK_CONNECTORS = { branch: '├', last: '└', plain: '-' } as const;
13
+
14
+ export type TBackgroundTaskConnector =
15
+ (typeof BACKGROUND_TASK_CONNECTORS)[keyof typeof BACKGROUND_TASK_CONNECTORS];
16
+
17
+ export interface IBackgroundTaskRow {
18
+ connector: TBackgroundTaskConnector;
19
+ /** Shared status glyph (⟳ running, ✓ done, ✗ failed, …) — see status-glyph.ts. */
20
+ marker: string;
21
+ /** SCREEN-1992: the five-word state beside the glyph — the word a reader hears, not a colour. */
22
+ state: string;
23
+ /** SCREEN-2002: the status KIND; the component resolves its colour from the live theme. */
24
+ statusKind: TUiStatusKind;
25
+ label: string;
26
+ segments: string[];
27
+ preview?: string;
28
+ /** SCREEN-1992: the entry's headline when it says more than the preview already does. */
29
+ headline?: string;
30
+ /** SCREEN-1992: `in 59s` while a schedule sleeps — rendered against `options.now`. */
31
+ countdown?: string;
32
+ accessibleText: string;
33
+ }
34
+
35
+ export interface IBackgroundTaskRowOptions {
36
+ isLast?: boolean;
37
+ /** Screen-reader mode ⇒ the plain connector. */
38
+ screenReader?: boolean;
39
+ /** The clock the countdown is rendered against; the panel's tick advances it. */
40
+ now?: Date;
41
+ }
42
+
43
+ /** The headline earns its place only when the row does not already say it (preview or subtitle). */
44
+ function resolveHeadline(
45
+ entry: IExecutionWorkspaceEntry,
46
+ preview: string | undefined,
47
+ subtitle: string | undefined,
48
+ ): string | undefined {
49
+ const text = entry.headline?.text;
50
+ if (text === undefined || text.length === 0 || text === preview) return undefined;
51
+ if (subtitle !== undefined && subtitle.includes(text)) return undefined;
52
+ return entry.headline?.kind === 'question' ? `? ${text}` : text;
53
+ }
54
+
55
+ function resolveConnector(options: IBackgroundTaskRowOptions): TBackgroundTaskConnector {
56
+ if (options.screenReader === true) return BACKGROUND_TASK_CONNECTORS.plain;
57
+ return options.isLast === false
58
+ ? BACKGROUND_TASK_CONNECTORS.branch
59
+ : BACKGROUND_TASK_CONNECTORS.last;
60
+ }
61
+
62
+ export function formatBackgroundTaskRow(
63
+ entry: IExecutionWorkspaceEntry,
64
+ options: IBackgroundTaskRowOptions = {},
65
+ ): IBackgroundTaskRow {
66
+ const row = formatExecutionWorkspaceEntryRow(entry);
67
+ // Symbol from the same (status, attention) classification that drives row.statusKind,
68
+ // so the marker's glyph and colour always agree (SCREEN-007).
69
+ const marker = STATUS_SYMBOL[workspaceStatusKind(entry.status, entry.attention)];
70
+ const segments = [row.statusLabel, row.subtitle].filter(
71
+ (segment): segment is string => typeof segment === 'string' && segment.length > 0,
72
+ );
73
+ const connector = resolveConnector(options);
74
+ const headline = resolveHeadline(entry, row.preview, row.subtitle);
75
+ const countdown =
76
+ entry.nextFireAt === undefined
77
+ ? undefined
78
+ : formatCountdown(entry.nextFireAt, options.now ?? new Date());
79
+ return {
80
+ connector,
81
+ marker,
82
+ state: entry.state,
83
+ statusKind: row.statusKind,
84
+ label: row.title,
85
+ segments,
86
+ preview: row.preview,
87
+ headline,
88
+ countdown,
89
+ accessibleText: [
90
+ `${connector} ${marker} ${entry.state} ${row.title}`,
91
+ ...segments,
92
+ headline,
93
+ row.preview,
94
+ countdown,
95
+ ]
96
+ .filter((part): part is string => typeof part === 'string' && part.length > 0)
97
+ .join(' · '),
98
+ };
99
+ }
@@ -0,0 +1,8 @@
1
+ export type {
2
+ TOnMissingArgsAction,
3
+ ITuiPickerItem,
4
+ ITuiCommandInteraction,
5
+ ITuiPickerInteraction,
6
+ ITuiConfirmInteraction,
7
+ TAnyTuiCommandInteraction,
8
+ } from '@robota-sdk/agent-interface-tui';
@@ -0,0 +1,129 @@
1
+ import { sanitizeTerminalText } from './sanitize-terminal-text.js';
2
+
3
+ import type { TUniversalValue } from '@robota-sdk/agent-core';
4
+
5
+ const MAX_PREVIEW_LINES = 4;
6
+ const SUCCESS_EXIT_CODE = 0;
7
+ const COMMAND_TOOL_NAMES = new Set(['Shell', 'Bash', 'BackgroundProcess']);
8
+
9
+ export interface ICommandOutputInput {
10
+ toolName: string;
11
+ firstArg?: string;
12
+ isRunning?: boolean;
13
+ result?: string;
14
+ toolResultData?: string;
15
+ }
16
+
17
+ export interface ICommandOutputSummary {
18
+ status: 'success' | 'error';
19
+ statusLabel: string;
20
+ previewLines: string[];
21
+ omittedLineCount: number;
22
+ transcriptHint?: string;
23
+ }
24
+
25
+ export function formatCommandOutputSummary(
26
+ tool: ICommandOutputInput,
27
+ ): ICommandOutputSummary | undefined {
28
+ if (!COMMAND_TOOL_NAMES.has(tool.toolName) || !tool.toolResultData) return undefined;
29
+
30
+ const parsed = parseToolResultData(tool.toolResultData);
31
+ const exitCode = getNumberValue(parsed, 'exitCode');
32
+ const successValue = getBooleanValue(parsed, 'success');
33
+ const output = buildOutputText(tool.toolResultData, parsed);
34
+ const lines = trimTrailingBlankLines(splitOutputLines(output));
35
+ // SEC-019: these lines ARE the command's stdout and stderr — the most directly attacker-chosen
36
+ // text in the whole surface. Sanitized where the summary is BUILT rather than where it is
37
+ // rendered, because this function exists to produce display text and a second render site would
38
+ // otherwise have to remember. `statusLabel` and `transcriptHint` are built from numbers and need
39
+ // nothing.
40
+ const previewLines = lines.slice(0, MAX_PREVIEW_LINES).map(sanitizeTerminalText);
41
+ const omittedLineCount = Math.max(0, lines.length - previewLines.length);
42
+ const isFailed =
43
+ tool.result === 'error' ||
44
+ successValue === false ||
45
+ (exitCode !== undefined && exitCode !== SUCCESS_EXIT_CODE);
46
+
47
+ return {
48
+ status: isFailed ? 'error' : 'success',
49
+ statusLabel: formatStatusLabel(isFailed, exitCode),
50
+ previewLines,
51
+ omittedLineCount,
52
+ transcriptHint:
53
+ omittedLineCount > 0
54
+ ? `... +${omittedLineCount} lines (full output in session transcript)`
55
+ : undefined,
56
+ };
57
+ }
58
+
59
+ function parseToolResultData(value: string): TUniversalValue {
60
+ try {
61
+ return JSON.parse(value) as TUniversalValue;
62
+ } catch {
63
+ return value;
64
+ }
65
+ }
66
+
67
+ function buildOutputText(raw: string, parsed: TUniversalValue): string {
68
+ if (!isUniversalObject(parsed)) return raw;
69
+
70
+ const output = getStringValue(parsed, 'output');
71
+ if (output !== undefined) return output;
72
+
73
+ const stdout = getStringValue(parsed, 'stdout');
74
+ const stderr = getStringValue(parsed, 'stderr');
75
+ const error = getStringValue(parsed, 'error');
76
+ const lines: string[] = [];
77
+ if (stdout) lines.push(stdout);
78
+ if (stderr) lines.push(prefixLines(stderr, '[stderr] '));
79
+ if (!stdout && !stderr && error) lines.push(error);
80
+ return lines.join('\n');
81
+ }
82
+
83
+ function formatStatusLabel(isFailed: boolean, exitCode: number | undefined): string {
84
+ if (exitCode !== undefined && exitCode !== SUCCESS_EXIT_CODE) return `exit ${exitCode}`;
85
+ return isFailed ? 'error' : 'ok';
86
+ }
87
+
88
+ function splitOutputLines(output: string): string[] {
89
+ if (!output) return [];
90
+ return output.replace(/\r\n/g, '\n').split('\n');
91
+ }
92
+
93
+ function trimTrailingBlankLines(lines: string[]): string[] {
94
+ let end = lines.length;
95
+ while (end > 0 && lines[end - 1]!.trim().length === 0) {
96
+ end -= 1;
97
+ }
98
+ return lines.slice(0, end);
99
+ }
100
+
101
+ function prefixLines(value: string, prefix: string): string {
102
+ return splitOutputLines(value)
103
+ .map((line) => `${prefix}${line}`)
104
+ .join('\n');
105
+ }
106
+
107
+ function isUniversalObject(value: TUniversalValue): value is Record<string, TUniversalValue> {
108
+ return (
109
+ typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)
110
+ );
111
+ }
112
+
113
+ function getStringValue(source: TUniversalValue, key: string): string | undefined {
114
+ if (!isUniversalObject(source)) return undefined;
115
+ const value = source[key];
116
+ return typeof value === 'string' ? value : undefined;
117
+ }
118
+
119
+ function getNumberValue(source: TUniversalValue, key: string): number | undefined {
120
+ if (!isUniversalObject(source)) return undefined;
121
+ const value = source[key];
122
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
123
+ }
124
+
125
+ function getBooleanValue(source: TUniversalValue, key: string): boolean | undefined {
126
+ if (!isUniversalObject(source)) return undefined;
127
+ const value = source[key];
128
+ return typeof value === 'boolean' ? value : undefined;
129
+ }
@@ -0,0 +1,45 @@
1
+ import { findProviderDefinition } from '@robota-sdk/agent-core';
2
+ import {
3
+ applyActiveModelChange,
4
+ createNodeHostSettingsStore,
5
+ readSettings,
6
+ resolveGitBranchFromNodeHost,
7
+ } from '@robota-sdk/agent-framework';
8
+
9
+ import type { ITuiCliAdapter } from './tui-cli-adapter.js';
10
+ import type { IProviderDefinition } from '@robota-sdk/agent-core';
11
+ import type { CommandRegistry } from '@robota-sdk/agent-framework';
12
+ import type { TSettingsSource } from '@robota-sdk/agent-framework';
13
+
14
+ export interface IDefaultTuiCliAdapterOptions {
15
+ providerDefinitions: readonly IProviderDefinition[];
16
+ reloadPluginCommandSource: (registry: CommandRegistry) => void;
17
+ userSettingsPath: string;
18
+ settingsSources: readonly TSettingsSource[];
19
+ formatResumeCommand?: (sessionId: string) => string;
20
+ }
21
+
22
+ export function createDefaultTuiCliAdapter({
23
+ providerDefinitions,
24
+ reloadPluginCommandSource,
25
+ userSettingsPath,
26
+ settingsSources,
27
+ formatResumeCommand,
28
+ }: IDefaultTuiCliAdapterOptions): ITuiCliAdapter {
29
+ return {
30
+ getUserSettingsPath: () => userSettingsPath,
31
+ readSettings: (path) => readSettings(path),
32
+ reloadPluginCommandSource: (registry) => {
33
+ reloadPluginCommandSource(registry);
34
+ },
35
+ applyActiveModelChange: (_cwd, modelId, options) => {
36
+ const userStore = createNodeHostSettingsStore('user', userSettingsPath);
37
+ applyActiveModelChange(settingsSources, [userStore], modelId, options);
38
+ return { applied: true };
39
+ },
40
+ getGitBranch: (cwd) => resolveGitBranchFromNodeHost(cwd),
41
+ getProviderDisplayName: (type) =>
42
+ findProviderDefinition(providerDefinitions, type)?.displayName ?? type,
43
+ ...(formatResumeCommand === undefined ? {} : { formatResumeCommand }),
44
+ };
45
+ }