@mcp-use/cli 1.0.0 → 1.0.1
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/InputPrompt.d.ts +13 -0
- package/dist/InputPrompt.js +188 -0
- package/dist/MultilineInput.d.ts +13 -0
- package/dist/MultilineInput.js +154 -0
- package/dist/MultilineTextInput.d.ts +11 -0
- package/dist/MultilineTextInput.js +97 -0
- package/dist/PasteAwareInput.d.ts +13 -0
- package/dist/PasteAwareInput.js +183 -0
- package/dist/SimpleMultilineInput.d.ts +11 -0
- package/dist/SimpleMultilineInput.js +125 -0
- package/dist/app.d.ts +1 -5
- package/dist/app.js +291 -186
- package/dist/cli.js +2 -5
- package/dist/commands.d.ts +15 -30
- package/dist/commands.js +308 -568
- package/dist/components/AsciiLogo.d.ts +2 -0
- package/dist/components/AsciiLogo.js +7 -0
- package/dist/components/Footer.d.ts +5 -0
- package/dist/components/Footer.js +19 -0
- package/dist/components/InputPrompt.d.ts +13 -0
- package/dist/components/InputPrompt.js +188 -0
- package/dist/components/Messages.d.ts +21 -0
- package/dist/components/Messages.js +80 -0
- package/dist/components/ServerStatus.d.ts +7 -0
- package/dist/components/ServerStatus.js +36 -0
- package/dist/components/Spinner.d.ts +16 -0
- package/dist/components/Spinner.js +63 -0
- package/dist/components/ToolStatus.d.ts +8 -0
- package/dist/components/ToolStatus.js +33 -0
- package/dist/components/textInput.d.ts +1 -0
- package/dist/components/textInput.js +1 -0
- package/dist/logger.d.ts +10 -0
- package/dist/logger.js +48 -0
- package/dist/mcp-service.d.ts +5 -4
- package/dist/mcp-service.js +98 -207
- package/dist/services/agent-service.d.ts +56 -0
- package/dist/services/agent-service.js +203 -0
- package/dist/services/cli-service.d.ts +132 -0
- package/dist/services/cli-service.js +591 -0
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +4 -0
- package/dist/services/llm-service.d.ts +174 -0
- package/dist/services/llm-service.js +567 -0
- package/dist/services/mcp-config-service.d.ts +69 -0
- package/dist/services/mcp-config-service.js +426 -0
- package/dist/services/mcp-service.d.ts +1 -0
- package/dist/services/mcp-service.js +1 -0
- package/dist/services/utility-service.d.ts +47 -0
- package/dist/services/utility-service.js +208 -0
- package/dist/storage.js +4 -4
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/package.json +22 -8
- package/readme.md +68 -39
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface InputPromptProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
onHistoryUp?: () => void;
|
|
7
|
+
onHistoryDown?: () => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
mask?: string;
|
|
10
|
+
focus?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const InputPrompt: React.FC<InputPromptProps>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export const InputPrompt = ({ value, onChange, onSubmit, onHistoryUp, onHistoryDown, placeholder = 'Type your message...', mask, focus = true, }) => {
|
|
4
|
+
const [cursorPosition, setCursorPosition] = useState(value.length);
|
|
5
|
+
const [isMultiline, setIsMultiline] = useState(false);
|
|
6
|
+
const pasteBufferRef = useRef('');
|
|
7
|
+
const pasteTimeoutRef = useRef(null);
|
|
8
|
+
const lastInputTimeRef = useRef(Date.now());
|
|
9
|
+
// Track if we should allow multiline
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setIsMultiline(value.includes('\n'));
|
|
12
|
+
}, [value]);
|
|
13
|
+
// Update cursor position when value changes externally
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setCursorPosition(value.length);
|
|
16
|
+
}, [value]);
|
|
17
|
+
const handleSubmit = useCallback(() => {
|
|
18
|
+
const trimmedValue = value.trim();
|
|
19
|
+
if (trimmedValue) {
|
|
20
|
+
onSubmit(trimmedValue);
|
|
21
|
+
onChange('');
|
|
22
|
+
setCursorPosition(0);
|
|
23
|
+
setIsMultiline(false);
|
|
24
|
+
}
|
|
25
|
+
}, [value, onSubmit, onChange]);
|
|
26
|
+
const processPasteBuffer = useCallback(() => {
|
|
27
|
+
if (pasteBufferRef.current) {
|
|
28
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
29
|
+
const afterCursor = value.slice(cursorPosition);
|
|
30
|
+
// Process the paste buffer to handle carriage returns
|
|
31
|
+
let processedPaste = pasteBufferRef.current
|
|
32
|
+
.replace(/\r\n/g, '\n')
|
|
33
|
+
.replace(/\r/g, '\n');
|
|
34
|
+
const newValue = beforeCursor + processedPaste + afterCursor;
|
|
35
|
+
onChange(newValue);
|
|
36
|
+
setCursorPosition(cursorPosition + processedPaste.length);
|
|
37
|
+
if (processedPaste.includes('\n')) {
|
|
38
|
+
setIsMultiline(true);
|
|
39
|
+
}
|
|
40
|
+
pasteBufferRef.current = '';
|
|
41
|
+
}
|
|
42
|
+
}, [value, cursorPosition, onChange]);
|
|
43
|
+
useInput((input, key) => {
|
|
44
|
+
if (!focus)
|
|
45
|
+
return;
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const timeSinceLastInput = now - lastInputTimeRef.current;
|
|
48
|
+
lastInputTimeRef.current = now;
|
|
49
|
+
// Detect paste by checking if we're getting rapid inputs or multi-character input
|
|
50
|
+
const isProbablyPaste = input &&
|
|
51
|
+
(timeSinceLastInput < 50 || input.length > 1) &&
|
|
52
|
+
!key.ctrl &&
|
|
53
|
+
!key.meta;
|
|
54
|
+
if (isProbablyPaste) {
|
|
55
|
+
// Accumulate paste buffer
|
|
56
|
+
pasteBufferRef.current += input;
|
|
57
|
+
// Clear existing timeout
|
|
58
|
+
if (pasteTimeoutRef.current) {
|
|
59
|
+
clearTimeout(pasteTimeoutRef.current);
|
|
60
|
+
}
|
|
61
|
+
// Set new timeout to process paste
|
|
62
|
+
pasteTimeoutRef.current = setTimeout(() => {
|
|
63
|
+
processPasteBuffer();
|
|
64
|
+
pasteTimeoutRef.current = null;
|
|
65
|
+
}, 100);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Process any pending paste buffer first
|
|
69
|
+
if (pasteBufferRef.current) {
|
|
70
|
+
processPasteBuffer();
|
|
71
|
+
}
|
|
72
|
+
// Submit on Enter (without modifiers)
|
|
73
|
+
if (key.return && !key.ctrl && !key.meta && !key.shift) {
|
|
74
|
+
handleSubmit();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// New line on Ctrl+Enter, Meta+Enter, or Shift+Enter
|
|
78
|
+
if (key.return && (key.ctrl || key.meta || key.shift)) {
|
|
79
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
80
|
+
const afterCursor = value.slice(cursorPosition);
|
|
81
|
+
onChange(beforeCursor + '\n' + afterCursor);
|
|
82
|
+
setCursorPosition(cursorPosition + 1);
|
|
83
|
+
setIsMultiline(true);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// History navigation with up/down arrows (only in single-line mode)
|
|
87
|
+
if (!isMultiline) {
|
|
88
|
+
if (key.upArrow && onHistoryUp) {
|
|
89
|
+
onHistoryUp();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (key.downArrow && onHistoryDown) {
|
|
93
|
+
onHistoryDown();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Backspace
|
|
98
|
+
if (key.backspace || key.delete) {
|
|
99
|
+
if (cursorPosition > 0) {
|
|
100
|
+
const beforeCursor = value.slice(0, cursorPosition - 1);
|
|
101
|
+
const afterCursor = value.slice(cursorPosition);
|
|
102
|
+
onChange(beforeCursor + afterCursor);
|
|
103
|
+
setCursorPosition(cursorPosition - 1);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Navigation keys
|
|
108
|
+
if (key.leftArrow) {
|
|
109
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (key.rightArrow) {
|
|
113
|
+
setCursorPosition(Math.min(value.length, cursorPosition + 1));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Home/End keys
|
|
117
|
+
if (key.ctrl && input === 'a') {
|
|
118
|
+
setCursorPosition(0);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.ctrl && input === 'e') {
|
|
122
|
+
setCursorPosition(value.length);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Regular character input (not paste)
|
|
126
|
+
if (input && !key.ctrl && !key.meta) {
|
|
127
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
128
|
+
const afterCursor = value.slice(cursorPosition);
|
|
129
|
+
// Process input to handle carriage returns
|
|
130
|
+
const processedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
131
|
+
const newValue = beforeCursor + processedInput + afterCursor;
|
|
132
|
+
onChange(newValue);
|
|
133
|
+
setCursorPosition(cursorPosition + processedInput.length);
|
|
134
|
+
if (processedInput.includes('\n')) {
|
|
135
|
+
setIsMultiline(true);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// Render the input
|
|
140
|
+
const renderContent = () => {
|
|
141
|
+
if (!value && placeholder && !focus) {
|
|
142
|
+
return React.createElement(Text, { dimColor: true }, placeholder);
|
|
143
|
+
}
|
|
144
|
+
let displayValue = mask ? value.replace(/./g, mask) : value;
|
|
145
|
+
if (!isMultiline) {
|
|
146
|
+
// Single line rendering with cursor
|
|
147
|
+
const beforeCursor = displayValue.slice(0, cursorPosition);
|
|
148
|
+
const atCursor = displayValue[cursorPosition] || ' ';
|
|
149
|
+
const afterCursor = displayValue.slice(cursorPosition + 1);
|
|
150
|
+
return (React.createElement(Text, null,
|
|
151
|
+
beforeCursor,
|
|
152
|
+
focus && React.createElement(Text, { inverse: true }, atCursor),
|
|
153
|
+
afterCursor));
|
|
154
|
+
}
|
|
155
|
+
// Multiline rendering
|
|
156
|
+
const lines = displayValue.split('\n');
|
|
157
|
+
let pos = 0;
|
|
158
|
+
let cursorRow = 0;
|
|
159
|
+
let cursorCol = 0;
|
|
160
|
+
for (let row = 0; row < lines.length; row++) {
|
|
161
|
+
const lineLength = lines[row]?.length || 0;
|
|
162
|
+
if (pos + lineLength >= cursorPosition) {
|
|
163
|
+
cursorRow = row;
|
|
164
|
+
cursorCol = cursorPosition - pos;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
pos += lineLength + 1; // +1 for newline
|
|
168
|
+
}
|
|
169
|
+
return lines.map((line, row) => {
|
|
170
|
+
if (row === cursorRow && focus) {
|
|
171
|
+
const before = line.slice(0, cursorCol);
|
|
172
|
+
const at = line[cursorCol] || ' ';
|
|
173
|
+
const after = line.slice(cursorCol + 1);
|
|
174
|
+
return (React.createElement(Box, { key: row },
|
|
175
|
+
React.createElement(Text, null,
|
|
176
|
+
before,
|
|
177
|
+
React.createElement(Text, { inverse: true }, at),
|
|
178
|
+
after)));
|
|
179
|
+
}
|
|
180
|
+
return (React.createElement(Box, { key: row },
|
|
181
|
+
React.createElement(Text, null, line || ' ')));
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
185
|
+
renderContent(),
|
|
186
|
+
isMultiline && focus && (React.createElement(Box, { marginTop: 1 },
|
|
187
|
+
React.createElement(Text, { dimColor: true, italic: true }, "Enter to submit \u2022 Ctrl/Shift+Enter for new line")))));
|
|
188
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface MultilineInputProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
onHistoryUp?: () => void;
|
|
7
|
+
onHistoryDown?: () => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
mask?: string;
|
|
10
|
+
focus?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const MultilineInput: React.FC<MultilineInputProps>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export const MultilineInput = ({ value, onChange, onSubmit, onHistoryUp, onHistoryDown, placeholder = 'Type your message...', mask, focus = true }) => {
|
|
4
|
+
const [cursorPosition, setCursorPosition] = useState(value.length);
|
|
5
|
+
const [isMultiline, setIsMultiline] = useState(false);
|
|
6
|
+
// Track if we should allow multiline
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setIsMultiline(value.includes('\n'));
|
|
9
|
+
}, [value]);
|
|
10
|
+
// Update cursor position when value changes externally (e.g., from history)
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setCursorPosition(value.length);
|
|
13
|
+
}, [value]);
|
|
14
|
+
const handleSubmit = useCallback(() => {
|
|
15
|
+
const trimmedValue = value.trim();
|
|
16
|
+
if (trimmedValue) {
|
|
17
|
+
onSubmit(trimmedValue);
|
|
18
|
+
onChange('');
|
|
19
|
+
setCursorPosition(0);
|
|
20
|
+
setIsMultiline(false);
|
|
21
|
+
}
|
|
22
|
+
}, [value, onSubmit, onChange]);
|
|
23
|
+
useInput((input, key) => {
|
|
24
|
+
if (!focus)
|
|
25
|
+
return;
|
|
26
|
+
// Submit on Enter (without modifiers)
|
|
27
|
+
if (key.return && !key.ctrl && !key.meta && !key.shift) {
|
|
28
|
+
handleSubmit();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// New line on Ctrl+Enter, Meta+Enter, or Shift+Enter
|
|
32
|
+
if (key.return && (key.ctrl || key.meta || key.shift)) {
|
|
33
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
34
|
+
const afterCursor = value.slice(cursorPosition);
|
|
35
|
+
onChange(beforeCursor + '\n' + afterCursor);
|
|
36
|
+
setCursorPosition(cursorPosition + 1);
|
|
37
|
+
setIsMultiline(true);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// History navigation with up/down arrows (only in single-line mode)
|
|
41
|
+
if (!isMultiline) {
|
|
42
|
+
if (key.upArrow && onHistoryUp) {
|
|
43
|
+
onHistoryUp();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.downArrow && onHistoryDown) {
|
|
47
|
+
onHistoryDown();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Backspace
|
|
52
|
+
if (key.backspace || key.delete) {
|
|
53
|
+
if (cursorPosition > 0) {
|
|
54
|
+
const beforeCursor = value.slice(0, cursorPosition - 1);
|
|
55
|
+
const afterCursor = value.slice(cursorPosition);
|
|
56
|
+
onChange(beforeCursor + afterCursor);
|
|
57
|
+
setCursorPosition(cursorPosition - 1);
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Navigation keys
|
|
62
|
+
if (key.leftArrow) {
|
|
63
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (key.rightArrow) {
|
|
67
|
+
setCursorPosition(Math.min(value.length, cursorPosition + 1));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// Home/End keys
|
|
71
|
+
if (key.ctrl && input === 'a') {
|
|
72
|
+
setCursorPosition(0);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (key.ctrl && input === 'e') {
|
|
76
|
+
setCursorPosition(value.length);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// Regular character input (including paste)
|
|
80
|
+
if (input && !key.ctrl && !key.meta) {
|
|
81
|
+
// Handle potential paste operation (multi-character input)
|
|
82
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
83
|
+
const afterCursor = value.slice(cursorPosition);
|
|
84
|
+
// Process the input character by character to handle special characters
|
|
85
|
+
let processedInput = '';
|
|
86
|
+
for (let i = 0; i < input.length; i++) {
|
|
87
|
+
const char = input[i];
|
|
88
|
+
// Convert carriage returns to newlines
|
|
89
|
+
if (char === '\r') {
|
|
90
|
+
processedInput += '\n';
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
processedInput += char;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const newValue = beforeCursor + processedInput + afterCursor;
|
|
97
|
+
onChange(newValue);
|
|
98
|
+
setCursorPosition(cursorPosition + processedInput.length);
|
|
99
|
+
// Check if the input contains newlines (e.g., from paste)
|
|
100
|
+
if (processedInput.includes('\n')) {
|
|
101
|
+
setIsMultiline(true);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// Render the input
|
|
106
|
+
const renderContent = () => {
|
|
107
|
+
if (!value && placeholder && !focus) {
|
|
108
|
+
return React.createElement(Text, { dimColor: true }, placeholder);
|
|
109
|
+
}
|
|
110
|
+
let displayValue = mask ? value.replace(/./g, mask) : value;
|
|
111
|
+
if (!isMultiline) {
|
|
112
|
+
// Single line rendering with cursor
|
|
113
|
+
const beforeCursor = displayValue.slice(0, cursorPosition);
|
|
114
|
+
const atCursor = displayValue[cursorPosition] || ' ';
|
|
115
|
+
const afterCursor = displayValue.slice(cursorPosition + 1);
|
|
116
|
+
return (React.createElement(Text, null,
|
|
117
|
+
beforeCursor,
|
|
118
|
+
focus && React.createElement(Text, { inverse: true }, atCursor),
|
|
119
|
+
afterCursor));
|
|
120
|
+
}
|
|
121
|
+
// Multiline rendering
|
|
122
|
+
const lines = displayValue.split('\n');
|
|
123
|
+
let pos = 0;
|
|
124
|
+
let cursorRow = 0;
|
|
125
|
+
let cursorCol = 0;
|
|
126
|
+
for (let row = 0; row < lines.length; row++) {
|
|
127
|
+
const lineLength = lines[row]?.length || 0;
|
|
128
|
+
if (pos + lineLength >= cursorPosition) {
|
|
129
|
+
cursorRow = row;
|
|
130
|
+
cursorCol = cursorPosition - pos;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
pos += lineLength + 1; // +1 for newline
|
|
134
|
+
}
|
|
135
|
+
return lines.map((line, row) => {
|
|
136
|
+
if (row === cursorRow && focus) {
|
|
137
|
+
const before = line.slice(0, cursorCol);
|
|
138
|
+
const at = line[cursorCol] || ' ';
|
|
139
|
+
const after = line.slice(cursorCol + 1);
|
|
140
|
+
return (React.createElement(Box, { key: row },
|
|
141
|
+
React.createElement(Text, null,
|
|
142
|
+
before,
|
|
143
|
+
React.createElement(Text, { inverse: true }, at),
|
|
144
|
+
after)));
|
|
145
|
+
}
|
|
146
|
+
return React.createElement(Box, { key: row },
|
|
147
|
+
React.createElement(Text, null, line || ' '));
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
151
|
+
renderContent(),
|
|
152
|
+
isMultiline && focus && (React.createElement(Box, { marginTop: 1 },
|
|
153
|
+
React.createElement(Text, { dimColor: true, italic: true }, "Enter to submit \u2022 Ctrl/Shift+Enter for new line")))));
|
|
154
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface MultilineTextInputProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
mask?: string;
|
|
8
|
+
focus?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const MultilineTextInput: React.FC<MultilineTextInputProps>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export const MultilineTextInput = ({ value, onChange, onSubmit, placeholder = '', mask, focus = true }) => {
|
|
4
|
+
const [cursorPosition, setCursorPosition] = useState(value.length);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
setCursorPosition(value.length);
|
|
7
|
+
}, [value]);
|
|
8
|
+
useInput((input, key) => {
|
|
9
|
+
if (!focus)
|
|
10
|
+
return;
|
|
11
|
+
if (key.return && !key.shift) {
|
|
12
|
+
// Submit on Enter (without Shift)
|
|
13
|
+
onSubmit(value);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (key.return && key.shift) {
|
|
17
|
+
// New line on Shift+Enter
|
|
18
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
19
|
+
const afterCursor = value.slice(cursorPosition);
|
|
20
|
+
const newValue = beforeCursor + '\n' + afterCursor;
|
|
21
|
+
onChange(newValue);
|
|
22
|
+
setCursorPosition(cursorPosition + 1);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (key.backspace || key.delete) {
|
|
26
|
+
if (cursorPosition > 0) {
|
|
27
|
+
const beforeCursor = value.slice(0, cursorPosition - 1);
|
|
28
|
+
const afterCursor = value.slice(cursorPosition);
|
|
29
|
+
onChange(beforeCursor + afterCursor);
|
|
30
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (key.leftArrow) {
|
|
35
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.rightArrow) {
|
|
39
|
+
setCursorPosition(Math.min(value.length, cursorPosition + 1));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Handle paste (Ctrl+V or Cmd+V)
|
|
43
|
+
if ((key.ctrl || key.meta) && input === 'v') {
|
|
44
|
+
// Unfortunately, Ink doesn't provide clipboard content directly
|
|
45
|
+
// This is a limitation of terminal-based input
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Regular character input (including multi-character pastes)
|
|
49
|
+
if (input && !key.ctrl && !key.meta) {
|
|
50
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
51
|
+
const afterCursor = value.slice(cursorPosition);
|
|
52
|
+
const newValue = beforeCursor + input + afterCursor;
|
|
53
|
+
onChange(newValue);
|
|
54
|
+
setCursorPosition(cursorPosition + input.length);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// Render the input with cursor
|
|
58
|
+
const renderContent = () => {
|
|
59
|
+
let displayValue = value;
|
|
60
|
+
if (mask) {
|
|
61
|
+
displayValue = value.replace(/./g, mask);
|
|
62
|
+
}
|
|
63
|
+
if (!displayValue && placeholder && !focus) {
|
|
64
|
+
return React.createElement(Text, { dimColor: true }, placeholder);
|
|
65
|
+
}
|
|
66
|
+
// Add cursor to the display value
|
|
67
|
+
const beforeCursor = displayValue.slice(0, cursorPosition);
|
|
68
|
+
const atCursor = displayValue[cursorPosition] || ' ';
|
|
69
|
+
const afterCursor = displayValue.slice(cursorPosition + 1);
|
|
70
|
+
// Split by newlines and render each line
|
|
71
|
+
const beforeLines = beforeCursor.split('\n');
|
|
72
|
+
const afterLines = (atCursor + afterCursor).split('\n');
|
|
73
|
+
return (React.createElement(React.Fragment, null,
|
|
74
|
+
beforeLines.map((line, index) => {
|
|
75
|
+
const isLastBeforeLine = index === beforeLines.length - 1;
|
|
76
|
+
if (isLastBeforeLine && afterLines.length > 0) {
|
|
77
|
+
// This line contains the cursor
|
|
78
|
+
const firstAfterLine = afterLines[0] || '';
|
|
79
|
+
const cursorChar = firstAfterLine[0] || ' ';
|
|
80
|
+
const restOfLine = firstAfterLine.slice(1);
|
|
81
|
+
return (React.createElement(Box, { key: `line-${index}` },
|
|
82
|
+
React.createElement(Text, null,
|
|
83
|
+
line,
|
|
84
|
+
React.createElement(Text, { inverse: true }, cursorChar),
|
|
85
|
+
restOfLine)));
|
|
86
|
+
}
|
|
87
|
+
return React.createElement(Box, { key: `line-${index}` },
|
|
88
|
+
React.createElement(Text, null, line));
|
|
89
|
+
}),
|
|
90
|
+
afterLines.slice(1).map((line, index) => (React.createElement(Box, { key: `after-line-${index}` },
|
|
91
|
+
React.createElement(Text, null, line))))));
|
|
92
|
+
};
|
|
93
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
94
|
+
renderContent(),
|
|
95
|
+
focus && value.includes('\n') && (React.createElement(Box, { marginTop: 1 },
|
|
96
|
+
React.createElement(Text, { dimColor: true, italic: true }, "(Shift+Enter for new line, Enter to submit)")))));
|
|
97
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface PasteAwareInputProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
onHistoryUp?: () => void;
|
|
7
|
+
onHistoryDown?: () => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
mask?: string;
|
|
10
|
+
focus?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const PasteAwareInput: React.FC<PasteAwareInputProps>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
export const PasteAwareInput = ({ value, onChange, onSubmit, onHistoryUp, onHistoryDown, placeholder = 'Type your message...', mask, focus = true }) => {
|
|
4
|
+
const [cursorPosition, setCursorPosition] = useState(value.length);
|
|
5
|
+
const [isMultiline, setIsMultiline] = useState(false);
|
|
6
|
+
const pasteBufferRef = useRef('');
|
|
7
|
+
const pasteTimeoutRef = useRef(null);
|
|
8
|
+
const lastInputTimeRef = useRef(Date.now());
|
|
9
|
+
// Track if we should allow multiline
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
setIsMultiline(value.includes('\n'));
|
|
12
|
+
}, [value]);
|
|
13
|
+
// Update cursor position when value changes externally
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setCursorPosition(value.length);
|
|
16
|
+
}, [value]);
|
|
17
|
+
const handleSubmit = useCallback(() => {
|
|
18
|
+
const trimmedValue = value.trim();
|
|
19
|
+
if (trimmedValue) {
|
|
20
|
+
onSubmit(trimmedValue);
|
|
21
|
+
onChange('');
|
|
22
|
+
setCursorPosition(0);
|
|
23
|
+
setIsMultiline(false);
|
|
24
|
+
}
|
|
25
|
+
}, [value, onSubmit, onChange]);
|
|
26
|
+
const processPasteBuffer = useCallback(() => {
|
|
27
|
+
if (pasteBufferRef.current) {
|
|
28
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
29
|
+
const afterCursor = value.slice(cursorPosition);
|
|
30
|
+
// Process the paste buffer to handle carriage returns
|
|
31
|
+
let processedPaste = pasteBufferRef.current.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
32
|
+
const newValue = beforeCursor + processedPaste + afterCursor;
|
|
33
|
+
onChange(newValue);
|
|
34
|
+
setCursorPosition(cursorPosition + processedPaste.length);
|
|
35
|
+
if (processedPaste.includes('\n')) {
|
|
36
|
+
setIsMultiline(true);
|
|
37
|
+
}
|
|
38
|
+
pasteBufferRef.current = '';
|
|
39
|
+
}
|
|
40
|
+
}, [value, cursorPosition, onChange]);
|
|
41
|
+
useInput((input, key) => {
|
|
42
|
+
if (!focus)
|
|
43
|
+
return;
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const timeSinceLastInput = now - lastInputTimeRef.current;
|
|
46
|
+
lastInputTimeRef.current = now;
|
|
47
|
+
// Detect paste by checking if we're getting rapid inputs or multi-character input
|
|
48
|
+
const isProbablyPaste = input && (timeSinceLastInput < 50 || input.length > 1) && !key.ctrl && !key.meta;
|
|
49
|
+
if (isProbablyPaste) {
|
|
50
|
+
// Accumulate paste buffer
|
|
51
|
+
pasteBufferRef.current += input;
|
|
52
|
+
// Clear existing timeout
|
|
53
|
+
if (pasteTimeoutRef.current) {
|
|
54
|
+
clearTimeout(pasteTimeoutRef.current);
|
|
55
|
+
}
|
|
56
|
+
// Set new timeout to process paste
|
|
57
|
+
pasteTimeoutRef.current = setTimeout(() => {
|
|
58
|
+
processPasteBuffer();
|
|
59
|
+
pasteTimeoutRef.current = null;
|
|
60
|
+
}, 100);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Process any pending paste buffer first
|
|
64
|
+
if (pasteBufferRef.current) {
|
|
65
|
+
processPasteBuffer();
|
|
66
|
+
}
|
|
67
|
+
// Submit on Enter (without modifiers)
|
|
68
|
+
if (key.return && !key.ctrl && !key.meta && !key.shift) {
|
|
69
|
+
handleSubmit();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// New line on Ctrl+Enter, Meta+Enter, or Shift+Enter
|
|
73
|
+
if (key.return && (key.ctrl || key.meta || key.shift)) {
|
|
74
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
75
|
+
const afterCursor = value.slice(cursorPosition);
|
|
76
|
+
onChange(beforeCursor + '\n' + afterCursor);
|
|
77
|
+
setCursorPosition(cursorPosition + 1);
|
|
78
|
+
setIsMultiline(true);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// History navigation with up/down arrows (only in single-line mode)
|
|
82
|
+
if (!isMultiline) {
|
|
83
|
+
if (key.upArrow && onHistoryUp) {
|
|
84
|
+
onHistoryUp();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (key.downArrow && onHistoryDown) {
|
|
88
|
+
onHistoryDown();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Backspace
|
|
93
|
+
if (key.backspace || key.delete) {
|
|
94
|
+
if (cursorPosition > 0) {
|
|
95
|
+
const beforeCursor = value.slice(0, cursorPosition - 1);
|
|
96
|
+
const afterCursor = value.slice(cursorPosition);
|
|
97
|
+
onChange(beforeCursor + afterCursor);
|
|
98
|
+
setCursorPosition(cursorPosition - 1);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Navigation keys
|
|
103
|
+
if (key.leftArrow) {
|
|
104
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.rightArrow) {
|
|
108
|
+
setCursorPosition(Math.min(value.length, cursorPosition + 1));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Home/End keys
|
|
112
|
+
if (key.ctrl && input === 'a') {
|
|
113
|
+
setCursorPosition(0);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (key.ctrl && input === 'e') {
|
|
117
|
+
setCursorPosition(value.length);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// Regular character input (not paste)
|
|
121
|
+
if (input && !key.ctrl && !key.meta) {
|
|
122
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
123
|
+
const afterCursor = value.slice(cursorPosition);
|
|
124
|
+
// Process input to handle carriage returns
|
|
125
|
+
const processedInput = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
126
|
+
const newValue = beforeCursor + processedInput + afterCursor;
|
|
127
|
+
onChange(newValue);
|
|
128
|
+
setCursorPosition(cursorPosition + processedInput.length);
|
|
129
|
+
if (processedInput.includes('\n')) {
|
|
130
|
+
setIsMultiline(true);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Render the input
|
|
135
|
+
const renderContent = () => {
|
|
136
|
+
if (!value && placeholder && !focus) {
|
|
137
|
+
return React.createElement(Text, { dimColor: true }, placeholder);
|
|
138
|
+
}
|
|
139
|
+
let displayValue = mask ? value.replace(/./g, mask) : value;
|
|
140
|
+
if (!isMultiline) {
|
|
141
|
+
// Single line rendering with cursor
|
|
142
|
+
const beforeCursor = displayValue.slice(0, cursorPosition);
|
|
143
|
+
const atCursor = displayValue[cursorPosition] || ' ';
|
|
144
|
+
const afterCursor = displayValue.slice(cursorPosition + 1);
|
|
145
|
+
return (React.createElement(Text, null,
|
|
146
|
+
beforeCursor,
|
|
147
|
+
focus && React.createElement(Text, { inverse: true }, atCursor),
|
|
148
|
+
afterCursor));
|
|
149
|
+
}
|
|
150
|
+
// Multiline rendering
|
|
151
|
+
const lines = displayValue.split('\n');
|
|
152
|
+
let pos = 0;
|
|
153
|
+
let cursorRow = 0;
|
|
154
|
+
let cursorCol = 0;
|
|
155
|
+
for (let row = 0; row < lines.length; row++) {
|
|
156
|
+
const lineLength = lines[row]?.length || 0;
|
|
157
|
+
if (pos + lineLength >= cursorPosition) {
|
|
158
|
+
cursorRow = row;
|
|
159
|
+
cursorCol = cursorPosition - pos;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
pos += lineLength + 1; // +1 for newline
|
|
163
|
+
}
|
|
164
|
+
return lines.map((line, row) => {
|
|
165
|
+
if (row === cursorRow && focus) {
|
|
166
|
+
const before = line.slice(0, cursorCol);
|
|
167
|
+
const at = line[cursorCol] || ' ';
|
|
168
|
+
const after = line.slice(cursorCol + 1);
|
|
169
|
+
return (React.createElement(Box, { key: row },
|
|
170
|
+
React.createElement(Text, null,
|
|
171
|
+
before,
|
|
172
|
+
React.createElement(Text, { inverse: true }, at),
|
|
173
|
+
after)));
|
|
174
|
+
}
|
|
175
|
+
return React.createElement(Box, { key: row },
|
|
176
|
+
React.createElement(Text, null, line || ' '));
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
180
|
+
renderContent(),
|
|
181
|
+
isMultiline && focus && (React.createElement(Box, { marginTop: 1 },
|
|
182
|
+
React.createElement(Text, { dimColor: true, italic: true }, "Enter to submit \u2022 Ctrl/Shift+Enter for new line")))));
|
|
183
|
+
};
|