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"
import { formatResponse } from "@core/prompts/responses"
import { getWorkspaceBasename, resolveWorkspacePath } from "@core/workspace"
import { processFilesIntoText } from "@integrations/misc/extract-text"
import { ClineSayTool } from "@shared/ExtensionMessage"
import { fileExistsAtPath } from "@utils/fs"
import { arePathsEqual, getReadablePath, isLocatedInWorkspace } from "@utils/path"
import { HostProvider } from "@/hosts/host-provider"
import {
	checkReadinessJsonTwin,
	formatIntegrityError,
	gatherAndCheckReadinessIntegrity,
	looksLikeReadinessReport,
} from "@/services/cra/reportIntegrity"
import { telemetryService } from "@/services/telemetry"
import { ClineDefaultTool } from "@/shared/tools"
import type { ToolResponse } from "../../index"
import { showNotificationForApproval } from "../../utils"
import type { IFullyManagedTool } from "../ToolExecutorCoordinator"
import type { ToolValidator } from "../ToolValidator"
import type { TaskConfig } from "../types/TaskConfig"
import type { StronglyTypedUIHelpers } from "../types/UIHelpers"
import { applyModelContentFixes, stripFullFileCodeFenceWrapper } from "../utils/ModelContentProcessor"
import { ToolDisplayUtils } from "../utils/ToolDisplayUtils"
import { ToolResultUtils } from "../utils/ToolResultUtils"
import { emitCraMilestoneForWrite } from "./craFunnel"

/**
 * True if `absolutePath` is inside the Adsum extension install (read-only on a published build) or any
 * copy of the bundled samples (`demo-scenarios/…`, ours not the user's). Agent writes must never land
 * here — it fails on a published install and mutating the shipped sample corrupts it. Pure; exported
 * for tests. The `demo-scenarios` segment is matched even outside the install so a dev/source checkout
 * copy is protected too (an nRF/ESP firmware project would not legitimately contain that folder).
 */
export function isUnderExtensionOrSample(absolutePath: string, extensionRoot?: string): boolean {
	const norm = absolutePath.replace(/\\/g, "/")
	if (extensionRoot) {
		const root = extensionRoot.replace(/\\/g, "/").replace(/\/+$/, "")
		if (norm === root || norm.startsWith(`${root}/`)) {
			return true
		}
	}
	return /(^|\/)demo-scenarios(\/|$)/.test(norm)
}

export class WriteToFileToolHandler implements IFullyManagedTool {
	readonly name = ClineDefaultTool.FILE_NEW // This handler supports write_to_file, replace_in_file, and new_rule

	constructor(private validator: ToolValidator) {}

	getDescription(block: ToolUse): string {
		return `[${block.name} for '${block.params.path || block.params.absolutePath}']`
	}

	async handlePartialBlock(block: ToolUse, uiHelpers: StronglyTypedUIHelpers): Promise<void> {
		const rawRelPath = block.params.path || block.params.absolutePath
		const rawContent = block.params.content // for write_to_file
		const rawDiff = block.params.diff // for replace_in_file

		// Early return if we don't have enough data yet
		if (!rawRelPath || (!rawContent && !rawDiff)) {
			// Wait until we have the path and either content or diff
			return
		}

		const config = uiHelpers.getConfig()

		// Never open the diff editor on a degenerate path (filesystem root / blank). Opening on "/" or a
		// directory leaves the editor unmatched and the next update throws "User closed text editor". The
		// authoritative error is returned from execute(); here we simply refuse to open. See execute().
		if (this.isDegeneratePath(config.cwd, rawRelPath)) {
			return
		}

		// Creates file if it doesn't exist, and opens editor to stream content in. We don't want to handle this in the try/catch below since the error handler for it resets the diff view, which wouldn't be open if this failed.
		const result = await this.validateAndPrepareFileOperation(config, block, rawRelPath, rawDiff, rawContent)
		if (!result) {
			return
		}

		try {
			const { relPath, absolutePath, fileExists, diff, content, newContent } = result

			// Create and show partial UI message
			const sharedMessageProps: ClineSayTool = {
				tool: fileExists ? "editedExistingFile" : "newFileCreated",
				path: getReadablePath(
					config.cwd,
					uiHelpers.removeClosingTag(block, block.params.path ? "path" : "absolutePath", relPath),
				),
				content: diff || content,
				operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
			}
			const partialMessage = JSON.stringify(sharedMessageProps)

			// Handle auto-approval vs manual approval for partial
			if (await uiHelpers.shouldAutoApproveToolWithPath(block.name, relPath)) {
				await uiHelpers.removeLastPartialMessageIfExistsWithType("ask", "tool") // in case the user changes auto-approval settings mid stream
				await uiHelpers.say("tool", partialMessage, undefined, undefined, block.partial)
			} else {
				await uiHelpers.removeLastPartialMessageIfExistsWithType("say", "tool")
				await uiHelpers.ask("tool", partialMessage, block.partial).catch(() => {})
			}

			// CRITICAL: Open editor and stream content in real-time (from original code)
			if (!config.services.diffViewProvider.isEditing) {
				// Open the editor and prepare to stream content in
				await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
			}
			// Editor is open, stream content in real-time (false = don't finalize yet)
			await config.services.diffViewProvider.update(newContent, false)
		} catch (error) {
			// Reset diff view on error
			await config.services.diffViewProvider.revertChanges()
			await config.services.diffViewProvider.reset()
			throw error
		}
	}

	async execute(config: TaskConfig, block: ToolUse): Promise<ToolResponse> {
		const rawRelPath = block.params.path || block.params.absolutePath
		const rawContent = block.params.content // for write_to_file
		const rawDiff = block.params.diff // for replace_in_file

		// Extract provider information for telemetry
		const { providerId, modelId } = this.getModelInfo(config)

		// Validate required parameters based on tool type
		if (!rawRelPath) {
			config.taskState.consecutiveMistakeCount++
			await config.services.diffViewProvider.reset()
			return await config.callbacks.sayAndCreateMissingParamError(
				block.name,
				block.params.absolutePath ? "absolutePath" : "path",
			)
		}

		// Guard: reject a degenerate path that resolves to the filesystem root ("/").
		// This happens when the agent emits "/" or an empty path while no project folder
		// is open yet (e.g. ESP prototype-from-scratch). Opening a diff editor on "/" produces
		// an opaque "User closed text editor" error that the model cannot self-correct from.
		// Returning a clear message lets it recover immediately by asking the user for a real path.
		if (this.isDegeneratePath(config.cwd, rawRelPath)) {
			config.taskState.consecutiveMistakeCount++
			return formatResponse.toolError(
				`${block.name} path resolved to the filesystem root ("/"). ` +
					`Provide a full absolute path for the file, e.g. "/home/user/myproject/main/main.c". ` +
					`Ask the user where they want the project folder if it has not been decided yet.`,
			)
		}

		if (block.name === "replace_in_file" && !rawDiff) {
			config.taskState.consecutiveMistakeCount++
			await config.services.diffViewProvider.reset()
			return await config.callbacks.sayAndCreateMissingParamError(block.name, "diff")
		}

		if (block.name === "write_to_file" && !rawContent) {
			config.taskState.consecutiveMistakeCount++
			await config.services.diffViewProvider.reset()
			return await config.callbacks.sayAndCreateMissingParamError(block.name, "content")
		}

		if (block.name === "new_rule" && !rawContent) {
			config.taskState.consecutiveMistakeCount++
			await config.services.diffViewProvider.reset()
			return await config.callbacks.sayAndCreateMissingParamError(block.name, "content")
		}

		config.taskState.consecutiveMistakeCount = 0

		try {
			const result = await this.validateAndPrepareFileOperation(config, block, rawRelPath, rawDiff, rawContent)
			if (!result) {
				return "" // can only happen if the sharedLogic adds an error to userMessages
			}

			const { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext } = result

			// Guard: never write into the Adsum extension install or any bundled sample. On a published
			// install the extension dir is read-only (a write fails); and the bundled CRA/demo samples
			// (`demo-scenarios/…`) are ours, not the user's — mutating them corrupts the shipped sample.
			// The CRA bundled-sample run must read in place and write its output to the user's workspace or
			// an OS temp dir. (Prompt rules say the same; this enforces it for weaker models.)
			if (this.isExtensionOwnedPath(absolutePath)) {
				config.taskState.consecutiveMistakeCount++
				await config.services.diffViewProvider.reset()
				return formatResponse.toolError(
					`Refusing to write to "${getReadablePath(config.cwd, relPath)}": it is inside the Adsum extension ` +
						`or a bundled sample (read-only — mutating it corrupts the install). Write to the user's open ` +
						`project, or to an OS temp dir (e.g. /tmp/… or %TEMP%\\…). For the CRA bundled-sample preview, ` +
						`show the report inline or save to a namespaced folder under the user's Desktop — never into the sample.`,
				)
			}

			// Handle approval flow
			const sharedMessageProps: ClineSayTool = {
				tool: fileExists ? "editedExistingFile" : "newFileCreated",
				path: getReadablePath(config.cwd, relPath),
				content: diff || content,
				operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
			}
			// if isEditingFile false, that means we have the full contents of the file already.
			// it's important to note how this function works, you can't make the assumption that the block.partial conditional will always be called since it may immediately get complete, non-partial data. So this part of the logic will always be called.
			// in other words, you must always repeat the block.partial logic here
			if (!config.services.diffViewProvider.isEditing) {
				// show gui message before showing edit animation
				const partialMessage = JSON.stringify(sharedMessageProps)
				await config.callbacks.ask("tool", partialMessage, true).catch(() => {}) // sending true for partial even though it's not a partial, this shows the edit row before the content is streamed into the editor
				await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
			}
			await config.services.diffViewProvider.update(newContent, true)
			await setTimeoutPromise(300) // wait for diff view to update
			await config.services.diffViewProvider.scrollToFirstDiff()
			// showOmissionWarning(this.diffViewProvider.originalContent || "", newContent)

			const completeMessage = JSON.stringify({
				...sharedMessageProps,
				content: diff || content,
				operationIsLocatedInWorkspace: await isLocatedInWorkspace(relPath),
				// ? formatResponse.createPrettyPatch(
				// 		relPath,
				// 		this.diffViewProvider.originalContent,
				// 		newContent,
				// 	)
				// : undefined,
			} satisfies ClineSayTool)

			if (await config.callbacks.shouldAutoApproveToolWithPath(block.name, relPath)) {
				// Auto-approval flow
				await config.callbacks.removeLastPartialMessageIfExistsWithType("ask", "tool")
				await config.callbacks.say("tool", completeMessage, undefined, undefined, false)

				// Capture telemetry
				telemetryService.captureToolUsage(
					config.ulid,
					block.name,
					modelId,
					providerId,
					true,
					true,
					workspaceContext,
					block.isNativeToolCall,
				)

				// we need an artificial delay to let the diagnostics catch up to the changes
				await setTimeoutPromise(3_500)
			} else {
				// Manual approval flow with detailed feedback handling
				const notificationMessage = `Adsum IoT Coder wants to ${fileExists ? "edit" : "create"} ${getWorkspaceBasename(relPath, "WriteToFile.notification")}`

				// Show notification
				showNotificationForApproval(notificationMessage, config.autoApprovalSettings.enableNotifications)

				await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "tool")

				// Need a more customized tool response for file edits to highlight the fact that the file was not updated (particularly important for deepseek)

				const { response, text, images, files } = await config.callbacks.ask("tool", completeMessage, false)

				if (response !== "yesButtonClicked") {
					// Handle rejection with detailed messages
					const fileDeniedNote = fileExists
						? "The file was not updated, and maintains its original contents."
						: "The file was not created."

					// Process user feedback if provided (with file content processing)
					if (text || (images && images.length > 0) || (files && files.length > 0)) {
						let fileContentString = ""
						if (files && files.length > 0) {
							fileContentString = await processFilesIntoText(files)
						}

						// Push additional tool feedback using existing utilities
						ToolResultUtils.pushAdditionalToolFeedback(
							config.taskState.userMessageContent,
							text,
							images,
							fileContentString,
						)
						await config.callbacks.say("user_feedback", text, images, files)
					}

					// // Clean up the diff view when operation is rejected
					// await config.services.diffViewProvider.revertChanges()
					// await config.services.diffViewProvider.reset()

					config.taskState.didRejectTool = true
					telemetryService.captureToolUsage(
						config.ulid,
						block.name,
						modelId,
						providerId,
						false,
						false,
						workspaceContext,
						block.isNativeToolCall,
					)

					await config.services.diffViewProvider.revertChanges()
					return `The user denied this operation. ${fileDeniedNote}`
				} else {
					// User hit the approve button, and may have provided feedback
					if (text || (images && images.length > 0) || (files && files.length > 0)) {
						let fileContentString = ""
						if (files && files.length > 0) {
							fileContentString = await processFilesIntoText(files)
						}

						// Push additional tool feedback using existing utilities
						ToolResultUtils.pushAdditionalToolFeedback(
							config.taskState.userMessageContent,
							text,
							images,
							fileContentString,
						)
						await config.callbacks.say("user_feedback", text, images, files)
					}

					telemetryService.captureToolUsage(
						config.ulid,
						block.name,
						modelId,
						providerId,
						false,
						true,
						workspaceContext,
						block.isNativeToolCall,
					)
				}
			}

			// Run PreToolUse hook after approval but before execution
			try {
				const { ToolHookUtils } = await import("../utils/ToolHookUtils")
				await ToolHookUtils.runPreToolUseIfEnabled(config, block)
			} catch (error) {
				const { PreToolUseHookCancellationError } = await import("@core/hooks/PreToolUseHookCancellationError")
				if (error instanceof PreToolUseHookCancellationError) {
					await config.services.diffViewProvider.revertChanges()
					await config.services.diffViewProvider.reset()
					return formatResponse.toolDenied()
				}
				throw error
			}

			// CRA report integrity (F1): the readiness report is composed by the model and otherwise saved
			// unchecked, so a run can persist fabricated SBOM/CVE claims. Before committing, cross-check the
			// content against the artifacts this run actually produced (the SPDX + cve-scan json); on a provable
			// contradiction, cancel the write so the model rewrites with the real figures. Fails open on any
			// guard error (never blocks a legitimate report).
			const craIntegrityIssues = gatherAndCheckReadinessIntegrity(absolutePath, newContent)
			if (craIntegrityIssues.length > 0) {
				await config.services.diffViewProvider.revertChanges()
				await config.services.diffViewProvider.reset()
				// Honesty-moat health: how often the model produces a report that fails the cross-check + which
				// checks fired. Issue-KIND enums only (never report content); iot_platform auto-added by the provider.
				telemetryService.captureCraReportRejected({
					issueCount: craIntegrityIssues.length,
					kinds: [...new Set(craIntegrityIssues.map((i) => i.kind))],
				})
				return formatResponse.toolError(formatIntegrityError(craIntegrityIssues))
			}

			// H4 (0607 runs): the machine-readable twin drifted to a different ad-hoc schema every run — validate
			// `cra-readiness.json` against the canonical schema (keys + numeric counts) at the same write seam.
			const jsonTwinIssues = checkReadinessJsonTwin(absolutePath, newContent)
			if (jsonTwinIssues.length > 0) {
				await config.services.diffViewProvider.revertChanges()
				await config.services.diffViewProvider.reset()
				return formatResponse.toolError(formatIntegrityError(jsonTwinIssues))
			}

			// CRA completion seatbelt (design/31): record that the readiness report cleared the guarded write seam,
			// so attempt_completion can refuse a CRA run that only inline-dumped the report (2906c). Set AFTER the
			// integrity check so only a real, guard-passing report counts.
			if (looksLikeReadinessReport(absolutePath, newContent)) {
				config.taskState.craReadinessReportWritten = true
				// Remember the run folder so attempt_completion can verify the `cra-readiness.json` twin landed here too
				// (parity, 2906i: a real ESP run shipped the `.md` without its machine-readable twin).
				config.taskState.craReadinessReportDir = path.dirname(absolutePath)
			}

			// Mark the file as edited by Cline
			config.services.fileContextTracker.markFileAsEditedByCline(relPath)

			// Save the changes and get the result
			const { newProblemsMessage, userEdits, autoFormattingEdits, finalContent } =
				await config.services.diffViewProvider.saveChanges()

			config.taskState.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request

			// CRA funnel: detect host-side which milestone this write cleared, keyed on the OUTPUT artifact
			// path (the path itself is NEVER sent — payload is iot_platform only). Fire once per task.
			emitCraMilestoneForWrite(config, absolutePath)

			// Track file edit operation
			await config.services.fileContextTracker.trackFileContext(relPath, "cline_edited")

			// Reset the diff view
			await config.services.diffViewProvider.reset()

			// Handle user edits if any
			if (userEdits) {
				await config.services.fileContextTracker.trackFileContext(relPath, "user_edited")
				await config.callbacks.say(
					"user_feedback_diff",
					JSON.stringify({
						tool: fileExists ? "editedExistingFile" : "newFileCreated",
						path: relPath,
						diff: userEdits,
					}),
				)
				return formatResponse.fileEditWithUserChanges(
					relPath,
					userEdits,
					autoFormattingEdits,
					finalContent,
					newProblemsMessage,
				)
			} else {
				return formatResponse.fileEditWithoutUserChanges(relPath, autoFormattingEdits, finalContent, newProblemsMessage)
			}
		} catch (error) {
			// Reset diff view on error
			await config.services.diffViewProvider.revertChanges()
			await config.services.diffViewProvider.reset()
			throw error
		}
	}

	/**
	 * Shared validation and preparation logic used by both handlePartialBlock and execute methods.
	 * This validates file access permissions, checks if the file exists, and constructs the new content
	 * from either direct content or diff patches. It handles both creation of new files and modifications
	 * to existing ones.
	 *
	 * @param config The task configuration containing services and state
	 * @param block The tool use block containing the operation parameters
	 * @param relPath The relative path to the target file
	 * @param diff Optional diff content for replace operations
	 * @param content Optional direct content for write operations
	 * @param provider Optional provider string for telemetry (used when capturing diff edit failures)
	 * @returns Object containing validated path, file existence status, diff/content, and constructed new content,
	 *          or undefined if validation fails
	 */
	async validateAndPrepareFileOperation(config: TaskConfig, block: ToolUse, relPath: string, diff?: string, content?: string) {
		// Parse workspace hint and resolve path for multi-workspace support
		const pathResult = resolveWorkspacePath(config, relPath, "WriteToFileToolHandler.validateAndPrepareFileOperation")
		const { absolutePath, resolvedPath } =
			typeof pathResult === "string"
				? { 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) {
			// Show error and return early (full original behavior)
			await config.callbacks.say("clineignore_error", resolvedPath)

			// Push tool result and save checkpoint using existing utilities
			const errorResponse = formatResponse.toolError(formatResponse.clineIgnoreError(resolvedPath))
			ToolResultUtils.pushToolResult(
				errorResponse,
				block,
				config.taskState.userMessageContent,
				ToolDisplayUtils.getToolDescription,
				config.coordinator,
				config.taskState.toolUseIdMap,
			)
			if (!config.enableParallelToolCalling) {
				config.taskState.didAlreadyUseTool = true
			}

			return
		}

		// Check if file exists to determine the correct UI message
		let fileExists: boolean
		if (config.services.diffViewProvider.editType !== undefined) {
			fileExists = config.services.diffViewProvider.editType === "modify"
		} else {
			fileExists = await fileExistsAtPath(absolutePath)
			config.services.diffViewProvider.editType = fileExists ? "modify" : "create"
		}

		// Construct newContent from diff
		let newContent: string
		newContent = "" // default to original content if not editing

		if (diff) {
			// Handle replace_in_file with diff construction
			// Apply model-specific fixes (deepseek models tend to use unescaped html entities in diffs)
			diff = applyModelContentFixes(diff, config.api.getModel().id, resolvedPath)

			// open the editor if not done already.  This is to fix diff error when model provides correct search-replace text but Cline throws error
			// because file is not open.
			if (!config.services.diffViewProvider.isEditing) {
				await config.services.diffViewProvider.open(absolutePath, { displayPath: relPath })
			}

			try {
				newContent = await constructNewFileContent(
					diff,
					config.services.diffViewProvider.originalContent || "",
					!block.partial, // Pass the partial flag correctly
				)
			} catch (error) {
				// As we set the didAlreadyUseTool flag when the tool has failed once, we don't want to add the error message to the
				// userMessages array again on each new streaming chunk received.
				if (!config.enableParallelToolCalling && config.taskState.didAlreadyUseTool) {
					return
				}

				// Full original behavior - comprehensive error handling even for partial blocks
				// Removes any existing diff_error messages to avoid duplicates.
				await config.callbacks.removeLastPartialMessageIfExistsWithType("say", "diff_error")
				await config.callbacks.say("diff_error", relPath, undefined, undefined, true)

				// Extract provider information for telemetry
				const { providerId, modelId } = this.getModelInfo(config)

				// Extract error type from error message if possible
				const errorType =
					error instanceof Error && error.message.includes("does not match anything")
						? "search_not_found"
						: "other_diff_error"

				// Add telemetry for diff edit failure
				const isNativeToolCall = block.isNativeToolCall === true
				telemetryService.captureDiffEditFailure(config.ulid, modelId, providerId, errorType, isNativeToolCall)

				// Push tool result with detailed error using existing utilities
				const errorResponse = formatResponse.toolError(
					`${(error as Error)?.message}\n\n` +
						formatResponse.diffError(relPath, config.services.diffViewProvider.originalContent),
				)
				ToolResultUtils.pushToolResult(
					errorResponse,
					block,
					config.taskState.userMessageContent,
					ToolDisplayUtils.getToolDescription,
					config.coordinator,
					config.taskState.toolUseIdMap,
				)
				if (!config.enableParallelToolCalling) {
					config.taskState.didAlreadyUseTool = true
				}

				// Revert changes and reset diff view
				await config.services.diffViewProvider.revertChanges()
				await config.services.diffViewProvider.reset()

				return
			}
		} else if (content) {
			// Handle write_to_file with direct content
			newContent = content

			// pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock
			// markers (deepseek/llama) or extra escape characters (gemini). H2 (0607 runs): wrapper-strip is now
			// fence-aware — the old single-ended startsWith/endsWith strip ATE the legitimate closing ``` of any
			// markdown file ending in a fenced block (the CRA report's posture map — three runs shipped/repaired
			// broken mermaid diagrams because of this seam).
			newContent = stripFullFileCodeFenceWrapper(newContent)

			// Apply model-specific fixes (llama, gemini, and other models may add escape characters)
			newContent = applyModelContentFixes(newContent, config.api.getModel().id, resolvedPath)
		} else {
			// can't happen, since we already checked for content/diff above. but need to do this for type error
			return
		}

		return { relPath, absolutePath, fileExists, diff, content, newContent, workspaceContext }
	}

	/**
	 * True when a write path is degenerate — blank, or resolves to the filesystem root ("/").
	 * Opening the diff editor on such a path fails opaquely ("User closed text editor"); both the
	 * partial and execute paths use this to refuse before opening. Shared so the two stay in sync.
	 */
	private isDegeneratePath(cwd: string, relPath: string): boolean {
		if (!relPath || !relPath.trim()) {
			return true
		}
		if (relPath.trim() === "/") {
			return true
		}
		return path.resolve(cwd, relPath) === "/"
	}

	/** True if writing here would touch the extension install or a bundled sample (forbidden). */
	private isExtensionOwnedPath(absolutePath: string): boolean {
		let extensionRoot: string | undefined
		try {
			extensionRoot = HostProvider.get().extensionFsPath
		} catch {
			extensionRoot = undefined
		}
		return isUnderExtensionOrSample(absolutePath, extensionRoot)
	}

	private getModelInfo(config: TaskConfig) {
		// Extract provider information for telemetry
		const apiConfig = config.services.stateManager.getApiConfiguration()
		const currentMode = config.services.stateManager.getGlobalSettingsKey("mode")
		const providerId = (currentMode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider) as string
		const modelId = config.api.getModel().id
		return { providerId, modelId }
	}
}
