feat(mirror): capture engine and tool output via a pty - #92
Merged
Conversation
The session page showed the pit's own output and nothing else. teeOutput() patches process.stdout/stderr, but engines and tools are spawned with `stdio: "inherit"`, so the child writes to the tty's file descriptors directly and not one of its bytes passes through this process. Everything you actually went to the terminal to do — claude, opencode, gh, ugig — was invisible. A pipe is not the fix: every one of these programs checks isTTY and drops colour, prompts, and full-screen UI the moment it is talking to one. node-pty is not the fix either — it is a native module, and moshcode installs by untarring a release and running node. So run the child under script(1), which allocates a real pseudo-terminal using nothing but the base system, and follow its transcript. Verified end to end: the child still reports isTTY=true, its exit code still propagates (23 in and 23 out), and its output arrives in the sink. Capture only engages when a mirror is actually attached, so an unmirrored session keeps the exact inherit path it has today and the blast radius stays inside mirrored sessions. No usable script(1), or MOSHCODE_MIRROR_PTY=0, falls back the same way. `script -q` silences its "Script started/done" notices on the terminal but still writes them into the transcript, so those are stripped — anchored to the start and end so output that merely contains the words survives. Not covered: the session page renders SGR colour only (sessions.mjs drops every other escape), so a full-screen TUI will arrive but not render correctly. That needs xterm.js on the page, plus keystroke relay for arrow keys and history. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The session page showed the pit's own output and nothing else — "it only prints sometimes."
Why
teeOutput()patchesprocess.stdout/stderr, but children are spawned like this:inherithands the child the tty's own file descriptors, so its bytes never passthrough this process. Result: you get the banner, prompts,
/help, and hand-offnotices — and nothing from
claude,opencode,gh, orugig. Not intermittent;exactly and always.
mirror.mjsdocumented it as a known limitation.The transport was never the problem:
apps/pwaalready streams over SSE withreconnect.
Why script(1)
isTTYand drops colour,prompts, and full-screen UI when it sees one.
running node (
install.sh) — no compiler in that path.script(1)allocates the same real pseudo-terminal using only the base system.Verified end to end
Ran a probe child through the new path:
Blast radius
Capture engages only when a mirror is attached. No mirror → the untouched
inheritpath, byte for byte what ships today. No usablescript(1), orMOSHCODE_MIRROR_PTY=0→ same fallback. util-linux and BSD flag sets are bothhandled and pinned by tests; anything unrecognised falls back rather than guessing.
script -qsilences its "Script started/done" notices on the terminal but stillwrites them to the transcript, so those are stripped — anchored to start/end so
child output containing those words survives.
Tests
12 new in
test/pty.test.mjs(both flavours' argv,shQuoteinjection safety,banner stripping, follower streaming + tail drain). Full suite: 325 tests, 247
pass, 0 fail, 78 pre-existing skips.
Not in this PR
The page renders SGR colour only —
sessions.mjs:352drops every non-SGR escape. Soa full-screen TUI now arrives but won't render. Making the session page a real
terminal (arrow keys, history, cursor addressing) needs xterm.js plus raw keystroke
relay into the pty. That's the follow-up.
🤖 Generated with Claude Code