diff --git a/web/src/client/components/session-list.ts b/web/src/client/components/session-list.ts index 154f3ae8..38f7e979 100644 --- a/web/src/client/components/session-list.ts +++ b/web/src/client/components/session-list.ts @@ -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); } diff --git a/web/src/client/components/session-view.ts b/web/src/client/components/session-view.ts index 01bb2e9c..f634ef45 100644 --- a/web/src/client/components/session-view.ts +++ b/web/src/client/components/session-view.ts @@ -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; } -
+
@@ -479,14 +495,11 @@ export class SessionView extends LitElement { BACK
- ${this.session.command} - (${this.session.id.substring(0, 8)}...) +
${this.session.command}
+
${this.session.workingDir}
- - ${this.session.workingDir} - ${this.session.status.toUpperCase()} @@ -494,15 +507,13 @@ export class SessionView extends LitElement {
-
+
- ${this.isMobile ? html` - - ${!this.showMobileInput ? html` -
+ ${this.isMobile && !this.showMobileInput ? html` +
-
- ` : ''} +
+ ` : ''} - - ${this.showMobileInput ? html` + + ${this.isMobile && this.showMobileInput ? html`
@@ -627,7 +638,6 @@ export class SessionView extends LitElement {
- ` : ''} ` : ''}
`; diff --git a/web/src/client/renderer.ts b/web/src/client/renderer.ts index eceae76c..e30099de 100644 --- a/web/src/client/renderer.ts +++ b/web/src/client/renderer.ts @@ -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 { @@ -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'; + } + } } \ No newline at end of file