mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Clarify resume guidance in exit summaries (#41938)
## What changed - Show the exact `codex resume <thread-id>` command on its own indented line. - When a thread has a name, also explain that users can run `codex resume` and select the named thread from the picker. - Apply command highlighting to both resume options when color is enabled. ## Testing - Cover unnamed, empty-name, named, colored, and embedded-session exit summaries. GitOrigin-RevId: d97c04db8a34376c6d9ea839b515b04a384b85d1
This commit is contained in:
1 parent
115ffaf8bf
commit
0e03f88a30
5 files changed
+81
-39
No files matched your search
+44
-19
@@ -3735,7 +3735,10 @@ mod tests {
|
|||||||
AppExitInfo {
|
AppExitInfo {
|
||||||
token_usage,
|
token_usage,
|
||||||
thread_id,
|
thread_id,
|
||||||
resume_hint: codex_utils_cli::resume_hint(thread_name, thread_id),
|
resume_hint: thread_id.map(|thread_id| codex_tui::ResumableThread {
|
||||||
|
thread_id,
|
||||||
|
thread_name: thread_name.map(str::to_string),
|
||||||
|
}),
|
||||||
disconnect_info: None,
|
disconnect_info: None,
|
||||||
update_action: None,
|
update_action: None,
|
||||||
exit_reason: ExitReason::UserRequested,
|
exit_reason: ExitReason::UserRequested,
|
||||||
@@ -3844,27 +3847,26 @@ mod tests {
|
|||||||
lines,
|
lines,
|
||||||
vec![
|
vec![
|
||||||
"Token usage: total=2 input=0 output=2".to_string(),
|
"Token usage: total=2 input=0 output=2".to_string(),
|
||||||
"To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000"
|
"To continue this session, run:".to_string(),
|
||||||
.to_string(),
|
" codex resume 123e4567-e89b-12d3-a456-426614174000".to_string(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_exit_messages_includes_resume_hint_without_color() {
|
fn format_exit_messages_includes_resume_hint_without_color() {
|
||||||
let exit_info = sample_exit_info(
|
insta::allow_duplicates! {
|
||||||
Some("123e4567-e89b-12d3-a456-426614174000"),
|
for thread_name in [None, Some("")] {
|
||||||
/*thread_name*/ None,
|
let exit_info =
|
||||||
);
|
sample_exit_info(Some("123e4567-e89b-12d3-a456-426614174000"), thread_name);
|
||||||
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
|
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
|
||||||
assert_eq!(
|
insta::assert_snapshot!(lines.join("\n"), @"
|
||||||
lines,
|
Token usage: total=2 input=0 output=2
|
||||||
vec![
|
To continue this session, run:
|
||||||
"Token usage: total=2 input=0 output=2".to_string(),
|
codex resume 123e4567-e89b-12d3-a456-426614174000
|
||||||
"To continue this session, run codex resume 123e4567-e89b-12d3-a456-426614174000"
|
");
|
||||||
.to_string(),
|
}
|
||||||
]
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3874,8 +3876,14 @@ mod tests {
|
|||||||
/*thread_name*/ None,
|
/*thread_name*/ None,
|
||||||
);
|
);
|
||||||
let lines = exit_info.format_exit_messages(/*color_enabled*/ true);
|
let lines = exit_info.format_exit_messages(/*color_enabled*/ true);
|
||||||
assert_eq!(lines.len(), 2);
|
assert_eq!(
|
||||||
assert!(lines[1].contains("\u{1b}[36m"));
|
lines,
|
||||||
|
vec![
|
||||||
|
"Token usage: total=2 input=0 output=2",
|
||||||
|
"To continue this session, run:",
|
||||||
|
" \u{1b}[36mcodex resume 123e4567-e89b-12d3-a456-426614174000\u{1b}[39m",
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3885,11 +3893,28 @@ mod tests {
|
|||||||
Some("my-thread"),
|
Some("my-thread"),
|
||||||
);
|
);
|
||||||
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
|
let lines = exit_info.format_exit_messages(/*color_enabled*/ false);
|
||||||
|
insta::assert_snapshot!(lines.join("\n"), @"
|
||||||
|
Token usage: total=2 input=0 output=2
|
||||||
|
To continue this session, run:
|
||||||
|
codex resume 123e4567-e89b-12d3-a456-426614174000
|
||||||
|
Or run codex resume and select my-thread.
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn format_exit_messages_colors_commands_and_thread_name() {
|
||||||
|
let exit_info = sample_exit_info(
|
||||||
|
Some("123e4567-e89b-12d3-a456-426614174000"),
|
||||||
|
Some("my-thread"),
|
||||||
|
);
|
||||||
|
let lines = exit_info.format_exit_messages(/*color_enabled*/ true);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
lines,
|
lines,
|
||||||
vec![
|
vec![
|
||||||
"Token usage: total=2 input=0 output=2".to_string(),
|
"Token usage: total=2 input=0 output=2",
|
||||||
"To continue this session, run codex resume, then select my-thread (123e4567-e89b-12d3-a456-426614174000)".to_string(),
|
"To continue this session, run:",
|
||||||
|
" \u{1b}[36mcodex resume 123e4567-e89b-12d3-a456-426614174000\u{1b}[39m",
|
||||||
|
"Or run \u{1b}[36mcodex resume\u{1b}[39m and select \u{1b}[36mmy-thread\u{1b}[39m.",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ const COMMIT_ANIMATION_TICK: Duration = tui::TARGET_FRAME_INTERVAL;
|
|||||||
pub struct AppExitInfo {
|
pub struct AppExitInfo {
|
||||||
pub token_usage: TokenUsage,
|
pub token_usage: TokenUsage,
|
||||||
pub thread_id: Option<ThreadId>,
|
pub thread_id: Option<ThreadId>,
|
||||||
pub resume_hint: Option<String>,
|
pub resume_hint: Option<ResumableThread>,
|
||||||
pub disconnect_info: Option<DisconnectInfo>,
|
pub disconnect_info: Option<DisconnectInfo>,
|
||||||
pub update_action: Option<UpdateAction>,
|
pub update_action: Option<UpdateAction>,
|
||||||
pub exit_reason: ExitReason,
|
pub exit_reason: ExitReason,
|
||||||
@@ -443,6 +443,7 @@ impl AppExitInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub use exit_summary::DisconnectInfo;
|
pub use exit_summary::DisconnectInfo;
|
||||||
|
pub use exit_summary::ResumableThread;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum AppRunControl {
|
pub(crate) enum AppRunControl {
|
||||||
@@ -479,12 +480,6 @@ fn session_summary(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
struct ResumableThread {
|
|
||||||
thread_id: ThreadId,
|
|
||||||
thread_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resumable_thread(
|
fn resumable_thread(
|
||||||
thread_id: Option<ThreadId>,
|
thread_id: Option<ThreadId>,
|
||||||
thread_name: Option<String>,
|
thread_name: Option<String>,
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
//! Exit output distinguishes a disconnected client from a stopped embedded server.
|
//! Exit output distinguishes a disconnected client from a stopped embedded server.
|
||||||
|
//! Persisted sessions offer a direct UUID command and an optional named picker hint.
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::RemoteAppServerEndpoint;
|
use crate::RemoteAppServerEndpoint;
|
||||||
use crate::exec_command::escape_command;
|
use crate::exec_command::escape_command;
|
||||||
use crate::status::remote_connection::sanitized_websocket_url;
|
use crate::status::remote_connection::sanitized_websocket_url;
|
||||||
|
|
||||||
|
/// A persisted thread that can be resumed after the TUI exits.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ResumableThread {
|
||||||
|
pub thread_id: ThreadId,
|
||||||
|
pub thread_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Reconnection and stop guidance for a task owned by a persistent app server.
|
/// Reconnection and stop guidance for a task owned by a persistent app server.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct DisconnectInfo {
|
pub struct DisconnectInfo {
|
||||||
@@ -57,7 +65,7 @@ impl App {
|
|||||||
AppExitInfo {
|
AppExitInfo {
|
||||||
token_usage: self.token_usage(),
|
token_usage: self.token_usage(),
|
||||||
thread_id,
|
thread_id,
|
||||||
resume_hint: resume_hint_for_resumable_thread(
|
resume_hint: resumable_thread(
|
||||||
thread_id,
|
thread_id,
|
||||||
self.chat_widget.thread_name(),
|
self.chat_widget.thread_name(),
|
||||||
self.chat_widget.rollout_path().as_deref(),
|
self.chat_widget.rollout_path().as_deref(),
|
||||||
@@ -125,11 +133,19 @@ impl AppExitInfo {
|
|||||||
}
|
}
|
||||||
if let ExitReason::Archived(thread_id) = self.exit_reason {
|
if let ExitReason::Archived(thread_id) = self.exit_reason {
|
||||||
lines.push(format!("Session archived: {thread_id}"));
|
lines.push(format!("Session archived: {thread_id}"));
|
||||||
} else if let Some(resume_command) = self.resume_hint {
|
} else if let Some(thread) = self.resume_hint {
|
||||||
|
lines.push("To continue this session, run:".to_string());
|
||||||
lines.push(format!(
|
lines.push(format!(
|
||||||
"To continue this session, run {}",
|
" {}",
|
||||||
color_command(resume_command),
|
color_command(format!("codex resume {}", thread.thread_id)),
|
||||||
));
|
));
|
||||||
|
if let Some(thread_name) = thread.thread_name.filter(|name| !name.is_empty()) {
|
||||||
|
lines.push(format!(
|
||||||
|
"Or run {} and select {}.",
|
||||||
|
color_command("codex resume".to_string()),
|
||||||
|
color_command(thread_name),
|
||||||
|
));
|
||||||
|
}
|
||||||
} else if let Some(thread_id) = self.thread_id {
|
} else if let Some(thread_id) = self.thread_id {
|
||||||
lines.push(format!("Session ID: {thread_id}"));
|
lines.push(format!("Session ID: {thread_id}"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ async fn remote_disconnect_exit_summary_does_not_require_a_local_rollout_or_prin
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn embedded_exit_keeps_the_session_summary() {
|
async fn embedded_exit_keeps_the_session_summary() {
|
||||||
let (mut app, _, _) = make_test_app_with_channels().await;
|
let (mut app, _, _) = make_test_app_with_channels().await;
|
||||||
prepare_local_daemon_thread(&mut app).unwrap();
|
let thread_id = prepare_local_daemon_thread(&mut app).unwrap();
|
||||||
app.app_server_target = AppServerTarget::Embedded;
|
app.app_server_target = AppServerTarget::Embedded;
|
||||||
let mut exit_info = app.exit_info(ExitReason::UserRequested);
|
let mut exit_info = app.exit_info(ExitReason::UserRequested);
|
||||||
exit_info.token_usage = TokenUsage {
|
exit_info.token_usage = TokenUsage {
|
||||||
@@ -60,14 +60,19 @@ async fn embedded_exit_keeps_the_session_summary() {
|
|||||||
total_tokens: 2,
|
total_tokens: 2,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
exit_info.resume_hint = Some("codex resume THREAD_ID".to_string());
|
exit_info.resume_hint = Some(ResumableThread {
|
||||||
assert_eq!(
|
thread_id,
|
||||||
exit_info.format_exit_messages(/*color_enabled*/ false),
|
thread_name: None,
|
||||||
vec![
|
});
|
||||||
"Token usage: total=2 input=0 output=2",
|
let output = exit_info
|
||||||
"To continue this session, run codex resume THREAD_ID",
|
.format_exit_messages(/*color_enabled*/ false)
|
||||||
]
|
.join("\n")
|
||||||
);
|
.replace(&thread_id.to_string(), "THREAD_ID");
|
||||||
|
assert_snapshot!(output, @"
|
||||||
|
Token usage: total=2 input=0 output=2
|
||||||
|
To continue this session, run:
|
||||||
|
codex resume THREAD_ID
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prepare_local_daemon_thread(app: &mut App) -> Result<ThreadId> {
|
fn prepare_local_daemon_thread(app: &mut App) -> Result<ThreadId> {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use app::App;
|
|||||||
pub use app::AppExitInfo;
|
pub use app::AppExitInfo;
|
||||||
pub use app::DisconnectInfo;
|
pub use app::DisconnectInfo;
|
||||||
pub use app::ExitReason;
|
pub use app::ExitReason;
|
||||||
|
pub use app::ResumableThread;
|
||||||
use app_server_session::AppServerSession;
|
use app_server_session::AppServerSession;
|
||||||
use app_server_session::ThreadParamsMode;
|
use app_server_session::ThreadParamsMode;
|
||||||
use codex_app_server_client::AppServerClient;
|
use codex_app_server_client::AppServerClient;
|
||||||
|
|||||||
Reference in new issue
Block a user