Complete rewrite from tmux + JSONL parsing to a clean PTY-based approach. The proxy spawns Claude CLI in a pseudo-terminal via node-pty and relays terminal I/O as binary WebSocket frames to the simple-chat backend, which forwards them to the browser where xterm.js renders a full terminal. - node-pty PTY manager with 50KB replay buffer per session - Binary frame protocol with chatId multiplexing - WebSocket client with auto-reconnection and heartbeat - Directory listing and session listing for the web UI - README with setup instructions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
109 lines
2.3 KiB
TypeScript
109 lines
2.3 KiB
TypeScript
// === WebSocket Protocol Types (xterm.js + node-pty) ===
|
|
|
|
// Binary frame header for terminal data multiplexing:
|
|
// [1 byte direction][36 bytes chatId ASCII][...payload]
|
|
// Direction: 0x01 = PTY output (proxy→backend→frontend)
|
|
// 0x02 = user input (frontend→backend→proxy)
|
|
export const DIR_PTY_OUTPUT = 0x01;
|
|
export const DIR_USER_INPUT = 0x02;
|
|
export const CHAT_ID_LENGTH = 36; // UUID length
|
|
|
|
export function encodeBinaryFrame(direction: number, chatId: string, data: Buffer): Buffer {
|
|
const header = Buffer.alloc(1 + CHAT_ID_LENGTH);
|
|
header[0] = direction;
|
|
header.write(chatId, 1, CHAT_ID_LENGTH, 'ascii');
|
|
return Buffer.concat([header, data]);
|
|
}
|
|
|
|
export function decodeBinaryFrame(frame: Buffer): { direction: number; chatId: string; data: Buffer } {
|
|
const direction = frame[0];
|
|
const chatId = frame.subarray(1, 1 + CHAT_ID_LENGTH).toString('ascii');
|
|
const data = frame.subarray(1 + CHAT_ID_LENGTH);
|
|
return { direction, chatId, data };
|
|
}
|
|
|
|
// JSON control messages
|
|
|
|
export type BackendMessageType =
|
|
| 'sync_state'
|
|
| 'create_pty'
|
|
| 'kill_pty'
|
|
| 'resize'
|
|
| 'list_directories'
|
|
| 'list_sessions';
|
|
|
|
export type ProxyMessageType =
|
|
| 'pty_ready'
|
|
| 'pty_closed'
|
|
| 'directories'
|
|
| 'sessions_list'
|
|
| 'error';
|
|
|
|
export type WsMessageType = BackendMessageType | ProxyMessageType;
|
|
|
|
export interface WsMessage {
|
|
type: WsMessageType;
|
|
requestId?: string;
|
|
payload: unknown;
|
|
}
|
|
|
|
// === Payload types ===
|
|
|
|
export interface SyncStatePayload {
|
|
chats: AgentChatInfo[];
|
|
}
|
|
|
|
export interface AgentChatInfo {
|
|
id: string;
|
|
workDir: string | null;
|
|
sessionId: string | null;
|
|
cols?: number;
|
|
rows?: number;
|
|
}
|
|
|
|
export interface CreatePtyPayload {
|
|
chatId: string;
|
|
dir: string;
|
|
resumeSessionId?: string;
|
|
cols: number;
|
|
rows: number;
|
|
}
|
|
|
|
export interface KillPtyPayload {
|
|
chatId: string;
|
|
}
|
|
|
|
export interface ResizePayload {
|
|
chatId: string;
|
|
cols: number;
|
|
rows: number;
|
|
}
|
|
|
|
export interface PtyReadyPayload {
|
|
chatId: string;
|
|
}
|
|
|
|
export interface PtyClosedPayload {
|
|
chatId: string;
|
|
exitCode: number;
|
|
}
|
|
|
|
export interface ListDirectoriesPayload {
|
|
path: string;
|
|
}
|
|
|
|
export interface DirectoryEntry {
|
|
name: string;
|
|
isDirectory: boolean;
|
|
}
|
|
|
|
export interface DirectoriesPayload {
|
|
requestId: string;
|
|
entries: DirectoryEntry[];
|
|
}
|
|
|
|
export interface ErrorPayload {
|
|
chatId?: string;
|
|
message: string;
|
|
}
|