Synced from monorepo
Changes: - Detect the herdr multiplexer - Mark /gboom as non-production code - Bound peak memory when loading a large session - Add a subagent lifecycle soak bounding threads, fds, and heap - Stream inherited replay to bound fork memory - Copy full plan from plan approval with y - Stop armed signature verification from deleting the managed-deny smoke policy - Add source-tagged terminal version telemetry - Show the UI instantly and fetch models and settings in the background - Session test helpers - computer_reason on the ConversationHistoryDone trailer
This commit is contained in:
parent
47348d13ec
commit
b41c75a578
92 changed files with 9410 additions and 3788 deletions
|
|
@ -1,6 +1,9 @@
|
|||
//! `/gboom` easter egg: a tiny single-level raycaster shooter rendered in
|
||||
//! the terminal via the kitty graphics protocol.
|
||||
//!
|
||||
//! Not production code — this is for fun and is not maintained to the
|
||||
//! standards in `crates/codegen/AGENTS.md`.
|
||||
//!
|
||||
//! Typing `/gboom` (and nothing else) opens a modal overlay — the same
|
||||
//! surface the imagine-video player uses — and streams PNG frames via
|
||||
//! per-frame kitty `a=T` retransmission at the ~30 fps animation tick. The
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub mod image;
|
|||
pub mod keyboard;
|
||||
pub mod overlay;
|
||||
pub(crate) mod probe;
|
||||
pub mod term_version;
|
||||
pub mod tmux_probe;
|
||||
pub mod xtversion;
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ pub use keyboard::{
|
|||
KeyboardCapabilities, ModifierDelivery, ModifierFate, keyboard_capabilities,
|
||||
keyboard_capabilities_for_host,
|
||||
};
|
||||
pub use term_version::{TermVersion, TermVersionSource};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
|
@ -195,6 +197,15 @@ pub enum MultiplexerKind {
|
|||
/// cmux (Ghostty-backed macOS terminal multiplexer).
|
||||
#[strum(to_string = "cmux")]
|
||||
Cmux,
|
||||
/// herdr, a libghostty-backed agent multiplexer
|
||||
/// ([ogulcancelik/herdr](https://github.com/ogulcancelik/herdr)).
|
||||
///
|
||||
/// Its embedded emulator answers CSI queries itself, so it counts as
|
||||
/// CSI-intercepting. The accepted cost: a herdr pane typically has no
|
||||
/// other version signal (brand `Unknown`, no `TERM_PROGRAM_VERSION`), but
|
||||
/// the XTVERSION reply describes herdr's engine rather than the host.
|
||||
#[strum(to_string = "herdr")]
|
||||
Herdr,
|
||||
/// No recognized multiplexer detected (does not rule out unknown ones).
|
||||
#[default]
|
||||
#[strum(to_string = "None detected")]
|
||||
|
|
@ -203,9 +214,10 @@ pub enum MultiplexerKind {
|
|||
|
||||
impl MultiplexerKind {
|
||||
/// Whether this multiplexer intercepts CSI queries (e.g. XTVERSION)
|
||||
/// instead of passing them through to the outer terminal.
|
||||
/// instead of passing them through to the outer terminal. See
|
||||
/// [`Self::Herdr`] for the version signal herdr gives up by being here.
|
||||
pub fn intercepts_csi_queries(self) -> bool {
|
||||
matches!(self, Self::Tmux | Self::Screen | Self::Zellij)
|
||||
matches!(self, Self::Tmux | Self::Screen | Self::Zellij | Self::Herdr)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -275,10 +287,16 @@ pub struct TerminalContext {
|
|||
/// Value of tmux's `extended-keys` global option (`"on"`, `"off"`,
|
||||
/// `"always"`); populated only when `multiplexer == Tmux`.
|
||||
pub tmux_extended_keys: Option<String>,
|
||||
/// The `TERM_PROGRAM_VERSION` environment variable (e.g. `"3.5.6"` for
|
||||
/// iTerm2, `"1.1.3"` for Ghostty). Used for version-gating features
|
||||
/// that require a minimum terminal version.
|
||||
/// The `TERM_PROGRAM_VERSION` environment variable, falling back to
|
||||
/// `LC_TERMINAL_VERSION` (e.g. `"3.5.6"` for iTerm2, `"1.1.3"` for
|
||||
/// Ghostty). Used for version-gating features that require a minimum
|
||||
/// terminal version. Raw and **ungated**: inside tmux this is tmux's own
|
||||
/// version. For a brand-corroborated value use [`Self::env_term_version`].
|
||||
pub term_program_version: Option<String>,
|
||||
/// The brand-corroborated counterpart to `term_program_version` (see
|
||||
/// [`term_version`]). Resolved once in [`build_terminal_context_from_env`],
|
||||
/// so it does not re-derive if `brand` or `vte_version` change afterwards.
|
||||
pub env_term_version: Option<TermVersion>,
|
||||
}
|
||||
|
||||
impl TerminalContext {
|
||||
|
|
@ -553,12 +571,25 @@ impl TerminalContext {
|
|||
}
|
||||
}
|
||||
|
||||
/// The best available terminal version and the source that reported it.
|
||||
///
|
||||
/// Only the environment arm exists today. Final precedence is
|
||||
/// `da2 > xtversion > env` — probe arms insert **above** it, since a live
|
||||
/// self-report cannot be inherited or go stale.
|
||||
pub fn term_version(&self) -> (String, TermVersionSource) {
|
||||
match &self.env_term_version {
|
||||
Some(v) => (v.version.clone(), v.source),
|
||||
None => (String::new(), TermVersionSource::None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a flat snapshot of terminal details for telemetry.
|
||||
pub fn telemetry_snapshot(&self) -> xai_grok_telemetry::events::TerminalTelemetry {
|
||||
let os = crate::host::HostOs::current();
|
||||
let server = crate::host::DisplayServer::current();
|
||||
let kb = self.keyboard_capabilities();
|
||||
let route = crate::clipboard::clipboard_route();
|
||||
let (term_version, term_version_source) = self.term_version();
|
||||
xai_grok_telemetry::events::TerminalTelemetry {
|
||||
brand: self.brand.to_string(),
|
||||
multiplexer: self.multiplexer.to_string(),
|
||||
|
|
@ -572,6 +603,8 @@ impl TerminalContext {
|
|||
enter_modifier_fate: kb.enter_modifier.to_string(),
|
||||
tmux_version: self.tmux_version_or_na().to_owned(),
|
||||
xtversion: xtversion::detected().unwrap_or("").to_owned(),
|
||||
term_version,
|
||||
term_version_source: term_version_source.to_string(),
|
||||
hyperlink_osc8: self.hyperlink_capabilities().osc8.to_string(),
|
||||
hyperlink_skip_reason: self.hyperlink_skip_reason().unwrap_or("none").to_owned(),
|
||||
clipboard_route: route.to_string(),
|
||||
|
|
@ -879,8 +912,11 @@ fn infer_byobu_backend_from_mux_markers(env: &HashMap<String, String>) -> Option
|
|||
/// 1. Explicit `BYOBU_BACKEND` beats generic `TMUX`/`STY` clues.
|
||||
/// 2. `TMUX` beats `ZELLIJ` (tmux can nest inside Zellij but not vice-versa).
|
||||
/// 3. `STY` (GNU screen) is only chosen when neither `TMUX` nor `ZELLIJ` is set.
|
||||
/// 4. cmux markers classify only when no tmux/zellij/screen (or explicit
|
||||
/// Byobu backend) won — so a real mux nested inside cmux still wins.
|
||||
/// 4. herdr and cmux markers classify only when no tmux/zellij/screen (or
|
||||
/// explicit Byobu backend) won — a real mux nested inside either still
|
||||
/// wins. Between the two, `HERDR_ENV` beats the `CMUX_*` markers: the
|
||||
/// shape they appear in together is herdr running in a cmux panel, where
|
||||
/// the `CMUX_*` values are inherited rather than fresh.
|
||||
///
|
||||
/// This ensures one deterministic classification even when multiple markers
|
||||
/// are present (e.g., an inherited `ZELLIJ` var inside a tmux pane).
|
||||
|
|
@ -897,7 +933,9 @@ pub fn detect_multiplexer_from_env(env: &HashMap<String, String>) -> Multiplexer
|
|||
}
|
||||
|
||||
// Standard multiplexer markers, tmux > Zellij > screen.
|
||||
// Nested real multiplexers inside cmux must win over cmux itself.
|
||||
// Nested real multiplexers inside herdr/cmux must win over the host mux.
|
||||
// A herdr daemon first started from tmux freezes that TMUX into every pane:
|
||||
// classified tmux here, so OSC 52 gets a tmux DCS wrap herdr renders as text.
|
||||
if env_get(env, "TMUX").is_some() {
|
||||
return MultiplexerKind::Tmux;
|
||||
}
|
||||
|
|
@ -907,6 +945,12 @@ pub fn detect_multiplexer_from_env(env: &HashMap<String, String>) -> Multiplexer
|
|||
if env_get(env, "STY").is_some() {
|
||||
return MultiplexerKind::Screen;
|
||||
}
|
||||
// herdr sets HERDR_ENV=1 in every pane, overrides TERM to xterm-256color
|
||||
// and never sets TERM_PROGRAM, so this marker is its only documented,
|
||||
// stable signal.
|
||||
if env_get(env, "HERDR_ENV").is_some() {
|
||||
return MultiplexerKind::Herdr;
|
||||
}
|
||||
// cmux sets non-empty CMUX_SOCKET_PATH / CMUX_PANEL_ID / CMUX_BUNDLE_ID;
|
||||
// CMUX_SOCKET may be present but empty — env_get filters empties.
|
||||
if env_get(env, "CMUX_SOCKET_PATH").is_some()
|
||||
|
|
@ -952,6 +996,8 @@ pub fn build_terminal_context_from_env(env: &HashMap<String, String>) -> Termina
|
|||
let term_program_version = env_get(env, "TERM_PROGRAM_VERSION")
|
||||
.or_else(|| env_get(env, "LC_TERMINAL_VERSION"))
|
||||
.map(|s| s.to_owned());
|
||||
// Resolved here: the brand each version var must corroborate is in hand.
|
||||
let env_term_version = term_version::detect_env_term_version(env, brand);
|
||||
|
||||
TerminalContext {
|
||||
brand,
|
||||
|
|
@ -967,6 +1013,7 @@ pub fn build_terminal_context_from_env(env: &HashMap<String, String>) -> Termina
|
|||
vte_version,
|
||||
tmux_extended_keys: None,
|
||||
term_program_version,
|
||||
env_term_version,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
//! Terminal version capture from environment variables.
|
||||
//!
|
||||
//! **A version variable is trusted only when the environment corroborates the
|
||||
//! brand it belongs to.** These variables cross process, SSH and multiplexer
|
||||
//! boundaries, so an uncorroborated version is as likely to describe another
|
||||
//! program as the terminal drawing our output. For a variable that is itself
|
||||
//! the brand marker (`WEZTERM_VERSION`, `VTE_VERSION`), corroboration means no
|
||||
//! stronger marker outranked it in `detect_terminal_brand_from_env`.
|
||||
//!
|
||||
//! Never read: `ZELLIJ_VERSION`, which would make an Alacritty pane inside
|
||||
//! Zellij report Zellij's number as its own, and `KONSOLE_VERSION`, which has
|
||||
//! no `TerminalName::Konsole` to attach to.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{TerminalName, terminal_name_from_term_program};
|
||||
|
||||
/// Which environment variable produced a [`TermVersion`].
|
||||
///
|
||||
/// The rendered labels are stable telemetry values — do not rename them.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::Display)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum TermVersionSource {
|
||||
None,
|
||||
/// `TERM_PROGRAM_VERSION`, or its SSH-surviving `LC_TERMINAL_VERSION`
|
||||
/// mirror (iTerm2 only).
|
||||
TermProgram,
|
||||
#[strum(serialize = "wezterm")]
|
||||
WezTerm,
|
||||
Vte,
|
||||
}
|
||||
|
||||
/// A terminal version together with the source that reported it.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TermVersion {
|
||||
/// Raw, exactly as the source reported it (trimmed). Shapes vary by
|
||||
/// terminal: `"3.5.6"`, `"20240203-110809-5046fc22"`, `"7402"`.
|
||||
pub version: String,
|
||||
pub source: TermVersionSource,
|
||||
}
|
||||
|
||||
impl TermVersion {
|
||||
fn new(version: &str, source: TermVersionSource) -> Self {
|
||||
Self {
|
||||
version: version.to_owned(),
|
||||
source,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the brand `TERM_PROGRAM` names vouches for `env_brand`'s version.
|
||||
///
|
||||
/// Identity, widened for VS Code forks: they export `TERM_PROGRAM=vscode` from
|
||||
/// the same host process that writes their brand marker and draws our output,
|
||||
/// so the version is that host's and `brand` records whose numbering it is.
|
||||
/// One-directional, so a leaked marker cannot borrow another brand's version;
|
||||
/// Zed is excluded as it is not an xterm.js host.
|
||||
fn corroborates(named: TerminalName, env_brand: TerminalName) -> bool {
|
||||
named == env_brand
|
||||
|| (named == TerminalName::VsCode
|
||||
&& matches!(
|
||||
env_brand,
|
||||
TerminalName::VsCode | TerminalName::Cursor | TerminalName::Windsurf
|
||||
))
|
||||
}
|
||||
|
||||
/// Look up an env value, trimmed; `env_get` alone would pass whitespace.
|
||||
fn env_trimmed<'a>(env: &'a HashMap<String, String>, key: &str) -> Option<&'a str> {
|
||||
let value = super::env_get(env, key)?.trim();
|
||||
(!value.is_empty()).then_some(value)
|
||||
}
|
||||
|
||||
/// Resolve the terminal version from the environment, taking the first
|
||||
/// variable corroborated by `env_brand`.
|
||||
///
|
||||
/// `env_brand` is the *pre-refinement* brand, before
|
||||
/// `refine_unknown_brand_for_host` may rewrite `TerminalContext::brand`:
|
||||
/// the native-Windows `Unknown -> WindowsTerminal` guess must not license a
|
||||
/// version attribution.
|
||||
pub(super) fn detect_env_term_version(
|
||||
env: &HashMap<String, String>,
|
||||
env_brand: TerminalName,
|
||||
) -> Option<TermVersion> {
|
||||
// tmux >= 3.2 exports TERM_PROGRAM=tmux and its own TERM_PROGRAM_VERSION,
|
||||
// which ungated would land on whichever brand marker survived inside the
|
||||
// tmux server environment.
|
||||
let named_brand = env_trimmed(env, "TERM_PROGRAM").and_then(terminal_name_from_term_program);
|
||||
if let Some(version) = env_trimmed(env, "TERM_PROGRAM_VERSION")
|
||||
&& named_brand.is_some_and(|named| corroborates(named, env_brand))
|
||||
{
|
||||
return Some(TermVersion::new(version, TermVersionSource::TermProgram));
|
||||
}
|
||||
|
||||
// LC_TERMINAL_VERSION survives SSH where TERM_PROGRAM_VERSION does not,
|
||||
// but only iTerm2 sets the pair.
|
||||
if let Some(version) = env_trimmed(env, "LC_TERMINAL_VERSION")
|
||||
&& env_trimmed(env, "LC_TERMINAL").is_some_and(|v| v.eq_ignore_ascii_case("iterm2"))
|
||||
&& env_brand == TerminalName::Iterm2
|
||||
{
|
||||
return Some(TermVersion::new(version, TermVersionSource::TermProgram));
|
||||
}
|
||||
|
||||
if let Some(version) = env_trimmed(env, "WEZTERM_VERSION")
|
||||
&& env_brand == TerminalName::WezTerm
|
||||
{
|
||||
return Some(TermVersion::new(version, TermVersionSource::WezTerm));
|
||||
}
|
||||
|
||||
// Brand-only: `TerminalContext::is_vte_based()` also accepts a present
|
||||
// `vte_version`, which is the candidate here — routing the gate through it
|
||||
// would make it vacuous.
|
||||
if let Some(version) = env_trimmed(env, "VTE_VERSION")
|
||||
&& env_brand.is_vte_based()
|
||||
{
|
||||
return Some(TermVersion::new(version, TermVersionSource::Vte));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::terminal::{build_terminal_context_from_env, env_from};
|
||||
|
||||
fn resolved(pairs: &[(&str, &str)]) -> (String, TermVersionSource) {
|
||||
build_terminal_context_from_env(&env_from(pairs)).term_version()
|
||||
}
|
||||
|
||||
/// Unlike the others, this reads ambient host state and warms process-wide
|
||||
/// caches; the asserted fields still come only from the injected map.
|
||||
fn snapshot(pairs: &[(&str, &str)]) -> xai_grok_telemetry::events::TerminalTelemetry {
|
||||
build_terminal_context_from_env(&env_from(pairs)).telemetry_snapshot()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_labels_are_pinned() {
|
||||
assert_eq!(TermVersionSource::None.to_string(), "none");
|
||||
assert_eq!(TermVersionSource::TermProgram.to_string(), "term_program");
|
||||
assert_eq!(TermVersionSource::WezTerm.to_string(), "wezterm");
|
||||
assert_eq!(TermVersionSource::Vte.to_string(), "vte");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn term_program_version_wins_when_term_program_names_the_brand() {
|
||||
let (version, source) = resolved(&[
|
||||
("TERM_PROGRAM", "iTerm.app"),
|
||||
("TERM_PROGRAM_VERSION", "3.5.6"),
|
||||
("LC_TERMINAL", "iTerm2"),
|
||||
("LC_TERMINAL_VERSION", "3.4.0"),
|
||||
]);
|
||||
assert_eq!(version, "3.5.6");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn term_program_version_wins_over_wezterm_version() {
|
||||
let (version, source) = resolved(&[
|
||||
("TERM_PROGRAM", "WezTerm"),
|
||||
("TERM_PROGRAM_VERSION", "20240203-110809-5046fc22"),
|
||||
("WEZTERM_VERSION", "20230712-072601-f4abf8fd"),
|
||||
]);
|
||||
assert_eq!(version, "20240203-110809-5046fc22");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vscode_and_its_forks_keep_the_vscode_host_version() {
|
||||
let (version, source) = resolved(&[
|
||||
("VSCODE_GIT_ASKPASS_MAIN", "/home/u/.vscode-server/askpass"),
|
||||
("TERM_PROGRAM", "vscode"),
|
||||
("TERM_PROGRAM_VERSION", "1.99.3"),
|
||||
]);
|
||||
assert_eq!(version, "1.99.3");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
|
||||
let fork = build_terminal_context_from_env(&env_from(&[
|
||||
("CURSOR_TRACE_ID", "abc123"),
|
||||
("TERM_PROGRAM", "vscode"),
|
||||
("TERM_PROGRAM_VERSION", "1.99.3"),
|
||||
]));
|
||||
assert_eq!(fork.brand, TerminalName::Cursor);
|
||||
let (version, source) = fork.term_version();
|
||||
assert_eq!(version, "1.99.3");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_vscode_widening_is_one_directional() {
|
||||
// TERM_PROGRAM names Zed while the brand chain resolves Cursor —
|
||||
// neither vouches for the other.
|
||||
let (version, source) = resolved(&[
|
||||
("CURSOR_TRACE_ID", "abc123"),
|
||||
("TERM_PROGRAM", "zed"),
|
||||
("TERM_PROGRAM_VERSION", "0.180.0"),
|
||||
]);
|
||||
assert_eq!(version, "");
|
||||
assert_eq!(source, TermVersionSource::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lc_terminal_version_wins_when_term_program_version_is_absent() {
|
||||
let (version, source) =
|
||||
resolved(&[("LC_TERMINAL", "iTerm2"), ("LC_TERMINAL_VERSION", "3.5.6")]);
|
||||
assert_eq!(version, "3.5.6");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lc_terminal_version_ignored_without_lc_terminal() {
|
||||
let (version, source) = resolved(&[
|
||||
("ITERM_SESSION_ID", "w0t0p0:1234"),
|
||||
("LC_TERMINAL_VERSION", "3.5.6"),
|
||||
]);
|
||||
assert_eq!(version, "");
|
||||
assert_eq!(source, TermVersionSource::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wezterm_version_wins_over_vte_version() {
|
||||
let (version, source) = resolved(&[
|
||||
("WEZTERM_VERSION", "20240203-110809-5046fc22"),
|
||||
("VTE_VERSION", "7402"),
|
||||
]);
|
||||
assert_eq!(version, "20240203-110809-5046fc22");
|
||||
assert_eq!(source, TermVersionSource::WezTerm);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vte_version_wins_for_a_vte_brand() {
|
||||
let (version, source) = resolved(&[("VTE_VERSION", "7402")]);
|
||||
assert_eq!(version, "7402");
|
||||
assert_eq!(source, TermVersionSource::Vte);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wezterm_version_ignored_for_another_brand() {
|
||||
// An inherited WEZTERM_VERSION is not the Ghostty session's version.
|
||||
let (version, source) = resolved(&[
|
||||
("TERM_PROGRAM", "Ghostty"),
|
||||
("WEZTERM_VERSION", "20240203-110809-5046fc22"),
|
||||
]);
|
||||
assert_eq!(version, "");
|
||||
assert_eq!(source, TermVersionSource::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vte_version_ignored_for_a_non_vte_brand() {
|
||||
let (version, source) = resolved(&[("TERM", "alacritty"), ("VTE_VERSION", "7402")]);
|
||||
assert_eq!(version, "");
|
||||
assert_eq!(source, TermVersionSource::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tmux_term_program_version_is_not_the_terminal_version() {
|
||||
// tmux >= 3.2 exports TERM_PROGRAM=tmux plus its own version, and
|
||||
// iTerm2's releases are also 3.5.x — an ungated value would be
|
||||
// indistinguishable from a real one.
|
||||
let (version, source) = resolved(&[
|
||||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||||
("TERM_PROGRAM", "tmux"),
|
||||
("TERM_PROGRAM_VERSION", "3.5"),
|
||||
("ITERM_SESSION_ID", "w0t0p0:1234"),
|
||||
]);
|
||||
assert_eq!(version, "");
|
||||
assert_eq!(source, TermVersionSource::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterm2_in_tmux_falls_through_to_lc_terminal_version() {
|
||||
let (version, source) = resolved(&[
|
||||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||||
("TERM_PROGRAM", "tmux"),
|
||||
("TERM_PROGRAM_VERSION", "3.5"),
|
||||
("LC_TERMINAL", "iTerm2"),
|
||||
("LC_TERMINAL_VERSION", "3.5.6"),
|
||||
]);
|
||||
assert_eq!(version, "3.5.6");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blank_version_is_absent() {
|
||||
let (version, source) = resolved(&[
|
||||
("TERM_PROGRAM", "Ghostty"),
|
||||
("TERM_PROGRAM_VERSION", " \t "),
|
||||
]);
|
||||
assert_eq!(version, "");
|
||||
assert_eq!(source, TermVersionSource::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surrounding_whitespace_is_trimmed() {
|
||||
let (version, source) = resolved(&[
|
||||
("TERM_PROGRAM", "Ghostty"),
|
||||
("TERM_PROGRAM_VERSION", " 1.1.3\n"),
|
||||
]);
|
||||
assert_eq!(version, "1.1.3");
|
||||
assert_eq!(source, TermVersionSource::TermProgram);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn telemetry_snapshot_carries_version_and_source() {
|
||||
let populated = snapshot(&[("VTE_VERSION", "7402")]);
|
||||
assert_eq!(populated.term_version, "7402");
|
||||
assert_eq!(populated.term_version_source, "vte");
|
||||
|
||||
let empty = snapshot(&[("TERM", "xterm-256color")]);
|
||||
assert_eq!(empty.term_version, "");
|
||||
assert_eq!(empty.term_version_source, "none");
|
||||
}
|
||||
}
|
||||
|
|
@ -416,6 +416,34 @@ fn mux_tmux_nested_inside_cmux_wins() {
|
|||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mux_herdr_from_herdr_env() {
|
||||
let env = env_from(&[("HERDR_ENV", "1")]);
|
||||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Herdr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mux_tmux_nested_inside_herdr_wins() {
|
||||
// tmux started inside a herdr pane, or a stale TMUX frozen into the pane by
|
||||
// herdr's daemon — indistinguishable from the env, and tmux wins either way.
|
||||
let env = env_from(&[
|
||||
("TMUX", "/tmp/tmux-501/default,12345,0"),
|
||||
("HERDR_ENV", "1"),
|
||||
]);
|
||||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Tmux);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mux_herdr_nested_inside_cmux_wins() {
|
||||
// herdr in a cmux panel inherits the CMUX_* markers; the inner layer wins.
|
||||
let env = env_from(&[
|
||||
("CMUX_SOCKET_PATH", "/tmp/cmux.sock"),
|
||||
("CMUX_PANEL_ID", "1"),
|
||||
("HERDR_ENV", "1"),
|
||||
]);
|
||||
assert_eq!(detect_multiplexer_from_env(&env), MultiplexerKind::Herdr);
|
||||
}
|
||||
|
||||
// -- ambiguous marker precedence ------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
@ -1122,6 +1150,21 @@ fn mux_zellij_not_from_version_only() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zellij_version_is_never_the_terminal_version() {
|
||||
// The multiplexer's version must never be attributed to the emulator.
|
||||
let env = env_from(&[
|
||||
("TERM", "alacritty"),
|
||||
("ZELLIJ", "0"),
|
||||
("ZELLIJ_SESSION_NAME", "main"),
|
||||
("ZELLIJ_VERSION", "0.43.1"),
|
||||
]);
|
||||
let ctx = build_terminal_context_from_env(&env);
|
||||
assert_eq!(ctx.brand, TerminalName::Alacritty);
|
||||
assert_eq!(ctx.multiplexer, MultiplexerKind::Zellij);
|
||||
assert_eq!(ctx.term_version(), (String::new(), TermVersionSource::None));
|
||||
}
|
||||
|
||||
// -- Byobu inference edge cases -------------------------------------------
|
||||
|
||||
#[test]
|
||||
|
|
@ -1692,6 +1735,28 @@ fn shift_enter_available_unknown_with_multiplexer() {
|
|||
assert!(!ctx.shift_enter_unavailable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn herdr_over_ssh_pane_does_not_skip_kitty_keyboard() {
|
||||
// A real herdr pane reached over SSH: no TERM_PROGRAM, so the brand stays
|
||||
// Unknown. HERDR_ENV is what keeps this out of the unknown-no-multiplexer
|
||||
// skip, which would otherwise drop Shift+Enter (herdr speaks KKP).
|
||||
let env = env_from(&[
|
||||
("HERDR_ENV", "1"),
|
||||
("HERDR_PANE_ID", "3"),
|
||||
("TERM", "xterm-256color"),
|
||||
("COLORTERM", "truecolor"),
|
||||
("SSH_CONNECTION", "10.0.0.1 52000 10.0.0.2 22"),
|
||||
]);
|
||||
let ctx = build_terminal_context_from_env(&env);
|
||||
assert!(ctx.is_ssh);
|
||||
assert_eq!(ctx.brand, TerminalName::Unknown);
|
||||
// shift_enter_unavailable() reads env_brand, not brand.
|
||||
assert_eq!(ctx.env_brand, TerminalName::Unknown);
|
||||
assert_eq!(ctx.multiplexer, MultiplexerKind::Herdr);
|
||||
assert_eq!(ctx.kitty_skip_reason(), None);
|
||||
assert!(!ctx.shift_enter_unavailable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_dot_unreliable_on_vte() {
|
||||
let ctx = TerminalContext {
|
||||
|
|
|
|||
|
|
@ -193,6 +193,10 @@ mod tests {
|
|||
!gate_allows_probe(&ctx(brand, MultiplexerKind::Tmux)),
|
||||
"{brand:?} under tmux should be skipped"
|
||||
);
|
||||
assert!(
|
||||
!gate_allows_probe(&ctx(brand, MultiplexerKind::Herdr)),
|
||||
"{brand:?} under herdr should be skipped"
|
||||
);
|
||||
}
|
||||
// JediTerm renders the query as garbage and must never be probed.
|
||||
assert!(!gate_allows_probe(&ctx(
|
||||
|
|
|
|||
Loading…
Reference in a new issue