From e21fa3fff9a4e6c571bd0a7dd2df5ae0e2c302ef Mon Sep 17 00:00:00 2001 From: Evan <58194240+celestial-vault@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:12:19 -0700 Subject: [PATCH] migrate theme message protobus (#4012) Co-authored-by: Elephant Lumps --- proto/ui.proto | 3 + src/core/controller/index.ts | 6 -- src/core/controller/ui/subscribeToTheme.ts | 76 +++++++++++++++++++ src/core/webview/index.ts | 11 +-- src/shared/ExtensionMessage.ts | 1 - .../src/context/ExtensionStateContext.tsx | 32 ++++++-- 6 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 src/core/controller/ui/subscribeToTheme.ts diff --git a/proto/ui.proto b/proto/ui.proto index c07758c0e1..0cc2357d00 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -246,4 +246,7 @@ service UiService { // Subscribe to partial message updates (streaming Cline messages as they're built) rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage); + + // Subscribe to theme change events + rpc subscribeToTheme(EmptyRequest) returns (stream String); } diff --git a/src/core/controller/index.ts b/src/core/controller/index.ts index dee8827bbe..498acc8359 100644 --- a/src/core/controller/index.ts +++ b/src/core/controller/index.ts @@ -224,12 +224,6 @@ export class Controller { case "webviewDidLaunch": this.postStateToWebview() this.workspaceTracker?.populateFilePaths() // don't await - getTheme().then((theme) => - this.postMessageToWebview({ - type: "theme", - text: JSON.stringify(theme), - }), - ) // post last cached models in case the call to endpoint fails this.readOpenRouterModels().then((openRouterModels) => { if (openRouterModels) { diff --git a/src/core/controller/ui/subscribeToTheme.ts b/src/core/controller/ui/subscribeToTheme.ts new file mode 100644 index 0000000000..f4962d3891 --- /dev/null +++ b/src/core/controller/ui/subscribeToTheme.ts @@ -0,0 +1,76 @@ +import { Controller } from "../index" +import { EmptyRequest, String } from "@shared/proto/common" +import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler" +import { getTheme } from "@integrations/theme/getTheme" + +// Keep track of active theme subscriptions +const activeThemeSubscriptions = new Set() + +/** + * Subscribe to theme change events + * @param controller The controller instance + * @param request The empty request + * @param responseStream The streaming response handler + * @param requestId The ID of the request (passed by the gRPC handler) + */ +export async function subscribeToTheme( + controller: Controller, + request: EmptyRequest, + responseStream: StreamingResponseHandler, + requestId?: string, +): Promise { + // Add this subscription to the active subscriptions + activeThemeSubscriptions.add(responseStream) + + // Register cleanup when the connection is closed + const cleanup = () => { + activeThemeSubscriptions.delete(responseStream) + } + + // Register the cleanup function with the request registry if we have a requestId + if (requestId) { + getRequestRegistry().registerRequest(requestId, cleanup, { type: "theme_subscription" }, responseStream) + } + + // Send the current theme immediately upon subscription + const theme = await getTheme() + if (theme) { + try { + const themeEvent = String.create({ + value: JSON.stringify(theme), + }) + await responseStream( + themeEvent, + false, // Not the last message + ) + } catch (error) { + console.error("Error sending initial theme:", error) + activeThemeSubscriptions.delete(responseStream) + } + } +} + +/** + * Send a theme event to all active subscribers + * @param themeJson The JSON-stringified theme data + */ +export async function sendThemeEvent(themeJson: string): Promise { + // Send the event to all active subscribers + const promises = Array.from(activeThemeSubscriptions).map(async (responseStream) => { + try { + const event = String.create({ + value: themeJson, + }) + await responseStream( + event, + false, // Not the last message + ) + } catch (error) { + console.error("Error sending theme event:", error) + // Remove the subscription if there was an error + activeThemeSubscriptions.delete(responseStream) + } + }) + + await Promise.all(promises) +} diff --git a/src/core/webview/index.ts b/src/core/webview/index.ts index 18da52e5ed..71bb30fabc 100644 --- a/src/core/webview/index.ts +++ b/src/core/webview/index.ts @@ -8,6 +8,7 @@ import { findLast } from "@shared/array" import { readFile } from "fs/promises" import path from "node:path" import { WebviewProviderType } from "@/shared/webview/types" +import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -139,11 +140,11 @@ export class WebviewProvider implements vscode.WebviewViewProvider { vscode.workspace.onDidChangeConfiguration( async (e) => { if (e && e.affectsConfiguration("workbench.colorTheme")) { - // Sends latest theme name to webview - await this.controller.postMessageToWebview({ - type: "theme", - text: JSON.stringify(await getTheme()), - }) + // Send theme update via gRPC subscription + const theme = await getTheme() + if (theme) { + await sendThemeEvent(JSON.stringify(theme)) + } } if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) { // Update state when marketplace tab setting changes diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 60738f6d5c..15dbdbb08b 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -19,7 +19,6 @@ export interface ExtensionMessage { | "selectedImages" | "ollamaModels" | "lmStudioModels" - | "theme" | "workspaceUpdated" | "openRouterModels" | "openAiModels" diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 5a1668be98..1b90e2559b 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -197,12 +197,6 @@ export const ExtensionStateContextProvider: React.FC<{ const handleMessage = useCallback((event: MessageEvent) => { const message: ExtensionMessage = event.data switch (message.type) { - case "theme": { - if (message.text) { - setTheme(convertTextMateToHljs(JSON.parse(message.text))) - } - break - } case "workspaceUpdated": { setFilePaths(message.filePaths ?? []) break @@ -246,6 +240,7 @@ export const ExtensionStateContextProvider: React.FC<{ const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null) const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null) const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null) + const themeSubscriptionRef = useRef<(() => void) | null>(null) // Subscribe to state updates and UI events using the gRPC streaming API useEffect(() => { @@ -445,6 +440,27 @@ export const ExtensionStateContextProvider: React.FC<{ }, }) + // Subscribe to theme changes + themeSubscriptionRef.current = UiServiceClient.subscribeToTheme(EmptyRequest.create({}), { + onResponse: (response) => { + if (response.value) { + try { + const themeData = JSON.parse(response.value) + setTheme(convertTextMateToHljs(themeData)) + console.log("[DEBUG] Received theme update from gRPC stream") + } catch (error) { + console.error("Error parsing theme data:", error) + } + } + }, + onError: (error) => { + console.error("Error in theme subscription:", error) + }, + onComplete: () => { + console.log("Theme subscription completed") + }, + }) + // Still send the webviewDidLaunch message for other initialization vscode.postMessage({ type: "webviewDidLaunch" }) @@ -497,6 +513,10 @@ export const ExtensionStateContextProvider: React.FC<{ mcpMarketplaceUnsubscribeRef.current() mcpMarketplaceUnsubscribeRef.current = null } + if (themeSubscriptionRef.current) { + themeSubscriptionRef.current() + themeSubscriptionRef.current = null + } } }, [])