Skip to content

Commit 4266140

Browse files
author
Eric Wheeler
committed
refactor: Split environment details into modular components
Converted monolithic environment details implementation into separate context modules. Each module handles a specific aspect of the environment (VSCode, terminal, files, etc.). Replaced reminder.ts with integrated todo implementation and standardized XML attributes. Signed-off-by: Eric Wheeler <[email protected]>
1 parent 2f4d833 commit 4266140

File tree

8 files changed

+328
-292
lines changed

8 files changed

+328
-292
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { Task } from "../../task/Task"
2+
3+
export function getFileContext(cline: Task) {
4+
const recentlyModifiedFiles = cline.fileContextTracker.getAndClearRecentlyModifiedFiles()
5+
if (recentlyModifiedFiles.length > 0) {
6+
return {
7+
recentlyModified: {
8+
file: recentlyModifiedFiles.map((p) => ({ "@path": p })),
9+
},
10+
}
11+
}
12+
return {}
13+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as vscode from "vscode"
2+
3+
import type { ExperimentId } from "@roo-code/types"
4+
5+
import { EXPERIMENT_IDS, experiments as Experiments } from "../../../shared/experiments"
6+
import { formatLanguage } from "../../../shared/language"
7+
import { defaultModeSlug, getFullModeDetails } from "../../../shared/modes"
8+
import { getApiMetrics } from "../../../shared/getApiMetrics"
9+
import type { Task } from "../../task/Task"
10+
11+
export async function getMetadataContext(cline: Task) {
12+
const state = await cline.providerRef.deref()?.getState()
13+
const {
14+
mode,
15+
customModes,
16+
customModePrompts,
17+
experiments = {} as Record<ExperimentId, boolean>,
18+
customInstructions: globalCustomInstructions,
19+
language,
20+
} = state ?? {}
21+
22+
// High-Frequency Metadata
23+
const now = new Date()
24+
const timeZoneOffset = -now.getTimezoneOffset()
25+
const offsetHours = Math.floor(Math.abs(timeZoneOffset) / 60)
26+
const offsetMinutes = Math.abs(timeZoneOffset) % 60
27+
const offsetSign = timeZoneOffset >= 0 ? "+" : "-"
28+
const offsetString = `${offsetSign}${offsetHours
29+
.toString()
30+
.padStart(2, "0")}:${offsetMinutes.toString().padStart(2, "0")}`
31+
const isoDateWithOffset = now.toISOString().replace(/Z$/, offsetString)
32+
const time = {
33+
"@v": isoDateWithOffset,
34+
}
35+
36+
const { totalCost } = getApiMetrics(cline.clineMessages)
37+
const cost = {
38+
"@t": totalCost !== null ? totalCost.toFixed(2) : "0.00",
39+
"@c": "USD",
40+
"#text": "Must form responses to minimize cost growth",
41+
}
42+
43+
// Low-Frequency Metadata
44+
const currentMode = mode ?? defaultModeSlug
45+
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
46+
cwd: cline.cwd,
47+
globalCustomInstructions,
48+
language: language ?? formatLanguage(vscode.env.language),
49+
})
50+
51+
const { id: modelId } = cline.api.getModel()
52+
const modeInfo = {
53+
"@slug": currentMode,
54+
"@name": modeDetails.name,
55+
"@model": modelId,
56+
...(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING) && {
57+
"@role": modeDetails.roleDefinition,
58+
...(modeDetails.customInstructions && {
59+
"@customInstructions": modeDetails.customInstructions,
60+
}),
61+
}),
62+
}
63+
64+
return { time, cost, mode: modeInfo }
65+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import pWaitFor from "p-wait-for"
2+
import delay from "delay"
3+
4+
import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry"
5+
import { Terminal } from "../../../integrations/terminal/Terminal"
6+
import type { Task } from "../../task/Task"
7+
8+
export async function getTerminalContext(cline: Task) {
9+
const state = await cline.providerRef.deref()?.getState()
10+
const { terminalOutputLineLimit = 500 } = state ?? {}
11+
const terminalsData: any[] = []
12+
const busyTerminals = [
13+
...TerminalRegistry.getTerminals(true, cline.taskId),
14+
...TerminalRegistry.getBackgroundTerminals(true),
15+
]
16+
const inactiveTerminals = [
17+
...TerminalRegistry.getTerminals(false, cline.taskId),
18+
...TerminalRegistry.getBackgroundTerminals(false),
19+
]
20+
21+
// Wait for terminals to cool down if needed
22+
if (busyTerminals.length > 0 && cline.didEditFile) {
23+
await delay(300) // Delay after saving file to let terminals catch up
24+
await pWaitFor(() => busyTerminals.every((t) => !TerminalRegistry.isProcessHot(t.id)), {
25+
interval: 100,
26+
timeout: 5_000,
27+
}).catch(() => {})
28+
}
29+
cline.didEditFile = false
30+
31+
// Process active terminals
32+
busyTerminals.forEach((terminal) => {
33+
const cwd = terminal.getCurrentWorkingDirectory()
34+
const command = terminal.getLastCommand()
35+
let output = TerminalRegistry.getUnretrievedOutput(terminal.id)
36+
37+
const terminalData: any = {
38+
"@id": terminal.id.toString(),
39+
"@status": "Active",
40+
"@cwd": cwd,
41+
"@command": command,
42+
}
43+
44+
if (output) {
45+
terminalData["#cdata"] = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
46+
}
47+
48+
terminalsData.push(terminalData)
49+
})
50+
51+
// Process inactive terminals with output
52+
inactiveTerminals
53+
.filter((t) => t.getProcessesWithOutput().length > 0)
54+
.forEach((terminal) => {
55+
const cwd = terminal.getCurrentWorkingDirectory()
56+
const processes = terminal.getProcessesWithOutput()
57+
58+
processes.forEach((process) => {
59+
const output = process.getUnretrievedOutput()
60+
61+
if (output) {
62+
terminalsData.push({
63+
"@id": terminal.id.toString(),
64+
"@status": "Inactive",
65+
"@cwd": cwd,
66+
"@command": process.command,
67+
"#cdata": Terminal.compressTerminalOutput(output, terminalOutputLineLimit),
68+
})
69+
}
70+
})
71+
72+
terminal.cleanCompletedProcessQueue()
73+
})
74+
75+
return terminalsData.length > 0 ? { terminal: terminalsData } : undefined
76+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Task } from "../../task/Task"
2+
3+
export function getTodoContext(cline: Task) {
4+
if (cline.todoList && cline.todoList.length > 0) {
5+
const todoLines = cline.todoList
6+
.map((todo) => {
7+
let statusPrefix = "[ ]" // pending
8+
if (todo.status === "in_progress") statusPrefix = "[-]"
9+
else if (todo.status === "completed") statusPrefix = "[x]"
10+
return `${statusPrefix} ${todo.content}`
11+
})
12+
.join("\n")
13+
14+
const todoText = [
15+
todoLines,
16+
"IMPORTANT: When task status changes, remember to call the `update_todo_list` tool to update your progress.",
17+
].join("\n")
18+
19+
return {
20+
todo: {
21+
"#text": todoText,
22+
},
23+
}
24+
}
25+
return {
26+
todo: {
27+
"#text":
28+
"You have not created a todo list yet. Create one with `update_todo_list` if your task is complicated or involves multiple steps.",
29+
},
30+
}
31+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import path from "path"
2+
import * as vscode from "vscode"
3+
4+
import type { Task } from "../../task/Task"
5+
6+
export async function getVscodeEditorContext(cline: Task) {
7+
const state = await cline.providerRef.deref()?.getState()
8+
const { maxWorkspaceFiles = 200 } = state ?? {}
9+
10+
// Get visible files in the editor
11+
const visibleFilePaths = vscode.window.visibleTextEditors
12+
?.map((editor) => editor.document?.uri?.fsPath)
13+
.filter(Boolean)
14+
.map((absolutePath) => path.relative(cline.cwd, absolutePath))
15+
.slice(0, maxWorkspaceFiles)
16+
17+
const allowedVisibleFiles = cline.rooIgnoreController
18+
? cline.rooIgnoreController.filterPaths(visibleFilePaths)
19+
: visibleFilePaths.map((p) => p.toPosix())
20+
21+
const visibleFiles =
22+
allowedVisibleFiles?.length > 0
23+
? {
24+
file: allowedVisibleFiles.map((p) => ({ "@path": p })),
25+
}
26+
: undefined
27+
28+
// Get open tabs (high-frequency data - using compact representation)
29+
const { maxOpenTabsContext } = state ?? {}
30+
const maxTabs = maxOpenTabsContext ?? 20
31+
const openTabPaths = vscode.window.tabGroups.all
32+
.flatMap((group) => group.tabs)
33+
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
34+
.filter(Boolean)
35+
.map((absolutePath) => path.relative(cline.cwd, absolutePath).toPosix())
36+
.slice(0, maxTabs)
37+
38+
const allowedOpenTabs = cline.rooIgnoreController
39+
? cline.rooIgnoreController.filterPaths(openTabPaths)
40+
: openTabPaths
41+
42+
const openTabs =
43+
allowedOpenTabs?.length > 0
44+
? {
45+
t: allowedOpenTabs.map((p) => ({ "@p": p })),
46+
}
47+
: undefined
48+
49+
return { visibleFiles, openTabs }
50+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import path from "path"
2+
import os from "os"
3+
4+
import { listFiles } from "../../../services/glob/list-files"
5+
import { arePathsEqual } from "../../../utils/path"
6+
import { formatResponse } from "../../prompts/responses"
7+
import type { Task } from "../../task/Task"
8+
9+
export async function getWorkspaceContext(cline: Task, includeFileDetails: boolean) {
10+
if (!includeFileDetails) {
11+
return {}
12+
}
13+
const state = await cline.providerRef.deref()?.getState()
14+
const { maxWorkspaceFiles = 200, showRooIgnoredFiles = true } = state ?? {}
15+
16+
const isDesktop = arePathsEqual(cline.cwd, path.join(os.homedir(), "Desktop"))
17+
const workspaceData: any = { "@directory": cline.cwd.toPosix() }
18+
19+
if (isDesktop) {
20+
workspaceData.note = "Desktop files not shown automatically. Use list_files to explore if needed."
21+
} else if (maxWorkspaceFiles === 0) {
22+
workspaceData.note = "Workspace files context disabled. Use list_files to explore if needed."
23+
} else {
24+
const [files, didHitLimit] = await listFiles(cline.cwd, true, maxWorkspaceFiles)
25+
const formattedFilesList = formatResponse.formatFilesList(
26+
cline.cwd,
27+
files,
28+
didHitLimit,
29+
cline.rooIgnoreController,
30+
showRooIgnoredFiles,
31+
)
32+
33+
if (formattedFilesList && formattedFilesList !== "No files found.") {
34+
const fileLines = formattedFilesList.split("\n").filter((line) => line.trim() !== "")
35+
const fileObjects: any[] = []
36+
const dirObjects: any[] = []
37+
38+
fileLines.forEach((line) => {
39+
if (line.endsWith("/")) {
40+
dirObjects.push({ "@path": line })
41+
} else if (!line.includes("File list truncated")) {
42+
fileObjects.push({ "@path": line })
43+
}
44+
})
45+
46+
if (fileObjects.length > 0) workspaceData.file = fileObjects
47+
if (dirObjects.length > 0) workspaceData.directory = dirObjects
48+
if (didHitLimit) {
49+
workspaceData.note =
50+
"File list truncated. Use list_files on specific subdirectories if you need to explore further."
51+
}
52+
}
53+
}
54+
55+
return { workspace: workspaceData }
56+
}

0 commit comments

Comments
 (0)