Skip to content

feat(studio): split the schedule run time and confirm saves with a toast - #2370

Open
peppescg wants to merge 4 commits into
feat/studio-schedules-2299-inventoryfrom
feat/studio-schedules-2299-dialog
Open

peppescg wants to merge 4 commits into
feat/studio-schedules-2299-inventoryfrom
feat/studio-schedules-2299-dialog

Conversation

@peppescg

@peppescg peppescg commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Stack (2 of 4) for #2299 and #2341: the inventory and detail → this PR → the transcript dialog → the title= audit. Review bottom to top; this PR's own diff is its four commits.

Summary

The create and edit dialog follows the prototype: a one-shot run time takes separate date and time inputs, the submit button stays disabled until the form could be sent, and a Sonner toast confirms each save. The request bodies, the inline refusal and the edit locks are unchanged, and pinned first.

Part of #2299
Part of #2210

Review changes (23b6788c8)

In response to the review. Commit 23b6788c8 sits on top of the original two commits and doesn't rewrite them.

  • Date and Time are independent. The one-shot time has its own form field (oneShotTime), and the run time is derived from the date plus that time.
    • A time typed before the date is kept and used once a date is picked.
    • Clearing or editing the date, or one of its segments, never touches the time. An empty date leaves no run time, so Create stays disabled.
    • A date runs at 9:00 only when no time was ever chosen.
    • The Time input still ignores its transient empty values. The Date input's empty value no longer drops the time. It still clears the run time, so Create can't send a date the input no longer shows.
  • Why Create is disabled. Once a field is edited or the form is submitted, scheduleFormProblem's message ("Prompt is required.", "Run time is required.") shows as muted text-xs helper text above the footer. It is right-aligned under Create on desktop and above it on a phone, and it describes the Create button through aria-describedby. It goes away once the form could be sent. Enter in an incomplete form now shows it too.
  • Breakpoint guard (c684c4e03, after the rebase onto main). The helper text's sm:text-right is listed in breakpoints.test.ts, which now scans schedules: it follows DialogFooter's buttons, which become a right-aligned row from sm:, so moving it to the 500px pivot would right-align it above stacked buttons between 500 and 639px.
  • Nits. toastScheduleSaved moved above the DECISION comment, so that comment documents canEditSchedule again. sm:max-w-lg and its test are gone, because it is the Dialog default. The "Width" bullet above no longer applies.

Difference from the prototype. The prototype's joinOneShot fills in today's date when only a time is set, or when the date is cleared. Studio leaves the date empty and keeps Create disabled. That way a half-edited date never snaps to today mid-edit.

Tests.

  • Pins first. 4 new pins in schedule-dialog.pinning.test.tsx pass on 5557e7219 and after: changing the date keeps the time; a one-shot phrase fills both inputs; the edit dialog shows a stored one-shot's date and time, locked; and a complete form names no problem.

  • New tests.

    • schedule-dialog.test.tsx (5): time before date; date cleared and picked again; helper text hidden until touched; the text and aria-describedby follow the first problem; Enter in an untouched form.
    • schedule-form-value.test.ts (6) cover the new patch helpers.
    • e2e/schedules.spec.ts (1, desktop and phone) checks the time-first and keyboard date-clear cases in Chromium. Backspace on one date segment leaves the time at 15:30 and shows "Run time is required." as Create's description.
  • Changed tests. "a date on its own runs at 9:00" now covers only the never-chosen case. The old oneShotParts/joinOneShot unit test was replaced with tests of the helpers that replace them. The trigger-lock drift test also drifts oneShotTime.

  • Mutation proof (each applied alone, focused suite rerun):

    Mutation Failing tests
    A time without a date is dropped 2
    A cleared date clears the time 2 (and the browser test, desktop and phone)
    A date ignores the chosen time 5 (including a pin)
    No transient-empty guard on Time 2
    A phrase leaves the time unset 3 (including a pin)
    A stored one-shot has no time 2 (including a pin)
    The locked view keeps a drifted time 1
    Helper text shown before the form is touched 1
    Helper text never shown 2
    Create not described by it 2
    Submit doesn't mark the form touched 1
    Edits don't mark the form touched 1
    Helper text not muted 1

Gates (shared lock):

  • On 23b6788c8: task studio:check passes, with server tests 252/252 and web tests 1679/1679; task studio:browser reports 138 passed, 38 skipped.
  • After the rebase onto main, on c684c4e03: task studio:check passes (server 252/252, web 1727/1727). At the top of the restacked stack (97f3f8c7f): task studio:browser reports 153 passed, 41 skipped.

Screenshots. Before = 5557e72 (this PR before the review), After = 23b6788, Prototype = stack-08. Desktop frames are cropped to the dialog column. Red boxes mark real Before/After differences: the Time input and the helper text. Amber boxes mark what was reviewed and is unchanged: the Date input and the disabled Create button.

Time typed before the date, desktop light. Before, the time was dropped; After, it stays and the reason is shown.

Date segment cleared with Backspace, desktop light. Before, the time was lost; After, it stays at 15:30.

Helper text after typing only a name, desktop light.

Dark and 390 px frames

Time typed before the date:



Date segment cleared:



Helper text:



What changed

  • Run at. A one-shot task's run time comes from a Date and a Time input, grouped in a fieldset named "Run at", instead of the native datetime-local widget. A date on its own runs at 9:00. A cleared time segment keeps the last time. No date means no run time. The "Saves as …" line stays.
  • Submit gate. scheduleFormProblem (in schedule-form-value.ts) checks the name, the prompt, and the cron expression or a valid run time. The submit button stays disabled until it finds nothing, and Enter in an incomplete form sends nothing. The fields keep their native required. An edit checks the stored, locked trigger.
  • Toasts. "Scheduled task “name” created" after a create, and "… updated" after an edit from the inventory or the detail page. A refused save shows the refusal in the dialog and raises no toast. The Toaster was already mounted.
  • Width. sm:max-w-lg on the dialog. The Dialog primitive already defaults to it, so nothing moves.

Differences from the prototype

Kept deliberately (#2299): no timezone, run-limit or retry controls (the wire fields round-trip), the locked trigger and tool profile on edit, and no rename (#2145). The dialog keeps Studio's title, description and button labels ("Schedule a task", "Create schedule").

No regression

  • Pins first (3ec761fe8, schedule-dialog.pinning.test.tsx, 6 tests), on the dialog as mecatui: subagent / Task visibility (agent hierarchy dashboard) #1 leaves it: the cron create body in the browser's time zone; a one-shot wall clock sent as its absolute instant (set through whichever inputs the dialog has); the inline refusal; the required name and prompt; the edit body that keeps the stored trigger and limits without a name; and a stored one-shot saved unchanged. 6/6 pass before and after.

  • Change (5557e7219): schedule-dialog.test.tsx (9 tests) and 4 new unit tests in schedule-form-value.test.ts. No existing test changed.

  • Mutation proof:

    Mutation Failing tests
    Submit not disabled by the problem 3
    Submit handler's guard removed 1 (Enter)
    A cleared time segment resets the time 1
    No 9:00 default for a bare date 3
    No toast from the inventory 2
    No toast from the detail page 1
    Toast raised before the save 1 (refusal)

Screenshots

Before = 8cc7c23 (main), After = 5557e72, Prototype = stack-08. Before is main, so the inventory changes from #1 also show behind the dialog. The prototype's mock BFF gained echo handlers for POST/PUT /schedules so its toasts could be captured.

Run once, desktop light. Date and Time are separate, as in the prototype, and Create is enabled once the date is set.

Toast after a create, desktop light. Matches the prototype's toast.

Empty create dialog, desktop light. Create starts disabled, as in the prototype.

All other frames

Run once, dark and phone:



Empty create dialog, dark and phone:



Toast after a create, dark and phone:



Edit dialog (unchanged apart from the gate):




Toast after an edit from the detail page:




Gates

Through the shared lock, on 5557e7219:

  • task studio:check passes: lint, typecheck and the generated-artifact check pass; server tests 252/252; web tests 1666/1666 (192 files).
  • task studio:browser at the top of the stack: 136 passed, 38 skipped.
  • No BFF contract changes.

🤖 Generated with Claude Code

@peppescg
peppescg added this pull request to stack #2373 October 15, 2026 17:49

@kantord kantord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, the pins-first approach and the mutation table made the change easy to review, and the toast and the one-shot request body look right. I'm requesting changes for two things in the dialog, both small.

1. The time and date fields are not independent

The form shows a Date field and a Time field, but it keeps the run time as one combined string. When there is no date that string is empty, so the time has nowhere to live:

  • joinOneShot returns "" when the date is empty, which drops the time.
  • oneShotParts then gives the controlled Time input an empty value.

What that means for a user, reading the code:

  • Time typed before a date. The time is discarded, and picking a date afterwards gives 09:00 rather than what was typed. schedule-dialog.test.tsx pins this ("a date on its own runs at 9:00"), so I understand it is deliberate. With two visibly separate fields it is still surprising.
  • Date cleared or edited after a time is set. This goes through the same path, so I expect the time to be lost the same way. I couldn't run it in a browser, so please confirm. There is a test that clearing the date disables Create, but none that checks the time afterwards.
  • Asymmetry. The Time handler already ignores transient empty values (L286-L297: "Segment editing fires transient empty values; keep the last time"). The Date handler (L276-L283) has no equivalent guard.

Suggested fix: keep the time in its own state (or a oneShotTime field on FormValue) and derive the combined value from the date plus that time, defaulting to 09:00 only when no time has been chosen. Giving the Date input the same empty-value guard would cover the segment-editing case, but not the time-first one.

2. A disabled Create button gives no reason

scheduleFormProblem returns messages like "Name is required." (schedule-form-value.ts L142-L158), but problem is only used as a null check (L163, L168, L367). The message is never shown, so the button is disabled with no stated reason, and Enter in an incomplete form silently does nothing. The native required bubbles never appear either, because the form can't reach validation.

Suggested fix: show problem as muted helper text near the footer (or via aria-describedby on the button) once the form has been touched.

Nits, not blocking

  • toastScheduleSaved was inserted between canEditSchedule and its DECISION doc comment, so that comment now sits above the wrong function and canEditSchedule has none.
  • sm:max-w-lg (L182) is the Dialog primitive's default already, so the change and its test ("is sm:max-w-lg wide") don't test anything.
  • The Date and Time inputs are now named only by aria-label inside the "Run at" fieldset, and the old schedule-run-at id is gone. That looks fine, just noting it.

This PR is also stacked on #2369 (the toast tests rely on the Toaster added in its test kit), so it can't merge before that one.

Fully or partially written by an AI agent.

peppescg added a commit that referenced this pull request Oct 16, 2026
…reate is disabled

Review of #2370.

- The one-shot time has its own form field (`oneShotTime`), and the run time
  is derived from the date plus that time. A time chosen before the date is
  kept, and clearing or editing the date (or one of its segments) never
  touches the time. A date runs at 9:00 only when no time was ever chosen.
- Once the form has been edited or submitted, the reason Create is disabled
  (`scheduleFormProblem`) shows as muted helper text above the footer and
  describes the Create button through `aria-describedby`.
- `toastScheduleSaved` moves above the `DECISION` comment, so the comment
  documents `canEditSchedule` again.
- Drop `sm:max-w-lg` and its test: it is the Dialog default.

Pins first: four new pins in `schedule-dialog.pinning.test.tsx` pass on
the parent commit and after. A browser test covers the time-first and the
keyboard date-clear cases in Chromium.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
peppescg added a commit that referenced this pull request Oct 16, 2026
…lper text)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@peppescg

Copy link
Copy Markdown
Contributor Author

Thanks Daniel, both points were right. I confirmed the date-cleared case in Chromium: Backspace on one date segment wiped the time. Everything is in 7418f81, on top of the original commits:

  1. Date and Time are independent. The time now has its own form field (oneShotTime), and the run time is derived from the date plus that time. A time typed before the date is kept. Clearing or editing the date (or one segment of it) never touches the time. 9:00 applies only when no time was ever chosen. On the Date guard: an empty date no longer drops the time, but it still clears the run time, so Create can't send a date the input no longer shows. A Playwright test covers time-first and the keyboard date-clear on desktop and phone.
  2. Disabled Create gives a reason. Once the form is touched (an edit or a submit attempt), scheduleFormProblem's message shows as muted helper text above the footer and describes Create through aria-describedby. Enter in an incomplete form now shows it too.
  3. Nits: toastScheduleSaved moved above the DECISION comment, so it documents canEditSchedule again. sm:max-w-lg and its test are gone.

One difference from the prototype: its joinOneShot fills in today's date when only a time is set. I kept the date empty so a half-edited date doesn't snap to today. #2371 and #2372 are restacked.

@peppescg
peppescg requested a review from kantord October 16, 2026 14:07

@kantord kantord left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the fast and thorough fix, and for confirming the date-cleared case in Chromium. I read 7418f81d5 against both of my requests and they are resolved:

  • Date and Time are independent. The time now lives in its own oneShotTime field. A time typed before the date is kept, and clearing or editing the date never touches it, while an empty date still clears the run time so Create can't send a date the input no longer shows.
  • Disabled Create gives a reason. The scheduleFormProblem message now shows once the form is touched or a submit is tried, and it is linked to Create with aria-describedby.
  • Nits. The doc comment is back above canEditSchedule, and the no-op sm:max-w-lg and its test are gone.

I'm fine with keeping the date empty when only a time is set, rather than filling in today's date as the prototype does. I read the diff and CI but did not run the new Playwright test.

Approving. Merge order is still after #2369.

Fully or partially written by an AI agent.

@peppescg
peppescg force-pushed the feat/studio-schedules-2299-dialog branch 2 times, most recently from b7ea534 to 893131a Compare October 16, 2026 19:56
peppescg and others added 4 commits October 16, 2026 22:16
…ng the run time

Pins the create body (cron in the browser's time zone, and a one-shot
wall clock sent as its absolute instant), the inline refusal, the
required name and prompt, and the edit body that keeps the stored
trigger and limits. Every test passes on the current dialog.

Refs #2299

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- A one-shot task takes its run time from separate Date and Time
  inputs, grouped under "Run at", instead of the native datetime-local
  widget, as in the prototype. A date alone runs at 9:00; a cleared
  time segment keeps the last time; no date means no run time.
- The submit button stays disabled until scheduleFormProblem finds
  nothing missing: the name, the prompt, and the cron expression or a
  valid run time. Enter in an incomplete form sends nothing. The fields
  keep their native required marks. An edit checks the stored, locked
  trigger.
- A Sonner toast confirms a created task and a saved edit, from the
  inventory and from the detail page. A refused save raises no toast.
- The dialog states sm:max-w-lg, which the Dialog primitive already
  defaults to.

Refs #2299

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…reate is disabled

Review of #2370.

- The one-shot time has its own form field (`oneShotTime`), and the run time
  is derived from the date plus that time. A time chosen before the date is
  kept, and clearing or editing the date (or one of its segments) never
  touches the time. A date runs at 9:00 only when no time was ever chosen.
- Once the form has been edited or submitted, the reason Create is disabled
  (`scheduleFormProblem`) shows as muted helper text above the footer and
  describes the Create button through `aria-describedby`.
- `toastScheduleSaved` moves above the `DECISION` comment, so the comment
  documents `canEditSchedule` again.
- Drop `sm:max-w-lg` and its test: it is the Dialog default.

Pins first: four new pins in `schedule-dialog.pinning.test.tsx` pass on
the parent commit and after. A browser test covers the time-first and the
keyboard date-clear cases in Chromium.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t guard

The hint that says why Create is disabled is right-aligned from sm:, the
breakpoint where DialogFooter's buttons turn into a right-aligned row. Moving
it to the 500px pivot would right-align it above stacked buttons between 500
and 639px, so it stays on sm: and the guard lists it with that reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@peppescg
peppescg force-pushed the feat/studio-schedules-2299-dialog branch from 893131a to c684c4e Compare October 16, 2026 20:22

This branch has not been deployed

No deployments
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