mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-29 04:00:36 +00:00
feat(paste): add large paste placeholder and fix enter-submit on macOS
- Add large paste placeholder feature: when pasting text > 1000 chars, show a placeholder like '[Pasted Content 1500 chars]' instead of the full content. The full text is stored and expanded on submit. - Fix enter-submit on macOS: only apply recentPasteTime protection when pasteWorkaround is enabled (Windows or Node < 20). On macOS/Linux with modern Node, bracketed paste markers work reliably so the protection is unnecessary and caused the first Enter after paste to be ignored. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
2ed4ae773e
commit
c7b681ef5d
2 changed files with 58 additions and 6 deletions
|
|
@ -38,6 +38,7 @@ import { SCREEN_READER_USER_PREFIX } from '../textConstants.js';
|
||||||
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
|
import { useShellFocusState } from '../contexts/ShellFocusContext.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||||
|
import { useKeypressContext } from '../contexts/KeypressContext.js';
|
||||||
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
|
import { FEEDBACK_DIALOG_KEYS } from '../FeedbackDialog.js';
|
||||||
export interface InputPromptProps {
|
export interface InputPromptProps {
|
||||||
buffer: TextBuffer;
|
buffer: TextBuffer;
|
||||||
|
|
@ -111,6 +112,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
const isShellFocused = useShellFocusState();
|
const isShellFocused = useShellFocusState();
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
const uiActions = useUIActions();
|
const uiActions = useUIActions();
|
||||||
|
const { pasteWorkaround } = useKeypressContext();
|
||||||
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
|
const [justNavigatedHistory, setJustNavigatedHistory] = useState(false);
|
||||||
const [escPressCount, setEscPressCount] = useState(0);
|
const [escPressCount, setEscPressCount] = useState(0);
|
||||||
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
|
||||||
|
|
@ -118,6 +120,13 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
const [recentPasteTime, setRecentPasteTime] = useState<number | null>(null);
|
const [recentPasteTime, setRecentPasteTime] = useState<number | null>(null);
|
||||||
const pasteTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const pasteTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Large paste placeholder handling
|
||||||
|
const LARGE_PASTE_CHAR_THRESHOLD = 1000;
|
||||||
|
const [pendingPastes, setPendingPastes] = useState<Map<string, string>>(
|
||||||
|
new Map(),
|
||||||
|
);
|
||||||
|
const largePasteCounters = useRef<Map<number, number>>(new Map());
|
||||||
|
|
||||||
const [dirs, setDirs] = useState<readonly string[]>(
|
const [dirs, setDirs] = useState<readonly string[]>(
|
||||||
config.getWorkspaceContext().getDirectories(),
|
config.getWorkspaceContext().getDirectories(),
|
||||||
);
|
);
|
||||||
|
|
@ -186,6 +195,18 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
}
|
}
|
||||||
}, [showEscapePrompt, onEscapePromptChange]);
|
}, [showEscapePrompt, onEscapePromptChange]);
|
||||||
|
|
||||||
|
// Helper to generate unique placeholder for large pastes
|
||||||
|
const nextLargePastePlaceholder = useCallback((charCount: number): string => {
|
||||||
|
const base = `[Pasted Content ${charCount} chars]`;
|
||||||
|
const currentCount = largePasteCounters.current.get(charCount) || 0;
|
||||||
|
const nextCount = currentCount + 1;
|
||||||
|
largePasteCounters.current.set(charCount, nextCount);
|
||||||
|
if (nextCount === 1) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
return `${base} #${nextCount}`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Clear escape prompt timer on unmount
|
// Clear escape prompt timer on unmount
|
||||||
useEffect(
|
useEffect(
|
||||||
() => () => {
|
() => () => {
|
||||||
|
|
@ -204,10 +225,18 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
if (shellModeActive) {
|
if (shellModeActive) {
|
||||||
shellHistory.addCommandToHistory(submittedValue);
|
shellHistory.addCommandToHistory(submittedValue);
|
||||||
}
|
}
|
||||||
|
// Expand any large paste placeholders to their full content before submitting
|
||||||
|
let finalValue = submittedValue;
|
||||||
|
if (pendingPastes.size > 0) {
|
||||||
|
pendingPastes.forEach((fullContent, placeholder) => {
|
||||||
|
finalValue = finalValue.replace(placeholder, fullContent);
|
||||||
|
});
|
||||||
|
setPendingPastes(new Map());
|
||||||
|
}
|
||||||
// Clear the buffer *before* calling onSubmit to prevent potential re-submission
|
// Clear the buffer *before* calling onSubmit to prevent potential re-submission
|
||||||
// if onSubmit triggers a re-render while the buffer still holds the old value.
|
// if onSubmit triggers a re-render while the buffer still holds the old value.
|
||||||
buffer.setText('');
|
buffer.setText('');
|
||||||
onSubmit(submittedValue);
|
onSubmit(finalValue);
|
||||||
resetCompletionState();
|
resetCompletionState();
|
||||||
resetReverseSearchCompletionState();
|
resetReverseSearchCompletionState();
|
||||||
},
|
},
|
||||||
|
|
@ -218,6 +247,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
shellModeActive,
|
shellModeActive,
|
||||||
shellHistory,
|
shellHistory,
|
||||||
resetReverseSearchCompletionState,
|
resetReverseSearchCompletionState,
|
||||||
|
pendingPastes,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -330,8 +360,22 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
pasteTimeoutRef.current = null;
|
pasteTimeoutRef.current = null;
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Ensure we never accidentally interpret paste as regular input.
|
// Handle large pastes by showing a placeholder
|
||||||
|
const pasted = key.sequence.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||||
|
const charCount = [...pasted].length; // Proper Unicode char count
|
||||||
|
if (charCount > LARGE_PASTE_CHAR_THRESHOLD) {
|
||||||
|
const placeholder = nextLargePastePlaceholder(charCount);
|
||||||
|
setPendingPastes((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(placeholder, pasted);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
// Insert the placeholder as regular text
|
||||||
|
buffer.insert(placeholder, { paste: false });
|
||||||
|
} else {
|
||||||
|
// Normal paste handling for small content
|
||||||
buffer.handleInput(key);
|
buffer.handleInput(key);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,8 +663,10 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
|
|
||||||
if (keyMatchers[Command.SUBMIT](key)) {
|
if (keyMatchers[Command.SUBMIT](key)) {
|
||||||
if (buffer.text.trim()) {
|
if (buffer.text.trim()) {
|
||||||
// Check if a paste operation occurred recently to prevent accidental auto-submission
|
// Check if a paste operation occurred recently to prevent accidental auto-submission.
|
||||||
if (recentPasteTime !== null) {
|
// Only applies when pasteWorkaround is enabled (Windows or Node < 20), where bracketed
|
||||||
|
// paste markers may not work reliably and Enter key events can leak from pasted text.
|
||||||
|
if (pasteWorkaround && recentPasteTime !== null) {
|
||||||
// Paste occurred recently, ignore this submit to prevent auto-execution
|
// Paste occurred recently, ignore this submit to prevent auto-execution
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -719,6 +765,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||||
showShortcuts,
|
showShortcuts,
|
||||||
uiState,
|
uiState,
|
||||||
uiActions,
|
uiActions,
|
||||||
|
pasteWorkaround,
|
||||||
|
LARGE_PASTE_CHAR_THRESHOLD,
|
||||||
|
nextLargePastePlaceholder,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export type KeypressHandler = (key: Key) => void;
|
||||||
interface KeypressContextValue {
|
interface KeypressContextValue {
|
||||||
subscribe: (handler: KeypressHandler) => void;
|
subscribe: (handler: KeypressHandler) => void;
|
||||||
unsubscribe: (handler: KeypressHandler) => void;
|
unsubscribe: (handler: KeypressHandler) => void;
|
||||||
|
pasteWorkaround: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeypressContext = createContext<KeypressContextValue | undefined>(
|
const KeypressContext = createContext<KeypressContextValue | undefined>(
|
||||||
|
|
@ -799,7 +800,9 @@ export function KeypressProvider({
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeypressContext.Provider value={{ subscribe, unsubscribe }}>
|
<KeypressContext.Provider
|
||||||
|
value={{ subscribe, unsubscribe, pasteWorkaround }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</KeypressContext.Provider>
|
</KeypressContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue