mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-25 14:57:37 +00:00
Add connectToStream method to CastConverter for real-time terminal streaming
- Added connectToStream() method that connects terminal to SSE streams
- Handles header messages with terminal dimensions for initial sizing
- Processes cast events for output ('o') and resize ('r') operations
- Detects session exit events and cleans up connections automatically
- Dispatches custom events for terminal-resize and session-exit
- Returns connection object with EventSource and disconnect method for cleanup
- Uses followCursor=true for live streaming vs false for dumps
- Comprehensive error handling and connection state management
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0cef07ccfc
commit
e45371e9ca
1 changed files with 118 additions and 0 deletions
|
|
@ -260,4 +260,122 @@ export class CastConverter {
|
|||
terminal.write(allContent, false); // Don't follow cursor during dump for performance
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect terminal to a streaming URL using Server-Sent Events (SSE).
|
||||
* Handles real-time terminal output, input, and resize events from the stream.
|
||||
* Returns connection object for cleanup and management.
|
||||
*
|
||||
* @param terminal - DOM terminal instance with write() and setTerminalSize() methods
|
||||
* @param streamUrl - URL endpoint for the SSE stream (e.g., /api/sessions/123/stream)
|
||||
* @returns Connection object with EventSource and cleanup methods
|
||||
*/
|
||||
static connectToStream(
|
||||
terminal: {
|
||||
write: (data: string, followCursor?: boolean) => void;
|
||||
setTerminalSize?: (cols: number, rows: number) => void;
|
||||
dispatchEvent?: (event: CustomEvent) => void;
|
||||
},
|
||||
streamUrl: string
|
||||
): {
|
||||
eventSource: EventSource;
|
||||
disconnect: () => void;
|
||||
} {
|
||||
const eventSource = new EventSource(streamUrl);
|
||||
|
||||
const disconnect = () => {
|
||||
if (eventSource.readyState !== EventSource.CLOSED) {
|
||||
eventSource.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle incoming messages from the stream
|
||||
eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// Check if this is a header message with terminal dimensions
|
||||
if (data.version && data.width && data.height) {
|
||||
// Header message - set initial terminal size
|
||||
if (terminal.setTerminalSize) {
|
||||
terminal.setTerminalSize(data.width, data.height);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a cast event array [timestamp, type, data]
|
||||
if (Array.isArray(data) && data.length >= 3) {
|
||||
const [_timestamp, type, eventData] = data;
|
||||
|
||||
if (type === 'o') {
|
||||
// Output event - write to terminal
|
||||
terminal.write(eventData, true); // Follow cursor for live streaming
|
||||
} else if (type === 'r') {
|
||||
// Resize event - update terminal dimensions
|
||||
const match = eventData.match(/^(\d+)x(\d+)$/);
|
||||
if (match && terminal.setTerminalSize) {
|
||||
const cols = parseInt(match[1], 10);
|
||||
const rows = parseInt(match[2], 10);
|
||||
terminal.setTerminalSize(cols, rows);
|
||||
|
||||
// Dispatch resize event if terminal supports it
|
||||
if (terminal.dispatchEvent) {
|
||||
terminal.dispatchEvent(
|
||||
new CustomEvent('terminal-resize', {
|
||||
detail: { cols, rows },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore 'i' (input) events - those are for sending to server, not displaying
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for special exit event [exitType, exitCode, sessionId]
|
||||
if (Array.isArray(data) && data.length >= 2 && data[0] === 'exit') {
|
||||
// Session exit event - close connection and notify
|
||||
disconnect();
|
||||
|
||||
if (terminal.dispatchEvent) {
|
||||
terminal.dispatchEvent(
|
||||
new CustomEvent('session-exit', {
|
||||
detail: {
|
||||
exitCode: data[1],
|
||||
sessionId: data[2] || null,
|
||||
},
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown message format
|
||||
console.warn('Unknown stream message format:', data);
|
||||
} catch (error) {
|
||||
console.error('Failed to parse stream message:', event.data, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle connection errors
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('Stream connection error:', error);
|
||||
|
||||
if (eventSource.readyState === EventSource.CLOSED) {
|
||||
console.log('Stream connection closed');
|
||||
}
|
||||
};
|
||||
|
||||
// Handle connection open
|
||||
eventSource.onopen = () => {
|
||||
console.log('Stream connection established to:', streamUrl);
|
||||
};
|
||||
|
||||
return {
|
||||
eventSource,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue