zerg-ztc 0.1.11 → 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/ztc-audio-darwin-arm64 +0 -0
- package/dist/utils/dictation_native.d.ts.map +1 -1
- package/dist/utils/dictation_native.js +43 -23
- package/dist/utils/dictation_native.js.map +1 -1
- package/package.json +5 -4
- package/packages/ztc-dictation/Cargo.toml +0 -43
- package/packages/ztc-dictation/README.md +0 -65
- package/packages/ztc-dictation/index.d.ts +0 -16
- package/packages/ztc-dictation/index.js +0 -74
- package/packages/ztc-dictation/package.json +0 -41
- package/packages/ztc-dictation/src/main.rs +0 -430
- package/src/App.tsx +0 -910
- 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/dictation.ts +0 -11
- 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 -48
- 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 -88
- 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 -1660
- package/src/components/MessageList.tsx +0 -71
- package/src/components/SingleMessage.tsx +0 -298
- package/src/components/StatusBar.tsx +0 -55
- package/src/components/index.tsx +0 -8
- package/src/config/types.ts +0 -19
- 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/dictation.ts +0 -467
- package/src/utils/dictation_native.ts +0 -258
- 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/path_format.ts +0 -99
- 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/table.ts +0 -171
- package/src/utils/tool_summary.ts +0 -56
- package/src/utils/tool_trace.ts +0 -346
- 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
- /package/{packages/ztc-dictation/bin → bin}/.gitkeep +0 -0
|
@@ -1,1660 +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
|
-
isRecording as isLegacyRecording,
|
|
14
|
-
startRecording as startLegacyRecording,
|
|
15
|
-
stopRecordingAndTranscribe as stopLegacyRecording,
|
|
16
|
-
cancelRecording as cancelLegacyRecording,
|
|
17
|
-
isDictationAvailable as isLegacyDictationAvailable
|
|
18
|
-
} from '../utils/dictation.js';
|
|
19
|
-
import {
|
|
20
|
-
isNativeDictationAvailable,
|
|
21
|
-
isNativeRecording,
|
|
22
|
-
startNativeRecording,
|
|
23
|
-
stopNativeRecording,
|
|
24
|
-
cancelNativeRecording
|
|
25
|
-
} from '../utils/dictation_native.js';
|
|
26
|
-
|
|
27
|
-
// Use native dictation if available, otherwise fall back to legacy
|
|
28
|
-
const useNative = isNativeDictationAvailable();
|
|
29
|
-
const isRecording = () => useNative ? isNativeRecording() : isLegacyRecording();
|
|
30
|
-
const isDictationAvailable = () => useNative ? true : isLegacyDictationAvailable();
|
|
31
|
-
import {
|
|
32
|
-
createEmptyState,
|
|
33
|
-
insertText,
|
|
34
|
-
insertSegment,
|
|
35
|
-
insertBadge,
|
|
36
|
-
backspace,
|
|
37
|
-
deleteForward,
|
|
38
|
-
moveLeft,
|
|
39
|
-
moveRight,
|
|
40
|
-
moveWordLeft,
|
|
41
|
-
moveWordRight,
|
|
42
|
-
getPlainText,
|
|
43
|
-
serializeSegments,
|
|
44
|
-
PASTE_BADGE_THRESHOLD
|
|
45
|
-
} from '../ui/core/input_segments.js';
|
|
46
|
-
|
|
47
|
-
// Helper to check for Ctrl key combinations
|
|
48
|
-
// Handles multiple formats:
|
|
49
|
-
// 1. Ink's key.ctrl + letter
|
|
50
|
-
// 2. Traditional control characters (\x01 for Ctrl+A, etc.)
|
|
51
|
-
// 3. Kitty keyboard protocol: [<keycode>;5u where keycode is ASCII, 5 = Ctrl
|
|
52
|
-
function isCtrl(input: string, key: InputKey, letter: string): boolean {
|
|
53
|
-
const lowerLetter = letter.toLowerCase();
|
|
54
|
-
|
|
55
|
-
// Method 1: Ink's key.ctrl flag
|
|
56
|
-
if (key.ctrl && input === lowerLetter) return true;
|
|
57
|
-
|
|
58
|
-
// Method 2: Traditional control character (Ctrl+A = \x01, etc.)
|
|
59
|
-
const ctrlCode = lowerLetter.charCodeAt(0) - 96; // 'a' -> 1, 'b' -> 2, etc.
|
|
60
|
-
if (input === String.fromCharCode(ctrlCode)) return true;
|
|
61
|
-
|
|
62
|
-
// Method 3: Kitty keyboard protocol [<keycode>;5u (fallback, main handling is in handleKittyInput)
|
|
63
|
-
// keycode is ASCII code of the letter, 5 = Ctrl modifier
|
|
64
|
-
const asciiCode = lowerLetter.charCodeAt(0); // 'a' -> 97, 'r' -> 114, etc.
|
|
65
|
-
const kittyPattern = `[${asciiCode};5u`;
|
|
66
|
-
if (input === kittyPattern || input === `\x1b${kittyPattern}`) return true;
|
|
67
|
-
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface InputAreaProps {
|
|
72
|
-
onSubmit: (text: string) => void;
|
|
73
|
-
onCommand?: (command: string, args: string[]) => void;
|
|
74
|
-
commands?: Array<{
|
|
75
|
-
name: string;
|
|
76
|
-
description: string;
|
|
77
|
-
usage?: string;
|
|
78
|
-
}>;
|
|
79
|
-
onStateChange?: (state: InputState) => void;
|
|
80
|
-
onToast?: (message: string) => void;
|
|
81
|
-
onDictationStateChange?: (state: 'idle' | 'recording' | 'transcribing') => void;
|
|
82
|
-
cols?: number;
|
|
83
|
-
inputBus?: InputBus;
|
|
84
|
-
disabled?: boolean;
|
|
85
|
-
placeholder?: string;
|
|
86
|
-
historyEnabled?: boolean;
|
|
87
|
-
debug?: boolean;
|
|
88
|
-
cwd?: string; // Working directory for path tab completion
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export type { InputState } from '../ui/core/input_state.js';
|
|
92
|
-
|
|
93
|
-
type InputAction =
|
|
94
|
-
| { type: 'apply'; state: Partial<InputState> }
|
|
95
|
-
| { type: 'submit'; historyEnabled: boolean; historyEntry?: string }
|
|
96
|
-
| { type: 'history_nav'; direction: 'up' | 'down'; historyEnabled: boolean };
|
|
97
|
-
|
|
98
|
-
const initialState: InputState = createEmptyState();
|
|
99
|
-
|
|
100
|
-
function reducer(state: InputState, action: InputAction): InputState {
|
|
101
|
-
switch (action.type) {
|
|
102
|
-
case 'apply':
|
|
103
|
-
return { ...state, ...action.state };
|
|
104
|
-
case 'submit': {
|
|
105
|
-
const history = action.historyEnabled && action.historyEntry
|
|
106
|
-
? [...state.history.slice(-100), action.historyEntry]
|
|
107
|
-
: state.history;
|
|
108
|
-
return {
|
|
109
|
-
...state,
|
|
110
|
-
segments: [],
|
|
111
|
-
cursor: { index: 0, offset: 0 },
|
|
112
|
-
historyIdx: -1,
|
|
113
|
-
history
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
case 'history_nav': {
|
|
117
|
-
if (!action.historyEnabled || state.history.length === 0) return state;
|
|
118
|
-
|
|
119
|
-
let newIdx: number;
|
|
120
|
-
if (action.direction === 'up') {
|
|
121
|
-
newIdx = state.historyIdx === -1
|
|
122
|
-
? state.history.length - 1
|
|
123
|
-
: Math.max(0, state.historyIdx - 1);
|
|
124
|
-
} else {
|
|
125
|
-
newIdx = state.historyIdx === -1 ? -1 : state.historyIdx + 1;
|
|
126
|
-
if (newIdx >= state.history.length) newIdx = -1;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (newIdx === -1) {
|
|
130
|
-
return { ...state, historyIdx: -1, segments: [], cursor: { index: 0, offset: 0 } };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const historyValue = state.history[newIdx];
|
|
134
|
-
const nextSegments: InputSegment[] = historyValue.length > 0
|
|
135
|
-
? [{ type: 'text', text: historyValue }]
|
|
136
|
-
: [];
|
|
137
|
-
return {
|
|
138
|
-
...state,
|
|
139
|
-
historyIdx: newIdx,
|
|
140
|
-
segments: nextSegments,
|
|
141
|
-
cursor: { index: nextSegments.length ? 0 : 0, offset: historyValue.length }
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
default:
|
|
145
|
-
return state;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export const InputArea: React.FC<InputAreaProps> = ({
|
|
150
|
-
onSubmit,
|
|
151
|
-
onCommand,
|
|
152
|
-
commands = [],
|
|
153
|
-
onStateChange,
|
|
154
|
-
onToast,
|
|
155
|
-
onDictationStateChange,
|
|
156
|
-
cols = process.stdout.columns || 80,
|
|
157
|
-
inputBus,
|
|
158
|
-
disabled = false,
|
|
159
|
-
placeholder = 'Type a message...',
|
|
160
|
-
historyEnabled = true,
|
|
161
|
-
debug = false,
|
|
162
|
-
cwd = process.cwd()
|
|
163
|
-
}) => {
|
|
164
|
-
const [state, dispatch] = useReducer(reducer, initialState);
|
|
165
|
-
const stateRef = React.useRef(state);
|
|
166
|
-
const [badgePreview, setBadgePreview] = React.useState<string[] | null>(null);
|
|
167
|
-
const killRingRef = React.useRef<string[]>([]);
|
|
168
|
-
const killIndexRef = React.useRef<number>(-1);
|
|
169
|
-
const dictationBusyRef = React.useRef<boolean>(false);
|
|
170
|
-
|
|
171
|
-
// Bracketed paste mode support - buffer paste content between \x1b[200~ and \x1b[201~
|
|
172
|
-
const pasteBufferRef = React.useRef<string>('');
|
|
173
|
-
const isPastingRef = React.useRef<boolean>(false);
|
|
174
|
-
// Ref for handleClipboardImagePaste to avoid circular dependency
|
|
175
|
-
const handleClipboardImagePasteRef = React.useRef<((target: 'state' | 'overlay') => Promise<void>) | null>(null);
|
|
176
|
-
React.useEffect(() => {
|
|
177
|
-
stateRef.current = state;
|
|
178
|
-
onStateChange?.(state);
|
|
179
|
-
}, [onStateChange, state]);
|
|
180
|
-
|
|
181
|
-
React.useEffect(() => {
|
|
182
|
-
const segment = state.segments[state.cursor.index];
|
|
183
|
-
if (!segment || segment.type === 'text') {
|
|
184
|
-
setBadgePreview(null);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
if (segment.type === 'paste') {
|
|
188
|
-
const lines = segment.text.split('\n');
|
|
189
|
-
const preview = lines.slice(0, 3);
|
|
190
|
-
if (lines.length > 3) preview.push('…');
|
|
191
|
-
setBadgePreview(preview);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (segment.type === 'file') {
|
|
195
|
-
setBadgePreview([`[file] ${segment.path}`]);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (segment.type === 'image') {
|
|
199
|
-
renderImagePreview(segment.path, 40, 16).then(result => {
|
|
200
|
-
if (result && result.length > 0) {
|
|
201
|
-
setBadgePreview(result);
|
|
202
|
-
} else {
|
|
203
|
-
setBadgePreview([`[image] ${segment.path}`, 'Install chafa for preview.']);
|
|
204
|
-
}
|
|
205
|
-
}).catch(() => {
|
|
206
|
-
setBadgePreview([`[image] ${segment.path}`]);
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}, [state.cursor.index, state.segments]);
|
|
210
|
-
|
|
211
|
-
const pushKill = useCallback((text: string) => {
|
|
212
|
-
if (!text) return;
|
|
213
|
-
const ring = killRingRef.current;
|
|
214
|
-
ring.unshift(text);
|
|
215
|
-
if (ring.length > 20) ring.pop();
|
|
216
|
-
killIndexRef.current = 0;
|
|
217
|
-
}, []);
|
|
218
|
-
|
|
219
|
-
const yank = useCallback((current: InputState) => {
|
|
220
|
-
const ring = killRingRef.current;
|
|
221
|
-
if (ring.length === 0) return current;
|
|
222
|
-
const next = insertText(current, ring[0]);
|
|
223
|
-
return { ...next, historyIdx: -1 };
|
|
224
|
-
}, []);
|
|
225
|
-
|
|
226
|
-
const yankPop = useCallback((current: InputState) => {
|
|
227
|
-
const ring = killRingRef.current;
|
|
228
|
-
if (ring.length === 0) return current;
|
|
229
|
-
const idx = killIndexRef.current === -1 ? 0 : (killIndexRef.current + 1) % ring.length;
|
|
230
|
-
killIndexRef.current = idx;
|
|
231
|
-
const next = insertText(current, ring[idx]);
|
|
232
|
-
return { ...next, historyIdx: -1 };
|
|
233
|
-
}, []);
|
|
234
|
-
|
|
235
|
-
const killToStart = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
236
|
-
const segments = [...current.segments];
|
|
237
|
-
const cursor = current.cursor;
|
|
238
|
-
const seg = segments[cursor.index];
|
|
239
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
240
|
-
const killed = seg.text.slice(0, cursor.offset);
|
|
241
|
-
seg.text = seg.text.slice(cursor.offset);
|
|
242
|
-
return {
|
|
243
|
-
next: { ...current, segments, cursor: { index: cursor.index, offset: 0 }, historyIdx: -1 },
|
|
244
|
-
killed
|
|
245
|
-
};
|
|
246
|
-
}, []);
|
|
247
|
-
|
|
248
|
-
const killToEnd = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
249
|
-
const segments = [...current.segments];
|
|
250
|
-
const cursor = current.cursor;
|
|
251
|
-
const seg = segments[cursor.index];
|
|
252
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
253
|
-
const killed = seg.text.slice(cursor.offset);
|
|
254
|
-
seg.text = seg.text.slice(0, cursor.offset);
|
|
255
|
-
return {
|
|
256
|
-
next: { ...current, segments, cursor: { index: cursor.index, offset: seg.text.length }, historyIdx: -1 },
|
|
257
|
-
killed
|
|
258
|
-
};
|
|
259
|
-
}, []);
|
|
260
|
-
|
|
261
|
-
const killWordBackward = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
262
|
-
const segments = [...current.segments];
|
|
263
|
-
const cursor = current.cursor;
|
|
264
|
-
const seg = segments[cursor.index];
|
|
265
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
266
|
-
const before = seg.text.slice(0, cursor.offset);
|
|
267
|
-
const after = seg.text.slice(cursor.offset);
|
|
268
|
-
const trimmed = before.replace(/\s+$/, '');
|
|
269
|
-
const match = trimmed.match(/(\S+)\s*$/);
|
|
270
|
-
const killStart = match ? trimmed.length - match[1].length : before.length;
|
|
271
|
-
const killed = before.slice(killStart);
|
|
272
|
-
seg.text = before.slice(0, killStart) + after;
|
|
273
|
-
return {
|
|
274
|
-
next: { ...current, segments, cursor: { index: cursor.index, offset: killStart }, historyIdx: -1 },
|
|
275
|
-
killed
|
|
276
|
-
};
|
|
277
|
-
}, []);
|
|
278
|
-
|
|
279
|
-
const killWordForward = useCallback((current: InputState): { next: InputState; killed: string } => {
|
|
280
|
-
const segments = [...current.segments];
|
|
281
|
-
const cursor = current.cursor;
|
|
282
|
-
const seg = segments[cursor.index];
|
|
283
|
-
if (!seg || seg.type !== 'text') return { next: current, killed: '' };
|
|
284
|
-
const before = seg.text.slice(0, cursor.offset);
|
|
285
|
-
const after = seg.text.slice(cursor.offset);
|
|
286
|
-
const match = after.match(/^(\s*\S+)/);
|
|
287
|
-
const killed = match ? match[1] : '';
|
|
288
|
-
seg.text = before + after.slice(killed.length);
|
|
289
|
-
return {
|
|
290
|
-
next: { ...current, segments, cursor, historyIdx: -1 },
|
|
291
|
-
killed
|
|
292
|
-
};
|
|
293
|
-
}, []);
|
|
294
|
-
|
|
295
|
-
const transposeChars = useCallback((current: InputState): InputState => {
|
|
296
|
-
const segments = [...current.segments];
|
|
297
|
-
const cursor = current.cursor;
|
|
298
|
-
const seg = segments[cursor.index];
|
|
299
|
-
if (!seg || seg.type !== 'text') return current;
|
|
300
|
-
const text = seg.text;
|
|
301
|
-
if (text.length < 2) return current;
|
|
302
|
-
const idx = cursor.offset === 0 ? 0 : cursor.offset === text.length ? text.length - 2 : cursor.offset - 1;
|
|
303
|
-
const chars = text.split('');
|
|
304
|
-
const a = chars[idx];
|
|
305
|
-
chars[idx] = chars[idx + 1];
|
|
306
|
-
chars[idx + 1] = a;
|
|
307
|
-
seg.text = chars.join('');
|
|
308
|
-
return { ...current, segments, cursor: { index: cursor.index, offset: Math.min(text.length, cursor.offset + 1) }, historyIdx: -1 };
|
|
309
|
-
}, []);
|
|
310
|
-
|
|
311
|
-
const transposeWords = useCallback((current: InputState): InputState => {
|
|
312
|
-
const segments = [...current.segments];
|
|
313
|
-
const cursor = current.cursor;
|
|
314
|
-
const seg = segments[cursor.index];
|
|
315
|
-
if (!seg || seg.type !== 'text') return current;
|
|
316
|
-
const text = seg.text;
|
|
317
|
-
const left = text.slice(0, cursor.offset);
|
|
318
|
-
const right = text.slice(cursor.offset);
|
|
319
|
-
const leftMatch = left.match(/(\S+)\s*$/);
|
|
320
|
-
const rightMatch = right.match(/^\s*(\S+)/);
|
|
321
|
-
if (!leftMatch || !rightMatch) return current;
|
|
322
|
-
const leftWord = leftMatch[1];
|
|
323
|
-
const rightWord = rightMatch[1];
|
|
324
|
-
const leftStart = left.length - leftMatch[0].length;
|
|
325
|
-
const rightEnd = rightMatch[0].length;
|
|
326
|
-
const replacedLeft = left.slice(0, leftStart) + rightWord;
|
|
327
|
-
seg.text = replacedLeft + right.slice(rightEnd);
|
|
328
|
-
return { ...current, segments, cursor: { index: cursor.index, offset: replacedLeft.length }, historyIdx: -1 };
|
|
329
|
-
}, []);
|
|
330
|
-
|
|
331
|
-
// Tab completion state
|
|
332
|
-
const tabCompletionAlternativesRef = React.useRef<string[]>([]);
|
|
333
|
-
const lastTabPrefixRef = React.useRef<string>('');
|
|
334
|
-
|
|
335
|
-
// Handle tab completion for shell commands
|
|
336
|
-
const handleTabComplete = useCallback(async (current: InputState): Promise<InputState> => {
|
|
337
|
-
const plainText = getPlainText(current.segments);
|
|
338
|
-
|
|
339
|
-
// Only complete if this looks like a shell command with a path
|
|
340
|
-
// Match: !cd path, !ls path, !cat path, etc.
|
|
341
|
-
const shellMatch = plainText.match(/^!(\w+)\s+(.*)$/);
|
|
342
|
-
if (!shellMatch) {
|
|
343
|
-
// Also support just !cd without a path yet
|
|
344
|
-
if (plainText.match(/^!cd\s*$/)) {
|
|
345
|
-
// Show home directory
|
|
346
|
-
const result = await completePath('~/', cwd);
|
|
347
|
-
if (result && result.alternatives.length > 0) {
|
|
348
|
-
tabCompletionAlternativesRef.current = result.alternatives;
|
|
349
|
-
onToast?.(`Completions: ${result.alternatives.slice(0, 5).join(', ')}${result.alternatives.length > 5 ? '...' : ''}`);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return current;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
const partialPath = shellMatch[2];
|
|
356
|
-
const commandPrefix = `!${shellMatch[1]} `;
|
|
357
|
-
|
|
358
|
-
// Try to complete the path
|
|
359
|
-
const result = await completePath(partialPath, cwd);
|
|
360
|
-
if (!result) {
|
|
361
|
-
onToast?.('No completions');
|
|
362
|
-
return current;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// If we have alternatives and this is a repeat tab, show them
|
|
366
|
-
if (result.alternatives.length > 1) {
|
|
367
|
-
tabCompletionAlternativesRef.current = result.alternatives;
|
|
368
|
-
lastTabPrefixRef.current = partialPath;
|
|
369
|
-
onToast?.(`Completions: ${result.alternatives.slice(0, 6).join(' ')}${result.alternatives.length > 6 ? ' ...' : ''}`);
|
|
370
|
-
} else {
|
|
371
|
-
tabCompletionAlternativesRef.current = [];
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Update the input with the completed path
|
|
375
|
-
const newText = commandPrefix + result.completed;
|
|
376
|
-
const newSegments: InputSegment[] = [{ type: 'text', text: newText }];
|
|
377
|
-
return {
|
|
378
|
-
...current,
|
|
379
|
-
segments: newSegments,
|
|
380
|
-
cursor: { index: 0, offset: newText.length },
|
|
381
|
-
historyIdx: -1
|
|
382
|
-
};
|
|
383
|
-
}, [cwd, onToast]);
|
|
384
|
-
|
|
385
|
-
const inputRenderCount = React.useRef(0);
|
|
386
|
-
React.useEffect(() => {
|
|
387
|
-
inputRenderCount.current += 1;
|
|
388
|
-
const totalLength = state.segments.reduce((acc, segment) => (
|
|
389
|
-
segment.type === 'text' ? acc + segment.text.length : acc + 0
|
|
390
|
-
), 0);
|
|
391
|
-
debugLog(`InputArea render #${inputRenderCount.current} (valueLen=${totalLength})`);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const handleSubmit = useCallback(() => {
|
|
395
|
-
const serialized = serializeSegments(state.segments).trim();
|
|
396
|
-
if (!serialized) return;
|
|
397
|
-
|
|
398
|
-
const plainText = getPlainText(state.segments).trim();
|
|
399
|
-
const isCommand = state.segments.length === 1 && state.segments[0].type === 'text' && plainText.startsWith('/');
|
|
400
|
-
if (isCommand) {
|
|
401
|
-
const parts = plainText.slice(1).split(/\s+/);
|
|
402
|
-
const cmd = parts[0].toLowerCase();
|
|
403
|
-
const args = parts.slice(1);
|
|
404
|
-
|
|
405
|
-
// Handle /pi (paste image) command directly - intercept before passing to onCommand
|
|
406
|
-
if (cmd === 'pi' || cmd === 'paste-image') {
|
|
407
|
-
dispatch({ type: 'submit', historyEnabled, historyEntry: serialized });
|
|
408
|
-
if (handleClipboardImagePasteRef.current) {
|
|
409
|
-
void handleClipboardImagePasteRef.current('state');
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
onCommand?.(cmd, args);
|
|
415
|
-
} else {
|
|
416
|
-
onSubmit(serialized);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
dispatch({ type: 'submit', historyEnabled, historyEntry: serialized });
|
|
420
|
-
}, [state.segments, onSubmit, onCommand, historyEnabled]);
|
|
421
|
-
|
|
422
|
-
const navigateHistory = useCallback((direction: 'up' | 'down') => {
|
|
423
|
-
dispatch({ type: 'history_nav', direction, historyEnabled });
|
|
424
|
-
}, [historyEnabled]);
|
|
425
|
-
|
|
426
|
-
const { stdout } = useStdout();
|
|
427
|
-
const { exit } = useApp();
|
|
428
|
-
const overlayEnabled = process.env.ZTC_INPUT_OVERLAY === '1' && process.env.ZTC_WEB_MIRROR !== '1';
|
|
429
|
-
const overlayStateRef = React.useRef<InputState>(initialState);
|
|
430
|
-
const overlayBusyRef = React.useRef(false);
|
|
431
|
-
const pasteBusyRef = React.useRef(false);
|
|
432
|
-
|
|
433
|
-
const insertTextIntoState = useCallback((text: string) => {
|
|
434
|
-
const current = stateRef.current;
|
|
435
|
-
const next = insertText(current, text);
|
|
436
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
437
|
-
}, []);
|
|
438
|
-
|
|
439
|
-
const renderOverlay = useCallback((overlayState: InputState) => {
|
|
440
|
-
if (!stdout || !stdout.isTTY) return;
|
|
441
|
-
if (overlayBusyRef.current) return;
|
|
442
|
-
overlayBusyRef.current = true;
|
|
443
|
-
|
|
444
|
-
const rows = stdout.rows || 24;
|
|
445
|
-
const statusHeight = 1;
|
|
446
|
-
const cols = stdout.columns || 80;
|
|
447
|
-
const wrapped = wrapInputSegments(overlayState.segments, overlayState.cursor, cols);
|
|
448
|
-
const inputLines = Math.max(1, wrapped.lines.length);
|
|
449
|
-
const inputHeight = inputLines + 4;
|
|
450
|
-
const startRow = Math.max(1, rows - (inputHeight + statusHeight) + 1);
|
|
451
|
-
|
|
452
|
-
const prompt = disabled ? chalk.gray('❯ ') : chalk.blue('❯ ');
|
|
453
|
-
|
|
454
|
-
const lines: string[] = [];
|
|
455
|
-
if (disabled) {
|
|
456
|
-
lines.push(prompt + chalk.gray(placeholder));
|
|
457
|
-
} else if (overlayState.segments.length === 0) {
|
|
458
|
-
lines.push(prompt + chalk.inverse('|') + chalk.gray(placeholder));
|
|
459
|
-
} else {
|
|
460
|
-
wrapped.lines.forEach((lineTokens, index) => {
|
|
461
|
-
const prefix = index === 0 ? prompt : ' ';
|
|
462
|
-
let lineText = prefix;
|
|
463
|
-
lineTokens.forEach((token, tokenIndex) => {
|
|
464
|
-
const isCursor = index === wrapped.cursorLine && tokenIndex === wrapped.cursorCol;
|
|
465
|
-
if (isCursor) {
|
|
466
|
-
lineText += chalk.inverse(token.text || ' ');
|
|
467
|
-
} else if (token.style?.color === 'yellow') {
|
|
468
|
-
lineText += chalk.gray(token.text);
|
|
469
|
-
} else {
|
|
470
|
-
lineText += token.text;
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
if (index === wrapped.cursorLine && wrapped.cursorCol === lineTokens.length) {
|
|
474
|
-
lineText += chalk.inverse(' ');
|
|
475
|
-
}
|
|
476
|
-
lines.push(lineText);
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const plainText = overlayState.segments.length === 1 && overlayState.segments[0].type === 'text'
|
|
481
|
-
? overlayState.segments[0].text
|
|
482
|
-
: '';
|
|
483
|
-
const isCommandMode = plainText.startsWith('/');
|
|
484
|
-
const commandQuery = isCommandMode ? plainText.slice(1).trim() : '';
|
|
485
|
-
const commandMatches = isCommandMode
|
|
486
|
-
? commands.filter(c => c.name.startsWith(commandQuery)).slice(0, 4)
|
|
487
|
-
: [];
|
|
488
|
-
|
|
489
|
-
for (let i = 0; i < 4; i += 1) {
|
|
490
|
-
const cmd = commandMatches[i];
|
|
491
|
-
if (!cmd) {
|
|
492
|
-
lines.push(' ');
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
const usage = cmd.usage ? ` ${cmd.usage}` : '';
|
|
496
|
-
const line = `${chalk.cyan.bold(`/${cmd.name}`)}${chalk.white(usage)}${chalk.gray(` — ${cmd.description}`)}`;
|
|
497
|
-
lines.push(line);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
stdout.write('\x1b[s');
|
|
501
|
-
for (let i = 0; i < inputHeight; i += 1) {
|
|
502
|
-
const row = startRow + i;
|
|
503
|
-
const raw = lines[i] || '';
|
|
504
|
-
const trimmed = raw.length > cols ? raw.slice(0, cols) : raw;
|
|
505
|
-
stdout.write(`\x1b[${row};1H\x1b[2K${trimmed}`);
|
|
506
|
-
}
|
|
507
|
-
stdout.write('\x1b[u');
|
|
508
|
-
overlayBusyRef.current = false;
|
|
509
|
-
}, [commands, disabled, placeholder, stdout]);
|
|
510
|
-
|
|
511
|
-
const insertTextIntoOverlay = useCallback((text: string) => {
|
|
512
|
-
const current = overlayStateRef.current;
|
|
513
|
-
const next = insertText(current, text);
|
|
514
|
-
overlayStateRef.current = { ...next, historyIdx: -1 };
|
|
515
|
-
renderOverlay(overlayStateRef.current);
|
|
516
|
-
}, [renderOverlay]);
|
|
517
|
-
|
|
518
|
-
const handleClipboardImagePaste = useCallback(async (target: 'state' | 'overlay') => {
|
|
519
|
-
if (pasteBusyRef.current) return;
|
|
520
|
-
pasteBusyRef.current = true;
|
|
521
|
-
try {
|
|
522
|
-
const path = await saveClipboardImage();
|
|
523
|
-
if (!path) return;
|
|
524
|
-
onToast?.(`Image saved to ${path}`);
|
|
525
|
-
if (target === 'overlay') {
|
|
526
|
-
const current = overlayStateRef.current;
|
|
527
|
-
const next = insertBadge(current, { type: 'image', path });
|
|
528
|
-
overlayStateRef.current = { ...next, historyIdx: -1 };
|
|
529
|
-
renderOverlay(overlayStateRef.current);
|
|
530
|
-
} else {
|
|
531
|
-
const current = stateRef.current;
|
|
532
|
-
const next = insertBadge(current, { type: 'image', path });
|
|
533
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
534
|
-
}
|
|
535
|
-
} finally {
|
|
536
|
-
pasteBusyRef.current = false;
|
|
537
|
-
}
|
|
538
|
-
}, [insertTextIntoOverlay, insertTextIntoState, onToast, renderOverlay]);
|
|
539
|
-
|
|
540
|
-
// Update ref so handleSubmit can access it
|
|
541
|
-
handleClipboardImagePasteRef.current = handleClipboardImagePaste;
|
|
542
|
-
|
|
543
|
-
// Handle Kitty keyboard protocol sequences directly
|
|
544
|
-
// This is called when we detect a Kitty sequence in handleInput
|
|
545
|
-
const handleKittyInput = useCallback((char: string, key: InputKey, keycode: number, modifier: number) => {
|
|
546
|
-
if (disabled) return;
|
|
547
|
-
|
|
548
|
-
const isKittyCtrl = modifier === 5;
|
|
549
|
-
const isKittyMeta = modifier === 9;
|
|
550
|
-
const lowerChar = char.toLowerCase();
|
|
551
|
-
|
|
552
|
-
// Navigation
|
|
553
|
-
if (isKittyCtrl && lowerChar === 'a') {
|
|
554
|
-
dispatch({ type: 'apply', state: { cursor: { index: 0, offset: 0 } } });
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (isKittyCtrl && lowerChar === 'e') {
|
|
558
|
-
dispatch({ type: 'apply', state: { cursor: { index: state.segments.length, offset: 0 } } });
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (isKittyCtrl && lowerChar === 'b') {
|
|
562
|
-
dispatch({ type: 'apply', state: moveLeft(state) });
|
|
563
|
-
return;
|
|
564
|
-
}
|
|
565
|
-
if (isKittyCtrl && lowerChar === 'f') {
|
|
566
|
-
dispatch({ type: 'apply', state: moveRight(state) });
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
if (isKittyCtrl && lowerChar === 'p') {
|
|
570
|
-
navigateHistory('up');
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (isKittyCtrl && lowerChar === 'n') {
|
|
574
|
-
navigateHistory('down');
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Kill/yank
|
|
579
|
-
if (isKittyCtrl && lowerChar === 'u') {
|
|
580
|
-
const { next, killed } = killToStart(state);
|
|
581
|
-
pushKill(killed);
|
|
582
|
-
dispatch({ type: 'apply', state: next });
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
if (isKittyCtrl && lowerChar === 'k') {
|
|
586
|
-
const { next, killed } = killToEnd(state);
|
|
587
|
-
pushKill(killed);
|
|
588
|
-
dispatch({ type: 'apply', state: next });
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
if (isKittyCtrl && lowerChar === 'y') {
|
|
592
|
-
dispatch({ type: 'apply', state: yank(state) });
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (isKittyCtrl && lowerChar === 'w') {
|
|
596
|
-
const { next, killed } = killWordBackward(state);
|
|
597
|
-
pushKill(killed);
|
|
598
|
-
dispatch({ type: 'apply', state: next });
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Transpose
|
|
603
|
-
if (isKittyCtrl && lowerChar === 't') {
|
|
604
|
-
dispatch({ type: 'apply', state: transposeChars(state) });
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Delete forward
|
|
609
|
-
if (isKittyCtrl && lowerChar === 'd') {
|
|
610
|
-
dispatch({ type: 'apply', state: deleteForward(state) });
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Push-to-talk: Ctrl+R to toggle recording
|
|
615
|
-
if (isKittyCtrl && lowerChar === 'r') {
|
|
616
|
-
if (dictationBusyRef.current) return;
|
|
617
|
-
|
|
618
|
-
if (isRecording()) {
|
|
619
|
-
// Stop recording and transcribe
|
|
620
|
-
dictationBusyRef.current = true;
|
|
621
|
-
onDictationStateChange?.('transcribing');
|
|
622
|
-
|
|
623
|
-
if (useNative) {
|
|
624
|
-
stopNativeRecording()
|
|
625
|
-
.then((text) => {
|
|
626
|
-
if (text && text.trim()) {
|
|
627
|
-
// Submit the transcribed text directly
|
|
628
|
-
onSubmit(text.trim());
|
|
629
|
-
} else {
|
|
630
|
-
onToast?.('No speech detected');
|
|
631
|
-
}
|
|
632
|
-
})
|
|
633
|
-
.catch((err) => {
|
|
634
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
635
|
-
})
|
|
636
|
-
.finally(() => {
|
|
637
|
-
dictationBusyRef.current = false;
|
|
638
|
-
onDictationStateChange?.('idle');
|
|
639
|
-
});
|
|
640
|
-
} else {
|
|
641
|
-
stopLegacyRecording()
|
|
642
|
-
.then((result) => {
|
|
643
|
-
if (result.text && result.text.trim()) {
|
|
644
|
-
// Submit the transcribed text directly
|
|
645
|
-
onSubmit(result.text.trim());
|
|
646
|
-
} else {
|
|
647
|
-
onToast?.('No speech detected');
|
|
648
|
-
}
|
|
649
|
-
})
|
|
650
|
-
.catch((err) => {
|
|
651
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
652
|
-
})
|
|
653
|
-
.finally(() => {
|
|
654
|
-
dictationBusyRef.current = false;
|
|
655
|
-
onDictationStateChange?.('idle');
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
} else {
|
|
659
|
-
// Start recording
|
|
660
|
-
if (!isDictationAvailable()) {
|
|
661
|
-
onToast?.('Dictation not available. Build native/ztc-audio or install sox');
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
try {
|
|
665
|
-
if (useNative) {
|
|
666
|
-
startNativeRecording({ model: 'tiny' });
|
|
667
|
-
} else {
|
|
668
|
-
startLegacyRecording();
|
|
669
|
-
}
|
|
670
|
-
onDictationStateChange?.('recording');
|
|
671
|
-
} catch (err) {
|
|
672
|
-
onToast?.(`Recording error: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Meta key combinations
|
|
679
|
-
if (isKittyMeta && lowerChar === 'y') {
|
|
680
|
-
dispatch({ type: 'apply', state: yankPop(state) });
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
if (isKittyMeta && lowerChar === 't') {
|
|
684
|
-
dispatch({ type: 'apply', state: transposeWords(state) });
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
if (isKittyMeta && lowerChar === 'd') {
|
|
688
|
-
const { next, killed } = killWordForward(state);
|
|
689
|
-
pushKill(killed);
|
|
690
|
-
dispatch({ type: 'apply', state: next });
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
if (isKittyMeta && lowerChar === 'b') {
|
|
694
|
-
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (isKittyMeta && lowerChar === 'f') {
|
|
698
|
-
dispatch({ type: 'apply', state: moveWordRight(state) });
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Unknown Kitty sequence - don't insert as text
|
|
703
|
-
}, [disabled, killToEnd, killToStart, killWordBackward, killWordForward, navigateHistory, onDictationStateChange, onToast, pushKill, state, transposeChars, transposeWords, yank, yankPop]);
|
|
704
|
-
|
|
705
|
-
const handleInput = useCallback((input: string, key: InputKey) => {
|
|
706
|
-
// Detect Kitty keyboard protocol CSI u sequences
|
|
707
|
-
// Format: ESC [ <keycode> ; <modifiers> u
|
|
708
|
-
// Modifiers: 2=shift, 3=alt, 5=ctrl, 9=super/cmd
|
|
709
|
-
// Check for sequences with or without ESC (Ink may strip it)
|
|
710
|
-
|
|
711
|
-
// Ctrl+V or Cmd+V for image paste: [118;5u or [118;9u
|
|
712
|
-
const kittyPasteV = /\x1b?\[118;[59]u/.test(input);
|
|
713
|
-
if (kittyPasteV) {
|
|
714
|
-
void handleClipboardImagePaste('state');
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
// Handle Kitty keyboard protocol sequences
|
|
719
|
-
// Format: ESC? [ keycode ; modifier u
|
|
720
|
-
// Modifier 5 = Ctrl, modifier 9 = Cmd/Super
|
|
721
|
-
const kittyMatch = input.match(/\x1b?\[(\d+);(\d+)u/);
|
|
722
|
-
if (kittyMatch) {
|
|
723
|
-
const keycode = parseInt(kittyMatch[1], 10);
|
|
724
|
-
const modifier = parseInt(kittyMatch[2], 10);
|
|
725
|
-
const isKittyCtrl = modifier === 5;
|
|
726
|
-
const isKittyMeta = modifier === 9;
|
|
727
|
-
|
|
728
|
-
// Ctrl+C (99;5) or Cmd+C (99;9) - exit the app
|
|
729
|
-
if (keycode === 99 && (isKittyCtrl || isKittyMeta)) {
|
|
730
|
-
exit();
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Synthesize key flags for Kitty sequences so downstream handlers work
|
|
735
|
-
const kittyKey: InputKey = {
|
|
736
|
-
...key,
|
|
737
|
-
ctrl: isKittyCtrl || key.ctrl,
|
|
738
|
-
meta: isKittyMeta || key.meta,
|
|
739
|
-
};
|
|
740
|
-
// Convert keycode to character for isCtrl checks
|
|
741
|
-
const kittyChar = String.fromCharCode(keycode);
|
|
742
|
-
|
|
743
|
-
// Route to handlers based on keycode
|
|
744
|
-
// Let the normal handler flow process this with synthesized key flags
|
|
745
|
-
// by falling through with modified key/input
|
|
746
|
-
handleKittyInput(kittyChar, kittyKey, keycode, modifier);
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
if (disabled) return;
|
|
751
|
-
|
|
752
|
-
// Handle bracketed paste mode markers
|
|
753
|
-
// Note: ESC might be stripped or sent separately by Ink, so check both with and without ESC
|
|
754
|
-
const PASTE_START_FULL = '\x1b[200~';
|
|
755
|
-
const PASTE_START_SHORT = '[200~';
|
|
756
|
-
const PASTE_END_FULL = '\x1b[201~';
|
|
757
|
-
const PASTE_END_SHORT = '[201~';
|
|
758
|
-
|
|
759
|
-
const hasPasteStart = input.includes(PASTE_START_FULL) || input.includes(PASTE_START_SHORT);
|
|
760
|
-
const hasPasteEnd = input.includes(PASTE_END_FULL) || input.includes(PASTE_END_SHORT);
|
|
761
|
-
|
|
762
|
-
// Check for paste start marker
|
|
763
|
-
if (hasPasteStart) {
|
|
764
|
-
isPastingRef.current = true;
|
|
765
|
-
// Remove the paste start marker (try both variants)
|
|
766
|
-
let content = input.replace(PASTE_START_FULL, '').replace(PASTE_START_SHORT, '');
|
|
767
|
-
|
|
768
|
-
// Check if paste end is also in this chunk
|
|
769
|
-
const hasEndInChunk = content.includes(PASTE_END_FULL) || content.includes(PASTE_END_SHORT);
|
|
770
|
-
if (hasEndInChunk) {
|
|
771
|
-
let pasteContent = content.replace(PASTE_END_FULL, '').replace(PASTE_END_SHORT, '');
|
|
772
|
-
// Normalize line endings: \r\n -> \n, \r -> \n
|
|
773
|
-
pasteContent = pasteContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
774
|
-
isPastingRef.current = false;
|
|
775
|
-
pasteBufferRef.current = '';
|
|
776
|
-
// Process the complete paste
|
|
777
|
-
if (pasteContent.length > 0) {
|
|
778
|
-
const lineCount = pasteContent.split('\n').length;
|
|
779
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
780
|
-
const next = insertBadge(state, { type: 'paste', text: pasteContent });
|
|
781
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
782
|
-
} else {
|
|
783
|
-
const next = insertText(state, pasteContent);
|
|
784
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
785
|
-
}
|
|
786
|
-
} else {
|
|
787
|
-
// Empty paste - user may have tried to paste an image
|
|
788
|
-
// Try to extract image from system clipboard
|
|
789
|
-
void handleClipboardImagePaste('state');
|
|
790
|
-
}
|
|
791
|
-
} else {
|
|
792
|
-
pasteBufferRef.current = content;
|
|
793
|
-
}
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
// Check for paste end marker
|
|
798
|
-
if (hasPasteEnd) {
|
|
799
|
-
// Remove the paste end marker (try both variants)
|
|
800
|
-
const contentBeforeEnd = input.replace(PASTE_END_FULL, '').replace(PASTE_END_SHORT, '');
|
|
801
|
-
pasteBufferRef.current += contentBeforeEnd;
|
|
802
|
-
// Normalize line endings: \r\n -> \n, \r -> \n
|
|
803
|
-
const pasteContent = pasteBufferRef.current.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
804
|
-
isPastingRef.current = false;
|
|
805
|
-
pasteBufferRef.current = '';
|
|
806
|
-
const lineCount = pasteContent.split('\n').length;
|
|
807
|
-
// Process the complete paste
|
|
808
|
-
if (pasteContent.length > 0) {
|
|
809
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
810
|
-
const next = insertBadge(state, { type: 'paste', text: pasteContent });
|
|
811
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
812
|
-
} else {
|
|
813
|
-
const next = insertText(state, pasteContent);
|
|
814
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
815
|
-
}
|
|
816
|
-
} else {
|
|
817
|
-
// Empty paste - user may have tried to paste an image
|
|
818
|
-
// Try to extract image from system clipboard
|
|
819
|
-
void handleClipboardImagePaste('state');
|
|
820
|
-
}
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// If we're in the middle of a paste, buffer the content
|
|
825
|
-
if (isPastingRef.current) {
|
|
826
|
-
pasteBufferRef.current += input;
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Detect backspace via explicit key flag or known control codes
|
|
831
|
-
const backspaceFallback = input === '\b' || input === '\x7f' || input === '\x08';
|
|
832
|
-
const isBackspace = key.backspace || key.delete || backspaceFallback;
|
|
833
|
-
|
|
834
|
-
// Ignore completely empty input events that aren't backspace
|
|
835
|
-
// (some terminals send spurious empty events)
|
|
836
|
-
const safeKey = key as InputKey & { tab?: boolean; escape?: boolean };
|
|
837
|
-
if (
|
|
838
|
-
input === '' &&
|
|
839
|
-
!isBackspace &&
|
|
840
|
-
!safeKey.leftArrow &&
|
|
841
|
-
!safeKey.rightArrow &&
|
|
842
|
-
!safeKey.upArrow &&
|
|
843
|
-
!safeKey.downArrow &&
|
|
844
|
-
!safeKey.return &&
|
|
845
|
-
!safeKey.tab &&
|
|
846
|
-
!safeKey.escape &&
|
|
847
|
-
!safeKey.ctrl &&
|
|
848
|
-
!safeKey.meta
|
|
849
|
-
) {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Try to detect Cmd+V / Ctrl+V for image paste
|
|
854
|
-
// Note: Most terminals intercept Cmd+V, so this may not trigger. Use /pi command instead.
|
|
855
|
-
if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
|
|
856
|
-
void handleClipboardImagePaste('state');
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (key.return && !key.shift) {
|
|
861
|
-
handleSubmit();
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
if (key.return && key.shift) {
|
|
866
|
-
const next = insertText(state, '\n');
|
|
867
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (key.upArrow) {
|
|
872
|
-
navigateHistory('up');
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
if (key.downArrow) {
|
|
876
|
-
navigateHistory('down');
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Tab completion for shell commands
|
|
881
|
-
if (safeKey.tab || input === '\t') {
|
|
882
|
-
void handleTabComplete(state).then(nextState => {
|
|
883
|
-
if (nextState !== state) {
|
|
884
|
-
dispatch({ type: 'apply', state: nextState });
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
return;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (key.leftArrow) {
|
|
891
|
-
if (key.ctrl) {
|
|
892
|
-
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
893
|
-
} else {
|
|
894
|
-
dispatch({ type: 'apply', state: moveLeft(state) });
|
|
895
|
-
}
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
if (key.rightArrow) {
|
|
899
|
-
if (key.ctrl) {
|
|
900
|
-
dispatch({ type: 'apply', state: moveWordRight(state) });
|
|
901
|
-
} else {
|
|
902
|
-
dispatch({ type: 'apply', state: moveRight(state) });
|
|
903
|
-
}
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (isCtrl(input, key, 'a')) {
|
|
908
|
-
dispatch({ type: 'apply', state: { cursor: { index: 0, offset: 0 } } });
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
if (isCtrl(input, key, 'e')) {
|
|
912
|
-
dispatch({ type: 'apply', state: { cursor: { index: state.segments.length, offset: 0 } } });
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (isCtrl(input, key, 'b')) {
|
|
916
|
-
dispatch({ type: 'apply', state: moveLeft(state) });
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (isCtrl(input, key, 'f')) {
|
|
920
|
-
dispatch({ type: 'apply', state: moveRight(state) });
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (isCtrl(input, key, 'p')) {
|
|
924
|
-
navigateHistory('up');
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (isCtrl(input, key, 'n')) {
|
|
928
|
-
navigateHistory('down');
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
if (isBackspace) {
|
|
933
|
-
// On macOS, backspace key reports as key.delete=true, so always use backspace() for backward deletion
|
|
934
|
-
const next = backspace(state);
|
|
935
|
-
dispatch({ type: 'apply', state: next });
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if (isCtrl(input, key, 'u')) {
|
|
940
|
-
const { next, killed } = killToStart(state);
|
|
941
|
-
pushKill(killed);
|
|
942
|
-
dispatch({ type: 'apply', state: next });
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
if (isCtrl(input, key, 'k')) {
|
|
946
|
-
const { next, killed } = killToEnd(state);
|
|
947
|
-
pushKill(killed);
|
|
948
|
-
dispatch({ type: 'apply', state: next });
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
if (isCtrl(input, key, 'y')) {
|
|
952
|
-
dispatch({ type: 'apply', state: yank(state) });
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
if (key.meta && input === 'y') {
|
|
956
|
-
dispatch({ type: 'apply', state: yankPop(state) });
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
// Push-to-talk: Ctrl+R to toggle recording
|
|
960
|
-
if (isCtrl(input, key, 'r')) {
|
|
961
|
-
if (dictationBusyRef.current) return;
|
|
962
|
-
|
|
963
|
-
if (isRecording()) {
|
|
964
|
-
// Stop recording and transcribe
|
|
965
|
-
dictationBusyRef.current = true;
|
|
966
|
-
onDictationStateChange?.('transcribing');
|
|
967
|
-
|
|
968
|
-
if (useNative) {
|
|
969
|
-
stopNativeRecording()
|
|
970
|
-
.then((text) => {
|
|
971
|
-
if (text && text.trim()) {
|
|
972
|
-
// Submit the transcribed text directly
|
|
973
|
-
onSubmit(text.trim());
|
|
974
|
-
} else {
|
|
975
|
-
onToast?.('No speech detected');
|
|
976
|
-
}
|
|
977
|
-
})
|
|
978
|
-
.catch((err) => {
|
|
979
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
980
|
-
})
|
|
981
|
-
.finally(() => {
|
|
982
|
-
dictationBusyRef.current = false;
|
|
983
|
-
onDictationStateChange?.('idle');
|
|
984
|
-
});
|
|
985
|
-
} else {
|
|
986
|
-
stopLegacyRecording()
|
|
987
|
-
.then((result) => {
|
|
988
|
-
if (result.text && result.text.trim()) {
|
|
989
|
-
// Submit the transcribed text directly
|
|
990
|
-
onSubmit(result.text.trim());
|
|
991
|
-
} else {
|
|
992
|
-
onToast?.('No speech detected');
|
|
993
|
-
}
|
|
994
|
-
})
|
|
995
|
-
.catch((err) => {
|
|
996
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
997
|
-
})
|
|
998
|
-
.finally(() => {
|
|
999
|
-
dictationBusyRef.current = false;
|
|
1000
|
-
onDictationStateChange?.('idle');
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
} else {
|
|
1004
|
-
// Start recording
|
|
1005
|
-
if (!isDictationAvailable()) {
|
|
1006
|
-
onToast?.('Dictation not available. Build native/ztc-audio or install sox');
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
try {
|
|
1010
|
-
if (useNative) {
|
|
1011
|
-
startNativeRecording({ model: 'tiny' });
|
|
1012
|
-
} else {
|
|
1013
|
-
startLegacyRecording();
|
|
1014
|
-
}
|
|
1015
|
-
onDictationStateChange?.('recording');
|
|
1016
|
-
} catch (err) {
|
|
1017
|
-
onToast?.(`Recording error: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
if (isCtrl(input, key, 't')) {
|
|
1023
|
-
dispatch({ type: 'apply', state: transposeChars(state) });
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
if (key.meta && input === 't') {
|
|
1027
|
-
dispatch({ type: 'apply', state: transposeWords(state) });
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
if (isCtrl(input, key, 'd')) {
|
|
1031
|
-
dispatch({ type: 'apply', state: deleteForward(state) });
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
if (isCtrl(input, key, 'w')) {
|
|
1036
|
-
const { next, killed } = killWordBackward(state);
|
|
1037
|
-
pushKill(killed);
|
|
1038
|
-
dispatch({ type: 'apply', state: next });
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
if (key.meta && (input === '\b' || input === '\x7f')) {
|
|
1042
|
-
const { next, killed } = killWordBackward(state);
|
|
1043
|
-
pushKill(killed);
|
|
1044
|
-
dispatch({ type: 'apply', state: next });
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
if (key.meta && input === 'd') {
|
|
1048
|
-
const { next, killed } = killWordForward(state);
|
|
1049
|
-
pushKill(killed);
|
|
1050
|
-
dispatch({ type: 'apply', state: next });
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (key.meta && input === 'b') {
|
|
1054
|
-
dispatch({ type: 'apply', state: moveWordLeft(state) });
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
if (key.meta && input === 'f') {
|
|
1058
|
-
dispatch({ type: 'apply', state: moveWordRight(state) });
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (!key.ctrl && !key.meta && input) {
|
|
1063
|
-
if (input.includes('\n')) {
|
|
1064
|
-
const lineCount = input.split('\n').length;
|
|
1065
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
1066
|
-
const next = insertBadge(state, { type: 'paste', text: input });
|
|
1067
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
1068
|
-
return;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
const next = insertText(state, input);
|
|
1072
|
-
dispatch({ type: 'apply', state: { ...next, historyIdx: -1 } });
|
|
1073
|
-
}
|
|
1074
|
-
}, [disabled, exit, handleClipboardImagePaste, handleKittyInput, handleSubmit, navigateHistory, state]);
|
|
1075
|
-
|
|
1076
|
-
// Handle Kitty keyboard protocol sequences for overlay mode
|
|
1077
|
-
const handleKittyOverlayInput = useCallback((char: string, key: InputKey, keycode: number, modifier: number) => {
|
|
1078
|
-
if (disabled) return;
|
|
1079
|
-
|
|
1080
|
-
const current = overlayStateRef.current;
|
|
1081
|
-
const isKittyCtrl = modifier === 5;
|
|
1082
|
-
const isKittyMeta = modifier === 9;
|
|
1083
|
-
const lowerChar = char.toLowerCase();
|
|
1084
|
-
|
|
1085
|
-
// Navigation
|
|
1086
|
-
if (isKittyCtrl && lowerChar === 'a') {
|
|
1087
|
-
overlayStateRef.current = { ...current, cursor: { index: 0, offset: 0 } };
|
|
1088
|
-
renderOverlay(overlayStateRef.current);
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
if (isKittyCtrl && lowerChar === 'e') {
|
|
1092
|
-
overlayStateRef.current = { ...current, cursor: { index: current.segments.length, offset: 0 } };
|
|
1093
|
-
renderOverlay(overlayStateRef.current);
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
if (isKittyCtrl && lowerChar === 'b') {
|
|
1097
|
-
overlayStateRef.current = moveLeft(current);
|
|
1098
|
-
renderOverlay(overlayStateRef.current);
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (isKittyCtrl && lowerChar === 'f') {
|
|
1102
|
-
overlayStateRef.current = moveRight(current);
|
|
1103
|
-
renderOverlay(overlayStateRef.current);
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (isKittyCtrl && lowerChar === 'p') {
|
|
1107
|
-
if (historyEnabled && current.history.length > 0) {
|
|
1108
|
-
const newIdx = current.historyIdx === -1 ? current.history.length - 1 : Math.max(0, current.historyIdx - 1);
|
|
1109
|
-
const historyValue = current.history[newIdx] || '';
|
|
1110
|
-
overlayStateRef.current = {
|
|
1111
|
-
...current,
|
|
1112
|
-
historyIdx: newIdx,
|
|
1113
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
1114
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
1115
|
-
};
|
|
1116
|
-
renderOverlay(overlayStateRef.current);
|
|
1117
|
-
}
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
if (isKittyCtrl && lowerChar === 'n') {
|
|
1121
|
-
if (historyEnabled && current.history.length > 0) {
|
|
1122
|
-
let newIdx = current.historyIdx === -1 ? -1 : current.historyIdx + 1;
|
|
1123
|
-
if (newIdx >= current.history.length) newIdx = -1;
|
|
1124
|
-
if (newIdx === -1) {
|
|
1125
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
1126
|
-
} else {
|
|
1127
|
-
const historyValue = current.history[newIdx];
|
|
1128
|
-
overlayStateRef.current = {
|
|
1129
|
-
...current,
|
|
1130
|
-
historyIdx: newIdx,
|
|
1131
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
1132
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
renderOverlay(overlayStateRef.current);
|
|
1136
|
-
}
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// Kill/yank
|
|
1141
|
-
if (isKittyCtrl && lowerChar === 'u') {
|
|
1142
|
-
const { next, killed } = killToStart(current);
|
|
1143
|
-
pushKill(killed);
|
|
1144
|
-
overlayStateRef.current = { ...next, history: current.history, historyIdx: -1 };
|
|
1145
|
-
renderOverlay(overlayStateRef.current);
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
if (isKittyCtrl && lowerChar === 'k') {
|
|
1149
|
-
const { next, killed } = killToEnd(current);
|
|
1150
|
-
pushKill(killed);
|
|
1151
|
-
overlayStateRef.current = next;
|
|
1152
|
-
renderOverlay(overlayStateRef.current);
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (isKittyCtrl && lowerChar === 'y') {
|
|
1156
|
-
overlayStateRef.current = yank(current);
|
|
1157
|
-
renderOverlay(overlayStateRef.current);
|
|
1158
|
-
return;
|
|
1159
|
-
}
|
|
1160
|
-
if (isKittyCtrl && lowerChar === 'w') {
|
|
1161
|
-
const { next, killed } = killWordBackward(current);
|
|
1162
|
-
pushKill(killed);
|
|
1163
|
-
overlayStateRef.current = next;
|
|
1164
|
-
renderOverlay(overlayStateRef.current);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
// Transpose
|
|
1169
|
-
if (isKittyCtrl && lowerChar === 't') {
|
|
1170
|
-
overlayStateRef.current = transposeChars(current);
|
|
1171
|
-
renderOverlay(overlayStateRef.current);
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// Delete forward
|
|
1176
|
-
if (isKittyCtrl && lowerChar === 'd') {
|
|
1177
|
-
overlayStateRef.current = deleteForward(current);
|
|
1178
|
-
renderOverlay(overlayStateRef.current);
|
|
1179
|
-
return;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
// Push-to-talk: Ctrl+R to toggle recording
|
|
1183
|
-
if (isKittyCtrl && lowerChar === 'r') {
|
|
1184
|
-
if (dictationBusyRef.current) return;
|
|
1185
|
-
|
|
1186
|
-
if (isRecording()) {
|
|
1187
|
-
dictationBusyRef.current = true;
|
|
1188
|
-
onDictationStateChange?.('transcribing');
|
|
1189
|
-
|
|
1190
|
-
if (useNative) {
|
|
1191
|
-
stopNativeRecording()
|
|
1192
|
-
.then((text) => {
|
|
1193
|
-
if (text && text.trim()) {
|
|
1194
|
-
// Submit the transcribed text directly
|
|
1195
|
-
onSubmit(text.trim());
|
|
1196
|
-
overlayStateRef.current = { ...overlayStateRef.current, segments: [], cursor: { index: 0, offset: 0 }, historyIdx: -1 };
|
|
1197
|
-
renderOverlay(overlayStateRef.current);
|
|
1198
|
-
} else {
|
|
1199
|
-
onToast?.('No speech detected');
|
|
1200
|
-
}
|
|
1201
|
-
})
|
|
1202
|
-
.catch((err) => {
|
|
1203
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
1204
|
-
})
|
|
1205
|
-
.finally(() => {
|
|
1206
|
-
dictationBusyRef.current = false;
|
|
1207
|
-
onDictationStateChange?.('idle');
|
|
1208
|
-
});
|
|
1209
|
-
} else {
|
|
1210
|
-
stopLegacyRecording()
|
|
1211
|
-
.then((result) => {
|
|
1212
|
-
if (result.text && result.text.trim()) {
|
|
1213
|
-
// Submit the transcribed text directly
|
|
1214
|
-
onSubmit(result.text.trim());
|
|
1215
|
-
overlayStateRef.current = { ...overlayStateRef.current, segments: [], cursor: { index: 0, offset: 0 }, historyIdx: -1 };
|
|
1216
|
-
renderOverlay(overlayStateRef.current);
|
|
1217
|
-
} else {
|
|
1218
|
-
onToast?.('No speech detected');
|
|
1219
|
-
}
|
|
1220
|
-
})
|
|
1221
|
-
.catch((err) => {
|
|
1222
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
1223
|
-
})
|
|
1224
|
-
.finally(() => {
|
|
1225
|
-
dictationBusyRef.current = false;
|
|
1226
|
-
onDictationStateChange?.('idle');
|
|
1227
|
-
});
|
|
1228
|
-
}
|
|
1229
|
-
} else {
|
|
1230
|
-
if (!isDictationAvailable()) {
|
|
1231
|
-
onToast?.('Dictation not available. Build native/ztc-audio or install sox');
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
try {
|
|
1235
|
-
if (useNative) {
|
|
1236
|
-
startNativeRecording({ model: 'tiny' });
|
|
1237
|
-
} else {
|
|
1238
|
-
startLegacyRecording();
|
|
1239
|
-
}
|
|
1240
|
-
onDictationStateChange?.('recording');
|
|
1241
|
-
} catch (err) {
|
|
1242
|
-
onToast?.(`Recording error: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// Meta key combinations
|
|
1249
|
-
if (isKittyMeta && lowerChar === 'y') {
|
|
1250
|
-
overlayStateRef.current = yankPop(current);
|
|
1251
|
-
renderOverlay(overlayStateRef.current);
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
if (isKittyMeta && lowerChar === 't') {
|
|
1255
|
-
overlayStateRef.current = transposeWords(current);
|
|
1256
|
-
renderOverlay(overlayStateRef.current);
|
|
1257
|
-
return;
|
|
1258
|
-
}
|
|
1259
|
-
if (isKittyMeta && lowerChar === 'd') {
|
|
1260
|
-
const { next, killed } = killWordForward(current);
|
|
1261
|
-
pushKill(killed);
|
|
1262
|
-
overlayStateRef.current = next;
|
|
1263
|
-
renderOverlay(overlayStateRef.current);
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
if (isKittyMeta && lowerChar === 'b') {
|
|
1267
|
-
overlayStateRef.current = moveWordLeft(current);
|
|
1268
|
-
renderOverlay(overlayStateRef.current);
|
|
1269
|
-
return;
|
|
1270
|
-
}
|
|
1271
|
-
if (isKittyMeta && lowerChar === 'f') {
|
|
1272
|
-
overlayStateRef.current = moveWordRight(current);
|
|
1273
|
-
renderOverlay(overlayStateRef.current);
|
|
1274
|
-
return;
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
// Unknown Kitty sequence - don't insert as text
|
|
1278
|
-
}, [disabled, historyEnabled, killToEnd, killToStart, killWordBackward, killWordForward, onDictationStateChange, onToast, pushKill, renderOverlay, transposeChars, transposeWords, yank, yankPop]);
|
|
1279
|
-
|
|
1280
|
-
const handleOverlayInput = useCallback((input: string, key: InputKey) => {
|
|
1281
|
-
if (disabled) return;
|
|
1282
|
-
|
|
1283
|
-
// Handle Kitty keyboard protocol sequences for overlay mode
|
|
1284
|
-
const kittyMatch = input.match(/\x1b?\[(\d+);(\d+)u/);
|
|
1285
|
-
if (kittyMatch) {
|
|
1286
|
-
const keycode = parseInt(kittyMatch[1], 10);
|
|
1287
|
-
const modifier = parseInt(kittyMatch[2], 10);
|
|
1288
|
-
const kittyChar = String.fromCharCode(keycode);
|
|
1289
|
-
handleKittyOverlayInput(kittyChar, key, keycode, modifier);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
// Detect backspace via explicit key flag or known control codes
|
|
1294
|
-
const backspaceFallback = input === '\b' || input === '\x7f' || input === '\x08';
|
|
1295
|
-
const isBackspace = key.backspace || key.delete || backspaceFallback;
|
|
1296
|
-
|
|
1297
|
-
// Ignore completely empty input events that aren't backspace
|
|
1298
|
-
const safeKey = key as InputKey & { tab?: boolean; escape?: boolean };
|
|
1299
|
-
if (
|
|
1300
|
-
input === '' &&
|
|
1301
|
-
!isBackspace &&
|
|
1302
|
-
!safeKey.leftArrow &&
|
|
1303
|
-
!safeKey.rightArrow &&
|
|
1304
|
-
!safeKey.upArrow &&
|
|
1305
|
-
!safeKey.downArrow &&
|
|
1306
|
-
!safeKey.return &&
|
|
1307
|
-
!safeKey.tab &&
|
|
1308
|
-
!safeKey.escape &&
|
|
1309
|
-
!safeKey.ctrl &&
|
|
1310
|
-
!safeKey.meta
|
|
1311
|
-
) {
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
const current = overlayStateRef.current;
|
|
1316
|
-
|
|
1317
|
-
if ((key.ctrl || key.meta) && (input === 'v' || input === '\u0016')) {
|
|
1318
|
-
void handleClipboardImagePaste('overlay');
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
if (key.return && !key.shift) {
|
|
1323
|
-
const serialized = serializeSegments(current.segments).trim();
|
|
1324
|
-
if (serialized) {
|
|
1325
|
-
const plainText = getPlainText(current.segments).trim();
|
|
1326
|
-
const isCommand = current.segments.length === 1 && current.segments[0].type === 'text' && plainText.startsWith('/');
|
|
1327
|
-
if (isCommand) {
|
|
1328
|
-
const parts = plainText.slice(1).split(/\s+/);
|
|
1329
|
-
const cmd = parts[0].toLowerCase();
|
|
1330
|
-
const args = parts.slice(1);
|
|
1331
|
-
|
|
1332
|
-
// Handle /pi (paste image) command directly
|
|
1333
|
-
if (cmd === 'pi' || cmd === 'paste-image') {
|
|
1334
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
1335
|
-
renderOverlay(overlayStateRef.current);
|
|
1336
|
-
void handleClipboardImagePaste('overlay');
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
onCommand?.(cmd, args);
|
|
1341
|
-
} else {
|
|
1342
|
-
onSubmit(serialized);
|
|
1343
|
-
}
|
|
1344
|
-
if (historyEnabled) {
|
|
1345
|
-
current.history = [...current.history.slice(-100), serialized];
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
1349
|
-
renderOverlay(overlayStateRef.current);
|
|
1350
|
-
return;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (key.return && key.shift) {
|
|
1354
|
-
overlayStateRef.current = { ...insertText(current, '\n'), historyIdx: -1 };
|
|
1355
|
-
renderOverlay(overlayStateRef.current);
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (key.upArrow || key.downArrow) {
|
|
1360
|
-
const direction = key.upArrow ? 'up' : 'down';
|
|
1361
|
-
if (historyEnabled && current.history.length > 0) {
|
|
1362
|
-
let newIdx: number;
|
|
1363
|
-
if (direction === 'up') {
|
|
1364
|
-
newIdx = current.historyIdx === -1 ? current.history.length - 1 : Math.max(0, current.historyIdx - 1);
|
|
1365
|
-
} else {
|
|
1366
|
-
newIdx = current.historyIdx === -1 ? -1 : current.historyIdx + 1;
|
|
1367
|
-
if (newIdx >= current.history.length) newIdx = -1;
|
|
1368
|
-
}
|
|
1369
|
-
if (newIdx === -1) {
|
|
1370
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
1371
|
-
} else {
|
|
1372
|
-
const historyValue = current.history[newIdx];
|
|
1373
|
-
overlayStateRef.current = {
|
|
1374
|
-
...current,
|
|
1375
|
-
historyIdx: newIdx,
|
|
1376
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
1377
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
renderOverlay(overlayStateRef.current);
|
|
1381
|
-
}
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
if (key.leftArrow) {
|
|
1386
|
-
if (key.ctrl) {
|
|
1387
|
-
overlayStateRef.current = moveWordLeft(current);
|
|
1388
|
-
} else {
|
|
1389
|
-
overlayStateRef.current = moveLeft(current);
|
|
1390
|
-
}
|
|
1391
|
-
renderOverlay(overlayStateRef.current);
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
if (key.rightArrow) {
|
|
1396
|
-
if (key.ctrl) {
|
|
1397
|
-
overlayStateRef.current = moveWordRight(current);
|
|
1398
|
-
} else {
|
|
1399
|
-
overlayStateRef.current = moveRight(current);
|
|
1400
|
-
}
|
|
1401
|
-
renderOverlay(overlayStateRef.current);
|
|
1402
|
-
return;
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
if (isCtrl(input, key, 'a')) {
|
|
1406
|
-
overlayStateRef.current = { ...current, cursor: { index: 0, offset: 0 } };
|
|
1407
|
-
renderOverlay(overlayStateRef.current);
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
if (isCtrl(input, key, 'e')) {
|
|
1412
|
-
overlayStateRef.current = { ...current, cursor: { index: current.segments.length, offset: 0 } };
|
|
1413
|
-
renderOverlay(overlayStateRef.current);
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (isCtrl(input, key, 'b')) {
|
|
1417
|
-
overlayStateRef.current = moveLeft(current);
|
|
1418
|
-
renderOverlay(overlayStateRef.current);
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
if (isCtrl(input, key, 'f')) {
|
|
1422
|
-
overlayStateRef.current = moveRight(current);
|
|
1423
|
-
renderOverlay(overlayStateRef.current);
|
|
1424
|
-
return;
|
|
1425
|
-
}
|
|
1426
|
-
if (isCtrl(input, key, 'p')) {
|
|
1427
|
-
const direction = 'up';
|
|
1428
|
-
if (historyEnabled && current.history.length > 0) {
|
|
1429
|
-
let newIdx = current.historyIdx === -1 ? current.history.length - 1 : Math.max(0, current.historyIdx - 1);
|
|
1430
|
-
const historyValue = current.history[newIdx] || '';
|
|
1431
|
-
overlayStateRef.current = {
|
|
1432
|
-
...current,
|
|
1433
|
-
historyIdx: newIdx,
|
|
1434
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
1435
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
1436
|
-
};
|
|
1437
|
-
renderOverlay(overlayStateRef.current);
|
|
1438
|
-
}
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
1441
|
-
if (isCtrl(input, key, 'n')) {
|
|
1442
|
-
if (historyEnabled && current.history.length > 0) {
|
|
1443
|
-
let newIdx = current.historyIdx === -1 ? -1 : current.historyIdx + 1;
|
|
1444
|
-
if (newIdx >= current.history.length) newIdx = -1;
|
|
1445
|
-
if (newIdx === -1) {
|
|
1446
|
-
overlayStateRef.current = { ...createEmptyState(), history: current.history, historyIdx: -1 };
|
|
1447
|
-
} else {
|
|
1448
|
-
const historyValue = current.history[newIdx];
|
|
1449
|
-
overlayStateRef.current = {
|
|
1450
|
-
...current,
|
|
1451
|
-
historyIdx: newIdx,
|
|
1452
|
-
segments: historyValue.length > 0 ? [{ type: 'text', text: historyValue }] : [],
|
|
1453
|
-
cursor: { index: historyValue.length > 0 ? 0 : 0, offset: historyValue.length }
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
renderOverlay(overlayStateRef.current);
|
|
1457
|
-
}
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
if (isBackspace) {
|
|
1462
|
-
// On macOS, backspace key reports as key.delete=true, so always use backspace() for backward deletion
|
|
1463
|
-
overlayStateRef.current = backspace(current);
|
|
1464
|
-
renderOverlay(overlayStateRef.current);
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
if (isCtrl(input, key, 'u')) {
|
|
1469
|
-
const { next, killed } = killToStart(current);
|
|
1470
|
-
pushKill(killed);
|
|
1471
|
-
overlayStateRef.current = { ...next, history: current.history, historyIdx: -1 };
|
|
1472
|
-
renderOverlay(overlayStateRef.current);
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
if (isCtrl(input, key, 'k')) {
|
|
1476
|
-
const { next, killed } = killToEnd(current);
|
|
1477
|
-
pushKill(killed);
|
|
1478
|
-
overlayStateRef.current = next;
|
|
1479
|
-
renderOverlay(overlayStateRef.current);
|
|
1480
|
-
return;
|
|
1481
|
-
}
|
|
1482
|
-
if (isCtrl(input, key, 'y')) {
|
|
1483
|
-
overlayStateRef.current = yank(current);
|
|
1484
|
-
renderOverlay(overlayStateRef.current);
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
// Push-to-talk: Ctrl+R to toggle recording (overlay mode)
|
|
1488
|
-
if (isCtrl(input, key, 'r')) {
|
|
1489
|
-
if (dictationBusyRef.current) return;
|
|
1490
|
-
|
|
1491
|
-
if (isRecording()) {
|
|
1492
|
-
dictationBusyRef.current = true;
|
|
1493
|
-
onDictationStateChange?.('transcribing');
|
|
1494
|
-
|
|
1495
|
-
if (useNative) {
|
|
1496
|
-
stopNativeRecording()
|
|
1497
|
-
.then((text) => {
|
|
1498
|
-
if (text && text.trim()) {
|
|
1499
|
-
// Submit the transcribed text directly
|
|
1500
|
-
onSubmit(text.trim());
|
|
1501
|
-
overlayStateRef.current = { ...overlayStateRef.current, segments: [], cursor: { index: 0, offset: 0 }, historyIdx: -1 };
|
|
1502
|
-
renderOverlay(overlayStateRef.current);
|
|
1503
|
-
} else {
|
|
1504
|
-
onToast?.('No speech detected');
|
|
1505
|
-
}
|
|
1506
|
-
})
|
|
1507
|
-
.catch((err) => {
|
|
1508
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
1509
|
-
})
|
|
1510
|
-
.finally(() => {
|
|
1511
|
-
dictationBusyRef.current = false;
|
|
1512
|
-
onDictationStateChange?.('idle');
|
|
1513
|
-
});
|
|
1514
|
-
} else {
|
|
1515
|
-
stopLegacyRecording()
|
|
1516
|
-
.then((result) => {
|
|
1517
|
-
if (result.text && result.text.trim()) {
|
|
1518
|
-
// Submit the transcribed text directly
|
|
1519
|
-
onSubmit(result.text.trim());
|
|
1520
|
-
overlayStateRef.current = { ...overlayStateRef.current, segments: [], cursor: { index: 0, offset: 0 }, historyIdx: -1 };
|
|
1521
|
-
renderOverlay(overlayStateRef.current);
|
|
1522
|
-
} else {
|
|
1523
|
-
onToast?.('No speech detected');
|
|
1524
|
-
}
|
|
1525
|
-
})
|
|
1526
|
-
.catch((err) => {
|
|
1527
|
-
onToast?.(`Dictation error: ${err.message}`);
|
|
1528
|
-
})
|
|
1529
|
-
.finally(() => {
|
|
1530
|
-
dictationBusyRef.current = false;
|
|
1531
|
-
onDictationStateChange?.('idle');
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
} else {
|
|
1535
|
-
if (!isDictationAvailable()) {
|
|
1536
|
-
onToast?.('Dictation not available. Build native/ztc-audio or install sox');
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
try {
|
|
1540
|
-
if (useNative) {
|
|
1541
|
-
startNativeRecording({ model: 'tiny' });
|
|
1542
|
-
} else {
|
|
1543
|
-
startLegacyRecording();
|
|
1544
|
-
}
|
|
1545
|
-
onDictationStateChange?.('recording');
|
|
1546
|
-
} catch (err) {
|
|
1547
|
-
onToast?.(`Recording error: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
if (key.meta && input === 'y') {
|
|
1553
|
-
overlayStateRef.current = yankPop(current);
|
|
1554
|
-
renderOverlay(overlayStateRef.current);
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
if (isCtrl(input, key, 't')) {
|
|
1558
|
-
overlayStateRef.current = transposeChars(current);
|
|
1559
|
-
renderOverlay(overlayStateRef.current);
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
if (key.meta && input === 't') {
|
|
1563
|
-
overlayStateRef.current = transposeWords(current);
|
|
1564
|
-
renderOverlay(overlayStateRef.current);
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
if (isCtrl(input, key, 'd')) {
|
|
1568
|
-
overlayStateRef.current = deleteForward(current);
|
|
1569
|
-
renderOverlay(overlayStateRef.current);
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
if (isCtrl(input, key, 'w')) {
|
|
1574
|
-
const { next, killed } = killWordBackward(current);
|
|
1575
|
-
pushKill(killed);
|
|
1576
|
-
overlayStateRef.current = next;
|
|
1577
|
-
renderOverlay(overlayStateRef.current);
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
if (key.meta && (input === '\b' || input === '\x7f')) {
|
|
1581
|
-
const { next, killed } = killWordBackward(current);
|
|
1582
|
-
pushKill(killed);
|
|
1583
|
-
overlayStateRef.current = next;
|
|
1584
|
-
renderOverlay(overlayStateRef.current);
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
if (key.meta && input === 'd') {
|
|
1588
|
-
const { next, killed } = killWordForward(current);
|
|
1589
|
-
pushKill(killed);
|
|
1590
|
-
overlayStateRef.current = next;
|
|
1591
|
-
renderOverlay(overlayStateRef.current);
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
if (key.meta && input === 'b') {
|
|
1595
|
-
overlayStateRef.current = moveWordLeft(current);
|
|
1596
|
-
renderOverlay(overlayStateRef.current);
|
|
1597
|
-
return;
|
|
1598
|
-
}
|
|
1599
|
-
if (key.meta && input === 'f') {
|
|
1600
|
-
overlayStateRef.current = moveWordRight(current);
|
|
1601
|
-
renderOverlay(overlayStateRef.current);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
if (!key.ctrl && !key.meta && input) {
|
|
1606
|
-
if (input.includes('\n')) {
|
|
1607
|
-
const lineCount = input.split('\n').length;
|
|
1608
|
-
if (lineCount >= PASTE_BADGE_THRESHOLD) {
|
|
1609
|
-
overlayStateRef.current = insertBadge(current, { type: 'paste', text: input });
|
|
1610
|
-
} else {
|
|
1611
|
-
overlayStateRef.current = insertText(current, input);
|
|
1612
|
-
}
|
|
1613
|
-
} else {
|
|
1614
|
-
overlayStateRef.current = insertText(current, input);
|
|
1615
|
-
}
|
|
1616
|
-
overlayStateRef.current = { ...overlayStateRef.current, historyIdx: -1 };
|
|
1617
|
-
renderOverlay(overlayStateRef.current);
|
|
1618
|
-
}
|
|
1619
|
-
}, [disabled, handleKittyOverlayInput, historyEnabled, onCommand, onSubmit, renderOverlay]);
|
|
1620
|
-
|
|
1621
|
-
useInput((input, key) => {
|
|
1622
|
-
if (overlayEnabled) {
|
|
1623
|
-
handleOverlayInput(input, key);
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
handleInput(input, key);
|
|
1627
|
-
});
|
|
1628
|
-
|
|
1629
|
-
React.useEffect(() => {
|
|
1630
|
-
if (!inputBus) return;
|
|
1631
|
-
return inputBus.subscribe(({ input, key }) => {
|
|
1632
|
-
if (overlayEnabled) {
|
|
1633
|
-
handleOverlayInput(input, key);
|
|
1634
|
-
return;
|
|
1635
|
-
}
|
|
1636
|
-
handleInput(input, key);
|
|
1637
|
-
});
|
|
1638
|
-
}, [handleInput, handleOverlayInput, inputBus, overlayEnabled]);
|
|
1639
|
-
|
|
1640
|
-
React.useEffect(() => {
|
|
1641
|
-
if (!overlayEnabled) return;
|
|
1642
|
-
renderOverlay(overlayStateRef.current);
|
|
1643
|
-
}, [overlayEnabled, renderOverlay]);
|
|
1644
|
-
|
|
1645
|
-
const node = buildInputAreaView({
|
|
1646
|
-
state,
|
|
1647
|
-
placeholder,
|
|
1648
|
-
disabled,
|
|
1649
|
-
commands,
|
|
1650
|
-
cols,
|
|
1651
|
-
badgePreview,
|
|
1652
|
-
showBadgePreview: true,
|
|
1653
|
-
debug,
|
|
1654
|
-
renderContent: !overlayEnabled
|
|
1655
|
-
});
|
|
1656
|
-
|
|
1657
|
-
return <InkNode node={node} />;
|
|
1658
|
-
};
|
|
1659
|
-
|
|
1660
|
-
export default InputArea;
|