feat: show forked from session id in /status (#9330)

Summary:
- Add forked_from to SessionMeta/SessionConfiguredEvent and persist it
for forked sessions.
- Surface forked_from in /status for tui + tui2 and add snapshots.
This commit is contained in:
Anton Panasenko
2026-01-16 13:41:46 -08:00
committed by GitHub
Unverified
parent f1653dd4d3
commit c26fe64539
19 changed files with 267 additions and 0 deletions
@@ -52,6 +52,7 @@ pub fn create_fake_rollout(
// Build JSONL lines
let meta = SessionMeta {
id: conversation_id,
forked_from_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
originator: "codex".to_string(),
@@ -129,6 +130,7 @@ pub fn create_fake_rollout_with_text_elements(
// Build JSONL lines
let meta = SessionMeta {
id: conversation_id,
forked_from_id: None,
timestamp: meta_rfc3339.to_string(),
cwd: PathBuf::from("/"),
originator: "codex".to_string(),
+4
View File
@@ -588,6 +588,8 @@ impl Session {
));
}
let forked_from_id = initial_history.forked_from_id();
let (conversation_id, rollout_params) = match &initial_history {
InitialHistory::New | InitialHistory::Forked(_) => {
let conversation_id = ThreadId::default();
@@ -595,6 +597,7 @@ impl Session {
conversation_id,
RolloutRecorderParams::new(
conversation_id,
forked_from_id,
session_configuration.user_instructions.clone(),
session_source,
),
@@ -733,6 +736,7 @@ impl Session {
id: INITIAL_SUBMIT_ID.to_owned(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id,
model: session_configuration.model.clone(),
model_provider_id: config.model_provider_id.clone(),
approval_policy: session_configuration.approval_policy.value(),
+5
View File
@@ -57,6 +57,7 @@ pub struct RolloutRecorder {
pub enum RolloutRecorderParams {
Create {
conversation_id: ThreadId,
forked_from_id: Option<ThreadId>,
instructions: Option<String>,
source: SessionSource,
},
@@ -79,11 +80,13 @@ enum RolloutCmd {
impl RolloutRecorderParams {
pub fn new(
conversation_id: ThreadId,
forked_from_id: Option<ThreadId>,
instructions: Option<String>,
source: SessionSource,
) -> Self {
Self::Create {
conversation_id,
forked_from_id,
instructions,
source,
}
@@ -158,6 +161,7 @@ impl RolloutRecorder {
let (file, rollout_path, meta) = match params {
RolloutRecorderParams::Create {
conversation_id,
forked_from_id,
instructions,
source,
} => {
@@ -181,6 +185,7 @@ impl RolloutRecorder {
path,
Some(SessionMeta {
id: session_id,
forked_from_id,
timestamp,
cwd: config.cwd.clone(),
originator: originator().value,
+1
View File
@@ -641,6 +641,7 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> {
item: RolloutItem::SessionMeta(SessionMetaLine {
meta: SessionMeta {
id: conversation_id,
forked_from_id: None,
timestamp: ts.to_string(),
instructions: None,
cwd: ".".into(),
@@ -75,6 +75,7 @@ fn session_configured_produces_thread_started_event() {
"e1",
EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id,
forked_from_id: None,
model: "codex-mini-latest".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -257,6 +257,7 @@ mod tests {
id: "1".to_string(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: thread_id,
forked_from_id: None,
model: "gpt-4o".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -296,6 +297,7 @@ mod tests {
let rollout_file = NamedTempFile::new()?;
let session_configured_event = SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
model: "gpt-4o".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -359,6 +361,7 @@ mod tests {
let rollout_file = NamedTempFile::new()?;
let session_configured_event = SessionConfiguredEvent {
session_id: thread_id,
forked_from_id: None,
model: "gpt-4o".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
+22
View File
@@ -1376,6 +1376,22 @@ pub enum InitialHistory {
}
impl InitialHistory {
pub fn forked_from_id(&self) -> Option<ThreadId> {
match self {
InitialHistory::New => None,
InitialHistory::Resumed(resumed) => {
resumed.history.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => meta_line.meta.forked_from_id,
_ => None,
})
}
InitialHistory::Forked(items) => items.iter().find_map(|item| match item {
RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.id),
_ => None,
}),
}
}
pub fn get_rollout_items(&self) -> Vec<RolloutItem> {
match self {
InitialHistory::New => Vec::new(),
@@ -1459,6 +1475,8 @@ impl fmt::Display for SubAgentSource {
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, TS)]
pub struct SessionMeta {
pub id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub forked_from_id: Option<ThreadId>,
pub timestamp: String,
pub cwd: PathBuf,
pub originator: String,
@@ -1473,6 +1491,7 @@ impl Default for SessionMeta {
fn default() -> Self {
SessionMeta {
id: ThreadId::default(),
forked_from_id: None,
timestamp: String::new(),
cwd: PathBuf::new(),
originator: String::new(),
@@ -2003,6 +2022,8 @@ pub struct SkillsListEntry {
pub struct SessionConfiguredEvent {
/// Name left as session_id instead of thread_id for backwards compatibility.
pub session_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub forked_from_id: Option<ThreadId>,
/// Tell the client what model is being queried.
pub model: String,
@@ -2376,6 +2397,7 @@ mod tests {
id: "1234".to_string(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
model: "codex-mini-latest".to_string(),
model_provider_id: "openai".to_string(),
approval_policy: AskForApproval::Never,
+3
View File
@@ -1992,6 +1992,7 @@ mod tests {
let make_header = |is_first| {
let event = SessionConfiguredEvent {
session_id: ThreadId::new(),
forked_from_id: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -2033,6 +2034,7 @@ mod tests {
id: String::new(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: base_id,
forked_from_id: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -2075,6 +2077,7 @@ mod tests {
let thread_id = ThreadId::new();
let event = SessionConfiguredEvent {
session_id: thread_id,
forked_from_id: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
+5
View File
@@ -440,6 +440,7 @@ pub(crate) struct ChatWidget {
// Previous status header to restore after a transient stream retry.
retry_status_header: Option<String>,
thread_id: Option<ThreadId>,
forked_from: Option<ThreadId>,
frame_requester: FrameRequester,
// Whether to include the initial welcome banner on session configured
show_welcome_banner: bool,
@@ -600,6 +601,7 @@ impl ChatWidget {
.set_history_metadata(event.history_log_id, event.history_entry_count);
self.set_skills(None);
self.thread_id = Some(event.session_id);
self.forked_from = event.forked_from_id;
self.current_rollout_path = Some(event.rollout_path.clone());
let initial_messages = event.initial_messages.clone();
let model_for_header = event.model.clone();
@@ -1699,6 +1701,7 @@ impl ChatWidget {
current_status_header: String::from("Working"),
retry_status_header: None,
thread_id: None,
forked_from: None,
queued_user_messages: VecDeque::new(),
show_welcome_banner: is_first_run,
suppress_session_configured_redraw: false,
@@ -1797,6 +1800,7 @@ impl ChatWidget {
current_status_header: String::from("Working"),
retry_status_header: None,
thread_id: None,
forked_from: None,
queued_user_messages: VecDeque::new(),
show_welcome_banner: false,
suppress_session_configured_redraw: true,
@@ -2635,6 +2639,7 @@ impl ChatWidget {
token_info,
total_usage,
&self.thread_id,
self.forked_from,
self.rate_limit_snapshot.as_ref(),
self.plan_type,
Local::now(),
+2
View File
@@ -131,6 +131,7 @@ async fn resumed_initial_messages_render_history() {
let rollout_file = NamedTempFile::new().unwrap();
let configured = codex_core::protocol::SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
model: "test-model".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -428,6 +429,7 @@ async fn make_chatwidget_manual(
current_status_header: String::from("Working"),
retry_status_header: None,
thread_id: None,
forked_from: None,
frame_requester: FrameRequester::test_dummy(),
show_welcome_banner: true,
queued_user_messages: VecDeque::new(),
+14
View File
@@ -66,6 +66,7 @@ struct StatusHistoryCell {
model_provider: Option<String>,
account: Option<StatusAccountDisplay>,
session_id: Option<String>,
forked_from: Option<String>,
token_usage: StatusTokenUsageData,
rate_limits: StatusRateLimitData,
}
@@ -77,6 +78,7 @@ pub(crate) fn new_status_output(
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
session_id: &Option<ThreadId>,
forked_from: Option<ThreadId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
now: DateTime<Local>,
@@ -89,6 +91,7 @@ pub(crate) fn new_status_output(
token_info,
total_usage,
session_id,
forked_from,
rate_limits,
plan_type,
now,
@@ -106,6 +109,7 @@ impl StatusHistoryCell {
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
session_id: &Option<ThreadId>,
forked_from: Option<ThreadId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
now: DateTime<Local>,
@@ -134,6 +138,7 @@ impl StatusHistoryCell {
let model_provider = format_model_provider(config);
let account = compose_account_display(auth_manager, plan_type);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
let forked_from = forked_from.map(|id| id.to_string());
let default_usage = TokenUsage::default();
let (context_usage, context_window) = match token_info {
Some(info) => (&info.last_token_usage, info.model_context_window),
@@ -163,6 +168,7 @@ impl StatusHistoryCell {
model_provider,
account,
session_id,
forked_from,
token_usage,
rate_limits,
}
@@ -351,6 +357,9 @@ impl HistoryCell for StatusHistoryCell {
if self.session_id.is_some() {
push_label(&mut labels, &mut seen, "Session");
}
if self.session_id.is_some() && self.forked_from.is_some() {
push_label(&mut labels, &mut seen, "Forked from");
}
push_label(&mut labels, &mut seen, "Token usage");
if self.token_usage.context_window.is_some() {
push_label(&mut labels, &mut seen, "Context window");
@@ -403,6 +412,11 @@ impl HistoryCell for StatusHistoryCell {
if let Some(session) = self.session_id.as_ref() {
lines.push(formatter.line("Session", vec![Span::from(session.clone())]));
}
if self.session_id.is_some()
&& let Some(forked_from) = self.forked_from.as_ref()
{
lines.push(formatter.line("Forked from", vec![Span::from(forked_from.clone())]));
}
lines.push(Line::from(Vec::<Span<'static>>::new()));
// Hide token usage only for ChatGPT subscribers
@@ -0,0 +1,24 @@
---
source: tui/src/status/tests.rs
expression: sanitized
---
/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.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Approval: on-request │
│ Sandbox: read-only │
│ Agents.md: <none> │
│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │
│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
+66
View File
@@ -14,6 +14,7 @@ use codex_core::protocol::RateLimitWindow;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ReasoningEffort;
use insta::assert_snapshot;
@@ -146,6 +147,7 @@ async fn status_snapshot_includes_reasoning_details() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -161,6 +163,57 @@ async fn status_snapshot_includes_reasoning_details() {
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_includes_forked_from() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.model_provider_id = "openai".to_string();
config.cwd = PathBuf::from("/workspace/tests");
let auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
input_tokens: 800,
cached_input_tokens: 0,
output_tokens: 400,
reasoning_output_tokens: 0,
total_tokens: 1_200,
};
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 8, 9, 10, 11, 12)
.single()
.expect("valid time");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let session_id =
ThreadId::from_string("0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e").expect("session id");
let forked_from =
ThreadId::from_string("e9f18a88-8081-4e51-9d4e-8af5cde2d8dd").expect("forked id");
let composite = new_status_output(
&config,
&auth_manager,
Some(&token_info),
&usage,
&Some(session_id),
Some(forked_from),
None,
None,
captured_at,
&model_slug,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
for line in &mut rendered_lines {
*line = line.replace('\\', "/");
}
}
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_includes_monthly_limit() {
let temp_home = TempDir::new().expect("temp home");
@@ -202,6 +255,7 @@ async fn status_snapshot_includes_monthly_limit() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -246,6 +300,7 @@ async fn status_snapshot_shows_unlimited_credits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -289,6 +344,7 @@ async fn status_snapshot_shows_positive_credits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -332,6 +388,7 @@ async fn status_snapshot_hides_zero_credits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -373,6 +430,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -416,6 +474,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
&None,
None,
None,
None,
now,
&model_slug,
);
@@ -470,6 +529,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -517,6 +577,7 @@ async fn status_snapshot_shows_missing_limits_message() {
&None,
None,
None,
None,
now,
&model_slug,
);
@@ -578,6 +639,7 @@ async fn status_snapshot_includes_credits_and_limits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -629,6 +691,7 @@ async fn status_snapshot_shows_empty_limits_message() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -689,6 +752,7 @@ async fn status_snapshot_shows_stale_limits_message() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
now,
@@ -753,6 +817,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
now,
@@ -809,6 +874,7 @@ async fn status_context_window_uses_last_usage() {
&None,
None,
None,
None,
now,
&model_slug,
);
+3
View File
@@ -2627,6 +2627,7 @@ mod tests {
let make_header = |is_first| {
let event = SessionConfiguredEvent {
session_id: ThreadId::new(),
forked_from_id: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -2668,6 +2669,7 @@ mod tests {
id: String::new(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: base_id,
forked_from_id: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -2950,6 +2952,7 @@ mod tests {
let conversation_id = ThreadId::new();
let event = SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
+5
View File
@@ -383,6 +383,7 @@ pub(crate) struct ChatWidget {
// Previous status header to restore after a transient stream retry.
retry_status_header: Option<String>,
conversation_id: Option<ThreadId>,
forked_from: Option<ThreadId>,
frame_requester: FrameRequester,
// Whether to include the initial welcome banner on session configured
show_welcome_banner: bool,
@@ -522,6 +523,7 @@ impl ChatWidget {
.set_history_metadata(event.history_log_id, event.history_entry_count);
self.set_skills(None);
self.conversation_id = Some(event.session_id);
self.forked_from = event.forked_from_id;
self.current_rollout_path = Some(event.rollout_path.clone());
let initial_messages = event.initial_messages.clone();
let model_for_header = event.model.clone();
@@ -1502,6 +1504,7 @@ impl ChatWidget {
current_status_header: String::from("Working"),
retry_status_header: None,
conversation_id: None,
forked_from: None,
queued_user_messages: VecDeque::new(),
show_welcome_banner: is_first_run,
suppress_session_configured_redraw: false,
@@ -1598,6 +1601,7 @@ impl ChatWidget {
current_status_header: String::from("Working"),
retry_status_header: None,
conversation_id: None,
forked_from: None,
queued_user_messages: VecDeque::new(),
show_welcome_banner: false,
suppress_session_configured_redraw: true,
@@ -2400,6 +2404,7 @@ impl ChatWidget {
token_info,
total_usage,
&self.conversation_id,
self.forked_from,
self.rate_limit_snapshot.as_ref(),
self.plan_type,
Local::now(),
+2
View File
@@ -120,6 +120,7 @@ async fn resumed_initial_messages_render_history() {
let rollout_file = NamedTempFile::new().unwrap();
let configured = codex_core::protocol::SessionConfiguredEvent {
session_id: conversation_id,
forked_from_id: None,
model: "test-model".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
@@ -414,6 +415,7 @@ async fn make_chatwidget_manual(
current_status_header: String::from("Working"),
retry_status_header: None,
conversation_id: None,
forked_from: None,
frame_requester: FrameRequester::test_dummy(),
show_welcome_banner: true,
queued_user_messages: VecDeque::new(),
+14
View File
@@ -66,6 +66,7 @@ struct StatusHistoryCell {
model_provider: Option<String>,
account: Option<StatusAccountDisplay>,
session_id: Option<String>,
forked_from: Option<String>,
token_usage: StatusTokenUsageData,
rate_limits: StatusRateLimitData,
}
@@ -77,6 +78,7 @@ pub(crate) fn new_status_output(
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
session_id: &Option<ThreadId>,
forked_from: Option<ThreadId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
now: DateTime<Local>,
@@ -89,6 +91,7 @@ pub(crate) fn new_status_output(
token_info,
total_usage,
session_id,
forked_from,
rate_limits,
plan_type,
now,
@@ -106,6 +109,7 @@ impl StatusHistoryCell {
token_info: Option<&TokenUsageInfo>,
total_usage: &TokenUsage,
session_id: &Option<ThreadId>,
forked_from: Option<ThreadId>,
rate_limits: Option<&RateLimitSnapshotDisplay>,
plan_type: Option<PlanType>,
now: DateTime<Local>,
@@ -134,6 +138,7 @@ impl StatusHistoryCell {
let model_provider = format_model_provider(config);
let account = compose_account_display(auth_manager, plan_type);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
let forked_from = forked_from.map(|id| id.to_string());
let default_usage = TokenUsage::default();
let (context_usage, context_window) = match token_info {
Some(info) => (&info.last_token_usage, info.model_context_window),
@@ -163,6 +168,7 @@ impl StatusHistoryCell {
model_provider,
account,
session_id,
forked_from,
token_usage,
rate_limits,
}
@@ -351,6 +357,9 @@ impl HistoryCell for StatusHistoryCell {
if self.session_id.is_some() {
push_label(&mut labels, &mut seen, "Session");
}
if self.session_id.is_some() && self.forked_from.is_some() {
push_label(&mut labels, &mut seen, "Forked from");
}
push_label(&mut labels, &mut seen, "Token usage");
if self.token_usage.context_window.is_some() {
push_label(&mut labels, &mut seen, "Context window");
@@ -402,6 +411,11 @@ impl HistoryCell for StatusHistoryCell {
if let Some(session) = self.session_id.as_ref() {
lines.push(formatter.line("Session", vec![Span::from(session.clone())]));
}
if self.session_id.is_some()
&& let Some(forked_from) = self.forked_from.as_ref()
{
lines.push(formatter.line("Forked from", vec![Span::from(forked_from.clone())]));
}
lines.push(Line::from(Vec::<Span<'static>>::new()));
// Hide token usage only for ChatGPT subscribers
@@ -0,0 +1,24 @@
---
source: tui2/src/status/tests.rs
expression: sanitized
---
/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.1-codex-max (reasoning none, summaries auto) │
│ Directory: [[workspace]] │
│ Approval: on-request │
│ Sandbox: read-only │
│ Agents.md: <none> │
│ Session: 0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e │
│ Forked from: e9f18a88-8081-4e51-9d4e-8af5cde2d8dd │
│ │
│ Token usage: 1.2K total (800 input + 400 output) │
│ Context window: 100% left (1.2K used / 272K) │
│ Limits: data not available yet │
╰───────────────────────────────────────────────────────────────────────╯
+67
View File
@@ -14,6 +14,7 @@ use codex_core::protocol::RateLimitWindow;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_core::protocol::TokenUsageInfo;
use codex_protocol::ThreadId;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ReasoningEffort;
use insta::assert_snapshot;
@@ -146,6 +147,7 @@ async fn status_snapshot_includes_reasoning_details() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -160,6 +162,58 @@ async fn status_snapshot_includes_reasoning_details() {
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_includes_forked_from() {
let temp_home = TempDir::new().expect("temp home");
let mut config = test_config(&temp_home).await;
config.model = Some("gpt-5.1-codex-max".to_string());
config.model_provider_id = "openai".to_string();
config.cwd = PathBuf::from("/workspace/tests");
let auth_manager = test_auth_manager(&config);
let usage = TokenUsage {
input_tokens: 800,
cached_input_tokens: 0,
output_tokens: 400,
reasoning_output_tokens: 0,
total_tokens: 1_200,
};
let captured_at = chrono::Local
.with_ymd_and_hms(2024, 8, 9, 10, 11, 12)
.single()
.expect("valid time");
let model_slug = ModelsManager::get_model_offline(config.model.as_deref());
let token_info = token_info_for(&model_slug, &config, &usage);
let session_id =
ThreadId::from_string("0f0f3c13-6cf9-4aa4-8b80-7d49c2f1be2e").expect("session id");
let forked_from =
ThreadId::from_string("e9f18a88-8081-4e51-9d4e-8af5cde2d8dd").expect("forked id");
let composite = new_status_output(
&config,
&auth_manager,
Some(&token_info),
&usage,
&Some(session_id),
Some(forked_from),
None,
None,
captured_at,
&model_slug,
);
let mut rendered_lines = render_lines(&composite.display_lines(80));
if cfg!(windows) {
for line in &mut rendered_lines {
*line = line.replace('\\', "/");
}
}
let sanitized = sanitize_directory(rendered_lines).join("\n");
assert_snapshot!(sanitized);
}
#[tokio::test]
async fn status_snapshot_includes_monthly_limit() {
let temp_home = TempDir::new().expect("temp home");
@@ -201,6 +255,7 @@ async fn status_snapshot_includes_monthly_limit() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -244,6 +299,7 @@ async fn status_snapshot_shows_unlimited_credits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -287,6 +343,7 @@ async fn status_snapshot_shows_positive_credits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -330,6 +387,7 @@ async fn status_snapshot_hides_zero_credits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -371,6 +429,7 @@ async fn status_snapshot_hides_when_has_no_credits_flag() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -414,6 +473,7 @@ async fn status_card_token_usage_excludes_cached_tokens() {
&None,
None,
None,
None,
now,
&model_slug,
);
@@ -468,6 +528,7 @@ async fn status_snapshot_truncates_in_narrow_terminal() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -515,6 +576,7 @@ async fn status_snapshot_shows_missing_limits_message() {
&None,
None,
None,
None,
now,
&model_slug,
);
@@ -575,6 +637,7 @@ async fn status_snapshot_includes_credits_and_limits() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -626,6 +689,7 @@ async fn status_snapshot_shows_empty_limits_message() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
captured_at,
@@ -686,6 +750,7 @@ async fn status_snapshot_shows_stale_limits_message() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
now,
@@ -750,6 +815,7 @@ async fn status_snapshot_cached_limits_hide_credits_without_flag() {
Some(&token_info),
&usage,
&None,
None,
Some(&rate_display),
None,
now,
@@ -806,6 +872,7 @@ async fn status_context_window_uses_last_usage() {
&None,
None,
None,
None,
now,
&model_slug,
);