This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

Add session_start/session_exit events, relay integration, and pty list --remote

Nathan Herald (Apr 14, 2026, 5:06 PM +0200) e17fb022 6fe3ce22

+44 -13
+4
CHANGELOG.md
··· 9 9 - Add `pty list --remote` to include remote hosts in the text and JSON output 10 10 - Graceful degradation: if pty-relay is not installed, nothing changes 11 11 12 + ### Events 13 + - Add `session_start` event — emitted when a session is created, includes tags for filtering 14 + - Add `session_exit` event — emitted when a session's child process exits, includes exit code 15 + 12 16 ### TUI framework 13 17 - Export `SelectableGroup<T>` interface from `@myobie/pty/tui` 14 18 - `groupedSelectable` `renderHeader` callback now receives the full group object instead of `(title, count)`
+1
src/client-api.ts
··· 33 33 type EventRecord, type EventBase, 34 34 type BellEvent, type TitleChangeEvent, type NotificationEvent, 35 35 type FocusRequestEvent, type CursorVisibleEvent, 36 + type SessionStartEvent, type SessionExitEvent, 36 37 type SessionRestartEvent, type SessionFailedEvent, 37 38 type SupervisorStartEvent, type SupervisorStopEvent, 38 39 type FollowerOptions,
+20
src/events.ts
··· 8 8 NOTIFICATION: "notification", 9 9 FOCUS_REQUEST: "focus_request", 10 10 CURSOR_VISIBLE: "cursor_visible", 11 + SESSION_START: "session_start", 12 + SESSION_EXIT: "session_exit", 11 13 SESSION_RESTART: "session_restart", 12 14 SESSION_FAILED: "session_failed", 13 15 SUPERVISOR_START: "supervisor_start", ··· 46 48 type: "cursor_visible"; 47 49 } 48 50 51 + export interface SessionStartEvent extends EventBase { 52 + type: "session_start"; 53 + tags?: Record<string, string>; 54 + } 55 + 56 + export interface SessionExitEvent extends EventBase { 57 + type: "session_exit"; 58 + exitCode: number; 59 + } 60 + 49 61 export interface SessionRestartEvent extends EventBase { 50 62 type: "session_restart"; 51 63 restartCount: number; ··· 72 84 | NotificationEvent 73 85 | FocusRequestEvent 74 86 | CursorVisibleEvent 87 + | SessionStartEvent 88 + | SessionExitEvent 75 89 | SessionRestartEvent 76 90 | SessionFailedEvent 77 91 | SupervisorStartEvent ··· 277 291 return `${prefix} focus requested`; 278 292 case "cursor_visible": 279 293 return `${prefix} cursor restored`; 294 + case "session_start": { 295 + const tagStr = event.tags ? " " + Object.entries(event.tags).map(([k, v]) => `${k}=${v}`).join(" ") : ""; 296 + return `${prefix} started${tagStr}`; 297 + } 298 + case "session_exit": 299 + return `${prefix} exited (code ${event.exitCode})`; 280 300 case "session_restart": 281 301 return `${prefix} restarted (attempt ${event.restartCount}, backoff ${event.backoffMs}ms)`; 282 302 case "session_failed":
+4
src/server.ts
··· 289 289 this.exited = true; 290 290 this.exitCode = exitCode; 291 291 this.broadcast(encodeExit(exitCode)); 292 + this.emitEvent(EventType.SESSION_EXIT, { exitCode }); 292 293 // Save exit status immediately so the session shows as "exited" 293 294 // in pty list during the cleanup window. lastLines may be incomplete 294 295 // here since PTY data could still be in-flight — close() will ··· 320 321 displayCommand: options.displayCommand, 321 322 cwd: options.cwd, 322 323 createdAt: new Date().toISOString(), 324 + ...(options.tags && Object.keys(options.tags).length > 0 ? { tags: options.tags } : {}), 325 + }); 326 + this.emitEvent(EventType.SESSION_START, { 323 327 ...(options.tags && Object.keys(options.tags).length > 0 ? { tags: options.tags } : {}), 324 328 }); 325 329 resolve();
+4 -4
tests/events.test.ts
··· 361 361 362 362 // Send a bell character 363 363 socket.write(encodeData("printf '\\a'\n")); 364 - const events = await waitForEvents(name, 1); 364 + const events = await waitForEvents(name, 2); 365 365 socket.destroy(); 366 366 367 367 const bells = events.filter((e) => e.type === "bell"); ··· 379 379 380 380 // Set the terminal title via OSC 0 381 381 socket.write(encodeData("printf '\\033]0;My Custom Title\\a'\n")); 382 - const events = await waitForEvents(name, 1); 382 + const events = await waitForEvents(name, 2); 383 383 socket.destroy(); 384 384 385 385 const titles = events.filter((e) => e.type === "title_change"); ··· 428 428 429 429 // Send an iTerm2-style notification 430 430 socket.write(encodeData("printf '\\033]9;Build complete\\a'\n")); 431 - const events = await waitForEvents(name, 1); 431 + const events = await waitForEvents(name, 2); 432 432 socket.destroy(); 433 433 434 434 const notifs = events.filter((e) => e.type === "notification"); ··· 449 449 socket.write( 450 450 encodeData("printf '\\033]777;notify;Build;All tests passed\\a'\n") 451 451 ); 452 - const events = await waitForEvents(name, 1); 452 + const events = await waitForEvents(name, 2); 453 453 socket.destroy(); 454 454 455 455 const notifs = events.filter((e) => e.type === "notification");
+11 -9
tests/supervisor-hardening.test.ts
··· 246 246 it("kills orphaned daemon on waitForSocket timeout", () => { 247 247 const dir = makeSessionDir(); 248 248 249 - // Count server.js processes before 250 - const before = spawnSync("pgrep", ["-f", "server.js"], { encoding: "utf-8" }); 251 - const beforeCount = before.stdout.trim().split("\n").filter(Boolean).length; 252 - 253 249 // Try to spawn with a nonexistent command — daemon will crash 254 250 const result = runCli(dir, "run", "-d", "--name", "leak-test", "--", "/nonexistent/command"); 255 251 256 - // Count server.js processes after 257 - const after = spawnSync("pgrep", ["-f", "server.js"], { encoding: "utf-8" }); 258 - const afterCount = after.stdout.trim().split("\n").filter(Boolean).length; 252 + // The command should have failed 253 + expect(result.status).not.toBe(0); 259 254 260 - // Should not have leaked a process 261 - expect(afterCount).toBeLessThanOrEqual(beforeCount); 255 + // No PID file should exist (daemon was cleaned up) 256 + const pidFile = path.join(dir, "leak-test.pid"); 257 + let leaked = false; 258 + try { 259 + const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); 260 + // Check if this process is still alive 261 + try { process.kill(pid, 0); leaked = true; } catch {} 262 + } catch {} 263 + expect(leaked).toBe(false); 262 264 }, 15000); 263 265 }); 264 266