docs/design/2026-07-24-webshell-git-status-fast-path.md
日期:2026-07-24 状态:待确认
Web Shell 新建会话时,composer 工具条里的 git chip 出现得慢。根因(已逐行确认):
git status 子进程拖住——WorkspaceGitState.getStatus()
(packages/cli/src/serve/workspace-git-state.ts)里 branch 有毫秒级快路径
(resolveBranchName 读 HEAD 文件 + reflog watcher),但 HTTP 响应必须等
getGitWorkingTreeStatus() 完成——每次请求同步 spawn
git status --porcelain=v1 --branch -z(gitDiff.ts runGit,5s 超时,零缓存)。selectedWorkspaceGitStatus?.branch(App.tsx 7860–7871),它要等整个
HTTP + git status 往返(App.tsx 1480–1520 的 effect)才 setState。DaemonSessionProvider 的 metadata 拉取
(DaemonSessionProvider.tsx:1320,只用 .branch)与 App.tsx 的 git-status
effect 几乎同时发 GET /workspaces/:ws/git,daemon 端 spawn 两个相同子进程。activeWorkspaceCwd 依赖 GET /capabilities 先完成。目标:
wait: true fresh 请求;
有会话时另有 SSE 实时推送)。git status 子进程(并发去重 + stale-while-revalidate)。非目标:
?cwd= 路径不引入 watcher/缓存(维持现状:直接计算,避免每 worktree
泄漏一个 fs watcher)。worktree chip 延迟不变。git_branch_changed 现有语义。三层改动:daemon 缓存 + 后台刷新 + SSE 推送(P0),响应两阶段化(P1), 前端消费 SSE 并保留慢路径给需要的调用方(P2 重评估后见下文)。
WorkspaceGitState 缓存、去重、后台刷新、SSE 推送WorkspaceGitEntry 扩展:
interface WorkspaceGitEntry {
branch: string | undefined; // watcher 保持新鲜(现状)
dispose: () => void; // 现状
status?: GitWorkingTreeStatus; // 上次计算的原始 working-tree summary
statusComputedAt?: number; // epoch ms
statusPromise?: Promise<void>; // in-flight 去重
disposed?: boolean; // dispose 后禁止 publish
}
getStatus(cwd, bridge, opts?: { wait?: boolean }) 语义改为:
entry.branch ?? status.branch,v2 形状 +
computedAt);从未计算过时返回 branch-only { v, workspaceCwd, branch }
(无 computedAt,前端据此区分"未计算"与"clean")。wait: true:等待(或发起并等待,in-flight 复用)一次新鲜计算,
返回全量 status。计算失败降级 branch-only(现状语义)。后台刷新 refreshStatus(entry):
statusPromise 存在则直接返回它。bridge.publishWorkspaceEvent({ type: 'git_status_changed', data }) 推送
materialized 全量 status(data 即 DaemonWorkspaceGitStatus,含 workspaceCwd)。
首次计算(缓存为空)视为有差异,必推送——这是冷启动 chip 补齐计数器的通道。无 TTL。last-known + 每次 GET 触发后台刷新 + SSE 纠偏已足够;
节流 2s 承担" TTL 防爆"职责。wait: true 调用方总是拿到新鲜计算(in-flight 复用)。
路由(packages/cli/src/serve/routes/workspace-git.ts):
/workspace/git 与 /workspaces/:workspace/git 解析 ?wait=1,透传给
getStatus。默认 fast。?cwd= 分支维持现状(直接 getGitWorkingTreeStatus,不进缓存)。packages/sdk-typescript)events.ts:DAEMON_KNOWN_EVENT_TYPE_VALUES 增加 'git_status_changed'
(紧跟 'git_branch_changed')。旧 SDK 经 asKnownDaemonEvent 静默丢弃——
向后兼容,无需协议 bump(与 followup_suggestion 同模式)。ui/normalizer.ts:case 'git_status_changed': return [];(与
git_branch_changed 一样由 session mappers 处理,不进 UI 归一化流)。DaemonClient.workspaceGit 签名改为 options 对象:
workspaceGit(opts?: { cwd?: string; wait?: boolean }),拼 query
(cwd 与 wait=1 可组合)。迁移全部 4 个调用点(App.tsx、WorkspaceSection、
DaemonSessionProvider ×2 处)与 SDK 单测。packages/webui)session/types.ts:DaemonConnectionState 增加
gitStatus?: DaemonWorkspaceGitStatus(仅当前 workspace 的全量 status,
由 SSE 维护)。session/mappers.ts:updateConnectionFromDaemonEvent 增加
case 'git_status_changed'——data.workspaceCwd 与
current.workspaceCwd 不匹配则忽略(镜像 git_branch_changed 的守卫),
否则 setConnection({ ...current, gitStatus: data })。packages/web-shell)App.tsx git-status effect:composer 用客户端 stale-while-revalidate——
每次触发并发两个请求(worktree 会话除外,见下):
workspaceGit({ cwd: sessionWorktree?.path })(fast):last-known 秒回,
立即渲染(冷缓存 branch-only);workspaceGit({ wait: true })(fresh):daemon 后台算完即返回全量 status,
补齐计数器。两个请求在 daemon 端共享同一次计算(in-flight 去重),
不增加 git 子进程数。git_status_changed 走
每会话事件流(GET /session/:id/events),新建会话态(deferred connect,
无 sessionId)没有 SSE 订阅——只发 fast GET 时计数器要等 30s 轮询或
focus 才补上。fresh 请求不依赖会话存在,保证"branch 立即、计数器算完即得"
在所有会话态成立。(git_branch_changed 今天就有同样的无会话盲区,非回归。)App.tsx 另保留 SSE 同步 effect:connection.gitStatus 变化且
workspaceCwd 匹配、无 sessionWorktree 时写入 selectedWorkspaceGitStatus——
覆盖有会话时两次轮询之间的实时推送(另一客户端/CLI 触发的后台刷新
推送过来)。?cwd= 路径本就绕过缓存直接计算
(fast 与 wait 等价),行为不变。sidebar/WorkspaceSection.tsx:workspaceGit({ wait: true })——侧栏 chip
要计数器且没有 SSE/fresh 双发通道,保留阻塞语义(现状行为不变;非活跃
workspace 没有 SSE 通道)。原 P2(前端去重:provider 首拉存全量 status 给 App 复用)降格为不做:
P0 的 daemon 端 in-flight 去重已消除重复 git status 子进程(原问题的实质),
剩下的只是一次毫秒级本地 HTTP 往返。把全量 status 存进 provider 再让 App
复用会引入跨层耦合(provider→App 初始值协议),收益约等于零。
provider 两处 workspaceGit() 调用只取 .branch,走默认 fast path 即可,零改动。
?wait=1 query 为可选。DaemonSessionProvider(×2):只读 .branch——branch 始终新鲜(watcher),无影响。wait: true,语义不变。git status_changed 仅 publish 给该 workspace 的 session SSE bus
(publishWorkspaceEvent 现有机制,含多 workspace 隔离)。| 风险 | 缓解 |
|---|---|
| chip 先显示 branch 后出现计数器,工具条宽度抖动 | 已有隐藏测量副本(ChatEditor toolbar-measure)处理 re-measure;接受轻微 shift |
| branch-only 响应被误读为 "clean" | branch-only 不携带 computedAt;GitBranchIndicator 现有逻辑在 computedAt 缺失时不显示 "clean" |
| 缓存 status 与 watcher branch 不一致 | materialize 时 overlay entry.branch ?? status.branch(现状逻辑保留) |
| 后台刷新泄漏(dispose 后 publish) | disposed flag 守卫 |
| focus 风暴触发串行 git spawn | 2s 节流 + in-flight 复用 |
单测:
workspace-git-state.test.ts(扩展):fast path 立即返回 last-known;
冷缓存返回 branch-only 且无 computedAt;后台刷新有差异才 publish
git_status_changed;首次计算必 publish;并发 getStatus 只触发一次
getGitWorkingTreeStatus;2s 节流;wait: true 等待新鲜计算;
计算失败保留旧缓存不 publish;dispose 后不 publish。routes/workspace-git.test.ts(扩展):?wait=1 透传;worktree ?cwd=
路径不进缓存(维持直接计算)。DaemonClient.test.ts:options 对象 query 拼接(cwd / wait / 组合)。mappers.test.ts:git_status_changed 匹配/不匹配 workspaceCwd
两种分支。E2E(.qwen/e2e-tests/2026-07-24-git-chip-fast-branch.md,验证阶段补):
真 daemon + web shell,大工作区新建会话——chip(branch)在编辑器就绪后立刻出现,
计数器随后补齐;侧栏 chip 行为不变;focus/30s 轮询仍刷新;worktree 会话 chip 不变。