wip(tui): auto-cap resize reflow by terminal

This commit is contained in:
Felipe Coury
2026-04-23 18:07:08 -03:00
parent 561f93be58
commit 946870ea25
10 changed files with 220 additions and 20 deletions
+3 -2
View File
@@ -532,14 +532,15 @@ pub struct ModelAvailabilityNuxConfig {
pub shown_count: HashMap<String, u32>,
}
pub const DEFAULT_TERMINAL_RESIZE_REFLOW_MAX_ROWS: usize = 12_000;
pub const DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS: usize = 1_000;
/// Experimental terminal resize-reflow tuning knobs.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct TerminalResizeReflowToml {
/// Trim resize-reflow replay to the most recent rendered terminal rows when the transcript
/// exceeds this cap. Omit or set to `0` to keep all rendered rows.
/// exceeds this cap. Omit to use Codex's terminal-specific default. Set to `0` to keep all
/// rendered rows.
#[serde(default)]
#[schemars(range(min = 0))]
pub max_rows: Option<usize>,
+1 -1
View File
@@ -1939,7 +1939,7 @@
"properties": {
"max_rows": {
"default": null,
"description": "Trim resize-reflow replay to the most recent rendered terminal rows when the transcript exceeds this cap. Omit or set to `0` to keep all rendered rows.",
"description": "Trim resize-reflow replay to the most recent rendered terminal rows when the transcript exceeds this cap. Omit to use Codex's terminal-specific default. Set to `0` to keep all rendered rows.",
"format": "uint",
"minimum": 0.0,
"type": "integer"
+12 -2
View File
@@ -1335,6 +1335,10 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
cfg.terminal_resize_reflow,
TerminalResizeReflowConfig::default()
);
assert_eq!(
cfg.terminal_resize_reflow.max_rows,
TerminalResizeReflowMaxRows::Auto
);
let cfg = Config::load_from_base_config_with_overrides(
ConfigToml {
@@ -1352,7 +1356,10 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
.await
.expect("load overridden config");
assert_eq!(cfg.terminal_resize_reflow.max_rows, Some(9000));
assert_eq!(
cfg.terminal_resize_reflow.max_rows,
TerminalResizeReflowMaxRows::Limit(9000)
);
let cfg = Config::load_from_base_config_with_overrides(
ConfigToml {
@@ -1368,7 +1375,10 @@ async fn runtime_config_resolves_terminal_resize_reflow_defaults_and_overrides()
.await
.expect("load config with disabled resize reflow limits");
assert_eq!(cfg.terminal_resize_reflow.max_rows, None);
assert_eq!(
cfg.terminal_resize_reflow.max_rows,
TerminalResizeReflowMaxRows::Disabled
);
}
#[tokio::test]
+18 -8
View File
@@ -625,9 +625,20 @@ impl Default for MultiAgentV2Config {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TerminalResizeReflowMaxRows {
/// Use the runtime terminal detector to choose a scrollback-sized cap.
#[default]
Auto,
/// Keep all rendered transcript rows during resize reflow.
Disabled,
/// Keep at most this many rendered transcript rows during resize reflow.
Limit(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TerminalResizeReflowConfig {
pub max_rows: Option<usize>,
pub max_rows: TerminalResizeReflowMaxRows,
}
impl AuthManagerConfig for Config {
@@ -1488,17 +1499,16 @@ fn resolve_multi_agent_v2_config(
}
fn resolve_terminal_resize_reflow_config(config_toml: &ConfigToml) -> TerminalResizeReflowConfig {
let defaults = TerminalResizeReflowConfig::default();
let Some(tui) = config_toml.tui.as_ref() else {
return defaults;
return TerminalResizeReflowConfig::default();
};
TerminalResizeReflowConfig {
max_rows: tui
.terminal_resize_reflow
.max_rows
.and_then(|rows| (rows > 0).then_some(rows))
.or(defaults.max_rows),
max_rows: match tui.terminal_resize_reflow.max_rows {
Some(0) => TerminalResizeReflowMaxRows::Disabled,
Some(rows) => TerminalResizeReflowMaxRows::Limit(rows),
None => TerminalResizeReflowMaxRows::Auto,
},
}
}
+4 -1
View File
@@ -676,7 +676,10 @@ max_rows = 9000
app.refresh_in_memory_config_from_disk().await?;
assert_eq!(app.config.terminal_resize_reflow.max_rows, Some(9000));
assert_eq!(
app.config.terminal_resize_reflow.max_rows,
crate::legacy_core::config::TerminalResizeReflowMaxRows::Limit(9000)
);
Ok(())
}
+1 -1
View File
@@ -147,7 +147,7 @@ impl App {
}
fn resize_reflow_max_rows(&self) -> Option<usize> {
self.config.terminal_resize_reflow.max_rows
crate::resize_reflow_cap::resize_reflow_max_rows(self.config.terminal_resize_reflow)
}
/// Finish stream consolidation by repairing any resize work that happened during streaming.
+5 -4
View File
@@ -23,6 +23,7 @@ use assert_matches::assert_matches;
use crate::legacy_core::config::ConfigBuilder;
use crate::legacy_core::config::ConfigOverrides;
use crate::legacy_core::config::TerminalResizeReflowMaxRows;
use codex_app_server_protocol::AdditionalFileSystemPermissions;
use codex_app_server_protocol::AdditionalNetworkPermissions;
use codex_app_server_protocol::AdditionalPermissionProfile;
@@ -3705,7 +3706,7 @@ fn rendered_line_text(line: &Line<'static>) -> String {
#[tokio::test]
async fn capped_resize_reflow_renders_recent_suffix_only() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
app.config.terminal_resize_reflow.max_rows = Some(5);
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(5);
app.transcript_cells = (0..20)
.map(|i| plain_line_cell(format!("cell {i}")))
.collect();
@@ -3732,7 +3733,7 @@ async fn capped_resize_reflow_renders_recent_suffix_only() {
#[tokio::test]
async fn uncapped_resize_reflow_renders_all_cells_when_row_cap_absent() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
app.config.terminal_resize_reflow.max_rows = None;
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Disabled;
app.transcript_cells = (0..20)
.map(|i| plain_line_cell(format!("cell {i}")))
.collect();
@@ -3747,7 +3748,7 @@ async fn uncapped_resize_reflow_renders_all_cells_when_row_cap_absent() {
#[tokio::test]
async fn uncapped_resize_reflow_renders_all_cells_under_row_limit() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
app.config.terminal_resize_reflow.max_rows = Some(100);
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(100);
app.transcript_cells = (0..3)
.map(|i| plain_line_cell(format!("cell {i}")))
.collect();
@@ -3774,7 +3775,7 @@ async fn uncapped_resize_reflow_renders_all_cells_under_row_limit() {
async fn initial_replay_buffer_keeps_recent_rows_when_row_cap_present() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
enable_terminal_resize_reflow(&mut app);
app.config.terminal_resize_reflow.max_rows = Some(3);
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(3);
app.begin_initial_history_replay_buffer();
for index in 0..5 {
+1
View File
@@ -144,6 +144,7 @@ mod oss_selection;
mod pager_overlay;
pub(crate) mod public_widgets;
mod render;
mod resize_reflow_cap;
mod resume_picker;
mod selection_list;
mod session_log;
+174
View File
@@ -0,0 +1,174 @@
//! Terminal-specific row caps for resize reflow.
//!
//! The auto cap mirrors documented scrollback defaults for terminals we can identify. Console Host
//! does not expose its configured screen buffer through terminal metadata, so it usually lands in
//! the fallback bucket.
use codex_config::types::DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS;
use codex_terminal_detection::TerminalInfo;
use codex_terminal_detection::TerminalName;
use codex_terminal_detection::terminal_info;
use crate::legacy_core::config::TerminalResizeReflowConfig;
use crate::legacy_core::config::TerminalResizeReflowMaxRows;
const VSCODE_RESIZE_REFLOW_MAX_ROWS: usize = 1_000;
const WINDOWS_TERMINAL_RESIZE_REFLOW_MAX_ROWS: usize = 9_001;
const WEZTERM_RESIZE_REFLOW_MAX_ROWS: usize = 3_500;
const ALACRITTY_RESIZE_REFLOW_MAX_ROWS: usize = 10_000;
pub(crate) fn resize_reflow_max_rows(config: TerminalResizeReflowConfig) -> Option<usize> {
resize_reflow_max_rows_for(
config,
&terminal_info(),
crate::tui::running_in_vscode_terminal(),
)
}
fn resize_reflow_max_rows_for(
config: TerminalResizeReflowConfig,
terminal: &TerminalInfo,
running_in_vscode_terminal: bool,
) -> Option<usize> {
match config.max_rows {
TerminalResizeReflowMaxRows::Auto => Some(auto_resize_reflow_max_rows(
terminal.name,
running_in_vscode_terminal,
)),
TerminalResizeReflowMaxRows::Disabled => None,
TerminalResizeReflowMaxRows::Limit(max_rows) => Some(max_rows),
}
}
fn auto_resize_reflow_max_rows(
terminal_name: TerminalName,
running_in_vscode_terminal: bool,
) -> usize {
if running_in_vscode_terminal {
return VSCODE_RESIZE_REFLOW_MAX_ROWS;
}
match terminal_name {
TerminalName::VsCode => VSCODE_RESIZE_REFLOW_MAX_ROWS,
TerminalName::WindowsTerminal => WINDOWS_TERMINAL_RESIZE_REFLOW_MAX_ROWS,
TerminalName::WezTerm => WEZTERM_RESIZE_REFLOW_MAX_ROWS,
TerminalName::Alacritty => ALACRITTY_RESIZE_REFLOW_MAX_ROWS,
TerminalName::AppleTerminal
| TerminalName::Ghostty
| TerminalName::Iterm2
| TerminalName::WarpTerminal
| TerminalName::Kitty
| TerminalName::Konsole
| TerminalName::GnomeTerminal
| TerminalName::Vte
| TerminalName::Dumb
| TerminalName::Unknown => DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS,
}
}
#[cfg(test)]
mod tests {
use super::*;
use codex_terminal_detection::Multiplexer;
fn test_terminal(name: TerminalName) -> TerminalInfo {
TerminalInfo {
name,
term_program: None,
version: None,
term: None,
multiplexer: None,
}
}
#[test]
fn auto_resize_reflow_max_rows_uses_terminal_defaults() {
let cases = [
(TerminalName::VsCode, VSCODE_RESIZE_REFLOW_MAX_ROWS),
(
TerminalName::WindowsTerminal,
WINDOWS_TERMINAL_RESIZE_REFLOW_MAX_ROWS,
),
(TerminalName::WezTerm, WEZTERM_RESIZE_REFLOW_MAX_ROWS),
(TerminalName::Alacritty, ALACRITTY_RESIZE_REFLOW_MAX_ROWS),
(
TerminalName::Ghostty,
DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS,
),
(
TerminalName::Unknown,
DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS,
),
];
for (terminal_name, expected_max_rows) in cases {
assert_eq!(
auto_resize_reflow_max_rows(
terminal_name,
/*running_in_vscode_terminal*/ false
),
expected_max_rows
);
}
}
#[test]
fn auto_resize_reflow_max_rows_prefers_vscode_probe() {
assert_eq!(
auto_resize_reflow_max_rows(
TerminalName::WindowsTerminal,
/*running_in_vscode_terminal*/ true
),
VSCODE_RESIZE_REFLOW_MAX_ROWS
);
}
#[test]
fn configured_resize_reflow_max_rows_overrides_auto_detection() {
let terminal = test_terminal(TerminalName::VsCode);
let config = TerminalResizeReflowConfig {
max_rows: TerminalResizeReflowMaxRows::Limit(42),
};
assert_eq!(
resize_reflow_max_rows_for(
config, &terminal, /*running_in_vscode_terminal*/ false
),
Some(42)
);
}
#[test]
fn disabled_resize_reflow_max_rows_keeps_all_rows() {
let terminal = test_terminal(TerminalName::VsCode);
let config = TerminalResizeReflowConfig {
max_rows: TerminalResizeReflowMaxRows::Disabled,
};
assert_eq!(
resize_reflow_max_rows_for(
config, &terminal, /*running_in_vscode_terminal*/ false
),
None
);
}
#[test]
fn unknown_terminal_uses_fallback_even_under_multiplexer() {
let terminal = TerminalInfo {
name: TerminalName::Unknown,
term_program: None,
version: None,
term: Some("xterm-256color".to_string()),
multiplexer: Some(Multiplexer::Tmux { version: None }),
};
let config = TerminalResizeReflowConfig::default();
assert_eq!(
resize_reflow_max_rows_for(
config, &terminal, /*running_in_vscode_terminal*/ false
),
Some(DEFAULT_TERMINAL_RESIZE_REFLOW_FALLBACK_MAX_ROWS)
);
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ fn running_in_wsl() -> bool {
}
}
fn running_in_vscode_terminal() -> bool {
pub(crate) fn running_in_vscode_terminal() -> bool {
vscode_terminal_detected(
std::env::var("TERM_PROGRAM").ok().as_deref(),
windows_term_program().as_deref(),