Fix browser terminal paste for hosting clients.
Deploy Ladill Hosting / deploy (push) Successful in 42s

Use bracketed paste, reliably bind paste capture, stop blocking mousedown focus, and give shells a short prompt with horizontal-scroll readline so long pasted commands do not wrap incorrectly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-13 18:02:02 +00:00
co-authored by Cursor
parent 335c0fea61
commit a3ba2ac45b
3 changed files with 55 additions and 7 deletions
@@ -183,6 +183,14 @@ class RunHostingTerminalWorkerCommand extends Command
? $homeDirectory ? $homeDirectory
: $homeDirectory.$relativePath; : $homeDirectory.$relativePath;
// Short \W prompt + bracketed paste keep long pasted commands readable in
// the narrow browser terminal used by all hosting clients.
$inputrc = implode("\n", [
'set enable-bracketed-paste on',
'set horizontal-scroll-mode on',
'set blink-matching-paren on',
]);
return implode('; ', [ return implode('; ', [
'export HOME='.escapeshellarg($homeDirectory), 'export HOME='.escapeshellarg($homeDirectory),
'export USER='.escapeshellarg($account->username), 'export USER='.escapeshellarg($account->username),
@@ -192,7 +200,9 @@ class RunHostingTerminalWorkerCommand extends Command
'export COLORTERM=truecolor', 'export COLORTERM=truecolor',
'export COLUMNS='.(int) $cols, 'export COLUMNS='.(int) $cols,
'export LINES='.(int) $rows, 'export LINES='.(int) $rows,
'export PS1='.escapeshellarg($account->username.'@ladill:\w\$ '), 'export PS1='.escapeshellarg($account->username.'@ladill:\W\$ '),
'printf %s '.escapeshellarg($inputrc).' > /tmp/ladill-term-inputrc-$$',
'export INPUTRC=/tmp/ladill-term-inputrc-$$',
'cd '.escapeshellarg($workingDirectory).' 2>/dev/null || cd '.escapeshellarg($homeDirectory), 'cd '.escapeshellarg($workingDirectory).' 2>/dev/null || cd '.escapeshellarg($homeDirectory),
'stty rows '.(int) $rows.' cols '.(int) $cols.' echo 2>/dev/null || true', 'stty rows '.(int) $rows.' cols '.(int) $cols.' echo 2>/dev/null || true',
'exec /bin/bash --noprofile --norc -i', 'exec /bin/bash --noprofile --norc -i',
+40 -5
View File
@@ -101,10 +101,11 @@ window.hostingInteractiveTerminal = (config = {}) => {
this.queueInput(data); this.queueInput(data);
} }
}); });
// Delay initial fit to ensure container has dimensions // Delay initial fit to ensure container has dimensions
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.fitAddon.fit(); this.fitAddon.fit();
this.bindTerminalInputCapture();
this.term.focus(); this.term.focus();
}); });
@@ -113,7 +114,7 @@ window.hostingInteractiveTerminal = (config = {}) => {
this.resizeObserver = new ResizeObserver(() => this.queueResize()); this.resizeObserver = new ResizeObserver(() => this.queueResize());
this.resizeObserver.observe(this.$refs.terminal); this.resizeObserver.observe(this.$refs.terminal);
} }
window.addEventListener('resize', () => this.queueResize()); window.addEventListener('resize', () => this.queueResize());
window.addEventListener('beforeunload', () => this.shutdown(true)); window.addEventListener('beforeunload', () => this.shutdown(true));
@@ -256,7 +257,23 @@ window.hostingInteractiveTerminal = (config = {}) => {
}, },
handleTerminalPaste(event) { handleTerminalPaste(event) {
if (!this.shouldCaptureTerminalInput(event)) { if (!this.sessionId) {
return;
}
const terminalElement = this.$refs.terminal;
const target = event.target;
const inTerminal = Boolean(
terminalElement
&& target instanceof Node
&& (terminalElement === target || terminalElement.contains(target))
);
const termFocused = Boolean(
this.term?.textarea && document.activeElement === this.term.textarea
);
// Panel paste often targets the wrapper/div, not the xterm textarea.
if (!this.terminalActive && !inTerminal && !termFocused) {
return; return;
} }
@@ -266,14 +283,21 @@ window.hostingInteractiveTerminal = (config = {}) => {
return; return;
} }
this.terminalActive = true;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
event.stopImmediatePropagation?.(); event.stopImmediatePropagation?.();
this.queueInput(text.replace(/\r?\n/g, '\r'));
// Bracketed paste keeps bash/readline from mangling long pasted commands.
const normalized = text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\n/g, '\r');
this.queueInput(`\x1b[200~${normalized}\x1b[201~`);
}, },
shouldCaptureTerminalInput(event) { shouldCaptureTerminalInput(event) {
if (!this.sessionId || !this.terminalActive) { if (!this.sessionId) {
return false; return false;
} }
@@ -281,9 +305,19 @@ window.hostingInteractiveTerminal = (config = {}) => {
const terminalElement = this.$refs.terminal; const terminalElement = this.$refs.terminal;
if (terminalElement && target instanceof Node && terminalElement.contains(target)) { if (terminalElement && target instanceof Node && terminalElement.contains(target)) {
this.terminalActive = true;
return true; return true;
} }
if (this.term?.textarea && document.activeElement === this.term.textarea) {
this.terminalActive = true;
return true;
}
if (!this.terminalActive) {
return false;
}
if (!(target instanceof HTMLElement)) { if (!(target instanceof HTMLElement)) {
return false; return false;
} }
@@ -535,6 +569,7 @@ window.hostingInteractiveTerminal = (config = {}) => {
console.log('[Terminal] focusTerminal called'); console.log('[Terminal] focusTerminal called');
if (this.term) { if (this.term) {
this.terminalActive = true; this.terminalActive = true;
this.bindTerminalInputCapture();
this.$refs.terminal?.focus(); this.$refs.terminal?.focus();
this.term.focus(); this.term.focus();
console.log('[Terminal] terminalActive set to true'); console.log('[Terminal] terminalActive set to true');
@@ -57,7 +57,7 @@
<div class="relative min-h-0 flex-1 overflow-hidden"> <div class="relative min-h-0 flex-1 overflow-hidden">
<div <div
x-ref="terminal" x-ref="terminal"
@mousedown.prevent="focusTerminal()" @mousedown="focusTerminal()"
@click="focusTerminal()" @click="focusTerminal()"
tabindex="0" tabindex="0"
class="h-full w-full cursor-text bg-[#0a0a0a] p-2" class="h-full w-full cursor-text bg-[#0a0a0a] p-2"
@@ -89,6 +89,9 @@
<span class="font-mono">bash</span> <span class="font-mono">bash</span>
</div> </div>
<div class="flex items-center gap-1 text-[10px] text-white/25"> <div class="flex items-center gap-1 text-[10px] text-white/25">
<kbd class="rounded bg-white/5 px-1 py-0.5 font-mono">Ctrl/Cmd+V</kbd>
<span>paste</span>
<span class="mx-2 text-white/10">|</span>
<kbd class="rounded bg-white/5 px-1 py-0.5 font-mono">Ctrl+C</kbd> <kbd class="rounded bg-white/5 px-1 py-0.5 font-mono">Ctrl+C</kbd>
<span>cancel</span> <span>cancel</span>
<span class="mx-2 text-white/10">|</span> <span class="mx-2 text-white/10">|</span>