Files
cline/apps/vscode/webview-ui/src/context/ExtensionStateContext.tsx
T

1023 lines
36 KiB
TypeScript

import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { DEFAULT_PLATFORM, type ExtensionState } from "@shared/ExtensionMessage"
import { DEFAULT_MCP_DISPLAY_MODE } from "@shared/McpDisplayMode"
import type { UserInfo } from "@shared/proto/cline/account"
import { EmptyRequest } from "@shared/proto/cline/common"
import type { OpenRouterCompatibleModelInfo, ProviderModelsResponse } from "@shared/proto/cline/models"
import { OnboardingModelGroup, type TerminalProfile } from "@shared/proto/cline/state"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { fromProtobufModels } from "@shared/proto-conversions/models/typeConversion"
import type React from "react"
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import {
type ModelInfo,
openRouterDefaultModelId,
openRouterDefaultModelInfo,
requestyDefaultModelId,
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { Environment } from "../../../src/shared/config-types"
import type { McpServer, McpViewTab } from "../../../src/shared/mcp"
import {
createReplicaState,
type ReplicaState,
applyMessage as reducerApplyMessage,
applyStateSnapshot as reducerApplyStateSnapshot,
} from "../components/chat/chat-view/messageReducer"
import { McpServiceClient, ModelsServiceClient, StateServiceClient, UiServiceClient } from "../services/grpc-client"
export type ProviderId = string
interface ProviderModelsState {
providerId: ProviderId
models: Record<string, ModelInfo>
defaultModelId: string
configFingerprint: string
requestId: string
source?: string
fetchedAt: number
isLoading: boolean
isStale: boolean
error?: string
}
export interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
showWelcome: boolean
onboardingModels: OnboardingModelGroup | undefined
openRouterModels: Record<string, ModelInfo>
vercelAiGatewayModels: Record<string, ModelInfo>
hicapModels: Record<string, ModelInfo>
liteLlmModels: Record<string, ModelInfo>
openAiModels: string[]
requestyModels: Record<string, ModelInfo>
groqModels: Record<string, ModelInfo>
basetenModels: Record<string, ModelInfo>
huggingFaceModels: Record<string, ModelInfo>
providerModelsByProvider: Partial<Record<ProviderId, ProviderModelsState>>
latestModelRequestIdByProvider: Partial<Record<ProviderId, string>>
mcpServers: McpServer[]
totalTasksSize: number | null
lastDismissedCliBannerVersion: number
dismissedBanners?: Array<{ bannerId: string; dismissedAt: number }>
availableTerminalProfiles: TerminalProfile[]
// View state
showMarketplace: boolean
showMcp: boolean
mcpTab?: McpViewTab
showSettings: boolean
settingsTargetSection?: string
settingsInitialModelTab?: "recommended" | "free"
showHistory: boolean
showAccount: boolean
showWorktrees: boolean
showAnnouncement: boolean
expandTaskHeader: boolean
// Setters
setShowAnnouncement: (value: boolean) => void
setShouldShowAnnouncement: (value: boolean) => void
setMcpServers: (value: McpServer[]) => void
setRequestyModels: (value: Record<string, ModelInfo>) => void
setGroqModels: (value: Record<string, ModelInfo>) => void
setBasetenModels: (value: Record<string, ModelInfo>) => void
setHuggingFaceModels: (value: Record<string, ModelInfo>) => void
setGlobalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalClineRulesToggles: (toggles: Record<string, boolean>) => void
setLocalCursorRulesToggles: (toggles: Record<string, boolean>) => void
setLocalWindsurfRulesToggles: (toggles: Record<string, boolean>) => void
setLocalAgentsRulesToggles: (toggles: Record<string, boolean>) => void
setLocalWorkflowToggles: (toggles: Record<string, boolean>) => void
setGlobalWorkflowToggles: (toggles: Record<string, boolean>) => void
setGlobalSkillsToggles: (toggles: Record<string, boolean>) => void
setLocalSkillsToggles: (toggles: Record<string, boolean>) => void
setRemoteRulesToggles: (toggles: Record<string, boolean>) => void
setRemoteWorkflowToggles: (toggles: Record<string, boolean>) => void
setTotalTasksSize: (value: number | null) => void
setExpandTaskHeader: (value: boolean) => void
setShowWelcome: (value: boolean) => void
setOnboardingModels: (value: OnboardingModelGroup | undefined) => void
startProviderModelsRequest: (providerId: ProviderId, requestId: string) => void
applyProviderModelsResponse: (response: ProviderModelsResponse) => void
// Refresh functions
refreshOpenRouterModels: () => void
refreshVercelAiGatewayModels: () => void
refreshHicapModels: () => void
refreshLiteLlmModels: () => Promise<void>
setUserInfo: (userInfo?: UserInfo) => void
// Navigation state setters
setShowMarketplace: (value: boolean) => void
setShowMcp: (value: boolean) => void
setMcpTab: (tab?: McpViewTab) => void
// Navigation functions
navigateToMarketplace: () => void
navigateToMcp: (tab?: McpViewTab) => void
navigateToSettings: (targetSection?: string) => void
navigateToSettingsModelPicker: (opts: { targetSection?: string; initialModelTab?: "recommended" | "free" }) => void
navigateToHistory: () => void
navigateToAccount: () => void
navigateToWorktrees: () => void
navigateToChat: () => void
// Hide functions
hideSettings: () => void
hideHistory: () => void
hideAccount: () => void
hideWorktrees: () => void
hideAnnouncement: () => void
closeMarketplaceView: () => void
closeMcpView: () => void
// Event callbacks
onRelinquishControl: (callback: () => void) => () => void
}
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
export const ExtensionStateContextProvider: React.FC<{
children: React.ReactNode
}> = ({ children }) => {
// UI view state
const [showMarketplace, setShowMarketplace] = useState(false)
const [showMcp, setShowMcp] = useState(false)
const [mcpTab, setMcpTab] = useState<McpViewTab | undefined>(undefined)
const [showSettings, setShowSettings] = useState(false)
const [settingsTargetSection, setSettingsTargetSection] = useState<string | undefined>(undefined)
const [settingsInitialModelTab, setSettingsInitialModelTab] = useState<"recommended" | "free" | undefined>(undefined)
const [showHistory, setShowHistory] = useState(false)
const [showAccount, setShowAccount] = useState(false)
const [showWorktrees, setShowWorktrees] = useState(false)
const [showAnnouncement, setShowAnnouncement] = useState(false)
// Helper for MCP view
const closeMcpView = useCallback(() => {
setShowMcp(false)
setMcpTab(undefined)
}, [setShowMcp, setMcpTab])
const closeMarketplaceView = useCallback(() => {
setShowMarketplace(false)
}, [])
// Hide functions
const hideSettings = useCallback(() => {
setShowSettings(false)
setSettingsTargetSection(undefined)
setSettingsInitialModelTab(undefined)
}, [])
const hideHistory = useCallback(() => setShowHistory(false), [setShowHistory])
const hideAccount = useCallback(() => setShowAccount(false), [setShowAccount])
const hideWorktrees = useCallback(() => setShowWorktrees(false), [setShowWorktrees])
const hideAnnouncement = useCallback(() => setShowAnnouncement(false), [setShowAnnouncement])
// Navigation functions
const navigateToMcp = useCallback(
(tab?: McpViewTab) => {
setShowSettings(false)
setShowHistory(false)
setShowAccount(false)
setShowWorktrees(false)
closeMcpView()
if (tab) {
setMcpTab(tab)
}
setShowMarketplace(true)
},
[closeMcpView, setMcpTab, setShowSettings, setShowHistory, setShowAccount, setShowWorktrees],
)
const navigateToMarketplace = useCallback(() => {
setShowSettings(false)
closeMcpView()
setShowHistory(false)
setShowAccount(false)
setShowWorktrees(false)
setShowMarketplace(true)
}, [closeMcpView])
const navigateToSettings = useCallback(
(targetSection?: string) => {
closeMarketplaceView()
setShowHistory(false)
closeMcpView()
setShowAccount(false)
setShowWorktrees(false)
setSettingsTargetSection(targetSection)
setSettingsInitialModelTab(undefined)
setShowSettings(true)
},
[closeMarketplaceView, closeMcpView],
)
const navigateToSettingsModelPicker = useCallback(
(opts: { targetSection?: string; initialModelTab?: "recommended" | "free" }) => {
closeMarketplaceView()
setShowHistory(false)
closeMcpView()
setShowAccount(false)
setShowWorktrees(false)
setSettingsTargetSection(opts.targetSection)
setSettingsInitialModelTab(opts.initialModelTab)
setShowSettings(true)
},
[closeMarketplaceView, closeMcpView],
)
const navigateToHistory = useCallback(() => {
closeMarketplaceView()
setShowSettings(false)
closeMcpView()
setShowAccount(false)
setShowWorktrees(false)
setShowHistory(true)
}, [closeMarketplaceView, setShowSettings, closeMcpView, setShowAccount, setShowWorktrees, setShowHistory])
const navigateToAccount = useCallback(() => {
closeMarketplaceView()
setShowSettings(false)
closeMcpView()
setShowHistory(false)
setShowWorktrees(false)
setShowAccount(true)
}, [closeMarketplaceView, setShowSettings, closeMcpView, setShowHistory, setShowWorktrees, setShowAccount])
const navigateToWorktrees = useCallback(() => {
closeMarketplaceView()
setShowSettings(false)
closeMcpView()
setShowHistory(false)
setShowAccount(false)
setShowWorktrees(true)
}, [closeMarketplaceView, setShowSettings, closeMcpView, setShowHistory, setShowAccount, setShowWorktrees])
const navigateToChat = useCallback(() => {
closeMarketplaceView()
setShowSettings(false)
closeMcpView()
setShowHistory(false)
setShowAccount(false)
setShowWorktrees(false)
}, [closeMarketplaceView, setShowSettings, closeMcpView, setShowHistory, setShowAccount, setShowWorktrees])
const [state, setState] = useState<ExtensionState>({
version: "",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
browserSettings: DEFAULT_BROWSER_SETTINGS,
preferredLanguage: "English",
mode: "act",
platform: DEFAULT_PLATFORM,
environment: Environment.production,
telemetrySetting: "unset",
distinctId: "",
planActSeparateModelsSetting: true,
enableCheckpointsSetting: true,
mcpDisplayMode: DEFAULT_MCP_DISPLAY_MODE,
globalClineRulesToggles: {},
localClineRulesToggles: {},
localCursorRulesToggles: {},
localWindsurfRulesToggles: {},
localAgentsRulesToggles: {},
localWorkflowToggles: {},
globalWorkflowToggles: {},
shellIntegrationTimeout: 4000,
terminalReuseEnabled: true,
vscodeTerminalExecutionMode: "vscodeTerminal",
maxConsecutiveMistakes: 3,
defaultTerminalProfile: "default",
isNewUser: false,
welcomeViewCompleted: false,
onboardingModels: undefined,
mcpResponsesCollapsed: false, // Default value (expanded), will be overwritten by extension state
yoloModeToggled: false,
customPrompt: undefined,
useAutoCondense: false,
subagentsEnabled: false,
worktreesEnabled: { user: true, featureFlag: false },
favoritedModelIds: [],
lastDismissedInfoBannerVersion: 0,
lastDismissedModelBannerVersion: 0,
optOutOfRemoteConfig: false,
remoteConfigSettings: {},
backgroundCommandRunning: false,
backgroundCommandTaskId: undefined,
lastDismissedCliBannerVersion: 0,
backgroundEditEnabled: false,
showFeatureTips: true,
globalSkillsToggles: {},
localSkillsToggles: {},
// NEW: Add workspace information with defaults
workspaceRoots: [],
primaryRootIndex: 0,
isMultiRootWorkspace: false,
multiRootSetting: { user: false, featureFlag: false },
hooksEnabled: false,
})
const [expandTaskHeader, setExpandTaskHeader] = useState(true)
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
const [onboardingModels, setOnboardingModels] = useState<OnboardingModelGroup | undefined>(undefined)
const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
[openRouterDefaultModelId]: openRouterDefaultModelInfo,
})
const [vercelAiGatewayModels, setVercelAiGatewayModels] = useState<Record<string, ModelInfo>>({})
const [hicapModels, setHicapModels] = useState<Record<string, ModelInfo>>({})
const [liteLlmModels, setLiteLlmModels] = useState<Record<string, ModelInfo>>({})
const [totalTasksSize, setTotalTasksSize] = useState<number | null>(null)
const [availableTerminalProfiles, setAvailableTerminalProfiles] = useState<TerminalProfile[]>([])
const [openAiModels, _setOpenAiModels] = useState<string[]>([])
const [requestyModels, setRequestyModels] = useState<Record<string, ModelInfo>>({
[requestyDefaultModelId]: requestyDefaultModelInfo,
})
// Groq and Baseten model lists start empty. The pickers populate them
// from two sources: the SDK catalog over gRPC (`useProviderModels`)
// for the curated set, and the host-side refresh RPCs
// (`ModelsServiceClient.refreshGroqModelsRpc`,
// `ModelsServiceClient.refreshBasetenModels`) for any models the
// live API exposes on top of the SDK catalog.
const [groqModelsState, setGroqModels] = useState<Record<string, ModelInfo>>({})
const [basetenModelsState, setBasetenModels] = useState<Record<string, ModelInfo>>({})
const [huggingFaceModels, setHuggingFaceModels] = useState<Record<string, ModelInfo>>({})
const [providerModelsByProvider, setProviderModelsByProvider] = useState<Partial<Record<ProviderId, ProviderModelsState>>>({})
const [latestModelRequestIdByProvider, setLatestModelRequestIdByProvider] = useState<Partial<Record<ProviderId, string>>>({})
const latestModelRequestIdByProviderRef = useRef<Partial<Record<ProviderId, string>>>({})
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const startProviderModelsRequest = useCallback((providerId: ProviderId, requestId: string) => {
latestModelRequestIdByProviderRef.current = { ...latestModelRequestIdByProviderRef.current, [providerId]: requestId }
setLatestModelRequestIdByProvider((prev) => ({ ...prev, [providerId]: requestId }))
setProviderModelsByProvider((prev) => ({
...prev,
[providerId]: {
...(prev[providerId] ?? {
providerId,
models: {},
defaultModelId: "",
configFingerprint: "",
fetchedAt: 0,
isStale: false,
}),
providerId,
requestId,
isLoading: true,
error: undefined,
},
}))
}, [])
const applyProviderModelsResponse = useCallback((response: ProviderModelsResponse) => {
setProviderModelsByProvider((prevModels) => {
const latestRequestId = latestModelRequestIdByProviderRef.current[response.providerId]
if (latestRequestId !== response.requestId) {
console.debug("Dropping stale provider models response", {
providerId: response.providerId,
requestId: response.requestId,
latestRequestId,
})
return prevModels
}
return {
...prevModels,
[response.providerId]: {
providerId: response.providerId,
models: response.ok ? fromProtobufModels(response.models) : {},
defaultModelId: response.defaultModelId ?? "",
configFingerprint: response.configFingerprint,
requestId: response.requestId,
source: response.source,
fetchedAt: response.fetchedAt,
isLoading: false,
isStale: false,
error: response.ok ? undefined : response.error?.message,
},
}
})
}, [])
// References to store subscription cancellation functions
const stateSubscriptionRef = useRef<(() => void) | null>(null)
const marketplaceButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const worktreesButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
const openRouterModelsUnsubscribeRef = useRef<(() => void) | null>(null)
const liteLlmModelsUnsubscribeRef = useRef<(() => void) | null>(null)
const workspaceUpdatesUnsubscribeRef = useRef<(() => void) | null>(null)
const relinquishControlUnsubscribeRef = useRef<(() => void) | null>(null)
// Add ref for callbacks
const relinquishControlCallbacks = useRef<Set<() => void>>(new Set())
// Create hook function
const onRelinquishControl = useCallback((callback: () => void) => {
relinquishControlCallbacks.current.add(callback)
return () => {
relinquishControlCallbacks.current.delete(callback)
}
}, [])
const mcpServersSubscriptionRef = useRef<(() => void) | null>(null)
// Convergent-replica state for clineMessages. The partial-message stream and the full state
// snapshots both feed this reducer so the transcript converges correctly regardless of
// arrival order, duplication, or loss. See messageReducer.ts.
const replicaRef = useRef<ReplicaState>(createReplicaState())
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
// Set up state subscription
stateSubscriptionRef.current = StateServiceClient.subscribeToState(EmptyRequest.create({}), {
onResponse: (response: any) => {
if (response.stateJson) {
try {
const stateData = JSON.parse(response.stateJson) as ExtensionState
setState((prevState) => {
// Versioning logic for autoApprovalSettings
const incomingVersion = stateData.autoApprovalSettings?.version ?? 1
const currentVersion = prevState.autoApprovalSettings?.version ?? 1
const shouldUpdateAutoApproval = incomingVersion > currentVersion
// Route the snapshot's transcript through the convergent-replica reducer:
// merge by ts/seq within the same epoch (never truncate), replace on a
// newer epoch, ignore stale/older snapshots. Unstamped (classic/legacy)
// state defaults to epoch 0 / version 0, which merges.
replicaRef.current = reducerApplyStateSnapshot(
replicaRef.current,
stateData.clineMessages ?? [],
stateData.epoch ?? 0,
stateData.stateVersion ?? 0,
stateData.turnState,
)
stateData.clineMessages = replicaRef.current.messages
// Use the seq-gated turnState from the replica, NOT the raw snapshot's, so a
// late/stale snapshot carrying an older phase (e.g. "idle") cannot revert a
// newer phase (e.g. "streaming") and hide the Cancel button. Falls back to
// undefined for classic/legacy state.
stateData.turnState = replicaRef.current.turnState
const newState = {
...stateData,
autoApprovalSettings: shouldUpdateAutoApproval
? stateData.autoApprovalSettings
: prevState.autoApprovalSettings,
}
// Update welcome screen state based on API configuration if welcome view not in progress
if (!newState.welcomeViewCompleted && !showWelcome) {
setShowWelcome(true)
setOnboardingModels(newState.onboardingModels)
} else if (newState.welcomeViewCompleted) {
setShowWelcome(false)
setOnboardingModels(undefined)
}
setDidHydrateState(true)
return newState
})
} catch (error) {
console.error("Error parsing state JSON:", error)
console.log("[DEBUG] ERR getting state", error)
}
}
console.log('[DEBUG] ended "got subscribed state"')
},
onError: (error: any) => {
console.error("Error in state subscription:", error)
},
onComplete: () => {
console.log("State subscription completed")
},
})
// Subscribe to MCP button clicked events with webview type
mcpButtonUnsubscribeRef.current = UiServiceClient.subscribeToMcpButtonClicked(
{},
{
onResponse: () => {
console.log("[DEBUG] Received mcpButtonClicked event from gRPC stream")
navigateToMarketplace()
},
onError: (error: any) => {
console.error("Error in mcpButtonClicked subscription:", error)
},
onComplete: () => {
console.log("mcpButtonClicked subscription completed")
},
},
)
marketplaceButtonUnsubscribeRef.current = UiServiceClient.subscribeToMarketplaceButtonClicked(EmptyRequest.create({}), {
onResponse: () => {
console.log("[DEBUG] Received marketplaceButtonClicked event from gRPC stream")
navigateToMarketplace()
},
onError: (error: any) => {
console.error("Error in marketplaceButtonClicked subscription:", error)
},
onComplete: () => {
console.log("marketplaceButtonClicked subscription completed")
},
})
// Set up history button clicked subscription with webview type
historyButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToHistoryButtonClicked(
{},
{
onResponse: () => {
// When history button is clicked, navigate to history view
console.log("[DEBUG] Received history button clicked event from gRPC stream")
navigateToHistory()
},
onError: (error: any) => {
console.error("Error in history button clicked subscription:", error)
},
onComplete: () => {
console.log("History button clicked subscription completed")
},
},
)
// Subscribe to chat button clicked events with webview type
chatButtonUnsubscribeRef.current = UiServiceClient.subscribeToChatButtonClicked(
{},
{
onResponse: () => {
// When chat button is clicked, navigate to chat
console.log("[DEBUG] Received chat button clicked event from gRPC stream")
navigateToChat()
},
onError: (error: any) => {
console.error("Error in chat button subscription:", error)
},
onComplete: () => {},
},
)
// Subscribe to MCP servers updates
mcpServersSubscriptionRef.current = McpServiceClient.subscribeToMcpServers(EmptyRequest.create(), {
onResponse: (response: any) => {
console.log("[DEBUG] Received MCP servers update from gRPC stream")
if (response.mcpServers) {
setMcpServers(convertProtoMcpServersToMcpServers(response.mcpServers))
}
},
onError: (error: any) => {
console.error("Error in MCP servers subscription:", error)
},
onComplete: () => {
console.log("MCP servers subscription completed")
},
})
// Set up settings button clicked subscription
settingsButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToSettingsButtonClicked(EmptyRequest.create({}), {
onResponse: () => {
// When settings button is clicked, navigate to settings
navigateToSettings()
},
onError: (error: any) => {
console.error("Error in settings button clicked subscription:", error)
},
onComplete: () => {
console.log("Settings button clicked subscription completed")
},
})
// Set up worktrees button clicked subscription
worktreesButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToWorktreesButtonClicked(
EmptyRequest.create({}),
{
onResponse: () => {
// When worktrees button is clicked, navigate to worktrees
navigateToWorktrees()
},
onError: (error: any) => {
console.error("Error in worktrees button clicked subscription:", error)
},
onComplete: () => {
console.log("Worktrees button clicked subscription completed")
},
},
)
// Subscribe to partial message events
partialMessageUnsubscribeRef.current = UiServiceClient.subscribeToPartialMessage(EmptyRequest.create({}), {
onResponse: (protoMessage: any) => {
try {
// Validate critical fields
if (!protoMessage.ts || protoMessage.ts <= 0) {
console.error("Invalid timestamp in partial message:", protoMessage)
return
}
const partialMessage = convertProtoToClineMessage(protoMessage)
setState((prevState) => {
// Route through the convergent-replica reducer: merge by ts keeping the
// higher seq, fence stale epochs, never let an out-of-order or duplicate
// delivery corrupt the transcript. Unstamped (classic/legacy) messages
// default to epoch 0 and merge by ts as before.
const before = replicaRef.current
replicaRef.current = reducerApplyMessage(before, partialMessage)
if (replicaRef.current === before) {
// Stale/ignored — no change.
return prevState
}
return { ...prevState, clineMessages: replicaRef.current.messages }
})
} catch (error) {
console.error("Failed to process partial message:", error, protoMessage)
}
},
onError: (error: any) => {
console.error("Error in partialMessage subscription:", error)
},
onComplete: () => {
console.log("[DEBUG] partialMessage subscription completed")
},
})
// Subscribe to OpenRouter models updates
openRouterModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToOpenRouterModels(EmptyRequest.create({}), {
onResponse: (response: OpenRouterCompatibleModelInfo) => {
const models = fromProtobufModels(response.models)
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
},
onError: (error: any) => {
console.error("Error in OpenRouter models subscription:", error)
},
onComplete: () => {
console.log("OpenRouter models subscription completed")
},
})
// Subscribe to LiteLLM models updates
liteLlmModelsUnsubscribeRef.current = ModelsServiceClient.subscribeToLiteLlmModels(EmptyRequest.create({}), {
onResponse: (response: OpenRouterCompatibleModelInfo) => {
const models = fromProtobufModels(response.models)
setLiteLlmModels(models)
},
onError: (error: any) => {
console.error("Error in LiteLLM models subscription:", error)
},
onComplete: () => {
console.log("LiteLLM models subscription completed")
},
})
// Initialize webview using gRPC
UiServiceClient.initializeWebview(EmptyRequest.create({}))
.then(() => {
console.log("[DEBUG] Webview initialization completed via gRPC")
})
.catch((error) => {
console.error("Failed to initialize webview via gRPC:", error)
})
// Set up account button clicked subscription
accountButtonClickedSubscriptionRef.current = UiServiceClient.subscribeToAccountButtonClicked(EmptyRequest.create(), {
onResponse: () => {
// When account button is clicked, navigate to account view
console.log("[DEBUG] Received account button clicked event from gRPC stream")
navigateToAccount()
},
onError: (error: any) => {
console.error("Error in account button clicked subscription:", error)
},
onComplete: () => {
console.log("Account button clicked subscription completed")
},
})
// Fetch available terminal profiles on launch
StateServiceClient.getAvailableTerminalProfiles(EmptyRequest.create({}))
.then((response) => {
setAvailableTerminalProfiles(response.profiles)
})
.catch((error) => {
console.error("Failed to fetch available terminal profiles:", error)
})
// Subscribe to relinquish control events
relinquishControlUnsubscribeRef.current = UiServiceClient.subscribeToRelinquishControl(EmptyRequest.create({}), {
onResponse: () => {
// Call all registered callbacks
relinquishControlCallbacks.current.forEach((callback) => {
callback()
})
},
onError: (error: any) => {
console.error("Error in relinquishControl subscription:", error)
},
onComplete: () => {},
})
// Clean up subscriptions when component unmounts
return () => {
if (stateSubscriptionRef.current) {
stateSubscriptionRef.current()
stateSubscriptionRef.current = null
}
if (mcpButtonUnsubscribeRef.current) {
mcpButtonUnsubscribeRef.current()
mcpButtonUnsubscribeRef.current = null
}
if (marketplaceButtonUnsubscribeRef.current) {
marketplaceButtonUnsubscribeRef.current()
marketplaceButtonUnsubscribeRef.current = null
}
if (historyButtonClickedSubscriptionRef.current) {
historyButtonClickedSubscriptionRef.current()
historyButtonClickedSubscriptionRef.current = null
}
if (chatButtonUnsubscribeRef.current) {
chatButtonUnsubscribeRef.current()
chatButtonUnsubscribeRef.current = null
}
if (accountButtonClickedSubscriptionRef.current) {
accountButtonClickedSubscriptionRef.current()
accountButtonClickedSubscriptionRef.current = null
}
if (settingsButtonClickedSubscriptionRef.current) {
settingsButtonClickedSubscriptionRef.current()
settingsButtonClickedSubscriptionRef.current = null
}
if (worktreesButtonClickedSubscriptionRef.current) {
worktreesButtonClickedSubscriptionRef.current()
worktreesButtonClickedSubscriptionRef.current = null
}
if (partialMessageUnsubscribeRef.current) {
partialMessageUnsubscribeRef.current()
partialMessageUnsubscribeRef.current = null
}
if (openRouterModelsUnsubscribeRef.current) {
openRouterModelsUnsubscribeRef.current()
openRouterModelsUnsubscribeRef.current = null
}
if (liteLlmModelsUnsubscribeRef.current) {
liteLlmModelsUnsubscribeRef.current()
liteLlmModelsUnsubscribeRef.current = null
}
if (workspaceUpdatesUnsubscribeRef.current) {
workspaceUpdatesUnsubscribeRef.current()
workspaceUpdatesUnsubscribeRef.current = null
}
if (relinquishControlUnsubscribeRef.current) {
relinquishControlUnsubscribeRef.current()
relinquishControlUnsubscribeRef.current = null
}
if (mcpServersSubscriptionRef.current) {
mcpServersSubscriptionRef.current()
mcpServersSubscriptionRef.current = null
}
}
}, [])
const refreshOpenRouterModels = useCallback(() => {
ModelsServiceClient.refreshOpenRouterModelsRpc(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = fromProtobufModels(response.models)
setOpenRouterModels({
[openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
...models,
})
})
.catch((error: Error) => console.error("Failed to refresh OpenRouter models:", error))
}, [])
const refreshHicapModels = useCallback(() => {
ModelsServiceClient.refreshHicapModels(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = response.models
setHicapModels({
...models,
})
})
.catch((error: Error) => console.error("Failed to refresh Hicap models:", error))
}, [])
const refreshLiteLlmModels = useCallback(() => {
return ModelsServiceClient.refreshLiteLlmModelsRpc(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = fromProtobufModels(response.models)
setLiteLlmModels(models)
})
.catch((error: Error) => console.error("Failed to refresh LiteLLM models:", error))
}, [])
const refreshBasetenModels = useCallback(() => {
ModelsServiceClient.refreshBasetenModelsRpc(EmptyRequest.create({}))
.then((response) => {
// Live-fetched Baseten models. The SDK-curated catalog is
// pulled separately by BasetenModelPicker via
// `useProviderModels("baseten")` and merged on top of this
// dynamic slice at render time.
setBasetenModels(fromProtobufModels(response.models))
})
.catch((err) => console.error("Failed to refresh Baseten models:", err))
}, [])
const refreshVercelAiGatewayModels = useCallback(() => {
ModelsServiceClient.refreshVercelAiGatewayModelsRpc(EmptyRequest.create({}))
.then((response: OpenRouterCompatibleModelInfo) => {
const models = fromProtobufModels(response.models)
setVercelAiGatewayModels(models)
})
.catch((error: Error) => console.error("Failed to refresh Vercel AI Gateway models:", error))
}, [])
// Auto-refresh model lists on API key availability
useEffect(() => {
if (!openRouterModels || Object.keys(openRouterModels).length <= 1) {
refreshOpenRouterModels()
}
if (!vercelAiGatewayModels || Object.keys(vercelAiGatewayModels).length === 0) {
refreshVercelAiGatewayModels()
}
if (state.apiConfiguration?.basetenApiKey) {
refreshBasetenModels()
}
if (state.apiConfiguration?.liteLlmApiKey) {
refreshLiteLlmModels()
}
}, [
refreshOpenRouterModels,
refreshVercelAiGatewayModels,
state?.apiConfiguration?.basetenApiKey,
refreshBasetenModels,
state?.apiConfiguration?.liteLlmApiKey,
refreshLiteLlmModels,
])
const contextValue: ExtensionStateContextType = {
...state,
didHydrateState,
showWelcome,
onboardingModels,
openRouterModels,
vercelAiGatewayModels,
hicapModels,
liteLlmModels,
openAiModels,
requestyModels,
groqModels: groqModelsState,
basetenModels: basetenModelsState,
huggingFaceModels,
providerModelsByProvider,
latestModelRequestIdByProvider,
mcpServers,
totalTasksSize,
availableTerminalProfiles,
showMarketplace,
showMcp,
mcpTab,
showSettings,
settingsTargetSection,
settingsInitialModelTab,
showHistory,
showAccount,
showWorktrees,
showAnnouncement,
globalClineRulesToggles: state.globalClineRulesToggles || {},
localClineRulesToggles: state.localClineRulesToggles || {},
localCursorRulesToggles: state.localCursorRulesToggles || {},
localWindsurfRulesToggles: state.localWindsurfRulesToggles || {},
localAgentsRulesToggles: state.localAgentsRulesToggles || {},
localWorkflowToggles: state.localWorkflowToggles || {},
globalWorkflowToggles: state.globalWorkflowToggles || {},
remoteRulesToggles: state.remoteRulesToggles || {},
remoteWorkflowToggles: state.remoteWorkflowToggles || {},
enableCheckpointsSetting: state.enableCheckpointsSetting,
// Navigation functions
navigateToMarketplace,
navigateToMcp,
navigateToSettings,
navigateToSettingsModelPicker,
navigateToHistory,
navigateToAccount,
navigateToWorktrees,
navigateToChat,
// Hide functions
hideSettings,
hideHistory,
hideAccount,
hideWorktrees,
hideAnnouncement,
closeMarketplaceView,
setShowAnnouncement,
setShowWelcome,
setOnboardingModels,
startProviderModelsRequest,
applyProviderModelsResponse,
setShouldShowAnnouncement: (value) =>
setState((prevState) => ({
...prevState,
shouldShowAnnouncement: value,
})),
setMcpServers,
setRequestyModels,
setGroqModels,
setBasetenModels,
setHuggingFaceModels,
setShowMarketplace,
setShowMcp,
closeMcpView,
setGlobalClineRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
globalClineRulesToggles: toggles,
})),
setLocalClineRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localClineRulesToggles: toggles,
})),
setLocalCursorRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localCursorRulesToggles: toggles,
})),
setLocalWindsurfRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localWindsurfRulesToggles: toggles,
})),
setLocalAgentsRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localAgentsRulesToggles: toggles,
})),
setLocalWorkflowToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localWorkflowToggles: toggles,
})),
setGlobalWorkflowToggles: (toggles) =>
setState((prevState) => ({
...prevState,
globalWorkflowToggles: toggles,
})),
setGlobalSkillsToggles: (toggles) =>
setState((prevState) => ({
...prevState,
globalSkillsToggles: toggles,
})),
setLocalSkillsToggles: (toggles) =>
setState((prevState) => ({
...prevState,
localSkillsToggles: toggles,
})),
setRemoteRulesToggles: (toggles) =>
setState((prevState) => ({
...prevState,
remoteRulesToggles: toggles,
})),
setRemoteWorkflowToggles: (toggles) =>
setState((prevState) => ({
...prevState,
remoteWorkflowToggles: toggles,
})),
setMcpTab,
setTotalTasksSize,
refreshOpenRouterModels,
refreshVercelAiGatewayModels,
refreshHicapModels,
refreshLiteLlmModels,
onRelinquishControl,
setUserInfo: (userInfo?: UserInfo) => setState((prevState) => ({ ...prevState, userInfo })),
expandTaskHeader,
setExpandTaskHeader,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
}
export const useExtensionState = () => {
const context = useContext(ExtensionStateContext)
if (context === undefined) {
throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
}
return context
}