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
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ expression: "whole_status.replace(&directory,\n\"[[workspace]]\").replace(crate:
---
>_ OpenAI Codex (vVERSION)

Visit https://chatgpt.com/codex/settings/usage for up-to-date
information on rate limits and credits
Visit https://chatgpt.com/codex/settings/usage for up-to-date
information on rate limits and credits

Model: GPT-5.5 (reasoning medium, summaries auto)
Model provider: openai
Directory: [[workspace]]
Permissions: Read Only (Ask for approval)
Agents.md: <none>
Thread name: Clipboard example
Collaboration mode: Default
Session: 00000000-0000-0000-0000-000000000123
Model: GPT-5.5 (reasoning medium, summaries auto)
Model provider: openai
Directory: [[workspace]]
Permissions: Read Only (Ask for approval)
Agents.md: <none>
Thread name: Clipboard example
Collaboration mode: Default
Session: 00000000-0000-0000-0000-000000000123

Token usage: 0 total (0 input + 0 output)
Limits: data not available yet
Token usage: 0 total (0 input + 0 output)
Limits: data not available yet
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
source: tui/src/chatwidget/tests/status_command_tests.rs
expression: thread_usage
---
│ Thread usage: 50 credits · ~$1.82 │
│ Models: GPT-5.4 80%, GPT-5 Mini 20% │
│ Reasoning: Medium 20%, High 80% │
│ Speed: Fast mode 80%, Standard 20% │
│ Billed tokens: 120 input (25 cached) + 50 output │
╰──────────────────────────────────────────────────────────────────────╯
Thread usage: 50 credits · ~$1.82
Models: GPT-5.4 80%, GPT-5 Mini 20%
Reasoning: Medium 20%, High 80%
Speed: Fast mode 80%, Standard 20%
Billed tokens: 120 input (25 cached) + 50 output
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
source: tui/src/chatwidget/tests/status_command_tests.rs
expression: thread_usage
---
│ Thread usage: 50 credits │
│ Models: GPT-5.4 100% │
│ Reasoning: High 100% │
│ Speed: Fast mode 100% │
│ Billed tokens: 100 input (20 cached) + 40 output │
╰──────────────────────────────────────────────────────────────────────╯
Thread usage: 50 credits
Models: GPT-5.4 100%
Reasoning: High 100%
Speed: Fast mode 100%
Billed tokens: 100 input (20 cached) + 40 output
117 changes: 70 additions & 47 deletions codex-rs/tui/src/status/card.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use crate::history_cell::CompositeHistoryCell;
use crate::history_cell::HistoryCell;
use crate::history_cell::PlainHistoryCell;
use crate::history_cell::plain_lines;
use crate::history_cell::with_border_with_inner_width;
use crate::legacy_core::config::Config;
use crate::line_truncation::line_width;
use crate::style::accent_color;
Expand Down Expand Up @@ -33,7 +32,6 @@ use std::path::PathBuf;
use super::account::StatusAccountDisplay;
use super::format::FieldFormatter;
use super::format::push_label;
use super::format::truncate_line_to_width;
use super::helpers::compose_account_display;
use super::helpers::compose_model_display;
use super::helpers::format_directory_display;
Expand All @@ -49,7 +47,6 @@ use super::rate_limits::render_status_limit_progress_bar;
use super::remote_connection::RemoteConnectionStatus;
use super::thread_usage::StatusThreadUsage;
use crate::wrapping::RtOptions;
use crate::wrapping::adaptive_wrap_lines;
use crate::wrapping::word_wrap_lines;
use std::sync::Arc;
use std::sync::RwLock;
Expand Down Expand Up @@ -733,15 +730,11 @@ fn status_approval_label(

impl StatusHistoryCell {
fn content_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut title = vec![FieldFormatter::INDENT.into()];
title.extend(crate::history_cell::codex_title(CODEX_CLI_VERSION));
lines.push(Line::from(title));

let available_inner_width = usize::from(width.saturating_sub(4));
if available_inner_width == 0 {
let available_width = usize::from(width);
if available_width == 0 {
return Vec::new();
}
let mut lines = Vec::new();

let account_value = self.account.as_ref().map(|account| match account {
StatusAccountDisplay::ChatGpt { email, plan } => match (email, plan) {
Expand Down Expand Up @@ -799,27 +792,8 @@ impl StatusHistoryCell {
self.thread_usage.push_labels(&mut labels, &mut seen);

let formatter = FieldFormatter::from_labels(labels.iter().map(String::as_str));
let value_width = formatter.value_width(available_inner_width);

let note_first_line = Line::from(vec![
Span::from("Visit ").fg(accent_color()),
CHATGPT_USAGE_URL.fg(accent_color()).underlined(),
Span::from(" for up-to-date").fg(accent_color()),
]);
let note_second_line = Line::from(vec![
Span::from("information on rate limits and credits").fg(accent_color()),
]);
let note_lines = adaptive_wrap_lines(
[note_first_line, note_second_line],
RtOptions::new(available_inner_width),
);
lines.push(Line::from(Vec::<Span<'static>>::new()));
// The ChatGPT usage page only applies to providers backed by OpenAI auth;
// providers like Bedrock manage limits and billing elsewhere.
if self.show_chatgpt_usage_link {
lines.extend(note_lines);
lines.push(Line::from(Vec::<Span<'static>>::new()));
}
let value_width = formatter.value_width(available_width);

if let Some(remote_connection) = self.remote_connection.as_ref() {
let value = if remote_connection.is_local_daemon {
Line::from("Local background server")
Expand Down Expand Up @@ -847,7 +821,7 @@ impl StatusHistoryCell {
model_spans.push(Span::from(")").dim());
}

let directory_value = format_directory_display(&self.directory, Some(value_width));
let directory_value = format_directory_display(&self.directory, /*max_width*/ None);

lines.push(formatter.line("Model", model_spans));
if let Some(model_provider) = self.model_provider.as_ref() {
Expand Down Expand Up @@ -886,32 +860,81 @@ impl StatusHistoryCell {
lines.push(formatter.line("Context window", spans));
}

lines.extend(self.rate_limit_lines(&rate_limit_state, available_inner_width, &formatter));
lines.extend(self.rate_limit_lines(&rate_limit_state, available_width, &formatter));
let thread_usage_lines = self.thread_usage.lines(&formatter, value_width);
if !thread_usage_lines.is_empty() {
lines.push(Line::from(Vec::<Span<'static>>::new()));
lines.extend(thread_usage_lines);
}

lines
// Leave room for the two-column title mark even in a tiny terminal.
let indent = if available_width >= FieldFormatter::INDENT.len() + 2 {
FieldFormatter::INDENT
} else {
""
};
let mut title = vec![indent.into()];
title.extend(crate::history_cell::codex_title(CODEX_CLI_VERSION));
let mut rendered = word_wrap_lines(
[Line::from(title)],
RtOptions::new(available_width).subsequent_indent(indent.into()),
);
rendered.push(Line::default());
// Providers such as Bedrock manage limits and billing elsewhere.
if self.show_chatgpt_usage_link {
rendered.extend(word_wrap_lines(
[
Line::from(vec![
"Visit ".fg(accent_color()),
CHATGPT_USAGE_URL.fg(accent_color()).underlined(),
" for up-to-date".fg(accent_color()),
]),
"information on rate limits and credits"
.fg(accent_color())
.into(),
],
RtOptions::new(available_width)
.initial_indent(indent.into())
.subsequent_indent(indent.into())
.word_separator(textwrap::WordSeparator::AsciiSpace)
.word_splitter(textwrap::WordSplitter::NoHyphenation),
));
rendered.push(Line::default());
}
// Keep every value, including long paths and IDs, aligned beneath its first line.
// At widths too small for the label column, use the outer indent instead.
let continuation = if value_width > 0 {
formatter.continuation(Vec::new())
} else {
indent.into()
};
let options = RtOptions::new(available_width)
.subsequent_indent(continuation)
.word_splitter(textwrap::WordSplitter::NoHyphenation);
rendered.extend(lines.into_iter().flat_map(|line| {
let wrapped = word_wrap_lines([line.clone()], options.clone());
if wrapped
.iter()
.any(|line| line_width(line) > available_width)
{
// A wide grapheme may not fit after the continuation indent.
word_wrap_lines(
[line],
RtOptions::new(available_width)
.word_splitter(textwrap::WordSplitter::NoHyphenation),
)
} else {
wrapped
}
}));

rendered
}
}

impl HistoryCell for Arc<StatusHistoryCell> {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let available_inner_width = usize::from(width.saturating_sub(4));
if available_inner_width == 0 {
return Vec::new();
}
let lines = self.content_lines(width);
let content_width = lines.iter().map(line_width).max().unwrap_or(0);
let inner_width = content_width.min(available_inner_width);
let truncated_lines: Vec<Line<'static>> = lines
.into_iter()
.map(|line| truncate_line_to_width(line, inner_width))
.collect();

with_border_with_inner_width(truncated_lines, inner_width)
self.content_lines(width)
}

fn raw_lines(&self) -> Vec<Line<'static>> {
Expand Down
55 changes: 3 additions & 52 deletions codex-rs/tui/src/status/format.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use ratatui::prelude::*;
use ratatui::style::Stylize;
use std::collections::BTreeSet;
use unicode_segmentation::UnicodeSegmentation;

use crate::width::display_width;

Expand All @@ -14,7 +13,7 @@ pub(crate) struct FieldFormatter {
}

impl FieldFormatter {
pub(crate) const INDENT: &'static str = " ";
pub(crate) const INDENT: &'static str = " ";

pub(crate) fn from_labels<S>(labels: impl IntoIterator<Item = S>) -> Self
where
Expand All @@ -26,7 +25,7 @@ impl FieldFormatter {
.max()
.unwrap_or(0);
let indent_width = display_width(Self::INDENT);
let value_offset = indent_width + label_width + 1 + 3;
let value_offset = indent_width + label_width + 1 + 2;

Self {
indent: Self::INDENT,
Expand Down Expand Up @@ -74,7 +73,7 @@ impl FieldFormatter {
buf.push(':');

let label_width = display_width(label);
let padding = 3 + self.label_width.saturating_sub(label_width);
let padding = 2 + self.label_width.saturating_sub(label_width);
for _ in 0..padding {
buf.push(' ');
}
Expand All @@ -92,51 +91,3 @@ pub(crate) fn push_label(labels: &mut Vec<String>, seen: &mut BTreeSet<String>,
seen.insert(owned.clone());
labels.push(owned);
}

pub(crate) fn truncate_line_to_width(line: Line<'static>, max_width: usize) -> Line<'static> {
if max_width == 0 {
return Line::from(Vec::<Span<'static>>::new());
}

let mut used = 0usize;
let mut spans_out: Vec<Span<'static>> = Vec::new();

for span in line.spans {
let text = span.content.into_owned();
let style = span.style;
let span_width = display_width(text.as_str());

if span_width == 0 {
spans_out.push(Span::styled(text, style));
continue;
}

if used >= max_width {
break;
}

if used + span_width <= max_width {
used += span_width;
spans_out.push(Span::styled(text, style));
continue;
}

let mut truncated = String::new();
for grapheme in text.graphemes(/*is_extended*/ true) {
let grapheme_width = display_width(grapheme);
if used + grapheme_width > max_width {
break;
}
truncated.push_str(grapheme);
used += grapheme_width;
}

if !truncated.is_empty() {
spans_out.push(Span::styled(truncated, style));
}

break;
}

Line::from(spans_out)
}
2 changes: 1 addition & 1 deletion codex-rs/tui/src/status/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! This module turns protocol-level snapshots into stable display structures used by `/status`
//! output and footer/status-line helpers, while keeping rendering concerns out of transport-facing
//! code.
//! code. The status summary is borderless and wraps values without truncating paths or IDs.
//!
//! `rate_limits` is the main integration point for status-line usage-limit items: it converts raw
//! window snapshots into local-time labels and classifies data as available, stale, or missing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ expression: rendered
---
/status

╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Model: gpt-5.6-sol (reasoning none, summaries auto) │
│ Model provider: server-ollama │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, Ask for approval) │
│ Agents.md: <none> │
│ │
│ Token usage: 0 total (0 input + 0 output) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────────╯
>_ OpenAI Codex (v0.0.0)

Model: gpt-5.6-sol (reasoning none, summaries auto)
Model provider: server-ollama
Directory: [[workspace]]
Permissions: Custom (workspace with network access, Ask for approval)
Agents.md: <none>

Token usage: 0 total (0 input + 0 output)
Limits: data not available yet
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ expression: rendered
---
/status

╭─────────────────────────────────────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.0.0) │
│ │
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
│ information on rate limits and credits │
│ │
│ Model: gpt-5.6-sol (reasoning none, summaries auto) │
│ Model provider: server-openai │
│ Directory: [[workspace]] │
│ Permissions: Custom (workspace with network access, Ask for approval) │
│ Agents.md: <none> │
│ │
│ Token usage: 0 total (0 input + 0 output) │
│ Limits: data not available yet │
╰─────────────────────────────────────────────────────────────────────────────╯
>_ OpenAI Codex (v0.0.0)

Visit https://chatgpt.com/codex/settings/usage for up-to-date
information on rate limits and credits

Model: gpt-5.6-sol (reasoning none, summaries auto)
Model provider: server-openai
Directory: [[workspace]]
Permissions: Custom (workspace with network access, Ask for approval)
Agents.md: <none>

Token usage: 0 total (0 input + 0 output)
Limits: data not available yet
Loading
Loading