@defai.digital/ax-cli 3.7.2 → 3.8.1

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 +128 -56
  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 +35 -0
  8. package/dist/agent/llm-agent.js +137 -4
  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 +1 -0
  24. package/dist/analyzers/git/churn-calculator.js +25 -6
  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 +22 -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 +130 -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 +15 -0
  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 +486 -41
  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 +7 -0
  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 +17 -4
  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,18 +2,107 @@ 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
+ 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['{']--;
37
+ }
38
+ // If any bracket is unclosed, input is incomplete
39
+ if (brackets['('] > 0 || brackets['['] > 0 || brackets['{'] > 0) {
40
+ return true;
41
+ }
42
+ }
43
+ // Check for trailing operators
44
+ if (smartDetection.checkOperators) {
45
+ const trailingOperators = [
46
+ '+', '-', '*', '/', '%', '=', '==', '===', '!=', '!==',
47
+ '<', '>', '<=', '>=', '&&', '||', '&', '|', '^',
48
+ '?', ':', ',', '.', '..', '...', '=>',
49
+ ];
50
+ for (const op of trailingOperators) {
51
+ if (trimmed.endsWith(op)) {
52
+ return true;
53
+ }
54
+ }
55
+ }
56
+ // Check for incomplete statements
57
+ if (smartDetection.checkStatements) {
58
+ const incompleteKeywords = [
59
+ 'if', 'else', 'for', 'while', 'do', 'switch', 'case',
60
+ 'function', 'const', 'let', 'var', 'class', 'interface',
61
+ 'type', 'enum', 'import', 'export', 'return', 'throw',
62
+ 'try', 'catch', 'finally', 'async', 'await',
63
+ ];
64
+ // Check if line ends with a statement keyword (potentially incomplete)
65
+ 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;
70
+ }
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;
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, onAutoAcceptToggle, onThinkingModeToggle, onExternalEditor, onLargePaste, onPasteTruncated, onKeyboardHelp, projectDir, disabled = false, multiline = false, } = {}) {
6
80
  const [input, setInputState] = useState("");
7
81
  const [cursorPosition, setCursorPositionState] = useState(0);
8
82
  const [pastedBlocks, setPastedBlocks] = useState([]);
9
- const [pasteCounter, setPasteCounter] = useState(0);
83
+ // BUG FIX: Use ref instead of state to prevent race conditions with concurrent pastes
84
+ const pasteCounterRef = useRef(0);
10
85
  const [currentBlockAtCursor, setCurrentBlockAtCursor] = useState(null);
86
+ const [isPasting, setIsPasting] = useState(false); // v3.8.0: Track paste accumulation state
87
+ // Load input configuration from settings (Phase 1) // FIX: Use RequiredInputSettings type to match getInputConfig() return type
88
+ const [inputConfig] = useState(() => {
89
+ return getSettingsManager().getInputConfig();
90
+ });
91
+ // Load paste configuration from settings (v3.8.0)
92
+ const [pasteConfig] = useState(() => {
93
+ return getSettingsManager().getPasteSettings();
94
+ });
11
95
  const isMultilineRef = useRef(multiline);
12
96
  const pasteDetectorRef = useRef(new PasteDetector());
13
97
  const pasteTimeoutRef = useRef(null);
98
+ const bracketedPasteHandlerRef = useRef(new BracketedPasteHandler()); // v3.8.0: Bracketed paste mode handler
99
+ // v3.8.0: Fallback paste accumulation buffer (for terminals that send paste in chunks)
100
+ const fallbackPasteBufferRef = useRef('');
101
+ const fallbackPasteTimeoutRef = useRef(null);
102
+ const fallbackPasteLastChunkTimeRef = useRef(0); // Track when last chunk arrived
14
103
  // Keep ref in sync with prop to avoid stale closure
15
104
  isMultilineRef.current = multiline;
16
- const { addToHistory, navigateHistory, resetHistory, setOriginalInput, isNavigatingHistory, } = useInputHistory();
105
+ const { addToHistory, navigateHistory, resetHistory, setOriginalInput, isNavigatingHistory, } = useInputHistory(projectDir);
17
106
  const setInput = useCallback((text) => {
18
107
  setInputState(text);
19
108
  // Use functional update to get the current cursor position, avoiding stale closure
@@ -39,8 +128,17 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
39
128
  setCursorPositionState(0);
40
129
  setOriginalInput("");
41
130
  setPastedBlocks([]);
42
- setPasteCounter(0);
131
+ pasteCounterRef.current = 0; // BUG FIX: Reset counter ref
132
+ setIsPasting(false); // v3.8.0: Reset paste state
43
133
  pasteDetectorRef.current.reset();
134
+ bracketedPasteHandlerRef.current.reset(); // v3.8.0: Reset bracketed paste handler
135
+ // v3.8.0: Clear fallback paste buffer and timeout
136
+ fallbackPasteBufferRef.current = '';
137
+ fallbackPasteLastChunkTimeRef.current = 0;
138
+ if (fallbackPasteTimeoutRef.current) {
139
+ clearTimeout(fallbackPasteTimeoutRef.current);
140
+ fallbackPasteTimeoutRef.current = null;
141
+ }
44
142
  }, [setOriginalInput]);
45
143
  const insertAtCursor = useCallback((text) => {
46
144
  const result = insertText(input, cursorPosition, text);
@@ -48,29 +146,44 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
48
146
  setCursorPositionState(result.position);
49
147
  setOriginalInput(result.text);
50
148
  }, [input, cursorPosition, setOriginalInput]);
51
- // Handle paste completion (after accumulation timeout)
149
+ // Handle paste completion
150
+ // Note: No timeout or accumulation needed - Ink batches the entire paste for us
151
+ // BUG FIX: Create refs to track latest values without causing re-renders
152
+ const inputRef = useRef(input);
153
+ const cursorPositionRef = useRef(cursorPosition);
154
+ // Keep refs in sync with state
155
+ useEffect(() => {
156
+ inputRef.current = input;
157
+ cursorPositionRef.current = cursorPosition;
158
+ }, [input, cursorPosition]);
52
159
  const handlePasteComplete = useCallback((pastedContent) => {
53
- // Check if should collapse
160
+ // BUG FIX: Use refs to get CURRENT values, avoiding stale closures
161
+ const currentInput = inputRef.current;
162
+ const currentCursor = cursorPositionRef.current;
163
+ // Preserve all formatting - no trimming or normalization
164
+ // This ensures JSON indentation, newlines, and whitespace are intact
165
+ // Check if should collapse based on line count or character count
54
166
  if (shouldCollapsePaste(pastedContent)) {
55
- // Create pasted block
56
- const block = createPastedBlock(pasteCounter, pastedContent, cursorPosition);
57
- setPasteCounter(prev => prev + 1);
167
+ // BUG FIX: Use ref and increment immediately to prevent race conditions
168
+ const blockId = pasteCounterRef.current++;
169
+ // Create pasted block with CURRENT cursor position
170
+ const block = createPastedBlock(blockId, pastedContent, currentCursor);
58
171
  setPastedBlocks(prev => [...prev, block]);
59
172
  // Insert placeholder instead of full content
60
173
  const placeholder = generatePlaceholder(block);
61
- const result = insertText(input, cursorPosition, placeholder);
174
+ const result = insertText(currentInput, currentCursor, placeholder);
62
175
  setInputState(result.text);
63
176
  setCursorPositionState(result.position);
64
177
  setOriginalInput(result.text);
65
178
  }
66
179
  else {
67
- // Insert normally (below threshold)
68
- const result = insertText(input, cursorPosition, pastedContent);
180
+ // Insert normally (below threshold) with all formatting preserved
181
+ const result = insertText(currentInput, currentCursor, pastedContent);
69
182
  setInputState(result.text);
70
183
  setCursorPositionState(result.position);
71
184
  setOriginalInput(result.text);
72
185
  }
73
- }, [input, cursorPosition, pasteCounter, setOriginalInput]);
186
+ }, [setOriginalInput]);
74
187
  // Toggle collapse/expand for block at cursor
75
188
  const toggleBlockAtCursor = useCallback(() => {
76
189
  const block = findBlockAtCursor(input, cursorPosition, pastedBlocks);
@@ -142,6 +255,27 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
142
255
  return expandAllPlaceholders(text, pastedBlocks);
143
256
  }, [pastedBlocks]);
144
257
  const handleSubmit = useCallback(() => {
258
+ // BUG FIX: Check for pending paste accumulation before submitting
259
+ if (fallbackPasteBufferRef.current.length > 0) {
260
+ // There's accumulated paste content - flush it first
261
+ if (fallbackPasteTimeoutRef.current) {
262
+ clearTimeout(fallbackPasteTimeoutRef.current);
263
+ fallbackPasteTimeoutRef.current = null;
264
+ }
265
+ const accumulatedPaste = fallbackPasteBufferRef.current;
266
+ fallbackPasteBufferRef.current = '';
267
+ fallbackPasteLastChunkTimeRef.current = 0;
268
+ // Add accumulated paste to input before submitting
269
+ try {
270
+ handlePasteComplete(accumulatedPaste);
271
+ }
272
+ catch (error) {
273
+ console.error('Error flushing paste on submit:', error);
274
+ }
275
+ // Don't submit yet - let the paste complete first
276
+ // User can hit Enter again to submit
277
+ return;
278
+ }
145
279
  if (input.trim()) {
146
280
  // Expand all placeholders before submission
147
281
  const expandedInput = expandPlaceholdersForSubmit(input);
@@ -149,7 +283,7 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
149
283
  onSubmit?.(expandedInput);
150
284
  clearInput();
151
285
  }
152
- }, [input, addToHistory, onSubmit, clearInput, expandPlaceholdersForSubmit]);
286
+ }, [input, addToHistory, onSubmit, clearInput, expandPlaceholdersForSubmit, handlePasteComplete]);
153
287
  const handleInput = useCallback((inputChar, key) => {
154
288
  if (disabled)
155
289
  return;
@@ -175,17 +309,53 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
175
309
  onEscape?.();
176
310
  return;
177
311
  }
178
- // Handle Enter/Return
312
+ // Handle Enter/Return - Phase 1: Configurable multi-line input
179
313
  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);
314
+ const enterBehavior = inputConfig.enterBehavior || 'submit';
315
+ // Check if user pressed Shift+Enter (submit key in newline mode)
316
+ const isShiftEnter = key.shift && key.return;
317
+ if (enterBehavior === 'newline') {
318
+ // Newline mode: Enter inserts newline, Shift+Enter submits
319
+ if (isShiftEnter) {
320
+ handleSubmit();
321
+ }
322
+ else {
323
+ const result = insertText(input, cursorPosition, "\n");
324
+ setInputState(result.text);
325
+ setCursorPositionState(result.position);
326
+ setOriginalInput(result.text);
327
+ }
186
328
  }
187
- else {
188
- handleSubmit();
329
+ else if (enterBehavior === 'submit') {
330
+ // Submit mode (legacy): Enter submits, Shift+Enter inserts newline
331
+ if (isShiftEnter) {
332
+ const result = insertText(input, cursorPosition, "\n");
333
+ setInputState(result.text);
334
+ setCursorPositionState(result.position);
335
+ setOriginalInput(result.text);
336
+ }
337
+ else {
338
+ handleSubmit();
339
+ }
340
+ }
341
+ else if (enterBehavior === 'smart') {
342
+ // Smart mode: Auto-detect incomplete input
343
+ // Shift+Enter always submits, otherwise check if input is incomplete
344
+ if (isShiftEnter) {
345
+ // Explicit submit with Shift+Enter
346
+ handleSubmit();
347
+ }
348
+ else if (isIncompleteInput(input, inputConfig.smartDetection)) {
349
+ // Input appears incomplete, insert newline
350
+ const result = insertText(input, cursorPosition, "\n");
351
+ setInputState(result.text);
352
+ setCursorPositionState(result.position);
353
+ setOriginalInput(result.text);
354
+ }
355
+ else {
356
+ // Input looks complete, submit
357
+ handleSubmit();
358
+ }
189
359
  }
190
360
  return;
191
361
  }
@@ -341,46 +511,320 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
341
511
  setOriginalInput("");
342
512
  return;
343
513
  }
514
+ // Handle Shift+Tab: Toggle auto-accept mode (Phase 2)
515
+ // Check for Shift+Tab combination
516
+ if (key.shift && key.tab) {
517
+ onAutoAcceptToggle?.();
518
+ return;
519
+ }
520
+ // Handle Tab (alone): Toggle thinking mode (P2.4)
521
+ // Only if input is empty (to avoid interfering with autocomplete)
522
+ if (key.tab && !key.shift && !key.ctrl && !key.meta && input.length === 0) {
523
+ onThinkingModeToggle?.();
524
+ return;
525
+ }
526
+ // Handle Ctrl+G: Open external editor (Phase 2)
527
+ // Check both key.ctrl with 'g' and raw ASCII code \x07 (Ctrl+G = ASCII 7)
528
+ if ((key.ctrl && inputChar === "g") || inputChar === "\x07") {
529
+ // Call async external editor handler
530
+ onExternalEditor?.(input).then((editedContent) => {
531
+ if (editedContent !== null) {
532
+ setInputState(editedContent);
533
+ setCursorPositionState(editedContent.length);
534
+ }
535
+ }).catch(() => {
536
+ // Ignore errors - user will see error in UI
537
+ });
538
+ return;
539
+ }
540
+ // Handle ? key: Show keyboard shortcuts help (P1.4)
541
+ // Only trigger if input is empty (avoid interfering with normal typing)
542
+ if (inputChar === "?" && input.length === 0) {
543
+ onKeyboardHelp?.();
544
+ return;
545
+ }
344
546
  // Handle regular character input
345
547
  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);
548
+ // v3.8.0: Bracketed Paste Mode Detection
549
+ // Uses industry-standard escape sequences for reliable paste detection
550
+ // Falls back to simple batched detection if not supported
551
+ const { enableBracketedPaste, enableFallback } = pasteConfig;
552
+ // Use bracketed paste handler if enabled
553
+ if (enableBracketedPaste) {
554
+ // BUG FIX: Check for orphaned content from timeout first
555
+ const orphanedContent = bracketedPasteHandlerRef.current.retrieveOrphanedContent();
556
+ if (orphanedContent) {
557
+ // Timeout occurred and we have accumulated content without end marker
558
+ // Process it as a paste
559
+ try {
560
+ handlePasteComplete(orphanedContent);
561
+ }
562
+ catch (error) {
563
+ console.error('Error handling orphaned paste:', error);
564
+ // Continue despite error - don't block current input
565
+ }
566
+ // Continue processing current input normally
567
+ }
568
+ const result = bracketedPasteHandlerRef.current.handleInput(inputChar);
569
+ // Update pasting state for visual indicator
570
+ if (result.isAccumulating !== isPasting) {
571
+ setIsPasting(result.isAccumulating);
572
+ }
573
+ // If still accumulating, don't process yet
574
+ if (result.isAccumulating) {
575
+ return;
352
576
  }
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);
577
+ // If paste detected and complete via bracketed paste mode
578
+ if (result.isPaste && result.content) {
579
+ const pastedContent = result.content;
580
+ // Phase 3: Large paste handling with truncation
581
+ const settingsManager = getSettingsManager();
582
+ const pasteSettings = settingsManager.getPasteSettings();
583
+ const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
584
+ // Check if paste exceeds warning threshold
585
+ if (pastedContent.length >= warningThreshold) {
586
+ onLargePaste?.(pastedContent.length);
360
587
  }
361
- pasteTimeoutRef.current = null;
362
- }, 50);
588
+ // Handle truncation if needed
589
+ let finalContent = pastedContent;
590
+ if (!allowLargePaste && pastedContent.length > maxPasteLength) {
591
+ // Truncate the paste
592
+ finalContent = pastedContent.slice(0, maxPasteLength);
593
+ // Notify about truncation
594
+ onPasteTruncated?.(pastedContent.length, maxPasteLength);
595
+ }
596
+ // Handle entire paste at once
597
+ handlePasteComplete(finalContent);
598
+ return;
599
+ }
600
+ // Not detected via bracketed paste mode
601
+ // If fallback enabled AND looks like batched paste, accumulate chunks
602
+ if (enableFallback && result.content && result.content.length > 1) {
603
+ // BUG FIX: Accumulate chunks with timeout instead of immediate processing
604
+ // This prevents multiple blocks when paste arrives in chunks (SSH, tmux, etc)
605
+ const now = Date.now();
606
+ const isActiveAccumulation = fallbackPasteBufferRef.current.length > 0;
607
+ const timeSinceLastChunk = now - fallbackPasteLastChunkTimeRef.current;
608
+ // BUG FIX: Check buffer size to prevent overflow (100MB limit)
609
+ const MAX_BUFFER_SIZE = 100 * 1024 * 1024;
610
+ if (fallbackPasteBufferRef.current.length + result.content.length > MAX_BUFFER_SIZE) {
611
+ // Buffer overflow - process what we have immediately
612
+ const accumulatedContent = fallbackPasteBufferRef.current;
613
+ fallbackPasteBufferRef.current = '';
614
+ fallbackPasteLastChunkTimeRef.current = 0;
615
+ if (fallbackPasteTimeoutRef.current) {
616
+ clearTimeout(fallbackPasteTimeoutRef.current);
617
+ fallbackPasteTimeoutRef.current = null;
618
+ }
619
+ // Process accumulated content (truncated)
620
+ try {
621
+ handlePasteComplete(accumulatedContent);
622
+ }
623
+ catch (error) {
624
+ console.error('Error handling oversized paste:', error);
625
+ }
626
+ return;
627
+ }
628
+ // Add to accumulation buffer
629
+ fallbackPasteBufferRef.current += result.content;
630
+ fallbackPasteLastChunkTimeRef.current = now;
631
+ // Clear existing timeout
632
+ if (fallbackPasteTimeoutRef.current) {
633
+ clearTimeout(fallbackPasteTimeoutRef.current);
634
+ }
635
+ // Dynamic timeout: Use longer window if we're actively accumulating a paste
636
+ // Initial chunk: 200ms (fast for single-burst pastes)
637
+ // Subsequent chunks: 500ms (handles slow terminals/networks)
638
+ const timeoutMs = isActiveAccumulation && timeSinceLastChunk < 1000 ? 500 : 200;
639
+ // Set new timeout - if no more chunks arrive, process the paste
640
+ fallbackPasteTimeoutRef.current = setTimeout(() => {
641
+ // BUG FIX: Clear refs FIRST to prevent race conditions
642
+ const accumulatedContent = fallbackPasteBufferRef.current;
643
+ fallbackPasteBufferRef.current = '';
644
+ fallbackPasteLastChunkTimeRef.current = 0;
645
+ fallbackPasteTimeoutRef.current = null;
646
+ if (!accumulatedContent)
647
+ return;
648
+ const pastedContent = accumulatedContent.replace(/\r/g, '\n');
649
+ // Phase 3: Large paste handling with truncation
650
+ const settingsManager = getSettingsManager();
651
+ const pasteSettings = settingsManager.getPasteSettings();
652
+ const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
653
+ // Check if paste exceeds warning threshold
654
+ if (pastedContent.length >= warningThreshold) {
655
+ onLargePaste?.(pastedContent.length);
656
+ }
657
+ // Handle truncation if needed
658
+ let finalContent = pastedContent;
659
+ if (!allowLargePaste && pastedContent.length > maxPasteLength) {
660
+ // Truncate the paste
661
+ finalContent = pastedContent.slice(0, maxPasteLength);
662
+ // Notify about truncation
663
+ onPasteTruncated?.(pastedContent.length, maxPasteLength);
664
+ }
665
+ // Handle entire accumulated paste at once
666
+ // Note: handlePasteComplete will use CURRENT input/cursor values from React state
667
+ try {
668
+ handlePasteComplete(finalContent);
669
+ }
670
+ catch (error) {
671
+ // BUG FIX: Catch errors to prevent timeout callback crash
672
+ console.error('Error handling paste:', error);
673
+ // Don't attempt fallback - state might be inconsistent
674
+ // The paste content is lost but system remains stable
675
+ }
676
+ }, timeoutMs); // Dynamic timeout: 200ms initial, 500ms for active accumulation
677
+ return;
678
+ }
679
+ // Normal single character input
680
+ if (result.content) {
681
+ // BUG FIX: If there's a pending paste accumulation, add character to it
682
+ // This prevents splitting user input into paste + character
683
+ if (fallbackPasteBufferRef.current.length > 0) {
684
+ // Add single character to accumulation buffer
685
+ fallbackPasteBufferRef.current += result.content;
686
+ // Extend timeout slightly (paste likely complete, but give it 100ms more)
687
+ if (fallbackPasteTimeoutRef.current) {
688
+ clearTimeout(fallbackPasteTimeoutRef.current);
689
+ }
690
+ fallbackPasteTimeoutRef.current = setTimeout(() => {
691
+ const accumulatedContent = fallbackPasteBufferRef.current;
692
+ fallbackPasteBufferRef.current = '';
693
+ fallbackPasteTimeoutRef.current = null;
694
+ fallbackPasteLastChunkTimeRef.current = 0;
695
+ if (accumulatedContent) {
696
+ handlePasteComplete(accumulatedContent);
697
+ }
698
+ }, 100); // Short timeout for single char after paste
699
+ return;
700
+ }
701
+ // No pending paste - process normal input
702
+ const result2 = insertText(input, cursorPosition, result.content);
703
+ setInputState(result2.text);
704
+ setCursorPositionState(result2.position);
705
+ setOriginalInput(result2.text);
706
+ }
707
+ }
708
+ else if (enableFallback) {
709
+ // Fallback: Simple batched detection (legacy behavior when bracketed paste disabled)
710
+ // When inputChar.length > 1, it's likely pasted content
711
+ const isPaste = inputChar.length > 1;
712
+ if (isPaste) {
713
+ // BUG FIX: Use same dynamic accumulation as bracketed paste fallback
714
+ // This ensures consistent behavior regardless of bracketed paste support
715
+ const now = Date.now();
716
+ const isActiveAccumulation = fallbackPasteBufferRef.current.length > 0;
717
+ const timeSinceLastChunk = now - fallbackPasteLastChunkTimeRef.current;
718
+ // Normalize line endings: convert \r to \n
719
+ const normalizedInput = inputChar.replace(/\r/g, '\n');
720
+ // BUG FIX: Check buffer size to prevent overflow (100MB limit)
721
+ const MAX_BUFFER_SIZE = 100 * 1024 * 1024;
722
+ if (fallbackPasteBufferRef.current.length + normalizedInput.length > MAX_BUFFER_SIZE) {
723
+ // Buffer overflow - process what we have immediately
724
+ const accumulatedContent = fallbackPasteBufferRef.current;
725
+ fallbackPasteBufferRef.current = '';
726
+ fallbackPasteLastChunkTimeRef.current = 0;
727
+ if (fallbackPasteTimeoutRef.current) {
728
+ clearTimeout(fallbackPasteTimeoutRef.current);
729
+ fallbackPasteTimeoutRef.current = null;
730
+ }
731
+ // Process accumulated content (truncated)
732
+ try {
733
+ handlePasteComplete(accumulatedContent);
734
+ }
735
+ catch (error) {
736
+ console.error('Error handling oversized paste:', error);
737
+ }
738
+ return;
739
+ }
740
+ // Add to accumulation buffer
741
+ fallbackPasteBufferRef.current += normalizedInput;
742
+ fallbackPasteLastChunkTimeRef.current = now;
743
+ // Clear existing timeout
744
+ if (fallbackPasteTimeoutRef.current) {
745
+ clearTimeout(fallbackPasteTimeoutRef.current);
746
+ }
747
+ // Dynamic timeout: Use longer window if we're actively accumulating a paste
748
+ const timeoutMs = isActiveAccumulation && timeSinceLastChunk < 1000 ? 500 : 200;
749
+ // Set new timeout - if no more chunks arrive, process the paste
750
+ fallbackPasteTimeoutRef.current = setTimeout(() => {
751
+ // BUG FIX: Clear refs FIRST to prevent race conditions
752
+ const accumulatedContent = fallbackPasteBufferRef.current;
753
+ fallbackPasteBufferRef.current = '';
754
+ fallbackPasteLastChunkTimeRef.current = 0;
755
+ fallbackPasteTimeoutRef.current = null;
756
+ if (!accumulatedContent)
757
+ return;
758
+ // Phase 3: Large paste handling with truncation
759
+ const settingsManager = getSettingsManager();
760
+ const pasteSettings = settingsManager.getPasteSettings();
761
+ const { allowLargePaste, maxPasteLength, warningThreshold } = pasteSettings;
762
+ // Check if paste exceeds warning threshold
763
+ if (accumulatedContent.length >= warningThreshold) {
764
+ onLargePaste?.(accumulatedContent.length);
765
+ }
766
+ // Handle truncation if needed
767
+ let finalContent = accumulatedContent;
768
+ if (!allowLargePaste && accumulatedContent.length > maxPasteLength) {
769
+ // Truncate the paste
770
+ finalContent = accumulatedContent.slice(0, maxPasteLength);
771
+ // Notify about truncation
772
+ onPasteTruncated?.(accumulatedContent.length, maxPasteLength);
773
+ }
774
+ // Handle entire accumulated paste at once
775
+ // Note: handlePasteComplete will use CURRENT input/cursor values from React state
776
+ try {
777
+ handlePasteComplete(finalContent);
778
+ }
779
+ catch (error) {
780
+ // BUG FIX: Catch errors to prevent timeout callback crash
781
+ console.error('Error handling paste:', error);
782
+ // Don't attempt fallback - state might be inconsistent
783
+ // The paste content is lost but system remains stable
784
+ }
785
+ }, timeoutMs);
786
+ return;
787
+ }
788
+ else {
789
+ // Normal single character input
790
+ const result = insertText(input, cursorPosition, inputChar);
791
+ setInputState(result.text);
792
+ setCursorPositionState(result.position);
793
+ setOriginalInput(result.text);
794
+ }
363
795
  }
364
796
  else {
365
- // Normal character input
797
+ // Both bracketed paste and fallback disabled - normal input only
366
798
  const result = insertText(input, cursorPosition, inputChar);
367
799
  setInputState(result.text);
368
800
  setCursorPositionState(result.position);
369
801
  setOriginalInput(result.text);
370
802
  }
371
803
  }
372
- }, [disabled, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, input, cursorPosition, multiline, handleSubmit, navigateHistory, setOriginalInput, toggleBlockAtCursor, handlePasteComplete]);
804
+ }, [disabled, onSpecialKey, onVerboseToggle, onQuickActions, onBackgroundModeToggle, onCopyLastResponse, onAutoAcceptToggle, onThinkingModeToggle, onKeyboardHelp, onLargePaste, onPasteTruncated, input, cursorPosition, multiline, handleSubmit, navigateHistory, setOriginalInput, toggleBlockAtCursor, handlePasteComplete, pasteConfig, isPasting]);
373
805
  // Update current block at cursor when cursor position or input changes
374
806
  useEffect(() => {
375
807
  const block = findBlockAtCursor(input, cursorPosition, pastedBlocks);
376
808
  setCurrentBlockAtCursor(block);
377
809
  }, [input, cursorPosition, pastedBlocks]);
378
- // Cleanup paste timeout on unmount
810
+ // BUG FIX: Comprehensive cleanup on unmount
379
811
  useEffect(() => {
380
812
  return () => {
813
+ // Clear all timeouts
381
814
  if (pasteTimeoutRef.current) {
382
815
  clearTimeout(pasteTimeoutRef.current);
816
+ pasteTimeoutRef.current = null;
817
+ }
818
+ if (fallbackPasteTimeoutRef.current) {
819
+ clearTimeout(fallbackPasteTimeoutRef.current);
820
+ fallbackPasteTimeoutRef.current = null;
383
821
  }
822
+ // Reset all detectors and handlers
823
+ pasteDetectorRef.current.reset();
824
+ bracketedPasteHandlerRef.current.dispose();
825
+ // Clear buffers
826
+ fallbackPasteBufferRef.current = '';
827
+ fallbackPasteLastChunkTimeRef.current = 0;
384
828
  };
385
829
  }, []);
386
830
  return {
@@ -389,6 +833,7 @@ export function useEnhancedInput({ onSubmit, onEscape, onSpecialKey, onVerboseTo
389
833
  isMultiline: isMultilineRef.current,
390
834
  pastedBlocks,
391
835
  currentBlockAtCursor,
836
+ isPasting, // v3.8.0: Expose paste accumulation state for visual indicator
392
837
  setInput,
393
838
  setCursorPosition,
394
839
  clearInput,