feat(automation): Automations panel with lifecycle, runs, and alerts#1053
Conversation
Add directory-scoped automation and run state to the child store, fed by SSE events (definition.updated/deleted, run.updated) and the bootstrap automation.list snapshot. Writes are gated on the monotonic revision with tombstone fencing, so a route response that lands after a newer event is ignored and reconnect re-fetches converge regardless of arrival order. Expose a lazy automation.runs loader for the detail view. Part of #950 PR6 (frontend automations panel), data-layer slice.
Generalize the layout shell surface state from a settings-only boolean to a mutually-exclusive activeSurface (none | settings | automations). Settings keeps its full sidebar+main takeover; automations only takes over the main area and leaves the session sidebar live, so runs stay reachable in All chats. Add an Automations entry to the sidebar top cluster (toggles the surface) and an AutomationsSurface that reads the current project's definitions from the child store: empty state, a read-only list with a humanized schedule summary, and a minimal detail view. Includes a schedule-summary formatter with unit tests and an automations-surface snap target (empty / list / detail). Part of #950 PR6 (frontend automations panel).
Expose pause / resume / delete / runNow on the global-sync automation API. Each mutation applies the authoritative response immediately (revision-gated) so the UI updates without waiting for the SSE round-trip, and the matching event then no-ops. Reveal a one-click pause/resume action on row hover (text button, no new icon) with an error toast on failure. Extend the snap target with a hover shot. Part of #950 PR6 (frontend automations panel).
…e actions Replace the placeholder detail with the full read-only surface: title plus Run now / Pause-Resume / Delete actions, an Instructions column, and a right column with Status (active/paused, next run, last run), Details (project, repeats, model, reasoning), and Previous runs. Run rows lazy-load via automation.loadRuns on mount and open the run's chat session through the new layout openAutomationRun helper. Delete confirms through a dialog and returns to the list. Knobs stay read-only in v1; inline editing is deferred to PR7.
Remove the OPENCODE_ENABLE_AUTOMATE_TOOL env gate so the automate tool is always available now that the Automations panel ships. Narrow the agent-facing tool schema to the v1 surface: drop the context, recurring stop, and where.worktree knobs, and inject the defaults (fresh context, never-stop) in execute before the domain create parser. The frozen domain contract (Automation.create/update validation) is left untouched on purpose: it still accepts the full shape so HTTP routes, the SDK, and the structured error contract keep working. Enforcing the v1 narrowing only at the tool/product layer follows the "differentiation lives in the tool layer" guidance and avoids changing behavior the merged PR1-5 tests depend on. This diverges from the build-plan note that also asked for domain-level rejection.
Detect the rising edge of failureStreak >= 3 in the directory event reducer and fire one subtle toast, no auto-pause. The edge is gated on an accepted (revision-newer) update and a witnessed transition from below the threshold, so SSE replays and the bootstrap snapshot (which never runs this reducer path) stay quiet. The prior streak is snapshotted as a primitive before the store write because the Solid store proxy would otherwise read back the post-write value.
Walk the real workflow in Playwright: open the panel from the sidebar, seed an automation over the SDK, open its detail, pause it, and delete it through the confirm dialog. A second spec asserts Escape unwinds detail to the list and then closes the surface, and that opening Automations keeps the sidebar live (unlike the Settings takeover). Add stable data-action hooks on the detail actions and the delete-confirm button so the selectors do not ride on label text.
|
Warning Review limit reached
More reviews will be available in 59 minutes and 5 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds Automations UI: list/detail surface, delete dialog, schedule formatting, run-status icons, global-sync automation operations and SSE handling, revision-gated automation store and bootstrap merge, narrowed automate tool v1 input schema, i18n, and end-to-end + snapshot tests. ChangesAutomations Feature
Sequence DiagramsequenceDiagram
participant User
participant UI as AutomationsSurface
participant GlobalSync
participant SDK
participant Store
User->>UI: open automations surface
UI->>GlobalSync: request automations list / subscribe SSE
GlobalSync->>SDK: sdk.automation.list() / SSE events
SDK->>Store: mergeAutomationList / applyAutomationDefinition/run/tombstone
Store->>UI: automations state updated
User->>UI: select automation row
UI->>GlobalSync: automation.loadRuns(id)
GlobalSync->>SDK: sdk.automation.run.list()
SDK->>Store: merge runs
User->>UI: click pause
UI->>GlobalSync: automation.pause(id)
GlobalSync->>SDK: pause API
SDK->>Store: apply updated definition
Store->>UI: status shows Paused
User->>UI: click delete
UI->>GlobalSync: automation.delete(id)
GlobalSync->>SDK: delete API
SDK->>Store: apply tombstone
Store->>UI: row removed
🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
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 unit tests (beta)
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/dialog-delete-automation.tsx, packages/app/src/context/global-sync.tsx, packages/app/src/context/global-sync/automation-store.test.ts, packages/app/src/context/global-sync/automation-store.ts, packages/app/src/context/global-sync/bootstrap.test.ts, packages/app/src/context/global-sync/bootstrap.ts, packages/app/src/context/global-sync/child-store.ts, packages/app/src/context/global-sync/event-reducer.test.ts, packages/app/src/context/global-sync/event-reducer.ts, packages/app/src/context/global-sync/types.ts, packages/app/src/i18n/en.ts, packages/app/src/i18n/zh.ts, packages/app/src/pages/automations/automation-detail.tsx, packages/app/src/pages/automations/automation-list.tsx, packages/app/src/pages/automations/automation-run-status.tsx, packages/app/src/pages/automations/automation-schedule.test.ts, packages/app/src/pages/automations/automation-schedule.ts, packages/app/src/pages/automations/automations-surface.tsx, packages/app/src/pages/layout.tsx, packages/app/src/pages/layout/layout-shell-frame.tsx, packages/app/src/pages/layout/pawwork-sidebar.tsx)).
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 comprehensive Automations panel to the application, allowing users to list, view details, pause, resume, run, and delete automated tasks. It includes UI components, global state synchronization, localization support, and E2E tests, alongside simplifying the automate tool parameters in the backend. The review feedback highlights several improvement opportunities: guarding formatTimestamp against invalid dates to prevent potential crashes, using createEffect instead of onMount in AutomationDetail for reactive run loading, replacing the custom basename helper with the standard getFilename utility, hoisting notifyFailure to avoid temporal dead zone issues, and correcting the non-standard Tailwind class opacity-55 to opacity-50.
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: 6
🧹 Nitpick comments (2)
packages/opencode/test/tool/automate.test.ts (1)
215-223: ⚡ Quick winAssert the pinned v1 defaults in the echoed definition.
This is the main regression test for the v1-to-domain translation, but it never checks that recurring definitions are persisted with
context: "fresh"andstop.kind: "never". Adding those expectations would lock down the behavior this PR is introducing.🤖 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/opencode/test/tool/automate.test.ts` around lines 215 - 223, The test in automate.test.ts currently asserts the echoed automationDefinition fields but omits asserting the pinned v1 defaults for recurring definitions; update the assertion on result.metadata.automationDefinition (the object under test) to also include context: "fresh" and stop: { kind: "never" } so the test verifies the v1-to-domain translation persists those defaults for recurring automations (keep the other existing expectations like title, prompt, revision, paused, where, and sourceSessionID).packages/app/src/context/global-sync/bootstrap.test.ts (1)
43-45: ⚡ Quick winAdd a direct automation bootstrap assertion.
These changes only stub
automation.list()so the new branch can run. There’s still no test proving thatbootstrapDirectory()hydratesstore.automationcorrectly from a non-empty response, so a regression in themergeAutomationList(...)wiring would slip through unnoticed.Also applies to: 156-156
🤖 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/context/global-sync/bootstrap.test.ts` around lines 43 - 45, Stub automation.list() to return a non-empty list, call bootstrapDirectory(), and add a direct assertion that store.automation contains the expected hydrated entries; specifically ensure the test exercises bootstrapDirectory() and verifies mergeAutomationList(...) actually populates the shared store by asserting on store.automation (and/or its keys/values) after the call so the wiring from automation.list() → mergeAutomationList → store.automation is validated.
🤖 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/context/global-sync.tsx`:
- Around line 347-369: The automation mutation helpers (pauseAutomation,
resumeAutomation, deleteAutomation, runAutomationNow) grab [store, setStore]
then await SDK calls without pinning the child, so if the child is disposed
while the request is in flight the response may be applied to a stale store;
update each function to pin the directory before making the async SDK call
(e.g., call children.pin(directory) after peek), perform the await, apply the
returned data with
applyAutomationDefinition/applyAutomationTombstone/applyAutomationRun using the
pinned store variables, and always unpin in a finally block
(children.unpin(directory)) to mirror the pattern used in loadAutomationRuns.
In `@packages/app/src/pages/automations/automation-schedule.ts`:
- Around line 9-13: The summary text in formatInterval wrongly rounds minutes to
the nearest hour; update function formatInterval (parameters everyMs, t) so it
only uses the hour label when the minute count divides evenly by 60 (i.e.,
minutes % 60 === 0); otherwise keep and display the exact minutes. Concretely,
compute minutes = Math.round(everyMs / 60000), if minutes < 60 return minutes
wording, else if minutes % 60 === 0 compute hours = minutes / 60 and return
hours wording, otherwise return the minutes wording (do not round to hours).
- Around line 25-29: The current early return treats any cron with hour === "*"
as "Hourly" even when minute isn't 0 (e.g., "*/15 * * * *"); change the check to
only match the exact hourly shape by requiring minute === "0" as well (i.e.,
replace the hour-only branch with a condition like if (hour === "*" && minute
=== "0") return t("automations.schedule.hourly")). Keep the existing integer
validation for minuteNum/hourNum to fall back to
t("automations.schedule.custom") for unsupported shapes.
- Around line 33-34: The weekly summary currently calls
t("automations.schedule.weekly", { time }) which omits which weekday runs;
change the call in the branch that matches /^[0-6]$/ to pass a localized day
name as an extra param (e.g. t("automations.schedule.weekly", { time, day:
weekdayName })) using your existing weekday/localization helper or
Intl.DateTimeFormat to derive weekdayName from dow, and then update the
translation strings for the key automations.schedule.weekly in en.ts and zh.ts
to include the {{day}} placeholder so labels render like "Weekly on {{day}} at
{{time}}".
In `@packages/app/src/pages/automations/automations-surface.tsx`:
- Around line 35-49: The Escape handler registered in onMount (function
onEscape) is added in the capture phase (document.addEventListener("keydown",
onEscape, true)) which can preempt sidebar menu handlers; change the listener to
the bubbling phase by removing/setting the third arg to false for both
addEventListener and removeEventListener so menu handlers run first, and
additionally guard inside onEscape by checking event.composedPath() or
event.target.closest(...) for transient menu elements (e.g.,
DropdownMenu/ContextMenu elements exposed by the sidebar) and return early if
the event originated inside those menus; keep the existing checks for
dialog/select overlays and the existing logic around selectedID(),
setSelectedID, and props.onClose.
---
Nitpick comments:
In `@packages/app/src/context/global-sync/bootstrap.test.ts`:
- Around line 43-45: Stub automation.list() to return a non-empty list, call
bootstrapDirectory(), and add a direct assertion that store.automation contains
the expected hydrated entries; specifically ensure the test exercises
bootstrapDirectory() and verifies mergeAutomationList(...) actually populates
the shared store by asserting on store.automation (and/or its keys/values) after
the call so the wiring from automation.list() → mergeAutomationList →
store.automation is validated.
In `@packages/opencode/test/tool/automate.test.ts`:
- Around line 215-223: The test in automate.test.ts currently asserts the echoed
automationDefinition fields but omits asserting the pinned v1 defaults for
recurring definitions; update the assertion on
result.metadata.automationDefinition (the object under test) to also include
context: "fresh" and stop: { kind: "never" } so the test verifies the
v1-to-domain translation persists those defaults for recurring automations (keep
the other existing expectations like title, prompt, revision, paused, where, and
sourceSessionID).
🪄 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: a7ef76b3-9f89-4ee9-8db2-da5d0bdbd1ae
📒 Files selected for processing (27)
packages/app/e2e/automations/automations-panel.spec.tspackages/app/e2e/snap/automations-surface.snap.tspackages/app/src/components/dialog-delete-automation.tsxpackages/app/src/context/global-sync.tsxpackages/app/src/context/global-sync/automation-store.test.tspackages/app/src/context/global-sync/automation-store.tspackages/app/src/context/global-sync/bootstrap.test.tspackages/app/src/context/global-sync/bootstrap.tspackages/app/src/context/global-sync/child-store.tspackages/app/src/context/global-sync/event-reducer.test.tspackages/app/src/context/global-sync/event-reducer.tspackages/app/src/context/global-sync/types.tspackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/app/src/pages/automations/automation-detail.tsxpackages/app/src/pages/automations/automation-list.tsxpackages/app/src/pages/automations/automation-run-status.tsxpackages/app/src/pages/automations/automation-schedule.test.tspackages/app/src/pages/automations/automation-schedule.tspackages/app/src/pages/automations/automations-surface.tsxpackages/app/src/pages/layout.tsxpackages/app/src/pages/layout/layout-shell-frame.tsxpackages/app/src/pages/layout/pawwork-sidebar.tsxpackages/opencode/src/tool/automate.tspackages/opencode/src/tool/registry.tspackages/opencode/test/tool/automate.test.tspackages/opencode/test/tool/registry.test.ts
…ering - formatInterval shows seconds for sub-minute cadences and only collapses to an hour label when the minutes divide evenly, so 30s and 90m stop rounding to "1 min" / "2 h" - formatCron only labels a single fixed-minute, all-day cron as "Hourly"; stepped minutes like */15 fall back to a custom schedule - formatTimestamp returns "" for non-finite input instead of throwing a RangeError the timezone catch can't recover from - detail view reuses the shared getFilename helper for the project label
…ations - mergeAutomationList preserves definitions that arrived (via SSE) after the list request was issued, so a brand-new automation isn't reconciled away when a stale snapshot lands mid-flight - pause/resume/delete/runNow pin the directory store around the in-flight SDK call so the authoritative response isn't applied to a disposed store
The capture-phase Escape handler now bails while a dropdown or context menu is open, extending the existing dialog/select guard. Unlike the settings takeover, the sidebar stays interactive over the Automations panel, so its menus must dismiss on Escape instead of unwinding the panel.
The automate tool was the only tool whose LLM-facing schema used unions: a
top-level oneshot|recurring union plus a nested interval|cron rhythm union.
Models serialized those union nodes into JSON strings (`where` and `rhythm`
arrived as "{...}"), which failed decode, so the tool could not be called
reliably.
Collapse the surface to a flat struct — title, prompt, cron required;
recurring, timezone, model, variant optional — with zero union/anyOf nodes.
execute() translates it into the frozen Automation.CreateInput: project
defaults to the current instance, timezone to the host, model to the calling
session's model (else provider default), and recurring:false maps to a
one-shot whose fireAt is the next cron occurrence. Interval and sub-minute
cadence stay a UI/SDK feature, off the AI surface. The frozen domain contract
is unchanged.
…s open Opening Automations only replaces the main region, so the session route stays mounted and the sidebar row plus titlebar kept rendering the active session next to the Automations nav item — two things looked selected at once. Gate the titlebar's session title/folder and right-side panel controls on automationsOpen, suppress the sidebar row's active highlight, and hide the right-panel tab strip while the surface owns the main region. Also add the missing tooltip to the sidebar Automations button.
#1060) Pure extraction (layout governance slice 3) of the pawwork project/pinned organization actions out of packages/app/src/pages/layout.tsx into packages/app/src/pages/layout/pawwork-project-controls.ts. What moved (verbatim, no behavior change): - Pinned: togglePinnedSession, dragPawworkSession (cross-zone + intra-pinned reorder), movePinnedSessionByOne - Project view: setPawworkSortMode, toggleProjectCollapsed, expandPawworkProjectGroup - Project hide/rename: hideProject, unhideProject, handleRenameProject Boundary: - Exposed as createPawworkProjectControls(input) with reactive accessors (projects/sessions) and layout-owned renameProject/setWorkspaceName injected as deps. All 9 functions destructured back with identical names - every call site (sidebar render props, keyboard-nav expandPawworkProjectGroup, navigation helpers calling unhideProject) byte-unchanged. Inner param input->args in dragPawworkSession/movePinnedSessionByOne is a local rename only. - Navigation wrappers (navigateToSession/navigateToProject/openPawworkHome/ syncSessionRoute) intentionally stay in layout.tsx with the routing infra (slice 4). No DOM/copy/aria/command-id/storage-key/styling change. Verification: - bun turbo typecheck: 8/8 pass - cd packages/app && bun test src/pages/layout: 158 pass / 0 fail - /codex review (xhigh): no P0/P1/P2/P3 - confirmed reactive boundary preserved, imports all-unused, no TDZ, generic inference intact - All CI checks green (36/36), mergeable CLEAN Risk: low - mechanical extraction, layout.tsx 1704->1598 lines. Open PR #1053 (automation) also touches layout.tsx in non-overlapping regions and will rebase after this merge (per repo owner). Refs #1056, follow-up to #875.
Add hand-authored `play` and `pause` glyphs to the icon registry: pause reuses circle-check's ring with thin bars; play is a rounded landscape triangle stroked at 1.3 to match the family's measured weight (arrow-right shaft 1.31, circle-check ring 1.04). Reorder the detail action buttons to delete / pause / run-now, make pause an icon-only toggle (play when paused), give run-now a leading play icon, and add breathing room (gap-6 outer, gap-8 grid).
The e2e-smoke-tagging inventory test asserts the discovered @smoke titles match a hardcoded list; the new automations-panel.spec.ts smoke case was missing, failing unit-opencode. Add its entry in sorted position.
…ecent P3: weekly cron summaries showed only the time, so `0 9 * * 1` (Mon) and `0 9 * * 5` (Fri) rendered identically. Name the weekday via localized strings (en/zh) so single-day schedules are distinguishable. P2: the runs list loads only the most recent page and cannot page further, so "Previous runs" over-promised. Rename it to "Recent runs" and note at the mount call that dropping nextCursor is intentional.
…ut.tsx (#1061) Slice 4 of the layout governance line (#1056). Pure extraction: moves the 5 routing actions (syncSessionRoute / navigateToProject / navigateToSession / openPawworkHome / openProject) plus deep-link handling (handleDeepLinks + the onMount window deepLinkEvent listener) out of pages/layout.tsx into the new controller pages/layout/pawwork-routing-actions.ts. layout.tsx 1598 -> 1530. No user-visible behavior change (DOM/copy/aria/command IDs/storage keys/styles untouched). The functions are verbatim copies with an input. prefix; the controller takes minimal Pick<> capability slices as input. Preserved behaviors (confirmed via /codex consult before implementing, then verified by /codex review): - deep-link new-session goes through openProject(dir, false) + navigate to the ORIGINAL base64 directory slug, NOT openPawworkHome (which would resolve a different project root). - syncSessionRoute keeps returning root (consumed by activeRoute.sessionProject dedup) and keeps the requestAnimationFrame/untrack ordering verbatim. - navigateToSession keeps unhide-then-openSession ordering. Boundary: projectRoot / activeProjectRoot / shellNavigation stay in layout.tsx and are injected as input — avoids the shellNavigation-serves-settings coupling and the projectRoot<->shellNavigation cycle. No TDZ risk: openProject is captured by the startup createResource closure but the fetcher awaits readiness before calling it (codex-confirmed). Verification: bun run typecheck 8/8; cd packages/app && bun test src/pages/layout = 165 pass (incl. 7 new focused routing tests). /codex review GATE PASS (no P0/P1); 2 P2 test-quality findings fixed (cross-function call-order assertions + rAF scroll payload; resolved-root vs original-slug fixture). Refs #1056. Note: #1053 (automation) also touches pages/layout.tsx and will rebase on top of this.
Resolve layout.tsx conflicts from dev's session-controller refactor: - Drop removePinnedSessionIDs from the store import (dev removed its usage) and keep the AutomationsSurface import. - dev moved loadSessionByID out of layout.tsx into pawwork-session-controller; expose it from the controller's return so openAutomationRun keeps async session loading.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/opencode/test/tool/automate.test.ts (1)
12-20: 🏗️ Heavy liftUse
testEffect(...)for the new Effect-driven cases.These new integration tests are all manual
Effect.runPromise(...)calls with an ad hocctx(...)shim. Inpackages/opencode/test/**/*.test.ts, they should useconst it = testEffect(...); with thetmpdir/git/instance setup here, the individual cases would then beit.live(...)instead of hand-rolled runtime plumbing. As per coding guidelines "UsetestEffect(...)fromtest/lib/effect.tsfor tests that exercise Effect services or Effect-based workflows."Also applies to: 76-210
🤖 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/opencode/test/tool/automate.test.ts` around lines 12 - 20, Replace the ad-hoc Effect.runPromise calls and the manual ctx(...) shim in this test with the standard testEffect harness: import testEffect from "test/lib/effect.ts", set const it = testEffect(...), and convert each test case that currently calls Effect.runPromise(...) to use it.live(...) so the tmpdir/git/instance fixtures and Effect runtime are managed consistently; update references to the ctx(...) factory to use the testEffect-provided environment or adapt ctx to be created per-case inside it.live callbacks so tests no longer manually construct the runtime or abort signals.packages/opencode/src/tool/automate.ts (1)
90-154: ⚡ Quick winName this tool entrypoint with
Effect.fn(...).This is a new Effect entrypoint in
packages/opencode/src, but it is still an anonymousEffect.gen. Wrapping it inEffect.fn("Tool.automate.execute")keeps the behavior while restoring the repo’s tracing/naming convention. As per coding guidelines "UseEffect.fn("Domain.method")for named/traced effects andEffect.fnUntracedfor internal helpers; these accept pipeable operators as extra arguments to avoid unnecessary outer.pipe()wrappers".🤖 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/opencode/src/tool/automate.ts` around lines 90 - 154, The execute entrypoint currently returns an anonymous Effect.gen; wrap that Effect.gen invocation with a named Effect.fn to restore tracing (e.g., replace the current Effect.gen(...) return value in the execute: (params, ctx) => ... block by calling Effect.fn("Tool.automate.execute", /* the existing Effect.gen(...) */) so the effect keeps identical behavior but gains the "Tool.automate.execute" trace name; update the expression around the existing Effect.gen in the execute function accordingly.
🤖 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/opencode/src/tool/automate.ts`:
- Around line 66-79: The current sessionModel function swallows all exceptions
from MessageV2.stream; change the catch to only handle the "no persisted
messages" case by detecting the specific NotFoundError (or whatever concrete
error class/identifier the storage layer throws) and returning undefined for
that case, but rethrow any other errors so storage/corruption failures bubble
up; update the catch to check err instanceof NotFoundError (or err.name ===
'NotFoundError' if an instance check isn't available) and only return undefined
in that branch, otherwise throw err, keeping the rest of sessionModel and the
loop over MessageV2.stream unchanged.
- Around line 92-145: The timestamp "now" is captured too early (before async
model resolution/validation), causing race across cron boundaries; move sampling
of Date.now() to after the async/await path (i.e., after
validateModelAndVariantWith(provider, model, variant) returns) and before
computing the createInput and calling nextCronFireAfter and Automation.create so
both the oneshot fireAt and the { now } option passed to Automation.create use
the same fresh timestamp; update the code that declares/uses now (replace the
early const now = Date.now() with a new const now = Date.now() just before
building createInput / before Automation.create) and ensure
Automation.create(parsed, { now, sourceSessionID: ctx.sessionID }) still
receives that new now.
---
Nitpick comments:
In `@packages/opencode/src/tool/automate.ts`:
- Around line 90-154: The execute entrypoint currently returns an anonymous
Effect.gen; wrap that Effect.gen invocation with a named Effect.fn to restore
tracing (e.g., replace the current Effect.gen(...) return value in the execute:
(params, ctx) => ... block by calling Effect.fn("Tool.automate.execute", /* the
existing Effect.gen(...) */) so the effect keeps identical behavior but gains
the "Tool.automate.execute" trace name; update the expression around the
existing Effect.gen in the execute function accordingly.
In `@packages/opencode/test/tool/automate.test.ts`:
- Around line 12-20: Replace the ad-hoc Effect.runPromise calls and the manual
ctx(...) shim in this test with the standard testEffect harness: import
testEffect from "test/lib/effect.ts", set const it = testEffect(...), and
convert each test case that currently calls Effect.runPromise(...) to use
it.live(...) so the tmpdir/git/instance fixtures and Effect runtime are managed
consistently; update references to the ctx(...) factory to use the
testEffect-provided environment or adapt ctx to be created per-case inside
it.live callbacks so tests no longer manually construct the runtime or abort
signals.
🪄 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: a2392f69-bfa9-4727-b813-103b86559dba
📒 Files selected for processing (22)
packages/app/src/components/session/session-header.tsxpackages/app/src/context/global-sync.tsxpackages/app/src/context/global-sync/automation-store.test.tspackages/app/src/context/global-sync/automation-store.tspackages/app/src/context/global-sync/bootstrap.tspackages/app/src/context/shell-surface.tsxpackages/app/src/i18n/en.tspackages/app/src/i18n/zh.tspackages/app/src/pages/automations/automation-detail.tsxpackages/app/src/pages/automations/automation-schedule.test.tspackages/app/src/pages/automations/automation-schedule.tspackages/app/src/pages/automations/automations-surface.tsxpackages/app/src/pages/layout.tsxpackages/app/src/pages/layout/pawwork-sidebar.tsxpackages/app/src/pages/layout/sidebar-items.tsxpackages/app/src/pages/session/right-panel-tab-strip.tsxpackages/opencode/src/automation/derived.tspackages/opencode/src/tool/automate.tspackages/opencode/test/config/e2e-smoke-tagging.test.tspackages/opencode/test/server/automation-scheduler.test.tspackages/opencode/test/tool/automate.test.tspackages/ui/src/components/icon.tsx
✅ Files skipped from review due to trivial changes (3)
- packages/app/src/context/shell-surface.tsx
- packages/opencode/test/config/e2e-smoke-tagging.test.ts
- packages/app/src/i18n/en.ts
🚧 Files skipped from review as they are similar to previous changes (9)
- packages/app/src/pages/automations/automation-schedule.test.ts
- packages/app/src/context/global-sync/bootstrap.ts
- packages/app/src/pages/automations/automations-surface.tsx
- packages/app/src/context/global-sync.tsx
- packages/app/src/i18n/zh.ts
- packages/app/src/pages/layout.tsx
- packages/app/src/context/global-sync/automation-store.test.ts
- packages/app/src/pages/layout/pawwork-sidebar.tsx
- packages/app/src/pages/automations/automation-schedule.ts
…1062) Slice 5 of the layout governance line (#1056). Pure extraction: moves the workspace lifecycle actions (createWorkspace / createCurrentWorkspace / deleteWorkspace / resetWorkspace / renameWorkspace / toggleCurrentWorkspace) out of pages/layout.tsx into pages/layout/pawwork-workspace-lifecycle.ts. layout.tsx 1530 -> 1358. No user-visible behavior change (DOM / copy / aria / command IDs / storage keys / styles untouched). Functions are verbatim copies with an input. prefix; the controller takes minimal Pick<> capability slices as input. Boundary decisions: - setWorkspaceName / renameProject stay in layout.tsx (already injected into the slice-3 project-controls controller) and are injected here as shared primitives. - clearWorkspaceTerminals is injected, not imported. Importing @/context/terminal pulls @solidjs/router, which evals a client-only solid template at module load and breaks the controller's unit test under the server build. The exact type is recovered via a type-only `typeof import("@/context/terminal").clearWorkspaceTerminals` so no runtime module load enters the controller graph. - The dead closeProject / toggleProjectWorkspaces (zero references across packages) are left untouched; this slice is pure extraction, not cleanup. Verification: bun run typecheck 8/8; cd packages/app && bun test = 1667 pass (incl. 14 new focused lifecycle tests covering rename no-op guard; create dedup/child/navigate + failure abort; delete prune/reopen + navigate-before-remove ordering + leaveDeletedWorkspace early-return + failure no-mutate; reset clear-before-dispose-before-reset ordering + archive-only-unarchived + toast dismiss on both success and failure; toggle git/non-git; createCurrent dispatch). /codex review (xhigh): fixed [P1] — client-action-source.test.ts provenance guard followed the moved workspace.reset header-tagging from layout.tsx to the new file; fixed 2 [P2] — added the leaveDeletedWorkspace dialog-path test and pinned the reset failure-branch toaster.dismiss. gemini thread (centralize filter / optional chaining) resolved as out-of-scope for a verbatim extraction. CI: 10/10 required green. The non-required perf-probe-baseline advisory failed on a 60s homepage-cold timeout while capturing the BASE-branch baseline (not this PR's code) — environmental flake, rerun triggered. Refs #1056. #1053 (automation) also touches pages/layout.tsx and will rebase on top.
…utton
The pause/resume action is icon-only now (icon + aria-label, no text node), so
the e2e smoke's toHaveText("Resume") resolved to "" and failed e2e-artifacts.
Assert the aria-label instead, matching the button's accessible name.
…tion sessionModel() caught every error from MessageV2.stream and returned undefined, silently downgrading a corrupt-store/IO failure to the provider default model. Only swallow NotFoundError (missing session); rethrow everything else. One-shot creation sampled now before the async model resolution/validation, so across a cron boundary that stale instant could compute an already-due fireAt. Sample now after validation, just before building the create input. Adds unit coverage: a non-NotFound stream failure fails the tool without creating an automation, NotFound still falls back to the default model, and a clock crossing a minute boundary during validation pushes the one-shot fireAt to the next match.
Slice 6 of the layout governance line (#1056): pure extraction of the two workspace dialog components out of pages/layout.tsx. - Moved DialogDeleteWorkspace and DialogResetWorkspace verbatim into a new factory pages/layout/pawwork-workspace-dialogs.tsx → createPawworkWorkspaceDialogs(input). - Capabilities injected via a narrow input: globalSDK (Pick client), dialog (Pick close), language ({t}), params ({dir?}), currentDir, navigate, deleteWorkspace, resetWorkspace. No extra dialog powers (e.g. show) leaked in. - Preserved behavior exactly: delete's leaveDeletedWorkspace = !!params.dir && workspaceKey(currentDir())===workspaceKey(directory) with navigate→close→deleteWorkspace ordering; file.status onMount dirty probe; reset's close→resetWorkspace ordering and active-session filter (archived === undefined only). - Removed now-unused imports from layout.tsx (Dialog @opencode-ai/ui/dialog, Button @opencode-ai/ui/button); confirmed no residual references. - Named the factory createPawworkWorkspaceDialogs / PawworkWorkspaceDialogsInput for consistency with the file name and the sibling createPawworkWorkspaceLifecycle. No user-visible behavior / DOM / copy / aria / command ID / storage key / style changes. Verification: bun run typecheck (8/8), eslint, layout unit tests; codex custom-focus review PASS (no P0/P1/P2, independently re-verified ordering, injection boundary, import cleanup, naming, tsgo --noEmit). pages/layout.tsx 1234 → ~1086. Touches a file also occupied by open PR #1053 (rebase expected per #950 plan). Refs #1056.
Summary
Build the frontend Automations panel for issue #950 (frontend slice PR6). Adds a sidebar entry and a full-page surface to list, open, pause/resume, run-now, and delete automations, with run history, a repeated-failure alert, and global-sync data wiring. Also unhides the
automatetool (removes the env gate) and narrows its agent-facing schema to the v1 surface, so chat can create automations that match the panel. The frozen PR1-5 backend contract is unchanged. Manual creation form, templates, and NL confirmation are intentionally out of scope (PR7).Why
PR1-5 shipped the Automation domain, events, SDK, and scheduler, but the
automatetool stayed gated and users had no way to see or manage automations. This PR is the management UI plus the gate flip, which makes the feature usable end to end.Related Issue
#950
Human Review Status
Pending
Review Focus
automatetool schema (dropcontext/ recurringstop/where.worktree; injectcontext: fresh+ recurringstop: neverinexecute) and left domainAutomation.create/updatevalidation UNCHANGED. Codex P1 fix(config): wait for background installs on dispose #3 also asked to reject those at the domain layer; I intentionally did not, because the contract is frozen (PR1-5 merged), the domainStopkeeps all three kinds on purpose for the HTTP/SDK/structured-error contract (automation/index.ts:68-71), and domain rejections would change frozen behavior and break merged tests. Enforcement lives in the tool/product layer.event-reducer.ts): gated on an accepted (revision-newer) update and a witnessed sub-threshold transition, so SSE replays and the bootstrap snapshot stay quiet. The prior streak is snapshotted as a primitive before the store write (the Solid store proxy would otherwise read back the post-write value).Risk Notes
automatetool is now always on (env gate removed); chat can create automations, with the schema narrowed to the v1 shape.context=continue/stop=count/where.worktreevia raw API/SDK. No product surface emits them; PR7's create form will send explicit defaults (SDK types still require them).failureStreak >= 3.How To Verify
Screenshots or Recordings
Snap target
automations-surface(Chromium web renderer) covers empty state, list, list hover (pause/resume action), and detail. Runbun run snap automations-surfaceto regenerate the grid. The change is web-renderable (noplatform.*/ preload / IPC paths), sodev:desktopwas not required.Checklist
bug,enhancement,task,documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.app,ui,platform,harness,ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.P0,P1,P2,P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.Pending,Approved by @<reviewer>, orNot required: <reason>(default isPending; "not required" is restricted to bot-authored low-risk PRs).dev, and my PR title and commit messages use Conventional Commits in English.Summary by CodeRabbit
New Features
Tests