Repository navigation
feat(webui): run the WebUI's own scheduled tasks, and record how that ends - #21
Merged
Merged
Conversation
… ends The WebUI is a loopback process the user starts. Neither cron engine in the tree can serve it: the v1 path is pinned off for v2-compat hosts and its table is empty after the v2 migration moved the data out, and the v2 `CronService` is created only for an Electron owner and is not carried on the host contract in any case. Measured on a real WebUI host; ADR 0012 carries the full chain and the decision. So the WebUI runs its own: a store at `<dataDir>/webui/scheduled-tasks.sqlite` with its own table, an in-process tick beside the heartbeat in `WebuiService`, and a `WebuiScheduledTaskPort` whose six methods name operations rather than an engine. It never selects from the runtime's cron tables — that would be a second scheduler with none of v2's concurrency guards writing into the same agent queue as the desktop client. Being alive is the execution guarantee. Slots missed while the process was down are **not** replayed; they are counted (`missed_count`, `last_missed_at_ms`) so the surface can say so rather than imply the task never existed. `better-sqlite3` is declared here for the first time: the WebUI did not persist anything before, and it had been reaching the module through the workspace's hoisted `node_modules` without declaring it. ADR 0012 states the retirement condition as two independently checkable gates, and the probe test reports on them on every run — quietly while both are shut, and with a notice when the runtime moves. It never fails a build: a gate that goes red for something that is not a defect only teaches people to ignore red. Retirement is the adapter, the table migration, and tearing down this tick loop in the same change; two schedulers over one queue is what the decision exists to prevent.
antianqi
added a commit
that referenced
this pull request
Oct 5, 2026
…wo dark tabs Roadmap E 区 ships five rows. PR #20 took the Revert / Reapply row. This takes the two that the roadmap records as ❌ with the reason "设置 tab 禁用". That reason is only half of it. Both tabs were disabled because there was nothing behind them, not because a gate refused them: `SettingsModal`'s render chain is a series of `active === …` ternaries with branches for `desktop`, `usage`, `account` and `archived`, and everything else fell through to an `webui-settings-empty-panel` placeholder. Flipping `disabled` to `false` would have produced a clickable label above a blank pane — the same shape of defect the row already had. So both pages are built. 代码审查 (Review 审查模式 / 修复建议+跳转) The capability was already wired end to end and unused from here: the workspace review operations (`getWorkspaceReviewSummary`, `listWorkspaceReviewFileDiffs`, `searchWorkspaceReviewDiffs`) are registered, exposed on the transport and typed — the diff card's Review button already dispatches into the workspace panel. What was missing was a place to read a whole change set. The page lists the change set with per-file stats, loads each file's unified diff on expand in batches of five, and turns a diff line into a real editor jump: `projectWebuiReviewLines` numbers both sides from the hunk header, and a click resolves to the new-side line. A deleted line gets no target, because it is not in the file the editor will open — that is asserted directly rather than guarded, so the contract lives in a test instead of in an untestable line. Also plumbed: `workspaceDir` and `onOpenFileLine` now travel FoundationApp → UserMenu → SettingsModal. The jump reuses the shell's existing `#session=`/`open-file` path, so there is one navigation route rather than two. 工作树 (工作树隔离) Creating an isolated worktree already worked from the rail's context menu (「复制到新工作树」, with `worktreeVisible` / `worktreeUnavailableReason` on the server). This adds the other half of the row: seeing which parallel experiment branches exist and switching into one. A worktree is not a first-class field on a session, so it is inferred — git makes a second checkout with its own absolute path, and `isDefaultWorkspace` is the runtime's own statement of which one is primary. Grouping normalises path separators first, because a fork round-tripping a Windows path would otherwise report one checkout twice. Two tabs were left disabled on purpose: 语音 / 快捷键 / 个性化 / 连接 still have no content behind them, and the `disabled` gate is what keeps them from being clickable labels over an empty pane. Validation - pnpm check:source exit 0 (inventory +6) - pnpm typecheck:webui exit 0 - pnpm typecheck:webui-full exit 0 (covers the new .tsx tests) - pnpm build:webui exit 0 - node scripts/run-vitest-suite.mjs webui 9 failed | 1637 passed | 4 skipped - npx playwright test test/webui-browser/ 79 passed The 9 failures are the pre-existing Windows baseline and are unchanged from the count on the parent commit: 7 webui-boundary-check path assertions, 1 webui-design-tokens path assertion, 1 webui-service shutdown timeout. The same 9 were verified on a clean tree earlier in this branch. Test evidence Four new test files, 56 tests, plus one browser test: - review-state.test.ts (21) — lifecycle, the search filter, and the diff projection including both-side line numbering - review-panel.test.tsx (16) — the panel's render, and the tab-to-page wiring - worktree-state.test.ts (13) — path normalisation and checkout grouping - worktree-panel.test.tsx (6) — the worktree list render Negative injection: 17 mutations across all three new seams, 0 survivors, every implementation file restored byte-for-byte. Two of those mutations were equivalent on the first pass and both were fixed rather than waived: - `webuiReviewLineTarget` re-checked the line kind even though `projectWebuiReviewLines` only ever assigns a `newLine` to additions and context rows. Deleting the guard changed nothing, so nothing could observe it. The guard is gone and the invariant is now asserted directly in the test, which makes a projection that starts numbering deletions fail loudly (4 tests red). - The tab-to-page routing had no coverage at all. `SettingsModal` opens on `desktop` and only moves tab from a click handler, so no `renderToStaticMarkup` call can reach the `coding` or `worktree` branch — replacing `{active === "coding" ? <SettingsReviewPage` with `{false ? …` left the whole suite green. That is now covered twice: a source-level assertion with the reasoning written down, following the shape `rail-pin-affordance.test.ts` already uses, and a real browser test that clicks both tabs and fails if either lands on the empty-pane placeholder. One existing test changed rather than the code: `settings-modal.test.tsx` and `settings-account-tab.spec.mjs` both asserted that `coding` was among the tabs that must stay disabled. The two gates moved `coding` and `worktree` into the implemented group and left the remaining unimplemented tabs asserted, so neither test lost its teeth. Scope: the Review 审查模式, 修复建议+跳转 and 工作树隔离 rows. Diff 展示 was already ✅ and is untouched. Revert / Reapply still has its server half open — surfacing skipped files needs a field on `LocalTurnDiffMutationBody` in `@mavis/protocol/local`, which is the v1 layer under active rework in #21, so it does not belong in this PR. Described in the PR body.
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The WebUI runs its own scheduled tasks until the runtime offers one. ADR 0012 carries the reasoning; this is the implementation and the tripwire that watches for the day it should end.
Why not the runtime's cron
Measured on a real WebUI host, neither engine can serve it:
local-runtime-v2/src/compat/v1/runtime.ts) and its table,local_runtime_crons, holds 0 rows —migration-0002-copy-legacy-cron-data.tsmoved the data out.enableCroncomes fromownsElectronRuntimeCapabilities(runtimeOwnerKind), and the WebUI declaresruntimeOwnerKind: "tui".CreatedLocalRuntimeHostcarries onlyapplication?andcliService?.An earlier attempt to reach it by declaring the WebUI an Electron owner made it worse and was reverted:
isV2RuntimeOwnerneedscapabilities.electronHostfor that kind, and without it the whole v2 service group — including thecliServiceevery other feature uses — goes away.What this adds
A store at
<dataDir>/webui/scheduled-tasks.sqlitewith its own table, migrated through SQLite's ownuser_version(each stepIF NOT EXISTS, DDL and version number in one transaction, so a restart is a no-op and an interrupted upgrade resumes). An in-process tick beside the existing heartbeat inWebuiService. AndWebuiScheduledTaskPort— six methods that name operations, not an engine.It never selects from
local_runtime_cronsorlocal_runtime_v2_cron_definitions. Reading the latter from here would be a second scheduler with none of v2's concurrency guards (claimExecutionis an atomic conditional update;insertPendingScheduledconverges on a trigger id) writing into the same agent queue as the desktop client.The part that is a product decision, not a defect
The WebUI process being alive is the execution guarantee. It is started on demand, so when nobody is running it, nothing fires. Slots missed while it was down are not replayed — they are counted (
missed_count,last_missed_at_ms) so the surface can say so instead of implying the task never existed.Consequence, stated plainly: a scheduled task in the WebUI and one in the desktop client are two different things that cannot see each other.
The tripwire
ADR 0012's retirement condition is two independently checkable gates, and a test reports on them on every run. It never fails a build — a gate that goes red for something that is not a defect only teaches people to ignore red:
Silent while both are shut. A notice when the runtime moves, escalating to the retirement steps when both are open, and a distinctly-worded "this is not a retirement signal" when the probe itself cannot confirm. It writes to the real stream rather than
console, because vitest's default reporter swallows console output from passing tests — which would have made the report invisible in CI.Verification
verify --profile fulltest:webuicheck:sourcetypecheck:webuiserver / client / testThe new tests were written first and caught three real bugs in the implementation during the red-to-green pass, including a success path that never recorded its outcome and a
typeof null === "object"that crashed the whole handler.Not verified
WebuiServiceto a live tick is covered only by this PR's own suite, not by a browser or an end-to-end gate.createRequire, and this PR is the first to declare it inpackages/webui. CI'swindows-latestjob is the real check.