fix: remote panes lose TERM_PROGRAM, breaking inline image protocols - #2106
fix: remote panes lose TERM_PROGRAM, breaking inline image protocols#2106jvm wants to merge 6 commits into
Conversation
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe protocol version advances to 19, and client handshakes now carry optional outer terminal identity. The server propagates this identity to foreground pane environments, while monolithic and remote bridge paths initialize or forward it. ChangesOuter terminal identity
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ClientTransport
participant HeadlessServer
participant Pane
Client->>ClientTransport: Send Hello with term_program
ClientTransport->>HeadlessServer: Emit ClientConnected with normalized term_program
HeadlessServer->>Pane: Set outer terminal program
Pane->>Pane: Apply terminal identity to pane environment
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Greptile SummaryThe PR propagates the attached client’s terminal identity into newly spawned panes.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| src/client/mod.rs | Adds the detected outer-terminal identity to the client handshake. |
| src/protocol/wire.rs | Extends Hello with an optional terminal identity and bumps the protocol version. |
| src/server/client_transport.rs | Validates the reported identity and carries it into the server event stream. |
| src/server/clients.rs | Stores terminal identity as connection-scoped state. |
| src/server/headless.rs | Seeds startup identity and synchronizes pane identity with the foreground client lifecycle. |
| src/pane.rs | Applies the current terminal identity at pane spawn and removes stale terminal-specific variables. |
| src/remote/unix.rs | Passes terminal identity through the quoted remote bridge command for pre-attachment restoration. |
| src/terminal_notify.rs | Derives a normalized terminal-program name from explicit environment data or backend detection. |
Sequence Diagram
sequenceDiagram
participant T as Outer terminal
participant C as Client
participant S as Server
participant P as New pane
T->>C: Launch herdr
C->>C: Detect terminal identity
C->>S: Hello(term_program)
S->>S: Store identity per connection
S->>S: Select foreground client
S->>P: Spawn with TERM_PROGRAM
P-->>T: Render output through client
Reviews (4): Last reviewed commit: "fix: reject malformed client-reported te..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/server/headless.rs (1)
1052-1101: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPreserve the startup
TERM_PROGRAMseed until a real client disconnects.src/server/headless.rs:1052clearsOUTER_TERM_PROGRAMwheneverforeground_client_idisNone, buthandle_api_request_with_shutdown_check_inner()calls this before every API request, including the headlessWorkspaceCreate/TabCreaterequests fromhandle_deferred_requests_headless(). That wipes the launch identity seeded inrun_server()before those panes are spawned. Move the reset into the actual detach/disconnect path instead of the generic no-client sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b11d115b-fe40-474f-9420-01e02015d9f8
📒 Files selected for processing (16)
docs/next/api/herdr-api.schema.jsonsrc/client/mod.rssrc/main.rssrc/pane.rssrc/protocol/wire.rssrc/remote/unix.rssrc/server/autodetect.rssrc/server/client_transport.rssrc/server/clients.rssrc/server/headless.rssrc/terminal_notify.rstests/api_ping.rstests/cross_area.rstests/multi_client.rstests/server_headless.rstests/support/mod.rs
Panes inherit a snapshot of the server process environment, and the server is persistent: its TERM_PROGRAM belongs to whichever terminal started it, not to the client currently attached. Under `herdr --remote` the server is started by sshd and has no terminal identity at all, so remote panes saw no TERM_PROGRAM and applications fell back to their no-inline-image path. Carry the identity in ClientMessage::Hello, store it per connection, and apply it in apply_pane_terminal_env, which every pane spawn path already funnels through. The SSH bridge forwards no environment, so the wire is the only channel that reaches a remote server. TERM stays xterm-256color; only the outer identity is propagated. With no attached client TERM_PROGRAM is removed rather than left stale.
TERM_PROGRAM is not the only identity panes inherit from the server process. A server started under kitty and later attached from a terminal without inline image support still handed panes KITTY_WINDOW_ID, so applications selected a graphics protocol the real outer terminal could not render. Remove the identity variables that no longer describe the attached client alongside TERM_PROGRAM. Applications that read them fall back to the TERM_PROGRAM the client actually reported.
Move outer_term_program next to detect_backend in terminal_notify, which already owns terminal identification, and give the backend enum the TERM_PROGRAM spelling it maps to. The client no longer keeps its own copy of that table. Monolithic mode has no client to report an identity, so panes were losing TERM_PROGRAM with nothing set in its place. That process owns the terminal directly, so seed the identity from its own environment. Also fold the two identical no-foreground-client resets in sync_foreground_client_state into one, apply the identity without cloning it per spawn, keep a poisoned lock from stranding panes on a departed client, and extend the scrub list to the identity variables the previous set missed. LC_TERMINAL matters most: SSH forwards LC_* by default, so it reaches a remote server naming the local terminal.
…panes Panes are spawned while the app is built, which happens before any client can attach and report its terminal. Restored panes therefore had TERM_PROGRAM removed and never received one. Remote attach hits this every time: a server that has to restart restores the whole session first, so every pane the user lands in predates the client. Seed the identity at server startup from whoever launched the process. A locally spawned daemon inherits it from the client that started it. A remote server is started by the ssh bridge, which runs under `ssh -T` and so is given no pty and neither TERM_PROGRAM nor TERM; name the local terminal on that command line instead. An attaching client still overrides it.
1df625e to
4c59de6
Compare
|
Pushed a fix for the ubuntu failure: I could not have caught that locally — Grepped the macOS-excluded suites for any other assumptions this change could break; the remaining protocol references all derive from |
Two gaps in the identity propagation, both found in review. sync_foreground_client_state cleared the identity whenever no client was in the foreground, and it runs before every API request. The headless workspace and tab creates that spawn panes at startup have no client yet, so the launch seed was dropped before the panes it exists for were spawned. Clearing now happens in promote_latest_remaining_client, the actual detach transition. Pane children also inherited HERDR_OUTER_TERM_PROGRAM. It outranks TERM_PROGRAM in launched_outer_term_program, so a nested persistent herdr started inside a pane preferred the outer server's identity over the pane's own. It joins the stale identity variables stripped at spawn. Both tests fail without their fix.
|
@coderabbitai both findings were real and are fixed in 3c08fd5. Startup seed cleared before deferred panes spawn (Major). Confirmed: the Launch seed inherited by pane children (Minor). Fixed as you proposed, with the variable seeded in Both new tests were verified to fail with their fix reverted, so they pin the behavior rather than just passing. Full suite on Linux: 3117/3117, with |
|
Tip For best results, initiate chat on the files or code changes.
🐇 ✅ ✏️ Learnings added
You are interacting with an AI system. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/server/client_transport.rs (1)
522-533: 🎯 Functional Correctness | 🔵 TrivialConsider hardening the
term_programfilter beyond empty strings.This is the sole normalization point for client-reported
term_programbefore it flows (per the PR summary) into pane spawn environment. Only empty strings are filtered; NUL bytes or other control characters are passed through unchanged. Since this value is client-controlled (including overherdr --remote) and later used as an env var value for spawned processes, it's worth filtering out NUL bytes (and maybe other control characters) here as well, to avoid depending on downstream spawn-env handling to gracefully reject malformed values.🛡️ Proposed hardening
- term_program.filter(|value| !value.is_empty()) + term_program.filter(|value| !value.is_empty() && !value.contains('\0'))
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: a37b1c48-7bc4-4a19-8d83-8eed37859a64
📒 Files selected for processing (17)
docs/next/api/herdr-api.schema.jsonsrc/client/mod.rssrc/main.rssrc/pane.rssrc/protocol/wire.rssrc/remote/unix.rssrc/server/autodetect.rssrc/server/client_transport.rssrc/server/clients.rssrc/server/headless.rssrc/terminal_notify.rstests/api_ping.rstests/cli/sessions.rstests/cross_area.rstests/multi_client.rstests/server_headless.rstests/support/mod.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/api_ping.rs
- tests/cross_area.rs
The identity a client reports for itself becomes an environment value on every pane it spawns, and it arrives from whatever is on the other end of the socket, including a remote bridge. Only empty values were filtered. No real TERM_PROGRAM contains control characters, and an interior NUL cannot survive the conversion to a C string at spawn time, so a malformed value would have failed every pane spawn for that client. Treat it as no identity instead.
|
@coderabbitai took the nitpick too, in 0df12e4 — it sits on a real trust boundary, since the reported identity crosses the socket from a possibly remote client and lands in the spawn environment of every pane. Went slightly wider than the proposed diff: rejecting all control characters rather than NUL alone, since no real Linux suite now 3119/3119, lint clean on Linux and macOS. |
|
Tip For best results, initiate chat on the files or code changes.
🐇 ✅ ✏️ Learnings added
You are interacting with an AI system. |
|
thanks for the detailed work. i think the coding agent in your reproduction is pi: pi’s terminal ui chooses image support from we tested current herdr 0.7.5 over we do not want panes to claim they are running directly in the attached outer terminal. the outer client can change while a running process’s environment cannot, and forwarding the identity can make applications choose protocols herdr does not support. for example, yazi chooses the iterm image protocol when it sees wezterm, while herdr currently supports kitty graphics. this requires fix on upstream pi. |
Current behavior
Panes opened through
herdr --remote <host>have noTERM_PROGRAM. Applications that pick an inline image protocol from it fall back to no-image rendering, so reading an image appears to succeed but displays nothing.It is not remote-only. Pane children are seeded from a snapshot of the server process environment, and the server is persistent, so its terminal variables belong to whichever terminal started it:
KITTY_WINDOW_ID, so applications select a protocol the real outer terminal cannot render.apply_pane_terminal_envalready normalizesTERM/COLORTERMfor this reason and stops there.Expected behavior
A pane advertises the terminal that actually displays it, which is the attached client's outer terminal, not the server's.
Reproduction
Impact
Inline images do not render in remote panes. Any application keying off
TERM_PROGRAMsees no terminal, or on local sessions sees a stale one.Change
The client reports its outer terminal in
Hello; the server stores it per connection and applies it at pane spawn. There is no other channel to a remote server: the bridge runsssh -T, requests no pty, and forwards no environment.Two ordering cases needed handling beyond the handshake:
--no-sessionhas no client, so it seeds from its own environment.TERMstaysxterm-256color; only the outer identity is propagated. With no identity availableTERM_PROGRAMis removed rather than left stale.Commits are separable. The second drops the other identity variables that no longer describe the attached client (
KITTY_WINDOW_ID,LC_TERMINAL, ...); it can be dropped independently if you would rather not change what panes inherit.Things to flag
PROTOCOL_VERSION18 -> 19. Unavoidable given the ssh boundary, but it is a breaking change: servers must restart, and mismatched client/server pairs reject each other. Source protocol equalled the latest released tag, so this follows the bump rule inAGENTS.md.TERM_PROGRAM=kittyis synthesized. Kitty sets noTERM_PROGRAM, so for kitty this reconstructs a value rather than restoring one. It is what consumers check, and fabricatingKITTY_WINDOW_IDwould be worse, but it is the one judgement call here and I would rather you see it than find it.Hello-> server path for plugin metadata and was closed. This is scoped to the pane environment bug rather than that feature; say the word if you would still rather it went to Discussions first.Verification
just checkclean on the pinned 1.96.1 toolchain. Three pre-existing agent-detection failures (live_handoff_keeps_agent_started_pane_after_agent_exits,live_handoff_keeps_unmanaged_agent_name_bound_to_saved_session,pane_info_and_subscriptions_expose_done_agent_status) reproduce identically on an unmodified checkout here.TERM_PROGRAM=ghostty, and an image read in a coding agent renders inline. Also checked locally that panes track the attaching client and that--no-sessionreports its own terminal.Environment: herdr 0.7.5, stable, macOS 15 arm64, Ghostty local, Linux x86_64 remote.
🤖 Generated with Claude Code