Skip to content

Commit e21fa3f

Browse files
celestial-vaultElephant Lumps
andauthored
migrate theme message protobus (RooCodeInc#4012)
Co-authored-by: Elephant Lumps <[email protected]>
1 parent 5b3647f commit e21fa3f

File tree

6 files changed

+111
-18
lines changed

6 files changed

+111
-18
lines changed

proto/ui.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,7 @@ service UiService {
246246

247247
// Subscribe to partial message updates (streaming Cline messages as they're built)
248248
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
249+
250+
// Subscribe to theme change events
251+
rpc subscribeToTheme(EmptyRequest) returns (stream String);
249252
}

src/core/controller/index.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,6 @@ export class Controller {
224224
case "webviewDidLaunch":
225225
this.postStateToWebview()
226226
this.workspaceTracker?.populateFilePaths() // don't await
227-
getTheme().then((theme) =>
228-
this.postMessageToWebview({
229-
type: "theme",
230-
text: JSON.stringify(theme),
231-
}),
232-
)
233227
// post last cached models in case the call to endpoint fails
234228
this.readOpenRouterModels().then((openRouterModels) => {
235229
if (openRouterModels) {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { Controller } from "../index"
2+
import { EmptyRequest, String } from "@shared/proto/common"
3+
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
4+
import { getTheme } from "@integrations/theme/getTheme"
5+
6+
// Keep track of active theme subscriptions
7+
const activeThemeSubscriptions = new Set<StreamingResponseHandler>()
8+
9+
/**
10+
* Subscribe to theme change events
11+
* @param controller The controller instance
12+
* @param request The empty request
13+
* @param responseStream The streaming response handler
14+
* @param requestId The ID of the request (passed by the gRPC handler)
15+
*/
16+
export async function subscribeToTheme(
17+
controller: Controller,
18+
request: EmptyRequest,
19+
responseStream: StreamingResponseHandler,
20+
requestId?: string,
21+
): Promise<void> {
22+
// Add this subscription to the active subscriptions
23+
activeThemeSubscriptions.add(responseStream)
24+
25+
// Register cleanup when the connection is closed
26+
const cleanup = () => {
27+
activeThemeSubscriptions.delete(responseStream)
28+
}
29+
30+
// Register the cleanup function with the request registry if we have a requestId
31+
if (requestId) {
32+
getRequestRegistry().registerRequest(requestId, cleanup, { type: "theme_subscription" }, responseStream)
33+
}
34+
35+
// Send the current theme immediately upon subscription
36+
const theme = await getTheme()
37+
if (theme) {
38+
try {
39+
const themeEvent = String.create({
40+
value: JSON.stringify(theme),
41+
})
42+
await responseStream(
43+
themeEvent,
44+
false, // Not the last message
45+
)
46+
} catch (error) {
47+
console.error("Error sending initial theme:", error)
48+
activeThemeSubscriptions.delete(responseStream)
49+
}
50+
}
51+
}
52+
53+
/**
54+
* Send a theme event to all active subscribers
55+
* @param themeJson The JSON-stringified theme data
56+
*/
57+
export async function sendThemeEvent(themeJson: string): Promise<void> {
58+
// Send the event to all active subscribers
59+
const promises = Array.from(activeThemeSubscriptions).map(async (responseStream) => {
60+
try {
61+
const event = String.create({
62+
value: themeJson,
63+
})
64+
await responseStream(
65+
event,
66+
false, // Not the last message
67+
)
68+
} catch (error) {
69+
console.error("Error sending theme event:", error)
70+
// Remove the subscription if there was an error
71+
activeThemeSubscriptions.delete(responseStream)
72+
}
73+
})
74+
75+
await Promise.all(promises)
76+
}

src/core/webview/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { findLast } from "@shared/array"
88
import { readFile } from "fs/promises"
99
import path from "node:path"
1010
import { WebviewProviderType } from "@/shared/webview/types"
11+
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
1112

1213
/*
1314
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 {
139140
vscode.workspace.onDidChangeConfiguration(
140141
async (e) => {
141142
if (e && e.affectsConfiguration("workbench.colorTheme")) {
142-
// Sends latest theme name to webview
143-
await this.controller.postMessageToWebview({
144-
type: "theme",
145-
text: JSON.stringify(await getTheme()),
146-
})
143+
// Send theme update via gRPC subscription
144+
const theme = await getTheme()
145+
if (theme) {
146+
await sendThemeEvent(JSON.stringify(theme))
147+
}
147148
}
148149
if (e && e.affectsConfiguration("cline.mcpMarketplace.enabled")) {
149150
// Update state when marketplace tab setting changes

src/shared/ExtensionMessage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export interface ExtensionMessage {
1919
| "selectedImages"
2020
| "ollamaModels"
2121
| "lmStudioModels"
22-
| "theme"
2322
| "workspaceUpdated"
2423
| "openRouterModels"
2524
| "openAiModels"

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,6 @@ export const ExtensionStateContextProvider: React.FC<{
197197
const handleMessage = useCallback((event: MessageEvent) => {
198198
const message: ExtensionMessage = event.data
199199
switch (message.type) {
200-
case "theme": {
201-
if (message.text) {
202-
setTheme(convertTextMateToHljs(JSON.parse(message.text)))
203-
}
204-
break
205-
}
206200
case "workspaceUpdated": {
207201
setFilePaths(message.filePaths ?? [])
208202
break
@@ -246,6 +240,7 @@ export const ExtensionStateContextProvider: React.FC<{
246240
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
247241
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
248242
const mcpMarketplaceUnsubscribeRef = useRef<(() => void) | null>(null)
243+
const themeSubscriptionRef = useRef<(() => void) | null>(null)
249244

250245
// Subscribe to state updates and UI events using the gRPC streaming API
251246
useEffect(() => {
@@ -445,6 +440,27 @@ export const ExtensionStateContextProvider: React.FC<{
445440
},
446441
})
447442

443+
// Subscribe to theme changes
444+
themeSubscriptionRef.current = UiServiceClient.subscribeToTheme(EmptyRequest.create({}), {
445+
onResponse: (response) => {
446+
if (response.value) {
447+
try {
448+
const themeData = JSON.parse(response.value)
449+
setTheme(convertTextMateToHljs(themeData))
450+
console.log("[DEBUG] Received theme update from gRPC stream")
451+
} catch (error) {
452+
console.error("Error parsing theme data:", error)
453+
}
454+
}
455+
},
456+
onError: (error) => {
457+
console.error("Error in theme subscription:", error)
458+
},
459+
onComplete: () => {
460+
console.log("Theme subscription completed")
461+
},
462+
})
463+
448464
// Still send the webviewDidLaunch message for other initialization
449465
vscode.postMessage({ type: "webviewDidLaunch" })
450466

@@ -497,6 +513,10 @@ export const ExtensionStateContextProvider: React.FC<{
497513
mcpMarketplaceUnsubscribeRef.current()
498514
mcpMarketplaceUnsubscribeRef.current = null
499515
}
516+
if (themeSubscriptionRef.current) {
517+
themeSubscriptionRef.current()
518+
themeSubscriptionRef.current = null
519+
}
500520
}
501521
}, [])
502522

0 commit comments

Comments
 (0)