Add Schedules section to the Settings UI (#303)#304
Conversation
Wires a new 'Schedules' entry into the Settings page so the user
can manage the active project's scheduled sessions without leaving
the app. The component is a thin client over the existing REST
surface in server/routes/schedules.ts (the CLI is still the
source of truth; no server changes):
* List every schedule (including disabled) with worktree, prompt
preview, trigger type, next/last run, enabled state, source,
and last error inline.
* Create via a form that mirrors the CLI: worktree (from the
project's worktrees), prompt, and either a one-shot ISO
timestamp or a cron expression + timezone. Server-side
validation errors (bad cron / timezone) are surfaced inline.
'createdBy' defaults to 'ui'.
* Toggle enabled/disabled with a Switch.
* Delete with an AlertDialog confirmation.
* View runs for a schedule in a drawer; each run with a
'sessionId' is a button that switches the main view to that
session (mirrors the 'controller://' deep-link path the
transcript already uses).
Editing an existing schedule is intentionally not exposed — the
server has no PUT route for it (non-goal). The form resets on
close; the section refreshes after every mutation.
Plumbing: 'SettingsPage' now accepts an optional 'projectId'
and 'onOpenSession' prop so the section can be project-scoped
and so runs can deep-link to a session. Both are optional so
the page still renders without an active project.
A regression test in
'client/src/components/__tests__/schedules-section.test.tsx'
covers list rendering (cron and one-shot rows, disabled badge,
last run, last error, action controls), the create form (worktree
select, prompt, trigger toggle, both one-shot and cron fields,
empty-worktree state, validation errors), the delete
confirmation flow (data-testid contract + wiring to
'deleteSchedule'), and the enable/disable switch.
Review feedback on #303: instead of scoping the Schedules section to the active project, list every project's schedules in one view so the user can see everything that's about to fire without project-hopping. The server still has no cross-project '/api/schedules' endpoint (the CLI is the source of truth for the data model, per the issue's non-goals). The section fans out client-side: list projects, then list each project's schedules, then merge. Each row carries its projectId plus a denormalised projectName so the action handlers (toggle, delete, runs) don't need a separate lookup. The create form now picks a project first and a worktree second (worktrees are loaded on demand and cached per project). The delete confirm dialog names the project so the user is sure which schedule they're removing. Regression test: 18 cases in schedules-section.test.tsx now cover the project badge per row, the project + worktree selectors, the loading state for worktrees, the empty-projects state, and the cross-project delete wiring (the confirm action must call deleteSchedule with the row's projectId, not the section's). All 632 tests in the suite pass (the one pre-existing pty-manager failure is unrelated).
Without this, calling 'npm run dev' from a clean shell (no exported VITE_API_PORT) would have the dev:server script fall back to the default 3102, while the Vite client reads .env.local via its own loadEnv and proxies to whatever port is in there (e.g. 3129 for this worktree). The two would land on different ports, and every request would 404 — which is exactly the state this worktree was in before the fix. The loader is intentionally minimal (KEY=value with optional quotes, no expansion): we only need the port keys, and pulling in dotenv for three lines is overkill. Values already set in the parent shell win, so a script that exports VITE_API_PORT explicitly is not clobbered. Discovered while debugging the empty-projects / /api/shortcuts 404 state in the dev tooling on issue #303.
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23cc9bc07e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| onOpenSession({ | ||
| sessionId: run.sessionId, | ||
| worktreeId: runsFor.worktreeId, | ||
| }); |
There was a problem hiding this comment.
Pass the schedule project when opening runs
When the Schedules view is showing all projects, this callback only sends the sessionId and worktreeId; App.handleOpenSessionFromSchedule then fills in activeProjectId. If the user opens a run from any project other than the currently active one, the app navigates to the wrong project and SessionView fetches a non-existent or unrelated session. Include runsFor.projectId in the callback payload and use that for navigation.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 409bb18. The onOpenSession callback now carries the run's projectId (optional for future single-project callers), the SchedulesSection always forwards runsFor.projectId, and App.handleOpenSessionFromSchedule prefers the payload's projectId over the active one. Regression test added in schedules-section.test.tsx.
| } catch { | ||
| // One project's schedules failing shouldn't blank the whole | ||
| // view — log the failure server-side and skip it. | ||
| return [] as ScheduleWithProject[]; |
There was a problem hiding this comment.
Surface partial schedule fetch failures
If one project's /schedules request fails, this catch converts it to an empty list, so fetchAllSchedules still resolves and the Settings UI clears the error while silently omitting that project's schedules. In a corrupt-store or transient 500 scenario, users can see an incomplete list or even “No schedules yet” while active schedules still exist and may fire. Preserve enough failure information to show a partial-load error instead of dropping the project.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 409bb18. fetchAllSchedules now returns { schedules, failedProjectIds } (new AllSchedulesResult type) and the section renders a non-fatal amber banner — data-testid="schedule-partial-error" — with the failed project names. Partial failures no longer disappear. Two new test cases pin the contract.
| const list = await fetchScheduleRuns(schedule.projectId, schedule.id); | ||
| setRuns(list); |
There was a problem hiding this comment.
If the user clicks Runs for schedule A and then schedule B before A's request returns, A's later response still calls setRuns(list) while the dialog header now reflects B. On slow networks this can display A's sessions under B and make the run links use B's worktree context. Track the requested schedule id or abort the previous request before updating runs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 409bb18. openRuns bumps a useRef on every call and the async fetcher bails out of all three setState calls (runs, runsError, runsLoading) if a newer request has started. A new test case pins the contract via source matching.
Three fixes for the cross-project Schedules section, all from the Codex review on the PR. P2 #1: Pass the run's projectId to the deep-link callback. In the cross-project view, opening a run for a non-active project was navigating to the wrong session because the click payload dropped the projectId and the App handler fell back to activeProjectId. The onOpenSession contract now carries an optional projectId, the SchedulesSection always forwards runsFor.projectId, and the App handler prefers payload.projectId over activeProjectId. The optional annotation is non-breaking for future single-project callers. P2 #2: Surface partial schedule fetch failures. The cross-project fan-out used to swallow per-project failures and return an empty list, so the UI showed 'No schedules yet' (or an incomplete list) while active schedules were still around. fetchAllSchedules now returns { schedules, failedProjectIds } and the section renders a non-fatal amber banner with the failed project names. AllSchedulesResult is the new public type so future callers don't make the same mistake. P3: Drop stale runs responses. Clicking Runs for A and then B before A resolved would let A's late response overwrite B's dialog. openRuns now bumps a useRef on every call and the async fetcher bails out of the setState calls if a newer request has started. Cheap and self-contained. Tests: 4 new cases pin the source contracts (P2 partial-load banner + testid, run onClick projectId, App handler preference, stale-response guard). 22 of 22 in this file pass; 636 of 637 across the suite (the lone pty-manager failure is pre-existing and unrelated).
Summary
Adds a new Schedules entry to the in-app Settings page so the user can manage scheduled sessions on the active project without leaving the app.
client/src/components/schedules-section.tsx— self-contained component mounted fromSECTIONSinSettings.tsx(data-testid="settings-nav-schedules").client/src/api.ts(thin client over the existing REST surface inserver/routes/schedules.ts; no server changes).SettingsPagenow accepts an optionalprojectIdandonOpenSessionso the section is project-scoped and can deep-link from a run to its session.What works end-to-end
cron/runAt), next/last run, enabled state, source, last error.createdBy: "ui".AlertDialogconfirmation (data-testid="schedule-delete-confirm").sessionIdis a button that switches the main view to that session (same path the transcript'scontroller://links take).Non-goals (deferred per the issue)
Validation
client/src/components/__tests__/schedules-section.test.tsx— 16 new tests covering list rendering (cron + one-shot rows, disabled badge, last run, last error, all action controls), the create form (worktree select, prompt, trigger toggle, both one-shot and cron fields, empty-worktree state, validation errors), the delete confirmation wiring, and the enable/disable switch.settings-responsive.test.tsx,agents-section.test.tsx, etc.). The only failing test in the suite is the pre-existingpty-manager.test.tsfailure, unrelated to this change.vite buildsucceeds.Issue
Closes #303.