Qwen Code WebShell 独立会话(Standalone Chats)产品化落地指南:从显式会话上下文到完整聊天产品 UI
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文是 Qwen Code 仓库中docs/plans/2026-08-29-standalone-pr6-webshell-ui.md(PR6)的技术实现解析,围绕"如何把 PR5 交付的显式 daemon 会话上下文({ kind: 'standalone' })转化为 WebShell 中可见、可操作、可维护的独立聊天产品"展开。读完本文,你将掌握:独立会话与工作区会话在入口路由、能力协商、延迟创建、Recents 生命周期、项目专属表面隐藏、深链解析、异常状态呈现等七个层面的完整实现方案,以及对应的源码位置、契约边界与验证策略,可直接用于理解或评审packages/web-shell中的独立会话功能。
一、背景与目标:为什么需要 Standalone Chats
在独立会话功能出现之前,Qwen Code daemon 在客户端未传cwd创建会话时,会隐式地把"主工作区(primary workspace)"当作目标。这带来两个问题(见 standalone-daemon-sessions.md 的 Problem 小节):
- 顶层 New Chat 被项目绑定:即使用户没有选择任何项目,新建会话依然落在某个项目目录上;
- 会话生命周期被项目目录绑架:目录一旦被移动或删除,客户端只能报告"当前工作目录不存在",无法继续会话。
PR6 的目标是把 PR5 已在 daemon/SDK/provider 层就绪的独立会话能力,变成 WebShell 中的完整产品:
- Home 与全局 New Chat 创建独立会话(在具备能力的 daemon 上);
- 项目作用域的入口保持工作区绑定;
- 新增顶层 Recents 分组,对独立会话执行完整生命周期管理(重命名、导出、归档、删除等);
- 独立聊天隐藏所有项目专属表面(Git 状态、分支、项目文件、pin/group 等);
- 任何情况下都不回退到主工作区——除非 daemon 缺少
standalone_sessions_v1能力。
PR6 是纯 UI 改动,只涉及packages/web-shell:不新增 daemon 路由,不新增 SDK 方法。这是 #9811 WebShell cutover(fe34a5cf22)把 PR5 的 provider 表面从packages/webui迁入packages/web-shell/client/daemon/session/之后才可能实现的——provider 与产品 UI 同包,不存在跨包发布顺序问题。
二、PR6 消费的合并契约(daemon / SDK / provider 三层)
PR6 本身不加契约,只消费 PR3/PR4/PR5 已合并到origin/main的 v1 契约。理解这三层,是读懂后续 UI 逻辑的前提。
2.1 Daemon 层(packages/cli/src/serve)
路由由 registerStandaloneSessionRoutes 注册,核心路由族包括:
| 方法 | 路径 | 说明 |
|---|---|---|
| POST / GET | /standalone/sessions | 创建 / 分页列表(cursor、size、archiveState过滤) |
| GET | /standalone/sessions/:id | 精确查找(返回 summary 或creating状态) |
| POST | /standalone/sessions/:id/load | 加载 |
| POST | /standalone/sessions/:id/resume | 恢复 |
| POST | /standalone/sessions/:id/repair-directory | 修复私有工作目录 |
| PATCH | /standalone/sessions/:id/metadata | 重命名等元数据修改 |
| GET | /standalone/sessions/:id/export | 导出 |
| POST | /standalone/sessions/:id/archive/unarchive | 归档 / 取消归档(批量) |
| POST | /standalone/sessions/:id/delete | 删除(批量) |
能力标签standalone_sessions_v1在 capabilities.ts 注册,只有在完整依赖集(manager、service、route、managed-directory 生命周期、内嵌createServeApp配置)全部安装时才对外宣告——单一构建常量不足以宣告该能力。
契约的严格性体现在请求体校验:daemon 侧用requireExactBody对创建请求做白名单校验,只允许sessionId、modelServiceId、approvalMode三个字段(见 standalone-sessions.ts)。任何 workspace-only 请求键(cwd、workspaceCwd、sourceType、sourceId、sessionScope、branch、worktree)都会得到400 invalid_request。
2.2 SDK 层(packages/sdk-typescript/src/daemon)
standalone-sessions.ts 定义了能力常量与完整类型表面:
STANDALONE_SESSIONS_CAPABILITY = 'standalone_sessions_v1',另有STANDALONE_SESSION_OPTIONS_CAPABILITY;DaemonClient提供createStandaloneSession、listStandaloneSessions(Page)、getStandaloneSession(精确查找:creating / summary / not-found 三态)、loadStandaloneSession、resumeStandaloneSession、repairStandaloneSessionDirectory、renameStandaloneSession、exportStandaloneSession、archiveStandaloneSessions、unarchiveStandaloneSessions、deleteStandaloneSessions;- 所有独立会话方法都是能力门控的(capability-gated);
createStandaloneSession由客户端生成 UUID,从不自动重试,响应丢失时包装为DaemonStandaloneCreationOutcomeUnknownError,携带sessionId与精确查找用的recovery信息——这是后面"创建结果未知恢复状态机"的 SDK 基础;- 批处理方法返回部分成功信封:归档返回
archived / alreadyArchived / notFound / errors,删除返回removed / notFound / errors / fileCleanupPending(即使 HTTP 200 也可能是部分成功); DaemonSessionClient.createStandalone / loadStandalone / resumeStandalone静态方法与{ kind: 'standalone' }恢复策略位于 DaemonSessionClient.ts,恢复分派在 :710。
2.3 Session Provider 层(packages/web-shell/client/daemon/session,post-#9811)
DaemonProductSessionContext(types.ts:71)是一个判别联合:{ kind: 'workspace'; cwd } | { kind: 'standalone' } | { kind: 'live' };DaemonSessionProviderProps.sessionContext(types.ts:160)与遗留的workspaceCwd并存,两者冲突时在任何请求发出前直接抛错;createSession / loadSession / resumeSession动作接受options.sessionContext(types.ts:438-471)。独立会话创建不重试,且豁免通用的 30 秒动作超时;Live 创建被此 provider 拒绝;- 独立创建通道
createDetachedStandaloneSession(DaemonSessionProvider.tsx:3968)只把modelServiceId和approvalMode转发给DaemonSessionClient.createStandalone——workspace-only 字段永远不会到达 daemon; - 连接状态暴露
sessionContext与standaloneSession?: DaemonStandaloneConnectionState({ projectlessOutputDirectory?, workingDirectory?, creationRecovery?, errorCode? }),目录错误码被复制进standaloneSession.errorCode供 UI 分支; session-context.ts提供resolveProviderSessionContext、resolveActionSessionContext、sessionContextKey、restoreSessionContextMatches、getDaemonErrorCode、isDaemonErrorExplicitlyNonRetryable、getStandaloneConnectionState等辅助函数;- PR5 的隔离保证已内建:standalone/Live 会话在 provider 内跳过工作区 providers、skills、ACP preheat、Git status 与工作区事件失效。
三、入口点路由:每个可见入口绑定显式上下文
PR6 把每个可见入口点接到显式上下文(表来自设计契约,standalone-daemon-sessions.md):
| 入口点 | 新会话上下文 |
|---|---|
| Home / 全局 New Chat | standalone |
| 已选择或锁定项目内的 New Chat | workspace |
| Goals 与 Git 入口 | workspace |
| 当前会话的 New Chat | 继承显式上下文 |
| Live Voice | live |
实施映射(packages/web-shell/client,以下行号以 #9811 cutover 后的origin/main为准):
- Home / 全局 New Chat:侧边栏主导航
handleNewSession()(WebShellSidebar.tsx)→createNewSession()(App.tsx)。具备能力时设置 pending context{ kind: 'standalone' }; - Home 首条 prompt 路径:
ensureSessionForPrompt(App.tsx:6179)消费 pending context,而非解析 locked/selected/primary cwd; - 独立创建省略 workspace-only 字段:
createAndAttachSessionForPrompt总是传sourceType: WEB_SHELL_SESSION_SOURCE_TYPE,可能传worktree/branch——daemon 路由与 provider 独立通道都拒绝这些字段,因此创建流程分支:standalone pending context 只 dispatchcreateSession({ sessionContext: { kind: 'standalone' }, approvalMode? }),测试会断言两个分支的精确请求体; - 每个新会话调用方都传显式意图:全局 New Chat、当前会话 New Chat、
/new、/clear、新会话建议、shell API 都汇聚到同一个createNewSession()。调用方契约变成显式意图:{ kind: 'global' }(Home/全局 New Chat → 具备能力时 standalone)、{ kind: 'inherit' }(当前会话入口)、或{ kind: 'workspace', cwd }(项目 New Chat、Goals)。意图绝不从 undefined cwd 或当前状态推断; - 项目作用域 New Chat:
handleNewSession(wsCwd)(WebShellSidebar.tsx:5514)→createNewSession({ kind: 'workspace', cwd: wsCwd }); - Goals:
onCreateGoal(App.tsx:13223)先按现状解析目标 cwd(locked ?? selected ?? primary),再调createNewSession({ kind: 'workspace', cwd: resolved }, { keepView: true })+ensureSessionForPrompt,保持工作区绑定,忽略任何 standalone pending context,绝不传裸undefinedcwd; - Git:
resolveSessionForWorkspace(App.tsx:6661)与 composer Git chips(由gitModeEligible门控,App.tsx:6763)保持工作区绑定; - Scheduled Tasks 等工作区维护调用方:
ScheduledTasksDialog.onCreateViaChat解析显式工作区 cwd 并传{ kind: 'workspace', cwd }——持久化独立会话调度超出范围,活跃 standalone 上下文绝不能在此选 inherit/global; - 当前会话 New Chat(
{ kind: 'inherit' }):继承活跃connection.sessionContext——standalone → pending standalone,workspace → 带当前 cwd 的 pending workspace。Live 当前会话走既有 Live 专属startLive('new')路径(client/live/useLiveVoice),绝不静默改道为 standalone; - Split View 在 PR6 中排除 standalone 会话(所有入口):pane 身份以裸
sessionId字符串持久化(split URL、sessionStorage),pane 归属从工作区 catalog 推导。只挡 Recents 行不够——全局 footer 按钮、?split=、sessionStorage 恢复、外部受控splitSessionIds都能注入裸 standalone ID。PR6 对非工作区有效上下文禁用全局 Split View 入口,并在所有 seed/URL/storage/受控 prop 边界用fail-closed 的异步分类器在 pane 挂载前清洗 standalone ID:具备能力的 daemon 上,未映射 ID 用getStandaloneSession精确核查——standalone 或creatingID 被拒绝,只有404才继续走工作区归属解析; - 上下文传播:
WorkspaceSessionProvider(WorkspaceSessionProvider.tsx)改传解析后的sessionContextprop(provider 在兼容边界归一化遗留 cwd,两种形式在 cutover 期间都有效); - 加载既有会话:
loadSidebarSession(App.tsx:9114)对 standalone 条目调用loadSession(id, { sessionContext: { kind: 'standalone' } })。
只传workspaceCwd的遗留调用方保持现状行为不变。
四、能力协商的三态行为(capability dual behavior)
能力读取必须来自useWorkspace().capabilities.features——DaemonWorkspaceProvider在启动时(任何会话存在之前)调用一次client.capabilities(),因此入口点(在会话之前触发)不能依赖会话作用域的connection.capabilities。比较对象是@qwen-code/sdk/daemon的STANDALONE_SESSIONS_CAPABILITY。
能力值是三态的,且只有一种状态允许回退到遗留行为:
| 状态 | 行为 |
|---|---|
Loading(启动请求在途,capabilities === undefined) | Home 与全局 New Chat等待——standalone 决策被禁用而非默认化。把 loading 当 absent 处理,会在具备能力的 daemon 上静默创建主工作区会话 |
| Loaded-absent(旧 daemon) | 保留遗留行为:全局 New Chat 指向主工作区,不调用任何 standalone 路由;允许提示"独立聊天需要升级 daemon" |
| Load error | fail-closed:显式重试,能力解析前不创建任何上下文的会话 |
| Loaded-present,但创建失败 | 展示结构化错误并保留 standalone 意图供重试。503归属/运行时失败或409目录冲突后,绝不静默创建主工作区会话,绝不降级上下文 |
五、延迟创建与 Pending Context
WebShell 采用惰性创建:Home 流程中createNewSession只清空界面,ensureSessionForPrompt在提交时才真正物化 daemon 会话。PR6 在此延迟意图旁存储显式 pending context:
- App 级新增
pendingSessionContext: DaemonProductSessionContext | undefined状态(App.tsx),由上述每个入口点设置。pending context 是DaemonProductSessionContext,绝不是裸undefinedcwd——"还没有 cwd"不等于 standalone 语义; ensureSessionForPrompt(App.tsx:6179)优先消费 pending context:{ kind: 'standalone' }→ standalone 创建分支(无 workspace-only 字段);工作区 pending context → 沿用精确 cwd 路径;无 → 遗留 locked/selected/primary 解析;- pending context 在 attach 提交前一直保持权威:普通
409/503创建失败不发布 standalone 连接上下文,且被清空的 provider 可能仍持有旧工作区上下文——所以路由与草稿 UI 门禁读取pendingSessionContext ?? connection.sessionContext,pending 状态只在 create-and-attach 成功完成后清除;失败尝试保留 pending standalone 意图(及其错误)供重试; loadSidebarSession/switchWorkspace(App.tsx:8703)用已加载会话的显式上下文覆盖pending context;- provider 自身的延迟路径
shouldDeferInitialSessionCreation在底层保持不变。
六、Recents 分组与生命周期操作
6.1 独立 catalog 通道
侧边栏新增顶层Recents分组(StandaloneRecents.tsx,挂载于 WebShellSidebar.tsx)。现状不存在跨上下文列表:useScopedSessions/useWebShellSessions(session-catalog/session-catalog-hooks.ts)以SessionCatalogQuery { routeKind, workspaceCwd }(session-catalog/session-catalog-store.ts:11)为键,天然按工作区作用域。PR6 新增独立 catalog 通道,由listStandaloneSessionsPage(游标分页 +archiveState过滤)供数,而不是把 standalone 行塞进工作区 catalog store。Live 会话与项目会话保留各自分组;standalone 子会话(带parentSessionId的 sub-session)永不出现。
6.2 内部 cwd 永不泄露
独立会话摘要仍保留内部workspaceCwd用于协议路由,而既有 session-details 行会把workspaceCwd渲染成项目文件夹。因此 Recents 行使用standalone 专属视图模型:彻底丢弃workspaceCwd,并使用 standalone 专属动作分发,绝不把它传给项目详情、导航或工作区解析器。
6.3 逐会话操作
复用既有WebShellSidebarSessionActionItem菜单模式(WebShellSidebar.tsx:270:details | rename | export | delete | pin | archive),但分发到 standalone SDK 路由:
- rename→
renameStandaloneSession - export→
exportStandaloneSession - archive / unarchive→ 批量路由
- delete→
deleteStandaloneSessions
workspace-only 动作(pin、group)在 standalone 行上不提供。
删除保留既有二次确认对话框模式(DeleteSessionDialog),并说明转录与私有文件会被移除。成功后即使响应携带fileCleanupPending,会话也离开 Recents;该子集以非阻塞提示呈现,而非阻塞错误。
6.4 已归档会话保持可达
懒加载的 Archived 分区增加 standalonearchiveState=archived通道——该分区目前只由工作区 catalog 支撑,而 umbrella 契约要求已归档独立会话保持可见。由于 PR5 刻意跳过 standalone 上下文的工作区事件失效,standalone 通道在创建、prompt 完成与每次生命周期操作后显式刷新。
6.5 批量结果的逐 ID 解读
archive、unarchive、delete 即使 HTTP 200 也返回部分成功信封。catalog 只在目标 ID 出现在对应数组时更新:archived/alreadyArchived、unarchived/alreadyActive、removed/notFound;ID 出现在errors中则保留行并展示逐会话 code/message。fileCleanupPending只是对已移除 ID 的附加提示。
七、项目专属表面隐藏(Project-Only Surface Hiding)
在 standalone(或 Live)聊天中隐藏:工作区/项目选择器、Git 状态/分支/worktree 控件、项目文件浏览器、项目设置、pin/group 控件、附件/上传(standalone MVP 排除上传)。保留:模型、审批、工具、权限、转录与受支持的元数据控件。
7.1 门禁基于有效上下文
门禁条件为pendingSessionContext ?? connection.sessionContext——草稿 standalone 聊天在会话存在之前就隐藏项目表面,绝不依据 cwd 是否存在。既有可扩展模式是ordinaryWorkspaces/isKnownLiveWorkspaceCwd(App.tsx:2401-2405,已为 Live 运行时隐藏项目 UI),PR6 把它泛化为"当前聊天不是工作区上下文"。
7.2 Composer 状态隔离(不只是隐藏)
清空工作区会话会保留connection.commands/skills,createNewSession无 cwd 时会重载主工作区 skills,atWorkspaceCwd在无会话时回退到主工作区——所以useComposerCore会选中工作区作用域的草稿与输入历史键,在 standalone 草稿中暴露项目命令/skills。因此:commands、skills、atWorkspaceCwd、composer 存储身份全部从有效产品上下文推导——standalone 草稿跳过工作区 skill 重载,attach 前只保留本地内建命令,使用 standalone 专属草稿/历史身份(无主工作区回退),attach 后只用 standalone 会话支持的会话数据。
7.3 工作区效果与斜杠命令门禁
无会话的 App 从 locked/selected/primary 推导activeWorkspaceCwd并启动 Git-status effect;/diff、/log、/prs、/settings、/schedule、/memory add可能打开或默认落入工作区作用域流程——光隐藏控件不够。workspace-eligible cwd 只在effectiveContext.kind === 'workspace'时推导,并门控后台效果、命令发现与提交时 handler;测试断言 pending 与 attached standalone 上下文产生零工作区请求或面板。
7.4 通用转录分支排除
/branch与逐消息 Branch 动作调用POST /session/:id/branch,该路由以rejectStandalone: true注册(packages/cli/src/serve/routes/session.ts)——对 standalone 会话确定性失败。Pending 与 attached standalone 上下文省略 Branch 动作、从命令发现中移除/branch、阻止其提交时 handler。/fork保持独立——受保护的守卫式后台 fork-agent 工作仍受支持。
7.5 工作区 Web Terminal 门禁
webTerminalAvailable(App.tsx:3206)原本只查web_terminal能力,但 standalone 连接不暴露产品级workspaceCwd,TerminalPanel会在无 cwd 时打开 terminal 路由,daemon 以Terminal workspace unavailable拒绝。因此:terminal 可用性与打开 handler 要求effectiveContext.kind === 'workspace',进入非工作区上下文时丢弃或关闭继承的 terminal 标签页。standalone 会话仍通过会话权限管道在私有目录运行普通 Shell/tool 命令——此处只门禁独立 terminal 表面。
7.6 输入历史回退禁用
useComposerCore/useInputHistory在作用域历史为空时回退到无作用域的qwen-web-shell-history键——单独用 standalone 专属键仍会暴露工作区时代的历史提示。回退策略变为产品上下文感知:非工作区历史身份不做 legacy/global 回退。
7.7 组件清单与上传双通道
- 组件清单(
origin/main):composer 工作区选择器(components/WorkspaceSelector.tsx,渲染于ChatEditor.tsx:3116);Git chips/popovers(GitBranchIndicator、GitModePopover、BranchPickerPopover,由gitBranchVisible门控,ChatEditor.tsx:2435);Git 对话框(GitDialog等);项目设置面板(openPanel('settings')+ 工作区作用域useSettings);@-mention 文件浏览器(components/AtMentionPanel.tsx、hooks/useAtMentionMenu.ts);pin/group 控件(SESSION_ORGANIZATION_FEATURE侧边栏分组)。 - 上传有两条通道,standalone MVP 都隐藏:① 工作区文件上传——
hooks/useFileUpload.ts→client.uploadWorkspaceFile,经composer/AddMenu.tsx与 @-panel "Upload file" 渲染,已由fileUploadEnabled与workspace_file_upload能力 kill-switch(ChatEditor.tsx:1585-1588);② 内联 prompt 附件——useComposerCore.ts的pastedImages/pastedFiles(:1718-1719)。通道 2 是会话作用域,需要显式上下文检查而非能力检查。
7.8 上下文切换时丢弃附件
隐藏 paste/drop/upload 控件并不会清空useComposerCore已持有的pastedImages/pastedFiles,createNewSession也不会——工作区中添加的文件可能"搭便车"进入首条 standalone prompt。切换进 standalone 草稿时清空已持有附件并给出可见提示,提交时的非工作区守卫阻止陈旧或程序化提供的附件进入 standalone 会话。Live 聊天通过同一门禁获得同等处理(PR5 已在 provider 内为非工作区上下文跳过工作区 providers/skills/Git/preheat,本 PR 只隐藏可见控件)。
八、深链(Deep Links):/session/ ?context=standalone
WebShell 没有路由库:main.tsx+utils/sessionPath.ts拥有/session/<id>?workspace=<workspaceId>方案(getSessionIdFromUrl/getWorkspaceIdFromUrl,main.tsx:83-87;parseSessionId/buildSessionPathname),App 通过onSessionIdChange(App.tsx:8282)向上汇报上下文,让main.tsx可以replaceStateURL。
- 新 standalone 链接带显式上下文参数:
/session/<id>?context=standalone(无?workspace=)。UUID 在整个 daemon 内保留,因此 context 标记的链接不可能与工作区会话冲突。遗留无上下文链接保持既有工作区解析——此功能前不存在 standalone 会话,无需迁移。 - 解析发生在根部、provider 挂载之前:
main.tsx把 URL sessionId 传进WorkspaceSessionProvider,后者在 App 外挂载DaemonSessionProvider——若无显式上下文,resolveProviderSessionContext会继承主工作区并在任何 App 级查找之前启动工作区恢复。因此根部解析并校验context,通过WorkspaceSessionProvider传递显式上下文;对context=standalone抑制 provider 会话加载,直到精确getStandaloneSession查找选定路由:- active summary→ 以
{ kind: 'standalone' }挂载 provider; - archived summary 绝不直接加载(
getStandaloneSession对 active 与 archived 一视同仁返回 summary,但 daemon 以session_archived拒绝 load/resume)——流程先渲染归档状态、执行 Unarchive、校验目标 ID 出现在逐 ID 成功数组中,然后才挂载并加载; - not-found→ 既有 missing-session UI(
connection.missingSession→showMissingSessionState)。绝不猜测主工作区;冷加载测试断言零工作区加载请求。
- active summary→ 以
creating查找必须到达终态:解析器以有界退避轮询精确查找(如最多 30 秒、封顶次数),然后停在显式Retry动作,绝不在 resolving 状态永久悬挂。- 陈旧解析在开始加载前取消:provider 的 generation guard 只对启动后的加载排序,而精确查找与轮询发生在
loadSession之前——迟到的creating轮询在用户打开另一会话后解析,会启动一个错误获胜的加载。根部解析器(provider 之上)持有的 route-resolution token 在每次导航时失效,并在每次轮询前与loadSession前立即检查。 - fail-closed 边界情形:
?context=standalone携带冲突?workspace=参数 → 拒绝而非调和;不具备能力的 daemon → 显示"需要升级 daemon"状态,不解析到任何地方;具备能力但 Conversations 运行时不可用(503)→ 显示结构化错误与重试。 onSessionIdChange变得上下文感知:standalone 会话报告context=standalone并丢弃workspaceId;工作区与 Live 链接保留既有解析器。PR5 的切换语义已序列化跨上下文提交,迟到的解析不会覆盖更新的目标。
九、Standalone 状态呈现:typed state 而非字符串解析
PR6 渲染 PR5 的 typed 状态:
| 状态 | UI 行为 |
|---|---|
standaloneSession.workingDirectory.state === 'recreated' | 警告:转录幸存,但之前的工作区文件未被恢复 |
standaloneSession.errorCode === 'working_directory_missing' | 提供显式repair(repairStandaloneSessionDirectory),repair 从不重放 prompt。这需要一处 provider/App 范围内的过渡改动:当前sendPrompt的 catch 只把 prompt-admission409当作普通提示,从不把 typed code 复制进standaloneSession,SDK repair 调用既不清理 provider 错误状态也不重载会话——PR6 在 prompt-admission 拒绝时捕获 typed code 进standaloneSession.errorCode,把 Repair 所有者守卫到受影响会话,成功后重载同一 standalone 会话,仅在重载提交后清除错误 |
standaloneSession.errorCode === 'working_directory_compromised' | fail-closed 阻塞状态:服务拒绝复用已受损路径而非重建,因此不提供 Repair——给用户终态指引(导出转录、删除会话) |
standaloneSession.creationRecovery | 结果未知恢复状态机。provider 把保留 UUID 作为connection.sessionId发布(无附加会话),因此ensureSessionForPrompt会对既有 ID no-op 而提交在空会话引用上失败——恢复未决期间普通提交与未确认的新会话动作都被阻塞。所有者守卫的Check Status动作执行精确查找,对每个恢复状态转换:creating→ 有界轮询;existing→ 加载并 attach 同一 ID,先按isArchived分支(归档 summary 先渲染归档状态、Unarchive、校验逐 ID 成功数组、再加载,因为 daemon 以session_archived拒绝直接加载);absent→ 显式用户触发重试(绝不自动创建);unknown→ 持久恢复 UI |
删除响应携带fileCleanupPending | 非阻塞提示:文件清理会自动完成,转录已删除 |
四个转换全部有测试覆盖,包括冷链接与恢复路径上的归档 summary。
十、兼容性与回滚
- 无 daemon / SDK 变更:PR6 是纯 UI,基于已合并的 v1 契约;
- 旧 daemon→ 处处保留遗留主工作区行为;
- 旧 WebShell 对新 daemon→ 不变(从不调用 standalone 路由);
- post-#9811:provider 与产品 UI 同包,无跨包发布顺序顾虑。
十一、验证策略
单元 / 组件测试(vitest,packages/web-shell)
覆盖要点包括:路由表每一行的入口点→上下文映射(含当前会话继承与 Live 经startLive('new')路由);standalone 创建请求体精确断言(无workspaceCwd/sourceType/worktree/branch);能力三态(loading 在任何上下文都不创建、loaded-absent 走遗留路径且零 standalone 请求——可用 e2emockDaemon请求日志断言、load error fail-closed 带重试);具备能力但创建失败 → 渲染错误、pending standalone 意图保留且仍对门禁权威、不发出主工作区创建;Recents 列表/重命名/导出/归档/取消归档/删除流程、子会话排除、fileCleanupPending移除语义、视图模型绝不向详情/导航暴露内部 cwd;按有效上下文的表面隐藏(含首条 prompt 前的草稿 standalone 聊天;standalone 中上传不可用);深链(creating轮询到终态停在 Retry、冲突?workspace=拒绝、无能力 daemon 显示升级状态、not-found 显示 missing-session UI);recreated警告、仅working_directory_missing提供 repair、working_directory_compromisedfail-closed 无 repair 动作;prompt-admissionworking_directory_missing到达standaloneSession.errorCode、Repair 所有者守卫、成功重载并清除错误;迟到的creating轮询被用户会话切换取消后不启动加载;上下文切换清空持有附件并给可见提示、提交时守卫拒绝陈旧/程序化附件;已归档 standalone 通道在生命周期操作后列出并刷新、部分成功批量信封按动作处理;冷context=standalone加载零工作区加载请求、解析在 provider 挂载前于根部完成;每个新会话调用方传显式意图、无路径从 undefined cwd 或当前状态推断意图;Split View 入口(footer 非工作区禁用、?split=/sessionStorage/受控 prop 注入在 pane 挂载前拒绝或清洗);结果未知恢复四态转换、恢复未决期间阻塞普通提交、绝无自动重创建;已归档精确查找 summary 绝不直接加载(冷链接与结果恢复两路径都按isArchived分支、Unarchive 逐 ID 校验成功后再加载);workspace→standalone 草稿过渡隔离 composer commands/skills/atWorkspaceCwd/草稿/输入历史(含陈旧快照);Split View 裸 ID 分类器(延迟 catalog、standalone/creating拒绝、仅404继续工作区归属解析、受控宿主不能立即重新注入已清洗 ID);pending 与 attached standalone 上下文零工作区请求或面板(Git-status effect、/diff、/log、/prs、/settings、/schedule、/memory);空 standalone 输入历史保持为空而 legacy 全局历史键有数据;ScheduledTasksDialog.onCreateViaChat即使在活跃 standalone 上下文也强制工作区意图;standalone 上下文省略 Branch 动作、移除/branch发现、阻止 handler——零通用 branch 请求,/fork仍可用;session-overview、/resume、/delete、/release入口从 pending 与 attached standalone 上下文隐藏或阻止,standalone 会话的/resume <id>只经显式 standalone 上下文路由;standalone 与 Live 上下文不创建 terminal WebSocket 并丢弃继承的 terminal 标签页,工作区 terminal 不变。
相关测试文件:StandaloneRecents.test.tsx(packages/web-shell/client/components/sidebar/StandaloneRecents.test.tsx)、main.test.tsx(深链解析断言)、utils/sessionPath.test.ts。
E2E(Playwright,packages/web-shell/client/e2e)
- 扩展
utils/mockDaemon.ts场景:增加standalone_sessions_v1能力开关与 standalone 路由族; - 新增 web-shell.standalone.spec.ts:Home New Chat → standalone 创建体断言;项目 New Chat → workspace 创建;Recents 生命周期;能力 loading/absent/error 状态;具备能力时失败但意图保留;
- 手工 E2E 计划记录于
.qwen/e2e-tests/2026-08-29-webshell-standalone-chats.md,按仓库工作流在 merge 前对真实qwen servedaemon 做 dry-run。
常用命令:
cd packages/web-shell && npx vitest run <file> # 聚焦测试 npm run verify # lint + format:check + typecheck + test:ci npm run test:e2e # 根目录 npm run build && npm run typecheck十二、范围边界
PR6不新增:standalone 附件/上传(等待工作区上传后续)、把 standalone 会话移入/派生进项目、持久化 standalone 调度、存储配额、子会话管理 UI。SessionOverviewPanel与ResumeDialog在本 PR 保持工作区作用域,且所有入口都被门控:裸/resume、/delete、/release挂载以lockedWorkspaceCwd为键的工作区对话框,当其未定义时useScopedSessions选择主 WebShell catalog——所以/delete可能从 standalone 上下文作用于无关工作区会话。非工作区有效上下文隐藏或阻止侧边栏canOpenSessionsOverview入口、shellApi.openSessionOverview()与裸工作区对话框命令,发出零遗留 catalog 或破坏性请求。顶层 Recents 分组只存在于侧边栏。Split View 保持 workspace-only(上下文感知 pane 描述符是后续工作)。范围内唯一的 provider 改动是 prompt-admission 错误码捕获 + repair-and-reload 过渡(Repair UI 依赖它);provider 表面发现的其他 typed 缺口作为后续项提出。本 PR 不改变 daemon 生命周期行为、SDK 契约或 PR5 的 provider 切换语义。
结语
PR6 是独立会话从"协议能力"走向"产品体验"的关键一跳:它以DaemonProductSessionContext判别联合为单一事实来源,把入口路由、能力三态协商、延迟创建、Recents 生命周期、项目表面隐藏、深链解析与 typed 状态呈现串成一条不依赖workspaceCwd推断的完整链路,并在每个可能回退到主工作区的边界 fail-closed。无论是评审代码还是二次开发,本文梳理的 daemon 路由白名单、SDK 批量信封语义、pending context 权威性、Split View 分类器与结果未知恢复状态机,都是理解packages/web-shell独立会话实现的最短路径。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考