trusty-mpm 1.8.2

trusty-mpm: unified multi-agent orchestration platform (core, daemon, CLI, TUI, Telegram)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
//! Start every tm-managed Claude Code session on the classic renderer, with
//! its mouse capture off (issues #6495, #7160).
//!
//! Why: Claude Code's fullscreen renderer takes the alternate screen and
//! captures the mouse wheel, so a managed pane scrolls only the one live window
//! — the terminal's native scrollback and tmux's copy-mode history both stop
//! responding, which operators report as "everything is one window that will not
//! scroll". Claude Code names the escape hatch in its own failure text:
//! `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 forces that any time`. #7160 found
//! the fix incomplete: even under the classic renderer, Claude Code still sets
//! tmux's per-pane `mouse_any_flag` via its own escape sequence, so the wheel
//! still reaches the app instead of tmux copy-mode — `CLAUDE_CODE_DISABLE_MOUSE`
//! is the documented opt-out for that half. Setting either by hand in
//! `~/.claude/settings.json` fixes a session; tm now provisions both so nobody
//! has to find them.
//!
//! What: [`MANAGED_DEFAULTS`] is the one table — variable name, default value,
//! and pinned shell operand — that drives every carrier below, so a third
//! variable is a one-line addition to the table rather than a change repeated
//! at each launch site:
//!   * [`configured_shell_assignments`] composes the `env` operands,
//!     space-joined in table order, for the builders that emit an `env …`
//!     prefix string.
//!   * [`apply_default_to_command`] (and its hermetic core,
//!     [`apply_default_when_unset`]) mutates a [`std::process::Command`] for the
//!     builders that exec `claude` directly, applying each table entry
//!     independently.
//!
//! The mouse-capture default YIELDS to a value the launch already carries.
//! The renderer is ASSIGNED (#8405): config `tmux.alternate_screen` decides it
//! through [`configured_env`] — `=0` for `true`, `=1` for `false` — as an
//! explicit assignment on the daemon's launch spec, on the `tm launch` /
//! `tm connect` / `tm session start` / client `/connect` shell lines, on the
//! control-plane tmux backend's line, and on the `tm run` and bare-`tm`
//! in-place relaunch exec paths ([`apply_configured_to_command`]). The tmux
//! server's or the launching shell's inherited value cannot override it; a
//! settings-tier `env` entry still can. The two variables are decided
//! independently.
//!
//! Operator precedence, and where it comes from:
//!   * launch environment — the shell operand expands `${NAME-default}`, so the
//!     pane shell substitutes the default only when the variable is UNSET. A
//!     value the pane already exports reaches `claude` unchanged, including an
//!     empty one (`-`, not `:-`). [`apply_default_when_unset`] makes the same
//!     decision in Rust for the exec paths, reading the environment the child
//!     would otherwise inherit.
//!   * settings `env` block — Claude Code applies an allowlisted settings `env`
//!     entry ON TOP of the process environment, and both variables are on that
//!     allowlist, so an operator entry in any settings tier already outranks
//!     whatever tm exports. tm writes neither variable into any settings file,
//!     so there is nothing here for an operator entry to fight.
//!
//! Switching renderers from inside a session still works: Claude Code's own
//! switch re-execs with these variables dropped.
//!
//! Test: this module's `tests`, plus one per launch path for each variable —
//! the daemon's spawn/resume/attach paths by
//! `every_launch_path_carries_the_configured_fullscreen_renderer` and
//! `every_launch_path_carries_the_classic_renderer_when_alternate_screen_is_off`,
//! and
//! `claude_command_assigns_the_configured_renderer` /
//! `claude_command_defaults_the_mouse_capture_off`,
//! `inplace_session_command_assigns_the_configured_renderer` /
//! `inplace_session_command_defaults_the_mouse_capture_off`,
//! `client_session_command_assigns_the_configured_renderer` /
//! `client_session_command_defaults_the_mouse_capture_off`,
//! `test_build_launch_command_assigns_the_configured_renderer` /
//! `test_build_launch_command_defaults_the_mouse_capture_off`,
//! `inplace_exec_command_assigns_the_configured_renderer` /
//! `inplace_exec_command_defaults_the_mouse_capture_off`.

/// The Claude Code variable that selects the classic (non-fullscreen) renderer.
pub const ALT_SCREEN_ENV_VAR: &str = "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN";

/// The value tm provisions when the launch carries none.
pub const ALT_SCREEN_DEFAULT: &str = "1";

/// The value that selects Claude Code's fullscreen renderer — what a managed
/// launch carries when config `tmux.alternate_screen` is `true` (#8405).
pub const ALT_SCREEN_ENABLED: &str = "0";

/// The assignments a managed launch must carry for the configured
/// `tmux.alternate_screen` (#8405).
///
/// Why: the `${NAME-1}` default is decided by the pane's environment, and a
/// pane inherits the tmux SERVER's global environment — a snapshot of whatever
/// process first started the server, never the daemon's own env. With config
/// `alternate_screen: true`, tmux lets panes use the alternate screen, yet a
/// server that inherited `=1` (or no value at all, which defaults to `1`) still
/// told `claude` to stay on the classic renderer. The reverse leaks too: with
/// `alternate_screen: false`, a server that inherited `=0` put `claude` on the
/// fullscreen renderer while tmux discarded its alternate screen. Config is the
/// operator's statement of intent, so it decides in both directions.
/// What: `[(ALT_SCREEN_ENV_VAR, "0")]` when `alternate_screen` is `true`,
/// `[(ALT_SCREEN_ENV_VAR, "1")]` when `false`. `false` assigns `1` rather than
/// leaving the variable unset: unset lets an inherited `=0` through, and lets
/// Claude Code pick its own renderer default, which #6495 found to be
/// fullscreen. The caller puts these into an explicit assignment, which
/// [`apply_default_when_unset`] never overrides. A settings-tier `env` entry
/// still outranks it, as the module doc explains.
/// Test: `configured_env_forces_the_fullscreen_renderer_when_enabled`,
/// `configured_env_forces_the_classic_renderer_when_disabled`.
pub fn configured_env(alternate_screen: bool) -> Vec<(String, String)> {
    let value = if alternate_screen {
        ALT_SCREEN_ENABLED
    } else {
        ALT_SCREEN_DEFAULT
    };
    vec![(ALT_SCREEN_ENV_VAR.to_owned(), value.to_owned())]
}

/// The `env` operand text a shell-string launch line carries for the
/// configured `tmux.alternate_screen` (#8405).
///
/// Why: `tm launch`, `tm connect`, `tm session start` and the client
/// `/connect` path type an `env … claude` line into a tmux pane, so they need
/// the same config-decided value the daemon's launch spec carries.
/// What: one operand per [`MANAGED_DEFAULTS`] entry, in table order: a plain
/// `NAME=value` for each variable [`configured_env`] decides, the pinned
/// `NAME="${NAME-default}"` operand for the rest (the mouse default, #7160).
/// The values are `0`/`1`, so no quoting is needed.
/// Test: `configured_shell_assignments_assign_the_renderer_in_both_directions`.
pub fn configured_shell_assignments(alternate_screen: bool) -> String {
    let configured = configured_env(alternate_screen);
    MANAGED_DEFAULTS
        .iter()
        .map(
            |d| match configured.iter().find(|(name, _)| name == d.env_var) {
                Some((name, value)) => format!("{name}={value}"),
                None => d.shell_assignment.to_owned(),
            },
        )
        .collect::<Vec<_>>()
        .join(" ")
}

/// The 1.7.0 entry point for the shell-line operands (#8405).
///
/// Why: trusty-mpm 1.7.x is published; removing it would fail the semver gate.
/// What: [`configured_shell_assignments`] with the renderer read from the
/// operator's config ([`configured_alternate_screen`]).
///
/// Superseded by [`configured_shell_assignments`], which takes the renderer explicitly;
/// kept un-`#[deprecated]` because that attribute is a minor-level change
/// the 1.7.x patch semver gate refuses (#8405).
pub fn managed_shell_assignments() -> String {
    configured_shell_assignments(configured_alternate_screen())
}

/// [`configured_alternate_screen_at`] against the operator's own state home
/// (`~/.trusty-tools/trusty-mpm`) — the CLI launch paths' entry point (#8405).
///
/// Why: the `tm` CLI launches run in the operator's home, not under a daemon
/// framework root.
/// What: resolves the directory and delegates. An unresolvable home means no
/// config file exists, which resolves to the built-in default — the answer
/// [`crate::core::trusty_tools_config::TrustyToolsConfig::load`] gives too.
/// Test: `configured_alternate_screen_reads_the_named_root`,
/// `spawn_falls_back_with_the_tmux_option_on_an_unreadable_config`.
pub fn configured_alternate_screen() -> bool {
    configured_alternate_screen_in(operator_config_root().as_deref())
}

/// The operator's state home, `~/.trusty-tools/trusty-mpm`, where `config.yaml`
/// lives; `None` when the home directory cannot be resolved (#8405).
///
/// Why: each CLI launch seam takes the config root as a parameter so a test can
/// point it at a fixture; this is what production passes.
/// Test: exercised via each seam's `*_follows_the_configured_renderer` test.
pub fn operator_config_root() -> Option<std::path::PathBuf> {
    trusty_common::crate_config::crate_config_dir(crate::core::trusty_tools_config::CRATE_NAME)
}

/// [`configured_alternate_screen_at`] for an optional config root (#8405).
///
/// What: `None` (no resolvable home) resolves to [`tmux_option_fallback`], the
/// answer an absent config gives.
/// Test: `configured_alternate_screen_reads_the_named_root`.
pub fn configured_alternate_screen_in(crate_config_root: Option<&std::path::Path>) -> bool {
    match crate_config_root {
        Some(root) => configured_alternate_screen_at(root),
        None => tmux_option_fallback(),
    }
}

/// The renderer a launch uses when its config cannot be read (#8405).
///
/// Why: the same launch applies the tmux `alternate-screen` option from
/// [`crate::core::trusty_tools_config::TrustyToolsConfig::load`], which turns an
/// unreadable file into `TrustyToolsConfig::default()`. The renderer must fall
/// back to that same value, or tmux and `claude` disagree about the screen.
/// What: `resolve_tmux_options(&TrustyToolsConfig::default()).alternate_screen`.
/// Test: `spawn_falls_back_with_the_tmux_option_on_an_unreadable_config`
/// compares it with the tmux path's own answer for the same malformed file.
fn tmux_option_fallback() -> bool {
    use crate::core::trusty_tools_config::{TrustyToolsConfig, resolve_tmux_options};
    resolve_tmux_options(&TrustyToolsConfig::default()).alternate_screen
}

/// Read `tmux.alternate_screen` from the `config.yaml` under
/// `crate_config_root` (#8405).
///
/// Why: a managed launch must know the configured value to carry it, and must
/// read it from the state home the caller named (#8233), not `$HOME`. A config
/// that cannot be read never blocks a launch: the renderer is a display
/// preference, so the launch proceeds on [`tmux_option_fallback`] — the value
/// the tmux `alternate-screen` option falls back to in the same launch — and a
/// warning names the file and the error.
/// What: [`trusty_common::crate_config::load_at`] on
/// `<crate_config_root>/config.yaml`, resolved by
/// [`crate::core::trusty_tools_config::resolve_tmux_options`]. An absent file
/// resolves to the built-in default (`false`, #5364); a read or parse error
/// logs a `warn!` and returns [`tmux_option_fallback`].
/// Test: `configured_alternate_screen_reads_the_named_root`,
/// `configured_alternate_screen_defaults_off_without_a_config`,
/// `configured_alternate_screen_warns_and_falls_back_on_a_malformed_config`.
pub fn configured_alternate_screen_at(crate_config_root: &std::path::Path) -> bool {
    use crate::core::trusty_tools_config::{TrustyToolsConfig, resolve_tmux_options};
    let path = crate_config_root.join(trusty_common::crate_config::CONFIG_FILE);
    match trusty_common::crate_config::load_at::<TrustyToolsConfig>(&path) {
        Ok(config) => resolve_tmux_options(&config.unwrap_or_default()).alternate_screen,
        Err(err) => {
            let fallback = tmux_option_fallback();
            tracing::warn!(
                path = %path.display(),
                "#8405: cannot read tmux.alternate_screen ({err}); the launch proceeds with \
                 alternate_screen={fallback}, the value the tmux alternate-screen option \
                 falls back to"
            );
            fallback
        }
    }
}

/// Provision a `claude` [`std::process::Command`] with the config-decided
/// renderer and the remaining managed defaults (#8405).
///
/// Why: the exec launch paths (`tm run`, the bare-`tm` in-place relaunch) have
/// no shell and no launch spec, so this is their carrier for
/// [`configured_env`]; without it the pane's inherited value would still win.
/// What: `cmd.env` for each [`configured_env`] pair, then
/// [`apply_default_to_command`], which skips the variables just assigned.
/// Test: `configured_command_assigns_the_renderer_over_the_pane`.
pub fn apply_configured_to_command(cmd: &mut std::process::Command, alternate_screen: bool) {
    for (name, value) in configured_env(alternate_screen) {
        cmd.env(name, value);
    }
    apply_default_to_command(cmd);
}

/// The `env` operand every shell-string launch line carries (#6495).
///
/// Why: written as a literal rather than composed from
/// [`ALT_SCREEN_ENV_VAR`]/[`ALT_SCREEN_DEFAULT`] so a test can pin the exact
/// shell text. A composed constant compared against a composed expectation
/// would stay green if the `${NAME-default}` form itself were broken — and the
/// form IS the operator-precedence mechanism, not decoration around it.
/// What: `NAME="${NAME-1}"`, which the pane shell expands to the operator's
/// exported value when the variable is set and to `1` when it is not. The
/// `-` form (rather than `:-`) preserves an explicitly-empty operator value.
/// Double quotes are required: the single quotes the sibling assignments use
/// would suppress the expansion and hand `claude` the literal text.
///
/// POSIX `env` grammar puts this AFTER every `-u` flag on the line — see
/// [`crate::core::claude_env_scrub::env_unset_flags`].
/// Test: `shell_assignment_pins_the_defaulting_form`,
/// `shell_assignment_names_the_variable_and_the_default`.
pub const ALT_SCREEN_SHELL_ASSIGNMENT: &str =
    "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=\"${CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN-1}\"";

/// The Claude Code variable that opts the pane out of the app's own mouse
/// capture (#7160).
///
/// Why: tmux's per-pane `mouse_any_flag` is set by the application running in
/// the pane, via an escape sequence — independent of tmux's own `mouse`
/// server option and independent of [`ALT_SCREEN_ENV_VAR`]. Claude Code sets
/// it even under the classic renderer, so #6495's fix alone leaves the wheel
/// captured. Claude Code documents this variable as the opt-out.
pub const MOUSE_ENV_VAR: &str = "CLAUDE_CODE_DISABLE_MOUSE";

/// The value tm provisions when the launch carries none.
pub const MOUSE_DEFAULT: &str = "1";

/// The `env` operand every shell-string launch line carries (#7160) — the
/// mouse-capture counterpart to [`ALT_SCREEN_SHELL_ASSIGNMENT`], same form and
/// same reason for being a pinned literal rather than a composed one.
/// Test: `mouse_shell_assignment_pins_the_defaulting_form`,
/// `mouse_shell_assignment_names_the_variable_and_the_default`.
pub const MOUSE_SHELL_ASSIGNMENT: &str =
    "CLAUDE_CODE_DISABLE_MOUSE=\"${CLAUDE_CODE_DISABLE_MOUSE-1}\"";

/// One variable tm provisions a default for on every managed launch.
///
/// Why: pairs each variable's name, default, and pinned shell operand so
/// [`MANAGED_DEFAULTS`] can drive every carrier from one table instead of the
/// table's shape being re-derived at each carrier (#7160).
/// What: three fields, all `&'static str` so the table itself can be a `const`.
struct ManagedDefault {
    /// The environment variable name.
    env_var: &'static str,
    /// The value tm provisions when the launch carries none.
    default: &'static str,
    /// The pinned `NAME="${NAME-default}"` shell operand — see
    /// [`ALT_SCREEN_SHELL_ASSIGNMENT`]'s doc for why this is a literal rather
    /// than composed from the other two fields.
    shell_assignment: &'static str,
}

/// Every variable tm provisions a default for on a managed launch, in the
/// order every carrier emits them (#6495, #7160).
///
/// Why: the single source of truth [`configured_shell_assignments`] and
/// [`apply_default_when_unset`] both drive off, so a third variable is a
/// one-line addition here rather than a change repeated at every launch site
/// this module has callers in.
/// Test: `configured_shell_assignments_assign_the_renderer_in_both_directions`.
const MANAGED_DEFAULTS: &[ManagedDefault] = &[
    ManagedDefault {
        env_var: ALT_SCREEN_ENV_VAR,
        default: ALT_SCREEN_DEFAULT,
        shell_assignment: ALT_SCREEN_SHELL_ASSIGNMENT,
    },
    ManagedDefault {
        env_var: MOUSE_ENV_VAR,
        default: MOUSE_DEFAULT,
        shell_assignment: MOUSE_SHELL_ASSIGNMENT,
    },
];

/// Provision every managed default (#6495, #7160) on a `claude`
/// [`std::process::Command`].
///
/// Why: the exec launch paths (the bare-`tm` in-place relaunch, `tm run`) build
/// a `Command` with no shell in between, so [`configured_shell_assignments`]
/// and its `${NAME-default}` expansion cannot apply to them — but they inherit the
/// same pane environment and need the same defaults and the same operator
/// precedence, decided independently per variable.
/// What: delegates to [`apply_default_when_unset`] with a real-environment
/// lookup. The parent's environment is what the child inherits, so "already set
/// here" is exactly "the launch already carries a value".
/// Test: covered via [`apply_default_when_unset`], which takes the lookup as a
/// parameter — mirroring
/// [`crate::core::claude_env_scrub::markers_present_in_env`], and for the same
/// reason: `std::env::set_var` is `unsafe` in edition 2024 and mutates state
/// every test in the binary shares.
pub fn apply_default_to_command(cmd: &mut std::process::Command) {
    apply_default_when_unset(cmd, |name| std::env::var_os(name).is_some());
}

/// Pure core of [`apply_default_to_command`]: for every table entry, set its
/// default only when `is_set` reports that variable absent.
///
/// Why: [`std::process::Command::env`] overrides the inherited value
/// unconditionally, so an unguarded call would defeat an operator who exported
/// the variable in the pane — the exact override #6495/#7160 must not perform.
/// Each variable is decided independently: an operator who exported only one
/// of the two must still get the tm default for the other, never both-or-
/// neither.
/// An assignment the command already carries explicitly (a [`configured_env`]
/// pair, #8405) also wins: the default fills a gap and never replaces a value
/// the launch chose.
/// What: for each entry in [`MANAGED_DEFAULTS`], no-op when
/// `is_set(entry.env_var)` is true or `cmd` already sets or removes that
/// variable; otherwise `cmd.env(entry.env_var, entry.default)`.
/// Test: `command_defaults_apply_when_every_variable_is_unset`,
/// `command_defaults_yield_to_an_operator_value`,
/// `command_defaults_yield_per_variable_independently`,
/// `command_defaults_never_replace_an_explicit_assignment`.
pub fn apply_default_when_unset(cmd: &mut std::process::Command, is_set: impl Fn(&str) -> bool) {
    for entry in MANAGED_DEFAULTS {
        // #8405: before this guard, an unset pane variable let the default
        // overwrite a configured `=0` the launch had already assigned.
        let explicit = cmd.get_envs().any(|(name, _)| name == entry.env_var);
        if explicit || is_set(entry.env_var) {
            continue;
        }
        cmd.env(entry.env_var, entry.default);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The literal shell text, pinned. `:-` here would silently rewrite an
    /// operator's explicitly-empty value, and single quotes would hand `claude`
    /// the unexpanded text.
    #[test]
    fn shell_assignment_pins_the_defaulting_form() {
        assert_eq!(
            ALT_SCREEN_SHELL_ASSIGNMENT,
            "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=\"${CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN-1}\""
        );
        assert!(
            !ALT_SCREEN_SHELL_ASSIGNMENT.contains(":-"),
            "`:-` would override an operator value of empty string"
        );
    }

    /// The literal above and the two constants must describe the same variable,
    /// or a caller reading the constants would look at the wrong name.
    #[test]
    fn shell_assignment_names_the_variable_and_the_default() {
        assert_eq!(
            ALT_SCREEN_SHELL_ASSIGNMENT,
            format!("{ALT_SCREEN_ENV_VAR}=\"${{{ALT_SCREEN_ENV_VAR}-{ALT_SCREEN_DEFAULT}}}\"")
        );
    }

    /// #7160: the mouse-capture counterpart of `shell_assignment_pins_the_defaulting_form`.
    #[test]
    fn mouse_shell_assignment_pins_the_defaulting_form() {
        assert_eq!(
            MOUSE_SHELL_ASSIGNMENT,
            "CLAUDE_CODE_DISABLE_MOUSE=\"${CLAUDE_CODE_DISABLE_MOUSE-1}\""
        );
        assert!(
            !MOUSE_SHELL_ASSIGNMENT.contains(":-"),
            "`:-` would override an operator value of empty string"
        );
    }

    /// #7160: the mouse-capture counterpart of
    /// `shell_assignment_names_the_variable_and_the_default`.
    #[test]
    fn mouse_shell_assignment_names_the_variable_and_the_default() {
        assert_eq!(
            MOUSE_SHELL_ASSIGNMENT,
            format!("{MOUSE_ENV_VAR}=\"${{{MOUSE_ENV_VAR}-{MOUSE_DEFAULT}}}\"")
        );
    }

    /// Read the explicit overrides a `Command` carries: `get_envs` reports a set
    /// value as `(key, Some(value))` and an `env_remove` as `(key, None)`.
    fn override_for(cmd: &std::process::Command, name: &str) -> Option<Option<String>> {
        cmd.get_envs()
            .find(|(k, _)| *k == name)
            .map(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
    }

    #[test]
    fn command_defaults_apply_when_every_variable_is_unset() {
        let mut cmd = std::process::Command::new("claude");
        apply_default_when_unset(&mut cmd, |_| false);
        assert_eq!(
            override_for(&cmd, ALT_SCREEN_ENV_VAR),
            Some(Some(ALT_SCREEN_DEFAULT.to_string())),
            "an unset alternate-screen variable must be provisioned with the tm default"
        );
        assert_eq!(
            override_for(&cmd, MOUSE_ENV_VAR),
            Some(Some(MOUSE_DEFAULT.to_string())),
            "an unset mouse-capture variable must be provisioned with the tm default"
        );
    }

    #[test]
    fn command_defaults_yield_to_an_operator_value() {
        let mut cmd = std::process::Command::new("claude");
        apply_default_when_unset(&mut cmd, |_| true);
        assert_eq!(
            override_for(&cmd, ALT_SCREEN_ENV_VAR),
            None,
            "a value the launch already carries must reach claude untouched"
        );
        assert_eq!(
            override_for(&cmd, MOUSE_ENV_VAR),
            None,
            "a value the launch already carries must reach claude untouched"
        );
    }

    /// #7160: the two variables must be decided INDEPENDENTLY — an operator who
    /// exported only the alternate-screen variable must still get the tm
    /// default for mouse capture, never both-or-neither.
    #[test]
    fn command_defaults_yield_per_variable_independently() {
        let mut cmd = std::process::Command::new("claude");
        apply_default_when_unset(&mut cmd, |name| name == ALT_SCREEN_ENV_VAR);
        assert_eq!(
            override_for(&cmd, ALT_SCREEN_ENV_VAR),
            None,
            "the variable the launch already carries must reach claude untouched"
        );
        assert_eq!(
            override_for(&cmd, MOUSE_ENV_VAR),
            Some(Some(MOUSE_DEFAULT.to_string())),
            "the variable the launch does NOT carry must still be provisioned"
        );
    }

    /// #8405: a configured `=0` already on the command must survive a pane
    /// that exports nothing. Before the fix the default overwrote it with `1`,
    /// so config `alternate_screen: true` never reached `claude`.
    #[test]
    fn command_defaults_never_replace_an_explicit_assignment() {
        let mut cmd = std::process::Command::new("claude");
        for (name, value) in configured_env(true) {
            cmd.env(name, value);
        }
        apply_default_when_unset(&mut cmd, |_| false);
        assert_eq!(
            override_for(&cmd, ALT_SCREEN_ENV_VAR),
            Some(Some(ALT_SCREEN_ENABLED.to_string())),
            "the default must not overwrite the configured fullscreen renderer"
        );
        assert_eq!(
            override_for(&cmd, MOUSE_ENV_VAR),
            Some(Some(MOUSE_DEFAULT.to_string())),
            "a variable with no explicit assignment still gets the tm default"
        );
    }

    #[test]
    fn configured_env_forces_the_fullscreen_renderer_when_enabled() {
        assert_eq!(
            configured_env(true),
            vec![(ALT_SCREEN_ENV_VAR.to_owned(), "0".to_owned())]
        );
    }

    /// #8405: `false` forces the classic renderer, so a tmux server that
    /// inherited `=0` cannot put `claude` on the fullscreen one.
    #[test]
    fn configured_env_forces_the_classic_renderer_when_disabled() {
        assert_eq!(
            configured_env(false),
            vec![(ALT_SCREEN_ENV_VAR.to_owned(), "1".to_owned())]
        );
    }

    /// #8405: the shell operand is a plain assignment in both directions —
    /// never the `${NAME-1}` form a pane-exported `=0` could override — and
    /// the mouse default keeps its yielding form.
    #[test]
    fn configured_shell_assignments_assign_the_renderer_in_both_directions() {
        assert_eq!(
            configured_shell_assignments(true),
            format!("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=0 {MOUSE_SHELL_ASSIGNMENT}")
        );
        assert_eq!(
            configured_shell_assignments(false),
            format!("CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 {MOUSE_SHELL_ASSIGNMENT}")
        );
    }

    fn write_config(root: &std::path::Path, yaml: &str) {
        std::fs::create_dir_all(root).expect("create root");
        std::fs::write(root.join("config.yaml"), yaml).expect("write config");
    }

    #[test]
    fn configured_alternate_screen_reads_the_named_root() {
        let dir = tempfile::tempdir().expect("tempdir");
        write_config(dir.path(), "tmux:\n  alternate_screen: true\n");
        assert!(configured_alternate_screen_at(dir.path()));
        write_config(dir.path(), "tmux:\n  alternate_screen: false\n");
        assert!(!configured_alternate_screen_at(dir.path()));
    }

    #[test]
    fn configured_alternate_screen_defaults_off_without_a_config() {
        let dir = tempfile::tempdir().expect("tempdir");
        assert!(!configured_alternate_screen_at(dir.path()));
    }

    /// #8405: a malformed config falls back and warns, naming the file and the
    /// error, instead of blocking the launch. The fallback's agreement with the
    /// tmux option is pinned against the tmux path itself by
    /// `spawn_falls_back_with_the_tmux_option_on_an_unreadable_config`.
    #[test]
    fn configured_alternate_screen_warns_and_falls_back_on_a_malformed_config() {
        let dir = tempfile::tempdir().expect("tempdir");
        write_config(dir.path(), "tmux:\n  alternate_screen: [not, a, bool\n");
        let (value, logged) = capture_warnings(|| configured_alternate_screen_at(dir.path()));
        // #8405 review: the tmux option's fallback, restated from the tmux path
        // (`TrustyToolsConfig::load()` → `default()` → `resolve_tmux_options`),
        // so an inverted `tmux_option_fallback` fails here.
        let tmux_option = crate::core::trusty_tools_config::resolve_tmux_options(
            &crate::core::trusty_tools_config::TrustyToolsConfig::default(),
        )
        .alternate_screen;
        assert_eq!(value, tmux_option);
        let path = dir.path().join("config.yaml");
        assert!(logged.contains(&path.display().to_string()), "{logged}");
        assert!(
            logged.contains("YAML"),
            "the warning must carry the error: {logged}"
        );
    }

    /// #8405: `tm run` and the in-place relaunch carry the configured renderer
    /// as an explicit assignment, so the pane's inherited value cannot win in
    /// either direction — whatever the test process itself exports.
    #[test]
    fn configured_command_assigns_the_renderer_over_the_pane() {
        for (alternate_screen, want) in [(true, "0"), (false, "1")] {
            let mut cmd = std::process::Command::new("claude");
            apply_configured_to_command(&mut cmd, alternate_screen);
            assert_eq!(
                override_for(&cmd, ALT_SCREEN_ENV_VAR),
                Some(Some(want.to_string()))
            );
        }
    }

    /// Run `f` under a capturing subscriber; return its value and the log text.
    fn capture_warnings<T>(f: impl FnOnce() -> T) -> (T, String) {
        use tracing_subscriber::layer::SubscriberExt;
        crate::test_support::enable_event_capture();
        let buffer = trusty_common::log_buffer::LogBuffer::new(64);
        let subscriber = tracing_subscriber::registry().with(
            trusty_common::log_buffer::LogBufferLayer::new(buffer.clone()),
        );
        let value = tracing::subscriber::with_default(subscriber, f);
        (value, buffer.tail(64).join("\n"))
    }
}