feat: TUI undo op (#5629)
This commit is contained in:
@@ -37,6 +37,8 @@ use codex_core::protocol::TokenUsage;
|
||||
use codex_core::protocol::TokenUsageInfo;
|
||||
use codex_core::protocol::TurnAbortReason;
|
||||
use codex_core::protocol::TurnDiffEvent;
|
||||
use codex_core::protocol::UndoCompletedEvent;
|
||||
use codex_core::protocol::UndoStartedEvent;
|
||||
use codex_core::protocol::UserMessageEvent;
|
||||
use codex_core::protocol::ViewImageToolCallEvent;
|
||||
use codex_core::protocol::WebSearchBeginEvent;
|
||||
@@ -113,16 +115,9 @@ use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_git_tooling::CreateGhostCommitOptions;
|
||||
use codex_git_tooling::GhostCommit;
|
||||
use codex_git_tooling::GitToolingError;
|
||||
use codex_git_tooling::create_ghost_commit;
|
||||
use codex_git_tooling::restore_ghost_commit;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
const MAX_TRACKED_GHOST_COMMITS: usize = 20;
|
||||
|
||||
// Track information about an in-flight exec command.
|
||||
struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
@@ -267,9 +262,6 @@ pub(crate) struct ChatWidget {
|
||||
pending_notification: Option<Notification>,
|
||||
// Simple review mode flag; used to adjust layout and banners.
|
||||
is_review_mode: bool,
|
||||
// List of ghost commits corresponding to each turn.
|
||||
ghost_snapshots: Vec<GhostCommit>,
|
||||
ghost_snapshots_disabled: bool,
|
||||
// Whether to add a final message separator after the last message
|
||||
needs_final_message_separator: bool,
|
||||
|
||||
@@ -672,6 +664,31 @@ impl ChatWidget {
|
||||
debug!("BackgroundEvent: {message}");
|
||||
}
|
||||
|
||||
fn on_undo_started(&mut self, event: UndoStartedEvent) {
|
||||
self.bottom_pane.ensure_status_indicator();
|
||||
let message = event
|
||||
.message
|
||||
.unwrap_or_else(|| "Undo in progress...".to_string());
|
||||
self.set_status_header(message);
|
||||
}
|
||||
|
||||
fn on_undo_completed(&mut self, event: UndoCompletedEvent) {
|
||||
let UndoCompletedEvent { success, message } = event;
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
let message = message.unwrap_or_else(|| {
|
||||
if success {
|
||||
"Undo completed successfully.".to_string()
|
||||
} else {
|
||||
"Undo failed.".to_string()
|
||||
}
|
||||
});
|
||||
if success {
|
||||
self.add_info_message(message, None);
|
||||
} else {
|
||||
self.add_error_message(message);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_stream_error(&mut self, message: String) {
|
||||
if self.retry_status_header.is_none() {
|
||||
self.retry_status_header = Some(self.current_status_header.clone());
|
||||
@@ -989,8 +1006,6 @@ impl ChatWidget {
|
||||
suppress_session_configured_redraw: false,
|
||||
pending_notification: None,
|
||||
is_review_mode: false,
|
||||
ghost_snapshots: Vec::new(),
|
||||
ghost_snapshots_disabled: true,
|
||||
needs_final_message_separator: false,
|
||||
last_rendered_width: std::cell::Cell::new(None),
|
||||
feedback,
|
||||
@@ -1057,8 +1072,6 @@ impl ChatWidget {
|
||||
suppress_session_configured_redraw: true,
|
||||
pending_notification: None,
|
||||
is_review_mode: false,
|
||||
ghost_snapshots: Vec::new(),
|
||||
ghost_snapshots_disabled: true,
|
||||
needs_final_message_separator: false,
|
||||
last_rendered_width: std::cell::Cell::new(None),
|
||||
feedback,
|
||||
@@ -1211,7 +1224,7 @@ impl ChatWidget {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
}
|
||||
SlashCommand::Undo => {
|
||||
self.undo_last_snapshot();
|
||||
self.app_event_tx.send(AppEvent::CodexOp(Op::Undo));
|
||||
}
|
||||
SlashCommand::Diff => {
|
||||
self.add_diff_in_progress();
|
||||
@@ -1328,8 +1341,6 @@ impl ChatWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
self.capture_ghost_snapshot();
|
||||
|
||||
let mut items: Vec<UserInput> = Vec::new();
|
||||
|
||||
if !text.is_empty() {
|
||||
@@ -1362,57 +1373,6 @@ impl ChatWidget {
|
||||
self.needs_final_message_separator = false;
|
||||
}
|
||||
|
||||
fn capture_ghost_snapshot(&mut self) {
|
||||
if self.ghost_snapshots_disabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let options = CreateGhostCommitOptions::new(&self.config.cwd);
|
||||
match create_ghost_commit(&options) {
|
||||
Ok(commit) => {
|
||||
self.ghost_snapshots.push(commit);
|
||||
if self.ghost_snapshots.len() > MAX_TRACKED_GHOST_COMMITS {
|
||||
self.ghost_snapshots.remove(0);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.ghost_snapshots_disabled = true;
|
||||
let (message, hint) = match &err {
|
||||
GitToolingError::NotAGitRepository { .. } => (
|
||||
"Snapshots disabled: current directory is not a Git repository."
|
||||
.to_string(),
|
||||
None,
|
||||
),
|
||||
_ => (
|
||||
format!("Snapshots disabled after error: {err}"),
|
||||
Some(
|
||||
"Restart Codex after resolving the issue to re-enable snapshots."
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
};
|
||||
self.add_info_message(message, hint);
|
||||
tracing::warn!("failed to create ghost snapshot: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn undo_last_snapshot(&mut self) {
|
||||
let Some(commit) = self.ghost_snapshots.pop() else {
|
||||
self.add_info_message("No snapshot available to undo.".to_string(), None);
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(err) = restore_ghost_commit(&self.config.cwd, &commit) {
|
||||
self.add_error_message(format!("Failed to restore snapshot: {err}"));
|
||||
self.ghost_snapshots.push(commit);
|
||||
return;
|
||||
}
|
||||
|
||||
let short_id: String = commit.id().chars().take(8).collect();
|
||||
self.add_info_message(format!("Restored workspace to snapshot {short_id}"), None);
|
||||
}
|
||||
|
||||
/// Replay a subset of initial events into the UI to seed the transcript when
|
||||
/// resuming an existing session. This approximates the live event flow and
|
||||
/// is intentionally conservative: only safe-to-replay items are rendered to
|
||||
@@ -1510,6 +1470,8 @@ impl ChatWidget {
|
||||
EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => {
|
||||
self.on_background_event(message)
|
||||
}
|
||||
EventMsg::UndoStarted(ev) => self.on_undo_started(ev),
|
||||
EventMsg::UndoCompleted(ev) => self.on_undo_completed(ev),
|
||||
EventMsg::StreamError(StreamErrorEvent { message }) => self.on_stream_error(message),
|
||||
EventMsg::UserMessage(ev) => {
|
||||
if from_replay {
|
||||
|
||||
Reference in New Issue
Block a user