Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
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/many-boats-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Use a fallback terminal if VSCode shell integration fails
4 changes: 2 additions & 2 deletions .roomodes
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"customModes": [
{
"slug": "test",
"name": "Test",
"name": "🧪 Test",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, thank you for this!

"roleDefinition": "You are Roo, a Jest testing specialist with deep expertise in:\n- Writing and maintaining Jest test suites\n- Test-driven development (TDD) practices\n- Mocking and stubbing with Jest\n- Integration testing strategies\n- TypeScript testing patterns\n- Code coverage analysis\n- Test performance optimization\n\nYour focus is on maintaining high test quality and coverage across the codebase, working primarily with:\n- Test files in __tests__ directories\n- Mock implementations in __mocks__\n- Test utilities and helpers\n- Jest configuration and setup\n\nYou ensure tests are:\n- Well-structured and maintainable\n- Following Jest best practices\n- Properly typed with TypeScript\n- Providing meaningful coverage\n- Using appropriate mocking strategies",
"groups": [
"read",
Expand All @@ -20,7 +20,7 @@
},
{
"slug": "translate",
"name": "Translate",
"name": "🌐 Translate",
"roleDefinition": "You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources.",
"groups": [
"read",
Expand Down
4 changes: 2 additions & 2 deletions scripts/run-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
const { execSync } = require("child_process")

if (process.platform === "win32") {
Comment thread
cte marked this conversation as resolved.
execSync("npm-run-all test:* lint:*", { stdio: "inherit" })
execSync("npm-run-all test:*", { stdio: "inherit" })
} else {
execSync("npm-run-all -p test:* lint:*", { stdio: "inherit" })
execSync("npm-run-all -p test:*", { stdio: "inherit" })
}
12 changes: 12 additions & 0 deletions src/__mocks__/jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
import nock from "nock"

nock.disableNetConnect()

export function allowNetConnect(host?: string | RegExp) {
if (host) {
nock.enableNetConnect(host)
} else {
nock.enableNetConnect()
}
}

// Mock the logger globally for all tests
jest.mock("../utils/logging", () => ({
logger: {
Expand Down
30 changes: 30 additions & 0 deletions src/api/providers/__tests__/glama.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { GlamaHandler } from "../glama"
import { ApiHandlerOptions } from "../../../shared/api"

// Mock dependencies
jest.mock("../fetchers/cache", () => ({
getModels: jest.fn().mockImplementation(() => {
return Promise.resolve({
"anthropic/claude-3-7-sonnet": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Claude 3.7 Sonnet",
thinking: false,
supportsComputerUse: true,
},
"openai/gpt-4o": {
maxTokens: 4096,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
description: "GPT-4o",
},
})
}),
}))

// Mock OpenAI client
const mockCreate = jest.fn()
const mockWithResponse = jest.fn()
Expand Down
32 changes: 32 additions & 0 deletions src/api/providers/__tests__/openrouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,38 @@ import { ApiHandlerOptions } from "../../../shared/api"
// Mock dependencies
jest.mock("openai")
jest.mock("delay", () => jest.fn(() => Promise.resolve()))
jest.mock("../fetchers/cache", () => ({
getModels: jest.fn().mockImplementation(() => {
return Promise.resolve({
"anthropic/claude-3.7-sonnet": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Claude 3.7 Sonnet",
thinking: false,
supportsComputerUse: true,
},
"anthropic/claude-3.7-sonnet:thinking": {
maxTokens: 128000,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Claude 3.7 Sonnet with thinking",
thinking: true,
supportsComputerUse: true,
},
})
}),
}))

describe("OpenRouterHandler", () => {
const mockOptions: ApiHandlerOptions = {
Expand Down
18 changes: 18 additions & 0 deletions src/api/providers/__tests__/requesty.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// npx jest src/api/providers/__tests__/requesty.test.ts
Comment thread
cte marked this conversation as resolved.

import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import { ApiHandlerOptions, ModelInfo } from "../../../shared/api"
Expand All @@ -9,6 +11,22 @@ import { convertToR1Format } from "../../transform/r1-format"
jest.mock("openai")
jest.mock("../../transform/openai-format")
jest.mock("../../transform/r1-format")
jest.mock("../fetchers/cache", () => ({
getModels: jest.fn().mockResolvedValue({
"test-model": {
maxTokens: 8192,
contextWindow: 200_000,
supportsImages: true,
supportsComputerUse: true,
supportsPromptCache: true,
inputPrice: 3.0,
outputPrice: 15.0,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Test model description",
},
}),
}))

describe("RequestyHandler", () => {
let handler: RequestyHandler
Expand Down
52 changes: 52 additions & 0 deletions src/api/providers/__tests__/unbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,58 @@ import { ApiHandlerOptions } from "../../../shared/api"

import { UnboundHandler } from "../unbound"

// Mock dependencies
jest.mock("../fetchers/cache", () => ({
getModels: jest.fn().mockImplementation(() => {
return Promise.resolve({
"anthropic/claude-3-5-sonnet-20241022": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Claude 3.5 Sonnet",
thinking: false,
supportsComputerUse: true,
},
"anthropic/claude-3-7-sonnet-20250219": {
maxTokens: 8192,
contextWindow: 200000,
supportsImages: true,
supportsPromptCache: true,
inputPrice: 3,
outputPrice: 15,
cacheWritesPrice: 3.75,
cacheReadsPrice: 0.3,
description: "Claude 3.7 Sonnet",
thinking: false,
supportsComputerUse: true,
},
"openai/gpt-4o": {
maxTokens: 4096,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 5,
outputPrice: 15,
description: "GPT-4o",
},
"openai/o3-mini": {
maxTokens: 4096,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 1,
outputPrice: 3,
description: "O3 Mini",
},
})
}),
}))

// Mock OpenAI client
const mockCreate = jest.fn()
const mockWithResponse = jest.fn()
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/fetchers/__tests__/openrouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ nockBack.setMode("lockdown")

describe("OpenRouter API", () => {
describe("getOpenRouterModels", () => {
it("fetches models and validates schema", async () => {
it.skip("fetches models and validates schema", async () => {
Comment thread
cte marked this conversation as resolved.
const { nockDone } = await nockBack("openrouter-models.json")

const models = await getOpenRouterModels()
Expand Down
2 changes: 1 addition & 1 deletion src/api/providers/glama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class GlamaHandler extends RouterProvider implements SingleCompletionHand
constructor(options: ApiHandlerOptions) {
super({
options,
name: "unbound",
name: "glama",
baseURL: "https://glama.ai/api/gateway/openai/v1",
apiKey: options.glamaApiKey,
modelId: options.glamaModelId,
Expand Down
6 changes: 4 additions & 2 deletions src/core/tools/attemptCompletionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { executeCommand } from "./executeCommandTool"
import { type ExecuteCommandOptions, executeCommand } from "./executeCommandTool"

export async function attemptCompletionTool(
cline: Cline,
Expand Down Expand Up @@ -82,7 +82,9 @@ export async function attemptCompletionTool(
return
}

const [userRejected, execCommandResult] = await executeCommand(cline, command!)
const options: ExecuteCommandOptions = { command }

const [userRejected, execCommandResult] = await executeCommand(cline, options)

if (userRejected) {
cline.didRejectTool = true
Expand Down
81 changes: 64 additions & 17 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, To
import { formatResponse } from "../prompts/responses"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { ExitCodeDetails, RooTerminalProcess } from "../../integrations/terminal/types"
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
import { Terminal } from "../../integrations/terminal/Terminal"

class ShellIntegrationError extends Error {}

export async function executeCommandTool(
cline: Cline,
block: ToolUse,
Expand Down Expand Up @@ -52,13 +54,44 @@ export async function executeCommandTool(
return
}

const [userRejected, result] = await executeCommand(cline, command, customCwd)
const clineProvider = await cline.providerRef.deref()
const clineProviderState = await clineProvider?.getState()
const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}

if (userRejected) {
cline.didRejectTool = true
const options: ExecuteCommandOptions = {
command,
customCwd,
terminalShellIntegrationDisabled,
terminalOutputLineLimit,
}

pushToolResult(result)
try {
const [rejected, result] = await executeCommand(cline, options)

if (rejected) {
cline.didRejectTool = true
}

pushToolResult(result)
} catch (error: unknown) {
await cline.say("shell_integration_warning")
clineProvider?.setValue("terminalShellIntegrationDisabled", true)

if (error instanceof ShellIntegrationError) {
const [rejected, result] = await executeCommand(cline, {
...options,
terminalShellIntegrationDisabled: true,
})

if (rejected) {
cline.didRejectTool = true
}

pushToolResult(result)
} else {
pushToolResult(`Command failed to execute in terminal due to a shell integration error.`)
}
}

return
}
Expand All @@ -68,10 +101,21 @@ export async function executeCommandTool(
}
}

export type ExecuteCommandOptions = {
command: string
customCwd?: string
terminalShellIntegrationDisabled?: boolean
terminalOutputLineLimit?: number
}

export async function executeCommand(
cline: Cline,
command: string,
customCwd?: string,
{
command,
customCwd,
terminalShellIntegrationDisabled = false,
terminalOutputLineLimit = 500,
}: ExecuteCommandOptions,
): Promise<[boolean, ToolResponse]> {
let workingDir: string

Expand All @@ -94,13 +138,11 @@ export async function executeCommand(
let completed = false
let result: string = ""
let exitDetails: ExitCodeDetails | undefined
let shellIntegrationError: string | undefined

const clineProvider = await cline.providerRef.deref()
const clineProviderState = await clineProvider?.getState()
const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}
const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"

const callbacks = {
const callbacks: RooTerminalCallbacks = {
onLine: async (output: string, process: RooTerminalProcess) => {
const compressed = Terminal.compressTerminalOutput(output, terminalOutputLineLimit)
cline.say("command_output", compressed)
Expand All @@ -123,13 +165,14 @@ export async function executeCommand(
result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit)
completed = true
},
onShellExecutionComplete: (details: ExitCodeDetails) => {
exitDetails = details
},
onNoShellIntegration: async (message: string) => {
onShellExecutionComplete: (details: ExitCodeDetails) => (exitDetails = details),
}

if (terminalProvider === "vscode") {
callbacks.onNoShellIntegration = async (error: string) => {
telemetryService.captureShellIntegrationError(cline.taskId)
await cline.say("shell_integration_warning", message)
},
shellIntegrationError = error
}
}

const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider)
Expand All @@ -149,6 +192,10 @@ export async function executeCommand(
await process
cline.terminalProcess = undefined

if (shellIntegrationError) {
throw new ShellIntegrationError(shellIntegrationError)
}

// Wait for a short delay to ensure all messages are sent to the webview.
// This delay allows time for non-awaited promises to be created and
// for their associated messages to be sent to the webview, maintaining
Expand Down
2 changes: 2 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1503,8 +1503,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements

// Add model ID if available
const currentCline = this.getCurrentCline()

if (currentCline?.api) {
const { id: modelId } = currentCline.api.getModel()

if (modelId) {
properties.modelId = modelId
}
Expand Down
Loading