Skip to content

Commit c03656b

Browse files
Zexiclaude
authored andcommitted
fix: capture EffectBridge in Schedule timer + UI polish
- Schedule.Service: cron callback now runs the tick effect via an EffectBridge captured during create(). Without this the cron fires from a setTimeout context that has no InstanceRef, so bus.publish (which goes through InstanceState) dies with "InstanceRef not provided" and the schedule never injects its message. - Hydrate-on-startup is dropped for now (annotated TODO): we don't yet know how to recover the per-project instance context for each row after a sidecar restart, so schedules don't survive the process for now. - UI: fix i18n placeholders to {{count}} (double braces, what the i18n helper expects), and switch popover text classes from non-existent text-10-strong/text-11-regular/text-13-strong to the real text-12-medium/text-12-regular/text-14-medium utilities. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4cfe1ca commit c03656b

4 files changed

Lines changed: 32 additions & 39 deletions

File tree

packages/app/src/components/session-schedule-button.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function SessionScheduleButton() {
7676
<div class="relative flex items-center justify-center">
7777
<Icon name="clock" size="small" />
7878
<Show when={count() > 1}>
79-
<span class="absolute -top-1 -right-2 min-w-3.5 h-3.5 px-1 rounded-full bg-icon-info-base text-text-invert-strong text-10-strong leading-none flex items-center justify-center">
79+
<span class="absolute -top-1 -right-2 min-w-3.5 h-3.5 px-1 rounded-full bg-icon-info-base text-text-invert-strong text-12-medium leading-none flex items-center justify-center">
8080
{count()}
8181
</span>
8282
</Show>
@@ -89,8 +89,8 @@ export function SessionScheduleButton() {
8989
<Show when={shown()}>
9090
<div class="flex flex-col">
9191
<div class="px-4 py-3 border-b border-border-weak-base">
92-
<div class="text-13-strong text-text-strong">{language.t("schedule.popover.title")}</div>
93-
<div class="text-11-regular text-text-weak mt-0.5">
92+
<div class="text-14-medium text-text-strong">{language.t("schedule.popover.title")}</div>
93+
<div class="text-12-regular text-text-weak mt-0.5">
9494
{language.t("schedule.popover.subtitle", { count: count() })}
9595
</div>
9696
</div>
@@ -118,7 +118,7 @@ function ScheduleRow(props: { item: ScheduleInfo; onDelete: () => void; deleting
118118
const nextRun = createMemo(() => formatRelativeTime(language, props.item.nextRun))
119119

120120
const tooltipContent = () => (
121-
<div class="flex flex-col gap-1 text-11-regular">
121+
<div class="flex flex-col gap-1 text-12-regular">
122122
<div class="flex items-center gap-2">
123123
<span class="text-text-invert-base">{language.t("schedule.tooltip.last")}</span>
124124
<Show
@@ -146,7 +146,7 @@ function ScheduleRow(props: { item: ScheduleInfo; onDelete: () => void; deleting
146146
return (
147147
<div class="group flex items-start gap-2 px-4 py-3 border-b border-border-weak-base last:border-b-0 hover:bg-background-base">
148148
<div class="flex-1 min-w-0">
149-
<div class="font-mono text-11-regular text-text-weak mb-0.5">{props.item.expression}</div>
149+
<div class="font-mono text-12-regular text-text-weak mb-0.5">{props.item.expression}</div>
150150
<div class="text-12-regular text-text-base truncate" title={props.item.message}>
151151
{props.item.message}
152152
</div>

packages/app/src/i18n/en.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,9 +417,9 @@ export const dict = {
417417
"context.usage.clickToView": "Click to view context",
418418
"context.usage.view": "View context usage",
419419

420-
"schedule.button.label": "Scheduled tasks ({count})",
420+
"schedule.button.label": "Scheduled tasks ({{count}})",
421421
"schedule.popover.title": "Scheduled tasks",
422-
"schedule.popover.subtitle": "{count} active",
422+
"schedule.popover.subtitle": "{{count}} active",
423423
"schedule.row.info": "Details",
424424
"schedule.row.delete": "Delete schedule",
425425
"schedule.tooltip.last": "Last:",

packages/app/src/i18n/zh.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -413,9 +413,9 @@ export const dict = {
413413
"context.usage.clickToView": "点击查看上下文",
414414
"context.usage.view": "查看上下文用量",
415415

416-
"schedule.button.label": "定时任务({count})",
416+
"schedule.button.label": "定时任务({{count}})",
417417
"schedule.popover.title": "定时任务",
418-
"schedule.popover.subtitle": "{count} 个活跃",
418+
"schedule.popover.subtitle": "{{count}} 个活跃",
419419
"schedule.row.info": "详情",
420420
"schedule.row.delete": "删除定时任务",
421421
"schedule.tooltip.last": "上次:",

packages/opencode/src/session/schedule.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Bus } from "@/bus"
22
import { BusEvent } from "@/bus/bus-event"
3+
import { EffectBridge } from "@/effect/bridge"
34
import { Identifier } from "@/id/id"
45
import { Database } from "@/storage/db"
56
import * as Log from "@opencode-ai/core/util/log"
@@ -129,7 +130,10 @@ export const layer = Layer.effect(
129130
Effect.gen(function* () {
130131
const bus = yield* Bus.Service
131132

132-
const timers = new Map<ID, { cron: Cron; sessionID: SessionID }>()
133+
const timers = new Map<
134+
ID,
135+
{ cron: Cron; sessionID: SessionID; bridge: EffectBridge.Shape }
136+
>()
133137

134138
const recordRun: Interface["recordRun"] = Effect.fn("Schedule.recordRun")(
135139
function* (scheduleID, sessionID, runStatus, ranAt) {
@@ -161,43 +165,31 @@ export const layer = Layer.effect(
161165
})
162166
})
163167

164-
const startTimer = (scheduleID: ID, sessionID: SessionID, expression: string) => {
168+
const startTimer = (
169+
scheduleID: ID,
170+
sessionID: SessionID,
171+
expression: string,
172+
bridge: EffectBridge.Shape,
173+
) => {
165174
const cron = new Cron(expression, {}, () => {
166-
Effect.runPromise(tick(scheduleID)).catch((e) =>
175+
// Run tick inside the captured instance/workspace context so that
176+
// services hung off InstanceState (e.g. Bus.publish) resolve cleanly.
177+
bridge.promise(tick(scheduleID)).catch((e) =>
167178
log.error("schedule timer error", {
168179
scheduleID,
169180
error: e instanceof Error ? e.message : String(e),
170181
}),
171182
)
172183
})
173-
timers.set(scheduleID, { cron, sessionID })
184+
timers.set(scheduleID, { cron, sessionID, bridge })
174185
}
175186

176-
// Hydrate on startup, deferred via setTimeout so that the layer itself
177-
// can finish constructing without touching the database (the database
178-
// and its surrounding context are not always ready at layer-init time,
179-
// e.g. in tool-registry unit tests). Errors are swallowed; failed
180-
// hydration just means the existing rows won't get timers until the
181-
// next create()/list() touches the data manually.
182-
setTimeout(() => {
183-
try {
184-
const rows = Database.use((db) => db.select().from(ScheduleTable).all())
185-
for (const row of rows) {
186-
try {
187-
startTimer(row.id as ID, row.session_id as SessionID, row.expression)
188-
} catch (e) {
189-
log.error("failed to hydrate schedule", {
190-
scheduleID: row.id,
191-
error: e instanceof Error ? e.message : String(e),
192-
})
193-
}
194-
}
195-
} catch (e) {
196-
log.debug?.("schedule hydrate skipped", {
197-
error: e instanceof Error ? e.message : String(e),
198-
})
199-
}
200-
}, 0)
187+
// NOTE: Cron timers need an EffectBridge captured inside an active
188+
// InstanceContext (so that bus.publish / SessionPrompt run in the right
189+
// instance scope). We only have that during create(); hydrate-on-restart
190+
// is a TODO that needs per-project instance lookup. Until then,
191+
// schedules don't survive a sidecar restart — the rows persist in the
192+
// DB but no timers are reattached.
201193

202194
const list: Interface["list"] = Effect.fn("Schedule.list")(function* (sessionID: SessionID) {
203195
const rows = yield* Effect.sync(() =>
@@ -268,7 +260,8 @@ export const layer = Layer.effect(
268260
.run()
269261
}),
270262
)
271-
startTimer(id, input.sessionID, input.expression)
263+
const bridge = yield* EffectBridge.make()
264+
startTimer(id, input.sessionID, input.expression, bridge)
272265
yield* bus.publish(Event.Created, { scheduleID: id, sessionID: input.sessionID })
273266
return {
274267
id,

0 commit comments

Comments
 (0)