mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat: wire fork to codex cli (#8994)
## Summary - add `codex fork` subcommand and `/fork` slash command mirroring resume - extend session picker to support fork/resume actions with dynamic labels in tui/tui2 - wire fork selection flow through tui bootstraps and add fork-related tests
This commit is contained in:
1 parent
898e5f82f0
commit
4223948cf5
17 files changed
+754
-124
No files matched your search
+104
-10
@@ -19,7 +19,7 @@ use crate::model_migration::run_model_migration_prompt;
|
||||
use crate::pager_overlay::Overlay;
|
||||
use crate::render::highlight::highlight_bash_to_lines;
|
||||
use crate::render::renderable::Renderable;
|
||||
use crate::resume_picker::ResumeSelection;
|
||||
use crate::resume_picker::SessionSelection;
|
||||
use crate::transcript_copy_action::TranscriptCopyAction;
|
||||
use crate::transcript_copy_action::TranscriptCopyFeedback;
|
||||
use crate::transcript_copy_ui::TranscriptCopyUi;
|
||||
@@ -403,7 +403,7 @@ impl App {
|
||||
active_profile: Option<String>,
|
||||
initial_prompt: Option<String>,
|
||||
initial_images: Vec<PathBuf>,
|
||||
resume_selection: ResumeSelection,
|
||||
session_selection: SessionSelection,
|
||||
feedback: codex_feedback::CodexFeedback,
|
||||
is_first_run: bool,
|
||||
) -> Result<AppExitInfo> {
|
||||
@@ -436,8 +436,8 @@ impl App {
|
||||
}
|
||||
|
||||
let enhanced_keys_supported = tui.enhanced_keys_supported();
|
||||
let mut chat_widget = match resume_selection {
|
||||
ResumeSelection::StartFresh | ResumeSelection::Exit => {
|
||||
let mut chat_widget = match session_selection {
|
||||
SessionSelection::StartFresh | SessionSelection::Exit => {
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
@@ -453,12 +453,13 @@ impl App {
|
||||
};
|
||||
ChatWidget::new(init, thread_manager.clone())
|
||||
}
|
||||
ResumeSelection::Resume(path) => {
|
||||
SessionSelection::Resume(path) => {
|
||||
let resumed = thread_manager
|
||||
.resume_thread_from_rollout(config.clone(), path.clone(), auth_manager.clone())
|
||||
.await
|
||||
.wrap_err_with(|| {
|
||||
format!("Failed to resume session from {}", path.display())
|
||||
let path_display = path.display();
|
||||
format!("Failed to resume session from {path_display}")
|
||||
})?;
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: config.clone(),
|
||||
@@ -475,6 +476,29 @@ impl App {
|
||||
};
|
||||
ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured)
|
||||
}
|
||||
SessionSelection::Fork(path) => {
|
||||
let forked = thread_manager
|
||||
.fork_thread(usize::MAX, config.clone(), path.clone())
|
||||
.await
|
||||
.wrap_err_with(|| {
|
||||
let path_display = path.display();
|
||||
format!("Failed to fork session from {path_display}")
|
||||
})?;
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: config.clone(),
|
||||
frame_requester: tui.frame_requester(),
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
initial_prompt: initial_prompt.clone(),
|
||||
initial_images: initial_images.clone(),
|
||||
enhanced_keys_supported,
|
||||
auth_manager: auth_manager.clone(),
|
||||
models_manager: thread_manager.get_models_manager(),
|
||||
feedback: feedback.clone(),
|
||||
is_first_run,
|
||||
model: model.clone(),
|
||||
};
|
||||
ChatWidget::new_from_existing(init, forked.thread, forked.session_configured)
|
||||
}
|
||||
};
|
||||
|
||||
chat_widget.maybe_prompt_windows_sandbox_enable();
|
||||
@@ -1390,7 +1414,7 @@ impl App {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
ResumeSelection::Resume(path) => {
|
||||
SessionSelection::Resume(path) => {
|
||||
let summary = session_summary(
|
||||
self.chat_widget.token_usage(),
|
||||
self.chat_widget.conversation_id(),
|
||||
@@ -1438,14 +1462,84 @@ impl App {
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let path_display = path.display();
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to resume session from {}: {err}",
|
||||
path.display()
|
||||
"Failed to resume session from {path_display}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ResumeSelection::Exit | ResumeSelection::StartFresh => {}
|
||||
SessionSelection::Exit
|
||||
| SessionSelection::StartFresh
|
||||
| SessionSelection::Fork(_) => {}
|
||||
}
|
||||
|
||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||
tui.frame_requester().schedule_frame();
|
||||
}
|
||||
AppEvent::OpenForkPicker => {
|
||||
match crate::resume_picker::run_fork_picker(
|
||||
tui,
|
||||
&self.config.codex_home,
|
||||
&self.config.model_provider_id,
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
SessionSelection::Fork(path) => {
|
||||
let summary = session_summary(
|
||||
self.chat_widget.token_usage(),
|
||||
self.chat_widget.conversation_id(),
|
||||
);
|
||||
match self
|
||||
.server
|
||||
.fork_thread(usize::MAX, self.config.clone(), path.clone())
|
||||
.await
|
||||
{
|
||||
Ok(forked) => {
|
||||
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(),
|
||||
models_manager: self.server.get_models_manager(),
|
||||
feedback: self.feedback.clone(),
|
||||
is_first_run: false,
|
||||
model: self.current_model.clone(),
|
||||
};
|
||||
self.chat_widget = ChatWidget::new_from_existing(
|
||||
init,
|
||||
forked.thread,
|
||||
forked.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) => {
|
||||
let path_display = path.display();
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to fork session from {path_display}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
SessionSelection::Exit
|
||||
| SessionSelection::StartFresh
|
||||
| SessionSelection::Resume(_) => {}
|
||||
}
|
||||
|
||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||
|
||||
@@ -38,6 +38,9 @@ pub(crate) enum AppEvent {
|
||||
/// Open the resume picker inside the running TUI session.
|
||||
OpenResumePicker,
|
||||
|
||||
/// Open the fork picker inside the running TUI session.
|
||||
OpenForkPicker,
|
||||
|
||||
/// Request to exit the application gracefully.
|
||||
ExitRequest,
|
||||
|
||||
|
||||
@@ -1547,6 +1547,9 @@ impl ChatWidget {
|
||||
SlashCommand::Resume => {
|
||||
self.app_event_tx.send(AppEvent::OpenResumePicker);
|
||||
}
|
||||
SlashCommand::Fork => {
|
||||
self.app_event_tx.send(AppEvent::OpenForkPicker);
|
||||
}
|
||||
SlashCommand::Init => {
|
||||
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
|
||||
if init_target.exists() {
|
||||
|
||||
@@ -1302,6 +1302,15 @@ async fn slash_resume_opens_picker() {
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_fork_opens_picker() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Fork);
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_rollout_displays_current_path() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||
|
||||
@@ -32,6 +32,23 @@ pub struct Cli {
|
||||
#[clap(skip)]
|
||||
pub resume_show_all: bool,
|
||||
|
||||
// Internal controls set by the top-level `codex fork` subcommand.
|
||||
// These are not exposed as user flags on the base `codex` command.
|
||||
#[clap(skip)]
|
||||
pub fork_picker: bool,
|
||||
|
||||
#[clap(skip)]
|
||||
pub fork_last: bool,
|
||||
|
||||
/// Internal: fork a specific recorded session by id (UUID). Set by the
|
||||
/// top-level `codex fork <SESSION_ID>` wrapper; not exposed as a public flag.
|
||||
#[clap(skip)]
|
||||
pub fork_session_id: Option<String>,
|
||||
|
||||
/// Internal: show all sessions (disables cwd filtering and shows CWD column).
|
||||
#[clap(skip)]
|
||||
pub fork_show_all: bool,
|
||||
|
||||
/// Model the agent should use.
|
||||
#[arg(long, short = 'm')]
|
||||
pub model: Option<String>,
|
||||
@@ -103,6 +120,10 @@ impl From<codex_tui::Cli> for Cli {
|
||||
resume_last: cli.resume_last,
|
||||
resume_session_id: cli.resume_session_id,
|
||||
resume_show_all: cli.resume_show_all,
|
||||
fork_picker: cli.fork_picker,
|
||||
fork_last: cli.fork_last,
|
||||
fork_session_id: cli.fork_session_id,
|
||||
fork_show_all: cli.fork_show_all,
|
||||
model: cli.model,
|
||||
oss: cli.oss,
|
||||
oss_provider: cli.oss_provider,
|
||||
|
||||
+78
-27
@@ -451,28 +451,79 @@ async fn run_ratatui_app(
|
||||
initial_config
|
||||
};
|
||||
|
||||
// Determine resume behavior: explicit id, then resume last, then picker.
|
||||
let resume_selection = if let Some(id_str) = cli.resume_session_id.as_deref() {
|
||||
match find_thread_path_by_id_str(&config.codex_home, id_str).await? {
|
||||
Some(path) => resume_picker::ResumeSelection::Resume(path),
|
||||
None => {
|
||||
error!("Error finding conversation path: {id_str}");
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
let _ = tui.terminal.clear();
|
||||
if let Err(err) = writeln!(
|
||||
std::io::stdout(),
|
||||
"No saved session found with ID {id_str}. Run `codex resume` without an ID to choose from existing sessions."
|
||||
) {
|
||||
error!("Failed to write resume error message: {err}");
|
||||
}
|
||||
return Ok(AppExitInfo {
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
let mut missing_session_exit = |id_str: &str, action: &str| {
|
||||
error!("Error finding conversation path: {id_str}");
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
let _ = tui.terminal.clear();
|
||||
if let Err(err) = writeln!(
|
||||
std::io::stdout(),
|
||||
"No saved session found with ID {id_str}. Run `codex {action}` without an ID to choose from existing sessions."
|
||||
) {
|
||||
error!("Failed to write session error message: {err}");
|
||||
}
|
||||
Ok(AppExitInfo {
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
session_lines: Vec::new(),
|
||||
})
|
||||
};
|
||||
|
||||
let use_fork = cli.fork_picker || cli.fork_last || cli.fork_session_id.is_some();
|
||||
let session_selection = if use_fork {
|
||||
if let Some(id_str) = cli.fork_session_id.as_deref() {
|
||||
match find_thread_path_by_id_str(&config.codex_home, id_str).await? {
|
||||
Some(path) => resume_picker::SessionSelection::Fork(path),
|
||||
None => return missing_session_exit(id_str, "fork"),
|
||||
}
|
||||
} else if cli.fork_last {
|
||||
let provider_filter = vec![config.model_provider_id.clone()];
|
||||
match RolloutRecorder::list_threads(
|
||||
&config.codex_home,
|
||||
1,
|
||||
None,
|
||||
INTERACTIVE_SESSION_SOURCES,
|
||||
Some(provider_filter.as_slice()),
|
||||
&config.model_provider_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(page) => page
|
||||
.items
|
||||
.first()
|
||||
.map(|it| resume_picker::SessionSelection::Fork(it.path.clone()))
|
||||
.unwrap_or(resume_picker::SessionSelection::StartFresh),
|
||||
Err(_) => resume_picker::SessionSelection::StartFresh,
|
||||
}
|
||||
} else if cli.fork_picker {
|
||||
match resume_picker::run_fork_picker(
|
||||
&mut tui,
|
||||
&config.codex_home,
|
||||
&config.model_provider_id,
|
||||
cli.fork_show_all,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
resume_picker::SessionSelection::Exit => {
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
return Ok(AppExitInfo {
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
resume_picker::SessionSelection::StartFresh
|
||||
}
|
||||
} else if let Some(id_str) = cli.resume_session_id.as_deref() {
|
||||
match find_thread_path_by_id_str(&config.codex_home, id_str).await? {
|
||||
Some(path) => resume_picker::SessionSelection::Resume(path),
|
||||
None => return missing_session_exit(id_str, "resume"),
|
||||
}
|
||||
} else if cli.resume_last {
|
||||
let provider_filter = vec![config.model_provider_id.clone()];
|
||||
@@ -489,9 +540,9 @@ async fn run_ratatui_app(
|
||||
Ok(page) => page
|
||||
.items
|
||||
.first()
|
||||
.map(|it| resume_picker::ResumeSelection::Resume(it.path.clone()))
|
||||
.unwrap_or(resume_picker::ResumeSelection::StartFresh),
|
||||
Err(_) => resume_picker::ResumeSelection::StartFresh,
|
||||
.map(|it| resume_picker::SessionSelection::Resume(it.path.clone()))
|
||||
.unwrap_or(resume_picker::SessionSelection::StartFresh),
|
||||
Err(_) => resume_picker::SessionSelection::StartFresh,
|
||||
}
|
||||
} else if cli.resume_picker {
|
||||
match resume_picker::run_resume_picker(
|
||||
@@ -502,7 +553,7 @@ async fn run_ratatui_app(
|
||||
)
|
||||
.await?
|
||||
{
|
||||
resume_picker::ResumeSelection::Exit => {
|
||||
resume_picker::SessionSelection::Exit => {
|
||||
restore();
|
||||
session_log::log_session_end();
|
||||
return Ok(AppExitInfo {
|
||||
@@ -515,7 +566,7 @@ async fn run_ratatui_app(
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
resume_picker::ResumeSelection::StartFresh
|
||||
resume_picker::SessionSelection::StartFresh
|
||||
};
|
||||
|
||||
let Cli {
|
||||
@@ -560,7 +611,7 @@ async fn run_ratatui_app(
|
||||
active_profile,
|
||||
prompt,
|
||||
images,
|
||||
resume_selection,
|
||||
session_selection,
|
||||
feedback,
|
||||
should_show_trust_screen, // Proxy to: is it a first run in this directory?
|
||||
)
|
||||
|
||||
@@ -40,12 +40,42 @@ const PAGE_SIZE: usize = 25;
|
||||
const LOAD_NEAR_THRESHOLD: usize = 5;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ResumeSelection {
|
||||
pub enum SessionSelection {
|
||||
StartFresh,
|
||||
Resume(PathBuf),
|
||||
Fork(PathBuf),
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SessionPickerAction {
|
||||
Resume,
|
||||
Fork,
|
||||
}
|
||||
|
||||
impl SessionPickerAction {
|
||||
fn title(self) -> &'static str {
|
||||
match self {
|
||||
SessionPickerAction::Resume => "Resume a previous session",
|
||||
SessionPickerAction::Fork => "Fork a previous session",
|
||||
}
|
||||
}
|
||||
|
||||
fn action_label(self) -> &'static str {
|
||||
match self {
|
||||
SessionPickerAction::Resume => "resume",
|
||||
SessionPickerAction::Fork => "fork",
|
||||
}
|
||||
}
|
||||
|
||||
fn selection(self, path: PathBuf) -> SessionSelection {
|
||||
match self {
|
||||
SessionPickerAction::Resume => SessionSelection::Resume(path),
|
||||
SessionPickerAction::Fork => SessionSelection::Fork(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PageLoadRequest {
|
||||
codex_home: PathBuf,
|
||||
@@ -73,7 +103,40 @@ pub async fn run_resume_picker(
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
show_all: bool,
|
||||
) -> Result<ResumeSelection> {
|
||||
) -> Result<SessionSelection> {
|
||||
run_session_picker(
|
||||
tui,
|
||||
codex_home,
|
||||
default_provider,
|
||||
show_all,
|
||||
SessionPickerAction::Resume,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run_fork_picker(
|
||||
tui: &mut Tui,
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
show_all: bool,
|
||||
) -> Result<SessionSelection> {
|
||||
run_session_picker(
|
||||
tui,
|
||||
codex_home,
|
||||
default_provider,
|
||||
show_all,
|
||||
SessionPickerAction::Fork,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn run_session_picker(
|
||||
tui: &mut Tui,
|
||||
codex_home: &Path,
|
||||
default_provider: &str,
|
||||
show_all: bool,
|
||||
action: SessionPickerAction,
|
||||
) -> Result<SessionSelection> {
|
||||
let alt = AltScreenGuard::enter(tui);
|
||||
let (bg_tx, bg_rx) = mpsc::unbounded_channel();
|
||||
|
||||
@@ -113,6 +176,7 @@ pub async fn run_resume_picker(
|
||||
default_provider.clone(),
|
||||
show_all,
|
||||
filter_cwd,
|
||||
action,
|
||||
);
|
||||
state.start_initial_load();
|
||||
state.request_frame();
|
||||
@@ -151,7 +215,7 @@ pub async fn run_resume_picker(
|
||||
}
|
||||
|
||||
// Fallback – treat as cancel/new
|
||||
Ok(ResumeSelection::StartFresh)
|
||||
Ok(SessionSelection::StartFresh)
|
||||
}
|
||||
|
||||
/// RAII guard that ensures we leave the alt-screen on scope exit.
|
||||
@@ -190,6 +254,7 @@ struct PickerState {
|
||||
default_provider: String,
|
||||
show_all: bool,
|
||||
filter_cwd: Option<PathBuf>,
|
||||
action: SessionPickerAction,
|
||||
}
|
||||
|
||||
struct PaginationState {
|
||||
@@ -259,6 +324,7 @@ impl PickerState {
|
||||
default_provider: String,
|
||||
show_all: bool,
|
||||
filter_cwd: Option<PathBuf>,
|
||||
action: SessionPickerAction,
|
||||
) -> Self {
|
||||
Self {
|
||||
codex_home,
|
||||
@@ -283,6 +349,7 @@ impl PickerState {
|
||||
default_provider,
|
||||
show_all,
|
||||
filter_cwd,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,19 +357,19 @@ impl PickerState {
|
||||
self.requester.schedule_frame();
|
||||
}
|
||||
|
||||
async fn handle_key(&mut self, key: KeyEvent) -> Result<Option<ResumeSelection>> {
|
||||
async fn handle_key(&mut self, key: KeyEvent) -> Result<Option<SessionSelection>> {
|
||||
match key.code {
|
||||
KeyCode::Esc => return Ok(Some(ResumeSelection::StartFresh)),
|
||||
KeyCode::Esc => return Ok(Some(SessionSelection::StartFresh)),
|
||||
KeyCode::Char('c')
|
||||
if key
|
||||
.modifiers
|
||||
.contains(crossterm::event::KeyModifiers::CONTROL) =>
|
||||
{
|
||||
return Ok(Some(ResumeSelection::Exit));
|
||||
return Ok(Some(SessionSelection::Exit));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if let Some(row) = self.filtered_rows.get(self.selected) {
|
||||
return Ok(Some(ResumeSelection::Resume(row.path.clone())));
|
||||
return Ok(Some(self.action.selection(row.path.clone())));
|
||||
}
|
||||
}
|
||||
KeyCode::Up => {
|
||||
@@ -718,10 +785,7 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
|
||||
.areas(area);
|
||||
|
||||
// Header
|
||||
frame.render_widget_ref(
|
||||
Line::from(vec!["Resume a previous session".bold().cyan()]),
|
||||
header,
|
||||
);
|
||||
frame.render_widget_ref(Line::from(vec![state.action.title().bold().cyan()]), header);
|
||||
|
||||
// Search line
|
||||
let q = if state.query.is_empty() {
|
||||
@@ -738,9 +802,10 @@ fn draw_picker(tui: &mut Tui, state: &PickerState) -> std::io::Result<()> {
|
||||
render_list(frame, list, state, &metrics);
|
||||
|
||||
// Hint line
|
||||
let action_label = state.action.action_label();
|
||||
let hint_line: Line = vec![
|
||||
key_hint::plain(KeyCode::Enter).into(),
|
||||
" to resume ".dim(),
|
||||
format!(" to {action_label} ").dim(),
|
||||
" ".dim(),
|
||||
key_hint::plain(KeyCode::Esc).into(),
|
||||
" to start new ".dim(),
|
||||
@@ -1200,6 +1265,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
|
||||
let now = Utc::now();
|
||||
@@ -1349,6 +1415,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
|
||||
let page = RolloutRecorder::list_threads(
|
||||
@@ -1429,6 +1496,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
|
||||
state.reset_pagination();
|
||||
@@ -1497,6 +1565,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
state.reset_pagination();
|
||||
state.ingest_page(page(
|
||||
@@ -1528,6 +1597,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
|
||||
let mut items = Vec::new();
|
||||
@@ -1572,6 +1642,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
|
||||
let mut items = Vec::new();
|
||||
@@ -1616,6 +1687,7 @@ mod tests {
|
||||
String::from("openai"),
|
||||
true,
|
||||
None,
|
||||
SessionPickerAction::Resume,
|
||||
);
|
||||
state.reset_pagination();
|
||||
state.ingest_page(page(
|
||||
|
||||
@@ -20,6 +20,7 @@ pub enum SlashCommand {
|
||||
Review,
|
||||
New,
|
||||
Resume,
|
||||
Fork,
|
||||
Init,
|
||||
Compact,
|
||||
// Undo,
|
||||
@@ -45,6 +46,7 @@ impl SlashCommand {
|
||||
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
||||
SlashCommand::Review => "review my current changes and find issues",
|
||||
SlashCommand::Resume => "resume a saved chat",
|
||||
SlashCommand::Fork => "fork a saved chat",
|
||||
// SlashCommand::Undo => "ask Codex to undo a turn",
|
||||
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
|
||||
SlashCommand::Diff => "show git diff (including untracked files)",
|
||||
@@ -72,6 +74,7 @@ impl SlashCommand {
|
||||
match self {
|
||||
SlashCommand::New
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Fork
|
||||
| SlashCommand::Init
|
||||
| SlashCommand::Compact
|
||||
// | SlashCommand::Undo
|
||||
|
||||
Reference in new issue
Block a user