@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,473 @@
1
+ /**
2
+ * Rendering tests for MessageList — verifies that IHistoryEntry[]
3
+ * entries render with correct labels and content.
4
+ *
5
+ * These tests catch rendering bugs that data-flow tests miss:
6
+ * - tool execution list must show "Tool:" label with tool names
7
+ * - chat messages must show correct role labels
8
+ * - event entries must show formatted content
9
+ */
10
+
11
+ import React from 'react';
12
+ import chalk from 'chalk';
13
+ import { render } from 'ink-testing-library';
14
+ import { describe, it, expect } from 'vitest';
15
+ import MessageList from '../MessageList.js';
16
+ import { STATUS_SYMBOL, statusGlyphColor } from '../status-glyph.js';
17
+ import { DARK_THEME } from '../theme/built-in-themes.js';
18
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
19
+ import {
20
+ createUserMessage,
21
+ createAssistantMessage,
22
+ createSystemMessage,
23
+ createToolMessage,
24
+ messageToHistoryEntry,
25
+ } from '@robota-sdk/agent-core';
26
+
27
+ function makeToolSummaryEntry(): IHistoryEntry {
28
+ return {
29
+ id: 'tool-1',
30
+ timestamp: new Date(),
31
+ category: 'event',
32
+ type: 'tool-summary',
33
+ data: {
34
+ tools: [
35
+ { toolName: 'Read', firstArg: 'file.ts', isRunning: false, result: 'success' },
36
+ { toolName: 'Edit', firstArg: 'file.ts', isRunning: false, result: 'success' },
37
+ ],
38
+ summary: '✓ Read(file.ts)\n✓ Edit(file.ts)',
39
+ },
40
+ };
41
+ }
42
+
43
+ function makeSkillInvocationEntry(): IHistoryEntry {
44
+ return {
45
+ id: 'evt-1',
46
+ timestamp: new Date(),
47
+ category: 'event',
48
+ type: 'skill-activation',
49
+ data: {
50
+ type: 'skill-activation',
51
+ skillName: 'audit',
52
+ source: 'plugin',
53
+ invocation: 'user-slash',
54
+ mode: 'inject',
55
+ status: 'started',
56
+ timestamp: '2026-05-06T00:00:00.000Z',
57
+ message: 'Invoking plugin skill: audit',
58
+ },
59
+ };
60
+ }
61
+
62
+ describe('MessageList rendering', () => {
63
+ // ── Tool summary rendering ────────────────────────────────────
64
+
65
+ it('tool execution list renders with "Tool:" label and tool names', () => {
66
+ const history: IHistoryEntry[] = [makeToolSummaryEntry()];
67
+ const { lastFrame } = render(<MessageList history={history} />);
68
+ const output = lastFrame() ?? '';
69
+
70
+ expect(output).toContain('Tool:');
71
+ expect(output).toContain('Read(file.ts)');
72
+ expect(output).toContain('Edit(file.ts)');
73
+ });
74
+
75
+ // ── SCREEN-006: status colors come from the status symbol + theme colour SSOT ─
76
+
77
+ it('denied tool summary renders the denied status colour (yellowBright) and glyph', () => {
78
+ // Pin color bytes regardless of the test process's TTY detection (cjk-fallback precedent).
79
+ const originalChalkLevel = chalk.level;
80
+ chalk.level = 3;
81
+ const history: IHistoryEntry[] = [
82
+ {
83
+ id: 'tool-denied-1',
84
+ timestamp: new Date(),
85
+ category: 'event',
86
+ type: 'tool-summary',
87
+ data: {
88
+ tools: [{ toolName: 'Edit', firstArg: 'file.ts', isRunning: false, result: 'denied' }],
89
+ summary: '⊘ Edit(file.ts)',
90
+ },
91
+ },
92
+ ];
93
+ try {
94
+ const { lastFrame } = render(<MessageList history={history} />);
95
+ const output = lastFrame() ?? '';
96
+
97
+ expect(statusGlyphColor(DARK_THEME.colors, 'denied')).toBe('yellowBright');
98
+ expect(output).toContain(STATUS_SYMBOL.denied);
99
+ // chalk yellowBright opens with ESC[93m; the old hand-rolled mapping drifted to
100
+ // plain yellow (ESC[33m) — this pins the tool summary to the SSOT color.
101
+ expect(output).toContain('\u001b[93m');
102
+ } finally {
103
+ chalk.level = originalChalkLevel;
104
+ }
105
+ });
106
+
107
+ // ── Skill invocation rendering ────────────────────────────────
108
+
109
+ it('skill-activation event renders with "System:" label and message', () => {
110
+ const history: IHistoryEntry[] = [makeSkillInvocationEntry()];
111
+ const { lastFrame } = render(<MessageList history={history} />);
112
+ const output = lastFrame() ?? '';
113
+
114
+ expect(output).toContain('System:');
115
+ expect(output).toContain('Invoking plugin skill: audit');
116
+ });
117
+
118
+ // ── Chat message rendering ────────────────────────────────────
119
+
120
+ it('renders nothing for a recorded event that carries no message', () => {
121
+ const history: IHistoryEntry[] = ['provider-call-trace', 'usage-observation'].map((type) => ({
122
+ id: type,
123
+ timestamp: new Date(),
124
+ category: 'event',
125
+ type,
126
+ data: { traceId: 'trace-1', outcome: 'success' },
127
+ }));
128
+ const { lastFrame } = render(<MessageList history={history} />);
129
+ const output = chalk.reset(lastFrame() ?? '');
130
+
131
+ expect(output).not.toContain('System:');
132
+ expect(output).not.toContain('provider-call-trace');
133
+ expect(output).not.toContain('usage-observation');
134
+ });
135
+
136
+ it('user message renders with "You:" label', () => {
137
+ const history: IHistoryEntry[] = [messageToHistoryEntry(createUserMessage('hello'))];
138
+ const { lastFrame } = render(<MessageList history={history} />);
139
+ const output = lastFrame() ?? '';
140
+
141
+ expect(output).toContain('You:');
142
+ expect(output).toContain('hello');
143
+ });
144
+
145
+ it('assistant message renders with "Assistant:" label', () => {
146
+ const history: IHistoryEntry[] = [
147
+ messageToHistoryEntry(createAssistantMessage('response text')),
148
+ ];
149
+ const { lastFrame } = render(<MessageList history={history} />);
150
+ const output = lastFrame() ?? '';
151
+
152
+ expect(output).toContain('Assistant:');
153
+ expect(output).toContain('response text');
154
+ });
155
+
156
+ it('assistant message preserves CJK and emoji content', () => {
157
+ const content = '긴 한국어 응답과 emoji 🎉 를 표시합니다';
158
+ const history: IHistoryEntry[] = [messageToHistoryEntry(createAssistantMessage(content))];
159
+ const { lastFrame } = render(<MessageList history={history} />);
160
+
161
+ expect(lastFrame()).toContain(content);
162
+ });
163
+
164
+ it('assistant message renders markdown diff fenced code block content', () => {
165
+ const response = [
166
+ 'Patch preview:',
167
+ '',
168
+ '```diff',
169
+ '- const oldValue = true;',
170
+ '+ const newValue = true;',
171
+ '```',
172
+ ].join('\n');
173
+ const history: IHistoryEntry[] = [messageToHistoryEntry(createAssistantMessage(response))];
174
+ const { lastFrame } = render(<MessageList history={history} />);
175
+ const output = lastFrame() ?? '';
176
+
177
+ expect(output).toContain('Assistant:');
178
+ expect(output).toContain('Patch preview:');
179
+ expect(output).toContain('- const oldValue = true;');
180
+ expect(output).toContain('+ const newValue = true;');
181
+ });
182
+
183
+ it('tool message diff summary renders through markdown diff body format', () => {
184
+ const toolSummary = [
185
+ {
186
+ line: 'Edit(/src/index.ts)',
187
+ diffFile: '/src/index.ts',
188
+ diffLines: [
189
+ { type: 'remove', lineNumber: 1, text: 'const oldValue = true;' },
190
+ { type: 'add', lineNumber: 1, text: 'const newValue = true;' },
191
+ ],
192
+ },
193
+ ];
194
+ const history: IHistoryEntry[] = [
195
+ messageToHistoryEntry(
196
+ createToolMessage(JSON.stringify(toolSummary), {
197
+ toolCallId: 'call_1',
198
+ name: 'tools',
199
+ }),
200
+ ),
201
+ ];
202
+
203
+ const { lastFrame } = render(<MessageList history={history} />);
204
+ const output = lastFrame() ?? '';
205
+
206
+ expect(output).toContain('/src/index.ts');
207
+ expect(output).toContain('- 1 | const oldValue = true;');
208
+ expect(output).toContain('+ 1 | const newValue = true;');
209
+ expect(output).not.toContain('│ 1 - const oldValue = true;');
210
+ });
211
+
212
+ it('tool-summary event renders persisted edit diff metadata', () => {
213
+ const history: IHistoryEntry[] = [
214
+ {
215
+ id: 'summary_1',
216
+ timestamp: new Date(),
217
+ category: 'event',
218
+ type: 'tool-summary',
219
+ data: {
220
+ tools: [
221
+ {
222
+ toolName: 'Edit',
223
+ firstArg: '/src/index.ts',
224
+ isRunning: false,
225
+ result: 'success',
226
+ diffFile: '/src/index.ts',
227
+ diffLines: [
228
+ { type: 'remove', lineNumber: 1, text: 'const temporary = true;' },
229
+ { type: 'add', lineNumber: 1, text: 'const original = true;' },
230
+ ],
231
+ },
232
+ ],
233
+ summary: '✓ Edit(/src/index.ts)',
234
+ },
235
+ },
236
+ ];
237
+
238
+ const { lastFrame } = render(<MessageList history={history} />);
239
+ const output = lastFrame() ?? '';
240
+
241
+ expect(output).toContain('/src/index.ts');
242
+ expect(output).toContain('- 1 | const temporary = true;');
243
+ expect(output).toContain('+ 1 | const original = true;');
244
+ });
245
+
246
+ it('tool-summary event collapses long command output with transcript hint', () => {
247
+ const commandOutput = Array.from({ length: 7 }, (_, index) => `line-${index + 1}`).join('\n');
248
+ const history: IHistoryEntry[] = [
249
+ {
250
+ id: 'summary_command_long',
251
+ timestamp: new Date(),
252
+ category: 'event',
253
+ type: 'tool-summary',
254
+ data: {
255
+ tools: [
256
+ {
257
+ toolName: 'Bash',
258
+ firstArg: 'pnpm test',
259
+ isRunning: false,
260
+ result: 'success',
261
+ toolResultData: JSON.stringify({
262
+ success: true,
263
+ output: commandOutput,
264
+ exitCode: 0,
265
+ }),
266
+ },
267
+ ],
268
+ summary: '✓ Bash(pnpm test)',
269
+ },
270
+ },
271
+ ];
272
+
273
+ const { lastFrame } = render(<MessageList history={history} />);
274
+ const output = lastFrame() ?? '';
275
+
276
+ expect(output).toContain('✓ Bash(pnpm test)');
277
+ expect(output).toContain('line-1');
278
+ expect(output).toContain('line-4');
279
+ expect(output).not.toContain('line-5');
280
+ expect(output).toContain('... +3 lines (full output in session transcript)');
281
+ });
282
+
283
+ it('tool-summary event marks non-zero command exits as failed', () => {
284
+ const history: IHistoryEntry[] = [
285
+ {
286
+ id: 'summary_command_failed',
287
+ timestamp: new Date(),
288
+ category: 'event',
289
+ type: 'tool-summary',
290
+ data: {
291
+ tools: [
292
+ {
293
+ toolName: 'Bash',
294
+ firstArg: 'exit 42',
295
+ isRunning: false,
296
+ result: 'success',
297
+ toolResultData: JSON.stringify({ success: true, output: '', exitCode: 42 }),
298
+ },
299
+ ],
300
+ summary: '✓ Bash(exit 42)',
301
+ },
302
+ },
303
+ ];
304
+
305
+ const { lastFrame } = render(<MessageList history={history} />);
306
+ const output = lastFrame() ?? '';
307
+
308
+ expect(output).toContain('✗ Bash(exit 42)');
309
+ expect(output).toContain('exit 42');
310
+ });
311
+
312
+ it('usage-summary event renders compact token and cost visibility', () => {
313
+ const history: IHistoryEntry[] = [
314
+ {
315
+ id: 'usage_1',
316
+ timestamp: new Date(),
317
+ category: 'event',
318
+ type: 'usage-summary',
319
+ data: {
320
+ kind: 'exact',
321
+ scope: 'turn',
322
+ promptTokens: 100,
323
+ completionTokens: 50,
324
+ totalTokens: 150,
325
+ contextUsedTokens: 150,
326
+ contextMaxTokens: 1000,
327
+ contextUsedPercentage: 15,
328
+ costStatus: 'unknown',
329
+ },
330
+ },
331
+ ];
332
+
333
+ const { lastFrame } = render(<MessageList history={history} />);
334
+ const output = lastFrame() ?? '';
335
+
336
+ expect(output).toContain('Usage:');
337
+ expect(output).toContain('exact');
338
+ expect(output).toContain('150 tokens');
339
+ expect(output).toContain('in 100');
340
+ expect(output).toContain('out 50');
341
+ expect(output).toContain('cost unknown');
342
+ });
343
+
344
+ it('usage-summary from another source names it and omits the context window', () => {
345
+ const history: IHistoryEntry[] = [
346
+ {
347
+ id: 'usage_advisor',
348
+ timestamp: new Date(),
349
+ category: 'event',
350
+ type: 'usage-summary',
351
+ data: {
352
+ kind: 'exact',
353
+ scope: 'turn',
354
+ promptTokens: 3000,
355
+ completionTokens: 400,
356
+ totalTokens: 3400,
357
+ contextUsedTokens: 0,
358
+ contextMaxTokens: 0,
359
+ contextUsedPercentage: 0,
360
+ costStatus: 'estimated',
361
+ costUsd: 0.05,
362
+ source: { scope: 'tool', id: 'advisor:big-model', label: 'Advisor (big-model)' },
363
+ },
364
+ },
365
+ ];
366
+
367
+ const output = render(<MessageList history={history} />).lastFrame() ?? '';
368
+
369
+ expect(output).toContain('Usage (Advisor (big-model)):');
370
+ expect(output).toContain('out 400');
371
+ expect(output).not.toContain('Context');
372
+ });
373
+
374
+ it('system message renders with "System:" label', () => {
375
+ const history: IHistoryEntry[] = [
376
+ messageToHistoryEntry(createSystemMessage('Interrupted by user.')),
377
+ ];
378
+ const { lastFrame } = render(<MessageList history={history} />);
379
+ const output = lastFrame() ?? '';
380
+
381
+ expect(output).toContain('System:');
382
+ expect(output).toContain('Interrupted by user.');
383
+ });
384
+
385
+ // ── Display order after abort ─────────────────────────────────
386
+
387
+ it('abort display order: You → Tool → Assistant → System', () => {
388
+ const history: IHistoryEntry[] = [
389
+ messageToHistoryEntry(createUserMessage('/audit')),
390
+ makeToolSummaryEntry(),
391
+ messageToHistoryEntry(createAssistantMessage('partial response')),
392
+ messageToHistoryEntry(createSystemMessage('Interrupted by user.')),
393
+ ];
394
+
395
+ const { lastFrame } = render(<MessageList history={history} />);
396
+ const output = lastFrame() ?? '';
397
+
398
+ const youIdx = output.indexOf('You:');
399
+ const toolIdx = output.indexOf('Tool:');
400
+ const assistantIdx = output.indexOf('Assistant:');
401
+ const systemIdx = output.indexOf('Interrupted by user.');
402
+
403
+ // All must be present
404
+ expect(youIdx).toBeGreaterThanOrEqual(0);
405
+ expect(toolIdx).toBeGreaterThanOrEqual(0);
406
+ expect(assistantIdx).toBeGreaterThanOrEqual(0);
407
+ expect(systemIdx).toBeGreaterThanOrEqual(0);
408
+
409
+ // Order: You → Tool → Assistant → System
410
+ expect(youIdx).toBeLessThan(toolIdx);
411
+ expect(toolIdx).toBeLessThan(assistantIdx);
412
+ expect(assistantIdx).toBeLessThan(systemIdx);
413
+ });
414
+
415
+ // ── Mixed history ─────────────────────────────────────────────
416
+
417
+ it('renders mixed chat and event entries in order', () => {
418
+ const history: IHistoryEntry[] = [
419
+ messageToHistoryEntry(createUserMessage('hello')),
420
+ makeSkillInvocationEntry(),
421
+ makeToolSummaryEntry(),
422
+ messageToHistoryEntry(createAssistantMessage('done')),
423
+ ];
424
+
425
+ const { lastFrame } = render(<MessageList history={history} />);
426
+ const output = lastFrame() ?? '';
427
+
428
+ // All four entries rendered
429
+ expect(output).toContain('You:');
430
+ expect(output).toContain('Invoking plugin skill: audit');
431
+ expect(output).toContain('Tool:');
432
+ expect(output).toContain('Assistant:');
433
+ });
434
+ });
435
+
436
+ /**
437
+ * PEER-007 (issue #1915) — a co-driven transcript must say WHO drove each turn.
438
+ *
439
+ * Before this, every user message rendered `You:`, so a peer session's message was
440
+ * indistinguishable from the operator's own — the transcript asserted something false about who
441
+ * spoke. The label reads the DERIVED driver id (`peer:<session-id>`) carried on the message; it is
442
+ * display attribution and never an authorization signal (issue #1809).
443
+ */
444
+ describe('MessageList driver attribution (PEER-007)', () => {
445
+ it("labels a peer-driven message with the peer's id instead of You:", () => {
446
+ const entry = messageToHistoryEntry(
447
+ createUserMessage('deploy the staging branch', {
448
+ metadata: { driverId: 'peer:session-abc' },
449
+ }),
450
+ );
451
+ const { lastFrame } = render(<MessageList history={[entry]} />);
452
+ const output = chalk.reset(lastFrame() ?? '');
453
+
454
+ expect(output).toContain('peer:session-abc:');
455
+ expect(output).not.toContain('You:');
456
+ });
457
+
458
+ it('still labels the owner as You:', () => {
459
+ const entry = messageToHistoryEntry(
460
+ createUserMessage('deploy the staging branch', { metadata: { driverId: 'owner' } }),
461
+ );
462
+ const { lastFrame } = render(<MessageList history={[entry]} />);
463
+
464
+ expect(chalk.reset(lastFrame() ?? '')).toContain('You:');
465
+ });
466
+
467
+ it('labels an unattributed message You: — the pre-PEER-007 transcript is unchanged', () => {
468
+ const entry = messageToHistoryEntry(createUserMessage('deploy the staging branch'));
469
+ const { lastFrame } = render(<MessageList history={[entry]} />);
470
+
471
+ expect(chalk.reset(lastFrame() ?? '')).toContain('You:');
472
+ });
473
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * CLI-083 (issue #2287) — the org policy survives the TUI projection.
3
+ *
4
+ * `toChannelOptions` builds the channel's options field by field. `orgPolicy` was not among them, so
5
+ * a policy forwarded from the shell arrived here and was dropped, and `blockedCommands` enforcement
6
+ * stayed dead on the plain `robota` path — the most common one — after two rounds of wiring it.
7
+ *
8
+ * The reason it was silent is worth keeping: the shell forwards with
9
+ * `...(orgPolicy === null ? {} : { orgPolicy })`, and a SPREAD BYPASSES TypeScript's excess-property
10
+ * check. Written as `orgPolicy,` it would not have compiled against an `IRenderOptions` that lacks
11
+ * the field. The idiom chosen to be safe about optionality disabled the check that would have caught
12
+ * the missing declaration.
13
+ */
14
+
15
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+
19
+ import { loadOrgPolicy } from '@robota-sdk/agent-framework';
20
+ import { describe, expect, it, vi } from 'vitest';
21
+
22
+ import { toChannelOptions } from '../render.js';
23
+ import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
24
+ import { buildTuiSessionOptions } from '../tui-session-options.js';
25
+
26
+ import type { IRenderOptions } from '../render.js';
27
+
28
+ const POLICY = { blockedCommands: ['clear'], adminContact: 'ops@x' };
29
+
30
+ function renderOptions(extra: Partial<IRenderOptions> = {}): IRenderOptions {
31
+ return { cwd: '/work', provider: {} as never, ...extra } as IRenderOptions;
32
+ }
33
+
34
+ describe('CLI-083: the TUI projection carries the org policy', () => {
35
+ it('copies a supplied policy into the channel options', () => {
36
+ const channel = toChannelOptions(renderOptions({ orgPolicy: POLICY as never }));
37
+
38
+ expect((channel as { orgPolicy?: unknown }).orgPolicy).toEqual(POLICY);
39
+ });
40
+
41
+ it('carries none when none was supplied, so absence stays distinguishable', () => {
42
+ const channel = toChannelOptions(renderOptions());
43
+
44
+ expect((channel as { orgPolicy?: unknown }).orgPolicy).toBeUndefined();
45
+ });
46
+
47
+ it('preserves the same policy through the channel into the session', () => {
48
+ const channel = toChannelOptions(renderOptions({ orgPolicy: POLICY as never }));
49
+
50
+ expect((buildTuiSessionOptions(channel) as { orgPolicy?: unknown }).orgPolicy).toBe(POLICY);
51
+ });
52
+
53
+ it('enforces a blocked command from a policy file in a real TUI-owned session', async () => {
54
+ const home = realpathSync(mkdtempSync(join(tmpdir(), 'robota-tui-policy-')));
55
+ mkdirSync(join(home, '.robota'));
56
+ writeFileSync(join(home, '.robota', 'org-policy.json'), JSON.stringify(POLICY));
57
+ vi.stubEnv('HOME', home);
58
+ let channel: TuiInteractionChannel | undefined;
59
+ try {
60
+ const policy = loadOrgPolicy(join(home, '.robota', 'org-policy.json'));
61
+ expect(policy).not.toBeNull();
62
+ channel = new TuiInteractionChannel(
63
+ toChannelOptions(renderOptions({ cwd: home, orgPolicy: policy ?? undefined })),
64
+ );
65
+
66
+ const result = await channel.getSession().executeCommand('clear', '');
67
+
68
+ expect(result?.success).toBe(false);
69
+ expect(result?.message).toContain('Command /clear is blocked');
70
+ expect(result?.message).toContain('ops@x');
71
+ } finally {
72
+ await channel?.stop();
73
+ vi.unstubAllEnvs();
74
+ rmSync(home, { recursive: true, force: true });
75
+ }
76
+ });
77
+ });
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { toChannelOptions } from '../render.js';
4
+ import { buildTuiSessionOptions } from '../tui-session-options.js';
5
+
6
+ import type { IRenderOptions } from '../render.js';
7
+ import type { ITuiInteractionChannelOptions } from '../TuiInteractionChannel.js';
8
+ import type { IOutputStylePrompt } from '@robota-sdk/agent-framework';
9
+
10
+ const STYLE: IOutputStylePrompt = {
11
+ id: 'concise',
12
+ name: 'Concise',
13
+ instructions: 'Lead with the answer.',
14
+ keepCodingInstructions: true,
15
+ tokenCost: 'low',
16
+ };
17
+
18
+ const renderOptions = (over: Partial<IRenderOptions> = {}): IRenderOptions =>
19
+ ({ cwd: '/w', provider: {}, ...over }) as IRenderOptions;
20
+
21
+ describe('CLI-1988 output-style projection', () => {
22
+ it('preserves the resolved style from render options through the TUI session options', () => {
23
+ const channel = toChannelOptions(renderOptions({ outputStyle: STYLE }));
24
+ expect(channel.outputStyle).toBe(STYLE);
25
+
26
+ const session = buildTuiSessionOptions(channel as ITuiInteractionChannelOptions);
27
+ expect(session).toHaveProperty('outputStyle', STYLE);
28
+ });
29
+
30
+ it('omits the style when no startup selection was supplied', () => {
31
+ expect(
32
+ 'outputStyle' in buildTuiSessionOptions(renderOptions() as ITuiInteractionChannelOptions),
33
+ ).toBe(false);
34
+ });
35
+ });