@robota-sdk/agent-transport 3.0.0-beta.75 → 3.0.0-beta.77

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 (231) hide show
  1. package/LICENSE +661 -21
  2. package/README.md +17 -14
  3. package/dist/node/headless/index.cjs +1 -1
  4. package/dist/node/headless/index.d.ts +1 -1
  5. package/dist/node/headless/index.js +1 -1
  6. package/dist/node/{headless-mRYilLfC.js → headless-DG_MEbf0.js} +7 -4
  7. package/dist/node/headless-DG_MEbf0.js.map +1 -0
  8. package/dist/node/headless-xq77JBq0.cjs +18 -0
  9. package/dist/node/{index-E8Gx4-lc.d.ts → index-CrJ2Lhn3.d.ts} +16 -3
  10. package/dist/node/index-CrJ2Lhn3.d.ts.map +1 -0
  11. package/dist/node/index-JIvzrtaA.d.ts +52 -0
  12. package/dist/node/index-JIvzrtaA.d.ts.map +1 -0
  13. package/dist/node/{index-CYl7ksS6.d.ts → index-JTD_xtEQ.d.ts} +15 -2
  14. package/dist/node/index-JTD_xtEQ.d.ts.map +1 -0
  15. package/dist/node/index-y_HBOfzO.d.ts +52 -0
  16. package/dist/node/index-y_HBOfzO.d.ts.map +1 -0
  17. package/dist/node/index.cjs +1 -1
  18. package/dist/node/index.d.ts +10 -9
  19. package/dist/node/index.d.ts.map +1 -1
  20. package/dist/node/index.js +1 -1
  21. package/dist/node/index.js.map +1 -1
  22. package/dist/node/programmatic/index.cjs +1 -0
  23. package/dist/node/programmatic/index.d.ts +2 -0
  24. package/dist/node/programmatic/index.js +1 -0
  25. package/dist/node/programmatic-6wLf68qJ.cjs +1 -0
  26. package/dist/node/programmatic-70dxIXtS.js +2 -0
  27. package/dist/node/programmatic-70dxIXtS.js.map +1 -0
  28. package/dist/node/testing/index.cjs +1 -1
  29. package/dist/node/testing/index.d.ts +2 -21
  30. package/dist/node/testing/index.js +1 -2
  31. package/package.json +15 -71
  32. package/src/__tests__/programmatic/programmatic-ask-user.test.ts +32 -0
  33. package/src/__tests__/programmatic/programmatic-driver.test.ts +107 -0
  34. package/src/__tests__/programmatic/programmatic-flows.test.ts +140 -0
  35. package/src/__tests__/transport-registry.stop-all.test.ts +53 -0
  36. package/src/headless/HeadlessInteractionChannel.ts +22 -5
  37. package/src/headless/__tests__/headless-goal-runner.test.ts +108 -0
  38. package/src/headless/__tests__/headless-runner.test.ts +2 -1
  39. package/src/headless/headless-runner.ts +67 -1
  40. package/src/headless/headless-stream-json.ts +1 -1
  41. package/src/headless/print-terminal.ts +1 -0
  42. package/src/index.ts +2 -5
  43. package/src/programmatic/ProgrammaticInteractionChannel.ts +82 -0
  44. package/src/programmatic/createProgrammaticAgent.ts +71 -0
  45. package/src/programmatic/index.ts +5 -0
  46. package/src/testing/index.ts +7 -4
  47. package/src/transport-registry.ts +15 -12
  48. package/dist/node/headless-CT2ibQnr.cjs +0 -15
  49. package/dist/node/headless-mRYilLfC.js.map +0 -1
  50. package/dist/node/http/index.cjs +0 -1
  51. package/dist/node/http/index.d.ts +0 -2
  52. package/dist/node/http/index.js +0 -1
  53. package/dist/node/http-2Jiuflc1.js +0 -2
  54. package/dist/node/http-2Jiuflc1.js.map +0 -1
  55. package/dist/node/http-CBAvefLw.cjs +0 -1
  56. package/dist/node/index-BNccqSpv.d.ts +0 -86
  57. package/dist/node/index-BNccqSpv.d.ts.map +0 -1
  58. package/dist/node/index-BUhHIf7X.d.ts +0 -86
  59. package/dist/node/index-BUhHIf7X.d.ts.map +0 -1
  60. package/dist/node/index-BnAGE-u9.d.ts +0 -33
  61. package/dist/node/index-BnAGE-u9.d.ts.map +0 -1
  62. package/dist/node/index-BrQ4gGw0.d.ts +0 -213
  63. package/dist/node/index-BrQ4gGw0.d.ts.map +0 -1
  64. package/dist/node/index-CYl7ksS6.d.ts.map +0 -1
  65. package/dist/node/index-CoeBF21y.d.ts +0 -213
  66. package/dist/node/index-CoeBF21y.d.ts.map +0 -1
  67. package/dist/node/index-DHt-2VQ-.d.ts +0 -46
  68. package/dist/node/index-DHt-2VQ-.d.ts.map +0 -1
  69. package/dist/node/index-DMwKN5Le.d.ts +0 -33
  70. package/dist/node/index-DMwKN5Le.d.ts.map +0 -1
  71. package/dist/node/index-E8Gx4-lc.d.ts.map +0 -1
  72. package/dist/node/index-c0M42fsA.d.ts +0 -46
  73. package/dist/node/index-c0M42fsA.d.ts.map +0 -1
  74. package/dist/node/mcp/index.cjs +0 -1
  75. package/dist/node/mcp/index.d.ts +0 -2
  76. package/dist/node/mcp/index.js +0 -1
  77. package/dist/node/mcp-BOglBJNy.cjs +0 -1
  78. package/dist/node/mcp-D3BBVK7C.js +0 -2
  79. package/dist/node/mcp-D3BBVK7C.js.map +0 -1
  80. package/dist/node/rolldown-runtime-CMqjfN_6.cjs +0 -1
  81. package/dist/node/testing/index.d.ts.map +0 -1
  82. package/dist/node/testing/index.js.map +0 -1
  83. package/dist/node/tui/index.cjs +0 -1
  84. package/dist/node/tui/index.d.ts +0 -2
  85. package/dist/node/tui/index.js +0 -1
  86. package/dist/node/tui-CcH5EsQh.js +0 -25
  87. package/dist/node/tui-CcH5EsQh.js.map +0 -1
  88. package/dist/node/tui-DznRbcku.cjs +0 -24
  89. package/dist/node/ws/index.cjs +0 -1
  90. package/dist/node/ws/index.d.ts +0 -2
  91. package/dist/node/ws/index.js +0 -1
  92. package/dist/node/ws-Dc2RUwVs.js +0 -2
  93. package/dist/node/ws-Dc2RUwVs.js.map +0 -1
  94. package/dist/node/ws-QNMQn5kg.cjs +0 -1
  95. package/src/http/__tests__/http-transport.test.ts +0 -55
  96. package/src/http/__tests__/routes.test.ts +0 -168
  97. package/src/http/http-transport.ts +0 -41
  98. package/src/http/index.ts +0 -4
  99. package/src/http/routes.ts +0 -152
  100. package/src/mcp/__tests__/mcp-server.test.ts +0 -66
  101. package/src/mcp/__tests__/mcp-transport.test.ts +0 -46
  102. package/src/mcp/index.ts +0 -4
  103. package/src/mcp/mcp-server.ts +0 -163
  104. package/src/mcp/mcp-transport.ts +0 -48
  105. package/src/testing/__tests__/scripted-provider.test.ts +0 -73
  106. package/src/testing/scripted-provider.ts +0 -73
  107. package/src/tui/App.tsx +0 -491
  108. package/src/tui/BackgroundTaskPanel.tsx +0 -36
  109. package/src/tui/CjkTextInput.tsx +0 -199
  110. package/src/tui/ConfirmPrompt.tsx +0 -70
  111. package/src/tui/ContextWarningBanner.tsx +0 -34
  112. package/src/tui/ExecutionWorkspaceDetailPane.tsx +0 -64
  113. package/src/tui/ExecutionWorkspaceSwitcher.tsx +0 -187
  114. package/src/tui/InputArea.tsx +0 -310
  115. package/src/tui/InteractivePrompt.tsx +0 -59
  116. package/src/tui/ListPicker.tsx +0 -95
  117. package/src/tui/MenuSelect.tsx +0 -104
  118. package/src/tui/MessageList.tsx +0 -284
  119. package/src/tui/PermissionPrompt.tsx +0 -86
  120. package/src/tui/PluginTUI.tsx +0 -258
  121. package/src/tui/SessionPicker.tsx +0 -68
  122. package/src/tui/SessionStatusBar.tsx +0 -73
  123. package/src/tui/SlashAutocomplete.tsx +0 -110
  124. package/src/tui/StatusBar.tsx +0 -236
  125. package/src/tui/StreamingIndicator.tsx +0 -93
  126. package/src/tui/TextPrompt.tsx +0 -81
  127. package/src/tui/ToolCommandOutput.tsx +0 -39
  128. package/src/tui/ToolDiffBlock.tsx +0 -32
  129. package/src/tui/TransportTUI.tsx +0 -117
  130. package/src/tui/TuiInteractionChannel.ts +0 -495
  131. package/src/tui/UpdateNotice.tsx +0 -14
  132. package/src/tui/UsageSummaryEntry.tsx +0 -39
  133. package/src/tui/WaveText.tsx +0 -44
  134. package/src/tui/__tests__/InteractivePrompt.test.tsx +0 -82
  135. package/src/tui/__tests__/ListPicker.test.tsx +0 -159
  136. package/src/tui/__tests__/MenuSelect.test.tsx +0 -103
  137. package/src/tui/__tests__/PluginTUI.test.tsx +0 -167
  138. package/src/tui/__tests__/SlashAutocomplete.test.tsx +0 -140
  139. package/src/tui/__tests__/TextPrompt.test.tsx +0 -98
  140. package/src/tui/__tests__/TuiInteractionChannel.display-contract.test.ts +0 -239
  141. package/src/tui/__tests__/TuiInteractionChannel.lifecycle.test.ts +0 -297
  142. package/src/tui/__tests__/TuiInteractionChannel.requestAction.test.ts +0 -124
  143. package/src/tui/__tests__/UpdateNotice.test.tsx +0 -15
  144. package/src/tui/__tests__/abort-after-permission.test.tsx +0 -169
  145. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +0 -183
  146. package/src/tui/__tests__/background-task-panel.test.tsx +0 -53
  147. package/src/tui/__tests__/background-task-row-format.test.ts +0 -59
  148. package/src/tui/__tests__/channel-factory-integration.test.ts +0 -138
  149. package/src/tui/__tests__/cjk-text-input-flow.test.ts +0 -109
  150. package/src/tui/__tests__/cjk-text-input.test.ts +0 -191
  151. package/src/tui/__tests__/command-effect-handler.test.ts +0 -127
  152. package/src/tui/__tests__/command-output-summary.test.ts +0 -95
  153. package/src/tui/__tests__/compact-event-bridge.test.ts +0 -20
  154. package/src/tui/__tests__/confirm-permission-flow.test.ts +0 -130
  155. package/src/tui/__tests__/confirm-prompt.test.tsx +0 -87
  156. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +0 -110
  157. package/src/tui/__tests__/execution-workspace-view-model.test.ts +0 -93
  158. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +0 -125
  159. package/src/tui/__tests__/input-area-flow.test.ts +0 -164
  160. package/src/tui/__tests__/message-list-rendering.test.tsx +0 -353
  161. package/src/tui/__tests__/prompt-queue.test.tsx +0 -255
  162. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +0 -233
  163. package/src/tui/__tests__/pty/pty-driver.ts +0 -135
  164. package/src/tui/__tests__/pty/tui-pty.ptytest.ts +0 -61
  165. package/src/tui/__tests__/render-channel-options.test.ts +0 -32
  166. package/src/tui/__tests__/render-markdown.test.ts +0 -72
  167. package/src/tui/__tests__/selection-flow.test.ts +0 -61
  168. package/src/tui/__tests__/session-init-poller.test.ts +0 -102
  169. package/src/tui/__tests__/session-naming.test.ts +0 -64
  170. package/src/tui/__tests__/session-switch-channel.test.tsx +0 -307
  171. package/src/tui/__tests__/slash-routing-effects.test.ts +0 -228
  172. package/src/tui/__tests__/status-activity.test.ts +0 -71
  173. package/src/tui/__tests__/status-bar.test.tsx +0 -177
  174. package/src/tui/__tests__/streaming-indicator.test.tsx +0 -137
  175. package/src/tui/__tests__/text-prompt-flow.test.ts +0 -77
  176. package/src/tui/__tests__/tui-channel-init-failure.test.ts +0 -57
  177. package/src/tui/__tests__/tui-state-manager.test.ts +0 -401
  178. package/src/tui/background-task-row-format.ts +0 -53
  179. package/src/tui/command-interaction.ts +0 -9
  180. package/src/tui/command-output-summary.ts +0 -122
  181. package/src/tui/create-default-tui-cli-adapter.ts +0 -41
  182. package/src/tui/execution-workspace-view-model.ts +0 -123
  183. package/src/tui/flows/cjk-text-input-flow.ts +0 -285
  184. package/src/tui/flows/confirm-prompt-flow.ts +0 -45
  185. package/src/tui/flows/input-area-flow.ts +0 -189
  186. package/src/tui/flows/permission-prompt-flow.ts +0 -85
  187. package/src/tui/flows/selection-flow.ts +0 -126
  188. package/src/tui/flows/session-init-poller.ts +0 -77
  189. package/src/tui/flows/text-prompt-flow.ts +0 -98
  190. package/src/tui/hooks/command-effect-handler.ts +0 -97
  191. package/src/tui/hooks/command-effect-queue.ts +0 -39
  192. package/src/tui/hooks/side-effects-types.ts +0 -35
  193. package/src/tui/hooks/useAutocomplete.ts +0 -87
  194. package/src/tui/hooks/usePluginCallbacks.ts +0 -31
  195. package/src/tui/hooks/usePluginScreenData.ts +0 -85
  196. package/src/tui/hooks/useSideEffects.ts +0 -175
  197. package/src/tui/hooks/useSlashRouting.ts +0 -118
  198. package/src/tui/hooks/useStatusLineSettings.ts +0 -37
  199. package/src/tui/hooks/useTuiChannel.ts +0 -95
  200. package/src/tui/index.ts +0 -14
  201. package/src/tui/interactions/CommandConfirm.tsx +0 -36
  202. package/src/tui/interactions/CommandPicker.tsx +0 -77
  203. package/src/tui/interactions/__tests__/CommandConfirm.test.tsx +0 -124
  204. package/src/tui/interactions/__tests__/CommandPicker.test.tsx +0 -138
  205. package/src/tui/plugin-tui-handlers.ts +0 -163
  206. package/src/tui/render-markdown.ts +0 -130
  207. package/src/tui/render.tsx +0 -129
  208. package/src/tui/session-naming.ts +0 -33
  209. package/src/tui/status-activity.ts +0 -63
  210. package/src/tui/tui-cli-adapter-context.tsx +0 -13
  211. package/src/tui/tui-cli-adapter.ts +0 -25
  212. package/src/tui/tui-state-manager.ts +0 -226
  213. package/src/tui/tui-transport.ts +0 -35
  214. package/src/tui/types.ts +0 -15
  215. package/src/tui/utils/__tests__/edit-diff.test.ts +0 -426
  216. package/src/tui/utils/__tests__/paste-detection.test.ts +0 -116
  217. package/src/tui/utils/__tests__/paste-labels.test.ts +0 -46
  218. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +0 -227
  219. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +0 -104
  220. package/src/tui/utils/edit-diff.ts +0 -153
  221. package/src/tui/utils/paste-labels.ts +0 -9
  222. package/src/tui/utils/tool-call-extractor.ts +0 -92
  223. package/src/tui/utils/tool-diff-summary.ts +0 -75
  224. package/src/ws/__tests__/ws-handler.test.ts +0 -409
  225. package/src/ws/__tests__/ws-transport.test.ts +0 -53
  226. package/src/ws/index.ts +0 -13
  227. package/src/ws/ws-background-messages.ts +0 -170
  228. package/src/ws/ws-handler.ts +0 -280
  229. package/src/ws/ws-protocol.ts +0 -78
  230. package/src/ws/ws-transport-configurable.ts +0 -128
  231. package/src/ws/ws-transport.ts +0 -42
@@ -1,124 +0,0 @@
1
- /**
2
- * Unit tests for TuiInteractionChannel.requestAction() promise protocol.
3
- *
4
- * Tests the queue-based action resolution mechanism in isolation —
5
- * no Ink rendering, no InteractiveSession, no real provider required.
6
- */
7
- import { describe, it, expect, vi, beforeEach } from 'vitest';
8
-
9
- vi.mock('@robota-sdk/agent-framework', async () => {
10
- const actual = await vi.importActual<typeof import('@robota-sdk/agent-framework')>(
11
- '@robota-sdk/agent-framework',
12
- );
13
- return {
14
- ...actual,
15
- InteractiveSession: vi.fn().mockImplementation(() => ({
16
- getFullHistory: vi.fn().mockReturnValue([]),
17
- setName: vi.fn(),
18
- getSessionId: vi.fn().mockReturnValue('test-id'),
19
- isInitialized: false,
20
- on: vi.fn(),
21
- off: vi.fn(),
22
- })),
23
- CommandRegistry: vi.fn().mockImplementation(() => ({
24
- addModule: vi.fn(),
25
- })),
26
- };
27
- });
28
-
29
- import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
30
-
31
- import type { IAIProvider } from '@robota-sdk/agent-core';
32
- import type { IActionRequest } from '@robota-sdk/agent-interface-transport';
33
-
34
- function makeChannel(): TuiInteractionChannel {
35
- return new TuiInteractionChannel({
36
- cwd: '/tmp',
37
- provider: {} as IAIProvider,
38
- });
39
- }
40
-
41
- const PICK_ACTION: IActionRequest = {
42
- type: 'pick',
43
- id: 'mode',
44
- title: '/mode',
45
- items: [
46
- { label: 'plan', value: 'plan' },
47
- { label: 'default', value: 'default' },
48
- ],
49
- };
50
-
51
- const CONFIRM_ACTION: IActionRequest = {
52
- type: 'confirm',
53
- id: 'exit',
54
- message: 'Exit the session?',
55
- };
56
-
57
- describe('TuiInteractionChannel.requestAction', () => {
58
- let channel: TuiInteractionChannel;
59
-
60
- beforeEach(() => {
61
- channel = makeChannel();
62
- });
63
-
64
- it('sets pendingAction when requestAction is called', () => {
65
- void channel.requestAction(PICK_ACTION);
66
- expect(channel.pendingAction).toMatchObject({ type: 'pick', id: 'mode' });
67
- });
68
-
69
- it('resolves pick response when resolveAction is called', async () => {
70
- const responsePromise = channel.requestAction(PICK_ACTION);
71
- channel.resolveAction({ type: 'pick', item: { label: 'plan', value: 'plan' } });
72
- const response = await responsePromise;
73
- expect(response).toEqual({ type: 'pick', item: { label: 'plan', value: 'plan' } });
74
- });
75
-
76
- it('clears pendingAction after resolveAction', async () => {
77
- const responsePromise = channel.requestAction(PICK_ACTION);
78
- channel.resolveAction({ type: 'cancelled' });
79
- await responsePromise;
80
- expect(channel.pendingAction).toBeNull();
81
- });
82
-
83
- it('resolves confirm response when resolveAction is called', async () => {
84
- const responsePromise = channel.requestAction(CONFIRM_ACTION);
85
- channel.resolveAction({ type: 'confirm', confirmed: true });
86
- const response = await responsePromise;
87
- expect(response).toEqual({ type: 'confirm', confirmed: true });
88
- });
89
-
90
- it('resolves cancelled when resolveAction is called with cancelled', async () => {
91
- const responsePromise = channel.requestAction(PICK_ACTION);
92
- channel.resolveAction({ type: 'cancelled' });
93
- const response = await responsePromise;
94
- expect(response).toEqual({ type: 'cancelled' });
95
- });
96
-
97
- it('queues multiple actions and processes them sequentially', async () => {
98
- const p1 = channel.requestAction(PICK_ACTION);
99
- const p2 = channel.requestAction(CONFIRM_ACTION);
100
-
101
- // First action is pending immediately
102
- expect(channel.pendingAction).toMatchObject({ type: 'pick' });
103
-
104
- // Resolve first
105
- channel.resolveAction({ type: 'pick', item: { label: 'plan', value: 'plan' } });
106
- await p1;
107
-
108
- // Second action becomes pending after first resolves
109
- expect(channel.pendingAction).toMatchObject({ type: 'confirm' });
110
-
111
- // Resolve second
112
- channel.resolveAction({ type: 'confirm', confirmed: false });
113
- const r2 = await p2;
114
- expect(r2).toEqual({ type: 'confirm', confirmed: false });
115
- expect(channel.pendingAction).toBeNull();
116
- });
117
-
118
- it('calls onChange when pendingAction changes', () => {
119
- const onChange = vi.fn();
120
- channel.onChange = onChange;
121
- void channel.requestAction(PICK_ACTION);
122
- expect(onChange).toHaveBeenCalled();
123
- });
124
- });
@@ -1,15 +0,0 @@
1
- import React from 'react';
2
- import { render } from 'ink-testing-library';
3
- import { describe, expect, it } from 'vitest';
4
- import UpdateNotice from '../UpdateNotice.js';
5
-
6
- describe('UpdateNotice', () => {
7
- it('renders an update notice outside session history', () => {
8
- const { lastFrame } = render(
9
- <UpdateNotice message="Robota update available. Run npm install -g '@robota-sdk/agent-cli@latest'." />,
10
- );
11
-
12
- expect(lastFrame()).toContain('Robota update available');
13
- expect(lastFrame()).toContain('npm install');
14
- });
15
- });
@@ -1,169 +0,0 @@
1
- /**
2
- * Test: ESC abort after permission prompt was shown and dismissed.
3
- * Verifies that the global ESC handler remains available after overlays close.
4
- */
5
-
6
- import React, { useState, useCallback } from 'react';
7
- import { render } from 'ink-testing-library';
8
- import { Box, Text, useInput } from 'ink';
9
- import { describe, it, expect } from 'vitest';
10
-
11
- /**
12
- * Simulates App's global ESC handler with permission prompt overlay guard.
13
- * 1. Start with "thinking" active
14
- * 2. Permission prompt appears (App-level ESC ignores while overlay is active)
15
- * 3. Permission resolved (App-level ESC handles abort again)
16
- * 4. ESC should trigger abort
17
- */
18
- function AbortAfterPermissionApp({
19
- onAbort,
20
- onPermissionReady,
21
- }: {
22
- onAbort: () => void;
23
- onPermissionReady: (grantPermission: () => void) => void;
24
- }): React.ReactElement {
25
- const [isThinking, setIsThinking] = useState(true);
26
- const [permissionRequest, setPermissionRequest] = useState<{
27
- resolve: () => void;
28
- } | null>(null);
29
- const [aborted, setAborted] = useState(false);
30
-
31
- // Simulate permission prompt appearing after mount
32
- const showPermission = useCallback(() => {
33
- setPermissionRequest({
34
- resolve: () => {
35
- setPermissionRequest(null);
36
- },
37
- });
38
- }, []);
39
-
40
- // Give parent a way to grant permission
41
- React.useEffect(() => {
42
- // Show permission prompt immediately
43
- const pr = {
44
- resolve: () => setPermissionRequest(null),
45
- };
46
- setPermissionRequest(pr);
47
- onPermissionReady(() => pr.resolve());
48
- }, [onPermissionReady]);
49
-
50
- // App's ESC handler — same pattern as real App.tsx
51
- useInput((_input: string, key: { escape: boolean }) => {
52
- if (!key.escape || !isThinking) return;
53
- if (permissionRequest) return;
54
- setAborted(true);
55
- onAbort();
56
- });
57
-
58
- // Permission prompt's own useInput (when active)
59
- useInput(
60
- (_input: string, key: { return: boolean }) => {
61
- if (key.return && permissionRequest) {
62
- permissionRequest.resolve();
63
- }
64
- },
65
- { isActive: !!permissionRequest },
66
- );
67
-
68
- return (
69
- <Box flexDirection="column">
70
- {permissionRequest && <Text color="yellow">[Permission Required]</Text>}
71
- {!permissionRequest && isThinking && <Text color="cyan">Streaming...</Text>}
72
- {aborted && <Text color="red">Aborted!</Text>}
73
- <Text dimColor>
74
- thinking={String(isThinking)} permission={String(!!permissionRequest)} aborted=
75
- {String(aborted)}
76
- </Text>
77
- </Box>
78
- );
79
- }
80
-
81
- describe('ESC abort after permission prompt', () => {
82
- it('ESC works when no permission prompt was shown', async () => {
83
- let abortCalled = false;
84
- const grantHolder: { fn: (() => void) | null } = { fn: null };
85
-
86
- const { stdin, lastFrame } = render(
87
- <AbortAfterPermissionApp
88
- onAbort={() => {
89
- abortCalled = true;
90
- }}
91
- onPermissionReady={(fn) => {
92
- grantHolder.fn = fn;
93
- }}
94
- />,
95
- );
96
-
97
- // Wait for mount
98
- await new Promise((r) => setTimeout(r, 20));
99
-
100
- // Grant permission immediately
101
- grantHolder.fn?.();
102
- await new Promise((r) => setTimeout(r, 50));
103
-
104
- // Now ESC should work
105
- stdin.write('\x1B');
106
- await new Promise((r) => setTimeout(r, 50));
107
-
108
- expect(abortCalled).toBe(true);
109
- expect(lastFrame()!).toContain('Aborted!');
110
- });
111
-
112
- it('ESC works AFTER permission prompt was shown and dismissed', async () => {
113
- let abortCalled = false;
114
- const grantHolder: { fn: (() => void) | null } = { fn: null };
115
-
116
- const { stdin, lastFrame } = render(
117
- <AbortAfterPermissionApp
118
- onAbort={() => {
119
- abortCalled = true;
120
- }}
121
- onPermissionReady={(fn) => {
122
- grantHolder.fn = fn;
123
- }}
124
- />,
125
- );
126
-
127
- // Wait for permission prompt to appear
128
- await new Promise((r) => setTimeout(r, 20));
129
- expect(lastFrame()!).toContain('[Permission Required]');
130
-
131
- // Grant permission (dismiss prompt)
132
- grantHolder.fn?.();
133
- await new Promise((r) => setTimeout(r, 50));
134
-
135
- // Permission dismissed, streaming should show
136
- expect(lastFrame()!).toContain('Streaming...');
137
- expect(lastFrame()!).not.toContain('[Permission Required]');
138
-
139
- // Now press ESC — should trigger abort
140
- stdin.write('\x1B');
141
- await new Promise((r) => setTimeout(r, 50));
142
-
143
- expect(abortCalled).toBe(true);
144
- expect(lastFrame()!).toContain('Aborted!');
145
- });
146
-
147
- it('ESC does NOT work during permission prompt', async () => {
148
- let abortCalled = false;
149
-
150
- const { stdin, lastFrame } = render(
151
- <AbortAfterPermissionApp
152
- onAbort={() => {
153
- abortCalled = true;
154
- }}
155
- onPermissionReady={() => {}}
156
- />,
157
- );
158
-
159
- await new Promise((r) => setTimeout(r, 20));
160
- expect(lastFrame()!).toContain('[Permission Required]');
161
-
162
- // ESC during permission prompt should NOT trigger abort
163
- stdin.write('\x1B');
164
- await new Promise((r) => setTimeout(r, 50));
165
-
166
- expect(abortCalled).toBe(false);
167
- expect(lastFrame()!).not.toContain('Aborted!');
168
- });
169
- });
@@ -1,183 +0,0 @@
1
- /**
2
- * E2E-style test for abort during streaming.
3
- * Uses ink-testing-library to verify that:
4
- * 1. Streaming text debounce works (renders batched, not per-delta)
5
- * 2. ESC during streaming triggers abort
6
- * 3. Partial text is preserved after abort
7
- */
8
-
9
- import React, { useState, useCallback, useRef, useEffect } from 'react';
10
- import { render } from 'ink-testing-library';
11
- import { Box, Text, useInput } from 'ink';
12
- import { describe, it, expect } from 'vitest';
13
-
14
- /**
15
- * Minimal streaming component that simulates the debounced onTextDelta pattern.
16
- * Accepts deltas via a callback ref, renders accumulated text.
17
- */
18
- function StreamingTestApp({
19
- onReady,
20
- onAbort,
21
- }: {
22
- onReady: (appendDelta: (text: string) => void) => void;
23
- onAbort: () => void;
24
- }): React.ReactElement {
25
- const [text, setText] = useState('');
26
- const textRef = useRef('');
27
- const [aborted, setAborted] = useState(false);
28
- const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
29
- const renderCountRef = useRef(0);
30
-
31
- // Debounced delta handler (same pattern as InteractiveSession)
32
- const appendDelta = useCallback((delta: string) => {
33
- textRef.current += delta;
34
- if (!flushTimerRef.current) {
35
- flushTimerRef.current = setTimeout(() => {
36
- setText(textRef.current);
37
- flushTimerRef.current = null;
38
- }, 16);
39
- }
40
- }, []);
41
-
42
- // ESC handler
43
- useInput((_input, key) => {
44
- if (key.escape) {
45
- setAborted(true);
46
- onAbort();
47
- // Force flush any pending text
48
- if (flushTimerRef.current) {
49
- clearTimeout(flushTimerRef.current);
50
- flushTimerRef.current = null;
51
- }
52
- setText(textRef.current);
53
- }
54
- });
55
-
56
- // Notify parent that we're ready
57
- useEffect(() => {
58
- onReady(appendDelta);
59
- }, [onReady, appendDelta]);
60
-
61
- renderCountRef.current++;
62
-
63
- return (
64
- <Box flexDirection="column">
65
- <Text>{text}</Text>
66
- {aborted && <Text color="yellow">Interrupted by user.</Text>}
67
- <Text dimColor>renders: {renderCountRef.current}</Text>
68
- </Box>
69
- );
70
- }
71
-
72
- describe('Streaming abort E2E', () => {
73
- it('debounced streaming renders fewer times than delta count', async () => {
74
- let appendDelta: ((text: string) => void) | null = null;
75
-
76
- const { lastFrame } = render(
77
- React.createElement(StreamingTestApp, {
78
- onReady: (fn: (text: string) => void) => {
79
- appendDelta = fn;
80
- },
81
- onAbort: () => {},
82
- }),
83
- );
84
-
85
- // Wait for component to mount
86
- await new Promise((r) => setTimeout(r, 20));
87
- expect(appendDelta).not.toBeNull();
88
-
89
- // Send 20 rapid deltas
90
- for (let i = 0; i < 20; i++) {
91
- appendDelta!(`chunk${i} `);
92
- }
93
-
94
- // Wait for debounce flush
95
- await new Promise((r) => setTimeout(r, 50));
96
-
97
- const frame = lastFrame()!;
98
- // All text should be present
99
- expect(frame).toContain('chunk0');
100
- expect(frame).toContain('chunk19');
101
-
102
- // Render count should be MUCH less than 20 (debounced)
103
- const renderMatch = frame.match(/renders: (\d+)/);
104
- expect(renderMatch).not.toBeNull();
105
- const renderCount = parseInt(renderMatch![1], 10);
106
- // With 16ms debounce and ~50ms total time, expect 3-5 renders, not 20+
107
- expect(renderCount).toBeLessThan(10);
108
- });
109
-
110
- it('ESC during rapid streaming triggers abort and shows text', async () => {
111
- let appendDelta: ((text: string) => void) | null = null;
112
- let abortCalled = false;
113
-
114
- const { stdin, lastFrame } = render(
115
- React.createElement(StreamingTestApp, {
116
- onReady: (fn: (text: string) => void) => {
117
- appendDelta = fn;
118
- },
119
- onAbort: () => {
120
- abortCalled = true;
121
- },
122
- }),
123
- );
124
-
125
- await new Promise((r) => setTimeout(r, 20));
126
-
127
- // Send some deltas
128
- for (let i = 0; i < 5; i++) {
129
- appendDelta!(`line${i} `);
130
- }
131
-
132
- // Press ESC
133
- stdin.write('\x1B');
134
- await new Promise((r) => setTimeout(r, 50));
135
-
136
- expect(abortCalled).toBe(true);
137
-
138
- const frame = lastFrame()!;
139
- // Text should be visible (flush on abort)
140
- expect(frame).toContain('line0');
141
- expect(frame).toContain('line4');
142
- // Cancelled indicator
143
- expect(frame).toContain('Interrupted by user.');
144
- });
145
-
146
- it('ESC during ongoing streaming stops further rendering', async () => {
147
- let appendDelta: ((text: string) => void) | null = null;
148
- let abortCalled = false;
149
-
150
- const { stdin, lastFrame } = render(
151
- React.createElement(StreamingTestApp, {
152
- onReady: (fn: (text: string) => void) => {
153
- appendDelta = fn;
154
- },
155
- onAbort: () => {
156
- abortCalled = true;
157
- },
158
- }),
159
- );
160
-
161
- await new Promise((r) => setTimeout(r, 20));
162
-
163
- // Send first batch
164
- appendDelta!('before_abort ');
165
-
166
- // Wait for flush
167
- await new Promise((r) => setTimeout(r, 20));
168
-
169
- // Press ESC
170
- stdin.write('\x1B');
171
- await new Promise((r) => setTimeout(r, 30));
172
-
173
- expect(abortCalled).toBe(true);
174
-
175
- // Send more deltas AFTER abort (should still accumulate in ref but component should show Cancelled)
176
- appendDelta!('after_abort ');
177
- await new Promise((r) => setTimeout(r, 50));
178
-
179
- const frame = lastFrame()!;
180
- expect(frame).toContain('before_abort');
181
- expect(frame).toContain('Interrupted by user.');
182
- });
183
- });
@@ -1,53 +0,0 @@
1
- import React from 'react';
2
- import { describe, expect, it } from 'vitest';
3
- import { render } from 'ink-testing-library';
4
- import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
5
- import BackgroundTaskPanel from '../BackgroundTaskPanel.js';
6
-
7
- function makeEntry(overrides: Partial<IExecutionWorkspaceEntry>): IExecutionWorkspaceEntry {
8
- return {
9
- id: 'task:agent_1',
10
- sourceId: 'agent_1',
11
- kind: 'background_task',
12
- origin: { kind: 'slash_command', sessionId: 'session_1', commandName: 'agent' },
13
- taskKind: 'agent',
14
- status: 'running',
15
- title: 'general-purpose',
16
- subtitle: 'agent',
17
- preview: 'Analyze backlog',
18
- unread: false,
19
- attention: 'none',
20
- visibility: 'default',
21
- updatedAt: '2026-05-09T00:00:00.000Z',
22
- controls: ['select', 'cancel'],
23
- ...overrides,
24
- };
25
- }
26
-
27
- describe('BackgroundTaskPanel', () => {
28
- it('renders SDK workspace entries with compact markers instead of raw task ids', () => {
29
- const { lastFrame } = render(
30
- <BackgroundTaskPanel
31
- entries={[
32
- makeEntry({ id: 'task:agent_1', status: 'running' }),
33
- makeEntry({ id: 'task:agent_2', status: 'completed', preview: 'Done' }),
34
- makeEntry({
35
- id: 'task:agent_3',
36
- status: 'failed',
37
- attention: 'failed',
38
- preview: 'Timed out',
39
- }),
40
- ]}
41
- />,
42
- );
43
-
44
- const frame = lastFrame()!;
45
- expect(frame).toContain('Background work');
46
- expect(frame).toContain('├ □ general-purpose agent');
47
- expect(frame).toContain('├ ■ general-purpose agent · completed');
48
- expect(frame).toContain('└ ■ general-purpose agent · failed');
49
- expect(frame).not.toContain('agent_1');
50
- expect(frame).not.toContain('agent_2');
51
- expect(frame).not.toContain('agent_3');
52
- });
53
- });
@@ -1,59 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-transport';
3
- import { formatBackgroundTaskRow } from '../background-task-row-format.js';
4
-
5
- function makeEntry(overrides: Partial<IExecutionWorkspaceEntry>): IExecutionWorkspaceEntry {
6
- return {
7
- id: 'task:agent_1',
8
- sourceId: 'agent_1',
9
- kind: 'background_task',
10
- origin: { kind: 'slash_command', sessionId: 'session_1', commandName: 'agent' },
11
- taskKind: 'agent',
12
- status: 'running',
13
- title: 'Explore',
14
- subtitle: 'general-purpose',
15
- preview: 'Analyze backlog',
16
- unread: false,
17
- attention: 'none',
18
- visibility: 'default',
19
- updatedAt: '2026-05-09T00:00:00.000Z',
20
- controls: ['select', 'cancel'],
21
- ...overrides,
22
- };
23
- }
24
-
25
- describe('formatBackgroundTaskRow', () => {
26
- it('formats running SDK workspace entries without raw task ids', () => {
27
- const row = formatBackgroundTaskRow(makeEntry({ id: 'task:agent_1' }), { isLast: true });
28
-
29
- expect(row.connector).toBe('└');
30
- expect(row.marker).toBe('□');
31
- expect(row.label).toBe('Explore agent');
32
- expect(row.segments).toEqual(['running', 'agent · general-purpose']);
33
- expect(row.preview).toBe('Analyze backlog');
34
- expect(row.accessibleText).not.toContain('agent_1');
35
- });
36
-
37
- it('formats failed and completed rows from SDK-owned status and attention', () => {
38
- const failed = formatBackgroundTaskRow(
39
- makeEntry({
40
- id: 'task:agent_2',
41
- status: 'failed',
42
- attention: 'failed',
43
- preview: 'Timed out',
44
- }),
45
- { isLast: false },
46
- );
47
- const completed = formatBackgroundTaskRow(
48
- makeEntry({ id: 'task:agent_3', status: 'completed', preview: 'Summary ready' }),
49
- );
50
-
51
- expect(failed.connector).toBe('├');
52
- expect(failed.marker).toBe('■');
53
- expect(failed.color).toBe('red');
54
- expect(failed.preview).toBe('Timed out');
55
- expect(completed.marker).toBe('■');
56
- expect(completed.color).toBe('green');
57
- expect(completed.preview).toBe('Summary ready');
58
- });
59
- });