Browse Source

feat: Add configurable timeout for command execution (#5668)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <[email protected]>
Co-authored-by: Daniel Riccio <[email protected]>
Co-authored-by: Daniel <[email protected]>
Roomote Bot 7 months ago
parent
commit
e0196320b5
41 changed files with 344 additions and 2 deletions
  1. 4 0
      packages/types/src/terminal.ts
  2. 189 0
      src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts
  3. 43 0
      src/core/tools/__tests__/executeCommandTool.spec.ts
  4. 65 2
      src/core/tools/executeCommandTool.ts
  5. 1 0
      src/i18n/locales/ca/common.json
  6. 1 0
      src/i18n/locales/de/common.json
  7. 1 0
      src/i18n/locales/en/common.json
  8. 1 0
      src/i18n/locales/es/common.json
  9. 1 0
      src/i18n/locales/fr/common.json
  10. 1 0
      src/i18n/locales/hi/common.json
  11. 1 0
      src/i18n/locales/id/common.json
  12. 1 0
      src/i18n/locales/it/common.json
  13. 1 0
      src/i18n/locales/ja/common.json
  14. 1 0
      src/i18n/locales/ko/common.json
  15. 1 0
      src/i18n/locales/nl/common.json
  16. 1 0
      src/i18n/locales/pl/common.json
  17. 1 0
      src/i18n/locales/pt-BR/common.json
  18. 1 0
      src/i18n/locales/ru/common.json
  19. 1 0
      src/i18n/locales/tr/common.json
  20. 1 0
      src/i18n/locales/vi/common.json
  21. 1 0
      src/i18n/locales/zh-CN/common.json
  22. 1 0
      src/i18n/locales/zh-TW/common.json
  23. 7 0
      src/package.json
  24. 1 0
      src/package.nls.ca.json
  25. 1 0
      src/package.nls.de.json
  26. 1 0
      src/package.nls.es.json
  27. 1 0
      src/package.nls.fr.json
  28. 1 0
      src/package.nls.hi.json
  29. 1 0
      src/package.nls.id.json
  30. 1 0
      src/package.nls.it.json
  31. 1 0
      src/package.nls.ja.json
  32. 1 0
      src/package.nls.json
  33. 1 0
      src/package.nls.ko.json
  34. 1 0
      src/package.nls.nl.json
  35. 1 0
      src/package.nls.pl.json
  36. 1 0
      src/package.nls.pt-BR.json
  37. 1 0
      src/package.nls.ru.json
  38. 1 0
      src/package.nls.tr.json
  39. 1 0
      src/package.nls.vi.json
  40. 1 0
      src/package.nls.zh-CN.json
  41. 1 0
      src/package.nls.zh-TW.json

+ 4 - 0
packages/types/src/terminal.ts

@@ -25,6 +25,10 @@ export const commandExecutionStatusSchema = z.discriminatedUnion("status", [
 		executionId: z.string(),
 		status: z.literal("fallback"),
 	}),
+	z.object({
+		executionId: z.string(),
+		status: z.literal("timeout"),
+	}),
 ])
 
 export type CommandExecutionStatus = z.infer<typeof commandExecutionStatusSchema>

+ 189 - 0
src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts

@@ -0,0 +1,189 @@
+// Integration tests for command execution timeout functionality
+// npx vitest run src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts
+
+import * as vscode from "vscode"
+import * as fs from "fs/promises"
+import { executeCommand, ExecuteCommandOptions } from "../executeCommandTool"
+import { Task } from "../../task/Task"
+import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry"
+
+// Mock dependencies
+vitest.mock("vscode", () => ({
+	workspace: {
+		getConfiguration: vitest.fn(),
+	},
+}))
+
+vitest.mock("fs/promises")
+vitest.mock("../../../integrations/terminal/TerminalRegistry")
+vitest.mock("../../task/Task")
+
+describe("Command Execution Timeout Integration", () => {
+	let mockTask: any
+	let mockTerminal: any
+	let mockProcess: any
+
+	beforeEach(() => {
+		vitest.clearAllMocks()
+
+		// Mock fs.access to resolve successfully for working directory
+		;(fs.access as any).mockResolvedValue(undefined)
+
+		// Mock task
+		mockTask = {
+			cwd: "/test/directory",
+			terminalProcess: undefined,
+			providerRef: {
+				deref: vitest.fn().mockResolvedValue({
+					postMessageToWebview: vitest.fn(),
+				}),
+			},
+			say: vitest.fn().mockResolvedValue(undefined),
+		}
+
+		// Mock terminal process
+		mockProcess = {
+			abort: vitest.fn(),
+			then: vitest.fn(),
+			catch: vitest.fn(),
+		}
+
+		// Mock terminal
+		mockTerminal = {
+			runCommand: vitest.fn().mockReturnValue(mockProcess),
+			getCurrentWorkingDirectory: vitest.fn().mockReturnValue("/test/directory"),
+		}
+
+		// Mock TerminalRegistry
+		;(TerminalRegistry.getOrCreateTerminal as any).mockResolvedValue(mockTerminal)
+
+		// Mock VSCode configuration
+		const mockGetConfiguration = vitest.fn().mockReturnValue({
+			get: vitest.fn().mockReturnValue(0), // Default 0 (no timeout)
+		})
+		;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration())
+	})
+
+	it("should pass timeout configuration to executeCommand", async () => {
+		const customTimeoutMs = 15000 // 15 seconds in milliseconds
+		const options: ExecuteCommandOptions = {
+			executionId: "test-execution",
+			command: "echo test",
+			commandExecutionTimeout: customTimeoutMs,
+		}
+
+		// Mock a quick-completing process
+		const quickProcess = Promise.resolve()
+		mockTerminal.runCommand.mockReturnValue(quickProcess)
+
+		await executeCommand(mockTask as Task, options)
+
+		// Verify that the terminal was called with the command
+		expect(mockTerminal.runCommand).toHaveBeenCalledWith("echo test", expect.any(Object))
+	})
+
+	it("should handle timeout scenario", async () => {
+		const shortTimeoutMs = 100 // Very short timeout in milliseconds
+		const options: ExecuteCommandOptions = {
+			executionId: "test-execution",
+			command: "sleep 10",
+			commandExecutionTimeout: shortTimeoutMs,
+		}
+
+		// Create a process that never resolves but has an abort method
+		const longRunningProcess = new Promise(() => {
+			// Never resolves to simulate a hanging command
+		})
+
+		// Add abort method to the promise
+		;(longRunningProcess as any).abort = vitest.fn()
+
+		mockTerminal.runCommand.mockReturnValue(longRunningProcess)
+
+		// Execute with timeout
+		const result = await executeCommand(mockTask as Task, options)
+
+		// Should return timeout error
+		expect(result[0]).toBe(false) // Not rejected by user
+		expect(result[1]).toContain("terminated after exceeding")
+		expect(result[1]).toContain("0.1s") // Should show seconds in error message
+	}, 10000) // Increase test timeout to 10 seconds
+
+	it("should abort process on timeout", async () => {
+		const shortTimeoutMs = 50 // Short timeout in milliseconds
+		const options: ExecuteCommandOptions = {
+			executionId: "test-execution",
+			command: "sleep 10",
+			commandExecutionTimeout: shortTimeoutMs,
+		}
+
+		// Create a process that can be aborted
+		const abortSpy = vitest.fn()
+
+		// Mock the process to never resolve but be abortable
+		const neverResolvingPromise = new Promise(() => {})
+		;(neverResolvingPromise as any).abort = abortSpy
+
+		mockTerminal.runCommand.mockReturnValue(neverResolvingPromise)
+
+		await executeCommand(mockTask as Task, options)
+
+		// Verify abort was called
+		expect(abortSpy).toHaveBeenCalled()
+	}, 5000) // Increase test timeout to 5 seconds
+
+	it("should clean up timeout on successful completion", async () => {
+		const options: ExecuteCommandOptions = {
+			executionId: "test-execution",
+			command: "echo test",
+			commandExecutionTimeout: 5000,
+		}
+
+		// Mock a process that completes quickly
+		const quickProcess = Promise.resolve()
+		mockTerminal.runCommand.mockReturnValue(quickProcess)
+
+		const result = await executeCommand(mockTask as Task, options)
+
+		// Should complete successfully without timeout
+		expect(result[0]).toBe(false) // Not rejected
+		expect(result[1]).not.toContain("terminated after exceeding")
+	})
+
+	it("should use default timeout when not specified (0 = no timeout)", async () => {
+		const options: ExecuteCommandOptions = {
+			executionId: "test-execution",
+			command: "echo test",
+			// commandExecutionTimeout not specified, should use default (0)
+		}
+
+		const quickProcess = Promise.resolve()
+		mockTerminal.runCommand.mockReturnValue(quickProcess)
+
+		await executeCommand(mockTask as Task, options)
+
+		// Should complete without issues using default (no timeout)
+		expect(mockTerminal.runCommand).toHaveBeenCalled()
+	})
+
+	it("should not timeout when commandExecutionTimeout is 0", async () => {
+		const options: ExecuteCommandOptions = {
+			executionId: "test-execution",
+			command: "sleep 10",
+			commandExecutionTimeout: 0, // No timeout
+		}
+
+		// Create a process that resolves after a delay to simulate a long-running command
+		const longRunningProcess = new Promise((resolve) => {
+			setTimeout(resolve, 200) // 200ms delay
+		})
+
+		mockTerminal.runCommand.mockReturnValue(longRunningProcess)
+
+		const result = await executeCommand(mockTask as Task, options)
+
+		// Should complete successfully without timeout
+		expect(result[0]).toBe(false) // Not rejected
+		expect(result[1]).not.toContain("terminated after exceeding")
+	})
+})

+ 43 - 0
src/core/tools/__tests__/executeCommandTool.spec.ts

@@ -1,6 +1,7 @@
 // npx vitest run src/core/tools/__tests__/executeCommandTool.spec.ts
 
 import type { ToolUsage } from "@roo-code/types"
+import * as vscode from "vscode"
 
 import { Task } from "../../task/Task"
 import { formatResponse } from "../../prompts/responses"
@@ -12,6 +13,12 @@ vitest.mock("execa", () => ({
 	execa: vitest.fn(),
 }))
 
+vitest.mock("vscode", () => ({
+	workspace: {
+		getConfiguration: vitest.fn(),
+	},
+}))
+
 vitest.mock("../../task/Task")
 vitest.mock("../../prompts/responses")
 
@@ -266,4 +273,40 @@ describe("executeCommandTool", () => {
 			expect(mockExecuteCommand).not.toHaveBeenCalled()
 		})
 	})
+
+	describe("Command execution timeout configuration", () => {
+		it("should include timeout parameter in ExecuteCommandOptions", () => {
+			// This test verifies that the timeout configuration is properly typed
+			// The actual timeout logic is tested in integration tests
+			// Note: timeout is stored internally in milliseconds but configured in seconds
+			const timeoutSeconds = 15
+			const options = {
+				executionId: "test-id",
+				command: "echo test",
+				commandExecutionTimeout: timeoutSeconds * 1000, // Convert to milliseconds
+			}
+
+			// Verify the options object has the expected structure
+			expect(options.commandExecutionTimeout).toBe(15000)
+			expect(typeof options.commandExecutionTimeout).toBe("number")
+		})
+
+		it("should handle timeout parameter in function signature", () => {
+			// Test that the executeCommand function accepts timeout parameter
+			// This is a compile-time check that the types are correct
+			const mockOptions = {
+				executionId: "test-id",
+				command: "echo test",
+				customCwd: undefined,
+				terminalShellIntegrationDisabled: false,
+				terminalOutputLineLimit: 500,
+				commandExecutionTimeout: 0,
+			}
+
+			// Verify all required properties exist
+			expect(mockOptions.executionId).toBeDefined()
+			expect(mockOptions.command).toBeDefined()
+			expect(mockOptions.commandExecutionTimeout).toBeDefined()
+		})
+	})
 })

+ 65 - 2
src/core/tools/executeCommandTool.ts

@@ -1,5 +1,6 @@
 import fs from "fs/promises"
 import * as path from "path"
+import * as vscode from "vscode"
 
 import delay from "delay"
 
@@ -14,6 +15,8 @@ import { unescapeHtmlEntities } from "../../utils/text-normalization"
 import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
 import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
 import { Terminal } from "../../integrations/terminal/Terminal"
+import { Package } from "../../shared/package"
+import { t } from "../../i18n"
 
 class ShellIntegrationError extends Error {}
 
@@ -62,12 +65,21 @@ export async function executeCommandTool(
 			const clineProviderState = await clineProvider?.getState()
 			const { terminalOutputLineLimit = 500, terminalShellIntegrationDisabled = false } = clineProviderState ?? {}
 
+			// Get command execution timeout from VSCode configuration (in seconds)
+			const commandExecutionTimeoutSeconds = vscode.workspace
+				.getConfiguration(Package.name)
+				.get<number>("commandExecutionTimeout", 0)
+
+			// Convert seconds to milliseconds for internal use
+			const commandExecutionTimeout = commandExecutionTimeoutSeconds * 1000
+
 			const options: ExecuteCommandOptions = {
 				executionId,
 				command,
 				customCwd,
 				terminalShellIntegrationDisabled,
 				terminalOutputLineLimit,
+				commandExecutionTimeout,
 			}
 
 			try {
@@ -113,6 +125,7 @@ export type ExecuteCommandOptions = {
 	customCwd?: string
 	terminalShellIntegrationDisabled?: boolean
 	terminalOutputLineLimit?: number
+	commandExecutionTimeout?: number
 }
 
 export async function executeCommand(
@@ -123,8 +136,11 @@ export async function executeCommand(
 		customCwd,
 		terminalShellIntegrationDisabled = false,
 		terminalOutputLineLimit = 500,
+		commandExecutionTimeout = 0,
 	}: ExecuteCommandOptions,
 ): Promise<[boolean, ToolResponse]> {
+	// Convert milliseconds back to seconds for display purposes
+	const commandExecutionTimeoutSeconds = commandExecutionTimeout / 1000
 	let workingDir: string
 
 	if (!customCwd) {
@@ -211,8 +227,55 @@ export async function executeCommand(
 	const process = terminal.runCommand(command, callbacks)
 	cline.terminalProcess = process
 
-	await process
-	cline.terminalProcess = undefined
+	// Implement command execution timeout (skip if timeout is 0)
+	if (commandExecutionTimeout > 0) {
+		let timeoutId: NodeJS.Timeout | undefined
+		let isTimedOut = false
+
+		const timeoutPromise = new Promise<void>((_, reject) => {
+			timeoutId = setTimeout(() => {
+				isTimedOut = true
+				// Try to abort the process
+				if (cline.terminalProcess) {
+					cline.terminalProcess.abort()
+				}
+				reject(new Error(`Command execution timed out after ${commandExecutionTimeout}ms`))
+			}, commandExecutionTimeout)
+		})
+
+		try {
+			await Promise.race([process, timeoutPromise])
+		} catch (error) {
+			if (isTimedOut) {
+				// Handle timeout case
+				const status: CommandExecutionStatus = { executionId, status: "timeout" }
+				clineProvider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
+
+				// Add visual feedback for timeout
+				await cline.say("text", t("common:command_timeout", { seconds: commandExecutionTimeoutSeconds }))
+
+				cline.terminalProcess = undefined
+
+				return [
+					false,
+					`The command was terminated after exceeding a user-configured ${commandExecutionTimeoutSeconds}s timeout. Do not try to re-run the command.`,
+				]
+			}
+			throw error
+		} finally {
+			if (timeoutId) {
+				clearTimeout(timeoutId)
+			}
+			cline.terminalProcess = undefined
+		}
+	} else {
+		// No timeout - just wait for the process to complete
+		try {
+			await process
+		} finally {
+			cline.terminalProcess = undefined
+		}
+	}
 
 	if (shellIntegrationError) {
 		throw new ShellIntegrationError(shellIntegrationError)

+ 1 - 0
src/i18n/locales/ca/common.json

@@ -71,6 +71,7 @@
 		"url_page_not_found": "No s'ha trobat la pàgina. Comprova si la URL és correcta.",
 		"url_fetch_failed": "Error en obtenir el contingut de la URL: {{error}}",
 		"url_fetch_error_with_url": "Error en obtenir contingut per {{url}}: {{error}}",
+		"command_timeout": "L'execució de la comanda ha superat el temps d'espera de {{seconds}} segons",
 		"share_task_failed": "Ha fallat compartir la tasca. Si us plau, torna-ho a provar.",
 		"share_no_active_task": "No hi ha cap tasca activa per compartir",
 		"share_auth_required": "Es requereix autenticació. Si us plau, inicia sessió per compartir tasques.",

+ 1 - 0
src/i18n/locales/de/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "Die Seite wurde nicht gefunden. Bitte prüfe, ob die URL korrekt ist.",
 		"url_fetch_failed": "Fehler beim Abrufen des URL-Inhalts: {{error}}",
 		"url_fetch_error_with_url": "Fehler beim Abrufen des Inhalts für {{url}}: {{error}}",
+		"command_timeout": "Zeitüberschreitung bei der Befehlsausführung nach {{seconds}} Sekunden",
 		"share_task_failed": "Teilen der Aufgabe fehlgeschlagen. Bitte versuche es erneut.",
 		"share_no_active_task": "Keine aktive Aufgabe zum Teilen",
 		"share_auth_required": "Authentifizierung erforderlich. Bitte melde dich an, um Aufgaben zu teilen.",

+ 1 - 0
src/i18n/locales/en/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "The page was not found. Please check if the URL is correct.",
 		"url_fetch_failed": "Failed to fetch URL content: {{error}}",
 		"url_fetch_error_with_url": "Error fetching content for {{url}}: {{error}}",
+		"command_timeout": "Command execution timed out after {{seconds}} seconds",
 		"share_task_failed": "Failed to share task. Please try again.",
 		"share_no_active_task": "No active task to share",
 		"share_auth_required": "Authentication required. Please sign in to share tasks.",

+ 1 - 0
src/i18n/locales/es/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "La página no fue encontrada. Por favor verifica si la URL es correcta.",
 		"url_fetch_failed": "Error al obtener el contenido de la URL: {{error}}",
 		"url_fetch_error_with_url": "Error al obtener contenido para {{url}}: {{error}}",
+		"command_timeout": "La ejecución del comando superó el tiempo de espera de {{seconds}} segundos",
 		"share_task_failed": "Error al compartir la tarea. Por favor, inténtalo de nuevo.",
 		"share_no_active_task": "No hay tarea activa para compartir",
 		"share_auth_required": "Se requiere autenticación. Por favor, inicia sesión para compartir tareas.",

+ 1 - 0
src/i18n/locales/fr/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "La page n'a pas été trouvée. Vérifie si l'URL est correcte.",
 		"url_fetch_failed": "Échec de récupération du contenu de l'URL : {{error}}",
 		"url_fetch_error_with_url": "Erreur lors de la récupération du contenu pour {{url}} : {{error}}",
+		"command_timeout": "L'exécution de la commande a expiré après {{seconds}} secondes",
 		"share_task_failed": "Échec du partage de la tâche. Veuillez réessayer.",
 		"share_no_active_task": "Aucune tâche active à partager",
 		"share_auth_required": "Authentification requise. Veuillez vous connecter pour partager des tâches.",

+ 1 - 0
src/i18n/locales/hi/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "पेज नहीं मिला। कृपया जांचें कि URL सही है।",
 		"url_fetch_failed": "URL सामग्री प्राप्त करने में त्रुटि: {{error}}",
 		"url_fetch_error_with_url": "{{url}} के लिए सामग्री प्राप्त करने में त्रुटि: {{error}}",
+		"command_timeout": "कमांड निष्पादन {{seconds}} सेकंड के बाद समय समाप्त हो गया",
 		"share_task_failed": "कार्य साझा करने में विफल। कृपया पुनः प्रयास करें।",
 		"share_no_active_task": "साझा करने के लिए कोई सक्रिय कार्य नहीं",
 		"share_auth_required": "प्रमाणीकरण आवश्यक है। कार्य साझा करने के लिए कृपया साइन इन करें।",

+ 1 - 0
src/i18n/locales/id/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "Halaman tidak ditemukan. Silakan periksa apakah URL sudah benar.",
 		"url_fetch_failed": "Gagal mengambil konten URL: {{error}}",
 		"url_fetch_error_with_url": "Error mengambil konten untuk {{url}}: {{error}}",
+		"command_timeout": "Eksekusi perintah waktu habis setelah {{seconds}} detik",
 		"share_task_failed": "Gagal membagikan tugas. Silakan coba lagi.",
 		"share_no_active_task": "Tidak ada tugas aktif untuk dibagikan",
 		"share_auth_required": "Autentikasi diperlukan. Silakan masuk untuk berbagi tugas.",

+ 1 - 0
src/i18n/locales/it/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "La pagina non è stata trovata. Verifica se l'URL è corretto.",
 		"url_fetch_failed": "Errore nel recupero del contenuto URL: {{error}}",
 		"url_fetch_error_with_url": "Errore nel recupero del contenuto per {{url}}: {{error}}",
+		"command_timeout": "Esecuzione del comando scaduta dopo {{seconds}} secondi",
 		"share_task_failed": "Condivisione dell'attività fallita. Riprova.",
 		"share_no_active_task": "Nessuna attività attiva da condividere",
 		"share_auth_required": "Autenticazione richiesta. Accedi per condividere le attività.",

+ 1 - 0
src/i18n/locales/ja/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "ページが見つかりませんでした。URLが正しいか確認してください。",
 		"url_fetch_failed": "URLコンテンツの取得に失敗しました:{{error}}",
 		"url_fetch_error_with_url": "{{url}} のコンテンツ取得エラー:{{error}}",
+		"command_timeout": "コマンドの実行が{{seconds}}秒後にタイムアウトしました",
 		"share_task_failed": "タスクの共有に失敗しました",
 		"share_no_active_task": "共有するアクティブなタスクがありません",
 		"share_auth_required": "認証が必要です。タスクを共有するにはサインインしてください。",

+ 1 - 0
src/i18n/locales/ko/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "페이지를 찾을 수 없습니다. URL이 올바른지 확인해 주세요.",
 		"url_fetch_failed": "URL 콘텐츠 가져오기 실패: {{error}}",
 		"url_fetch_error_with_url": "{{url}} 콘텐츠 가져오기 오류: {{error}}",
+		"command_timeout": "명령 실행 시간이 {{seconds}}초 후 초과되었습니다",
 		"share_task_failed": "작업 공유에 실패했습니다",
 		"share_no_active_task": "공유할 활성 작업이 없습니다",
 		"share_auth_required": "인증이 필요합니다. 작업을 공유하려면 로그인하세요.",

+ 1 - 0
src/i18n/locales/nl/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "De pagina werd niet gevonden. Controleer of de URL correct is.",
 		"url_fetch_failed": "Fout bij ophalen van URL-inhoud: {{error}}",
 		"url_fetch_error_with_url": "Fout bij ophalen van inhoud voor {{url}}: {{error}}",
+		"command_timeout": "Time-out bij uitvoeren van commando na {{seconds}} seconden",
 		"share_task_failed": "Delen van taak mislukt",
 		"share_no_active_task": "Geen actieve taak om te delen",
 		"share_auth_required": "Authenticatie vereist. Log in om taken te delen.",

+ 1 - 0
src/i18n/locales/pl/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "Strona nie została znaleziona. Sprawdź, czy URL jest poprawny.",
 		"url_fetch_failed": "Błąd pobierania zawartości URL: {{error}}",
 		"url_fetch_error_with_url": "Błąd pobierania zawartości dla {{url}}: {{error}}",
+		"command_timeout": "Przekroczono limit czasu wykonania polecenia po {{seconds}} sekundach",
 		"share_task_failed": "Nie udało się udostępnić zadania",
 		"share_no_active_task": "Brak aktywnego zadania do udostępnienia",
 		"share_auth_required": "Wymagana autoryzacja. Zaloguj się, aby udostępniać zadania.",

+ 1 - 0
src/i18n/locales/pt-BR/common.json

@@ -71,6 +71,7 @@
 		"url_page_not_found": "A página não foi encontrada. Verifique se a URL está correta.",
 		"url_fetch_failed": "Falha ao buscar conteúdo da URL: {{error}}",
 		"url_fetch_error_with_url": "Erro ao buscar conteúdo para {{url}}: {{error}}",
+		"command_timeout": "A execução do comando excedeu o tempo limite após {{seconds}} segundos",
 		"share_task_failed": "Falha ao compartilhar tarefa",
 		"share_no_active_task": "Nenhuma tarefa ativa para compartilhar",
 		"share_auth_required": "Autenticação necessária. Faça login para compartilhar tarefas.",

+ 1 - 0
src/i18n/locales/ru/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "Страница не найдена. Проверь правильность URL.",
 		"url_fetch_failed": "Ошибка получения содержимого URL: {{error}}",
 		"url_fetch_error_with_url": "Ошибка получения содержимого для {{url}}: {{error}}",
+		"command_timeout": "Время выполнения команды истекло через {{seconds}} секунд",
 		"share_task_failed": "Не удалось поделиться задачей",
 		"share_no_active_task": "Нет активной задачи для совместного использования",
 		"share_auth_required": "Требуется аутентификация. Войдите в систему для совместного доступа к задачам.",

+ 1 - 0
src/i18n/locales/tr/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "Sayfa bulunamadı. URL'nin doğru olup olmadığını kontrol et.",
 		"url_fetch_failed": "URL içeriği getirme hatası: {{error}}",
 		"url_fetch_error_with_url": "{{url}} için içerik getirme hatası: {{error}}",
+		"command_timeout": "Komut çalıştırma {{seconds}} saniye sonra zaman aşımına uğradı",
 		"share_task_failed": "Görev paylaşılamadı",
 		"share_no_active_task": "Paylaşılacak aktif görev yok",
 		"share_auth_required": "Kimlik doğrulama gerekli. Görevleri paylaşmak için lütfen giriş yapın.",

+ 1 - 0
src/i18n/locales/vi/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "Không tìm thấy trang. Vui lòng kiểm tra URL có đúng không.",
 		"url_fetch_failed": "Lỗi lấy nội dung URL: {{error}}",
 		"url_fetch_error_with_url": "Lỗi lấy nội dung cho {{url}}: {{error}}",
+		"command_timeout": "Thực thi lệnh đã hết thời gian chờ sau {{seconds}} giây",
 		"share_task_failed": "Không thể chia sẻ nhiệm vụ",
 		"share_no_active_task": "Không có nhiệm vụ hoạt động để chia sẻ",
 		"share_auth_required": "Cần xác thực. Vui lòng đăng nhập để chia sẻ nhiệm vụ.",

+ 1 - 0
src/i18n/locales/zh-CN/common.json

@@ -72,6 +72,7 @@
 		"url_page_not_found": "页面未找到。请检查 URL 是否正确。",
 		"url_fetch_failed": "获取 URL 内容失败:{{error}}",
 		"url_fetch_error_with_url": "获取 {{url}} 内容时出错:{{error}}",
+		"command_timeout": "命令执行超时,{{seconds}} 秒后",
 		"share_task_failed": "分享任务失败。请重试。",
 		"share_no_active_task": "没有活跃任务可分享",
 		"share_auth_required": "需要身份验证。请登录以分享任务。",

+ 1 - 0
src/i18n/locales/zh-TW/common.json

@@ -67,6 +67,7 @@
 		"url_page_not_found": "找不到頁面。請檢查 URL 是否正確。",
 		"url_fetch_failed": "取得 URL 內容失敗:{{error}}",
 		"url_fetch_error_with_url": "取得 {{url}} 內容時發生錯誤:{{error}}",
+		"command_timeout": "命令執行超時,{{seconds}} 秒後",
 		"share_task_failed": "分享工作失敗。請重試。",
 		"share_no_active_task": "沒有活躍的工作可分享",
 		"share_auth_required": "需要身份驗證。請登入以分享工作。",

+ 7 - 0
src/package.json

@@ -338,6 +338,13 @@
 					"default": [],
 					"description": "%commands.deniedCommands.description%"
 				},
+				"roo-cline.commandExecutionTimeout": {
+					"type": "number",
+					"default": 0,
+					"minimum": 0,
+					"maximum": 600,
+					"description": "%commands.commandExecutionTimeout.description%"
+				},
 				"roo-cline.vsCodeLmModelSelector": {
 					"type": "object",
 					"properties": {

+ 1 - 0
src/package.nls.ca.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Ordres que es poden executar automàticament quan 'Aprova sempre les operacions d'execució' està activat",
 	"commands.deniedCommands.description": "Prefixos d'ordres que seran automàticament denegats sense demanar aprovació. En cas de conflictes amb ordres permeses, la coincidència de prefix més llarga té prioritat. Afegeix * per denegar totes les ordres.",
+	"commands.commandExecutionTimeout.description": "Temps màxim en segons per esperar que l'execució de l'ordre es completi abans d'esgotar el temps (0 = sense temps límit, 1-600s, per defecte: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Configuració per a l'API del model de llenguatge VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "El proveïdor del model de llenguatge (p. ex. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "La família del model de llenguatge (p. ex. gpt-4)",

+ 1 - 0
src/package.nls.de.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Befehle, die automatisch ausgeführt werden können, wenn 'Ausführungsoperationen immer genehmigen' aktiviert ist",
 	"commands.deniedCommands.description": "Befehlspräfixe, die automatisch abgelehnt werden, ohne nach Genehmigung zu fragen. Bei Konflikten mit erlaubten Befehlen hat die längste Präfix-Übereinstimmung Vorrang. Füge * hinzu, um alle Befehle abzulehnen.",
+	"commands.commandExecutionTimeout.description": "Maximale Zeit in Sekunden, die auf den Abschluss der Befehlsausführung gewartet wird, bevor ein Timeout auftritt (0 = kein Timeout, 1-600s, Standard: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Einstellungen für die VSCode-Sprachmodell-API",
 	"settings.vsCodeLmModelSelector.vendor.description": "Der Anbieter des Sprachmodells (z.B. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "Die Familie des Sprachmodells (z.B. gpt-4)",

+ 1 - 0
src/package.nls.es.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Comandos que pueden ejecutarse automáticamente cuando 'Aprobar siempre operaciones de ejecución' está activado",
 	"commands.deniedCommands.description": "Prefijos de comandos que serán automáticamente denegados sin solicitar aprobación. En caso de conflictos con comandos permitidos, la coincidencia de prefijo más larga tiene prioridad. Añade * para denegar todos los comandos.",
+	"commands.commandExecutionTimeout.description": "Tiempo máximo en segundos para esperar que se complete la ejecución del comando antes de que expire (0 = sin tiempo límite, 1-600s, predeterminado: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Configuración para la API del modelo de lenguaje VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "El proveedor del modelo de lenguaje (ej. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "La familia del modelo de lenguaje (ej. gpt-4)",

+ 1 - 0
src/package.nls.fr.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Commandes pouvant être exécutées automatiquement lorsque 'Toujours approuver les opérations d'exécution' est activé",
 	"commands.deniedCommands.description": "Préfixes de commandes qui seront automatiquement refusés sans demander d'approbation. En cas de conflit avec les commandes autorisées, la correspondance de préfixe la plus longue a la priorité. Ajouter * pour refuser toutes les commandes.",
+	"commands.commandExecutionTimeout.description": "Temps maximum en secondes pour attendre que l'exécution de la commande se termine avant expiration (0 = pas de délai, 1-600s, défaut : 0s)",
 	"settings.vsCodeLmModelSelector.description": "Paramètres pour l'API du modèle de langage VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "Le fournisseur du modèle de langage (ex: copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "La famille du modèle de langage (ex: gpt-4)",

+ 1 - 0
src/package.nls.hi.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "वे कमांड जो स्वचालित रूप से निष्पादित की जा सकती हैं जब 'हमेशा निष्पादन संचालन को स्वीकृत करें' सक्रिय हो",
 	"commands.deniedCommands.description": "कमांड प्रीफिक्स जो स्वचालित रूप से अस्वीकार कर दिए जाएंगे बिना अनुमोदन मांगे। अनुमतित कमांड के साथ संघर्ष की स्थिति में, सबसे लंबा प्रीफिक्स मैच प्राथमिकता लेता है। सभी कमांड को अस्वीकार करने के लिए * जोड़ें।",
+	"commands.commandExecutionTimeout.description": "कमांड निष्पादन पूरा होने का इंतजार करने के लिए अधिकतम समय सेकंड में, समय समाप्त होने से पहले (0 = कोई समय सीमा नहीं, 1-600s, डिफ़ॉल्ट: 0s)",
 	"settings.vsCodeLmModelSelector.description": "VSCode भाषा मॉडल API के लिए सेटिंग्स",
 	"settings.vsCodeLmModelSelector.vendor.description": "भाषा मॉडल का विक्रेता (उदा. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)",

+ 1 - 0
src/package.nls.id.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Perintah yang dapat dijalankan secara otomatis ketika 'Selalu setujui operasi eksekusi' diaktifkan",
 	"commands.deniedCommands.description": "Awalan perintah yang akan otomatis ditolak tanpa meminta persetujuan. Jika terjadi konflik dengan perintah yang diizinkan, pencocokan awalan terpanjang akan diprioritaskan. Tambahkan * untuk menolak semua perintah.",
+	"commands.commandExecutionTimeout.description": "Waktu maksimum dalam detik untuk menunggu eksekusi perintah selesai sebelum timeout (0 = tanpa timeout, 1-600s, default: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Pengaturan untuk API Model Bahasa VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "Vendor dari model bahasa (misalnya copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "Keluarga dari model bahasa (misalnya gpt-4)",

+ 1 - 0
src/package.nls.it.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Comandi che possono essere eseguiti automaticamente quando 'Approva sempre le operazioni di esecuzione' è attivato",
 	"commands.deniedCommands.description": "Prefissi di comandi che verranno automaticamente rifiutati senza richiedere approvazione. In caso di conflitti con comandi consentiti, la corrispondenza del prefisso più lungo ha la precedenza. Aggiungi * per rifiutare tutti i comandi.",
+	"commands.commandExecutionTimeout.description": "Tempo massimo in secondi per attendere il completamento dell'esecuzione del comando prima del timeout (0 = nessun timeout, 1-600s, predefinito: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Impostazioni per l'API del modello linguistico VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "Il fornitore del modello linguistico (es. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "La famiglia del modello linguistico (es. gpt-4)",

+ 1 - 0
src/package.nls.ja.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "'常に実行操作を承認する'が有効な場合に自動実行できるコマンド",
 	"commands.deniedCommands.description": "承認を求めずに自動的に拒否されるコマンドプレフィックス。許可されたコマンドとの競合がある場合、最長プレフィックスマッチが優先されます。すべてのコマンドを拒否するには * を追加してください。",
+	"commands.commandExecutionTimeout.description": "コマンド実行の完了を待つ最大時間(秒)、タイムアウトまで(0 = タイムアウトなし、1-600秒、デフォルト: 0秒)",
 	"settings.vsCodeLmModelSelector.description": "VSCode 言語モデル API の設定",
 	"settings.vsCodeLmModelSelector.vendor.description": "言語モデルのベンダー(例:copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "言語モデルのファミリー(例:gpt-4)",

+ 1 - 0
src/package.nls.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Commands that can be auto-executed when 'Always approve execute operations' is enabled",
 	"commands.deniedCommands.description": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
+	"commands.commandExecutionTimeout.description": "Maximum time in seconds to wait for command execution to complete before timing out (0 = no timeout, 1-600s, default: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API",
 	"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",

+ 1 - 0
src/package.nls.ko.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "'항상 실행 작업 승인' 이 활성화되어 있을 때 자동으로 실행할 수 있는 명령어",
 	"commands.deniedCommands.description": "승인을 요청하지 않고 자동으로 거부될 명령어 접두사. 허용된 명령어와 충돌하는 경우 가장 긴 접두사 일치가 우선됩니다. 모든 명령어를 거부하려면 *를 추가하세요.",
+	"commands.commandExecutionTimeout.description": "명령어 실행이 완료되기를 기다리는 최대 시간(초), 타임아웃 전까지 (0 = 타임아웃 없음, 1-600초, 기본값: 0초)",
 	"settings.vsCodeLmModelSelector.description": "VSCode 언어 모델 API 설정",
 	"settings.vsCodeLmModelSelector.vendor.description": "언어 모델 공급자 (예: copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "언어 모델 계열 (예: gpt-4)",

+ 1 - 0
src/package.nls.nl.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Commando's die automatisch kunnen worden uitgevoerd wanneer 'Altijd goedkeuren uitvoerbewerkingen' is ingeschakeld",
 	"commands.deniedCommands.description": "Commando-prefixen die automatisch worden geweigerd zonder om goedkeuring te vragen. Bij conflicten met toegestane commando's heeft de langste prefix-match voorrang. Voeg * toe om alle commando's te weigeren.",
+	"commands.commandExecutionTimeout.description": "Maximale tijd in seconden om te wachten tot commando-uitvoering voltooid is voordat er een timeout optreedt (0 = geen timeout, 1-600s, standaard: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Instellingen voor VSCode Language Model API",
 	"settings.vsCodeLmModelSelector.vendor.description": "De leverancier van het taalmodel (bijv. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "De familie van het taalmodel (bijv. gpt-4)",

+ 1 - 0
src/package.nls.pl.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Polecenia, które mogą być wykonywane automatycznie, gdy włączona jest opcja 'Zawsze zatwierdzaj operacje wykonania'",
 	"commands.deniedCommands.description": "Prefiksy poleceń, które będą automatycznie odrzucane bez pytania o zatwierdzenie. W przypadku konfliktów z dozwolonymi poleceniami, najdłuższe dopasowanie prefiksu ma pierwszeństwo. Dodaj * aby odrzucić wszystkie polecenia.",
+	"commands.commandExecutionTimeout.description": "Maksymalny czas w sekundach oczekiwania na zakończenie wykonania polecenia przed przekroczeniem limitu czasu (0 = brak limitu czasu, 1-600s, domyślnie: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Ustawienia dla API modelu językowego VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "Dostawca modelu językowego (np. copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "Rodzina modelu językowego (np. gpt-4)",

+ 1 - 0
src/package.nls.pt-BR.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Comandos que podem ser executados automaticamente quando 'Sempre aprovar operações de execução' está ativado",
 	"commands.deniedCommands.description": "Prefixos de comandos que serão automaticamente negados sem solicitar aprovação. Em caso de conflitos com comandos permitidos, a correspondência de prefixo mais longa tem precedência. Adicione * para negar todos os comandos.",
+	"commands.commandExecutionTimeout.description": "Tempo máximo em segundos para aguardar a conclusão da execução do comando antes do timeout (0 = sem timeout, 1-600s, padrão: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Configurações para a API do modelo de linguagem do VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "O fornecedor do modelo de linguagem (ex: copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "A família do modelo de linguagem (ex: gpt-4)",

+ 1 - 0
src/package.nls.ru.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Команды, которые могут быть автоматически выполнены, когда включена опция 'Всегда подтверждать операции выполнения'",
 	"commands.deniedCommands.description": "Префиксы команд, которые будут автоматически отклонены без запроса подтверждения. В случае конфликтов с разрешенными командами приоритет имеет самое длинное совпадение префикса. Добавьте * чтобы отклонить все команды.",
+	"commands.commandExecutionTimeout.description": "Максимальное время в секундах для ожидания завершения выполнения команды до истечения времени ожидания (0 = без тайм-аута, 1-600с, по умолчанию: 0с)",
 	"settings.vsCodeLmModelSelector.description": "Настройки для VSCode Language Model API",
 	"settings.vsCodeLmModelSelector.vendor.description": "Поставщик языковой модели (например, copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "Семейство языковой модели (например, gpt-4)",

+ 1 - 0
src/package.nls.tr.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "'Her zaman yürütme işlemlerini onayla' etkinleştirildiğinde otomatik olarak yürütülebilen komutlar",
 	"commands.deniedCommands.description": "Onay istenmeden otomatik olarak reddedilecek komut önekleri. İzin verilen komutlarla çakışma durumunda en uzun önek eşleşmesi öncelik alır. Tüm komutları reddetmek için * ekleyin.",
+	"commands.commandExecutionTimeout.description": "Komut yürütmesinin tamamlanmasını beklemek için maksimum süre (saniye), zaman aşımından önce (0 = zaman aşımı yok, 1-600s, varsayılan: 0s)",
 	"settings.vsCodeLmModelSelector.description": "VSCode dil modeli API'si için ayarlar",
 	"settings.vsCodeLmModelSelector.vendor.description": "Dil modelinin sağlayıcısı (örn: copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "Dil modelinin ailesi (örn: gpt-4)",

+ 1 - 0
src/package.nls.vi.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "Các lệnh có thể được thực thi tự động khi 'Luôn phê duyệt các thao tác thực thi' được bật",
 	"commands.deniedCommands.description": "Các tiền tố lệnh sẽ được tự động từ chối mà không yêu cầu phê duyệt. Trong trường hợp xung đột với các lệnh được phép, việc khớp tiền tố dài nhất sẽ được ưu tiên. Thêm * để từ chối tất cả các lệnh.",
+	"commands.commandExecutionTimeout.description": "Thời gian tối đa tính bằng giây để chờ việc thực thi lệnh hoàn thành trước khi hết thời gian chờ (0 = không có thời gian chờ, 1-600s, mặc định: 0s)",
 	"settings.vsCodeLmModelSelector.description": "Cài đặt cho API mô hình ngôn ngữ VSCode",
 	"settings.vsCodeLmModelSelector.vendor.description": "Nhà cung cấp mô hình ngôn ngữ (ví dụ: copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "Họ mô hình ngôn ngữ (ví dụ: gpt-4)",

+ 1 - 0
src/package.nls.zh-CN.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "当启用'始终批准执行操作'时可以自动执行的命令",
 	"commands.deniedCommands.description": "将自动拒绝而无需请求批准的命令前缀。与允许命令冲突时,最长前缀匹配优先。添加 * 拒绝所有命令。",
+	"commands.commandExecutionTimeout.description": "等待命令执行完成的最大时间(秒),超时前(0 = 无超时,1-600秒,默认:0秒)",
 	"settings.vsCodeLmModelSelector.description": "VSCode 语言模型 API 的设置",
 	"settings.vsCodeLmModelSelector.vendor.description": "语言模型的供应商(例如:copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "语言模型的系列(例如:gpt-4)",

+ 1 - 0
src/package.nls.zh-TW.json

@@ -28,6 +28,7 @@
 	"configuration.title": "Roo Code",
 	"commands.allowedCommands.description": "當啟用'始終批准執行操作'時可以自動執行的命令",
 	"commands.deniedCommands.description": "將自動拒絕而無需請求批准的命令前綴。與允許命令衝突時,最長前綴匹配優先。新增 * 拒絕所有命令。",
+	"commands.commandExecutionTimeout.description": "等待命令執行完成的最大時間(秒),逾時前(0 = 無逾時,1-600秒,預設:0秒)",
 	"settings.vsCodeLmModelSelector.description": "VSCode 語言模型 API 的設定",
 	"settings.vsCodeLmModelSelector.vendor.description": "語言模型供應商(例如:copilot)",
 	"settings.vsCodeLmModelSelector.family.description": "語言模型系列(例如:gpt-4)",