Browse Source

feat: support sending context to active editor panels (#5239)

* feat: add client-specific targeting for addToInput events

- Add client-specific targeting for addToInput events
- Update subscribeToAddToInput to accept client ID parameter
- Replace global event broadcasting with targeted client messaging
- Remove automatic sidebar focus when adding code to chat
- Use last active webview instance for context menu actions
- Maintain backward compatibility with subscription management

* add changeset

* remove debug profiler

* e2e test

* add type

* Add e2e test

* update teardown
Bee 5 months ago
parent
commit
4ab8559fce

+ 5 - 0
.changeset/itchy-ways-tickle.md

@@ -0,0 +1,5 @@
+---
+"claude-dev": patch
+---
+
+Support sending context to active webview when editor panels are opened.

+ 5 - 4
playwright.config.ts

@@ -17,15 +17,16 @@ export default defineConfig({
 		{
 			name: "setup test environment",
 			testMatch: /global\.setup\.ts/,
+			teardown: "cleanup test environment",
+		},
+		{
+			name: "cleanup test environment",
+			testMatch: /global\.teardown\.ts/,
 		},
 		{
 			name: "e2e tests",
 			testMatch: /.*\.test\.ts/,
 			dependencies: ["setup test environment"],
 		},
-		{
-			name: "cleanup test environment",
-			testMatch: /global\.teardown\.ts/,
-		},
 	],
 })

+ 1 - 1
proto/cline/ui.proto

@@ -227,7 +227,7 @@ service UiService {
   rpc onDidShowAnnouncement(EmptyRequest) returns (Boolean);
   
   // Subscribe to addToInput events (when user adds content via context menu)
-  rpc subscribeToAddToInput(EmptyRequest) returns (stream String);
+  rpc subscribeToAddToInput(StringRequest) returns (stream String);
   
   // Subscribe to MCP button clicked events
   rpc subscribeToMcpButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);

+ 8 - 7
src/core/controller/index.ts

@@ -33,7 +33,9 @@ import { getAllExtensionState, getGlobalState, updateGlobalState } from "../stor
 import { Task } from "../task"
 import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
 import { sendStateUpdate } from "./state/subscribeToState"
-import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
+import { sendAddToInputEvent, sendAddToInputEventToClient } from "./ui/subscribeToAddToInput"
+import { WebviewProvider } from "../webview"
+
 /*
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
 
@@ -522,11 +524,7 @@ export class Controller {
 
 	// 'Add to Cline' context menu in editor and code action
 	async addSelectedCodeToChat(code: string, filePath: string, languageId: string, diagnostics?: vscode.Diagnostic[]) {
-		// Ensure the sidebar view is visible
-		await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
-		await setTimeoutPromise(100)
-
-		// Send a response to webview with the selected code
+		// Post message to webview with the selected code
 		const fileMention = await this.getFileMentionFromPath(filePath)
 
 		let input = `${fileMention}\n\`\`\`\n${code}\n\`\`\``
@@ -535,7 +533,10 @@ export class Controller {
 			input += `\nProblems:\n${problemsString}`
 		}
 
-		await sendAddToInputEvent(input)
+		const lastActiveWebview = WebviewProvider.getLastActiveInstance()
+		if (lastActiveWebview) {
+			await sendAddToInputEventToClient(lastActiveWebview.getClientId(), input)
+		}
 
 		console.log("addSelectedCodeToChat", code, filePath, languageId)
 	}

+ 48 - 9
src/core/controller/ui/subscribeToAddToInput.ts

@@ -1,33 +1,42 @@
-import { Controller } from "../index"
-import { EmptyRequest } from "@shared/proto/cline/common"
-import { String as ProtoString } from "@shared/proto/cline/common"
-import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
+import type { String as ProtoString, StringRequest } from "@shared/proto/cline/common"
+import { getRequestRegistry, type StreamingResponseHandler } from "../grpc-handler"
+import type { Controller } from "../index"
 
 // Keep track of active addToInput subscriptions
 const activeAddToInputSubscriptions = new Set<StreamingResponseHandler<ProtoString>>()
 
+// Map client IDs to their subscription handlers for targeted sending
+const addToInputSubscriptions = new Map<string, StreamingResponseHandler<ProtoString>>()
+
 /**
  * Subscribe to addToInput events
  * @param controller The controller instance
- * @param request The empty request
+ * @param request The request containing the client ID
  * @param responseStream The streaming response handler
  * @param requestId The ID of the request (passed by the gRPC handler)
  */
 export async function subscribeToAddToInput(
 	_controller: Controller,
-	_request: EmptyRequest,
+	request: StringRequest,
 	responseStream: StreamingResponseHandler<ProtoString>,
 	requestId?: string,
 ): Promise<void> {
-	console.log("[DEBUG] set up addToInput subscription")
+	const clientId = request.value
+	if (!clientId) {
+		throw new Error("Client ID is required for addToInput subscription")
+	}
 
-	// Add this subscription to the active subscriptions
+	console.log("[DEBUG] set up addToInput subscription for client:", clientId)
+
+	// Add this subscription to both the general set and the client-specific map
 	activeAddToInputSubscriptions.add(responseStream)
+	addToInputSubscriptions.set(clientId, responseStream)
 
 	// Register cleanup when the connection is closed
 	const cleanup = () => {
 		activeAddToInputSubscriptions.delete(responseStream)
-		console.log("[DEBUG] Cleaned up addToInput subscription")
+		addToInputSubscriptions.delete(clientId)
+		console.log("[DEBUG] Cleaned up addToInput subscription for client:", clientId)
 	}
 
 	// Register the cleanup function with the request registry if we have a requestId
@@ -61,3 +70,33 @@ export async function sendAddToInputEvent(text: string): Promise<void> {
 
 	await Promise.all(promises)
 }
+
+/**
+ * Send an addToInput event to a specific webview by client ID
+ * @param clientId The ID of the client to send the event to
+ * @param text The text to add to the input
+ */
+export async function sendAddToInputEventToClient(clientId: string, text: string): Promise<void> {
+	const responseStream = addToInputSubscriptions.get(clientId)
+	if (!responseStream) {
+		console.warn(`No addToInput subscription found for client ID: ${clientId}`)
+		return
+	}
+
+	try {
+		const event: ProtoString = {
+			value: text,
+		}
+		await responseStream(
+			event,
+			false, // Not the last message
+		)
+		console.log("[DEBUG] sending addToInput event to client", clientId, ":", text.length, "chars")
+	} catch (error) {
+		console.error(`Error sending addToInput event to client ${clientId}:`, error)
+		// Remove the subscription if there was an error
+		addToInputSubscriptions.delete(clientId)
+		// Also remove from the general set
+		activeAddToInputSubscriptions.delete(responseStream)
+	}
+}

+ 42 - 16
src/core/webview/index.ts

@@ -21,6 +21,8 @@ export abstract class WebviewProvider {
 	controller: Controller
 	private clientId: string
 
+	private static lastActiveControllerId: string | null = null
+
 	constructor(
 		readonly context: vscode.ExtensionContext,
 
@@ -32,6 +34,7 @@ export abstract class WebviewProvider {
 
 		// Create controller with cache service
 		this.controller = new Controller(context, this.clientId)
+		WebviewProvider.setLastActiveControllerId(this.controller.id)
 	}
 
 	// Add a method to get the client ID
@@ -52,40 +55,62 @@ export abstract class WebviewProvider {
 	}
 
 	public static getVisibleInstance(): WebviewProvider | undefined {
-		return findLast(Array.from(this.activeInstances), (instance) => instance.isVisible() === true)
+		return findLast(Array.from(WebviewProvider.activeInstances), (instance) => instance.isVisible() === true)
 	}
 
 	public static getActiveInstance(): WebviewProvider | undefined {
-		return Array.from(this.activeInstances).find((instance) => {
-			if (
-				instance.getWebview() &&
-				instance.getWebview().viewType === "claude-dev.TabPanelProvider" &&
-				"active" in instance.getWebview()
-			) {
-				return instance.getWebview().active === true
+		return Array.from(WebviewProvider.activeInstances).find((instance) => {
+			const webview = instance.getWebview()
+			if (webview && webview.viewType === "claude-dev.TabPanelProvider" && "active" in webview) {
+				return webview.active === true
 			}
 			return false
 		})
 	}
 
 	public static getAllInstances(): WebviewProvider[] {
-		return Array.from(this.activeInstances)
+		return Array.from(WebviewProvider.activeInstances)
 	}
 
 	public static getSidebarInstance() {
-		return Array.from(this.activeInstances).find(
-			(instance) => instance.getWebview() && "onDidChangeVisibility" in instance.getWebview(),
+		return Array.from(WebviewProvider.activeInstances).find(
+			(instance) => instance.providerType === WebviewProviderType.SIDEBAR,
 		)
 	}
 
 	public static getTabInstances(): WebviewProvider[] {
-		return Array.from(this.activeInstances).filter(
-			(instance) => instance.getWebview() && "onDidChangeViewState" in instance.getWebview(),
-		)
+		return Array.from(WebviewProvider.activeInstances).filter((instance) => instance.providerType === WebviewProviderType.TAB)
+	}
+
+	public static getLastActiveInstance(): WebviewProvider | undefined {
+		const lastActiveId = WebviewProvider.getLastActiveControllerId()
+		if (!lastActiveId) {
+			return undefined
+		}
+		return Array.from(WebviewProvider.activeInstances).find((instance) => instance.controller.id === lastActiveId)
+	}
+
+	/**
+	 * Gets the last active controller ID with performance optimization
+	 * @returns The last active controller ID or null
+	 */
+	public static getLastActiveControllerId(): string | null {
+		return WebviewProvider.lastActiveControllerId || WebviewProvider.getSidebarInstance()?.controller.id || null
+	}
+
+	/**
+	 * Sets the last active controller ID with validation and performance optimization
+	 * @param controllerId The controller ID to set as last active
+	 */
+	public static setLastActiveControllerId(controllerId: string | null): void {
+		// Only update if the value is actually different to avoid unnecessary operations
+		if (WebviewProvider.lastActiveControllerId !== controllerId) {
+			WebviewProvider.lastActiveControllerId = controllerId
+		}
 	}
 
 	public static async disposeAllInstances() {
-		const instances = Array.from(this.activeInstances)
+		const instances = Array.from(WebviewProvider.activeInstances)
 		for (const instance of instances) {
 			await instance.dispose()
 		}
@@ -104,7 +129,7 @@ export abstract class WebviewProvider {
 	 *
 	 * @returns The webview instance (WebviewView, WebviewPanel, or similar)
 	 */
-	abstract getWebview(): any
+	abstract getWebview(): vscode.WebviewPanel | vscode.WebviewView | undefined
 
 	/**
 	 * Converts a local URI to a webview URI that can be used within the webview.
@@ -204,6 +229,7 @@ export abstract class WebviewProvider {
                     window.clineClientId = "${this.clientId}";
                 </script>
 				<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
+				<script src="http://localhost:8097"></script> 
 			</body>
 		</html>
 		`

+ 60 - 69
src/extension.ts

@@ -203,6 +203,7 @@ export async function activate(context: vscode.ExtensionContext) {
 		// Lock the editor group so clicking on files doesn't open them over the panel
 		await setTimeoutPromise(100)
 		await vscode.commands.executeCommand("workbench.action.lockEditorGroup")
+		return tabWebview
 	}
 
 	context.subscriptions.push(vscode.commands.registerCommand("cline.popoutButtonClicked", openClineInNewTab))
@@ -289,12 +290,16 @@ export async function activate(context: vscode.ExtensionContext) {
 	context.subscriptions.push(
 		vscode.commands.registerCommand("cline.addToChat", async (range?: vscode.Range, diagnostics?: vscode.Diagnostic[]) => {
 			await vscode.commands.executeCommand("cline.focusChatInput") // Ensure Cline is visible and input focused
-			await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
+			const activeWebview = WebviewProvider.getLastActiveInstance()
+			const clientId = activeWebview?.getClientId()
+			await pWaitFor(() => !!activeWebview)
 			const editor = vscode.window.activeTextEditor
-			if (!editor) {
+			if (!editor || !clientId) {
 				return
 			}
 
+			await sendFocusChatInputEvent(clientId)
+
 			// Use provided range if available, otherwise use current selection
 			// (vscode command passes an argument in the first param by default, so we need to ensure it's a Range object)
 			const textRange = range instanceof vscode.Range ? range : editor.selection
@@ -308,14 +313,13 @@ export async function activate(context: vscode.ExtensionContext) {
 			const filePath = editor.document.uri.fsPath
 			const languageId = editor.document.languageId
 
-			const visibleWebview = WebviewProvider.getVisibleInstance()
-			await visibleWebview?.controller.addSelectedCodeToChat(
+			await activeWebview?.controller.addSelectedCodeToChat(
 				selectedText,
 				filePath,
 				languageId,
 				Array.isArray(diagnostics) ? diagnostics : undefined,
 			)
-			telemetryService.captureButtonClick("codeAction_addToChat", visibleWebview?.controller.task?.taskId)
+			telemetryService.captureButtonClick("codeAction_addToChat", activeWebview?.controller.task?.taskId)
 		}),
 	)
 
@@ -476,8 +480,8 @@ export async function activate(context: vscode.ExtensionContext) {
 		vscode.commands.registerCommand("cline.fixWithCline", async (range: vscode.Range, diagnostics: vscode.Diagnostic[]) => {
 			// Add this line to focus the chat input first
 			await vscode.commands.executeCommand("cline.focusChatInput")
-			// Wait for a webview instance to become visible after focusing
-			await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
+			// Wait for a webview instance to become available after focusing
+			await pWaitFor(() => !!WebviewProvider.getLastActiveInstance())
 			const editor = vscode.window.activeTextEditor
 			if (!editor) {
 				return
@@ -487,17 +491,17 @@ export async function activate(context: vscode.ExtensionContext) {
 			const filePath = editor.document.uri.fsPath
 			const languageId = editor.document.languageId
 
-			// Send to sidebar provider with diagnostics
-			const visibleWebview = WebviewProvider.getVisibleInstance()
-			await visibleWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
-			telemetryService.captureButtonClick("codeAction_fixWithCline", visibleWebview?.controller.task?.taskId)
+			// Send to last active instance with diagnostics
+			const activeWebview = WebviewProvider.getLastActiveInstance()
+			await activeWebview?.controller.fixWithCline(selectedText, filePath, languageId, diagnostics)
+			telemetryService.captureButtonClick("codeAction_fixWithCline", activeWebview?.controller.task?.taskId)
 		}),
 	)
 
 	context.subscriptions.push(
 		vscode.commands.registerCommand("cline.explainCode", async (range: vscode.Range) => {
 			await vscode.commands.executeCommand("cline.focusChatInput") // Ensure Cline is visible and input focused
-			await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
+			await pWaitFor(() => !!WebviewProvider.getLastActiveInstance())
 			const editor = vscode.window.activeTextEditor
 			if (!editor) {
 				return
@@ -511,18 +515,18 @@ export async function activate(context: vscode.ExtensionContext) {
 				return
 			}
 			const filePath = editor.document.uri.fsPath
-			const visibleWebview = WebviewProvider.getVisibleInstance()
-			const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
+			const activeWebview = WebviewProvider.getLastActiveInstance()
+			const fileMention = activeWebview?.controller.getFileMentionFromPath(filePath) || filePath
 			const prompt = `Explain the following code from ${fileMention}:\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
-			await visibleWebview?.controller.initTask(prompt)
-			telemetryService.captureButtonClick("codeAction_explainCode", visibleWebview?.controller.task?.taskId)
+			await activeWebview?.controller.initTask(prompt)
+			telemetryService.captureButtonClick("codeAction_explainCode", activeWebview?.controller.task?.taskId)
 		}),
 	)
 
 	context.subscriptions.push(
 		vscode.commands.registerCommand("cline.improveCode", async (range: vscode.Range) => {
 			await vscode.commands.executeCommand("cline.focusChatInput") // Ensure Cline is visible and input focused
-			await pWaitFor(() => !!WebviewProvider.getVisibleInstance())
+			await pWaitFor(() => !!WebviewProvider.getLastActiveInstance())
 			const editor = vscode.window.activeTextEditor
 			if (!editor) {
 				return
@@ -536,73 +540,62 @@ export async function activate(context: vscode.ExtensionContext) {
 				return
 			}
 			const filePath = editor.document.uri.fsPath
-			const visibleWebview = WebviewProvider.getVisibleInstance()
-			const fileMention = visibleWebview?.controller.getFileMentionFromPath(filePath) || filePath
+			const activeWebview = WebviewProvider.getLastActiveInstance()
+			const fileMention = activeWebview?.controller.getFileMentionFromPath(filePath) || filePath
 			const prompt = `Improve the following code from ${fileMention} (e.g., suggest refactorings, optimizations, or better practices):\n\`\`\`${editor.document.languageId}\n${selectedText}\n\`\`\``
-			await visibleWebview?.controller.initTask(prompt)
-			telemetryService.captureButtonClick("codeAction_improveCode", visibleWebview?.controller.task?.taskId)
+			await activeWebview?.controller.initTask(prompt)
+			telemetryService.captureButtonClick("codeAction_improveCode", activeWebview?.controller.task?.taskId)
 		}),
 	)
 
 	// Register the focusChatInput command handler
 	context.subscriptions.push(
 		vscode.commands.registerCommand("cline.focusChatInput", async () => {
-			let activeWebviewProvider: WebviewProvider | undefined = WebviewProvider.getVisibleInstance()
-
-			// If a tab is visible and active, ensure it's fully revealed (might be redundant but safe)
-			if (activeWebviewProvider?.getWebview() && Object.hasOwn(activeWebviewProvider.getWebview(), "reveal")) {
-				const panelView = activeWebviewProvider.getWebview() as vscode.WebviewPanel
-				panelView.reveal(panelView.viewColumn)
-			} else if (!activeWebviewProvider) {
-				// No webview is currently visible, try to activate the sidebar
-				await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
-				await new Promise((resolve) => setTimeout(resolve, 200)) // Allow time for focus
-				activeWebviewProvider = WebviewProvider.getSidebarInstance()
-
-				if (!activeWebviewProvider) {
-					// Sidebar didn't become active (might be closed or not in current view container)
-					// Check for existing tab panels
-					const tabInstances = WebviewProvider.getTabInstances()
-					if (tabInstances.length > 0) {
-						const potentialTabInstance = tabInstances[tabInstances.length - 1] // Get the most recent one
-						if (potentialTabInstance.getWebview() && Object.hasOwn(potentialTabInstance.getWebview(), "reveal")) {
-							const panelView = potentialTabInstance.getWebview() as vscode.WebviewPanel
-							panelView.reveal(panelView.viewColumn)
-							activeWebviewProvider = potentialTabInstance
-						}
+			// Fast path: check for existing active instance
+			let activeWebview = WebviewProvider.getLastActiveInstance()
+
+			if (activeWebview) {
+				// Instance exists - just reveal and focus it
+				const webview = activeWebview.getWebview()
+				if (webview) {
+					if (webview && "reveal" in webview) {
+						webview.reveal()
+					} else if ("show" in webview) {
+						webview.show()
 					}
 				}
+			} else {
+				// No active instance - need to find or create one
+				WebviewProvider.setLastActiveControllerId(null)
 
-				if (!activeWebviewProvider) {
-					// No existing Cline view found at all, open a new tab
-					await vscode.commands.executeCommand("cline.openInNewTab")
-					// After openInNewTab, a new webview is created. We need to get this new instance.
-					// It might take a moment for it to register.
-					await pWaitFor(
-						() => {
-							const visibleInstance = WebviewProvider.getVisibleInstance()
-							// Ensure a boolean is returned
-							return !!(visibleInstance?.getWebview() && Object.hasOwn(visibleInstance.getWebview(), "reveal"))
-						},
-						{ timeout: 2000 },
-					)
-					activeWebviewProvider = WebviewProvider.getVisibleInstance()
+				// Check for existing tab instances first (cheaper than focusing sidebar)
+				const tabInstances = WebviewProvider.getTabInstances()
+				if (tabInstances.length > 0) {
+					activeWebview = tabInstances[tabInstances.length - 1]
+				} else {
+					// Try to focus sidebar
+					await vscode.commands.executeCommand("claude-dev.SidebarProvider.focus")
+
+					// Small delay for focus to complete
+					await new Promise((resolve) => setTimeout(resolve, 200))
+					// Last resort: create new tab
+					activeWebview = WebviewProvider.getSidebarInstance() || (await openClineInNewTab())
 				}
 			}
-			// At this point, activeWebviewProvider should be the one we want to send the message to.
-			// It could still be undefined if opening a new tab failed or timed out.
-			if (activeWebviewProvider) {
-				// Use the gRPC streaming method instead of sending a ProtoBus response to the webview
-				const clientId = activeWebviewProvider.getClientId()
-				sendFocusChatInputEvent(clientId)
-			} else {
+
+			// Send focus event
+			const clientId = activeWebview?.getClientId()
+			if (!clientId) {
 				console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
 				HostProvider.window.showMessage({
 					type: ShowMessageType.ERROR,
 					message: "Could not activate Cline view. Please try opening it manually from the Activity Bar.",
 				})
+				return
 			}
-			telemetryService.captureButtonClick("command_focusChatInput", activeWebviewProvider?.controller.task?.taskId)
+
+			sendFocusChatInputEvent(clientId)
+			telemetryService.captureButtonClick("command_focusChatInput", activeWebview.controller?.task?.taskId)
 		}),
 	)
 
@@ -656,9 +649,7 @@ function maybeSetupHostProviders(context: ExtensionContext) {
 		const outputChannel = vscode.window.createOutputChannel("Cline")
 		context.subscriptions.push(outputChannel)
 
-		const getCallbackUri = async function () {
-			return `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev`
-		}
+		const getCallbackUri = async () => `${vscode.env.uriScheme || "vscode"}://saoudrizwan.claude-dev`
 		HostProvider.initialize(createWebview, createDiffView, vscodeHostBridgeClient, outputChannel.appendLine, getCallbackUri)
 	}
 }

+ 1 - 1
src/hosts/external/ExternalWebviewProvider.ts

@@ -25,7 +25,7 @@ export class ExternalWebviewProvider extends WebviewProvider {
 		return true
 	}
 	override getWebview() {
-		return {}
+		return undefined
 	}
 
 	override resolveWebviewView(_: any): Promise<void> {

+ 6 - 1
src/hosts/vscode/VscodeWebviewProvider.ts

@@ -4,9 +4,9 @@ import { WebviewProvider } from "@core/webview"
 import { getTheme } from "@integrations/theme/getTheme"
 import type { Uri } from "vscode"
 import * as vscode from "vscode"
+import { HostProvider } from "@/hosts/host-provider"
 import type { ExtensionMessage } from "@/shared/ExtensionMessage"
 import type { WebviewProviderType } from "@/shared/webview/types"
-import { HostProvider } from "@/hosts/host-provider"
 import { WebviewMessage } from "@/shared/WebviewMessage"
 import { handleGrpcRequest, handleGrpcRequestCancel } from "@/core/controller/grpc-handler"
 
@@ -71,6 +71,7 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
 			webviewView.onDidChangeViewState(
 				async (e) => {
 					if (e?.webviewPanel?.visible && e.webviewPanel?.active) {
+						WebviewProvider.setLastActiveControllerId(this.controller.id)
 						//  Only send the event if the webview is active (focused)
 						await sendDidBecomeVisibleEvent(this.controller.id)
 					}
@@ -83,6 +84,7 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
 			webviewView.onDidChangeVisibility(
 				async () => {
 					if (this.webview?.visible) {
+						WebviewProvider.setLastActiveControllerId(this.controller.id)
 						await sendDidBecomeVisibleEvent(this.controller.id)
 					}
 				},
@@ -95,6 +97,9 @@ export class VscodeWebviewProvider extends WebviewProvider implements vscode.Web
 		// This happens when the user closes the view or when the view is closed programmatically
 		webviewView.onDidDispose(
 			async () => {
+				if (WebviewProvider.getLastActiveControllerId() === this.controller.id) {
+					WebviewProvider.setLastActiveControllerId(null)
+				}
 				await this.dispose()
 			},
 			null,

+ 42 - 0
src/test/e2e/editor.test.ts

@@ -0,0 +1,42 @@
+import { expect } from "@playwright/test"
+import { addSelectedCodeToClineWebview, getClineEditorWebviewFrame, openTab, toggleNotifications } from "./utils/common"
+import { e2e } from "./utils/helpers"
+
+e2e("code actions and editor panel", async ({ page, sidebar }) => {
+	await toggleNotifications(page)
+
+	await sidebar.getByRole("button", { name: "Get Started for Free" }).click({ delay: 100 })
+
+	// Verify the help improve banner is visible and can be closed.
+	await sidebar.getByRole("button", { name: "Close banner and enable" }).click()
+
+	// Verify the release banner is visible for new installs and can be closed.
+	await sidebar.getByTestId("close-button").locator("span").first().click()
+
+	// Sidebar - input should start empty
+	const sidebarInput = sidebar.getByTestId("chat-input")
+	await expect(sidebarInput).toBeEmpty()
+
+	// Open file tree and select code from file
+	await openTab(page, "Explorer ")
+	await page.getByRole("treeitem", { name: "index.html" }).locator("a").click()
+	await expect(sidebarInput).not.toBeFocused()
+
+	// Sidebar should be opened and visible after adding code to Cline
+	await addSelectedCodeToClineWebview(page)
+	await expect(sidebarInput).not.toBeEmpty()
+	await expect(sidebarInput).toBeFocused()
+
+	await page.getByRole("button", { name: "Open in Editor" }).click()
+	await page.waitForLoadState("load")
+	const clineEditorTab = page.getByRole("tab", { name: "Cline, Editor Group" })
+	await expect(clineEditorTab).toBeVisible()
+
+	// Editor Panel
+	const clineEditorWebview = await getClineEditorWebviewFrame(page)
+
+	await clineEditorWebview.getByTestId("chat-input").click()
+	await expect(clineEditorWebview.getByTestId("chat-input")).toBeEmpty()
+	await addSelectedCodeToClineWebview(page)
+	await expect(clineEditorWebview.getByTestId("chat-input")).not.toBeEmpty()
+})

+ 36 - 0
src/test/e2e/utils/common.ts

@@ -0,0 +1,36 @@
+import type { Page } from "@playwright/test"
+
+export const openTab = async (_page: Page, tabName: string) => {
+	await _page
+		.getByRole("tab", { name: new RegExp(`${tabName}`) })
+		.locator("a")
+		.click()
+}
+
+export const addSelectedCodeToClineWebview = async (_page: Page) => {
+	await _page.locator("div:nth-child(4) > span > span").first().click()
+	await _page.getByRole("textbox", { name: "The editor is not accessible" }).press("ControlOrMeta+a")
+
+	await _page.getByRole("listbox", { name: /Show Code Actions / }).click()
+	await _page.keyboard.press("Enter", { delay: 100 }) // First action - "Add to Cline"
+}
+
+export const getClineEditorWebviewFrame = async (_page: Page) => {
+	return _page.frameLocator("iframe.webview").last().frameLocator("iframe")
+}
+
+export const toggleNotifications = async (_page: Page) => {
+	await _page.keyboard.press("ControlOrMeta+Shift+p")
+	const editorSearchBar = _page.getByRole("textbox", { name: "Type the name of a command to" })
+	await editorSearchBar.click({ delay: 100 }) // Ensure focus
+	await editorSearchBar.fill("Toggle Do Not Disturb Mode")
+	await _page.keyboard.press("Enter")
+}
+
+export const closeBanners = async (sidebar: Page) => {
+	const banners = ["Get Started for Free", "Close banner and enable"]
+
+	for (const banner of banners) {
+		await sidebar.getByRole("button", { name: banner }).click({ delay: 100 })
+	}
+}

+ 1 - 1
src/test/e2e/utils/global.teardown.ts

@@ -5,7 +5,7 @@ import { ClineApiServerMock } from "../fixtures/server"
 import { getResultsDir, rmForRetries } from "./helpers"
 
 teardown("cleanup test environment", async () => {
-	await ClineApiServerMock.stopGlobalServer()
+	ClineApiServerMock.stopGlobalServer()
 		.then(() => console.log("ClineApiServerMock stopped successfully."))
 		.catch((error) => console.error("Error stopping ClineApiServerMock:", error))
 

+ 0 - 4
src/test/e2e/utils/helpers.ts

@@ -20,10 +20,6 @@ export class E2ETestHelper {
 	// Instance properties for caching
 	private cachedFrame: Frame | null = null
 
-	constructor() {
-		// Initialize any instance-specific state if needed
-	}
-
 	// Path utilities
 	public static escapeToPath(text: string): string {
 		return text.trim().toLowerCase().replaceAll(/\W/g, "_")

+ 8 - 1
webview-ui/src/components/chat/ChatView.tsx

@@ -263,7 +263,14 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
 
 	// Set up addToInput subscription
 	useEffect(() => {
-		const cleanup = UiServiceClient.subscribeToAddToInput(EmptyRequest.create({}), {
+		const clientId = (window as { clineClientId?: string }).clineClientId
+		if (!clientId) {
+			console.error("Client ID not found in window object for addToInput subscription")
+			return
+		}
+
+		const request = StringRequest.create({ value: clientId })
+		const cleanup = UiServiceClient.subscribeToAddToInput(request, {
 			onResponse: (event) => {
 				if (event.value) {
 					setInputValue((prevValue) => {