perf(web-shell): paint the composer git chip before git status completes#7680
Conversation
New sessions gated the chip on a full `git status --porcelain` subprocess behind GET /workspaces/:ws/git, so the branch chip appeared hundreds of milliseconds (worst case seconds) after the composer was ready. The daemon now keeps a per-workspace last-known summary with in-flight dedup and a 2s background-refresh throttle: the default GET returns the cached status (branch-only on a cold start) immediately and recomputes in the background, publishing git_status_changed over SSE only on a delta, while ?wait=1 keeps the previous blocking semantics. The composer fetches both paths concurrently — the fresh GET also covers the no-session state, which has no per-session SSE stream — so the branch paints in ~3ms and the counters land when the computation finishes. The sidebar keeps wait:true since it has no SSE fill-in path.
E2E test report (protocol-level, real daemon)Environment: macOS,
Notes:
|
|
Thanks for the PR! Template looks good ✓ Problem: observed and measured — the composer git chip waits for a full Direction: aligned — this is a straightforward UX latency fix for the Web Shell composer, squarely within the project's scope. The stale-while-revalidate pattern with SSE push-on-change is the right architecture for this kind of "show something fast, fill in details later" problem. CHANGELOG: no direct reference, but the area (Web Shell git chip responsiveness) is clearly relevant. Size: cross-package (cli, sdk-typescript, webui, web-shell) — 299 production logic lines, 551 test lines, 249 docs lines. Under the 500-line threshold; no maintainer escalation needed. Approach: the scope feels right. Every layer change (daemon cache → SDK event → webui mapper → web-shell consumer) is necessary for the end-to-end behavior. The design doc's rejected alternatives (TTL-only cache, daemon pre-warm, frontend-only dedup) are well-reasoned. The P2 frontend request dedup was correctly scoped out after the daemon-side in-flight dedup made it redundant. No unrelated changes or drive-by refactors. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测且有测量数据——composer git chip 必须等待完整的 方向:对齐——这是 Web Shell composer 的直接 UX 延迟修复,完全在项目范围内。stale-while-revalidate + SSE 变化推送是"先快速展示、再补齐细节"这类问题的正确架构。CHANGELOG:无直接引用,但领域(Web Shell git chip 响应速度)明显相关。 规模:跨包(cli、sdk-typescript、webui、web-shell)——299 行生产逻辑、551 行测试、249 行文档。低于 500 行阈值,无需维护者升级。 方案:范围合理。每层改动(daemon 缓存 → SDK 事件 → webui mapper → web-shell 消费)对端到端行为都是必要的。设计文档中被否决的备选方案(纯 TTL 缓存、daemon 预热、前端去重)论证充分。P2 前端请求去重在 daemon 端 in-flight 去重使其多余后被正确裁剪。无无关改动或顺手重构。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 2 render-shaping files:
Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
Code ReviewIndependent proposal: I would have done exactly what this PR does — add a per-workspace cached Comparison with the diff: the PR's approach matches my independent proposal at every layer. No simpler path was missed. No critical findings. Specific observations:
CI Test EvidenceAll substantive checks passed on the reviewed commit. Windows and macOS unit tests were skipped (fork PR restriction). No failures.
Real-Scenario TestingN/A — the change is in the Web Shell (browser-based UI), not the terminal UI. The daemon-side behavior is covered by the protocol-level E2E evidence in the PR description (cold-cache fast request 3.3ms vs 25.5ms blocking, SSE push on change, in-flight dedup verified). A maintainer can trigger the isolated 中文说明代码审查独立方案: 我的方案与本 PR 完全一致——在 与 diff 对比: PR 方案在每一层都与我的独立方案一致,未发现更简路径。 无关键发现。具体观察:
CI 测试证据审查提交上所有实质性检查通过。Windows 和 macOS 单测被跳过(fork PR 限制)。无失败。 真实场景测试不适用——改动在 Web Shell(浏览器 UI),非终端 UI。daemon 端行为由 PR 描述中的协议级 E2E 证据覆盖(冷缓存 fast 请求 3.3ms vs 阻塞 25.5ms、SSE 变化推送、in-flight 去重验证)。如需终端界面回归覆盖,维护者可触发隔离的 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 5/5 — clean across every stage; would merge without hesitation. Re-run at the current head after five rounds of review feedback were addressed. The PR is in better shape than at the initial triage: the EPIPE-unsafe The design is the right one — stale-while-revalidate with in-flight dedup and SSE push-on-change is exactly how you decouple a fast branch read from a slow working-tree scan. The implementation mirrors the existing Every call site in the codebase was audited and migrated: the session provider reads only the branch (kept live by the watcher), the composer gets the fast+fresh double fetch, the sidebar explicitly opts into blocking semantics, and worktree sessions keep their direct uncached computation. The test suite covers cold cache, dedup, throttle, wait semantics, failure-keeps-cache, dispose-suppresses-publish, workspace guard, double fetch, SSE mirror, and the worktree skip — 17 daemon state tests, 8 route tests, plus SDK, webui, and web-shell tests. CI is fully green on the reviewed commit (ubuntu unit, web-shell visuals, serve A/B, web-shell E2E smoke, daemon E2E, precheck, Java matrix). The stale 中文说明置信度:5/5 —— 每个阶段都干净,毫不犹豫可以合并。 在处理完五轮评审反馈后于当前 head 重新审查。PR 状态优于初次 triage:EPIPE 不安全的 设计正确——stale-while-revalidate + in-flight 去重 + SSE 变化推送正是将快速分支读取与慢速工作区扫描解耦的方式。实现在每一层(daemon → SDK → webui → web-shell)都镜像了既有的 代码库中每个调用点都已审计并迁移:session provider 只读分支(由 watcher 保持实时),composer 获得 fast+fresh 双发,侧栏显式选择阻塞语义,worktree 会话保持直接无缓存计算。测试套件覆盖冷缓存、去重、节流、wait 语义、失败保留缓存、dispose 抑制推送、workspace 守卫、双发、SSE 镜像和 worktree 跳过——17 个 daemon state 测试、8 个路由测试,加上 SDK、webui 和 web-shell 测试。 CI 在审查提交上全绿(ubuntu 单测、web-shell 视觉、serve A/B、web-shell E2E smoke、daemon E2E、precheck、Java 矩阵)。autofix bot 的过期 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
|
@qwen- code /takeover |
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /takeover |
|
🤝 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. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
Code Review — #7680
|
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed1. Worktree sessions skip
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressedrc:3647718417 —
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here.
中文说明
已审查——无阻断问题。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。
— qwen3.7-max via Qwen Code /review
| case 'git_status_changed': { | ||
| const data = getRecord(event.data); | ||
| const workspaceCwd = getString(data, 'workspaceCwd'); | ||
| setConnection((current) => | ||
| workspaceCwd && workspaceCwd !== current.workspaceCwd | ||
| ? current | ||
| : { | ||
| ...current, | ||
| gitStatus: data as unknown as DaemonWorkspaceGitStatus, | ||
| }, | ||
| ); |
There was a problem hiding this comment.
[Suggestion] The git_status_changed handler stores the raw SSE record via as unknown as DaemonWorkspaceGitStatus blind cast, while the adjacent git_branch_changed handler (three lines above) extracts each field with getString. The inconsistency suppresses all structural validation for this event.
Failure scenario: If a future daemon change alters the git_status_changed payload shape (renames a field, drops a required field like branch), the client silently stores the malformed object into connection.gitStatus. The App.tsx SSE mirror effect and any future consumer receive the broken object with no runtime error. The git_branch_changed handler would catch the same class of bug at extraction time.
| case 'git_status_changed': { | |
| const data = getRecord(event.data); | |
| const workspaceCwd = getString(data, 'workspaceCwd'); | |
| setConnection((current) => | |
| workspaceCwd && workspaceCwd !== current.workspaceCwd | |
| ? current | |
| : { | |
| ...current, | |
| gitStatus: data as unknown as DaemonWorkspaceGitStatus, | |
| }, | |
| ); | |
| case 'git_status_changed': { | |
| const data = getRecord(event.data); | |
| const workspaceCwd = getString(data, 'workspaceCwd'); | |
| const branch = getString(data, 'branch'); | |
| setConnection((current) => { | |
| if (workspaceCwd && workspaceCwd !== current.workspaceCwd) return current; | |
| const v = typeof data?.v === 'number' ? data.v : 2; | |
| return { | |
| ...current, | |
| gitStatus: { | |
| v, | |
| workspaceCwd: workspaceCwd ?? current.workspaceCwd ?? '', | |
| branch: branch ?? null, | |
| ...(typeof data?.staged === 'number' ? { staged: data.staged } : {}), | |
| ...(typeof data?.unstaged === 'number' ? { unstaged: data.unstaged } : {}), | |
| ...(typeof data?.untracked === 'number' ? { untracked: data.untracked } : {}), | |
| ...(typeof data?.ahead === 'number' ? { ahead: data.ahead } : {}), | |
| ...(typeof data?.behind === 'number' ? { behind: data.behind } : {}), | |
| ...(typeof data?.stashCount === 'number' ? { stashCount: data.stashCount } : {}), | |
| ...(data?.computedAt != null ? { computedAt: data.computedAt } : {}), | |
| } as DaemonWorkspaceGitStatus, | |
| }; | |
| }); | |
| break; | |
| } |
中文说明
git_status_changed 处理器通过 as unknown as DaemonWorkspaceGitStatus 盲转型存储原始 SSE 记录,而相邻的 git_branch_changed 处理器使用 getString 逐字段提取。不一致导致此事件缺少结构验证。如果未来 daemon 端修改了 payload 结构(重命名或丢弃字段),客户端会静默存储格式错误的对象。建议显式提取字段,与 git_branch_changed 的模式保持一致。
— qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed[rc:3648048233] Test for
|
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed[rc:3648536511] Critical —
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Agent 7 ran and confirmed all 16 workspaces build and all 5 PR-changed test files pass (505 tests), but the harness transcript could not verify its brief was read; verdict is capped at COMMENT per compose-review. Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
— qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Agent 1a: Line-by-line correctness, Agent 2: Security, Agent 3: Code quality, Agent 4: Performance & efficiency, Agent 5: Test coverage, Agent 6a: Undirected audit — attacker mindset, Agent 6b: Undirected audit — 3 AM oncall mindset, Agent 6c: Undirected audit — six-months-later maintainer, Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
— qwen3.7-max via Qwen Code /review
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Agent 4: Performance & efficiency, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
— qwen3.7-max via Qwen Code /review
…atcher dispose guard (QwenLM#7680)
|
🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed[rc:3648954297] Fresh-path
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline.
中文说明
已审查。 建议见行内评论。
— qwen3.7-max via Qwen Code /review
|
🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed[rc:3649082877] Assert
|
doudouOUC
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4 — no agent reported covering these; nobody read them. Not reviewed: every dimension — none of the 12 required agents is on record as launched with a prompt this skill built, so this diff was reviewed, if at all, from prompts the run wrote for itself: no record shows the severity bar, the finding format or this project's own rules reaching an agent. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.
— qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action takenThe two review comments on this round contain no actionable code findings:
There are no inline comments, no issue-level comments, no failed checks, and no still-red checks to address. No changes were made. 中文说明未采取任何操作本轮的两条评审评论均不包含可操作的代码发现:
没有行内评论、没有 issue 级评论、没有失败的检查、也没有持续红色的检查需要处理。未做任何更改。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
ytahdn
left a comment
There was a problem hiding this comment.
LGTM. Solid stale-while-revalidate design: in-flight dedup, 2s throttle, publish-only-on-change, disposed guard. Sidebar correctly opts into blocking semantics. No blocking findings after OCR + manual audit.
What this PR does
Makes the git branch chip in the Web Shell composer appear almost instantly when opening a new session. The daemon's git-status route now answers from a per-workspace cached summary — recomputed in the background with in-flight dedup and a short throttle, and pushed to clients over SSE only when the summary actually changes — while a new opt-in
waitquery flag preserves the previous blocking behavior for callers that need it. The composer issues both a fast (cached) and a fresh (blocking) request on each refresh; the two share a single daemon-sidegit statuscomputation, so the branch name paints in single-digit milliseconds and the dirty/ahead/behind counters land as soon as the working-tree scan finishes. The fresh request also covers the no-session state (before the first prompt), where no per-session event stream exists and an SSE push alone would never arrive. Sidebar workspace chips explicitly opt into the blocking behavior, and worktree sessions keep their direct, uncached computation.Why it's needed
Previously, creating a new session left the composer without a git chip for hundreds of milliseconds to several seconds: every status request synchronously spawned
git status --porcelainbehind the HTTP response (no caching, no dedup), and the chip rendered nothing until the full round trip completed. The branch name alone is available in milliseconds from a lightweight HEAD read with a live reflog watcher, but the response held it hostage to the heavy working-tree computation. On top of that, the same route was hit twice concurrently on connect (session metadata prefetch and the composer effect), spawning two identical git subprocesses.Reviewer Test Plan
How to verify
waitsemantics, failure keeps cache, dispose suppresses publish), routewaitflag passthrough, SDK query-string building, webui event mapping (workspace guard), and web-shell composer fast+fresh double fetch plus SSE mirror — all added or updated and green.npm run buildandnpm run typecheckare clean.git_status_changedevent follows on the session event stream; a second fast request serves the enriched cache immediately; touching an untracked file triggers one new push, and a quiet period triggers none.Evidence (Before & After)
Before: new-session composer shows no git chip until a full
git status --porcelainround trip completes (hundreds of ms on real repositories; up to the 5s subprocess timeout in the worst case). After (protocol timings, clean small repo): fast request returns{v: 2, workspaceCwd, branch: "main"}with no enriched fields in 3.3ms, vs 25.5ms for the same request with the blocking flag — and the enriched summary follows via the fresh request / SSE push. In the target scenario (new session in a large working tree) the perceived chip latency drops from the fullgit statusduration to one local HTTP round trip.Tested on
Environment (optional)
Local daemon from the built bundle (
node packages/cli/dist/index.js serve --workspace <repo> --no-web --safe-mode) with curl timing and a raw SSE subscription; unit tests via per-package vitest runs.Risk & Scope
git statusfails keeps the previous cache and stays silent.waitquery flag and thegit_status_changedevent are purely additive, and older SDK clients silently drop the unknown event.Linked Issues
N/A — raised from direct usage feedback.
中文说明
让 Web Shell 输入框里的 git 分支 chip 在新建会话时几乎立即出现。daemon 的 git status 路由改为从每个工作区的缓存摘要直接应答——后台异步重算(带 in-flight 去重和短节流),且仅在摘要真正变化时通过 SSE 推送给客户端;同时新增可选的
wait查询参数,为需要的调用方保留原来的阻塞语义。composer 每次刷新时并发发出 fast(读缓存)和 fresh(阻塞等新鲜结果)两个请求,二者在 daemon 端共享同一次git status计算,因此分支名在几毫秒内渲染出来,dirty/ahead/behind 等计数器在工作区扫描完成后立即补齐。fresh 请求还覆盖了无会话状态(首个 prompt 之前)——此时不存在按会话的事件流,仅靠 SSE 推送永远到不了。侧栏工作区 chip 显式选择保留阻塞语义;worktree 会话保持直接计算、不进缓存。此前,新建会话后 composer 的 git chip 要等几百毫秒到数秒才出现:每个 status 请求都在 HTTP 响应前同步 spawn
git status --porcelain(无缓存、无去重),chip 在整个往返完成前完全不渲染。分支名本身通过轻量的 HEAD 读取加 reflog watcher 毫秒级可得,却被重量的工作区计算拖住。此外,连接建立时同一路由会被并发打两次(会话元数据预取和 composer effect),spawn 两个相同的 git 子进程。验证方式:单测覆盖 daemon git state(缓存命中/未命中、in-flight 去重、节流、仅变化才推送、wait 语义、失败保留缓存、dispose 后不再推送)、路由 wait 参数透传、SDK query 拼接、webui 事件映射(workspace 守卫)、web-shell composer 双发与 SSE 镜像,全部新增或更新并通过;
npm run build与npm run typecheck干净。协议级 E2E(真实 daemon,数据见下方 E2E 评论):冷缓存 fast 请求约 3ms 返回 branch-only,阻塞请求约 25ms;会话事件流随后收到git_status_changed;第二次 fast 请求立即返回 enrich 后的缓存;新增未跟踪文件触发一次新推送,静置期不推送。手动验证:打开 Web Shell 新建会话,chip 应立即显示分支名,计数器随后补齐(大仓库更明显);侧栏文件夹 chip 计数器不变;切换分支仍能通过既有的分支事件更新 chip。主要风险:默认(不带参数)路由现在可能返回 last-known 数据(冷缓存时 branch-only,或落后一次后台刷新的摘要),而非总是阻塞等待新鲜计算。已逐一审计所有现存调用方:session provider 只读分支(由 reflog watcher 保持实时,不会过期),composer 是本次优化的服务对象,侧栏已显式改用阻塞参数。
git status失败时保留旧缓存且不发事件。未验证/范围外:浏览器级视觉断言(由协议级 E2E 计时和单测覆盖);worktree 会话有意保持无缓存直接计算(避免每个 worktree 泄漏一个 fs watcher);无 daemon 启动预热。无破坏性变更:响应形状不变(enriched 字段本就可选),wait参数与git_status_changed事件均为纯新增,旧版 SDK 客户端会静默丢弃未知事件。