@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,451 @@
1
+ /**
2
+ * Unit tests for TuiStateManager — pure TypeScript, no React.
3
+ * Tests event → state transitions that must survive any refactoring.
4
+ */
5
+
6
+ import { describe, it, expect, vi } from 'vitest';
7
+ import { TuiStateManager } from '../tui-state-manager.js';
8
+ import type { IExecutionResult, IToolState } from '@robota-sdk/agent-interface-session';
9
+
10
+ function makeResult(overrides?: Partial<IExecutionResult>): IExecutionResult {
11
+ return {
12
+ response: 'test response',
13
+ history: [],
14
+ toolSummaries: [],
15
+ contextState: {
16
+ usedPercentage: 10,
17
+ remainingPercentage: 90,
18
+ usedTokens: 1000,
19
+ maxTokens: 200000,
20
+ },
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ describe('TuiStateManager', () => {
26
+ // ── Streaming text ────────────────────────────────────────────
27
+
28
+ it('accumulates streaming text on text_delta', () => {
29
+ const mgr = new TuiStateManager();
30
+ mgr.onTextDelta('Hello');
31
+ mgr.onTextDelta(' world');
32
+ expect(mgr.streamingText).toBe('Hello world');
33
+ });
34
+
35
+ it('clears streaming text on thinking=true', () => {
36
+ const mgr = new TuiStateManager();
37
+ mgr.onTextDelta('old text');
38
+ mgr.onThinking(true);
39
+ expect(mgr.streamingText).toBe('');
40
+ });
41
+
42
+ it('clears streaming text on complete', () => {
43
+ const mgr = new TuiStateManager();
44
+ mgr.onTextDelta('streaming');
45
+ mgr.onComplete(makeResult());
46
+ expect(mgr.streamingText).toBe('');
47
+ });
48
+
49
+ it('clears streaming text on interrupted', () => {
50
+ const mgr = new TuiStateManager();
51
+ mgr.onTextDelta('partial');
52
+ mgr.onInterrupted();
53
+ expect(mgr.streamingText).toBe('');
54
+ });
55
+
56
+ // ── Tool state ────────────────────────────────────────────────
57
+
58
+ it('adds tool on tool_start', () => {
59
+ const mgr = new TuiStateManager();
60
+ const tool: IToolState = { toolName: 'Read', firstArg: 'file.ts', isRunning: true };
61
+ mgr.onToolStart(tool);
62
+ expect(mgr.activeTools).toHaveLength(1);
63
+ expect(mgr.activeTools[0]!.toolName).toBe('Read');
64
+ });
65
+
66
+ it('updates tool on tool_end', () => {
67
+ const mgr = new TuiStateManager();
68
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'file.ts', isRunning: true });
69
+ mgr.onToolEnd({ toolName: 'Read', firstArg: 'file.ts', isRunning: false, result: 'success' });
70
+ expect(mgr.activeTools[0]!.isRunning).toBe(false);
71
+ expect(mgr.activeTools[0]!.result).toBe('success');
72
+ });
73
+
74
+ it('clears tools on thinking=true (next execution start)', () => {
75
+ const mgr = new TuiStateManager();
76
+ mgr.onToolStart({ toolName: 'Read', firstArg: '', isRunning: true });
77
+ mgr.onThinking(true);
78
+ expect(mgr.activeTools).toEqual([]);
79
+ });
80
+
81
+ it('clears tools on complete (tool summary now in messages)', () => {
82
+ const mgr = new TuiStateManager();
83
+ mgr.onToolStart({ toolName: 'Read', firstArg: '', isRunning: true });
84
+ mgr.onComplete(makeResult());
85
+ expect(mgr.activeTools).toEqual([]);
86
+ });
87
+
88
+ it('clears tools on interrupted (tool summary now in messages)', () => {
89
+ const mgr = new TuiStateManager();
90
+ mgr.onToolStart({ toolName: 'Read', firstArg: '', isRunning: true });
91
+ mgr.onInterrupted();
92
+ expect(mgr.activeTools).toEqual([]);
93
+ });
94
+
95
+ it('clears rendered history, streaming text, and active tools on explicit history clear', () => {
96
+ const mgr = new TuiStateManager();
97
+ mgr.addEntry({
98
+ id: 'old',
99
+ timestamp: new Date('2026-05-03T00:00:00.000Z'),
100
+ category: 'chat',
101
+ type: 'user',
102
+ data: { role: 'user', content: 'old message' },
103
+ });
104
+ mgr.onTextDelta('partial');
105
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'file.ts', isRunning: true });
106
+
107
+ mgr.clearHistory();
108
+
109
+ expect(mgr.history).toEqual([]);
110
+ expect(mgr.streamingText).toBe('');
111
+ expect(mgr.activeTools).toEqual([]);
112
+ });
113
+
114
+ // ── Thinking state ────────────────────────────────────────────
115
+
116
+ it('sets isThinking on thinking event', () => {
117
+ const mgr = new TuiStateManager();
118
+ mgr.onThinking(true);
119
+ expect(mgr.isThinking).toBe(true);
120
+ mgr.onThinking(false);
121
+ expect(mgr.isThinking).toBe(false);
122
+ });
123
+
124
+ it('clears isAborting on thinking=false', () => {
125
+ const mgr = new TuiStateManager();
126
+ mgr.setAborting(true);
127
+ expect(mgr.isAborting).toBe(true);
128
+ mgr.onThinking(false);
129
+ expect(mgr.isAborting).toBe(false);
130
+ });
131
+
132
+ // ── Context state ─────────────────────────────────────────────
133
+
134
+ it('updates context on complete', () => {
135
+ const mgr = new TuiStateManager();
136
+ mgr.onComplete(
137
+ makeResult({
138
+ contextState: {
139
+ usedPercentage: 50,
140
+ remainingPercentage: 50,
141
+ usedTokens: 5000,
142
+ maxTokens: 10000,
143
+ },
144
+ }),
145
+ );
146
+ expect(mgr.contextState.percentage).toBe(50);
147
+ expect(mgr.contextState.usedTokens).toBe(5000);
148
+ });
149
+
150
+ // ── Messages ──────────────────────────────────────────────────
151
+
152
+ it('addMessage appends to messages', () => {
153
+ const mgr = new TuiStateManager();
154
+ mgr.addEntry({ role: 'user', content: 'hello' } as never);
155
+ mgr.addEntry({ role: 'assistant', content: 'world' } as never);
156
+ expect(mgr.history).toHaveLength(2);
157
+ });
158
+
159
+ it('addEntry is append-only — no windowing (SCREEN-010)', () => {
160
+ const mgr = new TuiStateManager();
161
+ for (let i = 0; i < 110; i++) {
162
+ mgr.addEntry({
163
+ id: `${i}`,
164
+ timestamp: new Date(),
165
+ category: 'chat',
166
+ type: 'user',
167
+ data: { content: `msg ${i}` },
168
+ } as never);
169
+ }
170
+ // No front-slicing: committed history must stay append-only so Ink <Static> can render it.
171
+ expect(mgr.history).toHaveLength(110);
172
+ expect(mgr.history[0]!.id).toBe('0');
173
+ });
174
+
175
+ it('syncHistory replaces with the authoritative session history — no windowing (SCREEN-010)', () => {
176
+ const mgr = new TuiStateManager();
177
+ mgr.addUserEcho({ id: 'old', timestamp: new Date(), category: 'chat', type: 'user' } as never);
178
+ // syncHistory is the SSOT replace: the session's full (growing) history becomes the committed list.
179
+ mgr.syncHistory([
180
+ { id: 'new1', timestamp: new Date(), category: 'chat', type: 'user' } as never,
181
+ { id: 'new2', timestamp: new Date(), category: 'chat', type: 'assistant' } as never,
182
+ ]);
183
+ expect(mgr.history.map((e) => e.id)).toEqual(['new1', 'new2']);
184
+ });
185
+
186
+ it('keeps a local notice in place across a sync so the next answer still commits', () => {
187
+ const entry = (id: string) =>
188
+ ({ id, timestamp: new Date(), category: 'chat', type: 'user' }) as never;
189
+ const mgr = new TuiStateManager();
190
+ mgr.syncHistory([entry('s1')]);
191
+ mgr.addEntry(entry('unknown-command-notice'));
192
+ mgr.addUserEcho(entry('echo'));
193
+ const committedBeforeSync = mgr.history.length;
194
+
195
+ mgr.syncHistory([entry('s1'), entry('user'), entry('assistant'), entry('usage')]);
196
+
197
+ expect(mgr.history.map((e) => e.id)).toEqual([
198
+ 's1',
199
+ 'unknown-command-notice',
200
+ 'user',
201
+ 'assistant',
202
+ 'usage',
203
+ ]);
204
+ // Ink <Static> prints only indices it has not printed yet: the answer must be among them.
205
+ expect(mgr.history.slice(committedBeforeSync).map((e) => e.id)).toContain('assistant');
206
+ });
207
+
208
+ it('forgets local notices when the history is cleared', () => {
209
+ const entry = (id: string) =>
210
+ ({ id, timestamp: new Date(), category: 'chat', type: 'user' }) as never;
211
+ const mgr = new TuiStateManager();
212
+ mgr.addEntry(entry('notice'));
213
+ mgr.clearHistory();
214
+ mgr.syncHistory([entry('s1')]);
215
+
216
+ expect(mgr.history.map((e) => e.id)).toEqual(['s1']);
217
+ });
218
+
219
+ it('TC-09: syncHistory keeps the full session history without front-truncation', () => {
220
+ const mgr = new TuiStateManager();
221
+ const entries = Array.from(
222
+ { length: 250 },
223
+ (_, i) => ({ id: `${i}`, timestamp: new Date(), category: 'chat', type: 'user' }) as never,
224
+ );
225
+ mgr.syncHistory(entries);
226
+ // No MAX cap: the committed list must keep every entry so Ink <Static> renders the growing tail.
227
+ expect(mgr.history).toHaveLength(250);
228
+ expect(mgr.history[0]!.id).toBe('0');
229
+ expect(mgr.history[249]!.id).toBe('249');
230
+ });
231
+
232
+ // ── onChange notification ──────────────────────────────────────
233
+
234
+ it('calls onChange on every non-debounced state change', () => {
235
+ const mgr = new TuiStateManager();
236
+ const onChange = vi.fn();
237
+ mgr.onChange = onChange;
238
+
239
+ mgr.onTextDelta('hi'); // debounced — does NOT call onChange immediately
240
+ mgr.onToolStart({ toolName: 'Read', firstArg: '', isRunning: true });
241
+ mgr.onThinking(true); // flushes debounce timer
242
+ mgr.addEntry({ role: 'user', content: 'test' } as never);
243
+ mgr.setAborting(true);
244
+ mgr.setPendingPrompt('queued');
245
+ mgr.setContextState({ percentage: 50, usedTokens: 5000, maxTokens: 10000 });
246
+
247
+ expect(onChange).toHaveBeenCalledTimes(6);
248
+ });
249
+
250
+ it('onTextDelta debounces notify calls', async () => {
251
+ const mgr = new TuiStateManager();
252
+ const onChange = vi.fn();
253
+ mgr.onChange = onChange;
254
+
255
+ mgr.onTextDelta('a');
256
+ mgr.onTextDelta('b');
257
+ mgr.onTextDelta('c');
258
+
259
+ expect(onChange).toHaveBeenCalledTimes(0);
260
+ expect(mgr.streamingText).toBe('abc');
261
+
262
+ await new Promise((r) => setTimeout(r, 400));
263
+ expect(onChange).toHaveBeenCalledTimes(1);
264
+ });
265
+
266
+ it('does not crash when onChange is null', () => {
267
+ const mgr = new TuiStateManager();
268
+ expect(() => mgr.onTextDelta('hi')).not.toThrow();
269
+ });
270
+
271
+ it('stores SDK execution workspace snapshots and preserves selected entry across updates', () => {
272
+ const mgr = new TuiStateManager();
273
+ const snapshot = {
274
+ sessionId: 'session_1',
275
+ selectedEntryId: 'main:session_1',
276
+ updatedAt: '2026-05-09T00:00:00.000Z',
277
+ entries: [
278
+ {
279
+ id: 'main:session_1',
280
+ sourceId: 'session_1',
281
+ kind: 'main_thread',
282
+ origin: { kind: 'user_prompt', sessionId: 'session_1' },
283
+ status: 'idle',
284
+ state: 'completed',
285
+ title: 'Main thread',
286
+ unread: false,
287
+ attention: 'none',
288
+ visibility: 'default',
289
+ updatedAt: '2026-05-09T00:00:00.000Z',
290
+ controls: ['select'],
291
+ },
292
+ {
293
+ id: 'task:agent_1',
294
+ sourceId: 'agent_1',
295
+ kind: 'background_task',
296
+ origin: { kind: 'slash_command', sessionId: 'session_1' },
297
+ taskKind: 'agent',
298
+ status: 'running',
299
+ state: 'working',
300
+ title: 'Explore',
301
+ unread: false,
302
+ attention: 'none',
303
+ visibility: 'default',
304
+ updatedAt: '2026-05-09T00:00:01.000Z',
305
+ controls: ['select', 'cancel'],
306
+ },
307
+ ],
308
+ } as const;
309
+
310
+ mgr.syncExecutionWorkspaceSnapshot(snapshot);
311
+ mgr.selectExecutionWorkspaceEntry('task:agent_1');
312
+ mgr.syncExecutionWorkspaceSnapshot({ ...snapshot, updatedAt: '2026-05-09T00:00:02.000Z' });
313
+
314
+ expect(mgr.selectedExecutionEntryId).toBe('task:agent_1');
315
+ expect(mgr.executionWorkspaceSnapshot?.selectedEntryId).toBe('task:agent_1');
316
+ });
317
+
318
+ // ── Display order: Tool → Robota ──────────────────────────────
319
+
320
+ it('streaming state is cleared on complete (tools moved to messages)', () => {
321
+ const mgr = new TuiStateManager();
322
+ mgr.onThinking(true);
323
+ mgr.onTextDelta('streaming response');
324
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'f.ts', isRunning: true });
325
+ mgr.onToolEnd({ toolName: 'Read', firstArg: 'f.ts', isRunning: false, result: 'success' });
326
+
327
+ expect(mgr.streamingText).toBe('streaming response');
328
+ expect(mgr.activeTools).toHaveLength(1);
329
+
330
+ mgr.onComplete(makeResult());
331
+
332
+ // After complete: streaming cleared, tools cleared
333
+ expect(mgr.streamingText).toBe('');
334
+ expect(mgr.activeTools).toEqual([]);
335
+ // Tool info is now in InteractiveSession's messages (not managed here)
336
+ });
337
+
338
+ it('streaming state is cleared on abort (tools moved to messages)', () => {
339
+ const mgr = new TuiStateManager();
340
+ mgr.onThinking(true);
341
+ mgr.onTextDelta('partial');
342
+ mgr.onToolStart({ toolName: 'Bash', firstArg: 'ls', isRunning: true });
343
+
344
+ mgr.onInterrupted();
345
+
346
+ expect(mgr.streamingText).toBe('');
347
+ expect(mgr.activeTools).toEqual([]);
348
+ });
349
+
350
+ // ── Full lifecycle: streaming → completion message order ──────
351
+
352
+ it('during streaming: tools and text visible in StreamingIndicator state', () => {
353
+ const mgr = new TuiStateManager();
354
+
355
+ // Execution starts
356
+ mgr.onThinking(true);
357
+
358
+ // Tools arrive
359
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'file.ts', isRunning: true });
360
+ mgr.onToolEnd({ toolName: 'Read', firstArg: 'file.ts', isRunning: false, result: 'success' });
361
+ mgr.onToolStart({ toolName: 'Edit', firstArg: 'file.ts', isRunning: true });
362
+
363
+ // Streaming text arrives
364
+ mgr.onTextDelta('Here is the ');
365
+ mgr.onTextDelta('result');
366
+
367
+ // During streaming: both active
368
+ expect(mgr.activeTools).toHaveLength(2);
369
+ expect(mgr.streamingText).toBe('Here is the result');
370
+ expect(mgr.isThinking).toBe(true);
371
+ });
372
+
373
+ it('after completion: streaming cleared, messages synced from session', () => {
374
+ const mgr = new TuiStateManager();
375
+
376
+ mgr.onThinking(true);
377
+ mgr.onToolStart({ toolName: 'Read', firstArg: 'f.ts', isRunning: true });
378
+ mgr.onToolEnd({ toolName: 'Read', firstArg: 'f.ts', isRunning: false, result: 'success' });
379
+ mgr.onTextDelta('response text');
380
+
381
+ // Complete event fires, then thinking ends
382
+ mgr.onComplete(makeResult());
383
+ mgr.onThinking(false);
384
+
385
+ // StreamingIndicator state: cleared
386
+ expect(mgr.streamingText).toBe('');
387
+ expect(mgr.activeTools).toEqual([]);
388
+ expect(mgr.isThinking).toBe(false);
389
+
390
+ // Simulate InteractiveSession history entries being synced
391
+ mgr.syncHistory([
392
+ { id: '1', timestamp: new Date(), category: 'chat', type: 'user' } as never,
393
+ { id: '2', timestamp: new Date(), category: 'event', type: 'tool-summary' } as never,
394
+ { id: '3', timestamp: new Date(), category: 'chat', type: 'assistant' } as never,
395
+ ]);
396
+
397
+ // MessageList now has correct order: user → tool-summary → assistant
398
+ expect(mgr.history).toHaveLength(3);
399
+ expect(mgr.history[0]!.type).toBe('user');
400
+ expect(mgr.history[1]!.type).toBe('tool-summary');
401
+ expect(mgr.history[2]!.type).toBe('assistant');
402
+ });
403
+
404
+ it('after abort: streaming cleared, messages synced with tool → robota → system', () => {
405
+ const mgr = new TuiStateManager();
406
+
407
+ mgr.onThinking(true);
408
+ mgr.onToolStart({ toolName: 'Bash', firstArg: 'ls', isRunning: true });
409
+ mgr.onTextDelta('partial answer');
410
+
411
+ mgr.onInterrupted();
412
+
413
+ expect(mgr.streamingText).toBe('');
414
+ expect(mgr.activeTools).toEqual([]);
415
+
416
+ // Simulate InteractiveSession history entries synced after abort
417
+ mgr.syncHistory([
418
+ { id: '1', timestamp: new Date(), category: 'chat', type: 'user' } as never,
419
+ { id: '2', timestamp: new Date(), category: 'event', type: 'tool-summary' } as never,
420
+ { id: '3', timestamp: new Date(), category: 'chat', type: 'assistant' } as never,
421
+ { id: '4', timestamp: new Date(), category: 'chat', type: 'system' } as never,
422
+ ]);
423
+
424
+ // Order: user → tool-summary → assistant → system
425
+ expect(mgr.history).toHaveLength(4);
426
+ expect(mgr.history[0]!.type).toBe('user');
427
+ expect(mgr.history[1]!.type).toBe('tool-summary');
428
+ expect(mgr.history[2]!.type).toBe('assistant');
429
+ expect(mgr.history[3]!.type).toBe('system');
430
+ });
431
+
432
+ it('next execution clears previous tools from StreamingIndicator', () => {
433
+ const mgr = new TuiStateManager();
434
+
435
+ // First execution
436
+ mgr.onThinking(true);
437
+ mgr.onToolStart({ toolName: 'Read', firstArg: '', isRunning: true });
438
+ mgr.onComplete(makeResult());
439
+ expect(mgr.activeTools).toEqual([]);
440
+
441
+ // Second execution starts
442
+ mgr.onThinking(true);
443
+ // Previous tools should not reappear
444
+ expect(mgr.activeTools).toEqual([]);
445
+
446
+ // New tools for second execution
447
+ mgr.onToolStart({ toolName: 'Write', firstArg: 'new.ts', isRunning: true });
448
+ expect(mgr.activeTools).toHaveLength(1);
449
+ expect(mgr.activeTools[0]!.toolName).toBe('Write');
450
+ });
451
+ });