Compare commits

...

2 Commits

Author SHA1 Message Date
Elephant Lumps 3254a5099f merge conflicts 2025-06-04 21:43:48 -07:00
Elephant Lumps 4fa16e9c7a migrate partialMessage 2025-06-04 21:40:27 -07:00
13 changed files with 722 additions and 59 deletions
+33
View File
@@ -0,0 +1,33 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { StreamingResponseHandler } from "./host-grpc-handler"
import { handleUriServiceRequest, handleUriServiceStreamingRequest } from "./uri/index"
import { handleWatchServiceRequest, handleWatchServiceStreamingRequest } from "./watch/index"
/**
* Configuration for a host service handler
*/
export interface HostServiceHandlerConfig {
requestHandler: (method: string, message: any) => Promise<any>
streamingHandler: (
method: string,
message: any,
responseStream: StreamingResponseHandler,
requestId?: string,
) => Promise<void>
}
/**
* Map of host service names to their handler configurations
*/
export const hostServiceHandlers: Record<string, HostServiceHandlerConfig> = {
"host.UriService": {
requestHandler: handleUriServiceRequest,
streamingHandler: handleUriServiceStreamingRequest,
},
"host.WatchService": {
requestHandler: handleWatchServiceRequest,
streamingHandler: handleWatchServiceStreamingRequest,
},
}
+22
View File
@@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create uri service registry
const uriService = createServiceRegistry("uri")
// Export the method handler types and registration function
export type UriMethodHandler = ServiceMethodHandler
export type UriStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = uriService.registerMethod
// Export the request handlers
export const handleUriServiceRequest = uriService.handleRequest
export const handleUriServiceStreamingRequest = uriService.handleStreamingRequest
export const isStreamingMethod = uriService.isStreamingMethod
// Register all uri methods
registerAllMethods()
+16
View File
@@ -0,0 +1,16 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { file } from "./file"
import { joinPath } from "./joinPath"
import { parse } from "./parse"
// Register all uri service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("file", file)
registerMethod("joinPath", joinPath)
registerMethod("parse", parse)
}
+22
View File
@@ -0,0 +1,22 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createServiceRegistry, ServiceMethodHandler, StreamingMethodHandler } from "../host-grpc-service"
import { StreamingResponseHandler } from "../host-grpc-handler"
import { registerAllMethods } from "./methods"
// Create watch service registry
const watchService = createServiceRegistry("watch")
// Export the method handler types and registration function
export type WatchMethodHandler = ServiceMethodHandler
export type WatchStreamingMethodHandler = StreamingMethodHandler
export const registerMethod = watchService.registerMethod
// Export the request handlers
export const handleWatchServiceRequest = watchService.handleRequest
export const handleWatchServiceStreamingRequest = watchService.handleStreamingRequest
export const isStreamingMethod = watchService.isStreamingMethod
// Register all watch methods
registerAllMethods()
+15
View File
@@ -0,0 +1,15 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
// Import all method implementations
import { registerMethod } from "./index"
import { subscribeToFile } from "./subscribeToFile"
// Streaming methods for this service
export const streamingMethods = ["subscribeToFile"]
// Register all watch service methods
export function registerAllMethods(): void {
// Register each method with the registry
registerMethod("subscribeToFile", subscribeToFile, { isStreaming: true })
}
+203
View File
@@ -18,6 +18,206 @@ message WebviewProviderTypeRequest {
WebviewProviderType providerType = 2;
}
// Enum for ClineMessage type
enum ClineMessageType {
ASK = 0;
SAY = 1;
}
// Enum for ClineAsk types
enum ClineAsk {
FOLLOWUP = 0;
PLAN_MODE_RESPOND = 1;
COMMAND = 2;
COMMAND_OUTPUT = 3;
COMPLETION_RESULT = 4;
TOOL = 5;
API_REQ_FAILED = 6;
RESUME_TASK = 7;
RESUME_COMPLETED_TASK = 8;
MISTAKE_LIMIT_REACHED = 9;
AUTO_APPROVAL_MAX_REQ_REACHED = 10;
BROWSER_ACTION_LAUNCH = 11;
USE_MCP_SERVER = 12;
NEW_TASK = 13;
CONDENSE = 14;
REPORT_BUG = 15;
}
// Enum for ClineSay types
enum ClineSay {
TASK = 0;
ERROR = 1;
API_REQ_STARTED = 2;
API_REQ_FINISHED = 3;
TEXT = 4;
REASONING = 5;
COMPLETION_RESULT_SAY = 6;
USER_FEEDBACK = 7;
USER_FEEDBACK_DIFF = 8;
API_REQ_RETRIED = 9;
COMMAND_SAY = 10;
COMMAND_OUTPUT_SAY = 11;
TOOL_SAY = 12;
SHELL_INTEGRATION_WARNING = 13;
BROWSER_ACTION_LAUNCH_SAY = 14;
BROWSER_ACTION = 15;
BROWSER_ACTION_RESULT = 16;
MCP_SERVER_REQUEST_STARTED = 17;
MCP_SERVER_RESPONSE = 18;
USE_MCP_SERVER_SAY = 19;
DIFF_ERROR = 20;
DELETED_API_REQS = 21;
CLINEIGNORE_ERROR = 22;
CHECKPOINT_CREATED = 23;
LOAD_MCP_DOCUMENTATION = 24;
INFO = 25;
}
// Enum for ClineSayTool tool types
enum ClineSayToolType {
EDITED_EXISTING_FILE = 0;
NEW_FILE_CREATED = 1;
READ_FILE = 2;
LIST_FILES_TOP_LEVEL = 3;
LIST_FILES_RECURSIVE = 4;
LIST_CODE_DEFINITION_NAMES = 5;
SEARCH_FILES = 6;
WEB_FETCH = 7;
}
// Enum for browser actions
enum BrowserAction {
LAUNCH = 0;
CLICK = 1;
TYPE = 2;
SCROLL_DOWN = 3;
SCROLL_UP = 4;
CLOSE = 5;
}
// Enum for MCP server request types
enum McpServerRequestType {
USE_MCP_TOOL = 0;
ACCESS_MCP_RESOURCE = 1;
}
// Enum for API request cancel reasons
enum ClineApiReqCancelReason {
STREAMING_FAILED = 0;
USER_CANCELLED = 1;
RETRIES_EXHAUSTED = 2;
}
// Message for conversation history deleted range
message ConversationHistoryDeletedRange {
int32 start_index = 1;
int32 end_index = 2;
}
// Message for ClineSayTool
message ClineSayTool {
ClineSayToolType tool = 1;
string path = 2;
string diff = 3;
string content = 4;
string regex = 5;
string file_pattern = 6;
bool operation_is_located_in_workspace = 7;
}
// Message for ClineSayBrowserAction
message ClineSayBrowserAction {
BrowserAction action = 1;
string coordinate = 2;
string text = 3;
}
// Message for BrowserActionResult
message BrowserActionResult {
string screenshot = 1;
string logs = 2;
string current_url = 3;
string current_mouse_position = 4;
}
// Message for ClineAskUseMcpServer
message ClineAskUseMcpServer {
string server_name = 1;
McpServerRequestType type = 2;
string tool_name = 3;
string arguments = 4;
string uri = 5;
}
// Message for ClinePlanModeResponse
message ClinePlanModeResponse {
string response = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskQuestion
message ClineAskQuestion {
string question = 1;
repeated string options = 2;
string selected = 3;
}
// Message for ClineAskNewTask
message ClineAskNewTask {
string context = 1;
}
// Message for API request retry status
message ApiReqRetryStatus {
int32 attempt = 1;
int32 max_attempts = 2;
int32 delay_sec = 3;
string error_snippet = 4;
}
// Message for ClineApiReqInfo
message ClineApiReqInfo {
string request = 1;
int32 tokens_in = 2;
int32 tokens_out = 3;
int32 cache_writes = 4;
int32 cache_reads = 5;
double cost = 6;
ClineApiReqCancelReason cancel_reason = 7;
string streaming_failed_message = 8;
ApiReqRetryStatus retry_status = 9;
}
// Main ClineMessage type
message ClineMessage {
int64 ts = 1;
ClineMessageType type = 2;
ClineAsk ask = 3;
ClineSay say = 4;
string text = 5;
string reasoning = 6;
repeated string images = 7;
repeated string files = 8;
bool partial = 9;
string last_checkpoint_hash = 10;
bool is_checkpoint_checked_out = 11;
bool is_operation_outside_workspace = 12;
int32 conversation_history_index = 13;
ConversationHistoryDeletedRange conversation_history_deleted_range = 14;
// Additional fields for specific ask/say types
ClineSayTool say_tool = 15;
ClineSayBrowserAction say_browser_action = 16;
BrowserActionResult browser_action_result = 17;
ClineAskUseMcpServer ask_use_mcp_server = 18;
ClinePlanModeResponse plan_mode_response = 19;
ClineAskQuestion ask_question = 20;
ClineAskNewTask ask_new_task = 21;
ClineApiReqInfo api_req_info = 22;
}
// UiService provides methods for managing UI interactions
service UiService {
// Scrolls to a specific settings section in the settings view
@@ -43,4 +243,7 @@ service UiService {
// Subscribe to settings button clicked events
rpc subscribeToSettingsButtonClicked(WebviewProviderTypeRequest) returns (stream Empty);
// Subscribe to partial message updates (streaming Cline messages as they're built)
rpc subscribeToPartialMessage(EmptyRequest) returns (stream ClineMessage);
}
@@ -0,0 +1,56 @@
import { Controller } from "../index"
import { EmptyRequest } from "@shared/proto/common"
import { ClineMessage } from "@shared/proto/ui"
import { StreamingResponseHandler, getRequestRegistry } from "../grpc-handler"
// Keep track of active partial message subscriptions
const activePartialMessageSubscriptions = new Set<StreamingResponseHandler>()
/**
* Subscribe to partial message 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 subscribeToPartialMessage(
controller: Controller,
request: EmptyRequest,
responseStream: StreamingResponseHandler,
requestId?: string,
): Promise<void> {
// Add this subscription to the active subscriptions
activePartialMessageSubscriptions.add(responseStream)
// Register cleanup when the connection is closed
const cleanup = () => {
activePartialMessageSubscriptions.delete(responseStream)
}
// Register the cleanup function with the request registry if we have a requestId
if (requestId) {
getRequestRegistry().registerRequest(requestId, cleanup, { type: "partial_message_subscription" }, responseStream)
}
}
/**
* Send a partial message event to all active subscribers
* @param partialMessage The ClineMessage to send
*/
export async function sendPartialMessageEvent(partialMessage: ClineMessage): Promise<void> {
// Send the event to all active subscribers
const promises = Array.from(activePartialMessageSubscriptions).map(async (responseStream) => {
try {
await responseStream(
partialMessage,
false, // Not the last message
)
} catch (error) {
console.error("Error sending partial message event:", error)
// Remove the subscription if there was an error
activePartialMessageSubscriptions.delete(responseStream)
}
})
await Promise.all(promises)
}
+10 -10
View File
@@ -72,6 +72,8 @@ import { ClineIgnoreController } from "@core/ignore/ClineIgnoreController"
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 { 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"
import { ModelContextTracker } from "@core/context/context-tracking/ModelContextTracker"
@@ -743,10 +745,8 @@ export class Task {
// todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener
// await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
throw new Error("Current ask promise was ignored 1")
} else {
// this is a new partial message, so add it with partial state
@@ -787,10 +787,8 @@ export class Task {
lastMessage.partial = false
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({
type: "partialMessage",
partialMessage: lastMessage,
})
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial=false message, so add it like normal
this.askResponse = undefined
@@ -866,7 +864,8 @@ export class Task {
lastMessage.images = images
lastMessage.files = files
lastMessage.partial = partial
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage)
} else {
// this is a new partial message, so add it with partial state
const sayTs = Date.now()
@@ -896,7 +895,8 @@ export class Task {
// instead of streaming partialMessage events, we do a save and post like normal to persist to disk
await this.saveClineMessagesAndUpdateHistory()
// await this.postStateToWebview()
await this.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage }) // more performant than an entire postStateToWebview
const protoMessage = convertClineMessageToProto(lastMessage)
await sendPartialMessageEvent(protoMessage) // more performant than an entire postStateToWebview
} else {
// this is a new partial=false message, so add it like normal
const sayTs = Date.now()
+38 -32
View File
@@ -37,25 +37,28 @@ function createToolCallTracker(webviewProvider: WebviewProvider): {
// Intercept messages to track tool usage
const originalPostMessageToWebview = webviewProvider.controller.postMessageToWebview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// Track tool calls
if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
const toolName = (message.partialMessage.text as any)?.tool
if (toolName) {
tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
}
}
// NOTE: Tool tracking via partialMessage has been migrated to gRPC streaming
// This interceptor is kept for potential future use with other message types
// Track tool failures
if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
const errorText = message.partialMessage.text
if (errorText && errorText.includes("Error executing tool")) {
const match = errorText.match(/Error executing tool: (\w+)/)
if (match && match[1]) {
const toolName = match[1]
tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
}
}
}
// Track tool calls - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "tool") {
// const toolName = (message.partialMessage.text as any)?.tool
// if (toolName) {
// tracker.toolCalls[toolName] = (tracker.toolCalls[toolName] || 0) + 1
// }
// }
// Track tool failures - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "error") {
// const errorText = message.partialMessage.text
// if (errorText && errorText.includes("Error executing tool")) {
// const match = errorText.match(/Error executing tool: (\w+)/)
// if (match && match[1]) {
// const toolName = match[1]
// tracker.toolFailures[toolName] = (tracker.toolFailures[toolName] || 0) + 1
// }
// }
// }
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
@@ -504,22 +507,25 @@ export function createMessageCatcher(webviewProvider: WebviewProvider): vscode.D
// Intercept outgoing messages from extension to webview
webviewProvider.controller.postMessageToWebview = async (message: ExtensionMessage) => {
// Check for completion_result message
if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// Complete the current task
completeTask()
}
// NOTE: Completion and ask message detection has been migrated to gRPC streaming
// This interceptor is kept for potential future use with other message types
// Check for ask messages that require user intervention
if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
const askType = message.partialMessage.ask as ClineAsk
const askText = message.partialMessage.text
// Check for completion_result message - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.say === "completion_result") {
// // Complete the current task
// completeTask()
// }
// Automatically respond to different types of asks
setTimeout(async () => {
await autoRespondToAsk(webviewProvider, askType, askText)
}, 100) // Small delay to ensure the message is processed first
}
// Check for ask messages that require user intervention - commented out as partialMessage is now handled via gRPC
// if (message.type === "partialMessage" && message.partialMessage?.type === "ask" && !message.partialMessage.partial) {
// const askType = message.partialMessage.ask as ClineAsk
// const askText = message.partialMessage.text
// // Automatically respond to different types of asks
// setTimeout(async () => {
// await autoRespondToAsk(webviewProvider, askType, askText)
// }, 100) // Small delay to ensure the message is processed first
// }
return originalPostMessageToWebview.call(webviewProvider.controller, message)
}
-2
View File
@@ -21,7 +21,6 @@ export interface ExtensionMessage {
| "lmStudioModels"
| "theme"
| "workspaceUpdated"
| "partialMessage"
| "openRouterModels"
| "openAiModels"
| "requestyModels"
@@ -46,7 +45,6 @@ export interface ExtensionMessage {
lmStudioModels?: string[]
vsCodeLmModels?: { vendor?: string; family?: string; version?: string; id?: string }[]
filePaths?: string[]
partialMessage?: ClineMessage
openRouterModels?: Record<string, ModelInfo>
openAiModels?: string[]
requestyModels?: Record<string, ModelInfo>
@@ -0,0 +1,253 @@
import { ClineMessage as AppClineMessage, ClineAsk as AppClineAsk, ClineSay as AppClineSay } from "@shared/ExtensionMessage"
import { ClineMessage as ProtoClineMessage, ClineMessageType, ClineAsk, ClineSay } from "@shared/proto/ui"
// Helper function to convert ClineAsk string to enum
function convertClineAskToProtoEnum(ask: AppClineAsk | undefined): ClineAsk | undefined {
if (!ask) {
return undefined
}
const mapping: Record<AppClineAsk, ClineAsk> = {
followup: ClineAsk.FOLLOWUP,
plan_mode_respond: ClineAsk.PLAN_MODE_RESPOND,
command: ClineAsk.COMMAND,
command_output: ClineAsk.COMMAND_OUTPUT,
completion_result: ClineAsk.COMPLETION_RESULT,
tool: ClineAsk.TOOL,
api_req_failed: ClineAsk.API_REQ_FAILED,
resume_task: ClineAsk.RESUME_TASK,
resume_completed_task: ClineAsk.RESUME_COMPLETED_TASK,
mistake_limit_reached: ClineAsk.MISTAKE_LIMIT_REACHED,
auto_approval_max_req_reached: ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED,
browser_action_launch: ClineAsk.BROWSER_ACTION_LAUNCH,
use_mcp_server: ClineAsk.USE_MCP_SERVER,
new_task: ClineAsk.NEW_TASK,
condense: ClineAsk.CONDENSE,
report_bug: ClineAsk.REPORT_BUG,
}
const result = mapping[ask]
if (result === undefined) {
console.warn(`Unknown ClineAsk value: ${ask}`)
}
return result
}
// Helper function to convert ClineAsk enum to string
function convertProtoEnumToClineAsk(ask: ClineAsk): AppClineAsk | undefined {
if (ask === ClineAsk.UNRECOGNIZED) {
console.warn("Received UNRECOGNIZED ClineAsk enum value")
return undefined
}
const mapping: Record<Exclude<ClineAsk, ClineAsk.UNRECOGNIZED>, AppClineAsk> = {
[ClineAsk.FOLLOWUP]: "followup",
[ClineAsk.PLAN_MODE_RESPOND]: "plan_mode_respond",
[ClineAsk.COMMAND]: "command",
[ClineAsk.COMMAND_OUTPUT]: "command_output",
[ClineAsk.COMPLETION_RESULT]: "completion_result",
[ClineAsk.TOOL]: "tool",
[ClineAsk.API_REQ_FAILED]: "api_req_failed",
[ClineAsk.RESUME_TASK]: "resume_task",
[ClineAsk.RESUME_COMPLETED_TASK]: "resume_completed_task",
[ClineAsk.MISTAKE_LIMIT_REACHED]: "mistake_limit_reached",
[ClineAsk.AUTO_APPROVAL_MAX_REQ_REACHED]: "auto_approval_max_req_reached",
[ClineAsk.BROWSER_ACTION_LAUNCH]: "browser_action_launch",
[ClineAsk.USE_MCP_SERVER]: "use_mcp_server",
[ClineAsk.NEW_TASK]: "new_task",
[ClineAsk.CONDENSE]: "condense",
[ClineAsk.REPORT_BUG]: "report_bug",
}
return mapping[ask]
}
// Helper function to convert ClineSay string to enum
function convertClineSayToProtoEnum(say: AppClineSay | undefined): ClineSay | undefined {
if (!say) {
return undefined
}
const mapping: Record<AppClineSay, ClineSay> = {
task: ClineSay.TASK,
error: ClineSay.ERROR,
api_req_started: ClineSay.API_REQ_STARTED,
api_req_finished: ClineSay.API_REQ_FINISHED,
text: ClineSay.TEXT,
reasoning: ClineSay.REASONING,
completion_result: ClineSay.COMPLETION_RESULT_SAY,
user_feedback: ClineSay.USER_FEEDBACK,
user_feedback_diff: ClineSay.USER_FEEDBACK_DIFF,
api_req_retried: ClineSay.API_REQ_RETRIED,
command: ClineSay.COMMAND_SAY,
command_output: ClineSay.COMMAND_OUTPUT_SAY,
tool: ClineSay.TOOL_SAY,
shell_integration_warning: ClineSay.SHELL_INTEGRATION_WARNING,
browser_action_launch: ClineSay.BROWSER_ACTION_LAUNCH_SAY,
browser_action: ClineSay.BROWSER_ACTION,
browser_action_result: ClineSay.BROWSER_ACTION_RESULT,
mcp_server_request_started: ClineSay.MCP_SERVER_REQUEST_STARTED,
mcp_server_response: ClineSay.MCP_SERVER_RESPONSE,
use_mcp_server: ClineSay.USE_MCP_SERVER_SAY,
diff_error: ClineSay.DIFF_ERROR,
deleted_api_reqs: ClineSay.DELETED_API_REQS,
clineignore_error: ClineSay.CLINEIGNORE_ERROR,
checkpoint_created: ClineSay.CHECKPOINT_CREATED,
load_mcp_documentation: ClineSay.LOAD_MCP_DOCUMENTATION,
info: ClineSay.INFO,
}
const result = mapping[say]
if (result === undefined) {
console.warn(`Unknown ClineSay value: ${say}`)
}
return result
}
// Helper function to convert ClineSay enum to string
function convertProtoEnumToClineSay(say: ClineSay): AppClineSay | undefined {
if (say === ClineSay.UNRECOGNIZED) {
console.warn("Received UNRECOGNIZED ClineSay enum value")
return undefined
}
const mapping: Record<Exclude<ClineSay, ClineSay.UNRECOGNIZED>, AppClineSay> = {
[ClineSay.TASK]: "task",
[ClineSay.ERROR]: "error",
[ClineSay.API_REQ_STARTED]: "api_req_started",
[ClineSay.API_REQ_FINISHED]: "api_req_finished",
[ClineSay.TEXT]: "text",
[ClineSay.REASONING]: "reasoning",
[ClineSay.COMPLETION_RESULT_SAY]: "completion_result",
[ClineSay.USER_FEEDBACK]: "user_feedback",
[ClineSay.USER_FEEDBACK_DIFF]: "user_feedback_diff",
[ClineSay.API_REQ_RETRIED]: "api_req_retried",
[ClineSay.COMMAND_SAY]: "command",
[ClineSay.COMMAND_OUTPUT_SAY]: "command_output",
[ClineSay.TOOL_SAY]: "tool",
[ClineSay.SHELL_INTEGRATION_WARNING]: "shell_integration_warning",
[ClineSay.BROWSER_ACTION_LAUNCH_SAY]: "browser_action_launch",
[ClineSay.BROWSER_ACTION]: "browser_action",
[ClineSay.BROWSER_ACTION_RESULT]: "browser_action_result",
[ClineSay.MCP_SERVER_REQUEST_STARTED]: "mcp_server_request_started",
[ClineSay.MCP_SERVER_RESPONSE]: "mcp_server_response",
[ClineSay.USE_MCP_SERVER_SAY]: "use_mcp_server",
[ClineSay.DIFF_ERROR]: "diff_error",
[ClineSay.DELETED_API_REQS]: "deleted_api_reqs",
[ClineSay.CLINEIGNORE_ERROR]: "clineignore_error",
[ClineSay.CHECKPOINT_CREATED]: "checkpoint_created",
[ClineSay.LOAD_MCP_DOCUMENTATION]: "load_mcp_documentation",
[ClineSay.INFO]: "info",
}
return mapping[say]
}
/**
* Convert application ClineMessage to proto ClineMessage
*/
export function convertClineMessageToProto(message: AppClineMessage): ProtoClineMessage {
// For sending messages, we need to provide values for required proto fields
const askEnum = message.ask ? convertClineAskToProtoEnum(message.ask) : undefined
const sayEnum = message.say ? convertClineSayToProtoEnum(message.say) : undefined
// Determine appropriate enum values based on message type
let finalAskEnum: ClineAsk = ClineAsk.FOLLOWUP // Proto default
let finalSayEnum: ClineSay = ClineSay.TEXT // Proto default
if (message.type === "ask") {
finalAskEnum = askEnum ?? ClineAsk.FOLLOWUP // Use FOLLOWUP as default for ask messages
} else if (message.type === "say") {
finalSayEnum = sayEnum ?? ClineSay.TEXT // Use TEXT as default for say messages
}
const protoMessage: ProtoClineMessage = {
ts: message.ts,
type: message.type === "ask" ? ClineMessageType.ASK : ClineMessageType.SAY,
ask: finalAskEnum,
say: finalSayEnum,
text: message.text ?? "",
reasoning: message.reasoning ?? "",
images: message.images ?? [],
files: message.files ?? [],
partial: message.partial ?? false,
lastCheckpointHash: message.lastCheckpointHash ?? "",
isCheckpointCheckedOut: message.isCheckpointCheckedOut ?? false,
isOperationOutsideWorkspace: message.isOperationOutsideWorkspace ?? false,
conversationHistoryIndex: message.conversationHistoryIndex ?? 0,
conversationHistoryDeletedRange: message.conversationHistoryDeletedRange
? {
startIndex: message.conversationHistoryDeletedRange[0],
endIndex: message.conversationHistoryDeletedRange[1],
}
: undefined,
}
return protoMessage
}
/**
* Convert proto ClineMessage to application ClineMessage
*/
export function convertProtoToClineMessage(protoMessage: ProtoClineMessage): AppClineMessage {
const message: AppClineMessage = {
ts: protoMessage.ts,
type: protoMessage.type === ClineMessageType.ASK ? "ask" : "say",
}
// Convert ask enum to string
if (protoMessage.type === ClineMessageType.ASK) {
const ask = convertProtoEnumToClineAsk(protoMessage.ask)
if (ask !== undefined) {
message.ask = ask
}
}
// Convert say enum to string
if (protoMessage.type === ClineMessageType.SAY) {
const say = convertProtoEnumToClineSay(protoMessage.say)
if (say !== undefined) {
message.say = say
}
}
// Convert other fields - preserve empty strings as they may be intentional
if (protoMessage.text !== "") {
message.text = protoMessage.text
}
if (protoMessage.reasoning !== "") {
message.reasoning = protoMessage.reasoning
}
if (protoMessage.images.length > 0) {
message.images = protoMessage.images
}
if (protoMessage.files.length > 0) {
message.files = protoMessage.files
}
if (protoMessage.partial) {
message.partial = protoMessage.partial
}
if (protoMessage.lastCheckpointHash !== "") {
message.lastCheckpointHash = protoMessage.lastCheckpointHash
}
if (protoMessage.isCheckpointCheckedOut) {
message.isCheckpointCheckedOut = protoMessage.isCheckpointCheckedOut
}
if (protoMessage.isOperationOutsideWorkspace) {
message.isOperationOutsideWorkspace = protoMessage.isOperationOutsideWorkspace
}
if (protoMessage.conversationHistoryIndex !== 0) {
message.conversationHistoryIndex = protoMessage.conversationHistoryIndex
}
// Convert conversationHistoryDeletedRange from object to tuple
if (protoMessage.conversationHistoryDeletedRange) {
message.conversationHistoryDeletedRange = [
protoMessage.conversationHistoryDeletedRange.startIndex,
protoMessage.conversationHistoryDeletedRange.endIndex,
]
}
return message
}
@@ -0,0 +1,11 @@
// AUTO-GENERATED FILE - DO NOT MODIFY DIRECTLY
// Generated by proto/build-proto.js
import { createGrpcClient } from "./host-grpc-client-base"
import { UriServiceDefinition } from "@shared/proto/host/uri"
import { WatchServiceDefinition } from "@shared/proto/host/watch"
const UriServiceClient = createGrpcClient(UriServiceDefinition)
const WatchServiceClient = createGrpcClient(WatchServiceDefinition)
export { UriServiceClient, WatchServiceClient }
@@ -2,6 +2,7 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useSt
import { useEvent } from "react-use"
import { EmptyRequest } from "@shared/proto/common"
import { WebviewProviderType as WebviewProviderTypeEnum, WebviewProviderTypeRequest } from "@shared/proto/ui"
import { convertProtoToClineMessage } from "@shared/proto-conversions/cline-message"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@shared/BrowserSettings"
import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "@shared/ChatSettings"
@@ -206,21 +207,6 @@ export const ExtensionStateContextProvider: React.FC<{
setFilePaths(message.filePaths ?? [])
break
}
case "partialMessage": {
const partialMessage = message.partialMessage!
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
break
}
case "openRouterModels": {
const updatedModels = message.openRouterModels ?? {}
setOpenRouterModels({
@@ -264,6 +250,7 @@ export const ExtensionStateContextProvider: React.FC<{
const chatButtonUnsubscribeRef = useRef<(() => void) | null>(null)
const accountButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const settingsButtonClickedSubscriptionRef = useRef<(() => void) | null>(null)
const partialMessageUnsubscribeRef = useRef<(() => void) | null>(null)
// Subscribe to state updates and UI events using the gRPC streaming API
useEffect(() => {
@@ -412,6 +399,43 @@ export const ExtensionStateContextProvider: React.FC<{
},
)
// Subscribe to partial message events
partialMessageUnsubscribeRef.current = UiServiceClient.subscribeToPartialMessage(EmptyRequest.create({}), {
onResponse: (protoMessage) => {
try {
console.log("[PARTIAL] Received partialMessage event from gRPC stream")
// Validate critical fields
if (!protoMessage.ts || protoMessage.ts <= 0) {
console.error("Invalid timestamp in partial message:", protoMessage)
return
}
const partialMessage = convertProtoToClineMessage(protoMessage)
console.log("[PARTIAL] Partial message:", partialMessage)
console.log("\n")
setState((prevState) => {
// worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
if (lastIndex !== -1) {
const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = partialMessage
return { ...prevState, clineMessages: newClineMessages }
}
return prevState
})
} catch (error) {
console.error("Failed to process partial message:", error, protoMessage)
}
},
onError: (error) => {
console.error("Error in partialMessage subscription:", error)
},
onComplete: () => {
console.log("[DEBUG] partialMessage subscription completed")
},
})
// Still send the webviewDidLaunch message for other initialization
vscode.postMessage({ type: "webviewDidLaunch" })
@@ -456,6 +480,10 @@ export const ExtensionStateContextProvider: React.FC<{
settingsButtonClickedSubscriptionRef.current()
settingsButtonClickedSubscriptionRef.current = null
}
if (partialMessageUnsubscribeRef.current) {
partialMessageUnsubscribeRef.current()
partialMessageUnsubscribeRef.current = null
}
}
}, [])