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.
Files changed (151) hide show
  1. package/bin/.gitkeep +0 -0
  2. package/bin/ztc-audio-darwin-arm64 +0 -0
  3. package/dist/App.d.ts.map +1 -1
  4. package/dist/App.js +63 -2
  5. package/dist/App.js.map +1 -1
  6. package/dist/agent/commands/dictation.d.ts +3 -0
  7. package/dist/agent/commands/dictation.d.ts.map +1 -0
  8. package/dist/agent/commands/dictation.js +10 -0
  9. package/dist/agent/commands/dictation.js.map +1 -0
  10. package/dist/agent/commands/index.d.ts.map +1 -1
  11. package/dist/agent/commands/index.js +2 -1
  12. package/dist/agent/commands/index.js.map +1 -1
  13. package/dist/agent/commands/types.d.ts +7 -0
  14. package/dist/agent/commands/types.d.ts.map +1 -1
  15. package/dist/components/InputArea.d.ts +1 -0
  16. package/dist/components/InputArea.d.ts.map +1 -1
  17. package/dist/components/InputArea.js +591 -43
  18. package/dist/components/InputArea.js.map +1 -1
  19. package/dist/components/SingleMessage.d.ts.map +1 -1
  20. package/dist/components/SingleMessage.js +157 -7
  21. package/dist/components/SingleMessage.js.map +1 -1
  22. package/dist/config/types.d.ts +6 -0
  23. package/dist/config/types.d.ts.map +1 -1
  24. package/dist/ui/views/status_bar.js +2 -2
  25. package/dist/ui/views/status_bar.js.map +1 -1
  26. package/dist/utils/dictation.d.ts +46 -0
  27. package/dist/utils/dictation.d.ts.map +1 -0
  28. package/dist/utils/dictation.js +409 -0
  29. package/dist/utils/dictation.js.map +1 -0
  30. package/dist/utils/dictation_native.d.ts +51 -0
  31. package/dist/utils/dictation_native.d.ts.map +1 -0
  32. package/dist/utils/dictation_native.js +236 -0
  33. package/dist/utils/dictation_native.js.map +1 -0
  34. package/dist/utils/path_format.d.ts +20 -0
  35. package/dist/utils/path_format.d.ts.map +1 -0
  36. package/dist/utils/path_format.js +90 -0
  37. package/dist/utils/path_format.js.map +1 -0
  38. package/dist/utils/table.d.ts +38 -0
  39. package/dist/utils/table.d.ts.map +1 -0
  40. package/dist/utils/table.js +133 -0
  41. package/dist/utils/table.js.map +1 -0
  42. package/dist/utils/tool_trace.d.ts +7 -2
  43. package/dist/utils/tool_trace.d.ts.map +1 -1
  44. package/dist/utils/tool_trace.js +156 -51
  45. package/dist/utils/tool_trace.js.map +1 -1
  46. package/package.json +5 -1
  47. package/src/App.tsx +0 -813
  48. package/src/agent/agent.ts +0 -534
  49. package/src/agent/backends/anthropic.ts +0 -86
  50. package/src/agent/backends/gemini.ts +0 -119
  51. package/src/agent/backends/inception.ts +0 -23
  52. package/src/agent/backends/index.ts +0 -17
  53. package/src/agent/backends/openai.ts +0 -23
  54. package/src/agent/backends/openai_compatible.ts +0 -143
  55. package/src/agent/backends/types.ts +0 -83
  56. package/src/agent/commands/clipboard.ts +0 -77
  57. package/src/agent/commands/config.ts +0 -204
  58. package/src/agent/commands/debug.ts +0 -23
  59. package/src/agent/commands/emulation.ts +0 -80
  60. package/src/agent/commands/execution.ts +0 -9
  61. package/src/agent/commands/help.ts +0 -20
  62. package/src/agent/commands/history.ts +0 -13
  63. package/src/agent/commands/index.ts +0 -46
  64. package/src/agent/commands/input_mode.ts +0 -22
  65. package/src/agent/commands/keybindings.ts +0 -40
  66. package/src/agent/commands/model.ts +0 -11
  67. package/src/agent/commands/models.ts +0 -116
  68. package/src/agent/commands/permissions.ts +0 -64
  69. package/src/agent/commands/retry.ts +0 -9
  70. package/src/agent/commands/shell.ts +0 -68
  71. package/src/agent/commands/skills.ts +0 -54
  72. package/src/agent/commands/status.ts +0 -19
  73. package/src/agent/commands/types.ts +0 -80
  74. package/src/agent/commands/update.ts +0 -32
  75. package/src/agent/factory.ts +0 -60
  76. package/src/agent/index.ts +0 -20
  77. package/src/agent/runtime/capabilities.ts +0 -7
  78. package/src/agent/runtime/memory.ts +0 -23
  79. package/src/agent/runtime/policy.ts +0 -48
  80. package/src/agent/runtime/session.ts +0 -18
  81. package/src/agent/runtime/tracing.ts +0 -23
  82. package/src/agent/tools/file.ts +0 -178
  83. package/src/agent/tools/index.ts +0 -52
  84. package/src/agent/tools/screenshot.ts +0 -821
  85. package/src/agent/tools/search.ts +0 -138
  86. package/src/agent/tools/shell.ts +0 -69
  87. package/src/agent/tools/skills.ts +0 -28
  88. package/src/agent/tools/types.ts +0 -14
  89. package/src/agent/tools/zerg.ts +0 -50
  90. package/src/cli.tsx +0 -163
  91. package/src/components/ActivityLine.tsx +0 -23
  92. package/src/components/FullScreen.tsx +0 -79
  93. package/src/components/Header.tsx +0 -27
  94. package/src/components/InputArea.tsx +0 -1096
  95. package/src/components/MessageList.tsx +0 -71
  96. package/src/components/SingleMessage.tsx +0 -59
  97. package/src/components/StatusBar.tsx +0 -55
  98. package/src/components/index.tsx +0 -8
  99. package/src/config/types.ts +0 -12
  100. package/src/config.ts +0 -186
  101. package/src/debug/logger.ts +0 -14
  102. package/src/emulation/README.md +0 -24
  103. package/src/emulation/catalog.ts +0 -82
  104. package/src/emulation/trace_style.ts +0 -8
  105. package/src/emulation/types.ts +0 -7
  106. package/src/skills/index.ts +0 -36
  107. package/src/skills/loader.ts +0 -135
  108. package/src/skills/registry.ts +0 -6
  109. package/src/skills/types.ts +0 -10
  110. package/src/types.ts +0 -84
  111. package/src/ui/README.md +0 -44
  112. package/src/ui/core/factory.ts +0 -9
  113. package/src/ui/core/index.ts +0 -4
  114. package/src/ui/core/input.ts +0 -38
  115. package/src/ui/core/input_segments.ts +0 -410
  116. package/src/ui/core/input_state.ts +0 -17
  117. package/src/ui/core/layout_yoga.ts +0 -122
  118. package/src/ui/core/style.ts +0 -38
  119. package/src/ui/core/types.ts +0 -54
  120. package/src/ui/ink/index.tsx +0 -1
  121. package/src/ui/ink/render.tsx +0 -60
  122. package/src/ui/views/activity_line.ts +0 -33
  123. package/src/ui/views/app.ts +0 -111
  124. package/src/ui/views/header.ts +0 -44
  125. package/src/ui/views/input_area.ts +0 -255
  126. package/src/ui/views/message_list.ts +0 -443
  127. package/src/ui/views/status_bar.ts +0 -114
  128. package/src/ui/vue/index.ts +0 -53
  129. package/src/ui/web/frame_render.tsx +0 -148
  130. package/src/ui/web/index.tsx +0 -1
  131. package/src/ui/web/render.tsx +0 -41
  132. package/src/utils/clipboard.ts +0 -39
  133. package/src/utils/clipboard_image.ts +0 -40
  134. package/src/utils/diff.ts +0 -52
  135. package/src/utils/image_preview.ts +0 -36
  136. package/src/utils/models.ts +0 -98
  137. package/src/utils/path_complete.ts +0 -173
  138. package/src/utils/shell.ts +0 -72
  139. package/src/utils/spinner_frames.ts +0 -1
  140. package/src/utils/spinner_verbs.ts +0 -23
  141. package/src/utils/tool_summary.ts +0 -56
  142. package/src/utils/tool_trace.ts +0 -216
  143. package/src/utils/update.ts +0 -44
  144. package/src/utils/version.ts +0 -15
  145. package/src/web/index.html +0 -352
  146. package/src/web/mirror-favicon.svg +0 -4
  147. package/src/web/mirror.html +0 -641
  148. package/src/web/mirror_hook.ts +0 -25
  149. package/src/web/mirror_server.ts +0 -204
  150. package/tsconfig.json +0 -22
  151. 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;