zerg-ztc 0.1.10 → 0.1.12
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/bin/.gitkeep +0 -0
- package/bin/ztc-audio-darwin-arm64 +0 -0
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +63 -2
- package/dist/App.js.map +1 -1
- package/dist/agent/commands/dictation.d.ts +3 -0
- package/dist/agent/commands/dictation.d.ts.map +1 -0
- package/dist/agent/commands/dictation.js +10 -0
- package/dist/agent/commands/dictation.js.map +1 -0
- package/dist/agent/commands/index.d.ts.map +1 -1
- package/dist/agent/commands/index.js +2 -1
- package/dist/agent/commands/index.js.map +1 -1
- package/dist/agent/commands/types.d.ts +7 -0
- package/dist/agent/commands/types.d.ts.map +1 -1
- package/dist/components/InputArea.d.ts +1 -0
- package/dist/components/InputArea.d.ts.map +1 -1
- package/dist/components/InputArea.js +591 -43
- package/dist/components/InputArea.js.map +1 -1
- package/dist/components/SingleMessage.d.ts.map +1 -1
- package/dist/components/SingleMessage.js +157 -7
- package/dist/components/SingleMessage.js.map +1 -1
- package/dist/config/types.d.ts +6 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/ui/views/status_bar.js +2 -2
- package/dist/ui/views/status_bar.js.map +1 -1
- package/dist/utils/dictation.d.ts +46 -0
- package/dist/utils/dictation.d.ts.map +1 -0
- package/dist/utils/dictation.js +409 -0
- package/dist/utils/dictation.js.map +1 -0
- package/dist/utils/dictation_native.d.ts +51 -0
- package/dist/utils/dictation_native.d.ts.map +1 -0
- package/dist/utils/dictation_native.js +236 -0
- package/dist/utils/dictation_native.js.map +1 -0
- package/dist/utils/path_format.d.ts +20 -0
- package/dist/utils/path_format.d.ts.map +1 -0
- package/dist/utils/path_format.js +90 -0
- package/dist/utils/path_format.js.map +1 -0
- package/dist/utils/table.d.ts +38 -0
- package/dist/utils/table.d.ts.map +1 -0
- package/dist/utils/table.js +133 -0
- package/dist/utils/table.js.map +1 -0
- package/dist/utils/tool_trace.d.ts +7 -2
- package/dist/utils/tool_trace.d.ts.map +1 -1
- package/dist/utils/tool_trace.js +156 -51
- package/dist/utils/tool_trace.js.map +1 -1
- package/package.json +5 -1
- package/src/App.tsx +0 -813
- package/src/agent/agent.ts +0 -534
- package/src/agent/backends/anthropic.ts +0 -86
- package/src/agent/backends/gemini.ts +0 -119
- package/src/agent/backends/inception.ts +0 -23
- package/src/agent/backends/index.ts +0 -17
- package/src/agent/backends/openai.ts +0 -23
- package/src/agent/backends/openai_compatible.ts +0 -143
- package/src/agent/backends/types.ts +0 -83
- package/src/agent/commands/clipboard.ts +0 -77
- package/src/agent/commands/config.ts +0 -204
- package/src/agent/commands/debug.ts +0 -23
- package/src/agent/commands/emulation.ts +0 -80
- package/src/agent/commands/execution.ts +0 -9
- package/src/agent/commands/help.ts +0 -20
- package/src/agent/commands/history.ts +0 -13
- package/src/agent/commands/index.ts +0 -46
- package/src/agent/commands/input_mode.ts +0 -22
- package/src/agent/commands/keybindings.ts +0 -40
- package/src/agent/commands/model.ts +0 -11
- package/src/agent/commands/models.ts +0 -116
- package/src/agent/commands/permissions.ts +0 -64
- package/src/agent/commands/retry.ts +0 -9
- package/src/agent/commands/shell.ts +0 -68
- package/src/agent/commands/skills.ts +0 -54
- package/src/agent/commands/status.ts +0 -19
- package/src/agent/commands/types.ts +0 -80
- package/src/agent/commands/update.ts +0 -32
- package/src/agent/factory.ts +0 -60
- package/src/agent/index.ts +0 -20
- package/src/agent/runtime/capabilities.ts +0 -7
- package/src/agent/runtime/memory.ts +0 -23
- package/src/agent/runtime/policy.ts +0 -48
- package/src/agent/runtime/session.ts +0 -18
- package/src/agent/runtime/tracing.ts +0 -23
- package/src/agent/tools/file.ts +0 -178
- package/src/agent/tools/index.ts +0 -52
- package/src/agent/tools/screenshot.ts +0 -821
- package/src/agent/tools/search.ts +0 -138
- package/src/agent/tools/shell.ts +0 -69
- package/src/agent/tools/skills.ts +0 -28
- package/src/agent/tools/types.ts +0 -14
- package/src/agent/tools/zerg.ts +0 -50
- package/src/cli.tsx +0 -163
- package/src/components/ActivityLine.tsx +0 -23
- package/src/components/FullScreen.tsx +0 -79
- package/src/components/Header.tsx +0 -27
- package/src/components/InputArea.tsx +0 -1096
- package/src/components/MessageList.tsx +0 -71
- package/src/components/SingleMessage.tsx +0 -59
- package/src/components/StatusBar.tsx +0 -55
- package/src/components/index.tsx +0 -8
- package/src/config/types.ts +0 -12
- package/src/config.ts +0 -186
- package/src/debug/logger.ts +0 -14
- package/src/emulation/README.md +0 -24
- package/src/emulation/catalog.ts +0 -82
- package/src/emulation/trace_style.ts +0 -8
- package/src/emulation/types.ts +0 -7
- package/src/skills/index.ts +0 -36
- package/src/skills/loader.ts +0 -135
- package/src/skills/registry.ts +0 -6
- package/src/skills/types.ts +0 -10
- package/src/types.ts +0 -84
- package/src/ui/README.md +0 -44
- package/src/ui/core/factory.ts +0 -9
- package/src/ui/core/index.ts +0 -4
- package/src/ui/core/input.ts +0 -38
- package/src/ui/core/input_segments.ts +0 -410
- package/src/ui/core/input_state.ts +0 -17
- package/src/ui/core/layout_yoga.ts +0 -122
- package/src/ui/core/style.ts +0 -38
- package/src/ui/core/types.ts +0 -54
- package/src/ui/ink/index.tsx +0 -1
- package/src/ui/ink/render.tsx +0 -60
- package/src/ui/views/activity_line.ts +0 -33
- package/src/ui/views/app.ts +0 -111
- package/src/ui/views/header.ts +0 -44
- package/src/ui/views/input_area.ts +0 -255
- package/src/ui/views/message_list.ts +0 -443
- package/src/ui/views/status_bar.ts +0 -114
- package/src/ui/vue/index.ts +0 -53
- package/src/ui/web/frame_render.tsx +0 -148
- package/src/ui/web/index.tsx +0 -1
- package/src/ui/web/render.tsx +0 -41
- package/src/utils/clipboard.ts +0 -39
- package/src/utils/clipboard_image.ts +0 -40
- package/src/utils/diff.ts +0 -52
- package/src/utils/image_preview.ts +0 -36
- package/src/utils/models.ts +0 -98
- package/src/utils/path_complete.ts +0 -173
- package/src/utils/shell.ts +0 -72
- package/src/utils/spinner_frames.ts +0 -1
- package/src/utils/spinner_verbs.ts +0 -23
- package/src/utils/tool_summary.ts +0 -56
- package/src/utils/tool_trace.ts +0 -216
- package/src/utils/update.ts +0 -44
- package/src/utils/version.ts +0 -15
- package/src/web/index.html +0 -352
- package/src/web/mirror-favicon.svg +0 -4
- package/src/web/mirror.html +0 -641
- package/src/web/mirror_hook.ts +0 -25
- package/src/web/mirror_server.ts +0 -204
- package/tsconfig.json +0 -22
- package/vite.config.ts +0 -363
|
@@ -1,1096 +0,0 @@
|
|
|
1
|
-
import React, { useReducer, useCallback } from 'react';
|
|
2
|
-
import { useInput, useStdout, useApp } from 'ink';
|
|
3
|
-
import { InkNode } from '../ui/ink/index.js';
|
|
4
|
-
import { buildInputAreaView, wrapInputSegments } from '../ui/views/input_area.js';
|
|
5
|
-
import { InputBus, InputKey } from '../ui/core/input.js';
|
|
6
|
-
import { InputState, InputSegment } from '../ui/core/input_state.js';
|
|
7
|
-
import { debugLog } from '../debug/logger.js';
|
|
8
|
-
import chalk from 'chalk';
|
|
9
|
-
import { saveClipboardImage } from '../utils/clipboard_image.js';
|
|
10
|
-
import { renderImagePreview } from '../utils/image_preview.js';
|
|
11
|
-
import { completePath } from '../utils/path_complete.js';
|
|
12
|
-
import {
|
|
13
|
-
createEmptyState,
|
|
14
|
-
insertText,
|
|
15
|
-
insertSegment,
|
|
16
|
-
insertBadge,
|
|
17
|
-
backspace,
|
|
18
|
-
deleteForward,
|
|
19
|
-
moveLeft,
|
|
20
|
-
moveRight,
|
|
21
|
-
moveWordLeft,
|
|
22
|
-
moveWordRight,
|
|
23
|
-
getPlainText,
|
|
24
|
-
serializeSegments,
|
|
25
|
-
PASTE_BADGE_THRESHOLD
|
|
26
|
-
} from '../ui/core/input_segments.js';
|
|
27
|
-
|
|
28
|
-
interface InputAreaProps {
|
|
29
|
-
onSubmit: (text: string) => void;
|
|
30
|
-
onCommand?: (command: string, args: string[]) => void;
|
|
31
|
-
commands?: Array<{
|
|
32
|
-
name: string;
|
|
33
|
-
description: string;
|
|
34
|
-
usage?: string;
|
|
35
|
-
}>;
|
|
36
|
-
onStateChange?: (state: InputState) => void;
|
|
37
|
-
onToast?: (message: string) => void;
|
|
38
|
-
cols?: number;
|
|
39
|
-
inputBus?: InputBus;
|
|
40
|
-
disabled?: boolean;
|
|
41
|
-
placeholder?: string;
|
|
42
|
-
historyEnabled?: boolean;
|
|
43
|
-
debug?: boolean;
|
|
44
|
-
cwd?: string; // Working directory for path tab completion
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type { InputState } from '../ui/core/input_state.js';
|
|
48
|
-
|
|
49
|
-
type InputAction =
|
|
50
|
-
| { type: 'apply'; state: Partial<InputState> }
|
|
51
|
-
| { type: 'submit'; historyEnabled: boolean; historyEntry?: string }
|
|
52
|
-
| { type: 'history_nav'; direction: 'up' | 'down'; historyEnabled: boolean };
|
|
53
|
-
|
|
54
|
-
const initialState: InputState = createEmptyState();
|
|
55
|
-
|
|
56
|
-
function reducer(state: InputState, action: InputAction): InputState {
|
|
57
|
-
switch (action.type) {
|
|
58
|
-
case 'apply':
|
|
59
|
-
return { ...state, ...action.state };
|
|
60
|
-
case 'submit': {
|
|
61
|
-
const history = action.historyEnabled && action.historyEntry
|
|
62
|
-
? [...state.history.slice(-100), action.historyEntry]
|
|
63
|
-
: state.history;
|
|
64
|
-
return {
|
|
65
|
-
...state,
|
|
66
|
-
segments: [],
|
|
67
|
-
cursor: { index: 0, offset: 0 },
|
|
68
|
-
historyIdx: -1,
|
|
69
|
-
history
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
case 'history_nav': {
|
|
73
|
-
if (!action.historyEnabled || state.history.length === 0) return state;
|
|
74
|
-
|
|
75
|
-
let newIdx: number;
|
|
76
|
-
if (action.direction === 'up') {
|
|
77
|
-
newIdx = state.historyIdx === -1
|
|
78
|
-
? state.history.length - 1
|
|
79
|
-
: Math.max(0, state.historyIdx - 1);
|
|
80
|
-
} else {
|
|
81
|
-
newIdx = state.historyIdx === -1 ? -1 : state.historyIdx + 1;
|
|
82
|
-
if (newIdx >= state.history.length) newIdx = -1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (newIdx === -1) {
|
|
86
|
-
return { ...state, historyIdx: -1, segments: [], cursor: { index: 0, offset: 0 } };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const historyValue = state.history[newIdx];
|
|
90
|
-
const nextSegments: InputSegment[] = historyValue.length > 0
|
|
91
|
-
? [{ type: 'text', text: historyValue }]
|
|
92
|
-
: [];
|
|
93
|
-
return {
|
|
94
|
-
...state,
|
|
95
|
-
historyIdx: newIdx,
|
|
96
|
-
segments: nextSegments,
|
|
97
|
-
cursor: { index: nextSegments.length ? 0 : 0, offset: historyValue.length }
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
default:
|
|
101
|
-
return state;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export const InputArea: React.FC<InputAreaProps> = ({
|
|
106
|
-
onSubmit,
|
|
107
|
-
onCommand,
|
|
108
|
-
commands = [],
|
|
109
|
-
onStateChange,
|
|
110
|
-
onToast,
|
|
111
|
-
cols = process.stdout.columns || 80,
|
|
112
|
-
inputBus,
|
|
113
|
-
disabled = false,
|
|
114
|
-
placeholder = 'Type a message...',
|
|
115
|
-
historyEnabled = true,
|
|
116
|
-
debug = false,
|
|
117
|
-
cwd = process.cwd()
|
|
118
|
-
}) => {
|
|
119
|
-
const [state, dispatch] = useReducer(reducer, initialState);
|
|
120
|
-
const stateRef = React.useRef(state);
|
|
121
|
-
const [badgePreview, setBadgePreview] = React.useState<string[] | null>(null);
|
|
122
|
-
const killRingRef = React.useRef<string[]>([]);
|
|
123
|
-
const killIndexRef = React.useRef<number>(-1);
|
|
124
|
-
|
|
125
|
-
// Bracketed paste mode support - buffer paste content between \x1b[200~ and \x1b[201~
|
|
126
|
-
const pasteBufferRef = React.useRef<string>('');
|
|
127
|
-
const isPastingRef = React.useRef<boolean>(false);
|
|
128
|
-
// Ref for handleClipboardImagePaste to avoid circular dependency
|
|
129
|
-
const handleClipboardImagePasteRef = React.useRef<((target: 'state' | 'overlay') => Promise<void>) | null>(null);
|
|
130
|
-
React.useEffect(() => {
|
|
131
|
-
stateRef.current = state;
|
|
132
|
-
onStateChange?.(state);
|
|
133
|
-
}, [onStateChange, state]);
|
|
134
|
-
|
|
135
|
-
React.useEffect(() => {
|
|
136
|
-
const segment = state.segments[state.cursor.index];
|
|
137
|
-
if (!segment || segment.type === 'text') {
|
|
138
|
-
setBadgePreview(null);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (segment.type === 'paste') {
|
|
142
|
-
const lines = segment.text.split('\n');
|
|
143
|
-
const preview = lines.slice(0, 3);
|
|
144
|
-
if (lines.length > 3) preview.push('…');
|
|
145
|
-
setBadgePreview(preview);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (segment.type === 'file') {
|
|
149
|
-
setBadgePreview([`[file] ${segment.path}`]);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (segment.type === 'image') {
|
|
153
|
-
renderImagePreview(segment.path, 40, 16).then(result => {
|
|
154
|
-
if (result && result.length > 0) {
|
|
155
|
-
setBadgePreview(result);
|
|
156
|
-
} else {
|
|
157
|
-
setBadgePreview([`[image] ${segment.path}`, 'Install chafa for preview.']);
|
|
158
|
-
}
|
|
159
|
-
}).catch(() => {
|
|
160
|
-
setBadgePreview([`[image] ${segment.path}`]);
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}, [state.cursor.index, state.segments]);
|
|
164
|
-
|
|
165
|
-
const pushKill = useCallback((text: string) => {
|
|
166
|
-
if (!text) return;
|
|
167
|
-
const ring = killRingRef.current;
|
|
168
|
-
ring.unshift(text);
|
|
169
|
-
if (ring.length > 20) ring.pop();
|
|
170
|
-
killIndexRef.current = 0;
|
|
171
|
-
}, []);
|
|
172
|
-
|
|
173
|
-
const yank = useCallback((current: InputState) => {
|
|
174
|
-
const ring = killRingRef.current;
|
|
175
|
-
if (ring.length === 0) return current;
|
|
176
|
-
const next = insertText(current, ring[0]);
|
|
177
|
-
return { ...next, historyIdx: -1 };
|
|
178
|
-
}, []);
|
|
179
|
-
|
|
180
|
-
const yankPop = useCallback((current: InputState) => {
|
|
181
|
-
const ring = killRingRef.current;
|
|
182
|
-
if (ring.length === 0) return current;
|
|
183
|
-
const idx = killIndexRef.current === -1 ? 0 : (killIndexRef.current + 1) % ring.length;
|
|
184
|
-
killIndexRef.current = idx;
|
|
185
|
-
const next = insertText(current, ring[idx]);
|
|
186
|
-
return { ...next, historyIdx: -1 };
|
|
187
|
-
}, []);
|
|
188
|
-
|
|
189
|
-
const killToStart = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
190
|
-
const segments = [...current.segments];
|
|
191
|
-
const cursor = current.cursor;
|
|
192
|
-
const seg = segments[cursor.index];
|
|
193
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
194
|
-
const killed = seg.text.slice(0, cursor.offset);
|
|
195
|
-
seg.text = seg.text.slice(cursor.offset);
|
|
196
|
-
return {
|
|
197
|
-
next: { ...current, segments, cursor: { index: cursor.index, offset: 0 }, historyIdx: -1 },
|
|
198
|
-
killed
|
|
199
|
-
};
|
|
200
|
-
}, []);
|
|
201
|
-
|
|
202
|
-
const killToEnd = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
203
|
-
const segments = [...current.segments];
|
|
204
|
-
const cursor = current.cursor;
|
|
205
|
-
const seg = segments[cursor.index];
|
|
206
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
207
|
-
const killed = seg.text.slice(cursor.offset);
|
|
208
|
-
seg.text = seg.text.slice(0, cursor.offset);
|
|
209
|
-
return {
|
|
210
|
-
next: { ...current, segments, cursor: { index: cursor.index, offset: seg.text.length }, historyIdx: -1 },
|
|
211
|
-
killed
|
|
212
|
-
};
|
|
213
|
-
}, []);
|
|
214
|
-
|
|
215
|
-
const killWordBackward = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
216
|
-
const segments = [...current.segments];
|
|
217
|
-
const cursor = current.cursor;
|
|
218
|
-
const seg = segments[cursor.index];
|
|
219
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
220
|
-
const before = seg.text.slice(0, cursor.offset);
|
|
221
|
-
const after = seg.text.slice(cursor.offset);
|
|
222
|
-
const trimmed = before.replace(/\s+$/, '');
|
|
223
|
-
const match = trimmed.match(/(\S+)\s*$/);
|
|
224
|
-
const killStart = match ? trimmed.length - match[1].length : before.length;
|
|
225
|
-
const killed = before.slice(killStart);
|
|
226
|
-
seg.text = before.slice(0, killStart) + after;
|
|
227
|
-
return {
|
|
228
|
-
next: { ...current, segments, cursor: { index: cursor.index, offset: killStart }, historyIdx: -1 },
|
|
229
|
-
killed
|
|
230
|
-
};
|
|
231
|
-
}, []);
|
|
232
|
-
|
|
233
|
-
const killWordForward = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
234
|
-
const segments = [...current.segments];
|
|
235
|
-
const cursor = current.cursor;
|
|
236
|
-
const seg = segments[cursor.index];
|
|
237
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
238
|
-
const before = seg.text.slice(0, cursor.offset);
|
|
239
|
-
const after = seg.text.slice(cursor.offset);
|
|
240
|
-
const match = after.match(/^(\s*\S+)/);
|
|
241
|
-
const killed = match ? match[1] : '';
|
|
242
|
-
seg.text = before + after.slice(killed.length);
|
|
243
|
-
return {
|
|
244
|
-
next: { ...current, segments, cursor, historyIdx: -1 },
|
|
245
|
-
killed
|
|
246
|
-
};
|
|
247
|
-
}, []);
|
|
248
|
-
|
|
249
|
-
const transposeChars = useCallback((current: InputState): InputState => {
|
|
250
|
-
const segments = [...current.segments];
|
|
251
|
-
const cursor = current.cursor;
|
|
252
|
-
const seg = segments[cursor.index];
|
|
253
|
-
if (!seg || seg.type !== 'text') return current;
|
|
254
|
-
const text = seg.text;
|
|
255
|
-
if (text.length < 2) return current;
|
|
256
|
-
const idx = cursor.offset === 0 ? 0 : cursor.offset === text.length ? text.length - 2 : cursor.offset - 1;
|
|
257
|
-
const chars = text.split('');
|
|
258
|
-
const a = chars[idx];
|
|
259
|
-
chars[idx] = chars[idx + 1];
|
|
260
|
-
chars[idx + 1] = a;
|
|
261
|
-
seg.text = chars.join('');
|
|
262
|
-
return { ...current, segments, cursor: { index: cursor.index, offset: Math.min(text.length, cursor.offset + 1) }, historyIdx: -1 };
|
|
263
|
-
}, []);
|
|
264
|
-
|
|
265
|
-
const transposeWords = useCallback((current: InputState): InputState => {
|
|
266
|
-
const segments = [...current.segments];
|
|
267
|
-
const cursor = current.cursor;
|
|
268
|
-
const seg = segments[cursor.index];
|
|
269
|
-
if (!seg || seg.type !== 'text') return current;
|
|
270
|
-
const text = seg.text;
|
|
271
|
-
const left = text.slice(0, cursor.offset);
|
|
272
|
-
const right = text.slice(cursor.offset);
|
|
273
|
-
const leftMatch = left.match(/(\S+)\s*$/);
|
|
274
|
-
const rightMatch = right.match(/^\s*(\S+)/);
|
|
275
|
-
if (!leftMatch || !rightMatch) return current;
|
|
276
|
-
const leftWord = leftMatch[1];
|
|
277
|
-
const rightWord = rightMatch[1];
|
|
278
|
-
const leftStart = left.length - leftMatch[0].length;
|
|
279
|
-
const rightEnd = rightMatch[0].length;
|
|
280
|
-
const replacedLeft = left.slice(0, leftStart) + rightWord;
|
|
281
|
-
seg.text = replacedLeft + right.slice(rightEnd);
|
|
282
|
-
return { ...current, segments, cursor: { index: cursor.index, offset: replacedLeft.length }, historyIdx: -1 };
|
|
283
|
-
}, []);
|
|
284
|
-
|
|
285
|
-
// Tab completion state
|
|
286
|
-
const tabCompletionAlternativesRef = React.useRef<string[]>([]);
|
|
287
|
-
const lastTabPrefixRef = React.useRef<string>('');
|
|
288
|
-
|
|
289
|
-
// Handle tab completion for shell commands
|
|
290
|
-
const handleTabComplete = useCallback(async (current: InputState): Promise<InputState> => {
|
|
291
|
-
const plainText = getPlainText(current.segments);
|
|
292
|
-
|
|
293
|
-
// Only complete if this looks like a shell command with a path
|
|
294
|
-
// Match: !cd path, !ls path, !cat path, etc.
|
|
295
|
-
const shellMatch = plainText.match(/^!(\w+)\s+(.*)$/);
|
|
296
|
-
if (!shellMatch) {
|
|
297
|
-
// Also support just !cd without a path yet
|
|
298
|
-
if (plainText.match(/^!cd\s*$/)) {
|
|
299
|
-
// Show home directory
|
|
300
|
-
const result = await completePath('~/', cwd);
|
|
301
|
-
if (result && result.alternatives.length > 0) {
|
|
302
|
-
tabCompletionAlternativesRef.current = result.alternatives;
|
|
303
|
-
onToast?.(`Completions: ${result.alternatives.slice(0, 5).join(', ')}${result.alternatives.length > 5 ? '...' : ''}`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return current;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const partialPath = shellMatch[2];
|
|
310
|
-
const commandPrefix = `!${shellMatch[1]} `;
|
|
311
|
-
|
|
312
|
-
// Try to complete the path
|
|
313
|
-
const result = await completePath(partialPath, cwd);
|
|
314
|
-
if (!result) {
|
|
315
|
-
onToast?.('No completions');
|
|
316
|
-
return current;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// If we have alternatives and this is a repeat tab, show them
|
|
320
|
-
if (result.alternatives.length > 1) {
|
|
321
|
-
tabCompletionAlternativesRef.current = result.alternatives;
|
|
322
|
-
lastTabPrefixRef.current = partialPath;
|
|
323
|
-
onToast?.(`Completions: ${result.alternatives.slice(0, 6).join(' ')}${result.alternatives.length > 6 ? ' ...' : ''}`);
|
|
324
|
-
} else {
|
|
325
|
-
tabCompletionAlternativesRef.current = [];
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Update the input with the completed path
|
|
329
|
-
const newText = commandPrefix + result.completed;
|
|
330
|
-
const newSegments: InputSegment[] = [{ type: 'text', text: newText }];
|
|
331
|
-
return {
|
|
332
|
-
...current,
|
|
333
|
-
segments: newSegments,
|
|
334
|
-
cursor: { index: 0, offset: newText.length },
|
|
335
|
-
historyIdx: -1
|
|
336
|
-
};
|
|
337
|
-
}, [cwd, onToast]);
|
|
338
|
-
|
|
339
|
-
const inputRenderCount = React.useRef(0);
|
|
340
|
-
React.useEffect(() => {
|
|
341
|
-
inputRenderCount.current += 1;
|
|
342
|
-
const totalLength = state.segments.reduce((acc, segment) => (
|
|
343
|
-
segment.type === 'text' ? acc + segment.text.length : acc + 0
|
|
344
|
-
), 0);
|
|
345
|
-
debugLog(`InputArea render #${inputRenderCount.current} (valueLen=${totalLength})`);
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const handleSubmit = useCallback(() => {
|
|
349
|
-
const serialized = serializeSegments(state.segments).trim();
|
|
350
|
-
if (!serialized) return;
|
|
351
|
-
|
|
352
|
-
const plainText = getPlainText(state.segments).trim();
|
|
353
|
-
const isCommand = state.segments.length === 1 && state.segments[0].type === 'text' && plainText.startsWith('/');
|
|
354
|
-
if (isCommand) {
|
|
355
|
-
const parts = plainText.slice(1).split(/\s+/);
|
|
356
|
-
const cmd = parts[0].toLowerCase();
|
|
357
|
-
const args = parts.slice(1);
|
|
358
|
-
|
|
359
|
-
// Handle /pi (paste image) command directly - intercept before passing to onCommand
|
|
360
|
-
if (cmd === 'pi' || cmd === 'paste-image') {
|
|
361
|
-
dispatch({ type: 'submit', historyEnabled, historyEntry: serialized });
|
|
362
|
-
if (handleClipboardImagePasteRef.current) {
|
|
363
|
-
void handleClipboardImagePasteRef.current('state');
|
|
364
|
-
}
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
onCommand?.(cmd, args);
|
|
369
|
-
} else {
|
|
370
|
-
onSubmit(serialized);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
dispatch({ type: 'submit', historyEnabled, historyEntry: serialized });
|
|
374
|
-
}, [state.segments, onSubmit, onCommand, historyEnabled]);
|
|
375
|
-
|
|
376
|
-
const navigateHistory = useCallback((direction: 'up' | 'down') => {
|
|
377
|
-
dispatch({ type: 'history_nav', direction, historyEnabled });
|
|
378
|
-
}, [historyEnabled]);
|
|
379
|
-
|
|
380
|
-
const { stdout } = useStdout();
|
|
381
|
-
const { exit } = useApp();
|
|
382
|
-
const overlayEnabled = process.env.ZTC_INPUT_OVERLAY === '1' && process.env.ZTC_WEB_MIRROR !== '1';
|
|
383
|
-
const overlayStateRef = React.useRef<InputState>(initialState);
|
|
384
|
-
const overlayBusyRef = React.useRef(false);
|
|
385
|
-
const pasteBusyRef = React.useRef(false);
|
|
386
|
-
|
|
387
|
-
const insertTextIntoState = useCallback((text: string) => {
|
|
388
|
-
const current = stateRef.current;
|
|
389
|
-
const next = insertText(current, text);
|
|
390
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
391
|
-
}, []);
|
|
392
|
-
|
|
393
|
-
const renderOverlay = useCallback((overlayState: InputState) => {
|
|
394
|
-
if (!stdout || !stdout.isTTY) return;
|
|
395
|
-
if (overlayBusyRef.current) return;
|
|
396
|
-
overlayBusyRef.current = true;
|
|
397
|
-
|
|
398
|
-
const rows = stdout.rows || 24;
|
|
399
|
-
const statusHeight = 1;
|
|
400
|
-
const cols = stdout.columns || 80;
|
|
401
|
-
const wrapped = wrapInputSegments(overlayState.segments, overlayState.cursor, cols);
|
|
402
|
-
const inputLines = Math.max(1, wrapped.lines.length);
|
|
403
|
-
const inputHeight = inputLines + 4;
|
|
404
|
-
const startRow = Math.max(1, rows - (inputHeight + statusHeight) + 1);
|
|
405
|
-
|
|
406
|
-
const prompt = disabled ? chalk.gray('❯ ') : chalk.blue('❯ ');
|
|
407
|
-
|
|
408
|
-
const lines: string[] = [];
|
|
409
|
-
if (disabled) {
|
|
410
|
-
lines.push(prompt + chalk.gray(placeholder));
|
|
411
|
-
} else if (overlayState.segments.length === 0) {
|
|
412
|
-
lines.push(prompt + chalk.inverse('|') + chalk.gray(placeholder));
|
|
413
|
-
} else {
|
|
414
|
-
wrapped.lines.forEach((lineTokens, index) => {
|
|
415
|
-
const prefix = index === 0 ? prompt : ' ';
|
|
416
|
-
let lineText = prefix;
|
|
417
|
-
lineTokens.forEach((token, tokenIndex) => {
|
|
418
|
-
const isCursor = index === wrapped.cursorLine && tokenIndex === wrapped.cursorCol;
|
|
419
|
-
if (isCursor) {
|
|
420
|
-
lineText += chalk.inverse(token.text || ' ');
|
|
421
|
-
} else if (token.style?.color === 'yellow') {
|
|
422
|
-
lineText += chalk.gray(token.text);
|
|
423
|
-
} else {
|
|
424
|
-
lineText += token.text;
|
|
425
|
-
}
|
|
426
|
-
});
|
|
427
|
-
if (index === wrapped.cursorLine && wrapped.cursorCol === lineTokens.length) {
|
|
428
|
-
lineText += chalk.inverse(' ');
|
|
429
|
-
}
|
|
430
|
-
lines.push(lineText);
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const plainText = overlayState.segments.length === 1 && overlayState.segments[0].type === 'text'
|
|
435
|
-
? overlayState.segments[0].text
|
|
436
|
-
: '';
|
|
437
|
-
const isCommandMode = plainText.startsWith('/');
|
|
438
|
-
const commandQuery = isCommandMode ? plainText.slice(1).trim() : '';
|
|
439
|
-
const commandMatches = isCommandMode
|
|
440
|
-
? commands.filter(c => c.name.startsWith(commandQuery)).slice(0, 4)
|
|
441
|
-
: [];
|
|
442
|
-
|
|
443
|
-
for (let i = 0; i < 4; i += 1) {
|
|
444
|
-
const cmd = commandMatches[i];
|
|
445
|
-
if (!cmd) {
|
|
446
|
-
lines.push(' ');
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
const usage = cmd.usage ? ` ${cmd.usage}` : '';
|
|
450
|
-
const line = `${chalk.cyan.bold(`/${cmd.name}`)}${chalk.white(usage)}${chalk.gray(` — ${cmd.description}`)}`;
|
|
451
|
-
lines.push(line);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
stdout.write('\x1b[s');
|
|
455
|
-
for (let i = 0; i < inputHeight; i += 1) {
|
|
456
|
-
const row = startRow + i;
|
|
457
|
-
const raw = lines[i] || '';
|
|
458
|
-
const trimmed = raw.length > cols ? raw.slice(0, cols) : raw;
|
|
459
|
-
stdout.write(`\x1b[${row};1H\x1b[2K${trimmed}`);
|
|
460
|
-
}
|
|
461
|
-
stdout.write('\x1b[u');
|
|
462
|
-
overlayBusyRef.current = false;
|
|
463
|
-
}, [commands, disabled, placeholder, stdout]);
|
|
464
|
-
|
|
465
|
-
const insertTextIntoOverlay = useCallback((text: string) => {
|
|
466
|
-
const current = overlayStateRef.current;
|
|
467
|
-
const next = insertText(current, text);
|
|
468
|
-
overlayStateRef.current = { ...next, historyIdx: -1 };
|
|
469
|
-
renderOverlay(overlayStateRef.current);
|
|
470
|
-
}, [renderOverlay]);
|
|
471
|
-
|
|
472
|
-
const handleClipboardImagePaste = useCallback(async (target: 'state' | 'overlay') => {
|
|
473
|
-
if (pasteBusyRef.current) return;
|
|
474
|
-
pasteBusyRef.current = true;
|
|
475
|
-
try {
|
|
476
|
-
const path = await saveClipboardImage();
|
|
477
|
-
if (!path) return;
|
|
478
|
-
onToast?.(`Image saved to ${path}`);
|
|
479
|
-
if (target === 'overlay') {
|
|
480
|
-
const current = overlayStateRef.current;
|
|
481
|
-
const next = insertBadge(current, { type: 'image', path });
|
|
482
|
-
overlayStateRef.current = { ...next, historyIdx: -1 };
|
|
483
|
-
renderOverlay(overlayStateRef.current);
|
|
484
|
-
} else {
|
|
485
|
-
const current = stateRef.current;
|
|
486
|
-
const next = insertBadge(current, { type: 'image', path });
|
|
487
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
488
|
-
}
|
|
489
|
-
} finally {
|
|
490
|
-
pasteBusyRef.current = false;
|
|
491
|
-
}
|
|
492
|
-
}, [insertTextIntoOverlay, insertTextIntoState, onToast, renderOverlay]);
|
|
493
|
-
|
|
494
|
-
// Update ref so handleSubmit can access it
|
|
495
|
-
handleClipboardImagePasteRef.current = handleClipboardImagePaste;
|
|
496
|
-
|
|
497
|
-
const handleInput = useCallback((input: string, key: InputKey) => {
|
|
498
|
-
// Detect Kitty keyboard protocol CSI u sequences
|
|
499
|
-
// Format: ESC [ <keycode> ; <modifiers> u
|
|
500
|
-
// Modifiers: 2=shift, 3=alt, 5=ctrl, 9=super/cmd
|
|
501
|
-
// Check for sequences with or without ESC (Ink may strip it)
|
|
502
|
-
|
|
503
|
-
// Ctrl+V or Cmd+V for image paste: [118;5u or [118;9u
|
|
504
|
-
const kittyPasteV = /\x1b?\[118;[59]u/.test(input);
|
|
505
|
-
if (kittyPasteV) {
|
|
506
|
-
void handleClipboardImagePaste('state');
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Consume any other Kitty sequences to prevent them from being displayed
|
|
511
|
-
// Match pattern: ESC? [ number ; number u
|
|
512
|
-
if (/\x1b?\[\d+;\d+u/.test(input)) {
|
|
513
|
-
// This is a Kitty keyboard sequence - don't display it as text
|
|
514
|
-
// Extract what key it is and handle accordingly
|
|
515
|
-
const match = input.match(/\x1b?\[(\d+);(\d+)u/);
|
|
516
|
-
if (match) {
|
|
517
|
-
const keycode = parseInt(match[1], 10);
|
|
518
|
-
const modifier = parseInt(match[2], 10);
|
|
519
|
-
// Ctrl+C (99;5) or Cmd+C (99;9) - exit the app
|
|
520
|
-
if (keycode === 99 && (modifier === 5 || modifier === 9)) {
|
|
521
|
-
exit();
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
// Ctrl+L (108;5) - could add clear screen here if needed
|
|
525
|
-
}
|
|
526
|
-
return; // Consume other Kitty sequences
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (disabled) return;
|
|
530
|
-
|
|
531
|
-
// Handle bracketed paste mode markers
|
|
532
|
-
// Note: ESC might be stripped or sent separately by Ink, so check both with and without ESC
|
|
533
|
-
const PASTE_START_FULL = '\x1b[200~';
|
|
534
|
-
const PASTE_START_SHORT = '[200~';
|
|
535
|
-
const PASTE_END_FULL = '\x1b[201~';
|
|
536
|
-
const PASTE_END_SHORT = '[201~';
|
|
537
|
-
|
|
538
|
-
const hasPasteStart = input.includes(PASTE_START_FULL) || input.includes(PASTE_START_SHORT);
|
|
539
|
-
const hasPasteEnd = input.includes(PASTE_END_FULL) || input.includes(PASTE_END_SHORT);
|
|
540
|
-
|
|
541
|
-
// Check for paste start marker
|
|
542
|
-
if (hasPasteStart) {
|
|
543
|
-
isPastingRef.current = true;
|
|
544
|
-
// Remove the paste start marker (try both variants)
|
|
545
|
-
let content = input.replace(PASTE_START_FULL, '').replace(PASTE_START_SHORT, '');
|
|
546
|
-
|
|
547
|
-
// Check if paste end is also in this chunk
|
|
548
|
-
const hasEndInChunk = content.includes(PASTE_END_FULL) || content.includes(PASTE_END_SHORT);
|
|
549
|
-
if (hasEndInChunk) {
|
|
550
|
-
let pasteContent = content.replace(PASTE_END_FULL, '').replace(PASTE_END_SHORT, '');
|
|
551
|
-
// Normalize line endings: \r\n -> \n, \r -> \n
|
|
552
|
-
pasteContent = pasteContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
553
|
-
isPastingRef.current = false;
|
|
554
|
-
pasteBufferRef.current = '';
|
|
555
|
-
// Process the complete paste
|
|
556
|
-
if (pasteContent.length > 0) {
|
|
557
|
-
const lineCount = pasteContent.split('\n').length;
|
|
558
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
559
|
-
const next = insertBadge(state, { type: 'paste', text: pasteContent });
|
|
560
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
561
|
-
} else {
|
|
562
|
-
const next = insertText(state, pasteContent);
|
|
563
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
564
|
-
}
|
|
565
|
-
} else {
|
|
566
|
-
// Empty paste - user may have tried to paste an image
|
|
567
|
-
// Try to extract image from system clipboard
|
|
568
|
-
void handleClipboardImagePaste('state');
|
|
569
|
-
}
|
|
570
|
-
} else {
|
|
571
|
-
pasteBufferRef.current = content;
|
|
572
|
-
}
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// Check for paste end marker
|
|
577
|
-
if (hasPasteEnd) {
|
|
578
|
-
// Remove the paste end marker (try both variants)
|
|
579
|
-
const contentBeforeEnd = input.replace(PASTE_END_FULL, '').replace(PASTE_END_SHORT, '');
|
|
580
|
-
pasteBufferRef.current += contentBeforeEnd;
|
|
581
|
-
// Normalize line endings: \r\n -> \n, \r -> \n
|
|
582
|
-
const pasteContent = pasteBufferRef.current.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
583
|
-
isPastingRef.current = false;
|
|
584
|
-
pasteBufferRef.current = '';
|
|
585
|
-
const lineCount = pasteContent.split('\n').length;
|
|
586
|
-
// Process the complete paste
|
|
587
|
-
if (pasteContent.length > 0) {
|
|
588
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
589
|
-
const next = insertBadge(state, { type: 'paste', text: pasteContent });
|
|
590
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
591
|
-
} else {
|
|
592
|
-
const next = insertText(state, pasteContent);
|
|
593
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
594
|
-
}
|
|
595
|
-
} else {
|
|
596
|
-
// Empty paste - user may have tried to paste an image
|
|
597
|
-
// Try to extract image from system clipboard
|
|
598
|
-
void handleClipboardImagePaste('state');
|
|
599
|
-
}
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// If we're in the middle of a paste, buffer the content
|
|
604
|
-
if (isPastingRef.current) {
|
|
605
|
-
pasteBufferRef.current += input;
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
// Detect backspace via explicit key flag or known control codes
|
|
610
|
-
const backspaceFallback = input === '\b' || input === '\x7f' || input === '\x08';
|
|
611
|
-
const isBackspace = key.backspace || key.delete || backspaceFallback;
|
|
612
|
-
|
|
613
|
-
// Ignore completely empty input events that aren't backspace
|
|
614
|
-
// (some terminals send spurious empty events)
|
|
615
|
-
const safeKey = key as InputKey & { tab?: boolean; escape?: boolean };
|
|
616
|
-
if (
|
|
617
|
-
input === '' &&
|
|
618
|
-
!isBackspace &&
|
|
619
|
-
!safeKey.leftArrow &&
|
|
620
|
-
!safeKey.rightArrow &&
|
|
621
|
-
!safeKey.upArrow &&
|
|
622
|
-
!safeKey.downArrow &&
|
|
623
|
-
!safeKey.return &&
|
|
624
|
-
!safeKey.tab &&
|
|
625
|
-
!safeKey.escape &&
|
|
626
|
-
!safeKey.ctrl &&
|
|
627
|
-
!safeKey.meta
|
|
628
|
-
) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Try to detect Cmd+V / Ctrl+V for image paste
|
|
633
|
-
// Note: Most terminals intercept Cmd+V, so this may not trigger. Use /pi command instead.
|
|
634
|
-
if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
|
|
635
|
-
void handleClipboardImagePaste('state');
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (key.return && !key.shift) {
|
|
640
|
-
handleSubmit();
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (key.return && key.shift) {
|
|
645
|
-
const next = insertText(state, '\n');
|
|
646
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
if (key.upArrow) {
|
|
651
|
-
navigateHistory('up');
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (key.downArrow) {
|
|
655
|
-
navigateHistory('down');
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Tab completion for shell commands
|
|
660
|
-
if (safeKey.tab || input === '\t') {
|
|
661
|
-
void handleTabComplete(state).then(nextState => {
|
|
662
|
-
if (nextState !== state) {
|
|
663
|
-
dispatch({ type: 'apply', state: nextState });
|
|
664
|
-
}
|
|
665
|
-
});
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (key.leftArrow) {
|
|
670
|
-
if (key.ctrl) {
|
|
671
|
-
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
672
|
-
} else {
|
|
673
|
-
dispatch({ type: 'apply', state: moveLeft(state) });
|
|
674
|
-
}
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
if (key.rightArrow) {
|
|
678
|
-
if (key.ctrl) {
|
|
679
|
-
dispatch({ type: 'apply', state: moveWordRight(state) });
|
|
680
|
-
} else {
|
|
681
|
-
dispatch({ type: 'apply', state: moveRight(state) });
|
|
682
|
-
}
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (key.ctrl && input === 'a') {
|
|
687
|
-
dispatch({ type: 'apply', state: { cursor: { index: 0, offset: 0 } } });
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
if (key.ctrl && input === 'e') {
|
|
691
|
-
dispatch({ type: 'apply', state: { cursor: { index: state.segments.length, offset: 0 } } });
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
if (key.ctrl && input === 'b') {
|
|
695
|
-
dispatch({ type: 'apply', state: moveLeft(state) });
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
if (key.ctrl && input === 'f') {
|
|
699
|
-
dispatch({ type: 'apply', state: moveRight(state) });
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
if (key.ctrl && input === 'p') {
|
|
703
|
-
navigateHistory('up');
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (key.ctrl && input === 'n') {
|
|
707
|
-
navigateHistory('down');
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
if (isBackspace) {
|
|
712
|
-
// On macOS, backspace key reports as key.delete=true, so always use backspace() for backward deletion
|
|
713
|
-
const next = backspace(state);
|
|
714
|
-
dispatch({ type: 'apply', state: next });
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (key.ctrl && input === 'u') {
|
|
719
|
-
const { next, killed } = killToStart(state);
|
|
720
|
-
pushKill(killed);
|
|
721
|
-
dispatch({ type: 'apply', state: next });
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
if (key.ctrl && input === 'k') {
|
|
725
|
-
const { next, killed } = killToEnd(state);
|
|
726
|
-
pushKill(killed);
|
|
727
|
-
dispatch({ type: 'apply', state: next });
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
if (key.ctrl && input === 'y') {
|
|
731
|
-
dispatch({ type: 'apply', state: yank(state) });
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (key.meta && input === 'y') {
|
|
735
|
-
dispatch({ type: 'apply', state: yankPop(state) });
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
if (key.ctrl && input === 't') {
|
|
739
|
-
dispatch({ type: 'apply', state: transposeChars(state) });
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
if (key.meta && input === 't') {
|
|
743
|
-
dispatch({ type: 'apply', state: transposeWords(state) });
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
if (key.ctrl && input === 'd') {
|
|
747
|
-
dispatch({ type: 'apply', state: deleteForward(state) });
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (key.ctrl && input === 'w') {
|
|
752
|
-
const { next, killed } = killWordBackward(state);
|
|
753
|
-
pushKill(killed);
|
|
754
|
-
dispatch({ type: 'apply', state: next });
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
if (key.meta && (input === '\b' || input === '\x7f')) {
|
|
758
|
-
const { next, killed } = killWordBackward(state);
|
|
759
|
-
pushKill(killed);
|
|
760
|
-
dispatch({ type: 'apply', state: next });
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
if (key.meta && input === 'd') {
|
|
764
|
-
const { next, killed } = killWordForward(state);
|
|
765
|
-
pushKill(killed);
|
|
766
|
-
dispatch({ type: 'apply', state: next });
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (key.meta && input === 'b') {
|
|
770
|
-
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
if (key.meta && input === 'f') {
|
|
774
|
-
dispatch({ type: 'apply', state: moveWordRight(state) });
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
if (!key.ctrl && !key.meta && input) {
|
|
779
|
-
if (input.includes('\n')) {
|
|
780
|
-
const lineCount = input.split('\n').length;
|
|
781
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
782
|
-
const next = insertBadge(state, { type: 'paste', text: input });
|
|
783
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
const next = insertText(state, input);
|
|
788
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
789
|
-
}
|
|
790
|
-
}, [disabled, exit, handleClipboardImagePaste, handleSubmit, navigateHistory, state]);
|
|
791
|
-
|
|
792
|
-
const handleOverlayInput = useCallback((input: string, key: InputKey) => {
|
|
793
|
-
if (disabled) return;
|
|
794
|
-
// Detect backspace via explicit key flag or known control codes
|
|
795
|
-
const backspaceFallback = input === '\b' || input === '\x7f' || input === '\x08';
|
|
796
|
-
const isBackspace = key.backspace || key.delete || backspaceFallback;
|
|
797
|
-
|
|
798
|
-
// Ignore completely empty input events that aren't backspace
|
|
799
|
-
const safeKey = key as InputKey & { tab?: boolean; escape?: boolean };
|
|
800
|
-
if (
|
|
801
|
-
input === '' &&
|
|
802
|
-
!isBackspace &&
|
|
803
|
-
!safeKey.leftArrow &&
|
|
804
|
-
!safeKey.rightArrow &&
|
|
805
|
-
!safeKey.upArrow &&
|
|
806
|
-
!safeKey.downArrow &&
|
|
807
|
-
!safeKey.return &&
|
|
808
|
-
!safeKey.tab &&
|
|
809
|
-
!safeKey.escape &&
|
|
810
|
-
!safeKey.ctrl &&
|
|
811
|
-
!safeKey.meta
|
|
812
|
-
) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const current = overlayStateRef.current;
|
|
817
|
-
|
|
818
|
-
if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
|
|
819
|
-
void handleClipboardImagePaste('overlay');
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (key.return && !key.shift) {
|
|
824
|
-
const serialized = serializeSegments(current.segments).trim();
|
|
825
|
-
if (serialized) {
|
|
826
|
-
const plainText = getPlainText(current.segments).trim();
|
|
827
|
-
const isCommand = current.segments.length === 1 && current.segments[0].type === 'text' && plainText.startsWith('/');
|
|
828
|
-
if (isCommand) {
|
|
829
|
-
const parts = plainText.slice(1).split(/\s+/);
|
|
830
|
-
const cmd = parts[0].toLowerCase();
|
|
831
|
-
const args = parts.slice(1);
|
|
832
|
-
|
|
833
|
-
// Handle /pi (paste image) command directly
|
|
834
|
-
if (cmd === 'pi' || cmd === 'paste-image') {
|
|
835
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
836
|
-
renderOverlay(overlayStateRef.current);
|
|
837
|
-
void handleClipboardImagePaste('overlay');
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
onCommand?.(cmd, args);
|
|
842
|
-
} else {
|
|
843
|
-
onSubmit(serialized);
|
|
844
|
-
}
|
|
845
|
-
if (historyEnabled) {
|
|
846
|
-
current.history = [...current.history.slice(-100), serialized];
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
850
|
-
renderOverlay(overlayStateRef.current);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
if (key.return && key.shift) {
|
|
855
|
-
overlayStateRef.current = { ...insertText(current, '\n'), historyIdx: -1 };
|
|
856
|
-
renderOverlay(overlayStateRef.current);
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (key.upArrow || key.downArrow) {
|
|
861
|
-
const direction = key.upArrow ? 'up' : 'down';
|
|
862
|
-
if (historyEnabled && current.history.length > 0) {
|
|
863
|
-
let newIdx: number;
|
|
864
|
-
if (direction === 'up') {
|
|
865
|
-
newIdx = current.historyIdx === -1 ? current.history.length - 1 : Math.max(0, current.historyIdx - 1);
|
|
866
|
-
} else {
|
|
867
|
-
newIdx = current.historyIdx === -1 ? -1 : current.historyIdx + 1;
|
|
868
|
-
if (newIdx >= current.history.length) newIdx = -1;
|
|
869
|
-
}
|
|
870
|
-
if (newIdx === -1) {
|
|
871
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
872
|
-
} else {
|
|
873
|
-
const historyValue = current.history[newIdx];
|
|
874
|
-
overlayStateRef.current = {
|
|
875
|
-
...current,
|
|
876
|
-
historyIdx: newIdx,
|
|
877
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
878
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
879
|
-
};
|
|
880
|
-
}
|
|
881
|
-
renderOverlay(overlayStateRef.current);
|
|
882
|
-
}
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (key.leftArrow) {
|
|
887
|
-
if (key.ctrl) {
|
|
888
|
-
overlayStateRef.current = moveWordLeft(current);
|
|
889
|
-
} else {
|
|
890
|
-
overlayStateRef.current = moveLeft(current);
|
|
891
|
-
}
|
|
892
|
-
renderOverlay(overlayStateRef.current);
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
if (key.rightArrow) {
|
|
897
|
-
if (key.ctrl) {
|
|
898
|
-
overlayStateRef.current = moveWordRight(current);
|
|
899
|
-
} else {
|
|
900
|
-
overlayStateRef.current = moveRight(current);
|
|
901
|
-
}
|
|
902
|
-
renderOverlay(overlayStateRef.current);
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (key.ctrl && input === 'a') {
|
|
907
|
-
overlayStateRef.current = { ...current, cursor: { index: 0, offset: 0 } };
|
|
908
|
-
renderOverlay(overlayStateRef.current);
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
if (key.ctrl && input === 'e') {
|
|
913
|
-
overlayStateRef.current = { ...current, cursor: { index: current.segments.length, offset: 0 } };
|
|
914
|
-
renderOverlay(overlayStateRef.current);
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
if (key.ctrl && input === 'b') {
|
|
918
|
-
overlayStateRef.current = moveLeft(current);
|
|
919
|
-
renderOverlay(overlayStateRef.current);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
if (key.ctrl && input === 'f') {
|
|
923
|
-
overlayStateRef.current = moveRight(current);
|
|
924
|
-
renderOverlay(overlayStateRef.current);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (key.ctrl && input === 'p') {
|
|
928
|
-
const direction = 'up';
|
|
929
|
-
if (historyEnabled && current.history.length > 0) {
|
|
930
|
-
let newIdx = current.historyIdx === -1 ? current.history.length - 1 : Math.max(0, current.historyIdx - 1);
|
|
931
|
-
const historyValue = current.history[newIdx] || '';
|
|
932
|
-
overlayStateRef.current = {
|
|
933
|
-
...current,
|
|
934
|
-
historyIdx: newIdx,
|
|
935
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
936
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
937
|
-
};
|
|
938
|
-
renderOverlay(overlayStateRef.current);
|
|
939
|
-
}
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
if (key.ctrl && input === 'n') {
|
|
943
|
-
if (historyEnabled && current.history.length > 0) {
|
|
944
|
-
let newIdx = current.historyIdx === -1 ? -1 : current.historyIdx + 1;
|
|
945
|
-
if (newIdx >= current.history.length) newIdx = -1;
|
|
946
|
-
if (newIdx === -1) {
|
|
947
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
948
|
-
} else {
|
|
949
|
-
const historyValue = current.history[newIdx];
|
|
950
|
-
overlayStateRef.current = {
|
|
951
|
-
...current,
|
|
952
|
-
historyIdx: newIdx,
|
|
953
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
954
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
renderOverlay(overlayStateRef.current);
|
|
958
|
-
}
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (isBackspace) {
|
|
963
|
-
// On macOS, backspace key reports as key.delete=true, so always use backspace() for backward deletion
|
|
964
|
-
overlayStateRef.current = backspace(current);
|
|
965
|
-
renderOverlay(overlayStateRef.current);
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
if (key.ctrl && input === 'u') {
|
|
970
|
-
const { next, killed } = killToStart(current);
|
|
971
|
-
pushKill(killed);
|
|
972
|
-
overlayStateRef.current = { ...next, history: current.history, historyIdx: -1 };
|
|
973
|
-
renderOverlay(overlayStateRef.current);
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (key.ctrl && input === 'k') {
|
|
977
|
-
const { next, killed } = killToEnd(current);
|
|
978
|
-
pushKill(killed);
|
|
979
|
-
overlayStateRef.current = next;
|
|
980
|
-
renderOverlay(overlayStateRef.current);
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
|
-
if (key.ctrl && input === 'y') {
|
|
984
|
-
overlayStateRef.current = yank(current);
|
|
985
|
-
renderOverlay(overlayStateRef.current);
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
if (key.meta && input === 'y') {
|
|
989
|
-
overlayStateRef.current = yankPop(current);
|
|
990
|
-
renderOverlay(overlayStateRef.current);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (key.ctrl && input === 't') {
|
|
994
|
-
overlayStateRef.current = transposeChars(current);
|
|
995
|
-
renderOverlay(overlayStateRef.current);
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
if (key.meta && input === 't') {
|
|
999
|
-
overlayStateRef.current = transposeWords(current);
|
|
1000
|
-
renderOverlay(overlayStateRef.current);
|
|
1001
|
-
return;
|
|
1002
|
-
}
|
|
1003
|
-
if (key.ctrl && input === 'd') {
|
|
1004
|
-
overlayStateRef.current = deleteForward(current);
|
|
1005
|
-
renderOverlay(overlayStateRef.current);
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (key.ctrl && input === 'w') {
|
|
1010
|
-
const { next, killed } = killWordBackward(current);
|
|
1011
|
-
pushKill(killed);
|
|
1012
|
-
overlayStateRef.current = next;
|
|
1013
|
-
renderOverlay(overlayStateRef.current);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
if (key.meta && (input === '\b' || input === '\x7f')) {
|
|
1017
|
-
const { next, killed } = killWordBackward(current);
|
|
1018
|
-
pushKill(killed);
|
|
1019
|
-
overlayStateRef.current = next;
|
|
1020
|
-
renderOverlay(overlayStateRef.current);
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
if (key.meta && input === 'd') {
|
|
1024
|
-
const { next, killed } = killWordForward(current);
|
|
1025
|
-
pushKill(killed);
|
|
1026
|
-
overlayStateRef.current = next;
|
|
1027
|
-
renderOverlay(overlayStateRef.current);
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
if (key.meta && input === 'b') {
|
|
1031
|
-
overlayStateRef.current = moveWordLeft(current);
|
|
1032
|
-
renderOverlay(overlayStateRef.current);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
if (key.meta && input === 'f') {
|
|
1036
|
-
overlayStateRef.current = moveWordRight(current);
|
|
1037
|
-
renderOverlay(overlayStateRef.current);
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
if (!key.ctrl && !key.meta && input) {
|
|
1042
|
-
if (input.includes('\n')) {
|
|
1043
|
-
const lineCount = input.split('\n').length;
|
|
1044
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
1045
|
-
overlayStateRef.current = insertBadge(current, { type: 'paste', text: input });
|
|
1046
|
-
} else {
|
|
1047
|
-
overlayStateRef.current = insertText(current, input);
|
|
1048
|
-
}
|
|
1049
|
-
} else {
|
|
1050
|
-
overlayStateRef.current = insertText(current, input);
|
|
1051
|
-
}
|
|
1052
|
-
overlayStateRef.current = { ...overlayStateRef.current, historyIdx: -1 };
|
|
1053
|
-
renderOverlay(overlayStateRef.current);
|
|
1054
|
-
}
|
|
1055
|
-
}, [disabled, historyEnabled, onCommand, onSubmit, renderOverlay]);
|
|
1056
|
-
|
|
1057
|
-
useInput((input, key) => {
|
|
1058
|
-
if (overlayEnabled) {
|
|
1059
|
-
handleOverlayInput(input, key);
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
handleInput(input, key);
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
React.useEffect(() => {
|
|
1066
|
-
if (!inputBus) return;
|
|
1067
|
-
return inputBus.subscribe(({ input, key }) => {
|
|
1068
|
-
if (overlayEnabled) {
|
|
1069
|
-
handleOverlayInput(input, key);
|
|
1070
|
-
return;
|
|
1071
|
-
}
|
|
1072
|
-
handleInput(input, key);
|
|
1073
|
-
});
|
|
1074
|
-
}, [handleInput, handleOverlayInput, inputBus, overlayEnabled]);
|
|
1075
|
-
|
|
1076
|
-
React.useEffect(() => {
|
|
1077
|
-
if (!overlayEnabled) return;
|
|
1078
|
-
renderOverlay(overlayStateRef.current);
|
|
1079
|
-
}, [overlayEnabled, renderOverlay]);
|
|
1080
|
-
|
|
1081
|
-
const node = buildInputAreaView({
|
|
1082
|
-
state,
|
|
1083
|
-
placeholder,
|
|
1084
|
-
disabled,
|
|
1085
|
-
commands,
|
|
1086
|
-
cols,
|
|
1087
|
-
badgePreview,
|
|
1088
|
-
showBadgePreview: true,
|
|
1089
|
-
debug,
|
|
1090
|
-
renderContent: !overlayEnabled
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
return <InkNode node={node} />;
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
export default InputArea;
|