Webview Bridge: gRPC-over-postMessage-Protokoll
Verantwortung
Die webview ist eine React-Anwendung, die in einer isolierten iframe-Sandbox läuft; zwischen ihr und dem Extension-Host besteht als einzige Verbindung webview.postMessage / webview.onDidReceiveMessage. Cline baut auf dieser nackten API ein „gRPC-over-postMessage"-Protokoll auf: Die webview-Seite verpackt jeden Aufruf als GrpcRequest (service + method + message + request_id), die Extension-Seite verteilt ankommende Nachrichten nach service/method an konkrete Handler, und deren Antwort wird als grpc_response zurückgeschickt. Das Protokoll wird zwischen apps/vscode/src/hosts/vscode/VscodeWebviewProvider.ts und apps/vscode/src/core/controller/grpc-handler.ts umgesetzt.
Die Aufgaben lassen sich in drei Schichten zerlegen. Die unterste Schicht ist HTML-Injektion und CSP: In Produktion verwendet Cline das gebündelte index.js + index.css, im Dev-Modus wird der lokale Vite-Dev-Server mit HMR angebunden. Die mittlere Schicht ist Nachrichtenempfang und -versand: setWebviewMessageListener registriert den onDidReceiveMessage-Callback, postMessageToWebview schickt eine ExtensionMessage zurück an die webview. Die oberste Schicht ist die Protokollverteilung: Nachrichten der webview haben nur zwei Typen (grpc_request und grpc_request_cancel); alle konkreten Geschäfte (askResponse, showTaskWithId, subscribeToState usw.) werden als gRPC service.method-Aufruf modelliert und über die serviceHandlers-Registry an den jeweiligen Handler geroutet.
Entwurfsmotivation
- Einheitliches Protokoll statt switch-case: Alter Cline-Code verteilte über einen großen switch mit
type: "askResponse" | "showTaskWithId" | ...; jeder neue webview-Aufruf erforderte Änderungen amExtensionMessage-Typ, am switch und am Handler. Mit dem Wechsel zu gRPC service/method (GrpcRequest:7) genügt es, in generiertem Code einen service.method-Handler zu registrieren; die webview-Seite ruft über einen typisierten Client auf, und beide Seiten sind typseitig synchron. - request_id verknüpft Anfrage und Antwort:
GrpcRequest.request_idist ein String (request_id:11). Die webview nutzt ihn, um die zugehörige pending Promise zu finden; die Extension-Seite verwendet ihn für Abbruch und Aufzeichnung.GrpcResponse.request_idwird unverändert zurückgeliefert, und die webview verteilt beim Empfang nach ID. - Streaming mit is_streaming + sequence_number:
handleStreamingRequest:113erzeugt einenresponseStream-Callback. Der Handler kann ihn mehrfach aufrufen und jedes Mal partielle Ergebnisse mitis_streaming: trueund einer ansteigendensequence_numberliefern; der letzte Aufruf mitisLast=trueschließt den Stream. Das passt zu Szenarien wie „ständige Task-Statusaktualisierung" oder „Stream der MCP-Server-Liste". - Aufzeichnungs-Middleware:
withRecordingMiddlewareumwickeltpostMessage(withRecordingMiddleware:23). Jedegrpc_responsewird anGrpcRecorderBuilder.recordResponseübergeben, sodass beim Debugging die komplette Request-Response-Sequenz zurückgespielt werden kann. - CSP streng, aber ohne HMR zu blockieren: Die CSP in Produktion nutzt Nonce +
script-src 'nonce-${nonce}' 'unsafe-eval'; connect-src lässt nur posthog und cline.bot zu (CSP meta:113). Im Dev-Modus öffnet die CSP localhost und ws, damit Vite HMR funktioniert (dev CSP:200). - ExtensionMessage reduziert auf grpc_response: Die Schnittstelle
ExtensionMessagebesitzt nur noch einen Typtype: "grpc_response"(ExtensionMessage:21). Jeglicher Inhalt von der Extension an die webview – Status-Updates, partielle Nachrichten, ask/say – steckt in einemgrpc_response.messageund läuft über das Muster „Extension ruft aktiv einen Streaming-Handler auf"; eigenständige Nachrichtentypen wieplusButtonClickedgibt es nicht mehr.
Schlüsseldateien
resolveWebviewView:53— Wird beim ersten Öffnen der Sidebar durch VSCode aufgerufen; injiziert HTML, registriert den Nachrichten-Listener und bindet Sichtbarkeits- und Dispose-Callbacks an.html injection:62— Wählt je nach dev/produktion zwischengetHMRHtmlContentundgetHtmlContent.setWebviewMessageListener:145— RegistriertonDidReceiveMessageund leitet Nachrichten anhandleWebviewMessageweiter.handleWebviewMessage:161— Einstiegs-switch; bearbeitet ausschließlichgrpc_requestundgrpc_request_cancel.postMessageToWebview:189— Einziger Ausgang der Extension, um Nachrichten an die webview zu senden; ruft direktwebview.postMessageauf.handleGrpcRequest:53— Gesamteinstieg; zuerst recordRequest, dann nachis_streamingauf unary oder streaming aufgeteilt.handleUnaryRequest:75— Unary-Modus: ruft den Handler auf, verpackt die Antwort in eingrpc_responseund schickt sie zurück.handleStreamingRequest:113— Streaming-Modus: erzeugt einenresponseStream-Callback, über den der Handler mehrfach partielle Ergebnisse liefert.handleGrpcRequestCancel:163— Bricht eine Anfrage ab; inGrpcRequestRegistrywird die Cleanup-Funktion gesucht und aufgerufen.getHandler:192— Sucht in derserviceHandlers-Registry nachserviceName.methodName; wirft bei einem Fehltreffer einen Fehler.ServiceRegistry:30— Service-Registry; jeder Service registriert eine Menge von Methods, unterteilt in unary/streaming.GrpcRequest:7— Anfragestruktur webview→Extension mit service/method/message/request_id/is_streaming.ExtensionMessage:21— Nachricht Extension→webview; es bleibt nurtype: "grpc_response".
Datenfluss
Wenn die webview eine Anfrage schickt, verpackt der clientseitig typisierte Client sie in einen GrpcRequest und ruft postMessage auf. Auf der Extension-Seite läuft die Nachricht über onDidReceiveMessage in 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 nimmt den GrpcRequest entgegen, ruft zuerst recordRequest auf, umwickelt postMessageToWebview dann mit withRecordingMiddleware (sodoch auch die Antworten aufgezeichnet werden) und verteilt schließlich nach request.is_streaming (is_streaming branch:63). Im unary-Fall wird der Handler einmal aufgerufen und das Ergebnis als grpc_response.message zurückgeschickt (unary response:86); im streaming-Fall erhält der Handler einen responseStream-Callback, über den er mehrfach grpc_response mit is_streaming: !isLast und einer sequence_number schickt. Die webview-Seite verteilt nach request_id auf die passende pending Promise; Streaming-Nachrichten werden der sequence nach zusammengeführt. Das konkrete Geschäft askResponse ist ein webview-Aufruf der cline.askResponse-Service-Methode AskResponse; der Handler in askResponse handler:14 bildet die Antwort auf ClineAskResponse ab und ruft task.handleWebviewAskResponse auf, was wiederum das pWaitFor in Task weckt.
Grenzen und Fehler
- Unbekannter Nachrichtentyp: Der default-Zweig von
handleWebviewMessageprotokolliert nur einen Fehler und wirft nicht (default error:177). Ein alter webview-Cache könnte nochplusButtonClicked-Nachrichten senden; die Extension toleriert das. - Fehler im Handler: Wirft ein unary-Handler, fängt der catch-Block
error.messageein, verpackt ihn alsgrpc_response.errorund schickt ihn zurück (unary error:96). Bei einem Fehler im streaming-Handler wird im catch-Block eineis_streaming: false-Fehlerantwort gesendet, die den Stream schließt (streaming error:147). - Unbekannter service/method: Findet
getHandlerservice oder method nicht, wirdUnknown service: ${serviceName}geworfen (unknown service:196). Der Fehler wird vom übergeordneten catch in ein grpc_response.error verpackt. - Streaming wird nicht aktiv geschlossen: Nach regulärem Ende des Handlers sagt der Kommentar explizit: „sendet keine Abschlussnachricht, der Stream bleibt offen" (
keep stream open:142). Erst eingrpc_request_canceldes Clients oder ein ausdrücklichesisLast=trueschließt den Stream. - postMessage vor der webview-Initialisierung:
postMessageToWebviewruft direktthis.webview?.webview.postMessage(message)auf; ist die webview nicht vorhanden, wirdundefinedzurückgegeben (postMessage undefined:189). Der Aufrufer muss diesen falsy-Wert tolerieren. - Konfigurationsänderung triggert state push: Auf
vscode.workspace.onDidChangeConfigurationwird gehört; ändert sichcline.mcpMarketplace.enabled, erfolgt sofortpostStateToWebview(config change:102), damit die webview der Einstellungsänderung folgt. - Aufzeichnungsfehler nicht tödlich:
withRecordingMiddlewareundrecordRequestsind jeweils in try/catch eingebettet (recordRequest try:42); fällt die Aufzeichnung aus, bleibt der Hauptfluss unberührt.
Zusammenfassung
Die webview bridge vereint die Kommunikation zwischen „Extension und React-Sandbox" als gRPC-over-postMessage: Alle Geschäfte sind service.method-Aufrufe, unary liefert eine Antwort, streaming schiebt mehrfach partielle Ergebnisse. So besteht ExtensionMessage nur noch aus einem Typ; neue Aufrufe erfordern lediglich die Registrierung eines Handlers im generierten Code, und beide Seiten sind automatisch typseitig synchron. Um einen konkreten unary-Aufruf bis zu Task zu verfolgen, siehe /startup/controller; um zu sehen, wie ask/say innerhalb von Task zu Chat-Nachrichten in der webview werden, siehe /agent-loop/task-class.
Siehe offizielle Dokumentation: Cline-Dokumentation · README