mirror of
https://github.com/cline/cline.git
synced 2026-09-02 15:52:29 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 449bb4fe67 | |||
| 6150d34ace |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.")
|
||||
|
||||
@@ -20,7 +20,6 @@ export interface ExtensionMessage {
|
||||
| "openAiModels"
|
||||
| "requestyModels"
|
||||
| "mcpServers"
|
||||
| "relinquishControl"
|
||||
| "mcpDownloadDetails"
|
||||
| "userCreditsBalance"
|
||||
| "userCreditsUsage"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user