import path from "path" import os from "os" import * as vscode from "vscode" import pWaitFor from "p-wait-for" import delay from "delay" import type { ExperimentId } from "@roo-code/types" import { formatLanguage } from "../../shared/language" import { defaultModeSlug, getFullModeDetails } from "../../shared/modes" import { getApiMetrics } from "../../shared/getApiMetrics" import { listFiles } from "../../services/glob/list-files" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { Terminal } from "../../integrations/terminal/Terminal" import { arePathsEqual } from "../../utils/path" import { formatResponse } from "../prompts/responses" import { getGitStatus } from "../../utils/git" import { Task } from "../task/Task" import { formatReminderSection } from "./reminder" export async function getEnvironmentDetails(cline: Task, includeFileDetails: boolean = false) { let details = "" const clineProvider = cline.providerRef.deref() const state = await clineProvider?.getState() const { maxWorkspaceFiles = 200 } = state ?? {} // It could be useful for cline to know if the user went from one or no // file to another between messages, so we always include this context. const visibleFilePaths = vscode.window.visibleTextEditors ?.map((editor) => editor.document?.uri?.fsPath) .filter(Boolean) .map((absolutePath) => path.relative(cline.cwd, absolutePath)) .slice(0, maxWorkspaceFiles) // Filter paths through rooIgnoreController const allowedVisibleFiles = cline.rooIgnoreController ? cline.rooIgnoreController.filterPaths(visibleFilePaths) : visibleFilePaths.map((p) => p.toPosix()).join("\n") if (allowedVisibleFiles) { details += "\n\n# VSCode Visible Files" details += `\n${allowedVisibleFiles}` } const { maxOpenTabsContext } = state ?? {} const maxTabs = maxOpenTabsContext ?? 20 const openTabPaths = vscode.window.tabGroups.all .flatMap((group) => group.tabs) .filter((tab) => tab.input instanceof vscode.TabInputText) .map((tab) => (tab.input as vscode.TabInputText).uri.fsPath) .filter(Boolean) .map((absolutePath) => path.relative(cline.cwd, absolutePath).toPosix()) .slice(0, maxTabs) // Filter paths through rooIgnoreController const allowedOpenTabs = cline.rooIgnoreController ? cline.rooIgnoreController.filterPaths(openTabPaths) : openTabPaths.map((p) => p.toPosix()).join("\n") if (allowedOpenTabs) { details += "\n\n# VSCode Open Tabs" details += `\n${allowedOpenTabs}` } // Get task-specific and background terminals. const busyTerminals = [ ...TerminalRegistry.getTerminals(true, cline.taskId), ...TerminalRegistry.getBackgroundTerminals(true), ] const inactiveTerminals = [ ...TerminalRegistry.getTerminals(false, cline.taskId), ...TerminalRegistry.getBackgroundTerminals(false), ] if (busyTerminals.length > 0) { if (cline.didEditFile) { await delay(300) // Delay after saving file to let terminals catch up. } // Wait for terminals to cool down. await pWaitFor(() => busyTerminals.every((t) => !TerminalRegistry.isProcessHot(t.id)), { interval: 100, timeout: 5_000, }).catch(() => {}) } // Reset, this lets us know when to wait for saved files to update terminals. cline.didEditFile = false // Waiting for updated diagnostics lets terminal output be the most // up-to-date possible. let terminalDetails = "" if (busyTerminals.length > 0) { // Terminals are cool, let's retrieve their output. terminalDetails += "\n\n# Actively Running Terminals" for (const busyTerminal of busyTerminals) { const cwd = busyTerminal.getCurrentWorkingDirectory() terminalDetails += `\n## Terminal ${busyTerminal.id} (Active)` terminalDetails += `\n### Working Directory: \`${cwd}\`` terminalDetails += `\n### Original command: \`${busyTerminal.getLastCommand()}\`` let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id) if (newOutput) { newOutput = Terminal.compressTerminalOutput(newOutput) terminalDetails += `\n### New Output\n${newOutput}` } } } // First check if any inactive terminals in this task have completed // processes with output. const terminalsWithOutput = inactiveTerminals.filter((terminal) => { const completedProcesses = terminal.getProcessesWithOutput() return completedProcesses.length > 0 }) // Only add the header if there are terminals with output. if (terminalsWithOutput.length > 0) { terminalDetails += "\n\n# Inactive Terminals with Completed Process Output" // Process each terminal with output. for (const inactiveTerminal of terminalsWithOutput) { let terminalOutputs: string[] = [] // Get output from completed processes queue. const completedProcesses = inactiveTerminal.getProcessesWithOutput() for (const process of completedProcesses) { let output = process.getUnretrievedOutput() if (output) { output = Terminal.compressTerminalOutput(output) terminalOutputs.push(`Command: \`${process.command}\`\n${output}`) } } // Clean the queue after retrieving output. inactiveTerminal.cleanCompletedProcessQueue() // Add this terminal's outputs to the details. if (terminalOutputs.length > 0) { const cwd = inactiveTerminal.getCurrentWorkingDirectory() terminalDetails += `\n## Terminal ${inactiveTerminal.id} (Inactive)` terminalDetails += `\n### Working Directory: \`${cwd}\`` terminalOutputs.forEach((output) => { terminalDetails += `\n### New Output\n${output}` }) } } } // console.log(`[Task#getEnvironmentDetails] terminalDetails: ${terminalDetails}`) // Add recently modified files section. const recentlyModifiedFiles = cline.fileContextTracker.getAndClearRecentlyModifiedFiles() if (recentlyModifiedFiles.length > 0) { details += "\n\n# Recently Modified Files\nThese files have been modified since you last accessed them (file was just edited so you may need to re-read it before editing):" for (const filePath of recentlyModifiedFiles) { details += `\n${filePath}` } } if (terminalDetails) { details += terminalDetails } // Get settings for time and cost display const { includeCurrentTime = true, includeCurrentCost = true, maxGitStatusFiles = 0 } = state ?? {} // Add current time information with timezone (if enabled). if (includeCurrentTime) { const now = new Date() const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset)) const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60)) const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}` details += `\n\n# Current Time\nCurrent time in ISO 8601 UTC format: ${now.toISOString()}\nUser time zone: ${timeZone}, UTC${timeZoneOffsetStr}` } // Add git status information (if enabled with maxGitStatusFiles > 0). if (maxGitStatusFiles > 0) { const gitStatus = await getGitStatus(cline.cwd, maxGitStatusFiles) if (gitStatus) { details += `\n\n# Git Status\n${gitStatus}` } } // Add context tokens information (if enabled). if (includeCurrentCost) { const { totalCost } = getApiMetrics(cline.clineMessages) details += `\n\n# Current Cost\n${totalCost !== null ? `$${totalCost.toFixed(2)}` : "(Not available)"}` } const { id: modelId } = cline.api.getModel() // Add current mode and any mode-specific warnings. const { mode, customModes, customModePrompts, experiments = {} as Record, customInstructions: globalCustomInstructions, language, } = state ?? {} const currentMode = mode ?? defaultModeSlug const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, { cwd: cline.cwd, globalCustomInstructions, language: language ?? formatLanguage(vscode.env.language), }) details += `\n\n# Current Mode\n` details += `${currentMode}\n` details += `${modeDetails.name}\n` details += `${modelId}\n` if (includeFileDetails) { details += `\n\n# Current Workspace Directory (${cline.cwd.toPosix()}) Files\n` const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop")) if (isDesktop) { // Don't want to immediately access desktop since it would show // permission popup. details += "(Desktop files not shown automatically. Use list_files to explore if needed.)" } else { const maxFiles = maxWorkspaceFiles ?? 200 // Early return for limit of 0 if (maxFiles === 0) { details += "(Workspace files context disabled. Use list_files to explore if needed.)" } else { const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles) const { showRooIgnoredFiles = false } = state ?? {} const result = formatResponse.formatFilesList( cline.cwd, files, didHitLimit, cline.rooIgnoreController, showRooIgnoredFiles, ) details += result } } } const todoListEnabled = state && typeof state.apiConfiguration?.todoListEnabled === "boolean" ? state.apiConfiguration.todoListEnabled : true const reminderSection = todoListEnabled ? formatReminderSection(cline.todoList) : "" return `\n${details.trim()}\n${reminderSection}\n` }