feat(web-shell): add a Scheduled Tasks management page#6348
Conversation
Add a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace. - Sidebar entry opens a full-pane page (replaces the chat area, not a modal) listing tasks with enable/disable toggle, delete, run-now, and human-readable schedules. - "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview. - "Create via chat" returns to the chat and primes the composer so the agent creates the task through its cron_create tool. - Daemon CRUD routes (GET/POST/PATCH/DELETE /scheduled-tasks) read/write the existing per-project scheduled_tasks.json; task firing stays with the session-side scheduler. - Extend DurableCronTask with optional name/enabled (backward compatible); the scheduler skips tasks with enabled:false. - Add /scheduled-tasks to the vite dev-server proxy allowlist so the page works under npm run dev:daemon.
|
Thanks for the PR! Template looks good ✓ — all required sections present and filled in, including bilingual description, reviewer test plan with step-by-step verification, before/after evidence (screenshots), and risk/scope assessment. Problem: this is a feature addition, not a bug fix. The gap is real — qwen-code ships a durable cron engine ( Direction: aligned. This reuses the existing cron engine rather than introducing a parallel scheduler — the right call. The Web Shell already proxies daemon routes, and adding a CRUD surface on top of the existing file format is the natural extension. The scope is a single coherent feature (page + daemon routes + core field additions), not unrelated bolt-ons. Approach: the scope feels right for the feature. Core changes are minimal (optional Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必填章节齐全,包括双语说明、复审测试计划(含逐步验证步骤)、前后对比证据(截图)、以及风险与范围评估。 问题:这是一个功能新增,不是 bug 修复。差距确实存在 — qwen-code 已经内置了持久化 cron 引擎( 方向:对齐。复用了既有的 cron 引擎而不是引入并行调度器 — 正确的做法。Web Shell 已经代理了 daemon 路由,在既有文件格式之上添加 CRUD 接口是自然的扩展。范围是一个完整的特性(页面 + daemon 路由 + core 字段新增),不是无关的拼凑。 方案:范围合理。core 改动最小化(在 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewI formed an independent proposal before reading the diff: to add a scheduled-tasks management page, I'd add optional Core changes (256 lines, well under 500-line threshold): Backward-compatible. Route ( UI: Clean separation — pure schedule helpers ( Cross-package exports: Tests: 205 tests across 7 files, all passing. Good coverage of edge cases: corrupt files, disabled-task lifecycle, cap enforcement, non-divisor minute intervals, URL-encoded task IDs. Real-Scenario TestingBuilt the project ( All CRUD operations behave as specified. Typecheck and build clean. 中文说明代码审查在阅读 diff 之前,我独立构思了方案:在 Core 改动(256 行,远低于 500 行阈值): 向后兼容。调度器正确过滤 路由: 验证全面 — cron(含不可能日期如2月30日)、提示词长度、名称长度、类型检查。重新启用时正确重置 UI: 干净的分离 — 纯函数式计划辅助器、React 组件处理加载态和过期数据保护、i18n 覆盖中英文。 测试: 7 个文件共 205 个测试,全部通过。 真实场景测试构建了项目,启动 daemon,通过 curl 测试了完整 CRUD 契约。所有操作行为正确。Typecheck 和 build 全绿。 — Qwen Code · qwen3.7-max |
Stage 3 ReflectionThis PR does what it says: adds a visual management page for durable cron tasks that already exist in the codebase. The motivation is clear (chat-only management is inconvenient), the implementation reuses existing infrastructure instead of creating a parallel system, and the core changes are backward-compatible optional fields with well-tested scheduler logic. Going back to my independent proposal: the PR matches it closely. The one improvement over my initial thinking is the durable-only cap fix — I would have kept the combined cap and missed the silent-failure scenario where session-only jobs crowd out durable loads. That's a meaningful correctness win embedded in what looks like a feature PR. The code is straightforward. No over-abstraction, no speculative features, no drive-by refactors. The 27 files / 2748 additions look large, but the bulk is test code (205 tests), i18n strings (EN + ZH), and CSS — the actual logic surface is tight. Every change in the diff serves the stated goal. Real-scenario testing confirms the CRUD contract works end-to-end against a running daemon. Unit tests pass (205/205). Typecheck and build are clean. One minor observation for the author's consideration (not a blocker): the PATCH route currently supports updating cron/prompt/recurring in addition to enabled/name, but the UI only uses toggle and delete. If those extra PATCH fields aren't planned for near-term UI use, they could be deferred to keep the API surface minimal. But a complete CRUD is also defensible. Approved. ✅ 中文说明Stage 3 反思这个 PR 做了它声明的事情:为代码库中已有的持久化 cron 任务添加可视化管理页面。动机明确(仅聊天管理不方便),实现复用了既有基础设施而非创建并行系统,core 改动是向后兼容的可选字段加上经过充分测试的调度器逻辑。 回到我的独立方案:PR 与之高度一致。一个改进是持久化专用上限修复 — 我会保留混合上限而错过会话级任务挤掉持久化加载的静默失败场景。这是一个嵌在功能 PR 里有意义的正确性修复。 代码直截了当。无过度抽象、无投机性功能、无顺手重构。27 个文件 / 2748 行新增看起来很多,但大部分是测试代码(205 个测试)、i18n 字符串(中英文)和 CSS — 实际逻辑面很紧凑。diff 里每个改动都服务于声明的目标。 真实场景测试确认 CRUD 契约在运行中的 daemon 上端到端工作。单元测试通过(205/205)。Typecheck 和 build 全绿。 一个小观察供作者参考(不是阻断项):PATCH 路由目前支持更新 cron/prompt/recurring(除了 enabled/name),但 UI 只用了开关和删除。如果这些额外的 PATCH 字段近期没有 UI 使用计划,可以延后以保持 API 面最小化。不过完整的 CRUD 也是合理的。 已通过 ✅ — Qwen Code · qwen3.7-max |
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
WebShellSidebar.test.tsx:248 |
Missing click test for the new onOpenScheduledTasks sidebar button — the existing onOpenDaemonStatus has 2 click tests (expanded + collapsed) but the new clock button has 0 |
Add a describe('scheduled tasks entry') block mirroring the daemon-status tests |
scheduled-tasks.test.ts:220 |
PATCH handler length-cap branches (cron > 200 chars, prompt > 100K chars) have no test — POST covers the same limits but PATCH doesn't | Add a PATCH test sending cron: 'x'.repeat(201) and asserting 400 / invalid_cron |
ScheduledTasksDialog.tsx:449 |
"Run now" button fires the prompt on disabled tasks with no confirmation — contradicts the semantic of the enable/disable toggle | Add disabled={!task.enabled} on the button, or a window.confirm when the task is off |
ScheduledTasksDialog.tsx:189 |
Delete confirmation passes task.prompt (up to 100K chars) to window.confirm — produces an unreadable/hanging dialog |
Truncate the label: rawLabel.length > 60 ? rawLabel.slice(0, 57) + '...' : rawLabel |
cronTasksFile.ts:276 |
isValidTask strict type check on optional enabled/name fields — "enabled": null (common in hand-edited JSON) fails validation and makes the entire file unreadable |
Coerce wrong-type optional fields to absent rather than rejecting the entry |
scheduled-tasks.ts:42 |
MAX_SCHEDULED_TASKS = 50 duplicates MAX_JOBS = 50 across packages with no compile-time coupling — drift risk if either is changed independently |
Export a shared constant from cronTasksFile.ts and import in both locations |
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
[Suggestion] cron_list tool doesn't surface new name/enabled fields
In packages/core/src/tools/cron-list.ts:74, the fileTasks.map mapping drops the new name and enabled fields added by this PR. A disabled task appears in cron_list output indistinguishable from an enabled one — the agent cannot tell that a disabled task will never fire. This creates a divergence between the management UI and the agent's view.
Suggested fix: add name: task.name and enabled: task.enabled !== false to the mapping.
— qwen3.7-max via Qwen Code /review
- cron_list: surface name/enabled so the agent can tell a disabled durable task from an active one (a disabled task no longer looks identical to an active one). - core: export only the tasks-file functions the daemon route actually uses (drop unused addCronTask / getCronFilePath / CRON_TASKS_DISPLAY_PATH from the public barrel). - CronScheduler: warn when a durable reload fails and the prior view is kept, since a just-disabled or -deleted task can keep firing until the next successful reload. - Extract the schedule helpers (buildCron / describeCron / parseHhmm / describeLastRun) into a pure module and add unit tests for them. - Add route tests for PATCH cron/prompt/recurring, empty-patch rejection, and POST field-length / boolean-type validation.
|
Re:
中文关于
|
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
Addressed in 76c4744: cron_list now surfaces name/enabled and tags disabled tasks with a , disabled marker (see PR comment). All four inline review threads are resolved and CI (Test ubuntu) is green. Dismissing this stale pre-fix review.
wenshao
left a comment
There was a problem hiding this comment.
- Log CRUD errors server-side (writeStderrLine) in each route catch block, matching the other daemon routes. - Share one id generator (generateCronTaskId in cronTasksFile) between the scheduler and the daemon route instead of duplicating it. - describeCron: recognize cron day-of-week 7 as an alternate notation for Sunday. - Reset the builder time to :00 when switching to the hourly frequency (its time picker is hidden, so it no longer silently carries the daily minute). - Tests: cron_list name/disabled output; route Feb-30 impossible-cron and corrupt-file 500 read-failure; describeCron dow=7.
- Run now: report sendPrompt rejections via the toast/error path instead of dropping the promise. - Block chat interaction while the full-pane Scheduled Tasks view is open, so the covered composer can't receive keystrokes/Escape. - Guard reload() with a request-sequence id so a slow load can't overwrite a newer list after a mutation. - Re-enabling a task that had genuinely fired resumes from now instead of catching up work paused while it was disabled. - Restrict "every N minutes" to divisors of 60 (a non-divisor */N fires more often than the label claims). - Show a Repeats / Runs once label on each card so tool-created one-shots aren't mistaken for repeating schedules. - Return generic 500 client messages (no internal file path); the detail is logged server-side. - Tests: SDK scheduled-task methods (method/URL/id-encoding/headers/errors); route re-enable behavior both ways.
All inline threads from this review are fixed and resolved across 18a04af + caadb8d (server-side logging, cron_list name/disabled + tests, id-generator dedup, GET/Feb-30 route tests, hourly-minute reset, dow=7). CI (Test ubuntu) is green on the head commit. Dismissing this stale review pinned to an older commit.
- Route error logs interpolate the actual task id instead of the literal ":id". - cron_list returnDisplay includes the task name (matching llmContent) so terminal /cron list shows UI-assigned names. - Truncate the delete-confirm label so an unnamed task's long prompt doesn't blow up the confirm() dialog. - Cap the create-form prompt textarea at MAX_PROMPT_LENGTH and drop the dead typeof-window guard. - Test generateCronTaskId (format + near-uniqueness).
wenshao
left a comment
There was a problem hiding this comment.
Downgraded from Request Changes to Comment: self-PR.
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
- Re-enable now resumes any recurring task from now (stamp on every false→true), not only ones that had already fired — a task disabled before its first run no longer catch-up-fires the slot it was paused through. - describeCron applies the same divisor-of-60 check as buildCron, so a hand-edited/persisted */45 falls back to the raw expression instead of a misleading "every 45 minutes". - Strengthen the corrupt-file route test to assert the generic client message and no leaked file path. - Tests: recurring-disabled-before-first-run and one-shot re-enable; describeCron non-divisor fallback.
Seed a pre-fields task (no name/enabled) directly to disk and assert the GET response normalizes it to name:null / enabled:true, guarding backward compatibility with existing scheduled_tasks.json files.
The daemon route accepts up to MAX_JOBS durable tasks on disk, but the scheduler previously capped durable loads against its combined job map (session-only + durable). A session holding session-only cron jobs could push the map to MAX_JOBS and make loadFileTasks silently skip durable tasks the route had already accepted — a create that returned 201 would then never fire. Cap durable installs against a durable-only count instead, and share one MAX_JOBS constant between the scheduler and the daemon route, so a successful create is always loadable. Adds a scheduler test that 40 session-only jobs no longer crowd out 20 durable loads.
BZ-D
left a comment
There was a problem hiding this comment.
LGTM. I re-reviewed the two latest commits. The durable-load cap now uses a durable-only budget and shares MAX_JOBS with the route, which addresses the created-but-not-loadable edge case I flagged. The added legacy normalization test also covers the backward-compatible name/enabled defaults.
Verified locally on the latest head (62485aa829ce1836660c7472a397a780cfde99f5):
packages/cli:npx vitest run src/serve/routes/scheduled-tasks.test.ts(19 passed)packages/core:npx vitest run src/services/cronScheduler.test.ts src/services/cronTasksFile.test.ts src/tools/cron-list.test.ts(143 passed)packages/web-shell: schedule helper/sidebar tests (37 passed)packages/webui: scheduled task actions test (6 passed)packages/core: build + typecheck passedpackages/webui: typecheck passed
I did not find additional blocking issues.
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
What this PR does
Adds a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace. The sidebar gains a clock entry that opens a full-pane page (it replaces the chat area, it is not a modal overlay) listing each task with an enable/disable toggle, a delete and a run-now action, and a human-readable schedule. "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview. "Create via chat" returns to the chat and pre-fills the composer so the user can describe the task in natural language and the agent creates it through its
cron_createtool. On the daemon side, new CRUD routes read and write the existing per-projectscheduled_tasks.json; task firing continues to be handled by the session-side scheduler.Why it's needed
qwen-code already ships a durable cron engine (
cron_create/cron_list/cron_deletebacked byscheduled_tasks.json), but until now the only way to see or manage those tasks was through chat tool-calls. This gives Web Shell users a visual place to review, create, toggle, edit, and delete their recurring tasks — the "Scheduled tasks" affordance other agent desktops offer — while reusing the existing engine instead of adding a second one.Reviewer Test Plan
How to verify
Run the daemon-backed Web Shell and open the page:
npm run dev:daemon, then open the printedhttp://localhost:5173/?token=…URL.enabled: false.cron_create, after which it shows on the page.Automated checks run locally on this branch:
enabled:falsepersists toscheduled_tasks.json→ invalid cron 400 → DELETE 200 → DELETE again 404 → 51st create 409 (max-tasks cap).role=dialogcount 0), create is a modal (role=dialogcount 1) that closes after Create, card renders "工作日 12:30", toggletrue→false, "Create via chat" returns to chat with the composer pre-filled. Zero console errors.enabled-skip and name/enabled round-trip), cli route 9, web-shell sidebar 29.tsctypecheck and eslint clean; fullnpm run buildgreen.Evidence (Before & After)
Before: there was no Scheduled Tasks page — durable tasks were only reachable via chat tool-calls.
After — the page (replaces chat), the create modal, and "Create via chat":
Tested on
Environment (optional)
npm run dev:daemon(vite dev server + daemon) and a built daemon (node packages/cli/dist/index.js serve --web).Risk & Scope
scheduled_tasks.json; firing still requires a live agent session holding the durable lock, so this PR intentionally does not add an always-on daemon-level scheduler.cron_create(durable: true), so a session-only task would not appear on the page.DurableCronTaskgains optionalname/enabledfields; existing task files load unchanged and tool-created tasks keep firing.Linked Issues
中文说明
这个 PR 做了什么
给 Web Shell 增加一个「定时任务」管理页,用于管理当前工作区的持久化 cron 任务。侧边栏新增一个时钟入口,点开后是一个占据右侧主区域的整页(它替换聊天区,而不是浮层弹窗),以卡片列出每个任务,带启用/停用开关、删除、立即运行,以及人类可读的计划描述。「新建定时任务」打开一个弹窗,内含计划构造器(每天 / 工作日 / 每周 / 每小时 / 每 N 分钟 / 自定义 cron)和实时预览。「通过聊天创建」会切回聊天并预填输入框,用户用自然语言描述任务,由 agent 通过它的
cron_create工具创建。daemon 侧新增的 CRUD 路由读写既有的按项目scheduled_tasks.json;任务的实际触发仍由会话侧的调度器负责。为什么需要
qwen-code 已经内置了一套持久化 cron 引擎(
cron_create/cron_list/cron_delete,由scheduled_tasks.json持久化),但此前查看或管理这些任务的唯一方式是聊天里的工具调用。这个改动给 Web Shell 用户一个可视化的地方来查看、创建、开关、编辑、删除周期任务——即其它 agent 桌面端常见的「定时任务」入口——并且复用既有引擎,而不是再造一套。复审测试计划
如何验证
跑起 daemon 版 Web Shell 并打开页面:
npm run dev:daemon,然后打开打印出的http://localhost:5173/?token=…。enabled: false的任务。cron_create创建,之后会出现在页面上。本分支上本地跑过的自动化检查:
enabled:false落盘scheduled_tasks.json→ 非法 cron 400 → DELETE 200 → 再 DELETE 404 → 第 51 个创建 409(任务上限)。role=dialog计数 0)、创建是弹窗(role=dialog计数 1)且创建后关闭、卡片渲染「工作日 12:30」、开关true→false、「通过聊天创建」切回聊天且输入框已预填。零 console 错误。enabled跳过与 name/enabled 往返)、cli 路由 9、web-shell 侧边栏 29。tsc类型检查与 eslint 通过;完整npm run build全绿。证据(前后对比)
之前:没有定时任务页——持久化任务只能通过聊天工具调用触达。
之后——整页(替换聊天)、创建弹窗、以及「通过聊天创建」:
(见上方英文部分的三张截图。)
测试平台
macOS ✅;Windows⚠️ 未测;Linux ⚠️ 未测。
运行环境(可选)
npm run dev:daemon(vite dev + daemon)以及构建后的 daemon(node packages/cli/dist/index.js serve --web)。风险与范围
scheduled_tasks.json;触发仍需要一个持有持久锁的活跃 agent 会话,因此本 PR 有意不引入常驻 daemon 级调度器。cron_create(durable: true),若建成仅本次会话的任务则不会出现在页面上。DurableCronTask新增可选name/enabled字段;既有任务文件照常加载,工具创建的任务照常触发。