Skip to content

Commit 8b98f61

Browse files
committed
Move environment details to a separate module, add tests
1 parent 49382b7 commit 8b98f61

File tree

5 files changed

+596
-348
lines changed

5 files changed

+596
-348
lines changed

src/core/Cline.ts

Lines changed: 5 additions & 258 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,11 @@ import {
3333
import { getApiMetrics } from "../shared/getApiMetrics"
3434
import { HistoryItem } from "../shared/HistoryItem"
3535
import { ClineAskResponse } from "../shared/WebviewMessage"
36-
import { defaultModeSlug, getModeBySlug, getFullModeDetails, isToolAllowedForMode } from "../shared/modes"
37-
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../shared/experiments"
38-
import { formatLanguage } from "../shared/language"
36+
import { defaultModeSlug, getModeBySlug } from "../shared/modes"
3937
import { ToolParamName, ToolResponse, DiffStrategy } from "../shared/tools"
4038

4139
// services
4240
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
43-
import { listFiles } from "../services/glob/list-files"
4441
import { BrowserSession } from "../services/browser/BrowserSession"
4542
import { McpHub } from "../services/mcp/McpHub"
4643
import { McpServerManager } from "../services/mcp/McpServerManager"
@@ -51,12 +48,11 @@ import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../servi
5148
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
5249
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
5350
import { RooTerminalProcess } from "../integrations/terminal/types"
54-
import { Terminal } from "../integrations/terminal/Terminal"
5551
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
5652

5753
// utils
5854
import { calculateApiCostAnthropic } from "../utils/cost"
59-
import { arePathsEqual, getWorkspacePath } from "../utils/path"
55+
import { getWorkspacePath } from "../utils/path"
6056

6157
// tools
6258
import { fetchInstructionsTool } from "./tools/fetchInstructionsTool"
@@ -91,6 +87,7 @@ import { ClineProvider } from "./webview/ClineProvider"
9187
import { validateToolUse } from "./mode-validator"
9288
import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace"
9389
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence"
90+
import { getEnvironmentDetails } from "./environment/getEnvironmentDetails"
9491

9592
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
9693

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

147144
rooIgnoreController?: RooIgnoreController
148-
private fileContextTracker: FileContextTracker
145+
fileContextTracker: FileContextTracker
149146
private urlContentFetcher: UrlContentFetcher
150147
browserSession: BrowserSession
151148
didEditFile: boolean = false
@@ -1995,261 +1992,11 @@ export class Cline extends EventEmitter<ClineEvents> {
19951992
}),
19961993
)
19971994

1998-
const environmentDetails = await this.getEnvironmentDetails(includeFileDetails)
1995+
const environmentDetails = await getEnvironmentDetails(this, includeFileDetails)
19991996

20001997
return [parsedUserContent, environmentDetails]
20011998
}
20021999

2003-
async getEnvironmentDetails(includeFileDetails: boolean = false) {
2004-
let details = ""
2005-
2006-
const { terminalOutputLineLimit = 500, maxWorkspaceFiles = 200 } =
2007-
(await this.providerRef.deref()?.getState()) ?? {}
2008-
2009-
// 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
2010-
details += "\n\n# VSCode Visible Files"
2011-
2012-
const visibleFilePaths = vscode.window.visibleTextEditors
2013-
?.map((editor) => editor.document?.uri?.fsPath)
2014-
.filter(Boolean)
2015-
.map((absolutePath) => path.relative(this.cwd, absolutePath))
2016-
.slice(0, maxWorkspaceFiles)
2017-
2018-
// Filter paths through rooIgnoreController
2019-
const allowedVisibleFiles = this.rooIgnoreController
2020-
? this.rooIgnoreController.filterPaths(visibleFilePaths)
2021-
: visibleFilePaths.map((p) => p.toPosix()).join("\n")
2022-
2023-
if (allowedVisibleFiles) {
2024-
details += `\n${allowedVisibleFiles}`
2025-
} else {
2026-
details += "\n(No visible files)"
2027-
}
2028-
2029-
details += "\n\n# VSCode Open Tabs"
2030-
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
2031-
const maxTabs = maxOpenTabsContext ?? 20
2032-
const openTabPaths = vscode.window.tabGroups.all
2033-
.flatMap((group) => group.tabs)
2034-
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
2035-
.filter(Boolean)
2036-
.map((absolutePath) => path.relative(this.cwd, absolutePath).toPosix())
2037-
.slice(0, maxTabs)
2038-
2039-
// Filter paths through rooIgnoreController
2040-
const allowedOpenTabs = this.rooIgnoreController
2041-
? this.rooIgnoreController.filterPaths(openTabPaths)
2042-
: openTabPaths.map((p) => p.toPosix()).join("\n")
2043-
2044-
if (allowedOpenTabs) {
2045-
details += `\n${allowedOpenTabs}`
2046-
} else {
2047-
details += "\n(No open tabs)"
2048-
}
2049-
2050-
// Get task-specific and background terminals.
2051-
const busyTerminals = [
2052-
...TerminalRegistry.getTerminals(true, this.taskId),
2053-
...TerminalRegistry.getBackgroundTerminals(true),
2054-
]
2055-
2056-
const inactiveTerminals = [
2057-
...TerminalRegistry.getTerminals(false, this.taskId),
2058-
...TerminalRegistry.getBackgroundTerminals(false),
2059-
]
2060-
2061-
if (busyTerminals.length > 0) {
2062-
if (this.didEditFile) {
2063-
await delay(300) // Delay after saving file to let terminals catch up.
2064-
}
2065-
2066-
// Wait for terminals to cool down.
2067-
await pWaitFor(() => busyTerminals.every((t) => !TerminalRegistry.isProcessHot(t.id)), {
2068-
interval: 100,
2069-
timeout: 5_000,
2070-
}).catch(() => {})
2071-
}
2072-
2073-
// Reset, this lets us know when to wait for saved files to update terminals.
2074-
this.didEditFile = false
2075-
2076-
// Waiting for updated diagnostics lets terminal output be the most
2077-
// up-to-date possible.
2078-
let terminalDetails = ""
2079-
2080-
if (busyTerminals.length > 0) {
2081-
// Terminals are cool, let's retrieve their output.
2082-
terminalDetails += "\n\n# Actively Running Terminals"
2083-
2084-
for (const busyTerminal of busyTerminals) {
2085-
terminalDetails += `\n## Original command: \`${busyTerminal.getLastCommand()}\``
2086-
let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id)
2087-
2088-
if (newOutput) {
2089-
newOutput = Terminal.compressTerminalOutput(newOutput, terminalOutputLineLimit)
2090-
terminalDetails += `\n### New Output\n${newOutput}`
2091-
}
2092-
}
2093-
}
2094-
2095-
// First check if any inactive terminals in this task have completed
2096-
// processes with output.
2097-
const terminalsWithOutput = inactiveTerminals.filter((terminal) => {
2098-
const completedProcesses = terminal.getProcessesWithOutput()
2099-
return completedProcesses.length > 0
2100-
})
2101-
2102-
// Only add the header if there are terminals with output.
2103-
if (terminalsWithOutput.length > 0) {
2104-
terminalDetails += "\n\n# Inactive Terminals with Completed Process Output"
2105-
2106-
// Process each terminal with output.
2107-
for (const inactiveTerminal of terminalsWithOutput) {
2108-
let terminalOutputs: string[] = []
2109-
2110-
// Get output from completed processes queue.
2111-
const completedProcesses = inactiveTerminal.getProcessesWithOutput()
2112-
2113-
for (const process of completedProcesses) {
2114-
let output = process.getUnretrievedOutput()
2115-
2116-
if (output) {
2117-
output = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
2118-
terminalOutputs.push(`Command: \`${process.command}\`\n${output}`)
2119-
}
2120-
}
2121-
2122-
// Clean the queue after retrieving output.
2123-
inactiveTerminal.cleanCompletedProcessQueue()
2124-
2125-
// Add this terminal's outputs to the details.
2126-
if (terminalOutputs.length > 0) {
2127-
terminalDetails += `\n## Terminal ${inactiveTerminal.id}`
2128-
terminalOutputs.forEach((output) => {
2129-
terminalDetails += `\n### New Output\n${output}`
2130-
})
2131-
}
2132-
}
2133-
}
2134-
2135-
// console.log(`[Cline#getEnvironmentDetails] terminalDetails: ${terminalDetails}`)
2136-
2137-
// Add recently modified files section.
2138-
const recentlyModifiedFiles = this.fileContextTracker.getAndClearRecentlyModifiedFiles()
2139-
2140-
if (recentlyModifiedFiles.length > 0) {
2141-
details +=
2142-
"\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):"
2143-
for (const filePath of recentlyModifiedFiles) {
2144-
details += `\n${filePath}`
2145-
}
2146-
}
2147-
2148-
if (terminalDetails) {
2149-
details += terminalDetails
2150-
}
2151-
2152-
// Add current time information with timezone.
2153-
const now = new Date()
2154-
2155-
const formatter = new Intl.DateTimeFormat(undefined, {
2156-
year: "numeric",
2157-
month: "numeric",
2158-
day: "numeric",
2159-
hour: "numeric",
2160-
minute: "numeric",
2161-
second: "numeric",
2162-
hour12: true,
2163-
})
2164-
2165-
const timeZone = formatter.resolvedOptions().timeZone
2166-
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
2167-
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset))
2168-
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60))
2169-
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`
2170-
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
2171-
2172-
// Add context tokens information.
2173-
const { contextTokens, totalCost } = getApiMetrics(this.clineMessages)
2174-
const modelInfo = this.api.getModel().info
2175-
const contextWindow = modelInfo.contextWindow
2176-
2177-
const contextPercentage =
2178-
contextTokens && contextWindow ? Math.round((contextTokens / contextWindow) * 100) : undefined
2179-
2180-
details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}`
2181-
details += `\n\n# Current Cost\n${totalCost !== null ? `$${totalCost.toFixed(2)}` : "(Not available)"}`
2182-
2183-
// Add current mode and any mode-specific warnings.
2184-
const {
2185-
mode,
2186-
customModes,
2187-
apiModelId,
2188-
customModePrompts,
2189-
experiments = {} as Record<ExperimentId, boolean>,
2190-
customInstructions: globalCustomInstructions,
2191-
language,
2192-
} = (await this.providerRef.deref()?.getState()) ?? {}
2193-
2194-
const currentMode = mode ?? defaultModeSlug
2195-
2196-
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
2197-
cwd: this.cwd,
2198-
globalCustomInstructions,
2199-
language: language ?? formatLanguage(vscode.env.language),
2200-
})
2201-
2202-
details += `\n\n# Current Mode\n`
2203-
details += `<slug>${currentMode}</slug>\n`
2204-
details += `<name>${modeDetails.name}</name>\n`
2205-
details += `<model>${apiModelId}</model>\n`
2206-
2207-
if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) {
2208-
details += `<role>${modeDetails.roleDefinition}</role>\n`
2209-
2210-
if (modeDetails.customInstructions) {
2211-
details += `<custom_instructions>${modeDetails.customInstructions}</custom_instructions>\n`
2212-
}
2213-
}
2214-
2215-
// Add warning if not in code mode.
2216-
if (
2217-
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], { apply_diff: this.diffEnabled }) &&
2218-
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: this.diffEnabled })
2219-
) {
2220-
const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode
2221-
const defaultModeName = getModeBySlug(defaultModeSlug, customModes)?.name ?? defaultModeSlug
2222-
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.`
2223-
}
2224-
2225-
if (includeFileDetails) {
2226-
details += `\n\n# Current Workspace Directory (${this.cwd.toPosix()}) Files\n`
2227-
const isDesktop = arePathsEqual(this.cwd, path.join(os.homedir(), "Desktop"))
2228-
2229-
if (isDesktop) {
2230-
// Don't want to immediately access desktop since it would show
2231-
// permission popup.
2232-
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
2233-
} else {
2234-
const maxFiles = maxWorkspaceFiles ?? 200
2235-
const [files, didHitLimit] = await listFiles(this.cwd, true, maxFiles)
2236-
const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {}
2237-
2238-
const result = formatResponse.formatFilesList(
2239-
this.cwd,
2240-
files,
2241-
didHitLimit,
2242-
this.rooIgnoreController,
2243-
showRooIgnoredFiles,
2244-
)
2245-
2246-
details += result
2247-
}
2248-
}
2249-
2250-
return `<environment_details>\n${details.trim()}\n</environment_details>`
2251-
}
2252-
22532000
// Checkpoints
22542001

22552002
private getCheckpointService() {

0 commit comments

Comments
 (0)