feat(web-shell): worktree-isolated sessions for parallel tasks - #7221
Conversation
|
Thanks for the PR! Clean re-run at the current head. Template looks good ✓ — all required sections present with a thorough design doc. Problem: Real user need, not theoretical. Multiple sessions in the same workspace share a working tree and can trample each other's file edits and git state. The CLI already has worktree infrastructure ( Direction: Aligned. The design doc ( Size: 624 production lines across 6 packages (acp-bridge, cli, core, sdk-typescript, web-shell, webui), plus 185 test lines and a 428-line design doc. The core change is minimal — 7 lines in Approach: Scope feels right for a full-stack feature. The daemon route handles validation, worktree creation, session relocation via Moving on to code review. 🔍 中文说明感谢贡献!在当前 head 上重新审查。 模板完整 ✓ — 所有必填部分齐全,附有详细的设计文档。 问题: 真实的用户需求,非理论性问题。同一 workspace 中的多个 session 共享工作目录,可能互相踩踏文件编辑和 git 状态。CLI 已有 worktree 基础设施( 方向: 对齐。设计文档结构清晰,阶段划分明确。本 PR 交付 Phase 1(daemon + SDK)和 Phase 2(Web Shell UI),合理地推迟了生命周期管理(Phase 3)。方案复用 规模: 624 行生产代码跨 6 个包,另有 185 行测试代码和 428 行设计文档。核心改动极小—— 方案: 范围合理。daemon 路由处理验证、worktree 创建、通过 进入代码审查 🔍 — Qwen Code · qwen3.7-max 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 changes against the PR base. 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'd add an optional Comparison: The PR matches this approach closely. A few observations: The daemon route ( The The core change ( The No critical blockers found. No AGENTS.md violations. Code follows project conventions (ESM, strict TS, collocated tests, kebab-case files). Tests: 5 new daemon route tests (success, non-git 400, invalid slug 400, non-object 400, creation failure 500) plus updated Real-Scenario TestingStarted the daemon from this PR's build against a test git repo and exercised the worktree API end-to-end: All scenarios behave as documented. Worktree creation, relocation, git status override, path traversal protection, error handling, auto-slug, and backward compatibility all verified. 中文说明代码审查独立方案: 在 对比: PR 方案与独立方案高度一致。daemon 路由(158 行)处理完整生命周期:输入验证、worktree 创建、 测试: 5 个新 daemon 路由测试 + 更新的 Web Shell 测试,全部 781 个测试通过。 真实场景测试从本 PR 构建启动 daemon,对测试 git 仓库执行端到端 worktree API 测试:worktree 创建、session 重定位、git 状态覆盖、路径遍历防护、错误处理、自动 slug 生成、向后兼容性——全部验证通过。 — Qwen Code · qwen3.7-max Reviewed at |
Review — worktree-isolated sessions (verified at c93a8d2)Read the full stack (route → bridge → SDK → web-shell) at the head commit, plus the pre-existing worktree infrastructure this builds on ( 1. Isolation does not survive load/resume — but the UI keeps claiming it (High)The restore path never rehydrates worktree state. After a daemon restart, loading the session goes through The metadata splits inconsistently at that point: the sidebar list keeps the ⑂ badge (sidecar enrichment survives Suggested fix: in the load route (or bridge restore), read the sidecar, rehydrate 2.
|
|
Confidence: 3/5 — clean review, all tests green, verified end-to-end at this head. The 500+ production line threshold (624 lines across 6 packages) triggers the maintainer-awareness cap per policy, though the author is a maintainer and the core change is only 7 lines. The PR does what it sets out to do — worktree-isolated sessions for the Web Shell — and does it cleanly. The design doc is thorough, the implementation reuses existing infrastructure ( My independent proposal matched the PR's approach almost exactly. The one thing I'd note is that The deferred items (worktree cleanup on session end, merge-back workflow, worktree-scoped Deferring to @wenshao for the maintainer-awareness sign-off on the 500+ production line threshold — the review itself found no blockers. 中文说明置信度:3/5 — 审查干净,所有测试通过,在此 head 上端到端验证。500+ 生产行阈值(624 行跨 6 个包)触发维护者知悉上限(政策要求),尽管作者本身即维护者,且核心改动仅 7 行。 PR 干净地实现了目标——Web Shell 的 worktree 隔离 session。设计文档详尽,实现复用已有基础设施,daemon 路由事务性地处理完整生命周期。安全姿态扎实:slug 验证防止路径遍历,git 状态 独立方案与 PR 方案几乎完全一致。唯一值得注意的是 推迟项(session 结束时 worktree 清理、merge-back 工作流、worktree 范围 转交 @wenshao 进行 500+ 生产行阈值的维护者知悉确认——审查本身未发现阻塞问题。 — Qwen Code · qwen3.7-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Two blockers before this can land: (1) the ?cwd= parameter in workspace-git.ts has a confirmed path traversal — startsWith allows sibling-directory escape and ../ traversal; needs path.resolve() + trailing-separator containment. (2) Three unit tests are failing, including the PR's own new worktree test (fake bridge doesn't propagate worktree to the response) and two existing GET /session/:id/status tests broken by the route rewrite. The feature itself works end-to-end — verified live — but the security fix and test fixes are needed first. See detailed notes above. 🙏
c93a8d2 to
46704c6
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — full build and npm test timed out at 120 seconds; Integration Tests (CLI, No Sandbox) was skipped in CI and did not run locally. Not reviewed: coverage — could not read the agents' transcripts (the CLI did not export QWEN_CODE_PROJECT_DIR / QWEN_CODE_SESSION_ID, so this run cannot find the harness's record of what its agents did), so this run cannot show that any of the diff was read. Not reviewed: verification — could not check that Step 4 and Step 5 ran (the CLI did not export QWEN_CODE_PROJECT_DIR / QWEN_CODE_SESSION_ID, so this run cannot find the harness's record of what its agents did).
— GPT-5 via Qwen Code /review
Add support for creating sessions in isolated git worktrees from the Web Shell, enabling multiple tasks to run in parallel within the same workspace without polluting the main working directory. Daemon: - POST /session accepts optional worktree param, creates worktree via GitWorktreeService, relocates session via changeSessionCwd - Worktree metadata persisted in SessionEntry, BridgeSessionSummary, and sidecar file (<sessionId>.worktree.json) for daemon restart recovery - GET /workspaces/:workspace/git supports ?cwd= for worktree-scoped git status queries (path.resolve + containment check) SDK: - CreateSessionRequest/DaemonSession/DaemonSessionSummary gain worktree field; DaemonSessionClient exposes worktree getter - WorkspaceDaemonClient.workspaceGit() accepts optional cwd param Web Shell: - Workspace branch pill dropdown offers 'New Worktree Task' (git repos only) with purple GitForkIcon and description - Git chip turns purple with GitForkIcon for worktree sessions - Session list shows inline ⑂ badge for worktree sessions - Empty-state welcome badge explains worktree isolation - Git status queries target worktree path, not workspace root - session_cwd_changed event filtered from chat transcript Design doc: docs/design/2026-07-19-webshell-worktree-sessions.md
03c8a90 to
159002d
Compare
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not reviewed: coverage — could not read the agents' transcripts (the CLI did not export QWEN_CODE_PROJECT_DIR / QWEN_CODE_SESSION_ID, so this run cannot find the harness's record of what its agents did), so this run cannot show that any of the diff was read. Not reviewed: verification — could not check that Step 4 and Step 5 ran (the CLI did not export QWEN_CODE_PROJECT_DIR / QWEN_CODE_SESSION_ID, so this run cannot find the harness's record of what its agents did). [Critical] Worktree isolation still begins after ACP session initialization. The bridge creates the channel, config, and cwd-bound MCP transports with the registered main workspace before changeSessionCwd runs. — Failure scenario: a project MCP server or configuration resolved relative to cwd remains bound to the main checkout even after the session is labelled as isolated. Spawn with a validated initial worktree cwd before config/MCP initialization, while retaining the main workspace only as the runtime owner. [Critical] Registered workspaces that are repository subdirectories still lose their selected scope. The route constructs GitWorktreeService(workspaceCwd) directly and relocates to the worktree root. — Failure scenario: /repo/packages/app creates a full-repository checkout under that subdirectory and the session starts at the repository root, so relative tools operate outside packages/app. Resolve the repository root, preserve the relative subdirectory, and use the corresponding path inside the worktree. [Critical] Restart load/resume still ignores the worktree sidecar, and archived listing still reads only the active sidecar path. — Failure scenario: after a daemon restart, a worktree-badged session loads with the main workspace cwd and can edit the main checkout; after archive, even the badge disappears because the sidecar moved under chats/archive. Validate and pass archive-aware sidecar cwd/metadata through bridge restore before ACP initialization. [Critical] Worktree creation still has no daemon capability contract. — Failure scenario: a new SDK or Web Shell sends worktree to an older daemon; Express ignores the unknown field and returns a normal main-checkout session that the client accepts as isolated. Advertise and require a worktree_sessions capability before exposing or accepting this request, and fail closed when absent.
— GPT-5 via Qwen Code /review
Re-review — verified at ae148f2 (work commit 159002d)Re-read the updated stack and re-checked each round-1 finding against the code as it stands. Also ran the suites locally at this head: Fixed
Still open
New this round
Merge stanceThe security blocker is resolved and the tests are green. What keeps me from approving is unchanged from round 1: findings 1 and 4 both end with the UI asserting isolation the session doesn't have — the one failure mode this feature must not exhibit. Fix 4 (small) and either fix or honestly rescope 1 (including the test-plan step 7 claim), and the rest can follow up. |
ae148f2 to
9e969ad
Compare
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— GPT-5 via Qwen Code /review
ytahdn
left a comment
There was a problem hiding this comment.
Not reviewed: Agent 0: Issue fidelity & root-cause ownership — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 0: Issue fidelity & root-cause ownership — its prompt was built, but no agent on record was launched with it.
Not reviewed: Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.
Not reviewed: verification — a verifier ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and the posted findings cannot be counted as verified against it.
[Critical] session.ts !res.writable path: client disconnect during spawn orphans worktree directory and branch — the disconnect-reap block kills the session but does not call removeUserWorktree. The catch-block rollback is unreachable from the return inside try. No sidecar is written, so the UI has no record. Repeated triggers accumulate orphaned directories that the agent-* stale cleanup won't collect.
[Critical] App.test.tsx: mock workspaceClient lacks sessionStatus method — App.tsx:1309 calls workspace.client.sessionStatus(sid) in a useEffect, but the test mock only has workspaceByCwd. This throws TypeError synchronously, causing 128 of 136 tests to fail during effect mounting.
— qwen3.7-max via Qwen Code /review
9e969ad to
5c8e610
Compare
R3/R4 fixes (5c8e610)Critical fixes:
Suggestions acknowledged:
|
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— GPT-5 via Qwen Code /review
5c8e610 to
f14ed8a
Compare
ytahdn
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: chunk 1, chunk 2, chunk 3, chunk 4, chunk 5, chunk 6 — no agent reported covering these; nobody read them. Not reviewed: every dimension — none of the 11 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
wenshao
left a comment
There was a problem hiding this comment.
— qwen3.8-max-preview via Qwen Code /review
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— GPT-5 via Qwen Code /review
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— GPT-5 via Qwen Code /review
f14ed8a to
9147152
Compare
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— GPT-5 via Qwen Code /review
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read. [Critical] The timeout behavior is not hypothetical: changeSessionCwd assigns the raw cdPromise settlement to entry.promptQueue and explicitly says the timeout is caller-facing only; withTimeout is only Promise.race([p, timeoutP]) and never aborts p. Therefore its caller can catch BridgeTimeoutError while the ACP sessionCd operation continues. Removing the worktree in that catch while killSession returned false can race the surviving session's late cd into the deleted path. Independently, if ACP eventually rejects, the live attached entry remains in the main checkout while its summary still advertises the removed worktree. The rollback still needs to settle/cancel relocation and transfer live-session ownership before filesystem cleanup.
— GPT-5 via Qwen Code /review
qqqys
left a comment
There was a problem hiding this comment.
Reviewed. Not reviewed: full review fan-out — unchanged head; this monitoring pass inspected the exact CI failure and PR replies only. Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.
— GPT-5 via Qwen Code /review
|
@qwen-code /triage |
|
Thanks for the thorough re-review. All confirmed fixes noted. Restart limitation: Now explicitly documented in the PR body's Risk & Scope section as the first Phase 3 item, with the specific restore mechanism described (read sidecar → re-issue Non-blocking notes acknowledged:
|
Description re-check (body edit @ 16:35Z)Verified the new Risk & Scope bullet against the code at Two leftover inconsistencies in the description itself:
No new code since 🤖 Generated with Claude Code — Claude Fable 5 |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
wenshao
left a comment
There was a problem hiding this comment.
changeSessionCwd's timeout is caller-facing only — bridge.ts ties entry.promptQueue to the RAW cd settlement ("Timeout is caller-facing only"), so a timed-out cd can still complete in the child after this catch runs. If a concurrent attach makes killSession return false and the raw cd then completes, the surviving session's cwd becomes the worktree path that removeUserWorktree just force-deleted, and files the agent writes there are lost. The thread reply's "if the Promise rejects, the cd did not complete" does not hold for the Promise.race timeout path — the race rejects on the timer while the cd is still in flight. Gate the removal on killed === true (mirroring removeSession), or distinguish a timeout from a definitive rejection before deleting. [Critical] Five previously filed blockers remain present in the code at this head (author-acknowledged, tracked for Phase 3): (1) registered workspaces that are repository subdirectories lose their selected scope — relocation targets the worktree root, so relative tools operate at repo scope instead of the selected packages/app scope; (2) session config and cwd-bound MCP transports initialize against the main workspace before changeSessionCwd runs, so a cwd-relative project MCP server stays bound to the main checkout while the session is labelled isolated; (3) load/resume never reads the worktree sidecar (no readWorktreeSession on the restore routes), so after a daemon restart a ⑂-badged session loads with the main workspace cwd and can edit the main checkout; (4) no daemon capability contract — an older daemon silently ignores the worktree field and returns a main-checkout session the client accepts as isolated (no fail-closed preflight); (5) the /diff slash command (App.tsx ~4565) is not gated for worktree sessions and opens the main checkout's dialog, while the footer-click guard at App.tsx ~7273 is.
— qwen3.8-max-preview via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
d3f173b to
738b6aa
Compare
wenshao
left a comment
There was a problem hiding this comment.
killSession returns false (session.ts:1421–1426; existing thread comment 3611002925). The cd-failure rollback is unchanged at this head: when killSession({ requireZeroAttaches: true }) returns false (a concurrent restoreSession attach — scope-agnostic, bridge.ts ~3817 — or the .catch(() => false) on throw), the worktree is deleted but the surviving entry keeps entry.worktree, and GET /session/:id/status keeps advertising isolation the session doesn't have. The sessionScope = 'thread' forcing prevents spawn-time coalescing but does not prevent attach-by-id via the session listing during the spawn→cd window. Clear the entry's worktree metadata on the surviving-session branch (needs a small bridge method), or defer setting it until the cd succeeds. [Critical] Re-checked, still standing (narrow trigger): the cd timeout race in the same rollback (existing thread 3610684625 / review 4730879851, and the previous review body 4731162403). changeSessionCwd's timeout is caller-facing only — bridge.ts ties entry.promptQueue to the RAW cd settlement ("Timeout is caller-facing only") — so a timed-out cd can still complete in the child after this catch has force-deleted the worktree; with a concurrent attach keeping killSession false, the surviving session's cwd becomes the deleted path and files written there are lost. Gate the removal on killed === true (mirroring removeSession), or distinguish a timeout from a definitive rejection before deleting. [Critical] Re-checked, still standing (author-acknowledged, tracked for Phase 3; the restart limitation is now declared in the PR body's Risk & Scope): (1) subdirectory workspace scope is lost after relocation (session.ts:1224 — relocation targets the worktree root); (2) session config and cwd-bound MCP transports initialize against the main workspace before changeSessionCwd runs; (3) load/resume never reads the worktree sidecar, so after a daemon restart a ⑂-badged session runs in the main checkout (the listing badge is now archive-state aware and survives archiving — that sub-part is fixed at this head); (4) no daemon capability contract — an older daemon silently ignores the worktree field and returns a main-checkout session the client accepts as isolated; (5) the /diff slash command (App.tsx ~4565) is not gated for worktree sessions while the footer click is.
— qwen3.8-max-preview via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
— qwen3.8-max-preview via Qwen Code /review
|
@qwen-code /triage |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
ytahdn
left a comment
There was a problem hiding this comment.
LGTM. 高质量实现,增量 review 无 Critical 缺陷。
核心确认:
POST /sessionworktree 创建 +changeSessionCwd重定位链路正确,spawn/cd 失败均有完整回滚?cwd=路径安全检查(realpathSync+path.relativecontainment check)到位- 强制
sessionScope: 'thread'防止 worktree session 被 attach 到普通 session enrichWorktreeSidecars从 sidecar 文件恢复元数据,daemon 重启后徽章存活- Web Shell 侧 git chip/session 列表/welcome badge/dropdown 全部覆盖
- 4 个 server 测试 + WorkspaceSection 测试更新
一个小观察(非阻塞):worktree session 禁用了 workspace 下拉菜单的 'Changes' 入口,用户无法从 UI 查看 worktree 内 diff(需走 /diff 命令),建议后续增量支持。
— qwen3.7-plus via Qwen Code /review
|
@qwen-code /triage |
Local end-to-end verification ✅ (with one restart-persistence gap
|
| Area | Check | Result |
|---|---|---|
| Create | POST /session {worktree:{slug:"alpha"}} → 200 + {slug,path,branch} |
✅ worktree-alpha |
| Create | worktree dir created under <repo>/.qwen/worktrees/alpha, real checkout, .qwen-session marker = sessionId |
✅ |
| Create | git worktree list registers branch worktree-alpha; sidecar <sid>.worktree.json written |
✅ |
| Isolation | edit + git add inside worktree → main workspace src/app.ts unchanged, main tree clean |
✅ |
| Git scoping | GET /workspaces/:ws/git → main; ?cwd=<worktree> → worktree-alpha (staged:3) |
✅ |
| Security | ?cwd=/etc, /tmp, .., sibling dir → all fall back to workspace root (containment holds) |
✅ |
| Validation | worktree=[] / "yes" → 400 invalid_worktree |
✅ |
| Validation | slug ../escape, "", a/b → 400 worktree_invalid_slug |
✅ |
| Validation | non-git workspace → 400 worktree_not_git_repo; {} auto-slug → 200 |
✅ |
| Scope | worktree session forced to sessionScope:'thread'; changeSessionCwd relocates the child (transcript records cwd=<worktree>, gitBranch=worktree-alpha) |
✅ |
| UI (live) | purple ⑂ sidebar badge on worktree sessions only · "New worktree task" in the workspace pill (git repos only) · composer chip → purple data-worktree="true" with the worktree branch when loaded |
✅ |
The one gap — restart persistence (Test-Plan step 7 / Risk note) ⚠️
The PR states: "Restart the daemon → the ⑂ icon persists in the session list (via sidecar file)" and "The sidebar ⑂ badge persists via sidecar enrichment."
Observed: after a daemon restart the entire worktree session row disappears from the list — not just the purple chip. In a clean single-session workspace the list returns total=0 after restart, even though the sidecar file is still on disk.
Root cause: changeSessionCwd relocates the child, so the persisted transcript records cwd = <repo>/.qwen/worktrees/<slug>. Every listing path in SessionService filters with sessionBelongsToCurrentProject(sessionId, firstRecord.cwd) (packages/core/src/services/sessionService.ts:420 and siblings). But:
getProjectHash(<repo>) = fa2c7d96…
getProjectHash(<repo>/.qwen/worktrees/alpha) = 8c28c00f… ← different project
so the worktree session fails the membership check; after a restart there is no live runtime-status fallback either, so it is filtered out before the new sidecar-enrichment loop in session-list.ts ever runs. The sidecar the PR writes is therefore never consulted for the case it was written for — the enrichment is effectively dead code on the restart path. (This is also worse than the Risk note, which says only the git chip/cwd degrade and the ⑂ badge survives.)
Suggested direction (non-blocking): enrich/keep worktree sessions using the sidecar or an index rather than gating on recordCwd project membership — e.g. treat a session whose sidecar originalCwd matches the workspace root as belonging to the project and skip the recordCwd re-check for it.
Minor observations (non-blocking)
- The daemon-created sidecar always writes
originalBranch:""andoriginalHeadCommit:""(hard-coded empty inPOST /session), unlike theEnterWorktreeToolpath that populates them. Harmless in the web-shell today, butWorktreeExitDialog's commit-count would read "unknown" for these sessions. - Creating a worktree makes the parent repo report
.qwen/as untracked (untracked:1in the git chip) unless.qwen/is git-ignored.
Verdict
Isolation, ?cwd= containment, and every validation gate behave correctly on a real daemon, and all three UI indicators render from live data. The single functional gap is that the advertised restart-persistence of the ⑂ mark doesn't work — the row vanishes from the list. Since full restart-isolation is already scoped to Phase 3, either fix the list-drop (small) or soften the Test-Plan/Risk wording so it doesn't claim the badge persists.
Reproduction
Real-daemon E2E harness output (GitWorktreeService not mocked):
== 1. POST /session {worktree:{slug:alpha}} ==
PASS response carries worktree.branch (worktree-alpha)
PASS worktree dir created on disk
PASS git registered the worktree branch
PASS sidecar <sid>.worktree.json written
== 2. isolation: edit inside worktree, main stays clean ==
PASS main workspace src/app.ts unchanged (export const x=1)
== 3. GET /git ?cwd scoping ==
PASS no cwd -> main branch (main)
PASS cwd=worktree -> worktree branch (worktree-alpha)
PASS cwd=/etc containment -> falls back to main (main)
== 4. validation gates ==
PASS worktree=[] -> invalid_worktree
PASS slug=../escape -> worktree_invalid_slug
== 5. drive one turn so the session persists a transcript ==
PASS worktree session listed BEFORE restart (1)
== 6. RESTART daemon, re-list ==
sidecar file still on disk: yes
sessions listed after restart: total=0 with-worktree=0 (expected by PR: with-worktree=1)
FAIL worktree session dropped after restart
==== SUMMARY: 11 passed, 1 failed ====
🇨🇳 中文版
本地端到端验证 ✅(存在一处重启持久化缺陷 ⚠️ )
我构建了本分支,并针对真实 qwen serve daemon + 真实 git 仓库做了验证——没有 mock GitWorktreeService(server.test.ts 新增的单测把它整体 mock 了,因此真实的 worktree 创建、磁盘隔离、sidecar 持久化和重启恢复此前都未被验证)。UI 部分在无头浏览器中针对 daemon 提供的生产 web-shell/dist 检查,渲染的是真实数据。
结论: 核心功能扎实且安全。唯一缺陷是重启持久化——daemon 重启后,worktree 会话会从会话列表中整行消失,这与测试计划第 7 步的说明相矛盾。
环境: feat/webshell-worktree-sessions @ 738b6aa(已合并 main)· Linux · Node 22 · npm ci && npm run build · 真实 packages/cli/dist daemon(127.0.0.1)+ packages/web-shell/dist · 用 mock OpenAI 驱动一次对话以持久化 transcript。
通过项——真实 daemon 11/12 项检查
- 创建:
POST /session {worktree:{slug}}→200且返回{slug,path,branch};在<repo>/.qwen/worktrees/<slug>下创建真实 worktree(含.qwen-session标记 = sessionId);git worktree list注册worktree-<slug>分支;写入 sidecar<sid>.worktree.json。✅ - 隔离: 在 worktree 内修改并
git add→ 主 workspace 的src/app.ts未变、工作树干净。✅ - git 作用域:
GET /workspaces/:ws/git→main;?cwd=<worktree>→worktree-<slug>(staged:3)。✅ - 安全(路径包含校验):
?cwd=/etc、/tmp、..、同级目录 → 全部回退到 workspace 根目录,未逃逸。✅ - 校验门:
worktree=[]/字符串 →400 invalid_worktree;slug../escape、空串、a/b→400 worktree_invalid_slug;非 git 仓库 →400 worktree_not_git_repo;{}自动 slug →200。✅ - 会话作用域: worktree 会话强制
sessionScope:'thread';changeSessionCwd把子进程重定位到 worktree(transcript 记录cwd=<worktree>、gitBranch=worktree-<slug>)。✅ - UI(实时数据): 侧边栏 worktree 会话显示紫色 ⑂ 徽标(普通会话无)· workspace pill 下拉出现"New worktree task"(仅 git 仓库)· 载入 worktree 会话后 composer git chip 变紫、
data-worktree="true"并显示 worktree 分支名。✅
唯一缺陷——重启持久化(测试计划第 7 步 / 风险说明)⚠️
PR 声称:"重启 daemon → ⑂ 图标通过 sidecar 文件在会话列表中保留"、"侧边栏 ⑂ 徽标通过 sidecar enrichment 保留"。
实测: daemon 重启后,worktree 会话整行从列表消失——不仅仅是紫色 chip。在只有一个会话的干净 workspace 中,重启后列表返回 total=0,尽管 sidecar 文件仍在磁盘上。
根因: changeSessionCwd 重定位了子进程,因此持久化的 transcript 记录 cwd = <repo>/.qwen/worktrees/<slug>。SessionService 的所有列表路径都会用 sessionBelongsToCurrentProject(sessionId, firstRecord.cwd) 过滤(packages/core/src/services/sessionService.ts:420 等)。但 getProjectHash(<repo>)(fa2c7d96…)≠ getProjectHash(<worktree>)(8c28c00f…),属于不同 project;重启后又没有存活的 runtime-status 兜底,因此该会话在本 PR 新增的 session-list.ts sidecar-enrichment 循环运行之前就被过滤掉了。也就是说,本 PR 写入的 sidecar 在它本该服务的重启场景里从未被读取——enrichment 在重启路径上形同废码。(这也比风险说明更严重:说明称仅 git chip/cwd 退化、⑂ 徽标会保留。)
修复方向(非阻塞): 用 sidecar 或独立索引来保留/补全 worktree 会话,而不是以 recordCwd 的 project 归属为前置条件——例如:当某会话的 sidecar originalCwd 等于 workspace 根目录时,即视为属于该 project,并对其跳过 recordCwd 复核。
次要观察(非阻塞)
- daemon 创建的 sidecar 总是写
originalBranch:""、originalHeadCommit:""(POST /session里硬编码为空),与EnterWorktreeTool路径不同。目前 web-shell 无害,但WorktreeExitDialog的提交计数对这些会话会显示"未知"。 - 创建 worktree 会让父仓库把
.qwen/报为未跟踪(git chip 中untracked:1),除非.qwen/被 gitignore。
结论
隔离、?cwd= 包含校验、所有校验门在真实 daemon 上均正确,三处 UI 标识也都基于实时数据渲染。唯一功能缺陷是宣称的 ⑂ 标记重启持久化不生效——整行从列表消失。鉴于完整的重启隔离已划入 Phase 3,建议要么修掉这个"列表丢失"(改动很小),要么弱化测试计划/风险说明的措辞,不再声称徽标会保留。
Verified locally on Linux against a real qwen serve daemon + real git repo (no GitWorktreeService mocking) and the production web-shell in a headless browser.
|
Thanks for the thorough real-daemon E2E verification. The root cause analysis is spot-on:
Fix: #7262 — adds a third membership check in Minor observations acknowledged:
|
…wenLM#7375) An empty visual preview means one of two opposite things: the change genuinely moves no pixel, or no scenario renders the UI it touches. The bot printed the same green check for both, so the second — a coverage gap, where the preview literally cannot see the feature — read as a clean bill of health. That has now happened three times (QwenLM#7035 primary label, QwenLM#7221 worktree badge, QwenLM#7365 empty-state toggle), each caught only because a maintainer noticed the missing image and asked. The signal to tell them apart was already there and unused: the render workflow only runs when the web-shell client or webui source changed, so an empty preview is by construction "UI code changed, nothing rendered differently". When no view changed, look at which files the PR touched. If any are render-shaping (.tsx / .css / .svg under the rendered surface, excluding test and scenario code), list them and say the result is ambiguous, with a pointer to where a scenario goes. Otherwise keep the green check — a logic-only PR with no visual delta is expected, and prompting there would train everyone to ignore the prompt when it matters. The path list comes from the PR files API in the privileged publish job, which never checks out PR code; if that call fails the comment falls back to the current wording. Co-authored-by: wenshao <wenshao@example.com>
|
Released in v0.20.1. |


What this PR does
Adds support for creating sessions in isolated git worktrees from the Web Shell, enabling multiple tasks to run in parallel within the same workspace without polluting the main working directory.
When a user creates a worktree session, the daemon creates a git worktree under
<repoRoot>/.qwen/worktrees/<slug>, spawns the session, and relocates its working directory into the worktree via the existingchangeSessionCwdbridge method. The Web Shell shows a purple ⑂ (GitForkIcon) indicator in the git chip, session list, and workspace dropdown to distinguish worktree sessions from normal ones.Key changes across the stack:
POST /sessionaccepts an optionalworktreeparameter, creates the worktree viaGitWorktreeService, and passes metadata throughBridgeSpawnRequest→SessionEntry→BridgeSessionSummary. Worktree info is persisted to a sidecar file (<sessionId>.worktree.json) and survives daemon restarts.CreateSessionRequestgainsworktree?: { slug?: string },DaemonSessionandDaemonSessionSummarygainworktree?: DaemonWorktreeInfo,DaemonSessionClientexposes aworktreegetter, andWorkspaceDaemonClient.workspaceGit()accepts an optionalcwdparameter for worktree-scoped git status queries.Why it's needed
Users working on multiple tasks in the same repository need isolation — one session's file edits,
git add, orgit checkoutshouldn't interfere with another's. Git worktrees provide this natively, but the workflow was manual (git worktree add+cd). This PR makes it a one-click experience in the Web Shell.Reviewer Test Plan
How to verify
npm run build && npm run dev:daemonEvidence (Before & After)
Before: No worktree isolation — all sessions share the same working directory.
After: Worktree sessions are visually distinguished with purple ⑂ indicators and operate in isolated git worktrees.
Tested on
Environment
npm run dev:daemonon macOS, Chrome browser.Risk & Scope
restoreWorktreeContext), and the purple git chip does not restore until the session is re-created. The sidebar ⑂ badge persists via sidecar enrichment. Restoring full isolation on load (read sidecar → re-issuechangeSessionCwd→ populateentry.worktree) is the first Phase 3 item. Also deferred: worktree cleanup on session end, merge-back workflow, worktree-scoped/diffdialog (the Changes dialog is disabled for worktree sessions since it would show the main workspace's diff).worktreeparameter is optional; omitting it preserves existing behavior.Linked Issues
Design doc:
docs/design/2026-07-19-webshell-worktree-sessions.md(included in this PR)中文说明
这个 PR 做了什么
在 Web Shell 中支持创建 git worktree 隔离会话,使同一 workspace 中的多个任务可以并行运行而不会污染主工作目录。
用户创建 worktree 会话时,daemon 在
<repoRoot>/.qwen/worktrees/<slug>下创建 git worktree,启动会话后通过已有的changeSessionCwd方法将工作目录切换到 worktree。Web Shell 通过紫色 ⑂ (GitForkIcon) 标识区分 worktree 会话和普通会话,包括 git chip、会话列表和 workspace 下拉菜单。主要改动:
POST /session接受可选的worktree参数,通过GitWorktreeService创建 worktree,元数据通过BridgeSpawnRequest→SessionEntry→BridgeSessionSummary传递。worktree 信息持久化到 sidecar 文件,daemon 重启后不丢失。CreateSessionRequest新增worktree字段,DaemonSession和DaemonSessionSummary新增worktree字段,DaemonSessionClient暴露worktreegetter,workspaceGit()支持可选cwd参数查询 worktree 的 git 状态。为什么需要
用户在同一仓库中处理多个任务时需要隔离——一个会话的文件编辑、
git add或git checkout不应影响另一个。Git worktree 原生提供这种隔离,但之前需要手动操作。本 PR 将其变为 Web Shell 中的一键操作。风险与范围
/diff对话框。worktree参数可选,不传则行为不变。