Webview Bridge:gRPC-over-postMessage 协议
职责
webview 是 React 应用,跑在独立的 iframe 沙箱里,和扩展宿主 (extension host) 之间只能通过 webview.postMessage / webview.onDidReceiveMessage 通信。Cline 在这层裸 API 上搭了一个「gRPC-over-postMessage」协议:webview 端把每次调用包装成 GrpcRequest (service + method + message + request_id),扩展端拿到后按 service/method 分发到具体 handler,handler 返回的结果再包成 grpc_response 推回去。这套协议在 apps/vscode/src/hosts/vscode/VscodeWebviewProvider.ts 和 apps/vscode/src/core/controller/grpc-handler.ts 之间落地。
它做的事可以拆三层。最底一层是 webview HTML 注入和 CSP,Cline 在生产用打包好的 index.js + index.css,dev 模式连本地 Vite dev server 走 HMR。中间一层是消息收发,setWebviewMessageListener 注册 onDidReceiveMessage 回调,postMessageToWebview 把 ExtensionMessage 推回 webview。最上一层是协议分发,webview 发来的消息只有两类 (grpc_request 和 grpc_request_cancel),所有具体业务 (askResponse、showTaskWithId、subscribeToState 等) 都被建模成 gRPC service.method 调用,由 serviceHandlers 注册表路由到具体 handler。
设计动机
- 统一协议替代 switch-case:旧 Cline 用
type: "askResponse" | "showTaskWithId" | ...的大 switch 分发,每加一个 webview 调用都要改ExtensionMessage类型 + switch + handler。改成 gRPC service/method 后 (GrpcRequest:7),新增调用只要在生成代码里注册一个 service.method handler,webview 端用 typed client 调,两端类型对齐。 - request_id 关联请求与响应:
GrpcRequest.request_id是字符串 (request_id:11),webview 用它找到对应的 pending Promise,扩展端用它做取消和录制。GrpcResponse.request_id原样带回,webview 收到后按 ID 派发。 - streaming 用 is_streaming + sequence_number:
handleStreamingRequest:113创建responseStream回调,handler 可以多次调它推 partial 结果,每次带is_streaming: true和递增的sequence_number,最后一次isLast=true关闭流。这适合「task 状态持续更新」「mcp server 列表流」这类场景。 - 录制中间件:
withRecordingMiddleware包裹postMessage(withRecordingMiddleware:23),每次有grpc_response都交给GrpcRecorderBuilder.recordResponse,用于调试时回放完整的请求-响应序列。 - CSP 严格但不挡 HMR:生产 CSP 用 nonce +
script-src 'nonce-${nonce}' 'unsafe-eval',connect-src 只放行 posthog 和 cline.bot (CSP meta:113)。dev 模式 CSP 放开 localhost 和 ws,让 Vite HMR 能跑 (dev CSP:200)。 - ExtensionMessage 只剩 grpc_response:
ExtensionMessage接口现在只有一个type: "grpc_response"(ExtensionMessage:21),所有从扩展推到 webview 的内容 (state 更新、partial message、ask/say) 都包在某个 grpc_response.message 里,走的是「扩展主动调 streaming handler 推」的模式,不再有独立的plusButtonClicked之类消息类型。
关键文件
resolveWebviewView:53— VSCode 首次打开 sidebar 时调,注入 HTML、注册消息监听、绑可见性/销毁回调。html injection:62— 按 dev/production 选择getHMRHtmlContent或getHtmlContent。setWebviewMessageListener:145— 注册onDidReceiveMessage,把消息转发给handleWebviewMessage。handleWebviewMessage:161— 入口 switch,只处理grpc_request和grpc_request_cancel。postMessageToWebview:189— 扩展推消息到 webview 的唯一出口,直接调webview.postMessage。handleGrpcRequest:53— 总入口,先 recordRequest,再按is_streaming分到 unary 或 streaming 处理。handleUnaryRequest:75— unary 模式:调 handler 拿 response,包成grpc_response推回去。handleStreamingRequest:113— streaming 模式:创建responseStream回调,handler 多次调它推 partial。handleGrpcRequestCancel:163— 取消请求,从GrpcRequestRegistry找到 cleanup 函数调掉。getHandler:192— 按serviceName.methodName从serviceHandlers注册表找 handler,找不到抛错。ServiceRegistry:30— 服务注册表,每个 service 注册一组 method,区分 unary/streaming。GrpcRequest:7— webview→扩展 的请求结构,带 service/method/message/request_id/is_streaming。ExtensionMessage:21— 扩展→webview 的消息,只剩type: "grpc_response"一种。
数据流
webview 发请求时,先在客户端用 typed client 包装成 GrpcRequest 然后 postMessage;扩展端 onDidReceiveMessage 收到后走 handleWebviewMessage:
// apps/vscode/src/hosts/vscode/VscodeWebviewProvider.ts
async handleWebviewMessage(message: WebviewMessage) {
const postMessageToWebview = (response: ExtensionMessage) => this.postMessageToWebview(response)
switch (message.type) {
case "grpc_request": {
if (message.grpc_request) {
await handleGrpcRequest(this.controller, postMessageToWebview, message.grpc_request)
}
break
}
case "grpc_request_cancel": {
if (message.grpc_request_cancel) {
await handleGrpcRequestCancel(postMessageToWebview, message.grpc_request_cancel)
}
break
}
default: {
Logger.error("Received unhandled WebviewMessage type:", JSON.stringify(message))
}
}
}handleGrpcRequest 拿到 GrpcRequest 后,先 recordRequest 录制,然后用 withRecordingMiddleware 包裹 postMessageToWebview 让响应也走录制,再按 request.is_streaming 分流 (is_streaming branch:63)。unary 调 handler 一次,把结果包成 grpc_response.message 推回 (unary response:86);streaming 把 responseStream 回调交给 handler,handler 每调一次回调就推一条 grpc_response 带 is_streaming: !isLast 和 sequence_number。webview 端按 request_id 派发到对应的 pending Promise,流式消息按 sequence 顺序合并。askResponse 这个具体业务就是 webview 调 cline.askResponse service 的 AskResponse method,handler 在 askResponse handler:14 里把 response 映射成 ClineAskResponse 后调 task.handleWebviewAskResponse,Task 里那个 pWaitFor 就被唤醒。
边界与失败
- 未识别的消息类型:
handleWebviewMessage的 default 分支只记 error 不抛 (default error:177),旧版 webview 缓存里可能还在发老的plusButtonClicked类型,扩展容忍。 - handler 抛错:unary handler 抛错时,catch 把
error.message包成grpc_response.error推回 (unary error:96)。streaming handler 抛错时,在 catch 里推一条is_streaming: false的 error 响应关闭流 (streaming error:147)。 - unknown service/method:
getHandler找不到 service 或 method 时抛Unknown service: ${serviceName}(unknown service:196),这种错会被上层 catch 包成 grpc_response.error。 - streaming 不主动关流:handler 正常结束后,代码注释明确说「不发最终消息,流要保持开」(
keep stream open:142),等客户端发grpc_request_cancel或服务端显式调isLast=true才关。 - webview 还未初始化时 postMessage:
postMessageToWebview直接this.webview?.webview.postMessage(message),webview 为空时返回 undefined (postMessage undefined:189),调用方要容忍这个 falsy。 - 配置变更触发 state push:监听
vscode.workspace.onDidChangeConfiguration,只要cline.mcpMarketplace.enabled变了就postStateToWebview(config change:102),让 webview 跟上设置变化。 - 录制失败不致命:
withRecordingMiddleware和recordRequest都 try/catch 包了 (recordRequest try:42) ,录制挂了不影响主流程。
小结
webview bridge 把「扩展↔React 沙箱」的通信统一成 gRPC-over-postMessage,所有具体业务都是 service.method 调用,unary 一次推回,streaming 多次推 partial。这样 ExtensionMessage 只剩一种 type,新增调用只需要在生成代码里注册 handler,两端类型自动对齐。要看一个具体的 unary 调用怎么落地到 Task 上,转 /startup/controller;看 Task 内部 ask/say 怎么变成 webview 上的聊天消息,转 /agent-loop/task-class。