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
- 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.
- 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.
- 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).
- 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
- PTY exit event — smallest change, backward-compatible, immediately useful
onData callback — small change, unblocks reactive patterns
- Command exit code tracking — highest value but requires shell integration work
writeAndWait helper — convenience API, can be built on top of onData + onCommandEnd
- 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
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
pnpm installorgit clone, the only way to check success is parsing terminal output with regexes like/ERR!/ior/fatal|error/i. These are brittle and miss edge cases.onOpen/onClose/onErrorcallbacks but no way to programmatically react to terminal output (e.g., detect when a dev server prints its URL).send()+ pollingreadNew()in a loop with manual stabilization logic.Proposed features
1. Command exit code tracking (highest priority)
Problem: No way to get
$?without typingecho $?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:
Imperative handle API:
Server option to enable:
Notes:
false) to avoid side effects for users who don't need itPROMPT_COMMAND), zsh (precmd), fish (fish_prompt); skip for unsupported shells2. 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:
Implementation: In
jabtermServer.ts, insideptyProcess.onExit(...), send the message via WebSocket before closing it:This is a small, backward-compatible change.
3.
onDatacallback on the React componentProblem: No way to react to terminal output programmatically from React. The only options are polling
readNew()or accessing xterm.js directly viagetXterm().Proposal: Add an
onDataprop:Implementation: In the React component's
ws.onmessagehandler, call the callback after writing to xterm:4.
writeAndWaithelper on the imperative handleProblem: The most common automation pattern is:
Proposal: Add a
writeAndWaitmethod that sends input and resolves when output stabilizes:Return type:
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:
\x1b]633;C\x07{ "type": "commandStart" }\x1b]633;D;{exitCode}\x07{ "type": "commandEnd", "exitCode": 0 }\x1b]633;P;Cwd={path}\x07{ "type": "cwdChange", "cwd": "/home/user" }\x1b]633;A\x07{ "type": "promptStart" }\x1b]633;B\x07{ "type": "promptEnd" }This would enable:
This is a larger effort and could be a follow-up to the simpler features above.
Priority order
onDatacallback — small change, unblocks reactive patternswriteAndWaithelper — convenience API, can be built on top ofonData+onCommandEndBackward compatibility
All proposed features are additive:
shellIntegrationis opt-in