Skip to content

Commit e64c765

Browse files
authored
Show action required in terminal title (#18372)
Implements #18162 This updates the TUI terminal title to show an explicit action-required state when Codex is blocked on user approval or input. The terminal title now uses the activity title item to cover both active work and blocked-on-user states, while still accepting the legacy spinner config value. Changes - Rename the terminal title item from `spinner` to `activity` while preserving legacy config compatibility - Show `[ ! ] Action Required `while approval or input overlays are active, with a blinking `[ . ]` alternate state - Suppress the normal working spinner while Codex is blocked on user action - Add targeted coverage for action-required title behavior and legacy title-item parsing Testing - Trigger an approval or input modal and confirm the tab title alternates between `[ ! ] Action Required` and `[ . ] Action Required` - Disable the activity title item and confirm the action-required title does not appear - Resolve the prompt and confirm the title returns to the normal spinning/idel state https://github.com/user-attachments/assets/e9ecc530-a6be-4fd7-b9a6-d550a790eb2c
1 parent e903d00 commit e64c765

20 files changed

Lines changed: 450 additions & 68 deletions

codex-rs/config/src/types.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,9 @@ pub struct Tui {
575575
/// Ordered list of terminal title item identifiers.
576576
///
577577
/// When set, the TUI renders the selected items into the terminal window/tab title.
578-
/// When unset, the TUI defaults to: `spinner` and `project`.
578+
/// When unset, the TUI defaults to: `activity` and `project`.
579+
/// The `activity` item spins while working and shows an action-required
580+
/// message when blocked on the user.
579581
#[serde(default)]
580582
pub terminal_title: Option<Vec<String>>,
581583

codex-rs/core/config.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2266,7 +2266,7 @@
22662266
},
22672267
"terminal_title": {
22682268
"default": null,
2269-
"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`.",
2269+
"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: `activity` and `project`. The `activity` item spins while working and shows an action-required message when blocked on the user.",
22702270
"items": {
22712271
"type": "string"
22722272
},

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,9 @@ pub struct Config {
455455

456456
/// Ordered list of terminal title item identifiers for the TUI.
457457
///
458-
/// When unset, the TUI defaults to: `project` and `spinner`.
458+
/// When unset, the TUI defaults to: `activity` and `project`.
459+
/// The `activity` item spins while working and shows an action-required
460+
/// message when blocked on the user.
459461
pub tui_terminal_title: Option<Vec<String>>,
460462

461463
/// Syntax highlighting theme override (kebab-case name).
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use super::TerminalTitleItem;
2+
3+
pub(crate) const ACTION_REQUIRED_PREVIEW_PREFIX: &str = "[ ! ] Action Required";
4+
5+
pub(crate) fn build_action_required_title_text<I, F>(
6+
prefix: &str,
7+
items: I,
8+
excluded_items: &[TerminalTitleItem],
9+
mut value_for: F,
10+
) -> String
11+
where
12+
I: IntoIterator<Item = TerminalTitleItem>,
13+
F: FnMut(TerminalTitleItem) -> Option<String>,
14+
{
15+
let mut parts = vec![prefix.to_string()];
16+
for item in items {
17+
if item == TerminalTitleItem::Spinner || excluded_items.contains(&item) {
18+
continue;
19+
}
20+
if let Some(value) = value_for(item) {
21+
parts.push(value);
22+
}
23+
}
24+
parts.join(" | ")
25+
}

codex-rs/tui/src/bottom_pane/app_link_view.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,10 @@ impl BottomPaneView for AppLinkView {
494494
self.complete = true;
495495
true
496496
}
497+
498+
fn terminal_title_requires_action(&self) -> bool {
499+
self.is_tool_suggestion()
500+
}
497501
}
498502

499503
impl crate::render::renderable::Renderable for AppLinkView {
@@ -630,6 +634,52 @@ mod tests {
630634
);
631635
}
632636

637+
#[test]
638+
fn regular_app_link_does_not_require_terminal_title_action() {
639+
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
640+
let tx = AppEventSender::new(tx_raw);
641+
let view = AppLinkView::new(
642+
AppLinkViewParams {
643+
app_id: "connector_1".to_string(),
644+
title: "Notion".to_string(),
645+
description: None,
646+
instructions: "Manage app".to_string(),
647+
url: "https://example.test/notion".to_string(),
648+
is_installed: true,
649+
is_enabled: true,
650+
suggest_reason: None,
651+
suggestion_type: None,
652+
elicitation_target: None,
653+
},
654+
tx,
655+
);
656+
657+
assert!(!view.terminal_title_requires_action());
658+
}
659+
660+
#[test]
661+
fn tool_suggestion_requires_terminal_title_action() {
662+
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
663+
let tx = AppEventSender::new(tx_raw);
664+
let view = AppLinkView::new(
665+
AppLinkViewParams {
666+
app_id: "connector_google_calendar".to_string(),
667+
title: "Google Calendar".to_string(),
668+
description: Some("Plan events and schedules.".to_string()),
669+
instructions: "Enable this app to use it for the current request.".to_string(),
670+
url: "https://example.test/google-calendar".to_string(),
671+
is_installed: true,
672+
is_enabled: false,
673+
suggest_reason: Some("Plan and reference events from your calendar".to_string()),
674+
suggestion_type: Some(AppLinkSuggestionType::Enable),
675+
elicitation_target: Some(suggestion_target()),
676+
},
677+
tx,
678+
);
679+
680+
assert!(view.terminal_title_requires_action());
681+
}
682+
633683
#[test]
634684
fn toggle_action_sends_set_app_enabled_and_updates_label() {
635685
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();

codex-rs/tui/src/bottom_pane/approval_overlay.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,10 @@ impl BottomPaneView for ApprovalOverlay {
528528
fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) -> bool {
529529
self.dismiss_resolved_request(request)
530530
}
531+
532+
fn terminal_title_requires_action(&self) -> bool {
533+
true
534+
}
531535
}
532536

533537
impl Renderable for ApprovalOverlay {

codex-rs/tui/src/bottom_pane/bottom_pane_view.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,13 @@ pub(crate) trait BottomPaneView: Renderable {
121121
fn dismiss_app_server_request(&mut self, _request: &ResolvedAppServerRequest) -> bool {
122122
false
123123
}
124+
125+
/// Whether this view means the session is blocked waiting for the user.
126+
///
127+
/// Views that return `true` surface an "Action Required" terminal title
128+
/// instead of the normal working spinner so terminal tabs clearly show that
129+
/// Codex needs user input.
130+
fn terminal_title_requires_action(&self) -> bool {
131+
false
132+
}
124133
}

codex-rs/tui/src/bottom_pane/mcp_server_elicitation.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,10 @@ impl BottomPaneView for McpServerElicitationOverlay {
16221622
}
16231623
}
16241624

1625+
fn terminal_title_requires_action(&self) -> bool {
1626+
true
1627+
}
1628+
16251629
fn on_ctrl_c(&mut self) -> CancellationEvent {
16261630
if !self.current_field_is_select() && !self.composer.current_text_with_pending().is_empty()
16271631
{

codex-rs/tui/src/bottom_pane/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ use ratatui::text::Line;
4545
use std::time::Duration;
4646
use std::time::Instant;
4747

48+
mod action_required_title;
4849
mod app_link_view;
4950
mod approval_overlay;
5051
mod mcp_server_elicitation;
@@ -53,6 +54,8 @@ mod request_user_input;
5354
mod status_line_setup;
5455
mod status_surface_preview;
5556
mod title_setup;
57+
pub(crate) use action_required_title::ACTION_REQUIRED_PREVIEW_PREFIX;
58+
pub(crate) use action_required_title::build_action_required_title_text;
5659
pub(crate) use app_link_view::AppLinkElicitationTarget;
5760
pub(crate) use app_link_view::AppLinkSuggestionType;
5861
pub(crate) use app_link_view::AppLinkView;
@@ -1023,6 +1026,11 @@ impl BottomPane {
10231026
self.is_task_running
10241027
}
10251028

1029+
pub(crate) fn terminal_title_requires_action(&self) -> bool {
1030+
self.active_view()
1031+
.is_some_and(bottom_pane_view::BottomPaneView::terminal_title_requires_action)
1032+
}
1033+
10261034
#[cfg(test)]
10271035
pub(crate) fn has_active_view(&self) -> bool {
10281036
!self.view_stack.is_empty()

codex-rs/tui/src/bottom_pane/request_user_input/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,10 @@ impl BottomPaneView for RequestUserInputOverlay {
12401240
}
12411241
}
12421242

1243+
fn terminal_title_requires_action(&self) -> bool {
1244+
true
1245+
}
1246+
12431247
fn on_ctrl_c(&mut self) -> CancellationEvent {
12441248
if self.confirm_unanswered_active() {
12451249
self.close_unanswered_confirmation();

0 commit comments

Comments
 (0)