mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-31 10:25:57 +00:00
Fix terminal layout and improve mobile UX
- Restructure session-view to use proper flexbox layout (header/xterm/buttons) - Container now exactly fits viewport height (100vh) - Header shows command and working directory stacked - XTerm gets all remaining space with horizontal scrolling - Mobile buttons integrated into layout instead of overlaying - Replace setTimeout with requestAnimationFrame for better performance - Add isPreview parameter to renderer for future preview scaling - Disable pointer events on preview terminals so clicks pass through 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
093aff4379
commit
34801bc687
3 changed files with 70 additions and 46 deletions
|
|
@ -145,8 +145,9 @@ export class SessionList extends LitElement {
|
|||
const session = this.sessions.find(s => s.id === sessionId);
|
||||
if (!session) return;
|
||||
|
||||
// Create renderer with smaller dimensions and font for preview
|
||||
const renderer = new Renderer(playerElement, 40, 12, 10000, 8); // 40x12 chars, 8px font
|
||||
// Create renderer with smaller dimensions for preview
|
||||
// Use responsive font sizing, starting with smaller font for previews
|
||||
const renderer = new Renderer(playerElement, 40, 12, 10000, 6, true); // 40x12 chars, 6px base font, isPreview=true
|
||||
this.renderers.set(sessionId, renderer);
|
||||
|
||||
// Terminal is already configured with disableStdin: true in renderer constructor
|
||||
|
|
@ -159,6 +160,11 @@ export class SessionList extends LitElement {
|
|||
|
||||
// Let the renderer handle the URL
|
||||
await renderer.loadFromUrl(url, isStream);
|
||||
|
||||
// Disable pointer events so clicks pass through to the card (after terminal is rendered)
|
||||
requestAnimationFrame(() => {
|
||||
renderer.setPointerEventsEnabled(false);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating renderer:', error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,6 +107,17 @@ export class SessionView extends LitElement {
|
|||
|
||||
if (changedProperties.has('session') && this.session) {
|
||||
this.createInteractiveTerminal();
|
||||
// Adjust terminal spacing after creating terminal
|
||||
requestAnimationFrame(() => {
|
||||
this.adjustTerminalForMobileButtons();
|
||||
});
|
||||
}
|
||||
|
||||
// Adjust terminal height for mobile buttons after render
|
||||
if (changedProperties.has('showMobileInput') || changedProperties.has('isMobile')) {
|
||||
requestAnimationFrame(() => {
|
||||
this.adjustTerminalForMobileButtons();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,13 +241,13 @@ export class SessionView extends LitElement {
|
|||
this.showMobileInput = !this.showMobileInput;
|
||||
if (this.showMobileInput) {
|
||||
// Focus the textarea after a short delay to ensure it's rendered
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = this.querySelector('#mobile-input-textarea') as HTMLTextAreaElement;
|
||||
if (textarea) {
|
||||
textarea.focus();
|
||||
this.adjustTextareaForKeyboard();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
} else {
|
||||
// Clean up viewport listener when closing overlay
|
||||
const textarea = this.querySelector('#mobile-input-textarea') as HTMLTextAreaElement;
|
||||
|
|
@ -315,7 +326,7 @@ export class SessionView extends LitElement {
|
|||
}
|
||||
|
||||
// Initial adjustment
|
||||
setTimeout(adjustLayout, 300);
|
||||
requestAnimationFrame(adjustLayout);
|
||||
}
|
||||
|
||||
private handleMobileInputChange(e: Event) {
|
||||
|
|
@ -403,6 +414,11 @@ export class SessionView extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private adjustTerminalForMobileButtons() {
|
||||
// Disabled for now to avoid viewport issues
|
||||
// The mobile buttons will overlay the terminal
|
||||
}
|
||||
|
||||
private startSessionStatusPolling() {
|
||||
if (this.sessionStatusInterval) {
|
||||
clearInterval(this.sessionStatusInterval);
|
||||
|
|
@ -468,7 +484,7 @@ export class SessionView extends LitElement {
|
|||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
<div class="h-screen flex flex-col bg-vs-bg font-mono" style="outline: none !important; box-shadow: none !important;">
|
||||
<div class="flex flex-col bg-vs-bg font-mono" style="height: 100vh; 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">
|
||||
|
|
@ -479,14 +495,11 @@ export class SessionView extends LitElement {
|
|||
BACK
|
||||
</button>
|
||||
<div class="text-vs-text">
|
||||
<span class="text-vs-accent">${this.session.command}</span>
|
||||
<span class="text-vs-muted text-xs ml-2">(${this.session.id.substring(0, 8)}...)</span>
|
||||
<div class="text-vs-accent">${this.session.command}</div>
|
||||
<div class="text-vs-muted text-xs">${this.session.workingDir}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<span class="text-vs-muted">
|
||||
${this.session.workingDir}
|
||||
</span>
|
||||
<span class="${this.session.status === 'running' ? 'text-vs-user' : 'text-vs-warning'}">
|
||||
${this.session.status.toUpperCase()}
|
||||
</span>
|
||||
|
|
@ -494,15 +507,13 @@ export class SessionView extends LitElement {
|
|||
</div>
|
||||
|
||||
<!-- Terminal Container -->
|
||||
<div class="flex-1 bg-black overflow-hidden">
|
||||
<div class="flex-1 bg-black overflow-x-auto overflow-y-hidden min-h-0" id="terminal-container">
|
||||
<div id="interactive-terminal" class="w-full h-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Input Controls -->
|
||||
${this.isMobile ? html`
|
||||
<!-- Quick Action Buttons (only when overlay is closed) -->
|
||||
${!this.showMobileInput ? html`
|
||||
<div class="fixed bottom-4 left-4 right-4 z-40">
|
||||
${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
|
||||
|
|
@ -564,11 +575,11 @@ export class SessionView extends LitElement {
|
|||
TYPE
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Full-Screen Input Overlay (only when opened) -->
|
||||
${this.showMobileInput ? html`
|
||||
<!-- 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">
|
||||
|
|
@ -627,7 +638,6 @@ export class SessionView extends LitElement {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ export class Renderer {
|
|||
private terminal: Terminal;
|
||||
private fitAddon: FitAddon;
|
||||
private webLinksAddon: WebLinksAddon;
|
||||
private isPreview: boolean;
|
||||
|
||||
constructor(container: HTMLElement, width: number = 80, height: number = 20, scrollback: number = 1000000, fontSize: number = 14) {
|
||||
constructor(container: HTMLElement, width: number = 80, height: number = 20, scrollback: number = 1000000, fontSize: number = 14, isPreview: boolean = false) {
|
||||
this.container = container;
|
||||
|
||||
this.isPreview = isPreview;
|
||||
|
||||
// Create terminal with options similar to the custom renderer
|
||||
this.terminal = new Terminal({
|
||||
cols: width,
|
||||
|
|
@ -65,13 +67,13 @@ export class Renderer {
|
|||
convertEol: true,
|
||||
altClickMovesCursor: false,
|
||||
rightClickSelectsWord: false,
|
||||
disableStdin: true // We handle input separately
|
||||
disableStdin: true, // We handle input separately
|
||||
});
|
||||
|
||||
// Add addons
|
||||
this.fitAddon = new FitAddon();
|
||||
this.webLinksAddon = new WebLinksAddon();
|
||||
|
||||
|
||||
this.terminal.loadAddon(this.fitAddon);
|
||||
this.terminal.loadAddon(this.webLinksAddon);
|
||||
|
||||
|
|
@ -84,7 +86,7 @@ export class Renderer {
|
|||
this.container.style.padding = '10px';
|
||||
this.container.style.backgroundColor = '#1e1e1e';
|
||||
this.container.style.overflow = 'hidden';
|
||||
|
||||
|
||||
// Create terminal wrapper
|
||||
const terminalWrapper = document.createElement('div');
|
||||
terminalWrapper.style.width = '100%';
|
||||
|
|
@ -93,10 +95,10 @@ export class Renderer {
|
|||
|
||||
// Open terminal in the wrapper
|
||||
this.terminal.open(terminalWrapper);
|
||||
|
||||
// Fit terminal to container
|
||||
|
||||
// Just use FitAddon
|
||||
this.fitAddon.fit();
|
||||
|
||||
|
||||
// Handle container resize
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.fitAddon.fit();
|
||||
|
|
@ -104,6 +106,7 @@ export class Renderer {
|
|||
resizeObserver.observe(this.container);
|
||||
}
|
||||
|
||||
|
||||
// Public API methods - maintain compatibility with custom renderer
|
||||
|
||||
async loadCastFile(url: string): Promise<void> {
|
||||
|
|
@ -115,16 +118,16 @@ export class Renderer {
|
|||
parseCastFile(content: string): void {
|
||||
const lines = content.trim().split('\n');
|
||||
let header: CastHeader | null = null;
|
||||
|
||||
|
||||
// Clear terminal
|
||||
this.terminal.clear();
|
||||
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
|
||||
|
||||
if (parsed.version && parsed.width && parsed.height) {
|
||||
// Header
|
||||
header = parsed;
|
||||
|
|
@ -136,7 +139,7 @@ export class Renderer {
|
|||
type: parsed[1],
|
||||
data: parsed[2]
|
||||
};
|
||||
|
||||
|
||||
if (event.type === 'o') {
|
||||
this.processOutput(event.data);
|
||||
} else if (event.type === 'r') {
|
||||
|
|
@ -173,11 +176,8 @@ export class Renderer {
|
|||
}
|
||||
|
||||
resize(width: number, height: number): void {
|
||||
this.terminal.resize(width, height);
|
||||
// Fit addon will handle the visual resize
|
||||
setTimeout(() => {
|
||||
this.fitAddon.fit();
|
||||
}, 0);
|
||||
// Ignore session resize and just use FitAddon
|
||||
this.fitAddon.fit();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
|
|
@ -192,13 +192,13 @@ export class Renderer {
|
|||
// Connect to any SSE URL
|
||||
connectToUrl(url: string): EventSource {
|
||||
const eventSource = new EventSource(url);
|
||||
|
||||
|
||||
// Don't clear terminal for live streams - just append new content
|
||||
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
|
||||
if (data.version && data.width && data.height) {
|
||||
// Header
|
||||
console.log('Received header:', data);
|
||||
|
|
@ -221,11 +221,11 @@ export class Renderer {
|
|||
console.warn('Failed to parse stream event:', event.data);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('Stream error:', error);
|
||||
};
|
||||
|
||||
|
||||
return eventSource;
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +238,7 @@ export class Renderer {
|
|||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
|
||||
|
||||
if (isStream) {
|
||||
// It's a stream URL, connect via SSE (don't clear - append to existing content)
|
||||
this.eventSource = this.connectToUrl(url);
|
||||
|
|
@ -250,7 +250,7 @@ export class Renderer {
|
|||
}
|
||||
|
||||
// Additional methods for terminal control
|
||||
|
||||
|
||||
focus(): void {
|
||||
this.terminal.focus();
|
||||
}
|
||||
|
|
@ -303,4 +303,12 @@ export class Renderer {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Disable all pointer events for previews so clicks pass through to parent
|
||||
setPointerEventsEnabled(enabled: boolean): void {
|
||||
const terminalElement = this.container.querySelector('.xterm') as HTMLElement;
|
||||
if (terminalElement) {
|
||||
terminalElement.style.pointerEvents = enabled ? 'auto' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue