Skip to content

refactor: clean up client side code for creating schedule#6805

Merged
lifeizhou-ap merged 11 commits intomainfrom
lifei/clean-up-schedule-client-side-function
Feb 2, 2026
Merged

refactor: clean up client side code for creating schedule#6805
lifeizhou-ap merged 11 commits intomainfrom
lifei/clean-up-schedule-client-side-function

Conversation

@lifeizhou-ap
Copy link
Collaborator

Summary

Type of Change

  • Feature
  • Bug fix
  • Refactor / Code quality
  • Performance improvement
  • Documentation
  • Tests
  • Security fix
  • Build / Release
  • Other (specify below)

AI Assistance

  • This PR was created or reviewed with AI assistance

Testing

Related Issues

Relates to #ISSUE_ID
Discussion: LINK (if any)

Screenshots/Demos (for UX changes)

Before:

After:

Copilot AI review requested due to automatic review settings January 29, 2026 10:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors schedule creation so the desktop UI sends a parsed Recipe object to the server (instead of a client-generated recipe_source path), centralizing recipe validation/YAML persistence on the backend and deduplicating recipe parsing helpers in the UI.

Changes:

  • Update schedule creation API contract from recipe_source: string to recipe: Recipe across UI types, OpenAPI, and server route.
  • Move recipe file/deeplink parsing into shared ui/desktop/src/recipe/index.ts helpers and use them from schedule/import flows.
  • Server now validates/sanitizes recipes and writes them to the scheduled recipes directory before scheduling.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
ui/desktop/src/schedule.ts Adjusts client schedule creation request payload to send a Recipe object.
ui/desktop/src/recipe/index.ts Adds shared helpers for parsing recipes from files and deeplinks via API/utilities.
ui/desktop/src/components/schedule/ScheduleModal.tsx Switches schedule creation flow to parse recipes locally and submit Recipe to API.
ui/desktop/src/components/recipes/ImportRecipeForm.tsx Removes duplicated parsing helpers and reuses shared recipe parsing utilities.
ui/desktop/src/api/types.gen.ts Updates generated API types for schedule creation to accept recipe.
ui/desktop/openapi.json Updates OpenAPI schema for schedule creation request to require recipe.
crates/goose-server/src/routes/schedule.rs Updates schedule creation endpoint to accept Recipe, validate it, persist YAML, and schedule the job.

Comment on lines 110 to 111
let recipe_path = scheduled_recipes_dir.join(format!("{}.yaml", req.id));
let yaml_content = req
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req.id is used directly to construct a filename ({id}.yaml), which allows path separators / .. and can lead to path traversal or invalid filenames; validate/sanitize the schedule id to a safe character set and reject anything else before building recipe_path.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the request id validation

Comment on lines 115 to 116
fs::write(&recipe_path, yaml_content)
.map_err(|e| ErrorResponse::internal(format!("Failed to save recipe file: {}", e)))?;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recipe file is written to disk before add_scheduled_job runs, so if the scheduler rejects the request (e.g., JobIdExists or cron parse error) the existing recipe file can be overwritten and/or a new orphaned file will be left behind; only persist the file after the scheduler has accepted the schedule (or write to a temp path and atomically rename/clean up on error).

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is fine, it is an edge case and saving the files in the schedule folder is ok.

Comment on lines 115 to 116
fs::write(&recipe_path, yaml_content)
.map_err(|e| ErrorResponse::internal(format!("Failed to save recipe file: {}", e)))?;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::fs::write is a blocking filesystem call inside an async Axum handler; switch to tokio::fs (or spawn_blocking) to avoid blocking the runtime under load.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 58
if (value.trim()) {
try {
const recipe = await parseDeepLink(value.trim());
const recipe = await parseDeeplink(value.trim());
if (recipe) {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseDeeplink only supports goose://recipe?config=... links, but this modal still presents/validates deeplinks as goose://bot or goose://recipe; either add goose://bot support in the parser or update the modal copy/validation so users aren’t told a format will work when it won’t.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only supports recipe deeplink. so it is fine

* main:
  docs: usage data collection (#6822)
  feat: platform extension migrator + code mode rename (#6611)
  feat: CLI flag to skip loading profile extensions (#6780)
  Swap canonical model from openrouter to models.dev (#6625)
  Hook thinking status (#6815)
  Fetch new skills hourly (#6814)
  copilot instructions: Update "No prerelease docs" instruction (#6795)
  refactor: centralize audience filtering before providers receive messages (#6728)
  update doc to remind contributors to activate hermit and document minimal npm and node version (#6727)
  nit: don't spit out compaction when in term mode as it fills up the screen (#6799)
  fix: correct tool support detection in Tetrate provider model fetching (#6808)
  Session manager fixes (#6809)
  fix(desktop): handle quoted paths with spaces in extension commands (#6430)
  fix: we can default gooseignore without writing it out (#6802)
  fix broken link (#6810)
  docs: add Beads MCP extension tutorial (#6792)
  feat(goose): add support for AWS_BEARER_TOKEN_BEDROCK environment variable (#6739)
Copilot AI review requested due to automatic review settings January 29, 2026 23:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment on lines +132 to +134
fs::write(&recipe_path, yaml_content)
.await
.map_err(|e| ErrorResponse::internal(format!("Failed to save recipe file: {}", e)))?;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs::write(&recipe_path, ...) will overwrite an existing scheduled recipe file (and can leave an orphan file) if add_scheduled_job later fails (e.g., JobIdExists or invalid cron); consider checking for an existing job/file first and/or writing to a temp file and cleaning it up on scheduler failure.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +26
let is_valid = !id.is_empty()
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ' ');

if !is_valid {
return Err(ErrorResponse::bad_request(
"Schedule name must use only alphanumeric characters, hyphens, underscores, or spaces"
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_schedule_id allows leading/trailing spaces, which can create hard-to-reference schedule IDs (and awkward filenames like "foo .yaml"); consider trimming id before validation and/or rejecting IDs that start/end with whitespace.

Suggested change
let is_valid = !id.is_empty()
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ' ');
if !is_valid {
return Err(ErrorResponse::bad_request(
"Schedule name must use only alphanumeric characters, hyphens, underscores, or spaces"
// Reject empty or all-whitespace IDs, and disallow leading/trailing whitespace.
let trimmed = id.trim();
let is_valid = !trimmed.is_empty()
&& trimmed.len() == id.len()
&& id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ' ');
if !is_valid {
return Err(ErrorResponse::bad_request(
"Schedule name must use only alphanumeric characters, hyphens, underscores, or spaces, and must not start or end with whitespace"

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings January 30, 2026 00:11
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

ui/desktop/src/components/schedule/ScheduleModal.tsx:81

  • The UI text suggests goose://bot links are supported, but parseDeeplink only accepts goose://recipe?config=..., so users will see confusing validation errors for bot links. Either update the placeholder/error messages to only mention goose://recipe?config=... or extend the parser to accept goose://bot?... as well.
        const recipe = await parseDeeplink(value.trim());
        if (!recipe) throw new Error();
        setParsedRecipe(recipe);
        if (recipe.title) {
          setScheduleIdFromTitle(recipe.title);
        }
      } catch {
        setParsedRecipe(null);
        setInternalValidationError('Invalid deep link. Please use a goose://recipe link.');
      }
    } else {
      setParsedRecipe(null);
    }
  }, []);

  useEffect(() => {
    if (isOpen) {

Comment on lines 117 to 121
}
}
} else {
setInternalValidationError('Invalid file type: Please select a YAML file (.yaml or .yml)');
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleBrowseFile awaits parseRecipeFromFile without handling failures; if the file read fails or parsing throws, the modal can end up with an unhandled rejection and no user-facing error. Wrap the read/parse in try/catch and set internalValidationError (and/or clear parsedRecipe) when parsing fails.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one might be valid @lifeizhou-ap if you want to take a look?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @zanesq for the review! I've added the error handling now

Comment on lines 19 to 21
let id = id.trim();
let is_valid = !id.is_empty()
&& id
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validate_schedule_id trims the input for validation but the untrimmed req.id is later used as the schedule ID and filename, which allows leading/trailing spaces (and can create problematic filenames, especially on Windows). Consider rejecting leading/trailing whitespace or returning/using the trimmed value when creating the job and recipe path.

Suggested change
let id = id.trim();
let is_valid = !id.is_empty()
&& id
let trimmed = id.trim();
let has_no_leading_or_trailing_whitespace = trimmed == id;
let is_valid = !trimmed.is_empty()
&& has_no_leading_or_trailing_whitespace
&& trimmed

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

@zanesq zanesq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved but can you take a look at this one before merging?

* main:
  fix: fixed the broken release (#6887)
  feat: Streamable HTTP transport for ACP + goose-acp usage (#6741)
  Add Laminar for Observability (#6514)
  Missed a couple of places that hard code J for the newline key (#6853)
  fix(ui): preserve working directory when creating new chat (#6789)
  blog: add 5 tips for building MCP Apps that work (#6855)
  docs: session isolation (#6846)
  upgrade react and electron to latest (#6845)
  Fix: Small update UI settings prompt injection (#6830)
  Remove autogenerated .gooseignore files that don't belong in repo (#6824)
  Fix case-insensitive matching for builtin extension names (#6825)
  docs: cli newline keybinding (#6823)
  Update version to 1.22.0 (#6821)
  Refactor: move persisting extension to session outside of route (#6685)
  acp: load configured extensions and refactor tests (#6803)
Copilot AI review requested due to automatic review settings February 2, 2026 04:26
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment on lines +124 to 151
let scheduled_recipes_dir = get_default_scheduled_recipes_dir().map_err(|e| {
ErrorResponse::internal(format!("Failed to get scheduled recipes directory: {}", e))
})?;

let recipe_path = scheduled_recipes_dir.join(format!("{}.yaml", id));
let yaml_content = req
.recipe
.to_yaml()
.map_err(|e| ErrorResponse::internal(format!("Failed to convert recipe to YAML: {}", e)))?;
fs::write(&recipe_path, yaml_content)
.await
.map_err(|e| ErrorResponse::internal(format!("Failed to save recipe file: {}", e)))?;

let job = ScheduledJob {
id: req.id,
source: req.recipe_source,
id,
source: recipe_path.to_string_lossy().into_owned(),
cron: req.cron,
last_run: None,
currently_running: false,
paused: false,
current_session_id: None,
process_start_time: None,
};

let scheduler = state.scheduler();
scheduler
.add_scheduled_job(job.clone(), true)
.add_scheduled_job(job.clone(), false)
.await
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recipe YAML is written to {scheduled_recipes_dir}/{id}.yaml before add_scheduled_job checks for duplicate IDs, so a request with an existing id (or one that later fails cron validation) can overwrite an existing schedule’s recipe file or leave an orphaned file. Consider checking for JobIdExists before writing and/or writing to a temp file and only renaming after add_scheduled_job succeeds (and cleaning up the temp file on error).

Copilot uses AI. Check for mistakes.
@lifeizhou-ap lifeizhou-ap added this pull request to the merge queue Feb 2, 2026
Merged via the queue into main with commit dd15fd0 Feb 2, 2026
32 of 34 checks passed
@lifeizhou-ap lifeizhou-ap deleted the lifei/clean-up-schedule-client-side-function branch February 2, 2026 08:56
stebbins pushed a commit to stebbins/goose that referenced this pull request Feb 4, 2026
kuccello pushed a commit to kuccello/goose that referenced this pull request Feb 7, 2026
Tyler-Hardin pushed a commit to Tyler-Hardin/goose that referenced this pull request Feb 11, 2026
Tyler-Hardin pushed a commit to Tyler-Hardin/goose that referenced this pull request Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants