"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SessionView = void 0; const lit_1 = require("lit"); const decorators_js_1 = require("lit/decorators.js"); let SessionView = class SessionView extends lit_1.LitElement { constructor() { super(...arguments); this.session = null; this.connected = false; this.player = null; this.sessionStatusInterval = null; this.showMobileInput = false; this.mobileInputText = ''; this.isMobile = false; this.touchStartX = 0; this.touchStartY = 0; this.keyboardHandler = (e) => { if (!this.session) return; e.preventDefault(); e.stopPropagation(); this.handleKeyboardInput(e); }; this.touchStartHandler = (e) => { if (!this.isMobile) return; const touch = e.touches[0]; this.touchStartX = touch.clientX; this.touchStartY = touch.clientY; }; this.touchEndHandler = (e) => { if (!this.isMobile) return; const touch = e.changedTouches[0]; const touchEndX = touch.clientX; const touchEndY = touch.clientY; const deltaX = touchEndX - this.touchStartX; const deltaY = touchEndY - this.touchStartY; // Check for horizontal swipe from left edge (back gesture) const isSwipeRight = deltaX > 100; const isVerticallyStable = Math.abs(deltaY) < 100; const startedFromLeftEdge = this.touchStartX < 50; if (isSwipeRight && isVerticallyStable && startedFromLeftEdge) { // Trigger back navigation this.handleBack(); } }; } // Disable shadow DOM to use Tailwind createRenderRoot() { return this; } connectedCallback() { super.connectedCallback(); this.connected = true; // Detect mobile device this.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768; // Add global keyboard event listener only for desktop if (!this.isMobile) { document.addEventListener('keydown', this.keyboardHandler); } else { // Add touch event listeners for mobile swipe gestures document.addEventListener('touchstart', this.touchStartHandler, { passive: true }); document.addEventListener('touchend', this.touchEndHandler, { passive: true }); } // Start polling session status this.startSessionStatusPolling(); } disconnectedCallback() { super.disconnectedCallback(); this.connected = false; // Remove global keyboard event listener if (!this.isMobile) { document.removeEventListener('keydown', this.keyboardHandler); } else { // Remove touch event listeners document.removeEventListener('touchstart', this.touchStartHandler); document.removeEventListener('touchend', this.touchEndHandler); } // Stop polling session status this.stopSessionStatusPolling(); // Cleanup player if exists if (this.player) { this.player = null; } } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has('session') && this.session) { // Use setTimeout to ensure DOM is rendered first setTimeout(() => { this.createInteractiveTerminal(); }, 10); } } createInteractiveTerminal() { if (!this.session) return; const terminalElement = this.querySelector('#interactive-terminal'); if (terminalElement && window.AsciinemaPlayer) { try { // For ended sessions, use snapshot instead of stream to avoid reloading const url = this.session.status === 'exited' ? `/api/sessions/${this.session.id}/snapshot` : `/api/sessions/${this.session.id}/stream`; const config = this.session.status === 'exited' ? { url } // Static snapshot : { driver: "eventsource", url }; // Live stream this.player = window.AsciinemaPlayer.create(config, terminalElement, { autoPlay: true, loop: false, controls: false, fit: 'both', terminalFontSize: '12px', idleTimeLimit: 0.5, preload: true, poster: 'npt:999999' }); // Disable focus outline and fullscreen functionality if (this.player && this.player.el) { // Remove focus outline this.player.el.style.outline = 'none'; this.player.el.style.border = 'none'; // Disable fullscreen hotkey by removing tabindex and preventing focus this.player.el.removeAttribute('tabindex'); this.player.el.style.pointerEvents = 'none'; // Find the terminal element and make it non-focusable const terminal = this.player.el.querySelector('.ap-terminal, .ap-screen, pre'); if (terminal) { terminal.removeAttribute('tabindex'); terminal.style.outline = 'none'; } } } catch (error) { console.error('Error creating interactive terminal:', error); } } } async handleKeyboardInput(e) { if (!this.session) return; let inputText = ''; // Handle special keys switch (e.key) { case 'Enter': inputText = 'enter'; break; case 'Escape': inputText = 'escape'; break; case 'ArrowUp': inputText = 'arrow_up'; break; case 'ArrowDown': inputText = 'arrow_down'; break; case 'ArrowLeft': inputText = 'arrow_left'; break; case 'ArrowRight': inputText = 'arrow_right'; break; case 'Tab': inputText = '\t'; break; case 'Backspace': inputText = '\b'; break; case 'Delete': inputText = '\x7f'; break; case ' ': inputText = ' '; break; default: // Handle regular printable characters if (e.key.length === 1) { inputText = e.key; } else { // Ignore other special keys return; } break; } // Handle Ctrl combinations if (e.ctrlKey && e.key.length === 1) { const charCode = e.key.toLowerCase().charCodeAt(0); if (charCode >= 97 && charCode <= 122) { // a-z inputText = String.fromCharCode(charCode - 96); // Ctrl+A = \x01, etc. } } // Send the input to the session try { const response = await fetch(`/api/sessions/${this.session.id}/input`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: inputText }) }); if (!response.ok) { console.error('Failed to send input to session'); } } catch (error) { console.error('Error sending input:', error); } } handleBack() { this.dispatchEvent(new CustomEvent('back')); } // Mobile input methods handleMobileInputToggle() { this.showMobileInput = !this.showMobileInput; if (this.showMobileInput) { // Focus the textarea after a short delay to ensure it's rendered setTimeout(() => { const textarea = this.querySelector('#mobile-input-textarea'); if (textarea) { textarea.focus(); this.adjustTextareaForKeyboard(); } }, 100); } else { // Clean up viewport listener when closing overlay const textarea = this.querySelector('#mobile-input-textarea'); if (textarea && textarea._viewportCleanup) { textarea._viewportCleanup(); } } } adjustTextareaForKeyboard() { // Adjust the layout when virtual keyboard appears const textarea = this.querySelector('#mobile-input-textarea'); const controls = this.querySelector('#mobile-controls'); if (!textarea || !controls) return; const adjustLayout = () => { const viewportHeight = window.visualViewport?.height || window.innerHeight; const windowHeight = window.innerHeight; const keyboardHeight = windowHeight - viewportHeight; // If keyboard is visible (viewport height is significantly smaller) if (keyboardHeight > 100) { // Move controls above the keyboard controls.style.transform = `translateY(-${keyboardHeight}px)`; 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'); const headerHeight = header?.offsetHeight || 60; const controlsHeight = controls?.offsetHeight || 120; const padding = 48; // Additional padding for spacing // Available height is viewport height minus header and controls (controls are now above keyboard) const maxTextareaHeight = viewportHeight - headerHeight - controlsHeight - padding; const inputArea = textarea.parentElement; if (inputArea && maxTextareaHeight > 0) { // Set the input area to not exceed the available space inputArea.style.height = `${maxTextareaHeight}px`; inputArea.style.maxHeight = `${maxTextareaHeight}px`; inputArea.style.overflow = 'hidden'; // Set textarea height within the container const labelHeight = 40; // Height of the label above textarea const textareaMaxHeight = Math.max(maxTextareaHeight - labelHeight, 80); textarea.style.height = `${textareaMaxHeight}px`; textarea.style.maxHeight = `${textareaMaxHeight}px`; } } else { // Reset position when keyboard is hidden controls.style.transform = 'translateY(0px)'; controls.style.transition = 'transform 0.3s ease'; // Reset textarea height and constraints const inputArea = textarea.parentElement; if (inputArea) { inputArea.style.height = ''; inputArea.style.maxHeight = ''; inputArea.style.overflow = ''; textarea.style.height = ''; textarea.style.maxHeight = ''; } } }; // Listen for viewport changes (keyboard show/hide) if (window.visualViewport) { window.visualViewport.addEventListener('resize', adjustLayout); // Clean up listener when overlay is closed const cleanup = () => { if (window.visualViewport) { window.visualViewport.removeEventListener('resize', adjustLayout); } }; // Store cleanup function for later use textarea._viewportCleanup = cleanup; } // Initial adjustment setTimeout(adjustLayout, 300); } handleMobileInputChange(e) { const textarea = e.target; this.mobileInputText = textarea.value; } async handleMobileInputSendOnly() { // Get the current value from the textarea directly const textarea = this.querySelector('#mobile-input-textarea'); const textToSend = textarea?.value?.trim() || this.mobileInputText.trim(); if (!textToSend) return; try { // Send text without enter key await this.sendInputText(textToSend); // Clear both the reactive property and textarea this.mobileInputText = ''; if (textarea) { textarea.value = ''; } // Trigger re-render to update button state this.requestUpdate(); // Hide the input overlay after sending this.showMobileInput = false; } catch (error) { console.error('Error sending mobile input:', error); // Don't hide the overlay if there was an error } } async handleMobileInputSend() { // Get the current value from the textarea directly const textarea = this.querySelector('#mobile-input-textarea'); const textToSend = textarea?.value?.trim() || this.mobileInputText.trim(); if (!textToSend) return; try { // Add enter key at the end to execute the command await this.sendInputText(textToSend + '\n'); // Clear both the reactive property and textarea this.mobileInputText = ''; if (textarea) { textarea.value = ''; } // Trigger re-render to update button state this.requestUpdate(); // Hide the input overlay after sending this.showMobileInput = false; } catch (error) { console.error('Error sending mobile input:', error); // Don't hide the overlay if there was an error } } async handleSpecialKey(key) { await this.sendInputText(key); } async sendInputText(text) { if (!this.session) return; try { const response = await fetch(`/api/sessions/${this.session.id}/input`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) }); if (!response.ok) { console.error('Failed to send input to session'); } } catch (error) { console.error('Error sending input:', error); } } startSessionStatusPolling() { if (this.sessionStatusInterval) { clearInterval(this.sessionStatusInterval); } // Poll every 2 seconds this.sessionStatusInterval = window.setInterval(() => { this.checkSessionStatus(); }, 2000); } stopSessionStatusPolling() { if (this.sessionStatusInterval) { clearInterval(this.sessionStatusInterval); this.sessionStatusInterval = null; } } async checkSessionStatus() { if (!this.session) return; try { const response = await fetch('/api/sessions'); if (!response.ok) return; const sessions = await response.json(); const currentSession = sessions.find((s) => s.id === this.session.id); if (currentSession && currentSession.status !== this.session.status) { // Session status changed this.session = { ...this.session, status: currentSession.status }; this.requestUpdate(); // If session ended, switch from stream to snapshot to prevent restarts if (currentSession.status === 'exited' && this.player && this.session.status === 'running') { console.log('Session ended, switching to snapshot view'); try { // Dispose the streaming player if (this.player.dispose) { this.player.dispose(); } this.player = null; // Recreate with snapshot setTimeout(() => { this.createInteractiveTerminal(); }, 100); } catch (error) { console.error('Error switching to snapshot:', error); } } } } catch (error) { console.error('Error checking session status:', error); } } render() { if (!this.session) { return (0, lit_1.html) `
No session selected
`; } return (0, lit_1.html) `
${this.session.command} (${this.session.id.substring(0, 8)}...)
${this.session.workingDir} ${this.session.status.toUpperCase()}
${this.isMobile ? (0, lit_1.html) ` ${!this.showMobileInput ? (0, lit_1.html) `
` : ''} ${this.showMobileInput ? (0, lit_1.html) `
Terminal Input
Type your command(s) below. Supports multiline input.
SEND: text only • SEND + ENTER: text with enter key
` : ''} ` : ''}
`; } }; exports.SessionView = SessionView; __decorate([ (0, decorators_js_1.property)({ type: Object }), __metadata("design:type", Object) ], SessionView.prototype, "session", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "connected", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "player", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "sessionStatusInterval", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "showMobileInput", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "mobileInputText", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "isMobile", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "touchStartX", void 0); __decorate([ (0, decorators_js_1.state)(), __metadata("design:type", Object) ], SessionView.prototype, "touchStartY", void 0); exports.SessionView = SessionView = __decorate([ (0, decorators_js_1.customElement)('session-view') ], SessionView); //# sourceMappingURL=session-view.js.map