@defai.digital/ax-cli 3.7.2 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +148 -53
  2. package/dist/agent/context-manager.d.ts +15 -1
  3. package/dist/agent/context-manager.js +50 -19
  4. package/dist/agent/context-manager.js.map +1 -1
  5. package/dist/agent/dependency-resolver.js +13 -7
  6. package/dist/agent/dependency-resolver.js.map +1 -1
  7. package/dist/agent/llm-agent.d.ts +37 -0
  8. package/dist/agent/llm-agent.js +318 -98
  9. package/dist/agent/llm-agent.js.map +1 -1
  10. package/dist/agent/status-reporter.d.ts +114 -0
  11. package/dist/agent/status-reporter.js +335 -0
  12. package/dist/agent/status-reporter.js.map +1 -0
  13. package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js +8 -2
  14. package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js.map +1 -1
  15. package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js +3 -1
  16. package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js.map +1 -1
  17. package/dist/analyzers/best-practices/rules/typescript/prefer-const.js +3 -1
  18. package/dist/analyzers/best-practices/rules/typescript/prefer-const.js.map +1 -1
  19. package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js +3 -1
  20. package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js.map +1 -1
  21. package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js +9 -3
  22. package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js.map +1 -1
  23. package/dist/analyzers/git/churn-calculator.d.ts +2 -0
  24. package/dist/analyzers/git/churn-calculator.js +42 -8
  25. package/dist/analyzers/git/churn-calculator.js.map +1 -1
  26. package/dist/analyzers/git/hotspot-detector.js +2 -2
  27. package/dist/analyzers/git/hotspot-detector.js.map +1 -1
  28. package/dist/analyzers/metrics/metrics-analyzer.js +1 -1
  29. package/dist/analyzers/metrics/metrics-analyzer.js.map +1 -1
  30. package/dist/analyzers/security/security-analyzer.js +1 -1
  31. package/dist/analyzers/security/security-analyzer.js.map +1 -1
  32. package/dist/checkpoint/manager.d.ts +1 -0
  33. package/dist/checkpoint/manager.js +49 -9
  34. package/dist/checkpoint/manager.js.map +1 -1
  35. package/dist/checkpoint/storage.js +2 -2
  36. package/dist/checkpoint/storage.js.map +1 -1
  37. package/dist/commands/mcp-migrate.d.ts +9 -0
  38. package/dist/commands/mcp-migrate.js +172 -0
  39. package/dist/commands/mcp-migrate.js.map +1 -0
  40. package/dist/commands/status.d.ts +7 -0
  41. package/dist/commands/status.js +211 -0
  42. package/dist/commands/status.js.map +1 -0
  43. package/dist/commands/vscode.d.ts +7 -0
  44. package/dist/commands/vscode.js +363 -0
  45. package/dist/commands/vscode.js.map +1 -0
  46. package/dist/index.js +79 -30
  47. package/dist/index.js.map +1 -1
  48. package/dist/llm/client.js +33 -4
  49. package/dist/llm/client.js.map +1 -1
  50. package/dist/mcp/automatosx-loader.d.ts +84 -0
  51. package/dist/mcp/automatosx-loader.js +238 -0
  52. package/dist/mcp/automatosx-loader.js.map +1 -0
  53. package/dist/mcp/client-mutex-patch.d.ts +36 -0
  54. package/dist/mcp/client-mutex-patch.js +75 -0
  55. package/dist/mcp/client-mutex-patch.js.map +1 -0
  56. package/dist/mcp/client-v2.d.ts +229 -0
  57. package/dist/mcp/client-v2.js +740 -0
  58. package/dist/mcp/client-v2.js.map +1 -0
  59. package/dist/mcp/client.d.ts +111 -13
  60. package/dist/mcp/client.js +168 -253
  61. package/dist/mcp/client.js.map +1 -1
  62. package/dist/mcp/config-detector-v2.d.ts +83 -0
  63. package/dist/mcp/config-detector-v2.js +328 -0
  64. package/dist/mcp/config-detector-v2.js.map +1 -0
  65. package/dist/mcp/config-detector.d.ts +90 -0
  66. package/dist/mcp/config-detector.js +242 -0
  67. package/dist/mcp/config-detector.js.map +1 -0
  68. package/dist/mcp/config-migrator-v2.d.ts +89 -0
  69. package/dist/mcp/config-migrator-v2.js +288 -0
  70. package/dist/mcp/config-migrator-v2.js.map +1 -0
  71. package/dist/mcp/config-migrator.d.ts +63 -0
  72. package/dist/mcp/config-migrator.js +269 -0
  73. package/dist/mcp/config-migrator.js.map +1 -0
  74. package/dist/mcp/config-v2.d.ts +106 -0
  75. package/dist/mcp/config-v2.js +417 -0
  76. package/dist/mcp/config-v2.js.map +1 -0
  77. package/dist/mcp/config.d.ts +12 -1
  78. package/dist/mcp/config.js +95 -10
  79. package/dist/mcp/config.js.map +1 -1
  80. package/dist/mcp/error-formatter.d.ts +46 -0
  81. package/dist/mcp/error-formatter.js +244 -0
  82. package/dist/mcp/error-formatter.js.map +1 -0
  83. package/dist/mcp/health.d.ts +5 -0
  84. package/dist/mcp/health.js +22 -2
  85. package/dist/mcp/health.js.map +1 -1
  86. package/dist/mcp/invariants.d.ts +141 -0
  87. package/dist/mcp/invariants.js +243 -0
  88. package/dist/mcp/invariants.js.map +1 -0
  89. package/dist/mcp/mutex-safe.d.ts +153 -0
  90. package/dist/mcp/mutex-safe.js +260 -0
  91. package/dist/mcp/mutex-safe.js.map +1 -0
  92. package/dist/mcp/mutex.d.ts +73 -0
  93. package/dist/mcp/mutex.js +137 -0
  94. package/dist/mcp/mutex.js.map +1 -0
  95. package/dist/mcp/reconnection.d.ts +4 -0
  96. package/dist/mcp/reconnection.js +25 -1
  97. package/dist/mcp/reconnection.js.map +1 -1
  98. package/dist/mcp/transports-v2.d.ts +152 -0
  99. package/dist/mcp/transports-v2.js +481 -0
  100. package/dist/mcp/transports-v2.js.map +1 -0
  101. package/dist/mcp/type-safety.d.ts +231 -0
  102. package/dist/mcp/type-safety.js +273 -0
  103. package/dist/mcp/type-safety.js.map +1 -0
  104. package/dist/planner/task-planner.js +13 -0
  105. package/dist/planner/task-planner.js.map +1 -1
  106. package/dist/planner/types.d.ts +6 -6
  107. package/dist/schemas/confirmation-schemas.d.ts +2 -2
  108. package/dist/schemas/settings-schemas.d.ts +196 -0
  109. package/dist/schemas/settings-schemas.js +146 -5
  110. package/dist/schemas/settings-schemas.js.map +1 -1
  111. package/dist/sdk/index.d.ts +118 -2
  112. package/dist/sdk/index.js +146 -4
  113. package/dist/sdk/index.js.map +1 -1
  114. package/dist/sdk/testing.d.ts +182 -0
  115. package/dist/sdk/testing.js +231 -0
  116. package/dist/sdk/testing.js.map +1 -1
  117. package/dist/sdk/version.d.ts +114 -15
  118. package/dist/sdk/version.js +137 -15
  119. package/dist/sdk/version.js.map +1 -1
  120. package/dist/tools/bash.js +54 -9
  121. package/dist/tools/bash.js.map +1 -1
  122. package/dist/tools/registry.d.ts +146 -0
  123. package/dist/tools/registry.js +170 -0
  124. package/dist/tools/registry.js.map +1 -0
  125. package/dist/tools/search.js +12 -2
  126. package/dist/tools/search.js.map +1 -1
  127. package/dist/tools/text-editor.js +84 -26
  128. package/dist/tools/text-editor.js.map +1 -1
  129. package/dist/ui/components/chat-history.js +6 -1
  130. package/dist/ui/components/chat-history.js.map +1 -1
  131. package/dist/ui/components/chat-input.d.ts +2 -1
  132. package/dist/ui/components/chat-input.js +5 -2
  133. package/dist/ui/components/chat-input.js.map +1 -1
  134. package/dist/ui/components/chat-interface.js +187 -5
  135. package/dist/ui/components/chat-interface.js.map +1 -1
  136. package/dist/ui/components/context-breakdown.d.ts +23 -0
  137. package/dist/ui/components/context-breakdown.js +124 -0
  138. package/dist/ui/components/context-breakdown.js.map +1 -0
  139. package/dist/ui/components/keyboard-help.d.ts +17 -0
  140. package/dist/ui/components/keyboard-help.js +116 -0
  141. package/dist/ui/components/keyboard-help.js.map +1 -0
  142. package/dist/ui/components/keyboard-hints.js +2 -2
  143. package/dist/ui/components/keyboard-hints.js.map +1 -1
  144. package/dist/ui/components/quick-actions.js +43 -7
  145. package/dist/ui/components/quick-actions.js.map +1 -1
  146. package/dist/ui/components/status-bar.d.ts +3 -0
  147. package/dist/ui/components/status-bar.js +25 -16
  148. package/dist/ui/components/status-bar.js.map +1 -1
  149. package/dist/ui/components/toast-notification.d.ts +42 -0
  150. package/dist/ui/components/toast-notification.js +30 -2
  151. package/dist/ui/components/toast-notification.js.map +1 -1
  152. package/dist/ui/components/tool-group-display.js +34 -4
  153. package/dist/ui/components/tool-group-display.js.map +1 -1
  154. package/dist/ui/components/welcome-panel.js +2 -2
  155. package/dist/ui/components/welcome-panel.js.map +1 -1
  156. package/dist/ui/hooks/use-enhanced-input.d.ts +9 -1
  157. package/dist/ui/hooks/use-enhanced-input.js +901 -90
  158. package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
  159. package/dist/ui/hooks/use-input-handler.d.ts +11 -1
  160. package/dist/ui/hooks/use-input-handler.js +67 -3
  161. package/dist/ui/hooks/use-input-handler.js.map +1 -1
  162. package/dist/ui/hooks/use-input-history.d.ts +1 -1
  163. package/dist/ui/hooks/use-input-history.js +50 -14
  164. package/dist/ui/hooks/use-input-history.js.map +1 -1
  165. package/dist/ui/utils/bracketed-paste-handler.d.ts +97 -0
  166. package/dist/ui/utils/bracketed-paste-handler.js +322 -0
  167. package/dist/ui/utils/bracketed-paste-handler.js.map +1 -0
  168. package/dist/ui/utils/change-summarizer.js +16 -6
  169. package/dist/ui/utils/change-summarizer.js.map +1 -1
  170. package/dist/ui/utils/tool-grouper.d.ts +10 -1
  171. package/dist/ui/utils/tool-grouper.js +143 -30
  172. package/dist/ui/utils/tool-grouper.js.map +1 -1
  173. package/dist/utils/auto-accept-logger.d.ts +173 -0
  174. package/dist/utils/auto-accept-logger.js +420 -0
  175. package/dist/utils/auto-accept-logger.js.map +1 -0
  176. package/dist/utils/background-task-manager.d.ts +11 -0
  177. package/dist/utils/background-task-manager.js +124 -38
  178. package/dist/utils/background-task-manager.js.map +1 -1
  179. package/dist/utils/confirmation-service.d.ts +1 -0
  180. package/dist/utils/confirmation-service.js +6 -1
  181. package/dist/utils/confirmation-service.js.map +1 -1
  182. package/dist/utils/encryption.d.ts +8 -0
  183. package/dist/utils/encryption.js +44 -27
  184. package/dist/utils/encryption.js.map +1 -1
  185. package/dist/utils/enhanced-error-messages.d.ts +33 -0
  186. package/dist/utils/enhanced-error-messages.js +420 -0
  187. package/dist/utils/enhanced-error-messages.js.map +1 -0
  188. package/dist/utils/error-handler.d.ts +13 -3
  189. package/dist/utils/error-handler.js +16 -4
  190. package/dist/utils/error-handler.js.map +1 -1
  191. package/dist/utils/external-editor.d.ts +47 -0
  192. package/dist/utils/external-editor.js +179 -0
  193. package/dist/utils/external-editor.js.map +1 -0
  194. package/dist/utils/history-migration.d.ts +9 -0
  195. package/dist/utils/history-migration.js +36 -0
  196. package/dist/utils/history-migration.js.map +1 -0
  197. package/dist/utils/paste-utils.js +12 -11
  198. package/dist/utils/paste-utils.js.map +1 -1
  199. package/dist/utils/rate-limiter.js +20 -1
  200. package/dist/utils/rate-limiter.js.map +1 -1
  201. package/dist/utils/safety-rules.d.ts +64 -0
  202. package/dist/utils/safety-rules.js +225 -0
  203. package/dist/utils/safety-rules.js.map +1 -0
  204. package/dist/utils/settings-manager.d.ts +89 -1
  205. package/dist/utils/settings-manager.js +359 -3
  206. package/dist/utils/settings-manager.js.map +1 -1
  207. package/dist/utils/token-counter.d.ts +2 -0
  208. package/dist/utils/token-counter.js +32 -9
  209. package/dist/utils/token-counter.js.map +1 -1
  210. package/dist/utils/version.d.ts +11 -2
  211. package/dist/utils/version.js +54 -21
  212. package/dist/utils/version.js.map +1 -1
  213. package/package.json +2 -1
@@ -2,78 +2,323 @@ import { useState, useCallback, useRef, useEffect } from "react";
2
2
  import { deleteCharBefore, deleteCharAfter, deleteWordBefore, deleteWordAfter, insertText, moveToLineStart, moveToPreviousWord, moveToNextWord, } from "../../utils/text-utils.js";
3
3
  import { useInputHistory } from "./use-input-history.js";
4
4
  import { PasteDetector, shouldCollapsePaste, createPastedBlock, generatePlaceholder, findBlockAtCursor, expandAllPlaceholders, } from "../../utils/paste-utils.js";
5
- export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, disabled = false, multiline = false, } = {}) {
5
+ import { getSettingsManager } from "../../utils/settings-manager.js";
6
+ import { BracketedPasteHandler } from "../utils/bracketed-paste-handler.js";
7
+ /**
8
+ * Check if input appears incomplete and should not be submitted
9
+ * Used for smart mode to auto-insert newlines for incomplete input
10
+ */
11
+ function isIncompleteInput(text, smartDetection) {
12
+ if (!smartDetection?.enabled)
13
+ return false;
14
+ const trimmed = text.trimEnd();
15
+ if (!trimmed)
16
+ return false;
17
+ // Check for unclosed brackets
18
+ if (smartDetection.checkBrackets) {
19
+ const brackets = {
20
+ '(': 0,
21
+ '[': 0,
22
+ '{': 0,
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 = '';
29
+ for (const char of trimmed) {
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;
61
+ }
62
+ // If any bracket is unclosed, input is incomplete
63
+ if (brackets['('] > 0 || brackets['['] > 0 || brackets['{'] > 0) {
64
+ return true;
65
+ }
66
+ }
67
+ // Check for trailing operators
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 "="
71
+ const trailingOperators = [
72
+ '===', '!==', '...', '&&', '||', '==', '!=', '<=', '>=', '=>',
73
+ '..', '+', '-', '*', '/', '%', '=', '<', '>', '&', '|', '^',
74
+ '?', ':', ',', '.',
75
+ ];
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
+ }
100
+ }
101
+ }
102
+ }
103
+ // Check for incomplete statements
104
+ if (smartDetection.checkStatements) {
105
+ const incompleteKeywords = [
106
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
107
+ 'function', 'const', 'let', 'var', 'class', 'interface',
108
+ 'type', 'enum', 'import', 'export', 'return', 'throw',
109
+ 'try', 'catch', 'finally', 'async', 'await',
110
+ ];
111
+ // Check if line ends with a statement keyword (potentially incomplete)
112
+ const lastLine = trimmed.split('\n').pop() || '';
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 !== '\\' ? '\\' : '';
128
+ }
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
+ }
163
+ }
164
+ }
165
+ return false;
166
+ }
167
+ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, onAutoAcceptToggle, onThinkingModeToggle, onExternalEditor, onLargePaste, onPasteTruncated, onKeyboardHelp, projectDir, disabled = false, multiline = false, } = {}) {
6
168
  const [input, setInputState] = useState("");
7
169
  const [cursorPosition, setCursorPositionState] = useState(0);
8
170
  const [pastedBlocks, setPastedBlocks] = useState([]);
9
- const [pasteCounter, setPasteCounter] = useState(0);
171
+ // BUG FIX: Use ref instead of state to prevent race conditions with concurrent pastes
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);
10
176
  const [currentBlockAtCursor, setCurrentBlockAtCursor] = useState(null);
177
+ const [isPasting, setIsPasting] = useState(false); // v3.8.0: Track paste accumulation state
178
+ // Load input configuration from settings (Phase 1) // FIX: Use RequiredInputSettings type to match getInputConfig() return type
179
+ const [inputConfig] = useState(() => {
180
+ return getSettingsManager().getInputConfig();
181
+ });
182
+ // Load paste configuration from settings (v3.8.0)
183
+ const [pasteConfig] = useState(() => {
184
+ return getSettingsManager().getPasteSettings();
185
+ });
11
186
  const isMultilineRef = useRef(multiline);
12
187
  const pasteDetectorRef = useRef(new PasteDetector());
13
188
  const pasteTimeoutRef = useRef(null);
189
+ const bracketedPasteHandlerRef = useRef(new BracketedPasteHandler()); // v3.8.0: Bracketed paste mode handler
190
+ // v3.8.0: Fallback paste accumulation buffer (for terminals that send paste in chunks)
191
+ const fallbackPasteBufferRef = useRef('');
192
+ const fallbackPasteTimeoutRef = useRef(null);
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);
14
199
  // Keep ref in sync with prop to avoid stale closure
15
200
  isMultilineRef.current = multiline;
16
- const { addToHistory, navigateHistory, resetHistory, setOriginalInput, isNavigatingHistory, } = useInputHistory();
201
+ onLargePasteRef.current = onLargePaste;
202
+ onPasteTruncatedRef.current = onPasteTruncated;
203
+ const { addToHistory, navigateHistory, resetHistory, setOriginalInput, isNavigatingHistory, } = useInputHistory(projectDir);
17
204
  const setInput = useCallback((text) => {
18
205
  setInputState(text);
19
206
  // Use functional update to get the current cursor position, avoiding stale closure
20
- setCursorPositionState((currentCursor) => Math.min(text.length, currentCursor));
207
+ const newCursor = Math.min(text.length, cursorPositionRef.current);
208
+ setCursorPositionState(newCursor);
21
209
  if (!isNavigatingHistory()) {
22
210
  setOriginalInput(text);
23
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;
24
219
  }, [isNavigatingHistory, setOriginalInput]);
25
220
  const setCursorPosition = useCallback((position) => {
26
- // Set cursor position with bounds checking based on current input length
27
- // Use separate state reads to avoid nested state updates
28
- setInputState((currentInput) => {
29
- const boundedPosition = Math.max(0, Math.min(currentInput.length, position));
30
- // Schedule cursor update after current state update completes
31
- queueMicrotask(() => {
32
- setCursorPositionState(boundedPosition);
33
- });
34
- return currentInput; // No change to input, just accessing for bounds check
35
- });
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;
36
228
  }, []);
37
229
  const clearInput = useCallback(() => {
38
230
  setInputState("");
39
231
  setCursorPositionState(0);
40
232
  setOriginalInput("");
233
+ pastedBlocksRef.current = [];
41
234
  setPastedBlocks([]);
42
- setPasteCounter(0);
235
+ pasteCounterRef.current = 0; // BUG FIX: Reset counter ref
236
+ setIsPasting(false); // v3.8.0: Reset paste state
43
237
  pasteDetectorRef.current.reset();
238
+ bracketedPasteHandlerRef.current.reset(); // v3.8.0: Reset bracketed paste handler
239
+ // v3.8.0: Clear fallback paste buffer and timeout
240
+ fallbackPasteBufferRef.current = '';
241
+ fallbackPasteLastChunkTimeRef.current = 0;
242
+ if (fallbackPasteTimeoutRef.current) {
243
+ clearTimeout(fallbackPasteTimeoutRef.current);
244
+ fallbackPasteTimeoutRef.current = null;
245
+ }
246
+ // BUG FIX: Synchronously update refs to prevent stale reads
247
+ inputRef.current = '';
248
+ cursorPositionRef.current = 0;
44
249
  }, [setOriginalInput]);
45
250
  const insertAtCursor = useCallback((text) => {
46
- 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);
47
257
  setInputState(result.text);
48
258
  setCursorPositionState(result.position);
49
259
  setOriginalInput(result.text);
50
- }, [input, cursorPosition, setOriginalInput]);
51
- // Handle paste completion (after accumulation timeout)
260
+ // BUG FIX: Synchronously update refs to prevent stale reads
261
+ inputRef.current = result.text;
262
+ cursorPositionRef.current = result.position;
263
+ }, [setOriginalInput]);
264
+ // Handle paste completion
265
+ // Note: No timeout or accumulation needed - Ink batches the entire paste for us
266
+ // BUG FIX: Create refs to track latest values without causing re-renders
267
+ const inputRef = useRef(input);
268
+ const cursorPositionRef = useRef(cursorPosition);
269
+ // Keep refs in sync with state
270
+ useEffect(() => {
271
+ inputRef.current = input;
272
+ cursorPositionRef.current = cursorPosition;
273
+ }, [input, cursorPosition]);
274
+ // BUG FIX: Keep pastedBlocksRef in sync with state
275
+ useEffect(() => {
276
+ pastedBlocksRef.current = pastedBlocks;
277
+ }, [pastedBlocks]);
52
278
  const handlePasteComplete = useCallback((pastedContent) => {
53
- // Check if should collapse
279
+ // BUG FIX: Use refs to get CURRENT values, avoiding stale closures
280
+ const currentInput = inputRef.current;
281
+ const currentCursor = cursorPositionRef.current;
282
+ // Preserve all formatting - no trimming or normalization
283
+ // This ensures JSON indentation, newlines, and whitespace are intact
284
+ // Check if should collapse based on line count or character count
54
285
  if (shouldCollapsePaste(pastedContent)) {
55
- // Create pasted block
56
- const block = createPastedBlock(pasteCounter, pastedContent, cursorPosition);
57
- setPasteCounter(prev => prev + 1);
58
- setPastedBlocks(prev => [...prev, block]);
286
+ // BUG FIX: Use ref and increment immediately to prevent race conditions
287
+ const blockId = pasteCounterRef.current++;
288
+ // Create pasted block with CURRENT cursor position
289
+ const block = createPastedBlock(blockId, pastedContent, currentCursor);
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);
59
295
  // Insert placeholder instead of full content
60
296
  const placeholder = generatePlaceholder(block);
61
- const result = insertText(input, cursorPosition, placeholder);
297
+ const result = insertText(currentInput, currentCursor, placeholder);
62
298
  setInputState(result.text);
63
299
  setCursorPositionState(result.position);
64
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;
65
304
  }
66
305
  else {
67
- // Insert normally (below threshold)
68
- const result = insertText(input, cursorPosition, pastedContent);
306
+ // Insert normally (below threshold) with all formatting preserved
307
+ const result = insertText(currentInput, currentCursor, pastedContent);
69
308
  setInputState(result.text);
70
309
  setCursorPositionState(result.position);
71
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;
72
314
  }
73
- }, [input, cursorPosition, pasteCounter, setOriginalInput]);
315
+ }, [setOriginalInput]);
74
316
  // Toggle collapse/expand for block at cursor
75
317
  const toggleBlockAtCursor = useCallback(() => {
76
- 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);
77
322
  if (!block)
78
323
  return;
79
324
  const placeholder = generatePlaceholder(block);
@@ -82,12 +327,12 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
82
327
  let searchStart = 0;
83
328
  let targetStart = -1;
84
329
  // Find the occurrence that contains the cursor
85
- while (searchStart < input.length) {
86
- const occurrenceStart = input.indexOf(placeholder, searchStart);
330
+ while (searchStart < currentInput.length) {
331
+ const occurrenceStart = currentInput.indexOf(placeholder, searchStart);
87
332
  if (occurrenceStart === -1)
88
333
  break;
89
334
  const occurrenceEnd = occurrenceStart + placeholder.length;
90
- if (cursorPosition >= occurrenceStart && cursorPosition <= occurrenceEnd) {
335
+ if (currentCursor >= occurrenceStart && currentCursor <= occurrenceEnd) {
91
336
  targetStart = occurrenceStart;
92
337
  break;
93
338
  }
@@ -96,28 +341,35 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
96
341
  if (targetStart === -1)
97
342
  return; // Should not happen
98
343
  // Replace only this specific occurrence
99
- const newInput = input.substring(0, targetStart) +
344
+ const newInput = currentInput.substring(0, targetStart) +
100
345
  block.content +
101
- input.substring(targetStart + placeholder.length);
346
+ currentInput.substring(targetStart + placeholder.length);
102
347
  setInputState(newInput);
103
348
  // Keep cursor at same position or adjust if needed
104
- const newCursor = cursorPosition + (block.content.length - placeholder.length);
105
- 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);
106
352
  setOriginalInput(newInput);
353
+ // BUG FIX: Synchronously update refs to prevent stale reads
354
+ inputRef.current = newInput;
355
+ cursorPositionRef.current = boundedCursor;
107
356
  // Update block state
108
- 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);
109
361
  }
110
362
  else {
111
363
  // Collapse: find the specific occurrence near cursor and replace it
112
364
  let searchStart = 0;
113
365
  let targetStart = -1;
114
366
  // Find the occurrence that contains the cursor
115
- while (searchStart < input.length) {
116
- const occurrenceStart = input.indexOf(block.content, searchStart);
367
+ while (searchStart < currentInput.length) {
368
+ const occurrenceStart = currentInput.indexOf(block.content, searchStart);
117
369
  if (occurrenceStart === -1)
118
370
  break;
119
371
  const occurrenceEnd = occurrenceStart + block.content.length;
120
- if (cursorPosition >= occurrenceStart && cursorPosition <= occurrenceEnd) {
372
+ if (currentCursor >= occurrenceStart && currentCursor <= occurrenceEnd) {
121
373
  targetStart = occurrenceStart;
122
374
  break;
123
375
  }
@@ -126,30 +378,70 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
126
378
  if (targetStart === -1)
127
379
  return; // Should not happen
128
380
  // Replace only this specific occurrence
129
- const newInput = input.substring(0, targetStart) +
381
+ const newInput = currentInput.substring(0, targetStart) +
130
382
  placeholder +
131
- input.substring(targetStart + block.content.length);
383
+ currentInput.substring(targetStart + block.content.length);
132
384
  setInputState(newInput);
133
385
  // Adjust cursor to end of placeholder
134
- setCursorPositionState(targetStart + placeholder.length);
386
+ const newCursor = targetStart + placeholder.length;
387
+ setCursorPositionState(newCursor);
135
388
  setOriginalInput(newInput);
389
+ // BUG FIX: Synchronously update refs to prevent stale reads
390
+ inputRef.current = newInput;
391
+ cursorPositionRef.current = newCursor;
136
392
  // Update block state
137
- 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);
138
397
  }
139
- }, [input, cursorPosition, pastedBlocks, setOriginalInput]);
398
+ }, [pastedBlocks, setOriginalInput]);
140
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
141
402
  const expandPlaceholdersForSubmit = useCallback((text) => {
142
- return expandAllPlaceholders(text, pastedBlocks);
143
- }, [pastedBlocks]);
403
+ return expandAllPlaceholders(text, pastedBlocksRef.current);
404
+ }, []);
144
405
  const handleSubmit = useCallback(() => {
145
- if (input.trim()) {
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
414
+ if (fallbackPasteBufferRef.current.length > 0) {
415
+ // There's accumulated paste content - flush it first
416
+ if (fallbackPasteTimeoutRef.current) {
417
+ clearTimeout(fallbackPasteTimeoutRef.current);
418
+ fallbackPasteTimeoutRef.current = null;
419
+ }
420
+ const accumulatedPaste = fallbackPasteBufferRef.current;
421
+ fallbackPasteBufferRef.current = '';
422
+ fallbackPasteLastChunkTimeRef.current = 0;
423
+ // Add accumulated paste to input before submitting
424
+ try {
425
+ handlePasteComplete(accumulatedPaste);
426
+ }
427
+ catch (error) {
428
+ console.error('Error flushing paste on submit:', error);
429
+ }
430
+ // Don't submit yet - let the paste complete first
431
+ // User can hit Enter again to submit
432
+ return;
433
+ }
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()) {
146
438
  // Expand all placeholders before submission
147
- const expandedInput = expandPlaceholdersForSubmit(input);
439
+ const expandedInput = expandPlaceholdersForSubmit(currentInput);
148
440
  addToHistory(expandedInput);
149
441
  onSubmit?.(expandedInput);
150
442
  clearInput();
151
443
  }
152
- }, [input, addToHistory, onSubmit, clearInput, expandPlaceholdersForSubmit]);
444
+ }, [addToHistory, onSubmit, clearInput, expandPlaceholdersForSubmit, handlePasteComplete]);
153
445
  const handleInput = useCallback((inputChar, key) => {
154
446
  if (disabled)
155
447
  return;
@@ -158,6 +450,21 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
158
450
  setInputState("");
159
451
  setCursorPositionState(0);
160
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;
161
468
  return;
162
469
  }
163
470
  // Handle Ctrl+P: Toggle expand/collapse for paste at cursor
@@ -175,17 +482,65 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
175
482
  onEscape?.();
176
483
  return;
177
484
  }
178
- // Handle Enter/Return
485
+ // Handle Enter/Return - Phase 1: Configurable multi-line input
179
486
  if (key.return) {
180
- if (isMultilineRef.current && key.shift) {
181
- // Shift+Enter in multiline mode inserts newline
182
- const result = insertText(input, cursorPosition, "\n");
183
- setInputState(result.text);
184
- setCursorPositionState(result.position);
185
- setOriginalInput(result.text);
487
+ const enterBehavior = inputConfig.enterBehavior || 'submit';
488
+ // Check if user pressed Shift+Enter (submit key in newline mode)
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;
493
+ if (enterBehavior === 'newline') {
494
+ // Newline mode: Enter inserts newline, Shift+Enter submits
495
+ if (isShiftEnter) {
496
+ handleSubmit();
497
+ }
498
+ else {
499
+ const result = insertText(currentInput, currentCursor, "\n");
500
+ setInputState(result.text);
501
+ setCursorPositionState(result.position);
502
+ setOriginalInput(result.text);
503
+ // BUG FIX: Synchronously update refs to prevent stale reads
504
+ inputRef.current = result.text;
505
+ cursorPositionRef.current = result.position;
506
+ }
186
507
  }
187
- else {
188
- handleSubmit();
508
+ else if (enterBehavior === 'submit') {
509
+ // Submit mode (legacy): Enter submits, Shift+Enter inserts newline
510
+ if (isShiftEnter) {
511
+ const result = insertText(currentInput, currentCursor, "\n");
512
+ setInputState(result.text);
513
+ setCursorPositionState(result.position);
514
+ setOriginalInput(result.text);
515
+ // BUG FIX: Synchronously update refs to prevent stale reads
516
+ inputRef.current = result.text;
517
+ cursorPositionRef.current = result.position;
518
+ }
519
+ else {
520
+ handleSubmit();
521
+ }
522
+ }
523
+ else if (enterBehavior === 'smart') {
524
+ // Smart mode: Auto-detect incomplete input
525
+ // Shift+Enter always submits, otherwise check if input is incomplete
526
+ if (isShiftEnter) {
527
+ // Explicit submit with Shift+Enter
528
+ handleSubmit();
529
+ }
530
+ else if (isIncompleteInput(currentInput, inputConfig.smartDetection)) {
531
+ // Input appears incomplete, insert newline
532
+ const result = insertText(currentInput, currentCursor, "\n");
533
+ setInputState(result.text);
534
+ setCursorPositionState(result.position);
535
+ setOriginalInput(result.text);
536
+ // BUG FIX: Synchronously update refs to prevent stale reads
537
+ inputRef.current = result.text;
538
+ cursorPositionRef.current = result.position;
539
+ }
540
+ else {
541
+ // Input looks complete, submit
542
+ handleSubmit();
543
+ }
189
544
  }
190
545
  return;
191
546
  }
@@ -195,6 +550,13 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
195
550
  if (historyInput !== null) {
196
551
  setInputState(historyInput);
197
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;
198
560
  }
199
561
  return;
200
562
  }
@@ -203,39 +565,64 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
203
565
  if (historyInput !== null) {
204
566
  setInputState(historyInput);
205
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;
206
575
  }
207
576
  return;
208
577
  }
209
578
  // Handle cursor movement - ignore meta flag for arrows as it's unreliable in terminals
210
579
  // Only do word movement if ctrl is pressed AND no arrow escape sequence is in inputChar
211
580
  if ((key.leftArrow || key.name === 'left') && key.ctrl && !inputChar.includes('[')) {
212
- 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);
213
583
  setCursorPositionState(newPos);
584
+ // BUG FIX: Synchronously update ref to prevent stale reads
585
+ cursorPositionRef.current = newPos;
214
586
  return;
215
587
  }
216
588
  if ((key.rightArrow || key.name === 'right') && key.ctrl && !inputChar.includes('[')) {
217
- 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);
218
591
  setCursorPositionState(newPos);
592
+ // BUG FIX: Synchronously update ref to prevent stale reads
593
+ cursorPositionRef.current = newPos;
219
594
  return;
220
595
  }
221
596
  // Handle regular cursor movement - single character (ignore meta flag)
222
597
  if (key.leftArrow || key.name === 'left') {
223
- 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);
224
600
  setCursorPositionState(newPos);
601
+ // BUG FIX: Synchronously update ref to prevent stale reads
602
+ cursorPositionRef.current = newPos;
225
603
  return;
226
604
  }
227
605
  if (key.rightArrow || key.name === 'right') {
228
- 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);
229
608
  setCursorPositionState(newPos);
609
+ // BUG FIX: Synchronously update ref to prevent stale reads
610
+ cursorPositionRef.current = newPos;
230
611
  return;
231
612
  }
232
613
  // Handle Home/End keys or Ctrl+A/E
233
614
  if ((key.ctrl && inputChar === "a") || key.name === "home") {
234
615
  setCursorPositionState(0); // Simple start of input
616
+ // BUG FIX: Synchronously update ref to prevent stale reads
617
+ cursorPositionRef.current = 0;
235
618
  return;
236
619
  }
237
620
  if ((key.ctrl && inputChar === "e") || key.name === "end") {
238
- 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;
239
626
  return;
240
627
  }
241
628
  // Handle deletion - check multiple ways backspace might be detected
@@ -247,47 +634,69 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
247
634
  inputChar === '\x7f' ||
248
635
  (key.delete && inputChar === '' && !key.shift);
249
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;
250
640
  if (key.ctrl || key.meta) {
251
641
  // Ctrl/Cmd + Backspace: Delete word before cursor
252
- const result = deleteWordBefore(input, cursorPosition);
642
+ const result = deleteWordBefore(currentInput, currentCursor);
253
643
  setInputState(result.text);
254
644
  setCursorPositionState(result.position);
255
645
  setOriginalInput(result.text);
646
+ // BUG FIX: Synchronously update refs to prevent stale reads
647
+ inputRef.current = result.text;
648
+ cursorPositionRef.current = result.position;
256
649
  }
257
650
  else {
258
651
  // Regular backspace
259
- const result = deleteCharBefore(input, cursorPosition);
652
+ const result = deleteCharBefore(currentInput, currentCursor);
260
653
  setInputState(result.text);
261
654
  setCursorPositionState(result.position);
262
655
  setOriginalInput(result.text);
656
+ // BUG FIX: Synchronously update refs to prevent stale reads
657
+ inputRef.current = result.text;
658
+ cursorPositionRef.current = result.position;
263
659
  }
264
660
  return;
265
661
  }
266
662
  // Handle forward delete (Del key) - but not if it was already handled as backspace above
267
663
  // Note: Ctrl+D is also treated as delete character (standard terminal behavior)
268
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;
269
668
  if (key.ctrl || key.meta) {
270
669
  // Ctrl/Cmd + Delete: Delete word after cursor
271
- const result = deleteWordAfter(input, cursorPosition);
670
+ const result = deleteWordAfter(currentInput, currentCursor);
272
671
  setInputState(result.text);
273
672
  setCursorPositionState(result.position);
274
673
  setOriginalInput(result.text);
674
+ // BUG FIX: Synchronously update refs to prevent stale reads
675
+ inputRef.current = result.text;
676
+ cursorPositionRef.current = result.position;
275
677
  }
276
678
  else {
277
679
  // Regular delete
278
- const result = deleteCharAfter(input, cursorPosition);
680
+ const result = deleteCharAfter(currentInput, currentCursor);
279
681
  setInputState(result.text);
280
682
  setCursorPositionState(result.position);
281
683
  setOriginalInput(result.text);
684
+ // BUG FIX: Synchronously update refs to prevent stale reads
685
+ inputRef.current = result.text;
686
+ cursorPositionRef.current = result.position;
282
687
  }
283
688
  return;
284
689
  }
285
690
  // Handle Ctrl+D: Delete character after cursor (standard terminal behavior)
286
691
  if (key.ctrl && inputChar === "d") {
287
- 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);
288
694
  setInputState(result.text);
289
695
  setCursorPositionState(result.position);
290
696
  setOriginalInput(result.text);
697
+ // BUG FIX: Synchronously update refs to prevent stale reads
698
+ inputRef.current = result.text;
699
+ cursorPositionRef.current = result.position;
291
700
  return;
292
701
  }
293
702
  // Handle Ctrl+K: Open quick actions menu
@@ -299,20 +708,30 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
299
708
  // Handle Ctrl+U: Delete from cursor to start of line
300
709
  // Check both key.ctrl with 'u' and raw ASCII code \x15 (Ctrl+U = ASCII 21)
301
710
  if ((key.ctrl && inputChar === "u") || inputChar === "\x15") {
302
- const lineStart = moveToLineStart(input, cursorPosition);
303
- 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);
304
716
  setInputState(newText);
305
717
  setCursorPositionState(lineStart);
306
718
  setOriginalInput(newText);
719
+ // BUG FIX: Synchronously update refs to prevent stale reads
720
+ inputRef.current = newText;
721
+ cursorPositionRef.current = lineStart;
307
722
  return;
308
723
  }
309
724
  // Handle Ctrl+W: Delete word before cursor
310
725
  // Check both key.ctrl with 'w' and raw ASCII code \x17 (Ctrl+W = ASCII 23)
311
726
  if ((key.ctrl && inputChar === "w") || inputChar === "\x17") {
312
- 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);
313
729
  setInputState(result.text);
314
730
  setCursorPositionState(result.position);
315
731
  setOriginalInput(result.text);
732
+ // BUG FIX: Synchronously update refs to prevent stale reads
733
+ inputRef.current = result.text;
734
+ cursorPositionRef.current = result.position;
316
735
  return;
317
736
  }
318
737
  // Handle Ctrl+O: Toggle verbose mode
@@ -339,48 +758,439 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
339
758
  setInputState("");
340
759
  setCursorPositionState(0);
341
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;
776
+ return;
777
+ }
778
+ // Handle Shift+Tab: Toggle auto-accept mode (Phase 2)
779
+ // Check for Shift+Tab combination
780
+ if (key.shift && key.tab) {
781
+ onAutoAcceptToggle?.();
782
+ return;
783
+ }
784
+ // Handle Tab (alone): Toggle thinking mode (P2.4)
785
+ // Only if input is empty (to avoid interfering with autocomplete)
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) {
788
+ onThinkingModeToggle?.();
789
+ return;
790
+ }
791
+ // Handle Ctrl+G: Open external editor (Phase 2)
792
+ // Check both key.ctrl with 'g' and raw ASCII code \x07 (Ctrl+G = ASCII 7)
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;
798
+ // Call async external editor handler
799
+ onExternalEditor?.(currentInput).then((editedContent) => {
800
+ // BUG FIX: Check if component is still mounted before updating state
801
+ if (!isMountedRef.current)
802
+ return;
803
+ if (editedContent !== null) {
804
+ setInputState(editedContent);
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;
825
+ }
826
+ }).catch(() => {
827
+ // Ignore errors - user will see error in UI
828
+ });
829
+ return;
830
+ }
831
+ // Handle ? key: Show keyboard shortcuts help (P1.4)
832
+ // Only trigger if input is empty (avoid interfering with normal typing)
833
+ // BUG FIX: Use inputRef.current to check current input length, avoiding stale closure
834
+ if (inputChar === "?" && inputRef.current.length === 0) {
835
+ onKeyboardHelp?.();
342
836
  return;
343
837
  }
344
838
  // Handle regular character input
345
839
  if (inputChar && !key.ctrl && !key.meta) {
346
- // Detect paste operation
347
- const isPaste = pasteDetectorRef.current.detectPaste(inputChar);
348
- if (isPaste) {
349
- // Clear any existing paste timeout
350
- if (pasteTimeoutRef.current) {
351
- clearTimeout(pasteTimeoutRef.current);
840
+ // v3.8.0: Bracketed Paste Mode Detection
841
+ // Uses industry-standard escape sequences for reliable paste detection
842
+ // Falls back to simple batched detection if not supported
843
+ const { enableBracketedPaste, enableFallback } = pasteConfig;
844
+ // Use bracketed paste handler if enabled
845
+ if (enableBracketedPaste) {
846
+ // BUG FIX: Check for orphaned content from timeout first
847
+ const orphanedContent = bracketedPasteHandlerRef.current.retrieveOrphanedContent();
848
+ if (orphanedContent) {
849
+ // Timeout occurred and we have accumulated content without end marker
850
+ // Process it as a paste
851
+ try {
852
+ handlePasteComplete(orphanedContent);
853
+ }
854
+ catch (error) {
855
+ console.error('Error handling orphaned paste:', error);
856
+ // Continue despite error - don't block current input
857
+ }
858
+ // Continue processing current input normally
859
+ }
860
+ const result = bracketedPasteHandlerRef.current.handleInput(inputChar);
861
+ // Update pasting state for visual indicator
862
+ if (result.isAccumulating !== isPasting) {
863
+ setIsPasting(result.isAccumulating);
864
+ }
865
+ // If still accumulating, don't process yet
866
+ if (result.isAccumulating) {
867
+ return;
868
+ }
869
+ // If paste detected and complete via bracketed paste mode
870
+ if (result.isPaste && result.content) {
871
+ const pastedContent = result.content;
872
+ // Phase 3: Large paste handling with truncation
873
+ const settingsManager = getSettingsManager();
874
+ const pasteSettings = settingsManager.getPasteSettings();
875
+ const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
876
+ // Check if paste exceeds warning threshold
877
+ if (pastedContent.length >= warningThreshold) {
878
+ onLargePaste?.(pastedContent.length);
879
+ }
880
+ // Handle truncation if needed
881
+ let finalContent = pastedContent;
882
+ if (!allowLargePaste && pastedContent.length > maxPasteLength) {
883
+ // Truncate the paste
884
+ finalContent = pastedContent.slice(0, maxPasteLength);
885
+ // Notify about truncation
886
+ onPasteTruncated?.(pastedContent.length, maxPasteLength);
887
+ }
888
+ // Handle entire paste at once
889
+ handlePasteComplete(finalContent);
890
+ return;
352
891
  }
353
- // Accumulate paste input
354
- pasteDetectorRef.current.accumulatePasteInput(inputChar);
355
- // Set timeout to finalize paste (50ms after last input)
356
- pasteTimeoutRef.current = setTimeout(() => {
357
- const accumulated = pasteDetectorRef.current.getAccumulatedInput();
358
- if (accumulated) {
359
- handlePasteComplete(accumulated);
892
+ // Not detected via bracketed paste mode
893
+ // If fallback enabled AND looks like batched paste, accumulate chunks
894
+ if (enableFallback && result.content && result.content.length > 1) {
895
+ // BUG FIX: Accumulate chunks with timeout instead of immediate processing
896
+ // This prevents multiple blocks when paste arrives in chunks (SSH, tmux, etc)
897
+ const now = Date.now();
898
+ const isActiveAccumulation = fallbackPasteBufferRef.current.length > 0;
899
+ const timeSinceLastChunk = now - fallbackPasteLastChunkTimeRef.current;
900
+ // BUG FIX: Check buffer size to prevent overflow (100MB limit)
901
+ const MAX_BUFFER_SIZE = 100 * 1024 * 1024;
902
+ if (fallbackPasteBufferRef.current.length + result.content.length > MAX_BUFFER_SIZE) {
903
+ // Buffer overflow - process what we have immediately
904
+ const accumulatedContent = fallbackPasteBufferRef.current;
905
+ fallbackPasteBufferRef.current = '';
906
+ fallbackPasteLastChunkTimeRef.current = 0;
907
+ if (fallbackPasteTimeoutRef.current) {
908
+ clearTimeout(fallbackPasteTimeoutRef.current);
909
+ fallbackPasteTimeoutRef.current = null;
910
+ }
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
926
+ try {
927
+ handlePasteComplete(finalContent);
928
+ }
929
+ catch (error) {
930
+ console.error('Error handling oversized paste:', error);
931
+ }
932
+ return;
360
933
  }
361
- pasteTimeoutRef.current = null;
362
- }, 50);
934
+ // Add to accumulation buffer
935
+ fallbackPasteBufferRef.current += result.content;
936
+ fallbackPasteLastChunkTimeRef.current = now;
937
+ // Clear existing timeout
938
+ if (fallbackPasteTimeoutRef.current) {
939
+ clearTimeout(fallbackPasteTimeoutRef.current);
940
+ }
941
+ // Dynamic timeout: Use longer window if we're actively accumulating a paste
942
+ // Initial chunk: 200ms (fast for single-burst pastes)
943
+ // Subsequent chunks: 500ms (handles slow terminals/networks)
944
+ const timeoutMs = isActiveAccumulation && timeSinceLastChunk < 1000 ? 500 : 200;
945
+ // Set new timeout - if no more chunks arrive, process the paste
946
+ fallbackPasteTimeoutRef.current = setTimeout(() => {
947
+ // BUG FIX: Check if component is still mounted
948
+ if (!isMountedRef.current)
949
+ return;
950
+ // BUG FIX: Clear refs FIRST to prevent race conditions
951
+ const accumulatedContent = fallbackPasteBufferRef.current;
952
+ fallbackPasteBufferRef.current = '';
953
+ fallbackPasteLastChunkTimeRef.current = 0;
954
+ fallbackPasteTimeoutRef.current = null;
955
+ if (!accumulatedContent)
956
+ return;
957
+ const pastedContent = accumulatedContent.replace(/\r/g, '\n');
958
+ // Phase 3: Large paste handling with truncation
959
+ const settingsManager = getSettingsManager();
960
+ const pasteSettings = settingsManager.getPasteSettings();
961
+ const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
962
+ // Check if paste exceeds warning threshold
963
+ // BUG FIX: Use ref to get current callback, avoiding stale closure
964
+ if (pastedContent.length >= warningThreshold) {
965
+ onLargePasteRef.current?.(pastedContent.length);
966
+ }
967
+ // Handle truncation if needed
968
+ let finalContent = pastedContent;
969
+ if (!allowLargePaste && pastedContent.length > maxPasteLength) {
970
+ // Truncate the paste
971
+ finalContent = pastedContent.slice(0, maxPasteLength);
972
+ // BUG FIX: Use ref to get current callback, avoiding stale closure
973
+ onPasteTruncatedRef.current?.(pastedContent.length, maxPasteLength);
974
+ }
975
+ // Handle entire accumulated paste at once
976
+ // Note: handlePasteComplete will use CURRENT input/cursor values from React state
977
+ try {
978
+ handlePasteComplete(finalContent);
979
+ }
980
+ catch (error) {
981
+ // BUG FIX: Catch errors to prevent timeout callback crash
982
+ console.error('Error handling paste:', error);
983
+ // Don't attempt fallback - state might be inconsistent
984
+ // The paste content is lost but system remains stable
985
+ }
986
+ }, timeoutMs); // Dynamic timeout: 200ms initial, 500ms for active accumulation
987
+ return;
988
+ }
989
+ // Normal single character input
990
+ if (result.content) {
991
+ // BUG FIX: If there's a pending paste accumulation, add character to it
992
+ // This prevents splitting user input into paste + character
993
+ if (fallbackPasteBufferRef.current.length > 0) {
994
+ // Add single character to accumulation buffer
995
+ fallbackPasteBufferRef.current += result.content;
996
+ // Extend timeout slightly (paste likely complete, but give it 100ms more)
997
+ if (fallbackPasteTimeoutRef.current) {
998
+ clearTimeout(fallbackPasteTimeoutRef.current);
999
+ }
1000
+ fallbackPasteTimeoutRef.current = setTimeout(() => {
1001
+ // BUG FIX: Check if component is still mounted
1002
+ if (!isMountedRef.current)
1003
+ return;
1004
+ const accumulatedContent = fallbackPasteBufferRef.current;
1005
+ fallbackPasteBufferRef.current = '';
1006
+ fallbackPasteTimeoutRef.current = null;
1007
+ fallbackPasteLastChunkTimeRef.current = 0;
1008
+ if (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
+ }
1030
+ }
1031
+ }, 100); // Short timeout for single char after paste
1032
+ return;
1033
+ }
1034
+ // No pending paste - process normal input
1035
+ // BUG FIX: Use refs to get CURRENT values, avoiding stale closures
1036
+ const result2 = insertText(inputRef.current, cursorPositionRef.current, result.content);
1037
+ setInputState(result2.text);
1038
+ setCursorPositionState(result2.position);
1039
+ setOriginalInput(result2.text);
1040
+ // BUG FIX: Synchronously update refs to prevent stale reads
1041
+ inputRef.current = result2.text;
1042
+ cursorPositionRef.current = result2.position;
1043
+ }
1044
+ }
1045
+ else if (enableFallback) {
1046
+ // Fallback: Simple batched detection (legacy behavior when bracketed paste disabled)
1047
+ // When inputChar.length > 1, it's likely pasted content
1048
+ const isPaste = inputChar.length > 1;
1049
+ if (isPaste) {
1050
+ // BUG FIX: Use same dynamic accumulation as bracketed paste fallback
1051
+ // This ensures consistent behavior regardless of bracketed paste support
1052
+ const now = Date.now();
1053
+ const isActiveAccumulation = fallbackPasteBufferRef.current.length > 0;
1054
+ const timeSinceLastChunk = now - fallbackPasteLastChunkTimeRef.current;
1055
+ // Normalize line endings: convert \r to \n
1056
+ const normalizedInput = inputChar.replace(/\r/g, '\n');
1057
+ // BUG FIX: Check buffer size to prevent overflow (100MB limit)
1058
+ const MAX_BUFFER_SIZE = 100 * 1024 * 1024;
1059
+ if (fallbackPasteBufferRef.current.length + normalizedInput.length > MAX_BUFFER_SIZE) {
1060
+ // Buffer overflow - process what we have immediately
1061
+ const accumulatedContent = fallbackPasteBufferRef.current;
1062
+ fallbackPasteBufferRef.current = '';
1063
+ fallbackPasteLastChunkTimeRef.current = 0;
1064
+ if (fallbackPasteTimeoutRef.current) {
1065
+ clearTimeout(fallbackPasteTimeoutRef.current);
1066
+ fallbackPasteTimeoutRef.current = null;
1067
+ }
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
1083
+ try {
1084
+ handlePasteComplete(finalContent);
1085
+ }
1086
+ catch (error) {
1087
+ console.error('Error handling oversized paste:', error);
1088
+ }
1089
+ return;
1090
+ }
1091
+ // Add to accumulation buffer
1092
+ fallbackPasteBufferRef.current += normalizedInput;
1093
+ fallbackPasteLastChunkTimeRef.current = now;
1094
+ // Clear existing timeout
1095
+ if (fallbackPasteTimeoutRef.current) {
1096
+ clearTimeout(fallbackPasteTimeoutRef.current);
1097
+ }
1098
+ // Dynamic timeout: Use longer window if we're actively accumulating a paste
1099
+ const timeoutMs = isActiveAccumulation && timeSinceLastChunk < 1000 ? 500 : 200;
1100
+ // Set new timeout - if no more chunks arrive, process the paste
1101
+ fallbackPasteTimeoutRef.current = setTimeout(() => {
1102
+ // BUG FIX: Check if component is still mounted
1103
+ if (!isMountedRef.current)
1104
+ return;
1105
+ // BUG FIX: Clear refs FIRST to prevent race conditions
1106
+ const accumulatedContent = fallbackPasteBufferRef.current;
1107
+ fallbackPasteBufferRef.current = '';
1108
+ fallbackPasteLastChunkTimeRef.current = 0;
1109
+ fallbackPasteTimeoutRef.current = null;
1110
+ if (!accumulatedContent)
1111
+ return;
1112
+ // Phase 3: Large paste handling with truncation
1113
+ const settingsManager = getSettingsManager();
1114
+ const pasteSettings = settingsManager.getPasteSettings();
1115
+ const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
1116
+ // Check if paste exceeds warning threshold
1117
+ // BUG FIX: Use ref to get current callback, avoiding stale closure
1118
+ if (accumulatedContent.length >= warningThreshold) {
1119
+ onLargePasteRef.current?.(accumulatedContent.length);
1120
+ }
1121
+ // Handle truncation if needed
1122
+ let finalContent = accumulatedContent;
1123
+ if (!allowLargePaste && accumulatedContent.length > maxPasteLength) {
1124
+ // Truncate the paste
1125
+ finalContent = accumulatedContent.slice(0, maxPasteLength);
1126
+ // BUG FIX: Use ref to get current callback, avoiding stale closure
1127
+ onPasteTruncatedRef.current?.(accumulatedContent.length, maxPasteLength);
1128
+ }
1129
+ // Handle entire accumulated paste at once
1130
+ // Note: handlePasteComplete will use CURRENT input/cursor values from React state
1131
+ try {
1132
+ handlePasteComplete(finalContent);
1133
+ }
1134
+ catch (error) {
1135
+ // BUG FIX: Catch errors to prevent timeout callback crash
1136
+ console.error('Error handling paste:', error);
1137
+ // Don't attempt fallback - state might be inconsistent
1138
+ // The paste content is lost but system remains stable
1139
+ }
1140
+ }, timeoutMs);
1141
+ return;
1142
+ }
1143
+ else {
1144
+ // Normal single character input
1145
+ // BUG FIX: Use refs to get CURRENT values, avoiding stale closures
1146
+ const result = insertText(inputRef.current, cursorPositionRef.current, inputChar);
1147
+ setInputState(result.text);
1148
+ setCursorPositionState(result.position);
1149
+ setOriginalInput(result.text);
1150
+ // BUG FIX: Synchronously update refs to prevent stale reads
1151
+ inputRef.current = result.text;
1152
+ cursorPositionRef.current = result.position;
1153
+ }
363
1154
  }
364
1155
  else {
365
- // Normal character input
366
- const result = insertText(input, cursorPosition, inputChar);
1156
+ // Both bracketed paste and fallback disabled - normal input only
1157
+ // BUG FIX: Use refs to get CURRENT values, avoiding stale closures
1158
+ const result = insertText(inputRef.current, cursorPositionRef.current, inputChar);
367
1159
  setInputState(result.text);
368
1160
  setCursorPositionState(result.position);
369
1161
  setOriginalInput(result.text);
1162
+ // BUG FIX: Synchronously update refs to prevent stale reads
1163
+ inputRef.current = result.text;
1164
+ cursorPositionRef.current = result.position;
370
1165
  }
371
1166
  }
372
- }, [disabled, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, input, cursorPosition, multiline, handleSubmit, navigateHistory, setOriginalInput, toggleBlockAtCursor, handlePasteComplete]);
1167
+ }, [disabled, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, onAutoAcceptToggle, onThinkingModeToggle, onKeyboardHelp, onLargePaste, onPasteTruncated, onExternalEditor, onEscape, inputConfig, multiline, handleSubmit, navigateHistory, setOriginalInput, toggleBlockAtCursor, handlePasteComplete, pasteConfig, isPasting]);
373
1168
  // Update current block at cursor when cursor position or input changes
374
1169
  useEffect(() => {
375
1170
  const block = findBlockAtCursor(input, cursorPosition, pastedBlocks);
376
1171
  setCurrentBlockAtCursor(block);
377
1172
  }, [input, cursorPosition, pastedBlocks]);
378
- // Cleanup paste timeout on unmount
1173
+ // BUG FIX: Comprehensive cleanup on unmount
379
1174
  useEffect(() => {
1175
+ isMountedRef.current = true;
380
1176
  return () => {
1177
+ // BUG FIX: Set mounted flag to false first to prevent async state updates
1178
+ isMountedRef.current = false;
1179
+ // Clear all timeouts
381
1180
  if (pasteTimeoutRef.current) {
382
1181
  clearTimeout(pasteTimeoutRef.current);
1182
+ pasteTimeoutRef.current = null;
1183
+ }
1184
+ if (fallbackPasteTimeoutRef.current) {
1185
+ clearTimeout(fallbackPasteTimeoutRef.current);
1186
+ fallbackPasteTimeoutRef.current = null;
383
1187
  }
1188
+ // Reset all detectors and handlers
1189
+ pasteDetectorRef.current.reset();
1190
+ bracketedPasteHandlerRef.current.dispose();
1191
+ // Clear buffers
1192
+ fallbackPasteBufferRef.current = '';
1193
+ fallbackPasteLastChunkTimeRef.current = 0;
384
1194
  };
385
1195
  }, []);
386
1196
  return {
@@ -389,6 +1199,7 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
389
1199
  isMultiline: isMultilineRef.current,
390
1200
  pastedBlocks,
391
1201
  currentBlockAtCursor,
1202
+ isPasting, // v3.8.0: Expose paste accumulation state for visual indicator
392
1203
  setInput,
393
1204
  setCursorPosition,
394
1205
  clearInput,