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
52 changes: 49 additions & 3 deletions codex-rs/tui/src/app_server_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1591,10 +1591,19 @@ async fn thread_session_state_from_thread_response(
pub(crate) fn app_server_rate_limit_snapshots(
response: GetAccountRateLimitsResponse,
) -> Vec<RateLimitSnapshot> {
let mut snapshots = Vec::new();
snapshots.push(response.rate_limits);
let primary_limit_id = response.rate_limits.limit_id.clone();
let mut snapshots = vec![response.rate_limits];
if let Some(by_limit_id) = response.rate_limits_by_limit_id {
snapshots.extend(by_limit_id.into_values());
snapshots.extend(by_limit_id.into_iter().filter_map(|(limit_id, snapshot)| {
if primary_limit_id.as_deref().is_some_and(|primary_limit_id| {
primary_limit_id == limit_id
|| Some(primary_limit_id) == snapshot.limit_id.as_deref()
}) {
None
} else {
Some(snapshot)
}
}));
}
snapshots
}
Expand Down Expand Up @@ -1635,6 +1644,43 @@ mod tests {
.expect("config should build")
}

fn rate_limit_snapshot(limit_id: &str) -> RateLimitSnapshot {
RateLimitSnapshot {
limit_id: Some(limit_id.to_string()),
limit_name: None,
primary: Some(codex_app_server_protocol::RateLimitWindow {
used_percent: 0,
window_duration_mins: Some(10_080),
resets_at: None,
}),
secondary: None,
credits: None,
plan_type: None,
rate_limit_reached_type: None,
}
}

#[test]
fn app_server_rate_limit_snapshots_deduplicates_top_level_limit_from_map() {
let response = GetAccountRateLimitsResponse {
rate_limits: rate_limit_snapshot("codex"),
rate_limits_by_limit_id: Some(HashMap::from([
("codex".to_string(), rate_limit_snapshot("codex")),
("other".to_string(), rate_limit_snapshot("other")),
])),
};

let snapshots = app_server_rate_limit_snapshots(response);

assert_eq!(
snapshots
.iter()
.map(|snapshot| snapshot.limit_id.as_deref())
.collect::<Vec<_>>(),
vec![Some("codex"), Some("other")]
);
}

#[tokio::test]
async fn thread_start_params_include_cwd_for_embedded_sessions() {
let temp_dir = tempfile::tempdir().expect("tempdir");
Expand Down
42 changes: 32 additions & 10 deletions codex-rs/tui/src/bottom_pane/status_line_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
//! - Permissions profile
//! - Approval mode
//! - Context usage (remaining %, used %, window size)
//! - Usage limits (5-hour, weekly)
//! - Usage limits (primary, secondary)
//! - Session info (thread title, thread ID, tokens used)
//! - Application version

Expand Down Expand Up @@ -100,10 +100,10 @@ pub(crate) enum StatusLineItem {
#[strum(to_string = "context-used", serialize = "context-usage")]
ContextUsed,

/// Remaining usage on the 5-hour rate limit.
/// Remaining usage on the primary rate limit.
FiveHourLimit,

/// Remaining usage on the weekly rate limit.
/// Remaining usage on the secondary rate limit.
WeeklyLimit,

/// Codex application version.
Expand Down Expand Up @@ -163,10 +163,10 @@ impl StatusLineItem {
"Percentage of context window used (omitted when unknown)"
}
StatusLineItem::FiveHourLimit => {
"Remaining usage on 5-hour usage limit (omitted when unavailable)"
"Remaining usage on the primary usage limit (omitted when unavailable)"
}
StatusLineItem::WeeklyLimit => {
"Remaining usage on weekly usage limit (omitted when unavailable)"
"Remaining usage on the secondary usage limit (omitted when unavailable)"
}
StatusLineItem::CodexVersion => "Codex application version",
StatusLineItem::ContextWindowSize => {
Expand Down Expand Up @@ -268,7 +268,11 @@ impl StatusLineSetupView {
if !used_ids.insert(item_id.clone()) {
continue;
}
items.push(Self::status_line_select_item(item, /*enabled*/ true));
items.push(Self::status_line_select_item(
item,
/*enabled*/ true,
&preview_data,
));
}
}

Expand All @@ -277,7 +281,11 @@ impl StatusLineSetupView {
if used_ids.contains(&item_id) {
continue;
}
items.push(Self::status_line_select_item(item, /*enabled*/ false));
items.push(Self::status_line_select_item(
item,
/*enabled*/ false,
&preview_data,
));
}

Self {
Expand Down Expand Up @@ -324,11 +332,25 @@ impl StatusLineSetupView {
}

/// Converts a [`StatusLineItem`] into a [`MultiSelectItem`] for the picker.
fn status_line_select_item(item: StatusLineItem, enabled: bool) -> MultiSelectItem {
fn status_line_select_item(
item: StatusLineItem,
enabled: bool,
preview_data: &StatusSurfacePreviewData,
) -> MultiSelectItem {
let default_name = item.to_string();
let default_description = item.description();
let (name, description) = match item {
StatusLineItem::FiveHourLimit | StatusLineItem::WeeklyLimit => (
preview_data.rate_limit_item_name(item.preview_item(), &default_name),
preview_data.rate_limit_item_description(item.preview_item(), default_description),
),
_ => (default_name, default_description.to_string()),
};

MultiSelectItem {
id: item.to_string(),
name: item.to_string(),
description: Some(item.description().to_string()),
name,
description: Some(description),
enabled,
orderable: true,
section_break_after: false,
Expand Down
90 changes: 88 additions & 2 deletions codex-rs/tui/src/bottom_pane/status_surface_preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ impl StatusSurfacePreviewItem {
StatusSurfacePreviewItem::ApprovalMode => "on-request",
StatusSurfacePreviewItem::ContextRemaining => "Context 0% left",
StatusSurfacePreviewItem::ContextUsed => "Context 0% used",
StatusSurfacePreviewItem::FiveHourLimit => "5h 0%",
StatusSurfacePreviewItem::WeeklyLimit => "weekly 0%",
StatusSurfacePreviewItem::FiveHourLimit => "primary 0%",
StatusSurfacePreviewItem::WeeklyLimit => "secondary 0%",
Comment thread
ase-openai marked this conversation as resolved.
StatusSurfacePreviewItem::CodexVersion => "0.0.0",
StatusSurfacePreviewItem::ContextWindowSize => "0 window",
StatusSurfacePreviewItem::UsedTokens => "0 used",
Expand Down Expand Up @@ -169,10 +169,49 @@ impl StatusSurfacePreviewData {
);
}

pub(crate) fn suppress_placeholder(&mut self, item: StatusSurfacePreviewItem) {
if self
.values
.get(&item)
.is_some_and(|value| value.is_placeholder)
{
self.values.remove(&item);
}
}

pub(crate) fn rate_limit_item_name(
&self,
item: StatusSurfacePreviewItem,
fallback: &str,
) -> String {
self.live_value_for(item)
.and_then(rate_limit_preview_copy)
.map(|copy| copy.name.to_string())
.unwrap_or_else(|| fallback.to_string())
}

pub(crate) fn rate_limit_item_description(
&self,
item: StatusSurfacePreviewItem,
fallback: &str,
) -> String {
self.live_value_for(item)
.and_then(rate_limit_preview_copy)
.map(|copy| copy.description.to_string())
.unwrap_or_else(|| fallback.to_string())
}

pub(crate) fn value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str> {
self.values.get(&item).map(|value| value.text.as_str())
}

fn live_value_for(&self, item: StatusSurfacePreviewItem) -> Option<&str> {
self.values
.get(&item)
.filter(|value| !value.is_placeholder)
.map(|value| value.text.as_str())
}

pub(crate) fn status_line_for_items<I>(
&self,
items: I,
Expand All @@ -188,3 +227,50 @@ impl StatusSurfacePreviewData {
status_line_from_segments(segments, use_theme_colors)
}
}

struct RateLimitPreviewCopy {
name: &'static str,
description: &'static str,
}

fn rate_limit_preview_copy(value: &str) -> Option<RateLimitPreviewCopy> {
let value = value.trim_start();
if value.starts_with("secondary usage ") {
Some(RateLimitPreviewCopy {
name: "secondary-usage-limit",
description: "Remaining usage on the secondary usage limit (omitted when unavailable)",
})
} else if value.starts_with("usage ") {
Some(RateLimitPreviewCopy {
name: "usage-limit",
description: "Remaining usage on the primary usage limit (omitted when unavailable)",
})
} else if value.starts_with("5h ") {
Some(RateLimitPreviewCopy {
name: "five-hour-limit",
description: "Remaining usage on the 5-hour usage limit (omitted when unavailable)",
})
} else if value.starts_with("daily ") {
Some(RateLimitPreviewCopy {
name: "daily-limit",
description: "Remaining usage on the daily usage limit (omitted when unavailable)",
})
} else if value.starts_with("weekly ") {
Some(RateLimitPreviewCopy {
name: "weekly-limit",
description: "Remaining usage on the weekly usage limit (omitted when unavailable)",
})
} else if value.starts_with("monthly ") {
Some(RateLimitPreviewCopy {
name: "monthly-limit",
description: "Remaining usage on the monthly usage limit (omitted when unavailable)",
})
} else if value.starts_with("annual ") {
Some(RateLimitPreviewCopy {
name: "annual-limit",
description: "Remaining usage on the annual usage limit (omitted when unavailable)",
})
} else {
None
}
}
37 changes: 28 additions & 9 deletions codex-rs/tui/src/bottom_pane/title_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ pub(crate) enum TerminalTitleItem {
/// Percentage of context window used.
#[strum(to_string = "context-used", serialize = "context-usage")]
ContextUsed,
/// Remaining usage on the 5-hour rate limit.
/// Remaining usage on the primary rate limit.
FiveHourLimit,
/// Remaining usage on the weekly rate limit.
/// Remaining usage on the secondary rate limit.
WeeklyLimit,
/// Codex application version.
CodexVersion,
Expand Down Expand Up @@ -107,10 +107,10 @@ impl TerminalTitleItem {
"Percentage of context window used (omitted when unknown)"
}
TerminalTitleItem::FiveHourLimit => {
"Remaining usage on 5-hour usage limit (omitted when unavailable)"
"Remaining usage on the primary usage limit (omitted when unavailable)"
}
TerminalTitleItem::WeeklyLimit => {
"Remaining usage on weekly usage limit (omitted when unavailable)"
"Remaining usage on the secondary usage limit (omitted when unavailable)"
}
TerminalTitleItem::CodexVersion => "Codex application version",
TerminalTitleItem::UsedTokens => "Total tokens used in session (omitted when zero)",
Expand Down Expand Up @@ -259,11 +259,13 @@ impl TerminalTitleSetupView {
.collect::<std::collections::HashSet<_>>();
let items = selected_items
.into_iter()
.map(|item| Self::title_select_item(item, /*enabled*/ true))
.map(|item| Self::title_select_item(item, /*enabled*/ true, &preview_data))
.chain(
TerminalTitleItem::iter()
.filter(|item| !selected_set.contains(item))
.map(|item| Self::title_select_item(item, /*enabled*/ false)),
.map(|item| {
Self::title_select_item(item, /*enabled*/ false, &preview_data)
}),
)
.collect();

Expand Down Expand Up @@ -309,11 +311,28 @@ impl TerminalTitleSetupView {
}
}

fn title_select_item(item: TerminalTitleItem, enabled: bool) -> MultiSelectItem {
fn title_select_item(
item: TerminalTitleItem,
enabled: bool,
preview_data: &StatusSurfacePreviewData,
) -> MultiSelectItem {
let default_name = item.to_string();
let default_description = item.description();
let (name, description) = match item.preview_item() {
Some(
preview_item @ (StatusSurfacePreviewItem::FiveHourLimit
| StatusSurfacePreviewItem::WeeklyLimit),
) => (
preview_data.rate_limit_item_name(preview_item, &default_name),
preview_data.rate_limit_item_description(preview_item, default_description),
),
_ => (default_name, default_description.to_string()),
};

MultiSelectItem {
id: item.to_string(),
name: item.to_string(),
description: Some(item.description().to_string()),
name,
description: Some(description),
enabled,
orderable: true,
section_break_after: false,
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,9 @@ use self::rate_limits::RateLimitErrorKind;
use self::rate_limits::RateLimitSwitchPromptState;
use self::rate_limits::RateLimitWarningState;
use self::rate_limits::app_server_rate_limit_error_kind;
pub(crate) use self::rate_limits::get_limits_duration;
pub(crate) use self::rate_limits::fallback_limit_label;
use self::rate_limits::is_app_server_cyber_policy_error;
pub(crate) use self::rate_limits::limit_label_for_window;
mod realtime;
mod rendering;
mod replay;
Expand Down
Loading
Loading