Compare commits

...

4 Commits

Author SHA1 Message Date
celestial-vault c80c4c60b2 changed grpc method; fixed a bug where keybinding doesn't show chatview if in another tab 2025-06-10 18:05:32 -07:00
celestial-vault 835d4e1972 move subscription in with the others 2025-06-10 17:35:54 -07:00
celestial-vault 5f34b524d7 merge conflicts 2025-06-10 17:33:06 -07:00
Elephant Lumps afd0059602 migrate focusChatInput 2025-06-02 10:30:26 -07:00
8 changed files with 149 additions and 16 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Migrate focusChatInput message to protobus
+3
View File
@@ -255,4 +255,7 @@ service UiService {
// Subscribe to relinquish control events
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
// Subscribe to focus chat input events with client ID
rpc subscribeToFocusChatInput(StringRequest) returns (stream Empty);
}
@@ -0,0 +1,62 @@
import { StringRequest, Empty } from "@shared/proto/common"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
import type { Controller } from "../index"
// Map client IDs to their subscription handlers
const focusChatInputSubscriptions = new Map<string, StreamingResponseHandler>()
/**
* Subscribe to focus chat input events
* @param controller The controller instance
* @param request The request containing the client ID
* @param responseStream The streaming response handler
* @param requestId The ID of the request
*/
export async function subscribeToFocusChatInput(
controller: Controller,
request: StringRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
const clientId = request.value
if (!clientId) {
throw new Error("Client ID is required for focusChatInput subscription")
}
// Store this subscription with its client ID
focusChatInputSubscriptions.set(clientId, responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
focusChatInputSubscriptions.delete(clientId)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "focus_chat_input_subscription" }, responseStream)
}
}
/**
* Send a focus chat input event to a specific webview by client ID
* @param clientId The ID of the client to send the event to
*/
export async function sendFocusChatInputEvent(clientId: string): Promise<void> {
const responseStream = focusChatInputSubscriptions.get(clientId)
if (!responseStream) {
console.warn(`No subscription found for client ID: ${clientId}`)
return
}
try {
const event = Empty.create({})
await responseStream(
event,
false, // Not the last message
)
} catch (error) {
console.error(`Error sending focus chat input event to client ${clientId}:`, error)
// Remove the subscription if there was an error
focusChatInputSubscriptions.delete(clientId)
}
}
+23
View File
@@ -9,6 +9,7 @@ import { readFile } from "fs/promises"
import path from "node:path"
import { WebviewProviderType } from "@/shared/webview/types"
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
import { v4 as uuidv4 } from "uuid"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -19,9 +20,11 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private static activeInstances: Set<WebviewProvider> = new Set()
private static clientIdMap = new Map<WebviewProvider, string>()
public view?: vscode.WebviewView | vscode.WebviewPanel
private disposables: vscode.Disposable[] = []
controller: Controller
private clientId: string
constructor(
readonly context: vscode.ExtensionContext,
@@ -29,9 +32,21 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
private readonly providerType: WebviewProviderType = WebviewProviderType.TAB, // Default to tab provider
) {
WebviewProvider.activeInstances.add(this)
this.clientId = uuidv4()
WebviewProvider.clientIdMap.set(this, this.clientId)
this.controller = new Controller(context, outputChannel, (message) => this.view?.webview.postMessage(message))
}
// Add a method to get the client ID
public getClientId(): string {
return this.clientId
}
// Add a static method to get the client ID for a specific instance
public static getClientIdForInstance(instance: WebviewProvider): string | undefined {
return WebviewProvider.clientIdMap.get(instance)
}
async dispose() {
if (this.view && "dispose" in this.view) {
this.view.dispose()
@@ -44,6 +59,8 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
}
await this.controller.dispose()
WebviewProvider.activeInstances.delete(this)
// Remove from client ID map
WebviewProvider.clientIdMap.delete(this)
}
public static getVisibleInstance(): WebviewProvider | undefined {
@@ -245,6 +262,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
</body>
@@ -358,6 +378,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
+4 -4
View File
@@ -24,6 +24,7 @@ import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToH
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
import { migratePlanActGlobalToWorkspaceStorage } from "./core/storage/state"
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -589,10 +590,9 @@ export async function activate(context: vscode.ExtensionContext) {
// 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) {
activeWebviewProvider.controller.postMessageToWebview({
type: "action",
action: "focusChatInput",
})
// Use the gRPC streaming method instead of postMessageToWebview
const clientId = activeWebviewProvider.getClientId()
sendFocusChatInputEvent(clientId)
} else {
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
vscode.window.showErrorMessage(
+1 -1
View File
@@ -25,7 +25,7 @@ export interface ExtensionMessage {
| "userCreditsPayments"
| "grpc_response" // New type for gRPC responses
text?: string
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
action?: "didBecomeVisible" | "accountLogoutClicked"
state?: ExtensionState
images?: string[]
files?: string[]
+24 -7
View File
@@ -95,7 +95,14 @@ export const MAX_IMAGES_AND_FILES_PER_MESSAGE = 20
const QUICK_WINS_HISTORY_THRESHOLD = 300
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetrySetting } = useExtensionState()
const {
version,
clineMessages: messages,
taskHistory,
apiConfiguration,
telemetrySetting,
navigateToChat,
} = useExtensionState()
const shouldShowQuickWins = false // !taskHistory || taskHistory.length < QUICK_WINS_HISTORY_THRESHOLD
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort)
@@ -699,12 +706,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
textAreaRef.current?.focus()
}
break
case "focusChatInput":
textAreaRef.current?.focus()
if (isHidden) {
window.dispatchEvent(new CustomEvent("chatButtonClicked"))
}
break
}
break
}
@@ -715,6 +716,22 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
useEvent("message", handleMessage)
// Listen for local focusChatInput event
useEffect(() => {
const handleFocusChatInput = () => {
if (isHidden) {
navigateToChat()
}
textAreaRef.current?.focus()
}
window.addEventListener("focusChatInput", handleFocusChatInput)
return () => {
window.removeEventListener("focusChatInput", handleFocusChatInput)
}
}, [isHidden])
// Set up addToInput subscription
useEffect(() => {
const cleanup = UiServiceClient.subscribeToAddToInput(EmptyRequest.create({}), {
@@ -7,10 +7,11 @@ import {
FileServiceClient,
McpServiceClient,
} from "../services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { EmptyRequest, StringRequest } from "@shared/proto/common"
import { UpdateSettingsRequest } from "@shared/proto/state"
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
@@ -26,9 +27,7 @@ import {
requestyDefaultModelInfo,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
import { vscode } from "../utils/vscode"
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
interface ExtensionStateContextType extends ExtensionState {
@@ -231,6 +230,9 @@ export const ExtensionStateContextProvider: React.FC<{
// References to store subscription cancellation functions
const stateSubscriptionRef = useRef<(() => void) | null>(null)
// Reference for focusChatInput subscription
const focusChatInputUnsubscribeRef = useRef<(() => void) | null>(null)
const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
@@ -552,6 +554,24 @@ export const ExtensionStateContextProvider: React.FC<{
onComplete: () => {},
})
// Subscribe to focus chat input events
const clientId = (window as any).clineClientId
if (clientId) {
const request = StringRequest.create({ value: clientId })
focusChatInputUnsubscribeRef.current = UiServiceClient.subscribeToFocusChatInput(request, {
onResponse: () => {
// Dispatch a local DOM event within this webview only
window.dispatchEvent(new CustomEvent("focusChatInput"))
},
onError: (error: Error) => {
console.error("Error in focusChatInput subscription:", error)
},
onComplete: () => {},
})
} else {
console.error("Client ID not found in window object")
}
// Clean up subscriptions when component unmounts
return () => {
if (stateSubscriptionRef.current) {
@@ -602,7 +622,10 @@ export const ExtensionStateContextProvider: React.FC<{
relinquishControlUnsubscribeRef.current()
relinquishControlUnsubscribeRef.current = null
}
if (focusChatInputUnsubscribeRef.current) {
focusChatInputUnsubscribeRef.current()
focusChatInputUnsubscribeRef.current = null
}
if (mcpServersSubscriptionRef.current) {
mcpServersSubscriptionRef.current()
mcpServersSubscriptionRef.current = null