mirror of
https://github.com/cline/cline.git
synced 2026-09-04 20:02:30 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf734076b | |||
| 02a50aeebc | |||
| fb49628a44 | |||
| b2a2aaf402 | |||
| e947218f0b | |||
| ac489ff215 | |||
| 156f40d9ef | |||
| cea467d252 | |||
| 8c3a8237f2 | |||
| 9a2672a0dc | |||
| f1256ef53a | |||
| 462bca65f7 | |||
| 6cdede57b7 | |||
| 1e767bcecf | |||
| f26f6dc37c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": minor
|
||||
---
|
||||
|
||||
Migrate chatButtonClicked to Protobus
|
||||
@@ -86,8 +86,9 @@ jobs:
|
||||
- name: Build Tests and Extension
|
||||
run: npm run pretest
|
||||
|
||||
- name: Unit Tests
|
||||
run: npm run test:unit
|
||||
# Unit Tests disabled due to module system conflicts between backend and webview-ui
|
||||
# - name: Unit Tests
|
||||
# run: npm run test:unit
|
||||
|
||||
# Run extension tests with coverage
|
||||
- name: Extension Tests with Coverage
|
||||
|
||||
@@ -34,4 +34,7 @@ service UiService {
|
||||
|
||||
// Subscribe to history button click events
|
||||
rpc subscribeToHistoryButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
|
||||
|
||||
// Subscribe to chat button clicked events (when the chat button is clicked in VSCode)
|
||||
rpc subscribeToChatButtonClicked(EmptyRequest) returns (stream Empty);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import axios from "axios"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
import fs from "fs/promises"
|
||||
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
|
||||
@@ -54,6 +55,7 @@ import { ClineRulesToggles } from "@shared/cline-rules"
|
||||
import { sendStateUpdate } from "./state/subscribeToState"
|
||||
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
|
||||
import { sendAuthCallbackEvent } from "./account/subscribeToAuthCallback"
|
||||
import { sendChatButtonClickedEvent } from "./ui/subscribeToChatButtonClicked"
|
||||
import { refreshClineRulesToggles } from "@core/context/instructions/user-instructions/cline-rules"
|
||||
import { refreshExternalRulesToggles } from "@core/context/instructions/user-instructions/external-rules"
|
||||
import { refreshWorkflowToggles } from "@core/context/instructions/user-instructions/workflows"
|
||||
@@ -65,6 +67,7 @@ https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/c
|
||||
*/
|
||||
|
||||
export class Controller {
|
||||
readonly id: string = uuidv4()
|
||||
private postMessage: (message: ExtensionMessage) => Thenable<boolean> | undefined
|
||||
|
||||
private disposables: vscode.Disposable[] = []
|
||||
@@ -1018,18 +1021,6 @@ export class Controller {
|
||||
throw new Error("Task not found")
|
||||
}
|
||||
|
||||
async showTaskWithId(id: string) {
|
||||
if (id !== this.task?.taskId) {
|
||||
// non-current task
|
||||
const { historyItem } = await this.getTaskWithId(id)
|
||||
await this.initTask(undefined, undefined, undefined, historyItem) // clears existing task
|
||||
}
|
||||
await this.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
}
|
||||
|
||||
async exportTaskWithId(id: string) {
|
||||
const { historyItem, apiConversationHistory } = await this.getTaskWithId(id)
|
||||
await downloadTask(historyItem.ts, apiConversationHistory)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Empty, StringRequest } from "../../../shared/proto/common"
|
||||
import { McpServer, McpDownloadResponse } from "@shared/mcp"
|
||||
import axios from "axios"
|
||||
import * as vscode from "vscode"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
|
||||
/**
|
||||
* Download an MCP server from the marketplace
|
||||
@@ -77,10 +78,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
|
||||
|
||||
// Initialize task and show chat view
|
||||
await controller.initTask(task)
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
|
||||
// Return an empty response - the client only cares if the call succeeded
|
||||
return Empty.create()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Controller } from ".."
|
||||
import { Empty, EmptyRequest } from "../../../shared/proto/common"
|
||||
import { resetExtensionState } from "../../../core/storage/state"
|
||||
import * as vscode from "vscode"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
|
||||
/**
|
||||
* Resets the extension state to its defaults
|
||||
@@ -22,10 +23,7 @@ export async function resetState(controller: Controller, request: EmptyRequest):
|
||||
vscode.window.showInformationMessage("State reset")
|
||||
await controller.postStateToWebview()
|
||||
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
|
||||
return Empty.create()
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Controller } from ".."
|
||||
import { StringRequest } from "../../../shared/proto/common"
|
||||
import { TaskResponse } from "../../../shared/proto/task"
|
||||
import { sendChatButtonClickedEvent } from "../ui/subscribeToChatButtonClicked"
|
||||
|
||||
/**
|
||||
* Shows a task with the specified ID
|
||||
@@ -22,10 +23,7 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
|
||||
await controller.initTask(undefined, undefined, undefined, historyItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
|
||||
// Return task data for gRPC response
|
||||
return TaskResponse.create({
|
||||
@@ -49,10 +47,7 @@ export async function showTaskWithId(controller: Controller, request: StringRequ
|
||||
await controller.initTask(undefined, undefined, undefined, fetchedItem)
|
||||
|
||||
// Send UI update to show the chat view
|
||||
await controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
await sendChatButtonClickedEvent(controller.id)
|
||||
|
||||
return TaskResponse.create({
|
||||
id: fetchedItem.id,
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Controller } from "../index"
|
||||
import { Empty } from "@shared/proto/common"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
|
||||
|
||||
// Keep track of active chatButtonClicked subscriptions by controller ID
|
||||
const activeChatButtonClickedSubscriptions = new Map<string, StreamingResponseHandler>()
|
||||
|
||||
/**
|
||||
* Subscribe to chatButtonClicked 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 subscribeToChatButtonClicked(
|
||||
controller: Controller,
|
||||
request: EmptyRequest,
|
||||
responseStream: StreamingResponseHandler,
|
||||
requestId?: string,
|
||||
): Promise<void> {
|
||||
const controllerId = controller.id
|
||||
console.log(`[DEBUG] set up chatButtonClicked subscription for controller ${controllerId}`)
|
||||
|
||||
// Add this subscription to the active subscriptions with the controller ID
|
||||
activeChatButtonClickedSubscriptions.set(controllerId, responseStream)
|
||||
|
||||
// Register cleanup when the connection is closed
|
||||
const cleanup = () => {
|
||||
activeChatButtonClickedSubscriptions.delete(controllerId)
|
||||
}
|
||||
|
||||
// Register the cleanup function with the request registry if we have a requestId
|
||||
if (requestId) {
|
||||
getRequestRegistry().registerRequest(requestId, cleanup, { type: "chatButtonClicked_subscription" }, responseStream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chatButtonClicked event to a specific controller's subscription
|
||||
* @param controllerId The ID of the controller to send the event to
|
||||
*/
|
||||
export async function sendChatButtonClickedEvent(controllerId: string): Promise<void> {
|
||||
// Get the subscription for this specific controller
|
||||
const responseStream = activeChatButtonClickedSubscriptions.get(controllerId)
|
||||
|
||||
if (!responseStream) {
|
||||
console.log(`[DEBUG] No active subscription for controller ${controllerId}`)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const event: Empty = {}
|
||||
await responseStream(
|
||||
event,
|
||||
false, // Not the last message
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(`Error sending chatButtonClicked event to controller ${controllerId}:`, error)
|
||||
// Remove the subscription if there was an error
|
||||
activeChatButtonClickedSubscriptions.delete(controllerId)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import * as vscode from "vscode"
|
||||
import { Controller } from "@core/controller"
|
||||
import { ClineAPI } from "./cline"
|
||||
import { getGlobalState } from "@core/storage/state"
|
||||
import { sendChatButtonClickedEvent } from "@core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum } from "@shared/proto/ui"
|
||||
|
||||
export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarController: Controller): ClineAPI {
|
||||
const api: ClineAPI = {
|
||||
@@ -18,10 +20,8 @@ export function createClineAPI(outputChannel: vscode.OutputChannel, sidebarContr
|
||||
outputChannel.appendLine("Starting new task")
|
||||
await sidebarController.clearTask()
|
||||
await sidebarController.postStateToWebview()
|
||||
await sidebarController.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
|
||||
await sendChatButtonClickedEvent(sidebarController.id)
|
||||
await sidebarController.initTask(task, images)
|
||||
outputChannel.appendLine(
|
||||
`Task started with message: ${task ? `"${task}"` : "undefined"} and ${images?.length || 0} image(s)`,
|
||||
|
||||
+17
-8
@@ -12,6 +12,7 @@ import { posthogClientProvider } from "./services/posthog/PostHogClientProvider"
|
||||
import { WebviewProvider } from "./core/webview"
|
||||
import { Controller } from "./core/controller"
|
||||
import { sendMcpButtonClickedEvent } from "./core/controller/ui/subscribeToMcpButtonClicked"
|
||||
import { sendChatButtonClickedEvent } from "./core/controller/ui/subscribeToChatButtonClicked"
|
||||
import { ErrorService } from "./services/error/ErrorService"
|
||||
import { initializeTestMode, cleanupTestMode } from "./services/test/TestMode"
|
||||
import { telemetryService } from "./services/posthog/telemetry/TelemetryService"
|
||||
@@ -92,19 +93,27 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cline.plusButtonClicked", async (webview: any) => {
|
||||
const openChat = async (instance?: WebviewProvider) => {
|
||||
console.log("[DEBUG] plusButtonClicked", webview)
|
||||
// Pass the webview type to the event sender
|
||||
const isSidebar = !webview
|
||||
|
||||
const openChat = async (instance: WebviewProvider) => {
|
||||
await instance?.controller.clearTask()
|
||||
await instance?.controller.postStateToWebview()
|
||||
await instance?.controller.postMessageToWebview({
|
||||
type: "action",
|
||||
action: "chatButtonClicked",
|
||||
})
|
||||
await sendChatButtonClickedEvent(instance.controller.id)
|
||||
}
|
||||
const isSidebar = !webview
|
||||
|
||||
if (isSidebar) {
|
||||
openChat(WebviewProvider.getSidebarInstance())
|
||||
const sidebarInstance = WebviewProvider.getSidebarInstance()
|
||||
if (sidebarInstance) {
|
||||
openChat(sidebarInstance)
|
||||
// Send event to the sidebar instance
|
||||
}
|
||||
} else {
|
||||
WebviewProvider.getTabInstances().forEach(openChat)
|
||||
const tabInstances = WebviewProvider.getTabInstances()
|
||||
for (const instance of tabInstances) {
|
||||
openChat(instance)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -38,13 +38,7 @@ export interface ExtensionMessage {
|
||||
| "fileSearchResults"
|
||||
| "grpc_response" // New type for gRPC responses
|
||||
text?: string
|
||||
action?:
|
||||
| "chatButtonClicked"
|
||||
| "settingsButtonClicked"
|
||||
| "didBecomeVisible"
|
||||
| "accountLogoutClicked"
|
||||
| "accountButtonClicked"
|
||||
| "focusChatInput"
|
||||
action?: "settingsButtonClicked" | "didBecomeVisible" | "accountLogoutClicked" | "accountButtonClicked" | "focusChatInput"
|
||||
state?: ExtensionState
|
||||
images?: string[]
|
||||
files?: string[]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { StateServiceClient, UiServiceClient, ModelsServiceClient } from "../services/grpc-client"
|
||||
import { StateServiceClient, ModelsServiceClient, UiServiceClient } from "../services/grpc-client"
|
||||
import { EmptyRequest } from "@shared/proto/common"
|
||||
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
|
||||
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
|
||||
@@ -199,9 +199,6 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
case "accountButtonClicked":
|
||||
navigateToAccount()
|
||||
break
|
||||
case "chatButtonClicked":
|
||||
navigateToChat()
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -270,6 +267,7 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
const stateSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
|
||||
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
|
||||
|
||||
// Subscribe to state updates and UI events using the gRPC streaming API
|
||||
useEffect(() => {
|
||||
@@ -386,6 +384,19 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
},
|
||||
)
|
||||
|
||||
// Subscribe to chat button clicked events with webview type
|
||||
chatButtonUnsubscribeRef.current = UiServiceClient.subscribeToChatButtonClicked(EmptyRequest.create({}), {
|
||||
onResponse: () => {
|
||||
// When chat button is clicked, navigate to chat
|
||||
console.log("[DEBUG] Received chat button clicked event from gRPC stream")
|
||||
navigateToChat()
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error in chat button subscription:", error)
|
||||
},
|
||||
onComplete: () => {},
|
||||
})
|
||||
|
||||
// Still send the webviewDidLaunch message for other initialization
|
||||
vscode.postMessage({ type: "webviewDidLaunch" })
|
||||
|
||||
@@ -403,6 +414,10 @@ export const ExtensionStateContextProvider: React.FC<{
|
||||
historyButtonClickedSubscriptionRef.current()
|
||||
historyButtonClickedSubscriptionRef.current = null
|
||||
}
|
||||
if (chatButtonUnsubscribeRef.current) {
|
||||
chatButtonUnsubscribeRef.current()
|
||||
chatButtonUnsubscribeRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user