zerg-ztc 0.1.3 → 0.1.5
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/App.d.ts.map +1 -1
- package/dist/App.js +183 -19
- package/dist/App.js.map +1 -1
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +3 -1
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/commands/config.d.ts.map +1 -1
- package/dist/agent/commands/config.js +68 -2
- package/dist/agent/commands/config.js.map +1 -1
- package/dist/agent/commands/index.d.ts.map +1 -1
- package/dist/agent/commands/index.js +4 -1
- package/dist/agent/commands/index.js.map +1 -1
- package/dist/agent/commands/input_mode.d.ts +3 -0
- package/dist/agent/commands/input_mode.d.ts.map +1 -0
- package/dist/agent/commands/input_mode.js +21 -0
- package/dist/agent/commands/input_mode.js.map +1 -0
- package/dist/agent/commands/keybindings.d.ts +3 -0
- package/dist/agent/commands/keybindings.d.ts.map +1 -0
- package/dist/agent/commands/keybindings.js +38 -0
- package/dist/agent/commands/keybindings.js.map +1 -0
- package/dist/agent/commands/types.d.ts +2 -0
- package/dist/agent/commands/types.d.ts.map +1 -1
- package/dist/agent/commands/update.d.ts +3 -0
- package/dist/agent/commands/update.d.ts.map +1 -0
- package/dist/agent/commands/update.js +33 -0
- package/dist/agent/commands/update.js.map +1 -0
- package/dist/cli.js +68 -16
- package/dist/cli.js.map +1 -1
- package/dist/components/ActivityLine.d.ts +11 -0
- package/dist/components/ActivityLine.d.ts.map +1 -0
- package/dist/components/ActivityLine.js +9 -0
- package/dist/components/ActivityLine.js.map +1 -0
- package/dist/components/FullScreen.d.ts +1 -0
- package/dist/components/FullScreen.d.ts.map +1 -1
- package/dist/components/FullScreen.js +30 -30
- package/dist/components/FullScreen.js.map +1 -1
- package/dist/components/InputArea.d.ts.map +1 -1
- package/dist/components/InputArea.js +476 -19
- package/dist/components/InputArea.js.map +1 -1
- package/dist/components/MessageList.d.ts +2 -1
- package/dist/components/MessageList.d.ts.map +1 -1
- package/dist/components/MessageList.js +41 -2
- package/dist/components/MessageList.js.map +1 -1
- package/dist/components/SingleMessage.d.ts +9 -0
- package/dist/components/SingleMessage.d.ts.map +1 -0
- package/dist/components/SingleMessage.js +27 -0
- package/dist/components/SingleMessage.js.map +1 -0
- package/dist/components/StatusBar.d.ts +2 -0
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +3 -1
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/config/types.d.ts +1 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/core/input_segments.d.ts +1 -0
- package/dist/ui/core/input_segments.d.ts.map +1 -1
- package/dist/ui/core/input_segments.js +46 -14
- package/dist/ui/core/input_segments.js.map +1 -1
- package/dist/ui/core/types.d.ts +1 -0
- package/dist/ui/core/types.d.ts.map +1 -1
- package/dist/ui/ink/render.d.ts +3 -1
- package/dist/ui/ink/render.d.ts.map +1 -1
- package/dist/ui/ink/render.js +7 -5
- package/dist/ui/ink/render.js.map +1 -1
- package/dist/ui/views/activity_line.d.ts +11 -0
- package/dist/ui/views/activity_line.d.ts.map +1 -0
- package/dist/ui/views/activity_line.js +20 -0
- package/dist/ui/views/activity_line.js.map +1 -0
- package/dist/ui/views/app.d.ts +5 -1
- package/dist/ui/views/app.d.ts.map +1 -1
- package/dist/ui/views/app.js +18 -14
- package/dist/ui/views/app.js.map +1 -1
- package/dist/ui/views/header.d.ts.map +1 -1
- package/dist/ui/views/header.js +7 -5
- package/dist/ui/views/header.js.map +1 -1
- package/dist/ui/views/input_area.d.ts.map +1 -1
- package/dist/ui/views/input_area.js +25 -12
- package/dist/ui/views/input_area.js.map +1 -1
- package/dist/ui/views/message_list.d.ts +3 -2
- package/dist/ui/views/message_list.d.ts.map +1 -1
- package/dist/ui/views/message_list.js +33 -19
- package/dist/ui/views/message_list.js.map +1 -1
- package/dist/ui/views/status_bar.d.ts +3 -1
- package/dist/ui/views/status_bar.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +8 -2
- package/dist/ui/views/status_bar.js.map +1 -1
- package/dist/utils/spinner_frames.d.ts +2 -0
- package/dist/utils/spinner_frames.d.ts.map +1 -0
- package/dist/utils/spinner_frames.js +2 -0
- package/dist/utils/spinner_frames.js.map +1 -0
- package/dist/utils/spinner_verbs.d.ts +4 -0
- package/dist/utils/spinner_verbs.d.ts.map +1 -0
- package/dist/utils/spinner_verbs.js +22 -0
- package/dist/utils/spinner_verbs.js.map +1 -0
- package/dist/utils/tool_trace.d.ts.map +1 -1
- package/dist/utils/tool_trace.js +12 -2
- package/dist/utils/tool_trace.js.map +1 -1
- package/dist/utils/update.d.ts +9 -0
- package/dist/utils/update.d.ts.map +1 -0
- package/dist/utils/update.js +37 -0
- package/dist/utils/update.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +16 -0
- package/dist/utils/version.js.map +1 -0
- package/package.json +1 -1
- package/src/App.tsx +226 -32
- package/src/agent/agent.ts +3 -1
- package/src/agent/commands/config.ts +76 -2
- package/src/agent/commands/index.ts +6 -0
- package/src/agent/commands/input_mode.ts +22 -0
- package/src/agent/commands/keybindings.ts +40 -0
- package/src/agent/commands/types.ts +2 -0
- package/src/agent/commands/update.ts +32 -0
- package/src/cli.tsx +77 -15
- package/src/components/ActivityLine.tsx +23 -0
- package/src/components/FullScreen.tsx +41 -35
- package/src/components/InputArea.tsx +489 -19
- package/src/components/MessageList.tsx +52 -6
- package/src/components/SingleMessage.tsx +59 -0
- package/src/components/StatusBar.tsx +6 -0
- package/src/components/index.tsx +3 -1
- package/src/config/types.ts +1 -0
- package/src/config.ts +8 -0
- package/src/types.ts +1 -0
- package/src/ui/core/input_segments.ts +49 -14
- package/src/ui/core/types.ts +1 -0
- package/src/ui/ink/render.tsx +16 -5
- package/src/ui/views/activity_line.ts +33 -0
- package/src/ui/views/app.ts +25 -13
- package/src/ui/views/header.ts +7 -5
- package/src/ui/views/input_area.ts +28 -17
- package/src/ui/views/message_list.ts +36 -20
- package/src/ui/views/status_bar.ts +11 -1
- package/src/utils/spinner_frames.ts +1 -0
- package/src/utils/spinner_verbs.ts +23 -0
- package/src/utils/tool_trace.ts +12 -2
- package/src/utils/update.ts +44 -0
- package/src/utils/version.ts +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { useReducer, useCallback } from 'react';
|
|
3
|
-
import { useInput, useStdout } from 'ink';
|
|
3
|
+
import { useInput, useStdout, useApp } from 'ink';
|
|
4
4
|
import { InkNode } from '../ui/ink/index.js';
|
|
5
5
|
import { buildInputAreaView, wrapInputSegments } from '../ui/views/input_area.js';
|
|
6
6
|
import { debugLog } from '../debug/logger.js';
|
|
@@ -61,6 +61,13 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
61
61
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
62
62
|
const stateRef = React.useRef(state);
|
|
63
63
|
const [badgePreview, setBadgePreview] = React.useState(null);
|
|
64
|
+
const killRingRef = React.useRef([]);
|
|
65
|
+
const killIndexRef = React.useRef(-1);
|
|
66
|
+
// Bracketed paste mode support - buffer paste content between \x1b[200~ and \x1b[201~
|
|
67
|
+
const pasteBufferRef = React.useRef('');
|
|
68
|
+
const isPastingRef = React.useRef(false);
|
|
69
|
+
// Ref for handleClipboardImagePaste to avoid circular dependency
|
|
70
|
+
const handleClipboardImagePasteRef = React.useRef(null);
|
|
64
71
|
React.useEffect(() => {
|
|
65
72
|
stateRef.current = state;
|
|
66
73
|
onStateChange?.(state);
|
|
@@ -96,6 +103,129 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
}, [state.cursor.index, state.segments]);
|
|
106
|
+
const pushKill = useCallback((text) => {
|
|
107
|
+
if (!text)
|
|
108
|
+
return;
|
|
109
|
+
const ring = killRingRef.current;
|
|
110
|
+
ring.unshift(text);
|
|
111
|
+
if (ring.length > 20)
|
|
112
|
+
ring.pop();
|
|
113
|
+
killIndexRef.current = 0;
|
|
114
|
+
}, []);
|
|
115
|
+
const yank = useCallback((current) => {
|
|
116
|
+
const ring = killRingRef.current;
|
|
117
|
+
if (ring.length === 0)
|
|
118
|
+
return current;
|
|
119
|
+
const next = insertText(current, ring[0]);
|
|
120
|
+
return { ...next, historyIdx: -1 };
|
|
121
|
+
}, []);
|
|
122
|
+
const yankPop = useCallback((current) => {
|
|
123
|
+
const ring = killRingRef.current;
|
|
124
|
+
if (ring.length === 0)
|
|
125
|
+
return current;
|
|
126
|
+
const idx = killIndexRef.current === -1 ? 0 : (killIndexRef.current + 1) % ring.length;
|
|
127
|
+
killIndexRef.current = idx;
|
|
128
|
+
const next = insertText(current, ring[idx]);
|
|
129
|
+
return { ...next, historyIdx: -1 };
|
|
130
|
+
}, []);
|
|
131
|
+
const killToStart = useCallback((current) => {
|
|
132
|
+
const segments = [...current.segments];
|
|
133
|
+
const cursor = current.cursor;
|
|
134
|
+
const seg = segments[cursor.index];
|
|
135
|
+
if (!seg || seg.type !== 'text')
|
|
136
|
+
return { next: current, killed: '' };
|
|
137
|
+
const killed = seg.text.slice(0, cursor.offset);
|
|
138
|
+
seg.text = seg.text.slice(cursor.offset);
|
|
139
|
+
return {
|
|
140
|
+
next: { ...current, segments, cursor: { index: cursor.index, offset: 0 }, historyIdx: -1 },
|
|
141
|
+
killed
|
|
142
|
+
};
|
|
143
|
+
}, []);
|
|
144
|
+
const killToEnd = useCallback((current) => {
|
|
145
|
+
const segments = [...current.segments];
|
|
146
|
+
const cursor = current.cursor;
|
|
147
|
+
const seg = segments[cursor.index];
|
|
148
|
+
if (!seg || seg.type !== 'text')
|
|
149
|
+
return { next: current, killed: '' };
|
|
150
|
+
const killed = seg.text.slice(cursor.offset);
|
|
151
|
+
seg.text = seg.text.slice(0, cursor.offset);
|
|
152
|
+
return {
|
|
153
|
+
next: { ...current, segments, cursor: { index: cursor.index, offset: seg.text.length }, historyIdx: -1 },
|
|
154
|
+
killed
|
|
155
|
+
};
|
|
156
|
+
}, []);
|
|
157
|
+
const killWordBackward = useCallback((current) => {
|
|
158
|
+
const segments = [...current.segments];
|
|
159
|
+
const cursor = current.cursor;
|
|
160
|
+
const seg = segments[cursor.index];
|
|
161
|
+
if (!seg || seg.type !== 'text')
|
|
162
|
+
return { next: current, killed: '' };
|
|
163
|
+
const before = seg.text.slice(0, cursor.offset);
|
|
164
|
+
const after = seg.text.slice(cursor.offset);
|
|
165
|
+
const trimmed = before.replace(/\s+$/, '');
|
|
166
|
+
const match = trimmed.match(/(\S+)\s*$/);
|
|
167
|
+
const killStart = match ? trimmed.length - match[1].length : before.length;
|
|
168
|
+
const killed = before.slice(killStart);
|
|
169
|
+
seg.text = before.slice(0, killStart) + after;
|
|
170
|
+
return {
|
|
171
|
+
next: { ...current, segments, cursor: { index: cursor.index, offset: killStart }, historyIdx: -1 },
|
|
172
|
+
killed
|
|
173
|
+
};
|
|
174
|
+
}, []);
|
|
175
|
+
const killWordForward = useCallback((current) => {
|
|
176
|
+
const segments = [...current.segments];
|
|
177
|
+
const cursor = current.cursor;
|
|
178
|
+
const seg = segments[cursor.index];
|
|
179
|
+
if (!seg || seg.type !== 'text')
|
|
180
|
+
return { next: current, killed: '' };
|
|
181
|
+
const before = seg.text.slice(0, cursor.offset);
|
|
182
|
+
const after = seg.text.slice(cursor.offset);
|
|
183
|
+
const match = after.match(/^(\s*\S+)/);
|
|
184
|
+
const killed = match ? match[1] : '';
|
|
185
|
+
seg.text = before + after.slice(killed.length);
|
|
186
|
+
return {
|
|
187
|
+
next: { ...current, segments, cursor, historyIdx: -1 },
|
|
188
|
+
killed
|
|
189
|
+
};
|
|
190
|
+
}, []);
|
|
191
|
+
const transposeChars = useCallback((current) => {
|
|
192
|
+
const segments = [...current.segments];
|
|
193
|
+
const cursor = current.cursor;
|
|
194
|
+
const seg = segments[cursor.index];
|
|
195
|
+
if (!seg || seg.type !== 'text')
|
|
196
|
+
return current;
|
|
197
|
+
const text = seg.text;
|
|
198
|
+
if (text.length < 2)
|
|
199
|
+
return current;
|
|
200
|
+
const idx = cursor.offset === 0 ? 0 : cursor.offset === text.length ? text.length - 2 : cursor.offset - 1;
|
|
201
|
+
const chars = text.split('');
|
|
202
|
+
const a = chars[idx];
|
|
203
|
+
chars[idx] = chars[idx + 1];
|
|
204
|
+
chars[idx + 1] = a;
|
|
205
|
+
seg.text = chars.join('');
|
|
206
|
+
return { ...current, segments, cursor: { index: cursor.index, offset: Math.min(text.length, cursor.offset + 1) }, historyIdx: -1 };
|
|
207
|
+
}, []);
|
|
208
|
+
const transposeWords = useCallback((current) => {
|
|
209
|
+
const segments = [...current.segments];
|
|
210
|
+
const cursor = current.cursor;
|
|
211
|
+
const seg = segments[cursor.index];
|
|
212
|
+
if (!seg || seg.type !== 'text')
|
|
213
|
+
return current;
|
|
214
|
+
const text = seg.text;
|
|
215
|
+
const left = text.slice(0, cursor.offset);
|
|
216
|
+
const right = text.slice(cursor.offset);
|
|
217
|
+
const leftMatch = left.match(/(\S+)\s*$/);
|
|
218
|
+
const rightMatch = right.match(/^\s*(\S+)/);
|
|
219
|
+
if (!leftMatch || !rightMatch)
|
|
220
|
+
return current;
|
|
221
|
+
const leftWord = leftMatch[1];
|
|
222
|
+
const rightWord = rightMatch[1];
|
|
223
|
+
const leftStart = left.length - leftMatch[0].length;
|
|
224
|
+
const rightEnd = rightMatch[0].length;
|
|
225
|
+
const replacedLeft = left.slice(0, leftStart) + rightWord;
|
|
226
|
+
seg.text = replacedLeft + right.slice(rightEnd);
|
|
227
|
+
return { ...current, segments, cursor: { index: cursor.index, offset: replacedLeft.length }, historyIdx: -1 };
|
|
228
|
+
}, []);
|
|
99
229
|
const inputRenderCount = React.useRef(0);
|
|
100
230
|
React.useEffect(() => {
|
|
101
231
|
inputRenderCount.current += 1;
|
|
@@ -110,8 +240,16 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
110
240
|
const isCommand = state.segments.length === 1 && state.segments[0].type === 'text' && plainText.startsWith('/');
|
|
111
241
|
if (isCommand) {
|
|
112
242
|
const parts = plainText.slice(1).split(/\s+/);
|
|
113
|
-
const cmd = parts[0];
|
|
243
|
+
const cmd = parts[0].toLowerCase();
|
|
114
244
|
const args = parts.slice(1);
|
|
245
|
+
// Handle /pi (paste image) command directly - intercept before passing to onCommand
|
|
246
|
+
if (cmd === 'pi' || cmd === 'paste-image') {
|
|
247
|
+
dispatch({ type: 'submit', historyEnabled, historyEntry: serialized });
|
|
248
|
+
if (handleClipboardImagePasteRef.current) {
|
|
249
|
+
void handleClipboardImagePasteRef.current('state');
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
115
253
|
onCommand?.(cmd, args);
|
|
116
254
|
}
|
|
117
255
|
else {
|
|
@@ -123,7 +261,8 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
123
261
|
dispatch({ type: 'history_nav', direction, historyEnabled });
|
|
124
262
|
}, [historyEnabled]);
|
|
125
263
|
const { stdout } = useStdout();
|
|
126
|
-
const
|
|
264
|
+
const { exit } = useApp();
|
|
265
|
+
const overlayEnabled = process.env.ZTC_INPUT_OVERLAY === '1' && process.env.ZTC_WEB_MIRROR !== '1';
|
|
127
266
|
const overlayStateRef = React.useRef(initialState);
|
|
128
267
|
const overlayBusyRef = React.useRef(false);
|
|
129
268
|
const pasteBusyRef = React.useRef(false);
|
|
@@ -234,13 +373,137 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
234
373
|
pasteBusyRef.current = false;
|
|
235
374
|
}
|
|
236
375
|
}, [insertTextIntoOverlay, insertTextIntoState, onToast, renderOverlay]);
|
|
376
|
+
// Update ref so handleSubmit can access it
|
|
377
|
+
handleClipboardImagePasteRef.current = handleClipboardImagePaste;
|
|
237
378
|
const handleInput = useCallback((input, key) => {
|
|
379
|
+
// Detect Kitty keyboard protocol CSI u sequences
|
|
380
|
+
// Format: ESC [ <keycode> ; <modifiers> u
|
|
381
|
+
// Modifiers: 2=shift, 3=alt, 5=ctrl, 9=super/cmd
|
|
382
|
+
// Check for sequences with or without ESC (Ink may strip it)
|
|
383
|
+
// Ctrl+V or Cmd+V for image paste: [118;5u or [118;9u
|
|
384
|
+
const kittyPasteV = /\x1b?\[118;[59]u/.test(input);
|
|
385
|
+
if (kittyPasteV) {
|
|
386
|
+
void handleClipboardImagePaste('state');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Consume any other Kitty sequences to prevent them from being displayed
|
|
390
|
+
// Match pattern: ESC? [ number ; number u
|
|
391
|
+
if (/\x1b?\[\d+;\d+u/.test(input)) {
|
|
392
|
+
// This is a Kitty keyboard sequence - don't display it as text
|
|
393
|
+
// Extract what key it is and handle accordingly
|
|
394
|
+
const match = input.match(/\x1b?\[(\d+);(\d+)u/);
|
|
395
|
+
if (match) {
|
|
396
|
+
const keycode = parseInt(match[1], 10);
|
|
397
|
+
const modifier = parseInt(match[2], 10);
|
|
398
|
+
// Ctrl+C (99;5) or Cmd+C (99;9) - exit the app
|
|
399
|
+
if (keycode === 99 && (modifier === 5 || modifier === 9)) {
|
|
400
|
+
exit();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
// Ctrl+L (108;5) - could add clear screen here if needed
|
|
404
|
+
}
|
|
405
|
+
return; // Consume other Kitty sequences
|
|
406
|
+
}
|
|
238
407
|
if (disabled)
|
|
239
408
|
return;
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
409
|
+
// Handle bracketed paste mode markers
|
|
410
|
+
// Note: ESC might be stripped or sent separately by Ink, so check both with and without ESC
|
|
411
|
+
const PASTE_START_FULL = '\x1b[200~';
|
|
412
|
+
const PASTE_START_SHORT = '[200~';
|
|
413
|
+
const PASTE_END_FULL = '\x1b[201~';
|
|
414
|
+
const PASTE_END_SHORT = '[201~';
|
|
415
|
+
const hasPasteStart = input.includes(PASTE_START_FULL) || input.includes(PASTE_START_SHORT);
|
|
416
|
+
const hasPasteEnd = input.includes(PASTE_END_FULL) || input.includes(PASTE_END_SHORT);
|
|
417
|
+
// Check for paste start marker
|
|
418
|
+
if (hasPasteStart) {
|
|
419
|
+
isPastingRef.current = true;
|
|
420
|
+
// Remove the paste start marker (try both variants)
|
|
421
|
+
let content = input.replace(PASTE_START_FULL, '').replace(PASTE_START_SHORT, '');
|
|
422
|
+
// Check if paste end is also in this chunk
|
|
423
|
+
const hasEndInChunk = content.includes(PASTE_END_FULL) || content.includes(PASTE_END_SHORT);
|
|
424
|
+
if (hasEndInChunk) {
|
|
425
|
+
let pasteContent = content.replace(PASTE_END_FULL, '').replace(PASTE_END_SHORT, '');
|
|
426
|
+
// Normalize line endings: \r\n -> \n, \r -> \n
|
|
427
|
+
pasteContent = pasteContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
428
|
+
isPastingRef.current = false;
|
|
429
|
+
pasteBufferRef.current = '';
|
|
430
|
+
// Process the complete paste
|
|
431
|
+
if (pasteContent.length > 0) {
|
|
432
|
+
const lineCount = pasteContent.split('\n').length;
|
|
433
|
+
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
434
|
+
const next = insertBadge(state, { type: 'paste', text: pasteContent });
|
|
435
|
+
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
const next = insertText(state, pasteContent);
|
|
439
|
+
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
// Empty paste - user may have tried to paste an image
|
|
444
|
+
// Try to extract image from system clipboard
|
|
445
|
+
void handleClipboardImagePaste('state');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
pasteBufferRef.current = content;
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// Check for paste end marker
|
|
454
|
+
if (hasPasteEnd) {
|
|
455
|
+
// Remove the paste end marker (try both variants)
|
|
456
|
+
const contentBeforeEnd = input.replace(PASTE_END_FULL, '').replace(PASTE_END_SHORT, '');
|
|
457
|
+
pasteBufferRef.current += contentBeforeEnd;
|
|
458
|
+
// Normalize line endings: \r\n -> \n, \r -> \n
|
|
459
|
+
const pasteContent = pasteBufferRef.current.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
460
|
+
isPastingRef.current = false;
|
|
461
|
+
pasteBufferRef.current = '';
|
|
462
|
+
const lineCount = pasteContent.split('\n').length;
|
|
463
|
+
// Process the complete paste
|
|
464
|
+
if (pasteContent.length > 0) {
|
|
465
|
+
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
466
|
+
const next = insertBadge(state, { type: 'paste', text: pasteContent });
|
|
467
|
+
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
const next = insertText(state, pasteContent);
|
|
471
|
+
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
// Empty paste - user may have tried to paste an image
|
|
476
|
+
// Try to extract image from system clipboard
|
|
477
|
+
void handleClipboardImagePaste('state');
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
243
480
|
}
|
|
481
|
+
// If we're in the middle of a paste, buffer the content
|
|
482
|
+
if (isPastingRef.current) {
|
|
483
|
+
pasteBufferRef.current += input;
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
// Detect backspace via explicit key flag or known control codes
|
|
487
|
+
const backspaceFallback = input === '\b' || input === '\x7f' || input === '\x08';
|
|
488
|
+
const isBackspace = key.backspace || key.delete || backspaceFallback;
|
|
489
|
+
// Ignore completely empty input events that aren't backspace
|
|
490
|
+
// (some terminals send spurious empty events)
|
|
491
|
+
const safeKey = key;
|
|
492
|
+
if (input === '' &&
|
|
493
|
+
!isBackspace &&
|
|
494
|
+
!safeKey.leftArrow &&
|
|
495
|
+
!safeKey.rightArrow &&
|
|
496
|
+
!safeKey.upArrow &&
|
|
497
|
+
!safeKey.downArrow &&
|
|
498
|
+
!safeKey.return &&
|
|
499
|
+
!safeKey.tab &&
|
|
500
|
+
!safeKey.escape &&
|
|
501
|
+
!safeKey.ctrl &&
|
|
502
|
+
!safeKey.meta) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// Try to detect Cmd+V / Ctrl+V for image paste
|
|
506
|
+
// Note: Most terminals intercept Cmd+V, so this may not trigger. Use /pi command instead.
|
|
244
507
|
if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
|
|
245
508
|
void handleClipboardImagePaste('state');
|
|
246
509
|
return;
|
|
@@ -288,20 +551,86 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
288
551
|
dispatch({ type: 'apply', state: { cursor: { index: state.segments.length, offset: 0 } } });
|
|
289
552
|
return;
|
|
290
553
|
}
|
|
291
|
-
if (key.
|
|
292
|
-
|
|
554
|
+
if (key.ctrl && input === 'b') {
|
|
555
|
+
dispatch({ type: 'apply', state: moveLeft(state) });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (key.ctrl && input === 'f') {
|
|
559
|
+
dispatch({ type: 'apply', state: moveRight(state) });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (key.ctrl && input === 'p') {
|
|
563
|
+
navigateHistory('up');
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (key.ctrl && input === 'n') {
|
|
567
|
+
navigateHistory('down');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (isBackspace) {
|
|
571
|
+
// On macOS, backspace key reports as key.delete=true, so always use backspace() for backward deletion
|
|
572
|
+
const next = backspace(state);
|
|
293
573
|
dispatch({ type: 'apply', state: next });
|
|
294
574
|
return;
|
|
295
575
|
}
|
|
296
576
|
if (key.ctrl && input === 'u') {
|
|
297
|
-
|
|
577
|
+
const { next, killed } = killToStart(state);
|
|
578
|
+
pushKill(killed);
|
|
579
|
+
dispatch({ type: 'apply', state: next });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (key.ctrl && input === 'k') {
|
|
583
|
+
const { next, killed } = killToEnd(state);
|
|
584
|
+
pushKill(killed);
|
|
585
|
+
dispatch({ type: 'apply', state: next });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (key.ctrl && input === 'y') {
|
|
589
|
+
dispatch({ type: 'apply', state: yank(state) });
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (key.meta && input === 'y') {
|
|
593
|
+
dispatch({ type: 'apply', state: yankPop(state) });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (key.ctrl && input === 't') {
|
|
597
|
+
dispatch({ type: 'apply', state: transposeChars(state) });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (key.meta && input === 't') {
|
|
601
|
+
dispatch({ type: 'apply', state: transposeWords(state) });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (key.ctrl && input === 'd') {
|
|
605
|
+
dispatch({ type: 'apply', state: deleteForward(state) });
|
|
298
606
|
return;
|
|
299
607
|
}
|
|
300
608
|
if (key.ctrl && input === 'w') {
|
|
301
|
-
const next =
|
|
609
|
+
const { next, killed } = killWordBackward(state);
|
|
610
|
+
pushKill(killed);
|
|
611
|
+
dispatch({ type: 'apply', state: next });
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (key.meta && (input === '\b' || input === '\x7f')) {
|
|
615
|
+
const { next, killed } = killWordBackward(state);
|
|
616
|
+
pushKill(killed);
|
|
617
|
+
dispatch({ type: 'apply', state: next });
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (key.meta && input === 'd') {
|
|
621
|
+
const { next, killed } = killWordForward(state);
|
|
622
|
+
pushKill(killed);
|
|
302
623
|
dispatch({ type: 'apply', state: next });
|
|
303
624
|
return;
|
|
304
625
|
}
|
|
626
|
+
if (key.meta && input === 'b') {
|
|
627
|
+
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (key.meta && input === 'f') {
|
|
631
|
+
dispatch({ type: 'apply', state: moveWordRight(state) });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
305
634
|
if (!key.ctrl && !key.meta && input) {
|
|
306
635
|
if (input.includes('\n')) {
|
|
307
636
|
const lineCount = input.split('\n').length;
|
|
@@ -314,13 +643,27 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
314
643
|
const next = insertText(state, input);
|
|
315
644
|
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
316
645
|
}
|
|
317
|
-
}, [disabled, handleSubmit, navigateHistory, state]);
|
|
646
|
+
}, [disabled, exit, handleClipboardImagePaste, handleSubmit, navigateHistory, state]);
|
|
318
647
|
const handleOverlayInput = useCallback((input, key) => {
|
|
319
648
|
if (disabled)
|
|
320
649
|
return;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
650
|
+
// Detect backspace via explicit key flag or known control codes
|
|
651
|
+
const backspaceFallback = input === '\b' || input === '\x7f' || input === '\x08';
|
|
652
|
+
const isBackspace = key.backspace || key.delete || backspaceFallback;
|
|
653
|
+
// Ignore completely empty input events that aren't backspace
|
|
654
|
+
const safeKey = key;
|
|
655
|
+
if (input === '' &&
|
|
656
|
+
!isBackspace &&
|
|
657
|
+
!safeKey.leftArrow &&
|
|
658
|
+
!safeKey.rightArrow &&
|
|
659
|
+
!safeKey.upArrow &&
|
|
660
|
+
!safeKey.downArrow &&
|
|
661
|
+
!safeKey.return &&
|
|
662
|
+
!safeKey.tab &&
|
|
663
|
+
!safeKey.escape &&
|
|
664
|
+
!safeKey.ctrl &&
|
|
665
|
+
!safeKey.meta) {
|
|
666
|
+
return;
|
|
324
667
|
}
|
|
325
668
|
const current = overlayStateRef.current;
|
|
326
669
|
if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
|
|
@@ -334,8 +677,15 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
334
677
|
const isCommand = current.segments.length === 1 && current.segments[0].type === 'text' && plainText.startsWith('/');
|
|
335
678
|
if (isCommand) {
|
|
336
679
|
const parts = plainText.slice(1).split(/\s+/);
|
|
337
|
-
const cmd = parts[0];
|
|
680
|
+
const cmd = parts[0].toLowerCase();
|
|
338
681
|
const args = parts.slice(1);
|
|
682
|
+
// Handle /pi (paste image) command directly
|
|
683
|
+
if (cmd === 'pi' || cmd === 'paste-image') {
|
|
684
|
+
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
685
|
+
renderOverlay(overlayStateRef.current);
|
|
686
|
+
void handleClipboardImagePaste('overlay');
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
339
689
|
onCommand?.(cmd, args);
|
|
340
690
|
}
|
|
341
691
|
else {
|
|
@@ -412,18 +762,125 @@ export const InputArea = ({ onSubmit, onCommand, commands = [], onStateChange, o
|
|
|
412
762
|
renderOverlay(overlayStateRef.current);
|
|
413
763
|
return;
|
|
414
764
|
}
|
|
415
|
-
if (key.
|
|
416
|
-
overlayStateRef.current =
|
|
765
|
+
if (key.ctrl && input === 'b') {
|
|
766
|
+
overlayStateRef.current = moveLeft(current);
|
|
767
|
+
renderOverlay(overlayStateRef.current);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (key.ctrl && input === 'f') {
|
|
771
|
+
overlayStateRef.current = moveRight(current);
|
|
772
|
+
renderOverlay(overlayStateRef.current);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (key.ctrl && input === 'p') {
|
|
776
|
+
const direction = 'up';
|
|
777
|
+
if (historyEnabled && current.history.length > 0) {
|
|
778
|
+
let newIdx = current.historyIdx === -1 ? current.history.length - 1 : Math.max(0, current.historyIdx - 1);
|
|
779
|
+
const historyValue = current.history[newIdx] || '';
|
|
780
|
+
overlayStateRef.current = {
|
|
781
|
+
...current,
|
|
782
|
+
historyIdx: newIdx,
|
|
783
|
+
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
784
|
+
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
785
|
+
};
|
|
786
|
+
renderOverlay(overlayStateRef.current);
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (key.ctrl && input === 'n') {
|
|
791
|
+
if (historyEnabled && current.history.length > 0) {
|
|
792
|
+
let newIdx = current.historyIdx === -1 ? -1 : current.historyIdx + 1;
|
|
793
|
+
if (newIdx >= current.history.length)
|
|
794
|
+
newIdx = -1;
|
|
795
|
+
if (newIdx === -1) {
|
|
796
|
+
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
const historyValue = current.history[newIdx];
|
|
800
|
+
overlayStateRef.current = {
|
|
801
|
+
...current,
|
|
802
|
+
historyIdx: newIdx,
|
|
803
|
+
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
804
|
+
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
renderOverlay(overlayStateRef.current);
|
|
808
|
+
}
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
if (isBackspace) {
|
|
812
|
+
// On macOS, backspace key reports as key.delete=true, so always use backspace() for backward deletion
|
|
813
|
+
overlayStateRef.current = backspace(current);
|
|
417
814
|
renderOverlay(overlayStateRef.current);
|
|
418
815
|
return;
|
|
419
816
|
}
|
|
420
817
|
if (key.ctrl && input === 'u') {
|
|
421
|
-
|
|
818
|
+
const { next, killed } = killToStart(current);
|
|
819
|
+
pushKill(killed);
|
|
820
|
+
overlayStateRef.current = { ...next, history: current.history, historyIdx: -1 };
|
|
821
|
+
renderOverlay(overlayStateRef.current);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (key.ctrl && input === 'k') {
|
|
825
|
+
const { next, killed } = killToEnd(current);
|
|
826
|
+
pushKill(killed);
|
|
827
|
+
overlayStateRef.current = next;
|
|
828
|
+
renderOverlay(overlayStateRef.current);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (key.ctrl && input === 'y') {
|
|
832
|
+
overlayStateRef.current = yank(current);
|
|
833
|
+
renderOverlay(overlayStateRef.current);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (key.meta && input === 'y') {
|
|
837
|
+
overlayStateRef.current = yankPop(current);
|
|
838
|
+
renderOverlay(overlayStateRef.current);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (key.ctrl && input === 't') {
|
|
842
|
+
overlayStateRef.current = transposeChars(current);
|
|
843
|
+
renderOverlay(overlayStateRef.current);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (key.meta && input === 't') {
|
|
847
|
+
overlayStateRef.current = transposeWords(current);
|
|
848
|
+
renderOverlay(overlayStateRef.current);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (key.ctrl && input === 'd') {
|
|
852
|
+
overlayStateRef.current = deleteForward(current);
|
|
422
853
|
renderOverlay(overlayStateRef.current);
|
|
423
854
|
return;
|
|
424
855
|
}
|
|
425
856
|
if (key.ctrl && input === 'w') {
|
|
426
|
-
|
|
857
|
+
const { next, killed } = killWordBackward(current);
|
|
858
|
+
pushKill(killed);
|
|
859
|
+
overlayStateRef.current = next;
|
|
860
|
+
renderOverlay(overlayStateRef.current);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (key.meta && (input === '\b' || input === '\x7f')) {
|
|
864
|
+
const { next, killed } = killWordBackward(current);
|
|
865
|
+
pushKill(killed);
|
|
866
|
+
overlayStateRef.current = next;
|
|
867
|
+
renderOverlay(overlayStateRef.current);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
if (key.meta && input === 'd') {
|
|
871
|
+
const { next, killed } = killWordForward(current);
|
|
872
|
+
pushKill(killed);
|
|
873
|
+
overlayStateRef.current = next;
|
|
874
|
+
renderOverlay(overlayStateRef.current);
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (key.meta && input === 'b') {
|
|
878
|
+
overlayStateRef.current = moveWordLeft(current);
|
|
879
|
+
renderOverlay(overlayStateRef.current);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (key.meta && input === 'f') {
|
|
883
|
+
overlayStateRef.current = moveWordRight(current);
|
|
427
884
|
renderOverlay(overlayStateRef.current);
|
|
428
885
|
return;
|
|
429
886
|
}
|