feat(v0.3): H1 — tmux keepalive (sessions survive a server restart)
- src/session/tmux.ts: sync tmux CLI wrappers (available/has/kill), best-effort - config: useTmux from USE_TMUX env (1/0/auto→detect tmux on PATH) - session: when useTmux, spawn 'tmux new-session -A -s web_<id> <shell>' (the node-pty proc is a tmux CLIENT); createSession takes an optional id for re-attach; Session.tmuxName; kill() runs tmux kill-session - manager: handleAttach re-attaches to a surviving 'web_<id>' tmux session after a restart (Case 3.5); shutdown kills only the client pty for tmux sessions so the shell keeps running - tests: USE_TMUX:0 in shared integration cfg (no tmux leakage); unit CFGs useTmux:false; integration 'H1' (real tmux): set var → restart server → re-attach → var survived. 208 tests green.
This commit is contained in:
@@ -37,6 +37,7 @@ import type {
|
||||
import { WS_OPEN } from '../types.js';
|
||||
import { serialize } from '../protocol.js';
|
||||
import { createRingBuffer } from './ring-buffer.js';
|
||||
import { tmuxName, killSession } from './tmux.js';
|
||||
|
||||
/** Send a server message to `ws` only if it is OPEN (M5). Never throws on a
|
||||
* closed socket; forwarding to a dead ws is simply a no-op. */
|
||||
@@ -57,14 +58,21 @@ export function createSession(
|
||||
dims: Dims,
|
||||
now: number,
|
||||
onExit: (session: Session) => void,
|
||||
// H1: pass an existing id to RE-ATTACH to a surviving tmux session (e.g. after
|
||||
// a server restart). Default = a fresh id for a brand-new session.
|
||||
id: string = randomUUID(),
|
||||
): Session {
|
||||
// Generate the id first so it can be injected into the shell env: Claude Code
|
||||
// hooks running inside this shell read $WEBTERM_SESSION to tell us which tab
|
||||
// an event belongs to, and POST to $WEBTERM_HOOK_URL (H2).
|
||||
const id = randomUUID();
|
||||
// The id is injected into the shell env so Claude Code hooks know which tab an
|
||||
// event belongs to ($WEBTERM_SESSION) and where to POST ($WEBTERM_HOOK_URL, H2).
|
||||
const tName = cfg.useTmux ? tmuxName(id) : null;
|
||||
|
||||
// M4: let a spawn failure (e.g. missing shell) propagate synchronously.
|
||||
const pty: IPty = spawn(cfg.shellPath, [], {
|
||||
// H1: under tmux, the node-pty process is a tmux CLIENT; `new-session -A`
|
||||
// attaches to web_<id> if it exists (restart survival) or creates it.
|
||||
const file = tName !== null ? 'tmux' : cfg.shellPath;
|
||||
const args = tName !== null ? ['new-session', '-A', '-s', tName, cfg.shellPath] : [];
|
||||
|
||||
// M4: let a spawn failure (e.g. missing shell / tmux) propagate synchronously.
|
||||
const pty: IPty = spawn(file, args, {
|
||||
name: 'xterm-256color',
|
||||
cols: dims.cols,
|
||||
rows: dims.rows,
|
||||
@@ -91,6 +99,7 @@ export function createSession(
|
||||
exitedAt: null,
|
||||
exitCode: null,
|
||||
claudeStatus: 'unknown',
|
||||
tmuxName: tName,
|
||||
pty,
|
||||
};
|
||||
|
||||
@@ -149,7 +158,12 @@ export function resize(session: Session, cols: number, rows: number): void {
|
||||
session.pty.resize(cols, rows);
|
||||
}
|
||||
|
||||
/** Kill the underlying PTY (process exit / idle reclaim). */
|
||||
/**
|
||||
* Kill the session (idle reclaim). Under tmux this ends the actual shell via
|
||||
* `tmux kill-session` (H1); killing only the client pty would leave the tmux
|
||||
* session running. Always also kills the client pty.
|
||||
*/
|
||||
export function kill(session: Session): void {
|
||||
if (session.tmuxName !== null) killSession(session.tmuxName);
|
||||
session.pty.kill();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user