TUI: skip goal replace prompt for completed goals (#23792)

## Why
Users reported that the replacement confirmation feels unnecessary when
the current thread goal is already complete. In that state, `/goal
<objective>` is starting fresh rather than interrupting active work.

## What changed
`/goal <objective>` now skips the replace confirmation when the existing
goal has `complete` status and uses the existing fresh replacement path.
Goals that are active, paused, blocked, usage-limited, or budget-limited
still require confirmation before being replaced.
This commit is contained in:
Eric Traut authored and GitHub committed 2026-05-21 10:45:43 -07:00
1 parent de80fa6e31
commit 4acb456bfe
1 file changed
+59 -7
+59 -7
View File
@@ -8,6 +8,7 @@ use crate::bottom_pane::SelectionViewParams;
use crate::bottom_pane::popup_consts::standard_popup_hint_line; use crate::bottom_pane::popup_consts::standard_popup_hint_line;
use crate::goal_display::goal_status_label; use crate::goal_display::goal_status_label;
use crate::goal_display::goal_usage_summary; use crate::goal_display::goal_usage_summary;
use codex_app_server_protocol::ThreadGoal;
use codex_app_server_protocol::ThreadGoalStatus; use codex_app_server_protocol::ThreadGoalStatus;
use codex_protocol::ThreadId; use codex_protocol::ThreadId;
@@ -116,25 +117,30 @@ impl App {
objective: String, objective: String,
mode: ThreadGoalSetMode, mode: ThreadGoalSetMode,
) { ) {
if matches!(mode, ThreadGoalSetMode::ConfirmIfExists) { let mode = if matches!(mode, ThreadGoalSetMode::ConfirmIfExists) {
let result = app_server.thread_goal_get(thread_id).await; let result = app_server.thread_goal_get(thread_id).await;
if self.current_displayed_thread_id() != Some(thread_id) { if self.current_displayed_thread_id() != Some(thread_id) {
return; return;
} }
match result { match result {
Ok(response) if response.goal.is_some() => { Ok(response) => match response.goal.as_ref() {
self.show_replace_thread_goal_confirmation(thread_id, objective); Some(goal) if should_confirm_before_replacing_goal(goal) => {
return; self.show_replace_thread_goal_confirmation(thread_id, objective);
} return;
Ok(_) => {} }
Some(_) => ThreadGoalSetMode::ReplaceExisting,
None => mode,
},
Err(err) => { Err(err) => {
self.chat_widget self.chat_widget
.add_error_message(thread_goal_error_message("read", &err)); .add_error_message(thread_goal_error_message("read", &err));
return; return;
} }
} }
} } else {
mode
};
let replacing_goal = matches!(mode, ThreadGoalSetMode::ReplaceExisting); let replacing_goal = matches!(mode, ThreadGoalSetMode::ReplaceExisting);
if replacing_goal { if replacing_goal {
@@ -296,6 +302,19 @@ fn is_ephemeral_thread_goal_error(err: &color_eyre::Report) -> bool {
}) })
} }
fn should_confirm_before_replacing_goal(goal: &ThreadGoal) -> bool {
// Completed goals are terminal, so `/goal <objective>` can start a fresh goal
// without asking the user to confirm replacing already-finished work.
match goal.status {
ThreadGoalStatus::Complete => false,
ThreadGoalStatus::Active
| ThreadGoalStatus::Paused
| ThreadGoalStatus::Blocked
| ThreadGoalStatus::UsageLimited
| ThreadGoalStatus::BudgetLimited => true,
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::history_cell::HistoryCell; use crate::history_cell::HistoryCell;
@@ -350,4 +369,37 @@ mod tests {
"Failed to read thread goal: thread/goal/get failed in TUI" "Failed to read thread goal: thread/goal/get failed in TUI"
); );
} }
#[test]
fn completed_goal_does_not_require_replace_confirmation() {
assert!(!should_confirm_before_replacing_goal(&test_goal(
ThreadGoalStatus::Complete
)));
}
#[test]
fn unfinished_goals_require_replace_confirmation() {
for status in [
ThreadGoalStatus::Active,
ThreadGoalStatus::Paused,
ThreadGoalStatus::Blocked,
ThreadGoalStatus::UsageLimited,
ThreadGoalStatus::BudgetLimited,
] {
assert!(should_confirm_before_replacing_goal(&test_goal(status)));
}
}
fn test_goal(status: ThreadGoalStatus) -> ThreadGoal {
ThreadGoal {
thread_id: ThreadId::new().to_string(),
objective: "Finish the thing.".to_string(),
status,
token_budget: None,
tokens_used: 0,
time_used_seconds: 0,
created_at: 1_776_272_400,
updated_at: 1_776_272_460,
}
}
} }