Compare commits

...

2 Commits

Author SHA1 Message Date
celestial-vault 449bb4fe67 merge conflicts 2025-06-06 19:39:46 -07:00
celestial-vault 6150d34ace migrate relinquishControl 2025-06-06 18:33:37 -07:00
11 changed files with 140 additions and 89 deletions
+3
View File
@@ -252,4 +252,7 @@ service UiService {
// Initialize webview when it launches
rpc initializeWebview(EmptyRequest) returns (Empty);
// Subscribe to relinquish control events
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
}
+2 -36
View File
@@ -42,6 +42,7 @@ import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { sendRelinquishControlEvent } from "./ui/subscribeToRelinquishControl"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -220,41 +221,6 @@ export class Controller {
await this.fetchMcpMarketplace(message.bool)
break
}
// case "openMcpMarketplaceServerDetails": {
// if (message.text) {
// const response = await fetch(`https://api.cline.bot/v1/mcp/marketplace/item?mcpId=${message.mcpId}`)
// const details: McpDownloadResponse = await response.json()
// if (details.readmeContent) {
// // Disable markdown preview markers
// const config = vscode.workspace.getConfiguration("markdown")
// await config.update("preview.markEditorSelection", false, true)
// // Create URI with base64 encoded markdown content
// const uri = vscode.Uri.parse(
// `${DIFF_VIEW_URI_SCHEME}:${details.name} README?${Buffer.from(details.readmeContent).toString("base64")}`,
// )
// // close existing
// const tabs = vscode.window.tabGroups.all
// .flatMap((tg) => tg.tabs)
// .filter((tab) => tab.label && tab.label.includes("README") && tab.label.includes("Preview"))
// for (const tab of tabs) {
// await vscode.window.tabGroups.close(tab)
// }
// // Show only the preview
// await vscode.commands.executeCommand("markdown.showPreview", uri, {
// sideBySide: true,
// preserveFocus: true,
// })
// }
// }
// this.postMessageToWebview({ type: "relinquishControl" })
// break
// }
// telemetry
case "telemetrySetting": {
@@ -280,7 +246,7 @@ export class Controller {
await this.deleteAllTaskHistory()
await this.postStateToWebview()
}
this.postMessageToWebview({ type: "relinquishControl" })
sendRelinquishControlEvent()
break
}
case "grpc_request": {
@@ -0,0 +1,55 @@
import { Controller } from "../index"
import { EmptyRequest, Empty } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active subscriptions
const activeRelinquishControlSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to relinquish control 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 subscribeToRelinquishControl(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activeRelinquishControlSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activeRelinquishControlSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "relinquish_control_subscription" }, responseStream)
}
}
/**
* Send a relinquish control event to all active subscribers
*/
export async function sendRelinquishControlEvent(): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activeRelinquishControlSubscriptions).map(async (responseStream) => {
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error("Error sending relinquish control event:", error)
// Remove the subscription if there was an error
activeRelinquishControlSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
+4 -3
View File
@@ -73,6 +73,7 @@ import { parseMentions } from "@core/mentions"
import { formatResponse } from "@core/prompts/responses"
import { addUserInstructions, SYSTEM_PROMPT } from "@core/prompts/system"
import { sendPartialMessageEvent } from "@core/controller/ui/subscribeToPartialMessage"
import { sendRelinquishControlEvent } from "@core/controller/ui/subscribeToRelinquishControl"
import { convertClineMessageToProto } from "@shared/proto-conversions/cline-message"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { FileContextTracker } from "@core/context/context-tracking/FileContextTracker"
@@ -517,11 +518,11 @@ export class Task {
await this.saveClineMessagesAndUpdateHistory()
await this.postMessageToWebview({ type: "relinquishControl" })
sendRelinquishControlEvent()
this.cancelTask() // the task is already cancelled by the provider beforehand, but we need to re-init to get the updated messages
} else {
await this.postMessageToWebview({ type: "relinquishControl" })
sendRelinquishControlEvent()
}
}
@@ -531,7 +532,7 @@ export class Task {
}
const relinquishButton = () => {
this.postMessageToWebview({ type: "relinquishControl" })
sendRelinquishControlEvent()
}
if (!this.enableCheckpoints) {
vscode.window.showInformationMessage("Checkpoints are disabled in settings. Cannot show diff.")
-1
View File
@@ -20,7 +20,6 @@ export interface ExtensionMessage {
| "openAiModels"
| "requestyModels"
| "mcpServers"
| "relinquishControl"
| "mcpDownloadDetails"
| "userCreditsBalance"
| "userCreditsUsage"
+7 -12
View File
@@ -237,7 +237,7 @@ export const ChatRowContent = ({
sendMessageFromChatRow,
onSetQuote,
}: ChatRowContentProps) => {
const { mcpServers, mcpMarketplaceCatalog } = useExtensionState()
const { mcpServers, mcpMarketplaceCatalog, onRelinquishControl } = useExtensionState()
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
const [quoteButtonState, setQuoteButtonState] = useState<QuoteButtonState>({
visible: false,
@@ -269,17 +269,12 @@ export const ChatRowContent = ({
const type = message.type === "ask" ? message.ask : message.say
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "relinquishControl": {
setSeeNewChangesDisabled(false)
break
}
}
}, [])
useEvent("message", handleMessage)
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
setSeeNewChangesDisabled(false)
})
}, [onRelinquishControl])
// --- Quote Button Logic ---
// MOVE handleQuoteClick INSIDE ChatRowContent
@@ -1,15 +1,14 @@
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { CheckpointsServiceClient } from "@/services/grpc-client"
import { flip, offset, shift, useFloating } from "@floating-ui/react"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { CheckpointRestoreRequest } from "@shared/proto/checkpoints"
import { Int64Request } from "@shared/proto/common"
import { ClineCheckpointRestore } from "@shared/WebviewMessage"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useEffect, useRef, useState } from "react"
import { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { useEvent } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface CheckmarkControlProps {
messageTs?: number
@@ -25,6 +24,7 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
const { refs, floatingStyles, update, placement } = useFloating({
placement: "bottom-end",
@@ -52,15 +52,16 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
}
}, [showRestoreConfirm, update])
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
}
}, [])
})
}, [onRelinquishControl])
const handleRestoreTask = async () => {
setRestoreTaskDisabled(true)
@@ -141,8 +142,6 @@ export const CheckmarkControl = ({ messageTs, isCheckpointCheckedOut }: Checkmar
setHasMouseEntered(false)
}
useEvent("message", handleMessage)
return (
<Container isMenuOpen={showRestoreConfirm} $isCheckedOut={isCheckpointCheckedOut} onMouseLeave={handleControlsMouseLeave}>
<i
@@ -1,12 +1,12 @@
import { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import { CheckpointsServiceClient } from "@/services/grpc-client"
import { ExtensionMessage } from "@shared/ExtensionMessage"
import { CheckpointRestoreRequest } from "@shared/proto/checkpoints"
import { Int64Request } from "@shared/proto/common"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCallback, useRef, useState } from "react"
import { useClickAway, useEvent } from "react-use"
import { useEffect, useRef, useState } from "react"
import { useClickAway } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface CheckpointOverlayProps {
messageTs?: number
@@ -21,6 +21,7 @@ export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
const [hasMouseEntered, setHasMouseEntered] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const tooltipRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
useClickAway(containerRef, () => {
if (showRestoreConfirm) {
@@ -29,21 +30,16 @@ export const CheckpointOverlay = ({ messageTs }: CheckpointOverlayProps) => {
}
})
const handleMessage = useCallback((event: MessageEvent) => {
const message: ExtensionMessage = event.data
switch (message.type) {
case "relinquishControl": {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
break
}
}
}, [])
useEvent("message", handleMessage)
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
setCompareDisabled(false)
setRestoreTaskDisabled(false)
setRestoreWorkspaceDisabled(false)
setRestoreBothDisabled(false)
setShowRestoreConfirm(false)
})
}, [onRelinquishControl])
const handleRestoreTask = async () => {
setRestoreTaskDisabled(true)
@@ -49,7 +49,7 @@ const CustomFilterRadio = ({ checked, onChange, icon, label }: CustomFilterRadio
const HistoryView = ({ onDone }: HistoryViewProps) => {
const extensionStateContext = useExtensionState()
const { taskHistory, filePaths } = extensionStateContext
const { taskHistory, filePaths, onRelinquishControl } = extensionStateContext
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
@@ -130,12 +130,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
[showFavoritesOnly, loadTaskHistory],
)
const handleMessage = useCallback((event: MessageEvent<ExtensionMessage>) => {
if (event.data.type === "relinquishControl") {
// Use the onRelinquishControl hook instead of message event
useEffect(() => {
return onRelinquishControl(() => {
setDeleteAllDisabled(false)
}
}, [])
useEvent("message", handleMessage)
})
}, [onRelinquishControl])
const { totalTasksSize, setTotalTasksSize } = extensionStateContext
@@ -1,9 +1,10 @@
import { McpServiceClient } from "@/services/grpc-client"
import { McpMarketplaceItem, McpServer } from "@shared/mcp"
import { StringRequest } from "@shared/proto/common"
import { useCallback, useMemo, useRef, useState } from "react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useEvent } from "react-use"
import styled from "styled-components"
import { useExtensionState } from "@/context/ExtensionStateContext"
interface McpMarketplaceCardProps {
item: McpMarketplaceItem
@@ -15,6 +16,7 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
const [isDownloading, setIsDownloading] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const githubLinkRef = useRef<HTMLDivElement>(null)
const { onRelinquishControl } = useExtensionState()
const handleMessage = useCallback((event: MessageEvent) => {
const message = event.data
@@ -22,14 +24,17 @@ const McpMarketplaceCard = ({ item, installedServers }: McpMarketplaceCardProps)
case "mcpDownloadDetails":
setIsDownloading(false)
break
case "relinquishControl":
setIsLoading(false)
break
}
}, [])
useEvent("message", handleMessage)
useEffect(() => {
return onRelinquishControl(() => {
setIsLoading(false)
})
}, [onRelinquishControl])
const githubAuthorUrl = useMemo(() => {
const url = new URL(item.githubUrl)
const pathParts = url.pathname.split("/")
@@ -93,6 +93,9 @@ interface ExtensionStateContextType extends ExtensionState {
hideAccount: () => void
hideAnnouncement: () => void
closeMcpView: () => void
// Event callbacks
onRelinquishControl: (callback: () => void) => () => void
}
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -241,6 +244,18 @@ export const ExtensionStateContextProvider: React.FC<{
const themeSubscriptionRef = useRef<(() => void) | null>(null)
const openRouterModelsUnsubscribeRef = 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)
}
}, [])
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
@@ -515,6 +530,18 @@ export const ExtensionStateContextProvider: React.FC<{
},
})
// Subscribe to relinquish control events
relinquishControlUnsubscribeRef.current = UiServiceClient.subscribeToRelinquishControl(EmptyRequest.create({}), {
onResponse: () => {
// Call all registered callbacks
relinquishControlCallbacks.current.forEach((callback) => callback())
},
onError: (error) => {
console.error("Error in relinquishControl subscription:", error)
},
onComplete: () => {},
})
// Clean up subscriptions when component unmounts
return () => {
if (stateSubscriptionRef.current) {
@@ -561,6 +588,10 @@ export const ExtensionStateContextProvider: React.FC<{
workspaceUpdatesUnsubscribeRef.current()
workspaceUpdatesUnsubscribeRef.current = null
}
if (relinquishControlUnsubscribeRef.current) {
relinquishControlUnsubscribeRef.current()
relinquishControlUnsubscribeRef.current = null
}
}
}, [])
@@ -736,6 +767,7 @@ export const ExtensionStateContextProvider: React.FC<{
setMcpTab,
setTotalTasksSize,
refreshOpenRouterModels,
onRelinquishControl,
}
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>