feat(v0.4): multi-device session sharing (backend)
Relax the one-WS-per-session invariant (#5) so multiple devices mirror the same live terminal (everyone sees output, anyone can type — tmux-style). - types.ts: Session.attachedWs → clients Set + clientDims Map; add cwd; SessionManager.list() + LiveSessionInfo - session.ts: broadcast() output/exit to all clients; attachWs JOINS (no kick, takes dims); detachWs removes one client, stamps detachedAt only when the last leaves; setClientDims resizes PTY to the MIN cols/rows across clients - manager.ts: join instead of kick; broadcast status; onExit drops only when a client was attached; list() for discovery - server.ts: GET /live-sessions; resize → per-client setClientDims; close → detach one client; permission gate uses clients.size - tests: rewrote session/manager tests for multi-client (join/broadcast/min-dims/ last-detach) + list(); 222 pass, tsc clean
This commit is contained in:
34
src/types.ts
34
src/types.ts
@@ -140,9 +140,15 @@ export interface SessionMeta {
|
||||
export interface Session {
|
||||
readonly meta: SessionMeta;
|
||||
readonly buffer: RingBuffer;
|
||||
/** attached ws, or null = detached but PTY still alive (vibe-coding core). */
|
||||
attachedWs: WebSocketLike | null;
|
||||
detachedAt: number | null; // detach time
|
||||
/** All currently-attached clients (multi-device mirror sharing). Empty set =
|
||||
* detached but PTY still alive (vibe-coding core). Output/exit/status are
|
||||
* broadcast to every client; any client can send input (shared control). */
|
||||
readonly clients: Set<WebSocketLike>;
|
||||
/** Per-client requested terminal dims; the PTY uses the MIN cols/rows across
|
||||
* all clients (tmux-style) so every device sees content without overflow. */
|
||||
readonly clientDims: Map<WebSocketLike, Dims>;
|
||||
/** Time the LAST client left (clients became empty); null while ≥1 attached. */
|
||||
detachedAt: number | null;
|
||||
/** last pty.onData timestamp; reapIdle liveness proxy (M3). */
|
||||
lastOutputAt: number;
|
||||
/** PTY exit time; null = alive. Once set: writeInput/resize are ignored (L4),
|
||||
@@ -151,6 +157,8 @@ export interface Session {
|
||||
exitCode: number | null;
|
||||
/** Claude Code activity from hooks (H2); updated by manager.handleHookEvent. */
|
||||
claudeStatus: ClaudeStatus;
|
||||
/** Spawn directory, for the /live-sessions label; null when unknown. */
|
||||
cwd: string | null;
|
||||
/** tmux session name (H1) when running under tmux, else null. */
|
||||
readonly tmuxName: string | null;
|
||||
readonly pty: IPty;
|
||||
@@ -159,14 +167,24 @@ export interface Session {
|
||||
// impl anchors [src/session/session.ts]:
|
||||
// createSession(cfg: Config, dims: Dims, now: number, onExit: (s: Session) => void): Session
|
||||
// // spawn failure THROWS, not swallowed (M4)
|
||||
// attachWs(session: Session, ws: WebSocketLike): WebSocketLike | null // returns kicked old ws
|
||||
// detachWs(session: Session, now: number): void // never kills PTY
|
||||
// writeInput(session: Session, data: string): void // no-op after exit (L4)
|
||||
// resize(session: Session, cols: number, rows: number): void // no-op after exit; idempotent
|
||||
// attachWs(session: Session, ws: WebSocketLike, dims: Dims): void // adds a client, replays buffer (no kick)
|
||||
// detachWs(session: Session, ws: WebSocketLike, now: number): void // removes one client; never kills PTY
|
||||
// writeInput(session: Session, data: string): void // no-op after exit (L4)
|
||||
// setClientDims(session: Session, ws: WebSocketLike, cols, rows): void // PTY = min over clients (L4 no-op after exit)
|
||||
// kill(session: Session): void
|
||||
|
||||
/* ──────────────────────── manager (§3.5) ─────────────────────── */
|
||||
|
||||
/** A live (or just-exited) server session, for the /live-sessions discovery list. */
|
||||
export interface LiveSessionInfo {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
clientCount: number; // how many devices are currently attached
|
||||
status: ClaudeStatus;
|
||||
exited: boolean;
|
||||
cwd: string | null;
|
||||
}
|
||||
|
||||
export interface SessionManager {
|
||||
handleAttach(
|
||||
ws: WebSocketLike,
|
||||
@@ -176,6 +194,8 @@ export interface SessionManager {
|
||||
cwd?: string,
|
||||
): Session;
|
||||
get(id: string): Session | undefined;
|
||||
/** Live sessions for multi-device discovery (newest first). */
|
||||
list(): LiveSessionInfo[];
|
||||
/** Set a session's Claude status (from a hook) and push it to the attached ws (H2/H3). */
|
||||
handleHookEvent(
|
||||
sessionId: string,
|
||||
|
||||
Reference in New Issue
Block a user