@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.
- package/README.md +148 -53
- package/dist/agent/context-manager.d.ts +15 -1
- package/dist/agent/context-manager.js +50 -19
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/dependency-resolver.js +13 -7
- package/dist/agent/dependency-resolver.js.map +1 -1
- package/dist/agent/llm-agent.d.ts +37 -0
- package/dist/agent/llm-agent.js +318 -98
- package/dist/agent/llm-agent.js.map +1 -1
- package/dist/agent/status-reporter.d.ts +114 -0
- package/dist/agent/status-reporter.js +335 -0
- package/dist/agent/status-reporter.js.map +1 -0
- package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js +8 -2
- package/dist/analyzers/best-practices/rules/typescript/no-magic-numbers.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/no-unused-vars.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-const.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-const.js.map +1 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js +3 -1
- package/dist/analyzers/best-practices/rules/typescript/prefer-readonly.js.map +1 -1
- package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js +9 -3
- package/dist/analyzers/code-smells/detectors/duplicate-code-detector.js.map +1 -1
- package/dist/analyzers/git/churn-calculator.d.ts +2 -0
- package/dist/analyzers/git/churn-calculator.js +42 -8
- package/dist/analyzers/git/churn-calculator.js.map +1 -1
- package/dist/analyzers/git/hotspot-detector.js +2 -2
- package/dist/analyzers/git/hotspot-detector.js.map +1 -1
- package/dist/analyzers/metrics/metrics-analyzer.js +1 -1
- package/dist/analyzers/metrics/metrics-analyzer.js.map +1 -1
- package/dist/analyzers/security/security-analyzer.js +1 -1
- package/dist/analyzers/security/security-analyzer.js.map +1 -1
- package/dist/checkpoint/manager.d.ts +1 -0
- package/dist/checkpoint/manager.js +49 -9
- package/dist/checkpoint/manager.js.map +1 -1
- package/dist/checkpoint/storage.js +2 -2
- package/dist/checkpoint/storage.js.map +1 -1
- package/dist/commands/mcp-migrate.d.ts +9 -0
- package/dist/commands/mcp-migrate.js +172 -0
- package/dist/commands/mcp-migrate.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +211 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/vscode.d.ts +7 -0
- package/dist/commands/vscode.js +363 -0
- package/dist/commands/vscode.js.map +1 -0
- package/dist/index.js +79 -30
- package/dist/index.js.map +1 -1
- package/dist/llm/client.js +33 -4
- package/dist/llm/client.js.map +1 -1
- package/dist/mcp/automatosx-loader.d.ts +84 -0
- package/dist/mcp/automatosx-loader.js +238 -0
- package/dist/mcp/automatosx-loader.js.map +1 -0
- package/dist/mcp/client-mutex-patch.d.ts +36 -0
- package/dist/mcp/client-mutex-patch.js +75 -0
- package/dist/mcp/client-mutex-patch.js.map +1 -0
- package/dist/mcp/client-v2.d.ts +229 -0
- package/dist/mcp/client-v2.js +740 -0
- package/dist/mcp/client-v2.js.map +1 -0
- package/dist/mcp/client.d.ts +111 -13
- package/dist/mcp/client.js +168 -253
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/config-detector-v2.d.ts +83 -0
- package/dist/mcp/config-detector-v2.js +328 -0
- package/dist/mcp/config-detector-v2.js.map +1 -0
- package/dist/mcp/config-detector.d.ts +90 -0
- package/dist/mcp/config-detector.js +242 -0
- package/dist/mcp/config-detector.js.map +1 -0
- package/dist/mcp/config-migrator-v2.d.ts +89 -0
- package/dist/mcp/config-migrator-v2.js +288 -0
- package/dist/mcp/config-migrator-v2.js.map +1 -0
- package/dist/mcp/config-migrator.d.ts +63 -0
- package/dist/mcp/config-migrator.js +269 -0
- package/dist/mcp/config-migrator.js.map +1 -0
- package/dist/mcp/config-v2.d.ts +106 -0
- package/dist/mcp/config-v2.js +417 -0
- package/dist/mcp/config-v2.js.map +1 -0
- package/dist/mcp/config.d.ts +12 -1
- package/dist/mcp/config.js +95 -10
- package/dist/mcp/config.js.map +1 -1
- package/dist/mcp/error-formatter.d.ts +46 -0
- package/dist/mcp/error-formatter.js +244 -0
- package/dist/mcp/error-formatter.js.map +1 -0
- package/dist/mcp/health.d.ts +5 -0
- package/dist/mcp/health.js +22 -2
- package/dist/mcp/health.js.map +1 -1
- package/dist/mcp/invariants.d.ts +141 -0
- package/dist/mcp/invariants.js +243 -0
- package/dist/mcp/invariants.js.map +1 -0
- package/dist/mcp/mutex-safe.d.ts +153 -0
- package/dist/mcp/mutex-safe.js +260 -0
- package/dist/mcp/mutex-safe.js.map +1 -0
- package/dist/mcp/mutex.d.ts +73 -0
- package/dist/mcp/mutex.js +137 -0
- package/dist/mcp/mutex.js.map +1 -0
- package/dist/mcp/reconnection.d.ts +4 -0
- package/dist/mcp/reconnection.js +25 -1
- package/dist/mcp/reconnection.js.map +1 -1
- package/dist/mcp/transports-v2.d.ts +152 -0
- package/dist/mcp/transports-v2.js +481 -0
- package/dist/mcp/transports-v2.js.map +1 -0
- package/dist/mcp/type-safety.d.ts +231 -0
- package/dist/mcp/type-safety.js +273 -0
- package/dist/mcp/type-safety.js.map +1 -0
- package/dist/planner/task-planner.js +13 -0
- package/dist/planner/task-planner.js.map +1 -1
- package/dist/planner/types.d.ts +6 -6
- package/dist/schemas/confirmation-schemas.d.ts +2 -2
- package/dist/schemas/settings-schemas.d.ts +196 -0
- package/dist/schemas/settings-schemas.js +146 -5
- package/dist/schemas/settings-schemas.js.map +1 -1
- package/dist/sdk/index.d.ts +118 -2
- package/dist/sdk/index.js +146 -4
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/testing.d.ts +182 -0
- package/dist/sdk/testing.js +231 -0
- package/dist/sdk/testing.js.map +1 -1
- package/dist/sdk/version.d.ts +114 -15
- package/dist/sdk/version.js +137 -15
- package/dist/sdk/version.js.map +1 -1
- package/dist/tools/bash.js +54 -9
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +170 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search.js +12 -2
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/text-editor.js +84 -26
- package/dist/tools/text-editor.js.map +1 -1
- package/dist/ui/components/chat-history.js +6 -1
- package/dist/ui/components/chat-history.js.map +1 -1
- package/dist/ui/components/chat-input.d.ts +2 -1
- package/dist/ui/components/chat-input.js +5 -2
- package/dist/ui/components/chat-input.js.map +1 -1
- package/dist/ui/components/chat-interface.js +187 -5
- package/dist/ui/components/chat-interface.js.map +1 -1
- package/dist/ui/components/context-breakdown.d.ts +23 -0
- package/dist/ui/components/context-breakdown.js +124 -0
- package/dist/ui/components/context-breakdown.js.map +1 -0
- package/dist/ui/components/keyboard-help.d.ts +17 -0
- package/dist/ui/components/keyboard-help.js +116 -0
- package/dist/ui/components/keyboard-help.js.map +1 -0
- package/dist/ui/components/keyboard-hints.js +2 -2
- package/dist/ui/components/keyboard-hints.js.map +1 -1
- package/dist/ui/components/quick-actions.js +43 -7
- package/dist/ui/components/quick-actions.js.map +1 -1
- package/dist/ui/components/status-bar.d.ts +3 -0
- package/dist/ui/components/status-bar.js +25 -16
- package/dist/ui/components/status-bar.js.map +1 -1
- package/dist/ui/components/toast-notification.d.ts +42 -0
- package/dist/ui/components/toast-notification.js +30 -2
- package/dist/ui/components/toast-notification.js.map +1 -1
- package/dist/ui/components/tool-group-display.js +34 -4
- package/dist/ui/components/tool-group-display.js.map +1 -1
- package/dist/ui/components/welcome-panel.js +2 -2
- package/dist/ui/components/welcome-panel.js.map +1 -1
- package/dist/ui/hooks/use-enhanced-input.d.ts +9 -1
- package/dist/ui/hooks/use-enhanced-input.js +901 -90
- package/dist/ui/hooks/use-enhanced-input.js.map +1 -1
- package/dist/ui/hooks/use-input-handler.d.ts +11 -1
- package/dist/ui/hooks/use-input-handler.js +67 -3
- package/dist/ui/hooks/use-input-handler.js.map +1 -1
- package/dist/ui/hooks/use-input-history.d.ts +1 -1
- package/dist/ui/hooks/use-input-history.js +50 -14
- package/dist/ui/hooks/use-input-history.js.map +1 -1
- package/dist/ui/utils/bracketed-paste-handler.d.ts +97 -0
- package/dist/ui/utils/bracketed-paste-handler.js +322 -0
- package/dist/ui/utils/bracketed-paste-handler.js.map +1 -0
- package/dist/ui/utils/change-summarizer.js +16 -6
- package/dist/ui/utils/change-summarizer.js.map +1 -1
- package/dist/ui/utils/tool-grouper.d.ts +10 -1
- package/dist/ui/utils/tool-grouper.js +143 -30
- package/dist/ui/utils/tool-grouper.js.map +1 -1
- package/dist/utils/auto-accept-logger.d.ts +173 -0
- package/dist/utils/auto-accept-logger.js +420 -0
- package/dist/utils/auto-accept-logger.js.map +1 -0
- package/dist/utils/background-task-manager.d.ts +11 -0
- package/dist/utils/background-task-manager.js +124 -38
- package/dist/utils/background-task-manager.js.map +1 -1
- package/dist/utils/confirmation-service.d.ts +1 -0
- package/dist/utils/confirmation-service.js +6 -1
- package/dist/utils/confirmation-service.js.map +1 -1
- package/dist/utils/encryption.d.ts +8 -0
- package/dist/utils/encryption.js +44 -27
- package/dist/utils/encryption.js.map +1 -1
- package/dist/utils/enhanced-error-messages.d.ts +33 -0
- package/dist/utils/enhanced-error-messages.js +420 -0
- package/dist/utils/enhanced-error-messages.js.map +1 -0
- package/dist/utils/error-handler.d.ts +13 -3
- package/dist/utils/error-handler.js +16 -4
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/external-editor.d.ts +47 -0
- package/dist/utils/external-editor.js +179 -0
- package/dist/utils/external-editor.js.map +1 -0
- package/dist/utils/history-migration.d.ts +9 -0
- package/dist/utils/history-migration.js +36 -0
- package/dist/utils/history-migration.js.map +1 -0
- package/dist/utils/paste-utils.js +12 -11
- package/dist/utils/paste-utils.js.map +1 -1
- package/dist/utils/rate-limiter.js +20 -1
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/safety-rules.d.ts +64 -0
- package/dist/utils/safety-rules.js +225 -0
- package/dist/utils/safety-rules.js.map +1 -0
- package/dist/utils/settings-manager.d.ts +89 -1
- package/dist/utils/settings-manager.js +359 -3
- package/dist/utils/settings-manager.js.map +1 -1
- package/dist/utils/token-counter.d.ts +2 -0
- package/dist/utils/token-counter.js +32 -9
- package/dist/utils/token-counter.js.map +1 -1
- package/dist/utils/version.d.ts +11 -2
- package/dist/utils/version.js +54 -21
- package/dist/utils/version.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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(
|
|
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(
|
|
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
|
-
}, [
|
|
315
|
+
}, [setOriginalInput]);
|
|
74
316
|
// Toggle collapse/expand for block at cursor
|
|
75
317
|
const toggleBlockAtCursor = useCallback(() => {
|
|
76
|
-
|
|
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 <
|
|
86
|
-
const occurrenceStart =
|
|
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 (
|
|
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 =
|
|
344
|
+
const newInput = currentInput.substring(0, targetStart) +
|
|
100
345
|
block.content +
|
|
101
|
-
|
|
346
|
+
currentInput.substring(targetStart + placeholder.length);
|
|
102
347
|
setInputState(newInput);
|
|
103
348
|
// Keep cursor at same position or adjust if needed
|
|
104
|
-
const newCursor =
|
|
105
|
-
|
|
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
|
-
|
|
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 <
|
|
116
|
-
const occurrenceStart =
|
|
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 (
|
|
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 =
|
|
381
|
+
const newInput = currentInput.substring(0, targetStart) +
|
|
130
382
|
placeholder +
|
|
131
|
-
|
|
383
|
+
currentInput.substring(targetStart + block.content.length);
|
|
132
384
|
setInputState(newInput);
|
|
133
385
|
// Adjust cursor to end of placeholder
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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,
|
|
143
|
-
}, [
|
|
403
|
+
return expandAllPlaceholders(text, pastedBlocksRef.current);
|
|
404
|
+
}, []);
|
|
144
405
|
const handleSubmit = useCallback(() => {
|
|
145
|
-
|
|
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(
|
|
439
|
+
const expandedInput = expandPlaceholdersForSubmit(currentInput);
|
|
148
440
|
addToHistory(expandedInput);
|
|
149
441
|
onSubmit?.(expandedInput);
|
|
150
442
|
clearInput();
|
|
151
443
|
}
|
|
152
|
-
}, [
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
const
|
|
711
|
+
// BUG FIX: Use refs to get CURRENT values, avoiding stale closures
|
|
712
|
+
const currentInput = inputRef.current;
|
|
713
|
+
const currentCursor = cursorPositionRef.current;
|
|
714
|
+
const lineStart = moveToLineStart(currentInput, currentCursor);
|
|
715
|
+
const newText = currentInput.slice(0, lineStart) + currentInput.slice(currentCursor);
|
|
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
|
-
|
|
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
|
-
//
|
|
347
|
-
|
|
348
|
-
if
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
//
|
|
366
|
-
|
|
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,
|
|
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
|
-
//
|
|
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,
|