mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat: /fork the current session instead of opening session picker (#9385)
Implemented /fork to fork the current session directly (no picker), handling it via a new ForkCurrentSession app event in both tui and tui2. Updated slash command descriptions/tooltips and adjusted the fork tests accordingly. Removed the unused in-session fork picker event.
This commit is contained in:
+49
-61
@@ -758,73 +758,61 @@ impl App {
|
|||||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
}
|
}
|
||||||
AppEvent::OpenForkPicker => {
|
AppEvent::ForkCurrentSession => {
|
||||||
match crate::resume_picker::run_fork_picker(
|
let summary =
|
||||||
tui,
|
session_summary(self.chat_widget.token_usage(), self.chat_widget.thread_id());
|
||||||
&self.config.codex_home,
|
if let Some(path) = self.chat_widget.rollout_path() {
|
||||||
&self.config.model_provider_id,
|
match self
|
||||||
false,
|
.server
|
||||||
)
|
.fork_thread(usize::MAX, self.config.clone(), path.clone())
|
||||||
.await?
|
.await
|
||||||
{
|
{
|
||||||
SessionSelection::Fork(path) => {
|
Ok(forked) => {
|
||||||
let summary = session_summary(
|
self.shutdown_current_thread().await;
|
||||||
self.chat_widget.token_usage(),
|
let init = crate::chatwidget::ChatWidgetInit {
|
||||||
self.chat_widget.thread_id(),
|
config: self.config.clone(),
|
||||||
);
|
frame_requester: tui.frame_requester(),
|
||||||
match self
|
app_event_tx: self.app_event_tx.clone(),
|
||||||
.server
|
initial_prompt: None,
|
||||||
.fork_thread(usize::MAX, self.config.clone(), path.clone())
|
initial_images: Vec::new(),
|
||||||
.await
|
enhanced_keys_supported: self.enhanced_keys_supported,
|
||||||
{
|
auth_manager: self.auth_manager.clone(),
|
||||||
Ok(forked) => {
|
models_manager: self.server.get_models_manager(),
|
||||||
self.shutdown_current_thread().await;
|
feedback: self.feedback.clone(),
|
||||||
let init = crate::chatwidget::ChatWidgetInit {
|
is_first_run: false,
|
||||||
config: self.config.clone(),
|
model: Some(self.current_model.clone()),
|
||||||
frame_requester: tui.frame_requester(),
|
};
|
||||||
app_event_tx: self.app_event_tx.clone(),
|
self.chat_widget = ChatWidget::new_from_existing(
|
||||||
initial_prompt: None,
|
init,
|
||||||
initial_images: Vec::new(),
|
forked.thread,
|
||||||
enhanced_keys_supported: self.enhanced_keys_supported,
|
forked.session_configured,
|
||||||
auth_manager: self.auth_manager.clone(),
|
);
|
||||||
models_manager: self.server.get_models_manager(),
|
self.current_model = model_info.slug.clone();
|
||||||
feedback: self.feedback.clone(),
|
if let Some(summary) = summary {
|
||||||
is_first_run: false,
|
let mut lines: Vec<Line<'static>> =
|
||||||
model: Some(self.current_model.clone()),
|
vec![summary.usage_line.clone().into()];
|
||||||
};
|
if let Some(command) = summary.resume_command {
|
||||||
self.chat_widget = ChatWidget::new_from_existing(
|
let spans = vec![
|
||||||
init,
|
"To continue this session, run ".into(),
|
||||||
forked.thread,
|
command.cyan(),
|
||||||
forked.session_configured,
|
];
|
||||||
);
|
lines.push(spans.into());
|
||||||
self.current_model = model_info.slug.clone();
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
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}"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let path_display = path.display();
|
||||||
|
self.chat_widget.add_error_message(format!(
|
||||||
|
"Failed to fork current session from {path_display}: {err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SessionSelection::Exit
|
} else {
|
||||||
| SessionSelection::StartFresh
|
self.chat_widget
|
||||||
| SessionSelection::Resume(_) => {}
|
.add_error_message("Current session is not ready to fork yet.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
}
|
}
|
||||||
AppEvent::InsertHistoryCell(cell) => {
|
AppEvent::InsertHistoryCell(cell) => {
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ pub(crate) enum AppEvent {
|
|||||||
/// Open the resume picker inside the running TUI session.
|
/// Open the resume picker inside the running TUI session.
|
||||||
OpenResumePicker,
|
OpenResumePicker,
|
||||||
|
|
||||||
/// Open the fork picker inside the running TUI session.
|
/// Fork the current session into a new thread.
|
||||||
OpenForkPicker,
|
ForkCurrentSession,
|
||||||
|
|
||||||
/// Request to exit the application.
|
/// Request to exit the application.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1994,7 +1994,7 @@ impl ChatWidget {
|
|||||||
self.app_event_tx.send(AppEvent::OpenResumePicker);
|
self.app_event_tx.send(AppEvent::OpenResumePicker);
|
||||||
}
|
}
|
||||||
SlashCommand::Fork => {
|
SlashCommand::Fork => {
|
||||||
self.app_event_tx.send(AppEvent::OpenForkPicker);
|
self.app_event_tx.send(AppEvent::ForkCurrentSession);
|
||||||
}
|
}
|
||||||
SlashCommand::Init => {
|
SlashCommand::Init => {
|
||||||
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
|
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
|
||||||
|
|||||||
@@ -1532,12 +1532,12 @@ async fn slash_resume_opens_picker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn slash_fork_opens_picker() {
|
async fn slash_fork_requests_current_fork() {
|
||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||||
|
|
||||||
chat.dispatch_command(SlashCommand::Fork);
|
chat.dispatch_command(SlashCommand::Fork);
|
||||||
|
|
||||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker));
|
assert_matches!(rx.try_recv(), Ok(AppEvent::ForkCurrentSession));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ impl SlashCommand {
|
|||||||
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
||||||
SlashCommand::Review => "review my current changes and find issues",
|
SlashCommand::Review => "review my current changes and find issues",
|
||||||
SlashCommand::Resume => "resume a saved chat",
|
SlashCommand::Resume => "resume a saved chat",
|
||||||
SlashCommand::Fork => "fork a saved chat",
|
SlashCommand::Fork => "fork the current chat",
|
||||||
// SlashCommand::Undo => "ask Codex to undo a turn",
|
// SlashCommand::Undo => "ask Codex to undo a turn",
|
||||||
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
|
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
|
||||||
SlashCommand::Diff => "show git diff (including untracked files)",
|
SlashCommand::Diff => "show git diff (including untracked files)",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Use /approvals to control when Codex asks for confirmation.
|
|||||||
Run /review to get a code review of your current changes.
|
Run /review to get a code review of your current changes.
|
||||||
Use /skills to list available skills or ask Codex to use one.
|
Use /skills to list available skills or ask Codex to use one.
|
||||||
Use /status to see the current model, approvals, and token usage.
|
Use /status to see the current model, approvals, and token usage.
|
||||||
Use /fork to branch a saved chat into a new thread.
|
Use /fork to branch the current chat into a new thread.
|
||||||
Use /init to create an AGENTS.md with project-specific guidance.
|
Use /init to create an AGENTS.md with project-specific guidance.
|
||||||
Use /mcp to list configured MCP tools.
|
Use /mcp to list configured MCP tools.
|
||||||
You can run any shell command from Codex using `!` (e.g. `!ls`)
|
You can run any shell command from Codex using `!` (e.g. `!ls`)
|
||||||
|
|||||||
+50
-60
@@ -1531,72 +1531,62 @@ impl App {
|
|||||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
}
|
}
|
||||||
AppEvent::OpenForkPicker => {
|
AppEvent::ForkCurrentSession => {
|
||||||
match crate::resume_picker::run_fork_picker(
|
let summary = session_summary(
|
||||||
tui,
|
self.chat_widget.token_usage(),
|
||||||
&self.config.codex_home,
|
self.chat_widget.conversation_id(),
|
||||||
&self.config.model_provider_id,
|
);
|
||||||
false,
|
if let Some(path) = self.chat_widget.rollout_path() {
|
||||||
)
|
match self
|
||||||
.await?
|
.server
|
||||||
{
|
.fork_thread(usize::MAX, self.config.clone(), path.clone())
|
||||||
SessionSelection::Fork(path) => {
|
.await
|
||||||
let summary = session_summary(
|
{
|
||||||
self.chat_widget.token_usage(),
|
Ok(forked) => {
|
||||||
self.chat_widget.conversation_id(),
|
self.shutdown_current_conversation().await;
|
||||||
);
|
let init = crate::chatwidget::ChatWidgetInit {
|
||||||
match self
|
config: self.config.clone(),
|
||||||
.server
|
frame_requester: tui.frame_requester(),
|
||||||
.fork_thread(usize::MAX, self.config.clone(), path.clone())
|
app_event_tx: self.app_event_tx.clone(),
|
||||||
.await
|
initial_prompt: None,
|
||||||
{
|
initial_images: Vec::new(),
|
||||||
Ok(forked) => {
|
enhanced_keys_supported: self.enhanced_keys_supported,
|
||||||
self.shutdown_current_conversation().await;
|
auth_manager: self.auth_manager.clone(),
|
||||||
let init = crate::chatwidget::ChatWidgetInit {
|
models_manager: self.server.get_models_manager(),
|
||||||
config: self.config.clone(),
|
feedback: self.feedback.clone(),
|
||||||
frame_requester: tui.frame_requester(),
|
is_first_run: false,
|
||||||
app_event_tx: self.app_event_tx.clone(),
|
model: Some(self.current_model.clone()),
|
||||||
initial_prompt: None,
|
};
|
||||||
initial_images: Vec::new(),
|
self.chat_widget = ChatWidget::new_from_existing(
|
||||||
enhanced_keys_supported: self.enhanced_keys_supported,
|
init,
|
||||||
auth_manager: self.auth_manager.clone(),
|
forked.thread,
|
||||||
models_manager: self.server.get_models_manager(),
|
forked.session_configured,
|
||||||
feedback: self.feedback.clone(),
|
);
|
||||||
is_first_run: false,
|
if let Some(summary) = summary {
|
||||||
model: Some(self.current_model.clone()),
|
let mut lines: Vec<Line<'static>> =
|
||||||
};
|
vec![summary.usage_line.clone().into()];
|
||||||
self.chat_widget = ChatWidget::new_from_existing(
|
if let Some(command) = summary.resume_command {
|
||||||
init,
|
let spans = vec![
|
||||||
forked.thread,
|
"To continue this session, run ".into(),
|
||||||
forked.session_configured,
|
command.cyan(),
|
||||||
);
|
];
|
||||||
if let Some(summary) = summary {
|
lines.push(spans.into());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
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}"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let path_display = path.display();
|
||||||
|
self.chat_widget.add_error_message(format!(
|
||||||
|
"Failed to fork current session from {path_display}: {err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SessionSelection::Exit
|
} else {
|
||||||
| SessionSelection::StartFresh
|
self.chat_widget
|
||||||
| SessionSelection::Resume(_) => {}
|
.add_error_message("Current session is not ready to fork yet.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leaving alt-screen may blank the inline viewport; force a redraw either way.
|
|
||||||
tui.frame_requester().schedule_frame();
|
tui.frame_requester().schedule_frame();
|
||||||
}
|
}
|
||||||
AppEvent::InsertHistoryCell(cell) => {
|
AppEvent::InsertHistoryCell(cell) => {
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ pub(crate) enum AppEvent {
|
|||||||
/// Open the resume picker inside the running TUI session.
|
/// Open the resume picker inside the running TUI session.
|
||||||
OpenResumePicker,
|
OpenResumePicker,
|
||||||
|
|
||||||
/// Open the fork picker inside the running TUI session.
|
/// Fork the current session into a new thread.
|
||||||
OpenForkPicker,
|
ForkCurrentSession,
|
||||||
|
|
||||||
/// Request to exit the application.
|
/// Request to exit the application.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1769,7 +1769,7 @@ impl ChatWidget {
|
|||||||
self.app_event_tx.send(AppEvent::OpenResumePicker);
|
self.app_event_tx.send(AppEvent::OpenResumePicker);
|
||||||
}
|
}
|
||||||
SlashCommand::Fork => {
|
SlashCommand::Fork => {
|
||||||
self.app_event_tx.send(AppEvent::OpenForkPicker);
|
self.app_event_tx.send(AppEvent::ForkCurrentSession);
|
||||||
}
|
}
|
||||||
SlashCommand::Init => {
|
SlashCommand::Init => {
|
||||||
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
|
let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME);
|
||||||
|
|||||||
@@ -1338,12 +1338,12 @@ async fn slash_resume_opens_picker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn slash_fork_opens_picker() {
|
async fn slash_fork_requests_current_fork() {
|
||||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
|
||||||
|
|
||||||
chat.dispatch_command(SlashCommand::Fork);
|
chat.dispatch_command(SlashCommand::Fork);
|
||||||
|
|
||||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker));
|
assert_matches!(rx.try_recv(), Ok(AppEvent::ForkCurrentSession));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ impl SlashCommand {
|
|||||||
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
|
||||||
SlashCommand::Review => "review my current changes and find issues",
|
SlashCommand::Review => "review my current changes and find issues",
|
||||||
SlashCommand::Resume => "resume a saved chat",
|
SlashCommand::Resume => "resume a saved chat",
|
||||||
SlashCommand::Fork => "fork a saved chat",
|
SlashCommand::Fork => "fork the current chat",
|
||||||
// SlashCommand::Undo => "ask Codex to undo a turn",
|
// SlashCommand::Undo => "ask Codex to undo a turn",
|
||||||
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
|
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
|
||||||
SlashCommand::Diff => "show git diff (including untracked files)",
|
SlashCommand::Diff => "show git diff (including untracked files)",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Use /approvals to control when Codex asks for confirmation.
|
|||||||
Run /review to get a code review of your current changes.
|
Run /review to get a code review of your current changes.
|
||||||
Use /skills to list available skills or ask Codex to use one.
|
Use /skills to list available skills or ask Codex to use one.
|
||||||
Use /status to see the current model, approvals, and token usage.
|
Use /status to see the current model, approvals, and token usage.
|
||||||
Use /fork to branch a saved chat into a new thread.
|
Use /fork to branch the current chat into a new thread.
|
||||||
Use /init to create an AGENTS.md with project-specific guidance.
|
Use /init to create an AGENTS.md with project-specific guidance.
|
||||||
Use /mcp to list configured MCP tools.
|
Use /mcp to list configured MCP tools.
|
||||||
You can run any shell command from Codex using `!` (e.g. `!ls`)
|
You can run any shell command from Codex using `!` (e.g. `!ls`)
|
||||||
|
|||||||
Reference in New Issue
Block a user