Skip to content
Open
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
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ pretty_assertions = { workspace = true }
rand = { workspace = true }
serial_test = { workspace = true }
vt100 = { workspace = true }
uuid = { workspace = true }
67 changes: 67 additions & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,73 @@ impl App {
}
tui.frame_requester().schedule_frame();
}
AppEvent::OpenResumePicker => {
match crate::resume_picker::run_resume_picker(
tui,
&self.config.codex_home,
&self.config.model_provider_id,
false,
)
.await?
{
ResumeSelection::Resume(path) => {
let summary = session_summary(
self.chat_widget.token_usage(),
self.chat_widget.conversation_id(),
);
match self
.server
.resume_conversation_from_rollout(
self.config.clone(),
path.clone(),
self.auth_manager.clone(),
)
.await
{
Ok(resumed) => {
self.shutdown_current_conversation().await;
let init = crate::chatwidget::ChatWidgetInit {
config: self.config.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
feedback: self.feedback.clone(),
};
self.chat_widget = ChatWidget::new_from_existing(
init,
resumed.conversation,
resumed.session_configured,
);
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().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 resume session from {}: {err}",
path.display()
));
}
}
}
ResumeSelection::Exit | ResumeSelection::StartFresh => {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this drop the CTRL-C ? Blocking the user on this screen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ctrl+c leads to exit.

}

// Leaving alt-screen may blank the inline viewport; force a redraw either way.
tui.frame_requester().schedule_frame();
}
AppEvent::InsertHistoryCell(cell) => {
let cell: Arc<dyn HistoryCell> = cell.into();
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub(crate) enum AppEvent {
/// Start a new session.
NewSession,

/// Open the resume picker inside the running TUI session.
OpenResumePicker,

/// Request to exit the application gracefully.
ExitRequest,

Expand Down
56 changes: 56 additions & 0 deletions codex-rs/tui/src/bottom_pane/chat_composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,62 @@ mod tests {
}
}

#[test]
fn slash_popup_resume_for_res_ui() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;

let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);

let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);

// Type "/res" humanlike so paste-burst doesn’t interfere.
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']);

let mut terminal = Terminal::new(TestBackend::new(60, 6)).expect("terminal");
terminal
.draw(|f| composer.render(f.area(), f.buffer_mut()))
.expect("draw composer");

// Snapshot should show /resume as the first entry for /res.
insta::assert_snapshot!("slash_popup_res", terminal.backend());
}

#[test]
fn slash_popup_resume_for_res_logic() {
use super::super::command_popup::CommandItem;
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']);

match &composer.active_popup {
ActivePopup::Command(popup) => match popup.selected_item() {
Some(CommandItem::Builtin(cmd)) => {
assert_eq!(cmd.command(), "resume")
}
Some(CommandItem::UserPrompt(_)) => {
panic!("unexpected prompt selected for '/res'")
}
None => panic!("no selected command for '/res'"),
},
_ => panic!("slash popup not active after typing '/res'"),
}
}

// Test helper: simulate human typing with a brief delay and flush the paste-burst buffer
fn type_chars_humanlike(composer: &mut ChatComposer, chars: &[char]) {
use crossterm::event::KeyCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
source: tui/src/bottom_pane/chat_composer.rs
assertion_line: 2385
expression: terminal.backend()
---
" "
"› /res "
" "
" "
" "
" /resume resume a saved chat "
3 changes: 3 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,9 @@ impl ChatWidget {
SlashCommand::New => {
self.app_event_tx.send(AppEvent::NewSession);
}
SlashCommand::Resume => {
self.app_event_tx.send(AppEvent::OpenResumePicker);
}
SlashCommand::Init => {
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
if init_target.exists() {
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,15 @@ fn slash_exit_requests_exit() {
assert_matches!(rx.try_recv(), Ok(AppEvent::ExitRequest));
}

#[test]
fn slash_resume_opens_picker() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();

chat.dispatch_command(SlashCommand::Resume);

assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker));
}

#[test]
fn slash_undo_sends_op() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
Expand Down
Loading
Loading