feat(automation): create card model picker, split entry, and chat echo#1143
Conversation
Manual side of issue #950 PR7. Adds the create-via-form path; the create-via-chat toggle and the NL echo card land in follow-up commits. - store: globalSync.automation.create wraps sdk.automation.create with a revision-gated apply (mirrors pause/runNow). - schedule: draft -> AutomationCreateInput builder (cron shapes the read side already humanizes; "once" -> one-shot fireAt via luxon in the definition timezone) plus a two-level Schedule popover (frequency inline list, time, weekday, raw cron). - create card: useDialog modal (title + prompt + Project | Schedule | Model bar). Model reuses the composer ModelSelectorPopover on an independent state so picking here never rewrites the composer selection. - templates: Daily brief / Weekly review / Project monitor prefill the card (empty-state buttons + in-card Use template); no separate gallery. - surface: New automation entry + empty-state template buttons; opens the card and selects the created automation. layout passes projectID. - i18n: zh + en create/template keys. Pinned per handoff: context=fresh, recurring stop=never, no worktree. Verified: bun run typecheck (packages/app) clean; eslint clean on changed files. Visual snap + E2E land with the remaining PR7 slices.
… via snap The Automations panel renders outside the per-directory LocalProvider, so the create card crashed on useLocal (the composer model picker can't be reused there). Resolve the model from the project default via useProviders in the surface and pass it in read-only; a panel-local model/variant picker is a follow-up. - create card: drop useLocal / ModelSelectorPopover; the model is a read-only chip seeded from the surface default. Fix bottom-bar overflow so Cancel/Create stay visible with a long project name (chips shrink, buttons pinned shrink-0). - surface: resolve the default model via useProviders().default(). - snap: extend automations-surface to open + fill the create card and expand the schedule popover (visual + user-path coverage). Verified: typecheck + eslint clean; bun run snap automations-surface passes (6-shot grid reviewed: empty / list / list-hover / detail / create-card / schedule).
Round out the issue #950 PR7 create surface. Model picker: the create card now drives the real composer model/variant picker instead of a read-only chip. The Automations panel renders outside the per-directory LocalProvider, so a panel-local controller (automation-model-state) backs the picker from useModels() plus dialog-local model/variant signals. model-picker.tsx now types its `model` prop against a narrow ModelPickerState interface that the full useLocal().model satisfies as a superset, so composer call sites are unaffected. The card self-seeds with the same priority as the composer (last-used, then project default, then first connected), matching the "default current/last model" spec. Split entry: "New automation" opens a menu with Create via chat / Create manually. Create via chat leaves the panel and opens a fresh session in the current project, prefilled with a short guiding prompt via the reactive ?prompt= bootstrap (works whether or not we are already on the new-session route). Echo card: when the agent runs the automate tool in chat, a lightweight card echoes the resolved definition (read from tool metadata, never parsed from prose) with a jump into the Automations panel focused on that automation. Navigation flows through the UI data context (onNavigateToAutomation) and a module-level bridge (automations-navigation) so the card stays app-agnostic; the panel reads the requested selection once on mount. Chinese copy: rename 自动化 to 定时任务 across app/ui zh dictionaries; it reads more naturally. English copy keeps "Automation(s)".
Functional E2E (automations-panel.spec.ts): create manually adds an automation and lands on its detail + list; an empty-state template prefills the create card; create via chat closes the panel and opens a session whose composer carries the guiding prompt; the automate tool card jumps into the panel focused on the new automation. Snap (automations-surface.snap.ts): follow the split entry through the New automation menu before Create manually, and add the menu to the grid.
📝 WalkthroughWalkthroughThis PR implements a full automation creation experience: schedule domain and picker, templates, model/variant picker refactor, a multi-field create dialog that calls a new global-sync create API, Automations surface/template integration, navigation wiring for deep-links and tool-card jumps, UI refinements, i18n, and extensive e2e/snapshot tests. ChangesAutomation Creation and Panel Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Suggested priority: P2 (includes user-path files (packages/app/src/components/prompt-input/model-picker.tsx, packages/app/src/context/global-sync.tsx, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/automations/automation-create-dialog.tsx, packages/app/src/pages/automations/automation-model-state.ts, packages/app/src/pages/automations/automation-schedule-form.ts, packages/app/src/pages/automations/automation-schedule-picker.tsx, packages/app/src/pages/automations/automation-templates.ts, packages/app/src/pages/automations/automations-surface.tsx, packages/app/src/pages/directory-layout.tsx, packages/app/src/pages/layout.tsx, packages/app/src/utils/automations-navigation.ts)).
P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.
There was a problem hiding this comment.
Code Review
This pull request introduces a manual creation flow for automations, featuring a new creation dialog, schedule picker, and starter templates, alongside integration with chat-based automation tools. Key feedback from the review highlights several critical improvements: ensuring reactive updates for deep-linked automations by replacing onMount with createEffect, defensively handling potentially missing metadata in the automate tool card to prevent runtime crashes, adding timezone validation fallbacks to avoid serialization errors, and validating custom cron inputs before allowing submission.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/app/src/pages/automations/automations-surface.tsx (1)
117-129: ⚡ Quick winConsider adding a directory guard in
openCreate.The function gates on
projectIDbut readsdirectorywithout validation. Ifprops.directory()returns an empty string (unlikely but possible during edge-case routing transitions), the dialog will be opened with invalid data.🛡️ Add directory guard
const openCreate = (template?: AutomationTemplate) => { const projectID = props.projectID() - if (!projectID) return const directory = props.directory() + if (!projectID || !directory) return dialog.show(() => (🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/automations/automations-surface.tsx` around lines 117 - 129, openCreate currently guards only projectID but still reads props.directory(); add a guard to validate directory (e.g., const directory = props.directory(); if (!directory) return) before calling dialog.show so AutomationCreateDialog never opens with an empty/invalid directory. Update the openCreate function to check both projectID and directory (use a trimmed truthy check if needed) and then pass the validated directory into AutomationCreateDialog while keeping the existing onCreated behavior (setSelectedID).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app/src/pages/automations/automation-schedule-picker.tsx`:
- Around line 39-42: The setTime handler currently accepts any integers from the
"HH:MM" string and can produce out-of-range hours/minutes; update setTime to
validate and clamp values before calling props.onChange: parse hour/minute as
integers (still guard with Number.isInteger), then clamp hour to 0–23 and minute
to 0–59 (or bail if parsing fails), build the sanitized schedule object from
props.value with the clamped hour/minute, and pass that to props.onChange so
downstream cron/oneshot generation always receives valid time fields.
---
Nitpick comments:
In `@packages/app/src/pages/automations/automations-surface.tsx`:
- Around line 117-129: openCreate currently guards only projectID but still
reads props.directory(); add a guard to validate directory (e.g., const
directory = props.directory(); if (!directory) return) before calling
dialog.show so AutomationCreateDialog never opens with an empty/invalid
directory. Update the openCreate function to check both projectID and directory
(use a trimmed truthy check if needed) and then pass the validated directory
into AutomationCreateDialog while keeping the existing onCreated behavior
(setSelectedID).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 0f2802d9-f05c-4b4b-9406-85f25e602c1c
📒 Files selected for processing (20)
packages/app/e2e/automations/automations-panel.spec.tspackages/app/e2e/snap/automations-surface.snap.tspackages/app/src/components/prompt-input/model-picker.tsxpackages/app/src/context/global-sync.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/app/src/pages/automations/automation-create-dialog.tsxpackages/app/src/pages/automations/automation-model-state.tspackages/app/src/pages/automations/automation-schedule-form.tspackages/app/src/pages/automations/automation-schedule-picker.tsxpackages/app/src/pages/automations/automation-templates.tspackages/app/src/pages/automations/automations-surface.tsxpackages/app/src/pages/directory-layout.tsxpackages/app/src/pages/layout.tsxpackages/app/src/utils/automations-navigation.tspackages/ui/src/components/message-part/tools/automate.tsxpackages/ui/src/components/message-part/tools/index.tspackages/ui/src/context/data.tsxpackages/ui/src/i18n/en.tspackages/ui/src/i18n/zh.ts
Address design review on the PR7 create surface: - Drop the unclickable Project knob; the surface already pins the automation to its current directory. - Let the Model knob shrink and truncate long model names instead of overflowing the bar. - Rebuild the Schedule popover on the design-system Select (frequency, hour/minute, weekday) instead of hand-rolled and native controls, so it stops drifting from the shared picker contract. - Reduce the schedule to four frequencies (daily / weekdays / weekly / once); drop hourly and the opaque custom-cron option. Weekdays is now its own frequency, so no weekday multi-select is needed. - Unify chevrons at size-3 and use plus-small for the New automation CTA. - Remove the now-orphaned hourly/custom frequency i18n keys.
The shared UI Popover and Dialog context hand-rolled window-capture focusin/pointerdown/keydown dismiss listeners that were not layer-aware. A popover opened inside a modal dialog therefore fought the dialog's focus trap instead of pausing it, and Escape was handled non-layer-aware. Drop the hand-rolled machinery and rely on Kobalte's native DismissableLayer: layer-aware Escape (top-most layer closes first) and outside dismiss. This is the prerequisite for making the create-card pickers modal — with the old window listeners, modal alone still flashed.
The dropdown chevron-down was 14px, competing with 13px body text. Drop it to 12px in icon.css (box + svg) and select.css so the affordance sits quietly below the text. Remove the now-dead size-3 utility on the Automations CTA chevron — the global rule wins on specificity anyway.
The model picker hand-rolled its focus-outside dismiss with window listeners and an isPickerContentTarget guard. Inside a modal dialog (the Automations create card) the parent dialog's focus trap steals focus on open, the non-layer-aware dismiss treated that as a focus-outside and flashed the picker shut, then wedged it closed because focus stayed trapped on the dialog title. Delegate the dismiss to Kobalte's native DismissableLayer, which already recognises the parent dialog as an ancestor layer and the nested Thinking popover as a child layer. Keep only a tiny closeCause flag so the composer can still restore focus to the prompt on escape/select; an outside dismiss leaves focus where it went. Add a modal opt-in (default false preserves the composer's close-on-focus-leave behaviour) so the create card can run the picker modal and trap focus back into itself. Covered by a new prompt-footer e2e: Escape closes the picker and returns focus to the prompt.
…er, modal pickers Last PR of the Automations series (#950). Replace the standalone AutomationSchedulePicker with an inline schedule row (AutomationScheduleControls: frequency segmented switch + weekday/time popovers) and add an AutomationFolderPicker so the automation can be filed against any open project instead of only the active one. All four create-card pickers (time, weekday, folder, model) run modal inside the dialog so the parent dialog's focus trap can't flash them shut, and every trigger shares the same cursor-pointer knob styling. Narrow the dialog to 600px to match the denser layout. E2E: schedule picker opens/selects/layers-escape under the dialog focus trap, and the model picker survives the focus trap and reopens.
The pickers used cursor-pointer, which gives the web hand cursor on hover. PawWork follows the native macOS convention — Button, popover content, and reset <a> all use cursor: default — so the pickers stuck out. Match them: explicit cursor-default keeps the arrow over the div-based triggers (and avoids the text I-beam over their labels).
Threading modal onto the create card's ModelSelectorPopover (the earlier flash fix) pushed the same focus-trap flash down to the nested thinking submenu: the outer popover is now modal, so its focus scope keeps an active trap; when the non-modal thinking popover opened and autofocused its portalled content, the outer trap immediately pulled focus back and the non-modal inner DismissableLayer dismissed itself — the submenu flashed open then shut and looked stuck closed. Fix at the lowest correct seam: thread modal down to ThinkingLevelSection so the inner popover's modality matches the outer. Modal outer (create card) -> modal inner traps focus into itself and stays open; non-modal outer (composer) -> inner stays non-modal, unchanged. Verified: e2e time-samples the submenu option count ([4,0,0..] before, [4,4,4..] after) so a single auto-retrying toBeVisible can't pass on the flash frame; red before the fix, green after. Composer thinking suite unaffected.
The create card files an automation against any open project via the folder picker, but its model list, default and validation came from useModels/useProviders, which are scoped to the *routed* project (params.dir) — not the selected folder. Switching the folder to project B while routed on A left the picker showing A's models and seeding A's default, so submitting filed B's automation with a model B may not have connected: a failed create or an unintended model. Extract createModelsView(providers, visibility) from the models context and add useScopedModels(dir), which builds the view from a directory's providers while sharing the global per-user prefs (visibility, recent, variant). useProviders now takes an optional directory override. The create card scopes both to its selected folder and re-seeds/validates the model when the folder changes, so it can only ever submit a model the target project can run. Unit test (createModelsView): the view reflects only the providers it is given — swapping scope swaps models/find — and honours the shared visibility map.
Two review concerns (#1143) verified as already-correct, locked with regression guards rather than speculative code: - A second automate tool-card jump must focus its own automation. The surface is <Show>-gated and remounts on each open, so onMount already re-reads the requested id; the test drives the real two-card flow. - A model picker outside dismiss must leave focus where the pointer went (e.g. the composer prompt), not snap back to the trigger. Kobalte's native DismissableLayer already keeps focus off the trigger on outside interaction; the test guards that the dismiss refactor didn't regress it.
The surface read the pending deep-link automation id once on mount, which only worked because the lazy <Show> remounts the panel on every open. That made a reactive signal (requestedAutomationID) load-bearing on an implicit remount, an easy thing to break later. Track it with createEffect so the selection applies whether the panel was freshly mounted or already open.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/app/src/pages/automations/automations-surface.tsx (1)
118-122:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard create dialog opening when directory context is missing.
openCreatechecksprojectIDbut notdirectory. Ifdirectoryis empty, Line 122 still opensAutomationCreateDialog, and the submit path later calls create with invalid context.Proposed fix
const openCreate = (template?: AutomationTemplate) => { const projectID = props.projectID() - if (!projectID) return const directory = props.directory() + if (!projectID || !directory) return dialog.show(() => ( <AutomationCreateDialog directory={directory} projectID={projectID} template={template} onCreated={(definition) => setSelectedID(definition.id)} /> )) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/app/src/pages/automations/automations-surface.tsx` around lines 118 - 122, openCreate currently guards only projectID but not directory, so AutomationCreateDialog can open with an empty directory leading to invalid create calls; update openCreate to fetch directory via props.directory(), return early (do not call dialog.show) when directory is falsy, and ensure any passed template handling still respects this guard so the submit path (which calls create using the directory) never runs without a valid directory; reference the openCreate function and AutomationCreateDialog to locate where to add the guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/app/src/pages/automations/automations-surface.tsx`:
- Around line 118-122: openCreate currently guards only projectID but not
directory, so AutomationCreateDialog can open with an empty directory leading to
invalid create calls; update openCreate to fetch directory via
props.directory(), return early (do not call dialog.show) when directory is
falsy, and ensure any passed template handling still respects this guard so the
submit path (which calls create using the directory) never runs without a valid
directory; reference the openCreate function and AutomationCreateDialog to
locate where to add the guard.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bc709b3b-3339-41d5-9135-1a166e24a7d3
📒 Files selected for processing (7)
packages/app/e2e/automations/automations-panel.spec.tspackages/app/e2e/prompt/prompt-footer-focus.spec.tspackages/app/src/context/models.test.tspackages/app/src/context/models.tsxpackages/app/src/hooks/use-providers.tspackages/app/src/pages/automations/automation-create-dialog.tsxpackages/app/src/pages/automations/automations-surface.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/app/e2e/automations/automations-panel.spec.ts
- packages/app/src/pages/automations/automation-create-dialog.tsx
Summary
Completes the issue #950 PR7 create surface (manual create card landed earlier in this branch):
LocalProvider, a panel-local controller (automation-model-state) backs the picker fromuseModels()plus dialog-local model/variant signals.model-picker.tsxnow types itsmodelprop against a narrowModelPickerStateinterface that the fulluseLocal().modelsatisfies as a superset, so composer call sites are untouched. The card self-seeds with the composer's priority (last-used → project default → first connected), matching the "default current/last model" spec.?prompt=bootstrap (works whether or not we are already on the new-session route).automatetool in chat, a lightweight card echoes the resolved definition (read from tool metadata, never parsed from prose) with a jump into the Automations panel focused on that automation. Navigation flows through the UI data context (onNavigateToAutomation) and a module-level bridge (automations-navigation) so the card stays app-agnostic; the panel reads the requested selection once on mount.Why
PR7 is the create slice of #950. The remaining gaps were a usable model control (the read-only chip couldn't pick a model), the conversational entry (the product's core surface), and the create echo so a chat-created automation is discoverable.
Related Issue
#950
Human Review Status
Pending
Review Focus
model-picker.tsx: theModelState→ModelPickerStateprop retype. The fulluseLocal().modelis a structural superset, so composer behavior is unchanged — worth a second look.uidata context gained an optionalonNavigateToAutomation; theautomatetool renderer is new. Both are additive.requestedAutomationIDis set before the panel opens and consumed once on the surface's mount (panel is lazily mounted), cleared on manual opens.Risk Notes
Renderer-only changes (SolidJS router navigation, dialog, message-part rendering). No
platform.*/window.api/ preload / IPC / updater / deep-link / packaging surface is touched, sodev:desktopwas not required;snap(web Chromium) plus Playwright E2E cover the changed surface. Theuidata context change is an added optional prop (backward compatible).How To Verify
Screenshots or Recordings
Visual check via the
automations-surfacesnap target (grid underdocs/design/preview/screenshots/automations-surface.png): the split New-automation menu, the filled create card with the model chip, and the schedule popover all render as expected.Checklist
bug,enhancement,task,documentation.app,ui,platform,harness,ci.P0,P1,P2,P3.Pending,Approved by @<reviewer>, orNot required: <reason>.dev, and my PR title and commit messages use Conventional Commits in English.Summary by CodeRabbit
New Features
Tests
Refactor
Internationalization