@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.
@@ -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 (char === '(')
26
- brackets['(']++;
27
- else if (char === ')')
28
- brackets['(']--;
29
- else if (char === '[')
30
- brackets['[']++;
31
- else if (char === ']')
32
- brackets['[']--;
33
- else if (char === '{')
34
- brackets['{']++;
35
- else if (char === '}')
36
- brackets['{']--;
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 (const op of trailingOperators) {
51
- if (trimmed.endsWith(op)) {
52
- return true;
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
- const words = lastLine.trim().split(/\s+/);
67
- const lastWord = words[words.length - 1];
68
- if (incompleteKeywords.includes(lastWord)) {
69
- return true;
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
- // Check for statement keywords at start of last line without closing
72
- const firstWord = words[0];
73
- if (incompleteKeywords.includes(firstWord) && !lastLine.includes('{') && !lastLine.includes(';')) {
74
- return true;
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
- setCursorPositionState((currentCursor) => Math.min(text.length, currentCursor));
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
- // Set cursor position with bounds checking based on current input length
116
- // Use separate state reads to avoid nested state updates
117
- setInputState((currentInput) => {
118
- const boundedPosition = Math.max(0, Math.min(currentInput.length, position));
119
- // Schedule cursor update after current state update completes
120
- queueMicrotask(() => {
121
- setCursorPositionState(boundedPosition);
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
- const result = insertText(input, cursorPosition, text);
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
- }, [input, cursorPosition, setOriginalInput]);
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
- setPastedBlocks(prev => [...prev, block]);
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
- const block = findBlockAtCursor(input, cursorPosition, pastedBlocks);
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 < input.length) {
199
- const occurrenceStart = input.indexOf(placeholder, searchStart);
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 (cursorPosition >= occurrenceStart && cursorPosition <= occurrenceEnd) {
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 = input.substring(0, targetStart) +
344
+ const newInput = currentInput.substring(0, targetStart) +
213
345
  block.content +
214
- input.substring(targetStart + placeholder.length);
346
+ currentInput.substring(targetStart + placeholder.length);
215
347
  setInputState(newInput);
216
348
  // Keep cursor at same position or adjust if needed
217
- const newCursor = cursorPosition + (block.content.length - placeholder.length);
218
- setCursorPositionState(Math.min(newInput.length, newCursor));
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
- setPastedBlocks(prev => prev.map(b => (b.id === block.id ? { ...b, collapsed: false } : b)));
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 < input.length) {
229
- const occurrenceStart = input.indexOf(block.content, searchStart);
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 (cursorPosition >= occurrenceStart && cursorPosition <= occurrenceEnd) {
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 = input.substring(0, targetStart) +
381
+ const newInput = currentInput.substring(0, targetStart) +
243
382
  placeholder +
244
- input.substring(targetStart + block.content.length);
383
+ currentInput.substring(targetStart + block.content.length);
245
384
  setInputState(newInput);
246
385
  // Adjust cursor to end of placeholder
247
- setCursorPositionState(targetStart + placeholder.length);
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
- setPastedBlocks(prev => prev.map(b => (b.id === block.id ? { ...b, collapsed: true } : b)));
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
- }, [input, cursorPosition, pastedBlocks, setOriginalInput]);
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, pastedBlocks);
256
- }, [pastedBlocks]);
403
+ return expandAllPlaceholders(text, pastedBlocksRef.current);
404
+ }, []);
257
405
  const handleSubmit = useCallback(() => {
258
- // BUG FIX: Check for pending paste accumulation before submitting
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
- if (input.trim()) {
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(input);
439
+ const expandedInput = expandPlaceholdersForSubmit(currentInput);
282
440
  addToHistory(expandedInput);
283
441
  onSubmit?.(expandedInput);
284
442
  clearInput();
285
443
  }
286
- }, [input, addToHistory, onSubmit, clearInput, expandPlaceholdersForSubmit, handlePasteComplete]);
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(input, cursorPosition, "\n");
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(input, cursorPosition, "\n");
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(input, inputConfig.smartDetection)) {
530
+ else if (isIncompleteInput(currentInput, inputConfig.smartDetection)) {
349
531
  // Input appears incomplete, insert newline
350
- const result = insertText(input, cursorPosition, "\n");
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
- const newPos = moveToPreviousWord(input, cursorPosition);
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
- const newPos = moveToNextWord(input, cursorPosition);
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
- const newPos = Math.max(0, cursorPosition - 1);
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
- const newPos = Math.min(input.length, cursorPosition + 1);
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
- setCursorPositionState(input.length); // Simple end of input
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(input, cursorPosition);
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(input, cursorPosition);
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(input, cursorPosition);
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(input, cursorPosition);
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
- const result = deleteCharAfter(input, cursorPosition);
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
- const lineStart = moveToLineStart(input, cursorPosition);
473
- const newText = input.slice(0, lineStart) + input.slice(cursorPosition);
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
- const result = deleteWordBefore(input, cursorPosition);
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
- if (key.tab && !key.shift && !key.ctrl && !key.meta && input.length === 0) {
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?.(input).then((editedContent) => {
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
- if (inputChar === "?" && input.length === 0) {
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
- // Process accumulated content (truncated)
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(accumulatedContent);
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
- onLargePaste?.(pastedContent.length);
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
- // Notify about truncation
663
- onPasteTruncated?.(pastedContent.length, maxPasteLength);
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
- handlePasteComplete(accumulatedContent);
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
- const result2 = insertText(input, cursorPosition, result.content);
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
- // Process accumulated content (truncated)
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(accumulatedContent);
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
- onLargePaste?.(accumulatedContent.length);
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
- // Notify about truncation
772
- onPasteTruncated?.(accumulatedContent.length, maxPasteLength);
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
- const result = insertText(input, cursorPosition, inputChar);
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
- const result = insertText(input, cursorPosition, inputChar);
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, input, cursorPosition, multiline, handleSubmit, navigateHistory, setOriginalInput, toggleBlockAtCursor, handlePasteComplete, pasteConfig, isPasting]);
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);