Skip to content

Commit 59a68c8

Browse files
celestial-vaultElephant Lumps
andauthored
migrate focusChatInput protobus (RooCodeInc#3986)
* migrate focusChatInput * move subscription in with the others * changed grpc method; fixed a bug where keybinding doesn't show chatview if in another tab --------- Co-authored-by: Elephant Lumps <[email protected]>
1 parent 336eb46 commit 59a68c8

File tree

8 files changed

+149
-16
lines changed

8 files changed

+149
-16
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
Migrate focusChatInput message to protobus

proto/ui.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,4 +255,7 @@ service UiService {
255255

256256
// Subscribe to relinquish control events
257257
rpc subscribeToRelinquishControl(EmptyRequest) returns (stream Empty);
258+
259+
// Subscribe to focus chat input events with client ID
260+
rpc subscribeToFocusChatInput(StringRequest) returns (stream Empty);
258261
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { StringRequest, Empty } from "@shared/proto/common"
2+
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
3+
import type { Controller } from "../index"
4+
5+
// Map client IDs to their subscription handlers
6+
const focusChatInputSubscriptions = new Map<string, StreamingResponseHandler>()
7+
8+
/**
9+
* Subscribe to focus chat input events
10+
* @param controller The controller instance
11+
* @param request The request containing the client ID
12+
* @param responseStream The streaming response handler
13+
* @param requestId The ID of the request
14+
*/
15+
export async function subscribeToFocusChatInput(
16+
controller: Controller,
17+
request: StringRequest,
18+
responseStream: StreamingResponseHandler,
19+
requestId?: string,
20+
): Promise<void> {
21+
const clientId = request.value
22+
if (!clientId) {
23+
throw new Error("Client ID is required for focusChatInput subscription")
24+
}
25+
26+
// Store this subscription with its client ID
27+
focusChatInputSubscriptions.set(clientId, responseStream)
28+
29+
// Register cleanup when the connection is closed
30+
const cleanup = () => {
31+
focusChatInputSubscriptions.delete(clientId)
32+
}
33+
34+
// Register the cleanup function with the request registry if we have a requestId
35+
if (requestId) {
36+
getRequestRegistry().registerRequest(requestId, cleanup, { type: "focus_chat_input_subscription" }, responseStream)
37+
}
38+
}
39+
40+
/**
41+
* Send a focus chat input event to a specific webview by client ID
42+
* @param clientId The ID of the client to send the event to
43+
*/
44+
export async function sendFocusChatInputEvent(clientId: string): Promise<void> {
45+
const responseStream = focusChatInputSubscriptions.get(clientId)
46+
if (!responseStream) {
47+
console.warn(`No subscription found for client ID: ${clientId}`)
48+
return
49+
}
50+
51+
try {
52+
const event = Empty.create({})
53+
await responseStream(
54+
event,
55+
false, // Not the last message
56+
)
57+
} catch (error) {
58+
console.error(`Error sending focus chat input event to client ${clientId}:`, error)
59+
// Remove the subscription if there was an error
60+
focusChatInputSubscriptions.delete(clientId)
61+
}
62+
}

src/core/webview/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { readFile } from "fs/promises"
99
import path from "node:path"
1010
import { WebviewProviderType } from "@/shared/webview/types"
1111
import { sendThemeEvent } from "@core/controller/ui/subscribeToTheme"
12+
import { v4 as uuidv4 } from "uuid"
1213

1314
/*
1415
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -19,19 +20,33 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
1920
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.
2021
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
2122
private static activeInstances: Set<WebviewProvider> = new Set()
23+
private static clientIdMap = new Map<WebviewProvider, string>()
2224
public view?: vscode.WebviewView | vscode.WebviewPanel
2325
private disposables: vscode.Disposable[] = []
2426
controller: Controller
27+
private clientId: string
2528

2629
constructor(
2730
readonly context: vscode.ExtensionContext,
2831
private readonly outputChannel: vscode.OutputChannel,
2932
private readonly providerType: WebviewProviderType = WebviewProviderType.TAB, // Default to tab provider
3033
) {
3134
WebviewProvider.activeInstances.add(this)
35+
this.clientId = uuidv4()
36+
WebviewProvider.clientIdMap.set(this, this.clientId)
3237
this.controller = new Controller(context, outputChannel, (message) => this.view?.webview.postMessage(message))
3338
}
3439

40+
// Add a method to get the client ID
41+
public getClientId(): string {
42+
return this.clientId
43+
}
44+
45+
// Add a static method to get the client ID for a specific instance
46+
public static getClientIdForInstance(instance: WebviewProvider): string | undefined {
47+
return WebviewProvider.clientIdMap.get(instance)
48+
}
49+
3550
async dispose() {
3651
if (this.view && "dispose" in this.view) {
3752
this.view.dispose()
@@ -44,6 +59,8 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
4459
}
4560
await this.controller.dispose()
4661
WebviewProvider.activeInstances.delete(this)
62+
// Remove from client ID map
63+
WebviewProvider.clientIdMap.delete(this)
4764
}
4865

4966
public static getVisibleInstance(): WebviewProvider | undefined {
@@ -245,6 +262,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
245262
<script type="text/javascript" nonce="${nonce}">
246263
// Inject the provider type
247264
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
265+
266+
// Inject the client ID
267+
window.clineClientId = "${this.clientId}";
248268
</script>
249269
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
250270
</body>
@@ -358,6 +378,9 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
358378
<script type="text/javascript" nonce="${nonce}">
359379
// Inject the provider type
360380
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
381+
382+
// Inject the client ID
383+
window.clineClientId = "${this.clientId}";
361384
</script>
362385
${reactRefresh}
363386
<script type="module" src="${scriptUri}"></script>

src/extension.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { sendHistoryButtonClickedEvent } from "./core/controller/ui/subscribeToH
2424
import { sendAccountButtonClickedEvent } from "./core/controller/ui/subscribeToAccountButtonClicked"
2525
import { migratePlanActGlobalToWorkspaceStorage } from "./core/storage/state"
2626

27+
import { sendFocusChatInputEvent } from "./core/controller/ui/subscribeToFocusChatInput"
2728
/*
2829
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
2930
@@ -589,10 +590,9 @@ export async function activate(context: vscode.ExtensionContext) {
589590
// At this point, activeWebviewProvider should be the one we want to send the message to.
590591
// It could still be undefined if opening a new tab failed or timed out.
591592
if (activeWebviewProvider) {
592-
activeWebviewProvider.controller.postMessageToWebview({
593-
type: "action",
594-
action: "focusChatInput",
595-
})
593+
// Use the gRPC streaming method instead of postMessageToWebview
594+
const clientId = activeWebviewProvider.getClientId()
595+
sendFocusChatInputEvent(clientId)
596596
} else {
597597
console.error("FocusChatInput: Could not find or activate a Cline webview to focus.")
598598
vscode.window.showErrorMessage(

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface ExtensionMessage {
2525
| "userCreditsPayments"
2626
| "grpc_response" // New type for gRPC responses
2727
text?: string
28-
action?: "didBecomeVisible" | "accountLogoutClicked" | "focusChatInput"
28+
action?: "didBecomeVisible" | "accountLogoutClicked"
2929
state?: ExtensionState
3030
images?: string[]
3131
files?: string[]

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,14 @@ export const MAX_IMAGES_AND_FILES_PER_MESSAGE = 20
9595
const QUICK_WINS_HISTORY_THRESHOLD = 300
9696

9797
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
98-
const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetrySetting } = useExtensionState()
98+
const {
99+
version,
100+
clineMessages: messages,
101+
taskHistory,
102+
apiConfiguration,
103+
telemetrySetting,
104+
navigateToChat,
105+
} = useExtensionState()
99106
const shouldShowQuickWins = false // !taskHistory || taskHistory.length < QUICK_WINS_HISTORY_THRESHOLD
100107
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
101108
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
699706
textAreaRef.current?.focus()
700707
}
701708
break
702-
case "focusChatInput":
703-
textAreaRef.current?.focus()
704-
if (isHidden) {
705-
window.dispatchEvent(new CustomEvent("chatButtonClicked"))
706-
}
707-
break
708709
}
709710
break
710711
}
@@ -715,6 +716,22 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
715716

716717
useEvent("message", handleMessage)
717718

719+
// Listen for local focusChatInput event
720+
useEffect(() => {
721+
const handleFocusChatInput = () => {
722+
if (isHidden) {
723+
navigateToChat()
724+
}
725+
textAreaRef.current?.focus()
726+
}
727+
728+
window.addEventListener("focusChatInput", handleFocusChatInput)
729+
730+
return () => {
731+
window.removeEventListener("focusChatInput", handleFocusChatInput)
732+
}
733+
}, [isHidden])
734+
718735
// Set up addToInput subscription
719736
useEffect(() => {
720737
const cleanup = UiServiceClient.subscribeToAddToInput(EmptyRequest.create({}), {

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import {
77
FileServiceClient,
88
McpServiceClient,
99
} from "../services/grpc-client"
10-
import { EmptyRequest } from "@shared/proto/common"
10+
import { EmptyRequest, StringRequest } from "@shared/proto/common"
1111
import { UpdateSettingsRequest } from "@shared/proto/state"
1212
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
1313
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
14+
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
1415
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
1516
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
1617
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
@@ -26,9 +27,7 @@ import {
2627
requestyDefaultModelInfo,
2728
} from "../../../src/shared/api"
2829
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
29-
import { convertProtoMcpServersToMcpServers } from "@shared/proto-conversions/mcp/mcp-server-conversion"
3030
import { convertTextMateToHljs } from "../utils/textMateToHljs"
31-
import { vscode } from "../utils/vscode"
3231
import { OpenRouterCompatibleModelInfo } from "@shared/proto/models"
3332

3433
interface ExtensionStateContextType extends ExtensionState {
@@ -231,6 +230,9 @@ export const ExtensionStateContextProvider: React.FC<{
231230

232231
// References to store subscription cancellation functions
233232
const stateSubscriptionRef = useRef<(() => void) | null>(null)
233+
234+
// Reference for focusChatInput subscription
235+
const focusChatInputUnsubscribeRef = useRef<(() => void) | null>(null)
234236
const mcpButtonUnsubscribeRef = useRef<(() => void) | null>(null)
235237
const historyButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
236238
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
@@ -552,6 +554,24 @@ export const ExtensionStateContextProvider: React.FC<{
552554
onComplete: () => {},
553555
})
554556

557+
// Subscribe to focus chat input events
558+
const clientId = (window as any).clineClientId
559+
if (clientId) {
560+
const request = StringRequest.create({ value: clientId })
561+
focusChatInputUnsubscribeRef.current = UiServiceClient.subscribeToFocusChatInput(request, {
562+
onResponse: () => {
563+
// Dispatch a local DOM event within this webview only
564+
window.dispatchEvent(new CustomEvent("focusChatInput"))
565+
},
566+
onError: (error: Error) => {
567+
console.error("Error in focusChatInput subscription:", error)
568+
},
569+
onComplete: () => {},
570+
})
571+
} else {
572+
console.error("Client ID not found in window object")
573+
}
574+
555575
// Clean up subscriptions when component unmounts
556576
return () => {
557577
if (stateSubscriptionRef.current) {
@@ -602,7 +622,10 @@ export const ExtensionStateContextProvider: React.FC<{
602622
relinquishControlUnsubscribeRef.current()
603623
relinquishControlUnsubscribeRef.current = null
604624
}
605-
625+
if (focusChatInputUnsubscribeRef.current) {
626+
focusChatInputUnsubscribeRef.current()
627+
focusChatInputUnsubscribeRef.current = null
628+
}
606629
if (mcpServersSubscriptionRef.current) {
607630
mcpServersSubscriptionRef.current()
608631
mcpServersSubscriptionRef.current = null

0 commit comments

Comments
 (0)