@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.
- package/LICENSE +21 -0
- package/dist/node/headless/index.cjs +1 -0
- package/dist/node/headless/index.d.ts +2 -0
- package/dist/node/headless/index.js +1 -0
- package/dist/node/headless-CWEpJXFK.js +7 -0
- package/dist/node/headless-CWEpJXFK.js.map +1 -0
- package/dist/node/headless-CsZFelG9.cjs +6 -0
- package/dist/node/http/index.cjs +1 -0
- package/dist/node/http/index.d.ts +2 -0
- package/dist/node/http/index.js +1 -0
- package/dist/node/http-CM3TJhrF.cjs +1 -0
- package/dist/node/http-DwO1AHG-.js +2 -0
- package/dist/node/http-DwO1AHG-.js.map +1 -0
- package/dist/node/index--Ti9NzQX.d.ts +64 -0
- package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
- package/dist/node/index-B_rcr14p.d.ts +47 -0
- package/dist/node/index-B_rcr14p.d.ts.map +1 -0
- package/dist/node/index-C9LWCL4l.d.ts +34 -0
- package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
- package/dist/node/index-CAr3ioVh.d.ts +64 -0
- package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
- package/dist/node/index-CEs25wVk.d.ts +213 -0
- package/dist/node/index-CEs25wVk.d.ts.map +1 -0
- package/dist/node/index-CvXLpjJO.d.ts +213 -0
- package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
- package/dist/node/index-D34WUfFH.d.ts +26 -0
- package/dist/node/index-D34WUfFH.d.ts.map +1 -0
- package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
- package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
- package/dist/node/index-k3TUjA-T.d.ts +26 -0
- package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
- package/dist/node/index-nBlMTFkZ.d.ts +34 -0
- package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +1 -0
- package/dist/node/mcp/index.cjs +1 -0
- package/dist/node/mcp/index.d.ts +2 -0
- package/dist/node/mcp/index.js +1 -0
- package/dist/node/mcp-BXBwF6Wu.js +2 -0
- package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
- package/dist/node/mcp-DcHuGokt.cjs +1 -0
- package/dist/node/tui/index.cjs +1 -0
- package/dist/node/tui/index.d.ts +2 -0
- package/dist/node/tui/index.js +1 -0
- package/dist/node/tui-CeD_6rSo.cjs +24 -0
- package/dist/node/tui-zmDTPk4b.js +25 -0
- package/dist/node/tui-zmDTPk4b.js.map +1 -0
- package/dist/node/ws/index.cjs +1 -0
- package/dist/node/ws/index.d.ts +2 -0
- package/dist/node/ws/index.js +1 -0
- package/dist/node/ws-B-oRccFl.js +2 -0
- package/dist/node/ws-B-oRccFl.js.map +1 -0
- package/dist/node/ws-COnIgnmn.cjs +1 -0
- package/package.json +141 -0
- package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
- package/src/headless/__tests__/headless-runner.test.ts +484 -0
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
- package/src/headless/__tests__/headless-transport.test.ts +268 -0
- package/src/headless/headless-runner.ts +141 -0
- package/src/headless/headless-stream-json.ts +142 -0
- package/src/headless/headless-transport.ts +43 -0
- package/src/headless/index.ts +4 -0
- package/src/http/__tests__/http-transport.test.ts +55 -0
- package/src/http/__tests__/routes.test.ts +168 -0
- package/src/http/http-transport.ts +42 -0
- package/src/http/index.ts +4 -0
- package/src/http/routes.ts +151 -0
- package/src/index.ts +5 -0
- package/src/mcp/__tests__/mcp-server.test.ts +66 -0
- package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp-server.ts +162 -0
- package/src/mcp/mcp-transport.ts +48 -0
- package/src/tui/App.tsx +478 -0
- package/src/tui/BackgroundTaskPanel.tsx +34 -0
- package/src/tui/CjkTextInput.tsx +204 -0
- package/src/tui/ConfirmPrompt.tsx +69 -0
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
- package/src/tui/InkTerminal.ts +42 -0
- package/src/tui/InputArea.tsx +298 -0
- package/src/tui/InteractivePrompt.tsx +57 -0
- package/src/tui/ListPicker.tsx +94 -0
- package/src/tui/MenuSelect.tsx +103 -0
- package/src/tui/MessageList.tsx +282 -0
- package/src/tui/PermissionPrompt.tsx +84 -0
- package/src/tui/PluginTUI.tsx +256 -0
- package/src/tui/SessionPicker.tsx +66 -0
- package/src/tui/SessionStatusBar.tsx +66 -0
- package/src/tui/SlashAutocomplete.tsx +110 -0
- package/src/tui/StatusBar.tsx +213 -0
- package/src/tui/StreamingIndicator.tsx +91 -0
- package/src/tui/TextPrompt.tsx +80 -0
- package/src/tui/ToolCommandOutput.tsx +37 -0
- package/src/tui/ToolDiffBlock.tsx +30 -0
- package/src/tui/TransportTUI.tsx +116 -0
- package/src/tui/UpdateNotice.tsx +14 -0
- package/src/tui/UsageSummaryEntry.tsx +38 -0
- package/src/tui/WaveText.tsx +44 -0
- package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
- package/src/tui/__tests__/ListPicker.test.tsx +159 -0
- package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
- package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
- package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
- package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
- package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
- package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
- package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
- package/src/tui/__tests__/command-output-summary.test.ts +95 -0
- package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
- package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
- package/src/tui/__tests__/input-area-flow.test.ts +152 -0
- package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
- package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
- package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
- package/src/tui/__tests__/render-markdown.test.ts +72 -0
- package/src/tui/__tests__/selection-flow.test.ts +61 -0
- package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
- package/src/tui/__tests__/status-activity.test.ts +71 -0
- package/src/tui/__tests__/status-bar.test.tsx +157 -0
- package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
- package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
- package/src/tui/background-task-row-format.ts +52 -0
- package/src/tui/command-output-summary.ts +122 -0
- package/src/tui/execution-workspace-view-model.ts +123 -0
- package/src/tui/flows/cjk-text-input-flow.ts +285 -0
- package/src/tui/flows/confirm-prompt-flow.ts +45 -0
- package/src/tui/flows/input-area-flow.ts +186 -0
- package/src/tui/flows/permission-prompt-flow.ts +76 -0
- package/src/tui/flows/selection-flow.ts +126 -0
- package/src/tui/flows/text-prompt-flow.ts +98 -0
- package/src/tui/hooks/command-effect-handler.ts +98 -0
- package/src/tui/hooks/command-effect-queue.ts +39 -0
- package/src/tui/hooks/model-change-side-effect.ts +63 -0
- package/src/tui/hooks/side-effects-types.ts +38 -0
- package/src/tui/hooks/use-interactive-session-init.ts +50 -0
- package/src/tui/hooks/useAutocomplete.ts +85 -0
- package/src/tui/hooks/useInteractiveSession.ts +273 -0
- package/src/tui/hooks/usePermissionQueue.ts +51 -0
- package/src/tui/hooks/usePluginCallbacks.ts +30 -0
- package/src/tui/hooks/usePluginScreenData.ts +84 -0
- package/src/tui/hooks/useSideEffects.ts +210 -0
- package/src/tui/hooks/useSlashRouting.ts +117 -0
- package/src/tui/hooks/useStatusLineSettings.ts +35 -0
- package/src/tui/index.ts +3 -0
- package/src/tui/plugin-tui-handlers.ts +163 -0
- package/src/tui/render-markdown.ts +129 -0
- package/src/tui/render.tsx +60 -0
- package/src/tui/status-activity.ts +63 -0
- package/src/tui/tui-cli-adapter-context.tsx +12 -0
- package/src/tui/tui-cli-adapter.ts +25 -0
- package/src/tui/tui-state-manager.ts +225 -0
- package/src/tui/tui-transport.ts +32 -0
- package/src/tui/types.ts +14 -0
- package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/tui/utils/edit-diff.ts +152 -0
- package/src/tui/utils/paste-labels.ts +9 -0
- package/src/tui/utils/tool-call-extractor.ts +91 -0
- package/src/tui/utils/tool-diff-summary.ts +75 -0
- package/src/ws/__tests__/ws-handler.test.ts +407 -0
- package/src/ws/__tests__/ws-transport.test.ts +53 -0
- package/src/ws/index.ts +13 -0
- package/src/ws/ws-background-messages.ts +170 -0
- package/src/ws/ws-handler.ts +279 -0
- package/src/ws/ws-protocol.ts +76 -0
- package/src/ws/ws-transport-configurable.ts +123 -0
- package/src/ws/ws-transport.ts +42 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IExecutionDetailRecord,
|
|
3
|
+
IExecutionWorkspaceEntry,
|
|
4
|
+
IExecutionWorkspaceSnapshot,
|
|
5
|
+
TExecutionWorkspaceStatus,
|
|
6
|
+
} from '@robota-sdk/agent-framework';
|
|
7
|
+
|
|
8
|
+
const ACTIVE_STATUSES: readonly TExecutionWorkspaceStatus[] = [
|
|
9
|
+
'active',
|
|
10
|
+
'queued',
|
|
11
|
+
'running',
|
|
12
|
+
'waiting_permission',
|
|
13
|
+
'sleeping',
|
|
14
|
+
];
|
|
15
|
+
const DETAIL_RECORD_TEXT_LIMIT = 160;
|
|
16
|
+
const PREVIEW_WHITESPACE = /\s+/g;
|
|
17
|
+
const PREVIEW_SEPARATOR = ' ';
|
|
18
|
+
|
|
19
|
+
export interface IExecutionWorkspaceEntryRow {
|
|
20
|
+
id: string;
|
|
21
|
+
radio: '●' | '○';
|
|
22
|
+
title: string;
|
|
23
|
+
subtitle?: string;
|
|
24
|
+
statusLabel: string;
|
|
25
|
+
preview?: string;
|
|
26
|
+
color: string;
|
|
27
|
+
isSelected: boolean;
|
|
28
|
+
accessibleText: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface IExecutionWorkspaceEntryRowOptions {
|
|
32
|
+
selectedEntryId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getDefaultBackgroundWorkspaceEntries(
|
|
36
|
+
snapshot: IExecutionWorkspaceSnapshot | null,
|
|
37
|
+
): IExecutionWorkspaceEntry[] {
|
|
38
|
+
return (snapshot?.entries ?? []).filter(
|
|
39
|
+
(entry) => entry.kind === 'background_task' && entry.visibility === 'default',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function countActiveBackgroundWorkspaceEntries(
|
|
44
|
+
snapshot: IExecutionWorkspaceSnapshot | null,
|
|
45
|
+
): number {
|
|
46
|
+
return getDefaultBackgroundWorkspaceEntries(snapshot).filter((entry) =>
|
|
47
|
+
ACTIVE_STATUSES.includes(entry.status),
|
|
48
|
+
).length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function formatExecutionWorkspaceEntryRow(
|
|
52
|
+
entry: IExecutionWorkspaceEntry,
|
|
53
|
+
options: IExecutionWorkspaceEntryRowOptions = {},
|
|
54
|
+
): IExecutionWorkspaceEntryRow {
|
|
55
|
+
const isSelected = entry.id === options.selectedEntryId;
|
|
56
|
+
const row = {
|
|
57
|
+
id: entry.id,
|
|
58
|
+
radio: isSelected ? '●' : '○',
|
|
59
|
+
title: formatEntryTitle(entry),
|
|
60
|
+
subtitle: formatEntrySubtitle(entry),
|
|
61
|
+
statusLabel: formatStatusLabel(entry.status),
|
|
62
|
+
preview: trimPreview(entry.preview ?? entry.currentAction),
|
|
63
|
+
color: getEntryColor(entry),
|
|
64
|
+
isSelected,
|
|
65
|
+
} satisfies Omit<IExecutionWorkspaceEntryRow, 'accessibleText'>;
|
|
66
|
+
return { ...row, accessibleText: formatAccessibleText(row) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatExecutionDetailRecord(record: IExecutionDetailRecord): string {
|
|
70
|
+
const text = record.text.trim().replace(PREVIEW_WHITESPACE, PREVIEW_SEPARATOR);
|
|
71
|
+
if (!text) return record.kind;
|
|
72
|
+
return text.length > DETAIL_RECORD_TEXT_LIMIT
|
|
73
|
+
? `${text.slice(0, DETAIL_RECORD_TEXT_LIMIT)}...`
|
|
74
|
+
: text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatEntryTitle(entry: IExecutionWorkspaceEntry): string {
|
|
78
|
+
if (entry.kind === 'main_thread') return entry.title;
|
|
79
|
+
if (entry.kind === 'background_group') return `${entry.title} group`;
|
|
80
|
+
if (entry.taskKind === 'agent') return `${entry.title} agent`;
|
|
81
|
+
if (entry.taskKind === 'process') return entry.title || 'Process';
|
|
82
|
+
if (entry.taskKind === 'scheduled') return entry.title || 'Scheduled';
|
|
83
|
+
return entry.title;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatEntrySubtitle(entry: IExecutionWorkspaceEntry): string | undefined {
|
|
87
|
+
if (entry.kind === 'main_thread') return entry.subtitle;
|
|
88
|
+
const parts = [
|
|
89
|
+
entry.taskKind,
|
|
90
|
+
entry.subtitle,
|
|
91
|
+
entry.attention === 'none' ? undefined : entry.attention,
|
|
92
|
+
];
|
|
93
|
+
return (
|
|
94
|
+
parts
|
|
95
|
+
.filter((part): part is string => typeof part === 'string' && part.length > 0)
|
|
96
|
+
.join(' · ') || undefined
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatStatusLabel(status: TExecutionWorkspaceStatus): string {
|
|
101
|
+
return status.replace(/_/g, ' ');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getEntryColor(entry: IExecutionWorkspaceEntry): string {
|
|
105
|
+
if (entry.attention === 'failed' || entry.status === 'failed') return 'red';
|
|
106
|
+
if (entry.attention === 'permission' || entry.status === 'waiting_permission') return 'yellow';
|
|
107
|
+
if (entry.status === 'completed') return 'green';
|
|
108
|
+
if (entry.status === 'cancelled') return 'yellow';
|
|
109
|
+
if (ACTIVE_STATUSES.includes(entry.status)) return 'cyan';
|
|
110
|
+
return 'white';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function trimPreview(value: string | undefined): string | undefined {
|
|
114
|
+
const preview = value?.trim().replace(PREVIEW_WHITESPACE, PREVIEW_SEPARATOR);
|
|
115
|
+
return preview || undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatAccessibleText(row: Omit<IExecutionWorkspaceEntryRow, 'accessibleText'>): string {
|
|
119
|
+
const parts = [row.radio, row.title, row.statusLabel, row.subtitle, row.preview];
|
|
120
|
+
return parts
|
|
121
|
+
.filter((part): part is string => typeof part === 'string' && part.length > 0)
|
|
122
|
+
.join(' · ');
|
|
123
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import stringWidth from 'string-width';
|
|
2
|
+
|
|
3
|
+
const PASTE_START = '[200~';
|
|
4
|
+
const PASTE_END = '[201~';
|
|
5
|
+
const LAST_ASCII_CONTROL_CODE = 0x1f;
|
|
6
|
+
const DELETE_CONTROL_CODE = 0x7f;
|
|
7
|
+
|
|
8
|
+
export interface ICjkTextInputFlowState {
|
|
9
|
+
value: string;
|
|
10
|
+
cursor: number;
|
|
11
|
+
isPasting: boolean;
|
|
12
|
+
pasteBuffer: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ICjkTextInputKey {
|
|
16
|
+
ctrl?: boolean;
|
|
17
|
+
tab?: boolean;
|
|
18
|
+
shift?: boolean;
|
|
19
|
+
upArrow?: boolean;
|
|
20
|
+
downArrow?: boolean;
|
|
21
|
+
return?: boolean;
|
|
22
|
+
leftArrow?: boolean;
|
|
23
|
+
rightArrow?: boolean;
|
|
24
|
+
backspace?: boolean;
|
|
25
|
+
delete?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ICjkTextInputFlowOptions {
|
|
29
|
+
availableWidth?: number;
|
|
30
|
+
canPaste: boolean;
|
|
31
|
+
enableVerticalNavigation?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type TCjkTextInputEffect =
|
|
35
|
+
| { type: 'none' }
|
|
36
|
+
| { type: 'change'; value: string }
|
|
37
|
+
| { type: 'submit'; value: string }
|
|
38
|
+
| { type: 'paste'; text: string; cursor: number }
|
|
39
|
+
| { type: 'render' };
|
|
40
|
+
|
|
41
|
+
interface ICjkTextInputFlowResult {
|
|
42
|
+
state: ICjkTextInputFlowState;
|
|
43
|
+
effect: TCjkTextInputEffect;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createCjkTextInputFlowState(value: string): ICjkTextInputFlowState {
|
|
47
|
+
return { value, cursor: value.length, isPasting: false, pasteBuffer: '' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function syncCjkTextInputFlowState(
|
|
51
|
+
state: ICjkTextInputFlowState,
|
|
52
|
+
value: string,
|
|
53
|
+
cursorHint: number | null,
|
|
54
|
+
): ICjkTextInputFlowState {
|
|
55
|
+
if (value === state.value) {
|
|
56
|
+
return state;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
value,
|
|
61
|
+
cursor: cursorHint != null ? Math.min(cursorHint, value.length) : value.length,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function applyCjkTextInput(
|
|
66
|
+
state: ICjkTextInputFlowState,
|
|
67
|
+
input: string,
|
|
68
|
+
key: ICjkTextInputKey,
|
|
69
|
+
options: ICjkTextInputFlowOptions,
|
|
70
|
+
): ICjkTextInputFlowResult {
|
|
71
|
+
const pasteResult = applyPasteBoundaryInput(state, input, options);
|
|
72
|
+
if (pasteResult !== undefined) return pasteResult;
|
|
73
|
+
const controlResult = applyControlInput(state, input, key, options);
|
|
74
|
+
if (controlResult !== undefined) return controlResult;
|
|
75
|
+
const cursorResult = applyCursorInput(state, key, options);
|
|
76
|
+
if (cursorResult !== undefined) return cursorResult;
|
|
77
|
+
return insertPrintableInput(state, input);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function applyCjkTextPaste(
|
|
81
|
+
state: ICjkTextInputFlowState,
|
|
82
|
+
text: string,
|
|
83
|
+
options: ICjkTextInputFlowOptions,
|
|
84
|
+
): ICjkTextInputFlowResult {
|
|
85
|
+
const normalizedText = text.replace(/\r\n?/g, '\n');
|
|
86
|
+
if (normalizedText.length === 0) {
|
|
87
|
+
return { state, effect: { type: 'none' } };
|
|
88
|
+
}
|
|
89
|
+
if (normalizedText.includes('\n') && options.canPaste) {
|
|
90
|
+
return { state, effect: { type: 'paste', text: normalizedText, cursor: state.cursor } };
|
|
91
|
+
}
|
|
92
|
+
return insertPrintableInput(state, normalizedText);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyPasteBoundaryInput(
|
|
96
|
+
state: ICjkTextInputFlowState,
|
|
97
|
+
input: string,
|
|
98
|
+
options: ICjkTextInputFlowOptions,
|
|
99
|
+
): ICjkTextInputFlowResult | undefined {
|
|
100
|
+
if (input === PASTE_START || input.startsWith(PASTE_START)) {
|
|
101
|
+
return startBracketedPaste(state, input);
|
|
102
|
+
}
|
|
103
|
+
if (state.isPasting) {
|
|
104
|
+
return continueBracketedPaste(state, input, options);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyControlInput(
|
|
110
|
+
state: ICjkTextInputFlowState,
|
|
111
|
+
input: string,
|
|
112
|
+
key: ICjkTextInputKey,
|
|
113
|
+
options: ICjkTextInputFlowOptions,
|
|
114
|
+
): ICjkTextInputFlowResult | undefined {
|
|
115
|
+
if ((key.ctrl === true && input === 'c') || key.tab === true) {
|
|
116
|
+
return { state, effect: { type: 'none' } };
|
|
117
|
+
}
|
|
118
|
+
if (key.return === true) {
|
|
119
|
+
return { state, effect: { type: 'submit', value: state.value } };
|
|
120
|
+
}
|
|
121
|
+
if (input.length > 1 && (input.includes('\n') || input.includes('\r')) && options.canPaste) {
|
|
122
|
+
return {
|
|
123
|
+
state,
|
|
124
|
+
effect: { type: 'paste', text: input.replace(/\r\n?/g, '\n'), cursor: state.cursor },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function applyCursorInput(
|
|
131
|
+
state: ICjkTextInputFlowState,
|
|
132
|
+
key: ICjkTextInputKey,
|
|
133
|
+
options: ICjkTextInputFlowOptions,
|
|
134
|
+
): ICjkTextInputFlowResult | undefined {
|
|
135
|
+
if (key.upArrow === true || key.downArrow === true) {
|
|
136
|
+
if (options.enableVerticalNavigation === false) {
|
|
137
|
+
return { state, effect: { type: 'none' } };
|
|
138
|
+
}
|
|
139
|
+
return moveCursorVertically(
|
|
140
|
+
state,
|
|
141
|
+
key.upArrow === true ? 'up' : 'down',
|
|
142
|
+
options.availableWidth,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (key.leftArrow === true) {
|
|
146
|
+
return moveCursorHorizontally(state, 'left');
|
|
147
|
+
}
|
|
148
|
+
if (key.rightArrow === true) {
|
|
149
|
+
return moveCursorHorizontally(state, 'right');
|
|
150
|
+
}
|
|
151
|
+
if (key.backspace === true || key.delete === true) {
|
|
152
|
+
return deleteBeforeCursor(state);
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function filterPrintable(input: string | null | undefined): string {
|
|
158
|
+
if (!input || input.length === 0) return '';
|
|
159
|
+
let output = '';
|
|
160
|
+
for (const char of input) {
|
|
161
|
+
const code = char.charCodeAt(0);
|
|
162
|
+
if (code > LAST_ASCII_CONTROL_CODE && code !== DELETE_CONTROL_CODE) {
|
|
163
|
+
output += char;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return output;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function insertAtCursor(
|
|
170
|
+
value: string,
|
|
171
|
+
cursor: number,
|
|
172
|
+
input: string,
|
|
173
|
+
): { value: string; cursor: number } {
|
|
174
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
175
|
+
return { value: next, cursor: cursor + input.length };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function displayOffset(chars: string[], charIndex: number, width: number): number {
|
|
179
|
+
let offset = 0;
|
|
180
|
+
for (let i = 0; i < charIndex && i < chars.length; i++) {
|
|
181
|
+
const w = stringWidth(chars[i]!);
|
|
182
|
+
const col = offset % width;
|
|
183
|
+
if (col + w > width) offset += width - col;
|
|
184
|
+
offset += w;
|
|
185
|
+
}
|
|
186
|
+
return offset;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function charIndexAtDisplayOffset(chars: string[], target: number, width: number): number {
|
|
190
|
+
let offset = 0;
|
|
191
|
+
for (let i = 0; i < chars.length; i++) {
|
|
192
|
+
if (offset >= target) return i;
|
|
193
|
+
const w = stringWidth(chars[i]!);
|
|
194
|
+
const col = offset % width;
|
|
195
|
+
if (col + w > width) offset += width - col;
|
|
196
|
+
offset += w;
|
|
197
|
+
}
|
|
198
|
+
return chars.length;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function startBracketedPaste(
|
|
202
|
+
state: ICjkTextInputFlowState,
|
|
203
|
+
input: string,
|
|
204
|
+
): ICjkTextInputFlowResult {
|
|
205
|
+
return {
|
|
206
|
+
state: { ...state, isPasting: true, pasteBuffer: input.slice(PASTE_START.length) },
|
|
207
|
+
effect: { type: 'none' },
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function continueBracketedPaste(
|
|
212
|
+
state: ICjkTextInputFlowState,
|
|
213
|
+
input: string,
|
|
214
|
+
options: ICjkTextInputFlowOptions,
|
|
215
|
+
): ICjkTextInputFlowResult {
|
|
216
|
+
if (input !== PASTE_END && !input.includes(PASTE_END)) {
|
|
217
|
+
return {
|
|
218
|
+
state: { ...state, pasteBuffer: state.pasteBuffer + input },
|
|
219
|
+
effect: { type: 'none' },
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const beforeMarker = input.split(PASTE_END)[0] ?? '';
|
|
223
|
+
const nextState = { ...state, isPasting: false, pasteBuffer: '' };
|
|
224
|
+
return applyCjkTextPaste(nextState, state.pasteBuffer + beforeMarker, options);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function moveCursorVertically(
|
|
228
|
+
state: ICjkTextInputFlowState,
|
|
229
|
+
direction: 'up' | 'down',
|
|
230
|
+
availableWidth: number | undefined,
|
|
231
|
+
): ICjkTextInputFlowResult {
|
|
232
|
+
if (!availableWidth || availableWidth <= 0) {
|
|
233
|
+
return { state, effect: { type: 'none' } };
|
|
234
|
+
}
|
|
235
|
+
const chars = [...state.value];
|
|
236
|
+
const offset = displayOffset(chars, state.cursor, availableWidth);
|
|
237
|
+
const target = direction === 'up' ? offset - availableWidth : offset + availableWidth;
|
|
238
|
+
if (target < 0) {
|
|
239
|
+
return { state, effect: { type: 'none' } };
|
|
240
|
+
}
|
|
241
|
+
const cursor = charIndexAtDisplayOffset(chars, target, availableWidth);
|
|
242
|
+
if (cursor === state.cursor) {
|
|
243
|
+
return { state, effect: { type: 'none' } };
|
|
244
|
+
}
|
|
245
|
+
return { state: { ...state, cursor }, effect: { type: 'render' } };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function moveCursorHorizontally(
|
|
249
|
+
state: ICjkTextInputFlowState,
|
|
250
|
+
direction: 'left' | 'right',
|
|
251
|
+
): ICjkTextInputFlowResult {
|
|
252
|
+
if (direction === 'left' && state.cursor > 0) {
|
|
253
|
+
return { state: { ...state, cursor: state.cursor - 1 }, effect: { type: 'render' } };
|
|
254
|
+
}
|
|
255
|
+
if (direction === 'right' && state.cursor < state.value.length) {
|
|
256
|
+
return { state: { ...state, cursor: state.cursor + 1 }, effect: { type: 'render' } };
|
|
257
|
+
}
|
|
258
|
+
return { state, effect: { type: 'none' } };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function deleteBeforeCursor(state: ICjkTextInputFlowState): ICjkTextInputFlowResult {
|
|
262
|
+
if (state.cursor === 0) {
|
|
263
|
+
return { state, effect: { type: 'none' } };
|
|
264
|
+
}
|
|
265
|
+
const value = state.value.slice(0, state.cursor - 1) + state.value.slice(state.cursor);
|
|
266
|
+
return {
|
|
267
|
+
state: { ...state, value, cursor: state.cursor - 1 },
|
|
268
|
+
effect: { type: 'change', value },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function insertPrintableInput(
|
|
273
|
+
state: ICjkTextInputFlowState,
|
|
274
|
+
input: string,
|
|
275
|
+
): ICjkTextInputFlowResult {
|
|
276
|
+
const printable = filterPrintable(input);
|
|
277
|
+
if (printable.length === 0) {
|
|
278
|
+
return { state, effect: { type: 'none' } };
|
|
279
|
+
}
|
|
280
|
+
const result = insertAtCursor(state.value, state.cursor, printable);
|
|
281
|
+
return {
|
|
282
|
+
state: { ...state, value: result.value, cursor: result.cursor },
|
|
283
|
+
effect: { type: 'change', value: result.value },
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applySelectionInput,
|
|
3
|
+
getDirectionalSelectionInputAction,
|
|
4
|
+
type ISelectionFlowState,
|
|
5
|
+
type ISelectionInputKey,
|
|
6
|
+
type TSelectionEffect,
|
|
7
|
+
type TSelectionInputAction,
|
|
8
|
+
} from './selection-flow.js';
|
|
9
|
+
|
|
10
|
+
export type TConfirmPromptInputAction = TSelectionInputAction | { type: 'shortcut'; index: number };
|
|
11
|
+
|
|
12
|
+
export function getConfirmPromptInputAction(
|
|
13
|
+
input: string,
|
|
14
|
+
key: ISelectionInputKey,
|
|
15
|
+
optionCount: number,
|
|
16
|
+
): TConfirmPromptInputAction | undefined {
|
|
17
|
+
const action = getDirectionalSelectionInputAction({ ...key, escape: false });
|
|
18
|
+
if (action !== undefined) {
|
|
19
|
+
return action;
|
|
20
|
+
}
|
|
21
|
+
if (optionCount === 2 && input === 'y') {
|
|
22
|
+
return { type: 'shortcut', index: 0 };
|
|
23
|
+
}
|
|
24
|
+
if (optionCount === 2 && input === 'n') {
|
|
25
|
+
return { type: 'shortcut', index: 1 };
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function applyConfirmPromptInput(
|
|
31
|
+
state: ISelectionFlowState,
|
|
32
|
+
action: TConfirmPromptInputAction,
|
|
33
|
+
optionCount: number,
|
|
34
|
+
): { state: ISelectionFlowState; effect: TSelectionEffect } {
|
|
35
|
+
if (state.resolved) {
|
|
36
|
+
return { state, effect: { type: 'none' } };
|
|
37
|
+
}
|
|
38
|
+
if (typeof action !== 'string') {
|
|
39
|
+
return {
|
|
40
|
+
state: { ...state, selectedIndex: action.index, resolved: true },
|
|
41
|
+
effect: { type: 'select', index: action.index },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return applySelectionInput(state, action, { itemCount: optionCount });
|
|
45
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { IHistoryEntry, TUniversalValue } from '@robota-sdk/agent-core';
|
|
2
|
+
import type { ICommand } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { parseSlashInput } from '../hooks/useAutocomplete.js';
|
|
4
|
+
|
|
5
|
+
export interface IAutocompleteInputKey {
|
|
6
|
+
upArrow?: boolean;
|
|
7
|
+
downArrow?: boolean;
|
|
8
|
+
escape?: boolean;
|
|
9
|
+
tab?: boolean;
|
|
10
|
+
backspace?: boolean;
|
|
11
|
+
delete?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TAutocompletePopupAction = 'previous' | 'next' | 'close' | 'complete';
|
|
15
|
+
export type TPendingPromptInputAction = 'cancelQueue';
|
|
16
|
+
export type TPromptHistoryInputAction = 'previous' | 'next';
|
|
17
|
+
|
|
18
|
+
export type TCommandSelectionResult =
|
|
19
|
+
| { type: 'insert'; value: string; selectedIndex?: number }
|
|
20
|
+
| { type: 'submit'; value: string };
|
|
21
|
+
|
|
22
|
+
export interface IPasteLabelChange {
|
|
23
|
+
value: string;
|
|
24
|
+
cursorHint: number;
|
|
25
|
+
label: string;
|
|
26
|
+
lineCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IPromptHistoryNavigationState {
|
|
30
|
+
selectedIndex: number | null;
|
|
31
|
+
draft: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IPromptHistoryNavigationResult {
|
|
35
|
+
value: string;
|
|
36
|
+
cursorHint: number;
|
|
37
|
+
state: IPromptHistoryNavigationState;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getAutocompletePopupAction(
|
|
41
|
+
key: IAutocompleteInputKey,
|
|
42
|
+
): TAutocompletePopupAction | undefined {
|
|
43
|
+
if (key.upArrow === true) return 'previous';
|
|
44
|
+
if (key.downArrow === true) return 'next';
|
|
45
|
+
if (key.escape === true) return 'close';
|
|
46
|
+
if (key.tab === true) return 'complete';
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getPendingPromptInputAction(
|
|
51
|
+
key: IAutocompleteInputKey,
|
|
52
|
+
): TPendingPromptInputAction | undefined {
|
|
53
|
+
if (key.backspace === true || key.delete === true) {
|
|
54
|
+
return 'cancelQueue';
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getPromptHistoryInputAction(
|
|
60
|
+
key: IAutocompleteInputKey,
|
|
61
|
+
): TPromptHistoryInputAction | undefined {
|
|
62
|
+
if (key.upArrow === true) return 'previous';
|
|
63
|
+
if (key.downArrow === true) return 'next';
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createPromptHistoryNavigationState(): IPromptHistoryNavigationState {
|
|
68
|
+
return { selectedIndex: null, draft: '' };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function navigatePromptHistory(
|
|
72
|
+
value: string,
|
|
73
|
+
history: readonly string[],
|
|
74
|
+
state: IPromptHistoryNavigationState,
|
|
75
|
+
action: TPromptHistoryInputAction,
|
|
76
|
+
): IPromptHistoryNavigationResult {
|
|
77
|
+
if (history.length === 0) {
|
|
78
|
+
return { value, cursorHint: value.length, state };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (action === 'previous') {
|
|
82
|
+
const selectedIndex =
|
|
83
|
+
state.selectedIndex === null ? history.length - 1 : Math.max(0, state.selectedIndex - 1);
|
|
84
|
+
const nextValue = history[selectedIndex] ?? value;
|
|
85
|
+
return {
|
|
86
|
+
value: nextValue,
|
|
87
|
+
cursorHint: nextValue.length,
|
|
88
|
+
state: { selectedIndex, draft: state.selectedIndex === null ? value : state.draft },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (state.selectedIndex === null) {
|
|
93
|
+
return { value, cursorHint: value.length, state };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (state.selectedIndex < history.length - 1) {
|
|
97
|
+
const selectedIndex = state.selectedIndex + 1;
|
|
98
|
+
const nextValue = history[selectedIndex] ?? value;
|
|
99
|
+
return {
|
|
100
|
+
value: nextValue,
|
|
101
|
+
cursorHint: nextValue.length,
|
|
102
|
+
state: { ...state, selectedIndex },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
value: state.draft,
|
|
108
|
+
cursorHint: state.draft.length,
|
|
109
|
+
state: createPromptHistoryNavigationState(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function appendPromptHistory(history: readonly string[], value: string): string[] {
|
|
114
|
+
const prompt = value.trim();
|
|
115
|
+
if (prompt.length === 0) return [...history];
|
|
116
|
+
if (history[history.length - 1] === prompt) return [...history];
|
|
117
|
+
return [...history, prompt];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function extractPromptHistory(entries: readonly IHistoryEntry[]): string[] {
|
|
121
|
+
let prompts: string[] = [];
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
if (entry.category !== 'chat' || entry.type !== 'user') continue;
|
|
124
|
+
const data = entry.data as Record<string, TUniversalValue> | undefined;
|
|
125
|
+
if (typeof data?.content !== 'string') continue;
|
|
126
|
+
prompts = appendPromptHistory(prompts, data.content);
|
|
127
|
+
}
|
|
128
|
+
return prompts;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function moveAutocompleteSelection(
|
|
132
|
+
selectedIndex: number,
|
|
133
|
+
commandCount: number,
|
|
134
|
+
direction: 'previous' | 'next',
|
|
135
|
+
): number {
|
|
136
|
+
if (commandCount === 0) return 0;
|
|
137
|
+
if (direction === 'previous') {
|
|
138
|
+
return selectedIndex > 0 ? selectedIndex - 1 : commandCount - 1;
|
|
139
|
+
}
|
|
140
|
+
return selectedIndex < commandCount - 1 ? selectedIndex + 1 : 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resolveTabCompletion(value: string, command: ICommand): TCommandSelectionResult {
|
|
144
|
+
const parsed = parseSlashInput(value);
|
|
145
|
+
if (parsed.parentCommand) {
|
|
146
|
+
return { type: 'insert', value: `/${parsed.parentCommand} ${command.name} ` };
|
|
147
|
+
}
|
|
148
|
+
if (command.subcommands && command.subcommands.length > 0) {
|
|
149
|
+
return { type: 'insert', value: `/${command.name} `, selectedIndex: 0 };
|
|
150
|
+
}
|
|
151
|
+
return { type: 'insert', value: `/${command.name} ` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function resolveEnterCommandSelection(
|
|
155
|
+
value: string,
|
|
156
|
+
command: ICommand,
|
|
157
|
+
): TCommandSelectionResult {
|
|
158
|
+
const parsed = parseSlashInput(value);
|
|
159
|
+
if (parsed.parentCommand) {
|
|
160
|
+
return { type: 'submit', value: `/${parsed.parentCommand} ${command.name}` };
|
|
161
|
+
}
|
|
162
|
+
if (command.subcommands && command.subcommands.length > 0) {
|
|
163
|
+
return { type: 'insert', value: `/${command.name} `, selectedIndex: 0 };
|
|
164
|
+
}
|
|
165
|
+
return { type: 'submit', value: `/${command.name}` };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function createPasteLabelChange(
|
|
169
|
+
value: string,
|
|
170
|
+
cursorPosition: number,
|
|
171
|
+
pasteId: number,
|
|
172
|
+
text: string,
|
|
173
|
+
): IPasteLabelChange {
|
|
174
|
+
const lineCount = text.split('\n').length;
|
|
175
|
+
const label = `[Pasted text #${pasteId} +${lineCount} lines]`;
|
|
176
|
+
return {
|
|
177
|
+
value: value.slice(0, cursorPosition) + label + value.slice(cursorPosition),
|
|
178
|
+
cursorHint: cursorPosition + label.length,
|
|
179
|
+
label,
|
|
180
|
+
lineCount,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function shouldSubmitInput(text: string): boolean {
|
|
185
|
+
return text.trim().length > 0;
|
|
186
|
+
}
|