@robota-sdk/agent-transport 3.0.0-beta.64 → 3.0.0-beta.65
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.
- package/dist/node/headless/index.cjs +1 -1
- package/dist/node/{headless-CsZFelG9.cjs → headless-DJ5pnxM6.cjs} +1 -1
- package/dist/node/http/index.cjs +1 -1
- package/dist/node/{http-CM3TJhrF.cjs → http-CuQE6V6t.cjs} +1 -1
- package/dist/node/{index-CAr3ioVh.d.ts → index-CSgNoyPK.d.ts} +23 -2
- package/dist/node/index-CSgNoyPK.d.ts.map +1 -0
- package/dist/node/{index--Ti9NzQX.d.ts → index-_dNm-2J3.d.ts} +23 -2
- package/dist/node/index-_dNm-2J3.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.ts +2 -2
- package/dist/node/index.js +1 -1
- package/dist/node/mcp/index.cjs +1 -1
- package/dist/node/{mcp-DcHuGokt.cjs → mcp-BiJsIywJ.cjs} +1 -1
- package/dist/node/tui/index.cjs +1 -1
- package/dist/node/tui/index.d.ts +2 -2
- package/dist/node/tui/index.js +1 -1
- package/dist/node/tui-BIpIcT7-.cjs +24 -0
- package/dist/node/tui-DBLn1T15.js +25 -0
- package/dist/node/tui-DBLn1T15.js.map +1 -0
- package/dist/node/ws/index.cjs +1 -1
- package/dist/node/{ws-COnIgnmn.cjs → ws-XRTSFZOK.cjs} +1 -1
- package/package.json +5 -5
- package/src/tui/App.tsx +3 -0
- package/src/tui/InputArea.tsx +64 -5
- package/src/tui/SlashAutocomplete.tsx +2 -3
- package/src/tui/__tests__/input-area-flow.test.ts +19 -0
- package/src/tui/command-interaction.ts +35 -0
- package/src/tui/flows/input-area-flow.ts +8 -1
- package/src/tui/index.ts +8 -0
- package/src/tui/interactions/CommandConfirm.tsx +35 -0
- package/src/tui/interactions/CommandPicker.tsx +76 -0
- package/src/tui/render.tsx +2 -0
- package/dist/node/index--Ti9NzQX.d.ts.map +0 -1
- package/dist/node/index-CAr3ioVh.d.ts.map +0 -1
- package/dist/node/tui-CeD_6rSo.cjs +0 -24
- package/dist/node/tui-zmDTPk4b.js +0 -25
- package/dist/node/tui-zmDTPk4b.js.map +0 -1
package/dist/node/ws/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`../ws-
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require(`../ws-XRTSFZOK.cjs`);exports.WsTransport=e.t,exports.createWsHandler=e.r,exports.createWsTransport=e.n;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
require(`./tui-
|
|
1
|
+
require(`./tui-BIpIcT7-.cjs`);let e=require(`node:http`),t=require(`ws`);function n(e,t,n){if(n.type===`get-background-tasks`){t({type:`background_tasks`,tasks:e.listBackgroundTasks(n.filter)});return}if(n.type===`get-background-task`){i(e,t,n);return}if(n.type===`get-background-job-groups`){t({type:`background_job_groups`,groups:e.listBackgroundJobGroups()});return}if(n.type===`get-background-job-group`){o(e,t,n);return}if(n.type===`wait-background-job-group`){s(e,t,n);return}a(e,t,n)}function r(e,t,n){if(!n.taskId){t({type:`protocol_error`,message:`taskId is required`});return}if(n.type===`cancel-background-task`){l(t,`cancel`,n.taskId,e.cancelBackgroundTask(n.taskId,n.reason));return}if(n.type===`close-background-task`){l(t,`close`,n.taskId,e.closeBackgroundTask(n.taskId));return}c(e,t,n)}function i(e,t,n){if(!n.taskId){t({type:`protocol_error`,message:`taskId is required`});return}t({type:`background_task`,taskId:n.taskId,task:e.getBackgroundTask(n.taskId)??null})}function a(e,t,n){if(!n.taskId){t({type:`protocol_error`,message:`taskId is required`});return}e.readBackgroundTaskLog(n.taskId,n.cursor).then(e=>t({type:`background_task_log`,taskId:n.taskId,page:e}),e=>t({type:`protocol_error`,message:e.message}))}function o(e,t,n){if(!n.groupId){t({type:`protocol_error`,message:`groupId is required`});return}t({type:`background_job_group`,groupId:n.groupId,group:e.getBackgroundJobGroup(n.groupId)??null})}function s(e,t,n){if(!n.groupId){t({type:`protocol_error`,message:`groupId is required`});return}e.waitBackgroundJobGroup(n.groupId).then(e=>t({type:`background_job_group`,groupId:n.groupId,group:e}),e=>t({type:`protocol_error`,message:e.message}))}function c(e,t,n){if(!n.input){t({type:`protocol_error`,message:`input is required`});return}l(t,`send`,n.taskId,e.sendBackgroundTask(n.taskId,n.input))}function l(e,t,n,r){r.then(()=>e({type:`background_task_control_result`,action:t,taskId:n,success:!0}),r=>e({type:`background_task_control_result`,action:t,taskId:n,success:!1,message:r.message}))}function u(e){let t=d(e.session,e.send);return{onMessage:f(e.session,e.send),cleanup:t}}function d(e,t){let n=e=>t({type:`user_message`,content:e}),r=e=>t({type:`text_delta`,delta:e}),i=e=>t({type:`tool_start`,state:e}),a=e=>t({type:`tool_end`,state:e}),o=e=>t({type:`thinking`,isThinking:e}),s=e=>t({type:`complete`,result:e}),c=e=>t({type:`interrupted`,result:e}),l=e=>t({type:`error`,message:e.message}),u=e=>t({type:`background_task_event`,event:e}),d=e=>t({type:`background_job_group_event`,event:e}),f=e=>t({type:`execution_workspace_event`,snapshot:e.snapshot});return e.on(`user_message`,n),e.on(`text_delta`,r),e.on(`tool_start`,i),e.on(`tool_end`,a),e.on(`thinking`,o),e.on(`complete`,s),e.on(`interrupted`,c),e.on(`error`,l),e.on(`background_task_event`,u),e.on(`background_job_group_event`,d),e.on(`execution_workspace_event`,f),()=>{e.off(`user_message`,n),e.off(`text_delta`,r),e.off(`tool_start`,i),e.off(`tool_end`,a),e.off(`thinking`,o),e.off(`complete`,s),e.off(`interrupted`,c),e.off(`error`,l),e.off(`background_task_event`,u),e.off(`background_job_group_event`,d),e.off(`execution_workspace_event`,f)}}function f(e,t){return n=>{let r=p(n,t);r&&m(e,t,r)}}function p(e,t){try{return JSON.parse(e)}catch{return t({type:`protocol_error`,message:`Invalid JSON`}),null}}function m(e,t,i){if(g(i)){b(e,t,i);return}if(_(i)){x(e,t,i);return}if(v(i)){n(e,t,i);return}if(y(i)){r(e,t,i);return}t({type:`protocol_error`,message:`Unknown message type: ${h(i)}`})}function h(e){return e.type}function g(e){return e.type===`submit`||e.type===`command`||e.type===`abort`||e.type===`cancel-queue`}function _(e){return e.type===`get-messages`||e.type===`get-context`||e.type===`get-executing`||e.type===`get-pending`||e.type===`get-execution-workspace`}function v(e){return e.type===`get-background-tasks`||e.type===`get-background-task`||e.type===`read-background-task-log`||e.type===`get-background-job-groups`||e.type===`get-background-job-group`||e.type===`wait-background-job-group`}function y(e){return e.type===`cancel-background-task`||e.type===`close-background-task`||e.type===`send-background-task`}function b(e,t,n){if(n.type===`submit`){if(!n.prompt){t({type:`protocol_error`,message:`prompt is required`});return}e.submit(n.prompt)}else if(n.type===`command`){if(!n.name){t({type:`protocol_error`,message:`name is required`});return}e.executeCommand(n.name,n.args??``).then(e=>{t({type:`command_result`,name:n.name,message:e?.message??`Unknown command: ${n.name}`,success:e?.success??!1,data:e?.data})})}else n.type===`abort`?e.abort():e.cancelQueue()}function x(e,t,n){n.type===`get-messages`?t({type:`messages`,messages:e.getMessages()}):n.type===`get-context`?t({type:`context`,state:e.getContextState()}):n.type===`get-executing`?t({type:`executing`,executing:e.isExecuting()}):n.type===`get-execution-workspace`?t({type:`execution_workspace_event`,snapshot:e.getExecutionWorkspaceSnapshot()}):t({type:`pending`,pending:e.getPendingPrompt()})}function S(e){let t=null,n=null;return{name:`ws`,onMessage:null,attach(e){t=e},async start(){if(!t)throw Error(`No session attached. Call attach() first.`);let r=u({session:t,send:e.send});n=r.cleanup,this.onMessage=r.onMessage},async stop(){n?.(),n=null,this.onMessage=null}}}const C=7070;var w=class{name=`ws`;defaultEnabled=!0;optionsSchema={port:{type:`number`,description:`WebSocket server port`,default:C},maxRetries:{type:`number`,description:`Port retry attempts when port is occupied`,default:20}};session=null;stopFn=null;port;maxRetries;constructor(e={}){this.port=e.port??C,this.maxRetries=e.maxRetries??20}attach(e){this.session=e}async start(){if(!this.session)throw Error(`WsTransport: attach() must be called before start()`);let e=await this.bindWithRetry(this.session,this.port,this.maxRetries);this.stopFn=e.stop}async stop(){await this.stopFn?.(),this.stopFn=null}validateOptions(e){let{port:t,maxRetries:n}=e;return!(t!==void 0&&(typeof t!=`number`||t<1||t>65535)||n!==void 0&&(typeof n!=`number`||n<0))}bindWithRetry(e,t,n){return this.tryBind(e,t).catch(r=>{if(r.code===`EADDRINUSE`&&n>0)return this.bindWithRetry(e,t+1,n-1);throw r})}tryBind(n,r){return new Promise((i,a)=>{let o=(0,e.createServer)((e,t)=>{t.writeHead(400).end(`WebSocket endpoint`)});o.on(`error`,e=>{o.close(),a(e)}),o.listen(r,`127.0.0.1`,()=>{let e=new t.WebSocketServer({server:o});e.on(`connection`,e=>{let r=n=>{e.readyState===t.WebSocket.OPEN&&e.send(JSON.stringify(n))},{onMessage:i,cleanup:a}=u({session:n,send:r});e.on(`message`,e=>i(String(e))),e.on(`close`,a),e.on(`error`,a),r({type:`messages`,messages:n.getMessages()}),r({type:`execution_workspace_event`,snapshot:n.getExecutionWorkspaceSnapshot()})}),i({stop:()=>new Promise(t=>{e.close(()=>o.close(()=>t()))})})})})}};Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return S}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return w}});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robota-sdk/agent-transport",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.65",
|
|
4
4
|
"description": "Consolidated transport package for Robota SDK — TUI, headless, HTTP, WebSocket, and MCP adapters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/node/index.js",
|
|
@@ -108,9 +108,9 @@
|
|
|
108
108
|
"string-width": "^8.2.0",
|
|
109
109
|
"ws": "^8.18.3",
|
|
110
110
|
"zod": "^3.24.4",
|
|
111
|
-
"@robota-sdk/agent-
|
|
112
|
-
"@robota-sdk/agent-
|
|
113
|
-
"@robota-sdk/agent-framework": "3.0.0-beta.
|
|
111
|
+
"@robota-sdk/agent-core": "3.0.0-beta.65",
|
|
112
|
+
"@robota-sdk/agent-interface-transport": "3.0.0-beta.65",
|
|
113
|
+
"@robota-sdk/agent-framework": "3.0.0-beta.65"
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
116
|
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"tsx": "^4.7.0",
|
|
124
124
|
"typescript": "^5.3.3",
|
|
125
125
|
"vitest": "^1.6.1",
|
|
126
|
-
"@robota-sdk/agent-command": "3.0.0-beta.
|
|
126
|
+
"@robota-sdk/agent-command": "3.0.0-beta.65"
|
|
127
127
|
},
|
|
128
128
|
"license": "MIT",
|
|
129
129
|
"publishConfig": {
|
package/src/tui/App.tsx
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
import { TuiCliAdapterProvider } from './tui-cli-adapter-context.js';
|
|
42
42
|
import type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
43
43
|
import type { CommandRegistry } from '@robota-sdk/agent-framework';
|
|
44
|
+
import type { ITuiCommandInteraction } from './command-interaction.js';
|
|
44
45
|
|
|
45
46
|
interface IProps {
|
|
46
47
|
cwd: string;
|
|
@@ -67,6 +68,7 @@ interface IProps {
|
|
|
67
68
|
cliAdapter: ITuiCliAdapter;
|
|
68
69
|
reloadPluginCommandSource?: (registry: CommandRegistry) => void;
|
|
69
70
|
agentName?: string;
|
|
71
|
+
resolveInteraction?: (commandName: string) => ITuiCommandInteraction | undefined;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export default function App(props: IProps): React.ReactElement {
|
|
@@ -470,6 +472,7 @@ function AppInner(
|
|
|
470
472
|
registry={registry}
|
|
471
473
|
sessionName={sessionName}
|
|
472
474
|
history={history}
|
|
475
|
+
resolveInteraction={props.resolveInteraction}
|
|
473
476
|
/>
|
|
474
477
|
{/* Permanent blank line below input — required for Korean IME stability. */}
|
|
475
478
|
<Text> </Text>
|
package/src/tui/InputArea.tsx
CHANGED
|
@@ -8,6 +8,8 @@ import type { CommandRegistry, ICommand } from '@robota-sdk/agent-framework';
|
|
|
8
8
|
import CjkTextInput from './CjkTextInput.js';
|
|
9
9
|
import WaveText from './WaveText.js';
|
|
10
10
|
import SlashAutocomplete from './SlashAutocomplete.js';
|
|
11
|
+
import CommandPicker from './interactions/CommandPicker.js';
|
|
12
|
+
import CommandConfirm from './interactions/CommandConfirm.js';
|
|
11
13
|
import { expandPasteLabels } from './utils/paste-labels.js';
|
|
12
14
|
import { useAutocomplete } from './hooks/useAutocomplete.js';
|
|
13
15
|
import {
|
|
@@ -24,6 +26,17 @@ import {
|
|
|
24
26
|
resolveTabCompletion,
|
|
25
27
|
shouldSubmitInput,
|
|
26
28
|
} from './flows/input-area-flow.js';
|
|
29
|
+
import {
|
|
30
|
+
isPickerInteraction,
|
|
31
|
+
isConfirmInteraction,
|
|
32
|
+
type ITuiCommandInteraction,
|
|
33
|
+
type ITuiPickerItem,
|
|
34
|
+
} from './command-interaction.js';
|
|
35
|
+
|
|
36
|
+
interface IActiveInteraction {
|
|
37
|
+
commandName: string;
|
|
38
|
+
interaction: ITuiCommandInteraction;
|
|
39
|
+
}
|
|
27
40
|
|
|
28
41
|
interface IProps {
|
|
29
42
|
onSubmit: (value: string) => void;
|
|
@@ -34,6 +47,7 @@ interface IProps {
|
|
|
34
47
|
registry?: CommandRegistry;
|
|
35
48
|
sessionName?: string;
|
|
36
49
|
history?: readonly IHistoryEntry[];
|
|
50
|
+
resolveInteraction?: (commandName: string) => ITuiCommandInteraction | undefined;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
/**
|
|
@@ -65,9 +79,11 @@ export default function InputArea({
|
|
|
65
79
|
registry,
|
|
66
80
|
sessionName,
|
|
67
81
|
history,
|
|
82
|
+
resolveInteraction,
|
|
68
83
|
}: IProps): React.ReactElement {
|
|
69
84
|
const [value, setValue] = useState('');
|
|
70
85
|
const [cursorHint, setCursorHint] = useState<number | null>(null);
|
|
86
|
+
const [activeInteraction, setActiveInteraction] = useState<IActiveInteraction | null>(null);
|
|
71
87
|
const [historyState, setHistoryState] = useState(createPromptHistoryNavigationState);
|
|
72
88
|
const [localPromptHistory, setLocalPromptHistory] = useState<string[]>([]);
|
|
73
89
|
const restoredPromptHistory = useMemo(() => extractPromptHistory(history ?? []), [history]);
|
|
@@ -139,7 +155,8 @@ export default function InputArea({
|
|
|
139
155
|
/** Enter: insert and execute command immediately */
|
|
140
156
|
const enterSelectCommand = useCallback(
|
|
141
157
|
(cmd: ICommand): void => {
|
|
142
|
-
const
|
|
158
|
+
const interaction = resolveInteraction?.(cmd.name);
|
|
159
|
+
const result = resolveEnterCommandSelection(value, cmd, interaction);
|
|
143
160
|
if (result.type === 'insert') {
|
|
144
161
|
setValue(result.value);
|
|
145
162
|
if (result.selectedIndex !== undefined) {
|
|
@@ -147,10 +164,17 @@ export default function InputArea({
|
|
|
147
164
|
}
|
|
148
165
|
return;
|
|
149
166
|
}
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
if (result.type === 'open-interaction' && interaction?.onMissingArgs) {
|
|
168
|
+
setShowPopup(false);
|
|
169
|
+
setActiveInteraction({ commandName: result.commandName, interaction });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (result.type === 'submit') {
|
|
173
|
+
setValue('');
|
|
174
|
+
submitPrompt(result.value);
|
|
175
|
+
}
|
|
152
176
|
},
|
|
153
|
-
[value, submitPrompt, setSelectedIndex],
|
|
177
|
+
[value, submitPrompt, setSelectedIndex, resolveInteraction, setShowPopup],
|
|
154
178
|
);
|
|
155
179
|
|
|
156
180
|
const handleSubmit = useCallback(
|
|
@@ -238,9 +262,44 @@ export default function InputArea({
|
|
|
238
262
|
return { left: '┌' + '─'.repeat(innerWidth), label: '', right: '┐' };
|
|
239
263
|
})();
|
|
240
264
|
|
|
265
|
+
const handlePickerSelect = useCallback(
|
|
266
|
+
(item: ITuiPickerItem): void => {
|
|
267
|
+
if (!activeInteraction) return;
|
|
268
|
+
setActiveInteraction(null);
|
|
269
|
+
submitPrompt(`/${activeInteraction.commandName} ${item.value}`);
|
|
270
|
+
},
|
|
271
|
+
[activeInteraction, submitPrompt],
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const handleConfirm = useCallback((): void => {
|
|
275
|
+
if (!activeInteraction) return;
|
|
276
|
+
setActiveInteraction(null);
|
|
277
|
+
submitPrompt(`/${activeInteraction.commandName}`);
|
|
278
|
+
}, [activeInteraction, submitPrompt]);
|
|
279
|
+
|
|
280
|
+
const handleInteractionCancel = useCallback((): void => {
|
|
281
|
+
setActiveInteraction(null);
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
241
284
|
return (
|
|
242
285
|
<Box flexDirection="column">
|
|
243
|
-
{
|
|
286
|
+
{activeInteraction && isPickerInteraction(activeInteraction.interaction) && (
|
|
287
|
+
<CommandPicker
|
|
288
|
+
commandName={activeInteraction.commandName}
|
|
289
|
+
interaction={activeInteraction.interaction}
|
|
290
|
+
onSelect={handlePickerSelect}
|
|
291
|
+
onCancel={handleInteractionCancel}
|
|
292
|
+
/>
|
|
293
|
+
)}
|
|
294
|
+
{activeInteraction && isConfirmInteraction(activeInteraction.interaction) && (
|
|
295
|
+
<CommandConfirm
|
|
296
|
+
commandName={activeInteraction.commandName}
|
|
297
|
+
interaction={activeInteraction.interaction}
|
|
298
|
+
onConfirm={handleConfirm}
|
|
299
|
+
onCancel={handleInteractionCancel}
|
|
300
|
+
/>
|
|
301
|
+
)}
|
|
302
|
+
{!activeInteraction && showPopup && (
|
|
244
303
|
<SlashAutocomplete
|
|
245
304
|
commands={filteredCommands}
|
|
246
305
|
selectedIndex={selectedIndex}
|
|
@@ -51,8 +51,7 @@ function CommandRow(props: {
|
|
|
51
51
|
const indicator = isSelected ? '▸ ' : ' ';
|
|
52
52
|
const nameColor = isSelected ? 'cyan' : undefined;
|
|
53
53
|
const dimmed = !isSelected;
|
|
54
|
-
const
|
|
55
|
-
const namePart = capName(displayLabel, nameColWidth);
|
|
54
|
+
const namePart = capName(cmd.name, nameColWidth);
|
|
56
55
|
const text = showSlash
|
|
57
56
|
? `${indicator}/${namePart} ${cmd.description ?? ''}`
|
|
58
57
|
: `${indicator}${namePart} ${cmd.description ?? ''}`;
|
|
@@ -82,7 +81,7 @@ export default function SlashAutocomplete({
|
|
|
82
81
|
|
|
83
82
|
const nameColWidth = Math.min(
|
|
84
83
|
NAME_COL_MAX,
|
|
85
|
-
Math.max(...visibleCommands.map((c) =>
|
|
84
|
+
Math.max(...visibleCommands.map((c) => c.name.length)),
|
|
86
85
|
);
|
|
87
86
|
|
|
88
87
|
return (
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
shouldSubmitInput,
|
|
15
15
|
} from '../flows/input-area-flow.js';
|
|
16
16
|
import type { ICommand } from '@robota-sdk/agent-framework';
|
|
17
|
+
import type { ITuiPickerInteraction } from '../command-interaction.js';
|
|
17
18
|
import {
|
|
18
19
|
createAssistantMessage,
|
|
19
20
|
createSystemMessage,
|
|
@@ -66,6 +67,24 @@ describe('input area flow', () => {
|
|
|
66
67
|
expect(result).toEqual({ type: 'submit', value: '/help' });
|
|
67
68
|
});
|
|
68
69
|
|
|
70
|
+
it('Given interaction declared and no args When enter selects command Then open-interaction is returned', () => {
|
|
71
|
+
const result = resolveEnterCommandSelection('/ex', command('exit'), {
|
|
72
|
+
onMissingArgs: 'confirm',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result).toEqual({ type: 'open-interaction', commandName: 'exit' });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('Given interaction declared but subcommand selected (args present) When enter selects Then submits', () => {
|
|
79
|
+
const pickerInteraction: ITuiPickerInteraction = {
|
|
80
|
+
onMissingArgs: 'picker',
|
|
81
|
+
getItems: () => [],
|
|
82
|
+
};
|
|
83
|
+
const result = resolveEnterCommandSelection('/mode plan', command('plan'), pickerInteraction);
|
|
84
|
+
|
|
85
|
+
expect(result).toEqual({ type: 'submit', value: '/mode plan' });
|
|
86
|
+
});
|
|
87
|
+
|
|
69
88
|
it('Given multiline paste When label change is created Then label is inserted at cursor', () => {
|
|
70
89
|
const result = createPasteLabelChange('abef', 2, 7, 'c\nd\ne');
|
|
71
90
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type TOnMissingArgsAction = 'picker' | 'wizard' | 'confirm';
|
|
2
|
+
|
|
3
|
+
export interface ITuiPickerItem {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ITuiCommandInteraction {
|
|
10
|
+
onMissingArgs?: TOnMissingArgsAction;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ITuiPickerInteraction extends ITuiCommandInteraction {
|
|
14
|
+
onMissingArgs: 'picker';
|
|
15
|
+
getItems(): ITuiPickerItem[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ITuiConfirmInteraction extends ITuiCommandInteraction {
|
|
19
|
+
onMissingArgs: 'confirm';
|
|
20
|
+
message: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TAnyTuiCommandInteraction = ITuiPickerInteraction | ITuiConfirmInteraction;
|
|
24
|
+
|
|
25
|
+
export function isPickerInteraction(
|
|
26
|
+
interaction: ITuiCommandInteraction,
|
|
27
|
+
): interaction is ITuiPickerInteraction {
|
|
28
|
+
return interaction.onMissingArgs === 'picker';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isConfirmInteraction(
|
|
32
|
+
interaction: ITuiCommandInteraction,
|
|
33
|
+
): interaction is ITuiConfirmInteraction {
|
|
34
|
+
return interaction.onMissingArgs === 'confirm';
|
|
35
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IHistoryEntry, TUniversalValue } from '@robota-sdk/agent-core';
|
|
2
2
|
import type { ICommand } from '@robota-sdk/agent-framework';
|
|
3
3
|
import { parseSlashInput } from '../hooks/useAutocomplete.js';
|
|
4
|
+
import type { ITuiCommandInteraction } from '../command-interaction.js';
|
|
4
5
|
|
|
5
6
|
export interface IAutocompleteInputKey {
|
|
6
7
|
upArrow?: boolean;
|
|
@@ -17,7 +18,8 @@ export type TPromptHistoryInputAction = 'previous' | 'next';
|
|
|
17
18
|
|
|
18
19
|
export type TCommandSelectionResult =
|
|
19
20
|
| { type: 'insert'; value: string; selectedIndex?: number }
|
|
20
|
-
| { type: 'submit'; value: string }
|
|
21
|
+
| { type: 'submit'; value: string }
|
|
22
|
+
| { type: 'open-interaction'; commandName: string };
|
|
21
23
|
|
|
22
24
|
export interface IPasteLabelChange {
|
|
23
25
|
value: string;
|
|
@@ -154,11 +156,16 @@ export function resolveTabCompletion(value: string, command: ICommand): TCommand
|
|
|
154
156
|
export function resolveEnterCommandSelection(
|
|
155
157
|
value: string,
|
|
156
158
|
command: ICommand,
|
|
159
|
+
interaction?: ITuiCommandInteraction,
|
|
157
160
|
): TCommandSelectionResult {
|
|
158
161
|
const parsed = parseSlashInput(value);
|
|
159
162
|
if (parsed.parentCommand) {
|
|
160
163
|
return { type: 'submit', value: `/${parsed.parentCommand} ${command.name}` };
|
|
161
164
|
}
|
|
165
|
+
// parentCommand is empty → no args provided beyond the command name itself
|
|
166
|
+
if (interaction?.onMissingArgs) {
|
|
167
|
+
return { type: 'open-interaction', commandName: command.name };
|
|
168
|
+
}
|
|
162
169
|
if (command.subcommands && command.subcommands.length > 0) {
|
|
163
170
|
return { type: 'insert', value: `/${command.name} `, selectedIndex: 0 };
|
|
164
171
|
}
|
package/src/tui/index.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
export { TuiTransport } from './tui-transport.js';
|
|
2
2
|
export type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
3
3
|
export type { IRenderOptions } from './render.js';
|
|
4
|
+
export type {
|
|
5
|
+
TOnMissingArgsAction,
|
|
6
|
+
ITuiPickerItem,
|
|
7
|
+
ITuiCommandInteraction,
|
|
8
|
+
ITuiPickerInteraction,
|
|
9
|
+
ITuiConfirmInteraction,
|
|
10
|
+
TAnyTuiCommandInteraction,
|
|
11
|
+
} from './command-interaction.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import type { ITuiConfirmInteraction } from '../command-interaction.js';
|
|
4
|
+
|
|
5
|
+
interface IProps {
|
|
6
|
+
commandName: string;
|
|
7
|
+
interaction: ITuiConfirmInteraction;
|
|
8
|
+
onConfirm: () => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function CommandConfirm({
|
|
13
|
+
commandName,
|
|
14
|
+
interaction,
|
|
15
|
+
onConfirm,
|
|
16
|
+
onCancel,
|
|
17
|
+
}: IProps): React.ReactElement {
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (key.return || input === 'y' || input === 'Y') {
|
|
20
|
+
onConfirm();
|
|
21
|
+
} else if (key.escape || input === 'n' || input === 'N') {
|
|
22
|
+
onCancel();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Box paddingX={1}>
|
|
28
|
+
<Text bold color="yellow">
|
|
29
|
+
/{commandName}:{' '}
|
|
30
|
+
</Text>
|
|
31
|
+
<Text>{interaction.message} </Text>
|
|
32
|
+
<Text dimColor>[y/n]</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
3
|
+
import type { ITuiPickerInteraction, ITuiPickerItem } from '../command-interaction.js';
|
|
4
|
+
|
|
5
|
+
interface IProps {
|
|
6
|
+
commandName: string;
|
|
7
|
+
interaction: ITuiPickerInteraction;
|
|
8
|
+
onSelect: (item: ITuiPickerItem) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const MAX_VISIBLE = 8;
|
|
13
|
+
const OUTER_CHROME = 4;
|
|
14
|
+
const MIN_ROW_WIDTH = 40;
|
|
15
|
+
|
|
16
|
+
function useRowWidth(): number {
|
|
17
|
+
const { stdout } = useStdout();
|
|
18
|
+
return Math.max(MIN_ROW_WIDTH, (stdout.columns ?? 80) - OUTER_CHROME);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function CommandPicker({
|
|
22
|
+
commandName,
|
|
23
|
+
interaction,
|
|
24
|
+
onSelect,
|
|
25
|
+
onCancel,
|
|
26
|
+
}: IProps): React.ReactElement {
|
|
27
|
+
const items = interaction.getItems();
|
|
28
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
29
|
+
const rowWidth = useRowWidth();
|
|
30
|
+
|
|
31
|
+
useInput((input, key) => {
|
|
32
|
+
if (key.upArrow) {
|
|
33
|
+
setSelectedIndex((i) => (i > 0 ? i - 1 : items.length - 1));
|
|
34
|
+
} else if (key.downArrow) {
|
|
35
|
+
setSelectedIndex((i) => (i < items.length - 1 ? i + 1 : 0));
|
|
36
|
+
} else if (key.return) {
|
|
37
|
+
const item = items[selectedIndex];
|
|
38
|
+
if (item) onSelect(item);
|
|
39
|
+
} else if (key.escape || input === 'q') {
|
|
40
|
+
onCancel();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const scrollOffset = (() => {
|
|
45
|
+
if (items.length <= MAX_VISIBLE) return 0;
|
|
46
|
+
if (selectedIndex < MAX_VISIBLE) return 0;
|
|
47
|
+
return Math.min(selectedIndex - MAX_VISIBLE + 1, items.length - MAX_VISIBLE);
|
|
48
|
+
})();
|
|
49
|
+
const visibleItems = items.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1}>
|
|
53
|
+
<Text bold color="cyan">
|
|
54
|
+
/{commandName}
|
|
55
|
+
</Text>
|
|
56
|
+
{visibleItems.map((item, i) => {
|
|
57
|
+
const isSelected = scrollOffset + i === selectedIndex;
|
|
58
|
+
const indicator = isSelected ? '▸ ' : ' ';
|
|
59
|
+
return (
|
|
60
|
+
<Box key={item.value} width={rowWidth}>
|
|
61
|
+
<Text
|
|
62
|
+
color={isSelected ? 'cyan' : undefined}
|
|
63
|
+
dimColor={!isSelected}
|
|
64
|
+
wrap="truncate-end"
|
|
65
|
+
>
|
|
66
|
+
{indicator}
|
|
67
|
+
{item.label}
|
|
68
|
+
{item.description != null ? ` ${item.description}` : ''}
|
|
69
|
+
</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
<Text dimColor>↑↓ navigate · Enter select · Esc cancel</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
);
|
|
76
|
+
}
|
package/src/tui/render.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from '@robota-sdk/agent-framework';
|
|
20
20
|
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
21
21
|
import type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
22
|
+
import type { ITuiCommandInteraction } from './command-interaction.js';
|
|
22
23
|
|
|
23
24
|
export interface IRenderOptions {
|
|
24
25
|
cwd: string;
|
|
@@ -45,6 +46,7 @@ export interface IRenderOptions {
|
|
|
45
46
|
cliAdapter: ITuiCliAdapter;
|
|
46
47
|
reloadPluginCommandSource?: (registry: CommandRegistry) => void;
|
|
47
48
|
agentName?: string;
|
|
49
|
+
resolveInteraction?: (commandName: string) => ITuiCommandInteraction | undefined;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export async function renderApp(options: IRenderOptions): Promise<void> {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index--Ti9NzQX.d.ts","names":[],"sources":["../../src/tui/tui-cli-adapter.ts","../../src/tui/render.tsx","../../src/tui/tui-transport.ts"],"mappings":";;;;;UAOiB,cAAA;EACf,mBAAA;EACA,YAAA,CAAa,IAAA,WAAe,MAAA,SAAe,eAAA;EAC3C,aAAA,CAAc,IAAA,UAAc,QAAA,EAAU,MAAA,SAAe,eAAA;EACrD,cAAA,CAAe,IAAA;EACf,uBAAA,CACE,IAAA,UACA,KAAA,EAAO,+BAAA,GACN,0BAAA;EACH,yBAAA,CAA0B,QAAA,EAAU,eAAA;EACpC,sBAAA,CACE,GAAA,UACA,OAAA,UACA,OAAA;IAAY,gBAAA;EAAA;IACT,OAAA;EAAA;EACL,YAAA,CAAa,GAAA;EACb,sBAAA,CAAuB,IAAA;AAAA;;;UCDR,cAAA;EACf,GAAA;EACA,QAAA,EAAU,WAAA;EACV,gBAAA;EACA,YAAA;EACA,OAAA;EACA,QAAA;EACA,cAAA,GAAiB,eAAA;EACjB,QAAA;EACA,OAAA;EACA,YAAA,GAAe,wBAAA;EACf,eAAA;EACA,wBAAA;EACA,WAAA;EACA,WAAA;EACA,qBAAA,GAAwB,qBAAA;EACxB,qBAAA,GAAwB,sBAAA;EACxB,cAAA,YAA0B,cAAA;EAC1B,mBAAA,GAAsB,oBAAA;EACtB,SAAA,GAAY,YAAA;EACZ,mBAAA,GAAsB,OAAA;EACtB,iBAAA,GAAoB,sBAAA,CAAuB,mBAAA;EAC3C,UAAA,EAAY,cAAA;EACZ,yBAAA,IAA6B,QAAA,EAAU,eAAA;EACvC,SAAA;AAAA;;;cCzCW,YAAA,YAAwB,sBAAA,CAAuB,mBAAA;EAAA,SACjD,IAAA;EAAA,SACA,cAAA;EAAA,SACA,aAAA;EAAA,iBAEQ,OAAA;cAEL,OAAA,EAAS,cAAA;EAIrB,MAAA,CAAO,QAAA,EAAU,mBAAA;EAIX,KAAA,CAAA,GAAS,OAAA;EAIT,IAAA,CAAA,GAAQ,OAAA;EAId,eAAA,CAAgB,QAAA,EAAU,MAAA,SAAe,eAAA;AAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-CAr3ioVh.d.ts","names":[],"sources":["../../src/tui/tui-cli-adapter.ts","../../src/tui/render.tsx","../../src/tui/tui-transport.ts"],"mappings":";;;;;UAOiB,cAAA;EACf,mBAAA;EACA,YAAA,CAAa,IAAA,WAAe,MAAA,SAAe,eAAA;EAC3C,aAAA,CAAc,IAAA,UAAc,QAAA,EAAU,MAAA,SAAe,eAAA;EACrD,cAAA,CAAe,IAAA;EACf,uBAAA,CACE,IAAA,UACA,KAAA,EAAO,+BAAA,GACN,0BAAA;EACH,yBAAA,CAA0B,QAAA,EAAU,eAAA;EACpC,sBAAA,CACE,GAAA,UACA,OAAA,UACA,OAAA;IAAY,gBAAA;EAAA;IACT,OAAA;EAAA;EACL,YAAA,CAAa,GAAA;EACb,sBAAA,CAAuB,IAAA;AAAA;;;UCDR,cAAA;EACf,GAAA;EACA,QAAA,EAAU,WAAA;EACV,gBAAA;EACA,YAAA;EACA,OAAA;EACA,QAAA;EACA,cAAA,GAAiB,eAAA;EACjB,QAAA;EACA,OAAA;EACA,YAAA,GAAe,wBAAA;EACf,eAAA;EACA,wBAAA;EACA,WAAA;EACA,WAAA;EACA,qBAAA,GAAwB,qBAAA;EACxB,qBAAA,GAAwB,sBAAA;EACxB,cAAA,YAA0B,cAAA;EAC1B,mBAAA,GAAsB,oBAAA;EACtB,SAAA,GAAY,YAAA;EACZ,mBAAA,GAAsB,OAAA;EACtB,iBAAA,GAAoB,sBAAA,CAAuB,mBAAA;EAC3C,UAAA,EAAY,cAAA;EACZ,yBAAA,IAA6B,QAAA,EAAU,eAAA;EACvC,SAAA;AAAA;;;cCzCW,YAAA,YAAwB,sBAAA,CAAuB,mBAAA;EAAA,SACjD,IAAA;EAAA,SACA,cAAA;EAAA,SACA,aAAA;EAAA,iBAEQ,OAAA;cAEL,OAAA,EAAS,cAAA;EAIrB,MAAA,CAAO,QAAA,EAAU,mBAAA;EAIX,KAAA,CAAA,GAAS,OAAA;EAIT,IAAA,CAAA,GAAQ,OAAA;EAId,eAAA,CAAgB,QAAA,EAAU,MAAA,SAAe,eAAA;AAAA"}
|