mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(mobile): shell Paste inserts a multi-line block as one bracketed paste
The shell accessory bar's Paste overlay sent its text raw and then an Enter (`app.sendInput(text)` + a delayed `\r`), so every newline in a multi-line paste reached the shell as Enter and each line ran on its own the moment it arrived, followed by one more Enter. Send it the way a terminal paste does instead: one write wrapped in ESC[200~ ... ESC[201~ with newlines as CR, through the same path Compose already uses (its bracketed write is extracted into `_sendBracketedPaste`, unchanged for Compose). Paste never presses Enter: the block lands at the prompt for the user to review and run. The overlay is only reached for shell sessions (the shell bar's Paste button, and Compose redirecting a shell), so agent sessions are unaffected. Also: a paste over the input-frame budget is refused with a toast and keeps its text for trimming (the raw path could exceed the WS frame limit), and the paste goes to the session the overlay was opened for, not whichever tab is active at Send. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa3730aace
commit
d157911166
2 files changed
+98
-6
No files matched your search
@@ -437,3 +437,78 @@ describe('mobile prompt composer', () => {
|
||||
expect((document.querySelector('.paste-send') as HTMLButtonElement).disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// The shell bar's Paste button (and Compose, which redirects here for a shell
|
||||
// session) must deliver a multi-line block the way a real terminal paste does:
|
||||
// one bracketed write, so the shell takes the block as one edit instead of
|
||||
// running every line the moment its newline arrives, and no Enter, so the
|
||||
// user reviews the block at the prompt before running it.
|
||||
describe('mobile shell Paste', () => {
|
||||
beforeEach(() => vi.restoreAllMocks());
|
||||
|
||||
function openShellPaste() {
|
||||
const harness = loadComposer();
|
||||
harness.app.sessions.set('session-1', { mode: 'shell' });
|
||||
harness.bar.composePrompt();
|
||||
const input = harness.document.querySelector('.paste-textarea') as HTMLTextAreaElement;
|
||||
const send = harness.document.querySelector('.paste-overlay .paste-send') as HTMLButtonElement;
|
||||
return { ...harness, input, send };
|
||||
}
|
||||
|
||||
it('sends a multi-line block once, bracketed, and never presses Enter', () => {
|
||||
const { app, document, input, send, runTimers } = openShellPaste();
|
||||
expect(document.querySelector('.prompt-composer-overlay')).toBeNull();
|
||||
input.value = 'cd ~/dotfiles\ngit pull\n./sync.sh';
|
||||
|
||||
send.click();
|
||||
runTimers();
|
||||
|
||||
expect(app._sendInputAsync).toHaveBeenCalledOnce();
|
||||
expect(app._sendInputAsync).toHaveBeenCalledWith(
|
||||
'session-1',
|
||||
'\x1b[200~cd ~/dotfiles\rgit pull\r./sync.sh\x1b[201~'
|
||||
);
|
||||
expect(document.querySelector('.paste-overlay')).toBeNull();
|
||||
});
|
||||
|
||||
it('normalizes CRLF line endings to the CR a terminal paste carries', () => {
|
||||
const { app, input, send } = openShellPaste();
|
||||
input.value = 'echo one\r\necho two';
|
||||
|
||||
send.click();
|
||||
|
||||
expect(app._sendInputAsync).toHaveBeenCalledWith('session-1', '\x1b[200~echo one\recho two\x1b[201~');
|
||||
});
|
||||
|
||||
it('refuses a paste longer than one input frame and keeps the text for trimming', () => {
|
||||
const { app, bar, document, input, send } = openShellPaste();
|
||||
input.value = 'x'.repeat(bar._composerMaxLength + 1);
|
||||
|
||||
send.click();
|
||||
|
||||
expect(app._sendInputAsync).not.toHaveBeenCalled();
|
||||
expect(app.showToast).toHaveBeenCalledWith(expect.stringContaining('too long'), 'error');
|
||||
expect(document.querySelector('.paste-overlay')).not.toBeNull();
|
||||
expect(input.value.length).toBe(bar._composerMaxLength + 1);
|
||||
});
|
||||
|
||||
it('delivers to the shell it was opened for after a tab switch', () => {
|
||||
const { app, input, send } = openShellPaste();
|
||||
input.value = 'ls\npwd';
|
||||
app.activeSessionId = 'session-2';
|
||||
|
||||
send.click();
|
||||
|
||||
expect(app._sendInputAsync).toHaveBeenCalledWith('session-1', '\x1b[200~ls\rpwd\x1b[201~');
|
||||
});
|
||||
|
||||
it('sends nothing for an empty paste', () => {
|
||||
const { app, document, send, runTimers } = openShellPaste();
|
||||
|
||||
send.click();
|
||||
runTimers();
|
||||
|
||||
expect(app._sendInputAsync).not.toHaveBeenCalled();
|
||||
expect(document.querySelector('.paste-overlay')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user