@robota-sdk/agent-transport 3.0.0-beta.64

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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/dist/node/headless/index.cjs +1 -0
  3. package/dist/node/headless/index.d.ts +2 -0
  4. package/dist/node/headless/index.js +1 -0
  5. package/dist/node/headless-CWEpJXFK.js +7 -0
  6. package/dist/node/headless-CWEpJXFK.js.map +1 -0
  7. package/dist/node/headless-CsZFelG9.cjs +6 -0
  8. package/dist/node/http/index.cjs +1 -0
  9. package/dist/node/http/index.d.ts +2 -0
  10. package/dist/node/http/index.js +1 -0
  11. package/dist/node/http-CM3TJhrF.cjs +1 -0
  12. package/dist/node/http-DwO1AHG-.js +2 -0
  13. package/dist/node/http-DwO1AHG-.js.map +1 -0
  14. package/dist/node/index--Ti9NzQX.d.ts +64 -0
  15. package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
  16. package/dist/node/index-B_rcr14p.d.ts +47 -0
  17. package/dist/node/index-B_rcr14p.d.ts.map +1 -0
  18. package/dist/node/index-C9LWCL4l.d.ts +34 -0
  19. package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
  20. package/dist/node/index-CAr3ioVh.d.ts +64 -0
  21. package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
  22. package/dist/node/index-CEs25wVk.d.ts +213 -0
  23. package/dist/node/index-CEs25wVk.d.ts.map +1 -0
  24. package/dist/node/index-CvXLpjJO.d.ts +213 -0
  25. package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
  26. package/dist/node/index-D34WUfFH.d.ts +26 -0
  27. package/dist/node/index-D34WUfFH.d.ts.map +1 -0
  28. package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
  29. package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
  30. package/dist/node/index-k3TUjA-T.d.ts +26 -0
  31. package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
  32. package/dist/node/index-nBlMTFkZ.d.ts +34 -0
  33. package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
  34. package/dist/node/index.cjs +1 -0
  35. package/dist/node/index.d.ts +6 -0
  36. package/dist/node/index.js +1 -0
  37. package/dist/node/mcp/index.cjs +1 -0
  38. package/dist/node/mcp/index.d.ts +2 -0
  39. package/dist/node/mcp/index.js +1 -0
  40. package/dist/node/mcp-BXBwF6Wu.js +2 -0
  41. package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
  42. package/dist/node/mcp-DcHuGokt.cjs +1 -0
  43. package/dist/node/tui/index.cjs +1 -0
  44. package/dist/node/tui/index.d.ts +2 -0
  45. package/dist/node/tui/index.js +1 -0
  46. package/dist/node/tui-CeD_6rSo.cjs +24 -0
  47. package/dist/node/tui-zmDTPk4b.js +25 -0
  48. package/dist/node/tui-zmDTPk4b.js.map +1 -0
  49. package/dist/node/ws/index.cjs +1 -0
  50. package/dist/node/ws/index.d.ts +2 -0
  51. package/dist/node/ws/index.js +1 -0
  52. package/dist/node/ws-B-oRccFl.js +2 -0
  53. package/dist/node/ws-B-oRccFl.js.map +1 -0
  54. package/dist/node/ws-COnIgnmn.cjs +1 -0
  55. package/package.json +141 -0
  56. package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
  57. package/src/headless/__tests__/headless-runner.test.ts +484 -0
  58. package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
  59. package/src/headless/__tests__/headless-transport.test.ts +268 -0
  60. package/src/headless/headless-runner.ts +141 -0
  61. package/src/headless/headless-stream-json.ts +142 -0
  62. package/src/headless/headless-transport.ts +43 -0
  63. package/src/headless/index.ts +4 -0
  64. package/src/http/__tests__/http-transport.test.ts +55 -0
  65. package/src/http/__tests__/routes.test.ts +168 -0
  66. package/src/http/http-transport.ts +42 -0
  67. package/src/http/index.ts +4 -0
  68. package/src/http/routes.ts +151 -0
  69. package/src/index.ts +5 -0
  70. package/src/mcp/__tests__/mcp-server.test.ts +66 -0
  71. package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
  72. package/src/mcp/index.ts +4 -0
  73. package/src/mcp/mcp-server.ts +162 -0
  74. package/src/mcp/mcp-transport.ts +48 -0
  75. package/src/tui/App.tsx +478 -0
  76. package/src/tui/BackgroundTaskPanel.tsx +34 -0
  77. package/src/tui/CjkTextInput.tsx +204 -0
  78. package/src/tui/ConfirmPrompt.tsx +69 -0
  79. package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
  80. package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
  81. package/src/tui/InkTerminal.ts +42 -0
  82. package/src/tui/InputArea.tsx +298 -0
  83. package/src/tui/InteractivePrompt.tsx +57 -0
  84. package/src/tui/ListPicker.tsx +94 -0
  85. package/src/tui/MenuSelect.tsx +103 -0
  86. package/src/tui/MessageList.tsx +282 -0
  87. package/src/tui/PermissionPrompt.tsx +84 -0
  88. package/src/tui/PluginTUI.tsx +256 -0
  89. package/src/tui/SessionPicker.tsx +66 -0
  90. package/src/tui/SessionStatusBar.tsx +66 -0
  91. package/src/tui/SlashAutocomplete.tsx +110 -0
  92. package/src/tui/StatusBar.tsx +213 -0
  93. package/src/tui/StreamingIndicator.tsx +91 -0
  94. package/src/tui/TextPrompt.tsx +80 -0
  95. package/src/tui/ToolCommandOutput.tsx +37 -0
  96. package/src/tui/ToolDiffBlock.tsx +30 -0
  97. package/src/tui/TransportTUI.tsx +116 -0
  98. package/src/tui/UpdateNotice.tsx +14 -0
  99. package/src/tui/UsageSummaryEntry.tsx +38 -0
  100. package/src/tui/WaveText.tsx +44 -0
  101. package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
  102. package/src/tui/__tests__/ListPicker.test.tsx +159 -0
  103. package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
  104. package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
  105. package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
  106. package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
  107. package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
  108. package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
  109. package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
  110. package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
  111. package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
  112. package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
  113. package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
  114. package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
  115. package/src/tui/__tests__/command-output-summary.test.ts +95 -0
  116. package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
  117. package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
  118. package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
  119. package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
  120. package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
  121. package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
  122. package/src/tui/__tests__/input-area-flow.test.ts +152 -0
  123. package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
  124. package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
  125. package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
  126. package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
  127. package/src/tui/__tests__/render-markdown.test.ts +72 -0
  128. package/src/tui/__tests__/selection-flow.test.ts +61 -0
  129. package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
  130. package/src/tui/__tests__/status-activity.test.ts +71 -0
  131. package/src/tui/__tests__/status-bar.test.tsx +157 -0
  132. package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
  133. package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
  134. package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
  135. package/src/tui/background-task-row-format.ts +52 -0
  136. package/src/tui/command-output-summary.ts +122 -0
  137. package/src/tui/execution-workspace-view-model.ts +123 -0
  138. package/src/tui/flows/cjk-text-input-flow.ts +285 -0
  139. package/src/tui/flows/confirm-prompt-flow.ts +45 -0
  140. package/src/tui/flows/input-area-flow.ts +186 -0
  141. package/src/tui/flows/permission-prompt-flow.ts +76 -0
  142. package/src/tui/flows/selection-flow.ts +126 -0
  143. package/src/tui/flows/text-prompt-flow.ts +98 -0
  144. package/src/tui/hooks/command-effect-handler.ts +98 -0
  145. package/src/tui/hooks/command-effect-queue.ts +39 -0
  146. package/src/tui/hooks/model-change-side-effect.ts +63 -0
  147. package/src/tui/hooks/side-effects-types.ts +38 -0
  148. package/src/tui/hooks/use-interactive-session-init.ts +50 -0
  149. package/src/tui/hooks/useAutocomplete.ts +85 -0
  150. package/src/tui/hooks/useInteractiveSession.ts +273 -0
  151. package/src/tui/hooks/usePermissionQueue.ts +51 -0
  152. package/src/tui/hooks/usePluginCallbacks.ts +30 -0
  153. package/src/tui/hooks/usePluginScreenData.ts +84 -0
  154. package/src/tui/hooks/useSideEffects.ts +210 -0
  155. package/src/tui/hooks/useSlashRouting.ts +117 -0
  156. package/src/tui/hooks/useStatusLineSettings.ts +35 -0
  157. package/src/tui/index.ts +3 -0
  158. package/src/tui/plugin-tui-handlers.ts +163 -0
  159. package/src/tui/render-markdown.ts +129 -0
  160. package/src/tui/render.tsx +60 -0
  161. package/src/tui/status-activity.ts +63 -0
  162. package/src/tui/tui-cli-adapter-context.tsx +12 -0
  163. package/src/tui/tui-cli-adapter.ts +25 -0
  164. package/src/tui/tui-state-manager.ts +225 -0
  165. package/src/tui/tui-transport.ts +32 -0
  166. package/src/tui/types.ts +14 -0
  167. package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
  168. package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
  169. package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
  170. package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
  171. package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
  172. package/src/tui/utils/edit-diff.ts +152 -0
  173. package/src/tui/utils/paste-labels.ts +9 -0
  174. package/src/tui/utils/tool-call-extractor.ts +91 -0
  175. package/src/tui/utils/tool-diff-summary.ts +75 -0
  176. package/src/ws/__tests__/ws-handler.test.ts +407 -0
  177. package/src/ws/__tests__/ws-transport.test.ts +53 -0
  178. package/src/ws/index.ts +13 -0
  179. package/src/ws/ws-background-messages.ts +170 -0
  180. package/src/ws/ws-handler.ts +279 -0
  181. package/src/ws/ws-protocol.ts +76 -0
  182. package/src/ws/ws-transport-configurable.ts +123 -0
  183. package/src/ws/ws-transport.ts +42 -0
@@ -0,0 +1,273 @@
1
+ import { useState, useCallback, useEffect } from 'react';
2
+ import { InteractiveSession, CommandRegistry } from '@robota-sdk/agent-framework';
3
+ import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
4
+ import type {
5
+ IBackgroundTaskRunner,
6
+ ICommandHostAdapters,
7
+ ICommandModule,
8
+ IInteractiveSession,
9
+ IInteractiveSessionStore,
10
+ TSubagentRunnerFactory,
11
+ IExecutionDetailPage,
12
+ IExecutionWorkspaceSnapshot,
13
+ TShellExecFn,
14
+ } from '@robota-sdk/agent-framework';
15
+ import type {
16
+ IAIProvider,
17
+ TPermissionMode,
18
+ IHistoryEntry,
19
+ TSessionEndReason,
20
+ } from '@robota-sdk/agent-core';
21
+ import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
22
+ import type { IPermissionRequest } from '../types.js';
23
+ import { TuiStateManager } from '../tui-state-manager.js';
24
+ import { useSlashRouting } from './useSlashRouting.js';
25
+ import { CommandEffectQueue, type ICommandEffectQueue } from './command-effect-queue.js';
26
+ import { usePermissionQueue } from './usePermissionQueue.js';
27
+ import { initializeSession, type IInitState } from './use-interactive-session-init.js';
28
+
29
+ const SESSION_INIT_POLL_MS = 200;
30
+
31
+ export interface IInteractiveSessionProps {
32
+ cwd: string;
33
+ provider: IAIProvider;
34
+ permissionMode?: TPermissionMode;
35
+ maxTurns?: number;
36
+ sessionStore?: IInteractiveSessionStore;
37
+ resumeSessionId?: string;
38
+ forkSession?: boolean;
39
+ sessionName?: string;
40
+ backgroundTaskRunners?: IBackgroundTaskRunner[];
41
+ subagentRunnerFactory?: TSubagentRunnerFactory;
42
+ commandModules?: readonly ICommandModule[];
43
+ commandHostAdapters?: ICommandHostAdapters;
44
+ shellExec?: TShellExecFn;
45
+ transportRegistry?: ITransportRegistryView<IInteractiveSession>;
46
+ language?: string;
47
+ reloadPluginCommandSource?: (registry: CommandRegistry) => void;
48
+ agentName?: string;
49
+ }
50
+
51
+ export interface IInteractiveSessionState {
52
+ interactiveSession: InteractiveSession;
53
+ registry: CommandRegistry;
54
+ commandEffectQueue: ICommandEffectQueue;
55
+ history: IHistoryEntry[];
56
+ addEntry: (entry: IHistoryEntry) => void;
57
+ streamingText: string;
58
+ activeTools: import('@robota-sdk/agent-framework').IToolState[];
59
+ isThinking: boolean;
60
+ isAborting: boolean;
61
+ isShuttingDown: boolean;
62
+ pendingPrompt: string | null;
63
+ executionWorkspaceSnapshot: IExecutionWorkspaceSnapshot | null;
64
+ selectedExecutionEntryId?: string;
65
+ permissionRequest: IPermissionRequest | null;
66
+ contextState: { percentage: number; usedTokens: number; maxTokens: number };
67
+ handleSubmit: (input: string) => Promise<void>;
68
+ handleAbort: () => void;
69
+ handleCancelQueue: () => void;
70
+ handleShutdown: (reason?: TSessionEndReason) => Promise<void>;
71
+ selectExecutionWorkspaceEntry: (entryId: string) => void;
72
+ readExecutionWorkspaceDetail: (entryId: string) => Promise<IExecutionDetailPage>;
73
+ }
74
+
75
+ interface IHistoryReadableSession {
76
+ getFullHistory(): IHistoryEntry[];
77
+ }
78
+
79
+ interface IHistorySyncManager {
80
+ syncHistory(entries: IHistoryEntry[]): void;
81
+ }
82
+
83
+ export function applyCompactEventToManager(
84
+ interactiveSession: IHistoryReadableSession,
85
+ manager: IHistorySyncManager,
86
+ ): void {
87
+ manager.syncHistory(interactiveSession.getFullHistory());
88
+ }
89
+
90
+ export function applySkillActivationEventToManager(
91
+ interactiveSession: IHistoryReadableSession,
92
+ manager: IHistorySyncManager,
93
+ ): void {
94
+ manager.syncHistory(interactiveSession.getFullHistory());
95
+ }
96
+
97
+ function syncExecutionWorkspaceFromSession(
98
+ interactiveSession: InteractiveSession,
99
+ manager: TuiStateManager,
100
+ ): void {
101
+ try {
102
+ manager.syncExecutionWorkspaceSnapshot(
103
+ interactiveSession.getExecutionWorkspaceSnapshot({
104
+ selectedEntryId: manager.selectedExecutionEntryId,
105
+ }),
106
+ );
107
+ } catch {
108
+ // allow-fallback: session may not be initialized yet; swallow until ready
109
+ /* Session not initialized yet */
110
+ }
111
+ }
112
+
113
+ export function useInteractiveSession(props: IInteractiveSessionProps): IInteractiveSessionState {
114
+ const [, forceRender] = useState(0);
115
+ const [isShuttingDown, setIsShuttingDown] = useState(false);
116
+ const { permissionHandler, permissionRequest } = usePermissionQueue();
117
+
118
+ // Initialize once — useState lazy initializer runs exactly once per mount, safe for Concurrent Mode
119
+ const [initState] = useState<IInitState>(() => initializeSession(props, permissionHandler));
120
+ const { interactiveSession, registry, manager, commandEffectQueue } = initState;
121
+
122
+ manager.onChange = () => forceRender((n) => n + 1);
123
+
124
+ // Sync restored history immediately (session resume restores history in constructor)
125
+ if (manager.history.length === 0) {
126
+ const restored = interactiveSession.getFullHistory();
127
+ if (restored.length > 0) {
128
+ manager.syncHistory(restored);
129
+ }
130
+ }
131
+
132
+ // Start transports (settings-driven: WS + web-monitor based on registry config).
133
+ useEffect(() => {
134
+ if (!props.transportRegistry) return;
135
+ const reg = props.transportRegistry;
136
+ reg.startAll(interactiveSession).catch(() => undefined);
137
+ return () => {
138
+ reg.stopAll().catch(() => undefined);
139
+ };
140
+ }, [interactiveSession, props.transportRegistry]);
141
+
142
+ // Connect InteractiveSession events to TuiStateManager
143
+ useEffect(() => {
144
+ const onCompact = (): void => applyCompactEventToManager(interactiveSession, manager);
145
+ const onSkillActivation = (): void =>
146
+ applySkillActivationEventToManager(interactiveSession, manager);
147
+ const onExecutionWorkspaceEvent = (
148
+ event: import('@robota-sdk/agent-framework').IExecutionWorkspaceEvent,
149
+ ): void => manager.syncExecutionWorkspaceSnapshot(event.snapshot);
150
+
151
+ interactiveSession.on('text_delta', manager.onTextDelta);
152
+ interactiveSession.on('tool_start', manager.onToolStart);
153
+ interactiveSession.on('tool_end', manager.onToolEnd);
154
+ interactiveSession.on('thinking', manager.onThinking);
155
+ interactiveSession.on('complete', manager.onComplete);
156
+ interactiveSession.on('interrupted', manager.onInterrupted);
157
+ interactiveSession.on('error', manager.onError);
158
+ interactiveSession.on('context_update', manager.onContextUpdate);
159
+ interactiveSession.on('compact', onCompact);
160
+ interactiveSession.on('skill_activation', onSkillActivation);
161
+ interactiveSession.on('execution_workspace_event', onExecutionWorkspaceEvent);
162
+
163
+ // Sync context state and restored history after async initialization
164
+ const initCheck = setInterval(() => {
165
+ try {
166
+ const ctx = interactiveSession.getContextState();
167
+ manager.setContextState({
168
+ percentage: ctx.usedPercentage,
169
+ usedTokens: ctx.usedTokens,
170
+ maxTokens: ctx.maxTokens,
171
+ });
172
+ const restored = interactiveSession.getFullHistory();
173
+ if (restored.length > 0) {
174
+ manager.syncHistory(restored);
175
+ }
176
+ syncExecutionWorkspaceFromSession(interactiveSession, manager);
177
+ clearInterval(initCheck);
178
+ } catch {
179
+ // allow-fallback: session initializes asynchronously; poll until ready
180
+ /* Not yet initialized */
181
+ }
182
+ }, SESSION_INIT_POLL_MS);
183
+
184
+ return () => {
185
+ clearInterval(initCheck);
186
+ interactiveSession.off('text_delta', manager.onTextDelta);
187
+ interactiveSession.off('tool_start', manager.onToolStart);
188
+ interactiveSession.off('tool_end', manager.onToolEnd);
189
+ interactiveSession.off('thinking', manager.onThinking);
190
+ interactiveSession.off('complete', manager.onComplete);
191
+ interactiveSession.off('interrupted', manager.onInterrupted);
192
+ interactiveSession.off('error', manager.onError);
193
+ interactiveSession.off('context_update', manager.onContextUpdate);
194
+ interactiveSession.off('compact', onCompact);
195
+ interactiveSession.off('skill_activation', onSkillActivation);
196
+ interactiveSession.off('execution_workspace_event', onExecutionWorkspaceEvent);
197
+ };
198
+ }, [interactiveSession, manager]);
199
+
200
+ // Sync messages on every thinking state change:
201
+ // - thinking=true: "You:" and "System: Invoking..." are already in messages
202
+ // - thinking=false: complete/interrupted messages are in messages
203
+ useEffect(() => {
204
+ manager.syncHistory(interactiveSession.getFullHistory());
205
+ syncExecutionWorkspaceFromSession(interactiveSession, manager);
206
+ if (!manager.isThinking) {
207
+ manager.setPendingPrompt(interactiveSession.getPendingPrompt());
208
+ }
209
+ }, [manager.isThinking, interactiveSession, manager]);
210
+
211
+ const handleSubmit = useSlashRouting(
212
+ interactiveSession,
213
+ registry,
214
+ manager,
215
+ commandEffectQueue,
216
+ props.reloadPluginCommandSource,
217
+ );
218
+
219
+ const handleAbort = useCallback(() => {
220
+ manager.setAborting(true);
221
+ interactiveSession.abort();
222
+ }, [interactiveSession, manager]);
223
+
224
+ const handleCancelQueue = useCallback(() => {
225
+ interactiveSession.cancelQueue();
226
+ manager.setPendingPrompt(null);
227
+ }, [interactiveSession, manager]);
228
+
229
+ const handleShutdown = useCallback(
230
+ async (reason: TSessionEndReason = 'prompt_input_exit'): Promise<void> => {
231
+ if (isShuttingDown) return;
232
+ setIsShuttingDown(true);
233
+ manager.addEntry(messageToHistoryEntry(createSystemMessage('Shutting down...')));
234
+ await interactiveSession.shutdown({ reason, message: 'CLI shutdown' });
235
+ },
236
+ [interactiveSession, manager, isShuttingDown],
237
+ );
238
+
239
+ const selectExecutionWorkspaceEntry = useCallback(
240
+ (entryId: string): void => manager.selectExecutionWorkspaceEntry(entryId),
241
+ [manager],
242
+ );
243
+
244
+ const readExecutionWorkspaceDetail = useCallback(
245
+ (entryId: string): Promise<IExecutionDetailPage> =>
246
+ interactiveSession.readExecutionWorkspaceDetail(entryId),
247
+ [interactiveSession],
248
+ );
249
+
250
+ return {
251
+ interactiveSession,
252
+ registry,
253
+ commandEffectQueue,
254
+ history: manager.history,
255
+ addEntry: (entry: IHistoryEntry) => manager.addEntry(entry),
256
+ streamingText: manager.streamingText,
257
+ activeTools: manager.activeTools,
258
+ isThinking: manager.isThinking,
259
+ isAborting: manager.isAborting,
260
+ isShuttingDown,
261
+ pendingPrompt: manager.pendingPrompt,
262
+ executionWorkspaceSnapshot: manager.executionWorkspaceSnapshot,
263
+ selectedExecutionEntryId: manager.selectedExecutionEntryId,
264
+ permissionRequest,
265
+ contextState: manager.contextState,
266
+ handleSubmit,
267
+ handleAbort,
268
+ handleCancelQueue,
269
+ handleShutdown,
270
+ selectExecutionWorkspaceEntry,
271
+ readExecutionWorkspaceDetail,
272
+ };
273
+ }
@@ -0,0 +1,51 @@
1
+ import { useState, useRef, useCallback } from 'react';
2
+ import type { TToolArgs } from '@robota-sdk/agent-core';
3
+ import type { TPermissionResultValue } from '@robota-sdk/agent-framework';
4
+ import type { IPermissionRequest } from '../types.js';
5
+
6
+ export function usePermissionQueue(): {
7
+ permissionHandler: (toolName: string, toolArgs: TToolArgs) => Promise<TPermissionResultValue>;
8
+ permissionRequest: IPermissionRequest | null;
9
+ } {
10
+ const [permissionRequest, setPermissionRequest] = useState<IPermissionRequest | null>(null);
11
+ const permissionQueueRef = useRef<
12
+ Array<{
13
+ toolName: string;
14
+ toolArgs: TToolArgs;
15
+ resolve: (result: TPermissionResultValue) => void;
16
+ }>
17
+ >([]);
18
+ const processingRef = useRef(false);
19
+
20
+ const processNextPermission = useCallback(() => {
21
+ if (processingRef.current) return;
22
+ const next = permissionQueueRef.current[0];
23
+ if (!next) {
24
+ setPermissionRequest(null);
25
+ return;
26
+ }
27
+ processingRef.current = true;
28
+ setPermissionRequest({
29
+ toolName: next.toolName,
30
+ toolArgs: next.toolArgs,
31
+ resolve: (result: TPermissionResultValue) => {
32
+ permissionQueueRef.current.shift();
33
+ processingRef.current = false;
34
+ setPermissionRequest(null);
35
+ next.resolve(result);
36
+ setTimeout(() => processNextPermission(), 0);
37
+ },
38
+ });
39
+ }, []);
40
+
41
+ const permissionHandler = useCallback(
42
+ (toolName: string, toolArgs: TToolArgs): Promise<TPermissionResultValue> =>
43
+ new Promise<TPermissionResultValue>((resolve) => {
44
+ permissionQueueRef.current.push({ toolName, toolArgs, resolve });
45
+ processNextPermission();
46
+ }),
47
+ [processNextPermission],
48
+ );
49
+
50
+ return { permissionHandler, permissionRequest };
51
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Hook: returns a no-op ICommandPluginAdapter for when no plugin adapter is provided.
3
+ *
4
+ * In normal usage, App receives commandHostAdapters.plugin from the CLI (agent-cli), which
5
+ * constructs the real adapter. This fallback exists only for test environments where no adapter
6
+ * is injected.
7
+ */
8
+
9
+ import { useMemo } from 'react';
10
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-framework';
11
+
12
+ function createNoOpPluginAdapter(): ICommandPluginAdapter {
13
+ return {
14
+ listInstalled: async () => [],
15
+ listAvailablePlugins: async () => [],
16
+ install: async () => undefined,
17
+ uninstall: async () => undefined,
18
+ enable: async () => undefined,
19
+ disable: async () => undefined,
20
+ marketplaceAdd: async () => '',
21
+ marketplaceRemove: async () => undefined,
22
+ marketplaceUpdate: async () => undefined,
23
+ marketplaceList: async () => [],
24
+ reloadPlugins: async () => ({ loadedPluginCount: 0 }),
25
+ };
26
+ }
27
+
28
+ export function usePluginCallbacks(_cwd: string): ICommandPluginAdapter {
29
+ return useMemo(() => createNoOpPluginAdapter(), []);
30
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Hook: fetch data for PluginTUI screens.
3
+ * Extracted from PluginTUI.tsx for single-responsibility.
4
+ */
5
+
6
+ import { useState, useEffect } from 'react';
7
+ import type { ICommandPluginAdapter } from '@robota-sdk/agent-framework';
8
+ import type { IMenuSelectItem } from '../MenuSelect.js';
9
+
10
+ export function usePluginScreenData(
11
+ screen: string,
12
+ marketplace: string | undefined,
13
+ callbacks: ICommandPluginAdapter,
14
+ refreshCounter: number,
15
+ stackLength: number,
16
+ ): { items: IMenuSelectItem[]; loading: boolean; error: string | undefined } {
17
+ const [items, setItems] = useState<IMenuSelectItem[]>([]);
18
+ const [loading, setLoading] = useState(false);
19
+ const [error, setError] = useState<string | undefined>();
20
+
21
+ useEffect(() => {
22
+ setItems([]);
23
+ setError(undefined);
24
+
25
+ if (screen === 'marketplace-list') {
26
+ setLoading(true);
27
+ callbacks
28
+ .marketplaceList()
29
+ .then((sources) => {
30
+ const baseItems: IMenuSelectItem[] = [{ label: 'Add Marketplace', value: '__add__' }];
31
+ const sourceItems: IMenuSelectItem[] = sources.map((s) => ({
32
+ label: s.name,
33
+ value: s.name,
34
+ hint: s.type,
35
+ }));
36
+ setItems([...baseItems, ...sourceItems]);
37
+ setLoading(false);
38
+ })
39
+ .catch((err) => {
40
+ setError(err instanceof Error ? err.message : String(err));
41
+ setLoading(false);
42
+ });
43
+ } else if (screen === 'marketplace-browse') {
44
+ const mp = marketplace ?? '';
45
+ setLoading(true);
46
+ callbacks
47
+ .listAvailablePlugins(mp)
48
+ .then((plugins) => {
49
+ setItems(
50
+ plugins.map((p) => ({
51
+ label: p.name,
52
+ value: p.name,
53
+ hint: p.installed ? 'installed' : p.description,
54
+ })),
55
+ );
56
+ setLoading(false);
57
+ })
58
+ .catch((err) => {
59
+ setError(err instanceof Error ? err.message : String(err));
60
+ setLoading(false);
61
+ });
62
+ } else if (screen === 'installed-list') {
63
+ setLoading(true);
64
+ callbacks
65
+ .listInstalled()
66
+ .then((plugins) => {
67
+ setItems(
68
+ plugins.map((p) => ({
69
+ label: p.name,
70
+ value: p.name,
71
+ hint: p.description,
72
+ })),
73
+ );
74
+ setLoading(false);
75
+ })
76
+ .catch((err) => {
77
+ setError(err instanceof Error ? err.message : String(err));
78
+ setLoading(false);
79
+ });
80
+ }
81
+ }, [stackLength, screen, marketplace, callbacks, refreshCounter]);
82
+
83
+ return { items, loading, error };
84
+ }
@@ -0,0 +1,210 @@
1
+ import { useState, useRef, useCallback } from 'react';
2
+ import { useApp } from 'ink';
3
+ import type { ICommandInteraction, ICommandResult } from '@robota-sdk/agent-framework';
4
+ import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
5
+ import type { TSessionEndReason } from '@robota-sdk/agent-core';
6
+ import type { TInteractivePrompt } from './side-effects-types.js';
7
+ import type { IUseSideEffectsOptions, IUseSideEffectsResult } from './side-effects-types.js';
8
+ import { applyCommandEffects } from './command-effect-handler.js';
9
+ import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
10
+ import {
11
+ addModelChangeCancelledMessage,
12
+ applyConfirmedModelChange,
13
+ } from './model-change-side-effect.js';
14
+
15
+ const EXIT_DELAY_MS = 500;
16
+
17
+ export function useSideEffects({
18
+ cwd,
19
+ providerOverride,
20
+ interactiveSession,
21
+ commandEffectQueue,
22
+ addEntry,
23
+ baseHandleSubmit,
24
+ setSessionName,
25
+ setStatusLineSettings,
26
+ showSessionPickerOnStart,
27
+ openAgentSwitcher,
28
+ }: IUseSideEffectsOptions): IUseSideEffectsResult {
29
+ const { exit } = useApp();
30
+ const cliAdapter = useTuiCliAdapter();
31
+ const [pendingModelId, setPendingModelId] = useState<string | null>(null);
32
+ const pendingModelChangeRef = useRef<string | null>(null);
33
+ const [pendingInteractionPrompt, setPendingInteractionPrompt] =
34
+ useState<TInteractivePrompt | null>(null);
35
+ const commandInteractionRef = useRef<ICommandInteraction | null>(null);
36
+ const [showPluginTUI, setShowPluginTUI] = useState(false);
37
+ const [showSessionPicker, setShowSessionPicker] = useState(showSessionPickerOnStart ?? false);
38
+ const [showTransportTUI, setShowTransportTUI] = useState(false);
39
+
40
+ const requestShutdown = useCallback(
41
+ (reason: TSessionEndReason, message: string): void => {
42
+ addEntry(messageToHistoryEntry(createSystemMessage('Shutting down...')));
43
+ setTimeout(() => {
44
+ void interactiveSession.shutdown({ reason, message }).finally(() => exit());
45
+ }, EXIT_DELAY_MS);
46
+ },
47
+ [interactiveSession, addEntry, exit],
48
+ );
49
+
50
+ const applyEffects = useCallback(
51
+ (effects: Parameters<typeof applyCommandEffects>[0]): boolean =>
52
+ applyCommandEffects(effects, {
53
+ addEntry,
54
+ requestShutdown,
55
+ requestModelChange: (modelId) => {
56
+ pendingModelChangeRef.current = modelId;
57
+ setPendingModelId(modelId);
58
+ },
59
+ openPluginTUI: () => setShowPluginTUI(true),
60
+ openSessionPicker: () => setShowSessionPicker(true),
61
+ openTransportTUI: () => setShowTransportTUI(true),
62
+ openAgentSwitcher: () => openAgentSwitcher?.(),
63
+ renameSession: (name) => {
64
+ interactiveSession.setName(name);
65
+ setSessionName(name);
66
+ },
67
+ applyStatusLinePatch: (patch) => {
68
+ setStatusLineSettings(
69
+ cliAdapter.applyStatusLineSettings(cliAdapter.getUserSettingsPath(), patch),
70
+ );
71
+ return true;
72
+ },
73
+ cliAdapter,
74
+ }),
75
+ [
76
+ addEntry,
77
+ cliAdapter,
78
+ interactiveSession,
79
+ requestShutdown,
80
+ setSessionName,
81
+ setStatusLineSettings,
82
+ ],
83
+ );
84
+
85
+ const applyCommandResult = useCallback(
86
+ (result: ICommandResult): void => {
87
+ if (result.message.length > 0) {
88
+ addEntry(messageToHistoryEntry(createSystemMessage(result.message)));
89
+ }
90
+ if (result.interaction !== undefined) {
91
+ commandInteractionRef.current = result.interaction;
92
+ setPendingInteractionPrompt(result.interaction.prompt);
93
+ return;
94
+ }
95
+ commandInteractionRef.current = null;
96
+ setPendingInteractionPrompt(null);
97
+ if (result.effects !== undefined && result.effects.length > 0) {
98
+ applyEffects(result.effects);
99
+ }
100
+ },
101
+ [addEntry, applyEffects],
102
+ );
103
+
104
+ const applyQueuedCommandState = useCallback((): boolean => {
105
+ const queued = commandEffectQueue.drain();
106
+ if (queued === undefined) {
107
+ return false;
108
+ }
109
+ if (queued.type === 'interaction') {
110
+ const { interaction } = queued;
111
+ commandInteractionRef.current = interaction;
112
+ setPendingInteractionPrompt(interaction.prompt);
113
+ return true;
114
+ }
115
+ return applyEffects(queued.effects);
116
+ }, [applyEffects, commandEffectQueue]);
117
+
118
+ const handleSubmit = useCallback(
119
+ async (input: string): Promise<void> => {
120
+ await baseHandleSubmit(input);
121
+ applyQueuedCommandState();
122
+ },
123
+ [baseHandleSubmit, applyQueuedCommandState],
124
+ );
125
+
126
+ const handleModelConfirm = useCallback(
127
+ (index: number) => {
128
+ const modelId = pendingModelChangeRef.current;
129
+ setPendingModelId(null);
130
+ pendingModelChangeRef.current = null;
131
+ if (index === 0 && modelId) {
132
+ applyConfirmedModelChange({
133
+ cwd,
134
+ modelId,
135
+ providerOverride,
136
+ addEntry,
137
+ requestShutdown,
138
+ applyModelChange: (c, m, opts) => {
139
+ cliAdapter.applyActiveModelChange(c, m, opts);
140
+ return { applied: true };
141
+ },
142
+ });
143
+ } else {
144
+ addModelChangeCancelledMessage(addEntry);
145
+ }
146
+ },
147
+ [cwd, providerOverride, addEntry, cliAdapter, requestShutdown],
148
+ );
149
+
150
+ const handleInteractionSubmit = useCallback(
151
+ async (value: string): Promise<void> => {
152
+ const interaction = commandInteractionRef.current;
153
+ if (interaction === null) {
154
+ setPendingInteractionPrompt(null);
155
+ return;
156
+ }
157
+ try {
158
+ // allow-fallback: user-facing error display for plugin interaction submit
159
+ const result = await interaction.submit(value);
160
+ applyCommandResult(result);
161
+ } catch (err) {
162
+ // allow-fallback: user-facing error display for plugin interaction submit
163
+ commandInteractionRef.current = null;
164
+ setPendingInteractionPrompt(null);
165
+ addEntry(
166
+ messageToHistoryEntry(
167
+ createSystemMessage(`Failed: ${err instanceof Error ? err.message : String(err)}`),
168
+ ),
169
+ );
170
+ }
171
+ },
172
+ [addEntry, applyCommandResult],
173
+ );
174
+
175
+ const handleInteractionCancel = useCallback(() => {
176
+ const interaction = commandInteractionRef.current;
177
+ commandInteractionRef.current = null;
178
+ setPendingInteractionPrompt(null);
179
+ if (interaction?.cancel === undefined) {
180
+ addEntry(messageToHistoryEntry(createSystemMessage('Command interaction cancelled.')));
181
+ return;
182
+ }
183
+ Promise.resolve(interaction.cancel())
184
+ .then((result) => applyCommandResult(result))
185
+ .catch((err) => {
186
+ // allow-fallback: user-facing error display for interaction cancel
187
+ addEntry(
188
+ messageToHistoryEntry(
189
+ createSystemMessage(`Failed: ${err instanceof Error ? err.message : String(err)}`),
190
+ ),
191
+ );
192
+ });
193
+ }, [addEntry, applyCommandResult]);
194
+
195
+ return {
196
+ handleSubmit,
197
+ pendingModelId,
198
+ pendingInteractionPrompt,
199
+ showPluginTUI,
200
+ showSessionPicker,
201
+ showTransportTUI,
202
+ setPendingModelId,
203
+ setShowPluginTUI,
204
+ setShowSessionPicker,
205
+ setShowTransportTUI,
206
+ handleModelConfirm,
207
+ handleInteractionSubmit,
208
+ handleInteractionCancel,
209
+ };
210
+ }