Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions codex-rs/tui/src/app/background_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const TOKEN_ACTIVITY_FETCH_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(/*secs*/ 15);
const RATE_LIMIT_RESET_REQUEST_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(/*secs*/ 15);
const WORKSPACE_HEADLINE_FETCH_TIMEOUT: std::time::Duration =
std::time::Duration::from_millis(/*millis*/ 2000);

impl App {
pub(super) fn fetch_mcp_inventory(
Expand Down Expand Up @@ -158,6 +160,29 @@ impl App {
});
}

pub(super) fn refresh_status_line_workspace_headline(
&mut self,
app_server: &AppServerSession,
request_id: u64,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let result = tokio::time::timeout(
WORKSPACE_HEADLINE_FETCH_TIMEOUT,
fetch_workspace_messages(request_handle),
)
.await
.map_err(|_| "account/workspaceMessages/read timed out in TUI".to_string())
.and_then(|result| {
result
.map(crate::workspace_messages::workspace_headline_from_response)
.map_err(|err| err.to_string())
});
app_event_tx.send(AppEvent::StatusLineWorkspaceHeadlineUpdated { request_id, result });
});
}

pub(super) fn send_add_credits_nudge_email(
&mut self,
app_server: &AppServerSession,
Expand Down Expand Up @@ -796,6 +821,19 @@ pub(super) async fn consume_rate_limit_reset_credit_request(
.wrap_err("account/rateLimitResetCredit/consume failed in TUI")
}

pub(super) async fn fetch_workspace_messages(
request_handle: AppServerRequestHandle,
) -> Result<codex_app_server_protocol::GetWorkspaceMessagesResponse> {
let request_id = RequestId::String(format!("workspace-messages-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::GetWorkspaceMessages {
request_id,
params: None,
})
.await
.wrap_err("account/workspaceMessages/read failed in TUI")
}

pub(super) async fn send_add_credits_nudge_email(
request_handle: AppServerRequestHandle,
credit_type: AddCreditsNudgeCreditType,
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,9 @@ impl App {
AppEvent::RefreshTokenActivity { request_id } => {
self.refresh_token_activity(app_server, request_id);
}
AppEvent::RefreshStatusLineWorkspaceHeadline { request_id } => {
self.refresh_status_line_workspace_headline(app_server, request_id);
}
AppEvent::OpenThreadGoalMenu { thread_id } => {
self.open_thread_goal_menu(app_server, thread_id).await;
}
Expand Down Expand Up @@ -2010,6 +2013,14 @@ impl App {
self.chat_widget.set_status_line_git_summary(cwd, summary);
self.refresh_status_line();
}
AppEvent::StatusLineWorkspaceHeadlineUpdated { request_id, result } => {
if self
.chat_widget
.set_status_line_workspace_headline(request_id, result)
{
tui.frame_requester().schedule_frame();
}
}
AppEvent::StatusLineSetupCancelled => {
self.chat_widget.cancel_status_line_setup();
}
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,11 @@ pub(crate) enum AppEvent {
result: Result<GetAccountTokenUsageResponse, String>,
},

/// Fetch workspace messages for the status-line headline item.
RefreshStatusLineWorkspaceHeadline {
request_id: u64,
},

/// Commit settled asynchronous usage output after active-output barriers clear.
CommitPendingUsageOutput,

Expand Down Expand Up @@ -977,6 +982,11 @@ pub(crate) enum AppEvent {
cwd: PathBuf,
summary: crate::chatwidget::StatusLineGitSummary,
},
/// Async update of the workspace notification headline for status line rendering.
StatusLineWorkspaceHeadlineUpdated {
request_id: u64,
result: Result<crate::workspace_messages::WorkspaceHeadlineFetchResult, String>,
},
/// Apply a user-confirmed status-line item ordering/selection.
StatusLineSetup {
items: Vec<StatusLineItem>,
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/tui/src/bottom_pane/status_line_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ pub(crate) enum StatusLineItem {
/// Current thread title (if set by user).
ThreadTitle,

/// Current workspace notification headline.
WorkspaceHeadline,
Comment thread
xli-oai marked this conversation as resolved.
Comment thread
xli-oai marked this conversation as resolved.

/// Latest checklist task progress from `update_plan` (if available).
TaskProgress,
}
Expand Down Expand Up @@ -185,6 +188,9 @@ impl StatusLineItem {
StatusLineItem::ThreadTitle => {
"Current thread title, or thread identifier when unnamed"
}
StatusLineItem::WorkspaceHeadline => {
"Workspace notification headline (Enterprise workspaces only; omitted when unavailable)"
}
StatusLineItem::TaskProgress => {
"Latest task progress from update_plan (omitted until available)"
}
Expand Down Expand Up @@ -217,6 +223,7 @@ impl StatusLineItem {
StatusLineItem::FastMode => StatusSurfacePreviewItem::FastMode,
StatusLineItem::RawOutput => StatusSurfacePreviewItem::RawOutput,
StatusLineItem::ThreadTitle => StatusSurfacePreviewItem::ThreadTitle,
StatusLineItem::WorkspaceHeadline => StatusSurfacePreviewItem::WorkspaceHeadline,
StatusLineItem::TaskProgress => StatusSurfacePreviewItem::TaskProgress,
}
}
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/bottom_pane/status_line_style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl StatusLineAccent {
StatusLineItem::FastMode | StatusLineItem::RawOutput => Self::Mode,
StatusLineItem::Permissions => Self::Mode,
StatusLineItem::ApprovalMode => Self::Mode,
StatusLineItem::ThreadTitle => Self::Thread,
StatusLineItem::ThreadTitle | StatusLineItem::WorkspaceHeadline => Self::Thread,
StatusLineItem::TaskProgress => Self::Progress,
}
}
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/bottom_pane/status_surface_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub(crate) enum StatusSurfacePreviewItem {
SessionId,
FastMode,
RawOutput,
WorkspaceHeadline,
Model,
ModelWithReasoning,
Reasoning,
Expand Down Expand Up @@ -62,6 +63,7 @@ impl StatusSurfacePreviewItem {
StatusSurfacePreviewItem::SessionId => "550e8400-e29b-41d4",
StatusSurfacePreviewItem::FastMode => "Fast on",
StatusSurfacePreviewItem::RawOutput => "raw output",
StatusSurfacePreviewItem::WorkspaceHeadline => "Workspace headline",
StatusSurfacePreviewItem::Model => "gpt-5.2-codex",
StatusSurfacePreviewItem::ModelWithReasoning => "gpt-5.2-codex medium",
StatusSurfacePreviewItem::Reasoning => "medium",
Expand Down Expand Up @@ -94,6 +96,7 @@ impl StatusSurfacePreviewItem {
Self::SessionId,
Self::FastMode,
Self::RawOutput,
Self::WorkspaceHeadline,
Self::Model,
Self::ModelWithReasoning,
Self::Reasoning,
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,16 @@ pub(crate) struct ChatWidget {
status_line_git_summary_pending: bool,
// True once we've attempted a Git summary lookup for the current CWD.
status_line_git_summary_lookup_complete: bool,
// Cached workspace notification headline for the status line.
status_line_workspace_headline: Option<String>,
// Request ID for the async workspace headline fetch currently in flight.
status_line_workspace_headline_pending_request_id: Option<u64>,
// Request ID to assign to the next workspace headline fetch.
next_status_line_workspace_headline_request_id: u64,
// Last time a workspace headline fetch was requested.
status_line_workspace_headline_last_requested_at: Option<Instant>,
// Set after the backend reports the workspace-message feature gate is disabled.
status_line_workspace_messages_disabled: bool,
// Current thread-goal status shown in the status line when plan mode is inactive.
current_goal_status_indicator: Option<GoalStatusIndicator>,
current_goal_status: Option<GoalStatusState>,
Expand Down Expand Up @@ -1188,6 +1198,7 @@ impl ChatWidget {
{
self.refresh_terminal_title();
}
self.refresh_status_line_if_workspace_headline_due();
}

fn flush_active_cell(&mut self) {
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/chatwidget/constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ impl ChatWidget {
status_line_git_summary_cwd: None,
status_line_git_summary_pending: false,
status_line_git_summary_lookup_complete: false,
status_line_workspace_headline: None,
status_line_workspace_headline_pending_request_id: None,
next_status_line_workspace_headline_request_id: 0,
status_line_workspace_headline_last_requested_at: None,
status_line_workspace_messages_disabled: false,
current_goal_status_indicator: None,
current_goal_status: None,
external_editor_state: ExternalEditorState::Closed,
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/chatwidget/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ impl ChatWidget {
self.clear_pending_token_activity_refreshes();
self.clear_pending_rate_limit_reset_requests();
}
self.status_line_workspace_headline = None;
self.status_line_workspace_headline_pending_request_id = None;
self.status_line_workspace_headline_last_requested_at = None;
self.status_line_workspace_messages_disabled = false;
self.status_account_display = status_account_display;
self.plan_type = plan_type;
self.has_chatgpt_account = has_chatgpt_account;
Expand All @@ -232,6 +236,7 @@ impl ChatWidget {
.set_connectors_enabled(self.connectors_enabled());
self.bottom_pane
.set_token_activity_command_enabled(has_codex_backend_auth);
self.refresh_status_line();
}

/// Set the syntax theme override in the widget's config copy.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
source: tui/src/chatwidget/tests/status_surface_previews.rs
expression: status_line_popup_snapshot(&mut chat)
---
Configure Status Line
Select which items to display in the status line.

Type to search
>
› [x] Use theme colors Apply colors from the active /theme
───────────────────────
[x] workspace-headline Workspace notification headline (Enterprise workspaces only; omitted …
[ ] model Current model name
[ ] model-with-reasoning Current model name with reasoning level
[ ] reasoning Current reasoning level
[ ] current-dir Current working directory
[ ] project-name Project name (omitted when unavailable)

Workspace maintenance starts at 5pm
Press space to toggle; ←/→ to move; enter to confirm and close; esc to close
95 changes: 95 additions & 0 deletions codex-rs/tui/src/chatwidget/status_surfaces.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ impl StatusSurfaceSelections {
.status_line_items
.contains(&StatusLineItem::BranchChanges)
}

fn uses_workspace_headline(&self) -> bool {
self.status_line_items
.contains(&StatusLineItem::WorkspaceHeadline)
}
}

/// Cached project-root display name keyed by the cwd used for the last lookup.
Expand Down Expand Up @@ -157,6 +162,15 @@ impl ChatWidget {
self.request_status_line_git_summary(cwd);
}
}

if !selections.uses_workspace_headline() {
self.status_line_workspace_headline = None;
self.status_line_workspace_headline_pending_request_id = None;
self.status_line_workspace_headline_last_requested_at = None;
self.status_line_workspace_messages_disabled = false;
} else {
self.request_status_line_workspace_headline_if_due(Instant::now());
}
}

fn refresh_status_line_from_selections(&mut self, selections: &StatusSurfaceSelections) {
Expand Down Expand Up @@ -553,6 +567,85 @@ impl ChatWidget {
});
}

fn request_status_line_workspace_headline_if_due(&mut self, now: Instant) {
if !self.status_line_workspace_headline_should_fetch(now) {
return;
}
let request_id = self.next_status_line_workspace_headline_request_id;
self.next_status_line_workspace_headline_request_id = self
.next_status_line_workspace_headline_request_id
.wrapping_add(/*rhs*/ 1);
self.status_line_workspace_headline_pending_request_id = Some(request_id);
self.status_line_workspace_headline_last_requested_at = Some(now);
self.app_event_tx
.send(AppEvent::RefreshStatusLineWorkspaceHeadline { request_id });
}

fn status_line_workspace_headline_should_fetch(&self, now: Instant) -> bool {
if self
.status_line_workspace_headline_pending_request_id
.is_some()
|| self.status_line_workspace_messages_disabled
|| !self.has_codex_backend_auth
{
return false;
}

self.status_line_workspace_headline_last_requested_at
.is_none_or(|last_requested_at| {
now.saturating_duration_since(last_requested_at)
>= crate::workspace_messages::WORKSPACE_HEADLINE_REFRESH_INTERVAL
})
}

pub(super) fn refresh_status_line_if_workspace_headline_due(&mut self) {
let now = Instant::now();
if self.status_line_workspace_headline_should_fetch(now)
&& self
.status_line_items_with_invalids()
.0
.contains(&StatusLineItem::WorkspaceHeadline)
{
self.refresh_status_line();
}
}

pub(crate) fn set_status_line_workspace_headline(
&mut self,
request_id: u64,
result: Result<crate::workspace_messages::WorkspaceHeadlineFetchResult, String>,
) -> bool {
if self.status_line_workspace_headline_pending_request_id != Some(request_id) {
return false;
}
self.status_line_workspace_headline_pending_request_id = None;
match result {
Ok(crate::workspace_messages::WorkspaceHeadlineFetchResult::Available(headline)) => {
self.status_line_workspace_messages_disabled = false;
self.status_line_workspace_headline = headline;
Comment thread
xli-oai marked this conversation as resolved.
Comment thread
xli-oai marked this conversation as resolved.
}
Ok(crate::workspace_messages::WorkspaceHeadlineFetchResult::FeatureDisabled) => {
self.status_line_workspace_messages_disabled = true;
self.status_line_workspace_headline = None;
}
Err(err) => {
tracing::debug!(error = %err, "failed to fetch workspace headline");
}
}

if !self.status_line_workspace_messages_disabled
&& self
.status_line_items_with_invalids()
.0
.contains(&StatusLineItem::WorkspaceHeadline)
{
self.frame_requester
.schedule_frame_in(crate::workspace_messages::WORKSPACE_HEADLINE_REFRESH_INTERVAL);
}
self.refresh_status_line();
true
}

/// Resolves a display string for one configured status-line item.
///
/// Returning `None` means "omit this item for now", not "configuration error". Callers rely on
Expand Down Expand Up @@ -653,6 +746,7 @@ impl ChatWidget {
}
},
),
StatusLineItem::WorkspaceHeadline => self.status_line_workspace_headline.clone(),
Comment thread
xli-oai marked this conversation as resolved.
StatusLineItem::TaskProgress => self.terminal_title_task_progress(),
}
}
Expand Down Expand Up @@ -693,6 +787,7 @@ impl ChatWidget {
StatusSurfacePreviewItem::SessionId => StatusLineItem::SessionId,
StatusSurfacePreviewItem::FastMode => StatusLineItem::FastMode,
StatusSurfacePreviewItem::RawOutput => StatusLineItem::RawOutput,
StatusSurfacePreviewItem::WorkspaceHeadline => StatusLineItem::WorkspaceHeadline,
StatusSurfacePreviewItem::Model => StatusLineItem::ModelName,
StatusSurfacePreviewItem::ModelWithReasoning => StatusLineItem::ModelWithReasoning,
StatusSurfacePreviewItem::Reasoning => StatusLineItem::Reasoning,
Expand Down
Loading
Loading