docs(pane): restore eight backpressure and probe invariants

Second pass over what #268 stripped, this time reading each comment as
a claim to check rather than prose to paste back. All eight hold, so
this is documentation only — but they were worth checking, and two of
them are the kind that gets "simplified" by someone who cannot see why:

- `OutputGate::queued` is `AtomicI64` because add runs per PTY read at
  ~100k/s and sub per socket write, so neither may take a lock; signed
  so a decrement racing a reset drifts negative instead of underflowing.
- `wait_below_high_water` returns before touching the mutex when the
  backlog is under the mark, which is the common path.
- `MAX_RING_SEGMENTS` bounds a leak: drag-resize cuts a segment per
  column, none of them filling `RING_CAP`, so attach would degrade
  linearly over a pane's life. Past the cap the two oldest merge.
- `CAPABILITY_ENV` keeps `TERM`/`COLORTERM` out of reach of the user's
  env map — a fact about what the far end decodes, not a preference.
- `ForegroundProbes::agent` and `::cwd` distinguish "no reading" from
  "nothing there"; the caller applies the outer Option, never flattens
  it, so a backend with no process-table view cannot wipe an agent
  identified by sentinel events.
- The spawn path avoids appending argv to `new_default_prog()`, which
  portable-pty panics on by design.

Also fix an unresolved `[`ForwardEntry`]` link I added in 61002ec. It
only fails under `--document-private-items`; plain `cargo doc --no-deps`
never resolves private-item links, so it is the wrong gate for this
tree and I have moved to the stricter one.
This commit is contained in:
l0ng-ai
2026-08-16 05:33:16 +08:00
parent 43961cae20
commit aeef80d25a
2 changed files with 37 additions and 1 deletions
+36
View File
@@ -97,6 +97,11 @@ fn apply_shell_integration(
resolved_program: &str,
integration: &shell_integration::Injection,
) {
// `CommandBuilder::new_default_prog()` preserves the Unix login-shell argv0
// shape, but portable-pty intentionally panics if argv is appended to that
// sentinel builder. Integrations that need argv (fish `-C`, bash `--rcfile`,
// PowerShell flags) must use an explicit command builder first. Env-only zsh
// integration keeps the default login-shell path.
if integration.replaces_argv || (cmd.is_default_prog() && !integration.args.is_empty()) {
*cmd = CommandBuilder::new(resolved_program);
}
@@ -437,6 +442,9 @@ fn shell_env_path(
.find(|candidate| is_file(candidate))
}
/// Env keys that describe our emulator's real capabilities. A user's `env` map
/// must not override these: the answer isn't a preference, it's a fact about
/// what the pane on the other end can decode.
const CAPABILITY_ENV: [&str; 2] = ["TERM", "COLORTERM"];
fn names_capability_env(key: &str) -> bool {
@@ -592,16 +600,32 @@ fn apply_common_command_setup(
const RING_CAP: usize = 8 * 1024 * 1024;
/// Cap on the ring's geometry segments. The client does a full grid reflow per
/// replayed `Size`, and drag-resizing a pane whose TUI redraws on every
/// SIGWINCH cuts a segment per column change — tiny segments that never fill
/// `RING_CAP`, so over a long-lived pane's life they would accumulate without
/// bound and attach would degrade linearly. Past the cap the two *oldest*
/// segments merge (the older one's bytes replay at the newer one's geometry):
/// like the byte cap, precision degrades from the oldest scrollback first.
const MAX_RING_SEGMENTS: usize = 64;
const REMOTE_CONTEXT_POLL_INTERVAL: Duration = Duration::from_millis(500);
pub(crate) struct OutputGate {
/// Bytes handed to the writer channel but not yet written out. Atomic —
/// `add` runs per PTY read (~100k/s at full drain) and `sub` per socket
/// write, so the hot paths must not take a lock. Signed so a late
/// decrement racing a `reset` only drifts permissive (negative) instead
/// of underflowing.
queued: AtomicI64,
park: Mutex<()>,
drained: Condvar,
}
impl OutputGate {
/// Max Output bytes in flight before the PTY reader pauses. Sized to
/// swallow a big burst whole (a 10+ MB `cat`, a build log dump) so the
/// PTY drains at device speed and the client parses in its own time —
/// while still bounding what a nonstop flood (`yes`) can pin per pane.
const HIGH_WATER: i64 = 16 * 1024 * 1024;
const MAX_WAIT: Duration = Duration::from_secs(2);
@@ -635,6 +659,10 @@ impl OutputGate {
self.queued.load(Ordering::Relaxed)
}
/// Park the caller (the PTY reader; it must hold no locks) while the
/// backlog is at/over the high-water mark, up to [`Self::MAX_WAIT`].
/// Lock-free when the backlog is below the mark — the common case, checked
/// before every PTY read.
fn wait_below_high_water(&self) {
if self.queued.load(Ordering::Relaxed) < Self::HIGH_WATER {
return;
@@ -868,7 +896,15 @@ enum PaneBackend {
struct ForegroundProbes {
remote: Box<dyn Fn() -> Option<RemoteContext> + Send>,
/// Outer `None` means this backend has no process-table view of the PTY
/// foreground at all (native SSH; Windows, where ConPTY has no foreground
/// process group) — "no opinion", never applied, so it can't wipe an agent
/// identified another way. Inner `None` is a reading: nothing is running.
agent: Box<dyn Fn() -> Option<Option<(crate::core::cli_agent::CLIAgent, Vec<String>)>> + Send>,
/// The PTY owner's cwd straight from the process table. `None` means "no
/// reading" (native SSH, Windows, or a process we can't inspect), never
/// "no cwd" — see [`apply_probed_cwd`] for how a reading is reconciled with
/// the shell's own OSC 7 report.
cwd: Box<dyn Fn() -> Option<PathBuf> + Send>,
}
+1 -1
View File
@@ -139,7 +139,7 @@ impl SshManager {
}
/// Always empty: loopback forwards are not a registry of their own. A
/// forward `ensure_loopback` sets up is an ordinary [`ForwardEntry`] under
/// forward `ensure_loopback` sets up is an ordinary `ForwardEntry` under
/// the owning pane or workspace, so the managed list is where it shows up
/// and where it is closed from.
///