feat(web-shell): refresh composer skills incrementally after toggles - #9131
Conversation
Hosts can apply Skill toggles incrementally without a full task reload or suppressing skills.* events. Co-authored-by: Cursor <cursoragent@cursor.com>
The new normalizer parser pushed the browser daemon bundle over the 186KB cap. Raise it to 187KB and pin the review gaps that were cheap to close. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
E2E Test Report
Repository-wide build and typecheck remain blocked by pre-existing local dependency artifacts outside this diff: Core cannot find Sharp's |
# Conflicts: # packages/cli/src/serve/workspace-service/__tests__/facade.test.ts
The 190KB cap overflowed by 491 bytes after merging main, so the SDK build fails before tests run. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- round-5 reverse-audit catch-path staleness-guard finding — already reported as R1-1 (comment 3796222512, [comment 3797837422](https://github.com/QwenLM/qwen-code/pull/9…
Unresolved, please confirm:
- [Critical] R9-1 secondary clause (comment 3815862715, packages/web-shell/client/App.tsx:4744) — the daemon can emit
activation: 'applied'withsessionsRefreshed: 0when the br…
Deferred under the convergence posture (round 16, not a blocker) — recorded, not requested in this round:
packages/webui/src/daemon/session/DaemonSessionProvider.tsx:4492 — [probe] within-batch same-id dedupe (seenSkillMutationIds) untested on the replay path — guard-deletion mutant doubles a batch mutation, all provider tests stay greenpackages/webui/src/daemon/session/types.ts:557 — [review] skillsVersion has zero production read sites — provider tests certify a signal nobody consumespackages/web-shell/client/App.tsx:534 — [probe] mergeSkillToggles same-skill double-toggle in one signal update untested — concat mutant pins the fallback, all tests greenpackages/web-shell/client/App.tsx:4671 — [probe] silent initial-load half of the notifyOnError split unpinned — toast-on-connect mutant survives all 504 testspackages/web-shell/client/App.test.tsx:10895 — [probe] late-resolving reload tests resolve with the identical payload — stale-response guard + cancelled cleanup unpinnedpackages/web-shell/client/App.test.tsx:11078 — [probe] the || !loaded reload-failure branch unpinned — mutant marks a failed session-bound refresh handled and pins the stale fallback, ships greenpackages/web-shell/client/App.test.tsx:11036 — [probe] duplicate concurrent snapshot fetches pinned by exact call counts — the first (successful) response is discarded when the racing reconcile fetch fails
中文说明
已审查。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。
收敛姿态下延后(第 16 轮,非阻断)——已记录,本轮不要求修改:共 7 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.15)
chiga0
left a comment
There was a problem hiding this comment.
Web Shell Skill Toggle Refresh — first review.
What I checked
availableSessionSkillInfos — correctly derives SkillInfo from the active session's skills name list + commands, falling back to empty description when no matching command exists. Sort is stable and case-normalised via localeCompare. ✅
sessionSkillsReflectToggle — returns false when sessionSkills === undefined (session not yet reporting), preventing premature removal of the fallback before the session catches up. The every predicate correctly handles the case where a disabled skill is absent from the enabled set. ✅
mergeSkillToggles — last-write-wins semantics via Map keyed on lowercase name; correct for consecutive toggle events where later events supersede earlier ones. ✅
mappers.ts — _meta.availableSkills fallback: backward-compatible; the flat availableSkills field still takes priority over _meta (two tests at lines 845 and 863 pin both directions). ✅
bumpWorkspaceEventSignals dedup: skill-toggle mutations are deduped within a batch via seenSkillMutationIds and across renders via existingIds derived from prior signal state — prevents the double-increment from a server emitting two workspace.settings.changed events per toggle. ✅
handledSkillMutationKeysRef cleanup on context switch: iterates the Set and deletes matching entries during the loop. JS Set iteration with deletion is well-defined — elements already visited are gone, unvisited elements that haven't been deleted are visited once. ✅
connectionSkillSnapshotRef: updated on every render (not in an effect) so the ref is current when the async reloadLoadedSkills callback reads it. Correct pattern for avoiding stale closure reads inside async callbacks. ✅
CI scope
Test (ubuntu-latest, Node 22.x) ✅ · web-shell E2E Smoke ✅ · Dependency CVE audit ✅.
Test (macos-latest/windows-latest) SKIPPED — platform behaviour unverified.
No blocking findings. doudouOUC already approved (round 1).
Reviewed with AI assistance.
Local real-stack verification of #9131 — ✅ core fix confirmed, 2 non-blocking notesI built this PR locally and drove it against a real Verdict: the reported bug is real, this PR fixes it, and I found no regression. Two small notes below are worth a look but nothing I'd block on. Harness
Instrumentation: the SSE body of Results
1 — the headline bug and its fixBaseline: both arms have a live session and both offer
…and 10 s later the PR build has dropped it while the base build still offers the disabled skill: The single {"type":"settings_changed","data":{"key":"skills.disabled","value":["web-search"],"scope":"workspace",
"mutation":{"id":"eb406619-…","kind":"skill_toggle","skills":[{"name":"web-search","enabled":false}],
"activation":"applied","sessionsRefreshed":2,"sessionsFailed":0}}}Note the PR build made zero extra 2 — why base fails only in the disable directionTwo wire facts from the live daemon explain it and are worth recording:
3 — both hunks are load-bearing (mutation A/B on the PR tree)
So the 4 — partial activation, dedup, and failed refreshTo reach the
Notes (non-blocking)N1 — the new error message never reaches the user. N2 — the mutation bookkeeping grows without bound. N3 — reviewer test-plan step 3 is not reproducible through the UI. For a tab that has been opened but has not yet sent a prompt, I measured no Regression checks
中文版本#9131 本地真实环境验证 — ✅ 核心修复成立,2 条非阻塞建议我在本地构建了这个 PR,并用真实的 结论:所描述的 bug 真实存在,本 PR 确实修好了它,未发现回归。 下面两条建议值得看一眼,但都不足以阻塞合并。 验证环境
埋点:在页面内对 结果
1 —— 核心 bug 与修复基线:两条腿都有活跃 session,都能补全出 两条腿收到的那条 2 —— 为什么 base 只在「禁用」方向出错来自实跑 daemon 的两个线上事实解释了这一点,值得记录:
3 —— 两处改动都是必需的(在 PR 树上做变异 A/B)
即: 4 —— partial 激活、去重与刷新失败为了走到
建议(非阻塞)N1 —— 新加的错误文案实际上永远不会出现。 N2 —— mutation 记账无上界增长。 N3 —— 评审测试计划第 3 步无法通过 UI 复现。 对于「已打开但尚未发过 prompt」的标签页,我实测到根本没有 回归检查
|
|
Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with |
wenshao
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R17-6 per-context handled-key history replay forces a redundant /workspace/skills fetch per session switch — core mechanism already reported in the open thread at App.tsx (comment 3798641362)
- R17-10 failed mutation-triggered reload parks the composer on the pre-toggle snapshot with no retry — same root cause as the open failed-reload thread (comment 3798641366)
Unresolved, please confirm:
- [Critical] R9-1 secondary clause (comment 3815862715, packages/web-shell/client/App.tsx:4744): the applied fast path ignores sessionsRefreshed — the client-side hole is probe-confirmed at this head (an applied/sessionsFailed:0/sessionsRefreshed:0 muta…
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Deferred under the convergence posture (round 17, not a blocker) — recorded, not requested in this round:
packages/webui/src/daemon/session/DaemonSessionProvider.tsx:4581 — [probe] new skillsVersion signal has no production read site; useDaemonSkills wires no reloadpackages/web-shell/client/App.tsx:499 — [probe] session-path argumentHint propagation in availableSessionSkillInfos is untested (spread-deletion mutant survives all 504 tests)packages/web-shell/client/App.test.tsx:4669 — [review] emitSkillMutation helper bumps skillsVersion and replaces lastSkillMutation for duplicate ids, unlike the provider's no-change semanticspackages/web-shell/client/App.test.tsx:10805 — [review] no test asserts the mutation-driven reload fetches the connection's workspace (workspaceByCwd mock ignores cwd)packages/web-shell/client/App.test.tsx:10822 — [review] workspace re-entry/reconnect fires two identical /workspace/skills fetches per commit; the duplication is pinned by the call-count testspackages/web-shell/client/App.test.tsx:10898 — [probe] race tests resolve stale in-flight fetches with identical values — the staleness guard is unpinned (guard-deletion mutant ships 504/504 green)packages/web-shell/client/App.test.tsx:10881 — [probe] fast-path priorPending.length === 0 conjunct has no discriminating test (conjunct-deletion mutant ships 504/504 green)packages/webui/src/daemon/session/types.ts:558 — [review] resolved mutations resurrect on every reconnect — the ledger persists while handled keys are cleared on disconnect
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未决,请确认:共 1 条(原文未翻译,列表见上方英文部分)。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
收敛姿态下延后(第 17 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.14)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- skillsVersion has zero production read sites (packages/webui/src/daemon/session/types.ts:557, DaemonSessionProvider.tsx:4579) — already recorded in the round-14/15/16 deferred lists
- skillMutationsByCwd append-only retention × handled-keys cleared on disconnect → reconnect/session-switch replay (packages/webui/src/daemon/session/types.ts:559) — already recorded in the round-8/9/10/15/16 deferred lists and open…
- within-batch seenSkillMutationIds dedup untested on the replay path (packages/webui/src/daemon/session/DaemonSessionProvider.tsx:4492) — already recorded in the round-8/9/10/11/14/16 deferred lists
中文说明
本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.15)
Review R1-1: the resilience suite's only cancelled-related assertion —
toContain("needs.review-pr.result == 'cancelled'") — matched the old and
the new gate alike, so reverting the two != 'cancelled' conjuncts would
bring back the #9131 false-fallback regression with the suite green.
Pin the full compound clause, grouping included, so dropping either
conjunct (or the parenthesization) fails the test, and rewrite the
rationale comment that still asserted the falsified premise that a
run-level cancel takes the queued fallback job down with it.
Mutation-verified: the gate test fails on a reverted-gate mutant and
passes on the PR gate.
e51199b
|
Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
6 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- skillsVersion has zero production read sites (packages/webui/src/daemon/session/types.ts:557) — already recorded in the round-14/15/16/17 deferred lists
- emitSkillMutation helper bumps skillsVersion for duplicate ids (packages/web-shell/client/App.test.tsx:4668) — already recorded in the round-9 and round-17 deferred lists
- priorPending.length === 0 fast-path conjunct has no isolating test (packages/web-shell/client/App.tsx:4743) — already recorded in the round-14 and round-16 deferred lists
- skillMutationsByCwd append-only retention x handled-keys cleared on disconnect (packages/webui/src/daemon/session/DaemonSessionProvider.tsx:4590) — already recorded in the round-8/9/10/15/16/17 deferred lists and open threads
- in-batch seenSkillMutationIds dedupe untested on the replay path (packages/webui/src/daemon/session/DaemonSessionProvider.tsx:4492) — already recorded in the round-8/9/10/11/14/16/17 deferred lists
- catch-path staleness toast in reloadLoadedSkills (packages/web-shell/client/App.tsx:4687) — already reported as R1-1 (comments 3796222512, 3797837422, 3798641333), confirmed still open at this head
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Deferred under the convergence posture (round 18, not a blocker) — recorded, not requested in this round:
packages/webui/src/daemon/session/DaemonSessionProvider.tsx:4492 — [review] legacy mutation-less skills settings event untested (design-doc promise unverified)packages/web-shell/client/App.tsx:4746 — [probe] fast-path guard conjuncts (sessionId, activation, sessionsFailed) have no isolating tests — three deletion mutants ship 505/505 greenpackages/web-shell/client/App.test.tsx:11155 — [probe] unknown-skills reload test pins refresh count with a loose inequality — double-fire mutant ships green
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 6 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
收敛姿态下延后(第 18 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.15)
…QwenLM#9716) * fix(review): stop the fallback comment from firing on superseded runs The fallback-comment gate admitted every review-pr result of 'cancelled' on the assumption that a run-level cancel takes the queued fallback job down with it, leaving job-level timeout as the only live source of a cancelled review step. That assumption does not hold: the job is guarded by always(), which keeps it running through a run-level cancel. On PR QwenLM#9131 a same-head pull_request_target pair started 1s apart, the newer run cancelled the older inside authorize, and the older run posted a false "review did not complete" (run 32558544379) while the surviving run was still reviewing — same head, so the in-step head-moved guard could not catch it. The two cancels are separable through needs: a job-level timeout cancels review-pr alone while its upstream jobs completed long before, whereas a run-level cancel sweeps the whole chain. 'cancelled' now opens the gate only when neither authorize nor delay-automatic-review was itself cancelled. * test(review): pin the fallback gate's upstream-cancel conjuncts Review R1-1: the resilience suite's only cancelled-related assertion — toContain("needs.review-pr.result == 'cancelled'") — matched the old and the new gate alike, so reverting the two != 'cancelled' conjuncts would bring back the QwenLM#9131 false-fallback regression with the suite green. Pin the full compound clause, grouping included, so dropping either conjunct (or the parenthesization) fails the test, and rewrite the rationale comment that still asserted the falsified premise that a run-level cancel takes the queued fallback job down with it. Mutation-verified: the gate test fails on a reverted-gate mutant and passes on the PR gate. * test(review): assert the bare cancelled disjunct is absent from the gate Review R2-1: the compound-clause pin proves presence, not absence — a merge-conflict resolution keeping both sides of the gate hunk re-adds the bare "== 'cancelled' ||" disjunct beside the intact compound clause, reopening the gate on every cancelled review-pr with the suite green. Add the negative assertion; inside the compound clause the substring is followed by ' &&', so it holds on the intended gate. Also take the round-2 deferred note: the job comment now says a run-level cancel landing after the upstream chain finished still opens the gate and is suppressed by the in-step head-moved guard, and why a same-head twin cannot land that late. Mutation-verified: with the bare disjunct re-added the gate test fails on the negation; the pristine workflow passes. * test(review): pin the cancelled-check count; credit the PR-state check Review R3-2: the negative assertion rejected only one rendering of the bare disjunct — a parenthesized or respaced re-addition beside the intact compound clause escaped both pins with the gate reopened on every cancelled review-pr. Replace the negation with an occurrence-count pin: exactly one "needs.review-pr.result == 'cancelled'" in the gate catches any rendering, while the compound pin keeps guarding the conjuncts. Review R3-1: the rationale comment's list of late run-level cancels missed the closed-action flavor — a same-head cancel hours in with the upstream chain green, stopped only by the in-step PR-state check the comment never credited. Credit it. Mutation-verified: bare, parenthesized, and reverted-conjunct gate mutants each fail the gate test; the pristine workflow passes at base parity.
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 30 passed · 0 failed · 30 total Flakiness gate: ✅ 3 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:30 通过 · 0 失败 · 30 总计 抖动门:✅ 3 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9131 verification —
|
| fixture (real daemon shape) | head skills |
base skills |
cell |
|---|---|---|---|
| F1 nested, pre-redaction | ['web-search'] |
[] |
flip |
| F2 nested, redacted SSE shape | ['web-search'] |
[] |
flip |
| F6 nested with non-strings | ['ok'] (filtered) |
[] |
flip |
| F3 authoritative disable-all (empty nested) | [] |
[] |
parity |
| F4 flat legacy | ['legacy-skill'] |
identical | parity |
| F5 both shapes present | ['flat-skill'] (flat wins) |
identical | parity |
| F7 neither | [] |
identical | parity |
This also proves the premise of the whole PR: the daemon emits availableSkills nested under _meta (Session.ts), the SDK does not flatten it, and the base mapper read only the flat key — so on base, connection.skills from live command updates is always [], which is precisely why the stale workspace snapshot always won.
Test-level A/B (02-test-ab-head-vs-base.png)
Identical HEAD test files run against head source vs base source (base worktree at HEAD^1, node_modules wired; realpath-asserted that @qwen-code/sdk resolves into the head tree — the PR leaves packages/sdk-typescript, package.json, package-lock.json untouched, and @qwen-code/webui is fully vi.mock-ed in the web-shell tests, so the control is clean; webui tests compile base source directly).
| suite | @ HEAD | @ BASE | expectation on base |
|---|---|---|---|
App.test.tsx |
520/520 | 11 failed / 509 passed | the 11 new skill tests fail |
DaemonSessionProvider.test.tsx |
239/239 | 2 failed / 237 passed | the 2 new signal tests fail |
mappers.test.ts |
32/32 | 1 failed / 31 passed | the nested-read test fails |
All 14 base-side failures are the intended behavioral assertions, not import/compile errors. The first App failure prints the exact bug from issue #9123: expected [ { name: 'stale-skill', … } ] to deeply equal [] — the stale workspace snapshot reintroducing a disabled Skill after an authoritative empty update. The one new App test that passes on base ("uses the workspace Skill snapshot while the session Skill list is unknown") asserts behavior base already had (always-workspace-snapshot), so it is not evidence either way on base and is pinned at HEAD by mutation M5 instead.
Mutation matrix (03-m1-live-mutation-red.png; full table in capture 02)
Single-guard reverts in a scratch worktree at HEAD, same test files, each row a real vitest run:
| mutation | red set | classification |
|---|---|---|
| M1 mapper nested-read reverted | 1 (nested wire-shape test); flat-preference control stays green | guard pinned |
| M2 provider dedupe reverted | 2 (dedupe + replay-batch signal tests) | guard pinned |
M3 composer source-selection reverted to loadedSkillsReady |
4 (active-session snapshot, applied-no-refresh, partial-activation, workspace-scoped revalidation) | guard pinned |
| M4 reconciliation effect disabled | 10 (every mutation-driven refresh path); the applied-path test stays green | guard pinned |
M5 connection.skills === undefined clause removed |
1 (unknown-session-list fallback test) | guard pinned |
Union of M1–M5 red sets is 15; base red is 14 (base's always-workspace behavior coincidentally satisfies M5's test). Every guard the PR introduces is pinned by at least one mutation row, and every new test is pinned by at least one row or the base cell — no survivors, no vacuous tests. Positive controls: the flat-preference test inside M1's file and the 509/237/31 green tests in the base cells prove the harnesses collect and execute the mutated files.
Reviewer Test Plan walkthrough
- Active session enable appears via live update, no reload — "uses the active session command snapshot for Skill enable and empty disable updates" asserts
reloadSessionnot called and the command appearing; green @ head, red @ base. ✔ - Disable last Skill → empty autocomplete — same test's second phase asserts
skills === []and the command gone. ✔ - Pre-session toggle refreshes once; same mutation id not re-refreshed — "refreshes session-less composer Skills once for a deferred mutation" (loadSkillsStatus call count pinned at 2 across re-renders) plus provider dedupe test (two events, one id →
skillsVersion+1). ✔ - Partial activation: workspace snapshot temporarily authoritative, later live update regains authority — "uses a refreshed workspace Skill snapshot after partial activation". ✔
- Failed fallback refresh surfaces; unrelated legacy settings event still reloads — "surfaces a failed deferred Skill snapshot refresh" (toast) + provider test's
ui.themeevent still bumpingsettingsVersion. ✔
All five steps are executable and pinned; no unreachable step.
Findings (non-blocking observations)
No blocking findings. The following are completeness notes, each verified, none changing the verdict:
- Redundant defence, correct as-is: M4 shows the reconciliation effect's applied-fast-path is unobservable by the applied-path test (source selection alone passes it). It is still needed to clear a stale
loadedSkillsFallbackin other flows (its absence flips 10 other tests). Classification per the matrix taxonomy: redundant defence, not dead code — keep. settingsVersionde-bump blast radius is bounded: skill_toggle events no longer bump the generic signal. Consumers enumerated: voice settings (unrelated keys),useDaemonProviders(unrelated),useDaemonSettings/settings dialog — andskills.disabled/skills.enabledareshowInDialog: falseinsettingsSchema.ts, so no dialog consumer displays them. The skill-manager UI is driven by the new skill signal. No consumer left stale.- Envelope unchanged, no sibling regression:
updateConnectionFromDaemonEventaccepts only the wrappeddata.updateenvelope (identical at base); the flat persisted-transcript shape handled by the redactor serves other consumers and never reached this mapper at base either. - Intermediate bundle-budget commits are historical: the PR's commits mention raising the SDK browser bundle cap (186→190KB), but the aggregate diff touches no build config; base
MAX_DAEMON_BROWSER_BUNDLE_BYTESis already 208KB. Nothing to gate.
Not covered
- The author's external Chromium 5/5 E2E matrix was not re-run; no committed Playwright spec covers this surface (checked
packages/web-shell/client/e2e/), so there is nothing in-repo to execute. Unit + wire-oracle evidence stands in its place. - Per-commit attribution: depth-2 checkout,
git rev-list HEAD^1..HEAD^2returns 1 vs 84 commits in the metadata; only the aggregateHEAD^1..HEADdiff was verified. - Daemon-side mutation production (feat(daemon): attach skill-toggle mutation metadata to settings_changed #9051) verified present at base by code reading (
workspace-service/index.tsemitsmutationonsettings_changed; SDK types at base) and by provider tests replaying the real event shapes — not by driving a live daemon end-to-end. - Windows/Linux native browser runs (author-declared N/A).
Methodology
Environment: node:22-bookworm container, merge-ref checkout at depth 2 (HEAD merge, HEAD^1 base, HEAD^2 head). Base and mutation worktrees under tmp/ with the root node_modules symlinked; realpath of @qwen-code/sdk asserted to point into the head tree and shown harmless (SDK untouched by the PR; webui vi-mocked in web-shell tests). Wire-oracle harness (harness/mapper-wire-oracle.mjs) compiles head/base mappers.ts standalone (esbuild) and drives identical fixtures through the exported updateConnectionFromDaemonEvent — no mocks of the unit under test. Test-level cells and mutations ran the repo's own vitest suites; every mutation was applied in a scratch worktree and restored (sha-verified). Raw logs in logs/, harnesses in harness/, captures in evidence/ (01: wire-oracle run; 02: A/B + matrix parsed from the real logs; 03: live M1 mutation run). Gates: full packages/webui suite 599/599, full packages/web-shell suite 4113/4113, tsc --noEmit clean in both.
Flakiness gate log
rounds=5 files=3 skipped=0
file packages/web-shell/client/App.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/App.test.tsx
file packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: (cd packages/webui) npx --no-install vitest run ./src/daemon/session/DaemonSessionProvider.test.tsx
file packages/webui/src/daemon/session/mappers.test.ts: (cd packages/webui) npx --no-install vitest run ./src/daemon/session/mappers.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/web-shell/client/App.test.tsx: PPPPP
packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: PPPPP
packages/webui/src/daemon/session/mappers.test.ts: PPPPP
verdict: pass
summary: 3 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 1 · packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: P (exit 0)
round 1 · packages/webui/src/daemon/session/mappers.test.ts: P (exit 0)
round 2 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 2 · packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: P (exit 0)
round 2 · packages/webui/src/daemon/session/mappers.test.ts: P (exit 0)
round 3 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 3 · packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: P (exit 0)
round 3 · packages/webui/src/daemon/session/mappers.test.ts: P (exit 0)
round 4 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 4 · packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: P (exit 0)
round 4 · packages/webui/src/daemon/session/mappers.test.ts: P (exit 0)
round 5 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 5 · packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx: P (exit 0)
round 5 · packages/webui/src/daemon/session/mappers.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Released in v0.22.2. |







What this PR does
Adds a dedicated, de-duplicated workspace signal for Skill-toggle mutations and makes Web Shell choose the correct composer source for each runtime state. Active sessions use the latest authoritative command snapshot, while session-less, deferred, and partial activation paths refresh the workspace Skill snapshot. Failed fallback refreshes are surfaced instead of leaving stale suggestions silently.
Why it's needed
Web Shell previously loaded a workspace Skill snapshot and always merged it back into composer commands. That could reintroduce a disabled Skill after the active session had already published an authoritative empty command list. The generic settings signal also discarded Skill mutation metadata, so the two settings events from one toggle could not be de-duplicated or handled according to activation outcome.
Reviewer Test Plan
How to verify
Evidence (Before & After)
Before: after an authoritative empty command update, the stale workspace snapshot reintroduced
/web-searchand it remained visible in autocomplete after 10 seconds.After: the same Chromium flow removes
/web-searchimmediately without a task/transcript reload. The independent Chromium matrix passed 5/5 scenarios.Tested on
Environment (optional)
Node.js 22.14.0, Playwright Chromium, mock daemon event stream.
Risk & Scope
Linked Issues
Closes #9123
Depends on #9051
中文说明
这个 PR 做了什么
为 Skill 切换 mutation 增加独立且按 id 去重的 workspace signal,并让 Web Shell 根据 runtime 状态选择正确的 composer 数据源。活跃 session 使用最新的权威命令快照;无 session、deferred 和 partial activation 路径会刷新 workspace Skill 快照。fallback 刷新失败会直接提示,不再静默保留过期建议。
为什么需要
Web Shell 之前会读取 workspace Skill 快照,并始终把它重新合并进 composer commands。即使活跃 session 已经发布权威的空命令列表,旧快照仍可能把已禁用 Skill 加回来。通用 settings signal 还会丢弃 Skill mutation 元数据,因此无法对一次切换产生的两条 settings 事件去重,也无法根据 activation 结果选择处理方式。
评审测试计划
如何验证
证据(修复前后)
修复前:收到权威空命令更新后,旧 workspace 快照仍会重新加入
/web-search,10 秒后 autocomplete 中依然可见。修复后:同一 Chromium 流程会立即移除
/web-search,且不重载 task/transcript。独立 Chromium 验证矩阵 5/5 通过。测试环境
运行环境(可选)
Node.js 22.14.0、Playwright Chromium、mock daemon event stream。
风险与范围
关联 Issue
Closes #9123
Depends on #9051