Merge pull request #1713 from QwenLM/fix/enter-submit

feat(paste): add large paste placeholder and fix enter-submit on macOS
This commit is contained in:
pomelo 2026-02-06 17:44:14 +08:00 committed by GitHub
commit a6885ccb4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 444 additions and 7 deletions

View file

@ -2133,6 +2133,291 @@ describe('InputPrompt', () => {
expect(mockBuffer.handleInput).toHaveBeenCalled();
unmount();
});
describe('large paste placeholder', () => {
it('should create placeholder for paste > 1000 characters', async () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
// Create a paste with 1001 characters
const largeContent = 'x'.repeat(1001);
// Simulate bracketed paste
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Verify placeholder was inserted, not the full content
expect(mockBuffer.insert).toHaveBeenCalledWith(
'[Pasted Content 1001 chars]',
{ paste: false },
);
expect(mockBuffer.insert).toHaveBeenCalledTimes(1);
unmount();
});
it('should create placeholder for paste > 10 lines', async () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
// Create a paste with 11 lines (each line is short)
const multiLineContent = Array(11).fill('line').join('\n');
// Simulate bracketed paste
stdin.write(`\x1b[200~${multiLineContent}\x1b[201~`);
await wait();
// Verify placeholder was inserted
expect(mockBuffer.insert).toHaveBeenCalledWith(
expect.stringMatching(/\[Pasted Content \d+ chars\]/),
{ paste: false },
);
unmount();
});
it('should use sequential IDs for multiple pastes of same size', async () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const largeContent = 'x'.repeat(1001);
// First paste
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Second paste
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Verify both placeholders were created with correct IDs
expect(mockBuffer.insert).toHaveBeenCalledWith(
'[Pasted Content 1001 chars]',
{ paste: false },
);
expect(mockBuffer.insert).toHaveBeenCalledWith(
'[Pasted Content 1001 chars] #2',
{ paste: false },
);
unmount();
});
it('should expand placeholder to full content on submit', async () => {
const largeContent = 'x'.repeat(1001);
mockBuffer.text = '[Pasted Content 1001 chars]';
mockBuffer.lines = [mockBuffer.text];
mockBuffer.cursor = [0, mockBuffer.text.length];
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
// First paste to set up the placeholder
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Wait for paste protection to expire
await new Promise((resolve) => setTimeout(resolve, 600));
// Submit the input
stdin.write('\r');
await wait();
// Verify onSubmit was called with expanded content
expect(props.onSubmit).toHaveBeenCalledWith(largeContent);
unmount();
});
it('should expand same-size placeholders correctly when #2 appears first', async () => {
const firstPaste = 'x'.repeat(1001);
const secondPaste = 'y'.repeat(1001);
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
stdin.write(`\x1b[200~${firstPaste}\x1b[201~`);
await wait();
stdin.write(`\x1b[200~${secondPaste}\x1b[201~`);
await wait();
mockBuffer.text =
'[Pasted Content 1001 chars] #2\n[Pasted Content 1001 chars]';
mockBuffer.lines = mockBuffer.text.split('\n');
mockBuffer.cursor = [1, '[Pasted Content 1001 chars]'.length];
// Wait for paste protection to expire
await new Promise((resolve) => setTimeout(resolve, 600));
stdin.write('\r');
await wait();
expect(props.onSubmit).toHaveBeenCalledWith(
`${secondPaste}\n${firstPaste}`,
);
unmount();
});
it('should write expanded placeholder content to shell history', async () => {
props.shellModeActive = true;
const largeContent = 'x'.repeat(1001);
mockBuffer.text = '[Pasted Content 1001 chars]';
mockBuffer.lines = [mockBuffer.text];
mockBuffer.cursor = [0, mockBuffer.text.length];
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Wait for paste protection to expire
await new Promise((resolve) => setTimeout(resolve, 600));
stdin.write('\r');
await wait();
expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith(
largeContent,
);
expect(props.onSubmit).toHaveBeenCalledWith(largeContent);
unmount();
});
it('should delete entire placeholder on backspace', async () => {
const placeholderText = '[Pasted Content 1001 chars]';
mockBuffer.text = placeholderText;
mockBuffer.lines = [placeholderText];
mockBuffer.cursor = [0, placeholderText.length];
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
// First set up a placeholder via paste
const largeContent = 'x'.repeat(1001);
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Press backspace to delete the placeholder
stdin.write('\x7f'); // backspace character
await wait();
// Verify replaceRangeByOffset was called to delete entire placeholder
expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalledWith(
0,
placeholderText.length,
'',
);
unmount();
});
it('should reuse placeholder ID after deletion', async () => {
// Set up mocks that actually update buffer state
vi.mocked(mockBuffer.insert).mockImplementation((text: string) => {
mockBuffer.text += text;
mockBuffer.lines = [mockBuffer.text];
mockBuffer.cursor = [0, mockBuffer.text.length];
});
vi.mocked(mockBuffer.replaceRangeByOffset).mockImplementation(
(start: number, end: number, replacement: string) => {
mockBuffer.text =
mockBuffer.text.slice(0, start) +
replacement +
mockBuffer.text.slice(end);
mockBuffer.lines = [mockBuffer.text];
mockBuffer.cursor = [0, start];
},
);
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const largeContent = 'x'.repeat(1001);
// First paste - gets ID 1
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Verify first placeholder was inserted
expect(mockBuffer.text).toBe('[Pasted Content 1001 chars]');
// Press backspace to delete the placeholder (cursor is at end of placeholder)
stdin.write('\x7f');
await wait();
// Verify the placeholder was deleted (buffer is now empty)
expect(mockBuffer.text).toBe('');
// Second paste - should reuse ID 1 since the first was deleted
stdin.write(`\x1b[200~${largeContent}\x1b[201~`);
await wait();
// Verify the ID was reused (no #2 suffix)
const insertCalls = vi.mocked(mockBuffer.insert).mock.calls;
const lastCall = insertCalls[insertCalls.length - 1];
expect(lastCall[0]).toBe('[Pasted Content 1001 chars]');
unmount();
});
it('should handle mixed pastes with different character counts', async () => {
const { stdin, unmount } = renderWithProviders(
<InputPrompt {...props} />,
);
await wait();
const content1001 = 'x'.repeat(1001);
const content1500 = 'y'.repeat(1500);
// Paste 1001 chars
stdin.write(`\x1b[200~${content1001}\x1b[201~`);
await wait();
// Paste 1500 chars
stdin.write(`\x1b[200~${content1500}\x1b[201~`);
await wait();
// Paste 1001 chars again (should get ID #2 for 1001)
stdin.write(`\x1b[200~${content1001}\x1b[201~`);
await wait();
// Verify placeholders with correct IDs
expect(mockBuffer.insert).toHaveBeenCalledWith(
'[Pasted Content 1001 chars]',
{ paste: false },
);
expect(mockBuffer.insert).toHaveBeenCalledWith(
'[Pasted Content 1500 chars]',
{ paste: false },
);
expect(mockBuffer.insert).toHaveBeenCalledWith(
'[Pasted Content 1001 chars] #2',
{ paste: false },
);
unmount();
});
});
});
function clean(str: string | undefined): string {
if (!str) return '';