Skip to content

Commit 2e1d949

Browse files
authored
Add support for multiple command execution strategies (#2820)
1 parent 8e85a12 commit 2e1d949

File tree

75 files changed

+2153
-1602
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+2153
-1602
lines changed

src/activate/__tests__/registerCommands.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
// npx jest src/activate/__tests__/registerCommands.test.ts
2+
3+
import * as vscode from "vscode"
4+
import { ClineProvider } from "../../core/webview/ClineProvider"
5+
6+
import { getVisibleProviderOrLog } from "../registerCommands"
7+
8+
jest.mock("execa", () => ({
9+
execa: jest.fn(),
10+
}))
11+
112
jest.mock("vscode", () => ({
213
CodeActionKind: {
314
QuickFix: { value: "quickfix" },
@@ -8,12 +19,6 @@ jest.mock("vscode", () => ({
819
},
920
}))
1021

11-
import * as vscode from "vscode"
12-
import { ClineProvider } from "../../core/webview/ClineProvider"
13-
14-
// Import the helper function from the actual file
15-
import { getVisibleProviderOrLog } from "../registerCommands"
16-
1722
jest.mock("../../core/webview/ClineProvider")
1823

1924
describe("getVisibleProviderOrLog", () => {

src/core/Cline.ts

Lines changed: 56 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../servi
5050
// integrations
5151
import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
5252
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
53+
import { RooTerminalProcess } from "../integrations/terminal/types"
5354
import { Terminal } from "../integrations/terminal/Terminal"
5455
import { TerminalRegistry } from "../integrations/terminal/TerminalRegistry"
5556

@@ -197,6 +198,9 @@ export class Cline extends EventEmitter<ClineEvents> {
197198
// metrics
198199
private toolUsage: ToolUsage = {}
199200

201+
// terminal
202+
public terminalProcess?: RooTerminalProcess
203+
200204
constructor({
201205
provider,
202206
apiConfiguration,
@@ -480,6 +484,14 @@ export class Cline extends EventEmitter<ClineEvents> {
480484
this.askResponseImages = images
481485
}
482486

487+
async handleTerminalOperation(terminalOperation: "continue" | "abort") {
488+
if (terminalOperation === "continue") {
489+
this.terminalProcess?.continue()
490+
} else if (terminalOperation === "abort") {
491+
this.terminalProcess?.abort()
492+
}
493+
}
494+
483495
async say(
484496
type: ClineSay,
485497
text?: string,
@@ -1974,6 +1986,7 @@ export class Cline extends EventEmitter<ClineEvents> {
19741986

19751987
// 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
19761988
details += "\n\n# VSCode Visible Files"
1989+
19771990
const visibleFilePaths = vscode.window.visibleTextEditors
19781991
?.map((editor) => editor.document?.uri?.fsPath)
19791992
.filter(Boolean)
@@ -2012,11 +2025,12 @@ export class Cline extends EventEmitter<ClineEvents> {
20122025
details += "\n(No open tabs)"
20132026
}
20142027

2015-
// Get task-specific and background terminals
2028+
// Get task-specific and background terminals.
20162029
const busyTerminals = [
20172030
...TerminalRegistry.getTerminals(true, this.taskId),
20182031
...TerminalRegistry.getBackgroundTerminals(true),
20192032
]
2033+
20202034
const inactiveTerminals = [
20212035
...TerminalRegistry.getTerminals(false, this.taskId),
20222036
...TerminalRegistry.getBackgroundTerminals(false),
@@ -2027,77 +2041,66 @@ export class Cline extends EventEmitter<ClineEvents> {
20272041
}
20282042

20292043
if (busyTerminals.length > 0) {
2030-
// wait for terminals to cool down
2044+
// Wait for terminals to cool down.
20312045
await pWaitFor(() => busyTerminals.every((t) => !TerminalRegistry.isProcessHot(t.id)), {
20322046
interval: 100,
20332047
timeout: 15_000,
20342048
}).catch(() => {})
20352049
}
20362050

2037-
// we want to get diagnostics AFTER terminal cools down for a few reasons: terminal could be scaffolding a project, dev servers (compilers like webpack) will first re-compile and then send diagnostics, etc
2038-
/*
2039-
let diagnosticsDetails = ""
2040-
const diagnostics = await this.diagnosticsMonitor.getCurrentDiagnostics(this.didEditFile || terminalWasBusy) // if cline ran a command (ie npm install) or edited the workspace then wait a bit for updated diagnostics
2041-
for (const [uri, fileDiagnostics] of diagnostics) {
2042-
const problems = fileDiagnostics.filter((d) => d.severity === vscode.DiagnosticSeverity.Error)
2043-
if (problems.length > 0) {
2044-
diagnosticsDetails += `\n## ${path.relative(this.cwd, uri.fsPath)}`
2045-
for (const diagnostic of problems) {
2046-
// let severity = diagnostic.severity === vscode.DiagnosticSeverity.Error ? "Error" : "Warning"
2047-
const line = diagnostic.range.start.line + 1 // VSCode lines are 0-indexed
2048-
const source = diagnostic.source ? `[${diagnostic.source}] ` : ""
2049-
diagnosticsDetails += `\n- ${source}Line ${line}: ${diagnostic.message}`
2050-
}
2051-
}
2052-
}
2053-
*/
2054-
this.didEditFile = false // reset, this lets us know when to wait for saved files to update terminals
2051+
// Reset, this lets us know when to wait for saved files to update terminals.
2052+
this.didEditFile = false
20552053

2056-
// waiting for updated diagnostics lets terminal output be the most up-to-date possible
2054+
// Waiting for updated diagnostics lets terminal output be the most
2055+
// up-to-date possible.
20572056
let terminalDetails = ""
2057+
20582058
if (busyTerminals.length > 0) {
2059-
// terminals are cool, let's retrieve their output
2059+
// Terminals are cool, let's retrieve their output.
20602060
terminalDetails += "\n\n# Actively Running Terminals"
2061+
20612062
for (const busyTerminal of busyTerminals) {
20622063
terminalDetails += `\n## Original command: \`${busyTerminal.getLastCommand()}\``
20632064
let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id)
2065+
20642066
if (newOutput) {
20652067
newOutput = Terminal.compressTerminalOutput(newOutput, terminalOutputLineLimit)
20662068
terminalDetails += `\n### New Output\n${newOutput}`
2067-
} else {
2068-
// details += `\n(Still running, no new output)` // don't want to show this right after running the command
20692069
}
20702070
}
20712071
}
20722072

2073-
// First check if any inactive terminals in this task have completed processes with output
2073+
// First check if any inactive terminals in this task have completed
2074+
// processes with output.
20742075
const terminalsWithOutput = inactiveTerminals.filter((terminal) => {
20752076
const completedProcesses = terminal.getProcessesWithOutput()
20762077
return completedProcesses.length > 0
20772078
})
20782079

2079-
// Only add the header if there are terminals with output
2080+
// Only add the header if there are terminals with output.
20802081
if (terminalsWithOutput.length > 0) {
20812082
terminalDetails += "\n\n# Inactive Terminals with Completed Process Output"
20822083

2083-
// Process each terminal with output
2084+
// Process each terminal with output.
20842085
for (const inactiveTerminal of terminalsWithOutput) {
20852086
let terminalOutputs: string[] = []
20862087

2087-
// Get output from completed processes queue
2088+
// Get output from completed processes queue.
20882089
const completedProcesses = inactiveTerminal.getProcessesWithOutput()
2090+
20892091
for (const process of completedProcesses) {
20902092
let output = process.getUnretrievedOutput()
2093+
20912094
if (output) {
20922095
output = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
20932096
terminalOutputs.push(`Command: \`${process.command}\`\n${output}`)
20942097
}
20952098
}
20962099

2097-
// Clean the queue after retrieving output
2100+
// Clean the queue after retrieving output.
20982101
inactiveTerminal.cleanCompletedProcessQueue()
20992102

2100-
// Add this terminal's outputs to the details
2103+
// Add this terminal's outputs to the details.
21012104
if (terminalOutputs.length > 0) {
21022105
terminalDetails += `\n## Terminal ${inactiveTerminal.id}`
21032106
terminalOutputs.forEach((output) => {
@@ -2107,15 +2110,11 @@ export class Cline extends EventEmitter<ClineEvents> {
21072110
}
21082111
}
21092112

2110-
// details += "\n\n# VSCode Workspace Errors"
2111-
// if (diagnosticsDetails) {
2112-
// details += diagnosticsDetails
2113-
// } else {
2114-
// details += "\n(No errors detected)"
2115-
// }
2113+
// console.log(`[Cline#getEnvironmentDetails] terminalDetails: ${terminalDetails}`)
21162114

2117-
// Add recently modified files section
2115+
// Add recently modified files section.
21182116
const recentlyModifiedFiles = this.fileContextTracker.getAndClearRecentlyModifiedFiles()
2117+
21192118
if (recentlyModifiedFiles.length > 0) {
21202119
details +=
21212120
"\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):"
@@ -2128,8 +2127,9 @@ export class Cline extends EventEmitter<ClineEvents> {
21282127
details += terminalDetails
21292128
}
21302129

2131-
// Add current time information with timezone
2130+
// Add current time information with timezone.
21322131
const now = new Date()
2132+
21332133
const formatter = new Intl.DateTimeFormat(undefined, {
21342134
year: "numeric",
21352135
month: "numeric",
@@ -2139,22 +2139,26 @@ export class Cline extends EventEmitter<ClineEvents> {
21392139
second: "numeric",
21402140
hour12: true,
21412141
})
2142+
21422143
const timeZone = formatter.resolvedOptions().timeZone
21432144
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
21442145
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset))
21452146
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60))
21462147
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`
21472148
details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})`
21482149

2149-
// Add context tokens information
2150+
// Add context tokens information.
21502151
const { contextTokens, totalCost } = getApiMetrics(this.clineMessages)
21512152
const modelInfo = this.api.getModel().info
21522153
const contextWindow = modelInfo.contextWindow
2154+
21532155
const contextPercentage =
21542156
contextTokens && contextWindow ? Math.round((contextTokens / contextWindow) * 100) : undefined
2157+
21552158
details += `\n\n# Current Context Size (Tokens)\n${contextTokens ? `${contextTokens.toLocaleString()} (${contextPercentage}%)` : "(Not available)"}`
21562159
details += `\n\n# Current Cost\n${totalCost !== null ? `$${totalCost.toFixed(2)}` : "(Not available)"}`
2157-
// Add current mode and any mode-specific warnings
2160+
2161+
// Add current mode and any mode-specific warnings.
21582162
const {
21592163
mode,
21602164
customModes,
@@ -2164,28 +2168,31 @@ export class Cline extends EventEmitter<ClineEvents> {
21642168
customInstructions: globalCustomInstructions,
21652169
language,
21662170
} = (await this.providerRef.deref()?.getState()) ?? {}
2171+
21672172
const currentMode = mode ?? defaultModeSlug
2173+
21682174
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
21692175
cwd: this.cwd,
21702176
globalCustomInstructions,
21712177
language: language ?? formatLanguage(vscode.env.language),
21722178
})
2179+
21732180
details += `\n\n# Current Mode\n`
21742181
details += `<slug>${currentMode}</slug>\n`
21752182
details += `<name>${modeDetails.name}</name>\n`
21762183
details += `<model>${apiModelId}</model>\n`
2184+
21772185
if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) {
21782186
details += `<role>${modeDetails.roleDefinition}</role>\n`
2187+
21792188
if (modeDetails.customInstructions) {
21802189
details += `<custom_instructions>${modeDetails.customInstructions}</custom_instructions>\n`
21812190
}
21822191
}
21832192

2184-
// Add warning if not in code mode
2193+
// Add warning if not in code mode.
21852194
if (
2186-
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], {
2187-
apply_diff: this.diffEnabled,
2188-
}) &&
2195+
!isToolAllowedForMode("write_to_file", currentMode, customModes ?? [], { apply_diff: this.diffEnabled }) &&
21892196
!isToolAllowedForMode("apply_diff", currentMode, customModes ?? [], { apply_diff: this.diffEnabled })
21902197
) {
21912198
const currentModeName = getModeBySlug(currentMode, customModes)?.name ?? currentMode
@@ -2196,20 +2203,24 @@ export class Cline extends EventEmitter<ClineEvents> {
21962203
if (includeFileDetails) {
21972204
details += `\n\n# Current Workspace Directory (${this.cwd.toPosix()}) Files\n`
21982205
const isDesktop = arePathsEqual(this.cwd, path.join(os.homedir(), "Desktop"))
2206+
21992207
if (isDesktop) {
2200-
// don't want to immediately access desktop since it would show permission popup
2208+
// Don't want to immediately access desktop since it would show
2209+
// permission popup.
22012210
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
22022211
} else {
22032212
const maxFiles = maxWorkspaceFiles ?? 200
22042213
const [files, didHitLimit] = await listFiles(this.cwd, true, maxFiles)
22052214
const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {}
2215+
22062216
const result = formatResponse.formatFilesList(
22072217
this.cwd,
22082218
files,
22092219
didHitLimit,
22102220
this.rooIgnoreController,
22112221
showRooIgnoredFiles,
22122222
)
2223+
22132224
details += result
22142225
}
22152226
}

src/core/__tests__/Cline.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import { ApiConfiguration, ModelInfo } from "../../shared/api"
1313
import { ApiStreamChunk } from "../../api/transform/stream"
1414
import { ContextProxy } from "../config/ContextProxy"
1515

16+
jest.mock("execa", () => ({
17+
execa: jest.fn(),
18+
}))
19+
1620
// Mock RooIgnoreController
1721
jest.mock("../ignore/RooIgnoreController")
1822

src/core/mentions/index.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
import * as vscode from "vscode"
1+
import fs from "fs/promises"
22
import * as path from "path"
3+
4+
import * as vscode from "vscode"
5+
import { isBinaryFile } from "isbinaryfile"
6+
37
import { openFile } from "../../integrations/misc/open-file"
48
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
59
import { mentionRegexGlobal } from "../../shared/context-mentions"
6-
import fs from "fs/promises"
10+
711
import { extractTextFromFile } from "../../integrations/misc/extract-text"
8-
import { isBinaryFile } from "isbinaryfile"
912
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
1013
import { getCommitInfo, getWorkingState } from "../../utils/git"
11-
import { getLatestTerminalOutput } from "../../integrations/terminal/get-latest-output"
1214
import { getWorkspacePath } from "../../utils/path"
1315
import { FileContextTracker } from "../context-tracking/FileContextTracker"
1416

@@ -221,3 +223,50 @@ async function getWorkspaceProblems(cwd: string): Promise<string> {
221223
}
222224
return result
223225
}
226+
227+
/**
228+
* Gets the contents of the active terminal
229+
* @returns The terminal contents as a string
230+
*/
231+
export async function getLatestTerminalOutput(): Promise<string> {
232+
// Store original clipboard content to restore later
233+
const originalClipboard = await vscode.env.clipboard.readText()
234+
235+
try {
236+
// Select terminal content
237+
await vscode.commands.executeCommand("workbench.action.terminal.selectAll")
238+
239+
// Copy selection to clipboard
240+
await vscode.commands.executeCommand("workbench.action.terminal.copySelection")
241+
242+
// Clear the selection
243+
await vscode.commands.executeCommand("workbench.action.terminal.clearSelection")
244+
245+
// Get terminal contents from clipboard
246+
let terminalContents = (await vscode.env.clipboard.readText()).trim()
247+
248+
// Check if there's actually a terminal open
249+
if (terminalContents === originalClipboard) {
250+
return ""
251+
}
252+
253+
// Clean up command separation
254+
const lines = terminalContents.split("\n")
255+
const lastLine = lines.pop()?.trim()
256+
257+
if (lastLine) {
258+
let i = lines.length - 1
259+
260+
while (i >= 0 && !lines[i].trim().startsWith(lastLine)) {
261+
i--
262+
}
263+
264+
terminalContents = lines.slice(Math.max(i, 0)).join("\n")
265+
}
266+
267+
return terminalContents
268+
} finally {
269+
// Restore original clipboard content
270+
await vscode.env.clipboard.writeText(originalClipboard)
271+
}
272+
}

0 commit comments

Comments
 (0)