Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/telemetry/src/TelemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
}
Expand Down
34 changes: 34 additions & 0 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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<typeof rooCodeTelemetryEventSchema>
Expand Down
4 changes: 3 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,10 @@ export class Task extends EventEmitter<TaskEvents> 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
Expand Down Expand Up @@ -351,6 +352,7 @@ export class Task extends EventEmitter<TaskEvents> 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.
Expand Down
54 changes: 52 additions & 2 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2580,14 +2580,64 @@ export class ClineProvider
return task
}

public async cancelTask(): Promise<void> {
public async cancelTask(clickCount?: number): Promise<void> {
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)
Comment on lines +2605 to +2606
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variables taskHistory and currentTaskItem are declared but never used in the telemetry tracking logic. These can be removed to clean up the code.

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)

Expand Down
2 changes: 1 addition & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
const [didClickCancel, setDidClickCancel] = useState(false)
const [terminationClickCount, setTerminationClickCount] = useState(0)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
const prevExpandedRowsRef = useRef<Record<number, boolean>>()
Expand Down Expand Up @@ -412,6 +413,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setPrimaryButtonText(t("chat:resumeTask.title"))
setSecondaryButtonText(t("chat:terminate.title"))
setDidClickCancel(false) // special case where we reset the cancel button state
setTerminationClickCount(0) // Reset termination click count when resuming
break
case "resume_completed_task":
setSendingDisabled(false)
Expand Down Expand Up @@ -470,6 +472,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
everVisibleMessagesTsRef.current.clear() // Clear for new task
setCurrentFollowUpTs(null) // Clear follow-up answered state for new task
setIsCondensing(false) // Reset condensing state when switching tasks
setTerminationClickCount(0) // Reset termination click count for new task
// Note: sendingDisabled is not reset here as it's managed by message effects

// Clear any pending auto-approval timeout from previous task
Expand Down Expand Up @@ -730,7 +733,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const trimmedInput = text?.trim()

if (isStreaming) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding an inline comment to explain that the first cancel click is interpreted as a course correction, while subsequent clicks indicate task abandonment.

vscode.postMessage({ type: "cancelTask" })
const newClickCount = terminationClickCount + 1
setTerminationClickCount(newClickCount)
vscode.postMessage({ type: "cancelTask", clickCount: newClickCount })
setDidClickCancel(true)
return
}
Expand Down Expand Up @@ -769,7 +774,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk(undefined)
setEnableButtons(false)
},
[clineAsk, startNewTask, isStreaming],
[clineAsk, startNewTask, isStreaming, terminationClickCount],
)

const { info: model } = useSelectedModel(apiConfiguration)
Expand Down