From 884713cca594e29dff4c9f60b736cb5b34bcac20 Mon Sep 17 00:00:00 2001 From: DodgyBadger <4453217+DodgyBadger@users.noreply.github.com> Date: Sun, 20 Sep 2026 07:21:20 +0000 Subject: [PATCH] fix(mobile): retain oversized composer drafts --- src/web/public/keyboard-accessory.js | 10 +++++++++- test/mobile-prompt-composer.test.ts | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/web/public/keyboard-accessory.js b/src/web/public/keyboard-accessory.js index 9b95e79d..bf0d3cd5 100644 --- a/src/web/public/keyboard-accessory.js +++ b/src/web/public/keyboard-accessory.js @@ -654,6 +654,9 @@ const KeyboardAccessoryBar = { _composerDrafts: new Map(), _composerUploads: new Map(), _composerOverlay: null, + // Leave room for both six-character bracketed-paste markers under the + // WebSocket input frame's 64 KiB character limit. + _composerMaxLength: 65524, /** HTML for simple mode: arrows, commands, Compose, Esc, dismiss */ _simpleButtons: ` @@ -1193,7 +1196,12 @@ const KeyboardAccessoryBar = { // Match xterm's prepareTextForTerminal(): CR keeps embedded newlines inside // the single-line input transport and is what terminal.paste() emitted. const pasteText = text.replace(/\r?\n/g, '\r'); - app._sendInputAsync(sessionId, `\x1b[200~${pasteText}\x1b[201~`); + const payload = `\x1b[200~${pasteText}\x1b[201~`; + if (payload.length > 65536) { + app.showToast?.(`Prompt is too long to send (maximum ${this._composerMaxLength.toLocaleString()} characters)`, 'error'); + return false; + } + app._sendInputAsync(sessionId, payload); setTimeout(() => app._sendInputAsync(sessionId, '\r', { useMux: true }), 120); return true; }, diff --git a/test/mobile-prompt-composer.test.ts b/test/mobile-prompt-composer.test.ts index 0dc98aeb..43c1d777 100644 --- a/test/mobile-prompt-composer.test.ts +++ b/test/mobile-prompt-composer.test.ts @@ -269,6 +269,21 @@ describe('mobile prompt composer', () => { expect(app._echoPassthroughSessions.has('session-2')).toBe(true); }); + it('keeps an oversized prompt as a draft instead of queueing a rejected frame', () => { + const { app, bar, document } = loadComposer(); + bar.composePrompt(); + const input = textarea(document); + input.value = 'x'.repeat(65525); + input.dispatchEvent(new document.defaultView!.Event('input', { bubbles: true })); + + (document.querySelector('.paste-send') as HTMLButtonElement).click(); + + expect(app._sendInputAsync).not.toHaveBeenCalled(); + expect(app.showToast).toHaveBeenCalledWith(expect.stringContaining('too long'), 'error'); + expect(document.querySelector('.prompt-composer-overlay')).not.toBeNull(); + expect(bar._composerDrafts.get('session-1')).toHaveLength(65525); + }); + it('preserves the draft and focuses xterm when Use terminal keyboard is chosen', () => { const { app, bar, document, localEcho, runTimers } = loadComposer(); const composeButton = mountComposeButton(bar, document);