Merge remote-tracking branch 'origin/main'

# Conflicts:
#	web/src/client/components/app-header.ts
#	web/src/client/components/session-card.ts
#	web/src/client/components/session-view.ts
This commit is contained in:
Peter Steinberger 2025-06-17 01:06:52 +02:00
commit 8b3833ee0e
3 changed files with 251 additions and 308 deletions

View file

@ -18,12 +18,12 @@ export class AppHeader extends LitElement {
private handleKillAll() {
if (this.killingAll) return;
this.killingAll = true;
this.requestUpdate();
this.dispatchEvent(new CustomEvent('kill-all-sessions'));
// Reset the state after a delay to allow for the kill operations to complete
setTimeout(() => {
this.killingAll = false;
@ -32,8 +32,8 @@ export class AppHeader extends LitElement {
}
render() {
const runningSessions = this.sessions.filter((session) => session.status === 'running');
const runningSessions = this.sessions.filter(session => session.status === 'running');
// Reset killing state if no more running sessions
if (this.killingAll && runningSessions.length === 0) {
this.killingAll = false;
@ -41,64 +41,48 @@ export class AppHeader extends LitElement {
return html`
<div class="p-4 border-b border-vs-border">
<div class="flex items-center justify-between">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div class="text-vs-user font-mono text-sm">VibeTunnel</div>
<div class="flex items-center gap-3">
<label
class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors"
>
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
<label class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors">
<div class="relative">
<input
type="checkbox"
class="sr-only"
.checked=${this.hideExited}
@change=${(e: Event) =>
this.dispatchEvent(
new CustomEvent('hide-exited-change', {
detail: (e.target as HTMLInputElement).checked,
})
)}
/>
<div
class="w-4 h-4 border border-vs-border rounded bg-vs-bg-secondary flex items-center justify-center transition-all ${this
.hideExited
? 'bg-vs-user border-vs-user'
: 'hover:border-vs-accent'}"
@change=${(e: Event) => this.dispatchEvent(new CustomEvent('hide-exited-change', { detail: (e.target as HTMLInputElement).checked }))}
>
${this.hideExited
? html`
<svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
`
: ''}
<div class="w-4 h-4 border border-vs-border rounded bg-vs-bg-secondary flex items-center justify-center transition-all ${
this.hideExited ? 'bg-vs-user border-vs-user' : 'hover:border-vs-accent'
}">
${this.hideExited ? html`
<svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"></path>
</svg>
` : ''}
</div>
</div>
hide exited
</label>
${runningSessions.length > 0 && !this.killingAll
? html`
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-4 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleKillAll}
>
KILL ALL (${runningSessions.length})
</button>
`
: ''}
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleCreateSession}
>
CREATE SESSION
</button>
<div class="flex gap-2">
${runningSessions.length > 0 && !this.killingAll ? html`
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 sm:px-4 py-2 border-none rounded transition-colors text-sm whitespace-nowrap"
@click=${this.handleKillAll}
>
KILL ALL (${runningSessions.length})
</button>
` : ''}
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 sm:px-4 py-2 border-none rounded transition-colors text-sm whitespace-nowrap"
@click=${this.handleCreateSession}
>
CREATE SESSION
</button>
</div>
</div>
</div>
</div>
`;
}
}
}

View file

@ -1,4 +1,4 @@
import { LitElement, html, PropertyValues } from 'lit';
import { LitElement, html, css, PropertyValues } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { Renderer } from '../renderer.js';
@ -35,6 +35,7 @@ export class SessionCard extends LitElement {
this.startRefresh();
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.refreshInterval) {
@ -96,13 +97,11 @@ export class SessionCard extends LitElement {
}
private handleCardClick() {
this.dispatchEvent(
new CustomEvent('session-select', {
detail: this.session,
bubbles: true,
composed: true,
})
);
this.dispatchEvent(new CustomEvent('session-select', {
detail: this.session,
bubbles: true,
composed: true
}));
}
private async handleKillClick(e: Event) {
@ -120,7 +119,7 @@ export class SessionCard extends LitElement {
// Send kill request
try {
const response = await fetch(`/api/sessions/${this.session.id}`, {
method: 'DELETE',
method: 'DELETE'
});
if (!response.ok) {
@ -181,69 +180,60 @@ export class SessionCard extends LitElement {
}
render() {
const _isRunning = this.session.status === 'running';
const isRunning = this.session.status === 'running';
return html`
<div
class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden ${this
.killing
? 'opacity-60'
: ''}"
@click=${this.handleCardClick}
>
<div class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden ${this.killing ? 'opacity-60' : ''}"
@click=${this.handleCardClick}>
<!-- Compact Header -->
<div class="flex justify-between items-center px-3 py-2 border-b border-vs-border">
<div class="text-vs-text text-xs font-mono truncate pr-2 flex-1">
${this.session.command}
<div class="text-vs-text text-xs font-mono pr-2 flex-1 min-w-0">
<div class="truncate" title="${this.session.command}">${this.session.command}</div>
</div>
${this.session.status === 'running'
? html`
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-0.5 border-none text-xs disabled:opacity-50 flex-shrink-0 rounded"
@click=${this.handleKillClick}
?disabled=${this.killing}
>
${this.killing ? 'killing...' : 'kill'}
</button>
`
: ''}
${this.session.status === 'running' ? html`
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-2 py-0.5 border-none text-xs disabled:opacity-50 flex-shrink-0 rounded"
@click=${this.handleKillClick}
?disabled=${this.killing}
>
${this.killing ? 'killing...' : 'kill'}
</button>
` : ''}
</div>
<!-- XTerm renderer (main content) -->
<div class="session-preview bg-black overflow-hidden" style="aspect-ratio: 640/480;">
${this.killing
? html`
<div class="w-full h-full flex items-center justify-center text-vs-warning">
<div class="text-center font-mono">
<div class="text-4xl mb-2">${this.getKillingText()}</div>
<div class="text-sm">Killing session...</div>
</div>
</div>
`
: html` <div id="player" class="w-full h-full"></div> `}
${this.killing ? html`
<div class="w-full h-full flex items-center justify-center text-vs-warning">
<div class="text-center font-mono">
<div class="text-4xl mb-2">${this.getKillingText()}</div>
<div class="text-sm">Killing session...</div>
</div>
</div>
` : html`
<div id="player" class="w-full h-full"></div>
`}
</div>
<!-- Compact Footer -->
<div class="px-3 py-2 text-vs-muted text-xs border-t border-vs-border">
<div class="flex justify-between items-center">
<span class="${this.getStatusColor()} text-xs flex items-center gap-1">
<div class="flex justify-between items-center min-w-0">
<span class="${this.getStatusColor()} text-xs flex items-center gap-1 flex-shrink-0">
<div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div>
${this.getStatusText()}
</span>
${this.session.pid
? html`
<span
class="cursor-pointer hover:text-vs-accent transition-colors"
@click=${this.handlePidClick}
title="Click to copy PID"
>
PID: ${this.session.pid} <span class="opacity-50">(click to copy)</span>
</span>
`
: ''}
${this.session.pid ? html`
<span
class="cursor-pointer hover:text-vs-accent transition-colors text-xs flex-shrink-0 ml-2"
@click=${this.handlePidClick}
title="Click to copy PID"
>
PID: ${this.session.pid} <span class="opacity-50">(click to copy)</span>
</span>
` : ''}
</div>
<div class="truncate text-xs opacity-75" title="${this.session.workingDir}">
${this.session.workingDir}
<div class="text-xs opacity-75 min-w-0 mt-1">
<div class="truncate" title="${this.session.workingDir}">${this.session.workingDir}</div>
</div>
</div>
</div>
@ -270,4 +260,5 @@ export class SessionCard extends LitElement {
}
return this.session.status === 'running' ? 'bg-green-500' : 'bg-orange-500';
}
}
}

View file

@ -78,9 +78,8 @@ export class SessionView extends LitElement {
}
// Detect mobile device
this.isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768;
this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
window.innerWidth <= 768;
// Only add listeners if not already added
if (!this.isMobile && !this.keyboardListenerAdded) {
@ -92,6 +91,7 @@ export class SessionView extends LitElement {
document.addEventListener('touchend', this.touchEndHandler, { passive: true });
this.touchListenersAdded = true;
}
}
disconnectedCallback() {
@ -113,6 +113,7 @@ export class SessionView extends LitElement {
this.touchListenersAdded = false;
}
// Stop loading animation
this.stopLoading();
@ -157,7 +158,7 @@ export class SessionView extends LitElement {
// Create the interactive terminal div inside the container
const container = this.querySelector('#terminal-container') as HTMLElement;
if (!container) return;
terminalElement = document.createElement('div');
terminalElement.id = 'interactive-terminal';
terminalElement.className = 'w-full h-full';
@ -185,10 +186,7 @@ export class SessionView extends LitElement {
}, delay);
// Listen for session exit events
terminalElement.addEventListener(
'session-exit',
this.handleSessionExit.bind(this) as EventListener
);
terminalElement.addEventListener('session-exit', this.handleSessionExit.bind(this) as EventListener);
}
private async handleKeyboardInput(e: KeyboardEvent) {
@ -202,10 +200,10 @@ export class SessionView extends LitElement {
// Handle clipboard shortcuts: Cmd+C/V on macOS, Shift+Ctrl+C/V on Linux/Windows
const isMacOS = navigator.platform.toLowerCase().includes('mac');
const isPasteShortcut =
const isPasteShortcut =
(isMacOS && e.metaKey && e.key === 'v' && !e.ctrlKey && !e.shiftKey) ||
(!isMacOS && e.ctrlKey && e.shiftKey && e.key === 'V');
const isCopyShortcut =
const isCopyShortcut =
(isMacOS && e.metaKey && e.key === 'c' && !e.ctrlKey && !e.shiftKey) ||
(!isMacOS && e.ctrlKey && e.shiftKey && e.key === 'C');
@ -276,8 +274,7 @@ export class SessionView extends LitElement {
// Handle Ctrl combinations (but not if we already handled Ctrl+Enter above)
if (e.ctrlKey && e.key.length === 1 && e.key !== 'Enter') {
const charCode = e.key.toLowerCase().charCodeAt(0);
if (charCode >= 97 && charCode <= 122) {
// a-z
if (charCode >= 97 && charCode <= 122) { // a-z
inputText = String.fromCharCode(charCode - 96); // Ctrl+A = \x01, etc.
}
}
@ -287,9 +284,9 @@ export class SessionView extends LitElement {
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ text: inputText }),
body: JSON.stringify({ text: inputText })
});
if (!response.ok) {
@ -322,6 +319,7 @@ export class SessionView extends LitElement {
this.session = { ...this.session, status: 'exited' };
this.requestUpdate();
// Switch to snapshot mode
requestAnimationFrame(() => {
this.createInteractiveTerminal();
@ -368,9 +366,7 @@ export class SessionView extends LitElement {
controls.style.transition = 'transform 0.3s ease';
// Calculate available space for textarea
const header = this.querySelector(
'.flex.items-center.justify-between.p-4.border-b'
) as HTMLElement;
const header = this.querySelector('.flex.items-center.justify-between.p-4.border-b') as HTMLElement;
const headerHeight = header?.offsetHeight || 60;
const controlsHeight = controls?.offsetHeight || 120;
const padding = 48; // Additional padding for spacing
@ -505,13 +501,14 @@ export class SessionView extends LitElement {
}
};
private async handlePaste() {
if (!this.session) return;
try {
// Try clipboard API first (requires user activation)
const clipboardText = await navigator.clipboard.readText();
if (clipboardText) {
// Send the clipboard text to the terminal
await this.sendInputText(clipboardText);
@ -520,7 +517,7 @@ export class SessionView extends LitElement {
console.error('Failed to read from clipboard:', error);
// Show user a message about using Ctrl+V instead
console.log('Tip: Try using Ctrl+V (Cmd+V on Mac) to paste instead');
// Fallback: try to use the older document.execCommand method
try {
const textArea = document.createElement('textarea');
@ -530,14 +527,14 @@ export class SessionView extends LitElement {
textArea.style.top = '-9999px';
document.body.appendChild(textArea);
textArea.focus();
if (document.execCommand('paste')) {
const pastedText = textArea.value;
if (pastedText) {
await this.sendInputText(pastedText);
}
}
document.body.removeChild(textArea);
} catch (fallbackError) {
console.error('Fallback paste method also failed:', fallbackError);
@ -552,17 +549,14 @@ export class SessionView extends LitElement {
try {
// Get the terminal instance from the renderer
const terminal = this.renderer.getTerminal();
// Get the selected text from the terminal
const selectedText = terminal.getSelection();
if (selectedText) {
// Write the selected text to clipboard
await navigator.clipboard.writeText(selectedText);
console.log(
'Text copied to clipboard:',
selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : '')
);
console.log('Text copied to clipboard:', selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : ''));
} else {
console.log('No text selected for copying');
}
@ -573,7 +567,7 @@ export class SessionView extends LitElement {
if (this.renderer) {
const terminal = this.renderer.getTerminal();
const selectedText = terminal.getSelection();
if (selectedText) {
const textArea = document.createElement('textarea');
textArea.value = selectedText;
@ -581,14 +575,11 @@ export class SessionView extends LitElement {
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
if (document.execCommand('copy')) {
console.log(
'Text copied to clipboard (fallback):',
selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : '')
);
console.log('Text copied to clipboard (fallback):', selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : ''));
}
document.body.removeChild(textArea);
}
}
@ -605,9 +596,9 @@ export class SessionView extends LitElement {
const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ text }),
body: JSON.stringify({ text })
});
if (!response.ok) {
@ -645,45 +636,43 @@ export class SessionView extends LitElement {
return frames[this.loadingFrame % frames.length];
}
render() {
if (!this.session) {
return html` <div class="p-4 text-vs-muted">No session selected</div> `;
return html`
<div class="p-4 text-vs-muted">
No session selected
</div>
`;
}
return html`
<style>
session-view *,
session-view *:focus,
session-view *:focus-visible {
session-view *, session-view *:focus, session-view *:focus-visible {
outline: none !important;
box-shadow: none !important;
}
session-view:focus {
outline: 2px solid #007acc !important;
outline: 2px solid #007ACC !important;
outline-offset: -2px;
}
</style>
<div
class="flex flex-col bg-vs-bg font-mono"
style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;"
>
<div class="flex flex-col bg-vs-bg font-mono" style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;">
<!-- Compact Header -->
<div
class="flex items-center justify-between px-3 py-2 border-b border-vs-border bg-vs-bg-secondary text-sm"
>
<div class="flex items-center gap-3">
<div class="flex items-center justify-between px-3 py-2 border-b border-vs-border bg-vs-bg-secondary text-sm min-w-0">
<div class="flex items-center gap-3 min-w-0 flex-1">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1 border-none rounded transition-colors text-xs"
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-2 py-1 border-none rounded transition-colors text-xs flex-shrink-0"
@click=${this.handleBack}
>
BACK
</button>
<div class="text-vs-text">
<div class="text-vs-accent">${this.session.command}</div>
<div class="text-vs-muted text-xs">${this.session.workingDir}</div>
<div class="text-vs-text min-w-0 flex-1 overflow-hidden">
<div class="text-vs-accent text-xs sm:text-sm overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap" title="${this.session.command}">${this.session.command}</div>
<div class="text-vs-muted text-xs overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap" title="${this.session.workingDir}">${this.session.workingDir}</div>
</div>
</div>
<div class="flex items-center gap-3 text-xs">
<div class="flex items-center gap-3 text-xs flex-shrink-0 ml-2">
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
${this.session.status.toUpperCase()}
</span>
@ -691,168 +680,147 @@ export class SessionView extends LitElement {
</div>
<!-- Terminal Container -->
<div
class="flex-1 bg-black overflow-hidden min-h-0 relative"
id="terminal-container"
style="max-width: 100vw; height: 100%;"
>
${this.loading
? html`
<!-- Loading overlay -->
<div
class="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center"
>
<div class="text-vs-text font-mono text-center">
<div class="text-2xl mb-2">${this.getLoadingText()}</div>
<div class="text-sm text-vs-muted">Connecting to session...</div>
</div>
</div>
`
: ''}
<div class="flex-1 bg-black overflow-hidden min-h-0 relative" id="terminal-container" style="max-width: 100vw; height: 100%;">
${this.loading ? html`
<!-- Loading overlay -->
<div class="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center">
<div class="text-vs-text font-mono text-center">
<div class="text-2xl mb-2">${this.getLoadingText()}</div>
<div class="text-sm text-vs-muted">Connecting to session...</div>
</div>
</div>
` : ''}
</div>
<!-- Mobile Input Controls -->
${this.isMobile && !this.showMobileInput
? html`
<div class="flex-shrink-0 p-4 bg-vs-bg">
<!-- First row: Arrow keys -->
<div class="flex gap-2 mb-2">
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_up')}
>
<span class="text-xl"></span>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_down')}
>
<span class="text-xl"></span>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_left')}
>
<span class="text-xl"></span>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_right')}
>
<span class="text-xl"></span>
</button>
</div>
<!-- Second row: Special keys -->
<div class="flex gap-2">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\t')}
>
<span class="text-xl"></span>
</button>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('enter')}
>
<span class="text-xl"></span>
</button>
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('escape')}
>
ESC
</button>
<button
class="bg-vs-error text-vs-text hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\x03')}
>
^C
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleMobileInputToggle}
>
TYPE
</button>
</div>
${this.isMobile && !this.showMobileInput ? html`
<div class="flex-shrink-0 p-4 bg-vs-bg">
<!-- First row: Arrow keys -->
<div class="flex gap-2 mb-2">
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_up')}
>
<span class="text-xl"></span>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_down')}
>
<span class="text-xl"></span>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_left')}
>
<span class="text-xl"></span>
</button>
<button
class="flex-1 bg-vs-muted text-vs-bg hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('arrow_right')}
>
<span class="text-xl"></span>
</button>
</div>
`
: ''}
<!-- Second row: Special keys -->
<div class="flex gap-2">
<button
class="bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\t')}
>
<span class="text-xl"></span>
</button>
<button
class="bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('enter')}
>
<span class="text-xl"></span>
</button>
<button
class="bg-vs-warning text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('escape')}
>
ESC
</button>
<button
class="bg-vs-error text-vs-text hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${() => this.handleSpecialKey('\x03')}
>
^C
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-3 py-2 border-none rounded transition-colors text-sm"
@click=${this.handleMobileInputToggle}
>
TYPE
</button>
</div>
</div>
` : ''}
<!-- Full-Screen Input Overlay (only when opened) -->
${this.isMobile && this.showMobileInput
? html`
<div
class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col"
style="height: 100vh; height: 100dvh;"
>
<!-- Input Header -->
<div
class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0"
${this.isMobile && this.showMobileInput ? html`
<div class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col" style="height: 100vh; height: 100dvh;">
<!-- Input Header -->
<div class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0">
<div class="text-vs-text font-mono text-sm">Terminal Input</div>
<button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
@click=${this.handleMobileInputToggle}
>
<div class="text-vs-text font-mono text-sm">Terminal Input</div>
×
</button>
</div>
<!-- Input Area with dynamic height -->
<div class="flex-1 p-4 flex flex-col min-h-0">
<div class="text-vs-muted text-sm mb-2 flex-shrink-0">
Type your command(s) below. Supports multiline input.
</div>
<textarea
id="mobile-input-textarea"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border font-mono text-sm p-4 resize-none outline-none"
placeholder="Enter your command here..."
.value=${this.mobileInputText}
@input=${this.handleMobileInputChange}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleMobileInputSend();
}
}}
style="min-height: 120px; margin-bottom: 16px;"
></textarea>
</div>
<!-- Controls - Fixed above keyboard -->
<div id="mobile-controls" class="fixed bottom-0 left-0 right-0 p-4 border-t border-vs-border bg-vs-bg-secondary z-60" style="padding-bottom: max(1rem, env(safe-area-inset-bottom)); transform: translateY(0px);">
<!-- Send Buttons Row -->
<div class="flex gap-2 mb-3">
<button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
@click=${this.handleMobileInputToggle}
class="flex-1 bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSendOnly}
?disabled=${!this.mobileInputText.trim()}
>
×
SEND
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSend}
?disabled=${!this.mobileInputText.trim()}
>
SEND + ENTER
</button>
</div>
<!-- Input Area with dynamic height -->
<div class="flex-1 p-4 flex flex-col min-h-0">
<div class="text-vs-muted text-sm mb-2 flex-shrink-0">
Type your command(s) below. Supports multiline input.
</div>
<textarea
id="mobile-input-textarea"
class="flex-1 bg-vs-bg text-vs-text border border-vs-border font-mono text-sm p-4 resize-none outline-none"
placeholder="Enter your command here..."
.value=${this.mobileInputText}
@input=${this.handleMobileInputChange}
@keydown=${(e: KeyboardEvent) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.handleMobileInputSend();
}
}}
style="min-height: 120px; margin-bottom: 16px;"
></textarea>
</div>
<!-- Controls - Fixed above keyboard -->
<div
id="mobile-controls"
class="fixed bottom-0 left-0 right-0 p-4 border-t border-vs-border bg-vs-bg-secondary z-60"
style="padding-bottom: max(1rem, env(safe-area-inset-bottom)); transform: translateY(0px);"
>
<!-- Send Buttons Row -->
<div class="flex gap-2 mb-3">
<button
class="flex-1 bg-vs-user text-vs-text hover:bg-vs-accent font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSendOnly}
?disabled=${!this.mobileInputText.trim()}
>
SEND
</button>
<button
class="flex-1 bg-vs-function text-vs-bg hover:bg-vs-highlight font-mono px-4 py-3 border-none rounded transition-colors text-sm font-bold"
@click=${this.handleMobileInputSend}
?disabled=${!this.mobileInputText.trim()}
>
SEND + ENTER
</button>
</div>
<div class="text-vs-muted text-xs text-center">
SEND: text only SEND + ENTER: text with enter key
</div>
<div class="text-vs-muted text-xs text-center">
SEND: text only SEND + ENTER: text with enter key
</div>
</div>
`
: ''}
</div>
` : ''}
</div>
`;
}
}
}