From a15d43ce2dad89bee92d13b4e94b3c445c84d8af Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 26 Oct 2025 03:55:51 +0000 Subject: [PATCH] fix: Add terminationClickCount to useCallback dependency array - Fixes ESLint react-hooks/exhaustive-deps warning - Ensures callback properly updates when termination click count changes --- packages/telemetry/src/TelemetryService.ts | 34 +++++++++++++ packages/types/src/telemetry.ts | 34 +++++++++++++ src/core/task/Task.ts | 4 +- src/core/webview/ClineProvider.ts | 54 ++++++++++++++++++++- src/core/webview/webviewMessageHandler.ts | 2 +- src/shared/WebviewMessage.ts | 1 + webview-ui/src/components/chat/ChatView.tsx | 9 +++- 7 files changed, 132 insertions(+), 6 deletions(-) diff --git a/packages/telemetry/src/TelemetryService.ts b/packages/telemetry/src/TelemetryService.ts index 5ea4cef936fa..eac7d7d1f458 100644 --- a/packages/telemetry/src/TelemetryService.ts +++ b/packages/telemetry/src/TelemetryService.ts @@ -77,6 +77,40 @@ export class TelemetryService { this.captureEvent(TelemetryEventName.TASK_COMPLETED, { taskId }) } + public captureTaskTerminated( + taskId: string, + properties: { + terminationSource: "button" | "command" | "api" + elapsedTime: number + taskState: string + clickCount: number + intent: "correction" | "abandonment" | "guidance" + }, + ): void { + this.captureEvent(TelemetryEventName.TASK_TERMINATED, { taskId, ...properties }) + } + + public captureTaskCorrected( + taskId: string, + properties: { + elapsedTime: number + taskState: string + }, + ): void { + this.captureEvent(TelemetryEventName.TASK_CORRECTED, { taskId, ...properties }) + } + + public captureTaskAbandoned( + taskId: string, + properties: { + elapsedTime: number + taskState: string + clickCount: number + }, + ): void { + this.captureEvent(TelemetryEventName.TASK_ABANDONED, { taskId, ...properties }) + } + public captureConversationMessage(taskId: string, source: "user" | "assistant"): void { this.captureEvent(TelemetryEventName.TASK_CONVERSATION_MESSAGE, { taskId, source }) } diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 29612d42a2f8..38bc1087927c 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -23,6 +23,9 @@ export enum TelemetryEventName { TASK_COMPLETED = "Task Completed", TASK_MESSAGE = "Task Message", TASK_CONVERSATION_MESSAGE = "Conversation Message", + TASK_TERMINATED = "Task Terminated", + TASK_CORRECTED = "Task Corrected", + TASK_ABANDONED = "Task Abandoned", LLM_COMPLETION = "LLM Completion", MODE_SWITCH = "Mode Switched", MODE_SELECTOR_OPENED = "Mode Selector Opened", @@ -231,6 +234,37 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ cost: z.number().optional(), }), }), + z.object({ + type: z.literal(TelemetryEventName.TASK_TERMINATED), + properties: z.object({ + ...telemetryPropertiesSchema.shape, + taskId: z.string(), + terminationSource: z.enum(["button", "command", "api"]), + elapsedTime: z.number(), + taskState: z.string(), + clickCount: z.number(), + intent: z.enum(["correction", "abandonment", "guidance"]), + }), + }), + z.object({ + type: z.literal(TelemetryEventName.TASK_CORRECTED), + properties: z.object({ + ...telemetryPropertiesSchema.shape, + taskId: z.string(), + elapsedTime: z.number(), + taskState: z.string(), + }), + }), + z.object({ + type: z.literal(TelemetryEventName.TASK_ABANDONED), + properties: z.object({ + ...telemetryPropertiesSchema.shape, + taskId: z.string(), + elapsedTime: z.number(), + taskState: z.string(), + clickCount: z.number(), + }), + }), ]) export type RooCodeTelemetryEvent = z.infer diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 74cbd2a11005..8788b2938123 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -152,9 +152,10 @@ export class Task extends EventEmitter implements TaskLike { childTaskId?: string readonly instanceId: string - readonly metadata: TaskMetadata + readonly metadata: TaskMetadata & { startTime?: number } todoList?: TodoItem[] + terminationClickCount: number = 0 readonly rootTask: Task | undefined = undefined readonly parentTask: Task | undefined = undefined @@ -351,6 +352,7 @@ export class Task extends EventEmitter implements TaskLike { this.metadata = { task: historyItem ? historyItem.task : task, images: historyItem ? [] : images, + startTime: Date.now(), } // Normal use-case is usually retry similar history task with new workspace. diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6c1d612943a6..224b9277d21f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2580,14 +2580,64 @@ export class ClineProvider return task } - public async cancelTask(): Promise { + public async cancelTask(clickCount?: number): Promise { const task = this.getCurrentTask() if (!task) { return } - console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`) + console.log( + `[cancelTask] cancelling task ${task.taskId}.${task.instanceId} with click count: ${clickCount || 1}`, + ) + + // Update task's termination click count if provided + if (clickCount !== undefined) { + task.terminationClickCount = clickCount + } + + // Track task termination for telemetry + const taskStartTime = task.metadata?.startTime || Date.now() + const elapsedTime = Date.now() - taskStartTime + const taskState = task.taskStatus || "unknown" + + // Get termination context from task history + const taskHistory = this.getGlobalState("taskHistory") ?? [] + const currentTaskItem = taskHistory.find((item: HistoryItem) => item.id === task.taskId) + const terminationClickCount = task.terminationClickCount || clickCount || 1 + + // Determine user intent based on context + let intent: "correction" | "abandonment" | "guidance" = "correction" + if (terminationClickCount > 1) { + intent = "abandonment" + } else if (task.isStreaming) { + intent = "correction" + } else if (task.taskAsk) { + intent = "guidance" + } + + // Capture telemetry event + TelemetryService.instance.captureTaskTerminated(task.taskId, { + terminationSource: "button", + elapsedTime, + taskState, + clickCount: terminationClickCount, + intent, + }) + + // Also capture specific intent events + if (intent === "correction") { + TelemetryService.instance.captureTaskCorrected(task.taskId, { + elapsedTime, + taskState, + }) + } else if (intent === "abandonment") { + TelemetryService.instance.captureTaskAbandoned(task.taskId, { + elapsedTime, + taskState, + clickCount: terminationClickCount, + }) + } const { historyItem, uiMessagesFilePath } = await this.getTaskWithId(task.taskId) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 38b51c712380..92a365c85aba 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1045,7 +1045,7 @@ export const webviewMessageHandler = async ( break } case "cancelTask": - await provider.cancelTask() + await provider.cancelTask(message.clickCount) break case "allowedCommands": { // Validate and sanitize the commands array diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e460f20384c4..4baea199cdaf 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -279,6 +279,7 @@ export interface WebviewMessage { upsellId?: string // For dismissUpsell list?: string[] // For dismissedUpsells response organizationId?: string | null // For organization switching + clickCount?: number // For task termination tracking codeIndexSettings?: { // Global state settings codebaseIndexEnabled: boolean diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index b454c97bef96..dc0daed2cdcf 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -183,6 +183,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(undefined) const [secondaryButtonText, setSecondaryButtonText] = useState(undefined) const [didClickCancel, setDidClickCancel] = useState(false) + const [terminationClickCount, setTerminationClickCount] = useState(0) const virtuosoRef = useRef(null) const [expandedRows, setExpandedRows] = useState>({}) const prevExpandedRowsRef = useRef>() @@ -412,6 +413,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction