Repository navigation
feat(scheduled): 定时任务页改造为「任务 + 运行记录」主从视图(#1291 阶段一) - #1298
Conversation
The Scheduled route could only list tasks beside one global run feed, so checking a single automation meant switching tabs, scrolling a history that mixed every task, and leaving the page to read a result. It now pairs a task column with the selected task's own page: last outcome and duration, next occurrence, instruction, that task's own runs, and the selected run's transcript read in place through a bounded session read. A run's transcript is the automation's output, not a conversation the reader started, so the host now reports `scheduledRun` on every session summary and search hit. The flag is derived from `task_runs` on read and never stored on the session row, which keeps the ownership honest and returns a task's transcripts to the ordinary lists when the task is deleted. refs vastsa#1291
The Scheduled route owns a run's transcript and the session list is no longer an entry point to it, so the chat surface needed its own way back: a scheduled run's conversation now shows a back row in the top bar. It names the route and, when the run row itself was the origin, the task the run belongs to. Returning restores that task and that run — the exact row the reader was reading — and steps back through the navigation history when this route is directly behind the conversation. Conversation ownership becomes an explicit per-task setting in the same change: a task either opens a new conversation for every run or continues one, and the reuse path only continues a session that still exists and still belongs to the task's project. The editor states the choice beside the task's other execution facts, and the task page reports it with the run it produced. refs vastsa#1291
A task could only run on a clock, so "every 30 minutes" had to be approximated with an hourly task. An interval cadence now states its own span as a count with a minute or hour unit (5 minutes to 24 hours, stored as minutes), counts from the moment it was armed, and is refused without its value instead of being saved as a task that never fires. The task row reports the span rather than a clock, and the value rides along with every armed cadence, so switching a task between a calendar and an interval loses neither. Opening the form also left the task column and the task page rendered below it, competing with the draft; the form owns the page while it is open. The selected task and the selected run gained an accent tint with a leading bar, because the previous raised fill was white on white in the light theme and read as no selection at all. refs vastsa#1291
|
感谢提交并附上了详细验证记录。我核对了 PR 与 #1291 的需求,当前变更覆盖了按任务读取运行记录、页内转写、侧栏/搜索过滤,以及删除任务后恢复会话可见性的主要路径。 审查发现一处可复现的边界问题: 另外,仓库当前 |
|
按仓库 AGENTS.md §15 的临时外部 PR 类型政策关闭;上方审查评论记录了权限原因及已发现的最近运行状态问题。 |
|
更正:PR 作者是受维护者授权的定向开发贡献者,因此适用 代码审查发现的运行状态问题仍有效:任务列从全局最近 100 条运行记录推导每个任务的最近状态,因此最后一次运行落在窗口之外的任务会误显示为“从未运行”。请修复后再继续合并审查。 |
The task column derived every row's outcome from one global window of the newest 100 runs, so a task that had been idle while other tasks produced runs was reported as never run — and a task whose run scrolled out of the window lost its running indicator the same way. `scheduled.listRuns` now answers `latestPerTask`: one newest run per task, independent of how much history the table holds, which is what the column reads. The single-task history read and the global window keep their previous shapes. Reported by @vastsa while reviewing vastsa#1298. refs vastsa#1291
|
回复 review 里「最近运行状态」那条:确认是真实缺陷,已经修好,分支也更新了(head 现在是 原因:任务列原来读的是 修法按你的建议做成 host 侧有界读取:
验证:
另外看到 PR 又被打开了。AGENTS.md §15 的类型政策我理解,如果这条仍然适用,我这边不做额外动作;如果需要我按别的方式配合(比如拆成 补充两点:
|
vastsa
left a comment
There was a problem hiding this comment.
审查结论:实现方向、宿主契约和测试都站得住,但有 3 处需要先修(都在本 PR 自己写下的承诺范围内),另有 2 处需要定一下边界。修完我再合入。
我的验证(把 origin/main 9c4093bd0 合进 PR head 7162dc65 的本地 worktree 上跑)
pnpm build:js、pnpm --filter @pi-desktop/desktop typecheck、pnpm lint通过;cargo test -p host-core --locked— 741 passed。- desktop 测试 3407 项:3406 通过,唯一失败是
npm-executable.test.mjs的「5 秒预算」用例在并发跑测时超时(4981ms > 5000ms 预算),单独重跑 12/12 通过 —— 负载下的偶发,与本 PR 无关。 - i18n:9 个语种新增 30 个键完全对齐,无缺失/多余,非英语语种是真实翻译。
- 派生标记:
scheduled_run只在三处 SQL 读取(sessions.rs:1238/1452、session_search.rs:100),都带AS scheduled_run;其余构造点显式false;旧 host 缺字段时为undefined,渲染层用=== true(session-origin.ts:21、ConversationTopbar.tsx:97),不会误藏。 - 删除语义成立:
task_runs.task_idCASCADE +session_idSET NULL,删任务确实把转写放回普通列表;reuse只从task_runs取且校验deleted_at/项目,begin_run失败只清理本次新建的会话。 latestPerTask与taskId互斥、走(task_id, started_at DESC)索引,任务列不再读全局窗口 —— 之前那条缺陷已修好。- 注意:head 现在不含最新 main(
main已前进到9c4093bd0),合入前需要先把 main 合进分支。
需要先修
-
Cmd+K 的「最近会话」仍然列出并能进入自动化转写。
apps/desktop/src/components/SearchDialog.tsx:148:const source = q ? search.hits.map((hit) => hit.session) : sessions;—— 空查询走的是 store 全量sessions,而过滤只加在apps/desktop/src/hooks/use-session-search.ts:35的命中那一路上。自动化会话的updatedAt通常最新,进前 30 条,所以打开搜索面板第一屏就能看到、也能切进去。托盘同样:apps/desktop/electron/main/tray-sessions.ts:89,102直接用session.list的结果构建「最近会话」。这与本 PR 写进规范的句子冲突(docs/spec/04-ux/01-ui-ia.md§3.3「the SessionList and global session search never list it」、docs/spec/04-ux/08-component-spec.md§6)。建议复用同一判定:渲染层用isAutomationSession,托盘在 main 进程里内联session.scheduledRun === true。 -
英文规范有一处重复残句。
docs/spec/04-ux/01-ui-ia.md:266-268:…and whether it is paused or/running.之后第 268 行又出现whether it is paused or running. The task page shows…。应删掉重复片语,让 268 行从The task page shows开始。 -
中文规范两处与英文版不一致/被截断。
docs/zh-CN/spec/04-ux/01-ui-ia.md:213段落以「…以及暂停或运行中状态;」结束就没了:英文版接着写的「任务页显示最近一次运行、下次执行、项目、权限、模型、指令折叠、本任务运行历史、页内读取转写、返回入口、侧边栏时钟与全局搜索进入本页、立即运行停留在本页」整段在中文版缺失;原来那句「侧边栏时钟与全局搜索均可进入」被删掉后没有替换。docs/zh-CN/spec/03-runtime/04-data-storage.md:919-920:config_json 保存 schedule: {hour, minute, weekday}后面直接接`workspacePath`、会话模式…,中间丢了nextRunAt(毫秒时间戳)与intervalMinutes(5–1440),与英文版不一致,而下一段已经在用intervalMinutes × 60_000。
(顺带一个小语病:
docs/spec/06-delivery/04-e2e-test-plan.md:432的pause/resume; Run now/select an interval cadence…读起来像「Run now select…」,补个分号即可。)
需要定边界(已确认存在,但不算回归,可以选择不在本 PR 修)
-
保留策略会让旧转写回到侧栏。
crates/host-core/src/db/migrations.rs:4,116-124每次打开数据库按任务只保留最近 100 条 run。被删掉 run 的会话随即失去scheduled_run标记,于是下次启动后它们重新出现在侧栏;同时这些 run 已不在任务页的历史里(行已删)。按新加的间隔周期算:interval 5 分钟约 8 小时、30 分钟约 2 天、hourly 约 4 天就会发生。改动前它们本来就在侧栏,所以不是回归,但和「转写只在定时任务页」的承诺不一致 —— 要么在 data-storage §4.11 / component-spec §20A 写明这条边界,要么按你之前在 #1291 里的思路,用持久的 origin 字段解决。 -
TASK_RUN_LIMIT = 200(host clamp 1..200):进程长时间不重启、某任务 run 数超过 200 时,第 200 条以前的 run 的转写在页内历史里读不到(下次启动被 prune 后又冒回侧栏)。窗口很窄,同第 4 条一起看即可。 -
两个小点,不影响合入:搜索是在渲染层按页过滤(整页都是 automation 命中时用户看到 0 条但仍需点「加载更多」,host 侧过滤可消除);
task_runs(session_id)没有索引,新增的EXISTS每行查一次(当前量级无碍)。
未做
真机 E2E 我没有跑(worktree 里没有补构建产物);scripts/e2e-scheduled.mjs 仍在找已经取消的 Reasoning 菜单项,这条在 main 上就存在,与本 PR 无关。
改好 1–3(第 4/5 条如果决定只写文档也请在 PR 里说一声),并把 main 合进分支后回我,我核对 CI 后合入。
The Command-K recents view and the tray still offered a scheduled run's transcript, because only the SessionList and search hits applied the ownership rule: the palette's empty-query list reads the store directly, and the tray builds its recents from `session.list`. Both now go through the same rule — one `listableSessions` helper in the renderer and its main-process twin for the tray — so no session list offers a conversation that belongs to the Scheduled page. Also repairs the spec text an earlier change left truncated (a duplicated English fragment, the Chinese UI/IA paragraph and the config_json field list), records the retention boundary a fast cadence reaches — a pruned run takes its session's ownership marker with it and that transcript returns to the ordinary lists — and indexes `task_runs(session_id)` for the ownership probe that summaries and search run per row. Reported by @vastsa in review of vastsa#1298. refs vastsa#1291
|
三条必修项都改好了;4/5 我选择只写文档,第 6 点里补了一个索引。 1. Cmd+K 的最近会话 / 托盘确认属实,两处都绕过了归属判定:搜索面板空查询读的是 store 全量 改法:新增共用判定 测试: 这里我没有顺手动 Projects 页:项目索引里的会话列表与计数属于项目管理面(涉及批量删除与「N 个会话」),把它们排除出去会改变计数语义。如果你认为那里也该排除,我按你的结论改。 2. 英文规范重复残句已删掉重复片语, 3. 中文规范截断 / e2e 语病
4 / 5. 边界:选择只写文档理由是持久化 origin 需要会话表迁移,还会牵扯远程会话与 fork/复制语义,不适合塞进这个已经很大的 PR。已写进:
如果你希望这次就上持久化 origin,我可以单独开 PR,不动这个分支。 6. 两个小点
验证(干净构建)
我这边没有别的异议了,麻烦你再核一遍。 |
The reordering left the previous three lines in place, so the step list repeated "fields and verify its first occurrence is one hour away; pause/resume; Run now" and trailed off at "conversation, use model tool calls…". Restores the original "observe automatic completion; delete the settled task." step, which the Chinese plan still states.
|
三条必修项都在
本地核验(head
我在你的分支上补了一个提交( CI 四项已经在这个 head 上全绿,我这就合入。 |
复核通过:三条必修项已在 da6a883 改好,本地与 CI 全部通过;另外补了 e2e 步骤的重复文本修复(26768614e)。
摘要
Issue #1291 的阶段一:把定时任务页改成「任务列 + 选中任务页」,让每次运行的结果留在页内可读,并把定时任务的会话从主界面的会话列表与全局搜索里收回去。之后按你的反馈补了会话模式、间隔周期,以及两处 UI 细节。
分支
feat/scheduled-run-detail,rebase 到origin/main3e34ca07d,共 3 个提交:feat(scheduled): read one task's runs inside the Scheduled pagefeat(scheduled): return from a run's conversation to its taskfeat(scheduled): add an interval cadence and a form that owns its pagerefs #1291(阶段二、三仍未做,见文末)
包含内容
宿主
scheduledRun:对task_runs做EXISTS,不写入会话行。会话列表与全局搜索据此隐藏自动化转写,删除任务后转写自动回到普通列表,不会变得无法访问。scheduled.listRuns支持{ taskId, limit };scheduled.create/update支持任务级会话模式perRun/reuse(reuse只在上一次会话仍存在、且仍属于同一项目时继续,否则新开)。interval周期与schedule.intervalMinutes(5–1440 分钟,按经过时间排程;interval缺少该值时写入被拒绝,而不是存成一个永不触发的任务)。AI 工具ScheduledTaskCreate/Update的 schema 同步放开该周期与字段。渲染层
listableSessions,主进程trayVisibleSessions)。文档 / 测试
未包含(后续阶段)
验证
任务候选基于
origin/main82f94b4ba(已把 main 合并进分支,pnpm check:pr-base通过):pnpm build:js、pnpm lint、pnpm --filter @pi-desktop/desktop typecheck、pnpm docs:check(546 页)pnpm --filter @pi-desktop/desktop test— 3410 通过pnpm --filter @pi-desktop/i18n test— 29 通过;pnpm --filter @pi-desktop/shared test— 1158 通过cargo test -p host-core --locked— 741 通过;cargo fmt --checklatestPerTask修复另加两条 host 用例:全局窗口丢任务 vs 每任务最新一条、latestPerTask与taskId互斥真机验证(zh-CN、独立配置目录、本地 SSE 夹具模型,建了 3 个任务,其中一个为 30 分钟间隔任务):
(后续补测:给一个任务塞 140 条更新的完成记录、把另一任务记录整体前移 200 分钟,任务列里空闲任务仍显示「已完成 · 3小时前 · 1s」而非「尚未运行」。)
已知问题(都先于本 PR 存在于 main)
—— 这条是我本地scripts/check-architecture.mjs失败main引用过期(还停在 v0.15.10)造成的误判:用 PR 的 base 作为参照跑check-architecture是通过的(New TS/TSX files checked: 12),CI 的四项检查也全部通过。已在下面更正。scripts/e2e-scheduled.mjs在 main 上已过期:它仍在.composer-menu-entry里找 Reasoning 项,而 UI 早已换成推理等级滑块(参见ComposerModelPicker.tsx里 [Feature] 推理等级交互优化 #417 的注释)。所以上面的端到端结论来自我自己的真机脚本,而不是这个脚本。构建产物相对路径少一级—— main 的 fix(electron): resolve main output root from inside Rollup chunks #1307(fix(electron): resolve main output root from inside Rollup chunks)已经修好这一条;我删掉了当时为跑真机临时补齐的构建产物,最近一次真机验证是在干净构建上跑的。AI 辅助说明
本 PR 的实现、文档与验证由 AI 编码代理协助完成:模型
deepseek-v4.1-flash,思考程度max。上面所有命令与真机结论都是我在本地实际执行的;已知失败项(架构检查、过期 E2E 脚本、构建产物路径)也照实列出,没有隐藏。