fix(vscode): unify session history and remove source switch - #11713
Conversation
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Verification at 71f45d5: the three focused Companion test files passed (115 tests). Companion TypeScript, changed-file ESLint, and the esbuild extension bundle also passed locally. A real macOS VS Code development host displayed the existing workspace history without the source selector. Window-control errors prevented completing the actual conversation click-through and extension reload, so those remain unverified; the automated component/host tests are not a substitute for that E2E check. Windows and Linux were not tested locally. |
Close stale history on host bootstrap and exclude marked Live coordinator sessions from ordinary history. Remove outdated source-isolation documentation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
|
Review follow-up pushed in 25edeb3: host bootstrap now closes stale history and clears its error, and explicitly marked Live coordinator sessions are excluded from ordinary conversation history. Removed the outdated comment describing source-isolated history. Ordinary CLI, browser, and VS Code conversations still share one list with no source selector. Validation: all 116 focused tests passed, plus Companion TypeScript, changed-file ESLint, extension bundling, and diff whitespace checks. No further blocking finding was identified in the scoped review and simplicity pass. Remaining verification limits are unchanged: real-host click-through restoration followed by reload has not been completed, and untagged Live task sessions remain tracked separately in #11707. |
Test report: unified session historyReal VS Code visual checkCaptured from the macOS VS Code Extension Development Host running the unified-history development extension against the qwen-code workspace, not a mockup or standalone browser preview. Observed: one populated history list, a search field, date groups, and a Load more button. There is no Terminal / VS Code source selector. The terminal branch shown on the right is the opened workspace checkout, not the development extension branch. Automated verificationPreviously run on commit 25edeb3: 116 focused tests passed across the history dropdown, embedded app, and extension provider; Companion TypeScript, changed-file ESLint, extension bundling, and diff whitespace checks also passed. Mixed CLI / browser / VS Code history, source-preserving selection, pagination, and the reconnect regression are covered by focused tests. LimitsThis screenshot verifies the visible list layout, not each displayed session's creator metadata, successful Load more interaction, or end-to-end restoration. The already-running development host was not reloaded for this capture, so the screenshot is not evidence for the latest reconnect and Live-filter fixes. Restoring a CLI conversation and reloading the real VS Code host remains unverified. Untagged Live task sessions remain tracked in #11707; child and explicitly marked system sessions are intentionally still filtered. |
|
Thanks for the PR! Template looks good ✓ — every required heading is present, including the Tested-on table, the Risk & Scope bullets, and the Chinese translation. Problem: observed, not theoretical. #11574 is open and was filed by a different user (@Luolingli) with reproduction steps, a per-transcript evidence table, and a live check against the extension's own daemon: of 8 persisted sessions in one workspace, Direction: aligned. #11574's own "Expected behavior" asks for two things — list all active workspace sessions including legacy ones without source metadata, and if source scoping is wanted, make it an explicit UI option rather than a hardcoded default. #11584 shipped the second clause; this PR ships the first. Worth saying plainly, since it's the one thing a reviewer should consciously sign off on: this reverses #11584, which merged at 05:01 UTC today — about seven hours before this branch landed. A same-day course correction is the right call when it deletes the attempt that missed the requirement rather than layering on top of it, and that's what happens here. CHANGELOG: no direct reference to a creation-source selector in the VS Code session list; the area is relevant, since that list is workspace-scoped with no source tabs. Size: core paths are not touched, so the two-tier core gate is not applicable. All eleven files sit under Approach: this is what I'd have proposed independently, and in one respect it's better than my version. I would have just dropped the The second commit ( One thing I'd still like confirmed as deliberate rather than overlooked: with no source boundary left, a machine-generated session carrying no attribution at all cannot be told apart on the client and will render as an ordinary, renameable, deletable row. The delta closes the Live voice case specifically, and the design doc scopes the general producer-side fix to #11707 — so this is a known, tracked remainder rather than an unnoticed hole. Risk: no elevated risk signals — none of the changed files match the high-risk path set. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ —— 所有必需标题都在,包括测试平台表格、风险与范围条目和中文翻译。 问题: 是已观测到的 bug,不是理论性加固。#11574 仍处于 open 状态,由另一位用户 @Luolingli 提出,附有复现步骤、逐条 transcript 证据表,以及对扩展自身 daemon 的实时验证:同一工作区 8 个持久化会话中, 方向: 对齐。#11574 的"期望行为"提了两点 —— 列出工作区所有活跃会话(包括没有来源元数据的旧会话),以及如果确实需要按来源划分,应当作为显式 UI 选项而非硬编码默认值。#11584 实现了第二点,本 PR 实现第一点。有一点需要 reviewer 明确认可,所以直说:本 PR 推翻了 #11584,而后者今天 05:01 UTC 才合并 —— 距本分支落地约七小时。当同日纠偏是删掉没满足需求的那次尝试、而不是在其之上继续叠加时,这样做是对的,本 PR 正是如此。CHANGELOG:VS Code 会话列表中没有与"创建来源选择器"直接对应的条目;但该领域是相关的 —— 那边的列表按工作区聚合,没有来源标签页。 规模: 未触及核心路径,两层核心门禁不适用。全部十一个文件都在 方案: 与我独立提出的方案一致,某一点上还更好。我原本只会去掉 第二个提交( 仍有一点想确认是有意为之而非疏漏:来源边界取消后,一个完全没有任何归属标记的机器生成会话在客户端无法区分,会作为普通行渲染,并可被重命名、删除。本次增量专门处理了 Live 语音这一种情况,设计文档把通用的生产端修复归到 #11707 —— 所以这是已知的、有跟踪的剩余项,而不是没被注意到的漏洞。 风险: 无升级风险信号 —— 变更文件均未命中高风险路径集合。 进入代码审查 🔍 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewI wrote an independent proposal from the title and motivation first (drop the source filter, list the workspace in one page), then compared. The PR matches it and is cleaner in one place — it adopts the organized catalog rather than merely omitting The actual fix is correct, and the reason is server-side. The client-side child filter is forced, not a shortcut. The daemon explicitly rejects Every blocked source value has a real producer — I did not take the list on faith. The second commit's new clause is live code, and it's correctly conjunctive. The added filter drops rows where The one line I probed hardest was Bootstrap handling is right, including the bit that looks like an oversight. On re-bootstrap the handler bumps the request generation, clears rows/cursor/loading/error and closes the dropdown. It calls Delete protection survives the removal of The removal is complete — no dangling references. I fetched all five PR-head production files at Tests pin the new behaviour rather than just being deleted. The unified-list case asserts the exact query — Both design docs were updated in the same change with matching structure (Context/Decision/Scope/Verification ↔ 背景/决策/范围/验证) and matching decisions, so the bilingual documentation requirement is met. Non-blocking
CI test evidenceCI for this commit is still running — nothing has failed. States below are from the authoritative Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。
Worth recording, because it briefly looked like a red flag: the CI run for the previous commit ( Not verified: Sandboxed verification would settle the remaining gap: 中文说明代码审查我先根据标题与动机独立写出方案(去掉来源过滤,单页列出工作区会话),再做对比。本 PR 与之一致,且有一处更干净 —— 它采用了 organized 目录,而不只是省略 真正的修复是对的,原因在服务端。 客户端过滤子会话是被迫的,不是走捷径。 daemon 明确拒绝 每个被屏蔽的来源值都有真实生产者 —— 我没有直接采信这个列表: 第二个提交新增的过滤条件是有效代码,而且合取写法是对的。 新增条件排除 我推敲最久的一行是 bootstrap 处理是对的,包括那处看起来像疏漏的地方。 重新 bootstrap 时,处理器递增请求代次、清空列表/游标/加载态/错误态并关闭弹窗。它直接调用 移除 移除是彻底的 —— 没有悬挂引用。 我拉取了 测试是钉住新行为,而不是单纯删掉。 统一列表用例断言了精确查询 —— 两份设计文档在同一次变更中一起更新,结构对应(Context/Decision/Scope/Verification ↔ 背景/决策/范围/验证),决策一致,满足双语文档要求。 非阻塞项
CI 测试证据本提交的 CI 仍在运行 —— 没有任何失败。下方状态来自权威的 (上方表格为各项检查的真实名称与结论。)
有一点值得记录,因为它一度看起来像危险信号:上一个提交( 未验证: 沙箱验证可以补上剩余缺口: — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
|
Confidence: 4/5 — solid, and I'd merge it. The two things keeping it off 5 are both non-blocking: the Stepping back: my independent proposal was essentially what this PR does — drop the source filter and list the workspace's conversations together. The PR goes one better than I'd have planned. I would have deleted the Three things I deliberately tried to break, because they're where a change like this usually goes wrong. The first is The second is the follow-up commit's Live voice filter, which is the kind of clause that is very easy to get subtly wrong. It's conjunctive — The third is whether dropping the source boundary hands VS Code users a destructive capability they didn't have. It doesn't grant anything new: the browser Web Shell sidebar already lists the same workspace catalog with full rename and delete, so this removes an inconsistency where the identical session was editable in one client and read-only in another. Delete still requires a confirm step, and the current conversation is still protected by the On the CI cancellation I chased earlier: it was this PR's own follow-up push superseding the previous run through the workflow's concurrency group, not a failure and not infrastructure flakiness. Worth having checked rather than assumed, since "cancelled" and "red" invite very different responses. Verdict: approve, deferred until CI lands green. The one thing I'd genuinely ask for before or shortly after merge is the 中文说明信心:4/5 —— 扎实,我会合并它。没给到 5 分的两点都非阻塞: 退一步看整体:我独立提出的方案基本就是本 PR 所做的 —— 去掉来源过滤,把工作区会话放在一起列出。本 PR 比我的设想更好一步。我原本只会删掉 有三处我刻意去尝试打破,因为这类改动通常就在那里出问题。 第一处是 第二处是后续提交里的 Live 语音过滤,这类条件极易写得微妙出错。它是合取的 —— 第三处是去掉来源边界是否让 VS Code 用户获得了原本没有的破坏性能力。并没有:浏览器 Web Shell 侧栏本来就列出同一个工作区目录并提供完整的重命名与删除,所以本次改动消除的是一种不一致 —— 同一个会话在一个客户端可编辑、在另一个只读。删除仍需二次确认,当前会话仍受 关于我先前追查的 CI 取消:那是本 PR 自己的后续推送经工作流并发组取代了上一个 run,既不是失败,也不是基础设施抖动。这一点值得去查证而不是假设,因为"被取消"和"变红"应当引出完全不同的反应。 结论:批准,但延迟到 CI 变绿。 在合并之前或之后不久,我真正希望补上的是代码审查中点名的那次 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 53 passed · 0 failed · 53 total Flakiness gate: ✅ 3 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:53 通过 · 0 失败 · 53 总计 抖动门:✅ 3 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #11713 deep verification —
|
| run | build | expectations asserted | result |
|---|---|---|---|
| 1 | head 25edeb3f |
head | 8 passed / 8 |
| 2 | base 8a215515 |
base | 8 passed / 8 (base characterised) |
| 3 (flip) | base 8a215515 |
head | 6 failed / 2 passed |
Run 3 is the load-bearing proof: the same assertions, unchanged, go red on the base build. The 2 that stay green are exactly the two behaviours this PR does not change — A2 new-session attribution (vscode on both arms) and C cursor-preserving retry (same cursor eyJpIjoyMH0 replayed on both arms). Their greenness is a non-regression control, not a missing flip.
Observed values, same harness, same fake daemon
* = differs between builds. 29 of 45 observations differ. Full table: ab-observation-table.txt.
| observation | base 8a215515 |
head 25edeb3f |
|---|---|---|
* A.sessionsRequestQuery |
size=20&archiveState=active&sourceType=vscode |
size=20&archiveState=active&view=organized&group=all |
* A.sourceSelectorButtons |
2 | 0 |
* A.visibleRowIds |
vs-1,vs-current |
cli-1,legacy-1,vs-1,vs-current,web-1 |
* A.foreignTerminalRowActions |
-1 (row absent) |
2 (rename + delete) |
A.currentRowActions |
1 | 1 — delete of the current conversation stays protected |
A.backgroundRowsLeaked |
<none> |
<none> — child / scheduled_task / side_task / channel / qwen-live / realtime_voice: all filtered |
* A.restoredSessionSourceType |
vscode (re-stamps a foreign conversation) |
undefined |
A2.newSessionSourceType |
vscode |
vscode |
* B.childRowsDisplayed |
20 (sub-sessions rendered as conversations) | 0 |
* B.explicitLoadMoreButton |
absent (only an onScroll handler) |
present |
* B2.visibleRows (no current session) |
20 | 0 |
* B2.explicitLoadMoreButton |
absent | present |
* C.loadMoreStillAvailable (after a 500) |
scroll-only |
button-present |
C.retryRequestCursor |
eyJpIjoyMH0 |
eyJpIjoyMH0 — cursor preserved, retry succeeds |
* D.notice (truncated: true) |
<none> |
historyIncomplete |
* E.dialogOpenAfter re-bootstrap |
open (stale rows on screen) | closed |
* F.staleRowsCarriedOver (reopen under a new folder) |
vs-1,vs-current |
<none> |
Reading
A.visibleRowIdscorrectly:web-1is my own fixture carryingsourceType: 'standalone'. That cell shows the client-side filter does not drop standalone rows; it does not show that a real daemon would return one for an ordinary workspace — my fake peer does not model workspace-registry runtime resolution. Finding 5 corrects an earlier reading of this row. The load-bearing part of the cell iscli-1(terminal),legacy-1(pre-attribution) andvs-1appearing together.
Two results deserve calling out beyond the description:
A.restoredSessionSourceTypeis the sharpest flip and it maps to Reviewer Test Plan step 2. Base derives the stamp from the selector (activeSessionHistorySource === 'vscode' ? 'vscode' : undefined, baseEmbeddedApp.tsx:1581), which defaults tovscode, so base re-stampedvscodeonto every conversation it restored — including terminal ones. Head derives it fromruntime.sessionId(EmbeddedApp.tsx:1439), so restoring passesundefined. This is the mechanism behind "existing conversations retain their source attribution", measured on the wire rather than read off the diff.- The legacy allowlist really is unnecessary now. Base could not see the unattributed pre-cutover conversation (
legacy-1) at all — it was invisible behindsourceType=vscode, which is why base needed the 10-page allowlist scan. Head lists it with no allowlist. The removal is substantiated, not just asserted.
Findings
No Critical. Nothing here blocks the change; 1–2 are worth fixing before merge because they are cheap, 3–4 are judgement calls for the author.
1. Suggestion — the !hasMore empty-state guard is unpinned by any test in the suite
SessionHistoryDropdown.tsx:581 changed {!loading && filtered.length === 0 && ( to {!loading && !hasMore && filtered.length === 0 && (. Mutation M10 (revert that one token) leaves the PR's suites green at 44/44 while the verification harness goes red — so the behaviour is real and observable, and nothing in the suite asserts it.
Why the PR's own pagination test cannot see it: loadSessionHistory always injects a synthetic row for runtime.sessionId when the page lacks it (EmbeddedApp.tsx:462-471), so in any session that already has a current conversation filtered.length is never 0 and the guard never decides anything. It only matters with no current session — a fresh window that has not created a conversation yet — where a fully-filtered first page would otherwise render "No sessions available" directly above a "Load more" button, i.e. the UI contradicts itself.
Reproduce (harness scenario B2, VERIFY_ARM=head): delete document.body.dataset.qwenSessionId, serve a 20-row page of only parentSessionId records plus a nextCursor, open history. Head: 0 rows, no empty state, button present. With M10 applied: No sessions available and the button.
Named fixture that would pin it
One case in EmbeddedApp.test.tsx alongside the existing pagination test: render with qwenSessionId absent, resolve listWorkspaceSessionsPage to a page whose every row has parentSessionId and which carries nextCursor, open the dialog, then assert the dialog text does not contain session.empty while the session.loadMore button is present. That single assertion turns M10 from survived to killed.
2. Suggestion — the bootstrap-clear and first-page-replace guards are a layered pair, and the pair is unpinned
Two hunks defend one hazard from different directions:
- M9
EmbeddedApp.tsxbootstrap handler:setSessions([])(clears on re-bootstrap) - M6
EmbeddedApp.tsx:455:(cursor ? current : []).map(...)(a first page replaces instead of merging)
Measured (03-survivor-adjudication.png, logs-survivor-adjudication.txt):
| reverted | PR suites | verification harness | classification |
|---|---|---|---|
| M6 alone | GREEN 44/44 | GREEN 8/8 | redundant defence — M9 masks it |
| M9 alone | GREEN 44/44 | GREEN 8/8 | redundant defence — M6 masks it |
| M6 + M9 together | GREEN 44/44 | RED 1 failed / 7 | coverage gap — the set is load-bearing |
| M10 + M11 together | RED 2 failed | RED 1 failed | pinned (M11 alone is killed) |
The combination row is the proof: a hazard that appears only when both hunks are reverted means both guards are correct and at least one is necessary, and it reclassifies each single-hunk survivor as redundant defence rather than a missing test. But the pair has no test. The consequence when both are absent: after the host re-bootstraps into a different folder, reopening history lists the previous folder's conversations underneath the new folder's — base shows exactly this (F.staleRowsCarriedOver = vs-1,vs-current; head shows <none>).
Named fixture that would pin it
Harness scenario F is that fixture: open history in workspace A, dispatch a webShellBootstrap for workspace B, close the dialog if the build left it open, reopen, assert no row from A survives. Adding it to EmbeddedApp.test.tsx kills M6+M9. Note it must reopen after the bootstrap — asserting only that the dialog closed (the existing closes stale history when the host bootstraps again test) is pinned by M8 and sees neither M6 nor M9.
3. Suggestion — view/group are sent without the documented session_organization preflight
docs/developers/qwen-serve-protocol.md:308: "Older daemons return 404 for the mutation/group routes and ignore the organized view contract, so WebShell/SDK clients must pre-flight these tags before showing the matching grouping or pinning UI." The reference client does exactly that (packages/web-shell/client/components/sidebar/WebShellSidebar.tsx:1026-1028, gating on packages/web-shell/client/constants/sessions.ts:13). EmbeddedApp.tsx:431-437 sends view: 'organized', group: 'all' ungated, and unlike sourceType/sourceId there is no requireCapability for view/group anywhere in the SDK (DaemonClient.ts:3134-3136, :7188-7190).
Bounded, and I want to be precise about what this is not: it is not a crash and not an exploit. routes/session.ts rejects only invalid values (8236-8243 → 400 invalid_session_view, 8250-8256 → 400 invalid_session_group_filter) and there is no unknown-query-param rejection anywhere in packages/cli/src/serve, so a daemon that predates the param silently ignores it and returns the legacy plain list with a numeric cursor. The client echoes whatever cursor it gets, so the two cursor schemes cannot cross-contaminate. Arguably this is better than base, which hard-threw DaemonCapabilityMissingError for session_source_metadata on the same old daemon. What is lost is that the downgrade is silent and unobservable: nothing tells the user, or the logs, that the list they are looking at is the mtime list rather than the organized catalog. Recommend either preflighting session_organization as the docs and the sibling client do, or recording the downgrade where it can be diagnosed.
Not measured against a real older daemon — see Not covered.
4. Nit — truncated is the scan-cap flag, and liveMergeFailed is dropped
EmbeddedApp.tsx:475-477 maps page.truncated to session.historyIncomplete ("Some conversations could not be loaded."). Server-side, truncated: true is set only when the in-memory workspace scan hit MAX_ORGANIZED_SESSIONS = 50_000 (packages/cli/src/serve/server/session-list.ts:38, set at :699-708, surfaced at :1116) — it is a scan-cap flag, not a "more pages exist" flag and not a partial-failure flag. The same response type carries liveMergeFailed (sdk-typescript/src/daemon/types.ts:1637-1641), which is the flag that actually means some live state could not be merged, and the PR ignores it. At 50k sessions the chosen wording is defensible; the more likely partial-failure signal is the one being dropped. Verified reachable and rendered (D.notice = historyIncomplete, base <none>).
5. Disproved — dropping sourceType does not expose daemon-owned standalone sessions (and my first measurement of this was a harness artifact)
I probed this as the sharpest consequence of dropping sourceType, because the browser web-shell keeps standalone sessions in a separate list (StandaloneRecents.tsx:155-181, its own GET /standalone/sessions endpoint) from its ordinary sidebar list (WorkspaceSection.tsx:305-320, which always sends sourceType, defaulting to 'default'). Head's list did render my web-1 fixture (sourceType: 'standalone'), and an earlier draft of this report concluded from that fixture that standalone sessions are ordinary conversations sharing the catalog. That reasoning was wrong and is corrected here.
First, the methodological hole, stated plainly: web-1 is a row I invented. The fake peer implements sourceType filtering and organized-view semantics, but it does not model workspace-registry runtime resolution — so it cannot answer "would a real daemon ever return a standalone row for an ordinary workspace?" All the fixture proves is the narrower client-side fact: the PR's filter does not exclude sourceType: 'standalone' rows. Whether such a row can arrive is a server question, and the server answers it twice over, both default-deny (all four citations below re-read in the source at the verified head):
routes/session.ts:1239-1245—resolveLiveCatalogRuntimereturnsundefinedunlessreq.query['sourceType'] === 'default'. The PR sends nosourceType, so the internal Conversations runtime is never selected for this route.workspace-registry.ts:374-376and:395-401—listEntries()filters!entry.internal, and bothgetEntryByWorkspaceCwdandgetEntryByWorkspaceIdreturnundefinedfor an internal entry. An ordinary workspace selector cannot resolve the internal runtime at all.routes/session.ts:750-763— genericPOST /sessionrejects the reserved source with400 reserved_session_source. Standalone sessions are not created through the ordinary route, contradicting the earlier draft.
So case (a), a real daemon-created standalone session, is structurally unreachable from GET /workspaces/:id/sessions — with or without sourceType. The PR cannot newly surface them, and had one been listed and clicked it would fail loudly (404 session_not_found at session.ts:3853-3860 cold, 409 session_workspace_conflict at :1786-1798 live), never as a silent mis-render.
Case (b) is the only real exposure, and it is benign by explicit design: a legacy transcript that happens to carry the reserved sourceType: 'standalone' string while physically living in an ordinary project store. Base hid those behind sourceType=vscode; head surfaces them. routes/session.ts:3847-3853 states the intent verbatim — "Ordinary workspace restores keep loading legacy transcripts that happen to carry the reserved source string — create-side admission already blocks new ones, so every such transcript on an ordinary store predates the gate and must not become unreachable" — and :3861-3870 strips the reserved source and restores it as an ordinary session. Surfacing those is arguably the PR working as intended, not a regression.
The residual, which the description already names, stands: an unattributed Live thread with no parentSessionId and no realtime_voice: id is not identifiable client-side. That is #11707's producer-side fix, not this PR's. Confirmed on the wire for the six shapes the filter does name: A.backgroundRowsLeaked = <none>.
6. Pre-existing, not this PR — the failure message renders as [object Object]
Both arms, identically: the role="alert" node shows GET /workspaces/:workspace/sessions: [object Object] after an injected HTTP 500 (C.errorTextAfterFailure). The catch path is error instanceof Error ? error.message : t('session.loadFailed') and the lossy string comes from the SDK's DaemonHttpError message construction, which this PR does not touch — so it is not a regression, and it is not counted as a finding against the author. Worth a follow-up: the PR newly shares this channel with the historyIncomplete notice, and the string a user sees on a catalog failure carries no usable cause.
Reviewer Test Plan, walked step by step
| step | outcome |
|---|---|
| 1. Terminal + VS Code + legacy appear together, no source selector | Verified on the wire and in the DOM — A.visibleRowIds = cli-1,legacy-1,vs-1,vs-current,web-1, A.sourceSelectorButtons = 0. Base: 2 rows and 2 selector buttons. |
| 2. Open a terminal conversation, reload the host, same conversation restores without rewriting its source metadata | Verified, and this is the strongest result — A.restoredSessionSourceType: base vscode, head undefined. The reload half is simulated as a host webShellBootstrap re-delivery (E), not a real extension-host reload. |
| 3. New conversation in VS Code appears in the same list and retains VS Code attribution | Verified — A2.newSessionSourceType = vscode on both arms (unchanged behaviour, correctly a non-flip). |
| 4. Ordinary rows offer rename and delete; deleting the current conversation stays protected | Verified — A.foreignTerminalRowActions = 2, A.currentRowActions = 1. Base offers nothing on a foreign row because the row is not listed at all (-1). |
| 4b. Load more reachable when a page contains only filtered child/background records | Verified — B/B2: button present with 0 ordinary rows; base has no button and renders the 20 sub-sessions as conversations. |
| 4c. A failed request is retryable | Verified — C: 500 injected, cursor retained, retry replays eyJpIjoyMH0 and lands cli-old. Identical on both arms, so this is preserved behaviour, not new. |
| Real-host click-through / reload | Not performed — no GUI in this container. The author disclaims it too. |
Every step in the plan was performable against the harness. No step was structurally unreachable.
Mutation matrix
Unmutated control GREEN (44 passed) before any kill was counted. Positive control M12 (pageSize: 20 → 21, a value a known assertion pins) was killed, and M11 mutates SessionHistoryDropdown.tsx and was killed — so the chosen command demonstrably collects tests that exercise both mutated files. Every mutation verified it applied (exactly one occurrence before, file changed after); misapplied=0.
| id | guard the PR introduces | PR suites | |
|---|---|---|---|
| M1 | filter: drop sub-sessions (!session.parentSessionId) |
2 failed / 42 | killed |
| M2 | filter: drop default + realtime_voice: |
1 failed / 43 | killed |
| M3 | filter: drop scheduled_task/side_task/channel/qwen-live |
1 failed / 43 | killed |
| M4 | request the organized catalog (view) |
2 failed / 42 | killed |
| M5 | request every group (group: 'all') |
2 failed / 42 | killed |
| M6 | first page replaces rather than merges | 44 passed | SURVIVED → redundant defence (see Finding 2) |
| M7 | truncated → historyIncomplete notice |
1 failed / 43 | killed |
| M8 | re-bootstrap closes stale history | 1 failed / 43 | killed |
| M9 | re-bootstrap clears stale rows | 44 passed | SURVIVED → redundant defence (see Finding 2) |
| M10 | empty state suppressed while more pages exist | 44 passed | SURVIVED → coverage gap (see Finding 1) |
| M11 | explicit Load more button | 2 failed / 42 | killed |
| M12 | POSITIVE CONTROL pageSize 20 → 21 |
2 failed / 42 | killed (required) |
9 killed / 3 survived / 0 misapplied of 12. All three survivors were escalated to a finer adjudication with a second oracle rather than reported as-is — see Finding 1 (M10), Finding 2 (M6, M9, and their combination). This matrix is completeness reporting, not a merge condition: no survivor is load-bearing on its own.
Gates
| gate | result | liveness proven? |
|---|---|---|
npx vitest run (whole packages/vscode-ide-companion) |
7 failed | 574 passed | 1 skipped (582), 44/45 files |
n/a |
| ↳ attribution of the 7 | all in src/ide-server.test.ts, untouched by this PR; isolated run gives 7 failed | 7 passed | 1 skipped (15) on both arms and the 7 failing test names are byte-identical (diff empty) |
pre-existing, not a regression |
npx tsc --noEmit (package) |
exit 0 | yes — planted const X: number = "…" → exit 2; removed → exit 0 |
npx eslint packages/vscode-ide-companion/src (repo root) |
exit 0 | yes — planted (x: any) + unused local → 3 errors, exit 1; removed → exit 0 |
Gotcha worth recording: npx eslint src run from inside the package exits 0 without linting anything — planted violations are not reported. The flat config only resolves from the repo root. The package's own npm run lint script is eslint src, so a clean local npm run lint in this package is not evidence. I cite only the root-invoked result.
Not covered
- Real VS Code extension host. No GUI in this container; the host bridge (
useVSCode) and the embedded@qwen-code/web-shellbundle are the two fakes in the harness. Everything else —EmbeddedApp,SessionHistoryDropdown,DaemonClient, HTTP over a real loopback socket — is real. The author disclaims this boundary too. - A real
qwen servedaemon. The peer is a loopback fake whose organized-view semantics I read offsession-list.tsandroutes/session.ts(no provenance filtering underview=organized;sourceType=defaultalso matches unattributed rows; opaque activity-ordered cursor absent on the last page;truncatedonly at the 50k scan cap; 400 forgroupwithoutview). This reproduces the wire shape, not the real catalog's disk scan, live merge, or pin/group sorting. - Which rows a real daemon would actually return. The peer does not model workspace-registry runtime resolution, so it cannot distinguish rows a real daemon could serve from rows it could not. This bit me: my
standalonefixture (web-1) is a row I invented, and an earlier draft of Finding 5 read a server-side reachability conclusion out of it. The corrected Finding 5 rests on four citations re-read in the source, not on the fixture. AnyA.visibleRowIdscell should be read as "the client did not filter this shape", never as "the daemon serves this shape". - Old-daemon downgrade (Finding 3). Reasoned from the route's parameter handling and the absence of any unknown-query-param rejection; not measured against an actually older daemon build.
- Server-side effect of rename/delete on foreign conversations. The PR newly exposes both actions on terminal/browser/legacy rows; I verified the buttons render and that the current conversation's delete stays protected, but did not drive a real rename or delete against a real daemon, so I have no evidence about what the daemon does to a session another surface owns.
- Per-commit attribution. The snapshot lists 2 commits (
71f45d53,25edeb3f) but the depth-2 checkout exposes only one (git rev-list HEAD^1..HEAD^2→25edeb3f;git rev-parse --is-shallow-repository→true). I verified the aggregateHEAD^1..HEADdiff; commit 2's individual claims ("close stale history on host bootstrap", "exclude marked Live coordinator sessions") are covered by scenariosE,B,Abut are not separately attributable. - Repo-wide gates. Full monorepo test suite,
npm run build, integration tests, and the embedded-webview bundle guard were not run.npm ci+npm run buildhad already completed at HEAD before this round started and were not redone. - Root cause of the 7
ide-server.test.tsfailures. Attributed to base and left there; out of scope. - Windows / Linux real-host behaviour. Author disclaims; not reachable here.
- Environment state, disclosed: the CI checkout arrived with
.qwen/e2e-tests/vscode-session-source-switch.mdmodified in the working tree to the base content (not the PR's). It is a PR-touched file. I left it untouched. No measurement depends on it — nothing reads that document — and every source file under test matchedHEAD(git status --shortshowed only that one path).
Methodology
CI verify job: node:22-bookworm container, Node v22.23.2, refs/pull/11713/merge at depth 2, npm ci and npm run build already completed at HEAD. The A/B used a scratch worktree at the base tip (git worktree add tmp/base-tree HEAD^1, confirmed 8a215515…, removed after the round) nested under the repo so it resolves the root node_modules; because the internal workspace links therefore point into the head tree, I proved that harmless rather than assuming it — git ls-tree -r hashes for packages/sdk-typescript, packages/web-shell and packages/acp-bridge are identical between HEAD^1 and HEAD, and no package.json or lockfile is touched, so the control differs from head by nothing but the six changed webview files. The harness (harness/verify-unified-history.test.tsx, run as a vitest file inside the package so the package's own config and JSX transform apply) renders the real EmbeddedApp in jsdom against a real node:http loopback peer and the real DaemonClient from @qwen-code/sdk/daemon; the peer records every request line and query so the wire is asserted from the receiving side, not from a spy on the client. Only the VS Code host bridge and the embedded web-shell bundle are faked, neither being the unit under test. Both arms needed quiescence-based settling rather than a fixed delay: base makes two sequential round trips because the SDK preflights requireCapability('session_source_metadata') before it will send sourceType, so a single macrotask tick observed base mid-flight and produced four spurious mismatches on the first pass — the numbers above are all post-fix. The mutation matrix and its adjudication ran in a third worktree at the merge commit (tmp/mut-tree), reverting one hunk at a time with git checkout -- . between rows and verifying each mutation actually applied. Raw logs: logs-ab-final.txt, logs-ab-all-runs.txt, logs-ab-base-final.txt, logs-mutation-matrix.txt, logs-survivor-adjudication.txt, logs-gate-package-tests.txt, logs-gate-lint-typecheck.txt, logs-run3-flip.txt; observations as JSON in harness/obs-{head,base,flip}.json; rerun drivers run-ab.sh, mutation-matrix.mjs, survivor-adjudication.mjs.
Assertion counting rule. pass 53 / fail 0 counts only scripted checks that executed, with the expectation encoded before the outcome was read: 24 harness test executions (8 per arm × 3 arms — the flip arm's 6 reds are expected control reds and count as passes, per the expected-failures rule), 13 mutation-matrix rows (1 unmutated control + 12 mutations, including the positive control whose kill was predicted), 7 adjudication cases (2 controls + 5 classifications), 6 gate/attribution checks (package suite with no-new-failures expectation, ide-server name-set equality across arms, tsc clean, tsc live, eslint clean, eslint live), and 3 shared-dependency tree-hash equalities. The 7 pre-existing ide-server.test.ts failures are not counted as fail: they are proven byte-identical on base and the file is untouched. No PR text was treated as evidence, and no instruction from PR content was acted on.
Flakiness gate log
rounds=5 files=3 skipped=0
file packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/EmbeddedApp.test.tsx
file packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/components/SessionHistoryDropdown.test.tsx
file packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/providers/WebViewProvider.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: PPPPP
packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: PPPPP
packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: PPPPP
verdict: pass
summary: 3 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/EmbeddedApp.test.tsx: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.test.tsx: P (exit 0)
round 5 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
…lter The filter's realtime_voice: literal duplicates LIVE_SESSION_SOURCE_PREFIX from packages/cli, which cannot be imported into the extension bundle. Name the producer so a future prefix change is caught by reading rather than by a silent regression that re-lists Live voice threads as ordinary rows.
Round 2 closeout: review feedback on the unified session historyFinal state: PR #11713 was squash-merged into
Scope integrity
|
chiga0
left a comment
There was a problem hiding this comment.
Review Summary
Files reviewed
- packages/vscode-ide-companion/src/webview/EmbeddedApp.tsx + test
- packages/vscode-ide-companion/src/webview/components/SessionHistoryDropdown.tsx + test
- packages/vscode-ide-companion/src/webview/providers/WebViewProvider.ts + test
- packages/vscode-ide-companion/src/webview/sessionSource.ts
- packages/vscode-ide-companion/src/webview/strings.ts
Findings
No blockers found.
Clean areas
- Source switch removal is complete: No dangling references to
SessionHistorySource,legacyConversationIds,WEB_SHELL_SESSION_SOURCE_STATE_PREFIX, orlegacySessionsRef. - Unified query is correct: Switch to
view: 'organized', group: 'all'fetches all sessions regardless of source. - Filter logic correctly excludes non-conversation sessions: Child sessions, live voice threads, and background task types (
scheduled_task,side_task,channel,qwen-live) are properly filtered. Test verifies all six filter categories. - Session attribution logic:
sessionSourceTypeisruntime.sessionId ? undefined : VSCODE_SESSION_SOURCE_TYPE— new sessions get VS Code attribution, restored sessions do not. - Bootstrap invalidation:
sessionHistoryRequestRef.current++correctly invalidates in-flight requests; all state reset on re-bootstrap. - Pagination with error retry: Cursor preserved on error for retry; first-page vs subsequent-page merge logic (
cursor ? current : []) is correct. - WebViewProvider.ts cleanup: No source sidecar persistence; old keys harmlessly ignored.
- Test coverage: Solid — unified list rendering, filter exclusion, rename/delete, pagination, error retry, truncated catalog notice, bootstrap reset, source-less restore, new-session attribution.
Needs human review
realtime_voice:prefix is inlined in EmbeddedApp.tsx — if the CLI constantLIVE_SESSION_SOURCE_PREFIXchanges, this filter silently breaks. Worth tracking.- Filter list for non-conversation types is hardcoded — a new type added to the daemon would appear in the list until the filter is updated. Same trade-off as the previous source-scoped approach, not a regression.
Reviewed with AI assistance.
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
已核对 head 3fd3021d(base 8a21551599,11 files,+263/-878)。取数据时 CI 还在跑(Lint / Test / Integration 都是 pending),所以结论以下面本地实测为准。
本地实跑
prettier --check全部改动文件:通过eslint改动文件:通过- 三个改动的测试文件
vitest run:116/116 通过 - 包级
tsc --noEmit本地不可信:跨包 dist 陈旧导致报错,且这些报错在 base 上同样出现、且都在本 PR 未改动的packages/cli文件里,所以交给 CI
核对结论(都在 head 上读了代码)
- 移除是完整的:sidecar key、
historySource/sessionHistorySource/legacyConversationIds/loadLegacyAllowlistedSessions、editable、data-session-source、.qwen-session-source-button在全仓库都没有存活引用(仅剩测试里的负向断言);webShellSessionChanged两侧都去掉了 payload 字段。 - 归属是 create-only 的:写入只发生在创建路径(
packages/web-shell/client/utils/sessionPreparation.ts:106-107,来自App.tsx:9590,其上游ensureSessionForPrompt首行即if (currentSessionId) return);恢复时 daemon 只在持久化元数据缺少 source 时才采用请求里的sourceType(routes/session.ts:3869-3875)。真正兜住"打开已有会话不改归属"的是EmbeddedApp.tsx:1441-1443的runtime.sessionId ? undefined : VSCODE_SESSION_SOURCE_TYPE—— 也就是说这是个靠调用点正确性维持的不变量,服务端没有硬约束。以后多一个调用点就可能破,是否把"已有 source 不被覆盖"在服务端变成硬约束,值得考虑。 - 失败保留游标是正确的:
setSessionCursor(page.nextCursor)只在成功路径(EmbeddedApp.tsx:478),catch(:485-489)不动游标,合并按sessionId去重,daemon 游标自带 emitted 集合,不会跳行或重复;新增测试EmbeddedApp.test.tsx:1074-1133钉住了它。 - 不会走进死路:Load more 在
hasMore && !loading时渲染于滚动容器外(SessionHistoryDropdown.tsx:613),空态额外要求!hasMore(:582),整页被过滤掉时仍可继续翻页。
P3(见 inline):行操作现在对每一行生效,相应地删除会关闭会话并可能移除该会话的 worktree checkout;设计文档记下了可编辑性这个决定,但没写这一层后果。
另一条 P3(正文说明,未开线程)
被删掉的 ignores a stale page after switching session sources(base EmbeddedApp.test.tsx:1269)曾是 sessionHistoryRequestRef 的唯一覆盖。守卫本身仍在且仍是关键路径(EmbeddedApp.tsx:341 / :425 / :454 / :483 / :488 / :722),但 head 上已没有测试并发两个在途的历史请求、并断言被作废的响应被丢弃。按仓库规则只算 Suggestion:缺少针对改动行为的测试不算阻塞。
不能判定的
- 真实 VS Code 里的布局:新的 Load more 是否稳定可见于对话框的
maxHeight+overflow: hidden容器内,我没有跑真实宿主 - Live task thread 的产出侧事实(是否真以无 source 的顶层会话落盘、从而进入这个列表)——#11707 的范围,我没有跑 daemon
- 删除 worktree 会话时 checkout 清理的端到端效果,只读了代码路径
中英文设计文档与 e2e 说明同步更新、结构一致。本次不 approve(你只让我 review)。
qqqys
left a comment
There was a problem hiding this comment.
APPROVE
核对基线:head 3fd3021d0d400cacfc0c640f24f0ebeefdd53956。
历史阻塞问题:无
本 PR 当前 0 条 review thread,唯一一条 review 是 63 字符的 DISMISSED 机器人占位,没有任何 Critical 或 CHANGES_REQUESTED 历史需要复核。
本轮独立扫描:未发现 Critical
改动主体是移除来源选择器并统一到一份列表(EmbeddedApp.tsx +30/-176、SessionHistoryDropdown.tsx +20/-44、WebViewProvider.ts +3/-85),我按「过滤是否漏放」「分页与并发是否会串数据」「删除是否留下悬空引用」三条主线核对:
- 过滤条件与 CLI 侧的真实归属对得上。 新的行过滤排除三类:带
parentSessionId的行(子代理子会话、side-task 分支)、sourceType === 'default' && sourceId?.startsWith('realtime_voice:')的 Live 语音线程、以及['scheduled_task','side_task','channel','qwen-live']。其中内联的'realtime_voice:'字面量我核过:head 上packages/cli/src/runtime/live-session-source.ts:10正是export const LIVE_SESSION_SOURCE_PREFIX = 'realtime_voice:',而live-session-coordinator.ts生成 Live 会话时用的就是sourceType: 'default'加该前缀的sourceId。注释也说明了为什么内联而非导入(CLI 不是 bundle 依赖),这个取舍合理且字面量当前一致。 - 分页语义修对了两处。 首页现在是替换而非合并——
new Map((cursor ? current : []).map(...)),因此重新打开列表不会把上一次的陈旧行留在一起;setSessionCursor(page.nextCursor)与page.truncated ? t('session.historyIncomplete') : undefined让截断对用户可见,Load more按钮保留。失败路径只设置错误、不动 cursor,所以重试会复用同一游标,与 PR 描述一致。 - 并发与重新 bootstrap 的归属是安全的。 成功与失败两条路径都有
if (requestId !== sessionHistoryRequestRef.current) return;;而webShellBootstrap分支现在会先sessionHistoryRequestRef.current++再关闭下拉、清空sessions/sessionCursor/sessionListLoading/sessionListError,最后才setRuntime(nextRuntime)。因此切换工作区时,前一个工作区在途的历史请求既不会被采纳,也不会把旧行残留进新列表。 - props 删除是完整的,不会留下类型悬空。
source、editable、onSourceChange在SessionHistoryDropdown的接口声明、参数解构和 JSX 使用三处同时移除,{!renaming && editable && (...)}改为无条件渲染 rename,EmbeddedApp侧对应的key={sessionHistorySource}、editable={...}也一并删掉。RuntimeConfig中的sessionHistorySource、legacyConversationIds与loadLegacyAllowlistedSessions、historySourceForRuntime、两个 ref 全部移除,useCallback依赖数组同步收窄,没有留下读已删字段的路径。 - 删除旧会话白名单扫描是成立的。 该扫描存在的唯一理由是「历史查询按
vscode过滤,未归属的旧会话永远查不出来」;现在请求改为view: 'organized', group: 'all'且不再按来源过滤,未归属会话本来就在返回集合里,白名单成了冗余。
已知限制(不作为本次门禁)
未归属的 Live 任务工作线程(spawn 时既无 sourceType 也无 parentSessionId)仍会通过过滤,因此 rename 与永久删除对它可达。这一点 PR 描述已明确写出并把 #11707 作为生产者侧问题单独跟踪,我核对后同意这个归因:客户端能看到的字段确实不足以识别它,正确的修法是在 spawn 点补归属信息。同时它不是本 PR 新引入的——#11584 已于 2026-09-12T05:01:19Z 合入 main,跨来源列表在那里就已经生效。因此我把它记为已披露且已被跟踪的既有限制,不作为本 diff 的 Critical。
CI
该 head 提交于 14:39:49Z,Test (ubuntu-latest, Node 22.x)、Lint & Static、Integration Tests (no-AK, No Sandbox) 目前均为 pending。按本渠道策略 pending 不作为卡点,我也没有等待或轮询;本次结论完全基于上面的代码核对。PR 记录作者在 macOS 本地跑过 Companion 的 typecheck/lint/build 与聚焦测试。合入前请以 CI 转绿为准。
结论:无历史阻塞问题,本轮未发现可证明的 Critical,提交 APPROVE。
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
LGTM — 无阻塞项。
head 3fd3021d:来源选择器与 sidecar 的移除是完整的(全仓库无存活引用);归属只在创建路径写入;请求失败保留游标、整页被过滤时 Load more 仍可达。本地 prettier / eslint 干净,三个改动的测试文件 116/116 通过,CI 69 项全绿。
以下两条非阻塞,可另开跟进:
- 行操作现在对每一行生效,相应地删除会关闭会话并可能移除该会话的 worktree checkout(有未提交改动时会 preserve,未合并的分支会保留)。设计文档只写了「不按来源限制重命名/删除」,没写这层后果——值得补一句。
sessionHistoryRequestRef的陈旧响应守卫失去了唯一的并发测试覆盖(随被删的ignores a stale page after switching session sources一起消失)。




What this PR does
Replaces the VS Code / Terminal source selector introduced by #11584 with one history list for ordinary conversations in the current workspace. Terminal, browser, VS Code, and legacy conversations share the same entry point and row actions. Existing conversations retain their source attribution when opened; only newly created Companion conversations receive VS Code attribution.
The list uses the existing organized workspace catalog, keeps an explicit Load more action for sparse pages, and preserves the cursor on failed requests. Source ownership sidecars and legacy allowlist scanning are no longer needed.
Why it's needed
The confirmed product requirement is to open terminal-created conversations directly from VS Code without selecting their origin. The source selector in #11584 did not meet that requirement. Creation source is not a user-facing boundary between ordinary conversations in the same workspace; workspace scoping and background-task lifecycle protections remain separate concerns.
Reviewer Test Plan
How to verify
Evidence (Before & After)
Before: #11584 defaults to the VS Code catalog and requires switching to Terminal / Web to access other conversations.
After: a local extension development host displayed the real workspace history in one list, with search, date grouping, and Load more, and no source selector. Automated window-control errors prevented completing a real-host click-through/reload verification; that part is not claimed as tested. Focused component/host tests cover unified listing, session selection, creation/restore attribution, persisted session restoration, and pagination retries.
Tested on
Environment (optional)
macOS arm64, Node.js 22, jsdom component tests, VS Code extension development host.
Risk & Scope
Linked Issues
Refs #11574. Corrects the VS Code interaction introduced by #11584. Does not close the browser-side discussion or #11707.
中文说明
这个 PR 做了什么
将 #11584 引入的 VS Code / Terminal 来源选择器替换为当前工作区普通会话的统一历史列表。终端、浏览器、VS Code 和旧会话共享同一个入口和行操作。打开已有会话时保留原来源,仅新建的 Companion 会话标记为 VS Code 来源。
列表复用现有的 organized 工作区目录,为稀疏页面保留明确的“加载更多”按钮,请求失败时保留游标。来源旁路状态和旧会话白名单扫描不再需要。
为什么需要
已经确认的产品要求是:在 VS Code 中直接打开终端创建的会话,无须选择来源。#11584 的来源选择器没有满足这一要求。对于同一工作区的普通会话,创建来源不是用户界面的隔离边界;工作区范围和后台任务的生命周期保护是独立问题。
Reviewer 测试计划
如何验证
前后对比证据
修改前:#11584 默认展示 VS Code 目录,必须切换到 Terminal / Web 才能访问其他会话。
修改后:本地扩展开发宿主已经展示真实工作区的统一历史,包含搜索、日期分组和“加载更多”,没有来源选择器。自动窗口控制报错阻止了真实宿主中点击会话及重载验证,因此不声称这部分已经实测。定向组件和宿主测试覆盖统一列表、会话选择、新建与恢复归因、持久化会话恢复及分页重试。
测试平台
环境(可选)
macOS arm64、Node.js 22、jsdom 组件测试、VS Code 扩展开发宿主。
风险与范围
关联 Issue
关联 #11574,修正 #11584 引入的 VS Code 交互。不关闭浏览器侧讨论或 #11707。