Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/pretty-peaches-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Split Cline.getEnvironmentDetails out into a standalone function
265 changes: 5 additions & 260 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,11 @@ import {
import { getApiMetrics } from "../shared/getApiMetrics"
import { HistoryItem } from "../shared/HistoryItem"
import { ClineAskResponse } from "../shared/WebviewMessage"
import { defaultModeSlug, getModeBySlug, getFullModeDetails, isToolAllowedForMode } from "../shared/modes"
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
import { formatLanguage } from "../shared/language"
import { defaultModeSlug, getModeBySlug } from "../shared/modes"
import { ToolParamName, ToolResponse, DiffStrategy } from "../shared/tools"

// services
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { listFiles } from "../services/glob/list-files"
import { BrowserSession } from "../services/browser/BrowserSession"
import { McpHub } from "../services/mcp/McpHub"
import { McpServerManager } from "../services/mcp/McpServerManager"
Expand All @@ -51,12 +48,11 @@ import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../servi
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
import { RooTerminalProcess } from "../integrations/terminal/types"
import { Terminal } from "../integrations/terminal/Terminal"
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"

// utils
import { calculateApiCostAnthropic } from "../utils/cost"
import { arePathsEqual, getWorkspacePath } from "../utils/path"
import { getWorkspacePath } from "../utils/path"

// tools
import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
Expand Down Expand Up @@ -91,6 +87,7 @@ import { ClineProvider } from "./webview/ClineProvider"
import { validateToolUse } from "./mode-validator"
import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace"
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence"
import { getEnvironmentDetails } from "./environment/getEnvironmentDetails"

type UserContent = Array<Anthropic.Messages.ContentBlockParam>

Expand Down Expand Up @@ -145,7 +142,7 @@ export class Cline extends EventEmitter<ClineEvents> {
private promptCacheKey: string

rooIgnoreController?: RooIgnoreController
private fileContextTracker: FileContextTracker
fileContextTracker: FileContextTracker
private urlContentFetcher: UrlContentFetcher
browserSession: BrowserSession
didEditFile: boolean = false
Expand Down Expand Up @@ -1021,7 +1018,7 @@ export class Cline extends EventEmitter<ClineEvents> {
)

const parsedUserContent = await this.parseUserContent(userContent)
const environmentDetails = await this.getEnvironmentDetails(includeFileDetails)
const environmentDetails = await getEnvironmentDetails(this, includeFileDetails)

// Add environment details as its own text block, separate from tool
// results.
Expand Down Expand Up @@ -2077,258 +2074,6 @@ export class Cline extends EventEmitter<ClineEvents> {
)
}

// Environment

public async getEnvironmentDetails(includeFileDetails: boolean = false) {
let details = ""

const { terminalOutputLineLimit = 500, maxWorkspaceFiles = 200 } =
(await this.providerRef.deref()?.getState()) ?? {}

// 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
details += "\n\n# VSCode Visible Files"

const visibleFilePaths = vscode.window.visibleTextEditors
?.map((editor) => editor.document?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(this.cwd, absolutePath))
.slice(0, maxWorkspaceFiles)

// Filter paths through rooIgnoreController
const allowedVisibleFiles = this.rooIgnoreController
? this.rooIgnoreController.filterPaths(visibleFilePaths)
: visibleFilePaths.map((p) => p.toPosix()).join("\n")

if (allowedVisibleFiles) {
details += `\n${allowedVisibleFiles}`
} else {
details += "\n(No visible files)"
}

details += "\n\n# VSCode Open Tabs"
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
const maxTabs = maxOpenTabsContext ?? 20
const openTabPaths = vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
.filter(Boolean)
.map((absolutePath) => path.relative(this.cwd, absolutePath).toPosix())
.slice(0, maxTabs)

// Filter paths through rooIgnoreController
const allowedOpenTabs = this.rooIgnoreController
? this.rooIgnoreController.filterPaths(openTabPaths)
: openTabPaths.map((p) => p.toPosix()).join("\n")

if (allowedOpenTabs) {
details += `\n${allowedOpenTabs}`
} else {
details += "\n(No open tabs)"
}

// Get task-specific and background terminals.
const busyTerminals = [
...TerminalRegistry.getTerminals(true, this.taskId),
...TerminalRegistry.getBackgroundTerminals(true),
]

const inactiveTerminals = [
...TerminalRegistry.getTerminals(false, this.taskId),
...TerminalRegistry.getBackgroundTerminals(false),
]

if (busyTerminals.length > 0) {
if (this.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.
this.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) {
terminalDetails += `\n## Original command: \`${busyTerminal.getLastCommand()}\``
let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id)

if (newOutput) {
newOutput = Terminal.compressTerminalOutput(newOutput, terminalOutputLineLimit)
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, terminalOutputLineLimit)
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) {
terminalDetails += `\n## Terminal ${inactiveTerminal.id}`
terminalOutputs.forEach((output) => {
terminalDetails += `\n### New Output\n${output}`
})
}
}
}

// console.log(`[Cline#getEnvironmentDetails] terminalDetails: ${terminalDetails}`)

// Add recently modified files section.
const recentlyModifiedFiles = this.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
}

// Add current time information with timezone.
const now = new Date()

const formatter = new Intl.DateTimeFormat(undefined, {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
})

const timeZone = formatter.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\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`

// Add context tokens information.
const { contextTokens, totalCost } = getApiMetrics(this.clineMessages)
const modelInfo = this.api.getModel().info
const contextWindow = modelInfo.contextWindow

const contextPercentage =
contextTokens && contextWindow ? Math.round((contextTokens / contextWindow) * 100) : undefined

details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}`
details += `\n\n# Current Cost\n${totalCost !== null ? `$${totalCost.toFixed(2)}` : "(Not available)"}`

// Add current mode and any mode-specific warnings.
const {
mode,
customModes,
apiModelId,
customModePrompts,
experiments = {} as Record<ExperimentId, boolean>,
customInstructions: globalCustomInstructions,
language,
} = (await this.providerRef.deref()?.getState()) ?? {}

const currentMode = mode ?? defaultModeSlug

const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
cwd: this.cwd,
globalCustomInstructions,
language: language ?? formatLanguage(vscode.env.language),
})

details += `\n\n# Current Mode\n`
details += `<slug>${currentMode}</slug>\n`
details += `<name>${modeDetails.name}</name>\n`
details += `<model>${apiModelId}</model>\n`

if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) {
details += `<role>${modeDetails.roleDefinition}</role>\n`

if (modeDetails.customInstructions) {
details += `<custom_instructions>${modeDetails.customInstructions}</custom_instructions>\n`
}
}

// Add warning if not in code mode.
if (
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], { apply_diff: this.diffEnabled }) &&
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: this.diffEnabled })
) {
const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode
const defaultModeName = getModeBySlug(defaultModeSlug, customModes)?.name ?? defaultModeSlug
details += `\n\nNOTE: You are currently in '${currentModeName}' mode, which does not allow write operations. To write files, the user will need to switch to a mode that supports file writing, such as '${defaultModeName}' mode.`
}

if (includeFileDetails) {
details += `\n\n# Current Workspace Directory (${this.cwd.toPosix()}) Files\n`
const isDesktop = arePathsEqual(this.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
const [files, didHitLimit] = await listFiles(this.cwd, true, maxFiles)
const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {}

const result = formatResponse.formatFilesList(
this.cwd,
files,
didHitLimit,
this.rooIgnoreController,
showRooIgnoredFiles,
)

details += result
}
}

return `<environment_details>\n${details.trim()}\n</environment_details>`
}

// Checkpoints

private getCheckpointService() {
Expand Down
Loading
Loading