@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,298 @@
1
+ import React, { useState, useCallback, useRef, useMemo } from 'react';
2
+
3
+ const PENDING_PROMPT_DISPLAY_MAX = 50;
4
+ const PENDING_PROMPT_TAIL_KEEP = 47;
5
+ import { Box, Text, useInput, useWindowSize } from 'ink';
6
+ import type { IHistoryEntry } from '@robota-sdk/agent-core';
7
+ import type { CommandRegistry, ICommand } from '@robota-sdk/agent-framework';
8
+ import CjkTextInput from './CjkTextInput.js';
9
+ import WaveText from './WaveText.js';
10
+ import SlashAutocomplete from './SlashAutocomplete.js';
11
+ import { expandPasteLabels } from './utils/paste-labels.js';
12
+ import { useAutocomplete } from './hooks/useAutocomplete.js';
13
+ import {
14
+ appendPromptHistory,
15
+ createPasteLabelChange,
16
+ createPromptHistoryNavigationState,
17
+ extractPromptHistory,
18
+ getAutocompletePopupAction,
19
+ getPendingPromptInputAction,
20
+ getPromptHistoryInputAction,
21
+ moveAutocompleteSelection,
22
+ navigatePromptHistory,
23
+ resolveEnterCommandSelection,
24
+ resolveTabCompletion,
25
+ shouldSubmitInput,
26
+ } from './flows/input-area-flow.js';
27
+
28
+ interface IProps {
29
+ onSubmit: (value: string) => void;
30
+ onCancelQueue?: () => void;
31
+ isDisabled: boolean;
32
+ isAborting?: boolean;
33
+ pendingPrompt?: string | null;
34
+ registry?: CommandRegistry;
35
+ sessionName?: string;
36
+ history?: readonly IHistoryEntry[];
37
+ }
38
+
39
+ /**
40
+ * Known limitation: Korean IME last character may be dropped on Enter.
41
+ * This is an Ink raw mode limitation — no compositionstart/compositionend
42
+ * events are available in terminal raw mode.
43
+ * Reference: https://github.com/anthropics/claude-code/issues/3045
44
+ */
45
+ /**
46
+ * Layout constants for InputArea border box (columns).
47
+ * Used to compute available text width from terminal columns.
48
+ *
49
+ * Box borderStyle="single" adds 1 column per side (left + right).
50
+ * paddingLeft={1} adds 1 column inside the box.
51
+ * Prompt "> " takes 2 columns.
52
+ */
53
+ const BORDER_HORIZONTAL = 2;
54
+ const PADDING_LEFT = 1;
55
+ const PROMPT_WIDTH = 2;
56
+ const INPUT_AREA_OVERHEAD = BORDER_HORIZONTAL + PADDING_LEFT + PROMPT_WIDTH;
57
+ const DEFAULT_TERMINAL_COLUMNS = 80;
58
+
59
+ export default function InputArea({
60
+ onSubmit,
61
+ onCancelQueue,
62
+ isDisabled,
63
+ isAborting,
64
+ pendingPrompt,
65
+ registry,
66
+ sessionName,
67
+ history,
68
+ }: IProps): React.ReactElement {
69
+ const [value, setValue] = useState('');
70
+ const [cursorHint, setCursorHint] = useState<number | null>(null);
71
+ const [historyState, setHistoryState] = useState(createPromptHistoryNavigationState);
72
+ const [localPromptHistory, setLocalPromptHistory] = useState<string[]>([]);
73
+ const restoredPromptHistory = useMemo(() => extractPromptHistory(history ?? []), [history]);
74
+ const promptHistory = useMemo(
75
+ () =>
76
+ localPromptHistory.reduce<string[]>(
77
+ (prompts, prompt) => appendPromptHistory(prompts, prompt),
78
+ restoredPromptHistory,
79
+ ),
80
+ [restoredPromptHistory, localPromptHistory],
81
+ );
82
+ const pasteStore = useRef<Map<number, string>>(new Map());
83
+ const { columns } = useWindowSize();
84
+ const terminalColumns = columns > 0 ? columns : DEFAULT_TERMINAL_COLUMNS;
85
+ const availableWidth = Math.max(1, terminalColumns - INPUT_AREA_OVERHEAD);
86
+ const pasteIdRef = useRef(0);
87
+
88
+ const {
89
+ showPopup,
90
+ filteredCommands,
91
+ selectedIndex,
92
+ setSelectedIndex,
93
+ isSubcommandMode,
94
+ setShowPopup,
95
+ } = useAutocomplete(value, registry);
96
+
97
+ const handlePaste = useCallback((text: string, cursorPosition: number) => {
98
+ pasteIdRef.current += 1;
99
+ const id = pasteIdRef.current;
100
+ pasteStore.current.set(id, text);
101
+ setValue((prev) => {
102
+ const change = createPasteLabelChange(prev, cursorPosition, id, text);
103
+ setCursorHint(change.cursorHint);
104
+ return change.value;
105
+ });
106
+ }, []);
107
+
108
+ const resetHistoryNavigation = useCallback(() => {
109
+ setHistoryState(createPromptHistoryNavigationState());
110
+ }, []);
111
+
112
+ const recordPromptHistory = useCallback((prompt: string): void => {
113
+ setLocalPromptHistory((prev) => appendPromptHistory(prev, prompt));
114
+ }, []);
115
+
116
+ const submitPrompt = useCallback(
117
+ (prompt: string): void => {
118
+ recordPromptHistory(prompt);
119
+ resetHistoryNavigation();
120
+ onSubmit(prompt);
121
+ },
122
+ [onSubmit, recordPromptHistory, resetHistoryNavigation],
123
+ );
124
+
125
+ /** Tab: insert command into input field without executing */
126
+ const tabCompleteCommand = useCallback(
127
+ (cmd: ICommand): void => {
128
+ const result = resolveTabCompletion(value, cmd);
129
+ if (result.type === 'insert') {
130
+ setValue(result.value);
131
+ if (result.selectedIndex !== undefined) {
132
+ setSelectedIndex(result.selectedIndex);
133
+ }
134
+ }
135
+ },
136
+ [value, setSelectedIndex],
137
+ );
138
+
139
+ /** Enter: insert and execute command immediately */
140
+ const enterSelectCommand = useCallback(
141
+ (cmd: ICommand): void => {
142
+ const result = resolveEnterCommandSelection(value, cmd);
143
+ if (result.type === 'insert') {
144
+ setValue(result.value);
145
+ if (result.selectedIndex !== undefined) {
146
+ setSelectedIndex(result.selectedIndex);
147
+ }
148
+ return;
149
+ }
150
+ setValue('');
151
+ submitPrompt(result.value);
152
+ },
153
+ [value, submitPrompt, setSelectedIndex],
154
+ );
155
+
156
+ const handleSubmit = useCallback(
157
+ (text: string): void => {
158
+ if (!shouldSubmitInput(text)) return;
159
+
160
+ if (showPopup && filteredCommands[selectedIndex]) {
161
+ enterSelectCommand(filteredCommands[selectedIndex]);
162
+ return;
163
+ }
164
+
165
+ // Expand paste labels before submitting
166
+ const expanded = expandPasteLabels(text.trim(), pasteStore.current);
167
+
168
+ setValue('');
169
+ // Reset paste state
170
+ pasteStore.current.clear();
171
+ pasteIdRef.current = 0;
172
+
173
+ submitPrompt(expanded);
174
+ },
175
+ [showPopup, filteredCommands, selectedIndex, enterSelectCommand, submitPrompt],
176
+ );
177
+
178
+ useInput(
179
+ (
180
+ _input: string,
181
+ key: { upArrow: boolean; downArrow: boolean; escape: boolean; tab: boolean },
182
+ ) => {
183
+ if (!showPopup) return;
184
+ const action = getAutocompletePopupAction(key);
185
+ if (action === 'previous' || action === 'next') {
186
+ setSelectedIndex((prev) =>
187
+ moveAutocompleteSelection(prev, filteredCommands.length, action),
188
+ );
189
+ } else if (action === 'close') {
190
+ setShowPopup(false);
191
+ } else if (action === 'complete') {
192
+ const cmd = filteredCommands[selectedIndex];
193
+ if (cmd) tabCompleteCommand(cmd);
194
+ }
195
+ },
196
+ { isActive: showPopup && !isDisabled },
197
+ );
198
+
199
+ useInput(
200
+ (_input, key) => {
201
+ const action = getPromptHistoryInputAction(key);
202
+ if (!action) return;
203
+ const result = navigatePromptHistory(value, promptHistory, historyState, action);
204
+ setValue(result.value);
205
+ setCursorHint(result.cursorHint);
206
+ setHistoryState(result.state);
207
+ },
208
+ { isActive: !showPopup && !isDisabled && !pendingPrompt },
209
+ );
210
+
211
+ // Backspace cancels queued prompt
212
+ useInput(
213
+ (_input, key) => {
214
+ if (getPendingPromptInputAction(key) === 'cancelQueue' && pendingPrompt) {
215
+ onCancelQueue?.();
216
+ }
217
+ },
218
+ { isActive: !!pendingPrompt },
219
+ );
220
+
221
+ const borderColor = isAborting
222
+ ? 'yellow'
223
+ : pendingPrompt
224
+ ? 'cyan'
225
+ : isDisabled
226
+ ? 'gray'
227
+ : 'green';
228
+ const innerWidth = Math.max(1, terminalColumns - BORDER_HORIZONTAL);
229
+
230
+ // Build top border with optional session name title (right-aligned, 2 chars from edge)
231
+ const topBorder = (() => {
232
+ if (sessionName) {
233
+ const label = ` "${sessionName}" `;
234
+ const rightPad = 2;
235
+ const leftLen = Math.max(0, innerWidth - label.length - rightPad);
236
+ return { left: '┌' + '─'.repeat(leftLen), label, right: '─'.repeat(rightPad) + '┐' };
237
+ }
238
+ return { left: '┌' + '─'.repeat(innerWidth), label: '', right: '┐' };
239
+ })();
240
+
241
+ return (
242
+ <Box flexDirection="column">
243
+ {showPopup && (
244
+ <SlashAutocomplete
245
+ commands={filteredCommands}
246
+ selectedIndex={selectedIndex}
247
+ visible={showPopup}
248
+ isSubcommandMode={isSubcommandMode}
249
+ />
250
+ )}
251
+ <Text color={borderColor}>
252
+ {topBorder.left}
253
+ {topBorder.label ? (
254
+ <Text backgroundColor={borderColor} color="black" bold>
255
+ {topBorder.label}
256
+ </Text>
257
+ ) : null}
258
+ {topBorder.right}
259
+ </Text>
260
+ <Box borderStyle="single" borderTop={false} borderColor={borderColor} paddingLeft={1}>
261
+ {isAborting ? (
262
+ <Text color="yellow"> Interrupting...</Text>
263
+ ) : pendingPrompt ? (
264
+ <Text color="cyan">
265
+ {' '}
266
+ Queued:{' '}
267
+ {pendingPrompt.length > PENDING_PROMPT_DISPLAY_MAX
268
+ ? pendingPrompt.slice(0, PENDING_PROMPT_TAIL_KEEP) + '...'
269
+ : pendingPrompt}{' '}
270
+ <Text dimColor>(Backspace to cancel)</Text>
271
+ </Text>
272
+ ) : isDisabled ? (
273
+ <WaveText text=" Waiting for response... (ESC to interrupt)" />
274
+ ) : (
275
+ <Box>
276
+ <Text color="green" bold>
277
+ {'> '}
278
+ </Text>
279
+ <CjkTextInput
280
+ value={value}
281
+ onChange={(v) => {
282
+ setValue(v);
283
+ resetHistoryNavigation();
284
+ setCursorHint(null); // reset after normal typing
285
+ }}
286
+ onSubmit={handleSubmit}
287
+ onPaste={handlePaste}
288
+ placeholder="Type a message or /help"
289
+ availableWidth={availableWidth}
290
+ cursorHint={cursorHint}
291
+ enableVerticalNavigation={false}
292
+ />
293
+ </Box>
294
+ )}
295
+ </Box>
296
+ </Box>
297
+ );
298
+ }
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import type {
4
+ TCommandInteractionPrompt as TInteractivePrompt,
5
+ ICommandChoicePromptOption as IChoicePromptOption,
6
+ } from '@robota-sdk/agent-framework';
7
+ import ListPicker from './ListPicker.js';
8
+ import TextPrompt from './TextPrompt.js';
9
+
10
+ interface IInteractivePromptProps {
11
+ prompt: TInteractivePrompt;
12
+ onSubmit: (value: string) => void;
13
+ onCancel: () => void;
14
+ }
15
+
16
+ export default function InteractivePrompt({
17
+ prompt,
18
+ onSubmit,
19
+ onCancel,
20
+ }: IInteractivePromptProps): React.ReactElement {
21
+ if (prompt.kind === 'text') {
22
+ return (
23
+ <TextPrompt
24
+ key={`text:${prompt.title}`}
25
+ title={prompt.title}
26
+ description={prompt.description}
27
+ placeholder={prompt.placeholder}
28
+ allowEmpty={prompt.allowEmpty}
29
+ masked={prompt.masked}
30
+ validate={prompt.validate}
31
+ onSubmit={onSubmit}
32
+ onCancel={onCancel}
33
+ />
34
+ );
35
+ }
36
+
37
+ return (
38
+ <Box flexDirection="column">
39
+ <Text bold>{prompt.title}</Text>
40
+ {prompt.description !== undefined && prompt.description.length > 0 && (
41
+ <Text dimColor>{prompt.description}</Text>
42
+ )}
43
+ <ListPicker<IChoicePromptOption>
44
+ items={[...prompt.options]}
45
+ maxVisible={prompt.maxVisible}
46
+ renderItem={(option, isSelected) => (
47
+ <Text color={isSelected ? 'cyan' : undefined}>
48
+ {isSelected ? '> ' : ' '}
49
+ {option.label}
50
+ </Text>
51
+ )}
52
+ onSelect={(option) => onSubmit(option.value)}
53
+ onCancel={onCancel}
54
+ />
55
+ </Box>
56
+ );
57
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Generic list picker with arrow-key navigation and viewport scrolling.
3
+ * Renders items via a user-supplied renderItem function.
4
+ * Shows a limited number of items at a time; scrolls as the cursor moves.
5
+ */
6
+
7
+ import React, { useState, useRef, useCallback } from 'react';
8
+ import { Box, Text, useInput } from 'ink';
9
+ import {
10
+ applySelectionInput,
11
+ createSelectionFlowState,
12
+ getVerticalSelectionInputAction,
13
+ normalizeSelectionState,
14
+ type ISelectionFlowState,
15
+ type TSelectionInputAction,
16
+ } from './flows/selection-flow.js';
17
+
18
+ /** Default number of visible items */
19
+ const DEFAULT_MAX_VISIBLE = 3;
20
+
21
+ export interface IListPickerProps<T> {
22
+ /** Items to display in the list */
23
+ items: T[];
24
+ /** Render function for each item — receives the item and whether it is currently selected */
25
+ renderItem: (item: T, isSelected: boolean) => React.ReactElement;
26
+ /** Called when the user presses Enter on the highlighted item */
27
+ onSelect: (item: T) => void;
28
+ /** Called when the user presses Escape */
29
+ onCancel: () => void;
30
+ /** Maximum number of items visible at once (default: 10) */
31
+ maxVisible?: number;
32
+ }
33
+
34
+ export default function ListPicker<T>({
35
+ items,
36
+ renderItem,
37
+ onSelect,
38
+ onCancel,
39
+ maxVisible = DEFAULT_MAX_VISIBLE,
40
+ }: IListPickerProps<T>): React.ReactElement {
41
+ const [state, setState] = useState<ISelectionFlowState>(() => createSelectionFlowState());
42
+ const stateRef = useRef(state);
43
+ const applyAction = useCallback(
44
+ (action: TSelectionInputAction): void => {
45
+ const result = applySelectionInput(stateRef.current, action, {
46
+ itemCount: items.length,
47
+ maxVisible,
48
+ });
49
+ stateRef.current = result.state;
50
+ setState(result.state);
51
+ if (result.effect.type === 'cancel') {
52
+ onCancel();
53
+ } else if (result.effect.type === 'select') {
54
+ const item = items[result.effect.index];
55
+ if (item !== undefined) {
56
+ onSelect(item);
57
+ }
58
+ }
59
+ },
60
+ [items, maxVisible, onCancel, onSelect],
61
+ );
62
+
63
+ useInput((_input, key) => {
64
+ const action = getVerticalSelectionInputAction(key);
65
+ if (action !== undefined) {
66
+ applyAction(action);
67
+ }
68
+ });
69
+
70
+ if (items.length === 0) {
71
+ return <Box />;
72
+ }
73
+
74
+ const normalizedState = normalizeSelectionState(state, { itemCount: items.length, maxVisible });
75
+ if (normalizedState !== state) {
76
+ stateRef.current = normalizedState;
77
+ }
78
+ const { selectedIndex, scrollOffset } = normalizedState;
79
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisible);
80
+ const hasMore = scrollOffset + maxVisible < items.length;
81
+ const hasLess = scrollOffset > 0;
82
+
83
+ return (
84
+ <Box flexDirection="column">
85
+ {hasLess && <Text dimColor> ↑ {scrollOffset} more above</Text>}
86
+ {visibleItems.map((item, index) => (
87
+ <Box key={scrollOffset + index} marginBottom={1}>
88
+ {renderItem(item, scrollOffset + index === selectedIndex)}
89
+ </Box>
90
+ ))}
91
+ {hasMore && <Text dimColor> ↓ {items.length - scrollOffset - maxVisible} more below</Text>}
92
+ </Box>
93
+ );
94
+ }
@@ -0,0 +1,103 @@
1
+ import React, { useState, useCallback, useRef } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import {
4
+ applySelectionInput,
5
+ createSelectionFlowState,
6
+ getVerticalSelectionInputAction,
7
+ normalizeSelectionState,
8
+ type ISelectionFlowState,
9
+ type TSelectionInputAction,
10
+ } from './flows/selection-flow.js';
11
+
12
+ export interface IMenuSelectItem {
13
+ label: string;
14
+ value: string;
15
+ hint?: string;
16
+ }
17
+
18
+ interface IProps {
19
+ title: string;
20
+ items: IMenuSelectItem[];
21
+ onSelect: (value: string) => void;
22
+ onBack: () => void;
23
+ loading?: boolean;
24
+ error?: string;
25
+ }
26
+
27
+ export default function MenuSelect({
28
+ title,
29
+ items,
30
+ onSelect,
31
+ onBack,
32
+ loading,
33
+ error,
34
+ }: IProps): React.ReactElement {
35
+ const [state, setState] = useState<ISelectionFlowState>(() => createSelectionFlowState());
36
+ const stateRef = useRef(state);
37
+ const isEnabled = !loading && !error;
38
+ const applyAction = useCallback(
39
+ (action: TSelectionInputAction): void => {
40
+ const result = applySelectionInput(stateRef.current, action, {
41
+ itemCount: items.length,
42
+ enabled: isEnabled,
43
+ });
44
+ stateRef.current = result.state;
45
+ setState(result.state);
46
+ if (result.effect.type === 'cancel') {
47
+ onBack();
48
+ } else if (result.effect.type === 'select') {
49
+ const item = items[result.effect.index];
50
+ if (item !== undefined) {
51
+ onSelect(item.value);
52
+ }
53
+ }
54
+ },
55
+ [isEnabled, items, onBack, onSelect],
56
+ );
57
+
58
+ useInput((input, key) => {
59
+ const action = getVerticalSelectionInputAction(key);
60
+ if (action !== undefined) {
61
+ applyAction(action);
62
+ }
63
+ });
64
+
65
+ const normalizedState = normalizeSelectionState(state, { itemCount: items.length });
66
+ if (normalizedState !== state) {
67
+ stateRef.current = normalizedState;
68
+ }
69
+ const selected = normalizedState.selectedIndex;
70
+
71
+ return (
72
+ <Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1}>
73
+ <Text color="yellow" bold>
74
+ {title}
75
+ </Text>
76
+ {loading && (
77
+ <Box marginTop={1}>
78
+ <Text dimColor>Loading...</Text>
79
+ </Box>
80
+ )}
81
+ {error && (
82
+ <Box marginTop={1} flexDirection="column">
83
+ <Text color="red">{error}</Text>
84
+ <Text dimColor>Press Esc to go back</Text>
85
+ </Box>
86
+ )}
87
+ {!loading && !error && (
88
+ <Box flexDirection="column" marginTop={1}>
89
+ {items.map((item, i) => (
90
+ <Box key={item.value}>
91
+ <Text color={i === selected ? 'cyan' : undefined} bold={i === selected}>
92
+ {i === selected ? '> ' : ' '}
93
+ {item.label}
94
+ </Text>
95
+ {item.hint && <Text dimColor> {item.hint}</Text>}
96
+ </Box>
97
+ ))}
98
+ </Box>
99
+ )}
100
+ <Text dimColor>{loading || error ? '' : ' ↑↓ Navigate Enter Select Esc Back'}</Text>
101
+ </Box>
102
+ );
103
+ }