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:
Mario Zechner 2025-06-16 11:55:22 +02:00
parent 093aff4379
commit 34801bc687
3 changed files with 70 additions and 46 deletions

View file

@ -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);
}

View file

@ -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>
`;

View file

@ -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';
}
}
}