mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
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:
commit
8b3833ee0e
3 changed files with 251 additions and 308 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue