@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,163 @@
1
+ import type { IPendingPermissionRequest } from './types.js';
2
+ import type { IActionRequest, TActionResponse, TToolArgs } from '@robota-sdk/agent-core';
3
+ import type { TPermissionResultValue } from '@robota-sdk/agent-interface-session';
4
+
5
+ interface IUserActionQueueEntry {
6
+ presentedRequest: IActionRequest;
7
+ resolve: (response: TActionResponse) => void;
8
+ id?: string;
9
+ }
10
+
11
+ interface IPermissionQueueEntry {
12
+ toolName: string;
13
+ toolArgs: TToolArgs;
14
+ canPersistProjectPermission?: boolean;
15
+ requestedByPeer?: string;
16
+ resolve: (result: TPermissionResultValue) => void;
17
+ id?: string;
18
+ }
19
+
20
+ /** Serializes action prompts and guarantees every pending promise is settled during teardown. */
21
+ export class TuiUserActionQueue {
22
+ private entries: IUserActionQueueEntry[] = [];
23
+ private processing = false;
24
+
25
+ constructor(private readonly onChange: () => void) {}
26
+
27
+ get current(): IActionRequest | null {
28
+ return this.processing ? (this.entries[0]?.presentedRequest ?? null) : null;
29
+ }
30
+
31
+ enqueue(request: IActionRequest, id?: string): Promise<TActionResponse> {
32
+ return new Promise<TActionResponse>((resolve) => {
33
+ this.entries.push({
34
+ presentedRequest: { ...request },
35
+ resolve,
36
+ ...(id !== undefined ? { id } : {}),
37
+ });
38
+ this.processNext();
39
+ });
40
+ }
41
+
42
+ resolveCurrent(expectedRequest: IActionRequest, response: TActionResponse): void {
43
+ if (this.entries[0]?.presentedRequest !== expectedRequest) return;
44
+ const pending = this.entries.shift();
45
+ if (!pending) return;
46
+ this.processing = false;
47
+ this.onChange();
48
+ pending.resolve(response);
49
+ this.processNext();
50
+ }
51
+
52
+ dismissById(id: string): boolean {
53
+ const dismissed = this.entries.filter((entry) => entry.id === id);
54
+ if (dismissed.length === 0) return false;
55
+ this.entries = this.entries.filter((entry) => entry.id !== id);
56
+ this.processing = false;
57
+ for (const entry of dismissed) entry.resolve({ type: 'cancelled' });
58
+ this.processNext();
59
+ return true;
60
+ }
61
+
62
+ cancelAll(): void {
63
+ const queued = this.entries;
64
+ this.entries = [];
65
+ this.processing = false;
66
+ for (const pending of queued) pending.resolve({ type: 'cancelled' });
67
+ this.onChange();
68
+ }
69
+
70
+ private processNext(): void {
71
+ if (this.processing) return;
72
+ if (!this.entries[0]) {
73
+ this.onChange();
74
+ return;
75
+ }
76
+ this.processing = true;
77
+ this.onChange();
78
+ }
79
+ }
80
+
81
+ /** Serializes permission prompts and denies every pending request during teardown. */
82
+ export class TuiPermissionQueue {
83
+ private entries: IPermissionQueueEntry[] = [];
84
+ private processing = false;
85
+ private currentRequest: IPendingPermissionRequest | null = null;
86
+
87
+ constructor(private readonly onChange: () => void) {}
88
+
89
+ get current(): IPendingPermissionRequest | null {
90
+ return this.currentRequest;
91
+ }
92
+
93
+ enqueue(
94
+ toolName: string,
95
+ toolArgs: TToolArgs,
96
+ id?: string,
97
+ canPersistProjectPermission?: boolean,
98
+ requestedByPeer?: string,
99
+ ): Promise<TPermissionResultValue> {
100
+ return new Promise<TPermissionResultValue>((resolve) => {
101
+ this.entries.push({
102
+ toolName,
103
+ toolArgs,
104
+ resolve,
105
+ ...(id !== undefined ? { id } : {}),
106
+ ...(canPersistProjectPermission !== undefined ? { canPersistProjectPermission } : {}),
107
+ ...(requestedByPeer !== undefined ? { requestedByPeer } : {}),
108
+ });
109
+ this.processNext();
110
+ });
111
+ }
112
+
113
+ dismissById(id: string): boolean {
114
+ const dismissed = this.entries.filter((entry) => entry.id === id);
115
+ if (dismissed.length === 0) return false;
116
+ const dismissedCurrent = this.entries[0]?.id === id;
117
+ this.entries = this.entries.filter((entry) => entry.id !== id);
118
+ for (const entry of dismissed) entry.resolve(false);
119
+ if (dismissedCurrent) {
120
+ this.processing = false;
121
+ this.currentRequest = null;
122
+ this.processNext();
123
+ }
124
+ return true;
125
+ }
126
+
127
+ cancelAll(): void {
128
+ const queued = this.entries;
129
+ this.entries = [];
130
+ this.processing = false;
131
+ this.currentRequest = null;
132
+ for (const pending of queued) pending.resolve(false);
133
+ this.onChange();
134
+ }
135
+
136
+ private processNext(): void {
137
+ if (this.processing) return;
138
+ const next = this.entries[0];
139
+ if (!next) {
140
+ this.currentRequest = null;
141
+ this.onChange();
142
+ return;
143
+ }
144
+ this.processing = true;
145
+ this.currentRequest = {
146
+ toolName: next.toolName,
147
+ toolArgs: next.toolArgs,
148
+ ...(next.canPersistProjectPermission !== undefined
149
+ ? { canPersistProjectPermission: next.canPersistProjectPermission }
150
+ : {}),
151
+ ...(next.requestedByPeer !== undefined ? { requestedByPeer: next.requestedByPeer } : {}),
152
+ resolve: (result) => {
153
+ if (this.entries[0] !== next) return;
154
+ this.entries.shift();
155
+ this.processing = false;
156
+ this.currentRequest = null;
157
+ next.resolve(result);
158
+ setTimeout(() => this.processNext(), 0);
159
+ },
160
+ };
161
+ this.onChange();
162
+ }
163
+ }
@@ -0,0 +1,52 @@
1
+ import type { TuiStateManager } from './tui-state-manager.js';
2
+ import type {
3
+ IInteractiveSession,
4
+ IInteractiveSessionEvents,
5
+ TInteractiveEventName,
6
+ } from '@robota-sdk/agent-interface-session';
7
+
8
+ export interface ITuiSessionEventBinding {
9
+ event: TInteractiveEventName;
10
+ handler: (...args: never[]) => void;
11
+ }
12
+
13
+ export interface ITuiSessionBinder {
14
+ <E extends TInteractiveEventName>(event: E, handler: IInteractiveSessionEvents[E]): void;
15
+ }
16
+
17
+ export function bindTuiSessionNoticeEvents(
18
+ bind: ITuiSessionBinder,
19
+ manager: TuiStateManager,
20
+ ): void {
21
+ bind('plan_event', (payload) => manager.addSessionEventNotice({ event: 'plan_event', payload }));
22
+ bind('context_file_refreshed', (payload) =>
23
+ manager.addSessionEventNotice({ event: 'context_file_refreshed', payload }),
24
+ );
25
+ bind('branch_event', (payload) =>
26
+ manager.addSessionEventNotice({ event: 'branch_event', payload }),
27
+ );
28
+ }
29
+
30
+ /** Isolate a TUI-owned projection failure from the session operation that already committed. */
31
+ export function bindTuiSessionEvent<E extends TInteractiveEventName>(
32
+ session: Pick<IInteractiveSession, 'on'>,
33
+ event: E,
34
+ handler: IInteractiveSessionEvents[E],
35
+ onDeliveryError: (error: Error, event: E) => void,
36
+ bindings: ITuiSessionEventBinding[],
37
+ ): void {
38
+ const ownedHandler = ((...args: never[]): void => {
39
+ try {
40
+ (handler as (...values: never[]) => void)(...args);
41
+ } catch (error) {
42
+ const normalized = error instanceof Error ? error : new Error(String(error));
43
+ try {
44
+ onDeliveryError(normalized, event);
45
+ } catch {
46
+ // Diagnostics cannot make a committed session operation fail.
47
+ }
48
+ }
49
+ }) as IInteractiveSessionEvents[E];
50
+ session.on(event, ownedHandler);
51
+ bindings.push({ event, handler: ownedHandler as (...args: never[]) => void });
52
+ }
@@ -0,0 +1,140 @@
1
+ import { printablePeerDriver } from '@robota-sdk/agent-core';
2
+
3
+ import { attributedUserEcho } from './attributed-user-echo.js';
4
+ import { bindTuiSessionEvent, bindTuiSessionNoticeEvents } from './tui-session-binding.js';
5
+
6
+ import type { AttentionCoordinator } from './attention/attention-coordinator.js';
7
+ import type { ITuiSessionEventBinding } from './tui-session-binding.js';
8
+ import type { TuiStateManager } from './tui-state-manager.js';
9
+ import type { IActionRequest, TActionResponse, TToolArgs } from '@robota-sdk/agent-core';
10
+ import type { InteractiveSession } from '@robota-sdk/agent-framework';
11
+ import type { IExecutionWorkspaceEvent } from '@robota-sdk/agent-interface-execution';
12
+ import type {
13
+ IExecutionResult,
14
+ IInteractiveSessionEvents,
15
+ TInteractiveEventName,
16
+ TPermissionResultValue,
17
+ } from '@robota-sdk/agent-interface-session';
18
+
19
+ export interface ITuiSessionEventProjectorOptions {
20
+ session: InteractiveSession;
21
+ manager: TuiStateManager;
22
+ onUserMessage: (content: string) => void;
23
+ requestPermission: (
24
+ toolName: string,
25
+ toolArgs: TToolArgs,
26
+ id: string,
27
+ canPersistProjectPermission?: boolean,
28
+ requestedByPeer?: string,
29
+ ) => Promise<TPermissionResultValue>;
30
+ askUser: (request: IActionRequest, id: string) => Promise<TActionResponse>;
31
+ dismissPrompt: (id: string) => void;
32
+ onDeliveryError?: (error: Error, event: TInteractiveEventName) => void;
33
+ /** SCREEN-1992: fed the events the interval recap counts; absent ⇒ nothing is counted. */
34
+ attention?: AttentionCoordinator;
35
+ }
36
+
37
+ /** Projects runtime session events into TUI state and owns the complete listener lifecycle. */
38
+ export class TuiSessionEventProjector {
39
+ private readonly bindings: ITuiSessionEventBinding[] = [];
40
+
41
+ constructor(private readonly options: ITuiSessionEventProjectorOptions) {}
42
+
43
+ wire(): void {
44
+ if (this.bindings.length > 0) return;
45
+ const { session, manager, attention } = this.options;
46
+
47
+ const onUserMessage = (content: string): void => {
48
+ this.options.onUserMessage(content);
49
+ manager.addUserEcho(attributedUserEcho(content, session));
50
+ };
51
+ const syncHistory = (): void => manager.syncHistory(session.getFullHistory());
52
+ const onComplete = (result: IExecutionResult): void => {
53
+ manager.onComplete(result);
54
+ attention?.onComplete();
55
+ syncHistory();
56
+ };
57
+ const onError = (error: Error): void => {
58
+ manager.onError(error);
59
+ attention?.onError();
60
+ syncHistory();
61
+ };
62
+ const onExecutionWorkspaceEvent = (event: IExecutionWorkspaceEvent): void => {
63
+ manager.syncExecutionWorkspaceSnapshot(event.snapshot);
64
+ attention?.onWorkspaceSnapshot(event.snapshot);
65
+ };
66
+
67
+ this.bind('user_message', onUserMessage);
68
+ this.bind('text_delta', manager.onTextDelta);
69
+ this.bind('tool_start', manager.onToolStart);
70
+ this.bind('tool_end', manager.onToolEnd);
71
+ this.bind('thinking', manager.onThinking);
72
+ this.bind('complete', onComplete);
73
+ this.bind('interrupted', manager.onInterrupted);
74
+ this.bind('error', onError);
75
+ this.bind('context_update', manager.onContextUpdate);
76
+ this.bind('compact', syncHistory);
77
+ this.bind('skill_activation', syncHistory);
78
+ this.bind('memory_event', syncHistory);
79
+ this.bind('execution_workspace_event', onExecutionWorkspaceEvent);
80
+ this.bind('history_cleared', () => manager.clearHistory());
81
+ // SCREEN-1992: subscribed regardless, so the channel's subscribed set equals the classification.
82
+ this.bind('turn_source', (source) => attention?.onTurnSource(source));
83
+ bindTuiSessionNoticeEvents(this.bind.bind(this), manager);
84
+
85
+ const onPermissionRequest: IInteractiveSessionEvents['permission_request'] = ({
86
+ id,
87
+ toolName,
88
+ toolArgs,
89
+ canPersistProjectPermission,
90
+ requesterDriverId,
91
+ }) => {
92
+ attention?.onNeedsInput();
93
+ // A peer turn's ask names the peer, printed only as a plain identifier.
94
+ const requestedByPeer = requesterDriverId?.startsWith('peer:')
95
+ ? printablePeerDriver(requesterDriverId)
96
+ : undefined;
97
+ void this.options
98
+ .requestPermission(toolName, toolArgs, id, canPersistProjectPermission, requestedByPeer)
99
+ .then((result) => session.resolvePermission(id, result))
100
+ .catch(() => session.resolvePermission(id, false));
101
+ };
102
+ const onAskRequest: IInteractiveSessionEvents['ask_request'] = ({ id, request }) => {
103
+ attention?.onNeedsInput();
104
+ void this.options
105
+ .askUser(request, id)
106
+ .then((response) => session.resolveAsk(id, response))
107
+ .catch(() => session.resolveAsk(id, { type: 'cancelled' }));
108
+ };
109
+ this.bind('permission_request', onPermissionRequest);
110
+ this.bind('ask_request', onAskRequest);
111
+ this.bind('prompt_resolved', ({ id }) => this.options.dismissPrompt(id));
112
+ attention?.wire();
113
+ }
114
+
115
+ unwire(): void {
116
+ const { session } = this.options;
117
+ this.options.attention?.unwire();
118
+ for (const { event, handler } of this.bindings) {
119
+ session.off(event, handler as IInteractiveSessionEvents[typeof event]);
120
+ }
121
+ this.bindings.length = 0;
122
+ }
123
+
124
+ private bind<E extends TInteractiveEventName>(
125
+ event: E,
126
+ handler: IInteractiveSessionEvents[E],
127
+ ): void {
128
+ bindTuiSessionEvent(
129
+ this.options.session,
130
+ event,
131
+ handler,
132
+ (error) => {
133
+ const report = this.options.onDeliveryError;
134
+ if (report) report(error, event);
135
+ else this.options.manager.addSessionEventDeliveryError(error, event);
136
+ },
137
+ this.bindings,
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,82 @@
1
+ import type {
2
+ IBranchEvent,
3
+ IContextFileRefreshedEvent,
4
+ IPlanApprovalEvent,
5
+ TInteractiveEventName,
6
+ } from '@robota-sdk/agent-interface-session';
7
+
8
+ export type TTuiSessionEventClassification = 'channel' | 'react-side-effect' | 'non-surface';
9
+
10
+ /** Exhaustive TUI-owner policy over the shared session-event vocabulary. */
11
+ export const TUI_SESSION_EVENT_CLASSIFICATION = {
12
+ text_delta: 'channel',
13
+ tool_start: 'channel',
14
+ tool_end: 'channel',
15
+ thinking: 'channel',
16
+ complete: 'channel',
17
+ error: 'channel',
18
+ context_update: 'channel',
19
+ compact: 'channel',
20
+ interrupted: 'channel',
21
+ skill_activation: 'channel',
22
+ background_task_event: 'non-surface',
23
+ background_job_group_event: 'non-surface',
24
+ execution_workspace_event: 'channel',
25
+ user_message: 'channel',
26
+ turn_source: 'channel',
27
+ context_file_refreshed: 'channel',
28
+ memory_event: 'channel',
29
+ goal_event: 'non-surface',
30
+ plan_event: 'channel',
31
+ branch_event: 'channel',
32
+ permission_request: 'channel',
33
+ ask_request: 'channel',
34
+ prompt_resolved: 'channel',
35
+ ui_intent: 'react-side-effect',
36
+ session_renamed: 'react-side-effect',
37
+ history_cleared: 'channel',
38
+ } as const satisfies Record<TInteractiveEventName, TTuiSessionEventClassification>;
39
+
40
+ export interface ITuiSessionEventNotice {
41
+ id: string;
42
+ event:
43
+ | 'plan_event'
44
+ | 'context_file_refreshed'
45
+ | 'branch_event'
46
+ | 'delivery-error'
47
+ /** SCREEN-1992: TUI-originated — the interval recap on return, not a session event. */
48
+ | 'attention-recap';
49
+ message: string;
50
+ }
51
+
52
+ export type TTuiNoticeInput =
53
+ | { event: 'plan_event'; payload: IPlanApprovalEvent }
54
+ | { event: 'context_file_refreshed'; payload: IContextFileRefreshedEvent }
55
+ | { event: 'branch_event'; payload: IBranchEvent };
56
+
57
+ /** Pure deterministic projection from domain event to TUI render state. */
58
+ export function createTuiSessionEventNotice(
59
+ id: string,
60
+ input: TTuiNoticeInput,
61
+ ): ITuiSessionEventNotice {
62
+ switch (input.event) {
63
+ case 'plan_event':
64
+ return {
65
+ id,
66
+ event: input.event,
67
+ message: `Plan ${input.payload.type.replaceAll('_', ' ')}`,
68
+ };
69
+ case 'context_file_refreshed':
70
+ return {
71
+ id,
72
+ event: input.event,
73
+ message: `Context refreshed: ${input.payload.filePath}`,
74
+ };
75
+ case 'branch_event':
76
+ return {
77
+ id,
78
+ event: input.event,
79
+ message: `Branch ${input.payload.kind.replaceAll('_', ' ')}: ${input.payload.branchId} @ ${input.payload.checkpointId}`,
80
+ };
81
+ }
82
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ createTuiSessionEventNotice,
3
+ type ITuiSessionEventNotice,
4
+ type TTuiNoticeInput,
5
+ } from './tui-session-events.js';
6
+
7
+ import type { TInteractiveEventName } from '@robota-sdk/agent-interface-session';
8
+
9
+ const NOTICE_LIMIT = 50;
10
+
11
+ /** Bounded append-only render projection, deliberately separate from canonical session history. */
12
+ export class TuiSessionNoticeStore {
13
+ private sequence = 0;
14
+ readonly notices: ITuiSessionEventNotice[] = [];
15
+
16
+ add(input: TTuiNoticeInput): void {
17
+ this.append(createTuiSessionEventNotice(this.nextId(), input));
18
+ }
19
+
20
+ addDeliveryError(error: Error, event: TInteractiveEventName): void {
21
+ this.append({
22
+ id: this.nextId(),
23
+ event: 'delivery-error',
24
+ message: `Session event delivery failed (${event}): ${error.message}`,
25
+ });
26
+ }
27
+
28
+ /** SCREEN-1992: one line for the unattended interval that just ended. */
29
+ addAttentionRecap(line: string): void {
30
+ this.append({ id: this.nextId(), event: 'attention-recap', message: line });
31
+ }
32
+
33
+ private nextId(): string {
34
+ this.sequence += 1;
35
+ return `session-event-${this.sequence}`;
36
+ }
37
+
38
+ private append(notice: ITuiSessionEventNotice): void {
39
+ this.notices.push(notice);
40
+ const overflow = this.notices.length - NOTICE_LIMIT;
41
+ if (overflow > 0) this.notices.splice(0, overflow);
42
+ }
43
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * ARCH-005 — the TUI's session-option mapping, split out of `TuiInteractionChannel`.
3
+ *
4
+ * Pure: it maps the channel's already-resolved options onto the framework's `TInteractiveSessionOptions`.
5
+ * RUNTIME-001 — the channel then builds through the shared `buildRuntimeSession` seam (agent-framework),
6
+ * never a private `new InteractiveSession`: one session-construction SSOT across the TUI, print, and
7
+ * `--serve`.
8
+ */
9
+
10
+ import type { ITuiInteractionChannelOptions } from './tui-channel-options.js';
11
+ import type { TInteractiveSessionOptions } from '@robota-sdk/agent-framework';
12
+
13
+ /** Map the channel's resolved options onto the framework session-construction options. */
14
+ export function buildTuiSessionOptions(
15
+ opts: ITuiInteractionChannelOptions,
16
+ ): TInteractiveSessionOptions {
17
+ return {
18
+ cwd: opts.cwd,
19
+ ...(opts.livePromptTrace ? { livePromptTrace: opts.livePromptTrace } : {}),
20
+ provider: opts.provider,
21
+ ...(opts.providerErrorGuidance !== undefined
22
+ ? { providerErrorGuidance: opts.providerErrorGuidance }
23
+ : {}),
24
+ ...(opts.promptFileReferenceTag !== undefined
25
+ ? { promptFileReferenceTag: opts.promptFileReferenceTag }
26
+ : {}),
27
+ ...(opts.modelCommandToolPrefix !== undefined
28
+ ? { modelCommandToolPrefix: opts.modelCommandToolPrefix }
29
+ : {}),
30
+ ...(opts.subagentHookEnvironmentNames !== undefined
31
+ ? { subagentHookEnvironmentNames: opts.subagentHookEnvironmentNames }
32
+ : {}),
33
+ ...(opts.observerFailureWarningCode !== undefined
34
+ ? { observerFailureWarningCode: opts.observerFailureWarningCode }
35
+ : {}),
36
+ ...(opts.commandHookShell !== undefined ? { commandHookShell: opts.commandHookShell } : {}),
37
+ ...(opts.bare === true ? { bare: true } : {}),
38
+ ...(opts.skipConfiguredHooks === true ? { skipConfiguredHooks: true } : {}),
39
+ ...(opts.orgPolicy !== undefined ? { orgPolicy: opts.orgPolicy } : {}),
40
+ ...(opts.projectAccess !== undefined ? { projectAccess: opts.projectAccess } : {}),
41
+ ...(opts.projectSettingsPaths !== undefined
42
+ ? { projectSettingsPaths: opts.projectSettingsPaths }
43
+ : {}),
44
+ ...(opts.baselinePermissionAllow !== undefined
45
+ ? { baselinePermissionAllow: opts.baselinePermissionAllow }
46
+ : {}),
47
+ ...(opts.taskContext !== undefined ? { taskContext: opts.taskContext } : {}),
48
+ ...(opts.contributionSources !== undefined
49
+ ? { contributionSources: opts.contributionSources }
50
+ : {}),
51
+ ...(opts.skillRoots !== undefined ? { skillRoots: opts.skillRoots } : {}),
52
+ ...(opts.userSettingsSources !== undefined
53
+ ? { userSettingsSources: opts.userSettingsSources }
54
+ : {}),
55
+ ...(opts.editCheckpointStore !== undefined
56
+ ? { editCheckpointStore: opts.editCheckpointStore }
57
+ : {}),
58
+ // CLI-076: forward the resolved model so `--model` takes effect rather than falling through to the
59
+ // session's config/default model.
60
+ ...(opts.model !== undefined ? { model: opts.model } : {}),
61
+ ...(opts.effort !== undefined ? { effort: opts.effort } : {}),
62
+ ...(opts.temperature !== undefined ? { temperature: opts.temperature } : {}),
63
+ ...(opts.maxOutputTokens !== undefined ? { maxOutputTokens: opts.maxOutputTokens } : {}),
64
+ ...(opts.presetSystemPrompt !== undefined
65
+ ? { presetSystemPrompt: opts.presetSystemPrompt }
66
+ : {}),
67
+ ...(opts.responseFormat !== undefined ? { responseFormat: opts.responseFormat } : {}),
68
+ ...(opts.outputStyle !== undefined ? { outputStyle: opts.outputStyle } : {}),
69
+ permissionMode: opts.permissionMode,
70
+ maxTurns: opts.maxTurns,
71
+ // REMOTE-007: no injected permission/ask handlers — the TUI subscribes to the session's
72
+ // transport-neutral `permission_request`/`ask_request` events (wireSessionEvents) and answers via
73
+ // `resolvePermission`/`resolveAsk`. The local Ink queues + rendering are unchanged.
74
+ sessionStore: opts.sessionStore,
75
+ disableSessionLoops: opts.disableSessionLoops,
76
+ resolveDefaultLoopPrompt: opts.resolveDefaultLoopPrompt,
77
+ resumeSessionId: opts.resumeSessionId,
78
+ forkSession: opts.forkSession,
79
+ ...(opts.workspaceMovedFrom !== undefined
80
+ ? { workspaceMovedFrom: opts.workspaceMovedFrom }
81
+ : {}),
82
+ sessionName: opts.sessionName,
83
+ backgroundTaskRunners: opts.backgroundTaskRunners,
84
+ ...(opts.toolCallHandoff !== undefined ? { toolCallHandoff: opts.toolCallHandoff } : {}),
85
+ subagentRunnerFactory: opts.subagentRunnerFactory,
86
+ ...(opts.agentDefinitions !== undefined ? { agentDefinitions: opts.agentDefinitions } : {}),
87
+ ...(opts.agentDefinitionRoots !== undefined
88
+ ? { agentDefinitionRoots: opts.agentDefinitionRoots }
89
+ : {}),
90
+ ...(opts.pluginDirectories !== undefined ? { pluginDirectories: opts.pluginDirectories } : {}),
91
+ ...(opts.additionalTools !== undefined ? { additionalTools: opts.additionalTools } : {}),
92
+ ...(opts.defaultTools !== undefined ? { defaultTools: opts.defaultTools } : {}),
93
+ commandModules: opts.commandModules,
94
+ commandHostAdapters: opts.commandHostAdapters,
95
+ shellExec: opts.shellExec,
96
+ ...(opts.remoteCommandPolicy ? { remoteCommandPolicy: opts.remoteCommandPolicy } : {}),
97
+ language: opts.language,
98
+ agentName: opts.agentName,
99
+ activePresetId: opts.activePresetId,
100
+ persona: opts.persona,
101
+ systemPrompt: opts.systemPrompt,
102
+ appendSystemPrompt: opts.appendSystemPrompt,
103
+ allowedTools: opts.allowedTools,
104
+ deniedTools: opts.deniedTools,
105
+ enableParallelSubagents: opts.enableParallelSubagents,
106
+ selfVerification: opts.selfVerification,
107
+ terminalHandoff: opts.terminalHandoff,
108
+ // #1844: the session reads these when `/provider switch` hot-swaps. Absent, the switch throws
109
+ // "Unknown provider: <name>. Currently supported: " with an EMPTY list — measured, not inferred.
110
+ ...(opts.providerDefinitions ? { providerDefinitions: opts.providerDefinitions } : {}),
111
+ // SELFHOST-008 P6: forward the surface-resolved memory fields only when present (absent ⇒ OFF).
112
+ ...(opts.memoryStore ? { memoryStore: opts.memoryStore } : {}),
113
+ ...(opts.automaticMemory ? { automaticMemory: opts.automaticMemory } : {}),
114
+ ...(opts.recallMemory ? { recallMemory: opts.recallMemory } : {}),
115
+ // SCREEN-1993: the owner-prompt append (absent ⇒ the controller records nothing).
116
+ ...(opts.promptHistory ? { promptHistory: opts.promptHistory } : {}),
117
+ };
118
+ }