feat(scheduled-tasks): replace the schedules table with calendar views#4979
feat(scheduled-tasks): replace the schedules table with calendar views#4979emir-karabeg wants to merge 4 commits into
Conversation
…dcrumbs - Resource.Table: remove internal sorting (defaultSort/sortValues) and the emptyMessage state — rows render in the order given, chrome always paints - Resource: root is now the positioning context for overlays; consumers (files, tables, knowledge, document) wrap detail views in <Resource> instead of hand-rolled divs - ResourceHeader: root titles no longer truncate during initial layout; LocationFocusVeil gates the portal on mount to fix a hydration mismatch - Toasts: drop the StackDismiss ring and stack countdown — each toast runs its own timer; remove the Mod+E clear-notifications command; align toast typography and icons with chip chrome - Breadcrumbs: use the canonical '…' placeholder while names load - incident.io: fix display name and catalog slug (with redirect) - Add dev:capped / dev:full:capped scripts with a 4GB heap cap
Add month/time calendar views for scheduled tasks with toolbar, event chips, and a create-task modal, backed by calendar-grid and schedule-events utils (with tests) and a use-calendar hook. Replace the old schedule-modal/context-menu flow. Rename the "Mothership" agent to "Sim" and the chat surface to "Chat" across landing copy, constitution, block metadata, API error messages, and copilot/data-drain internals. Drop unused workspace route layouts.
A non-modal DropdownMenu portals outside an open dialog's react-remove-scroll subtree, so its content cannot be wheel-scrolled (e.g. the time picker in the scheduled-task create modal). ModalContent now marks its subtree via an InsideModal context, and the emcn DropdownMenu root upgrades itself to modal inside dialogs so it mounts its own scroll lock and focus scope; page-level menus keep their consumer-chosen modality. Also stretch the create-task modal's date/time chip controls to full width and drop the dead EDGE_GUTTER constant left behind by the equal-tracks calendar layout.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
@greptile run |
PR SummaryMedium Risk Overview Resource shell changes: emcn: Product copy renames Mothership/Copilot to Sim (agent) and Chat (surface) across landing, docs, settings, blocks, APIs, and constitution; minor label fixes (e.g. incident.io). Removes the Mod+E “clear notifications” global command from workspace permissions. Reviewed by Cursor Bugbot for commit cdb3ae4. Configure here. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cdb3ae4. Configure here.
| if (onSubmit) onSubmit(draft) | ||
| else logger.info('Scheduled task draft captured (not persisted this phase)', draft) | ||
| close() | ||
| } |
There was a problem hiding this comment.
Modal form not reset on close
Medium Severity
Closing the create modal does not clear local prompt, launchDate, or launchTime. Reopening from the header keeps the same React key (none), so cancelled or submitted drafts reappear in the form.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cdb3ae4. Configure here.
|
|
||
| const next = useCallback(() => setAnchor((current) => advanceAnchor(current, scope, 1)), [scope]) | ||
| const prev = useCallback(() => setAnchor((current) => advanceAnchor(current, scope, -1)), [scope]) | ||
| const goToday = useCallback(() => setAnchor(new Date()), []) |
There was a problem hiding this comment.
Today highlight uses stale date
Medium Severity
today is fixed at hook mount while goToday sets anchor to the current time. After midnight or a long session, Today navigation and isToday styling can disagree because the grid still compares days against the old today value.
Reviewed by Cursor Bugbot for commit cdb3ae4. Configure here.
Greptile SummaryThis PR replaces the scheduled-tasks table with a month/week/day calendar view, adds a new
Confidence Score: 3/5The calendar UI is well-structured and the emcn dialog/dropdown fix is sound, but two issues need attention before this reaches users: the Schedule button silently drops the user input with no feedback, and the frozen today reference means the time indicator and today-column highlight become incorrect after midnight without a page reload. The calendar math is clean and fully tested. The InsideModal/DropdownMenu fix is correct. However, the CreateTaskModal submit path closes without any user feedback — a user who fills in the prompt and clicks Schedule gets no toast, no error, no confirmation, and no task is created. Additionally, useCalendar freezes today at mount: after midnight the isToday column marking and the CurrentTimeIndicator placement diverge from actual current time, persisting until the component unmounts. use-calendar.ts (frozen today) and create-task-modal.tsx (silent no-op submit) need attention before merging to a user-facing environment. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
ST[ScheduledTasks page] --> UC[useCalendar hook]
UC --> SC[ScheduleCalendar]
SC --> CT[CalendarToolbar]
SC --> MG[MonthGrid]
SC --> TG[TimeGrid]
TG --> CTI[CurrentTimeIndicator]
ST --> CTM[CreateTaskModal]
CTM --> CD[ChipDropdown time picker]
UC -- selectSlot --> CTM
UC -- openCreate --> CTM
Reviews (1): Last reviewed commit: "fix(emcn): force dropdown menus modal in..." | Re-trigger Greptile |
| const handleSubmit = () => { | ||
| const draft: CreateTaskDraft = { | ||
| prompt: prompt.trim(), | ||
| launchDate, | ||
| launchTime, | ||
| timezone: DEFAULT_TIMEZONE, | ||
| } | ||
| if (onSubmit) onSubmit(draft) | ||
| else logger.info('Scheduled task draft captured (not persisted this phase)', draft) | ||
| close() |
There was a problem hiding this comment.
Stub submit silently closes with no user feedback
When onSubmit is not supplied (the current call site passes none), handleSubmit logs to the console and closes the modal. A user who fills in the prompt and clicks "Schedule" sees the modal disappear without any toast, error, or "coming soon" indicator — they will believe a task was created. Even as a deliberate stub, showing a brief toast.info("Scheduling support coming soon") or keeping the button disabled until the feature is wired would avoid this false-success UX.
There was a problem hiding this comment.
Fixed in 2e52a5d — the stub path now shows toast.info('Scheduling is not available yet — this task was not created') so there's no false-success UX while persistence is pending the one-time-launch contract work.
| const today = useMemo(() => new Date(), []) | ||
| const [scope, setScope] = useState<CalendarScope>('week') | ||
| const [anchor, setAnchor] = useState<Date>(() => new Date()) | ||
| const [selectedSlot, setSelectedSlot] = useState<CalendarSlot | null>(null) | ||
| const [isCreateOpen, setIsCreateOpen] = useState(false) | ||
|
|
||
| const next = useCallback(() => setAnchor((current) => advanceAnchor(current, scope, 1)), [scope]) | ||
| const prev = useCallback(() => setAnchor((current) => advanceAnchor(current, scope, -1)), [scope]) | ||
| const goToday = useCallback(() => setAnchor(new Date()), []) |
There was a problem hiding this comment.
Frozen
today causes stale isToday marks after midnight
today is captured once with useMemo(() => new Date(), []) and never refreshed. The grid's isToday flags and the today-column indicator in TimeGrid (which only renders <CurrentTimeIndicator /> on day.isToday) are both derived from this frozen value. After midnight, the indicator stays on the previous day's column even though CurrentTimeIndicator internally ticks to the new time. Similarly, goToday sets the anchor to new Date() (actual today) but the today prop fed to buildCalendarGrid remains yesterday, so the wrong column gets highlighted. Adding a useEffect that refreshes today at midnight — or simply not memoizing it — would fix the mismatch.
There was a problem hiding this comment.
Fixed in 2e52a5d — today is now state refreshed by a minute-interval poll guarded by isSameDay, so it only re-renders on actual day rollover. Chose the poll over a one-shot timeout aimed at midnight because intervals survive device sleep/clock changes.
| const target = headerHeight + timeToOffset(new Date()) - region.clientHeight / 2 | ||
| region.scrollTo({ top: Math.max(0, target) }) |
There was a problem hiding this comment.
The
scrollTo call has no behavior option, so the jump is instant on scope change and on Today clicks. Adding behavior: 'smooth' gives a nicer orientation cue when the view shifts.
| const target = headerHeight + timeToOffset(new Date()) - region.clientHeight / 2 | |
| region.scrollTo({ top: Math.max(0, target) }) | |
| const target = headerHeight + timeToOffset(new Date()) - region.clientHeight / 2 | |
| region.scrollTo({ top: Math.max(0, target), behavior: 'smooth' }) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Adopted with a tweak in 2e52a5d: smooth scrolling applies only to Today presses (an orientation cue), while mount and scope switches keep instant positioning — animating initial placement reads as a glitch rather than a cue. Style note for next time: we prefer behavior: 'smooth' only for user-initiated re-orientation, never for initial positioning.
There was a problem hiding this comment.
That's a clean distinction — smooth only where it communicates intent, instant everywhere else. Rule saved.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
Greptile SummaryThis PR replaces the scheduled-tasks table with a three-scope calendar (month/week/day), adds a
Confidence Score: 4/5Safe to merge for the calendar UI and modal-dropdown fix; the The apps/sim/app/workspace/[workspaceId]/scheduled-tasks/hooks/use-calendar.ts (stale Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant ScheduledTasks
participant useCalendar
participant ScheduleCalendar
participant CreateTaskModal
User->>ScheduledTasks: mount
ScheduledTasks->>useCalendar: "init (scope=week, anchor=now, today=now frozen)"
useCalendar-->>ScheduledTasks: state
ScheduledTasks->>ScheduleCalendar: render(scope, anchor, today)
User->>ScheduleCalendar: click time slot
ScheduleCalendar->>ScheduledTasks: onSelectSlot(date, time)
ScheduledTasks->>useCalendar: selectSlot(date, time)
useCalendar-->>ScheduledTasks: "selectedSlot set, isCreateOpen=true"
ScheduledTasks->>CreateTaskModal: "open=true, slot"
User->>CreateTaskModal: fill prompt, click Schedule
CreateTaskModal->>CreateTaskModal: logger.info stub, no persistence
CreateTaskModal-->>ScheduledTasks: onOpenChange(false)
ScheduledTasks->>useCalendar: closeCreate()
User->>ScheduleCalendar: click Today
ScheduleCalendar->>ScheduledTasks: onToday()
ScheduledTasks->>useCalendar: "goToday sets anchor=new Date()"
ScheduleCalendar->>ScheduleCalendar: scrollSignal++ scrollTo(timeToOffset(now))
|
| * mutations are event-driven; there are no effects. Opens on the `week` scope. | ||
| */ | ||
| export function useCalendar(): UseCalendarReturn { | ||
| const today = useMemo(() => new Date(), []) |
There was a problem hiding this comment.
Stale
today reference after midnight
today is memoized with an empty deps array, so it freezes the date at component mount and is never refreshed. If the page stays open past midnight and the user clicks "Today" (which correctly advances anchor to the new date via setAnchor(new Date())), today remains the previous day — so buildCalendarGrid marks the OLD day as isToday while the actual current day renders unhighlighted. The mismatch is visible: the "today" highlight sits on yesterday while the current-time indicator line correctly tracks the real time.
A simple fix is to derive today from the current clock on each render of the dependent computation (pass new Date() directly into buildCalendarGrid instead of memoizing it), or to refresh it with a daily setInterval.
There was a problem hiding this comment.
Already addressed in 2e52a5d (this comment was reviewed against the prior commit) — today is state refreshed by an isSameDay-guarded minute poll, and goToday now agrees with the highlight after midnight.
| const target = headerHeight + timeToOffset(new Date()) - region.clientHeight / 2 | ||
| region.scrollTo({ top: Math.max(0, target) }) | ||
| }, [scope, scrollSignal]) | ||
|
|
||
| return ( | ||
| <div className='relative flex min-h-0 flex-1 flex-col overflow-hidden'> | ||
| <CalendarToolbar | ||
| scope={scope} | ||
| label={label} | ||
| onPrev={onPrev} | ||
| onNext={onNext} | ||
| onToday={handleToday} | ||
| onScopeChange={onScopeChange} | ||
| /> |
There was a problem hiding this comment.
Scroll-to-now fires before the time-grid DOM is painted on scope switch
useEffect is gated on [scope, scrollSignal]. When the user switches from month to week or day, React re-renders synchronously and schedules the effect — but because the outgoing branch's DOM is still in tree until the paint, region.querySelector('[data-time-grid-header]') can briefly find zero nodes (the month grid has no such element) or a stale layout, so headerHeight evaluates to 0 and the scroll target is computed from timeToOffset alone. On slower devices the first scope-switch may land the user at slightly the wrong scroll position. Wrapping the non-month branch in requestAnimationFrame would guarantee layout is settled before reading heights.
There was a problem hiding this comment.
Declining this one — the mechanism described doesn't occur. React runs passive effects after the commit phase, so when the effect fires on a scope switch the month grid is already unmounted and [data-time-grid-header] is in the DOM; getBoundingClientRect() forces a synchronous fresh layout, so headerHeight can't read 0 or stale. Wrapping in requestAnimationFrame would only delay the scroll by a frame and make the unpositioned grid briefly visible.
There was a problem hiding this comment.
You're right — I had the effect timing wrong. useEffect fires after paint, not just after commit, so by the time the scroll runs on a scope switch the new branch is already in the DOM and getBoundingClientRect() reflects the settled layout. The headerHeight === 0 scenario I described can't happen for the reason you stated. Withdrawing the comment.
…ck, smooth Today scroll - useCalendar: today was frozen at mount, so after midnight the isToday column highlight and the current-time indicator stayed on the previous day. today is now state refreshed by a sleep-resilient minute poll that only re-renders when the calendar day actually changes - CreateTaskModal: the stub submit closed silently, reading as false success; it now shows an info toast that the task was not created - ScheduleCalendar: Today presses scroll smoothly as an orientation cue; mount and scope switches keep instant positioning


Summary
cronExpression, so one-time persistence lands in a follow-up. The grid components already accepteventsByDay/eventsByHourprops so rendering real schedules as events only touches the container latercalendar-grid.ts,schedule-events.ts, 12 tests); view state lives in auseCalendarhook, components follow the one-folder-per-component convention with barrelsDropdownMenuinside dialogs: a non-modal menu portals outside the dialog'sreact-remove-scrollsubtree so its content can't be wheel-scrolled.ModalContentnow marks its subtree via anInsideModalcontext and the menu root upgrades itself to modal inside dialogs; page-level menus keep their consumer-chosen modality. (Depends on the singleton dedupe shipped in fix(deps): dedupe radix focus-scope/dismissable-layer so in-modal dropdowns open #4977)Resourceshell simplifications:Resource.Tabledrops internal sorting and empty-state handling, the root becomes the overlay positioning context, toasts run independent timers with chip-aligned typography, breadcrumbs use the canonical…loading placeholderType of Change
Testing
bunx vitest runon the scheduled-tasks utils — 12/12 passing (grid derivation, anchor advancement, labels, event bucketing)bunx tsc --noEmitclean; biome clean on all touched filesChecklist