Skip to content

Feature request: command exit codes, PTY exit events, output callback, and writeAndWait helper #26

@holiber

Description

@holiber

Motivation

While building terminal automation on top of jabterm (in the browser2video project), several gaps emerged between what jabterm provides and what consumers of a browser terminal library need for reliable automation and testing. The workarounds are fragile and would benefit from first-class support in jabterm.

Real-world pain points

  1. Exit code detection — after running pnpm install or git clone, the only way to check success is parsing terminal output with regexes like /ERR!/i or /fatal|error/i. These are brittle and miss edge cases.
  2. PTY exit information — when the shell process exits, the client only receives a WebSocket close code (1000 vs 1011) but not the actual exit code or signal.
  3. Reacting to output — the React component has onOpen/onClose/onError callbacks but no way to programmatically react to terminal output (e.g., detect when a dev server prints its URL).
  4. Command-and-wait pattern — the most common automation pattern is "send a command, wait for it to finish". This currently requires send() + polling readNew() in a loop with manual stabilization logic.

Proposed features

1. Command exit code tracking (highest priority)

Problem: No way to get $? without typing echo $? visibly in the terminal.

Proposal: Shell integration that injects a PROMPT_COMMAND (bash) / precmd (zsh) hook to capture $? after each command and forward it as a structured WebSocket message.

Server → Client message:

{ "type": "commandEnd", "exitCode": 0 }

React component API:

<JabTerm
  wsUrl="ws://localhost:3223"
  onCommandEnd={(exitCode: number) => {
    if (exitCode !== 0) console.error(`Command failed with code ${exitCode}`);
  }}
/>

Imperative handle API:

const handle = ref.current!;
handle.getLastExitCode();        // returns number | null
handle.waitForCommandEnd(10000); // Promise<number> — resolves with exit code

Server option to enable:

createJabtermServer({
  shellIntegration: true, // injects PROMPT_COMMAND/precmd hooks
});

Notes:

  • Should be opt-in (default false) to avoid side effects for users who don't need it
  • Needs shell-specific handling: bash (PROMPT_COMMAND), zsh (precmd), fish (fish_prompt); skip for unsupported shells
  • The hook should emit an OSC escape sequence that the server intercepts from PTY output before forwarding to the client

2. PTY exit event forwarded to client

Problem: The server already handles ptyProcess.onExit({ exitCode, signal }) but only logs it and closes the WebSocket. The client can't distinguish between exit code 0 vs 130 (Ctrl+C) vs 1 (error).

Proposal: Send a structured message before closing the WebSocket:

Server → Client message:

{ "type": "ptyExit", "exitCode": 0, "signal": null }

React component API:

<JabTerm
  wsUrl="ws://localhost:3223"
  onExit={(exitCode: number, signal: number | null) => {
    console.log(`Shell exited: code=${exitCode}, signal=${signal}`);
  }}
/>

Implementation: In jabtermServer.ts, inside ptyProcess.onExit(...), send the message via WebSocket before closing it:

ptyProcess.onExit(({ exitCode, signal }) => {
  // ... existing cleanup ...
  if (ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "ptyExit", exitCode, signal }));
  }
  // ... existing close logic ...
});

This is a small, backward-compatible change.


3. onData callback on the React component

Problem: No way to react to terminal output programmatically from React. The only options are polling readNew() or accessing xterm.js directly via getXterm().

Proposal: Add an onData prop:

<JabTerm
  wsUrl="ws://localhost:3223"
  onData={(data: string) => {
    if (data.includes("Local:")) {
      // Dev server is ready
    }
  }}
/>

Implementation: In the React component's ws.onmessage handler, call the callback after writing to xterm:

ws.onmessage = (ev) => {
  const data = typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);
  term.write(data);
  onData?.(data);
};

4. writeAndWait helper on the imperative handle

Problem: The most common automation pattern is:

handle.send("pnpm install\n");
// ... poll readNew() in a loop waiting for prompt or specific output ...

Proposal: Add a writeAndWait method that sends input and resolves when output stabilizes:

// Wait for output to stabilize (no new data for `quietMs`)
const output = await handle.writeAndWait("pnpm install\n", {
  quietMs: 500,      // resolve after 500ms of silence (default: 300)
  timeout: 120000,   // overall timeout
});

// Wait for a specific marker in output
const output = await handle.writeAndWait("pnpm install\n", {
  waitFor: "done in",  // resolve when this string appears
  timeout: 120000,
});

// Wait for exit code (requires shellIntegration)
const output = await handle.writeAndWait("pnpm install\n", {
  waitForCommand: true, // resolve on next commandEnd event
  timeout: 120000,
});

Return type:

interface WriteAndWaitResult {
  output: string;         // captured output during the command
  exitCode?: number;      // if shellIntegration is enabled
}

5. Shell integration protocol (stretch goal)

Problem: Advanced automation needs structured information about the terminal state: current working directory, whether a command is running, command boundaries.

Proposal: Full shell integration similar to VS Code / iTerm2, using OSC escape sequences:

Event OSC Sequence WS Message
Command start \x1b]633;C\x07 { "type": "commandStart" }
Command end \x1b]633;D;{exitCode}\x07 { "type": "commandEnd", "exitCode": 0 }
Cwd change \x1b]633;P;Cwd={path}\x07 { "type": "cwdChange", "cwd": "/home/user" }
Prompt start \x1b]633;A\x07 { "type": "promptStart" }
Prompt end \x1b]633;B\x07 { "type": "promptEnd" }

This would enable:

handle.getCwd();           // "/home/user/project"
handle.isRunning();        // true/false
handle.getLastExitCode();  // 0

This is a larger effort and could be a follow-up to the simpler features above.


Priority order

  1. PTY exit event — smallest change, backward-compatible, immediately useful
  2. onData callback — small change, unblocks reactive patterns
  3. Command exit code tracking — highest value but requires shell integration work
  4. writeAndWait helper — convenience API, can be built on top of onData + onCommandEnd
  5. Full shell integration — stretch goal, builds on exit code tracking

Backward compatibility

All proposed features are additive:

  • New WS message types are ignored by older clients
  • New props/methods are optional
  • shellIntegration is opt-in

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions