Compare commits

...

3 Commits

Author SHA1 Message Date
Arafatkatze 042d3a3fdb Refactoring 2025-09-30 17:39:05 -07:00
Arafatkatze ec5348180b Taking out command runner 2025-09-30 17:39:05 -07:00
Arafatkatze 2793a8fd38 Adding Telemetry for multi root workspace 2025-09-30 17:39:03 -07:00
6 changed files with 281 additions and 23 deletions
@@ -4,6 +4,7 @@ import { WorkspacePathAdapter } from "@core/workspace/WorkspacePathAdapter"
import { showSystemNotification } from "@integrations/notifications"
import { COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
import { ClineAsk } from "@shared/ExtensionMessage"
import { arePathsEqual } from "@utils/path"
import { fixModelHtmlEscaping } from "@utils/string"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
@@ -81,13 +82,17 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
let executionDir: string = config.cwd
let actualCommand: string = command
let workspaceHintUsed = false
let workspaceHint: string | undefined
if (config.isMultiRootEnabled && config.workspaceManager) {
// Check if command has a workspace hint prefix
// e.g., "@backend:npm install" or just "npm install"
const commandMatch = command.match(/^@(\w+):(.+)$/)
if (commandMatch) {
const workspaceHint = commandMatch[1]
workspaceHintUsed = true
workspaceHint = commandMatch[1]
actualCommand = commandMatch[2].trim()
// Find the workspace root for this hint
@@ -122,13 +127,35 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
? autoApproveResult
: [autoApproveResult, false]
// Determine workspace context for telemetry
const resolvedToNonPrimary = !arePathsEqual(executionDir, config.cwd)
const workspaceContext = {
isMultiRootEnabled: config.isMultiRootEnabled || false,
usedWorkspaceHint: workspaceHintUsed,
resolvedToNonPrimary,
resolutionMethod: (workspaceHintUsed ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
}
// Capture workspace path resolution telemetry
if (config.isMultiRootEnabled && config.workspaceManager) {
telemetryService.captureWorkspacePathResolved(
config.ulid,
"ExecuteCommandToolHandler",
workspaceHintUsed ? "hint_provided" : "fallback_to_primary",
workspaceHintUsed ? "workspace_name" : undefined,
resolvedToNonPrimary, // resolution success = resolved to different workspace
undefined, // TODO: could calculate workspace index if needed
true,
)
}
if ((!requiresApprovalPerLLM && autoApproveSafe) || (requiresApprovalPerLLM && autoApproveSafe && autoApproveAll)) {
// Auto-approve flow
await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "command")
await config.callbacks.say("command", actualCommand, undefined, undefined, false)
config.taskState.consecutiveAutoApprovedRequestsCount++
didAutoApprove = true
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
} else {
// Manual approval flow
showNotificationForApprovalIfAutoApprovalEnabled(
@@ -143,10 +170,17 @@ export class ExecuteCommandToolHandler implements IFullyManagedTool {
config,
)
if (!didApprove) {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
false,
workspaceContext,
)
return formatResponse.toolDenied()
}
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true, workspaceContext)
}
// Setup timeout notification for long-running auto-approved commands
@@ -1,8 +1,9 @@
import path from "node:path"
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { listFiles } from "@services/glob/list-files"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
@@ -69,6 +70,15 @@ export class ListFilesToolHandler implements IFullyManagedTool {
const { absolutePath, displayPath } =
typeof pathResult === "string" ? { absolutePath: pathResult, displayPath: relDirPath! } : pathResult
// Determine workspace context for telemetry
const fallbackAbsolutePath = path.resolve(config.cwd, relDirPath ?? "")
const workspaceContext = {
isMultiRootEnabled: config.isMultiRootEnabled || false,
usedWorkspaceHint: typeof pathResult !== "string", // multi-root path result indicates hint usage
resolvedToNonPrimary: !arePathsEqual(absolutePath, fallbackAbsolutePath),
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
}
// Execute the actual list files operation
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
@@ -91,7 +101,7 @@ export class ListFilesToolHandler implements IFullyManagedTool {
config.taskState.consecutiveAutoApprovedRequestsCount++
// Capture telemetry
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to view directory ${getWorkspaceBasename(absolutePath, "ListFilesToolHandler.notification")}/`
@@ -107,10 +117,24 @@ export class ListFilesToolHandler implements IFullyManagedTool {
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
if (!didApprove) {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
false,
workspaceContext,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
true,
workspaceContext,
)
}
}
@@ -1,8 +1,9 @@
import path from "node:path"
import type { ToolUse } from "@core/assistant-message"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { extractFileContent } from "@integrations/misc/extract-file-content"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { telemetryService } from "@/services/telemetry"
import { ClineSayTool } from "@/shared/ExtensionMessage"
import { ClineDefaultTool } from "@/shared/tools"
@@ -72,6 +73,15 @@ export class ReadFileToolHandler implements IFullyManagedTool {
const { absolutePath, displayPath } =
typeof pathResult === "string" ? { absolutePath: pathResult, displayPath: relPath! } : pathResult
// Determine workspace context for telemetry
const fallbackAbsolutePath = path.resolve(config.cwd, relPath ?? "")
const workspaceContext = {
isMultiRootEnabled: config.isMultiRootEnabled || false,
usedWorkspaceHint: typeof pathResult !== "string", // multi-root path result indicates hint usage
resolvedToNonPrimary: !arePathsEqual(absolutePath, fallbackAbsolutePath),
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
}
// Handle approval flow
const sharedMessageProps = {
tool: "readFile",
@@ -89,7 +99,7 @@ export class ReadFileToolHandler implements IFullyManagedTool {
config.taskState.consecutiveAutoApprovedRequestsCount++
// Capture telemetry
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to read ${getWorkspaceBasename(absolutePath, "ReadFileToolHandler.notification")}`
@@ -105,10 +115,24 @@ export class ReadFileToolHandler implements IFullyManagedTool {
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
if (!didApprove) {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
false,
workspaceContext,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
true,
workspaceContext,
)
}
}
@@ -1,6 +1,6 @@
import type { ToolUse } from "@core/assistant-message"
import { regexSearchFiles } from "@services/ripgrep"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import * as path from "path"
import { formatResponse } from "@/core/prompts/responses"
import { parseWorkspaceInlinePath } from "@/core/workspace/utils/parseWorkspaceInlinePath"
@@ -227,17 +227,68 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
// Determine which paths to search
const searchPaths = this.determineSearchPaths(config, parsedPath, workspaceHint, relDirPath!)
// Determine workspace context for telemetry
const primaryWorkspaceRoot = searchPaths[0]?.workspaceRoot
const resolvedToNonPrimary =
searchPaths.length === 0
? true
: searchPaths.length > 1 || (primaryWorkspaceRoot ? !arePathsEqual(primaryWorkspaceRoot, config.cwd) : true)
const workspaceContext = {
isMultiRootEnabled: config.isMultiRootEnabled || false,
usedWorkspaceHint: !!workspaceHint,
resolvedToNonPrimary,
resolutionMethod: (workspaceHint ? "hint" : searchPaths.length > 1 ? "path_detection" : "primary_fallback") as
| "hint"
| "primary_fallback"
| "path_detection",
}
// Capture workspace path resolution telemetry
if (config.isMultiRootEnabled && config.workspaceManager) {
const resolutionType = workspaceHint
? "hint_provided"
: searchPaths.length > 1
? "cross_workspace_search"
: "fallback_to_primary"
telemetryService.captureWorkspacePathResolved(
config.ulid,
"SearchFilesToolHandler",
resolutionType,
workspaceHint ? "workspace_name" : undefined,
searchPaths.length > 0, // resolution success = found paths to search
undefined, // TODO: could calculate primary workspace index
true,
)
}
// Execute searches in all relevant workspaces in parallel
const searchPromises = searchPaths.map(({ absolutePath, workspaceName, workspaceRoot }) =>
this.executeSearch(config, absolutePath, workspaceName, workspaceRoot, regex, filePattern),
)
// Wait for all searches to complete
const searchStartTime = performance.now()
const searchResults = await Promise.all(searchPromises)
const searchDurationMs = performance.now() - searchStartTime
// Format and combine results
const results = this.formatSearchResults(config, searchResults, searchPaths)
// Capture workspace search pattern telemetry
if (config.isMultiRootEnabled && config.workspaceManager) {
const searchType = workspaceHint ? "targeted" : searchPaths.length > 1 ? "cross_workspace" : "primary_only"
const resultsFound = searchResults.some((result) => result.resultCount > 0)
telemetryService.captureWorkspaceSearchPattern(
config.ulid,
searchType,
searchPaths.length,
!!workspaceHint,
resultsFound,
searchDurationMs,
)
}
const sharedMessageProps = {
tool: "searchFiles",
path: getReadablePath(config.cwd, relDirPath!),
@@ -256,7 +307,7 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
config.taskState.consecutiveAutoApprovedRequestsCount++
// Capture telemetry
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
} else {
// Manual approval flow
const notificationMessage = `Cline wants to search files for ${regex}`
@@ -272,10 +323,24 @@ export class SearchFilesToolHandler implements IFullyManagedTool {
const didApprove = await ToolResultUtils.askApprovalAndPushFeedback("tool", completeMessage, config)
if (!didApprove) {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
false,
workspaceContext,
)
return formatResponse.toolDenied()
} else {
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
true,
workspaceContext,
)
}
}
@@ -1,3 +1,4 @@
import path from "node:path"
import { setTimeout as setTimeoutPromise } from "node:timers/promises"
import type { ToolUse } from "@core/assistant-message"
import { constructNewFileContent } from "@core/assistant-message/diff"
@@ -6,7 +7,7 @@ import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { ClineSayTool } from "@shared/ExtensionMessage"
import { fileExistsAtPath } from "@utils/fs"
import { getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { fixModelHtmlEscaping, removeInvalidChars } from "@utils/string"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
@@ -121,7 +122,8 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
return "" // can only happen if the sharedLogic adds an error to userMessages
}
const { relPath, absolutePath, fileExists, diff, content, newContent } = result
const { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext } = result
// Handle approval flow
const sharedMessageProps: ClineSayTool = {
@@ -163,7 +165,7 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
config.taskState.consecutiveAutoApprovedRequestsCount++
// Capture telemetry
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true)
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, true, true, workspaceContext)
// we need an artificial delay to let the diagnostics catch up to the changes
await setTimeoutPromise(3_500)
@@ -212,7 +214,14 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
// await config.services.diffViewProvider.reset()
config.taskState.didRejectTool = true
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, false)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
false,
workspaceContext,
)
await config.services.diffViewProvider.revertChanges()
return `The user denied this operation. ${fileDeniedNote}`
@@ -234,7 +243,14 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
await config.callbacks.say("user_feedback", text, images, files)
}
telemetryService.captureToolUsage(config.ulid, block.name, config.api.getModel().id, false, true)
telemetryService.captureToolUsage(
config.ulid,
block.name,
config.api.getModel().id,
false,
true,
workspaceContext,
)
}
}
@@ -304,6 +320,15 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
? { absolutePath: pathResult, resolvedPath: relPath }
: { absolutePath: pathResult.absolutePath, resolvedPath: pathResult.resolvedPath }
// Determine workspace context for telemetry
const fallbackAbsolutePath = path.resolve(config.cwd, relPath)
const workspaceContext = {
isMultiRootEnabled: config.isMultiRootEnabled || false,
usedWorkspaceHint: typeof pathResult !== "string", // multi-root path result indicates hint usage
resolvedToNonPrimary: !arePathsEqual(absolutePath, fallbackAbsolutePath),
resolutionMethod: (typeof pathResult !== "string" ? "hint" : "primary_fallback") as "hint" | "primary_fallback",
}
// Check clineignore access first
const accessValidation = this.validator.checkClineIgnorePath(resolvedPath)
if (!accessValidation.ok) {
@@ -420,6 +445,6 @@ export class WriteToFileToolHandler implements IFullyManagedTool {
newContent = newContent.trimEnd() // remove any trailing newlines, since it's automatically inserted by the editor
return { relPath, absolutePath, fileExists, diff, content, newContent }
return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
}
}
+87 -1
View File
@@ -191,6 +191,8 @@ export class TelemetryService {
MENTION_USED: "task.mention_used",
MENTION_FAILED: "task.mention_failed",
MENTION_SEARCH_RESULTS: "task.mention_search_results",
// Multi-workspace search pattern tracking
WORKSPACE_SEARCH_PATTERN: "task.workspace_search_pattern",
},
// UI interaction events for tracking user engagement
UI: {
@@ -580,10 +582,24 @@ export class TelemetryService {
* Records when a tool is used during task execution
* @param ulid Unique identifier for the task
* @param tool Name of the tool being used
* @param modelId The model ID being used
* @param autoApproved Whether the tool was auto-approved based on settings
* @param success Whether the tool execution was successful
* @param workspaceContext Optional workspace context for multi-root workspace tracking
*/
public captureToolUsage(ulid: string, tool: string, modelId: string, autoApproved: boolean, success: boolean) {
public captureToolUsage(
ulid: string,
tool: string,
modelId: string,
autoApproved: boolean,
success: boolean,
workspaceContext?: {
isMultiRootEnabled: boolean
usedWorkspaceHint: boolean
resolvedToNonPrimary: boolean
resolutionMethod: "hint" | "primary_fallback" | "path_detection"
},
) {
this.capture({
event: TelemetryService.EVENTS.TASK.TOOL_USED,
properties: {
@@ -592,6 +608,13 @@ export class TelemetryService {
autoApproved,
success,
modelId,
// Workspace context (optional)
...(workspaceContext && {
workspace_multi_root_enabled: workspaceContext.isMultiRootEnabled,
workspace_hint_used: workspaceContext.usedWorkspaceHint,
workspace_resolved_non_primary: workspaceContext.resolvedToNonPrimary,
workspace_resolution_method: workspaceContext.resolutionMethod,
}),
},
})
}
@@ -1246,6 +1269,69 @@ export class TelemetryService {
})
}
/**
* Records workspace path resolution events
* @param ulid Unique identifier for the task
* @param context The component/handler where resolution occurred
* @param resolutionType Type of resolution performed
* @param hintType Type of workspace hint provided (if any)
* @param resolutionSuccess Whether the resolution was successful
* @param targetWorkspaceIndex Index of the resolved workspace (0=primary, 1=secondary, etc.)
* @param isMultiRootEnabled Whether multi-root mode is enabled
*/
public captureWorkspacePathResolved(
ulid: string,
context: string,
resolutionType: "hint_provided" | "fallback_to_primary" | "cross_workspace_search",
hintType?: "workspace_name" | "workspace_path" | "invalid",
resolutionSuccess?: boolean,
targetWorkspaceIndex?: number,
isMultiRootEnabled?: boolean,
) {
this.capture({
event: TelemetryService.EVENTS.WORKSPACE.PATH_RESOLVED,
properties: {
ulid,
context,
resolution_type: resolutionType,
hint_type: hintType,
resolution_success: resolutionSuccess,
target_workspace_index: targetWorkspaceIndex,
is_multi_root_enabled: isMultiRootEnabled,
},
})
}
/**
* Records multi-workspace search patterns and performance
* @param ulid Unique identifier for the task
* @param searchType Type of search performed
* @param workspaceCount Number of workspaces searched
* @param hintProvided Whether a workspace hint was provided
* @param resultsFound Whether search results were found
* @param searchDurationMs Optional search duration in milliseconds
*/
public captureWorkspaceSearchPattern(
ulid: string,
searchType: "targeted" | "cross_workspace" | "primary_only",
workspaceCount: number,
hintProvided: boolean,
resultsFound: boolean,
searchDurationMs?: number,
) {
this.capture({
event: TelemetryService.EVENTS.TASK.WORKSPACE_SEARCH_PATTERN,
properties: {
ulid,
search_type: searchType,
workspace_count: workspaceCount,
hint_provided: hintProvided,
results_found: resultsFound,
search_duration_ms: searchDurationMs,
},
})
}
/**
* Checks if a specific telemetry category is enabled
* @param category The telemetry category to check