Skip to content

Webview Bridge:gRPC-over-postMessage 協定

源码版本v4.0.10

職責

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.tsapps/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_requestgrpc_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 之類訊息類型。

關鍵檔案

資料流

webview 發請求時,先在客戶端用 typed client 包裝成 GrpcRequest 然後 postMessage;擴充端 onDidReceiveMessage 收到後走 handleWebviewMessage:

typescript
// 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_responseis_streaming: !isLastsequence_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 跟上設定變化。
  • 錄製失敗不致命:withRecordingMiddlewarerecordRequest 都 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

對照官方資料:Cline 文件 · README