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

@ -32,7 +32,7 @@ export class AppHeader extends LitElement {
} }
render() { 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 // Reset killing state if no more running sessions
if (this.killingAll && runningSessions.length === 0) { if (this.killingAll && runningSessions.length === 0) {
@ -41,61 +41,45 @@ export class AppHeader extends LitElement {
return html` return html`
<div class="p-4 border-b border-vs-border"> <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="text-vs-user font-mono text-sm">VibeTunnel</div>
<div class="flex items-center gap-3"> <div class="flex flex-col sm:flex-row sm:items-center gap-3">
<label <label class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors">
class="flex items-center gap-2 text-vs-text text-sm cursor-pointer hover:text-vs-accent transition-colors"
>
<div class="relative"> <div class="relative">
<input <input
type="checkbox" type="checkbox"
class="sr-only" class="sr-only"
.checked=${this.hideExited} .checked=${this.hideExited}
@change=${(e: Event) => @change=${(e: Event) => this.dispatchEvent(new CustomEvent('hide-exited-change', { detail: (e.target as HTMLInputElement).checked }))}
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'}"
> >
${this.hideExited <div class="w-4 h-4 border border-vs-border rounded bg-vs-bg-secondary flex items-center justify-center transition-all ${
? html` this.hideExited ? 'bg-vs-user border-vs-user' : 'hover:border-vs-accent'
<svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20"> }">
<path ${this.hideExited ? html`
fill-rule="evenodd" <svg class="w-3 h-3 text-vs-bg" fill="currentColor" viewBox="0 0 20 20">
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" <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>
clip-rule="evenodd" </svg>
></path> ` : ''}
</svg>
`
: ''}
</div> </div>
</div> </div>
hide exited hide exited
</label> </label>
${runningSessions.length > 0 && !this.killingAll <div class="flex gap-2">
? html` ${runningSessions.length > 0 && !this.killingAll ? html`
<button <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" 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} @click=${this.handleKillAll}
> >
KILL ALL (${runningSessions.length}) KILL ALL (${runningSessions.length})
</button> </button>
` ` : ''}
: ''} <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"
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}
@click=${this.handleCreateSession} >
> CREATE SESSION
CREATE SESSION </button>
</button> </div>
</div> </div>
</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 { customElement, property, state } from 'lit/decorators.js';
import { Renderer } from '../renderer.js'; import { Renderer } from '../renderer.js';
@ -35,6 +35,7 @@ export class SessionCard extends LitElement {
this.startRefresh(); this.startRefresh();
} }
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
if (this.refreshInterval) { if (this.refreshInterval) {
@ -96,13 +97,11 @@ export class SessionCard extends LitElement {
} }
private handleCardClick() { private handleCardClick() {
this.dispatchEvent( this.dispatchEvent(new CustomEvent('session-select', {
new CustomEvent('session-select', { detail: this.session,
detail: this.session, bubbles: true,
bubbles: true, composed: true
composed: true, }));
})
);
} }
private async handleKillClick(e: Event) { private async handleKillClick(e: Event) {
@ -120,7 +119,7 @@ export class SessionCard extends LitElement {
// Send kill request // Send kill request
try { try {
const response = await fetch(`/api/sessions/${this.session.id}`, { const response = await fetch(`/api/sessions/${this.session.id}`, {
method: 'DELETE', method: 'DELETE'
}); });
if (!response.ok) { if (!response.ok) {
@ -181,69 +180,60 @@ export class SessionCard extends LitElement {
} }
render() { render() {
const _isRunning = this.session.status === 'running'; const isRunning = this.session.status === 'running';
return html` return html`
<div <div class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden ${this.killing ? 'opacity-60' : ''}"
class="bg-vs-bg border border-vs-border rounded shadow cursor-pointer overflow-hidden ${this @click=${this.handleCardClick}>
.killing
? 'opacity-60'
: ''}"
@click=${this.handleCardClick}
>
<!-- Compact Header --> <!-- Compact Header -->
<div class="flex justify-between items-center px-3 py-2 border-b border-vs-border"> <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"> <div class="text-vs-text text-xs font-mono pr-2 flex-1 min-w-0">
${this.session.command} <div class="truncate" title="${this.session.command}">${this.session.command}</div>
</div> </div>
${this.session.status === 'running' ${this.session.status === 'running' ? html`
? html` <button
<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"
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}
@click=${this.handleKillClick} ?disabled=${this.killing}
?disabled=${this.killing} >
> ${this.killing ? 'killing...' : 'kill'}
${this.killing ? 'killing...' : 'kill'} </button>
</button> ` : ''}
`
: ''}
</div> </div>
<!-- XTerm renderer (main content) --> <!-- XTerm renderer (main content) -->
<div class="session-preview bg-black overflow-hidden" style="aspect-ratio: 640/480;"> <div class="session-preview bg-black overflow-hidden" style="aspect-ratio: 640/480;">
${this.killing ${this.killing ? html`
? html` <div class="w-full h-full flex items-center justify-center text-vs-warning">
<div class="w-full h-full flex items-center justify-center text-vs-warning"> <div class="text-center font-mono">
<div class="text-center font-mono"> <div class="text-4xl mb-2">${this.getKillingText()}</div>
<div class="text-4xl mb-2">${this.getKillingText()}</div> <div class="text-sm">Killing session...</div>
<div class="text-sm">Killing session...</div> </div>
</div> </div>
</div> ` : html`
` <div id="player" class="w-full h-full"></div>
: html` <div id="player" class="w-full h-full"></div> `} `}
</div> </div>
<!-- Compact Footer --> <!-- Compact Footer -->
<div class="px-3 py-2 text-vs-muted text-xs border-t border-vs-border"> <div class="px-3 py-2 text-vs-muted text-xs border-t border-vs-border">
<div class="flex justify-between items-center"> <div class="flex justify-between items-center min-w-0">
<span class="${this.getStatusColor()} text-xs flex items-center gap-1"> <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> <div class="w-2 h-2 rounded-full ${this.getStatusDotColor()}"></div>
${this.getStatusText()} ${this.getStatusText()}
</span> </span>
${this.session.pid ${this.session.pid ? html`
? html` <span
<span class="cursor-pointer hover:text-vs-accent transition-colors text-xs flex-shrink-0 ml-2"
class="cursor-pointer hover:text-vs-accent transition-colors" @click=${this.handlePidClick}
@click=${this.handlePidClick} title="Click to copy PID"
title="Click to copy PID" >
> PID: ${this.session.pid} <span class="opacity-50">(click to copy)</span>
PID: ${this.session.pid} <span class="opacity-50">(click to copy)</span> </span>
</span> ` : ''}
`
: ''}
</div> </div>
<div class="truncate text-xs opacity-75" title="${this.session.workingDir}"> <div class="text-xs opacity-75 min-w-0 mt-1">
${this.session.workingDir} <div class="truncate" title="${this.session.workingDir}">${this.session.workingDir}</div>
</div> </div>
</div> </div>
</div> </div>
@ -270,4 +260,5 @@ export class SessionCard extends LitElement {
} }
return this.session.status === 'running' ? 'bg-green-500' : 'bg-orange-500'; 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 // Detect mobile device
this.isMobile = this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
window.innerWidth <= 768;
// Only add listeners if not already added // Only add listeners if not already added
if (!this.isMobile && !this.keyboardListenerAdded) { if (!this.isMobile && !this.keyboardListenerAdded) {
@ -92,6 +91,7 @@ export class SessionView extends LitElement {
document.addEventListener('touchend', this.touchEndHandler, { passive: true }); document.addEventListener('touchend', this.touchEndHandler, { passive: true });
this.touchListenersAdded = true; this.touchListenersAdded = true;
} }
} }
disconnectedCallback() { disconnectedCallback() {
@ -113,6 +113,7 @@ export class SessionView extends LitElement {
this.touchListenersAdded = false; this.touchListenersAdded = false;
} }
// Stop loading animation // Stop loading animation
this.stopLoading(); this.stopLoading();
@ -185,10 +186,7 @@ export class SessionView extends LitElement {
}, delay); }, delay);
// Listen for session exit events // Listen for session exit events
terminalElement.addEventListener( terminalElement.addEventListener('session-exit', this.handleSessionExit.bind(this) as EventListener);
'session-exit',
this.handleSessionExit.bind(this) as EventListener
);
} }
private async handleKeyboardInput(e: KeyboardEvent) { private async handleKeyboardInput(e: KeyboardEvent) {
@ -276,8 +274,7 @@ export class SessionView extends LitElement {
// Handle Ctrl combinations (but not if we already handled Ctrl+Enter above) // Handle Ctrl combinations (but not if we already handled Ctrl+Enter above)
if (e.ctrlKey && e.key.length === 1 && e.key !== 'Enter') { if (e.ctrlKey && e.key.length === 1 && e.key !== 'Enter') {
const charCode = e.key.toLowerCase().charCodeAt(0); const charCode = e.key.toLowerCase().charCodeAt(0);
if (charCode >= 97 && charCode <= 122) { if (charCode >= 97 && charCode <= 122) { // a-z
// a-z
inputText = String.fromCharCode(charCode - 96); // Ctrl+A = \x01, etc. 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`, { const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ text: inputText }), body: JSON.stringify({ text: inputText })
}); });
if (!response.ok) { if (!response.ok) {
@ -322,6 +319,7 @@ export class SessionView extends LitElement {
this.session = { ...this.session, status: 'exited' }; this.session = { ...this.session, status: 'exited' };
this.requestUpdate(); this.requestUpdate();
// Switch to snapshot mode // Switch to snapshot mode
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.createInteractiveTerminal(); this.createInteractiveTerminal();
@ -368,9 +366,7 @@ export class SessionView extends LitElement {
controls.style.transition = 'transform 0.3s ease'; controls.style.transition = 'transform 0.3s ease';
// Calculate available space for textarea // Calculate available space for textarea
const header = this.querySelector( const header = this.querySelector('.flex.items-center.justify-between.p-4.border-b') as HTMLElement;
'.flex.items-center.justify-between.p-4.border-b'
) as HTMLElement;
const headerHeight = header?.offsetHeight || 60; const headerHeight = header?.offsetHeight || 60;
const controlsHeight = controls?.offsetHeight || 120; const controlsHeight = controls?.offsetHeight || 120;
const padding = 48; // Additional padding for spacing const padding = 48; // Additional padding for spacing
@ -505,6 +501,7 @@ export class SessionView extends LitElement {
} }
}; };
private async handlePaste() { private async handlePaste() {
if (!this.session) return; if (!this.session) return;
@ -559,10 +556,7 @@ export class SessionView extends LitElement {
if (selectedText) { if (selectedText) {
// Write the selected text to clipboard // Write the selected text to clipboard
await navigator.clipboard.writeText(selectedText); await navigator.clipboard.writeText(selectedText);
console.log( console.log('Text copied to clipboard:', selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : ''));
'Text copied to clipboard:',
selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : '')
);
} else { } else {
console.log('No text selected for copying'); console.log('No text selected for copying');
} }
@ -583,10 +577,7 @@ export class SessionView extends LitElement {
textArea.select(); textArea.select();
if (document.execCommand('copy')) { if (document.execCommand('copy')) {
console.log( console.log('Text copied to clipboard (fallback):', selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : ''));
'Text copied to clipboard (fallback):',
selectedText.substring(0, 50) + (selectedText.length > 50 ? '...' : '')
);
} }
document.body.removeChild(textArea); document.body.removeChild(textArea);
@ -605,9 +596,9 @@ export class SessionView extends LitElement {
const response = await fetch(`/api/sessions/${this.session.id}/input`, { const response = await fetch(`/api/sessions/${this.session.id}/input`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ text }), body: JSON.stringify({ text })
}); });
if (!response.ok) { if (!response.ok) {
@ -645,45 +636,43 @@ export class SessionView extends LitElement {
return frames[this.loadingFrame % frames.length]; return frames[this.loadingFrame % frames.length];
} }
render() { render() {
if (!this.session) { 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` return html`
<style> <style>
session-view *, session-view *, session-view *:focus, session-view *:focus-visible {
session-view *:focus,
session-view *:focus-visible {
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
session-view:focus { session-view:focus {
outline: 2px solid #007acc !important; outline: 2px solid #007ACC !important;
outline-offset: -2px; outline-offset: -2px;
} }
</style> </style>
<div <div class="flex flex-col bg-vs-bg font-mono" style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;">
class="flex flex-col bg-vs-bg font-mono"
style="height: 100vh; height: 100dvh; outline: none !important; box-shadow: none !important;"
>
<!-- Compact Header --> <!-- Compact Header -->
<div <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">
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 min-w-0 flex-1">
>
<div class="flex items-center gap-3">
<button <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} @click=${this.handleBack}
> >
BACK BACK
</button> </button>
<div class="text-vs-text"> <div class="text-vs-text min-w-0 flex-1 overflow-hidden">
<div class="text-vs-accent">${this.session.command}</div> <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">${this.session.workingDir}</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> </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'}"> <span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
${this.session.status.toUpperCase()} ${this.session.status.toUpperCase()}
</span> </span>
@ -691,167 +680,146 @@ export class SessionView extends LitElement {
</div> </div>
<!-- Terminal Container --> <!-- Terminal Container -->
<div <div class="flex-1 bg-black overflow-hidden min-h-0 relative" id="terminal-container" style="max-width: 100vw; height: 100%;">
class="flex-1 bg-black overflow-hidden min-h-0 relative" ${this.loading ? html`
id="terminal-container" <!-- Loading overlay -->
style="max-width: 100vw; height: 100%;" <div class="absolute inset-0 bg-black bg-opacity-80 flex items-center justify-center">
> <div class="text-vs-text font-mono text-center">
${this.loading <div class="text-2xl mb-2">${this.getLoadingText()}</div>
? html` <div class="text-sm text-vs-muted">Connecting to session...</div>
<!-- Loading overlay --> </div>
<div </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> </div>
<!-- Mobile Input Controls --> <!-- Mobile Input Controls -->
${this.isMobile && !this.showMobileInput ${this.isMobile && !this.showMobileInput ? html`
? html` <div class="flex-shrink-0 p-4 bg-vs-bg">
<div class="flex-shrink-0 p-4 bg-vs-bg"> <!-- First row: Arrow keys -->
<!-- First row: Arrow keys --> <div class="flex gap-2 mb-2">
<div class="flex gap-2 mb-2"> <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"
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')}
@click=${() => this.handleSpecialKey('arrow_up')} >
> <span class="text-xl"></span>
<span class="text-xl"></span> </button>
</button> <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"
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')}
@click=${() => this.handleSpecialKey('arrow_down')} >
> <span class="text-xl"></span>
<span class="text-xl"></span> </button>
</button> <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"
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')}
@click=${() => this.handleSpecialKey('arrow_left')} >
> <span class="text-xl"></span>
<span class="text-xl"></span> </button>
</button> <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"
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')}
@click=${() => this.handleSpecialKey('arrow_right')} >
> <span class="text-xl"></span>
<span class="text-xl"></span> </button>
</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> </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) --> <!-- Full-Screen Input Overlay (only when opened) -->
${this.isMobile && this.showMobileInput ${this.isMobile && this.showMobileInput ? html`
? html` <div class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col" style="height: 100vh; height: 100dvh;">
<div <!-- Input Header -->
class="fixed inset-0 bg-vs-bg-secondary bg-opacity-95 z-50 flex flex-col" <div class="flex items-center justify-between p-4 border-b border-vs-border flex-shrink-0">
style="height: 100vh; height: 100dvh;" <div class="text-vs-text font-mono text-sm">Terminal Input</div>
> <button
<!-- Input Header --> class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer"
<div @click=${this.handleMobileInputToggle}
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>
</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 <button
class="text-vs-muted hover:text-vs-text text-lg leading-none border-none bg-transparent cursor-pointer" 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.handleMobileInputToggle} @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> </button>
</div> </div>
<!-- Input Area with dynamic height --> <div class="text-vs-muted text-xs text-center">
<div class="flex-1 p-4 flex flex-col min-h-0"> SEND: text only SEND + ENTER: text with enter key
<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> </div>
</div> </div>
` </div>
: ''} ` : ''}
</div> </div>
`; `;
} }