@defai.digital/ax-cli 3.8.1 → 3.8.3
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/README.md +51 -1
- package/dist/agent/llm-agent.d.ts +2 -0
- package/dist/agent/llm-agent.js +185 -98
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/analyzers/git/churn-calculator.d.ts +1 -0
- package/dist/analyzers/git/churn-calculator.js +17 -2
- package/dist/analyzers/git/churn-calculator.js.map +1 -1
- package/dist/llm/client.js +17 -6
- package/dist/llm/client.js.map +1 -1
- package/dist/mcp/client-v2.js +1 -3
- package/dist/mcp/client-v2.js.map +1 -1
- package/dist/mcp/content-length-transport.d.ts +71 -0
- package/dist/mcp/content-length-transport.js +190 -0
- package/dist/mcp/content-length-transport.js.map +1 -0
- package/dist/mcp/mutex.js +13 -6
- package/dist/mcp/mutex.js.map +1 -1
- package/dist/mcp/reconnection.js +10 -1
- package/dist/mcp/reconnection.js.map +1 -1
- package/dist/mcp/transports.d.ts +9 -0
- package/dist/mcp/transports.js +23 -6
- package/dist/mcp/transports.js.map +1 -1
- package/dist/ui/hooks/use-enhanced-input.js +464 -98
- package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
- package/dist/utils/rate-limiter.js +13 -1
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/token-counter.js +15 -5
- package/dist/utils/token-counter.js.map +1 -1
- package/package.json +2 -2
|
@@ -21,19 +21,43 @@ function isIncompleteInput(text, smartDetection) {
|
|
|
21
21
|
'[': 0,
|
|
22
22
|
'{': 0,
|
|
23
23
|
};
|
|
24
|
+
// BUG FIX: Track string/template literal state to avoid counting brackets inside strings
|
|
25
|
+
let inSingleQuote = false;
|
|
26
|
+
let inDoubleQuote = false;
|
|
27
|
+
let inBacktick = false;
|
|
28
|
+
let prevChar = '';
|
|
24
29
|
for (const char of trimmed) {
|
|
25
|
-
if
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
// Handle escape sequences - skip if previous char was backslash
|
|
31
|
+
if (prevChar === '\\') {
|
|
32
|
+
prevChar = ''; // Reset to avoid double-escape issues
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Track string state
|
|
36
|
+
if (char === "'" && !inDoubleQuote && !inBacktick) {
|
|
37
|
+
inSingleQuote = !inSingleQuote;
|
|
38
|
+
}
|
|
39
|
+
else if (char === '"' && !inSingleQuote && !inBacktick) {
|
|
40
|
+
inDoubleQuote = !inDoubleQuote;
|
|
41
|
+
}
|
|
42
|
+
else if (char === '`' && !inSingleQuote && !inDoubleQuote) {
|
|
43
|
+
inBacktick = !inBacktick;
|
|
44
|
+
}
|
|
45
|
+
// Only count brackets outside of strings
|
|
46
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
47
|
+
if (char === '(')
|
|
48
|
+
brackets['(']++;
|
|
49
|
+
else if (char === ')')
|
|
50
|
+
brackets['(']--;
|
|
51
|
+
else if (char === '[')
|
|
52
|
+
brackets['[']++;
|
|
53
|
+
else if (char === ']')
|
|
54
|
+
brackets['[']--;
|
|
55
|
+
else if (char === '{')
|
|
56
|
+
brackets['{']++;
|
|
57
|
+
else if (char === '}')
|
|
58
|
+
brackets['{']--;
|
|
59
|
+
}
|
|
60
|
+
prevChar = char;
|
|
37
61
|
}
|
|
38
62
|
// If any bracket is unclosed, input is incomplete
|
|
39
63
|
if (brackets['('] > 0 || brackets['['] > 0 || brackets['{'] > 0) {
|
|
@@ -42,14 +66,37 @@ function isIncompleteInput(text, smartDetection) {
|
|
|
42
66
|
}
|
|
43
67
|
// Check for trailing operators
|
|
44
68
|
if (smartDetection.checkOperators) {
|
|
69
|
+
// BUG FIX: Order operators by length (longest first) to avoid false positives
|
|
70
|
+
// e.g., "===" should be checked before "==" and "="
|
|
45
71
|
const trailingOperators = [
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'?', ':', ',', '.',
|
|
72
|
+
'===', '!==', '...', '&&', '||', '==', '!=', '<=', '>=', '=>',
|
|
73
|
+
'..', '+', '-', '*', '/', '%', '=', '<', '>', '&', '|', '^',
|
|
74
|
+
'?', ':', ',', '.',
|
|
49
75
|
];
|
|
50
|
-
for
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
// BUG FIX: Don't check for trailing operators if line ends inside a string
|
|
77
|
+
// Check if we're inside an unclosed string by counting unescaped quotes
|
|
78
|
+
const lastLine = trimmed.split('\n').pop() || '';
|
|
79
|
+
let inSingleQuote = false;
|
|
80
|
+
let inDoubleQuote = false;
|
|
81
|
+
let inBacktick = false;
|
|
82
|
+
let prevChar = '';
|
|
83
|
+
for (const char of lastLine) {
|
|
84
|
+
if (prevChar !== '\\') {
|
|
85
|
+
if (char === "'" && !inDoubleQuote && !inBacktick)
|
|
86
|
+
inSingleQuote = !inSingleQuote;
|
|
87
|
+
else if (char === '"' && !inSingleQuote && !inBacktick)
|
|
88
|
+
inDoubleQuote = !inDoubleQuote;
|
|
89
|
+
else if (char === '`' && !inSingleQuote && !inDoubleQuote)
|
|
90
|
+
inBacktick = !inBacktick;
|
|
91
|
+
}
|
|
92
|
+
prevChar = char === '\\' && prevChar !== '\\' ? '\\' : '';
|
|
93
|
+
}
|
|
94
|
+
// Only check for trailing operators if we're not inside a string
|
|
95
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
96
|
+
for (const op of trailingOperators) {
|
|
97
|
+
if (trimmed.endsWith(op)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
53
100
|
}
|
|
54
101
|
}
|
|
55
102
|
}
|
|
@@ -63,15 +110,56 @@ function isIncompleteInput(text, smartDetection) {
|
|
|
63
110
|
];
|
|
64
111
|
// Check if line ends with a statement keyword (potentially incomplete)
|
|
65
112
|
const lastLine = trimmed.split('\n').pop() || '';
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
113
|
+
// BUG FIX: Check if we're inside a string before checking keywords
|
|
114
|
+
let inSingleQuote = false;
|
|
115
|
+
let inDoubleQuote = false;
|
|
116
|
+
let inBacktick = false;
|
|
117
|
+
let prevChar = '';
|
|
118
|
+
for (const char of lastLine) {
|
|
119
|
+
if (prevChar !== '\\') {
|
|
120
|
+
if (char === "'" && !inDoubleQuote && !inBacktick)
|
|
121
|
+
inSingleQuote = !inSingleQuote;
|
|
122
|
+
else if (char === '"' && !inSingleQuote && !inBacktick)
|
|
123
|
+
inDoubleQuote = !inDoubleQuote;
|
|
124
|
+
else if (char === '`' && !inSingleQuote && !inDoubleQuote)
|
|
125
|
+
inBacktick = !inBacktick;
|
|
126
|
+
}
|
|
127
|
+
prevChar = char === '\\' && prevChar !== '\\' ? '\\' : '';
|
|
70
128
|
}
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
129
|
+
// Only check keywords if we're not inside a string
|
|
130
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
131
|
+
const words = lastLine.trim().split(/\s+/);
|
|
132
|
+
const lastWord = words[words.length - 1];
|
|
133
|
+
if (incompleteKeywords.includes(lastWord)) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
// Check for statement keywords at start of last line without closing
|
|
137
|
+
// BUG FIX: Check for { and ; outside of strings, not just anywhere in line
|
|
138
|
+
const firstWord = words[0];
|
|
139
|
+
if (incompleteKeywords.includes(firstWord)) {
|
|
140
|
+
// Check if { or ; appears outside strings
|
|
141
|
+
let hasClosingOutsideString = false;
|
|
142
|
+
let inSQ = false, inDQ = false, inBT = false;
|
|
143
|
+
let prev = '';
|
|
144
|
+
for (const c of lastLine) {
|
|
145
|
+
if (prev !== '\\') {
|
|
146
|
+
if (c === "'" && !inDQ && !inBT)
|
|
147
|
+
inSQ = !inSQ;
|
|
148
|
+
else if (c === '"' && !inSQ && !inBT)
|
|
149
|
+
inDQ = !inDQ;
|
|
150
|
+
else if (c === '`' && !inSQ && !inDQ)
|
|
151
|
+
inBT = !inBT;
|
|
152
|
+
}
|
|
153
|
+
if (!inSQ && !inDQ && !inBT && (c === '{' || c === ';')) {
|
|
154
|
+
hasClosingOutsideString = true;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
prev = c === '\\' && prev !== '\\' ? '\\' : '';
|
|
158
|
+
}
|
|
159
|
+
if (!hasClosingOutsideString) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
75
163
|
}
|
|
76
164
|
}
|
|
77
165
|
return false;
|
|
@@ -82,6 +170,9 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
82
170
|
const [pastedBlocks, setPastedBlocks] = useState([]);
|
|
83
171
|
// BUG FIX: Use ref instead of state to prevent race conditions with concurrent pastes
|
|
84
172
|
const pasteCounterRef = useRef(0);
|
|
173
|
+
// BUG FIX: Use ref for pastedBlocks to ensure expandPlaceholdersForSubmit always has current value
|
|
174
|
+
// This prevents race conditions when paste + submit happen rapidly before React re-renders
|
|
175
|
+
const pastedBlocksRef = useRef(pastedBlocks);
|
|
85
176
|
const [currentBlockAtCursor, setCurrentBlockAtCursor] = useState(null);
|
|
86
177
|
const [isPasting, setIsPasting] = useState(false); // v3.8.0: Track paste accumulation state
|
|
87
178
|
// Load input configuration from settings (Phase 1) // FIX: Use RequiredInputSettings type to match getInputConfig() return type
|
|
@@ -100,33 +191,46 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
100
191
|
const fallbackPasteBufferRef = useRef('');
|
|
101
192
|
const fallbackPasteTimeoutRef = useRef(null);
|
|
102
193
|
const fallbackPasteLastChunkTimeRef = useRef(0); // Track when last chunk arrived
|
|
194
|
+
// BUG FIX: Use refs for callbacks used in setTimeout to avoid stale closures
|
|
195
|
+
const onLargePasteRef = useRef(onLargePaste);
|
|
196
|
+
const onPasteTruncatedRef = useRef(onPasteTruncated);
|
|
197
|
+
// BUG FIX: Track mounted state for async operations
|
|
198
|
+
const isMountedRef = useRef(true);
|
|
103
199
|
// Keep ref in sync with prop to avoid stale closure
|
|
104
200
|
isMultilineRef.current = multiline;
|
|
201
|
+
onLargePasteRef.current = onLargePaste;
|
|
202
|
+
onPasteTruncatedRef.current = onPasteTruncated;
|
|
105
203
|
const { addToHistory, navigateHistory, resetHistory, setOriginalInput, isNavigatingHistory, } = useInputHistory(projectDir);
|
|
106
204
|
const setInput = useCallback((text) => {
|
|
107
205
|
setInputState(text);
|
|
108
206
|
// Use functional update to get the current cursor position, avoiding stale closure
|
|
109
|
-
|
|
207
|
+
const newCursor = Math.min(text.length, cursorPositionRef.current);
|
|
208
|
+
setCursorPositionState(newCursor);
|
|
110
209
|
if (!isNavigatingHistory()) {
|
|
111
210
|
setOriginalInput(text);
|
|
112
211
|
}
|
|
212
|
+
// BUG FIX: Clear pasted blocks when input is completely replaced
|
|
213
|
+
// The old block metadata is no longer valid for the new text
|
|
214
|
+
pastedBlocksRef.current = [];
|
|
215
|
+
setPastedBlocks([]);
|
|
216
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
217
|
+
inputRef.current = text;
|
|
218
|
+
cursorPositionRef.current = newCursor;
|
|
113
219
|
}, [isNavigatingHistory, setOriginalInput]);
|
|
114
220
|
const setCursorPosition = useCallback((position) => {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
return currentInput; // No change to input, just accessing for bounds check
|
|
124
|
-
});
|
|
221
|
+
// BUG FIX: Use inputRef for bounds checking instead of nested state update with queueMicrotask
|
|
222
|
+
// This avoids race conditions where input changes between scheduling and execution
|
|
223
|
+
const currentInputLength = inputRef.current.length;
|
|
224
|
+
const boundedPosition = Math.max(0, Math.min(currentInputLength, position));
|
|
225
|
+
setCursorPositionState(boundedPosition);
|
|
226
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
227
|
+
cursorPositionRef.current = boundedPosition;
|
|
125
228
|
}, []);
|
|
126
229
|
const clearInput = useCallback(() => {
|
|
127
230
|
setInputState("");
|
|
128
231
|
setCursorPositionState(0);
|
|
129
232
|
setOriginalInput("");
|
|
233
|
+
pastedBlocksRef.current = [];
|
|
130
234
|
setPastedBlocks([]);
|
|
131
235
|
pasteCounterRef.current = 0; // BUG FIX: Reset counter ref
|
|
132
236
|
setIsPasting(false); // v3.8.0: Reset paste state
|
|
@@ -139,13 +243,24 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
139
243
|
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
140
244
|
fallbackPasteTimeoutRef.current = null;
|
|
141
245
|
}
|
|
246
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
247
|
+
inputRef.current = '';
|
|
248
|
+
cursorPositionRef.current = 0;
|
|
142
249
|
}, [setOriginalInput]);
|
|
143
250
|
const insertAtCursor = useCallback((text) => {
|
|
144
|
-
|
|
251
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
252
|
+
// This ensures insertAtCursor sees the latest input/cursor even if called
|
|
253
|
+
// rapidly after other state updates
|
|
254
|
+
const currentInput = inputRef.current;
|
|
255
|
+
const currentCursor = cursorPositionRef.current;
|
|
256
|
+
const result = insertText(currentInput, currentCursor, text);
|
|
145
257
|
setInputState(result.text);
|
|
146
258
|
setCursorPositionState(result.position);
|
|
147
259
|
setOriginalInput(result.text);
|
|
148
|
-
|
|
260
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
261
|
+
inputRef.current = result.text;
|
|
262
|
+
cursorPositionRef.current = result.position;
|
|
263
|
+
}, [setOriginalInput]);
|
|
149
264
|
// Handle paste completion
|
|
150
265
|
// Note: No timeout or accumulation needed - Ink batches the entire paste for us
|
|
151
266
|
// BUG FIX: Create refs to track latest values without causing re-renders
|
|
@@ -156,6 +271,10 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
156
271
|
inputRef.current = input;
|
|
157
272
|
cursorPositionRef.current = cursorPosition;
|
|
158
273
|
}, [input, cursorPosition]);
|
|
274
|
+
// BUG FIX: Keep pastedBlocksRef in sync with state
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
pastedBlocksRef.current = pastedBlocks;
|
|
277
|
+
}, [pastedBlocks]);
|
|
159
278
|
const handlePasteComplete = useCallback((pastedContent) => {
|
|
160
279
|
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
161
280
|
const currentInput = inputRef.current;
|
|
@@ -168,13 +287,20 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
168
287
|
const blockId = pasteCounterRef.current++;
|
|
169
288
|
// Create pasted block with CURRENT cursor position
|
|
170
289
|
const block = createPastedBlock(blockId, pastedContent, currentCursor);
|
|
171
|
-
|
|
290
|
+
// BUG FIX: Sync ref immediately so expandPlaceholdersForSubmit has current value
|
|
291
|
+
// even if submit happens before React processes the state update
|
|
292
|
+
const newBlocks = [...pastedBlocksRef.current, block];
|
|
293
|
+
pastedBlocksRef.current = newBlocks;
|
|
294
|
+
setPastedBlocks(newBlocks);
|
|
172
295
|
// Insert placeholder instead of full content
|
|
173
296
|
const placeholder = generatePlaceholder(block);
|
|
174
297
|
const result = insertText(currentInput, currentCursor, placeholder);
|
|
175
298
|
setInputState(result.text);
|
|
176
299
|
setCursorPositionState(result.position);
|
|
177
300
|
setOriginalInput(result.text);
|
|
301
|
+
// BUG FIX: Synchronously update refs to prevent stale reads in rapid operations
|
|
302
|
+
inputRef.current = result.text;
|
|
303
|
+
cursorPositionRef.current = result.position;
|
|
178
304
|
}
|
|
179
305
|
else {
|
|
180
306
|
// Insert normally (below threshold) with all formatting preserved
|
|
@@ -182,11 +308,17 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
182
308
|
setInputState(result.text);
|
|
183
309
|
setCursorPositionState(result.position);
|
|
184
310
|
setOriginalInput(result.text);
|
|
311
|
+
// BUG FIX: Synchronously update refs to prevent stale reads in rapid operations
|
|
312
|
+
inputRef.current = result.text;
|
|
313
|
+
cursorPositionRef.current = result.position;
|
|
185
314
|
}
|
|
186
315
|
}, [setOriginalInput]);
|
|
187
316
|
// Toggle collapse/expand for block at cursor
|
|
188
317
|
const toggleBlockAtCursor = useCallback(() => {
|
|
189
|
-
|
|
318
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
319
|
+
const currentInput = inputRef.current;
|
|
320
|
+
const currentCursor = cursorPositionRef.current;
|
|
321
|
+
const block = findBlockAtCursor(currentInput, currentCursor, pastedBlocks);
|
|
190
322
|
if (!block)
|
|
191
323
|
return;
|
|
192
324
|
const placeholder = generatePlaceholder(block);
|
|
@@ -195,12 +327,12 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
195
327
|
let searchStart = 0;
|
|
196
328
|
let targetStart = -1;
|
|
197
329
|
// Find the occurrence that contains the cursor
|
|
198
|
-
while (searchStart <
|
|
199
|
-
const occurrenceStart =
|
|
330
|
+
while (searchStart < currentInput.length) {
|
|
331
|
+
const occurrenceStart = currentInput.indexOf(placeholder, searchStart);
|
|
200
332
|
if (occurrenceStart === -1)
|
|
201
333
|
break;
|
|
202
334
|
const occurrenceEnd = occurrenceStart + placeholder.length;
|
|
203
|
-
if (
|
|
335
|
+
if (currentCursor >= occurrenceStart && currentCursor <= occurrenceEnd) {
|
|
204
336
|
targetStart = occurrenceStart;
|
|
205
337
|
break;
|
|
206
338
|
}
|
|
@@ -209,28 +341,35 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
209
341
|
if (targetStart === -1)
|
|
210
342
|
return; // Should not happen
|
|
211
343
|
// Replace only this specific occurrence
|
|
212
|
-
const newInput =
|
|
344
|
+
const newInput = currentInput.substring(0, targetStart) +
|
|
213
345
|
block.content +
|
|
214
|
-
|
|
346
|
+
currentInput.substring(targetStart + placeholder.length);
|
|
215
347
|
setInputState(newInput);
|
|
216
348
|
// Keep cursor at same position or adjust if needed
|
|
217
|
-
const newCursor =
|
|
218
|
-
|
|
349
|
+
const newCursor = currentCursor + (block.content.length - placeholder.length);
|
|
350
|
+
const boundedCursor = Math.min(newInput.length, newCursor);
|
|
351
|
+
setCursorPositionState(boundedCursor);
|
|
219
352
|
setOriginalInput(newInput);
|
|
353
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
354
|
+
inputRef.current = newInput;
|
|
355
|
+
cursorPositionRef.current = boundedCursor;
|
|
220
356
|
// Update block state
|
|
221
|
-
|
|
357
|
+
// BUG FIX: Sync ref immediately for expandPlaceholdersForSubmit
|
|
358
|
+
const updatedBlocks = pastedBlocksRef.current.map(b => b.id === block.id ? { ...b, collapsed: false } : b);
|
|
359
|
+
pastedBlocksRef.current = updatedBlocks;
|
|
360
|
+
setPastedBlocks(updatedBlocks);
|
|
222
361
|
}
|
|
223
362
|
else {
|
|
224
363
|
// Collapse: find the specific occurrence near cursor and replace it
|
|
225
364
|
let searchStart = 0;
|
|
226
365
|
let targetStart = -1;
|
|
227
366
|
// Find the occurrence that contains the cursor
|
|
228
|
-
while (searchStart <
|
|
229
|
-
const occurrenceStart =
|
|
367
|
+
while (searchStart < currentInput.length) {
|
|
368
|
+
const occurrenceStart = currentInput.indexOf(block.content, searchStart);
|
|
230
369
|
if (occurrenceStart === -1)
|
|
231
370
|
break;
|
|
232
371
|
const occurrenceEnd = occurrenceStart + block.content.length;
|
|
233
|
-
if (
|
|
372
|
+
if (currentCursor >= occurrenceStart && currentCursor <= occurrenceEnd) {
|
|
234
373
|
targetStart = occurrenceStart;
|
|
235
374
|
break;
|
|
236
375
|
}
|
|
@@ -239,23 +378,39 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
239
378
|
if (targetStart === -1)
|
|
240
379
|
return; // Should not happen
|
|
241
380
|
// Replace only this specific occurrence
|
|
242
|
-
const newInput =
|
|
381
|
+
const newInput = currentInput.substring(0, targetStart) +
|
|
243
382
|
placeholder +
|
|
244
|
-
|
|
383
|
+
currentInput.substring(targetStart + block.content.length);
|
|
245
384
|
setInputState(newInput);
|
|
246
385
|
// Adjust cursor to end of placeholder
|
|
247
|
-
|
|
386
|
+
const newCursor = targetStart + placeholder.length;
|
|
387
|
+
setCursorPositionState(newCursor);
|
|
248
388
|
setOriginalInput(newInput);
|
|
389
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
390
|
+
inputRef.current = newInput;
|
|
391
|
+
cursorPositionRef.current = newCursor;
|
|
249
392
|
// Update block state
|
|
250
|
-
|
|
393
|
+
// BUG FIX: Sync ref immediately for expandPlaceholdersForSubmit
|
|
394
|
+
const updatedBlocks = pastedBlocksRef.current.map(b => b.id === block.id ? { ...b, collapsed: true } : b);
|
|
395
|
+
pastedBlocksRef.current = updatedBlocks;
|
|
396
|
+
setPastedBlocks(updatedBlocks);
|
|
251
397
|
}
|
|
252
|
-
}, [
|
|
398
|
+
}, [pastedBlocks, setOriginalInput]);
|
|
253
399
|
// Expand all placeholders for submission
|
|
400
|
+
// BUG FIX: Use ref to always get current pastedBlocks, avoiding stale closure
|
|
401
|
+
// This ensures rapid paste + submit sequences work correctly before React re-renders
|
|
254
402
|
const expandPlaceholdersForSubmit = useCallback((text) => {
|
|
255
|
-
return expandAllPlaceholders(text,
|
|
256
|
-
}, [
|
|
403
|
+
return expandAllPlaceholders(text, pastedBlocksRef.current);
|
|
404
|
+
}, []);
|
|
257
405
|
const handleSubmit = useCallback(() => {
|
|
258
|
-
// BUG FIX: Check for
|
|
406
|
+
// BUG FIX: Check for active bracketed paste mode before submitting
|
|
407
|
+
// If we're in the middle of receiving a bracketed paste, don't submit yet
|
|
408
|
+
if (bracketedPasteHandlerRef.current.isAccumulating()) {
|
|
409
|
+
// Bracketed paste is in progress - wait for it to complete
|
|
410
|
+
// The enter key will be part of the paste content
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// BUG FIX: Check for pending fallback paste accumulation before submitting
|
|
259
414
|
if (fallbackPasteBufferRef.current.length > 0) {
|
|
260
415
|
// There's accumulated paste content - flush it first
|
|
261
416
|
if (fallbackPasteTimeoutRef.current) {
|
|
@@ -276,14 +431,17 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
276
431
|
// User can hit Enter again to submit
|
|
277
432
|
return;
|
|
278
433
|
}
|
|
279
|
-
|
|
434
|
+
// BUG FIX: Use inputRef.current to get the latest input value
|
|
435
|
+
// This ensures we see updates from handlePasteComplete or other operations
|
|
436
|
+
const currentInput = inputRef.current;
|
|
437
|
+
if (currentInput.trim()) {
|
|
280
438
|
// Expand all placeholders before submission
|
|
281
|
-
const expandedInput = expandPlaceholdersForSubmit(
|
|
439
|
+
const expandedInput = expandPlaceholdersForSubmit(currentInput);
|
|
282
440
|
addToHistory(expandedInput);
|
|
283
441
|
onSubmit?.(expandedInput);
|
|
284
442
|
clearInput();
|
|
285
443
|
}
|
|
286
|
-
}, [
|
|
444
|
+
}, [addToHistory, onSubmit, clearInput, expandPlaceholdersForSubmit, handlePasteComplete]);
|
|
287
445
|
const handleInput = useCallback((inputChar, key) => {
|
|
288
446
|
if (disabled)
|
|
289
447
|
return;
|
|
@@ -292,6 +450,21 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
292
450
|
setInputState("");
|
|
293
451
|
setCursorPositionState(0);
|
|
294
452
|
setOriginalInput("");
|
|
453
|
+
// BUG FIX: Clear pasted blocks when input is cleared
|
|
454
|
+
pastedBlocksRef.current = [];
|
|
455
|
+
setPastedBlocks([]);
|
|
456
|
+
// BUG FIX: Also clear pending paste accumulation to avoid unexpected behavior
|
|
457
|
+
fallbackPasteBufferRef.current = '';
|
|
458
|
+
fallbackPasteLastChunkTimeRef.current = 0;
|
|
459
|
+
if (fallbackPasteTimeoutRef.current) {
|
|
460
|
+
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
461
|
+
fallbackPasteTimeoutRef.current = null;
|
|
462
|
+
}
|
|
463
|
+
setIsPasting(false);
|
|
464
|
+
bracketedPasteHandlerRef.current.reset();
|
|
465
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
466
|
+
inputRef.current = '';
|
|
467
|
+
cursorPositionRef.current = 0;
|
|
295
468
|
return;
|
|
296
469
|
}
|
|
297
470
|
// Handle Ctrl+P: Toggle expand/collapse for paste at cursor
|
|
@@ -314,25 +487,34 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
314
487
|
const enterBehavior = inputConfig.enterBehavior || 'submit';
|
|
315
488
|
// Check if user pressed Shift+Enter (submit key in newline mode)
|
|
316
489
|
const isShiftEnter = key.shift && key.return;
|
|
490
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
491
|
+
const currentInput = inputRef.current;
|
|
492
|
+
const currentCursor = cursorPositionRef.current;
|
|
317
493
|
if (enterBehavior === 'newline') {
|
|
318
494
|
// Newline mode: Enter inserts newline, Shift+Enter submits
|
|
319
495
|
if (isShiftEnter) {
|
|
320
496
|
handleSubmit();
|
|
321
497
|
}
|
|
322
498
|
else {
|
|
323
|
-
const result = insertText(
|
|
499
|
+
const result = insertText(currentInput, currentCursor, "\n");
|
|
324
500
|
setInputState(result.text);
|
|
325
501
|
setCursorPositionState(result.position);
|
|
326
502
|
setOriginalInput(result.text);
|
|
503
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
504
|
+
inputRef.current = result.text;
|
|
505
|
+
cursorPositionRef.current = result.position;
|
|
327
506
|
}
|
|
328
507
|
}
|
|
329
508
|
else if (enterBehavior === 'submit') {
|
|
330
509
|
// Submit mode (legacy): Enter submits, Shift+Enter inserts newline
|
|
331
510
|
if (isShiftEnter) {
|
|
332
|
-
const result = insertText(
|
|
511
|
+
const result = insertText(currentInput, currentCursor, "\n");
|
|
333
512
|
setInputState(result.text);
|
|
334
513
|
setCursorPositionState(result.position);
|
|
335
514
|
setOriginalInput(result.text);
|
|
515
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
516
|
+
inputRef.current = result.text;
|
|
517
|
+
cursorPositionRef.current = result.position;
|
|
336
518
|
}
|
|
337
519
|
else {
|
|
338
520
|
handleSubmit();
|
|
@@ -345,12 +527,15 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
345
527
|
// Explicit submit with Shift+Enter
|
|
346
528
|
handleSubmit();
|
|
347
529
|
}
|
|
348
|
-
else if (isIncompleteInput(
|
|
530
|
+
else if (isIncompleteInput(currentInput, inputConfig.smartDetection)) {
|
|
349
531
|
// Input appears incomplete, insert newline
|
|
350
|
-
const result = insertText(
|
|
532
|
+
const result = insertText(currentInput, currentCursor, "\n");
|
|
351
533
|
setInputState(result.text);
|
|
352
534
|
setCursorPositionState(result.position);
|
|
353
535
|
setOriginalInput(result.text);
|
|
536
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
537
|
+
inputRef.current = result.text;
|
|
538
|
+
cursorPositionRef.current = result.position;
|
|
354
539
|
}
|
|
355
540
|
else {
|
|
356
541
|
// Input looks complete, submit
|
|
@@ -365,6 +550,13 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
365
550
|
if (historyInput !== null) {
|
|
366
551
|
setInputState(historyInput);
|
|
367
552
|
setCursorPositionState(historyInput.length);
|
|
553
|
+
// BUG FIX: Clear pasted blocks when navigating history
|
|
554
|
+
// History entries don't have associated paste blocks
|
|
555
|
+
pastedBlocksRef.current = [];
|
|
556
|
+
setPastedBlocks([]);
|
|
557
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
558
|
+
inputRef.current = historyInput;
|
|
559
|
+
cursorPositionRef.current = historyInput.length;
|
|
368
560
|
}
|
|
369
561
|
return;
|
|
370
562
|
}
|
|
@@ -373,39 +565,64 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
373
565
|
if (historyInput !== null) {
|
|
374
566
|
setInputState(historyInput);
|
|
375
567
|
setCursorPositionState(historyInput.length);
|
|
568
|
+
// BUG FIX: Clear pasted blocks when navigating history
|
|
569
|
+
// History entries don't have associated paste blocks
|
|
570
|
+
pastedBlocksRef.current = [];
|
|
571
|
+
setPastedBlocks([]);
|
|
572
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
573
|
+
inputRef.current = historyInput;
|
|
574
|
+
cursorPositionRef.current = historyInput.length;
|
|
376
575
|
}
|
|
377
576
|
return;
|
|
378
577
|
}
|
|
379
578
|
// Handle cursor movement - ignore meta flag for arrows as it's unreliable in terminals
|
|
380
579
|
// Only do word movement if ctrl is pressed AND no arrow escape sequence is in inputChar
|
|
381
580
|
if ((key.leftArrow || key.name === 'left') && key.ctrl && !inputChar.includes('[')) {
|
|
382
|
-
|
|
581
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
582
|
+
const newPos = moveToPreviousWord(inputRef.current, cursorPositionRef.current);
|
|
383
583
|
setCursorPositionState(newPos);
|
|
584
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
585
|
+
cursorPositionRef.current = newPos;
|
|
384
586
|
return;
|
|
385
587
|
}
|
|
386
588
|
if ((key.rightArrow || key.name === 'right') && key.ctrl && !inputChar.includes('[')) {
|
|
387
|
-
|
|
589
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
590
|
+
const newPos = moveToNextWord(inputRef.current, cursorPositionRef.current);
|
|
388
591
|
setCursorPositionState(newPos);
|
|
592
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
593
|
+
cursorPositionRef.current = newPos;
|
|
389
594
|
return;
|
|
390
595
|
}
|
|
391
596
|
// Handle regular cursor movement - single character (ignore meta flag)
|
|
392
597
|
if (key.leftArrow || key.name === 'left') {
|
|
393
|
-
|
|
598
|
+
// BUG FIX: Use ref to get CURRENT cursor position, avoiding stale closure
|
|
599
|
+
const newPos = Math.max(0, cursorPositionRef.current - 1);
|
|
394
600
|
setCursorPositionState(newPos);
|
|
601
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
602
|
+
cursorPositionRef.current = newPos;
|
|
395
603
|
return;
|
|
396
604
|
}
|
|
397
605
|
if (key.rightArrow || key.name === 'right') {
|
|
398
|
-
|
|
606
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
607
|
+
const newPos = Math.min(inputRef.current.length, cursorPositionRef.current + 1);
|
|
399
608
|
setCursorPositionState(newPos);
|
|
609
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
610
|
+
cursorPositionRef.current = newPos;
|
|
400
611
|
return;
|
|
401
612
|
}
|
|
402
613
|
// Handle Home/End keys or Ctrl+A/E
|
|
403
614
|
if ((key.ctrl && inputChar === "a") || key.name === "home") {
|
|
404
615
|
setCursorPositionState(0); // Simple start of input
|
|
616
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
617
|
+
cursorPositionRef.current = 0;
|
|
405
618
|
return;
|
|
406
619
|
}
|
|
407
620
|
if ((key.ctrl && inputChar === "e") || key.name === "end") {
|
|
408
|
-
|
|
621
|
+
// BUG FIX: Use ref to get CURRENT input length, avoiding stale closure
|
|
622
|
+
const endPos = inputRef.current.length;
|
|
623
|
+
setCursorPositionState(endPos); // Simple end of input
|
|
624
|
+
// BUG FIX: Synchronously update ref to prevent stale reads
|
|
625
|
+
cursorPositionRef.current = endPos;
|
|
409
626
|
return;
|
|
410
627
|
}
|
|
411
628
|
// Handle deletion - check multiple ways backspace might be detected
|
|
@@ -417,47 +634,69 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
417
634
|
inputChar === '\x7f' ||
|
|
418
635
|
(key.delete && inputChar === '' && !key.shift);
|
|
419
636
|
if (isBackspace) {
|
|
637
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
638
|
+
const currentInput = inputRef.current;
|
|
639
|
+
const currentCursor = cursorPositionRef.current;
|
|
420
640
|
if (key.ctrl || key.meta) {
|
|
421
641
|
// Ctrl/Cmd + Backspace: Delete word before cursor
|
|
422
|
-
const result = deleteWordBefore(
|
|
642
|
+
const result = deleteWordBefore(currentInput, currentCursor);
|
|
423
643
|
setInputState(result.text);
|
|
424
644
|
setCursorPositionState(result.position);
|
|
425
645
|
setOriginalInput(result.text);
|
|
646
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
647
|
+
inputRef.current = result.text;
|
|
648
|
+
cursorPositionRef.current = result.position;
|
|
426
649
|
}
|
|
427
650
|
else {
|
|
428
651
|
// Regular backspace
|
|
429
|
-
const result = deleteCharBefore(
|
|
652
|
+
const result = deleteCharBefore(currentInput, currentCursor);
|
|
430
653
|
setInputState(result.text);
|
|
431
654
|
setCursorPositionState(result.position);
|
|
432
655
|
setOriginalInput(result.text);
|
|
656
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
657
|
+
inputRef.current = result.text;
|
|
658
|
+
cursorPositionRef.current = result.position;
|
|
433
659
|
}
|
|
434
660
|
return;
|
|
435
661
|
}
|
|
436
662
|
// Handle forward delete (Del key) - but not if it was already handled as backspace above
|
|
437
663
|
// Note: Ctrl+D is also treated as delete character (standard terminal behavior)
|
|
438
664
|
if (key.delete && inputChar !== '') {
|
|
665
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
666
|
+
const currentInput = inputRef.current;
|
|
667
|
+
const currentCursor = cursorPositionRef.current;
|
|
439
668
|
if (key.ctrl || key.meta) {
|
|
440
669
|
// Ctrl/Cmd + Delete: Delete word after cursor
|
|
441
|
-
const result = deleteWordAfter(
|
|
670
|
+
const result = deleteWordAfter(currentInput, currentCursor);
|
|
442
671
|
setInputState(result.text);
|
|
443
672
|
setCursorPositionState(result.position);
|
|
444
673
|
setOriginalInput(result.text);
|
|
674
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
675
|
+
inputRef.current = result.text;
|
|
676
|
+
cursorPositionRef.current = result.position;
|
|
445
677
|
}
|
|
446
678
|
else {
|
|
447
679
|
// Regular delete
|
|
448
|
-
const result = deleteCharAfter(
|
|
680
|
+
const result = deleteCharAfter(currentInput, currentCursor);
|
|
449
681
|
setInputState(result.text);
|
|
450
682
|
setCursorPositionState(result.position);
|
|
451
683
|
setOriginalInput(result.text);
|
|
684
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
685
|
+
inputRef.current = result.text;
|
|
686
|
+
cursorPositionRef.current = result.position;
|
|
452
687
|
}
|
|
453
688
|
return;
|
|
454
689
|
}
|
|
455
690
|
// Handle Ctrl+D: Delete character after cursor (standard terminal behavior)
|
|
456
691
|
if (key.ctrl && inputChar === "d") {
|
|
457
|
-
|
|
692
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
693
|
+
const result = deleteCharAfter(inputRef.current, cursorPositionRef.current);
|
|
458
694
|
setInputState(result.text);
|
|
459
695
|
setCursorPositionState(result.position);
|
|
460
696
|
setOriginalInput(result.text);
|
|
697
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
698
|
+
inputRef.current = result.text;
|
|
699
|
+
cursorPositionRef.current = result.position;
|
|
461
700
|
return;
|
|
462
701
|
}
|
|
463
702
|
// Handle Ctrl+K: Open quick actions menu
|
|
@@ -469,20 +708,30 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
469
708
|
// Handle Ctrl+U: Delete from cursor to start of line
|
|
470
709
|
// Check both key.ctrl with 'u' and raw ASCII code \x15 (Ctrl+U = ASCII 21)
|
|
471
710
|
if ((key.ctrl && inputChar === "u") || inputChar === "\x15") {
|
|
472
|
-
|
|
473
|
-
const
|
|
711
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
712
|
+
const currentInput = inputRef.current;
|
|
713
|
+
const currentCursor = cursorPositionRef.current;
|
|
714
|
+
const lineStart = moveToLineStart(currentInput, currentCursor);
|
|
715
|
+
const newText = currentInput.slice(0, lineStart) + currentInput.slice(currentCursor);
|
|
474
716
|
setInputState(newText);
|
|
475
717
|
setCursorPositionState(lineStart);
|
|
476
718
|
setOriginalInput(newText);
|
|
719
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
720
|
+
inputRef.current = newText;
|
|
721
|
+
cursorPositionRef.current = lineStart;
|
|
477
722
|
return;
|
|
478
723
|
}
|
|
479
724
|
// Handle Ctrl+W: Delete word before cursor
|
|
480
725
|
// Check both key.ctrl with 'w' and raw ASCII code \x17 (Ctrl+W = ASCII 23)
|
|
481
726
|
if ((key.ctrl && inputChar === "w") || inputChar === "\x17") {
|
|
482
|
-
|
|
727
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
728
|
+
const result = deleteWordBefore(inputRef.current, cursorPositionRef.current);
|
|
483
729
|
setInputState(result.text);
|
|
484
730
|
setCursorPositionState(result.position);
|
|
485
731
|
setOriginalInput(result.text);
|
|
732
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
733
|
+
inputRef.current = result.text;
|
|
734
|
+
cursorPositionRef.current = result.position;
|
|
486
735
|
return;
|
|
487
736
|
}
|
|
488
737
|
// Handle Ctrl+O: Toggle verbose mode
|
|
@@ -509,6 +758,21 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
509
758
|
setInputState("");
|
|
510
759
|
setCursorPositionState(0);
|
|
511
760
|
setOriginalInput("");
|
|
761
|
+
// BUG FIX: Clear pasted blocks when input is cleared
|
|
762
|
+
pastedBlocksRef.current = [];
|
|
763
|
+
setPastedBlocks([]);
|
|
764
|
+
// BUG FIX: Also clear pending paste accumulation to avoid unexpected behavior
|
|
765
|
+
fallbackPasteBufferRef.current = '';
|
|
766
|
+
fallbackPasteLastChunkTimeRef.current = 0;
|
|
767
|
+
if (fallbackPasteTimeoutRef.current) {
|
|
768
|
+
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
769
|
+
fallbackPasteTimeoutRef.current = null;
|
|
770
|
+
}
|
|
771
|
+
setIsPasting(false);
|
|
772
|
+
bracketedPasteHandlerRef.current.reset();
|
|
773
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
774
|
+
inputRef.current = '';
|
|
775
|
+
cursorPositionRef.current = 0;
|
|
512
776
|
return;
|
|
513
777
|
}
|
|
514
778
|
// Handle Shift+Tab: Toggle auto-accept mode (Phase 2)
|
|
@@ -519,18 +783,45 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
519
783
|
}
|
|
520
784
|
// Handle Tab (alone): Toggle thinking mode (P2.4)
|
|
521
785
|
// Only if input is empty (to avoid interfering with autocomplete)
|
|
522
|
-
|
|
786
|
+
// BUG FIX: Use inputRef.current to check current input length, avoiding stale closure
|
|
787
|
+
if (key.tab && !key.shift && !key.ctrl && !key.meta && inputRef.current.length === 0) {
|
|
523
788
|
onThinkingModeToggle?.();
|
|
524
789
|
return;
|
|
525
790
|
}
|
|
526
791
|
// Handle Ctrl+G: Open external editor (Phase 2)
|
|
527
792
|
// Check both key.ctrl with 'g' and raw ASCII code \x07 (Ctrl+G = ASCII 7)
|
|
528
793
|
if ((key.ctrl && inputChar === "g") || inputChar === "\x07") {
|
|
794
|
+
// BUG FIX: Use inputRef.current to get the latest input value
|
|
795
|
+
// This ensures the external editor receives current content even if
|
|
796
|
+
// there were rapid updates just before Ctrl+G was pressed
|
|
797
|
+
const currentInput = inputRef.current;
|
|
529
798
|
// Call async external editor handler
|
|
530
|
-
onExternalEditor?.(
|
|
799
|
+
onExternalEditor?.(currentInput).then((editedContent) => {
|
|
800
|
+
// BUG FIX: Check if component is still mounted before updating state
|
|
801
|
+
if (!isMountedRef.current)
|
|
802
|
+
return;
|
|
531
803
|
if (editedContent !== null) {
|
|
532
804
|
setInputState(editedContent);
|
|
533
805
|
setCursorPositionState(editedContent.length);
|
|
806
|
+
// BUG FIX: Also update original input for history tracking
|
|
807
|
+
setOriginalInput(editedContent);
|
|
808
|
+
// BUG FIX: Clear pasted blocks - external editor returns fully expanded content
|
|
809
|
+
// The old block metadata is no longer valid
|
|
810
|
+
pastedBlocksRef.current = [];
|
|
811
|
+
setPastedBlocks([]);
|
|
812
|
+
// BUG FIX: Clear pending paste accumulation to prevent it from overwriting
|
|
813
|
+
// the external editor content when timeout fires
|
|
814
|
+
fallbackPasteBufferRef.current = '';
|
|
815
|
+
fallbackPasteLastChunkTimeRef.current = 0;
|
|
816
|
+
if (fallbackPasteTimeoutRef.current) {
|
|
817
|
+
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
818
|
+
fallbackPasteTimeoutRef.current = null;
|
|
819
|
+
}
|
|
820
|
+
setIsPasting(false);
|
|
821
|
+
bracketedPasteHandlerRef.current.reset();
|
|
822
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
823
|
+
inputRef.current = editedContent;
|
|
824
|
+
cursorPositionRef.current = editedContent.length;
|
|
534
825
|
}
|
|
535
826
|
}).catch(() => {
|
|
536
827
|
// Ignore errors - user will see error in UI
|
|
@@ -539,7 +830,8 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
539
830
|
}
|
|
540
831
|
// Handle ? key: Show keyboard shortcuts help (P1.4)
|
|
541
832
|
// Only trigger if input is empty (avoid interfering with normal typing)
|
|
542
|
-
|
|
833
|
+
// BUG FIX: Use inputRef.current to check current input length, avoiding stale closure
|
|
834
|
+
if (inputChar === "?" && inputRef.current.length === 0) {
|
|
543
835
|
onKeyboardHelp?.();
|
|
544
836
|
return;
|
|
545
837
|
}
|
|
@@ -616,9 +908,23 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
616
908
|
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
617
909
|
fallbackPasteTimeoutRef.current = null;
|
|
618
910
|
}
|
|
619
|
-
//
|
|
911
|
+
// BUG FIX: Apply paste settings like other paste paths
|
|
912
|
+
const settingsManager = getSettingsManager();
|
|
913
|
+
const pasteSettings = settingsManager.getPasteSettings();
|
|
914
|
+
const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
|
|
915
|
+
// Check if paste exceeds warning threshold
|
|
916
|
+
if (accumulatedContent.length >= warningThreshold) {
|
|
917
|
+
onLargePaste?.(accumulatedContent.length);
|
|
918
|
+
}
|
|
919
|
+
// Handle truncation if needed
|
|
920
|
+
let finalContent = accumulatedContent;
|
|
921
|
+
if (!allowLargePaste && accumulatedContent.length > maxPasteLength) {
|
|
922
|
+
finalContent = accumulatedContent.slice(0, maxPasteLength);
|
|
923
|
+
onPasteTruncated?.(accumulatedContent.length, maxPasteLength);
|
|
924
|
+
}
|
|
925
|
+
// Process accumulated content
|
|
620
926
|
try {
|
|
621
|
-
handlePasteComplete(
|
|
927
|
+
handlePasteComplete(finalContent);
|
|
622
928
|
}
|
|
623
929
|
catch (error) {
|
|
624
930
|
console.error('Error handling oversized paste:', error);
|
|
@@ -638,6 +944,9 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
638
944
|
const timeoutMs = isActiveAccumulation && timeSinceLastChunk < 1000 ? 500 : 200;
|
|
639
945
|
// Set new timeout - if no more chunks arrive, process the paste
|
|
640
946
|
fallbackPasteTimeoutRef.current = setTimeout(() => {
|
|
947
|
+
// BUG FIX: Check if component is still mounted
|
|
948
|
+
if (!isMountedRef.current)
|
|
949
|
+
return;
|
|
641
950
|
// BUG FIX: Clear refs FIRST to prevent race conditions
|
|
642
951
|
const accumulatedContent = fallbackPasteBufferRef.current;
|
|
643
952
|
fallbackPasteBufferRef.current = '';
|
|
@@ -651,16 +960,17 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
651
960
|
const pasteSettings = settingsManager.getPasteSettings();
|
|
652
961
|
const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
|
|
653
962
|
// Check if paste exceeds warning threshold
|
|
963
|
+
// BUG FIX: Use ref to get current callback, avoiding stale closure
|
|
654
964
|
if (pastedContent.length >= warningThreshold) {
|
|
655
|
-
|
|
965
|
+
onLargePasteRef.current?.(pastedContent.length);
|
|
656
966
|
}
|
|
657
967
|
// Handle truncation if needed
|
|
658
968
|
let finalContent = pastedContent;
|
|
659
969
|
if (!allowLargePaste && pastedContent.length > maxPasteLength) {
|
|
660
970
|
// Truncate the paste
|
|
661
971
|
finalContent = pastedContent.slice(0, maxPasteLength);
|
|
662
|
-
//
|
|
663
|
-
|
|
972
|
+
// BUG FIX: Use ref to get current callback, avoiding stale closure
|
|
973
|
+
onPasteTruncatedRef.current?.(pastedContent.length, maxPasteLength);
|
|
664
974
|
}
|
|
665
975
|
// Handle entire accumulated paste at once
|
|
666
976
|
// Note: handlePasteComplete will use CURRENT input/cursor values from React state
|
|
@@ -688,21 +998,48 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
688
998
|
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
689
999
|
}
|
|
690
1000
|
fallbackPasteTimeoutRef.current = setTimeout(() => {
|
|
1001
|
+
// BUG FIX: Check if component is still mounted
|
|
1002
|
+
if (!isMountedRef.current)
|
|
1003
|
+
return;
|
|
691
1004
|
const accumulatedContent = fallbackPasteBufferRef.current;
|
|
692
1005
|
fallbackPasteBufferRef.current = '';
|
|
693
1006
|
fallbackPasteTimeoutRef.current = null;
|
|
694
1007
|
fallbackPasteLastChunkTimeRef.current = 0;
|
|
695
1008
|
if (accumulatedContent) {
|
|
696
|
-
|
|
1009
|
+
// BUG FIX: Apply paste settings (truncation/warnings) like other paste paths
|
|
1010
|
+
const settingsManager = getSettingsManager();
|
|
1011
|
+
const pasteSettings = settingsManager.getPasteSettings();
|
|
1012
|
+
const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
|
|
1013
|
+
// Check if paste exceeds warning threshold
|
|
1014
|
+
if (accumulatedContent.length >= warningThreshold) {
|
|
1015
|
+
onLargePasteRef.current?.(accumulatedContent.length);
|
|
1016
|
+
}
|
|
1017
|
+
// Handle truncation if needed
|
|
1018
|
+
let finalContent = accumulatedContent;
|
|
1019
|
+
if (!allowLargePaste && accumulatedContent.length > maxPasteLength) {
|
|
1020
|
+
finalContent = accumulatedContent.slice(0, maxPasteLength);
|
|
1021
|
+
onPasteTruncatedRef.current?.(accumulatedContent.length, maxPasteLength);
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
handlePasteComplete(finalContent);
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
// BUG FIX: Catch errors to prevent timeout callback crash
|
|
1028
|
+
console.error('Error handling accumulated paste:', error);
|
|
1029
|
+
}
|
|
697
1030
|
}
|
|
698
1031
|
}, 100); // Short timeout for single char after paste
|
|
699
1032
|
return;
|
|
700
1033
|
}
|
|
701
1034
|
// No pending paste - process normal input
|
|
702
|
-
|
|
1035
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
1036
|
+
const result2 = insertText(inputRef.current, cursorPositionRef.current, result.content);
|
|
703
1037
|
setInputState(result2.text);
|
|
704
1038
|
setCursorPositionState(result2.position);
|
|
705
1039
|
setOriginalInput(result2.text);
|
|
1040
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
1041
|
+
inputRef.current = result2.text;
|
|
1042
|
+
cursorPositionRef.current = result2.position;
|
|
706
1043
|
}
|
|
707
1044
|
}
|
|
708
1045
|
else if (enableFallback) {
|
|
@@ -728,9 +1065,23 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
728
1065
|
clearTimeout(fallbackPasteTimeoutRef.current);
|
|
729
1066
|
fallbackPasteTimeoutRef.current = null;
|
|
730
1067
|
}
|
|
731
|
-
//
|
|
1068
|
+
// BUG FIX: Apply paste settings like other paste paths
|
|
1069
|
+
const settingsManager = getSettingsManager();
|
|
1070
|
+
const pasteSettings = settingsManager.getPasteSettings();
|
|
1071
|
+
const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
|
|
1072
|
+
// Check if paste exceeds warning threshold
|
|
1073
|
+
if (accumulatedContent.length >= warningThreshold) {
|
|
1074
|
+
onLargePaste?.(accumulatedContent.length);
|
|
1075
|
+
}
|
|
1076
|
+
// Handle truncation if needed
|
|
1077
|
+
let finalContent = accumulatedContent;
|
|
1078
|
+
if (!allowLargePaste && accumulatedContent.length > maxPasteLength) {
|
|
1079
|
+
finalContent = accumulatedContent.slice(0, maxPasteLength);
|
|
1080
|
+
onPasteTruncated?.(accumulatedContent.length, maxPasteLength);
|
|
1081
|
+
}
|
|
1082
|
+
// Process accumulated content
|
|
732
1083
|
try {
|
|
733
|
-
handlePasteComplete(
|
|
1084
|
+
handlePasteComplete(finalContent);
|
|
734
1085
|
}
|
|
735
1086
|
catch (error) {
|
|
736
1087
|
console.error('Error handling oversized paste:', error);
|
|
@@ -748,6 +1099,9 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
748
1099
|
const timeoutMs = isActiveAccumulation && timeSinceLastChunk < 1000 ? 500 : 200;
|
|
749
1100
|
// Set new timeout - if no more chunks arrive, process the paste
|
|
750
1101
|
fallbackPasteTimeoutRef.current = setTimeout(() => {
|
|
1102
|
+
// BUG FIX: Check if component is still mounted
|
|
1103
|
+
if (!isMountedRef.current)
|
|
1104
|
+
return;
|
|
751
1105
|
// BUG FIX: Clear refs FIRST to prevent race conditions
|
|
752
1106
|
const accumulatedContent = fallbackPasteBufferRef.current;
|
|
753
1107
|
fallbackPasteBufferRef.current = '';
|
|
@@ -760,16 +1114,17 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
760
1114
|
const pasteSettings = settingsManager.getPasteSettings();
|
|
761
1115
|
const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
|
|
762
1116
|
// Check if paste exceeds warning threshold
|
|
1117
|
+
// BUG FIX: Use ref to get current callback, avoiding stale closure
|
|
763
1118
|
if (accumulatedContent.length >= warningThreshold) {
|
|
764
|
-
|
|
1119
|
+
onLargePasteRef.current?.(accumulatedContent.length);
|
|
765
1120
|
}
|
|
766
1121
|
// Handle truncation if needed
|
|
767
1122
|
let finalContent = accumulatedContent;
|
|
768
1123
|
if (!allowLargePaste && accumulatedContent.length > maxPasteLength) {
|
|
769
1124
|
// Truncate the paste
|
|
770
1125
|
finalContent = accumulatedContent.slice(0, maxPasteLength);
|
|
771
|
-
//
|
|
772
|
-
|
|
1126
|
+
// BUG FIX: Use ref to get current callback, avoiding stale closure
|
|
1127
|
+
onPasteTruncatedRef.current?.(accumulatedContent.length, maxPasteLength);
|
|
773
1128
|
}
|
|
774
1129
|
// Handle entire accumulated paste at once
|
|
775
1130
|
// Note: handlePasteComplete will use CURRENT input/cursor values from React state
|
|
@@ -787,21 +1142,29 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
787
1142
|
}
|
|
788
1143
|
else {
|
|
789
1144
|
// Normal single character input
|
|
790
|
-
|
|
1145
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
1146
|
+
const result = insertText(inputRef.current, cursorPositionRef.current, inputChar);
|
|
791
1147
|
setInputState(result.text);
|
|
792
1148
|
setCursorPositionState(result.position);
|
|
793
1149
|
setOriginalInput(result.text);
|
|
1150
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
1151
|
+
inputRef.current = result.text;
|
|
1152
|
+
cursorPositionRef.current = result.position;
|
|
794
1153
|
}
|
|
795
1154
|
}
|
|
796
1155
|
else {
|
|
797
1156
|
// Both bracketed paste and fallback disabled - normal input only
|
|
798
|
-
|
|
1157
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
1158
|
+
const result = insertText(inputRef.current, cursorPositionRef.current, inputChar);
|
|
799
1159
|
setInputState(result.text);
|
|
800
1160
|
setCursorPositionState(result.position);
|
|
801
1161
|
setOriginalInput(result.text);
|
|
1162
|
+
// BUG FIX: Synchronously update refs to prevent stale reads
|
|
1163
|
+
inputRef.current = result.text;
|
|
1164
|
+
cursorPositionRef.current = result.position;
|
|
802
1165
|
}
|
|
803
1166
|
}
|
|
804
|
-
}, [disabled, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, onAutoAcceptToggle, onThinkingModeToggle, onKeyboardHelp, onLargePaste, onPasteTruncated,
|
|
1167
|
+
}, [disabled, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, onAutoAcceptToggle, onThinkingModeToggle, onKeyboardHelp, onLargePaste, onPasteTruncated, onExternalEditor, onEscape, inputConfig, multiline, handleSubmit, navigateHistory, setOriginalInput, toggleBlockAtCursor, handlePasteComplete, pasteConfig, isPasting]);
|
|
805
1168
|
// Update current block at cursor when cursor position or input changes
|
|
806
1169
|
useEffect(() => {
|
|
807
1170
|
const block = findBlockAtCursor(input, cursorPosition, pastedBlocks);
|
|
@@ -809,7 +1172,10 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
|
|
|
809
1172
|
}, [input, cursorPosition, pastedBlocks]);
|
|
810
1173
|
// BUG FIX: Comprehensive cleanup on unmount
|
|
811
1174
|
useEffect(() => {
|
|
1175
|
+
isMountedRef.current = true;
|
|
812
1176
|
return () => {
|
|
1177
|
+
// BUG FIX: Set mounted flag to false first to prevent async state updates
|
|
1178
|
+
isMountedRef.current = false;
|
|
813
1179
|
// Clear all timeouts
|
|
814
1180
|
if (pasteTimeoutRef.current) {
|
|
815
1181
|
clearTimeout(pasteTimeoutRef.current);
|