@robota-sdk/agent-ui-terminal 3.0.0-beta.100

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 (348) hide show
  1. package/CHANGELOG.md +1204 -0
  2. package/LICENSE +661 -0
  3. package/README.md +50 -0
  4. package/dist/node/index.d.ts +1224 -0
  5. package/dist/node/index.d.ts.map +1 -0
  6. package/dist/node/index.js +33 -0
  7. package/dist/node/index.js.map +1 -0
  8. package/package.json +88 -0
  9. package/src/App.tsx +157 -0
  10. package/src/AppPresentation.tsx +190 -0
  11. package/src/AppView.tsx +26 -0
  12. package/src/BackgroundTaskPanel.tsx +73 -0
  13. package/src/CjkTextInput.tsx +336 -0
  14. package/src/ConfirmPrompt.tsx +140 -0
  15. package/src/ContextWarningBanner.tsx +45 -0
  16. package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
  17. package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
  18. package/src/HistorySearchOverlay.tsx +150 -0
  19. package/src/InputArea.tsx +338 -0
  20. package/src/ListPicker.tsx +158 -0
  21. package/src/MenuSelect.tsx +159 -0
  22. package/src/MessageList.tsx +308 -0
  23. package/src/MultiSelectList.tsx +294 -0
  24. package/src/PendingActionPrompt.tsx +121 -0
  25. package/src/PermissionPrompt.tsx +236 -0
  26. package/src/PluginTUI.tsx +258 -0
  27. package/src/RoleLabel.tsx +72 -0
  28. package/src/SafeText.tsx +66 -0
  29. package/src/SessionEventNotices.tsx +27 -0
  30. package/src/SessionPicker.tsx +92 -0
  31. package/src/SessionStatusBar.tsx +80 -0
  32. package/src/SlashAutocomplete.tsx +154 -0
  33. package/src/StatusBar.tsx +308 -0
  34. package/src/StreamingIndicator.tsx +145 -0
  35. package/src/SupervisedSessionView.tsx +872 -0
  36. package/src/TextPrompt.tsx +112 -0
  37. package/src/ThemePicker.tsx +366 -0
  38. package/src/ToolCommandOutput.tsx +48 -0
  39. package/src/ToolDiffBlock.tsx +44 -0
  40. package/src/TransportTUI.tsx +166 -0
  41. package/src/TuiInteractionChannel.ts +530 -0
  42. package/src/UpdateNotice.tsx +18 -0
  43. package/src/UsageSummaryEntry.tsx +49 -0
  44. package/src/WaveText.tsx +70 -0
  45. package/src/__tests__/ListPicker.test.tsx +188 -0
  46. package/src/__tests__/MenuSelect.test.tsx +127 -0
  47. package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
  48. package/src/__tests__/PluginTUI.test.tsx +167 -0
  49. package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
  50. package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
  51. package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
  52. package/src/__tests__/TextPrompt.test.tsx +112 -0
  53. package/src/__tests__/TransportTUI.test.tsx +124 -0
  54. package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
  55. package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
  56. package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
  57. package/src/__tests__/UpdateNotice.test.tsx +15 -0
  58. package/src/__tests__/__fixtures__/korean-text.json +17 -0
  59. package/src/__tests__/abort-after-permission.test.tsx +160 -0
  60. package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
  61. package/src/__tests__/attached-app-session-switch.test.tsx +317 -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__/checkpoint-store-per-channel.test.ts +81 -0
  69. package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
  70. package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
  71. package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
  72. package/src/__tests__/cjk-text-input.test.ts +196 -0
  73. package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
  74. package/src/__tests__/command-output-summary.test.ts +95 -0
  75. package/src/__tests__/command-result-handler.test.ts +171 -0
  76. package/src/__tests__/compact-event-bridge.test.ts +20 -0
  77. package/src/__tests__/confirm-permission-flow.test.ts +149 -0
  78. package/src/__tests__/confirm-prompt.test.tsx +103 -0
  79. package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
  80. package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
  81. package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
  82. package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
  83. package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
  84. package/src/__tests__/external-prompt-notice.test.tsx +100 -0
  85. package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
  86. package/src/__tests__/fixtures/fake-editor.sh +7 -0
  87. package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
  88. package/src/__tests__/fork-attach.test.tsx +235 -0
  89. package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
  90. package/src/__tests__/helpers/terminal-profiles.ts +152 -0
  91. package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
  92. package/src/__tests__/history-clear-broadcast.test.ts +66 -0
  93. package/src/__tests__/history-search-overlay.test.tsx +329 -0
  94. package/src/__tests__/humanize-tool-name.test.ts +30 -0
  95. package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
  96. package/src/__tests__/input-area-flow.test.ts +199 -0
  97. package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
  98. package/src/__tests__/interaction-channel-charter.test.ts +13 -0
  99. package/src/__tests__/key-hint-consistency.test.tsx +249 -0
  100. package/src/__tests__/key-hint-footer.test.tsx +69 -0
  101. package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
  102. package/src/__tests__/message-list-rendering.test.tsx +474 -0
  103. package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
  104. package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
  105. package/src/__tests__/output-style-reaches-session.test.ts +35 -0
  106. package/src/__tests__/palette-consistency.test.ts +329 -0
  107. package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
  108. package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
  109. package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
  110. package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
  111. package/src/__tests__/product-display-name.test.tsx +79 -0
  112. package/src/__tests__/prompt-queue.test.tsx +260 -0
  113. package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
  114. package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
  115. package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
  116. package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
  117. package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
  118. package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
  119. package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
  120. package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
  121. package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
  122. package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
  123. package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
  124. package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
  125. package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
  126. package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
  127. package/src/__tests__/pty/isolated-home.ts +97 -0
  128. package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
  129. package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
  130. package/src/__tests__/pty/pty-driver.ts +155 -0
  131. package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
  132. package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
  133. package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
  134. package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
  135. package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
  136. package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
  137. package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
  138. package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
  139. package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
  140. package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
  141. package/src/__tests__/pty/spawn-pty.test.ts +120 -0
  142. package/src/__tests__/pty/spawn-pty.ts +229 -0
  143. package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
  144. package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
  145. package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
  146. package/src/__tests__/pty-driver-dispose.test.ts +43 -0
  147. package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
  148. package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
  149. package/src/__tests__/render-channel-options.test.ts +201 -0
  150. package/src/__tests__/render-lifecycle.test.ts +36 -0
  151. package/src/__tests__/render-markdown.test.ts +164 -0
  152. package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
  153. package/src/__tests__/safe-text-boundary.test.tsx +75 -0
  154. package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
  155. package/src/__tests__/screen-reader-input.test.tsx +106 -0
  156. package/src/__tests__/screen-reader-labels.test.ts +91 -0
  157. package/src/__tests__/screen-reader-menus.test.tsx +200 -0
  158. package/src/__tests__/screen-reader-pacing.test.ts +230 -0
  159. package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
  160. package/src/__tests__/screen-reader-render-options.test.ts +262 -0
  161. package/src/__tests__/screen-reader-stdout.test.ts +322 -0
  162. package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
  163. package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
  164. package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
  165. package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
  166. package/src/__tests__/selection-flow.test.ts +61 -0
  167. package/src/__tests__/session-event-notices.test.tsx +31 -0
  168. package/src/__tests__/session-init-poller.test.ts +102 -0
  169. package/src/__tests__/session-picker.test.tsx +93 -0
  170. package/src/__tests__/session-switch-channel.test.tsx +470 -0
  171. package/src/__tests__/shutdown-signal.test.ts +34 -0
  172. package/src/__tests__/status-activity.test.ts +71 -0
  173. package/src/__tests__/status-bar.test.tsx +290 -0
  174. package/src/__tests__/status-glyph.test.ts +30 -0
  175. package/src/__tests__/streaming-indicator.test.tsx +178 -0
  176. package/src/__tests__/terminal-capabilities.test.ts +153 -0
  177. package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
  178. package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
  179. package/src/__tests__/text-prompt-flow.test.ts +77 -0
  180. package/src/__tests__/theme-picker.test.tsx +345 -0
  181. package/src/__tests__/transport-boundary.test.ts +11 -0
  182. package/src/__tests__/tui-app-channel-port.test.ts +31 -0
  183. package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
  184. package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
  185. package/src/__tests__/tui-interaction-queues.test.ts +149 -0
  186. package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
  187. package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
  188. package/src/__tests__/tui-state-manager.test.ts +482 -0
  189. package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
  190. package/src/__tests__/wave-text.test.tsx +133 -0
  191. package/src/__tests__/wire-tui-channel.test.ts +1457 -0
  192. package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
  193. package/src/app-banner.tsx +42 -0
  194. package/src/app-static-items.ts +35 -0
  195. package/src/app-view-model.ts +140 -0
  196. package/src/attached-session-connection.ts +14 -0
  197. package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
  198. package/src/attention/__tests__/attention-tracker.test.ts +103 -0
  199. package/src/attention/__tests__/countdown.test.ts +15 -0
  200. package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
  201. package/src/attention/__tests__/interval-recap.test.ts +49 -0
  202. package/src/attention/attention-coordinator.ts +71 -0
  203. package/src/attention/attention-tracker.ts +124 -0
  204. package/src/attention/countdown.ts +24 -0
  205. package/src/attention/focus-input-filter.ts +131 -0
  206. package/src/attention/interval-recap.ts +148 -0
  207. package/src/attention/time-units.ts +6 -0
  208. package/src/attention-bell.ts +79 -0
  209. package/src/attributed-user-echo.ts +30 -0
  210. package/src/background-task-row-format.ts +99 -0
  211. package/src/command-interaction.ts +8 -0
  212. package/src/command-output-summary.ts +129 -0
  213. package/src/create-default-tui-cli-adapter.ts +45 -0
  214. package/src/execution-workspace-switcher-selection.ts +118 -0
  215. package/src/execution-workspace-view-model.ts +139 -0
  216. package/src/external-prompt-notice.tsx +38 -0
  217. package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
  218. package/src/flows/__tests__/defer-submit.test.ts +88 -0
  219. package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
  220. package/src/flows/background-focus-flow.ts +44 -0
  221. package/src/flows/cjk-cursor-motion.ts +75 -0
  222. package/src/flows/cjk-text-deletion.ts +53 -0
  223. package/src/flows/cjk-text-input-flow.ts +248 -0
  224. package/src/flows/cjk-text-input-types.ts +24 -0
  225. package/src/flows/confirm-prompt-flow.ts +90 -0
  226. package/src/flows/defer-submit.ts +67 -0
  227. package/src/flows/fork-attach-flow.ts +62 -0
  228. package/src/flows/input-area-flow.ts +188 -0
  229. package/src/flows/permission-prompt-flow.ts +108 -0
  230. package/src/flows/real-cursor-flow.ts +83 -0
  231. package/src/flows/selection-flow.ts +205 -0
  232. package/src/flows/session-init-poller.ts +76 -0
  233. package/src/flows/text-prompt-flow.ts +93 -0
  234. package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
  235. package/src/history-search/history-search-flow.ts +94 -0
  236. package/src/history-search/useHistorySearch.ts +336 -0
  237. package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
  238. package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
  239. package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
  240. package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
  241. package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
  242. package/src/hooks/command-result-handler.ts +44 -0
  243. package/src/hooks/side-effects-types.ts +45 -0
  244. package/src/hooks/useAppController.ts +271 -0
  245. package/src/hooks/useAppInputBindings.ts +163 -0
  246. package/src/hooks/useAppInteractionState.ts +84 -0
  247. package/src/hooks/useAppLifecycleState.ts +81 -0
  248. package/src/hooks/useAppOverlays.ts +90 -0
  249. package/src/hooks/useAppScreenState.ts +81 -0
  250. package/src/hooks/useAppSubmissionState.ts +45 -0
  251. package/src/hooks/useAppThemeState.ts +157 -0
  252. package/src/hooks/useAppWorkspaceState.ts +149 -0
  253. package/src/hooks/useAppearanceSettings.ts +30 -0
  254. package/src/hooks/useAutocomplete.ts +101 -0
  255. package/src/hooks/useBackgroundPanel.ts +58 -0
  256. package/src/hooks/useCountdownTick.ts +24 -0
  257. package/src/hooks/useExecutionDetailPage.ts +69 -0
  258. package/src/hooks/useForkAttach.ts +60 -0
  259. package/src/hooks/useInputAreaHistorySearch.ts +72 -0
  260. package/src/hooks/useInputAreaKeys.ts +114 -0
  261. package/src/hooks/useNumberedSelection.ts +93 -0
  262. package/src/hooks/usePluginCallbacks.ts +38 -0
  263. package/src/hooks/usePluginScreenData.ts +85 -0
  264. package/src/hooks/useRealCursorPosition.ts +125 -0
  265. package/src/hooks/useRenderMarkdown.ts +38 -0
  266. package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
  267. package/src/hooks/useSideEffects.ts +144 -0
  268. package/src/hooks/useStatusLineSettings.ts +50 -0
  269. package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
  270. package/src/hooks/useTuiChannel.ts +141 -0
  271. package/src/humanize-tool-name.ts +51 -0
  272. package/src/index.ts +72 -0
  273. package/src/input-area-rules.tsx +71 -0
  274. package/src/json-value.ts +18 -0
  275. package/src/key-hint-footer.tsx +54 -0
  276. package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
  277. package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
  278. package/src/keybindings/keybinding-catalogue.ts +171 -0
  279. package/src/keybindings/keybinding-registry.ts +303 -0
  280. package/src/keybindings/keybinding-syntax.ts +155 -0
  281. package/src/keybindings/keybindings-context.tsx +124 -0
  282. package/src/keybindings/node-keybindings-source.ts +189 -0
  283. package/src/numbered-list.tsx +113 -0
  284. package/src/plugin-tui-handlers.ts +163 -0
  285. package/src/product-display-name-context.tsx +30 -0
  286. package/src/render-markdown.ts +209 -0
  287. package/src/render.tsx +746 -0
  288. package/src/sanitize-terminal-text.ts +171 -0
  289. package/src/screen-reader-announcement.ts +52 -0
  290. package/src/screen-reader-context.tsx +32 -0
  291. package/src/screen-reader-labels.ts +64 -0
  292. package/src/screen-reader-pacing-context.tsx +47 -0
  293. package/src/screen-reader-pacing.ts +178 -0
  294. package/src/screen-reader-stdout.ts +328 -0
  295. package/src/short-session-id.ts +9 -0
  296. package/src/shutdown-signal.ts +34 -0
  297. package/src/slash-command-input.ts +11 -0
  298. package/src/status-activity.ts +70 -0
  299. package/src/status-glyph.ts +78 -0
  300. package/src/terminal-capabilities-context.tsx +27 -0
  301. package/src/terminal-capabilities.ts +77 -0
  302. package/src/terminal-focus-reporting.ts +50 -0
  303. package/src/terminal-handoff-controller.ts +98 -0
  304. package/src/terminal-marks.ts +63 -0
  305. package/src/theme/__tests__/built-in-themes.test.ts +117 -0
  306. package/src/theme/__tests__/color-vision.test.ts +82 -0
  307. package/src/theme/__tests__/theme-context.test.tsx +146 -0
  308. package/src/theme/__tests__/theme-document.test.ts +222 -0
  309. package/src/theme/__tests__/theme-registry.test.ts +138 -0
  310. package/src/theme/__tests__/theme-styles.test.ts +233 -0
  311. package/src/theme/built-in-themes.ts +342 -0
  312. package/src/theme/color-vision.ts +171 -0
  313. package/src/theme/index.ts +45 -0
  314. package/src/theme/theme-context.tsx +81 -0
  315. package/src/theme/theme-contracts.ts +139 -0
  316. package/src/theme/theme-document.ts +278 -0
  317. package/src/theme/theme-registry.ts +127 -0
  318. package/src/theme/theme-styles.ts +177 -0
  319. package/src/tool-summary-status.ts +59 -0
  320. package/src/tui-app-channel-port.ts +111 -0
  321. package/src/tui-channel-lifecycle-coordinator.ts +149 -0
  322. package/src/tui-channel-options.ts +184 -0
  323. package/src/tui-cli-adapter-context.tsx +13 -0
  324. package/src/tui-cli-adapter.ts +24 -0
  325. package/src/tui-interaction-queues.ts +171 -0
  326. package/src/tui-session-binding.ts +52 -0
  327. package/src/tui-session-event-projector.ts +149 -0
  328. package/src/tui-session-events.ts +89 -0
  329. package/src/tui-session-notice-store.ts +43 -0
  330. package/src/tui-session-options.ts +134 -0
  331. package/src/tui-state-manager.ts +347 -0
  332. package/src/types.ts +21 -0
  333. package/src/use-terminal-title.ts +29 -0
  334. package/src/utils/__tests__/edit-diff.test.ts +426 -0
  335. package/src/utils/__tests__/paste-detection.test.ts +116 -0
  336. package/src/utils/__tests__/paste-labels.test.ts +46 -0
  337. package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
  338. package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
  339. package/src/utils/edit-diff.ts +151 -0
  340. package/src/utils/input-top-border.ts +19 -0
  341. package/src/utils/paste-labels.ts +9 -0
  342. package/src/utils/tool-call-extractor.ts +92 -0
  343. package/src/utils/tool-diff-summary.ts +75 -0
  344. package/src/waiting-loop-stop-notice.ts +23 -0
  345. package/src/wire-history-sync.ts +159 -0
  346. package/src/wire-tui-channel.ts +963 -0
  347. package/src/wire-tui-client-commands.ts +142 -0
  348. package/src/wire-tui-projection.ts +136 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * SCREEN-005 User Execution Test Scenarios — prompt footers and key-hint affordance on the BUILT
3
+ * CLI binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
4
+ *
5
+ * Scenario 1: `/` opens the autocomplete → unified footer + `> ` indicator on the selected row.
6
+ * Scenario 2: a replayed model turn drives a CMD-004 ask in each shape (single-select,
7
+ * multi-select with the dynamic `(min N)` segment, free-text) → all footers share the grammar.
8
+ * Scenario 3: a replayed Shell tool call opens the permission prompt → footer names the real keys
9
+ * (`←→ Navigate · Enter Confirm`, no Esc); pressing Esc resolves nothing (documented hard-stop).
10
+ */
11
+
12
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
18
+
19
+ import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
20
+ import { formatKeyHints } from '../../key-hint-footer.js';
21
+ import { LIST_PICKER_DEFAULT_FOOTER_HINTS } from '../../ListPicker.js';
22
+ import { getMultiSelectFooterHints } from '../../MultiSelectList.js';
23
+ import { PERMISSION_PROMPT_FOOTER_HINTS } from '../../PermissionPrompt.js';
24
+ import { SLASH_AUTOCOMPLETE_FOOTER_HINTS } from '../../SlashAutocomplete.js';
25
+ import { TEXT_PROMPT_FOOTER_HINTS } from '../../TextPrompt.js';
26
+
27
+ import type { IPtySession } from './pty-driver.js';
28
+
29
+ const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures');
30
+ const ASK_SHAPES_FIXTURE = join(FIXTURES, 'screen-005-ask-shapes.jsonl');
31
+ const PERMISSION_FIXTURE = join(FIXTURES, 'screen-005-permission.jsonl');
32
+
33
+ function escapeRegExp(text: string): RegExp {
34
+ return new RegExp(text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
35
+ }
36
+
37
+ const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
38
+
39
+ describe('SCREEN-005 prompt footers through the real binary', () => {
40
+ let projectDir: string;
41
+ let session: IPtySession | undefined;
42
+
43
+ beforeEach(() => {
44
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen005-pty-')));
45
+ writeTuiProviderSettings(projectDir);
46
+ });
47
+
48
+ afterEach(async () => {
49
+ await session?.disposeAsync();
50
+ session = undefined;
51
+ rmSync(projectDir, { recursive: true, force: true });
52
+ });
53
+
54
+ it('S1: slash autocomplete shows the unified footer and the > indicator on the selected row', async () => {
55
+ session = spawnTui({ projectDir, homeDir: join(projectDir, 'home') });
56
+ await session.waitFor(/Type a message or \/help/);
57
+
58
+ await session.sendKeys('/');
59
+ const expectedFooter = formatKeyHints(SLASH_AUTOCOMPLETE_FOOTER_HINTS);
60
+ await session.waitFor(escapeRegExp(expectedFooter), 20_000);
61
+
62
+ const frame = session.snapshot();
63
+ expect(frame).toContain(expectedFooter);
64
+ // The highlighted first row carries the selection indicator in front of the command name.
65
+ expect(frame).toMatch(/> \/\w+/);
66
+ }, 60_000);
67
+
68
+ it('S2: single-select, multi-select (dynamic min segment), and free-text asks share the grammar', async () => {
69
+ session = spawnTui({
70
+ projectDir,
71
+ homeDir: join(projectDir, 'home'),
72
+ args: ['--session-log', ASK_SHAPES_FIXTURE, '--name', 'shapes-fixture'],
73
+ });
74
+ await session.waitFor(/Type a message or \/help/);
75
+
76
+ await session.sendKeys('go');
77
+ await session.pressEnter();
78
+
79
+ // Shape 1 — single-select (ListPicker default footer).
80
+ await session.waitFor(/PICK_SINGLE/, 20_000);
81
+ await session.waitFor(escapeRegExp(formatKeyHints(LIST_PICKER_DEFAULT_FOOTER_HINTS)), 10_000);
82
+ expect(session.snapshot()).toContain('> Red'); // indicator on the highlighted option
83
+ await session.pressEnter(); // answer "Red"
84
+
85
+ // Shape 2 — multi-select: min segment shows until satisfiable, then drops.
86
+ await session.waitFor(/PICK_MULTI/, 20_000);
87
+ const unsatisfied = formatKeyHints(
88
+ getMultiSelectFooterHints({ canConfirm: false, minSelect: 1 }),
89
+ );
90
+ await session.waitFor(escapeRegExp(unsatisfied), 10_000);
91
+ const beforeToggle = session.outputOffset();
92
+ await session.sendKeys(' '); // toggle Alpha → selection satisfiable
93
+ const satisfied = formatKeyHints(getMultiSelectFooterHints({ canConfirm: true, minSelect: 1 }));
94
+ await session.waitForSince(beforeToggle, escapeRegExp(satisfied), 10_000);
95
+ await session.pressEnter(); // confirm
96
+
97
+ // Shape 3 — free-text (TextPrompt footer).
98
+ await session.waitFor(/TYPE_TEXT/, 20_000);
99
+ await session.waitFor(escapeRegExp(formatKeyHints(TEXT_PROMPT_FOOTER_HINTS)), 10_000);
100
+ await session.sendKeys('hello');
101
+ await session.pressEnter();
102
+
103
+ // The replayed follow-up proves every ask resolved through the shared-grammar prompts.
104
+ await session.waitFor(/ALL_SHAPES_DONE/, 20_000);
105
+ }, 60_000);
106
+
107
+ it('S3: permission prompt names the real keys and Esc resolves nothing (explicit hard-stop)', async () => {
108
+ session = spawnTui({
109
+ projectDir,
110
+ homeDir: join(projectDir, 'home'),
111
+ args: ['--session-log', PERMISSION_FIXTURE, '--name', 'perm-fixture'],
112
+ });
113
+ await session.waitFor(/Type a message or \/help/);
114
+
115
+ await session.sendKeys('go');
116
+ await session.pressEnter();
117
+
118
+ // The replayed Shell call needs approval in default permission mode → prompt renders.
119
+ await session.waitFor(/Permission Required/, 20_000);
120
+ const expectedFooter = formatKeyHints(PERMISSION_PROMPT_FOOTER_HINTS);
121
+ await session.waitFor(escapeRegExp(expectedFooter), 10_000);
122
+
123
+ // Esc is suppressed (a dismissal would be an implicit deny): nothing resolves, the prompt stays.
124
+ session.pressEscape();
125
+ await delay(500);
126
+ const afterEsc = session.snapshot();
127
+ expect(afterEsc).not.toContain('AFTER_PERMISSION_FLOW');
128
+ expect(afterEsc).toContain('Allow [y]'); // prompt still displayed, still unresolved
129
+
130
+ // Resolve explicitly (allow) once the prompt is armed → the tool runs and the turn continues.
131
+ await session.waitFor(/> Allow \[y\]/, 10_000);
132
+ await session.sendKeys('y');
133
+ await session.waitFor(/AFTER_PERMISSION_FLOW/, 20_000);
134
+ }, 60_000);
135
+ });
@@ -0,0 +1,140 @@
1
+ /**
2
+ * SCREEN-006 User Execution Test Scenarios — color/motion degradation and palette-token colors on
3
+ * the BUILT CLI binary in a real PTY (run by `test:pty`; requires `pnpm build:deps` first).
4
+ *
5
+ * Scenario 1 (NO_COLOR legibility): `NO_COLOR=1` through a replayed conversation → the raw pty
6
+ * transcript contains ZERO SGR color sequences (30-38/40-48/90-97/100-107 params, incl. 38;5 /
7
+ * 48;5 extended forms). Because NO color code may appear at all, this also proves the repeated
8
+ * waiting-state frames show zero WaveText color churn (motion is static: the
9
+ * `isInteractiveColorTerminal()` gate creates no interval under NO_COLOR).
10
+ * Scenario 2 (normal-run consistency): color on → the persisted tool summary renders the
11
+ * `STATUS_SYMBOL.success` glyph in the resolved theme's status color (green, ESC[32m — sourced
12
+ * from `colors.status` through `statusGlyphColor`), the assistant label renders the accent token
13
+ * (cyan, ESC[36m), and the markdown diff block carries the theme's diff SGR pairs (38;5;120 on
14
+ * 48;5;22 for added lines under the default `dark` theme).
15
+ * Reachability note (recorded limit): a permission DENY short-circuits before tool-start, so the
16
+ * framework emits no tool-summary entry for it — the denied (`yellowBright`) unification is
17
+ * therefore pinned at component level (`message-list-rendering.test.tsx`), while this scenario
18
+ * evidences the same glyph→status-colour sourcing end-to-end via the success kind.
19
+ */
20
+
21
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
22
+ import { tmpdir } from 'node:os';
23
+ import { dirname, join } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+
26
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
27
+
28
+ import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
29
+ import { DARK_THEME } from '../../theme/built-in-themes.js';
30
+ import { background, foreground } from '../../theme/index.js';
31
+
32
+ import type { IPtySession } from './pty-driver.js';
33
+
34
+ const FIXTURES = join(dirname(fileURLToPath(import.meta.url)), 'fixtures');
35
+ const REPLAY_FIXTURE = join(FIXTURES, 'replay-conversation.jsonl');
36
+ const COLORS_FIXTURE = join(FIXTURES, 'screen-006-colors.jsonl');
37
+
38
+ /** SGR params that set a color (foreground/background, basic/bright/extended). */
39
+ const COLOR_PARAM = /^(?:3[0-8]|39|4[0-8]|49|9[0-7]|10[0-7])$/;
40
+
41
+ /** Every SGR color param present in a raw pty transcript (e.g. ['33', '38;5;210'→'38']). */
42
+ function sgrColorParams(raw: string): string[] {
43
+ const params: string[] = [];
44
+ // eslint-disable-next-line no-control-regex -- scanning raw SGR escape bytes by design
45
+ for (const match of raw.matchAll(/\x1b\[([0-9;]*)m/g)) {
46
+ // In an extended sequence (38;5;n / 48;5;n) the payload params are not themselves
47
+ // color-setters, but the leading 38/48 is — flagging it is sufficient and exact.
48
+ for (const param of (match[1] === '' ? '0' : match[1]).split(';')) {
49
+ if (COLOR_PARAM.test(param)) {
50
+ params.push(param);
51
+ break; // one flagged param per sequence is enough evidence
52
+ }
53
+ }
54
+ }
55
+ return params;
56
+ }
57
+
58
+ describe('SCREEN-006 color/motion through the real binary', () => {
59
+ let projectDir: string;
60
+ let session: IPtySession | undefined;
61
+
62
+ beforeEach(() => {
63
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-screen006-pty-')));
64
+ writeTuiProviderSettings(projectDir);
65
+ });
66
+
67
+ afterEach(async () => {
68
+ await session?.disposeAsync();
69
+ session = undefined;
70
+ rmSync(projectDir, { recursive: true, force: true });
71
+ });
72
+
73
+ it('S1: NO_COLOR=1 → legible output, zero SGR color sequences, no animation', async () => {
74
+ session = spawnTui({
75
+ projectDir,
76
+ homeDir: join(projectDir, 'home'),
77
+ args: ['--session-log', REPLAY_FIXTURE],
78
+ env: { NO_COLOR: '1' },
79
+ });
80
+
81
+ await session.waitFor(/Type a message or \/help/);
82
+ await session.sendKeys('hello');
83
+ await session.pressEnter();
84
+
85
+ // The replayed turn completes and the output stays legible (content + labels present).
86
+ await session.waitFor(/REPLAYED_ANSWER_42/, 20_000);
87
+ await session.waitFor(/Type a message or \/help/, 20_000);
88
+
89
+ const stripped = session.snapshot();
90
+ expect(stripped).toContain('You:');
91
+ expect(stripped).toContain('REPLAYED_ANSWER_42');
92
+
93
+ // Zero SGR color sequences in the ENTIRE raw transcript — which also proves the
94
+ // waiting-state (WaveText) frames carried zero color churn under NO_COLOR.
95
+ expect(sgrColorParams(session.raw())).toEqual([]);
96
+ }, 60_000);
97
+
98
+ it('S2: color on → tool summary uses the themes status color, labels use accent, diff uses the themes pairs', async () => {
99
+ session = spawnTui({
100
+ projectDir,
101
+ homeDir: join(projectDir, 'home'),
102
+ args: ['--session-log', COLORS_FIXTURE],
103
+ });
104
+
105
+ await session.waitFor(/Type a message or \/help/);
106
+ await session.sendKeys('go');
107
+ await session.pressEnter();
108
+
109
+ // The replayed Shell call needs approval in default permission mode → allow it.
110
+ await session.waitFor(/Permission Required/, 20_000);
111
+ // Its keys answer once it is armed, which the selection cursor shows.
112
+ await session.waitFor(/> Allow \[y\]/, 10_000);
113
+ await session.sendKeys('y');
114
+
115
+ // The turn continues: the second replayed response carries the markdown diff block,
116
+ // and the persisted tool summary lands at turn end.
117
+ await session.waitFor(/SCREEN006_DIFF_DONE/, 20_000);
118
+ await session.waitFor(/✓ Shell/, 10_000);
119
+
120
+ const stripped = session.snapshot();
121
+ const raw = session.raw();
122
+
123
+ // Persisted tool summary: SSOT glyph + the theme's status colour (success → green, ESC[32m)
124
+ // rendered by the unified MessageList path (STATUS_SYMBOL + statusGlyphColor).
125
+ expect(stripped).toContain('✓ Shell');
126
+ // eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
127
+ expect(raw).toMatch(/\x1b\[32m[^\x1b]*✓ Shell/);
128
+
129
+ // Assistant label renders the accent token (cyan → ESC[36m ... "Fixture Agent:").
130
+ // eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
131
+ expect(raw).toMatch(/\x1b\[36m(?:\x1b\[[0-9;]*m)*Fixture Agent:/);
132
+
133
+ // Markdown diff block carries the resolved theme's SGR pair (added line: light green on a dark
134
+ // green background) — SCREEN-2002 moved the values into the theme, the bytes are unchanged.
135
+ const addedBackground = background(DARK_THEME.markdown.diffAddedBackground)('x').split('x')[0];
136
+ const addedForeground = foreground(DARK_THEME.markdown.diffAdded)('x').split('x')[0];
137
+ expect(raw).toContain(`${addedBackground}${addedForeground}`);
138
+ expect(stripped).toContain('+ added line');
139
+ }, 60_000);
140
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * SCREEN-010 TC-05: chat-window scrollback layout, on the real binary.
3
+ *
4
+ * Runs in the PTY vitest project against the BUILT CLI. Automates the former "manual" smoke
5
+ * (per the never-ask-the-user-to-test rule): generates committed output larger than a small viewport
6
+ * and asserts the committed history is emitted to the terminal's native scrollback while the input
7
+ * prompt + status bar stay pinned at the bottom. Once committed content is in scrollback and the input
8
+ * is pinned, native terminal scroll-back is the terminal's own behavior (not the app's), so this is
9
+ * the automatable essence of TC-05.
10
+ *
11
+ * CLI-2004 TC-22 adds the same proof under `--screen-reader`. Ink's `alternateScreen` defaults to
12
+ * false and nothing in the tree sets it — this is the GUARD on that, not new behaviour: the
13
+ * alternate screen has no scrollback, and reviewing earlier output is the whole reason a reader can
14
+ * use this TUI at all. The flagless case above cannot serve as the guard, because it asserts on the
15
+ * boot banner and on `Idle`, both of which the mode suppresses.
16
+ */
17
+
18
+ import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
19
+ import { tmpdir } from 'node:os';
20
+ import { join } from 'node:path';
21
+
22
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
23
+
24
+ import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
25
+
26
+ import type { IPtySession } from './pty-driver.js';
27
+
28
+ /**
29
+ * The heading of the COMMITTED `/help` output, and the reason it is this precise.
30
+ *
31
+ * `Available commands` without the colon is also `/help`'s own DESCRIPTION, which the slash
32
+ * autocomplete renders while the command is still being typed — four times over, before Enter is
33
+ * handled at all. Waiting on that returns immediately and leaves the snapshot below racing the
34
+ * commit; the case then asserts "the prompt is below the committed output" against a frame in which
35
+ * nothing has been committed yet. It passed only because the commit usually won that race, and a
36
+ * loaded runner is where it does not. The colon appears only in the committed block.
37
+ */
38
+ const COMMITTED_HELP_HEADING = /Available commands:/;
39
+
40
+ /** Wait budgets: one `waitFor`, and a whole case, on a loaded CI runner. */
41
+ const PROMPT_WAIT_MS = 30_000;
42
+ const COMMANDS_WAIT_MS = 20_000;
43
+ const CASE_TIMEOUT_MS = 60_000;
44
+
45
+ describe('SCREEN-010 chat-window scrollback layout (real binary)', () => {
46
+ let projectDir: string;
47
+ let session: IPtySession | undefined;
48
+
49
+ beforeEach(() => {
50
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-scrollback-pty-')));
51
+ writeTuiProviderSettings(projectDir);
52
+ });
53
+
54
+ afterEach(async () => {
55
+ await session?.disposeAsync();
56
+ session = undefined;
57
+ rmSync(projectDir, { recursive: true, force: true });
58
+ });
59
+
60
+ it(
61
+ 'TC-05: committed history fills scrollback beyond the viewport while the input stays pinned',
62
+ async () => {
63
+ // Small viewport so the committed /help output exceeds it and scrolls into scrollback.
64
+ session = spawnTui({ projectDir, homeDir: join(projectDir, 'home'), rows: 16 });
65
+
66
+ await session.waitFor(/Type a message or \/help/);
67
+ await session.waitFor(/Idle/);
68
+
69
+ // /help commits a long command list (> 16 rows) to history → Ink <Static> → scrollback.
70
+ await session.sendKeys('/help');
71
+ await session.pressEnter();
72
+ await session.waitFor(COMMITTED_HELP_HEADING, COMMANDS_WAIT_MS);
73
+
74
+ const snap = session.snapshot();
75
+
76
+ // Committed content is present (emitted to scrollback), and the banner committed at boot remains.
77
+ const committedIdx = snap.search(COMMITTED_HELP_HEADING);
78
+ expect(committedIdx).toBeGreaterThanOrEqual(0);
79
+ expect((snap.match(/v\d+\.\d+\.\d+/g) ?? []).length).toBeGreaterThanOrEqual(1);
80
+
81
+ // The input prompt re-renders at the BOTTOM (live region): its last occurrence is below the
82
+ // committed command-list output — i.e. the input is pinned while committed history sits above it.
83
+ const lastPromptIdx = snap.lastIndexOf('Type a message');
84
+ expect(lastPromptIdx).toBeGreaterThan(committedIdx);
85
+
86
+ // The status bar is also pinned in the live region.
87
+ expect(snap.lastIndexOf('Idle')).toBeGreaterThan(committedIdx);
88
+ },
89
+ CASE_TIMEOUT_MS,
90
+ );
91
+
92
+ it(
93
+ 'CLI-2004 TC-22: --screen-reader never switches to the alternate screen and keeps history in scrollback',
94
+ async () => {
95
+ session = spawnTui({
96
+ projectDir,
97
+ homeDir: join(projectDir, 'home'),
98
+ rows: 16,
99
+ args: ['--screen-reader'],
100
+ env: { PRODUCT_SCREEN_READER_STARTUP_QUIET_MS: '0' },
101
+ });
102
+
103
+ await session.waitFor(/Type a message or \/help/, PROMPT_WAIT_MS);
104
+
105
+ await session.sendKeys('/help');
106
+ await session.pressEnter();
107
+ await session.waitFor(/Available commands|\/exit/i, COMMANDS_WAIT_MS);
108
+
109
+ // No alternate-screen switch at ANY point in the run — the mode must not buy a tidy frame at
110
+ // the price of the terminal's own scrollback.
111
+ expect(session.raw()).not.toContain('\x1b[?1049h');
112
+
113
+ const snap = session.snapshot();
114
+ const committedIdx = snap.search(/Available commands|\/exit/i);
115
+ expect(committedIdx).toBeGreaterThanOrEqual(0);
116
+
117
+ // The input stays pinned below the committed history — asserted without the banner or `Idle`,
118
+ // which the mode suppresses.
119
+ expect(snap.lastIndexOf('Type a message')).toBeGreaterThan(committedIdx);
120
+ },
121
+ CASE_TIMEOUT_MS,
122
+ );
123
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * SCREEN-1993 TC-10: the transcript decision's evidence, on the real binary.
3
+ *
4
+ * The item asked for a way to "search prompt history and conversation transcripts"; the spec ships
5
+ * prompt-history search and REJECTS an in-app transcript viewer because the terminal's own scrollback
6
+ * already holds the whole transcript (`<Static>` commits every message, and nothing ever switches to
7
+ * the alternate screen, which has no scrollback). That reason is only as good as its proof: resume a
8
+ * persisted session of more than 100 messages in a small viewport and assert every message text is
9
+ * in the captured output without a single key pressed — and that `ESC [ ? 1049 h` never appears.
10
+ */
11
+
12
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+
16
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
17
+
18
+ import { spawnTui, writeTuiProviderSettings } from './pty-driver.js';
19
+
20
+ import type { IPtySession } from './pty-driver.js';
21
+
22
+ const SESSION_ID = 'screen-1993-transcript';
23
+ const TURNS = 60; // 120 messages: 60 user prompts and 60 assistant replies
24
+ const PROMPT_WAIT_MS = 30_000;
25
+ const FIXTURE_YEAR = 2026;
26
+ const CASE_TIMEOUT_MS = 90_000;
27
+
28
+ function messageText(index: number, role: 'user' | 'assistant'): string {
29
+ return role === 'user'
30
+ ? `transcript prompt ${index} of ${TURNS}`
31
+ : `transcript reply ${index} of ${TURNS}`;
32
+ }
33
+
34
+ /** A persisted session in the store's versioned envelope: `messages` for the model, `history` for the UI. */
35
+ function seedSession(homeDir: string, cwd: string): void {
36
+ const at = (index: number): string =>
37
+ new Date(Date.UTC(FIXTURE_YEAR, 0, 1, 0, 0, index)).toISOString();
38
+ const messages = [];
39
+ const history = [];
40
+ for (let index = 1; index <= TURNS; index += 1) {
41
+ for (const role of ['user', 'assistant'] as const) {
42
+ const content = messageText(index, role);
43
+ messages.push({
44
+ id: `m-${role}-${index}`,
45
+ timestamp: at(index),
46
+ state: 'complete',
47
+ role,
48
+ content,
49
+ });
50
+ history.push({
51
+ id: `${role}-${index}`,
52
+ timestamp: at(index),
53
+ category: 'chat',
54
+ type: role,
55
+ data: { role, content },
56
+ });
57
+ }
58
+ }
59
+ const sessionsDir = join(homeDir, 'state', 'sessions');
60
+ mkdirSync(sessionsDir, { recursive: true });
61
+ writeFileSync(
62
+ join(sessionsDir, `${SESSION_ID}.json`),
63
+ JSON.stringify({
64
+ schemaVersion: 1,
65
+ record: {
66
+ id: SESSION_ID,
67
+ name: 'transcript-fixture',
68
+ cwd,
69
+ createdAt: at(0),
70
+ updatedAt: at(TURNS),
71
+ messages,
72
+ history,
73
+ },
74
+ }),
75
+ 'utf8',
76
+ );
77
+ }
78
+
79
+ describe('SCREEN-1993 TC-10: the resumed transcript lives in native scrollback', () => {
80
+ let projectDir: string;
81
+ let session: IPtySession | undefined;
82
+
83
+ beforeEach(() => {
84
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-1993-scrollback-')));
85
+ writeTuiProviderSettings(projectDir);
86
+ seedSession(join(projectDir, 'home'), projectDir);
87
+ });
88
+
89
+ afterEach(async () => {
90
+ await session?.disposeAsync();
91
+ session = undefined;
92
+ rmSync(projectDir, { recursive: true, force: true });
93
+ });
94
+
95
+ it(
96
+ 'every one of 120 restored messages is in the terminal output with no key pressed, and no alternate screen',
97
+ async () => {
98
+ session = spawnTui({
99
+ projectDir,
100
+ homeDir: join(projectDir, 'home'),
101
+ rows: 16,
102
+ args: ['--resume', SESSION_ID, '--name', 'transcript-fixture'],
103
+ });
104
+ await session.waitFor(/Type a message or \/help/, PROMPT_WAIT_MS);
105
+ await session.waitFor(new RegExp(messageText(TURNS, 'assistant')), PROMPT_WAIT_MS);
106
+
107
+ const snapshot = session.snapshot();
108
+ const missing: string[] = [];
109
+ for (let index = 1; index <= TURNS; index += 1) {
110
+ for (const role of ['user', 'assistant'] as const) {
111
+ if (!snapshot.includes(messageText(index, role))) missing.push(messageText(index, role));
112
+ }
113
+ }
114
+ expect(missing).toEqual([]);
115
+ // The transcript is above the live region: the input is pinned below the last message.
116
+ expect(snapshot.lastIndexOf('Type a message')).toBeGreaterThan(
117
+ snapshot.indexOf(messageText(TURNS, 'assistant')),
118
+ );
119
+ // The alternate screen has no scrollback; the TUI never enters it.
120
+ expect(session.raw()).not.toContain('\x1b[?1049h');
121
+ },
122
+ CASE_TIMEOUT_MS,
123
+ );
124
+ });