// SPDX-Licence-Identifier: EUPL-1.2 import { LitElement, html, css, nothing } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { connectProcessEvents, type ProcessEvent } from './shared/events.js'; interface OutputLine { text: string; stream: 'stdout' | 'stderr'; timestamp: number; } /** * — Live stdout/stderr stream for a selected process. * * Connects to the WS endpoint and filters for process.output events matching * the given process-id. Displays output in a terminal-style scrollable area * with colour-coded stream indicators (stdout/stderr). */ @customElement('core-process-output') export class ProcessOutput extends LitElement { static styles = css` :host { display: block; font-family: system-ui, -apple-system, sans-serif; margin-top: 0.75rem; } .output-header { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0.75rem; background: #1e1e1e; border-radius: 0.5rem 0.5rem 0 0; colour: #d4d4d4; font-size: 0.75rem; } .output-title { font-weight: 600; } .output-actions { display: flex; gap: 0.5rem; } button.clear-btn { padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.6875rem; cursor: pointer; background: #333; colour: #d4d4d4; border: 1px solid #555; transition: background 0.15s; } button.clear-btn:hover { background: #444; } .auto-scroll-toggle { display: flex; align-items: center; gap: 0.25rem; font-size: 0.6875rem; colour: #d4d4d4; cursor: pointer; } .auto-scroll-toggle input { cursor: pointer; } .output-body { background: #1e1e1e; border-radius: 0 0 0.5rem 0.5rem; padding: 0.5rem 0.75rem; max-height: 24rem; overflow-y: auto; font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace; font-size: 0.8125rem; line-height: 1.5; } .line { white-space: pre-wrap; word-break: break-all; } .line.stdout { colour: #d4d4d4; } .line.stderr { colour: #f87171; } .stream-tag { display: inline-block; width: 3rem; font-size: 0.625rem; font-weight: 600; text-transform: uppercase; opacity: 0.5; margin-right: 0.5rem; } .empty { text-align: center; padding: 2rem; colour: #6b7280; font-size: 0.8125rem; } .waiting { colour: #9ca3af; font-style: italic; padding: 1rem; text-align: center; font-size: 0.8125rem; } `; @property({ attribute: 'api-url' }) apiUrl = ''; @property({ attribute: 'ws-url' }) wsUrl = ''; @property({ attribute: 'process-id' }) processId = ''; @state() private lines: OutputLine[] = []; @state() private autoScroll = true; @state() private connected = false; private ws: WebSocket | null = null; connectedCallback() { super.connectedCallback(); if (this.wsUrl && this.processId) { this.connect(); } } disconnectedCallback() { super.disconnectedCallback(); this.disconnect(); } updated(changed: Map) { if (changed.has('processId') || changed.has('wsUrl')) { this.disconnect(); this.lines = []; if (this.wsUrl && this.processId) { this.connect(); } } if (this.autoScroll) { this.scrollToBottom(); } } private connect() { this.ws = connectProcessEvents(this.wsUrl, (event: ProcessEvent) => { const data = event.data; if (!data) return; // Filter for output events matching our process ID const channel = event.channel ?? event.type ?? ''; if (channel === 'process.output' && data.id === this.processId) { this.lines = [ ...this.lines, { text: data.line ?? '', stream: data.stream === 'stderr' ? 'stderr' : 'stdout', timestamp: Date.now(), }, ]; } }); this.ws.onopen = () => { this.connected = true; }; this.ws.onclose = () => { this.connected = false; }; } private disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } this.connected = false; } private handleClear() { this.lines = []; } private handleAutoScrollToggle() { this.autoScroll = !this.autoScroll; } private scrollToBottom() { const body = this.shadowRoot?.querySelector('.output-body'); if (body) { body.scrollTop = body.scrollHeight; } } render() { if (!this.processId) { return html`
Select a process to view its output.
`; } return html`
Output: ${this.processId}
${this.lines.length === 0 ? html`
Waiting for output\u2026
` : this.lines.map( (line) => html`
${line.stream}${line.text}
`, )}
`; } } declare global { interface HTMLElementTagNameMap { 'core-process-output': ProcessOutput; } }