Skip to content

feat(web-shell): show git state hints beside branch picker actions - #10397

Merged
wenshao merged 2 commits into
QwenLM:mainfrom
wenshao:feat/webshell-branch-picker-action-hints
Aug 28, 2026
Merged

feat(web-shell): show git state hints beside branch picker actions#10397
wenshao merged 2 commits into
QwenLM:mainfrom
wenshao:feat/webshell-branch-picker-action-hints

Conversation

@wenshao

@wenshao wenshao commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

The branch picker popover (opened from the workspace folder's git chip in the sidebar, the composer chip, or the Environment panel) now shows a short state hint at the right edge of its Update Project, Commit, and Push rows, so the user can judge before clicking:

  • Update Project shows ↓3 · origin/main when behind, "Up to date" (dimmed) when in sync, and a warning-toned ↓3 · uncommitted changes when behind with a dirty tree. It is disabled with "No upstream" when the branch has no tracking branch, and with "Upstream gone" when the tracked remote branch was deleted and pruned.
  • Commit shows how many changes the commit would sweep up, calling out untracked entries (5 changes (2 untracked)), or "No changes" (dimmed) on a clean tree. "Changes" rather than "files" because the daemon's counters are entry-granular (a partially staged file counts twice, an untracked directory once).
  • Push shows ↑2 when ahead, "Nothing to push" (dimmed) when in sync, "Sets upstream on push" when there is no usable upstream, and a warning-toned ↑2 ↓1 · update first when the branch is both ahead and behind.
  • Disabling follows what git itself refuses: Update Project is disabled during a merge/rebase/cherry-pick, with conflicted entries, on a detached HEAD, or without a usable upstream; Push is disabled only on a detached HEAD (a push does not consult the index), and shows the in-progress operation or conflict as a warning on an enabled row otherwise.

Rows that are merely uninteresting (up to date, nothing to push, no changes) are only dimmed, never disabled, since the action is still harmless.

The popover gets a new optional status prop (the same DaemonWorkspaceGitStatus the trigger chips already hold) and fetches its own working-tree status when it opens, so all three entry points (sidebar chip, composer chip, Environment panel) see fresh counters rather than their caller's last poll; the newer of the two by computedAt wins, and the sidebar receives the fetched status back through onStatusRefreshed to keep its chip in step. Ahead/behind/upstream come from the branch listing the popover already fetches on open; if a status the daemon computed later disagrees with that listing (upstream unset, HEAD detached, new commits from a terminal), the listing is re-fetched once so the rows follow the repo rather than the snapshot. The counter normalisation is shared with GitBranchIndicator rather than re-implemented.

One small core change: parseBranchLines now reads git's gone tracking state into an optional upstreamGone on the branch listing (GitBranchInfo / DaemonGitBranchInfo), since neither the listing nor the status could previously distinguish a deleted upstream from a healthy in-sync one.

Why it's needed

The three actions have consequences that are invisible until after the click: git pull without an upstream fails, a pull onto a dirty tree may be refused with a 409, Commit runs git add -A and therefore sweeps in untracked files, and Push with no upstream creates a remote branch. The daemon already computes everything needed to say so up front — the chip that opens the popover even renders ↑n ↓n and a dirty dot — but none of it reached the popover, so users clicked blind and read the outcome from the status line afterwards.

Related: #10390 changes what happens after clicking Update Project on a dirty tree (stash / discard resolution panel). This PR is about what the user sees before clicking and is independent of it; the two touch adjacent code in BranchPickerPopover.tsx and i18n.tsx, so whichever merges second will need a small conflict resolution. The dirty-tree warning stays accurate under #10390 — it just leads to the resolution panel instead of an error.

Reviewer Test Plan

How to verify

Unit coverage is the primary check. deriveActionHints is exported and covered as a decision table (13 cases: in-sync/clean, behind, behind+dirty, no upstream, ahead, ahead+behind, changed/untracked counts, in-progress operation, conflicts, detached HEAD, listing-vs-status precedence, v1 status without tree summary), plus rendered assertions that the rows carry the expected disabled state and data-tone, and that opening the popover asks the caller to refresh status exactly once even when the parent passes a new inline callback on re-render.

cd packages/web-shell && npx vitest run \
  client/components/BranchPickerPopover.test.tsx \
  client/components/sidebar/WorkspaceSection.test.tsx \
  client/components/panels/EnvironmentPanel.test.tsx
# Test Files  4 passed (4) · Tests  170 passed (170)   (round 2, plus client/components/ChatEditor.test.tsx)
cd packages/core && npx vitest run src/utils/git-branches.test.ts   # 58 passed (gone-upstream listing)

For a manual check in qwen serve + Web Shell, open a trusted git workspace and click the branch chip on the sidebar folder header after putting the repo in each state: in sync and clean → all three rows dimmed with "Up to date" / "No changes" / "Nothing to push"; git reset --hard HEAD~3↓3 · origin/main; also edit a tracked file and add an untracked one → warning-toned ↓3 · uncommitted changes and 2 changes (1 untracked); git checkout -b x with one local commit → Update Project disabled "No upstream", Push "Sets upstream on push"; an in-progress conflicting rebase → Update Project and Push disabled with "Rebasing"; git checkout --detach → both disabled with "Detached HEAD"; a conflicting merge on the branch → Update Project disabled "Merging", Push enabled with a warning "Merging"; push a branch with -u, delete it on the remote, git fetch --prune → Update Project disabled "Upstream gone", Push "Sets upstream on push". Switch the UI language to 中文 to see the localized copy. The full manual plan is in .qwen/e2e-tests/2026-08-28-webshell-branch-picker-action-hints.md.

Evidence (Before & After)

Round 2 (1f960ddb36, after review) re-captured the After column with the revised copy and rules and added two states; the Before column is from round 1 and is unchanged by the review. Captured from the real stack: npm run bundlenode dist/cli.js serve --workspace <fixture> (isolated QWEN_HOME, fake OpenAI env, no model calls) → Playwright clicks the sidebar git chip and screenshots the popover. Both columns were taken against the same daemon workspace in the same git state, seconds apart: "Before" is the same bundle with the web-shell UI rebuilt from origin/main's client files, "After" is this branch. Per-row disabled / hint text / data-tone were also read back from the DOM into a ledger (dom-ledger.jsonl next to the images); every Before row has no hint and stays enabled, including the states where the click would fail.

Repo state Before After
In sync, clean tree before-sync after-sync
Behind 3, clean before-behind after-behind
Behind 3 + modified tracked file + untracked file before-behind-dirty after-behind-dirty
New branch, 1 commit, no upstream before-no-upstream after-no-upstream
Ahead 1 / behind 1 before-ahead-behind after-ahead-behind
Rebase in progress with a conflict (HEAD detached) before-rebase-conflict after-rebase-conflict
Detached HEAD before-detached after-detached
Merge in progress with a conflict, on the branch (round 2) (as every other Before row: no hint, all rows enabled) after-merge-conflict
Upstream deleted on the remote and pruned (round 2) (as every other Before row: no hint, all rows enabled) after-upstream-gone

zh-CN, behind 3 with uncommitted changes:

after-behind-dirty-zh

Assets: round 1 wenshao/qwen-code@2d7c61f, round 2 wenshao/qwen-code@447a954 (branch pr-assets/10397-verify).

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

macOS; unit tests (vitest + jsdom) plus the bundled qwen serve + headless Chromium (Playwright) run above.

Risk & Scope

  • Main risk or tradeoff: Update Project (and Push on a detached HEAD) are now disabled in states git refuses. The popover fetches status on open and re-fetches the listing when a newer status contradicts it, so a rebase finished in a terminal clears the disabled row once the chip's status updates (focus / poll / SSE) without reopening; between those refreshes the row reflects the last known state. The listing-vs-status recency check compares the daemon's computedAt with the browser clock, so on a remote daemon with a skewed clock the re-fetch may not fire (the hints then degrade to open-time behaviour, never to a wrong enable).
  • Not validated / out of scope: the composer-chip and Environment-panel entry points were not screenshotted (same component, same props; only the sidebar chip was driven). Windows/Linux not run locally (pure client code, no platform-specific paths).
  • Breaking changes / migration notes: none. The new props are optional; callers that do not pass status still get the popover's own on-open fetch. upstreamGone is an additive optional field on the branch listing.

Linked Issues

Related to #10390 (dirty-working-tree pull resolution; independent, adjacent code).

中文说明

这个 PR 做了什么

分支选择弹窗(从侧栏工作区文件夹的 git 芯片、composer 芯片或环境面板打开)现在会在 更新项目提交推送 三行右侧显示一个简短的状态提示,让用户在点击前就能判断:

  • 更新项目:落后时显示 ↓3 · origin/main;同步时显示"已是最新"(变灰);落后且工作区有未提交更改时显示警告色的 ↓3 · 有未提交更改;分支没有上游时禁用并显示"无上游分支",所跟踪的远程分支已被删除并 prune 时禁用并显示"上游分支已不存在"。
  • 提交:显示这次提交会带入多少处更改,并单独标出未跟踪项(5 处更改(2 未跟踪));工作区干净时显示"无更改"(变灰)。用"更改"而非"文件",是因为 daemon 的计数按条目计(部分暂存的文件算两条,未跟踪目录算一条)。
  • 推送:领先时显示 ↑2;同步时显示"无待推送"(变灰);没有可用上游时显示"推送时设置上游";既领先又落后时显示警告色的 ↑2 ↓1 · 请先更新
  • 禁用规则跟随 git 自身的拒绝:merge/rebase/cherry-pick 进行中、存在冲突条目、HEAD 游离、或没有可用上游时禁用"更新项目";"推送"只在 HEAD 游离时禁用(push 不看索引),其它情况下把进行中的操作或冲突作为警告显示在可点击的行上。

只是"没什么可做"的行(已是最新、无待推送、无更改)只变灰、不禁用,因为操作本身无害。

弹窗新增可选的 status prop(就是触发芯片已经持有的 DaemonWorkspaceGitStatus),并在打开时自行拉取一次工作区状态,因此三个入口(侧栏芯片、composer 芯片、环境面板)看到的都是新鲜计数而不是各自调用方上次轮询的结果;两者按 computedAt 取较新者,侧栏通过 onStatusRefreshed 拿回这份状态让芯片同步。ahead/behind/upstream 取自弹窗打开时本来就会拉取的分支列表;若之后到达的、daemon 计算时间更晚的状态与该列表不一致(上游被取消、HEAD 游离、终端里新提交),会重新拉取一次列表,让各行跟随仓库而不是打开时的快照。计数归一化与 GitBranchIndicator 共用,不再各写一份。

core 有一处小改动:parseBranchLines 现在把 git 的 gone 跟踪状态读入分支列表的可选字段 upstreamGoneGitBranchInfo / DaemonGitBranchInfo),此前无论列表还是状态都无法区分"上游已删除"和"健康且同步"。

为什么需要

这三个操作的后果在点击之前是看不见的:没有上游时 git pull 会失败;脏工作区上 pull 可能被 409 拒绝;"提交"会执行 git add -A,因此会把未跟踪文件一并带入;没有上游时"推送"会创建远程分支。daemon 已经算出了提前说明所需的全部信息——打开弹窗的那个芯片甚至已经渲染了 ↑n ↓n 和脏点——但这些都没有传到弹窗里,用户只能盲点,然后事后从状态行读结果。

相关:#10390 改的是在脏工作区上点击"更新项目"之后发生的事(stash / 放弃 的解决面板)。本 PR 关注的是点击之前用户看到什么,与之相互独立;两者在 BranchPickerPopover.tsxi18n.tsx 中改到相邻代码,后合入的一方需要做一次小的冲突解决。脏工作区警告在 #10390 合入后依然准确——只是点下去会进入解决面板而不是报错。

审阅测试计划

如何验证

主要依靠单元测试。deriveActionHints 已导出并以决策表方式覆盖(13 个用例:同步且干净、落后、落后且脏、无上游、领先、既领先又落后、变更/未跟踪计数、进行中操作、冲突、游离 HEAD、分支列表优先于状态、无树摘要的 v1 状态),另有渲染层断言确认各行携带预期的 disabled 状态与 data-tone,以及打开弹窗时恰好请求一次状态刷新——即使父组件在重渲染时传入新的内联回调。

cd packages/web-shell && npx vitest run \
  client/components/BranchPickerPopover.test.tsx \
  client/components/sidebar/WorkspaceSection.test.tsx \
  client/components/panels/EnvironmentPanel.test.tsx
# Test Files  4 passed (4) · Tests  170 passed (170)   (第二轮,另含 client/components/ChatEditor.test.tsx)
cd packages/core && npx vitest run src/utils/git-branches.test.ts   # 58 passed(上游 gone 的分支列表)

qwen serve + Web Shell 中手动检查:打开一个受信任的 git 工作区,把仓库置于以下各状态后点击侧栏文件夹头部的分支芯片:同步且干净 → 三行都变灰,显示"已是最新"/"无更改"/"无待推送";git reset --hard HEAD~3↓3 · origin/main;再改一个已跟踪文件并新增一个未跟踪文件 → 警告色的 ↓3 · 有未提交更改2 处更改(1 未跟踪)git checkout -b x 并本地提交一次 → 更新项目禁用并显示"无上游分支",推送显示"推送时设置上游";进行中的冲突 rebase → 更新项目与推送禁用并显示"变基中";git checkout --detach → 两者禁用并显示"游离 HEAD";分支上进行中的冲突 merge → 更新项目禁用显示"合并中",推送可点并带警告"合并中";用 -u 推送分支后在远端删除并 git fetch --prune → 更新项目禁用显示"上游分支已不存在",推送显示"推送时设置上游"。切换界面语言到中文查看本地化文案。完整手测计划见 .qwen/e2e-tests/2026-08-28-webshell-branch-picker-action-hints.md

证据(前后对比)

第二轮(1f960ddb36,评审后)用修订后的文案与规则重截了 After 列并新增两个状态;Before 列来自第一轮,评审未改变它。取自真实栈:npm run bundlenode dist/cli.js serve --workspace <夹具仓库>(隔离 QWEN_HOME、伪造 OpenAI 环境变量,不调用模型)→ Playwright 点击侧栏 git 芯片并截取弹窗。两列都是对着同一个 daemon 工作区、同一 git 状态、相隔几秒截取的:"Before" 是同一份 bundle 但 web-shell 前端用 origin/main 的客户端文件重建,"After" 是本分支。每行的 disabled / 提示文本 / data-tone 也从 DOM 读回记入台账(与图片同目录的 dom-ledger.jsonl);Before 的每一行都没有提示且始终可点,包括点击必然失败的那些状态。

仓库状态 Before After
同步且干净 before-sync after-sync
落后 3,干净 before-behind after-behind
落后 3 + 已修改文件 + 未跟踪文件 before-behind-dirty after-behind-dirty
新分支,1 个提交,无上游 before-no-upstream after-no-upstream
领先 1 / 落后 1 before-ahead-behind after-ahead-behind
rebase 进行中且有冲突(HEAD 游离) before-rebase-conflict after-rebase-conflict
游离 HEAD before-detached after-detached
merge 进行中且有冲突,仍在分支上(第二轮新增) (同其它 Before:无提示,全部可点) after-merge-conflict
上游在远端被删且已 prune(第二轮新增) (同其它 Before:无提示,全部可点) after-upstream-gone

中文界面,落后 3 且有未提交更改:

after-behind-dirty-zh

资源:第一轮 wenshao/qwen-code@2d7c61f,第二轮 wenshao/qwen-code@447a954(分支 pr-assets/10397-verify)。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS;单元测试(vitest + jsdom)加上面的 bundle 版 qwen serve + 无头 Chromium(Playwright)实跑。

风险与范围

  • 主要风险或取舍:在 git 会拒绝的状态下,"更新项目"(以及游离 HEAD 时的"推送")现在会被禁用。弹窗打开时自取状态、且在更新的状态与列表矛盾时重拉列表,所以终端里完成的 rebase 会在芯片状态更新(聚焦 / 轮询 / SSE)后自动解除禁用,无需重开;两次刷新之间显示的是最后已知状态。列表与状态的新旧比较用的是 daemon 的 computedAt 与浏览器时钟,远程 daemon 时钟偏差时重拉可能不触发(此时退化为打开时的行为,绝不会错误地放开)。
  • 未验证 / 不在范围内:composer 芯片和环境面板两个入口没有截图(同一组件、同样的 props,只驱动了侧栏芯片)。Windows/Linux 未在本地运行(纯客户端代码,无平台相关路径)。
  • 破坏性变更 / 迁移说明:无。新 prop 均为可选;不传 status 的调用方也有弹窗自身的打开时拉取。upstreamGone 是分支列表上新增的可选字段。

关联 Issue

#10390 相关(脏工作区 pull 解决面板;相互独立、代码相邻)。

The branch picker's Update Project / Commit / Push rows gave no context before
the click: the user could not tell whether there was anything to pull, how many
files a commit would sweep up, or whether a push would create a remote branch.
The daemon already exposes all of this — ahead/behind/upstream on the branch
listing fetched when the picker opens, and dirty counts / in-progress operation
on the polled workspace status held by every trigger chip — but neither reached
the popover.

Add a `status` prop and derive a hint per action from the two sources:

- Hard blockers (merge/rebase/cherry-pick in progress, conflicts, detached
  HEAD, pull without an upstream) disable the row and name the reason, since
  the daemon would reject the request anyway.
- Soft states (up to date, nothing to push, clean tree) only dim the row.
- Informational hints show `↓n · upstream`, `↑n`, and the file count a commit
  would include (calling out untracked files, which `git add -A` sweeps in).
- Warnings flag pulling behind with uncommitted changes and pushing while also
  behind.

The branch listing wins over the polled status for ahead/behind because it is
fetched on open; the popover also asks the caller to refresh the status when it
opens so the dirty counts are not a stale poll. The refresh callback is held in
a ref so an inline handler cannot re-arm the open effect on every render.

Wire the prop from the sidebar workspace header, the composer chip, and the
Environment panel, and add en/zh-CN copy for the new hints.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Re-running the gate at @wenshao's request — same head as the first pass (1f960dd), no new commits since.

  • Template: complete ✓ — the reviewer test plan and risk section remain unusually thorough.
  • Problem: an observed gap, not a theoretical one. The daemon already computes everything these rows need — the chip that opens the popover renders ↑n ↓n and a dirty dot today — yet the Update Project / Commit / Push rows stay silent until after the click. Verifiable in the pre-PR code.
  • Direction: aligned. Web Shell workspace/git UX is an active surface, and showing consequences before the click is the same instinct as the git-safety guardrails other agents have landed. No direct CHANGELOG reference for this exact affordance, but the area is clearly relevant.
  • Size: ~366 production lines (web-shell UI incl. CSS 354, core 10, sdk types 2) vs ~730 test lines and a 58-line E2E plan — under the 500-line awareness bar. Cross-package by necessity: the popover needs one bit the core listing never exposed (git's [gone] upstream state), hence the 10-line parseBranchLines addition. Confidence basis for the core touch: additive optional field, the daemon route passes the listing through as-is, the only consumer is the popover hint, and it's covered by a real-repo test (push, delete the remote branch, fetch --prune, assert upstreamGone).
  • Approach: matches what I'd propose independently — thread the trigger chip's status into the popover, derive per-row hints as a pure function, disable only what git itself refuses, fetch fresh status on open. The "changes, not files" wording for commit counts is a nice correctness catch (porcelain entries ≠ files).
  • Risk: no high-risk path matches; no elevated risk signals.

Moving on to code review. 🔍

中文说明

@wenshao 的请求重新跑门禁 —— 与首轮相同的 head(1f960dd),此后没有新提交。

  • 模板:完整 ✓
  • 问题:是已观测到的缺口,不是理论问题。daemon 已经计算了这些行所需的全部信息——打开弹窗的 chip 今天就会渲染 ↑n ↓n 和脏工作区圆点——但 Update Project / Commit / Push 三行在点击前一直保持沉默。可在改动前的代码中验证。
  • 方向:对齐。Web Shell 的 git UX 是活跃方向;在点击前展示后果与其他 agent 已落地的 git 安全防护是同一思路。
  • 规模:约 366 行生产代码(web-shell UI 含 CSS 354、core 10、sdk 类型 2),约 730 行测试,另加 58 行 E2E 计划——低于 500 行关注线。跨包是必要的:弹窗需要 core 分支列表从未暴露的一个信息(git 的 [gone] 上游状态),因此有 10 行 parseBranchLines 改动。置信依据:纯增量可选字段、路由原样透传、唯一消费者是弹窗提示,且有真实仓库测试覆盖。
  • 方案:与我独立设想的方案一致——把 chip 的状态传入弹窗、用纯函数推导每行提示、只禁用 git 本身会拒绝的操作、打开时拉取新状态。commit 计数用"changes"而非"files"是一个正确的细节(porcelain 条目 ≠ 文件)。
  • 风险:未命中高风险路径,无升级风险信号。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 1f960ddb36b1904e0da442e1342c6014e98f2e8e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Code review

No blockers. The implementation matches my independent proposal and holds up on close reading:

  • Hint derivation is a pure, exported decision table (deriveActionHints) — 13 unit cases cover in-sync/behind/behind+dirty/no-upstream/gone-upstream/ahead/ahead+behind/change counts/in-progress operations/conflicts/detached HEAD, plus listing-vs-status precedence and the v1-status case. Disabling mirrors what the daemon's git routes actually refuse (detached_head 409, no_upstream 400 in workspace-git-branches.ts), so a row is never disabled for an action the backend would accept or enabled-blind to one it won't.
  • Race hygiene is right: request-id guards on both fetches; the listing re-fetch reconciles at most once per status computedAt (a persistent disagreement can't loop); onStatusRefreshed lives in a ref so an inline parent callback can't re-arm the open effect; a status fetched for a previous workspace is dropped on switch.
  • Reuse before new code: deriveStatus / hasComputedTreeSummary are shared with GitBranchIndicator instead of re-implemented, and the v1-status path (no computed tree summary → no commit hint rather than a false "No changes") is handled and tested.
  • The one core change is genuinely minimal: parseBranchLines reads git's own gone tracking state into an optional upstreamGone. Verified the path end to end — the daemon route passes result.local through as-is, DaemonGitBranchInfo gains the matching optional field, and nothing else consumes it. Additive and optional: no other surface changes shape.

One nit, non-blocking: listingFetchedAt (client wall clock) is compared against the daemon's computedAt. Fine while qwen serve and the browser share a host; worth remembering if a remote daemon ever serves the shell — clock skew would only delay or skip a listing re-fetch, so worst case is a stale row, not a wrong one.

Test evidence — the PR's own CI on 1f960dd (not re-run here; the CI path never executes PR code)

All pull_request workflow runs on this SHA completed green. Context for the raw check list: the cancelled check-runs visible on the commit belong to the first CI attempt, superseded by the re-run; Test (macos/windows) and Integration Tests (CLI) are gated off by the workflow on the successful run (skipped, not failed); the still-running review-pr check is bot orchestration (pull_request_target), not PR CI.

Check Conclusion
Qwen Code CI — Test (ubuntu-latest, Node 22.x) ✅ success
Qwen Code CI — Desktop Shell (ubuntu-22.04) ✅ success
Qwen Code CI — Desktop Shell (windows-2022) ✅ success
Qwen Code CI — Integration Tests (no-AK, No Sandbox) ✅ success
Qwen Code CI — web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Qwen Live Host CI — Live Host (macos-latest) ✅ success
Security Checks — Secret scan (TruffleHog) ✅ success
Security Checks — Dependency CVE audit ✅ success
Web-shell Visuals — Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success

The decision table means the suite pins the change — delete the disable logic and the deriveActionHints tests fail. The visuals job additionally renders the real UI against a mock daemon (before/after screenshots are in this thread). What neither covers is the one remaining hop: a live daemon's status flowing through the on-open fetch (wait=1 semantics, computedAt precedence over the chip's last poll). Sandboxed verification would settle that: @qwen-code /verify — the jsdom suite mocks the workspace client, so the real serve→popover wiring is the only claim currently resting on code reading rather than an exercised run.

中文说明

代码审查

无阻塞问题。实现与我独立设想的方案一致,细读之下站得住:

  • 提示推导是纯函数决策表deriveActionHints):13 个用例覆盖 同步/落后/落后且工作区脏/无上游/上游已删除/领先/领先且落后/变更计数/进行中操作/冲突/detached HEAD,以及"列表优先于状态"的取值顺序和 v1 状态。禁用与 daemon git 路由实际拒绝的情形一致(workspace-git-branches.ts 中的 detached_head 409、no_upstream 400),不会出现"后端会执行的操作被置灰"或"后端拒绝的操作盲点击"。
  • 竞态处理正确:两个拉取都有 request-id 防护;列表重取按状态 computedAt 至多一次,持续不一致不会成环;onStatusRefreshed 放在 ref 里,父组件的内联回调不会反复触发打开副作用;切换工作区时丢弃旧状态。
  • 复用优先:与 GitBranchIndicator 共享 deriveStatus / hasComputedTreeSummary;v1 状态(无树摘要)不显示提交提示而非误报"无更改",且有测试。
  • 唯一的 core 改动足够小parseBranchLines 读取 git 自身的 gone 跟踪态。已端到端核实——路由原样透传 result.local,类型只增可选字段,无其他消费者。

一个小问题(不阻塞):listingFetchedAt(浏览器时钟)与 daemon 的 computedAt 比较。qwen serve 与浏览器同机时没有问题;若未来有远端 daemon,时钟偏差最多让列表重取延后或跳过(最坏是行显示过期,不会错)。

测试证据 —— 该 SHA 上 PR 自身的 CI(此处未重跑;CI 路径从不执行 PR 代码)

该 SHA 的全部 pull_request 工作流运行均为绿色。原始检查列表的背景:提交上可见的 cancelled 检查属于被重跑取代的首次尝试;成功运行中 Test (macos/windows)Integration Tests (CLI) 被工作流门禁跳过(非失败);仍在运行的 review-pr 是机器人编排任务(pull_request_target),不属于 PR CI。

决策表意味着测试钉住了改动——删掉禁用逻辑,deriveActionHints 的测试就会失败。visuals 任务还在 mock daemon 下渲染了真实 UI(前后对比截图见本评论区)。两者都未覆盖的只剩最后一跳:真实 daemon 的状态经由打开时的拉取流入(wait=1 语义、computedAt 优先于 chip 上次轮询值)。沙箱验证可以补足:@qwen-code /verify —— jsdom 套件 mock 了 workspace 客户端,真实 serve→弹窗链路是目前唯一只靠代码阅读支撑的断言。

Qwen Code · qwen3.8-max

Reviewed at 1f960ddb36b1904e0da442e1342c6014e98f2e8e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean across every stage; the single reservation is the live serve→popover hop that no jsdom test can reach, and it's a thin one (an identity JSON passthrough plus one wait=1 fetch) — named with its remedy in the review above.

Stepping back: this is the kind of PR the gate should wave through. The problem is observable in the product today — the chip renders the state, the popover hides it — and the fix is the minimal version of itself: one pure derivation function, one genuinely-needed core bit (upstreamGone, which no existing surface exposed), shared normalisation instead of a copy, and roughly twice as much test code as production code. Every prior review suggestion is resolved into the code at this SHA, and a maintainer has already approved it here. My independent proposal and the PR's approach are the same approach; I found no simpler path it missed. In six months this reads as care, not cleverness.

CI is fully green on 1f960dd (all pull_request runs completed success), so approval goes in now, pinned to the reviewed commit.

中文说明

置信度:4/5 —— 各阶段均干净;唯一的保留是 jsdom 测试够不到的"真实 serve→弹窗"链路,而它很薄(一个原样 JSON 透传加一次 wait=1 拉取)——上文已连同补救手段一并指出。

退一步看:这正是门禁应当放行的那种 PR。问题在今天的产品里就能观察到——chip 渲染了状态、弹窗却藏着——而修复是它自身的最小形态:一个纯推导函数、一个确实需要的 core 增量(upstreamGone,此前任何界面都拿不到)、共享的归一化而非复制、以及约为生产代码两倍的测试代码。此前的每条评审建议都已在此 SHA 的代码中落实,且已有一位维护者在此批准。我独立设想的方案与 PR 的方案是同一个方案;我没有找到它漏掉的更简路径。六个月后回看,这份代码体现的是用心,而不是炫技。

1f960dd 上 CI 全绿(所有 pull_request 运行成功结束),因此现在提交批准,并钉在所审提交上。

Qwen Code · qwen3.8-max

Reviewed at 1f960ddb36b1904e0da442e1342c6014e98f2e8e · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 1f960dd. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship — CI landed green after the review. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

Test Plan (not a blocker): client/components/BranchPickerPopover.test.tsxno such file or directory; client/components/sidebar/WorkspaceSection.test.tsxno such file or directory; client/components/panels/EnvironmentPanel.test.tsxno such file or directory.

中文说明

已审查——无阻断问题。 建议见行内评论。

Test Plan(非阻断):client/components/BranchPickerPopover.test.tsxno such file or directory; client/components/sidebar/WorkspaceSection.test.tsxno such file or directory; client/components/panels/EnvironmentPanel.test.tsxno such file or directory

— qwen3-max via Qwen Code /review (v0.22.2)

Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx Outdated
Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx Outdated
Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx Outdated
Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx
Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx Outdated
Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx Outdated
Comment thread packages/web-shell/client/components/BranchPickerPopover.tsx
Comment thread packages/web-shell/client/components/ChatEditor.tsx
Comment thread packages/web-shell/client/components/panels/EnvironmentPanel.tsx
Follow-ups from review of the action hints:

- Push is disabled only on a detached HEAD. A push does not consult the
  index, so an in-progress merge or conflicted entries are shown as a
  warning on an enabled row instead of blocking it; pull keeps blocking in
  those states because `git pull` refuses them. The doc comment now says what
  git actually rejects rather than claiming the daemon would.
- A deleted-and-pruned upstream (`[gone]`) is surfaced end to end: core's
  `parseBranchLines` reads the `gone` tracking state into a new
  `upstreamGone` flag on the branch listing, and the picker treats it as no
  upstream — pull is disabled with "Upstream gone" (plain `git pull` always
  fails there) and push says it will set the upstream (which is what
  `--set-upstream` does).
- The commit hint counts "changes", not "files": the status counters are
  entry-granular (a partially staged file counts twice, an untracked directory
  once), so the old copy overstated or understated the file count.
- The push hint without an upstream says "Sets upstream on push" instead of
  promising a remote branch, since the daemon cannot know whether the push
  remote resolves or HEAD has commits.
- The popover fetches its own working-tree status on open, so all three entry
  points (sidebar chip, composer chip, Environment panel) see fresh counters
  instead of their caller's last poll; the sidebar receives it back through
  `onStatusRefreshed` to keep its chip in step. The newer of the caller's
  status and the fetched one wins.
- While open, a status the daemon computed after the listing that disagrees
  with it on upstream/detached/ahead/behind triggers one listing re-fetch, so
  the rows follow a terminal-side `--unset-upstream` or checkout instead of
  the snapshot from open time.
- The counter normalisation and the "enriched fields were computed" predicate
  are now exported from GitBranchIndicator and shared with the picker rather
  than re-implemented.

Tests cover the decision-table changes, the dimmed (muted) row styling, the
"changes" wording for a partially staged file, the on-open status fetch and
reconciliation re-fetch, the gone-upstream listing in core, and the status
pass-through at the ChatEditor and EnvironmentPanel call sites.
@wenshao

wenshao commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 28, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Reviewed at head 1f960dd: all 9 suggestion threads resolved with author replies, and the follow-up fix commit genuinely implements their substance — disabling is now reserved for what git itself refuses (pull during merge/rebase/cherry-pick, unmerged entries, detached HEAD, unusable upstream; push --set-upstream only detached) while conflicts/in-progress ops are warnings on enabled rows; upstreamGone parsing covers the deleted-upstream [gone] state (git-branches.ts + tests); freshness now keys on computedAt instead of an unconditional listing override; deriveStatus/hasComputedTreeSummary are reused instead of re-normalized. Heavy test coverage (+600 in BranchPickerPopover.test.tsx plus call-site wiring tests) and web-shell E2E Smoke green on the exact head.

@wenshao

wenshao commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen /triage

@wenshao

wenshao commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 28, 2026 17:37
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 278 passed · 0 failed · 278 total

Flakiness gate: ✅ 5 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:278 通过 · 0 失败 · 278 总计

抖动门:✅ 5 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR 10397 verification report

Verdict: findings — 278/278 scripted assertions passed (0 unexpected failures); one Suggestion-level design inconsistency and one Nice-to-have coverage gap, both measured. Verified head: 1f960ddb36b1904e0da442e1342c6014e98f2e8e (merge-ref checkout df1f8b6e5f, base 4ef780cbfa).

中文摘要
  • 结论findings。核心主张经 A/B 与真实 git 预言机证明成立;278 项脚本断言全部通过,无意外失败。发现 1 个 Suggestion 级设计不一致 + 1 个 Nice-to-have 覆盖缺口,均非阻塞。
  • A/B 结论:core 的 parseBranchLines 在 head 上对真实 [gone] 上游输出 upstreamGone: true,base 构建在同构夹具上完全没有该字段(14/14 检查通过);base 的弹窗组件没有任何 hint/禁用面。11 个真实仓库状态下,deriveActionHints 的禁用/提示/色调与 PR 文档决策表逐项一致,且"UI 禁用 ⇒ git 真实拒绝"全部成立(19/19)。
  • Findings:(1) Suggestion——在 behind-only 与 ahead+behind 状态下,daemon 实际执行的 git push 会被 git 以 non-fast-forward 确定性拒绝,但 Push 行仍保持可点(灰显 "Nothing to push" / 警告 "update first"),与 PR 自述规则"disabling follows what git itself refuses"不一致;点击后错误可见、无数据丢失。给出了经测的最小修复(detached || (!s.operation && hasUpstream === true && behind > 0),PR 套件 170/170 保持绿)。(2) Nice-to-have——newerStatus 的同时间戳 tie-break 无测试固定(变异后 170/170 仍绿)。
  • 未覆盖:浅克隆(depth 2)无法逐 commit 归因;composer 芯片/环境面板入口仅有 prop 透传的单测,未端到端驱动;未走真实 daemon HTTP 传输(路由为透传,已读码确认);未重跑 Playwright 手测与整仓 typecheck/lint(PR 自身 CI 覆盖);revert/bisect 操作状态未进矩阵(i18n 键与同分支逻辑已断言)。

Central claim and A/B

Central claim: the branch picker's Update/Commit/Push rows show git-state hints whose disabling is faithful to what git itself refuses (pull: no usable upstream / gone upstream / detached / in-progress operation / conflicts; push: detached HEAD only).

A/B proof of the load-bearing data path (harness 3, evidence/01-ab-core-upstream-gone.png): identical real fixtures (bare remote + clone, gone upstream produced by push --delete + fetch --prune) driven through the HEAD core dist vs a base (HEAD^1) core dist rebuilt in a scratch worktree.

Cell Oracle head base
A: gone-upstream listing real git for-each-ref output upstreamGone: true, upstream still named field absent
B: workspace status real git status --branch header ## feat...origin/feat [gone] hasUpstream: true (status cannot express gone) same
C: UI input shape field census gone ≠ healthy (the flag) gone indistinguishable from healthy tracking
D: component surface source census hints + disabled wiring no status prop, no hint element, disabled={!!busyAction} only
E: fixture equivalence git pull/push verdicts identical across arms identical

14/14 checks pass; base cannot even see the gone state, head can — the core change is load-bearing.

Behavioral oracle (harness 2, evidence/02-fidelity-matrix-hints-vs-git.png): 11 real repo states; listing + status captured by the real core functions (fetchGitBranches, getGitWorkingTreeStatus, with computedAt stamped exactly as WorkspaceGitState.materialize does), fed to the real deriveActionHints with the real EN translator; the same states then handed to the real gitPull/gitPush (the daemon's exact calls) for the verdicts. 19/19 assertions pass, including:

  • soundness: every disabled row is a state git actually refused (11/11 states cross-checked);
  • completeness: pull disabled in all six states git unconditionally refuses (no-upstream, gone, detached, rebase/merge/cherry-pick conflict);
  • push stays enabled and succeeds during a conflicted merge while on a branch (isolated fixture: ahead 1, behind 0 — real git push OK), confirming "push does not consult the index";
  • gone-upstream: real status header prints [gone] yet hasUpstream: true, so listingContradictsStatus's upstreamComparable guard is exactly what prevents a pointless re-fetch — and it holds on real data;
  • i18n: all 10 new hint keys + 5 git.operation.* + git.conflicted/git.detached resolve in en and zh-CN with no key passthrough and no silent EN fallback.

Reviewer Test Plan walkthrough

Plan step Result
vitest run 4 web-shell files ran: 4 files / 170 tests passed — matches the PR's claimed counts exactly (evidence/04-gates-head-green.png)
vitest run core git-branches.test.ts ran: 58 passed — matches claim
Manual states (sync / behind / behind+dirty / no-upstream / ahead+behind / rebase-conflict / detached / merge-conflict / gone) all nine covered by the fidelity matrix with real git verdicts, plus cherry-pick added; hint text/tone/disabled match the plan's copy in every state
zh-CN copy asserted at key level for all new keys (no EN fallback); not screenshotted

Extra gate: GitBranchIndicator.test.tsx (the shared-helper refactor's home) — 11 passed.

Mutation matrix (vacuity + pinning)

# Mutation Suite Result Classification
M1 revert git-branches.ts to base core killed 1/58 — expected undefined to be true on the gone assertion new core test not vacuous
M2 drop && !upstreamGone web-shell killed 1/27 — gone test flips to upToDate (evidence/03-mutation-m2-killed.png) clause load-bearing (positive control)
M3 drop upstreamComparable guard web-shell killed 1/27 — gone-listing disagreement test guard load-bearing
M4 pushDisabled = detached || operation web-shell killed 2/27 — both "only warns on push" tests enabled-during-operation rule pinned
M5 newerStatus tie-break >=> web-shell survived 170/170 coverage gap (Finding 2)
M6 pushDisabled += hasUpstream && behind > 0 (candidate fix) web-shell killed 1/170 — but only the merge-op case (which carries behind: 1) behind/diverged axis unpinned except via the operation case
M6' refined: detached || (!s.operation && hasUpstream === true && behind > 0) web-shell green 170/170 measured suggested fix for Finding 1

Unmutated control runs were green before and after every mutation; every killed mutant failed in the file that was mutated, with the intended assertion.

Findings

F1 (Suggestion) — Push stays enabled in states where the daemon's own git push is deterministically refused

The PR's doc comment states "Disabling is reserved for what git itself refuses", but in two measured state classes the push row is enabled while the exact command the daemon runs (git push, since an upstream exists) is refused:

state push row real git push
behind-only (behind-clean, behind-dirty) enabled, dimmed "Nothing to push" ! [rejected] main -> main (non-fast-forward)
diverged (ahead-behind, cherry-pick-conflict) enabled, warning "↑1 ↓1 · update first" ! [rejected] … (non-fast-forward)

A push that moves the remote backward (behind-only) or divergently is refused unconditionally — unlike the divergent-pull refusal, which is config-dependent (fatal: Need to specify how to reconcile divergent branches only when pull.rebase/pull.ff are unset) and therefore defensibly left enabled with an info hint. The push refusal needs no config and the PR's own rule would disable the row; as shipped, the click fails in the status bar (loud, no data loss, and the hint text does tell the user to update first — hence Suggestion, not blocking).

Minimal suggested fix (measured)
const pushDisabled =
  detached || (!s.operation && hasUpstream === true && behind > 0);

Measured in a scratch copy: PR suite 170/170 green (the !s.operation carve-out preserves the pinned "only warns on push" operation case, which the naive variant without it breaks 1/170). The behind/diverged push cells of the fidelity matrix flip from REFUSED✗row-on to disabled. Note the behind-only row would then read dimmed "Nothing to push" and disabled, which is consistent; if the author prefers the current warn-and-let-the-error-surface behaviour, the doc comment should say so (the stated rule as written does not match the implementation).

F2 (Nice to have) — newerStatus equal-computedAt tie-break is unpinned

M5 (>=>) survived 170/170: nothing asserts which status wins when the caller's and the popover's own fetch carry identical stamps. Current behaviour (fetched wins ties) is reasonable; this is completeness reporting, not a merge condition.

Not covered

  • Per-commit attribution: the checkout is depth 2; only 1f960ddb36 is locally reachable while the snapshot lists two commits. All evidence is for the aggregate HEAD^1..HEAD diff.
  • Daemon HTTP transport: harnesses drove the real core functions directly; the branches route serializes result.local verbatim (read workspace-git-branches.ts), so upstreamGone passes through, but no live qwen serve round was made.
  • Composer chip / Environment panel entry points: prop pass-through asserted by their unit tests (gates); not driven end-to-end (same component/props as the sidebar path, per the PR's own scoping).
  • Playwright manual plan, Windows, zh screenshots: not re-run; i18n asserted at key level instead.
  • Repo-wide typecheck/lint: not run here (PR's CI covers); the base-tree core rebuild emitted pre-existing type noise unrelated to this PR.
  • revert/bisect operations: not in the fidelity matrix; their i18n keys resolve and they flow through the same s.operation blocker branch.
  • The working tree carried a pre-existing deletion (.qwen/e2e-tests/2026-08-28-webshell-branch-picker-action-hints.md) at session start; left untouched.

Methodology

Environment: CI verify container (node:22, git 2.39.5), merge-ref checkout. Harness 1 (harness/harness-1-gitstates.mjs) built 11 scratch repos with real bare remotes and drove the real core dist (head, and base rebuilt in tmp/base-tree, removed after) for listings, statuses, and pull/push verdicts; harness 2 (vitest, run inside packages/web-shell for CSS-module handling, removed after) ran the real deriveActionHints/listingContradictsStatus/getTranslator over the captured states; harness 3 compared arms. Captures via scripts/verify-capture.mjs. Raw logs: logs/states-head.json, logs/states-base.json, logs/harness2-results.json, logs/ab-results.json. Assertion counts: 19 (harness 2) + 14 (harness 3) + 6 (mutation rows M1–M6') + 239 (gates: 170+58+11) = 278.

Flakiness gate log

rounds=5 files=5 skipped=0
file packages/core/src/utils/git-branches.test.ts: (cd packages/core) npx --no-install vitest run ./src/utils/git-branches.test.ts
file packages/web-shell/client/components/BranchPickerPopover.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/BranchPickerPopover.test.tsx
file packages/web-shell/client/components/ChatEditor.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/ChatEditor.test.tsx
file packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/panels/EnvironmentPanel.test.tsx
file packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/sidebar/WorkspaceSection.test.tsx


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/core/src/utils/git-branches.test.ts: PPPPP
  packages/web-shell/client/components/BranchPickerPopover.test.tsx: PPPPP
  packages/web-shell/client/components/ChatEditor.test.tsx: PPPPP
  packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: PPPPP
  packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: PPPPP

verdict: pass
summary: 5 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/core/src/utils/git-branches.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/BranchPickerPopover.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/ChatEditor.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: P (exit 0)
round 2 · packages/core/src/utils/git-branches.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/BranchPickerPopover.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/ChatEditor.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: P (exit 0)
round 3 · packages/core/src/utils/git-branches.test.ts: P (exit 0)
round 3 · packages/web-shell/client/components/BranchPickerPopover.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/ChatEditor.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: P (exit 0)
round 3 · packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: P (exit 0)
round 4 · packages/core/src/utils/git-branches.test.ts: P (exit 0)
round 4 · packages/web-shell/client/components/BranchPickerPopover.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/ChatEditor.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: P (exit 0)
round 4 · packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: P (exit 0)
round 5 · packages/core/src/utils/git-branches.test.ts: P (exit 0)
round 5 · packages/web-shell/client/components/BranchPickerPopover.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/ChatEditor.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/panels/EnvironmentPanel.test.tsx: P (exit 0)
round 5 · packages/web-shell/client/components/sidebar/WorkspaceSection.test.tsx: P (exit 0)

Evidence images

01-ab-core-upstream-gone

02-fidelity-matrix-hints-vs-git

03-mutation-m2-killed

04-gates-head-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Aug 28, 2026
Merged via the queue into QwenLM:main with commit f63369e Aug 28, 2026
348 of 377 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants