mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: /resume per ID/name (#17222)
Support `/resume 00000-0000-0000-00000000` from the TUI (equivalent for the name)
This commit is contained in:
committed by
GitHub
Unverified
parent
8f705b0702
commit
6c5471feb2
+122
-89
@@ -47,6 +47,7 @@ use crate::read_session_model;
|
||||
use crate::render::highlight::highlight_bash_to_lines;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::resume_picker::SessionSelection;
|
||||
use crate::resume_picker::SessionTarget;
|
||||
#[cfg(test)]
|
||||
use crate::test_support::PathBufExt;
|
||||
use crate::tui;
|
||||
@@ -4047,6 +4048,108 @@ impl App {
|
||||
Ok(AppRunControl::Continue)
|
||||
}
|
||||
|
||||
async fn resume_target_session(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
app_server: &mut AppServerSession,
|
||||
target_session: SessionTarget,
|
||||
) -> Result<AppRunControl> {
|
||||
if self.ignore_same_thread_resume(&target_session) {
|
||||
tui.frame_requester().schedule_frame();
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
let current_cwd = self.config.cwd.to_path_buf();
|
||||
let resume_cwd = if self.remote_app_server_url.is_some() {
|
||||
current_cwd.clone()
|
||||
} else {
|
||||
match crate::resolve_cwd_for_resume_or_fork(
|
||||
tui,
|
||||
&self.config,
|
||||
¤t_cwd,
|
||||
target_session.thread_id,
|
||||
target_session.path.as_deref(),
|
||||
CwdPromptAction::Resume,
|
||||
/*allow_prompt*/ true,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
crate::ResolveCwdOutcome::Continue(Some(cwd)) => cwd,
|
||||
crate::ResolveCwdOutcome::Continue(None) => current_cwd.clone(),
|
||||
crate::ResolveCwdOutcome::Exit => {
|
||||
return Ok(AppRunControl::Exit(ExitReason::UserRequested));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut resume_config = match self
|
||||
.rebuild_config_for_resume_or_fallback(¤t_cwd, resume_cwd)
|
||||
.await
|
||||
{
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to rebuild configuration for resume: {err}"
|
||||
));
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
};
|
||||
self.apply_runtime_policy_overrides(&mut resume_config);
|
||||
|
||||
let summary = session_summary(
|
||||
self.chat_widget.token_usage(),
|
||||
self.chat_widget.thread_id(),
|
||||
self.chat_widget.thread_name(),
|
||||
);
|
||||
match app_server
|
||||
.resume_thread(resume_config.clone(), target_session.thread_id)
|
||||
.await
|
||||
{
|
||||
Ok(resumed) => {
|
||||
self.shutdown_current_thread(app_server).await;
|
||||
self.config = resume_config;
|
||||
tui.set_notification_settings(
|
||||
self.config.tui_notifications.method,
|
||||
self.config.tui_notifications.condition,
|
||||
);
|
||||
self.file_search
|
||||
.update_search_dir(self.config.cwd.to_path_buf());
|
||||
match self
|
||||
.replace_chat_widget_with_app_server_thread(tui, app_server, resumed)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if let Some(summary) = summary {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if let Some(usage_line) = summary.usage_line {
|
||||
lines.push(usage_line.into());
|
||||
}
|
||||
if let Some(command) = summary.resume_command {
|
||||
let spans =
|
||||
vec!["To continue this session, run ".into(), command.cyan()];
|
||||
lines.push(spans.into());
|
||||
}
|
||||
self.chat_widget.add_plain_history_lines(lines);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to attach to resumed app-server thread: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let path_display = target_session.display_label();
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to resume session from {path_display}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AppRunControl::Continue)
|
||||
}
|
||||
|
||||
async fn handle_event(
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
@@ -4097,97 +4200,13 @@ impl App {
|
||||
.await?
|
||||
{
|
||||
SessionSelection::Resume(target_session) => {
|
||||
if self.ignore_same_thread_resume(&target_session) {
|
||||
tui.frame_requester().schedule_frame();
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let current_cwd = self.config.cwd.to_path_buf();
|
||||
let resume_cwd = if self.remote_app_server_url.is_some() {
|
||||
current_cwd.clone()
|
||||
} else {
|
||||
match crate::resolve_cwd_for_resume_or_fork(
|
||||
tui,
|
||||
&self.config,
|
||||
¤t_cwd,
|
||||
target_session.thread_id,
|
||||
target_session.path.as_deref(),
|
||||
CwdPromptAction::Resume,
|
||||
/*allow_prompt*/ true,
|
||||
)
|
||||
match self
|
||||
.resume_target_session(tui, app_server, target_session)
|
||||
.await?
|
||||
{
|
||||
crate::ResolveCwdOutcome::Continue(Some(cwd)) => cwd,
|
||||
crate::ResolveCwdOutcome::Continue(None) => current_cwd.clone(),
|
||||
crate::ResolveCwdOutcome::Exit => {
|
||||
return Ok(AppRunControl::Exit(ExitReason::UserRequested));
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut resume_config = match self
|
||||
.rebuild_config_for_resume_or_fallback(¤t_cwd, resume_cwd)
|
||||
.await
|
||||
{
|
||||
Ok(cfg) => cfg,
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to rebuild configuration for resume: {err}"
|
||||
));
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
};
|
||||
self.apply_runtime_policy_overrides(&mut resume_config);
|
||||
let summary = session_summary(
|
||||
self.chat_widget.token_usage(),
|
||||
self.chat_widget.thread_id(),
|
||||
self.chat_widget.thread_name(),
|
||||
);
|
||||
match app_server
|
||||
.resume_thread(resume_config.clone(), target_session.thread_id)
|
||||
.await
|
||||
{
|
||||
Ok(resumed) => {
|
||||
self.shutdown_current_thread(app_server).await;
|
||||
self.config = resume_config;
|
||||
tui.set_notification_settings(
|
||||
self.config.tui_notifications.method,
|
||||
self.config.tui_notifications.condition,
|
||||
);
|
||||
self.file_search
|
||||
.update_search_dir(self.config.cwd.to_path_buf());
|
||||
match self
|
||||
.replace_chat_widget_with_app_server_thread(
|
||||
tui, app_server, resumed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
if let Some(summary) = summary {
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
if let Some(usage_line) = summary.usage_line {
|
||||
lines.push(usage_line.into());
|
||||
}
|
||||
if let Some(command) = summary.resume_command {
|
||||
let spans = vec![
|
||||
"To continue this session, run ".into(),
|
||||
command.cyan(),
|
||||
];
|
||||
lines.push(spans.into());
|
||||
}
|
||||
self.chat_widget.add_plain_history_lines(lines);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to attach to resumed app-server thread: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let path_display = target_session.display_label();
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to resume session from {path_display}: {err}"
|
||||
));
|
||||
AppRunControl::Continue => {}
|
||||
AppRunControl::Exit(reason) => {
|
||||
return Ok(AppRunControl::Exit(reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4199,6 +4218,20 @@ impl App {
|
||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
AppEvent::ResumeSessionByIdOrName(id_or_name) => {
|
||||
match crate::lookup_session_target_with_app_server(app_server, &id_or_name).await? {
|
||||
Some(target_session) => {
|
||||
return self
|
||||
.resume_target_session(tui, app_server, target_session)
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"No saved chat found matching '{id_or_name}'."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEvent::ForkCurrentSession => {
|
||||
self.session_telemetry.counter(
|
||||
"codex.thread.fork",
|
||||
|
||||
@@ -124,6 +124,9 @@ pub(crate) enum AppEvent {
|
||||
/// Open the resume picker inside the running TUI session.
|
||||
OpenResumePicker,
|
||||
|
||||
/// Resume a thread by UUID or thread name inside the running TUI session.
|
||||
ResumeSessionByIdOrName(String),
|
||||
|
||||
/// Fork the current session into a new thread.
|
||||
ForkCurrentSession,
|
||||
|
||||
|
||||
@@ -5452,6 +5452,17 @@ impl ChatWidget {
|
||||
}));
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
SlashCommand::Resume if !trimmed.is_empty() => {
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
.prepare_inline_args_submission(/*record_history*/ false)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.app_event_tx
|
||||
.send(AppEvent::ResumeSessionByIdOrName(prepared_args));
|
||||
self.bottom_pane.drain_pending_submission_state();
|
||||
}
|
||||
SlashCommand::SandboxReadRoot if !trimmed.is_empty() => {
|
||||
let Some((prepared_args, _prepared_elements)) = self
|
||||
.bottom_pane
|
||||
|
||||
@@ -447,6 +447,24 @@ async fn slash_resume_opens_picker() {
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_resume_with_arg_requests_named_session() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.bottom_pane.set_composer_text(
|
||||
"/resume my-saved-thread".to_string(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::ResumeSessionByIdOrName(id_or_name)) if id_or_name == "my-saved-thread"
|
||||
);
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_fork_requests_current_fork() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -132,6 +132,7 @@ impl SlashCommand {
|
||||
| SlashCommand::Rename
|
||||
| SlashCommand::Plan
|
||||
| SlashCommand::Fast
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::SandboxReadRoot
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user