diff --git a/crates/goose-cli/src/commands/schedule.rs b/crates/goose-cli/src/commands/schedule.rs index c1c0b74a70..2fdbed6986 100644 --- a/crates/goose-cli/src/commands/schedule.rs +++ b/crates/goose-cli/src/commands/schedule.rs @@ -180,10 +180,10 @@ pub async fn handle_schedule_remove(schedule_id: String) -> Result<()> { .await .context("Failed to initialize scheduler")?; - match scheduler.remove_scheduled_job(&schedule_id, true).await { + match scheduler.remove_scheduled_job(&schedule_id, false).await { Ok(_) => { println!( - "Scheduled job '{}' and its associated recipe removed.", + "Scheduled job '{}' removed. Associated recipe was kept.", schedule_id ); Ok(()) diff --git a/crates/goose-server/src/routes/schedule.rs b/crates/goose-server/src/routes/schedule.rs index e7ca2ebf90..ca856c2356 100644 --- a/crates/goose-server/src/routes/schedule.rs +++ b/crates/goose-server/src/routes/schedule.rs @@ -192,7 +192,7 @@ async fn list_schedules( ("id" = String, Path, description = "ID of the schedule to delete") ), responses( - (status = 204, description = "Scheduled job deleted successfully"), + (status = 204, description = "Scheduled job removed successfully"), (status = 404, description = "Scheduled job not found"), (status = 500, description = "Internal server error") ), @@ -205,13 +205,13 @@ async fn delete_schedule( ) -> Result { let scheduler = state.scheduler(); scheduler - .remove_scheduled_job(&id, true) + .remove_scheduled_job(&id, false) .await .map_err(|e| match e { goose::scheduler::SchedulerError::JobNotFound(msg) => { ErrorResponse::not_found(format!("Schedule not found: {}", msg)) } - _ => ErrorResponse::internal(format!("Error deleting schedule: {}", e)), + _ => ErrorResponse::internal(format!("Error removing schedule: {}", e)), })?; Ok(StatusCode::NO_CONTENT) } diff --git a/crates/goose/src/agents/schedule_tool.rs b/crates/goose/src/agents/schedule_tool.rs index 8778b1262a..f17e926d1e 100644 --- a/crates/goose/src/agents/schedule_tool.rs +++ b/crates/goose/src/agents/schedule_tool.rs @@ -281,14 +281,14 @@ impl Agent { ) })?; - match scheduler.remove_scheduled_job(job_id, true).await { + match scheduler.remove_scheduled_job(job_id, false).await { Ok(()) => Ok(vec![Content::text(format!( - "Successfully deleted job '{}'", + "Successfully removed schedule for job '{}'", job_id ))]), Err(e) => Err(ErrorData::new( ErrorCode::INTERNAL_ERROR, - format!("Failed to delete job: {}", e), + format!("Failed to remove schedule: {}", e), None, )), } diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index b43a5fdbdb..e2dd91dd64 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -1148,6 +1148,49 @@ mod tests { assert!(jobs[0].last_run.is_none(), "Paused job should not run"); } + #[tokio::test] + async fn test_remove_scheduled_job_respects_recipe_removal_flag() { + let temp_dir = tempdir().unwrap(); + let storage_path = temp_dir.path().join("schedule.json"); + let recipe_path = create_test_recipe(temp_dir.path(), "recipe_removal_flag_job"); + let session_manager = Arc::new(SessionManager::new(temp_dir.path().to_path_buf())); + let scheduler = Scheduler::new(storage_path, session_manager).await.unwrap(); + + let job = ScheduledJob { + id: "recipe_removal_flag_job".to_string(), + source: recipe_path.to_string_lossy().to_string(), + cron: "0 0 0 1 1 *".to_string(), + last_run: None, + currently_running: false, + paused: false, + current_session_id: None, + process_start_time: None, + }; + + scheduler + .add_scheduled_job(job.clone(), false) + .await + .unwrap(); + scheduler + .remove_scheduled_job("recipe_removal_flag_job", false) + .await + .unwrap(); + assert!( + recipe_path.exists(), + "Recipe should be kept when remove_recipe is false" + ); + + scheduler.add_scheduled_job(job, false).await.unwrap(); + scheduler + .remove_scheduled_job("recipe_removal_flag_job", true) + .await + .unwrap(); + assert!( + !recipe_path.exists(), + "Recipe should be deleted when remove_recipe is true" + ); + } + #[tokio::test] async fn test_job_with_no_prompt_does_not_panic() { let _guard = env_lock::lock_env([ diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 6a0b6316fa..558f48f628 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -2749,7 +2749,7 @@ ], "responses": { "204": { - "description": "Scheduled job deleted successfully" + "description": "Scheduled job removed successfully" }, "404": { "description": "Scheduled job not found" diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 2e07e055ca..666a708858 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -3767,7 +3767,7 @@ export type DeleteScheduleErrors = { export type DeleteScheduleResponses = { /** - * Scheduled job deleted successfully + * Scheduled job removed successfully */ 204: void; }; diff --git a/ui/desktop/src/components/schedule/SchedulesView.tsx b/ui/desktop/src/components/schedule/SchedulesView.tsx index 8bc6df9da3..4b4608b9ec 100644 --- a/ui/desktop/src/components/schedule/SchedulesView.tsx +++ b/ui/desktop/src/components/schedule/SchedulesView.tsx @@ -45,7 +45,10 @@ const i18n = defineMessages({ noSchedules: { id: 'schedulesView.noSchedules', defaultMessage: 'No schedules yet' }, scheduleUpdated: { id: 'schedulesView.scheduleUpdated', defaultMessage: 'Schedule Updated' }, scheduleUpdatedMsg: { id: 'schedulesView.scheduleUpdatedMsg', defaultMessage: 'Successfully updated schedule "{id}"' }, - confirmDelete: { id: 'schedulesView.confirmDelete', defaultMessage: 'Are you sure you want to delete schedule "{id}"?' }, + confirmDelete: { + id: 'schedulesView.confirmDelete', + defaultMessage: 'Remove schedule "{id}"? The recipe will be kept.', + }, schedulePaused: { id: 'schedulesView.schedulePaused', defaultMessage: 'Schedule Paused' }, schedulePausedMsg: { id: 'schedulesView.schedulePausedMsg', defaultMessage: 'Successfully paused schedule "{id}"' }, pauseError: { id: 'schedulesView.pauseError', defaultMessage: 'Pause Schedule Error' }, @@ -326,8 +329,8 @@ const SchedulesView: React.FC = ({ onClose: _onClose }) => { trackScheduleDeleted(true); await fetchSchedules(); } catch (error) { - console.error(`Failed to delete schedule "${id}":`, error); - const errorMsg = errorMessage(error, `Unknown error deleting "${id}".`); + console.error(`Failed to remove schedule "${id}":`, error); + const errorMsg = errorMessage(error, `Unknown error removing schedule "${id}".`); setApiError(errorMsg); trackScheduleDeleted(false, getErrorType(error)); } finally { diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 73704372d3..6c99f41193 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -3534,7 +3534,7 @@ "defaultMessage": "YAML" }, "schedulesView.confirmDelete": { - "defaultMessage": "Are you sure you want to delete schedule \"{id}\"?" + "defaultMessage": "Remove schedule \"{id}\"? The recipe will be kept." }, "schedulesView.createSchedule": { "defaultMessage": "Create Schedule"