mirror of
https://github.com/openai/codex.git
synced 2026-05-03 21:01:55 +03:00
add slash resume (#7302)
`codex resume` isn't that discoverable. Adding it to the slash commands can help
This commit is contained in:
@@ -113,7 +113,7 @@ pub async fn run_resume_picker(
|
||||
show_all,
|
||||
filter_cwd,
|
||||
);
|
||||
state.load_initial_page().await?;
|
||||
state.start_initial_load();
|
||||
state.request_frame();
|
||||
|
||||
let mut tui_events = alt.tui.event_stream().fuse();
|
||||
@@ -359,25 +359,28 @@ impl PickerState {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn load_initial_page(&mut self) -> Result<()> {
|
||||
let provider_filter = vec![self.default_provider.clone()];
|
||||
let page = RolloutRecorder::list_conversations(
|
||||
&self.codex_home,
|
||||
PAGE_SIZE,
|
||||
None,
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
Some(provider_filter.as_slice()),
|
||||
self.default_provider.as_str(),
|
||||
)
|
||||
.await?;
|
||||
fn start_initial_load(&mut self) {
|
||||
self.reset_pagination();
|
||||
self.all_rows.clear();
|
||||
self.filtered_rows.clear();
|
||||
self.seen_paths.clear();
|
||||
self.search_state = SearchState::Idle;
|
||||
self.selected = 0;
|
||||
self.ingest_page(page);
|
||||
Ok(())
|
||||
|
||||
let request_token = self.allocate_request_token();
|
||||
self.pagination.loading = LoadingState::Pending(PendingLoad {
|
||||
request_token,
|
||||
search_token: None,
|
||||
});
|
||||
self.request_frame();
|
||||
|
||||
(self.page_loader)(PageLoadRequest {
|
||||
codex_home: self.codex_home.clone(),
|
||||
cursor: None,
|
||||
request_token,
|
||||
search_token: None,
|
||||
default_provider: self.default_provider.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_background_event(&mut self, event: BackgroundEvent) -> Result<()> {
|
||||
@@ -1260,6 +1263,166 @@ mod tests {
|
||||
assert_snapshot!("resume_picker_table", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_picker_screen_snapshot() {
|
||||
use crate::custom_terminal::Terminal;
|
||||
use crate::test_backend::VT100Backend;
|
||||
use uuid::Uuid;
|
||||
|
||||
// Create real rollout files so the snapshot uses the actual listing pipeline.
|
||||
let tempdir = tempfile::tempdir().expect("tempdir");
|
||||
let sessions_root = tempdir.path().join("sessions");
|
||||
std::fs::create_dir_all(&sessions_root).expect("mkdir sessions root");
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
// Helper to write a rollout file with minimal meta + one user message.
|
||||
let write_rollout = |ts: DateTime<Utc>, cwd: &str, branch: &str, preview: &str| {
|
||||
let dir = sessions_root
|
||||
.join(ts.format("%Y").to_string())
|
||||
.join(ts.format("%m").to_string())
|
||||
.join(ts.format("%d").to_string());
|
||||
std::fs::create_dir_all(&dir).expect("mkdir date dirs");
|
||||
let filename = format!(
|
||||
"rollout-{}-{}.jsonl",
|
||||
ts.format("%Y-%m-%dT%H-%M-%S"),
|
||||
Uuid::new_v4()
|
||||
);
|
||||
let path = dir.join(filename);
|
||||
let meta = serde_json::json!({
|
||||
"timestamp": ts.to_rfc3339(),
|
||||
"item": {
|
||||
"SessionMeta": {
|
||||
"meta": {
|
||||
"id": Uuid::new_v4(),
|
||||
"timestamp": ts.to_rfc3339(),
|
||||
"cwd": cwd,
|
||||
"originator": "user",
|
||||
"cli_version": "0.0.0",
|
||||
"instructions": null,
|
||||
"source": "Cli",
|
||||
"model_provider": "openai",
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let user = serde_json::json!({
|
||||
"timestamp": ts.to_rfc3339(),
|
||||
"item": {
|
||||
"EventMsg": {
|
||||
"UserMessage": {
|
||||
"message": preview,
|
||||
"images": null
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let branch_meta = serde_json::json!({
|
||||
"timestamp": ts.to_rfc3339(),
|
||||
"item": {
|
||||
"EventMsg": {
|
||||
"SessionMeta": {
|
||||
"meta": {
|
||||
"git_branch": branch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
std::fs::write(&path, format!("{meta}\n{user}\n{branch_meta}\n"))
|
||||
.expect("write rollout");
|
||||
};
|
||||
|
||||
write_rollout(
|
||||
now - Duration::seconds(42),
|
||||
"/tmp/project",
|
||||
"feature/resume",
|
||||
"Fix resume picker timestamps",
|
||||
);
|
||||
write_rollout(
|
||||
now - Duration::minutes(35),
|
||||
"/tmp/other",
|
||||
"main",
|
||||
"Investigate lazy pagination cap",
|
||||
);
|
||||
|
||||
let loader: PageLoader = Arc::new(|_| {});
|
||||
let mut state = PickerState::new(
|
||||
PathBuf::from("/tmp"),
|
||||
FrameRequester::test_dummy(),
|
||||
loader,
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
);
|
||||
|
||||
let page = block_on_future(RolloutRecorder::list_conversations(
|
||||
&state.codex_home,
|
||||
PAGE_SIZE,
|
||||
None,
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
Some(&[String::from("openai")]),
|
||||
"openai",
|
||||
))
|
||||
.expect("list conversations");
|
||||
|
||||
let rows = rows_from_items(page.items);
|
||||
state.all_rows = rows.clone();
|
||||
state.filtered_rows = rows;
|
||||
state.view_rows = Some(4);
|
||||
state.selected = 0;
|
||||
state.scroll_top = 0;
|
||||
state.update_view_rows(4);
|
||||
|
||||
let metrics = calculate_column_metrics(&state.filtered_rows, state.show_all);
|
||||
|
||||
let width: u16 = 80;
|
||||
let height: u16 = 9;
|
||||
let backend = VT100Backend::new(width, height);
|
||||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||||
terminal.set_viewport_area(Rect::new(0, 0, width, height));
|
||||
|
||||
{
|
||||
let mut frame = terminal.get_frame();
|
||||
let area = frame.area();
|
||||
let [header, search, columns, list, hint] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(area.height.saturating_sub(4)),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
frame.render_widget_ref(
|
||||
Line::from(vec!["Resume a previous session".bold().cyan()]),
|
||||
header,
|
||||
);
|
||||
|
||||
frame.render_widget_ref(Line::from("Type to search".dim()), search);
|
||||
|
||||
render_column_headers(&mut frame, columns, &metrics);
|
||||
render_list(&mut frame, list, &state, &metrics);
|
||||
|
||||
let hint_line: Line = vec![
|
||||
key_hint::plain(KeyCode::Enter).into(),
|
||||
" to resume ".dim(),
|
||||
" ".dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to start new ".dim(),
|
||||
" ".dim(),
|
||||
key_hint::ctrl(KeyCode::Char('c')).into(),
|
||||
" to quit ".dim(),
|
||||
]
|
||||
.into();
|
||||
frame.render_widget_ref(hint_line, hint);
|
||||
}
|
||||
terminal.flush().expect("flush");
|
||||
|
||||
let snapshot = terminal.backend().to_string();
|
||||
assert_snapshot!("resume_picker_screen", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pageless_scrolling_deduplicates_and_keeps_order() {
|
||||
let loader: PageLoader = Arc::new(|_| {});
|
||||
|
||||
Reference in New Issue
Block a user