mirror of
https://github.com/openai/codex.git
synced 2026-11-04 00:24:43 +00:00
## Why Users have requested the ability to edit a goal's objective after a goal has been created. This PR exposes a new `/goal edit` command in the TUI to address this request. In the process of implementing this, I also noticed an existing bug in the goal runtime. When a goal's objective is updated through the `thread/goal/set` app server API, the goal runtime didn't emit a new steering prompt to tell the agent about the new objective. This PR also fixes this hole. ## What Changed - Adds `/goal edit` in the TUI, opening an edit box prefilled with the current goal objective. - Keeps active and paused goals in their current state, resets completed goals to active, keeps budget-limited goals budget-limited, and preserves the existing token budget. - Changes the existing `thread/goal/set` behavior so editing an objective preserves goal accounting instead of resetting it. The older reset-on-new-objective behavior was left over from before `thread/goal/clear`; clients that need to reset accounting can now clear the existing goal and create a new one. - Reuses the existing goal set API path; this does not add or change app-server protocol surface area. - Adds a dedicated goal runtime steering prompt when an externally persisted goal mutation changes the objective, so active turns receive the updated objective. ## Validation - Make sure `/goal edit` returns an error if no goal currently exists - Make sure `/goal edit` displays an edit box that can be optionally canceled with no side effects - Make sure that an edited goal results in a steer so the agent starts pursuing the new objective - Make sure the new objective is reflected in the goal if you use `/goal` to display the goal summary - Make sure that `/goal edit` doesn't reset the token budget, time/token accounting on the updated goal
219 lines
6.6 KiB
Rust
219 lines
6.6 KiB
Rust
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn goal_menu_active_snapshot() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_goal_summary(test_goal(
|
|
thread_id,
|
|
AppThreadGoalStatus::Active,
|
|
/*token_budget*/ Some(80_000),
|
|
));
|
|
|
|
assert_chatwidget_snapshot!("goal_menu_active", rendered_goal_summary(&mut rx));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn goal_menu_paused_snapshot() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_goal_summary(test_goal(
|
|
thread_id,
|
|
AppThreadGoalStatus::Paused,
|
|
/*token_budget*/ None,
|
|
));
|
|
|
|
assert_chatwidget_snapshot!("goal_menu_paused", rendered_goal_summary(&mut rx));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn goal_menu_budget_limited_snapshot() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_goal_summary(test_goal(
|
|
thread_id,
|
|
AppThreadGoalStatus::BudgetLimited,
|
|
/*token_budget*/ Some(80_000),
|
|
));
|
|
|
|
assert_chatwidget_snapshot!("goal_menu_budget_limited", rendered_goal_summary(&mut rx));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resume_paused_goal_prompt_snapshot() {
|
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_resume_paused_goal_prompt(
|
|
thread_id,
|
|
"Keep improving the bare goal command until it feels calm and useful.".to_string(),
|
|
);
|
|
|
|
assert_chatwidget_snapshot!(
|
|
"resume_paused_goal_prompt",
|
|
render_bottom_popup(&chat, /*width*/ 100)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn goal_edit_prompt_snapshot() {
|
|
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_goal_edit_prompt(
|
|
thread_id,
|
|
test_goal(
|
|
thread_id,
|
|
AppThreadGoalStatus::Active,
|
|
/*token_budget*/ Some(80_000),
|
|
),
|
|
);
|
|
|
|
assert_chatwidget_snapshot!(
|
|
"goal_edit_prompt",
|
|
render_bottom_popup(&chat, /*width*/ 100)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn goal_edit_prompt_submits_preserved_status_and_budget() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_goal_edit_prompt(
|
|
thread_id,
|
|
test_goal(
|
|
thread_id,
|
|
AppThreadGoalStatus::Paused,
|
|
/*token_budget*/ Some(80_000),
|
|
),
|
|
);
|
|
chat.handle_paste(" with clearer wording".to_string());
|
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
|
|
|
match rx.try_recv() {
|
|
Ok(AppEvent::SetThreadGoalObjective {
|
|
thread_id: event_thread_id,
|
|
objective,
|
|
mode:
|
|
crate::app_event::ThreadGoalSetMode::UpdateExisting {
|
|
status,
|
|
token_budget,
|
|
},
|
|
}) => {
|
|
assert_eq!(event_thread_id, thread_id);
|
|
assert_eq!(
|
|
objective,
|
|
"Keep improving the bare goal command until it feels calm and useful. with clearer wording"
|
|
);
|
|
assert_eq!(status, AppThreadGoalStatus::Paused);
|
|
assert_eq!(token_budget, Some(80_000));
|
|
}
|
|
other => panic!("expected SetThreadGoalObjective event, got {other:?}"),
|
|
}
|
|
assert!(chat.no_modal_or_popup_active());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn goal_edit_prompt_resets_terminal_status_to_active() {
|
|
let cases = [
|
|
AppThreadGoalStatus::BudgetLimited,
|
|
AppThreadGoalStatus::Complete,
|
|
];
|
|
|
|
for terminal_status in cases {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_goal_edit_prompt(
|
|
thread_id,
|
|
test_goal(
|
|
thread_id,
|
|
terminal_status,
|
|
/*token_budget*/ Some(80_000),
|
|
),
|
|
);
|
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
|
|
|
match rx.try_recv() {
|
|
Ok(AppEvent::SetThreadGoalObjective {
|
|
mode:
|
|
crate::app_event::ThreadGoalSetMode::UpdateExisting {
|
|
status,
|
|
token_budget,
|
|
},
|
|
..
|
|
}) => {
|
|
assert_eq!(status, AppThreadGoalStatus::Active);
|
|
assert_eq!(token_budget, Some(80_000));
|
|
}
|
|
other => panic!("expected SetThreadGoalObjective event, got {other:?}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resume_paused_goal_prompt_default_resumes_goal() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_resume_paused_goal_prompt(thread_id, "Finish the paused goal.".to_string());
|
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
|
|
|
match rx.try_recv() {
|
|
Ok(AppEvent::SetThreadGoalStatus {
|
|
thread_id: event_thread_id,
|
|
status,
|
|
}) => {
|
|
assert_eq!(event_thread_id, thread_id);
|
|
assert_eq!(status, AppThreadGoalStatus::Active);
|
|
}
|
|
other => panic!("expected SetThreadGoalStatus event, got {other:?}"),
|
|
}
|
|
assert!(chat.no_modal_or_popup_active());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn resume_paused_goal_prompt_can_leave_goal_paused() {
|
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
|
let thread_id = ThreadId::new();
|
|
|
|
chat.show_resume_paused_goal_prompt(thread_id, "Finish the paused goal.".to_string());
|
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
|
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
|
|
|
assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
|
|
assert!(chat.no_modal_or_popup_active());
|
|
}
|
|
|
|
fn test_goal(
|
|
thread_id: ThreadId,
|
|
status: AppThreadGoalStatus,
|
|
token_budget: Option<i64>,
|
|
) -> AppThreadGoal {
|
|
AppThreadGoal {
|
|
thread_id: thread_id.to_string(),
|
|
objective: "Keep improving the bare goal command until it feels calm and useful."
|
|
.to_string(),
|
|
status,
|
|
token_budget,
|
|
tokens_used: 12_500,
|
|
time_used_seconds: 90,
|
|
created_at: 1_776_272_400,
|
|
updated_at: 1_776_272_460,
|
|
}
|
|
}
|
|
|
|
fn rendered_goal_summary(
|
|
rx: &mut tokio::sync::mpsc::UnboundedReceiver<crate::app_event::AppEvent>,
|
|
) -> String {
|
|
drain_insert_history(rx)
|
|
.iter()
|
|
.map(|lines| lines_to_single_string(lines))
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|