mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-25 14:57:37 +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
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue