qwen-code/packages/web-shell/client/components/dialogs
Shaojin Wen dd62c3a8e5
feat(scheduled-tasks): gate an isolated run behind a precondition (#6619)
* feat(scheduled-tasks): gate an isolated run behind a precondition

An isolated scheduled task now takes an optional `condition` alongside its
prompt. On every fire the task's bound session evaluates the condition as an
ordinary cron turn, and only dispatches the prompt into a fresh sub-session
when that turn's verdict is YES.

The check deliberately runs in the bound session rather than a throwaway
sub-session:

  - It has exactly the semantics of a `shared` fire — same tools, same
    workspace approval mode — so it introduces no new permission surface.
  - The bound session of an isolated task is otherwise empty, so its
    transcript becomes the task's decision log: the record of why a fire did
    or did not happen.
  - No session is minted for a run that never occurs.

Everything that is not a YES skips the fire: NO, an unparseable answer, a
tool-loop error, a cancelled or timed-out turn. A precondition exists to
withhold an unattended run, so an ambiguous answer must withhold it too.

A `missed` (late-delivered) fire is judged the same way and then keeps its
existing in-session path — a precondition changes whether a fire runs, never
how. The Web Shell's "Run now" evaluates the condition before relaying the
dispatch through the model, so a manual run reproduces a scheduled one and
doubles as the way to test that a condition is written correctly.

The field is isolated-only. `POST`/`PATCH /scheduled-tasks` reject a condition
on a shared task, judging the combined post-patch state so a condition can be
stranded from neither side; the check is gated on the request actually
touching `condition` or `runMode`, so a hand-edited stranded task stays
editable. `isValidTask` requires a non-empty string, since the fire path gates
on truthiness and an empty condition would silently un-guard the task.

* fix(scheduled-tasks): harden the precondition against four review findings

Never judge a `missed` fire. That job is the scheduler's synthetic carrier:
one batched notification covering every one-shot missed in this load, built
from a spread of the first task, whose prompt is a notice ("these were missed
— ask the user before running them") rather than any task's command. Gating it
on the first task's precondition let a `NO` silently suppress the notice for
its siblings, which `removeMissedFromDisk` has already deleted. The scheduler
now strips per-task guard state from the carrier, and the session refuses to
judge a missed fire — the same contract enforced from both ends.

Distinguish a truncated turn from a clean one. A permission cancel or a
detected tool loop returns mid-tool-loop without aborting the turn's signal or
recording an error, so it reached `onComplete` as `'ok'`. A model that emits
`DECISION: YES` as text in the same streaming round as its tool call would then
release the fire on a verdict it never got to revise. Add an `'incomplete'`
outcome, set at both early returns.

Require the verdict to be the whole line. `\b` after the verdict accepted
`DECISION: YES, but I could not verify it` as a YES. The prompt asks for a line
that is exactly one of the two; a hedged answer is not a decision, and a
precondition must fail closed on an answer it cannot trust. Closing markdown
and terminal punctuation are still tolerated.

Require a bound session for a condition. The check is evaluated in the task's
own session — that is what makes its transcript a decision log. A task with no
`sessionId` (tool-created, or created with no bridge to bind one) fires through
the shared per-project durable owner, so its check would be injected into
whichever session holds that lock. Both create and update now reject a
condition on such a task instead of quietly relocating the check.

Also log the decision point: a non-ok outcome reaches stderr, since the
scheduler has already booked the run and `debugLogger` writes nothing unless a
debug log session is active.

* fix(scheduled-tasks): close four more precondition gaps from review

Read the verdict off the final non-empty line, not from anywhere in the text.
The prompt asks the model to *end* its reply with the verdict, so
`DECISION: YES\n\nBut I could not verify it` is not a decision — scanning the
whole reply took the conclusion off the wrong line and released the fire.

Mark a cut-short tool loop at its choke point. `loopDetected` was flagged, but
its sibling `repeatedDuplicateProviderToolCall` takes the quiet exit: it makes
`#buildNextMessageAfterToolRun` return null, ending the turn with no error and
no abort. A model that streamed `DECISION: YES` in that same round then
released the fire on an investigation it never finished. Both cases (and any
future one) are now marked where the follow-up message comes back null, rather
than by enumerating flags — enumerating them is how the sibling was missed.

Fail closed in the two consumers that cannot evaluate a precondition. The
headless and TUI `onFire` callbacks read only `prompt`/`cronExpr`/`missed`, so
a guarded task fired there with its guard ignored — the exact outcome the
precondition exists to prevent. Both now skip such a fire; only the ACP/daemon
session, which owns the sub-session dispatch the verdict gates, runs it.

Distinguish a withheld fire in the run history. The scheduler books the run the
moment it fires, before any verdict exists, so a task that deliberately did
nothing reported "ran at 02:00". `CronTaskRun.withheld` is stamped afterwards
by the evaluating session, addressed by the fire's own minute (the scheduler
writes `runs[].at` from the very `lastFiredAt` it hands to `onFire`), and the
Web Shell tags the entry. Best-effort and never awaited: losing a cosmetic
marker must not affect a fire that has already been decided.

* feat(scheduled-tasks): make the precondition readable, and translate it

The bound session of a guarded task is the feature's decision log, but it read
like a debug dump: every fire echoed the whole instruction wrapper the model
receives — five paragraphs of "end your reply with a final line that is exactly
one of…" — and nothing in it was translatable.

Echo a compact label instead. `CronQueueItem` gains an optional `echoText`: the
text the client shows when the text sent to the model is not fit to read. A
precondition turn now shows " Precondition check" and the user's own condition,
whitespace-collapsed and capped at 280 characters (surrogate-safe). The model
still receives the full wrapper.

Say what the check decided. The model's answer explains its reasoning but cannot
state the consequence, so the scheduler adds one line: the run was skipped
(precondition not met, or the check was cancelled / interrupted / failed), or it
is running — with a `qwen-session://` link to the sub-session that is doing the
work. Without that link the bound session of an isolated task shows nothing at
all for a fire that DID run: the work happens in a sibling the user cannot
reach from here.

The status line opens with a blank line. It is an `agent_message_chunk`, which
the client appends to the assistant message already on screen, and that message
ends on the verdict with no trailing newline — without the break the transcript
renders `DECISION: NO Precondition not met…`. A screenshot caught that; the
assertions did not, so there is now a test for it.

All seven strings go through `t()` and are translated in en/zh/zh-TW (the three
locales `check-i18n` holds to strict key parity). Session.ts had no i18n import
before this; `t()` is initialized on the ACP path by `gemini.tsx`.

Not addressed: the ACP cron path persists no user record at all, so the echo and
the status lines are live-only and a reload shows the model's answers with no
question above them. That is pre-existing — `client.ts` records a cron prompt via
`recordCronPrompt(message, displayText)` only on the core send path, which the
ACP session does not use.

* fix(web-shell): make qwen-session:// links actually clickable

`MarkdownLink` has an interception branch for `qwen-session://<id>` that renders
a button and dispatches `qwen:open-session` so the app shell can navigate. It
has never run.

react-markdown sanitizes every href through `defaultUrlTransform`, which allows
only `http(s)`, `irc(s)`, `mailto` and `xmpp` and rewrites everything else to
`''`. So the scheme was stripped before `components.a` was called: the branch
saw an empty href, fell through, and rendered a plain anchor with no href.
Clicking it did nothing.

Add a `urlTransform` that passes `qwen-session://` through and defers every
other url to the default sanitizer. Letting the scheme through is safe — the
interception branch never puts it in the DOM, it renders `href="#"` and
dispatches the id as an event — and the per-component `isSafeHref` /
`isSafeImageSrc` guards are unchanged.

Dead since #6535 (ac2f371c4) introduced it: the `create_sub_session` tool's link
works only because `ToolGroup` parses `[text](qwen-session://id)` out of plain
tool output with its own regex, never touching react-markdown. A precondition's
"running this task in a new session" line is the first such link to reach an
assistant message, which is how this surfaced.

* fix(scheduled-tasks): stop pretending a manual run evaluates the precondition

"Run now" wrapped the task's condition into the relayed prompt and let the model
decide whether to dispatch. That cannot be made correct from the client, and it
produced three defects:

  - `onRunPrompt` resolves at ADMISSION, so `runScheduledTask` appended an
    ordinary `manual` run record even when the model went on to decide the
    condition was false. The history reported a successful run for work that was
    never done, and — unlike a scheduled fire — nothing could stamp `withheld`.

  - The one-shot branch consumes the task (`/run` deletes it) BEFORE the prompt
    is even enqueued. A false precondition therefore destroyed the task without
    ever running it.

  - The manual wrapper offered only "holds" / "does not hold". The scheduled path
    treats an inability to determine the condition as NO and machine-parses a
    final verdict; here the model owned the dispatch decision, so a tool failure
    or an ambiguous answer had no branch at all and could still reach
    `create_sub_session`.

The verdict is only observable inside the session that computes it. So a manual
run no longer evaluates the guard: "Run now" means run now. The `manual` run
record and the one-shot consumption are truthful again, and there is no second
decision protocol to drift from the scheduler's.

The button says so — a guarded task's tooltip reads "Run now (runs immediately,
ignoring the precondition)", translated in en/zh.

A guarded manual run that reproduces a scheduled fire (verdict, `withheld`
stamping, one-shot consumed only on YES) would have to be dispatched daemon-side,
where the outcome exists. That is a separate change.
2026-07-10 05:56:45 +00:00
..
ApprovalModeDialog.module.css feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ApprovalModeDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ApprovalModeDialog.tsx Add harness infrastructure for web-shell package (#6517) 2026-07-09 08:11:58 +00:00
DaemonStatusDialog.module.css feat(web-shell): show Settings and Daemon Status as an in-place panel (#6341) 2026-07-06 05:06:46 +00:00
DaemonStatusDialog.test.tsx feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00
DaemonStatusDialog.tsx fix(web-shell): i18n for ~43 hardcoded English strings across 15 files (#6516) 2026-07-08 08:46:37 +00:00
DeleteSessionDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
DeleteSessionDialog.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
DialogPrimitives.module.css feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
DialogShell.module.css fix(web-shell): make dialog backdrop z-index configurable (#6572) 2026-07-09 06:05:30 +00:00
DialogShell.test.tsx feat(web-shell): time-series metrics charts on Daemon Status (#6307) 2026-07-05 03:06:09 +00:00
DialogShell.tsx Add harness infrastructure for web-shell package (#6517) 2026-07-09 08:11:58 +00:00
dialogStyles.ts feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
ExtensionsDialog.test.tsx Fix workspace skills for disabled extensions and ACP preheat (#6534) 2026-07-09 09:11:17 +00:00
ExtensionsDialog.tsx Fix workspace skills for disabled extensions and ACP preheat (#6534) 2026-07-09 09:11:17 +00:00
HelpDialog.module.css feat(web-shell): polish chat UI (#5893) 2026-06-26 15:38:23 +00:00
HelpDialog.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
McpDialog.module.css feat(web-shell): browse MCP server resources in the /mcp dialog (#5879) 2026-06-26 17:12:01 +00:00
McpDialog.tsx feat(web-shell): browse MCP server resources in the /mcp dialog (#5879) 2026-06-26 17:12:01 +00:00
ModelDialog.module.css feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ModelDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ModelDialog.tsx Add harness infrastructure for web-shell package (#6517) 2026-07-09 08:11:58 +00:00
ReleaseSessionDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ReleaseSessionDialog.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ResumeDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ResumeDialog.tsx Add harness infrastructure for web-shell package (#6517) 2026-07-09 08:11:58 +00:00
RewindDialog.module.css feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
RewindDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
RewindDialog.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ScheduledTasksDialog.module.css feat(scheduled-tasks): gate an isolated run behind a precondition (#6619) 2026-07-10 05:56:45 +00:00
ScheduledTasksDialog.test.tsx feat(scheduled-tasks): gate an isolated run behind a precondition (#6619) 2026-07-10 05:56:45 +00:00
ScheduledTasksDialog.tsx feat(scheduled-tasks): gate an isolated run behind a precondition (#6619) 2026-07-10 05:56:45 +00:00
scheduledTasksSchedule.test.ts feat(scheduled-tasks): run each task in its own dedicated, named session (#6389) 2026-07-07 06:22:36 +00:00
scheduledTasksSchedule.ts feat(scheduled-tasks): run each task in its own dedicated, named session (#6389) 2026-07-07 06:22:36 +00:00
SessionRow.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
SessionRow.tsx Add harness infrastructure for web-shell package (#6517) 2026-07-09 08:11:58 +00:00
SvgLineChart.module.css feat(web-shell): show Settings and Daemon Status as an in-place panel (#6341) 2026-07-06 05:06:46 +00:00
SvgLineChart.test.tsx feat(tui): Ctrl+O frozen transcript view and unified tool output rendering (#5666) 2026-07-09 23:40:29 +00:00
SvgLineChart.tsx feat(web-shell): time-series metrics charts on Daemon Status (#6307) 2026-07-05 03:06:09 +00:00
ThemeDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ThemeDialog.tsx Add harness infrastructure for web-shell package (#6517) 2026-07-09 08:11:58 +00:00
TokenHeatmap.module.css feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00
TokenHeatmap.test.tsx feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00
TokenHeatmap.tsx feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00
ToolsDialog.test.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
ToolsDialog.tsx feat(web-shell): overhaul list-dialog interaction, keyboard nav & a11y (#6128) 2026-07-02 12:19:17 +00:00
UsageDashboardTab.module.css feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00
UsageDashboardTab.test.tsx feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00
UsageDashboardTab.tsx feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388) 2026-07-06 13:43:41 +00:00