Skip to content

Commit 60cd0cf

Browse files
feat(tui): add /title terminal title configuration (#12334)
## Problem When multiple Codex sessions are open at once, terminal tabs and windows are hard to distinguish from each other. The existing status line only helps once the TUI is already focused, so it does not solve the "which tab is this?" problem. This PR adds a first-class `/title` command so the terminal window or tab title can carry a short, configurable summary of the current session. ## Screenshot <img width="849" height="320" alt="image" src="https://github.com/user-attachments/assets/8b112927-7890-45ed-bb1e-adf2f584663d" /> ## Mental model `/statusline` and `/title` are separate status surfaces with different constraints. The status line is an in-app footer that can be denser and more detailed. The terminal title is external terminal metadata, so it needs short, stable segments that still make multiple sessions easy to tell apart. The `/title` configuration is an ordered list of compact items. By default it renders `spinner,project`, so active sessions show lightweight progress first while idle sessions still stay easy to disambiguate. Each configured item is omitted when its value is not currently available rather than forcing a placeholder. ## Non-goals This does not merge `/title` into `/statusline`, and it does not add an arbitrary free-form title string. The feature is intentionally limited to a small set of structured items so the title stays short and reviewable. This also does not attempt to restore whatever title the terminal or shell had before Codex started. When Codex clears the title, it clears the title Codex last wrote. ## Tradeoffs A separate `/title` command adds some conceptual overlap with `/statusline`, but it keeps title-specific constraints explicit instead of forcing the status line model to cover two different surfaces. Title refresh can happen frequently, so the implementation now shares parsing and git-branch orchestration between the status line and title paths, and caches the derived project-root name by cwd. That keeps the hot path cheap without introducing background polling. ## Architecture The TUI gets a new `/title` slash command and a dedicated picker UI for selecting and ordering terminal-title items. The chosen ids are persisted in `tui.terminal_title`, with `spinner` and `project` as the default when the config is unset. `status` remains available as a separate text item, so configurations like `spinner,status` render compact progress like `⠋ Working`. `ChatWidget` now refreshes both status surfaces through a shared `refresh_status_surfaces()` path. That shared path parses configured items once, warns on invalid ids once, synchronizes shared cached state such as git-branch lookup, then renders the footer status line and terminal title from the same snapshot. Low-level OSC title writes live in `codex-rs/tui/src/terminal_title.rs`, which owns the terminal write path and last-mile sanitization before emitting OSC 0. ## Security Terminal-title text is treated as untrusted display content before Codex emits it. The write path strips control characters, removes invisible and bidi formatting characters that can make the title visually misleading, normalizes whitespace, and caps the emitted length. References used while implementing this: - [xterm control sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html) - [WezTerm escape sequences](https://wezterm.org/escape-sequences.html) - [CWE-150: Improper Neutralization of Escape, Meta, or Control Sequences](https://cwe.mitre.org/data/definitions/150.html) - [CERT VU#999008 (Trojan Source)](https://kb.cert.org/vuls/id/999008) - [Trojan Source disclosure site](https://trojansource.codes/) - [Unicode Bidirectional Algorithm (UAX #9)](https://www.unicode.org/reports/tr9/) - [Unicode Security Considerations (UTR #36)](https://www.unicode.org/reports/tr36/) ## Observability Unknown configured title item ids are warned about once instead of repeatedly spamming the transcript. Live preview applies immediately while the `/title` picker is open, and cancel rolls the in-memory title selection back to the pre-picker value. If terminal title writes fail, the TUI emits debug logs around set and clear attempts. The rendered status label intentionally collapses richer internal states into compact title text such as `Starting...`, `Ready`, `Thinking...`, `Working...`, `Waiting...`, and `Undoing...` when `status` is configured. ## Tests Ran: - `just fmt` - `cargo test -p codex-tui` At the moment, the red Windows `rust-ci` failures are due to existing `codex-core` `apply_patch_cli` stack-overflow tests that also reproduce on `main`. The `/title`-specific `codex-tui` suite is green.
1 parent fe287ac commit 60cd0cf

File tree

17 files changed

+1867
-294
lines changed

17 files changed

+1867
-294
lines changed

codex-rs/core/config.schema.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,14 @@
16701670
},
16711671
"type": "array"
16721672
},
1673+
"terminal_title": {
1674+
"default": null,
1675+
"description": "Ordered list of terminal title item identifiers.\n\nWhen set, the TUI renders the selected items into the terminal window/tab title. When unset, the TUI defaults to: `spinner` and `project`.",
1676+
"items": {
1677+
"type": "string"
1678+
},
1679+
"type": "array"
1680+
},
16731681
"theme": {
16741682
"default": null,
16751683
"description": "Syntax highlighting theme name (kebab-case).\n\nWhen set, overrides automatic light/dark theme detection. Use `/theme` in the TUI or see `$CODEX_HOME/themes` for custom themes.",

codex-rs/core/src/config/config_tests.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ fn config_toml_deserializes_model_availability_nux() {
237237
show_tooltips: true,
238238
alternate_screen: AltScreenMode::default(),
239239
status_line: None,
240+
terminal_title: None,
240241
theme: None,
241242
model_availability_nux: ModelAvailabilityNuxConfig {
242243
shown_count: HashMap::from([
@@ -921,6 +922,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
921922
show_tooltips: true,
922923
alternate_screen: AltScreenMode::Auto,
923924
status_line: None,
925+
terminal_title: None,
924926
theme: None,
925927
model_availability_nux: ModelAvailabilityNuxConfig::default(),
926928
}
@@ -4349,6 +4351,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
43494351
tool_suggest: ToolSuggestConfig::default(),
43504352
tui_alternate_screen: AltScreenMode::Auto,
43514353
tui_status_line: None,
4354+
tui_terminal_title: None,
43524355
tui_theme: None,
43534356
otel: OtelConfig::default(),
43544357
},
@@ -4491,6 +4494,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
44914494
tool_suggest: ToolSuggestConfig::default(),
44924495
tui_alternate_screen: AltScreenMode::Auto,
44934496
tui_status_line: None,
4497+
tui_terminal_title: None,
44944498
tui_theme: None,
44954499
otel: OtelConfig::default(),
44964500
};
@@ -4631,6 +4635,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
46314635
tool_suggest: ToolSuggestConfig::default(),
46324636
tui_alternate_screen: AltScreenMode::Auto,
46334637
tui_status_line: None,
4638+
tui_terminal_title: None,
46344639
tui_theme: None,
46354640
otel: OtelConfig::default(),
46364641
};
@@ -4757,6 +4762,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
47574762
tool_suggest: ToolSuggestConfig::default(),
47584763
tui_alternate_screen: AltScreenMode::Auto,
47594764
tui_status_line: None,
4765+
tui_terminal_title: None,
47604766
tui_theme: None,
47614767
otel: OtelConfig::default(),
47624768
};

codex-rs/core/src/config/edit.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,26 +60,40 @@ pub enum ConfigEdit {
6060
ClearPath { segments: Vec<String> },
6161
}
6262

63-
/// Produces a config edit that sets `[tui] theme = "<name>"`.
63+
/// Produces a config edit that sets `[tui].theme = "<name>"`.
6464
pub fn syntax_theme_edit(name: &str) -> ConfigEdit {
6565
ConfigEdit::SetPath {
6666
segments: vec!["tui".to_string(), "theme".to_string()],
6767
value: value(name.to_string()),
6868
}
6969
}
7070

71+
/// Produces a config edit that sets `[tui].status_line` to an explicit ordered list.
72+
///
73+
/// The array is written even when it is empty so "hide the status line" stays
74+
/// distinct from "unset, so use defaults".
7175
pub fn status_line_items_edit(items: &[String]) -> ConfigEdit {
72-
let mut array = toml_edit::Array::new();
73-
for item in items {
74-
array.push(item.clone());
75-
}
76+
let array = items.iter().cloned().collect::<toml_edit::Array>();
7677

7778
ConfigEdit::SetPath {
7879
segments: vec!["tui".to_string(), "status_line".to_string()],
7980
value: TomlItem::Value(array.into()),
8081
}
8182
}
8283

84+
/// Produces a config edit that sets `[tui].terminal_title` to an explicit ordered list.
85+
///
86+
/// The array is written even when it is empty so "disabled title updates" stays
87+
/// distinct from "unset, so use defaults".
88+
pub fn terminal_title_items_edit(items: &[String]) -> ConfigEdit {
89+
let array = items.iter().cloned().collect::<toml_edit::Array>();
90+
91+
ConfigEdit::SetPath {
92+
segments: vec!["tui".to_string(), "terminal_title".to_string()],
93+
value: TomlItem::Value(array.into()),
94+
}
95+
}
96+
8397
pub fn model_availability_nux_count_edits(shown_count: &HashMap<String, u32>) -> Vec<ConfigEdit> {
8498
let mut shown_count_entries: Vec<_> = shown_count.iter().collect();
8599
shown_count_entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));

codex-rs/core/src/config/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,11 @@ pub struct Config {
359359
/// `current-dir`.
360360
pub tui_status_line: Option<Vec<String>>,
361361

362+
/// Ordered list of terminal title item identifiers for the TUI.
363+
///
364+
/// When unset, the TUI defaults to: `project` and `spinner`.
365+
pub tui_terminal_title: Option<Vec<String>>,
366+
362367
/// Syntax highlighting theme override (kebab-case name).
363368
pub tui_theme: Option<String>,
364369

@@ -2823,6 +2828,7 @@ impl Config {
28232828
.map(|t| t.alternate_screen)
28242829
.unwrap_or_default(),
28252830
tui_status_line: cfg.tui.as_ref().and_then(|t| t.status_line.clone()),
2831+
tui_terminal_title: cfg.tui.as_ref().and_then(|t| t.terminal_title.clone()),
28262832
tui_theme: cfg.tui.as_ref().and_then(|t| t.theme.clone()),
28272833
otel: {
28282834
let t: OtelConfigToml = cfg.otel.unwrap_or_default();

codex-rs/core/src/config/types.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,13 @@ pub struct Tui {
752752
#[serde(default)]
753753
pub status_line: Option<Vec<String>>,
754754

755+
/// Ordered list of terminal title item identifiers.
756+
///
757+
/// When set, the TUI renders the selected items into the terminal window/tab title.
758+
/// When unset, the TUI defaults to: `spinner` and `project`.
759+
#[serde(default)]
760+
pub terminal_title: Option<Vec<String>>,
761+
755762
/// Syntax highlighting theme name (kebab-case).
756763
///
757764
/// When set, overrides automatic light/dark theme detection.

0 commit comments

Comments
 (0)