diff --git a/web/src/client/components/session-card.ts b/web/src/client/components/session-card.ts index 166caf43..6cc94258 100644 --- a/web/src/client/components/session-card.ts +++ b/web/src/client/components/session-card.ts @@ -22,14 +22,20 @@ export class SessionCard extends LitElement { @property({ type: Object }) session!: Session; @state() private renderer: Renderer | null = null; + + private refreshInterval: number | null = null; firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); this.createRenderer(); + this.startRefresh(); } disconnectedCallback() { super.disconnectedCallback(); + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + } if (this.renderer) { this.renderer.dispose(); this.renderer = null; @@ -43,11 +49,8 @@ export class SessionCard extends LitElement { // Create single renderer for this card this.renderer = new Renderer(playerElement, 40, 12, 10000, 6, true); - // Connect to appropriate endpoint based on session status - const isStream = this.session.status !== 'exited'; - const url = isStream - ? `/api/sessions/${this.session.id}/stream` - : `/api/sessions/${this.session.id}/snapshot`; + // Always use snapshot endpoint for cards + const url = `/api/sessions/${this.session.id}/snapshot`; // Wait a moment for freshly created sessions before connecting const sessionAge = Date.now() - new Date(this.session.startedAt).getTime(); @@ -55,11 +58,24 @@ export class SessionCard extends LitElement { setTimeout(() => { if (this.renderer) { - this.renderer.loadFromUrl(url, isStream); + this.renderer.loadFromUrl(url, false); // false = not a stream, use snapshot + // Disable pointer events so clicks pass through to the card + this.renderer.setPointerEventsEnabled(false); } }, delay); } + private startRefresh() { + this.refreshInterval = window.setInterval(() => { + if (this.renderer) { + const url = `/api/sessions/${this.session.id}/snapshot`; + this.renderer.loadFromUrl(url, false); + // Ensure pointer events stay disabled after refresh + this.renderer.setPointerEventsEnabled(false); + } + }, 10000); // Refresh every 10 seconds + } + private handleCardClick() { this.dispatchEvent(new CustomEvent('session-select', { detail: this.session, @@ -70,6 +86,7 @@ export class SessionCard extends LitElement { private handleKillClick(e: Event) { e.stopPropagation(); + e.preventDefault(); this.dispatchEvent(new CustomEvent('session-kill', { detail: this.session.id, bubbles: true, @@ -77,51 +94,81 @@ export class SessionCard extends LitElement { })); } + private async handlePidClick(e: Event) { + e.stopPropagation(); + e.preventDefault(); + + if (this.session.pid) { + try { + await navigator.clipboard.writeText(this.session.pid.toString()); + console.log('PID copied to clipboard:', this.session.pid); + } catch (error) { + console.error('Failed to copy PID to clipboard:', error); + // Fallback: select text manually + this.fallbackCopyToClipboard(this.session.pid.toString()); + } + } + } + + private fallbackCopyToClipboard(text: string) { + const textArea = document.createElement('textarea'); + textArea.value = text; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + console.log('PID copied to clipboard (fallback):', text); + } catch (error) { + console.error('Fallback copy failed:', error); + } + document.body.removeChild(textArea); + } + render() { const isRunning = this.session.status === 'running'; - const statusColor = isRunning ? 'text-green-400' : 'text-red-400'; - + return html`
- ${this.session.workingDir} -
-