Fix browser terminal paste for hosting clients.
Deploy Ladill Hosting / deploy (push) Successful in 42s
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:
@@ -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',
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ window.hostingInteractiveTerminal = (config = {}) => {
|
|||||||
// 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user