mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Move environment details to a separate module, add tests (#3078)
This commit is contained in:
parent
305185cd88
commit
78beb718c8
7 changed files with 606 additions and 358 deletions
5
.changeset/pretty-peaches-bake.md
Normal file
5
.changeset/pretty-peaches-bake.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Split Cline.getEnvironmentDetails out into a standalone function
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ import { ApiConfiguration, ModelInfo } from "../../shared/api"
|
|||
import { ApiStreamChunk } from "../../api/transform/stream"
|
||||
import { ContextProxy } from "../config/ContextProxy"
|
||||
|
||||
jest.mock("../environment/getEnvironmentDetails", () => ({
|
||||
getEnvironmentDetails: jest.fn().mockResolvedValue(""),
|
||||
}))
|
||||
|
||||
jest.mock("execa", () => ({
|
||||
execa: jest.fn(),
|
||||
}))
|
||||
|
|
@ -316,90 +320,7 @@ describe("Cline", () => {
|
|||
})
|
||||
|
||||
describe("getEnvironmentDetails", () => {
|
||||
let originalDate: DateConstructor
|
||||
let mockDate: Date
|
||||
|
||||
beforeEach(() => {
|
||||
originalDate = global.Date
|
||||
const fixedTime = new Date("2024-01-01T12:00:00Z")
|
||||
mockDate = new Date(fixedTime)
|
||||
mockDate.getTimezoneOffset = jest.fn().mockReturnValue(420) // UTC-7
|
||||
|
||||
class MockDate extends Date {
|
||||
constructor() {
|
||||
super()
|
||||
return mockDate
|
||||
}
|
||||
static override now() {
|
||||
return mockDate.getTime()
|
||||
}
|
||||
}
|
||||
|
||||
global.Date = MockDate as DateConstructor
|
||||
|
||||
// Create a proper mock of Intl.DateTimeFormat
|
||||
const mockDateTimeFormat = {
|
||||
resolvedOptions: () => ({
|
||||
timeZone: "America/Los_Angeles",
|
||||
}),
|
||||
format: () => "1/1/2024, 5:00:00 AM",
|
||||
}
|
||||
|
||||
const MockDateTimeFormat = function (this: any) {
|
||||
return mockDateTimeFormat
|
||||
} as any
|
||||
|
||||
MockDateTimeFormat.prototype = mockDateTimeFormat
|
||||
MockDateTimeFormat.supportedLocalesOf = jest.fn().mockReturnValue(["en-US"])
|
||||
|
||||
global.Intl.DateTimeFormat = MockDateTimeFormat
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
global.Date = originalDate
|
||||
})
|
||||
|
||||
it("should include timezone information in environment details", async () => {
|
||||
const cline = new Cline({
|
||||
provider: mockProvider,
|
||||
apiConfiguration: mockApiConfig,
|
||||
task: "test task",
|
||||
startTask: false,
|
||||
})
|
||||
|
||||
const details = await cline["getEnvironmentDetails"](false)
|
||||
|
||||
// Verify timezone information is present and formatted correctly.
|
||||
expect(details).toContain("America/Los_Angeles")
|
||||
expect(details).toMatch(/UTC-7:00/) // Fixed offset for America/Los_Angeles.
|
||||
expect(details).toContain("# Current Time")
|
||||
expect(details).toMatch(/1\/1\/2024.*5:00:00 AM.*\(America\/Los_Angeles, UTC-7:00\)/) // Full time string format.
|
||||
})
|
||||
|
||||
describe("API conversation handling", () => {
|
||||
/**
|
||||
* Mock environment details retrieval to avoid filesystem access in tests
|
||||
*
|
||||
* This setup:
|
||||
* 1. Prevents file listing operations that might cause test instability
|
||||
* 2. Preserves test-specific mocks when they exist (via _mockGetEnvironmentDetails)
|
||||
* 3. Provides a stable, empty environment by default
|
||||
*/
|
||||
beforeEach(() => {
|
||||
// Mock the method with a stable implementation
|
||||
jest.spyOn(Cline.prototype, "getEnvironmentDetails").mockImplementation(
|
||||
// Use 'any' type to allow for dynamic test properties
|
||||
async function (this: any, _verbose: boolean = false): Promise<string> {
|
||||
// Use test-specific mock if available
|
||||
if (this._mockGetEnvironmentDetails) {
|
||||
return this._mockGetEnvironmentDetails()
|
||||
}
|
||||
// Default to empty environment details for stability
|
||||
return ""
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
it("should clean conversation history before sending to API", async () => {
|
||||
// Cline.create will now use our mocked getEnvironmentDetails
|
||||
const [cline, task] = Cline.create({
|
||||
|
|
@ -420,12 +341,6 @@ describe("Cline", () => {
|
|||
const cleanMessageSpy = jest.fn().mockReturnValue(mockStreamForClean)
|
||||
jest.spyOn(cline.api, "createMessage").mockImplementation(cleanMessageSpy)
|
||||
|
||||
// Mock getEnvironmentDetails to return empty details.
|
||||
jest.spyOn(cline as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||
|
||||
// Mock loadContext to return unmodified content.
|
||||
jest.spyOn(cline as any, "parseUserContent").mockImplementation(async (content) => [content, ""])
|
||||
|
||||
// Add test message to conversation history.
|
||||
cline.apiConversationHistory = [
|
||||
{
|
||||
|
|
@ -574,12 +489,6 @@ describe("Cline", () => {
|
|||
configurable: true,
|
||||
})
|
||||
|
||||
// Mock environment details and context loading
|
||||
jest.spyOn(clineWithImages as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||
jest.spyOn(clineWithoutImages as any, "getEnvironmentDetails").mockResolvedValue("")
|
||||
jest.spyOn(clineWithImages as any, "parseUserContent").mockImplementation(async (content) => content)
|
||||
jest.spyOn(clineWithoutImages as any, "parseUserContent").mockImplementation(async (content) => content)
|
||||
|
||||
// Set up mock streams
|
||||
const mockStreamWithImages = (async function* () {
|
||||
yield { type: "text", text: "test response" }
|
||||
|
|
|
|||
316
src/core/environment/__tests__/getEnvironmentDetails.test.ts
Normal file
316
src/core/environment/__tests__/getEnvironmentDetails.test.ts
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
// npx jest src/core/environment/__tests__/getEnvironmentDetails.test.ts
|
||||
|
||||
import pWaitFor from "p-wait-for"
|
||||
import delay from "delay"
|
||||
|
||||
import { getEnvironmentDetails } from "../getEnvironmentDetails"
|
||||
import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments"
|
||||
import { defaultModeSlug, getFullModeDetails, getModeBySlug, isToolAllowedForMode } 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 { FileContextTracker } from "../../context-tracking/FileContextTracker"
|
||||
import { ApiHandler } from "../../../api/index"
|
||||
import { ClineProvider } from "../../webview/ClineProvider"
|
||||
import { RooIgnoreController } from "../../ignore/RooIgnoreController"
|
||||
import { formatResponse } from "../../prompts/responses"
|
||||
import { Cline } from "../../Cline"
|
||||
|
||||
jest.mock("vscode", () => ({
|
||||
window: {
|
||||
tabGroups: { all: [], onDidChangeTabs: jest.fn() },
|
||||
visibleTextEditors: [],
|
||||
},
|
||||
env: {
|
||||
language: "en-US",
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock("p-wait-for")
|
||||
|
||||
jest.mock("delay")
|
||||
|
||||
jest.mock("execa", () => ({
|
||||
execa: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("../../../shared/experiments")
|
||||
jest.mock("../../../shared/modes")
|
||||
jest.mock("../../../shared/getApiMetrics")
|
||||
jest.mock("../../../services/glob/list-files")
|
||||
jest.mock("../../../integrations/terminal/TerminalRegistry")
|
||||
jest.mock("../../../integrations/terminal/Terminal")
|
||||
jest.mock("../../../utils/path")
|
||||
jest.mock("../../prompts/responses")
|
||||
|
||||
describe("getEnvironmentDetails", () => {
|
||||
const mockCwd = "/test/path"
|
||||
const mockTaskId = "test-task-id"
|
||||
|
||||
type MockTerminal = {
|
||||
id: string
|
||||
getLastCommand: jest.Mock
|
||||
getProcessesWithOutput: jest.Mock
|
||||
cleanCompletedProcessQueue?: jest.Mock
|
||||
}
|
||||
|
||||
let mockCline: Partial<Cline>
|
||||
let mockProvider: any
|
||||
let mockState: any
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
mockState = {
|
||||
terminalOutputLineLimit: 100,
|
||||
maxWorkspaceFiles: 50,
|
||||
maxOpenTabsContext: 10,
|
||||
mode: "code",
|
||||
customModes: [],
|
||||
apiModelId: "test-model",
|
||||
experiments: {},
|
||||
customInstructions: "test instructions",
|
||||
language: "en",
|
||||
showRooIgnoredFiles: true,
|
||||
}
|
||||
|
||||
mockProvider = {
|
||||
getState: jest.fn().mockResolvedValue(mockState),
|
||||
}
|
||||
|
||||
mockCline = {
|
||||
cwd: mockCwd,
|
||||
taskId: mockTaskId,
|
||||
didEditFile: false,
|
||||
fileContextTracker: {
|
||||
getAndClearRecentlyModifiedFiles: jest.fn().mockReturnValue([]),
|
||||
} as unknown as FileContextTracker,
|
||||
rooIgnoreController: {
|
||||
filterPaths: jest.fn((paths: string[]) => paths.join("\n")),
|
||||
cwd: mockCwd,
|
||||
ignoreInstance: {},
|
||||
disposables: [],
|
||||
rooIgnoreContent: "",
|
||||
isPathIgnored: jest.fn(),
|
||||
getIgnoreContent: jest.fn(),
|
||||
updateIgnoreContent: jest.fn(),
|
||||
addToIgnore: jest.fn(),
|
||||
removeFromIgnore: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
} as unknown as RooIgnoreController,
|
||||
clineMessages: [],
|
||||
api: {
|
||||
getModel: jest.fn().mockReturnValue({ info: { contextWindow: 100000 } }),
|
||||
createMessage: jest.fn(),
|
||||
countTokens: jest.fn(),
|
||||
} as unknown as ApiHandler,
|
||||
diffEnabled: true,
|
||||
providerRef: {
|
||||
deref: jest.fn().mockReturnValue(mockProvider),
|
||||
[Symbol.toStringTag]: "WeakRef",
|
||||
} as unknown as WeakRef<ClineProvider>,
|
||||
}
|
||||
|
||||
// Mock other dependencies.
|
||||
;(getApiMetrics as jest.Mock).mockReturnValue({ contextTokens: 50000, totalCost: 0.25 })
|
||||
;(getFullModeDetails as jest.Mock).mockResolvedValue({
|
||||
name: "💻 Code",
|
||||
roleDefinition: "You are a code assistant",
|
||||
customInstructions: "Custom instructions",
|
||||
})
|
||||
;(isToolAllowedForMode as jest.Mock).mockReturnValue(true)
|
||||
;(listFiles as jest.Mock).mockResolvedValue([["file1.ts", "file2.ts"], false])
|
||||
;(formatResponse.formatFilesList as jest.Mock).mockReturnValue("file1.ts\nfile2.ts")
|
||||
;(arePathsEqual as jest.Mock).mockReturnValue(false)
|
||||
;(Terminal.compressTerminalOutput as jest.Mock).mockImplementation((output: string) => output)
|
||||
;(TerminalRegistry.getTerminals as jest.Mock).mockReturnValue([])
|
||||
;(TerminalRegistry.getBackgroundTerminals as jest.Mock).mockReturnValue([])
|
||||
;(TerminalRegistry.isProcessHot as jest.Mock).mockReturnValue(false)
|
||||
;(TerminalRegistry.getUnretrievedOutput as jest.Mock).mockReturnValue("")
|
||||
;(pWaitFor as unknown as jest.Mock).mockResolvedValue(undefined)
|
||||
;(delay as jest.Mock).mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
it("should return basic environment details", async () => {
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
expect(result).toContain("<environment_details>")
|
||||
expect(result).toContain("</environment_details>")
|
||||
expect(result).toContain("# VSCode Visible Files")
|
||||
expect(result).toContain("# VSCode Open Tabs")
|
||||
expect(result).toContain("# Current Time")
|
||||
expect(result).toContain("# Current Context Size (Tokens)")
|
||||
expect(result).toContain("# Current Cost")
|
||||
expect(result).toContain("# Current Mode")
|
||||
|
||||
expect(mockProvider.getState).toHaveBeenCalled()
|
||||
|
||||
expect(getFullModeDetails).toHaveBeenCalledWith("code", [], undefined, {
|
||||
cwd: mockCwd,
|
||||
globalCustomInstructions: "test instructions",
|
||||
language: "en",
|
||||
})
|
||||
|
||||
expect(getApiMetrics).toHaveBeenCalledWith(mockCline.clineMessages)
|
||||
})
|
||||
|
||||
it("should include file details when includeFileDetails is true", async () => {
|
||||
const result = await getEnvironmentDetails(mockCline as Cline, true)
|
||||
expect(result).toContain("# Current Workspace Directory")
|
||||
expect(result).toContain("Files")
|
||||
|
||||
expect(listFiles).toHaveBeenCalledWith(mockCwd, true, 50)
|
||||
|
||||
expect(formatResponse.formatFilesList).toHaveBeenCalledWith(
|
||||
mockCwd,
|
||||
["file1.ts", "file2.ts"],
|
||||
false,
|
||||
mockCline.rooIgnoreController,
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it("should not include file details when includeFileDetails is false", async () => {
|
||||
await getEnvironmentDetails(mockCline as Cline, false)
|
||||
expect(listFiles).not.toHaveBeenCalled()
|
||||
expect(formatResponse.formatFilesList).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle desktop directory specially", async () => {
|
||||
;(arePathsEqual as jest.Mock).mockReturnValue(true)
|
||||
const result = await getEnvironmentDetails(mockCline as Cline, true)
|
||||
expect(result).toContain("Desktop files not shown automatically")
|
||||
expect(listFiles).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should include recently modified files if any", async () => {
|
||||
;(mockCline.fileContextTracker!.getAndClearRecentlyModifiedFiles as jest.Mock).mockReturnValue([
|
||||
"modified1.ts",
|
||||
"modified2.ts",
|
||||
])
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
expect(result).toContain("# Recently Modified Files")
|
||||
expect(result).toContain("modified1.ts")
|
||||
expect(result).toContain("modified2.ts")
|
||||
})
|
||||
|
||||
it("should include active terminal information", async () => {
|
||||
const mockActiveTerminal = {
|
||||
id: "terminal-1",
|
||||
getLastCommand: jest.fn().mockReturnValue("npm test"),
|
||||
getProcessesWithOutput: jest.fn().mockReturnValue([]),
|
||||
} as MockTerminal
|
||||
|
||||
;(TerminalRegistry.getTerminals as jest.Mock).mockReturnValue([mockActiveTerminal])
|
||||
;(TerminalRegistry.getUnretrievedOutput as jest.Mock).mockReturnValue("Test output")
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
expect(result).toContain("# Actively Running Terminals")
|
||||
expect(result).toContain("Original command: `npm test`")
|
||||
expect(result).toContain("Test output")
|
||||
|
||||
mockCline.didEditFile = true
|
||||
await getEnvironmentDetails(mockCline as Cline)
|
||||
expect(delay).toHaveBeenCalledWith(300)
|
||||
|
||||
expect(pWaitFor).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should include inactive terminals with output", async () => {
|
||||
const mockProcess = {
|
||||
command: "npm build",
|
||||
getUnretrievedOutput: jest.fn().mockReturnValue("Build output"),
|
||||
}
|
||||
|
||||
const mockInactiveTerminal = {
|
||||
id: "terminal-2",
|
||||
getProcessesWithOutput: jest.fn().mockReturnValue([mockProcess]),
|
||||
cleanCompletedProcessQueue: jest.fn(),
|
||||
} as MockTerminal
|
||||
|
||||
;(TerminalRegistry.getTerminals as jest.Mock).mockImplementation((active: boolean) =>
|
||||
active ? [] : [mockInactiveTerminal],
|
||||
)
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
expect(result).toContain("# Inactive Terminals with Completed Process Output")
|
||||
expect(result).toContain("Terminal terminal-2")
|
||||
expect(result).toContain("Command: `npm build`")
|
||||
expect(result).toContain("Build output")
|
||||
|
||||
expect(mockInactiveTerminal.cleanCompletedProcessQueue).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should include warning when file writing is not allowed", async () => {
|
||||
;(isToolAllowedForMode as jest.Mock).mockReturnValue(false)
|
||||
;(getModeBySlug as jest.Mock).mockImplementation((slug: string) => {
|
||||
if (slug === "code") {
|
||||
return { name: "💻 Code" }
|
||||
}
|
||||
|
||||
if (slug === defaultModeSlug) {
|
||||
return { name: "Default Mode" }
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
expect(result).toContain("NOTE: You are currently in '💻 Code' mode, which does not allow write operations")
|
||||
})
|
||||
|
||||
it("should include experiment-specific details when Power Steering is enabled", async () => {
|
||||
mockState.experiments = { [EXPERIMENT_IDS.POWER_STEERING]: true }
|
||||
;(experiments.isEnabled as jest.Mock).mockReturnValue(true)
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
expect(result).toContain("<role>You are a code assistant</role>")
|
||||
expect(result).toContain("<custom_instructions>Custom instructions</custom_instructions>")
|
||||
})
|
||||
|
||||
it("should handle missing provider or state", async () => {
|
||||
// Mock provider to return null.
|
||||
mockCline.providerRef!.deref = jest.fn().mockReturnValue(null)
|
||||
|
||||
const result = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
// Verify the function still returns a result.
|
||||
expect(result).toContain("<environment_details>")
|
||||
expect(result).toContain("</environment_details>")
|
||||
|
||||
// Mock provider to return null state.
|
||||
mockCline.providerRef!.deref = jest.fn().mockReturnValue({
|
||||
getState: jest.fn().mockResolvedValue(null),
|
||||
})
|
||||
|
||||
const result2 = await getEnvironmentDetails(mockCline as Cline)
|
||||
|
||||
// Verify the function still returns a result.
|
||||
expect(result2).toContain("<environment_details>")
|
||||
expect(result2).toContain("</environment_details>")
|
||||
})
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
;(pWaitFor as unknown as jest.Mock).mockRejectedValue(new Error("Test error"))
|
||||
|
||||
const mockErrorTerminal = {
|
||||
id: "terminal-1",
|
||||
getLastCommand: jest.fn().mockReturnValue("npm test"),
|
||||
getProcessesWithOutput: jest.fn().mockReturnValue([]),
|
||||
} as MockTerminal
|
||||
|
||||
;(TerminalRegistry.getTerminals as jest.Mock).mockReturnValue([mockErrorTerminal])
|
||||
;(TerminalRegistry.getBackgroundTerminals as jest.Mock).mockReturnValue([])
|
||||
;(mockCline.fileContextTracker!.getAndClearRecentlyModifiedFiles as jest.Mock).mockReturnValue([])
|
||||
|
||||
await expect(getEnvironmentDetails(mockCline as Cline)).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
270
src/core/environment/getEnvironmentDetails.ts
Normal file
270
src/core/environment/getEnvironmentDetails.ts
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
import path from "path"
|
||||
import os from "os"
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import pWaitFor from "p-wait-for"
|
||||
import delay from "delay"
|
||||
|
||||
import { EXPERIMENT_IDS, experiments as Experiments, ExperimentId } from "../../shared/experiments"
|
||||
import { formatLanguage } from "../../shared/language"
|
||||
import { defaultModeSlug, getFullModeDetails, getModeBySlug, isToolAllowedForMode } 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 { Cline } from "../Cline"
|
||||
|
||||
export async function getEnvironmentDetails(cline: Cline, includeFileDetails: boolean = false) {
|
||||
let details = ""
|
||||
|
||||
const clineProvider = cline.providerRef.deref()
|
||||
const state = await clineProvider?.getState()
|
||||
const { terminalOutputLineLimit = 500, 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.
|
||||
details += "\n\n# VSCode Visible Files"
|
||||
|
||||
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${allowedVisibleFiles}`
|
||||
} else {
|
||||
details += "\n(No visible files)"
|
||||
}
|
||||
|
||||
details += "\n\n# VSCode Open Tabs"
|
||||
const { maxOpenTabsContext } = state ?? {}
|
||||
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(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${allowedOpenTabs}`
|
||||
} else {
|
||||
details += "\n(No open tabs)"
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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 = 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
|
||||
}
|
||||
|
||||
// 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(cline.clineMessages)
|
||||
const modelInfo = cline.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,
|
||||
} = 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 += `<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: cline.diffEnabled }) &&
|
||||
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: cline.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 (${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
|
||||
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxFiles)
|
||||
const { showRooIgnoredFiles = true } = state ?? {}
|
||||
|
||||
const result = formatResponse.formatFilesList(
|
||||
cline.cwd,
|
||||
files,
|
||||
didHitLimit,
|
||||
cline.rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
)
|
||||
|
||||
details += result
|
||||
}
|
||||
}
|
||||
|
||||
return `<environment_details>\n${details.trim()}\n</environment_details>`
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ const DIRS_TO_IGNORE = [
|
|||
export async function listFiles(dirPath: string, recursive: boolean, limit: number): Promise<[string[], boolean]> {
|
||||
// Handle special directories
|
||||
const specialResult = await handleSpecialDirectories(dirPath)
|
||||
|
||||
if (specialResult) {
|
||||
return specialResult
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,11 +85,13 @@ const ApiOptions = ({
|
|||
return Object.entries(headers)
|
||||
})
|
||||
|
||||
// Effect to synchronize internal customHeaders state with prop changes
|
||||
useEffect(() => {
|
||||
const propHeaders = apiConfiguration?.openAiHeaders || {}
|
||||
if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) setCustomHeaders(Object.entries(propHeaders))
|
||||
}, [apiConfiguration?.openAiHeaders])
|
||||
|
||||
if (JSON.stringify(customHeaders) !== JSON.stringify(Object.entries(propHeaders))) {
|
||||
setCustomHeaders(Object.entries(propHeaders))
|
||||
}
|
||||
}, [apiConfiguration?.openAiHeaders, customHeaders])
|
||||
|
||||
const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl)
|
||||
const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue