Commit graph

146 commits

Author SHA1 Message Date
dreamWB
c412d62981
feat(web-shell): add bottom status items (#6613) 2026-07-09 14:58:39 +00:00
Shaojin Wen
ac2f371c44
feat(scheduled-tasks): add isolated run mode via create_sub_session tool (#6535)
* feat(scheduled-tasks): add isolated run mode via create_sub_session tool

Introduce a new `create_sub_session` tool (daemon-only) that spawns a
fresh top-level sub-session with its own clean context and transcript.
Wire it into the cron scheduler as an `isolated` run mode so each
scheduled fire dispatches its prompt into a fresh sub-session instead
of accumulating in one shared transcript.

- Add `create_sub_session` tool with `first-turn` and `sent` completion modes
- Add `SubSessionLauncher` in cli/serve with concurrency cap, timeout, and truncation
- Extend ACP bridge `extMethod` dispatch for child→daemon sub-session requests
- Add `runMode` field (`shared`|`isolated`) to DurableCronTask, CronJob, and API types
- Add run-mode radio picker to ScheduledTasksDialog UI
- Fix AuthMessage hardcoded placeholder to use i18n key

* fix(scheduled-tasks): dispatch isolated fires daemon-side, not via the model

An `isolated` fire was relayed through the model: the fired prompt was
wrapped with an instruction to call `create_sub_session`. That tool's
default permission is `'ask'`, so under `ApprovalMode.DEFAULT` an
unattended fire reached `client.requestPermission`, found no SSE
subscriber, and was cancelled by the daemon's 5-minute permission
timeout. The task never ran, and the cancel was booked as a successful
run — the headline use case of a scheduled task was broken.

Route isolated fires straight to the daemon instead: the cron `onFire`
handler in `Session` calls the sub-session spawner directly, with no
model relay and no tool-permission gate. The prompt was already approved
when the task was created; laundering it back through the model only
re-opened that gate. `create_sub_session` keeps `'ask'` for
model-initiated calls, and the attended "Run now" button keeps its relay
(a user is present to answer the prompt).

Also fix orphan-session cleanup in the launcher. `closeSession` was
guarded only by `.catch()`, which covers an async rejection but not a
synchronous throw; because the call sits inside the launcher's own
`catch (err)` block, a sync throw escaped and replaced the real launch
error. Guard both shapes.

Tests:
- Cover isolated routing: dispatch, in-session fallback with no spawner,
  missed one-shot, dispatch failure (dropped, never run inline), and
  shared mode.
- Cover the orphan close, including a `closeSession` that throws.
- Replace the sent-mode concurrency test, which only asserted the slot
  was eventually released (moving the release to the drain's *start*
  kept it green) with one that asserts the slot is HELD while the drain
  runs, plus one that asserts it is released at `turn_complete`.

* fix(scheduled-tasks): honor the caller's AbortSignal and harden the spawn boundary

Four findings from review, all in the model-initiated `create_sub_session`
path (the scheduled `isolated` dispatch reaches none of them).

`execute()` took no parameters, so it silently dropped the parent turn's
`AbortSignal`. `Session.ts` awaits `invocation.execute(signal)` without
racing the abort itself, so cancelling a turn with a `first-turn`
sub-session in flight pinned the caller's tool loop until the daemon's
5-minute ceiling. Accept the signal and return as soon as it fires.

The sub-session is deliberately NOT cancelled and deliberately KEEPS its
concurrency slot: `sendPrompt` has no abort seam, so the sub-session runs
on. Releasing its slot on cancel — as the review suggested — would let the
caller over-admit against sub-sessions that are still consuming a bridge
session and model quota.

`handleCreateSubSession` trusted the child-supplied `callerSessionId`
verbatim, and that id keys the launcher's per-caller concurrency bucket: a
fabricated id starts a fresh bucket at zero (cap evasion) and a victim's id
burns their slots (DoS). Validate it with the connection's existing
`ownsSession` seam.

Every daemon session wires a spawner, sub-sessions included, and each gets
its own cap-sized bucket — so one prompt could fan out 5ⁿ sub-sessions until
`maxSessions` ran dry. Gate nesting at one level: the launcher remembers the
sessions it spawned and refuses to spawn from them. With `callerSessionId`
now authenticated, the gate cannot be sidestepped.

Cap the prompt at 100,000 chars (matching the scheduled-task REST route) and
the display name at 200, both at the bridge trust boundary and, for the
prompt, in the tool's own validation so the model gets an actionable error.

Not changed: `create_sub_session` stays in `PermissionManager.CORE_TOOLS`.
Membership there SUBJECTS a tool to the `coreTools` allowlist; it does not
exempt it. Removing it — as the review suggested — is what would let the
tool bypass a user's allowlist, the way `agent` and `send_message` do today.

* fix(core): do not spawn a sub-session for an already-cancelled turn

`raceCancellation(spawner({…}), signal)` evaluated the spawner as an
argument, so the spawn started before the abort was ever checked. A turn
cancelled before `execute()` ran still created a sub-session on the daemon
— and it kept a concurrency slot — while the tool reported itself
cancelled.

Take a thunk instead, so the pre-abort check happens before any daemon work
is started. Track whether the spawn actually began, and say so: "cancelled
before it started, no sub-session was created" is a different fact from
"a sub-session may already have been created and is not cancelled".

Regression test asserts the spawner is never called for a pre-aborted
signal; it fails against the eager-argument form.

* fix(serve): require callerSessionId and stop misreporting an early stream close

Two findings from review.

`awaitFirstTurn`'s `'incomplete'` stopReason was unreachable. The cleanup
`finally` calls `ac.abort()` unconditionally to tear the subscription down,
so by the time the stopReason ternary read `ac.signal.aborted` it was always
true. An event stream that closed before the turn finished (bridge teardown,
WS drop) was reported as a 5-minute wall-clock `'timeout'` — indistinguishable
from a real one. Track the timer firing in its own flag.

`callerSessionId` was validated only when present. Omitting it handed the
launcher `undefined`, which minted an `anon:<uuid>` bucket — a fresh
concurrency bucket per call, so no cap — and skipped the depth-1 nesting gate
(`info.callerSessionId !== undefined && …`). Authenticating the id closed
forgery but not omission. It is now required at the bridge boundary, and
required in `CreateSubSessionInfo`, so the launcher's anonymous fallback and
the gate's presence check are both gone. Every real caller has a session id —
the tool only ever runs inside a session's turn.

* fix(serve): surface dropped fires and drain timeouts; bound sub-sessions per workspace

Three findings from review.

A dropped `isolated` scheduled fire left no trace. `debugLogger.warn` writes
nothing unless a debug log session is active, and the scheduler persists the
fire as a run before dispatch — so a nightly task could fail forever while its
history claimed it ran. It now also writes to stderr, which the daemon forwards
from the child.

A sent-mode drain that hit its 30-minute ceiling was equally silent: the catch
saw `drainAc.signal.aborted` and skipped logging, the `finally` freed the
concurrency slot, and the sub-session — which the abort does not cancel — kept
burning a bridge session and model quota. The timer now records its own firing
(the controller cannot: `finally` aborts it on every exit path) and the timeout
is written to stderr. The drain ceiling is injectable for tests, mirroring
`firstTurnTimeoutMs`.

The per-caller concurrency cap trusts `callerSessionId`, and the bridge can only
authenticate that id as "a session on this channel". Every session of a
workspace shares one child process, so nothing at the transport can prove which
of them issued the call — and a per-session secret would be readable by the
whole process anyway. Rather than pretend otherwise, add a workspace-wide
ceiling on concurrent sub-sessions that holds no matter which bucket a launch is
charged to.
2026-07-09 12:02:39 +00:00
callmeYe
0907edb909
Fix long session timeline scrolling (#6526)
* fix(web-shell): hide long session timeline scrollbar

* fix(web-shell): lift timeline tooltip above popovers

* fix(web-shell): refine timeline tooltip behavior

* fix(web-shell): keep timeline tooltip anchored

* fix(web-shell): keep timeline tooltip below modals

* fix(web-shell): harden timeline tooltip recentering

* fix(web-shell): drop unused timeline tooltip var

* fix(web-shell): keep timeline programmatic scroll guard through frame

* fix(web-shell): preserve timeline tooltip on focus scroll

* ci(web-shell): add smoke test script
2026-07-09 11:43:21 +00:00
ytahdn
e64010c116
Fix workspace skills for disabled extensions and ACP preheat (#6534)
* fix(cli): keep workspace skills in sync with extensions

* fix(cli): address workspace skills review feedback

* test(cli): cover synthesized inactive extension skills

* fix(cli): address workspace skills review issues

* fix(cli): address workspace skills review followups

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-09 09:11:17 +00:00
ermin.zem
5c82857fea
Add harness infrastructure for web-shell package (#6517)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
* test(web-shell): add browser and lint harness

* test(web-shell): harden browser smoke harness

* fix(web-shell): guard mock daemon model state

* test(web-shell): remove unused scenario harness

* fix(web-shell): remove stale lint disables

* test(web-shell): make matchMedia stub writable

* fix(web-shell): exclude tests from package typecheck

* test(web-shell): tighten mock daemon route contract

* Update packages/web-shell/client/e2e/utils/mockDaemon.ts

Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>

* test(web-shell): clear stale SSE connections

* ci(web-shell): gate smoke on full CI profile

---------

Co-authored-by: ermin.zem <ermin.zem@alibaba-inc.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-09 08:11:58 +00:00
ytahdn
48e5d5d0d7
feat(web-shell): polish stats table layout and todo panel UI (#6559)
* feat(web-shell): polish stats table layout and todo panel UI

- Use CSS grid for model usage table with fixed column widths
- Add loading spinner for in_progress todo items
- Add strikethrough for completed todo items
- Introduce nested variant for PivotRow to show thoughts as output sub-item
- Clarify i18n labels: stats.prompt -> Input Tokens, stats.output -> Output Tokens

* fix(web-shell): address review suggestions for stats table and todo panel

- Add prefers-reduced-motion media query for todo spinner (accessibility)
- Right-align numeric columns in model usage table for magnitude comparison
- Consolidate duplicate i18n keys (stats.prompt/output → stats.inputTokens/outputTokens)

* fix(web-shell): address stats review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-09 06:06:51 +00:00
Changxiao Ruan
8c896f6b09
fix(web-shell): make dialog backdrop z-index configurable (#6572) 2026-07-09 06:05:30 +00:00
callmeYe
25423b1526
fix(cli): align memory dialog with managed memory (#6434)
* fix(cli): align memory dialog with managed memory

* test(cli): stabilize memory dialog path rendering

* fix(cli): make memory target switch exhaustive

* fix(cli): tighten memory dialog target handling

* fix(cli): handle headless managed memory dialog

* test(cli): cover desktop managed memory dialog branches

* fix(cli): open memory folders asynchronously

* test(cli): assert managed memory folder setup

* fix(cli): simplify memory folder opener

* fix(cli): clarify memory folder opener behavior
2026-07-09 01:04:56 +00:00
qwen-code-ci-bot
b330ec884f
chore(release): v0.19.8 (#6549)
* chore(release): v0.19.8

* docs(changelog): sync for v0.19.8

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-08 15:51:03 +00:00
Shaojin Wen
b2bee7040e
fix(web-shell): stabilize slash command i18n in split-view panes (#6546)
Split-view panes showed English descriptions for slash commands while
the main view showed Chinese. Two root causes:

1. ChatPane never merged getLocalCommands(t) into the command list,
   so ~33 built-in commands (help, model, clear, etc.) lacked i18n
   descriptions.

2. localizeBuiltinDescriptions required source === 'builtin-command',
   but the daemon omits _meta.source in some SSE event paths
   (available_commands_update), causing built-in commands to skip
   translation unpredictably across sessions.

3. Skill localization depended on connection.skills, which can be
   empty when SSE events deliver commands without availableSkills.

Fix: make the entire localization pipeline name-based and
session-independent — merge local commands, relax the source guard
to also translate when source is missing, and use skillDescriptionKey
directly instead of connection.skills for skill tagging.

Also adds missing autofix skill translation (EN + ZH).
2026-07-08 14:57:12 +00:00
ytahdn
b2b02d27ff
feat(web-shell): expose external split controls (#6523)
* feat(web-shell): expose external split controls

* fix(web-shell): tighten split controlled behavior

* fix(web-shell): address split control review

* fix(web-shell): sync controlled split exit

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-08 12:02:10 +00:00
Shaojin Wen
8296ce9e54
fix(web-shell): i18n for ~43 hardcoded English strings across 15 files (#6516)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(web-shell): i18n for ~43 hardcoded English strings across 15 files

Multi-round audit replaced hardcoded UI strings with t() calls and
added en/zh-CN i18n keys for:

- Session timeline labels, aria-labels, and kind labels (MessageList)
- SubAgent tab labels, tools count, pending/running status
- Auth protocol option labels, placeholders, API key masking
- Voice dictation UI states (VoiceButton)
- Copy tooltip, Runtime badge, N/A, Server/Agent fallbacks
- Mermaid code block labels, image alt text, model switch summary

* fix(web-shell): i18n for missed models placeholder in AuthMessage

* fix(web-shell): invalidate timeline cache on locale switch

Store the t function reference in sessionTimelineCache alongside the
message signature. When the locale changes, t gets a new reference,
triggering cache invalidation and re-generating entries with the new
language labels.
2026-07-08 08:46:37 +00:00
jifeng
5b2d1369b5
fix(web-shell): refine markdown table interactions (#6500)
* fix(web-shell): refine markdown table interactions

* fix(web-shell): preserve active column when closing filters
2026-07-08 08:37:56 +00:00
callmeYe
727c2d580c
fix(web-shell): prevent sidebar footer overflow (#6522) 2026-07-08 08:28:12 +00:00
Shaojin Wen
d8dc8043d6
feat(web-shell): restore the full composer in split-view panes (#6510)
* feat(web-shell): restore the full composer in split-view panes

The in-window split view rendered each pane through a deliberately minimal composer: typing "/" opened nothing, and the approval-mode, model, and voice controls were all hidden. Wire each pane's composer to its own session so all four work per pane.

The slash menu lists the pane session's own daemon commands and is submitted through that session, where the daemon executes it server-side — so e.g. /clear clears that pane's session, not the outer chat. The approval-mode and model pickers drive the pane session's own setApprovalMode / setModel actions, and the SDK reflects the change back on the connection. The shared model-visibility filter moves to utils/composerModels so the main chat and split panes hide the same models.

* feat(web-shell): auto-approve pending tool call when a pane switches to yolo

Address review feedback on #6510: switching a split pane to yolo (or auto-edit for an edit tool) now auto-approves a tool call already awaiting approval in that pane — mirroring App's handleSetMode so the shortcut behaves the same as in the single-session chat. The pending approval is read through a ref so the async setApprovalMode resolution targets the approval current at that time, not a stale one.

Also cover the two untested branches of handleSelectMode: the isDaemonApprovalMode guard (invalid mode is rejected without hitting the daemon, reported via onError) and the setApprovalMode async-rejection path.
2026-07-08 07:01:36 +00:00
Shaojin Wen
045bbee6ce
fix(web-shell): hide sidebar settings text when width is insufficient (#6494)
Prevent the 'Settings' label from wrapping to a new line when the
sidebar is narrow. Instead, the text is clipped via overflow:hidden
and only the gear icon remains visible.
2026-07-08 02:41:41 +00:00
qwen-code-ci-bot
86ae16a6d6
chore(release): v0.19.7 (#6484)
* chore(release): v0.19.7

* docs(changelog): sync for v0.19.7

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-07 17:25:48 +00:00
Shaojin Wen
65c82bed66
feat(web-shell): unify scheduled task sessions — bind chat-created tasks + clock icon (#6453)
* fix(web-shell): rename scheduled tasks "查看历史" to "查看对话"

* feat(serve): bind cron_create durable tasks to dedicated sessions via keepalive

The cron_create tool (core layer) writes durable tasks to disk without a
sessionId because it has no access to the session bridge. The keepalive
loop runs in the daemon process where the bridge IS available, so it
retroactively binds unbound tasks to dedicated sessions — the same flow
POST /scheduled-tasks uses for UI-created tasks. Each unbound task gets:
spawnOrAttach(sessionScope:'thread'), named  prompt, sessionId written
back to disk. This makes chat-created tasks show "查看对话" with a clock
icon in the session list, matching the UI's "新建定时任务".

* feat(serve): watch tasks file for immediate binding of new cron_create tasks

The keepalive interval is 2-5 minutes, so a chat-created task could wait
that long before being bound to a dedicated session — showing no "查看对话"
link until the next tick. Adding a file watcher (same directory-watch +
debounce pattern the scheduler uses) triggers an immediate tick when
cron_create writes to disk, so the task is bound within ~500ms.

* feat(serve): bind cron_create tasks to current session +  rename via keepalive

Switch from creating a separate dedicated session to binding the task to
the current chat session (so the first message is already in the
transcript). The keepalive then renames that session to  prompt — the
core layer can't rename sessions (no bridge access), but the daemon
process can. A Set tracks renamed sessions to avoid repeated
updateMetadata calls. Unbound tasks (legacy/CLI) still get new sessions
via the existing bind path.

* fix(core): keep createDurable() tasks unbound by default

Reverts the auto-binding of durable tasks to the current session in
createDurable(). Binding to a specific session means only that session
can fire the task (#shouldFireDurable), but non-daemon paths (TUI, ACP,
headless) have no keepalive to rehydrate the session after exit — making
tool-created durable tasks go dormant.

The daemon keepalive (bindAndNameSessions) already handles binding
unbound tasks to dedicated sessions with  naming, so daemon-mode
tasks get the same UX without the regression.

* fix(serve): roll back orphan sessions in keepalive binding + add tests

When bindAndNameSessions spawns a dedicated session for an unbound task
but the subsequent updateCronTasks write fails (or the task was deleted
between read and write), the spawned session was left behind with no
owning task — the next tick would see the task still unbound (or spawn
more orphans). Add rollback: closeSession + removeSession on failure,
matching the POST /scheduled-tasks rollback pattern.

Also add positive test coverage for the new binding paths:
- unbound task → spawn + name + write sessionId to disk
- bound task without  prefix → named exactly once (renamed Set dedup)
- task vanishes before write → spawned session is rolled back

* fix(serve): add timeout to spawnOrAttach in keepalive binding + test hardening

BZ-D: spawnOrAttach in bindAndNameSessions had no timeout boundary — a
hung spawn would keep running=true and stall all subsequent ticks,
stopping heartbeats/revives for every scheduled-task session. Wrap with
withTimeout (configurable via spawnTimeoutMs, default 30s) and attach a
background handler to clean up late-resolved orphans.

Also generalized withTimeout error messages to include the operation
name, and made spawn timeout configurable for tests.

Test improvements (GPT-5 review suggestions):
- Assert spawnOrAttach payload (workspaceCwd + sessionScope: thread)
- Verify SessionService.removeSession called during rollback
- Regression test: createDurable stays unbound after enableDurable
- Hung-spawn test: tick completes despite non-abortable spawn hang

* fix(serve): keepalive hardening + i18n sync (review suggestions)

- i18n: sync English 'View history' → 'View conversation' to match
  Chinese '查看对话'
- Prune renamed Set alongside reviveState when tasks are removed
- fs.watch: clarify null filename handling for Linux (treat as match)
- updateCronTasks: skip .map() when task not found (no-op optimization)
- Add tests: disabled unbound exclusion, naming failure resilience
2026-07-07 16:29:10 +00:00
jifeng
971d4ba27e
feat(web-shell): add column reorder, resize, and freeze controls to markdown table (#6444)
* feat(web-shell): add markdown table column controls

Support resizing, reordering, and freezing table columns while preserving visible-order copy behavior and selection stability.

* fix(web-shell): address markdown table review feedback

* fix(web-shell): refine markdown table column reordering

* fix(web-shell): address markdown table review feedback

---------

Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
2026-07-07 14:18:07 +00:00
Shaojin Wen
55b2886909
fix(web-shell): split-view pane fixes (remove "current" badge, clear composer on send) (#6454)
* fix(web-shell): remove meaningless "current" badge from split-view panes

In the split view every pane is an equal, independently interactive session (its own DaemonSessionProvider, SSE, transcript, and approvals), so tagging one pane as the workspace's "current" session carried no operational meaning. It only leaked a single-view concept into a peer-of-equals layout and reliably prompted "what is this?" questions from users. Panes are already identified by their titles.

Drop the isCurrent badge and its border highlight from ChatPane, and stop passing isCurrent from SplitView. The sidebar and session overview keep their "current" indicators, which are legitimate "you are here" navigation. currentSessionId is retained only to seed the initial pane.

* fix(web-shell): clear the split-view composer on send, not at turn end

A split-view pane kept the just-sent text sitting in its composer until the whole turn finished. handleSubmit committed the draft on the sendPrompt promise resolving, but that promise resolves via waitForAcceptedPromptCompletion (turn end), not at admission. Switch to the onAdmitted hook so the composer clears the moment the daemon accepts the prompt, matching the main view. A prompt rejected before admission still preserves the draft and surfaces the error.

* fix(web-shell): pass commitAccepted directly as onAdmitted; cover admit-then-fail

Review follow-up:
- commitAccepted is already `() => void`, so pass it directly as the onAdmitted option instead of wrapping it in a redundant `() => commitAccepted?.()` closure.
- Add a test for the turn failing after admission: the draft stays cleared (no second commit) and the error is still surfaced to onError.
2026-07-07 14:08:59 +00:00
ytahdn
1d19fe7172
fix(web-shell): refine tool call summaries (#6450)
* fix(web-shell): refine tool call summaries

* fix(web-shell): address tool summary review feedback

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-07 13:53:17 +00:00
ytahdn
40340ef505
fix(serve): classify interrupted model stream errors (#6422)
* fix(serve): classify interrupted model streams

* fix(serve): address interrupted stream review

* test(webui): cover legacy terminated turn error fallback

* fix(web-shell): preserve error message data shape

* test(daemon): cover turn error fallback boundaries

* fix(web-shell): preserve classified error data

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-07 13:38:56 +00:00
Shaojin Wen
17138b525f
fix(web-shell): hide rotating loading phrase in split-view pane status (#6447)
Split-view panes now render a compact streaming status: the spinner, elapsed time, token count, and cancel hint stay, but the rotating "witty" loading phrase is suppressed. StreamingStatus gains a showPhrase prop (default true, so the main chat is unchanged); when false it also skips the phrase-rotation timer so each pane avoids a needless interval.
2026-07-07 11:32:59 +00:00
ytahdn
bd6816b7ac
fix(web-shell): keep errored turns expanded (#6424)
* fix(web-shell): keep errored turns expanded

* test(web-shell): cover errored turns with final answer

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-07 09:00:04 +00:00
ytahdn
ce2fee926f
fix(web-shell): clear stale floating todos (#6425)
* fix(web-shell): clear stale floating todos

* test(web-shell): cover floating todo reset

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-07 08:59:53 +00:00
yuanyuanAli
c7d22dc1d4
fix(web-shell): improve user tags and mobile menu layout (#6441) 2026-07-07 08:44:25 +00:00
Shaojin Wen
f7296d0333
feat(web-shell): add Qwen logo beside the sidebar new-chat button (#6437)
Place the Qwen brand mark to the left of the sidebar's New chat button.
The artwork is the same SVG used for the browser-tab favicon (and the
QwenLM GitHub avatar), inlined rather than hot-linked because the Web
Shell CSP is `img-src 'self' data: blob:`, which blocks remote images.
When the sidebar is collapsed there is no room beside the compact
button, so the mark is hidden and only the New chat button remains.
2026-07-07 08:36:48 +00:00
Shaojin Wen
55652d4912
fix(web-shell): keep split-view session list fresh and preserve panes across view switches (#6418)
* fix(web-shell): keep split-view session list fresh and preserve panes across view switches

The in-window split view's "add pane" picker read a stale session snapshot —
`useSessions` only fetches on mount — so sessions created after entering the
split never appeared. And switching away from the split and back cleared the
panes, because the live pane set lived in local state that died on unmount while
the seed it re-mounted from was never updated (and the no-arg "Open Split View"
button reset it to empty).

- Reload the picker list when it opens and when the parent's session-list reload
  token changes, so it never offers a removed session or misses a new one.
- Mirror the live pane set up to the app via onPanesChange so it survives
  SplitView unmounting; restore it (instead of reseeding empty) when the split is
  reopened without an explicit selection.

* test(web-shell): cover split-view refresh/restore per review; coalesce token reloads

Addresses review feedback on #6418:
- SplitView: skip a token-driven reload while one is already in flight, so a
  burst of session-list changes (bulk create/delete) doesn't fire a redundant
  concurrent round-trip per bump (matches the sidebar's poll guard).
- SplitView test: the freshness test now proves the picker re-renders with the
  refreshed list — a session appearing only after reload shows up — not just
  that reload() was called.
- App test: cover the openSplitView preserve/restore path end-to-end — a reported
  pane set survives leaving the split and is restored on reopen.

* fix(web-shell): reload split picker on every token bump (drop in-flight guard)

The in-flight guard added in the previous commit could drop a session-list
reload token that arrives while a reload is still running: the effect has
already run for that token value, and clearing the in-flight flag in `finally`
doesn't re-run it, so the picker could stay stale after burst create/delete/
rename activity — and the split has no polling fallback to recover.

Reload on every distinct token bump instead. `useDaemonResource` serializes
responses via its sequence counter (last write wins), so overlapping reloads are
correct, and the token is bumped only on discrete session-change events — an
occasional redundant fetch is far cheaper than a lost refresh.

* test(web-shell): cover openSplitView explicit-selection branch (dedupe + cap)

Per review: the restore branch of openSplitView was covered but the
explicit-selection branch (dedupe + MAX_SPLIT_PANES cap, replacing any prior
set) was only exercised, not asserted. Add a `?split=` URL test with duplicate
and over-cap ids that asserts the split seeds exactly the deduped, capped
selection.
2026-07-07 08:25:51 +00:00
callmeYe
7e0e79b6bc
fix(daemon): preserve user message source metadata (#6385) 2026-07-07 07:08:28 +00:00
Shaojin Wen
001d20ff26
feat(scheduled-tasks): run each task in its own dedicated, named session (#6389)
* feat(scheduled-tasks): run each task in its own dedicated, named session

Scheduled tasks created through the Web Shell management page were never firing
in the daemon-only case: the durable-cron tick runs inside an active agent
session, and the Web Shell creates a session only lazily on the first prompt, so
a task created on the management page (with no chat open) had nothing ticking it.

This binds every management-page task to a dedicated session, minted at create
time and named " <task>". The task fires ONLY inside that session — its
transcript is the task's run history — instead of via the shared per-project
durable owner. A daemon-side keepalive heartbeats those sessions so the idle
reaper doesn't stop them, and a boot-time rehydration reloads them after a
restart. Archiving, deleting, or unarchiving the session disables, removes, or
re-enables the bound task (covered on both the REST and ACP surfaces).

Also adds task editing, a live next-run countdown, run history, a one-per-row
card layout, and a "run now" that executes in the task's bound session and
updates the last-run time. All resident-session management is opt-in and enabled
only by the real daemon (runQwenServe), so createServeApp embeds/tests are
unaffected.

* fix(scheduled-tasks): address code review on per-session task feature

Review fixes for #6389:

- Distinguish archive-disabled from user-disabled tasks: disableTasksForSessions
  now marks disabledByArchive; enableTasksForSessions only re-enables tasks
  carrying that flag, so a task the user deliberately disabled stays disabled
  across an archive/unarchive cycle. [Critical]
- Rehydrate task sessions concurrently with a per-session 30s timeout so one
  hung loadSession can't stall the boot sweep or leave healthy tasks dormant.
  [Critical]
- Await runScheduledTask + reload before executing the prompt in handleRunNow,
  so a record failure surfaces and the card's "last run" reflects the trigger.
  [Critical]
- Log keepalive/rehydrate read + heartbeat failures at debug instead of
  swallowing them silently, so a persistently-failing keepalive is diagnosable.
  [Critical]
- Add integration tests: deleteDaemonSessions -> removeTasksForSessions and
  unarchiveDaemonSessions -> enableTasksForSessions (guard the coupling). [Critical]
- DELETE route: single atomic updateCronTasks that captures the bound session
  and removes the task in one cycle, closing the read-then-remove TOCTOU.
- Stop the keepalive timer during shutdown (matters for embedders that don't
  process.exit) so it can't fire against a disposed bridge.
- Deduplicate DEFAULT_BUILDER: export it once from scheduledTasksSchedule and
  drop the dialog's copy so the create form and cron-reversal can't drift.
- Reject empty-string sessionId in isValidTask: a bound task with "" would
  silently run unbound under the scheduler's truthy guard.

* fix(scheduled-tasks): isolate task sessions, fix catch-up/jitter/revive

Second review round (#6389):

- Force `sessionScope: 'thread'` when minting a task's session. The daemon's
  default scope is 'single', which attaches to (reuses) the shared workspace
  session — so a second task, or a task alongside an open chat, would bind to
  the same session, rename it, land runs in the wrong transcript, and close it
  on delete. Thread scope guarantees each task an isolated session. [Critical]
- Re-seat a recurring task's schedule anchor to now when a PATCH changes its
  cron (or flips one-shot→recurring), not just on re-enable. A bound task's
  catch-up runs on every file-watch reload, so a bare cron edit to an
  expression with an already-past slot would fire immediately on save. [Critical]
- Revive a non-resident bound session from the keepalive when its heartbeat
  fails (reaper let it go while disabled/archived, now re-enabled). Covers the
  unarchive and PATCH false→true paths uniformly and retries each interval, so
  a re-enabled task actually resumes instead of showing a live countdown that
  never fires. Best-effort, timeout-bounded, non-blocking. [Critical]
- Report `nextRunAt` using the scheduler's jittered fire time
  (`nextDurableFireMs`) instead of the bare cron boundary, so the UI countdown
  lines up with the real fire (the tick offsets each fire by up to the jitter
  window) rather than expiring early and advancing prematurely.

All four are mutation-verified. The cross-daemon double-fire on bound tasks
(same session live in two schedulers) is a separate, architecturally-invasive
fix (claim-then-fire on the durable file) tracked as a follow-up.

* fix(scheduled-tasks): sync bound session name on task rename

Create names a task's session after the task (` <name>`), but a later PATCH
that renamed the task (or edited the prompt of an unnamed task) left the
session's display name stale. The PATCH route now re-applies
`updateSessionMetadata` with the task's effective label whenever that label
actually changes — a bare cron/enabled edit does not touch the session.
Best-effort: a metadata failure doesn't fail the committed schedule change.
Mutation-verified.

* fix(scheduled-tasks): mirror run sessionId on client type; clarify server wiring

Review follow-up (#6389, qqqys):

- [Medium] `DaemonScheduledTaskRun` now mirrors the daemon's `CronTaskRun`
  `sessionId?: string`, so run-attribution the wire already sends isn't silently
  dropped by the client type (not surfaced in the UI yet; passthrough cast means
  no mapping change needed).
- [Nit] Comment the `app.locals.stopScheduledTaskKeepalive` set site, noting it
  follows the same convention as `fsFactory`/`boundWorkspace`/`acpHandle` and is
  read by the run-qwen-serve shutdown path (kept the convention rather than
  diverge to a one-off return value / declaration merge).
- [Nit] Comment the outer `.catch(() => {})` on rehydrate as intentional
  defense-in-depth (the function already handles read + per-session failures).

* fix(scheduled-tasks): couple archive/enable + record manual run only on enqueue

Two [Critical] review items (#6389, gpt-5-codex):

- PATCH re-enable coupling: reject `enabled: true` on a task disabled BY
  archiving its session (`disabledByArchive`) with 409 `task_session_archived`.
  Re-enabling it here would show an enabled task with a countdown while its
  bound session stays archived and can never fire — the caller must unarchive
  the session (which clears the marker and reloads it). A user-disabled task
  (no marker) and non-enable edits are unaffected.

- Manual "run now" ordering: record the run only AFTER the prompt is enqueued,
  not before. `runTaskManually` now returns a promise that resolves on enqueue
  and rejects if the bound session can't be opened (archived/deleted), is
  superseded, or times out; the dialog awaits it before writing
  /scheduled-tasks/:id/run, so a failed session switch no longer leaves a
  phantom run in history. Runs are serialized (one pending at a time, button
  disabled) so two quick clicks can't drop a prompt on the single bound-run
  latch. Added coverage for failed session load and double-click; all new
  tests mutation-verified.

* fix(scheduled-tasks): close dormancy/orphan/overflow gaps from review

Five items from GPT-5 /review (#6389):

- [Critical] Bind tasks to sessions only when resident management is on:
  createServeApp now passes the bridge to the scheduled-task routes only when
  `manageScheduledTaskSessions` is set. Embedders that leave it off get UNBOUND
  tasks (shared-owner firing) instead of bound tasks nothing keeps resident or
  reloads (which would silently go dormant).
- [Critical] Keep the keepalive/revive loop running whenever task sessions are
  managed, not only when a reaper is active — archiving closes a task session,
  so a re-enabled one still needs reviving with the reaper disabled. Size the
  interval under the reaper window (≤ half of it) so a small idle timeout can't
  let a session be reaped before its first heartbeat.
- [Critical] Record a manual run only after the prompt is admitted: the bound
  run latch now resolves only if `sendPrompt` admitted the prompt and rejects on
  cancellation (e.g. onSubmitBefore) / failure, so a cancelled Run now no longer
  advances lastFiredAt or appends history.
- [Critical] Clamp the dialog's reload timer to the 32-bit setTimeout ceiling
  (~24.8 days) so a months-away schedule can't overflow and spin a reload loop.
- [Suggestion] Pre-check the task cap before spawning a session, so an over-cap
  create never mints an orphan task session it must roll back.

New tests (route unbound-when-no-bridge, cap-no-spawn, computeKeepaliveIntervalMs
bounds, far-future timer clamp) mutation-verified; full server suite green.

* fix(scheduled-tasks): guard catch-up double-fire, run-now hang, /run + cron edits

Four items from GPT-5 /review (#6389):

- [Critical] Bound-task catch-up could double-fire: detection ran on every
  file-watch reload and read the stale on-disk lastFiredAt, so a reload racing
  the async catch-up persist (a foreign write to the tasks file) re-detected and
  re-fired the same overdue slot. Track ids whose catch-up was DELIVERED but not
  yet persisted (`deliveredCatchUp`) and skip re-detecting them until the write
  lands; a merely-buffered-then-dropped catch-up isn't tracked, so it still
  re-detects from disk (recovery preserved).
- [Critical] "Run now" hung the full 30s switch timeout when the bound session
  was ALREADY the current, loaded one (no dep change → the consuming effect
  never re-ran). Fire the enqueue directly after loadSidebarSession resolves as
  well as from the effect; whoever runs first nulls the latch, so it runs once.
- [Critical] POST /run recorded a run with no enabled/disabledByArchive guard,
  unlike PATCH — a direct API caller could write a phantom "ran" record onto a
  paused/archived task. Return 409 task_disabled for a disabled task.
- [Suggestion] Anchor re-seat on cron edit compared the raw string, so a
  cosmetic change (`0 9 * * *` → `00 9 * * *`) dropped a pending catch-up.
  Compare the canonical (parsed) schedule instead.

(The setTimeout-overflow and keepalive-floor reports were already fixed in
2a12cba.) New tests for the first three + the cosmetic-cron case are
mutation-verified; full core scheduler + route suites green.

* fix(scheduled-tasks): block disabled-task run in UI; record manual run at admission

Two [Critical] review follow-ups (#6389):

- A disabled task could still EXECUTE from the Web Shell: the Run button was
  only gated on `runningTaskId`, so clicking it enqueued the prompt and the
  server's `/run` `task_disabled` guard merely refused the later history write —
  a real, unrecorded run. Gate `handleRunNow` and disable the button on
  `!task.enabled` too, so a disabled task's prompt is never enqueued.
- Manual run recorded only after the whole turn: the bound-run latch resolved
  via sendPrompt, which completes through waitForAcceptedPromptCompletion, so a
  long/permission-blocked run or a closed tab could execute without ever being
  recorded. Add an `onAdmitted` callback to sendPrompt (fired when the daemon
  accepts the prompt, before the turn) and resolve the manual-run latch at
  admission instead — cancellation before admission still rejects.

New dialog test (disabled task → no enqueue) mutation-verified; webui/web-shell
typecheck + existing session-action tests green.

* fix(scheduled-tasks): guard tick double-fire, cap rehydration, harden lifecycle writes

Review follow-ups (#6389):

- Extend the fire-persist re-detection guard to ON-TIME tick fires, not just
  catch-ups (renamed deliveredCatchUp → firePersistPending): a bound task fired
  by the tick advances lastFiredAt asynchronously, so a reload racing that write
  (bound detection runs every reload) could re-detect the slot and double-fire.
  The tick persist now adds its ids to the guard and clears them when the write
  lands, symmetric to the catch-up persist.
- Bound boot-rehydration concurrency (batches of 4): each loadSession forks a
  child, so loading up to 50 at once spiked the host and risked spawn failures
  that strand tasks. The keepalive revive path was already sequential.
- Archive disable failure is now logged (was fully swallowed) so a broken
  archive→pause coupling — where the keepalive would revive the just-archived
  session — is diagnosable.
- Unarchive re-enable failure is surfaced in the result `errors` and logged, and
  enableTasksForSessions also runs for already-active sessions — so a task left
  stranded ({enabled:false, disabledByArchive:true}) by a prior failed enable is
  recoverable by re-unarchiving, instead of being permanently stuck.
- Create rollback now removes the persisted session (close + removeSession), so
  the loser of a concurrent create at the cap boundary (passes the pre-check,
  loses the authoritative write) doesn't leave an orphan named session.

New tests (tick-fire guard, bounded rehydration, already-active recovery)
mutation-verified; full core scheduler + serve suites green.

* fix(scheduled-tasks): one-shot run/edit correctness; tick persist non-regression

Review follow-ups (#6389, ci-bot):

- [Critical] Manual /run on a ONE-SHOT task now removes it from the store. Its
  slot is still in the future, so stamping lastFiredAt=now didn't stop the
  scheduler firing it again at its original time — a double run. A one-shot's
  manual run IS its single fire, so the task is spent.
- [Critical] PATCH recurring:false now re-seats the one-shot's createdAt anchor.
  The old (long-past) anchor made the scheduler read it as a MISSED one-shot and
  fire + permanently delete it. Re-seating createdAt points its next fire at the
  upcoming occurrence. Also covers a cron edit on an existing one-shot.
- [Suggestion] The tick persist no longer regresses lastFiredAt: it skips the
  write when the on-disk stamp is already >= the tick slot (a concurrent manual
  /run or catch-up may have stamped newer), mirroring the catch-up persist guard.
- [Suggestion] Added the missing create-rollback test: a post-spawn commit
  failure closes AND removes the minted session (no orphan).

New tests (one-shot run removal, recurring→one-shot re-seat, rollback teardown)
mutation-verified; core scheduler + route suites green.

* fix(scheduled-tasks): ref-count fire guard, real rehydration cap, authoritative run check

Four [Critical] review follow-ups (#6389):

- Ref-count firePersistPending (was a boolean Set): the same task can have two
  lastFiredAt persists in flight (fired again before the first write landed);
  clearing on the first settle dropped the guard while the second was still
  pending, re-opening the double-fire window. The count holds it until the last
  persist settles.
- Rehydration concurrency is now enforced on the REAL loads: loadSession isn't
  abortable, so a timed-out load kept forking in the background while the next
  batch started. A bounded worker pool holds each slot until the underlying load
  actually settles, so in-flight child spawns never exceed the cap.
- Unarchive recovery reports failures for the full resume set: it enables both
  unarchived AND already-active sessions but only logged/returned errors for
  unarchived, so a failed already-active recovery surfaced errors:[] and left a
  task stranded. Deduped one list used for the call, log, and errors.
- Manual "run now" re-checks server-authoritative state before enqueuing: the
  dialog snapshot can be stale (another tab/API disabled/deleted the task), so
  it would execute the prompt and only the /run record would 409. It now
  refreshes, bails if gone/disabled, and enqueues the FRESH prompt/session.

New tests (ref-count, slot-held-past-timeout, stale-disabled re-check)
mutation-verified; core scheduler + serve + dialog suites green.

* fix(scheduled-tasks): catch-up non-regression, disabled-edit re-seat, run/timer/keepalive hardening

Review follow-ups (#6389):

- [Critical] Catch-up persist no longer regresses lastFiredAt: use `>=` like the
  tick persist, so a newer stamp (a cross-process manual /run) landing while the
  catch-up write is in flight isn't overwritten back to the older minute.
- [Critical] The PATCH anchor re-seat now runs for schedule edits even while the
  task is disabled — editing a disabled one-shot's cron then re-enabling it (two
  separate requests) no longer leaves a stale anchor that fires + deletes it.
- [High] Manual "run now" of a bound ONE-SHOT consumes it server-side (/run,
  which deletes) BEFORE enqueuing, so a record failure leaves a recoverable
  "recorded but never ran" instead of a silent double execution at its slot.
- [Medium] The dialog reload timer backs off a stuck past-due nextRunAt (fast
  reloads to catch a just-fired advance, then a slow lane) instead of spinning a
  1 Hz GET loop.
- [Medium] The manual-run latch bounds the admission phase with a timeout, so a
  send that wedges before admission degrades to a visible "run failed" instead
  of freezing the run controls.
- [Suggestion] Keepalive: an in-flight guard skips a tick while the previous
  pass runs (no duplicate concurrent loadSession spawns), and per-session
  exponential backoff stops retrying a permanently-gone session every interval.

New tests mutation-verified. Two deeper items (a task session winning the
durable lock and firing unbound tasks; tearing down a consumed one-shot's
session) are left open as tracked follow-ups — both need new daemon↔child
infrastructure.

* fix(scheduled-tasks): one-shot anchor on unarchive, memoize next-fire, sanitize + log

Review follow-ups (#6389):

- [Critical] enableTasksForSessions now re-seats a ONE-SHOT's createdAt anchor
  (not just recurring's lastFiredAt) on unarchive — otherwise unarchiving a task
  that was converted to recurring:false while disabled fires it as a missed
  one-shot and permanently deletes it.
- [Critical] Log the DELETE-path removeTasksForSessions failure (was fully
  swallowed) like the archive/unarchive paths — the session is already gone, so
  a silent write failure leaves the still-enabled bound task a permanent ghost.
- [Medium] Memoize nextDurableFireMs (deterministic per id/cron/recurring/anchor)
  — a sparse cron costs hundreds of ms per scan and the route recomputed it per
  task on every request, stalling the event loop for 50 yearly tasks.
- [Nit] The consumed one-shot /run response now nulls nextRunAt (it was
  advertising a future fire on an entity the next GET omits).
- [Suggestion] scheduledTaskSessionName strips terminal control sequences (the
  bridge title guard rejects them → silently drops the rename) and truncates on
  a code-point boundary (no lone surrogate broadcast as U+FFFD).
- [Critical/doc] Document that firePersistPending is instance-scoped — the
  narrow cross-instance restart window is an accepted edge.
- Added the missing test for editing an enabled one-shot's cron.

New tests mutation-adjacent; suites green. Two deeper items (session deleted
outside the daemon orphaning a bound task; surfacing bound tasks in cron_list)
are left open as tracked follow-ups.

* fix(scheduled-tasks): re-seat one-shot anchor on re-enable; guard duplicate revive

Two [Critical] review follow-ups (#6389):

- Re-enabling a one-shot now re-seats its createdAt anchor (added justReEnabled
  to the one-shot branch). A one-shot disabled past its slot then re-enabled was
  otherwise read as a missed one-shot on the next reload — fired immediately and
  permanently deleted. Updated the prior "leaves anchor untouched" test to the
  safe behavior (fires at next occurrence).
- Keepalive revive no longer spawns a duplicate child: loadSession isn't
  abortable, so a timed-out revive keeps running; a later tick (past its backoff)
  would start a SECOND load for the same session. An in-flight `reviving` set
  (cleared on the load's TRUE settlement, not the timeout) blocks that — without
  holding the sequential tick, so other sessions' heartbeats aren't delayed.
  Added a configurable reviveTimeoutMs for the test.

Both mutation-verified. (The one-shot /run session teardown raised again is the
same item as the open deferral — a synchronous close there would break the run,
which executes after /run; it's tracked for the keepalive orphan-sweep.)

* fix(scheduled-tasks): strip bidi override/isolate chars from session name

The bridge's title guard (hasControlCharacter) only rejects C0/DEL, so
Unicode bidi override/embedding/isolate controls (U+202A–202E, U+2066–2069)
slip past it and can visually reorder a scheduled-task session name in the
session list — a Trojan-Source-style attack (CVE-2021-42574). Strip them
alongside the existing terminal-control-sequence pass, matching core's
stripDisplayControlChars canonical set.

Adds a test built from code points so the test file itself carries no
reordering controls.

* fix(scheduled-tasks): close review findings — rehydrate deadlock, manual-run recording, shared helpers, tests

Addresses the review findings on the per-task-session work:

- keepalive rehydrate no longer awaits a non-abortable loadSession after its
  timeout. A genuinely hung load would pin its worker and, with enough hangs,
  wedge the whole boot sweep (Promise.all never settles) so later task sessions
  never rehydrated. The worker now records the timeout as failed and pulls the
  next queued session; the background load is left to settle. Rewrote the test
  that pinned the old "hold the slot" behavior into a no-wedge regression guard.
- web-shell manual run drops its pre-admission timeout. sendPrompt isn't
  abortable, so rejecting on the timer while the send was still in flight let a
  LATE admission execute an UNRECORDED run the user could retry into a
  duplicate. The run is now tied to admission (accepted prompts are always
  recorded); the "session never becomes active" phase stays bounded by the
  switch timeout in runTaskManually.
- extract collectBoundSessionIds() shared by the heartbeat + rehydrate passes
  (was duplicated) and isBoundTask() in the lifecycle module (was the lone
  `sessionId !== undefined` check vs. the strict one used everywhere else).
- spell the nextDurableFireMs cache-key separator as `\x00` rather than a
  literal NUL byte, so cronScheduler.ts no longer reads as binary to ripgrep.
- add App.test coverage for the manual-run orchestration (admission-resolve,
  cancel/error reject, immediate fire, supersede, switch timeout) and a
  keepalive test that a disabled task gets no heartbeat and no revive.

* fix(web-shell): "create via chat" opens a fresh session in scheduled tasks

The scheduled-tasks "Create via chat" button switched to the chat view but
stayed on the CURRENT session, piling the task-creation conversation onto
whatever the user was already doing. It now starts a new session first
(createNewSession) and jumps to it before priming the composer, so task
creation gets its own chat. Covered by a new App.test case asserting
clearSession() is called.

* fix(scheduled-tasks): address follow-up review findings

- keepalive rehydrate: guard the onError callback with try/catch. If it threw
  (e.g. stderr EPIPE during log rotation) the rejection escaped loadOne, failed
  its worker, and short-circuited Promise.all — stranding every other queued
  session.
- cronScheduler catch-up: use the strict `typeof sessionId === 'string' &&
  length > 0` bound-check instead of `!== undefined`, matching every other
  "is bound?" site.
- server rehydration: log the outer defense-in-depth catch instead of swallowing
  it, so an unexpected throw isn't a silent "tasks never fire".
- session-name sanitizer: also strip the standalone Bidi_Control marks U+061C /
  U+200E / U+200F, not just the override/isolate ranges.
- scheduled-tasks dialog: when a consumed one-shot then fails to deliver, show a
  specific "deleted but never ran — recreate it" error instead of the generic
  "run failed" that hid the deletion. Kept the deliberate consume-first ordering.

* fix(web-shell): don't prime the composer when "create via chat" can't start a new session

onCreateViaChat's deferred composer-priming ran unconditionally: if
createNewSession() failed, the task-starter text was dropped into the CURRENT
session (only onSessionIdChange was gated on success). Gate all post-create
side effects on `created`, matching handleMissingSessionNewSession. Adds an
App.test failure-path case (new session fails → composer not primed).

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-07 06:22:36 +00:00
callmeYe
686326863a
fix(web-shell): polish scheduled task timeline UI (#6386)
* feat(web-shell): mark scheduled task turns in timeline

* fix(web-shell): confine locate flash to message content

* fix(web-shell): flash parallel agent locate target

* fix(web-shell): keep scheduled marker source optional

* fix(web-shell): omit default scheduled timeline flag

* fix(web-shell): repair scheduled timeline UI conflict

* fix(web-shell): remove stale shell output prop

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-07 04:04:28 +00:00
Shaojin Wen
f41e95ac18
feat(web-shell): add Session Overview panel and in-window split view (#6400)
* feat(web-shell): add Session Overview panel and in-window split view

Add a large-screen "Session Overview" mission-control panel and an
in-window split view so users can monitor and drive multiple daemon
sessions at once.

- SessionOverviewPanel: ranked live cards (needs-approval -> running ->
  idle) merging the workspace session list with the detail=full status
  report. Multi-select opens the selected sessions as a split view in
  the current tab ("Open in split") or in a new browser tab ("Open in
  new tab", via a ?split=a,b URL).
- SplitView + ChatPane: one DaemonWorkspaceProvider hosting N
  DaemonSessionProvider panes, each a self-contained interactive chat
  (transcript, composer, streaming, tool/ask approvals). Browser focus
  scopes the keyboard per pane, so panes never contend over approvals.
- Sidebar entry points gated to large screens; the split view's Back
  returns to the Session Overview.

* refactor(web-shell): address review feedback on the session overview / split view

- SessionOverviewPanel: prune the selection Set when a session leaves the list
  (so a reappearing session isn't silently reselected) and make select-all use
  the intersection rather than prev.size.
- Extract isAskUserPermission into a shared util so App.tsx and ChatPane.tsx no
  longer keep verbatim copies that can drift.
- SplitView: dismiss the "add session" picker on Escape or a click outside it.
- Tests: MAX_PANES cap, popup-blocked path, checkbox-selects-without-navigating,
  stale-selection pruning, and a direct test for the extracted util.

* fix(web-shell): address /review findings on the split view

- ToolApproval: add a `keyboardActive` prop; split panes pass false so global
  Enter/Escape/digit shortcuts can't confirm the wrong session's approval, and
  the outer session's approval overlay is no longer rendered behind the split
  (where it would keep its global shortcuts while hidden).
- ChatPane: defer the composer commit until sendPrompt resolves, so a rejected
  prompt (transcript loading / disconnected / turn active) preserves the draft
  instead of silently dropping it.
- SplitView: include a per-mount nonce in each pane's clientId so two tabs
  opening the same split don't share a client id — which suppressOwnUserEcho
  would treat as a self-echo and drop from the transcript.
- SessionOverviewPanel: cap the split selection to MAX_SPLIT_PANES before
  building the ?split= URL or opening the in-window split, with a hint when more
  are selected; also dismiss the split picker on Escape / click-outside.
- Tests covering each.

* fix(web-shell): address second /review round on the split view

- SplitView: wrap each pane in its own ErrorBoundary, so a render crash in one
  pane (malformed block, unexpected tool shape) shows an inline fallback with a
  close action instead of white-screening the whole split.
- splitUrl / overview: carry the daemon token into the new-tab split URL's
  fragment. The current tab has already stripped the token from its URL, so a
  token-auth (`serve --open`) deployment would otherwise open the split tab
  unauthenticated. The token rides the hash (never sent to the server / logs).
- Tests: per-pane error isolation, token-in-fragment (and none without a token),
  and the overview polling effects (interval fires, document.hidden skips, and
  the in-flight guard prevents overlapping polls).

* fix(web-shell): hide the outer chat under the split and share app-level contexts

- App: hide (display:none) + aria-hide the outer chat subtree whenever
  mainView !== 'chat', not only when a panel is open. Previously the outer
  chat/composer/toolbar stayed reachable by keyboard/AT behind the full-page
  split (it was only covered visually). State is preserved (node stays mounted).
- App: wrap SplitView in the app-level WebShellCustomizationProvider and
  CompactModeContext so split panes render markdown / tool-headers / thinking
  the same way the single-session chat does. Todo contexts stay chat-only —
  they belong to the outer session, not the panes.

* refactor(web-shell): address review suggestions — coverage, dedup, split UX

- ToolApproval: add a dedicated test on the real component that the global
  keyboard shortcut is armed by default and NOT armed when keyboardActive=false
  (the cross-pane approval safety mechanism).
- SplitView: auto-exit to the Session Overview when the last pane is closed
  (guarded so an initial empty seed doesn't bounce straight back out).
- ChatPane: add tests for the cancel action, the empty/whitespace submit guard,
  and error routing to the onError prop.
- Extract the shared session-list page size + organization feature flag into
  constants/sessions.ts, used by the overview, split view, and sidebar, so the
  values can't drift between the three.

* fix(web-shell): surface outer approval + failed refresh in overview/split

- Split view: when the outer (main) session is waiting on an approval
  that's hidden behind the split, show a non-blocking notice banner with
  a "Go to it" button that returns to the chat where the approval lives.
- Auto-close the split (like the overview panel) when the viewport shrinks
  below the large-screen breakpoint, so users aren't stranded.
- Session Overview: surface a failed refresh inline (keeping the last-good
  cards) instead of silently swallowing it once cards are on screen.
- Tests: status-report poll cadence, picker dismiss (Escape / outside /
  inside click), inline refresh-failure banner.

* fix(web-shell): sever window.opener on split tab; tighten hidden-chat test

- openSelectedInNewTab now clears win.opener (the split tab carries a
  daemon token in its URL fragment) to prevent reverse tabnabbing, matching
  the existing bug-report window.open path.
- Strengthen the split-view App test so a missing outer-chat subtree fails
  instead of passing vacuously through an optional chain.

* fix(web-shell): split-view focus/stability/robustness follow-ups

- Refocus the composer after a shrink-driven split close so keyboard users
  aren't dropped onto <body> (skips when an approval or panel takes over).
- Stabilize SplitView onExit via useCallback so its last-pane-close effect
  doesn't re-fire on every App re-render.
- ChatPane: surface a per-pane connection-loss banner instead of silently
  showing stale messages when a pane's daemon connection drops.
- ChatPane: anchor the streaming timer to the active turn's start (last user
  message timestamp) so a pane opened mid-turn shows real elapsed time.
- Tests: split auto-close on shrink, outer-approval split notice + return-to-
  chat, connection banner, and streaming-timer anchoring.
2026-07-07 02:44:18 +00:00
ytahdn
be7e874fd1
Handle missing web-shell sessions without redirecting (#6357)
* fix(web-shell): handle missing session routes

* chore(web-shell): clarify missing session route handling

* fix(web-shell): address missing session review follow-up

* fix(web-shell): address missing session review issues

* test(web-shell): cover missing session status handling

* fix(webui): handle heartbeat terminal states

* fix(web-shell): preserve missing session state

* fix(webui): harden missing session diagnostics

* fix(web-shell): stabilize missing session recovery

* fix(webui): preserve missing session heartbeat state

* fix(webui): stabilize missing session recovery

* fix(webui): cover missing session review gaps

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-06 21:59:35 +00:00
ytahdn
0f98842ff2
fix(web-shell): refine tool detail cards (#6399)
Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-06 14:58:22 +00:00
Shaojin Wen
350191e101
feat(web-shell): add token-usage analytics dashboard to Daemon Status (#6388)
* feat(web-shell): add token-usage analytics dashboard to Daemon Status

Add a "统计 / Usage" tab to the Daemon Status page: a Today/7D/30D period toggle over the selected range's token totals and input/output/cache-read breakdown, a 12-month token heatmap (per-day tokens + cache-read tooltip, localized month labels), per-model token share, skill-call counts, and daily token/session charts.

Backend: a new read-only GET /usage/dashboard daemon API backed by a core usage-dashboard service that aggregates the durable local usage history (cross-project ~/.qwen), reusing loadUsageHistory + aggregateUsage. Skill counts are threaded through the shared usage pipeline. No new instrumentation — every metric is read from data qwen-code already persists.

* fix(web-shell): address usage-dashboard review feedback

- cap `aggregateUsage` topSkills at 25 like topTools, so the aggregate and dashboard payload stay bounded
- fix a DST drift in the heatmap grid: advance the day/month cursor by calendar day (setDate) instead of a fixed `i * MS_PER_DAY` offset
- cache the loaded history once (range-independent) so toggling Today/7D/30D re-aggregates from a single disk read; split a pure `buildUsageDashboard(records, opts)` out of `loadUsageDashboard`
- drop the unused per-day streak computation and the dead `daemon.usage.streak` i18n key
- add debug logging to the dashboard builder and a direct `aggregateUsage`-skills unit test

* fix(usage-dashboard): make the dashboard load read-only + fix cache coalescing

- Make the daemon dashboard side-effect free: `loadUsageHistory` gains a `persistRebuild` option, and the route passes `persistRebuild: false`, so serving a GET never writes to `~/.qwen`. The transcript-rebuild fallback previously persisted rebuilt records (including an in-progress session), violating the read-only contract.
- Fix cache coalescing on the slow path: a pending history load is now reused regardless of age (the TTL starts at settlement), so a request arriving after the TTL while the load is still pending no longer kicks off a second full load.
- Tests: read-only rebuild writes nothing, `metricsToUsageRecord` copies `SessionMetrics.skills`, and a pending load is shared past the TTL.
2026-07-06 13:43:41 +00:00
ytahdn
b726b7cdaa
fix(web-shell): constrain virtual scroll rows (#6362)
* fix(web-shell): constrain virtual scroll rows

* test(web-shell): cover virtual message rows

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-06 07:41:59 +00:00
Shaojin Wen
170ce7917d
feat(web-shell): show Settings and Daemon Status as an in-place panel (#6341)
* feat(web-shell): show Settings and Daemon Status as an in-place panel

The Settings and Daemon Status buttons opened centered modal overlays that
dimmed the whole app. Render them as a full-height panel that replaces the
chat surface instead — a Back button or Escape returns to the chat — with the
content left-aligned and filling the chat pane width rather than centered in a
narrow column. On the Daemon Status page, give the time-series charts a taller
plot and the overview cards a wider track so a wide window is actually used;
the tab grouping (overview / metrics / diagnostics) is kept.

* fix(web-shell): preserve composer draft and refine panel focus/escape

Address review feedback on the in-place Settings / Daemon Status panel:

- Keep the chat view (message list + composer) mounted and just hidden while a
  panel is shown, so typing a prompt, opening Settings/Status, then going Back
  no longer discards the unsent draft and attachments (the composer subtree was
  being unmounted and remounted empty).
- Focus the Back button when a panel opens and restore focus to the composer
  when it closes, replacing the focus management DialogShell used to provide.
- Reload workspace settings after the fast-model command resolves so the still-
  mounted Settings panel doesn't keep showing the previous value.
- Don't close the panel when Escape is handled inside the sidebar (its search
  input clears on Escape without stopping the event).
- Guard the overview card grid with min(100%, 340px) so a 340px track can't
  overflow a narrow panel.

* fix(web-shell): reset panel scroll when switching Settings/Status

Add key={activePanel} on the panel body so switching directly between Settings
and Daemon Status (activePanel goes 'settings' -> 'status' without passing
through null) remounts the scroll container and opens at the top instead of
inheriting the previous panel's scrollTop.

* fix(web-shell): restore new-chat vertical centering

The chatViewWrap added to keep the composer mounted while a panel is shown was
filling the pane, which cancelled the empty (new-chat) state's vertical
centering (welcome header + composer stuck to the top instead of centered).
Shrink the wrapper to its content in the empty state so
`.appChatEmpty .chatPane { justify-content: center }` centers it again, matching
how `.content` behaves there.

* fix(web-shell): restore session-org test destructuring dropped in merge

My earlier merge of origin/main 3-way-combined WebShellSidebar.test.tsx
incorrectly, dropping the `{ }` destructuring from the 7 session-organization
tests. renderSidebar returns `{ container, rerender }` (not the element), so
`const container = renderSidebar(...)` made `container.querySelector` throw.
Restore the file to origin/main so the tests pass again.

* fix(web-shell): surface pending approvals over Settings/Status panel

The in-place Settings/Daemon Status panel hides the chat footer with
display:none, and the ToolApproval / AskUserQuestion overlays live in
that footer. A gated tool call that arrived while a panel was open
rendered the approval into a hidden container, so the turn hung with no
visible prompt (reported [Critical]).

Close the panel when an actionable approval is pending so it surfaces.
Only actionable approvals count (pendingToolApproval / pendingAskUserApproval
already gate on canActOnPendingApproval), so a non-owner in a shared
session isn't yanked out of Settings by someone else's prompt. Leave
focus for the overlay rather than the composer on this path: ToolApproval
uses a window-level key handler that ignores editable targets, so
focusing the composer would swallow its shortcuts.

Also refocus the Back button on panel->panel switches (not just on open),
so focus no longer relies on the Back button being the same DOM node
across the keyed panel body, and expose SvgLineChart's plot height as
--chart-height (fallback 140px) so a constrained caller can shrink it.

Adds App-level tests: the panel auto-closes on a pending tool approval,
and stays open when the only block is a resolved (non-actionable) one.

* test(web-shell): cover AskUserQuestion auto-close; robust panel selector

Follow-up to the approval auto-close fix, addressing review suggestions:

- Add data-testid="inline-panel" to the panel <section> and query by it in
  App.test instead of querySelector('section'), which would false-positive if
  any future component renders a <section>.
- Add a companion test for the pendingAskUserApproval branch of the auto-close
  effect. The ask-user block carries toolCall.input.questions so
  isAskUserPermission() classifies it as an AskUserQuestion; mutation-checked
  (restricting the effect to pendingToolApproval fails only this test).
- Document why handleFastModelSelect's post-sendPrompt reload is not racy:
  sendPrompt awaits waitForAcceptedPromptCompletion, so the /model --fast turn
  has already applied when reloadWorkspaceSettings() runs. Also note why the
  command path needs the explicit reload that the setWorkspaceSetting pickers
  (vision/voice) get for free via the settingsVersion signal.

* fix(web-shell): close inline panel when resuming a session

The /resume <id> command and the ResumeDialog onSelect both call
loadSession() without closePanel(), unlike createNewSession and
loadSidebarSession. Loading a session means the user wants that chat, so
leaving a Settings/Daemon Status panel open would hide it. Add closePanel()
to both paths for consistency (a no-op when no panel is open).

Currently reachable only in theory — the composer that submits /resume is
display:none while a panel is shown — but the guard keeps the invariant if
a non-composer entry point (sidebar/shortcut) is ever added.

Adds a mutation-checked test (removing closePanel from the /resume handler
fails it) and gives the ChatEditor test mock a focus() method, since the
panel-close focus effect now runs editorRef.focus() on this no-approval path.

* fix(web-shell): keep composer dormant while an approval overlay is up

Follow-up to the approval auto-close fix. When an approval arrives while a
Settings/Status panel is open, the auto-close clears activePanel, so
interactionBlocked flips false and useComposerCore's dialogOpen effect
refocuses the still-mounted composer. ToolApproval ignores approval
shortcuts from editable targets, so the now-visible approval stops
responding to Enter/Escape/number keys until focus moves away.

Key the ChatEditor dialogOpen prop off the pending approval too, so the
composer stays blurred while an approval owns the keyboard. Consolidate the
"an approval overlay is active" condition — previously duplicated in the
auto-close effect and the panel focus guard — into a single
approvalOverlayActive value so the three consumers can't drift.

Uses the actionable-approval condition (pendingToolApproval /
pendingAskUserApproval) rather than raw pendingApproval, matching the
auto-close gating and avoiding a blurred composer when a non-actionable
approval renders no overlay.

Adds a mutation-checked test asserting dialogOpen is true while an approval
is pending with no panel open.

* test(web-shell): cover the Daemon Status panel branch

A review note observed that all five panel tests open via /settings, so the
activePanel === 'status' branch (DaemonStatusDialog) had no coverage — a
regression in the 'status' literal would go undetected. Add a test that
opens Daemon Status through the sidebar and asserts the panel opens and
auto-closes on a pending approval, exercising that branch and confirming the
auto-close is panel-type-agnostic.

Gives the WebShellSidebar test mock an onOpenDaemonStatus button, since
there is no slash command to open the status panel. Mutation-checked:
neutering setActivePanel('status') fails only this test.

* fix(web-shell): dismiss stacked dialogs on approval, focus overlay, a11y

Addresses a review round on the inline Settings/Status panel.

[Critical] When an approval arrives while a DialogShell sub-dialog (model
picker / approval-mode picker) is open over the panel, the auto-close
removed the panel but left the sub-dialog backdrop covering the footer
approval overlay, so the turn hung. Worse, the approval-mode picker stayed
usable — selecting "yolo" auto-approved (handleSetMode) a tool call the
user never saw. Dismiss the panel AND both sub-dialogs when an actionable
approval is pending.

[Critical] After the panel closes for an approval, focus fell to <body>
(the Back button unmounted; ToolApproval has no autofocus). Move focus onto
the ToolApproval overlay wrapper (tabindex=-1, so its window-listener
shortcuts keep working and Enter doesn't confirm early) once it is visible.
AskUserQuestion keeps managing its own focus.

Suggestions:
- Guard reloadWorkspaceSettings() rejection (was unhandled).
- aria-hidden the display:none chat view so AT can't wander into it.
- Close the panel before /model --fast so its response shows in the chat
  in context instead of piling up behind the hidden panel.
- Remove the dead .panelBodyInner wrapper (redundant width:100%).
- Rename data-mobile-drawer -> data-sidebar-shell (it wraps the desktop
  sidebar for all viewports) across App.tsx + standalone.css.

Tests (+5, mutation-checked): sub-dialog dismissal on approval, overlay
focus, Escape closes panel / sidebar-Escape does not, dialogOpen while a
panel replaces the chat. Adds an observable DialogShell test mock.

* fix(web-shell): restore composer focus after approval resolves; cleanups

[Critical] The panel focus effect consumed prevActivePanelRef to null on the
approval auto-close (correctly skipping editor focus then). When the approval
later resolved with no panel to return to, neither branch fired and focus was
left on <body> — the visible composer took no keyboard input. Track the
approvalOverlayActive transition too and restore composer focus on
approval-resolve-after-panel-close. (useComposerCore's dialogOpen effect also
covers this; the extra branch makes the panel effect self-contained.)

Suggestions:
- Log the reloadWorkspaceSettings() rejection instead of swallowing it.
- Remove the dead :global([data-dialog-fullscreen]) .grid rule (the
  allowFullscreen source was removed when Daemon Status became a panel).

Tests (+4, focus-restore mutation-checked): focus restored after an approval
resolves post-auto-close; Back-button closes the panel and restores focus;
fast-model pick closes the panel, sends /model --fast, and reloads settings;
chat view is aria-hidden while a panel is shown. Adds interactive
SettingsMessage / ModelDialog mocks and stable editor-focus / settings-reload
spies.

* fix(web-shell): make Settings/Status panel and Scheduled Tasks mutually exclusive

Opening Scheduled Tasks (mainView — a position:absolute z-index overlay) and
then Daemon Status (activePanel) rendered the Daemon Status panel *behind* the
Scheduled Tasks overlay, so the button looked like it did nothing. These are
mutually-exclusive full-pane views, so opening one must close the other.

Centralize into openPanel() / openScheduledTasks() helpers (last-opened wins)
and route all six open sites (the /settings and /schedule commands, the three
sidebar handlers, and the StatusBar) through them.

Mutation-checked tests: opening Daemon Status closes the Scheduled Tasks page
(the reported repro), and opening Scheduled Tasks closes an open panel.
2026-07-06 05:06:46 +00:00
Shaojin Wen
9a63c03224
feat(web-shell): add a Scheduled Tasks management page (#6348)
* feat(web-shell): add scheduled tasks management page

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.

* chore(web-shell): address review feedback on scheduled tasks

- 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.

* chore(web-shell): address second review round on scheduled tasks

- 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.

* chore(web-shell): address third review round on scheduled tasks

- 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.

* chore(web-shell): address fourth review round (minor suggestions)

- 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).

* chore(web-shell): address fifth review round on scheduled tasks

- 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.

* test(cli): cover legacy scheduled-task normalization on GET

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.

* fix(core): cap durable cron loads against a durable-only budget

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.
2026-07-06 03:47:17 +00:00
ytahdn
fa6e0f942c
fix(web-shell): suppress stale pending prompt refresh errors (#6352)
Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-06 03:02:49 +00:00
Shaojin Wen
edc0555ed1
feat(web-shell): named session groups and color tags in the sidebar (#6350)
* feat(web-shell): named session groups and color tags in the sidebar

Extend web-shell session organization with named groups (create / rename /
delete, assign a session to a group) alongside quick color tags, and surface
pin / archive state. The grouping data is plumbed end-to-end through the
daemon.

- core: session-organization-service carries group id / name / color and
  pin / archive metadata on organized-list entries
- sdk / acp-bridge: session-list entries gain groupId / groupName /
  groupColor / archivedAt; add SessionGroupColor and list-session-groups
  result types
- cli/serve: dispatch + session routes expose listing and assigning groups
- web-shell: sidebar group management UI (create / rename / delete groups,
  color picker, pin, archive) and reuse the shared "Group" label for the
  group action, dropping the redundant "Move to group" string

* fix(cli): exclude color-tagged sessions from the ungrouped filter

Color / named group / recent are mutually exclusive buckets in the web-shell
sidebar — a color-tagged session shows in its color section, not "recent".
But the organized session-list `group=ungrouped` filter only checked
`groupId == null`, so a color-tagged session with no named group leaked into
ungrouped results for REST/ACP consumers, disagreeing with the UI taxonomy.

Align the server filter: ungrouped means no named group and no color tag.
Adds an ACP session/list test asserting a color-tagged session is excluded
from group=ungrouped (fails on the old filter, passes on the new one).

* fix(web-shell): clear color tag when creating a group for a session

saveGroupEditor's create-with-target path assigned the new group but left any
existing color tag in place, unlike the sibling assignSessionGroup /
assignSessionColor paths that keep color and named group mutually exclusive.
Because color takes precedence in the sidebar's section bucketing, the session
stayed in its color section and the group assignment had no visible effect.

Send `color: null` alongside `groupId` on that path, and extend the
create-group dialog test to assert the assignment clears the color.

* fix(cli): exclude color-tagged sessions from the named-group filter

Follow-up to the ungrouped filter fix: the per-group filter (group=<id>) also
ignored color precedence. Core and the REST/ACP update paths can persist both
groupId and color, and the sidebar renders such a session in its color bucket,
so group=<id> API consumers saw a session the web-shell shows elsewhere.
Require `color == null` there too, matching the sidebar taxonomy
(color > group > recent).

Adds an ACP session/list test for a session with both groupId and color set.
2026-07-06 02:19:16 +00:00
ytahdn
a8a99f0ed6
fix(web-shell): finalize deferred gated submissions (#6342)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* fix(web-shell): finalize deferred gated submissions

* test(web-shell): fix sidebar render result usage

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-05 14:21:27 +00:00
Edenman
802c382ce1
feat(web-shell): support icon chips for mention tags (#6337)
Co-authored-by: 克竟 <dingbingzhi.dbz@alibaba-inc.com>
2026-07-05 13:25:32 +00:00
ytahdn
7605c8bd15
feat(web-shell): add onSessionChange and onSubmitBefore callbacks (#6333)
* feat(web-shell): add onSessionChange and onSubmitBefore callbacks

Add session-level event callbacks and a pre-submit interception hook
to WebShellProps, enabling external consumers to observe session
lifecycle events and gate prompt submissions.

New APIs:
- onSessionChange: fires on rename (SSE-driven), submit (direct and
  queued), and turn_complete (streamingState transition with error
  context including block ID).
- onSubmitBefore: async hook called before prompt submission; reject
  cancels the prompt with full retry-state rollback (lastSubmittedPrompt,
  lastSubmittedImages, retriedTurnErrorId, showRetryHint).

Sidebar integration:
- sessionListReloadToken triggers sidebar reload on session events
  with pollInFlightRef + document.hidden guards.
- Delayed 2s reload after submit to account for daemon registration lag.

Safety:
- isPreparingPrompt loading state during onSubmitBefore prevents
  duplicate submissions.
- streamingSessionIdRef prevents spurious turn_complete on session switch.
- All slash commands (including internal /language, /model) go through
  onSubmitBefore; queued prompts intentionally bypass it.

* fix(web-shell): add null initial value to delayedReloadTimerRef

React 19's useRef requires an explicit initial value argument.
Match the existing escapeTimerRef pattern: | null + null.

* fix(web-shell): move clearFollowup after onSubmitBefore gate and add tests

- Move clearFollowup() to after onSubmitBefore succeeds so that
  followup context is preserved when the before hook rejects
- Add null guard for clearTimeout on delayedReloadTimerRef
- Add 5 unit tests for sidebar sessionListReloadToken effect
  covering: token change, undefined, unchanged, document.hidden,
  and poll-in-flight gate conditions

Addresses PR #6333 review feedback.

* feat(web-shell): call onSubmitBefore for queued prompts

Previously enqueuePrompt bypassed onSubmitBefore entirely. Now the
before hook is also invoked for queued prompts — if it rejects, the
prompt is cancelled and not added to the queue. The composer still
clears synchronously (fire-and-forget) since the Composer's onSubmit
contract is synchronous (boolean | void).

Also updates the onSubmitBefore JSDoc to reflect this behavior.

Addresses PR #6333 review feedback on security gap.

* test(web-shell): cover session callback behavior

* fix(web-shell): preserve rejected queued prompts

* fix(web-shell): preserve rejected direct prompts

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-05 12:10:47 +00:00
jinye
fe816f625f
feat(cli): Surface daemon prompt queue status (#6325)
* feat(cli): surface daemon prompt queue status

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6325)

* codex: address PR review feedback (#6325)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6325)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-05 08:25:42 +00:00
jinye
7a528d078a
feat(daemon): Add session organization (#6305)
* feat(daemon): add session organization

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): address session organization review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(daemon): cover session organization review cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): Address session organization review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Harden session organization review edge cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Address session organization review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-05 07:52:56 +00:00
Shaojin Wen
adda526c3c
fix(web-shell): localize built-in command and skill descriptions in the slash menu (#6326)
The slash-command menu mixed languages in a zh-CN session: the local fallback
commands were translated, but daemon-advertised built-in commands (/bug,
/directory, /effort, …) and bundled/project skills (/dataviz, /bugfix, …) showed
the daemon's English descriptions.

The daemon fills descriptions from its own process language, which is independent
of the web-shell UI language, so the menu can only match the UI language by
re-localizing on the client.

- localizeBuiltinDescriptions() re-localizes built-in commands by name, guarded
  by source === 'builtin-command' so custom commands keep their own description.
- Skills are localized by name in the skill-tagging step (keyed off
  connection.skills), so it also works on the welcome screen before a session
  exists — skills only carry a reliable source once a session is created.
- Covers 20 daemon-only built-in commands and 27 skills (9 bundled + 18 project).
  Display-only: the model still receives the daemon's canonical English text.
  Unknown/user skills keep their authored descriptions.
2026-07-05 05:37:19 +00:00
Shaojin Wen
52a190b5c6
feat(web-shell): time-series metrics charts on Daemon Status (#6307)
* feat(web-shell): time-series metrics charts on Daemon Status

Add seven bottleneck-analysis line charts (concurrency, requests, API
latency, prompt latency, event-loop lag, memory, token burn) to the
Daemon Status dashboard, backed by a new server-side metrics ring.

The status endpoint is a point-in-time snapshot, so line charts need a
time series. A bounded ring buffer in the daemon (daemon-metrics-ring.ts)
seals one bucket every 5s (~15min retained) from three seams:
- HTTP request rate/latency via the telemetry middleware
- prompt queue-wait/duration via the bridge telemetry hooks
- per-round token usage sniffed at the bridge session/update fan-in
  (new DaemonBridgeTelemetryMetrics.tokenUsage hook)
plus memory / active sessions+prompts / a window-scoped event-loop lag
p99 read as gauges at seal time.

The series rides the existing GET /daemon/status contract
(runtime.metrics.series), threaded through the SDK types (JSON passthrough)
to a dependency-free inline-SVG chart component in web-shell -- no charting
library added to the CSP-strict serve --web bundle.

Tests: metrics-ring math, token-usage sniffing on the real sessionUpdate
path, and SVG chart rendering. Verified end-to-end against a live daemon
(GLM-5.2): requests/latency/memory/event-loop, real token burn and prompt
duration, with the concurrency gauge tracking active prompts.

* feat(web-shell): tabs, chart tooltips, and fullscreen for Daemon Status

Split the now chart-heavy Daemon Status dashboard into Overview / Metrics /
Diagnostics tabs (status badge, refresh, and issues stay global) so
monitoring, configuration, and troubleshooting each get their own space
instead of one long 70vh scroll.

Add an interactive hover cursor to the charts: a vertical time line, a dot on
each series, and a tooltip reading the bucket time plus every series' value at
that point -- previously only the latest value and peak were legible, from the
legend.

Add an opt-in fullscreen toggle to DialogShell (via allowFullscreen, wired for
Daemon Status) that expands the panel to near the full viewport; scrolling is
consolidated into the shell body so the content actually grows with it.

Tests: tab switching + diagnostics-behind-tab, SVG tooltip rendering, and the
DialogShell fullscreen toggle. Verified end-to-end against a live daemon
(GLM-5.2) with real request / token / prompt data.

* feat(web-shell): add CPU, LLM-latency, queue-depth, IPC & connection metrics

Extend the Daemon Status metrics ring with more bottleneck-analysis
dimensions, filling the two biggest gaps — resource cost had only memory
(no CPU), and latency had only client->daemon HTTP (not daemon->model):

- CPU %: process.cpuUsage() delta, core-normalized (memoryPressureMonitor
  formula, clamped 0-100), sampled alongside memory.
- LLM API latency p50/p95: the token frame's _meta.durationMs (the
  daemon->model round-trip), separating 'model is slow' from 'we are slow'.
- Prompt queue depth: a new bridge.pendingPromptTotal aggregate, folded into
  the concurrency chart beside active tasks.
- IPC pipe throughput: daemon<->ACP-child stdio bytes (already measured; now
  windowed via metricsRing.recordPipe).
- Connection counts (SSE/WS/ACP) and rate-limit rejections, read lazily in the
  sampler from the ACP handle registry and the rate limiter.

The tokenUsage telemetry hook is widened to carry durationMs. Verified
end-to-end against a live daemon (GLM-5.2): LLM p95 28.6s vs HTTP p95 324ms,
queue depth 1, IPC peak 0.3MB, SSE gauge 1 on a live stream.

* feat(web-shell): add ACP child process CPU/memory (self-reported over ACP)

The daemon's own CPU/memory only tell half the story — the real LLM/tool work
runs in the spawned 'qwen --acp' child, which is where the resource cost lives.
Surface it: the child self-reports its rss + cpuPercent to the daemon over a new
read-only ACP extMethod (qwen/status/workspace/resource); the bridge caches the
latest sample on the live channel, and the metrics sampler reads it
synchronously each tick (firing an async refresh for the next, off the hot path).

The child computes cpuPercent as a process.cpuUsage() delta between polls (no
dependency on MemoryPressureMonitor's tool-gated sampling), core-normalized and
clamped. Rendered as a second line on the CPU and Memory charts (daemon vs
child, side by side).

Verified end-to-end (GLM-5.2): child RSS ~300MB vs daemon RSS ~225MB, child CPU
tracking above the daemon's -- the child is the resource hog, now visible.

* test(web-shell): cover Metrics tab, chart rendering, and the recordRequest seam

Address review — the metrics dashboard's rendering and its HTTP data seam had
no tests:
- DaemonStatusDialog: switching to the Metrics tab renders the charts from the
  series (one SvgLineChart per card) and hides the Overview panel; an empty
  series shows the collecting-metrics placeholder.
- daemonTelemetryMiddleware: recordRequest fires once with (durationMs,
  statusCode) on a matched route (real status code; once across finish/close),
  is not called for unmatched routes, and is a silent no-op when omitted.

* fix(web-shell): enlarge Daemon Status charts in fullscreen

Fullscreen widened the panel but the charts stayed small — the grid just packed
in more 280px cards at a fixed 52px SVG height, so the extra viewport bought
more small charts, not bigger ones. Now the DialogShell body carries a
`data-dialog-fullscreen` marker; the chart grid switches to wider cards (min
480px → fewer columns) and the SVG grows to 120px, so fullscreen actually
enlarges the plots. Verified: 2 wide columns at 120px vs 3-4 columns at 52px.

* fix(web-shell): resolve chart colors in portal, guard child-resource polling

Address review (real-user + ci-bot):
- [Critical] Chart colors (--primary, --agent-blue-400) resolved to nothing in
  the DialogShell portal (createPortal to document.body escapes the app root that
  defines them), so ~half the chart lines rendered stroke:none. Add both vars to
  DialogShell's own theme scope. Verified: 25/25 path strokes colored (was 5 none).
- [Critical] refreshChildResource had no in-flight guard; requestWorkspaceStatus
  waits up to 10s (> the 5s cadence), so a degraded child accumulated concurrent
  polls. Add a single-flight guard.
- [Critical] getChildResourceSnapshot returned last-good rss/cpu forever; add a
  30s staleness window so a stuck child reads 0 instead of looking healthy.
- Exclude GET /daemon/status (the dashboard's own poll) from the metrics-ring
  request rate, so the Requests chart doesn't count itself.
- Fix cpuPercent JSDoc (percent of total capacity across cores, clamped [0,100])
  in the ring + SDK mirror; add a keep-in-sync cross-reference on the mirror.

Tests: recordRequest excludes /daemon/status; buildDaemonStatusResponse embeds
runtime.metrics.series when provided and omits it otherwise.

* fix(web-shell): address Daemon Status charts review feedback

Correctness fixes surfaced in review:

- bridgeClient: guard token accounting on a live `entry`. On the
  `session/load` path HistoryReplayer re-emits saved usage as live
  session/update frames before the session entry is registered, which
  otherwise dumped a session's historical token total into the current
  metrics window as a phantom burn spike with no model call.
- run-qwen-serve metrics sampler: wrap each tick in try/catch/finally so a
  throwing getter can't crash the daemon; reset the event-loop-lag histogram
  in finally so a thrown tick can't permanently discard it; skip the CPU
  delta (and leave the baseline untouched) when process.cpuUsage() throws;
  seed the rate-reject baseline on the first tick instead of reporting the
  whole since-start backlog as one spike.
- acpAgent workspaceResource: advance the child-CPU baseline only on a
  successful read, avoiding a ~2x phantom spike on the poll after a failure.
- bridge.pendingPromptTotal: count only queued prompts (state === 'queued'),
  not the running one, so the "Queued" chart reflects real backpressure and
  no longer shadows the "Active tasks" line.
- Make the new Daemon Status bridge hooks optional in AcpSessionBridge and
  optional-chain them in the sampler, so a bridge injected via
  RunQwenServeDeps.bridge that predates them degrades gracefully.

Robustness / UX:

- daemon-metrics-ring sanitizes non-finite gauges to 0 so a bad reading
  never serializes as JSON null and gaps the chart.
- child-resource refresh logs failures at debug for observability.
- formatBytes drops to KB/B for sub-MB pipe traffic (was "0.0 MB").
- SvgLineChart peak label is now i18n'd (daemon.charts.peak).
- Daemon Status tabs get the full WAI-ARIA tabs pattern: aria-controls,
  role=tabpanel, and Arrow/Home/End keyboard navigation with roving tabindex.

Tests: replay token guard (no live entry), pipe/gauge/sample-cap defenses,
large-value legend formatting, and tab keyboard navigation.

* fix(web-shell): keep Daemon Status fullscreen + tooltip correct in dialog portal

Two DialogShell-portal theme-scope issues surfaced by a follow-up review:

- Fullscreen was clamped back to 80vh on narrow screens: the
  `@media (max-width: 560px)` `.panel` rule has equal specificity and later
  source order than the base `.panelFullscreen`, so it won. Add a media-scoped
  `.panelFullscreen` override so fullscreen actually expands on mobile.
- SvgLineChart tooltip background used `var(--popover, var(--card))`, neither of
  which the portal theme scope defines, so the declaration dropped and the
  tooltip rendered transparent over the chart. Fall back to `--background`
  (which the dialog scope does define).

* fix(web-shell): flip chart tooltip below cursor near scroll-container top

The Daemon Status charts live inside DialogShell's overflow-y:auto body, so the
topmost chart's upward tooltip (bottom: calc(100% + 4px)) clipped against the
scroll container's top edge, truncating the time header / first series row on
hover. SvgLineChart now resolves its nearest scroll parent and flips the tooltip
below the cursor when the plot sits within ~one tooltip-height of that clip
boundary.

* fix(daemon-status): harden child-resource CPU/memory + sampler lag on failure

Follow-up review fixes:
- acpAgent: prevChildCpu inits to null (not {0,0}) and the workspaceResource
  handler gates the delta on a live prevCpu baseline, so an init-time
  cpuUsage() failure no longer manufactures a phantom spike on the first poll
  — mirrors the daemon sampler's safeCpuUsage null-on-failure contract.
- acpAgent: guard process.memoryUsage() too, reporting 0 rss on failure while
  keeping the already-computed cpuPercent instead of throwing the handler.
- bridge: require Number.isFinite() (typeof NaN === 'number' is true) and
  clamp cpuPercent to [0,100] when caching the child's self-report.
- run-qwen-serve sampler: gate the 5s child-resource refresh on an active
  SSE/WS client (idle staleness already reads 0), and hoist the event-loop
  lag read before the try so a thrown tick charts the real accumulated lag
  instead of a misleading 0.

* fix(daemon-status): protect artifact path from metrics callback + share CPU delta

Follow-up review fixes:
- bridgeClient: wrap recordLiveTokenUsage in try/catch so a throwing injected
  onTokenUsage callback can't skip the critical artifact processing after it —
  metrics are optional, artifacts are not.
- Extract computeCpuPercent() into daemon-metrics-ring and share it between the
  daemon self-sampler and the ACP child's workspaceResource handler, removing
  the duplicated delta/normalize/clamp math and giving it direct unit coverage
  (null sample, non-positive window, normalization, phantom-spike + negative
  clamps).
- Add a single-flight test for bridge.refreshChildResource (two rapid calls
  collapse to one in-flight RPC).
2026-07-05 03:06:09 +00:00
ytahdn
acfb00e1d5
feat(web-shell): add custom at mention panel (#6242)
* feat(web-shell): add custom at mention panel

* chore(web-shell): remove dev MCP resource server

* test(web-shell): cover at mention accept paths

* fix(web-shell): support keyboard at mention activation

* fix(web-shell): address at mention review feedback

* fix(web-shell): close stale at mention panels

* fix(web-shell): keep reopened at mention query empty

* fix(web-shell): address at mention review feedback

* fix(web-shell): harden at mention panel state

* fix(web-shell): stabilize at mention menu state

* fix(web-shell): cache at mention provider listings

* fix(web-shell): address at mention review follow-ups

* fix(web-shell): address at mention review threads

* fix(web-shell): address at mention review regressions

* fix(web-shell): relax auto at trigger cleanup

* chore: remove unrelated pr diff

* fix(web-shell): address at mention review followups

* fix(web-shell): harden at mention review edges

* test(web-shell): cover at mention disabled guards

* fix(web-shell): escape at mention provider delimiters

* fix(web-shell): escape unsafe at reference characters

* fix(web-shell): preserve escaped at mention context

* fix(web-shell): strip control chars from at mentions

* test(web-shell): cover at mention mcp resource guard

* fix(web-shell): propagate at panel text color

* test(web-shell): cover at mention provider failures

* fix(web-shell): handle escaped mcp resource searches

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-05 01:17:35 +00:00
zhangxy-zju
e9a7917d5e
feat(web-shell): support compact echarts full data blocks (#6232)
* feat(web-shell): support custom code block rendering

* fix(web-shell): harden custom code block rendering

* docs: add skill capability gating design

* fix(web-shell): make chart skill host supplied

* docs(web-shell): write chart skill in English

* docs(web-shell): document full-data chart payload

* docs(web-shell): use dataset-backed chart payload

* feat(web-shell): add echarts full-data renderer

* chore(web-shell): keep chart skill host supplied

* fix(web-shell): show loading for streaming chart blocks

* style(web-shell): polish echarts full-data renderer

* fix(web-shell): harden custom code block language parsing

* fix(web-shell): harden echarts full-data renderer

* fix(web-shell): polish echarts renderer followups

* fix(web-shell): reuse enhanced table for chart data

* fix(web-shell): recover chart renderer after errors

* fix(web-shell): harden chart option handling

* fix(web-shell): harden chart data rendering

* fix(web-shell): tighten chart renderer guardrails

* fix(web-shell): update chart fallback title

* fix(web-shell): polish chart renderer review fixes

* feat(web-shell): support compact echarts full data blocks

* docs(web-shell): add chart skill template

* fix(web-shell): address chart review suggestions

* fix(web-shell): preserve punctuation language aliases

* fix(web-shell): address chart follow-up review

* fix(web-shell): harden chart ref resolution

* fix(web-shell): cover chart sanitizer follow-ups

* fix(web-shell): address chart review follow-ups

* fix(web-shell): address chart review leftovers

* fix(web-shell): close chart review gaps

* fix(web-shell): handle latest chart review
2026-07-04 15:36:54 +00:00
jinye
59e771cef6
feat(daemon): Add session export endpoint (#6297)
* feat(daemon): add session export endpoint

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix PR integration capability baseline (#6297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address export tool call id review (#6297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-04 09:33:44 +00:00