diff --git a/packages/client/ui-schedule/README.i18n.yaml b/packages/client/ui-schedule/README.i18n.yaml index 96bad805a3..1371e74c19 100644 --- a/packages/client/ui-schedule/README.i18n.yaml +++ b/packages/client/ui-schedule/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 405b30feaf48c087 /deepseek-ai-dsh-client-ui-schedule/use-this-package: - en: 99626c29eb801b6f - zh: 9693d278f467ad90 + en: 68526e7df25338d4 + zh: 3bfdfa01dc9afaec /deepseek-ai-dsh-client-ui-schedule/use-this-package/time-zone-data-source: en: 4005b800e930f581 zh: ccb9318dad7f7e8f diff --git a/packages/client/ui-schedule/README.md b/packages/client/ui-schedule/README.md index 2db007cce9..fbd7582bd2 100644 --- a/packages/client/ui-schedule/README.md +++ b/packages/client/ui-schedule/README.md @@ -35,7 +35,7 @@ The destination menu offers a new conversation that the first delivery creates a The repeat editor offers once, minute, hour, day, week, month, and custom rules. Date and clock controls reuse existing pickers. Guided presets produce RRULE; minute and hour intervals start from the current minute in the selected zone. The Custom choice and Customize input do not name either syntax; the input accepts a two-line RRULE or a five-field Cron expression with an explicit zone. RRULE retains the zone in `DTSTART`; Cron retains its expression and selected zone as Cron timing. Complex RRULE values remain verbatim. The shared time parser checks new or changed Custom text before enabling Save; invalid or empty input keeps the draft and explains which field to change. Invalid Custom text offers manual correction, another repeat option, or correction by the Agent. The Host performs the final validation on save. Stored After and At tasks show date and clock fields immediately; a repeated local time uses UTC when needed to retain the exact instant. Every, Daily, and Weekly tasks show the matching controls when their interval or wall-clock precision is representable. Other stored timings remain in Custom and keep their typed timing until replaced. Viewing a task or editing other fields preserves its timing and committed occurrence. -Pause and resume update lifecycle state. Unsaved changes hold pause, resume, Run now, and Delete until you save or discard them. The detail menu offers Run now and Delete. It offers no queued-message cancellation: one inbox message may contain deliveries from several tasks. While a delivery to a reused conversation is queued or running, the detail disables its fields, Save, pause and resume, Run now, and Delete, and states that the task is queued or running and is managed in its conversation, which the bottom conversation entry opens. The minimum delivery interval disables another immediate run until the interval ends, while pause and deletion remain available once execution is idle. When the interval is the only cause, the Run now row stays focusable, and its tooltip states that the minimum run interval has not passed and when Run now becomes available. An archived linked conversation of a task already bound to a Session also disables Run now, because the Host refuses that delivery. A run that the Host refuses with `schedule_execution_pending` shows a status notice instead of an error. Deletion requires confirmation, removes the task and its saved records, retains Sessions and already-delivered messages, and reports the result through the existing app-wide toast. The confirmation focuses Cancel first; when it closes, focus returns to the detail menu button. The detail keeps the task until the Host answers. An applied deletion closes the detail or its task tab; a failed deletion keeps the task open for another attempt. +Pause and resume update lifecycle state. While a pause or resume, a catalog reload, or a deletion is in progress, the toggle keeps its look and focus and ignores activation. During a pause, resume, or Run now request, the rule fields reject input without turning grey, and Close and Discard keep their look and ignore activation. Unsaved changes hold pause, resume, Run now, and Delete until you save or discard them. The detail menu offers Run now and Delete. It offers no queued-message cancellation: one inbox message may contain deliveries from several tasks. While a delivery to a reused conversation is queued or running, the detail disables its fields, Save, pause and resume, Run now, and Delete, and states that the task is queued or running and is managed in its conversation, which the bottom conversation entry opens. The minimum delivery interval disables another immediate run until the interval ends, while pause and deletion remain available once execution is idle. When the interval is the only cause, the Run now row stays focusable, and its tooltip states that the minimum run interval has not passed and when Run now becomes available. An archived linked conversation of a task already bound to a Session also disables Run now, because the Host refuses that delivery. A run that the Host refuses with `schedule_execution_pending` shows a status notice instead of an error. Deletion requires confirmation, removes the task and its saved records, retains Sessions and already-delivered messages, and reports the result through the existing app-wide toast. The confirmation focuses Cancel first; when it closes, focus returns to the detail menu button. The detail keeps the task until the Host answers. An applied deletion closes the detail or its task tab; a failed deletion keeps the task open for another attempt. The list, detail, header, and Session hover show the later of the scheduled occurrence and the Host delivery floor as the next run. Creation and editing show one short minimum-interval hint for per-run conversation targets, read from `schedule.deliveryLimits`; a failed read offers Retry. Scheduled and immediate runs share this limit. diff --git a/packages/client/ui-schedule/README.zh.md b/packages/client/ui-schedule/README.zh.md index b51f75b673..51b523d0b9 100644 --- a/packages/client/ui-schedule/README.zh.md +++ b/packages/client/ui-schedule/README.zh.md @@ -35,7 +35,7 @@ kind: "package-reference" 重复编辑提供仅一次、每分钟、每小时、每天、每周、每月和自定义规则。日期和时间控件复用已有选择器。引导选项生成 RRULE;分钟和小时间隔从所选时区的当前分钟开始。自定义选项和自定义输入框不提示任何语法名称;输入框接受两行 RRULE 或带显式时区的五字段 Cron。RRULE 在 `DTSTART` 中保留时区;Cron 将表达式和所选时区保留为 Cron 时间类型。复杂 RRULE 保持原文。新的或已修改的自定义文本由共享时间解析器校验,校验通过后才能保存;无效或空输入保留草稿,并说明需要修改的字段。自定义文本无效时,提示手动修改规则、选择其他重复方式,或让智能体修改规则。宿主在保存时执行最终校验。已有 After、At 任务直接显示日期和时间控件;本地时间重复时,必要时使用 UTC 保留准确时点。Every、Daily、Weekly 任务在控件可表示其间隔或钟表时间精度时直接显示对应控件;其他时间规则仍在自定义中显示描述,并在替换前保留原类型。查看任务或编辑其他字段保留其时间规则及已提交发生时点。 -暂停和恢复修改生命周期状态。有未保存修改时,先保存或放弃更改,才能暂停、恢复、立即执行或删除。详情菜单提供立即执行和删除,不提供取消排队消息:同一条收件箱消息可能包含多个任务的投递。投递到复用对话的投递排队或执行期间,详情禁用字段、保存、暂停与恢复、立即执行和删除,并说明此任务已排队或执行中,请在会话中管理任务;详情底部的会话入口打开该会话。最小投递间隔结束前禁用再次立即执行,执行空闲后仍可暂停或删除。仅因间隔禁用时,立即执行项仍可聚焦,其提示说明未满最小运行间隔,并给出可以立即执行的时间。已经关联会话的任务,其关联对话已归档时,同样禁用立即执行,因为宿主拒绝这次投递。宿主以 `schedule_execution_pending` 拒绝的立即执行显示为状态提示,而非错误。删除需要确认,移除任务及其已保存记录,保留会话和已经投递的消息,并通过现有应用级提示报告结果。确认对话框首先聚焦“取消”;对话框关闭后,焦点回到详情菜单按钮。宿主答复之前,详情保持显示该任务。删除生效后关闭详情或其任务页签;删除失败时保留任务,以便再次尝试。 +暂停和恢复修改生命周期状态。暂停或恢复请求、目录重读或删除进行期间,切换按钮保持原有外观和焦点,并忽略点击。暂停、恢复或立即执行请求期间,规则字段拒绝输入但不变灰,关闭和“放弃”保持原有外观并忽略点击。有未保存修改时,先保存或放弃更改,才能暂停、恢复、立即执行或删除。详情菜单提供立即执行和删除,不提供取消排队消息:同一条收件箱消息可能包含多个任务的投递。投递到复用对话的投递排队或执行期间,详情禁用字段、保存、暂停与恢复、立即执行和删除,并说明此任务已排队或执行中,请在会话中管理任务;详情底部的会话入口打开该会话。最小投递间隔结束前禁用再次立即执行,执行空闲后仍可暂停或删除。仅因间隔禁用时,立即执行项仍可聚焦,其提示说明未满最小运行间隔,并给出可以立即执行的时间。已经关联会话的任务,其关联对话已归档时,同样禁用立即执行,因为宿主拒绝这次投递。宿主以 `schedule_execution_pending` 拒绝的立即执行显示为状态提示,而非错误。删除需要确认,移除任务及其已保存记录,保留会话和已经投递的消息,并通过现有应用级提示报告结果。确认对话框首先聚焦“取消”;对话框关闭后,焦点回到详情菜单按钮。宿主答复之前,详情保持显示该任务。删除生效后关闭详情或其任务页签;删除失败时保留任务,以便再次尝试。 列表、详情、页头和会话悬浮内容的下次运行时间,取计划发生时间和宿主最早允许投递时间中较晚的一项。创建和编辑“每次新建对话”时,通过 `schedule.deliveryLimits` 读取宿主配置,以一句短提示显示最小间隔;读取失败可重试。定时与立即执行共用此限制。 diff --git a/packages/client/ui-schedule/src/client/TaskDetail.tsx b/packages/client/ui-schedule/src/client/TaskDetail.tsx index cafd802378..8c1f612643 100644 --- a/packages/client/ui-schedule/src/client/TaskDetail.tsx +++ b/packages/client/ui-schedule/src/client/TaskDetail.tsx @@ -215,10 +215,13 @@ function TaskDetailView(props: TaskDetailProps): ReactNode { ? JSON.stringify(draft) !== JSON.stringify({ ...initial, cwd: initial.cwd || projects.defaultCwd }) : storedDirty const issue = dirty ? taskDraftIssue(draft, changes) : undefined const valid = issue === undefined - const [actionPending, setPending] = useState(false) + const [actionPending, setPending] = useState<'toggle' | 'run' | 'delete' | null>(null) const [actionFailure, setFailure] = useState(null) const saving = cached?.pending === true - const pending = actionPending || saving + const pending = actionPending !== null || saving + // A pause, resume, or run would otherwise flash its controls grey between two enabled states. + const actionHold = actionPending === 'toggle' || actionPending === 'run' + const nativeHold = pending && !actionHold const failure = !pending && dirty && base !== undefined && task !== undefined && base.updatedAt !== task.updatedAt ? 'host.conflict' : actionFailure ?? cached?.failure ?? null const [menuOpen, setMenuOpen] = useState(false) @@ -281,18 +284,20 @@ function TaskDetailView(props: TaskDetailProps): ReactNode { } else props.actions.setDraft(key, { base, draft, failure: outcome.failed, ...(task === undefined ? { initial } : {}) }) } const setActive = async (task: ScheduleCatalogEntry): Promise => { - setPending(true) + // The toggle stays focusable while busy, so a second press is ignored here instead of by `disabled`. + if (busy) return + setPending('toggle') const outcome = await hostActions.edit({ id: task.id, action: 'update', patch: { active: task.status !== 'active' }, expectedUpdatedAt: task.updatedAt ?? null }) if (!mounted.current) return - setPending(false) + setPending(null) setFailure(outcome === 'saved' ? null : outcome.failed) } const run = async (id: ScheduleId): Promise => { setMenuOpen(false) - setPending(true) + setPending('run') const outcome = await hostActions.run(id) if (!mounted.current) return - setPending(false) + setPending(null) setFailure(outcome === 'queued' ? null : outcome.failed) } @@ -321,8 +326,10 @@ function TaskDetailView(props: TaskDetailProps): ReactNode { }
{task !== undefined && <> - @@ -346,7 +353,8 @@ function TaskDetailView(props: TaskDetailProps): ReactNode { } {onClose !== undefined && } + disabled={nativeHold} aria-disabled={actionHold || undefined} + onClick={() => { if (!actionHold) onClose() }}>}
: failure === 'host.executionPending' ?

{t(failure)}

: failure !== null &&

{t(failure)}

} {draft.target === 'new' && } @@ -404,8 +412,8 @@ function TaskDetailView(props: TaskDetailProps): ReactNode { } {(tab === 'rule' || dirty) &&