Repository navigation
feat(web-shell): restore remote workspace add flow - #12085
Conversation
Superseded verification noteThe previous report targeted Current-head UI evidence for |
A Local add resumes through the same full-page navigation as a remote one, so the shells shown while the daemon's capabilities are still unknown, or when it cannot register workspaces at all, were announcing a local add as a remote one. Both now use the neutral dialog title, and the connection gate no longer offers to cancel a remote add on the local path. The typed-path autocomplete also stopped holding its previous results while the next lookup was pending. Clearing the list is only needed in browse mode, where the list is the primary control, so it is scoped there again.
Real-daemon browser verificationVerified Driven and observed:
A screenshot was captured at every step (location choice, both browsers, the gate, the empty-directory state, the added workspace, the badge control, Change computer, Cancel, Daemon Status) and the run is reproducible from the two daemon commands above; the observations quoted here are the rendered UI text, not a reading of the source. Not covered: macOS Chromium only, loopback ports standing in for two hosts (no real SSH tunnel or LAN path, so TLS and mixed-content are untested), a single saved computer, and no long-lived remote session or re-auth after a daemon restart. Review follow-upsSix suggestions came back from triage, none blocking. Two are fixed here, and the rest are answered against the current head.
The E2E specs cited in the body are still not part of the diff, so they cannot be re-run by a reviewer; the run above is the record that a real daemon drives the whole flow. |
|
Fixes for the copy and autocomplete findings are pushed in |
The sidebar's Add workspace action now renders in every standalone shell regardless of the daemon's registration capability, so an accessible-name substring match on "Add" resolves to two buttons and the extension archive smoke test fails strict mode. Match the dialog's button exactly, as the rest of the suite does.
|
|
Extension-manager smoke fix pushed in |
E2E test report — remote workspace add flowThe behavioural claim in this PR now has a committed test instead of a throwaway config: cd packages/web-shell
npx playwright test --config playwright.config.ts client/e2e/web-shell.remote-workspace-add.spec.ts --project=chromiumChromium on macOS. If a dev server from another checkout already owns the default port, pass a free one ( Two mock daemons are installed on one page — the page's own origin with a local workspace at
Test infrastructure added for this ( |
|
Scope ledger: the earliest reviewed head (3ccc4e8) was 17 files / 1,246 changed lines. The current PR diff at 0696655 is 31 files / 2,756 changed lines against base 2bd62fb. All current implementation files map to the verified-connections catalog, folder-source workspace flow, remote-host indicator, or the bounded F1 abandoned-hand-over fix. This is substantive round 4; scope-correction mode remains active, and N1/N4 stay outside this PR. |
chiga0
left a comment
There was a problem hiding this comment.
No blocking findings.
Approval blockers: none.
Scope: Source files only — packages/web-shell/client/ (all 28 changed files). Docs reviewed for consistency with code. E2E spec reviewed but not executed (no working tree). Windows behavior not exercised (author notes same).
Checked (static):
- Token paths —
startRemoteWorkspaceAddstripstokenandhashfrom the return URL before writing tosessionStorage.leaveRemoteWorkspaceAddstrips them again on the return leg.buildDaemonConnectionUrldeletesaddRemoteWorkspacealongside the existing params. No token reacheslocalStorageor a URL. - Return URL origin guard —
leaveRemoteWorkspaceAddrejects a stored URL whose.origindiffers fromwindow.location.origin. Since the web shell is a SPA served at one origin (daemon target is a query param, not a navigation), this guard covers both the local-reload and cross-origin remote paths correctly. remote-connections.tstrust model — stored values are re-validated viagetAllowedDaemonOrigin(origin) === originat every read. The newisRemoteConnectionKnownbypass inmain.tsxis intentional: only origins the user explicitly connected to skip the unfamiliar-target gate, and they pass the validator on each read.remoteWorkspaceAddActiveReflifecycle — traced all six mutation sites. Each is co-located with asetShowAddWorkspace*setter that forces a re-render;handleSubmit/onClosecloses the success path correctly.- Change-computer return path —
leaveRemoteWorkspaceAdd(true)appends?addRemoteWorkspace=connectto the validated return URL, callsconfirmDaemonTarget(savedDaemonOrigin)before navigating, then clearsRETURN_URL_KEY. App reads the param on load, opens the chooser dialog,clearRemoteWorkspaceAddStepremoves it. Round-trip is clean. - i18n —
sidebar.addRemoteWorkspaceremoved; dialog titles use the neutralsidebar.addWorkspaceTitle; cancel label updated in both locales; all new keys present in EN and ZH. navigateToDaemonAPI — additive optional third parameter, all existing callers unaffected.
Cross-check against qwen-code-ci-bot's earlier review:
Bot filed two non-blocking items: (1) copy inconsistency in dialog/sidebar labels — resolved in commits 5 and 6, confirmed fixed at head; (2) remoteWorkspaceAddActiveRef fragility — I traced the same paths and agree: correct today, but the pattern requires pairing every future ref mutation with a state setter. No bot finding survived unrebutted.
Unreviewed dimensions: execution tier not run (no working tree); Windows not exercised.
Reviewed with AI assistance.
Round 2 — findings disposition at
|
| # | Finding at 3ccc4e85 |
Disposition | Where |
|---|---|---|---|
| 1 | Copy still announces a remote add on the local path (sidebar action, both resumed DialogShell titles, connection-gate cancel) |
Fixed. sidebar.addRemoteWorkspace is gone from client/; the sidebar action uses sidebar.addWorkspace and both resumed shells use sidebar.addWorkspaceTitle; the gate reads "Cancel adding workspace" / "取消添加工作区". |
11d6bacd |
| 2 | Design docs still describe the operation as remote-only | Fixed. Both languages now describe the choice as this computer or a previously verified remote connection, and the acceptance criteria match; the badge is documented too. | e666df98, 67337b7f |
| 3 | The central claim is not pinned by any committed test | Fixed. New client/e2e/web-shell.remote-workspace-add.spec.ts — four @smoke scenarios in the repository's own suite, plus the mock-daemon support they need. Separate E2E report below. |
b045a7de |
| 4 | Only the positive standalone assertion exists | Fixed. New App.test.tsx case asserts data-has-add-workspace === 'false' for an embedded shell whose daemon lacks the capability. |
b045a7de |
| 5a | dismissible={!submitting} removes Escape and close with no way out if the mutation hangs |
Not a defect. workspaceActions.addWorkspace is wrapped in withActionTimeout(…, 'Add workspace timed out') (client/daemon/workspace/actions.ts:1135, 30 s default in client/daemon/timing.ts), so submit always settles and the dialog becomes dismissible again. |
no change |
| 5b | The suggestion effect blanked the typed-path autocomplete on every keystroke | Fixed. Scoped to browse mode, where the list is the primary control. | 11d6bacd |
| 6 | remote-workspace-add.ts has no typeof window guard |
Fixed. getRemoteWorkspaceAddStep() returns undefined outside a document, with a jsdom unit test that fails when the guard is removed. |
74fb08e |
Why the new tests are load-bearing rather than decorative
Each one was checked by breaking the code under it, not only by watching it pass:
- Forcing
remoteWorkspaceAddActiveReftofalsefails all four E2E scenarios; forcing the resumed dialog'sinitialPathtoundefinedfails the first one — the dialog appears but never lists the chosen computer's folders. - The embedded-shell assertion fails when the sidebar entry is passed unconditionally.
- The
typeof windowunit test fails with the guard removed.
The mock daemon's two new routes were matched to the real contracts instead of guessed: GET /workspace-path-suggestions answers DaemonWorkspacePathSuggestions (kind/dir/sep/suggestions[]/truncated, client/daemon/workspace/types.ts:400), which is exactly what AddWorkspaceDialog reads, and POST /workspaces answers DaemonAddWorkspaceResult (types.ts:351) while updating the capability snapshot so the post-add refresh sees the new workspace. An OPTIONS preflight branch I wrote first was deleted after a request log proved Playwright's route interception never issues one for these calls — dead code.
Gates at b045a7de
- Changed-file ESLint,
prettier --check,tsc --noEmit, andnpm run build: clean. - Web Shell unit suite: 8443 passed (the run needs
dist/, so build first forbuild-artifact.test.ts). One earlier run on this same head failed a focus assertion incomponents/composer/AddMenu.test.tsx; it passes in isolation and a full re-run of the same head is green, so it is a load-sensitive flake rather than something this diff reaches. - Web Shell E2E, full suite on a free port: 158 passed, 4 failed — all four in
web-shell.live-state-poll-interval.spec.ts. They fail identically with my changes reverted, so they are pre-existing and local to this machine; the same specs are green in CI.
One red check, and it comes from the base
Lint & Static failed on b045a7de inside Check lint gate freshness, before any linter ran. That step fails when main changes a gate-defining file after the branch last incorporated it, and it names eslint.config.js and eslint.legacy-core-barrel-imports.mjs from 6c962b3719c6 (#11163, merged today). The same lane was green on b910bfb4 about an hour earlier, so this is main moving, not the two new commits. Neither change touches this diff: one adds the generated web-shell confusables table to the ignore list, the other drops a packages/cli/src/serve/routes/ entry from the barrel-import allowlist, and the branch touches no packages/cli file and neither path. The lane's own remedy is to merge or rebase main into the branch, and main merges cleanly into it, so that stays a deliberate separate step rather than a side effect of this round.
Recorded but not acted on
- The three JSX branches keyed on
remoteWorkspaceAddActiveRef.currentand the two near-duplicateDialogShellblocks. I agree this is the least pleasant part of the change, and collapsing it would also remove the pairing requirement between the ref write and the state write. It is a refactor of code this PR just introduced, so it would widen the diff again; recording it here rather than doing it inside a verification commit. - The design questions from the first stage (
navigateToDaemon's third parameter, whether the "no cross-host continuation" constraint is formally retired on feat(serve): supported remote folders — connect clients to a remote daemon and manage its workspaces/sessions #11475, and whether the two ways of adding a workspace eventually merge). These are issue-thread decisions rather than defects in the diff, and none blocks merge. - The widened sidebar entry. On a standalone shell whose daemon lacks
dynamic_workspace_registration, the action is now visible and the Local branch reaches the "This server does not support adding workspaces." state wheremainhid the entry entirely. That is the deliberate trade-off for offering the Local option; the embedded-shell behaviour stays pinned by the newApp.test.tsxcase, and the ambiguous smoke locator this widening caused is fixed inb910bfb4.
@qwen-code /triage
|
Merged the latest Verified with Web Shell typecheck, changed-file ESLint, Prettier, the focused App test, and the four remote workspace-add Playwright scenarios. A fresh correctness/security review and a focused post-cleanup correctness pass found no actionable issues. @qwen-code /triage |
…icate chrome
Follow-up to the remote workspace add flow, addressing frontend review of
this PR.
Behaviour
- Choosing Local, or the remote daemon this tab is already connected to,
no longer reloads the whole shell. startRemoteWorkspaceAdd always went
through navigateToDaemon, whose same-origin branch is a full
window.location.reload() — so the common case (add another folder on the
daemon I am already on) tore down the open session, its socket, the
composer draft and the scroll position. Only a different computer now
takes the full-page handover.
- The location step is skipped when no remote computer has been connected:
it was a choice with one viable answer on every local add.
- The Add workspace entry is hidden again when the daemon cannot register
workspaces and there is no remote computer to pick instead. It used to
appear on any standalone shell and dead-end two steps later on "this
server does not support adding workspaces".
- Enter in the folder browser opens the typed directory instead of being a
silent no-op, and reports a non-absolute path. Registering a workspace
stays a deliberate button press.
- Escape in the folder browser closes the dialog. Browse mode keeps the
list open by design, so the list-dismiss interception left Escape doing
nothing at all.
Design
- The daemon is named once as a host chip beside the sidebar Project
heading, instead of badging every workspace row with the same fact —
the badge was derived per daemon, not per workspace, so it repeated N
times and added N screen-reader labels.
- The folder step names its computer in the dialog subtitle rather than
inside the field hint, and the hint now sits above the list it points
at ("Choose a folder below" rendered under the list on mobile).
- The path is navigable as breadcrumbs; the single parent-folder icon
button it subsumes is gone.
- Selected location cards get a check mark and a brighter icon, not just
a border tint, which was near-invisible on the dark theme.
- The disabled Remote card carries a Connect a computer action instead of
a dead-end sentence.
- The back button reads Change location, matching the step it returns to.
It said Change computer even for a Local add, which the code comment
already flagged as ambiguous.
Cleanup
- One WorkspaceAddStatusDialog replaces three sibling DialogShells
(loading, unsupported, browser) whose exclusivity rested on three
overlapping predicates.
- Drop four workspaceHost i18n keys that no code referenced, in both
locales.
- Format daemon origins through a shared helper rather than a bare
new URL(origin).host on a render path.
Not run locally: build, typecheck and tests. CI owns those.
Verification at
|
The parent-folder button already navigated upwards and the path input already shows and edits the full path, so the breadcrumb row was a third device for the same job — a feature added during review cleanup rather than a fix for anything. Restore the button and delete the crumb helper, its nav markup and its i18n key. Kept from that pass: the hint moved above the list it points at, the dialog subtitle naming the computer, and Enter opening the typed directory instead of doing nothing. Not run locally: build, typecheck and tests. CI owns those.
Current-head UI evidence at
|
|
@qwen-code /review |
|
Qwen Code review request accepted. Review is queued for an available runner; follow the workflow run for progress. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes. |
Maintainer verification — two real
|
| Piece | What it is |
|---|---|
| Web Shell | built from this head (npm run build), served by daemon A at http://127.0.0.1:4190 — not the vite dev server |
| Daemon A — This computer | qwen serve --port 4190 --workspace …/hostA/workA, loopback, isolated QWEN_HOME |
| Daemon B — connected computer | qwen serve --port 4191 --workspace …/hostB/workB --token … --require-auth --allow-origin http://127.0.0.1:4190 |
| Daemon C | a third qwen serve on :4192 for multi-computer cases |
| Filesystem | hostA/ and hostB/ hold disjoint directory names, so the two suggestion lists cannot be confused |
| Driver | Playwright/Chromium 1.58, desktop 1440×900 and mobile 390×844 |
Reviewer Test Plan — 9/9 reproduced
| # | Step | Result |
|---|---|---|
| 1 | Add a remote daemon through Settings > Connections | ✅ Verified through the gate, returned to the exact source URL with Connections selected (aria-current="page"), qwen-remote-connections = ["http://127.0.0.1:4191"], token only in sessionStorage under qwen-daemon-token:<origin>, never in localStorage and never in any request URL. Forget drops the origin and its token, and the Folder source list falls back to one entry. |
| 2 | Normal Add workspace | ✅ Folder browser opens in place, Folder source = This computer, zero radio controls, zero page loads, real hostA folders listed by the daemon. |
| 3 | Select a connected computer | ✅ Tab moves to ?daemon=http://127.0.0.1:4191 with no token parameter; the same dialog stays open and now lists hostB folders only; the cross-origin GET /workspace-path-suggestions to :4191 carries Authorization: Bearer …; remote rows get the blue globe badge. |
| 4 | Navigate, Parent folder, add | ✅ Suggestion descends, Parent folder walks up, POST /workspaces goes only to :4191; daemon B's /capabilities then lists the new workspace and …/daemon/workspaces/*.json contains it on disk (persist: true really persisted); the continuation marker and its sessionStorage key are gone. |
| 5 | Switch back to This computer, repeat, Cancel | ✅ Both directions keep the browser open; Cancel lands on the byte-exact source URL, dialog closed, no continuation state. |
| 6 | Invalid token, cancel from the gate | ✅ A wrong token keeps the operator on the gate with the flow marker intact; Cancel adding workspace restores the exact source page with no leftover state; re-entering the right token drops straight back into the remote folder browser. |
| 7 | Start on remote A, add from another computer, cancel | ✅ A catalogued remote boots with no unfamiliar-target gate; cancelling restores the remote source tab exactly, with no second confirmation. |
| 8 | Ordinary daemon switch | ✅ Switching through Connections moves the shell and does not open the folder browser or leave continuation state. |
| 9 | Desktop and 390 × 844 | ✅ Dialog 358 px wide inside a 390 px viewport; Folder source, Parent folder, Add this folder, Cancel all fully inside the viewport; the hand-over works on mobile too. |
Because the shell is served by the daemon, the production CSP is in play rather than the dev server's: GET / answers connect-src 'self', and GET /?daemon=http://127.0.0.1:4191 answers connect-src 'self' http://127.0.0.1:4191 ws://127.0.0.1:4191 — the cross-origin calls above really are made under that policy.
Extra probes beyond the plan: an unreachable connected computer degrades to the retrying gate and Cancel still restores the source page; an origin that is not in the catalog still faces the unfamiliar-target gate (the security control is intact).
Repo gates run locally at this head
packages/web-shell vitest 338 files / 8868 tests passed
web-shell.remote-workspace-add.spec 6 passed (chromium)
tsc -p tsconfig.json --noEmit clean
eslint packages/web-shell clean
Evidence
Settings > Connections after verifying the second computer
Folder source selector — one list, no separate Local/Remote step
The same browser, resumed on the connected computer (real directories from daemon B)
Remote marking — host chip on the Projects header, globe badge on the folder icons
Authentication failure stays inside the flow
390 × 844
F1 — an abandoned hand-over leaves its return location behind (minor, reproducible 100%)
The continuation is one-shot and tab-scoped, but it is only cleared on completion or cancellation. A reload — or the browser Back button — abandons the flow without clearing it, because the URL marker is stripped by App's effect while qwen-remote-workspace-return stays in sessionStorage. The next Cancel in any Add-workspace dialog then consumes that stale location.
Repro on the rig above:
- On
:4190, Add workspace → Folder source →127.0.0.1:4191(tab hands over). - Press F5. The dialog is gone (flow abandoned), but
sessionStorage['qwen-remote-workspace-return'] === "http://127.0.0.1:4190/"survives. - Open Add workspace again — a brand-new, purely local add on
:4191— and press Cancel. - The whole shell navigates away from
:4191back to:4190. Measured settled state:url = http://127.0.0.1:4190/, 0 globe badges, sidebar back toworkA.
The Back-button variant is milder but reachable with no refresh at all: after Back, cancelling an ordinary local add triggers a full page navigation (load event fired) instead of just closing the dialog.
Candidate fix — tested on the rig (head red → patched green → head red again), with remote-workspace-add.spec 6/6, client/config + App.test.tsx 1085/1085 and every happy-path probe still green:
// client/config/remote-workspace-add.ts
export function discardAbandonedRemoteWorkspaceAdd(): void {
try {
window.sessionStorage.removeItem(RETURN_URL_KEY);
} catch {
// Nothing to discard when storage is unavailable.
}
}// client/App.tsx
useEffect(() => {
if (initialRemoteWorkspaceAddStep) clearRemoteWorkspaceAddStep();
else if (standalone) discardAbandonedRemoteWorkspaceAdd();
}, [initialRemoteWorkspaceAddStep, standalone]);Safe because the return key is only ever written by startRemoteWorkspaceAdd/selectRemoteWorkspaceLocation immediately before a navigation that carries the marker — a marker-less boot in standalone mode means the flow is over.
Notes (not blockers)
- N1 — the catalog is written on every successful gate probe.
StandaloneAuthcallsrememberRemoteConnection(baseUrl)outside the connection-add branch, so confirming a one-off?daemon=link once permanently adds that origin toqwen-remote-connections, whichmain.tsxnow treats as a confirmed target for every future tab (the confirmation used to besessionStorage, i.e. per tab). Measured: fresh tab, empty catalog, one?daemon=…+ token at the gate →["http://127.0.0.1:4191"]persisted and the origin offered as a Folder source. Defensible ("the operator verified it", and it is removable in Settings), but worth a line in the PR description or scoping the write to the add flow. - N2 — mutation check on the new tests. 10 mutants across
remote-connections.ts,remote-workspace-add.ts,daemon.ts,App.tsx,AddWorkspaceDialog.tsx, each run againstclient/config, the three dialog suites,App.test.tsx,main.test.tsxand the new e2e spec. 3 are killed (catalog validation, forget-clears-token, remember-on-complete). I traced each of the 7 survivors:tokenstripping on both return legs is dead code (the URL is already stripped when it is written), the same-origin guard on both return legs guards a statesessionStoragecannot produce,showList'sbrowseDirectories ||is redundant once the first fetch setslistOpen, and the same-target reload marker is not reachable from the selector (it short-circuits on the current source). The one survivor that guards real behaviour ishasRemoteWorkspaceLocation— nothing pins the "entry enabled because a remote exists" case, which is also N4 below. Everything else is equivalent-by-construction, so no meaningful test hole. - N3 — zh copy.
workspaceHost.thisComputeris 这台计算机 butworkspaceHost.folderOnThisComputeris 这台电脑上的目录. One word, two spellings. - N4 — dead end on a daemon without
dynamic_workspace_registration. The unsupported/ loading status dialog has no Folder source selector, so a user whose local daemon lacks the feature (the entry is enabled because a remote exists) can only Cancel. Unreachable with today'sqwen serve(dynamicWorkspaceRegistrationAvailableis hard-codedtrue), so it only matters for an older or third-party daemon.
Not covered
Windows; a real second machine / non-loopback network; HTTPS or a TLS-terminating proxy; a daemon that does not advertise dynamic workspace registration; behaviour of a long-running session across the full-page hand-over.
Verdict
The workflow does what the description claims, on real daemons, including the parts a mock cannot prove: real CORS + bearer auth on the cross-origin suggestion calls, the registration actually landing (and persisting) on the selected daemon, and the credential never touching the URL. F1 is the only defect I found, and the fix is contained (one helper plus one line in an existing effect). From my side this is fine to merge; I'd land F1 either in this PR or as an immediate follow-up.
中文说明
维护者验证 —— 两个真实 qwen serve daemon + 生产构建的 Web Shell
在 macOS 上对 1b5c3d647c 做了真实环境验证(不是 mock 端点):被测 shell 是本地 npm run build 出来的 packages/web-shell/dist,由 qwen serve 自己托管(走生产静态资源路径与其 CSP);“已连接的计算机”是第二个 qwen serve,带真实 bearer token、--require-auth 和 --allow-origin。用 Chromium 驱动真实 UI:70 条脚本断言,66 条通过;4 条失败全部指向同一个缺陷(下面的 F1)。
装置
| 组成 | 说明 |
|---|---|
| Web Shell | 本 head 构建产物,由 daemon A 在 http://127.0.0.1:4190 托管 —— 不是 vite dev server |
| Daemon A(这台计算机) | qwen serve --port 4190 --workspace …/hostA/workA,loopback,隔离 QWEN_HOME |
| Daemon B(已连接的计算机) | qwen serve --port 4191 --workspace …/hostB/workB --token … --require-auth --allow-origin http://127.0.0.1:4190 |
| Daemon C | 第三个 qwen serve(:4192),用于多计算机场景 |
| 文件系统 | hostA/ 与 hostB/ 下目录名完全不重名,两边的目录建议不可能混淆 |
| 驱动 | Playwright/Chromium 1.58,桌面 1440×900 与手机 390×844 |
Reviewer Test Plan —— 9 条全部复现
| # | 步骤 | 结果 |
|---|---|---|
| 1 | 通过设置 > 连接添加远程 daemon | ✅ 经连接页验证后回到完全一致的来源 URL,分类仍是 Connections(aria-current="page");qwen-remote-connections = ["http://127.0.0.1:4191"];token 只在 sessionStorage 的 qwen-daemon-token:<origin>,不进 localStorage、不进任何请求 URL。移除会同时清掉 origin 和它的 token,目录来源列表退回单项。 |
| 2 | 普通添加工作区 | ✅ 目录浏览器就地打开,目录来源 = 这台计算机,没有任何 radio 控件,零次页面加载,列出的是 daemon 真实返回的 hostA 目录。 |
| 3 | 选择一台已连接计算机 | ✅ 标签页跳到 ?daemon=http://127.0.0.1:4191,URL 中没有 token 参数;同一个弹窗保持打开并只显示 hostB 的目录;跨域 GET /workspace-path-suggestions 带 Authorization: Bearer …;远程行出现蓝色地球徽标。 |
| 4 | 目录建议 / 上一级 / 添加 | ✅ 建议可下钻、上一级可回退;POST /workspaces 只打到 :4191;随后 daemon B 的 /capabilities 列出新 workspace,磁盘上 …/daemon/workspaces/*.json 也有(persist: true 真的落盘);续接标记与 sessionStorage 键均已清理。 |
| 5 | 切回“这台计算机”,再走一遍并取消 | ✅ 两个方向都保持目录浏览器打开;取消后回到逐字符一致的来源 URL,弹窗关闭,无残留状态。 |
| 6 | 无效 token 并从连接页取消 | ✅ 错误 token 停留在连接页且流程标记仍在;取消添加工作区恢复精确来源页面且无残留;再输入正确 token 直接回到远程目录浏览器。 |
| 7 | 从远程 A 开始、在另一台计算机上添加后取消 | ✅ 目录中的远程 origin 启动时不再出现陌生目标确认页;取消后精确恢复远程来源标签页,没有第二次确认。 |
| 8 | 普通 daemon 切换 | ✅ 通过连接切换只换 daemon,不会打开目录浏览器,也不留续接状态。 |
| 9 | 桌面与 390 × 844 | ✅ 390px 视口下弹窗宽 358px;目录来源、上一级、添加此文件夹、取消 全部完整落在视口内;手机视口下的交接同样可用。 |
因为 shell 由 daemon 托管,生效的是生产 CSP 而非 dev server 的:GET / 返回 connect-src 'self',GET /?daemon=http://127.0.0.1:4191 返回 connect-src 'self' http://127.0.0.1:4191 ws://127.0.0.1:4191 —— 上面那些跨域请求确实是在该策略下发出的。
计划外的补充探针:不可达的已连接计算机会退化为带重试的连接页,取消仍能恢复来源页面;不在目录中的 origin 依然会被陌生目标确认页拦下(安全控制未被削弱)。
本 head 上本地跑过的仓库门禁
packages/web-shell vitest 338 个文件 / 8868 条用例通过
web-shell.remote-workspace-add.spec 6 passed(chromium)
tsc -p tsconfig.json --noEmit 干净
eslint packages/web-shell 干净
F1 —— 被放弃的交接会留下返回地址(次要,100% 可复现)
续接确实是一次性、按标签页隔离的,但它只在「完成」或「取消」时清理。刷新页面(或按浏览器后退)会放弃流程却不清理:URL 上的标记被 App 的 effect 抹掉了,而 sessionStorage 里的 qwen-remote-workspace-return 还在。之后任何一个添加工作区弹窗的取消都会消费掉这个过期地址。
复现步骤:
- 在
:4190上添加工作区 → 目录来源 →127.0.0.1:4191(标签页交接)。 - 按 F5。弹窗消失(流程已放弃),但
sessionStorage['qwen-remote-workspace-return'] === "http://127.0.0.1:4190/"仍在。 - 再次打开添加工作区 —— 这是
:4191上一次全新的、纯本地的添加 —— 然后按取消。 - 整个 shell 会从
:4191跳回:4190。实测稳定后的状态:url = http://127.0.0.1:4190/,地球徽标 0 个,侧边栏回到workA。
后退按钮那一版症状更轻但完全不需要刷新:后退之后,取消一次普通的本地添加会触发整页导航(load 事件被触发),而不是只关闭弹窗。
候选修复 —— 已在装置上实测(head 红 → 打补丁绿 → 还原后又红),同时 remote-workspace-add.spec 6/6、client/config + App.test.tsx 1085/1085、所有正向探针保持绿:
// client/config/remote-workspace-add.ts
export function discardAbandonedRemoteWorkspaceAdd(): void {
try {
window.sessionStorage.removeItem(RETURN_URL_KEY);
} catch {
// Nothing to discard when storage is unavailable.
}
}// client/App.tsx
useEffect(() => {
if (initialRemoteWorkspaceAddStep) clearRemoteWorkspaceAddStep();
else if (standalone) discardAbandonedRemoteWorkspaceAdd();
}, [initialRemoteWorkspaceAddStep, standalone]);之所以安全:返回地址只由 startRemoteWorkspaceAdd / selectRemoteWorkspaceLocation 在带标记的导航前一刻写入,因此 standalone 下「启动时没有标记」就意味着流程已经结束。
备注(非阻塞)
- N1 —— 每次连接页探测成功都会写目录。
StandaloneAuth把rememberRemoteConnection(baseUrl)放在连接添加分支之外,于是用户对一个一次性的?daemon=链接确认一次,该 origin 就会被永久写入qwen-remote-connections;而main.tsx现在把目录中的 origin 视为已确认目标(以前的确认是sessionStorage,只在当前标签页有效)。实测:全新标签页、目录为空,在连接页输入一次?daemon=…+ token 之后,目录里出现该 origin,且它出现在目录来源列表中。这个取舍说得通(用户确实验证过,而且可在设置里移除),但建议在 PR 描述里写明,或把写入限定在添加连接流程内。 - N2 —— 对新测试做了变异检查。 在
remote-connections.ts、remote-workspace-add.ts、daemon.ts、App.tsx、AddWorkspaceDialog.tsx上共 10 个变异体,每个都跑client/config、三个弹窗套件、App.test.tsx、main.test.tsx以及新增的 e2e 规格。杀死 3 个(目录校验、移除时清 token、完成时写目录)。存活的 7 个我逐个追了可达性:两条返回路径上的token剥离是死代码(URL 在写入时就已剥掉 token);两处同源校验挡的是sessionStorage按 origin 隔离后不可能出现的状态;showList里的browseDirectories ||在首次请求把listOpen置真之后是冗余的;同目标 reload 分支的标记在选择器上走不到(选中当前来源会被短路)。唯一守着真实行为的存活变异是hasRemoteWorkspaceLocation—— 「因为存在远程才启用入口」这一路径没有测试钉住,也就是下面的 N4。其余都是构造上的等价变异,因此没有实质测试盲区。 - N3 —— 中文文案。
workspaceHost.thisComputer是「这台计算机」,但workspaceHost.folderOnThisComputer是「这台电脑上的目录」,同一个词两种说法。 - N4 —— 不支持动态注册的 daemon 会走进死路。 不支持 / 加载中的状态弹窗没有目录来源选择器,所以当入口是因为存在远程才被启用、而本地 daemon 不支持该能力时,用户只能取消。当前
qwen serve不可达(dynamicWorkspaceRegistrationAvailable硬编码为true),只对旧版或第三方 daemon 有意义。
未覆盖
Windows;真实的第二台机器 / 非 loopback 网络;HTTPS 或 TLS 终止代理;不宣告动态工作区注册能力的 daemon;长时间运行的会话在整页交接后的表现。
结论
这条工作流在真实 daemon 上确实做到了描述中的事情,包括 mock 证明不了的部分:跨域目录建议上的真实 CORS + bearer 鉴权、注册确实落在所选 daemon 上并持久化、凭据始终不进 URL。F1 是我找到的唯一缺陷,修复范围很小(一个辅助函数 + 已有 effect 里的一行)。从我这边看可以合并;建议 F1 在本 PR 或紧随其后的跟进 PR 中修掉。
…hind The remote-workspace-add continuation is one-shot and tab-scoped, but it was only cleared on completion or cancellation. A reload or the browser's Back button abandons the flow without clearing it: App's effect strips the URL marker while the qwen-remote-workspace-return key stays in sessionStorage, so the next Cancel in any Add-workspace dialog -- including a purely local one -- consumed that stale location and navigated the whole shell back to it. Discarding it on a marker-less standalone boot is safe: the key is only ever written by startRemoteWorkspaceAdd immediately before navigateToDaemon, and every navigateToDaemon success path either assigns a URL carrying the marker or replaceStates the marker in before reloading, while a failed switch removes the key again. So a boot without the marker means the hand-over is over. Also aligns the zh copy for workspaceHost.folderOnThisComputer with workspaceHost.thisComputer. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-closeout/jmu7rlkyy52
|
Landed F1 (the abandoned hand-over leaving its return location behind) at F1 — what changed (one helper plus one line, as you proposed):
I re-checked your safety precondition against the tree instead of taking it on trust, and it holds. The key has exactly one writer, The consumption side of the bug is confirmed too: Tests (jsdom):
Non-tautology, measured rather than assumed: reverting the Local run: Browser-level evidence is inherited from your rig, not re-captured here. I did not stand up the two-daemon + Playwright rig and did not re-run N3: N1, N2 and N4 are deliberately untouched — you labelled them non-blockers and this diff is already large, so I kept the change to this one defect. 中文小结:F1 已按你的方案落地(一个 helper + 一行 effect),并在代码里复核了「写 key 必然紧跟一次带 marker 的跳转」这个前提,两条 |
Round 2 — re-verified on
|
| Probe | 1b5c3d647c |
06966553aa |
|---|---|---|
| F1 — reload abandons the hand-over, then Cancel an unrelated add | 1 pass / 2 fail | 3 pass / 0 fail |
| F1c — Back button abandons it, then Cancel a local add | 2 pass / 2 fail | 4 pass / 0 fail |
After the abandoning reload, sessionStorage['qwen-remote-workspace-return'] is gone, and the next Cancel now closes the dialog without navigating: the tab stays on ?daemon=http://127.0.0.1:4191, sidebar still workB + beta-remote-checkout with globe badges. The Back-button variant fires no load event at all any more.
One thing to read correctly: my third probe (probeF1d) exists only to pin the defect's symptom — "after the stale-return cancel the tab is on the source daemon URL, 0 globe badges". On this head it reports 2 failures. That is the expected post-fix polarity, not a regression; the settled state it now measures is url = …?daemon=http://127.0.0.1:4191, badges = 2.
The fix cannot eat a live continuation — measured, not assumed
The risk in "discard on a marker-less standalone boot" is an App mount that happens inside a live flow. I looked for one:
- Traced the remount paths.
StandaloneAuthsetsacceptedexactly once (setAcceptedhas a single call site) and never re-gates, and both<App>mount sites render it without a changingkey, so App cannot remount mid-flow within one document. - Exercised the latest possible mount — the gate path. Hand-over to the remote with no stored token → gate → correct token → App mounts only then, after the marker-bearing URL. The return location survived (
qwen-remote-workspace-return === http://127.0.0.1:4190/) and Cancel still returned to the exact source page. - Exercised a mid-flow outage. Killed daemon B while the folder browser was open on it: the continuation survived, and Cancel after the outage still restored the source page.
7/7 green on those.
No regressions — the whole Reviewer Test Plan re-run
All nine steps re-measured on this head: 61/61 green (Connections add/verify/return/forget, direct folder browser, hand-over with token out of the URL, cross-origin suggestions with the bearer header, POST /workspaces only on the selected daemon plus its on-disk persistence, Cancel to the byte-exact source URL, the gate paths, ordinary daemon switches, unreachable target, desktop + 390×844).
Repo gates on this head (local)
packages/web-shell vitest 338 files / 8871 tests passed (8868 → 8871: the 3 new tests)
web-shell.remote-workspace-add.spec 6 passed (chromium)
tsc -p tsconfig.json --noEmit clean
eslint packages/web-shell clean
Your test claims, re-measured here
| Mutation | Result |
|---|---|
Revert the else if (standalone) … line |
Killed — discards a return location an abandoned hand-over left behind, expected 'http://localhost:3000/session/original' to be null (exactly the assertion you quoted) |
| Helper made a no-op | Killed — the config test plus the App witness, 2 red |
| Discard unconditionally (also when the hand-over is being resumed) | Killed — keeps the return location of a standalone boot that resumes the hand-over, so that guard has real teeth |
Drop the standalone condition (else discard…) |
Survives — nothing pins that the discard is standalone-only. Harmless today, since only the standalone shell ever writes the key; noting it as an unpinned condition rather than a defect. |
N3
Verified in the shipped bundle, not just the diff: 这台计算机上的目录 appears once in dist/assets/index-*.js and 这台电脑上的目录 is gone. zh UI shows 目录来源 = 这台计算机.
Verdict
F1 is genuinely closed at browser level on real daemons, the guard against an over-broad discard is real, and nothing in the plan regressed. N1/N2/N4 stay as agreed non-blockers. LGTM to merge from my side.
中文说明
第 2 轮 —— 在同样的两个真实 daemon 上复验 06966553aa
在原装置里重建了这个 head(qwen serve 自己托管构建出的 packages/web-shell/dist,第二个 qwen serve 带 --require-auth --token --allow-origin),把上一轮的全部探针重跑,并针对修复本身补了新探针。78 条断言,76 条绿;2 条红的是我用来“刻画缺陷症状”的探针,现在按设计就应该红(见下)。
F1 在真机上确实关闭了
同样的探针、同样的装置,只换了 commit:
| 探针 | 1b5c3d647c |
06966553aa |
|---|---|---|
| F1 —— 刷新放弃交接后,取消一次无关的添加 | 1 通过 / 2 失败 | 3 通过 / 0 失败 |
| F1c —— 用后退放弃交接后,取消一次本地添加 | 2 通过 / 2 失败 | 4 通过 / 0 失败 |
放弃流程的刷新之后 sessionStorage['qwen-remote-workspace-return'] 已不存在,下一次取消只关闭弹窗、不再导航:标签页仍停在 ?daemon=http://127.0.0.1:4191,侧边栏仍是 workB + beta-remote-checkout 且带地球徽标。后退那一版现在完全不再触发 load 事件。
需要正确解读的一点:第三个探针(probeF1d)的存在意义就是钉住缺陷的症状 —— “过期返回地址被消费后标签页停在来源 daemon、地球徽标为 0”。它在这个 head 上报 2 条失败,这是修复后应有的极性反转,不是回归;它现在实测到的稳定状态是 url = …?daemon=http://127.0.0.1:4191、badges = 2。
修复不会吃掉“进行中”的续接 —— 是实测,不是假设
“无 marker 的 standalone 启动就丢弃”这条规则的风险在于:有没有可能在流程仍然存活时发生一次 App 挂载。我逐条找了:
- 追了重新挂载路径:
StandaloneAuth的accepted只被设置一次(setAccepted只有一个调用点)且不会退回网关;两处<App>挂载点都没有会变化的key,所以同一个文档内 App 不会在流程中途重挂。 - 走了最晚的一次挂载 —— 网关路径:在没有已存 token 的情况下交接到远端 → 网关 → 输入正确 token → App 这时才挂载(URL 上仍带 marker)。返回地址存活(
qwen-remote-workspace-return === http://127.0.0.1:4190/),取消仍然精确回到来源页面。 - 制造了流程中途的 daemon 掉线:目录浏览器正开在 B 上时把 daemon B 杀掉,续接状态存活,掉线之后取消依旧恢复来源页面。
这三项 7/7 全绿。
没有回归 —— 整份 Reviewer Test Plan 重跑
九条全部在这个 head 上重测:61/61 绿(连接的添加/验证/返回/移除、直接打开目录浏览器、token 不进 URL 的交接、带 bearer 头的跨域目录建议、POST /workspaces 只打所选 daemon 及其落盘持久化、取消回到逐字符一致的来源 URL、两条网关路径、普通 daemon 切换、不可达目标、桌面与 390×844)。
本 head 的仓库门禁(本地)
packages/web-shell vitest 338 个文件 / 8871 条用例通过(8868 → 8871,即新增的 3 条)
web-shell.remote-workspace-add.spec 6 passed(chromium)
tsc -p tsconfig.json --noEmit 干净
eslint packages/web-shell 干净
你对测试的判断,我这边复测了一遍
| 变异 | 结果 |
|---|---|
回退 else if (standalone) … 那一行 |
被杀 —— discards a return location an abandoned hand-over left behind,报错正是你引用的 expected 'http://localhost:3000/session/original' to be null |
| 把 helper 改成空操作 | 被杀 —— config 用例 + App witness,共 2 条红 |
| 无条件丢弃(连正在续接时也丢) | 被杀 —— keeps the return location of a standalone boot that resumes the hand-over,说明这条防“过度丢弃”的用例是有辨别力的 |
去掉 standalone 条件(写成 else discard…) |
存活 —— 没有测试钉住“只在 standalone 下丢弃”。今天无害(只有独立 shell 会写这个 key),记为未钉住的条件,不算缺陷。 |
N3
不只看 diff,还在打包产物里核实:dist/assets/index-*.js 中 这台计算机上的目录 出现 1 次,这台电脑上的目录 为 0 次;中文 UI 里目录来源 = 这台计算机。
结论
F1 在真实 daemon 的浏览器层面确实关闭,防“过度丢弃”的守卫用例有效,计划内各步骤无回归。N1/N2/N4 按之前约定保持为非阻塞项。从我这边看:可以合并。
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
APPROVE at 0696655.
No Critical has ever been filed on this PR, and the one dismissed approval was voided by pushes rather than by a finding. Required CI is complete and green on this head; review-pr is the reviewer bot's own workflow and is excluded deliberately, since the main ruleset declares no required status checks.
This adds a flow that sends a bearer token to a user-supplied origin and stores connection state in the browser, so I checked the three properties that decide whether it is safe, on this head rather than on the earlier one the static review traced:
- The token never reaches persistent storage or a URL.
daemon.tswrites it tosessionStorageonly, with the tab-scoping rationale stated at the call site, andremote-connections.tspersistsstring[]origins tolocalStorage— no token, no workspace state. BothstartRemoteWorkspaceAddand the return leg callsearchParams.delete('token'), so neither the stored return URL nor the URL actually navigated can carry one. - Every stored origin is re-validated on read, not on write only.
readRemoteConnections()filters ontypeof origin === 'string' && getAllowedDaemonOrigin(origin) === origin && origin !== window.location.origin, de-duplicates, and returns an empty list on any parse failure — so a hand-edited or stale localStorage entry cannot smuggle an arbitrary origin into the panel, and the shell's own origin is never listed as a remote computer.rememberRemoteConnectionnormalizes and refuses self-origin before writing, too. - The return path is not an open redirect.
returnFromRemoteConnectionAdd()reads and removes the one-shot key first, then rejects unlessurl.origin === window.location.origin, strips the flow parameter, the token and the hash, re-validates thedaemonparameter throughgetAllowedDaemonOrigin, requiresconfirmDaemonTargeton the result, and only then assigns. A cross-origin or malformed stored URL is discarded rather than followed.
The F1 fix is the part I would most want measured rather than read, and it was: the maintainer rig re-ran the whole test plan on this head (61/61), traced the remount paths to show StandaloneAuth sets accepted once and neither App mount site uses a changing key, then exercised the latest possible mount through the token gate and a mid-flow daemon kill to confirm discarding on a marker-less boot cannot eat a live continuation (7/7). Local gates on the head are 8871 unit tests, the remote-workspace-add e2e spec, typecheck and lint.
No new Critical. Two notes, neither blocking:
- The
remoteWorkspaceAddActiveRefpattern requires every future ref mutation to be paired with a state setter that forces a re-render. It is correct at all six mutation sites today, but the invariant lives in a convention rather than in a type, so it is the likeliest place for a later edit to go wrong. - Windows was not exercised by the author or by the maintainer rig. Nothing here is platform-conditional — it is URL, storage and fetch behaviour in a browser — which is why it is a note.
已核对 head 0696655:无历史 Critical,required CI 全绿,token 存放、来源复验与返回地址守卫三条安全属性我在当前 head 上逐项验证,未发现新的 Critical。
qqqys
left a comment
There was a problem hiding this comment.
Critical-only review at 06966553, base 2bd62fbd.
Verdict: APPROVE — no merge-blocking correctness, security, data-loss, regression, or compatibility defect found.
Historical blocking issues
None exist. The PR has never carried a request for changes and has no review threads. Its single prior review records no blocking findings and was dismissed when a later commit landed; I did not treat that pass as evidence and re-verified the surfaces it covered myself.
What I reviewed
This is a large feature diff, so the scan concentrated on every part of it that can carry a trust, credential, or state-machine defect, and I read those in full rather than sampling.
The two new modules are fail-closed throughout. remote-workspace-add.ts builds its return URL by deleting the flow parameter, deleting token, and clearing the hash before writing to sessionStorage, so no credential is persisted; a storage failure returns false rather than throwing; and if the subsequent navigation does not start, it restores the URL and removes the stored key, so a failed hand-over leaves no residue. On the return leg it parses the saved URL, refuses it outright when url.origin !== window.location.origin, strips token, the hash and the flow parameter again, re-validates any daemon parameter through getAllowedDaemonOrigin, and only then confirms the target and assigns. remote-connections.ts applies the same discipline to its catalog: the stored list is parsed defensively, rejected unless it is an array, and every entry must satisfy getAllowedDaemonOrigin(origin) === origin and differ from the current origin before it survives, with de-duplication and a [] fallback on any throw. A tampered or corrupt storage value therefore cannot introduce a non-allowlisted origin. Forgetting a connection also clears that origin's stored token rather than orphaning the credential.
The daemon primitives keep their gate. navigateToDaemon still resolves getAllowedDaemonOrigin(raw) and returns false when it is absent or when the connection URL cannot be built; the new flow parameters are set only after that validation, and the token is persisted only once the origin is known. buildDaemonConnectionUrl now strips addRemoteWorkspace alongside the existing context, addWorkspace, workspaceReturn and token deletions, so a flow parameter cannot ride along into an unrelated connection.
The app-level wiring consumes each deep link exactly once. Both flow parameters are read through lazy useState initialisers gated on the standalone context, so they are sampled at mount rather than re-read per render, and an effect clears each after consumption. The cleanup effect that closes dialogs when project features are unavailable now skips the add-workspace dialog while a ref records the browse flow as active, which is what keeps a deep-linked flow from being dismissed immediately after it opens; the same effect exempts the deep-linked settings category. When no flow parameter is present the effect discards an abandoned hand-over's stored return location, which is the residue the most recent commit addresses.
The auth gate is not weakened. StandaloneAuth.tsx adds cancel affordances for the two flows in both languages and threads the same options object through onChangeTarget; the invalid-target and unconfirmed-target props, the token hint copy and the gate's own conditions are untouched.
The submitting dialog validates and cannot double-fire. The path is trimmed, an explicit error is raised for a non-absolute path, the submit control is disabled while submitting or while the trimmed path is empty, and a location change may be refused by the parent through a false return.
The connections panel neither renders nor retains a credential. Saved origins are keyed and titled by origin but displayed through formatOriginHost, which reduces to a host with a caught fallback; there is no dangerouslySetInnerHTML, no interpolated href, and no direct storage access in the component. Switching passes the token to a handler rather than to the DOM, forgetting routes through the module function that also clears the credential, and a newly added connection is remembered only after the switch succeeds, with the token input cleared afterwards.
What I did not review
Stated so it is not over-read: the remaining changed files carry no state or trust semantics and were not read line by line — the settings message rendering, the sidebar and workspace-section markup, two CSS modules, the added i18n strings, the small main.tsx and settings.ts deltas, and the two design documents. They are covered indirectly by the green lanes below.
CI
Fully settled on this head with no failures across its checks. The ubuntu test lane, the lint and static checks, the no-AK integration lane and both Desktop Shell lanes pass, which matters here because those are the lanes that build and exercise this client. The added coverage is substantial — on the order of fourteen hundred lines of unit and component tests plus an end-to-end spec for this specific flow — and is observed passing rather than assumed.











What this PR does
This PR adds Codex-style remote connections to the standalone Web Shell. Settings > Connections adds and manages verified daemon origins, while bearer tokens remain tab-scoped. Adding a computer verifies it through the existing connection gate and then returns to Settings > Connections. The normal Add workspace action opens the directory browser directly; its Folder source selector switches between this computer and connected remote computers without a separate Local/Remote step. Workspace rows from a remote daemon use a small blue globe badge on the folder icon; local rows keep the plain folder icon.
The directory-add continuation is deliberately one-shot and tab-scoped. Changing Folder source resumes the same browser on the selected computer, Cancel returns to the exact source URL, a successful add stays on the selected daemon, and ordinary daemon switches never resume the flow. The persistent catalog contains verified origins only, not tokens, workspaces, or cross-daemon project state.
The directory browser supports daemon-provided suggestions, parent navigation, manual absolute paths, loading/error/unsupported states, and responsive desktop/mobile controls. Authentication failures remain inside the same flow, while tokens remain origin-scoped in session storage and never enter the URL.
Why it's needed
After #11548 was narrowed to one selected daemon, adding a remote folder required switching the whole shell through Daemon Status and then manually entering an absolute path. It also treated the server address as part of each add operation instead of modeling a remote computer as a reusable connection.
This restores a productized workflow without reintroducing cross-daemon workspace aggregation or a second workspace data model.
Reviewer Test Plan
How to verify
Current-head UI evidence was captured manually against two local mock daemon endpoints. This evidence refresh did not run local lint, tests, or
npm ci; repository gates remain with CI.Evidence (Before & After)
Before: remote daemon details were entered as part of an independent add operation, so adding a workspace did not reuse a configured remote-computer concept.
After, Connections:
After, direct directory browser with a remote Folder source:
Tested on
Environment (optional)
macOS, Chromium Playwright against mocked local and cross-origin daemon endpoints.
Risk & Scope
Design: English · 简体中文
Linked Issues
Refs #11475
中文说明
本 PR 做了什么
本 PR 为独立 Web Shell 增加 Codex 风格的远程连接概念。设置 > 连接用于添加和管理验证过的 daemon origin,bearer token 仍只保存在当前标签页。添加计算机时会通过已有连接页完成验证,随后返回设置 > 连接。普通的添加工作区操作会直接打开目录浏览器;顶部的目录来源可以在这台计算机和已连接的远程计算机之间切换,不再弹出独立的“本地 / 远程”步骤。来自远程 daemon 的 workspace 会在文件夹图标右下角显示蓝色小地球,本地 workspace 保持普通文件夹图标。
目录添加续接被明确限制为当前标签页内的一次性流程。切换目录来源会在所选计算机上续接同一个浏览器,取消会返回准确的来源 URL,添加成功后留在所选 daemon,普通 daemon 切换不会续接该流程。持久化目录只包含验证过的 origin,不包含 token、workspace 或跨 daemon 项目状态。
目录浏览器支持 daemon 返回的目录建议、上一级导航、手工绝对路径、加载/错误/不支持状态,以及桌面和移动端响应式操作区。认证失败仍保留在同一流程中;token 继续按 origin 隔离在 session storage 中,且不会进入 URL。
为什么需要
#11548 收窄为单个已选 daemon 后,添加远程目录要求用户先通过 Daemon 状态切换整个 shell,再手工输入绝对路径;服务器地址也被当成每次添加操作的一部分,而不是一台可复用的远程计算机。
本 PR 恢复产品化流程,同时不重新引入跨 daemon workspace 聚合或第二套 workspace 数据模型。
Reviewer Test Plan
如何验证
当前 head 的 UI 证据通过两个本地 mock daemon 手工采集。本次证据更新没有在本地执行 lint、测试或
npm ci;仓库门禁由 CI 负责。证据(Before & After)
Before: 远程 daemon 信息属于一次独立的添加操作,普通“添加工作区”无法复用已配置的远程计算机。
After,连接设置:
After,直接打开并选择远程目录来源的目录浏览器:
测试平台
环境(可选)
macOS,Chromium Playwright,使用模拟的本地与跨来源 daemon endpoint。
风险与范围
设计文档:English · 简体中文
关联 Issue
Refs #11475