From 12f10fc9a31ac7e1a25c6762a11b3137f344188d Mon Sep 17 00:00:00 2001 From: Chris Hasson Date: Fri, 17 Oct 2025 09:18:58 -0700 Subject: [PATCH 1/3] feat(shared): add type-safe global state update handling Introduce type-safe utilities for global state updates across the extension. This includes new types in GlobalStateTypes.ts, updated message handling in webviewMessageHandler.ts and WebviewMessage.ts, and helper functions in globalStateHelpers.ts with comprehensive tests. Ensures compile-time safety for state key-value pairs and prevents runtime errors from invalid state updates. --- packages/types/src/global-settings.ts | 1 + src/core/webview/ClineProvider.ts | 3 ++ src/core/webview/webviewMessageHandler.ts | 17 ++++++++++- src/shared/ExtensionMessage.ts | 1 + src/shared/GlobalStateTypes.ts | 35 ++++++++++++++++++++++ src/shared/WebviewMessage.ts | 14 +++++++++ webview-ui/src/utils/globalStateHelpers.ts | 12 ++++++++ 7 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/shared/GlobalStateTypes.ts create mode 100644 webview-ui/src/utils/globalStateHelpers.ts diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 17101acaa98..2b74201c5f3 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -168,6 +168,7 @@ export const globalSettingsSchema = z.object({ terminalCommandApiConfigId: z.string().optional(), // kilocode_change ghostServiceSettings: ghostServiceSettingsSchema, // kilocode_change hasPerformedOrganizationAutoSwitch: z.boolean().optional(), // kilocode_change + lastViewedReleaseVersion: z.string().optional(), // kilocode_change: Track last viewed release version includeTaskHistoryInEnhance: z.boolean().optional(), historyPreviewCollapsed: z.boolean().optional(), reasoningBlockCollapsed: z.boolean().optional(), diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b2d1eb3c910..b7c1bc6607e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1965,6 +1965,7 @@ ${prompt} dismissedNotificationIds, // kilocode_change morphApiKey, // kilocode_change fastApplyModel, // kilocode_change: Fast Apply model selection + lastViewedReleaseVersion, // kilocode_change alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, includeDiagnosticMessages, @@ -2143,6 +2144,7 @@ ${prompt} dismissedNotificationIds: dismissedNotificationIds ?? [], // kilocode_change morphApiKey, // kilocode_change fastApplyModel: fastApplyModel ?? "auto", // kilocode_change: Fast Apply model selection + lastViewedReleaseVersion, // kilocode_change alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, includeDiagnosticMessages: includeDiagnosticMessages ?? true, @@ -2356,6 +2358,7 @@ ${prompt} dismissedNotificationIds: stateValues.dismissedNotificationIds ?? [], // kilocode_change morphApiKey: stateValues.morphApiKey, // kilocode_change fastApplyModel: stateValues.fastApplyModel ?? "auto", // kilocode_change: Fast Apply model selection + lastViewedReleaseVersion: stateValues.lastViewedReleaseVersion, // kilocode_change: Track last viewed release version historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, cloudUserInfo, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c98064e7617..726a8cb937e 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -8,11 +8,13 @@ import * as vscode from "vscode" import axios from "axios" import { getKiloBaseUriFromToken } from "@roo-code/types" import { + MaybeTypedWebviewMessage, ProfileData, SeeNewChangesPayload, TaskHistoryRequestPayload, TasksByIdRequestPayload, } from "../../shared/WebviewMessage" +import { isValidGlobalStateKey } from "../../shared/GlobalStateTypes" // kilocode_change end import { @@ -85,7 +87,7 @@ import { fetchAndRefreshOrganizationModesOnStartup, refreshOrganizationModes } f export const webviewMessageHandler = async ( provider: ClineProvider, - message: WebviewMessage, + message: MaybeTypedWebviewMessage, marketplaceManager?: MarketplaceManager, ) => { // Utility functions provided for concise get/update of global state via contextProxy API. @@ -3772,5 +3774,18 @@ export const webviewMessageHandler = async ( }) break } + // kilocode_change start: Type-safe global state handler + case "updateGlobalState": { + if ( + message.stateKey !== undefined && + message.stateValue !== undefined && + isValidGlobalStateKey(message.stateKey) + ) { + await updateGlobalState(message.stateKey, message.stateValue) + await provider.postStateToWebview() + } + break + } + // kilocode_change end: Type-safe global state handler } } diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 4be68b0c297..2afdf80f7bc 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -353,6 +353,7 @@ export type ExtensionState = Pick< | "terminalCommandApiConfigId" // kilocode_change | "dismissedNotificationIds" // kilocode_change | "ghostServiceSettings" // kilocode_change + | "lastViewedReleaseVersion" // kilocode_change: Track last viewed release version | "condensingApiConfigId" | "customCondensingPrompt" | "codebaseIndexConfig" diff --git a/src/shared/GlobalStateTypes.ts b/src/shared/GlobalStateTypes.ts new file mode 100644 index 00000000000..d3646ddcf8e --- /dev/null +++ b/src/shared/GlobalStateTypes.ts @@ -0,0 +1,35 @@ +import type { GlobalState } from "../../packages/types/src/global-settings.js" +import { isGlobalStateKey } from "../../packages/types/src/global-settings.js" + +/** + * Utility type to extract the value type for a specific GlobalState key + */ +export type GlobalStateValue = GlobalState[K] + +/** + * Type-safe global state update payload + * Ensures that the stateKey exists in GlobalState and the value matches the expected type + */ +export type GlobalStateUpdatePayload = { + stateKey: K + stateValue: GlobalStateValue +} + +/** + * Type guard to check if a key is a valid GlobalState key + * Uses the existing isGlobalStateKey function from global-settings.ts + */ +export function isValidGlobalStateKey(key: string): key is keyof GlobalState { + return isGlobalStateKey(key) +} + +/** + * Type-safe helper for creating global state update messages + * This function ensures compile-time type safety for stateKey-value pairs + */ +export function createGlobalStateUpdate( + stateKey: K, + stateValue: GlobalStateValue, +): GlobalStateUpdatePayload { + return { stateKey, stateValue } +} diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index bc40f791f0e..c1cdf6eee41 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -16,6 +16,7 @@ import { } from "@roo-code/types" import { Mode } from "./modes" +import { GlobalStateUpdatePayload } from "./GlobalStateTypes" export type ClineAskResponse = | "yesButtonClicked" @@ -34,6 +35,12 @@ export interface UpdateTodoListPayload { export type EditQueuedMessagePayload = Pick +// kilocode_change start: Type-safe global state update message +type UpdateGlobalStateMessage = { + type: "updateGlobalState" +} & GlobalStateUpdatePayload + +// Base message interface for all other message types export interface WebviewMessage { type: | "updateTodoList" @@ -267,6 +274,7 @@ export interface WebviewMessage { | "dismissNotificationId" // kilocode_change | "tasksByIdRequest" // kilocode_change | "taskHistoryRequest" // kilocode_change + | "updateGlobalState" // kilocode_change - compatibility with GlobalStateUpdatePayload | "shareTaskSuccess" | "exportMode" | "exportModeResult" @@ -292,6 +300,8 @@ export interface WebviewMessage { | "editQueuedMessage" | "dismissUpsell" | "getDismissedUpsells" + stateKey?: string // kilocode_change - compatibility with GlobalStateUpdatePayload + stateValue?: any // kilocode_change - compatibility with GlobalStateUpdatePayload text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -386,6 +396,10 @@ export interface WebviewMessage { } } +// kilocode_change start: Create discriminated union for type-safe messages +export type MaybeTypedWebviewMessage = WebviewMessage | UpdateGlobalStateMessage +// kilocode_change end: Create discriminated union for type-safe messages + // kilocode_change begin export type OrganizationRole = "owner" | "admin" | "member" diff --git a/webview-ui/src/utils/globalStateHelpers.ts b/webview-ui/src/utils/globalStateHelpers.ts new file mode 100644 index 00000000000..2081f02ee76 --- /dev/null +++ b/webview-ui/src/utils/globalStateHelpers.ts @@ -0,0 +1,12 @@ +import { vscode } from "@src/utils/vscode" +import type { GlobalState } from "@roo-code/types" +import { createGlobalStateUpdate, type GlobalStateValue } from "@roo/GlobalStateTypes" + +/** + * Type-safe helper for sending global state updates from the WebView + * This ensures compile-time type safety when updating global state + */ +export function updateHostGlobalState(stateKey: K, stateValue: GlobalStateValue): void { + const updatePayload = createGlobalStateUpdate(stateKey, stateValue) + vscode.postMessage({ type: "updateGlobalState", ...updatePayload }) +} From f552b2e2ca608ac22c08c15d8bf237de30f0f26a Mon Sep 17 00:00:00 2001 From: Chris Hasson Date: Fri, 17 Oct 2025 09:55:15 -0700 Subject: [PATCH 2/3] Remove lastViewedReleaseVersion for now --- src/shared/ExtensionMessage.ts | 1 - webview-ui/src/utils/globalStateHelpers.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 2afdf80f7bc..4be68b0c297 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -353,7 +353,6 @@ export type ExtensionState = Pick< | "terminalCommandApiConfigId" // kilocode_change | "dismissedNotificationIds" // kilocode_change | "ghostServiceSettings" // kilocode_change - | "lastViewedReleaseVersion" // kilocode_change: Track last viewed release version | "condensingApiConfigId" | "customCondensingPrompt" | "codebaseIndexConfig" diff --git a/webview-ui/src/utils/globalStateHelpers.ts b/webview-ui/src/utils/globalStateHelpers.ts index 2081f02ee76..3ae22b7aea6 100644 --- a/webview-ui/src/utils/globalStateHelpers.ts +++ b/webview-ui/src/utils/globalStateHelpers.ts @@ -10,3 +10,5 @@ export function updateHostGlobalState(stateKey: K, const updatePayload = createGlobalStateUpdate(stateKey, stateValue) vscode.postMessage({ type: "updateGlobalState", ...updatePayload }) } + +updateHostGlobalState("dismissedUpsells", []) From 123c732d9500f4f3a446e191fee19e2372623296 Mon Sep 17 00:00:00 2001 From: Chris Hasson Date: Fri, 17 Oct 2025 09:56:16 -0700 Subject: [PATCH 3/3] Remove lastViewedReleaseVersion for now --- packages/types/src/global-settings.ts | 1 - src/core/webview/ClineProvider.ts | 3 -- src/core/webview/webviewMessageHandler.ts | 29 ++++++++---------- src/shared/GlobalStateTypes.ts | 35 ---------------------- src/shared/WebviewMessage.ts | 18 +++++------ webview-ui/src/utils/globalStateHelpers.ts | 8 ++--- webview-ui/src/utils/vscode.ts | 2 +- 7 files changed, 25 insertions(+), 71 deletions(-) delete mode 100644 src/shared/GlobalStateTypes.ts diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 2b74201c5f3..17101acaa98 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -168,7 +168,6 @@ export const globalSettingsSchema = z.object({ terminalCommandApiConfigId: z.string().optional(), // kilocode_change ghostServiceSettings: ghostServiceSettingsSchema, // kilocode_change hasPerformedOrganizationAutoSwitch: z.boolean().optional(), // kilocode_change - lastViewedReleaseVersion: z.string().optional(), // kilocode_change: Track last viewed release version includeTaskHistoryInEnhance: z.boolean().optional(), historyPreviewCollapsed: z.boolean().optional(), reasoningBlockCollapsed: z.boolean().optional(), diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b7c1bc6607e..b2d1eb3c910 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1965,7 +1965,6 @@ ${prompt} dismissedNotificationIds, // kilocode_change morphApiKey, // kilocode_change fastApplyModel, // kilocode_change: Fast Apply model selection - lastViewedReleaseVersion, // kilocode_change alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, includeDiagnosticMessages, @@ -2144,7 +2143,6 @@ ${prompt} dismissedNotificationIds: dismissedNotificationIds ?? [], // kilocode_change morphApiKey, // kilocode_change fastApplyModel: fastApplyModel ?? "auto", // kilocode_change: Fast Apply model selection - lastViewedReleaseVersion, // kilocode_change alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false, followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000, includeDiagnosticMessages: includeDiagnosticMessages ?? true, @@ -2358,7 +2356,6 @@ ${prompt} dismissedNotificationIds: stateValues.dismissedNotificationIds ?? [], // kilocode_change morphApiKey: stateValues.morphApiKey, // kilocode_change fastApplyModel: stateValues.fastApplyModel ?? "auto", // kilocode_change: Fast Apply model selection - lastViewedReleaseVersion: stateValues.lastViewedReleaseVersion, // kilocode_change: Track last viewed release version historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, cloudUserInfo, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 726a8cb937e..653d5e5c772 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -6,15 +6,15 @@ import pWaitFor from "p-wait-for" import * as vscode from "vscode" // kilocode_change start import axios from "axios" -import { getKiloBaseUriFromToken } from "@roo-code/types" +import { getKiloBaseUriFromToken, isGlobalStateKey } from "@roo-code/types" import { MaybeTypedWebviewMessage, ProfileData, SeeNewChangesPayload, TaskHistoryRequestPayload, TasksByIdRequestPayload, + UpdateGlobalStateMessage, } from "../../shared/WebviewMessage" -import { isValidGlobalStateKey } from "../../shared/GlobalStateTypes" // kilocode_change end import { @@ -87,7 +87,7 @@ import { fetchAndRefreshOrganizationModesOnStartup, refreshOrganizationModes } f export const webviewMessageHandler = async ( provider: ClineProvider, - message: MaybeTypedWebviewMessage, + message: MaybeTypedWebviewMessage, // kilocode_change switch to MaybeTypedWebviewMessage for better type-safety marketplaceManager?: MarketplaceManager, ) => { // Utility functions provided for concise get/update of global state via contextProxy API. @@ -3501,6 +3501,16 @@ export const webviewMessageHandler = async ( break } // kilocode_change end + // kilocode_change start: Type-safe global state handler + case "updateGlobalState": { + const { stateKey, stateValue } = message as UpdateGlobalStateMessage + if (stateKey !== undefined && stateValue !== undefined && isGlobalStateKey(stateKey)) { + await updateGlobalState(stateKey, stateValue) + await provider.postStateToWebview() + } + break + } + // kilocode_change end: Type-safe global state handler case "insertTextToChatArea": provider.postMessageToWebview({ type: "insertTextToChatArea", text: message.text }) break @@ -3774,18 +3784,5 @@ export const webviewMessageHandler = async ( }) break } - // kilocode_change start: Type-safe global state handler - case "updateGlobalState": { - if ( - message.stateKey !== undefined && - message.stateValue !== undefined && - isValidGlobalStateKey(message.stateKey) - ) { - await updateGlobalState(message.stateKey, message.stateValue) - await provider.postStateToWebview() - } - break - } - // kilocode_change end: Type-safe global state handler } } diff --git a/src/shared/GlobalStateTypes.ts b/src/shared/GlobalStateTypes.ts deleted file mode 100644 index d3646ddcf8e..00000000000 --- a/src/shared/GlobalStateTypes.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { GlobalState } from "../../packages/types/src/global-settings.js" -import { isGlobalStateKey } from "../../packages/types/src/global-settings.js" - -/** - * Utility type to extract the value type for a specific GlobalState key - */ -export type GlobalStateValue = GlobalState[K] - -/** - * Type-safe global state update payload - * Ensures that the stateKey exists in GlobalState and the value matches the expected type - */ -export type GlobalStateUpdatePayload = { - stateKey: K - stateValue: GlobalStateValue -} - -/** - * Type guard to check if a key is a valid GlobalState key - * Uses the existing isGlobalStateKey function from global-settings.ts - */ -export function isValidGlobalStateKey(key: string): key is keyof GlobalState { - return isGlobalStateKey(key) -} - -/** - * Type-safe helper for creating global state update messages - * This function ensures compile-time type safety for stateKey-value pairs - */ -export function createGlobalStateUpdate( - stateKey: K, - stateValue: GlobalStateValue, -): GlobalStateUpdatePayload { - return { stateKey, stateValue } -} diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index c1cdf6eee41..d333321c576 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -12,11 +12,11 @@ import { // kilocode_change start CommitRange, HistoryItem, + GlobalState, // kilocode_change end } from "@roo-code/types" import { Mode } from "./modes" -import { GlobalStateUpdatePayload } from "./GlobalStateTypes" export type ClineAskResponse = | "yesButtonClicked" @@ -36,11 +36,14 @@ export interface UpdateTodoListPayload { export type EditQueuedMessagePayload = Pick // kilocode_change start: Type-safe global state update message -type UpdateGlobalStateMessage = { +export type GlobalStateValue = GlobalState[K] +export type UpdateGlobalStateMessage = { type: "updateGlobalState" -} & GlobalStateUpdatePayload + stateKey: K + stateValue: GlobalStateValue +} +// kilocode_change end: Type-safe global state update message -// Base message interface for all other message types export interface WebviewMessage { type: | "updateTodoList" @@ -274,7 +277,7 @@ export interface WebviewMessage { | "dismissNotificationId" // kilocode_change | "tasksByIdRequest" // kilocode_change | "taskHistoryRequest" // kilocode_change - | "updateGlobalState" // kilocode_change - compatibility with GlobalStateUpdatePayload + | "updateGlobalState" // kilocode_change | "shareTaskSuccess" | "exportMode" | "exportModeResult" @@ -300,8 +303,6 @@ export interface WebviewMessage { | "editQueuedMessage" | "dismissUpsell" | "getDismissedUpsells" - stateKey?: string // kilocode_change - compatibility with GlobalStateUpdatePayload - stateValue?: any // kilocode_change - compatibility with GlobalStateUpdatePayload text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud" @@ -396,9 +397,8 @@ export interface WebviewMessage { } } -// kilocode_change start: Create discriminated union for type-safe messages +// kilocode_change: Create discriminated union for type-safe messages export type MaybeTypedWebviewMessage = WebviewMessage | UpdateGlobalStateMessage -// kilocode_change end: Create discriminated union for type-safe messages // kilocode_change begin export type OrganizationRole = "owner" | "admin" | "member" diff --git a/webview-ui/src/utils/globalStateHelpers.ts b/webview-ui/src/utils/globalStateHelpers.ts index 3ae22b7aea6..a1e467fc83f 100644 --- a/webview-ui/src/utils/globalStateHelpers.ts +++ b/webview-ui/src/utils/globalStateHelpers.ts @@ -1,14 +1,10 @@ import { vscode } from "@src/utils/vscode" import type { GlobalState } from "@roo-code/types" -import { createGlobalStateUpdate, type GlobalStateValue } from "@roo/GlobalStateTypes" +import { GlobalStateValue } from "@roo/WebviewMessage" /** * Type-safe helper for sending global state updates from the WebView - * This ensures compile-time type safety when updating global state */ export function updateHostGlobalState(stateKey: K, stateValue: GlobalStateValue): void { - const updatePayload = createGlobalStateUpdate(stateKey, stateValue) - vscode.postMessage({ type: "updateGlobalState", ...updatePayload }) + vscode.postMessage({ type: "updateGlobalState", stateKey, stateValue }) } - -updateHostGlobalState("dismissedUpsells", []) diff --git a/webview-ui/src/utils/vscode.ts b/webview-ui/src/utils/vscode.ts index 2894f5af0a5..c2f8a7a5ee7 100644 --- a/webview-ui/src/utils/vscode.ts +++ b/webview-ui/src/utils/vscode.ts @@ -1,6 +1,6 @@ import type { WebviewApi } from "vscode-webview" -import { WebviewMessage } from "@roo/WebviewMessage" +import { MaybeTypedWebviewMessage as WebviewMessage } from "@roo/WebviewMessage" // kilocode_change - using MaybeTypedWebviewMessage /** * A utility wrapper around the acquireVsCodeApi() function, which enables