8000 fix(minimald): unwind the terminal when the session process exits (#1210) by norrietaylor · Pull Request #1261 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

fix(minimald): unwind the terminal when the session process exits (#1210) - #1261

Merged
norrietaylor merged 3 commits into
mainfrom
fix/1210-unwind-codes-on-process-exit-wt4
Aug 24, 2026
Merged

norrietaylor merged 3 commits into
mainfrom
fix/1210-unwind-codes-on-process-exit-wt4

Conversation

@norrietaylor
@norrietaylor norrietaylor commented Aug 21, 2026 •
Copy link
Copy Markdown
Member

Addresses the remaining half of #1210.

#1253 fixed the notification half — the user is now told when a session is
SIGKILLed — but it made the other half more visible: on teardown the terminal was
left in a broken state. Three of the four teardown arms wrote self.unwind_codes();
the TeardownDueToProcessExit arm did not, so a session whose process simply exited
left mouse reporting, the alt screen, and cursor visibility as the session had set them.

The ProcessExit arm now emits the same unwind sequence as its siblings, and the client
gained a blind unwind so the reset still happens against an old daemon or a dropped
transport.

Reviewer notes

  • The client's blind unwind writes \x1b[?1049l unconditionally, while the daemon's
    unwind_codes() gates it behind if live.alternate_screen(). On xterm-family
    terminals rmcup-from-the-normal-buffer does a DECRC-style cursor restore, so after a
    clean detach the cursor can jump to a previously-saved position. Cosmetic — but it
    is cosmetic damage introduced by a fix about cosmetic damage. The doc comment's claim
    that every sequence "is a no-op against a terminal that was never in that mode" holds
    for every code in the set except ?1049l; at minimum that claim wants qualifying.
  • session_via_ssh's interactive arm changed from exec() to fork+wait+exit, so min
    now sits between the pty and ssh. A signalled ssh used to make min die by that
    signal; it now exits 128+n. $? is identical, but a supervisor calling waitpid sees
    WIFEXITED where it saw WIFSIGNALED. Intentional and unit-tested.
  • scripts/e2e-attach-pty.py's hang guard SIGKILLs the pty child by pid — now min,
    not ssh — so a wedged attach can orphan an ssh plus its min proxy grandchild on the
    runner. Only reachable on the already-failing timeout path.
  • min dash's attach_and_resume does not arm TerminalUnwind, so the dash attach
    path lacks the belt-and-braces this PR adds to min session attach. Not a regression,
    but it is the other place a user attaches interactively.
  • notify_remote_teardown computes unwind_codes() unconditionally, allocating a fresh
    parser even when self.remote is None. Forced by the borrow checker; an
    if self.remote.is_some() pre-check would avoid it.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved terminal cleanup when interactive sessions exit unexpectedly or are interrupted.
    • Restores terminal modes such as mouse input, alternate screen, cursor visibility, bracketed paste, and application key modes.
    • Preserves and propagates the session’s exit status, including signal-based termination codes.
    • Preven 8000 ts unnecessary terminal resets during normal daemon-managed exits.

`TeardownDueToProcessExit` was the one teardown that carried no unwind
codes: its three siblings (supercede, shutdown, detach) each write them
before anything else, but a session process that died — SIGKILL included
— left the binding to render the exit notices and the shell-exit prompt
into whatever input modes the dead app had set, and the user's own shell
inherited them afterwards. A full-screen app killed mid-run therefore
left mouse reporting on and every mouse move printed escapes into the
shell (#1210).

The variant now carries the codes, computed at the single send site where
the host's parser is in scope, and the mainloop writes them before the
notices and before the prompt.

Belt-and-braces on the client: `min` arms an RAII `TerminalUnwind` around
an interactive attach so the tty is restored on every exit path, however
the attach ended. That means waiting on ssh rather than `exec()`ing it —
no Drop impl survives `exec()` — with ssh's status propagated as the
client's exit code so the observable behaviour is unchanged. It covers
the cases the daemon cannot: a daemon predating this fix, and a transport
that drops without a teardown at all.

Refs #1210
@norrietaylor
norrietaylor requested a review from a team as a code owner August 21, 2026 22:03
@coderabbitai
coderabbitai Bot commented Aug 21, 2026 •
Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72ec3937-9d07-4551-b879-569ba98257e4

📥 Commits

Reviewing files that changed from the base of the PR and between 5c6166a and 205519d.

📒 Files selected for processing (5)
  • crates/minimal/src/attach.rs
  • crates/minimal/src/lib.rs
  • crates/minimald/src/session_host.rs
  • crates/sessions/src/lib.rs
  • crates/sessions/src/terminal.rs

Included review availability: 0 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour.


📝 Walkthrough

Walkthrough

The change adds shared terminal reset sequences, client-side RAII cleanup, SSH exit-status propagation, and daemon-side teardown codes. The daemon sends state-specific reset bytes before exit notices. Tests cover reset composition, status handling, propagation, and output ordering.

Changes

Terminal teardown

Layer / File(s) Summary
Shared terminal reset contract
crates/sessions/src/terminal.rs, crates/sessions/src/lib.rs
The sessions crate exposes constants for disabling terminal modes, leaving alternate-screen mode, showing the cursor, resetting attributes, and disabling focus reporting.
Client attach cleanup
crates/minimal/src/attach.rs, crates/minimal/src/lib.rs
Interactive attach awaits SSH, returns SSH-derived exit codes, and uses TerminalUnwind for TTY cleanup after transport failures or signal termination. Tests cover reset bytes and exit-status mapping.
Daemon teardown propagation
crates/minimald/src/session_host.rs
The session host computes state-specific unwind bytes, includes them in process-exit teardown messages, and writes them before notices and the shell-exit prompt. Tests cover propagation, ordering, mouse reporting, and inactive modes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 20551

This change resets terminal state when a session process exits and adds a fallba 8000 ck client reset; no actionable merge-blocking risk remains beyond normal checks and review.

Suggested reviewers: evanspearman

Poem

A rabbit hops through terminal light,
Reset codes make the screen right.
SSH waits, then exits clean,
Modes unwind from every scene.
Ears up, the cursor shines—
Teardown follows careful lines.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the terminal unwind fix for session process exits and follows the repository’s Conventional Commit format.
Description check ✅ Passed The description clearly explains the problem, scope, design decisions, and reviewer concerns, but it omits explicit Testing and Checklist sections.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1210-unwind-codes-on-process-exit-wt4

Comment @coderabbitai help to get the list of available commands.

Comment thread crates/minimal/src/attach.rs Outdated
/// "only if currently set" narrowing — a client has no screen model to diff
/// against, and every sequence here is a no-op against a terminal that was
/// never in that mode.
const TERMINAL_UNWIND: &[u8] = concat!(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this already exists in the session host, grep for unwind codes

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right that it shouldn't live in two places — single-sourced in sessions now (1d3bb332), both sides reference the same constants.

But they're not equivalent, so I'd keep the session-host version:

  • unwind_codes() is state-aware — diffs live vs clean via input_mode_diff, emits ?1049l/?25h only when actually set.
  • The client constant is blind; it has no screen model to diff against.
  • Deleting the daemon side would make the common path stop narrowing. Strictly worse.

Your comment did surface a real bug in mine, though: ?1049l isn't the no-op I claimed. On xterm-family, rmcup-from-the-normal-buffer does a DECRC-style cursor restore — so after a clean detach the client's extra write can jump the cursor and redraw the prompt over existing content. A fix about cosmetic damage causing cosmetic damage.

So the fallback now fires only when ssh dies abnormally (255 or a signal):

  • Clean teardown → daemon spoke, client stays quiet, no duplicate write.
  • Broken transport → daemon never spoke, blind write earns its place.

Cross-PR note: I'd justified the fallback partly with "a daemon predating #1210 sends none." #1260 gates cmd_attach, and session_via_ssh sits downstream — once it lands, a CLI can't attach across builds at all. That half is void; dropped-transport is the only surviving case. Comment updated.

@twitchyliquid64
twitchyliquid64 commented Aug 21, 2026 •
Copy link
Copy Markdown
Contributor

Yeah if the plan is to replace the session host unwind codes we should move/delete that code in this PR also, duplicating it likely isn’t ideal

norrietaylor and others added 2 commits August 24, 2026 17:17
…back

The DEC private-mode sequences were spelled out twice — once in the
daemon's screen-aware `unwind_codes`, once in the `min` client's blind
fallback — so an edit to one could silently diverge from the other. Move
the byte literals into `sessions::terminal` as named constants and have
both sides reference them; the daemon keeps its narrowing (the
`input_mode_diff`, the `alternate_screen()` and `hide_cursor()` guards)
and its emitted bytes are unchanged.

The client's fallback also stops firing on the common path. `\x1b[?1049l`
is not inert against a terminal that never left the normal buffer: on
xterm-family terminals rmcup still performs a DECRC-style cursor restore,
so writing the blind set after a clean detach — where the daemon already
sent the correct narrow sequence — could jump the cursor to a stale saved
position and have the shell prompt redraw over existing content. The
guard is now disarmed unless ssh's exit says the transport broke (255, or
a death by signal); a normal exit, including the session process's own
non-zero status, proves the daemon was alive and speaking. A session
process that itself exits 255 is indistinguishable from an ssh transport
failure and gets a redundant unwind — documented at the predicate.

Also drops the stale second justification for the fallback: #1260
version-gates `cmd_attach`, so a CLI can no longer attach to a daemon
predating #1210 at all. The surviving reason is a dropped transport.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@norrietaylor
norrietaylor merged commit 8e44591 into main Aug 24, 2026
33 checks passed
@norrietaylor
norrietaylor deleted the fix/1210-unwind-codes-on-process-exit-wt4 branch August 24, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0