Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 77 additions & 9 deletions packages/cli/src/ui/components/InputPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,43 @@ export function isTerminalPasteTrusted(
return kittyProtocolSupported;
}


/**
* Helper function to create a placeholder for large paste content
* and store it for later substitution.
*/
function handleLargePaste(
text: string,
buffer: TextBuffer,
fastPasteContentRef: React.MutableRefObject<Record<string, string>>,
fastPasteCounterRef: React.MutableRefObject<number>,
): void {
const FAST_PASTE_THRESHOLD = 500;
if (text.length <= FAST_PASTE_THRESHOLD) {
return;
}

const lineCount = (text.match(/\n/g) || []).length + 1;
fastPasteCounterRef.current++;
const suffix =
fastPasteCounterRef.current > 1
? ` #${fastPasteCounterRef.current}`
: '';
const placeholder =
lineCount > 1
? `[Pasted Text: ${lineCount} lines${suffix}]`
: `[Pasted Text: ${text.length} chars${suffix}]`;

const offset = buffer.getOffset();
buffer.replaceRangeByOffset(offset, offset, placeholder);

// Use the proper addPastedContent method instead of direct mutation
buffer.addPastedContent(placeholder, text);

// Also store in our fast ref for combined lookup
fastPasteContentRef.current[placeholder] = text;
}

export interface InputPromptProps {
buffer: TextBuffer;
onSubmit: (value: string) => void;
Expand Down Expand Up @@ -169,6 +206,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
>(null);
const pasteTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const innerBoxRef = useRef<DOMElement>(null);
// Fast paste optimization refs - store large paste content to avoid buffer processing lag
const fastPasteContentRef = useRef<Record<string, string>>({});
const fastPasteCounterRef = useRef<number>(0);

const [reverseSearchActive, setReverseSearchActive] = useState(false);
const [commandSearchActive, setCommandSearchActive] = useState(false);
Expand Down Expand Up @@ -247,14 +287,22 @@ export const InputPrompt: React.FC<InputPromptProps> = ({

const handleSubmitAndClear = useCallback(
(submittedValue: string) => {
let processedValue = submittedValue;
if (buffer.pastedContent) {
// Replace placeholders like [Pasted Text: 6 lines] with actual content
processedValue = processedValue.replace(
PASTED_TEXT_PLACEHOLDER_REGEX,
(match) => buffer.pastedContent[match] || match,
);
}
let processedValue = submittedValue;
// Use our fast refs first, then fallback to buffer.pastedContent
const combinedContent = {
...buffer.pastedContent,
...fastPasteContentRef.current,
};
if (Object.keys(combinedContent).length > 0) {
// Replace placeholders like [Pasted Text: 6 lines] with actual content
processedValue = processedValue.replace(
PASTED_TEXT_PLACEHOLDER_REGEX,
(match) => combinedContent[match] || match,
);
// Clean up fast paste refs after submit
fastPasteContentRef.current = {};
fastPasteCounterRef.current = 0;
}

if (shellModeActive) {
shellHistory.addCommandToHistory(processedValue);
Expand Down Expand Up @@ -395,7 +443,17 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
stdout.write('\x1b]52;c;?\x07');
} else {
const textToInsert = await clipboardy.read();
buffer.insert(textToInsert, { paste: true });
// OPTIMIZATION: Intercept large pastes BEFORE buffer processing
if (textToInsert.length > 500) {
handleLargePaste(
textToInsert,
buffer,
fastPasteContentRef,
fastPasteCounterRef,
);
} else {
buffer.insert(textToInsert, { paste: true });
}
}
} catch (error) {
debugLogger.error('Error handling paste:', error);
Expand Down Expand Up @@ -520,6 +578,16 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
}, 40);
}
// Ensure we never accidentally interpret paste as regular input.
// OPTIMIZATION: Intercept large pastes BEFORE buffer processing
if (key.sequence && key.sequence.length > 500) {
handleLargePaste(
key.sequence,
buffer,
fastPasteContentRef,
fastPasteCounterRef,
);
return true;
}
buffer.handleInput(key);
return true;
}
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/ui/components/messages/UserMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => {

const displayText = useMemo(() => {
if (!text) return text;

// Truncate very long messages to prevent terminal freeze during rendering
const MAX_DISPLAY_LENGTH = 2000;
const MAX_LINES = 50;

if (text.length > MAX_DISPLAY_LENGTH) {
const lineCount = (text.match(/\n/g) || []).length + 1;
const preview = text.substring(0, 200).split('\n').slice(0, 3).join('\n');
return `${preview}\n\n[... Message with ${text.length.toLocaleString()} characters and ${lineCount} lines - sent to model ...]`;
}

const lines = text.split('\n');
if (lines.length > MAX_LINES) {
const previewLines = lines.slice(0, 5);
return `${previewLines.join('\n')}\n\n[... Message with ${lines.length} lines - sent to model ...]`;
}

return text
.split('\n')
.map((line) => {
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/ui/components/shared/text-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3178,6 +3178,13 @@ export function useTextBuffer({
[],
);

const addPastedContent = useCallback(
(id: string, text: string): void => {
dispatch({ type: 'add_pasted_content', payload: { id, text } });
},
[],
);

const getExpandedPasteAtLineCallback = useCallback(
(lineIndex: number): string | null =>
getExpandedPasteAtLine(lineIndex, expandedPaste),
Expand Down Expand Up @@ -3218,6 +3225,7 @@ export function useTextBuffer({
getLogicalPositionFromVisual,
getExpandedPasteAtLine: getExpandedPasteAtLineCallback,
togglePasteExpansion,
addPastedContent,
expandedPaste,
deleteWordLeft,
deleteWordRight,
Expand Down Expand Up @@ -3482,6 +3490,10 @@ export interface TextBuffer {
* If expanded, collapses back to placeholder.
*/
togglePasteExpansion(id: string, row: number, col: number): void;
/**
* Add content to pastedContent map
*/
addPastedContent(id: string, text: string): void;
/**
* The current expanded paste info (read-only).
*/
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/utils/shell-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,58 @@ export function parseCommandDetails(
*/
export function getShellConfiguration(): ShellConfiguration {
if (isWindows()) {
// Prefer Git Bash on Windows for better compatibility with AI-generated commands
// Try multiple detection methods for robustness

// Method 1: Search for bash.exe in PATH
const pathEnv = process.env['PATH'] || '';
const pathDirs = pathEnv.split(path.delimiter);
for (const dir of pathDirs) {
const bashPath = path.join(dir, 'bash.exe');
try {
if (fs.existsSync(bashPath)) {
// Verify it's actually Git Bash by checking parent directories
const normalizedPath = bashPath.toLowerCase();
if (normalizedPath.includes('git')) {
return {
executable: bashPath,
argsPrefix: ['-c'],
shell: 'bash',
};
}
}
} catch {
continue;
}
}

// Method 2: Check common Git for Windows installation paths
const commonPaths = [
'C:\\Program Files\\Git\\bin\\bash.exe',
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
process.env['PROGRAMFILES']
? `${process.env['PROGRAMFILES']}\\Git\\bin\\bash.exe`
: null,
process.env['PROGRAMFILES(X86)']
? `${process.env['PROGRAMFILES(X86)']}\\Git\\bin\\bash.exe`
: null,
].filter((p): p is string => p !== null);

for (const bashPath of commonPaths) {
try {
if (fs.existsSync(bashPath)) {
return {
executable: bashPath,
argsPrefix: ['-c'],
shell: 'bash',
};
}
} catch {
continue;
}
}

// Fallback to PowerShell if Git Bash not found
const comSpec = process.env['ComSpec'];
if (comSpec) {
const executable = comSpec.toLowerCase();
Expand Down