docs/design/2026-08-20-webshell-session-pr-binding.md
日期:2026-08-20 状态:已确认 MVP 范围
Web Shell 同时运行 20+ 会话时,侧栏信息不足以回答"哪个会话对应 PR #N"。 当前链路全断:
GitDialog.doCreatePr 创建 PR 拿到 {url, number} 后只显示状态消息,不回写(packages/web-shell/client/components/dialogs/GitDialog.tsx:502-553)。DaemonSessionSummary / BridgeSessionSummary 无 PR 字段;updateSessionMetadata 只放行 displayName。WebShellSidebar.tsx:3289-3302),不匹配分支名、worktree slug、PR 号。DaemonSessionSummary 与 BridgeSessionSummary(镜像,需同步)增加:
"prs": [{ "number": 9517, "url": "https://github.com/owner/repo/pull/9517" }]
prs 按绑定时间排序(最后一个 = 最新),上限 10 个(超出丢弃最旧)。同号重复绑定刷新 url 并移到最新位。number:正整数;url:http(s) URL(badge/tooltip 直接作为链接目标渲染,拒绝 javascript: 等 scheme——route、bridge、SDK 校验器、sidecar 校验四层统一要求)。updateSessionMetadata(sessionId, { pr: {number, url} }) 每次绑定一个,daemon 负责 upsert 进列表;读取/事件/响应均为完整 prs 数组。DaemonClient.updateSessionMetadata 的 metadata 参数扩展 pr?: { number: number; url: string }(单条);响应解析完整 prs 数组。/session/:id/metadata 与 workspace 作用域版本)校验 pr 后透传,bridge 更新成功后将 sidecar upsert 的完整列表回显在响应里。updateSessionMetadata(packages/acp-bridge/src/bridge.ts)先做全部校验再变更(组合请求不允许部分生效);upsert 进 live entry.prs(去重按 number,上限 10),session_metadata_updated SSE 事件 data 带完整 prs。session/update_metadata(acp-http/dispatch.ts)同样把最新绑定 upsert 进 sidecar。GitDialog.doCreatePr 成功后:仅用 dialog 已有的 sessionId(sessionIdRef.current,即连接会话或 dialog 已为提交信息生成等操作解析出的会话)调用 updateSessionMetadata(sessionId, { pr })。不调 resolveSessionForWorkspace——它可能创建幽灵会话或误绑"最近会话"。写入失败仅降级为 console 警告,不影响 PR 创建成功的状态展示。新增 sidecar <chatsDir>/<sessionId>.pr.json,复刻 worktree sidecar 模式:
packages/core/src/services/session-pr-service.ts:SessionPr 接口、数组 schema 校验({prs: [...]},容忍 ENOENT/JSON 损坏)、readSessionPrs / writeSessionPrs / upsertSessionPr(按 number 去重、移到最新、cap 10)。SessionService 增加 getPrSessionPathForArchiveState 路径助手;归档/取消归档移动 sidecar、删除会话时清理(与 worktree sidecar 一一对应)。session-list.ts 的 enrichPrSidecars 回填 persisted summary 的 prs;live 会话的 entry.prs 只含本 daemon 生命周期内的绑定,回填时与 sidecar 历史按 number 合并(live 的 url 优先,live-only 的排最后)。renderSessionRow:会话行标题旁渲染小号 badge(session.prs 非空时),显示最新 PR 号,多于一个时追加 +N;点击经 useExternalLinkOpener 打开最新 PR(desktop webview 下 target="_blank" 会被静默丢弃);click/doubleClick/keydown 均 stopPropagation(双击 badge 不触发重命名)。SessionDetailsTooltip:列出全部绑定 PR(最新在前),各为外链。filteredSessions 匹配逻辑扩展:label、sessionId 之外,增加任意一个绑定 PR 号(输入 9517 或 #9517 都命中)、branch.name、worktree.branch、worktree.slug(sessionMatchesGitQuery,WebShellSidebar 与 WorkspaceSection 共用)。session_metadata_updated 更新 store;bridge 的 markSessionCatalogChanged() 触发 catalog revision bump,侧栏 live-state 轮询(2s 周期)发现后自动 refetch——badge 在绑定后 ~2s 内出现(与改名等其他客户端变更的传播机制一致)。sidebar.sessionPr / sidebar.sessionPrMultiple 两个 key(EN/ZH)。gh pr create 的路径无法拦截,MVP 不覆盖;用户主力流程是 GitDialog。custom_title transcript 记录是因为标题属于会话内容流;PR 绑定是会话外部元数据,worktree sidecar 是同类先例,改动面更小。+N,tooltip 列全部,搜索匹配任意一个。上限 10 防无界增长。resolveSessionForWorkspace 创建新会话来绑定(会产生幽灵会话/误绑)。| 层 | 文件 |
|---|---|
| SDK 类型 | packages/sdk-typescript/src/daemon/types.ts(DaemonSessionSummary.pr) |
| SDK 事件 | packages/sdk-typescript/src/daemon/events.ts(MetadataUpdated data + 校验) |
| SDK 客户端 | packages/sdk-typescript/src/daemon/DaemonClient.ts(updateSessionMetadata 参数) |
| bridge 类型 | packages/acp-bridge/src/bridgeTypes.ts(BridgeSessionSummary.pr、metadata 参数) |
| bridge | packages/acp-bridge/src/bridge.ts(updateSessionMetadata 校验/存储/广播) |
| core | packages/core/src/services/session-pr-service.ts(新增)+ SessionService 路径助手/归档移动/删除清理 |
| daemon 路由 | packages/cli/src/serve/routes/session.ts(两个 PATCH 路由校验 + sidecar 写入)、acp-http/dispatch.ts(ACP session/update_metadata 的 sidecar 写入) |
| daemon 列表 | packages/cli/src/serve/server/session-list.ts(enrichPrSidecars) |
| web-shell | GitDialog.tsx(回写)、WebShellSidebar.tsx(badge + 搜索)、SessionDetailsTooltip.tsx(PR 行)、locale 文件 |
| 测试 | 上述各层的 collocated 单测 |
sourceType/sourceId 过滤管道是将来扩展的样板)。--worktree=#<pr> 的 pr-<n> slug 由搜索匹配 slug 顺带覆盖。gh pr create 的自动发现。无。