From 27558b87d2b3c3ec9fa85c111ded6c66b4606fb3 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 17 Sep 2025 23:45:23 +0000 Subject: [PATCH] feat: implement enhanced logging feature for troubleshooting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add enhancedLoggingEnabled toggle in Settings → Info (About) section - Create logEnhancedError and logEnhanced utility functions - Add sanitization for sensitive data in logs - Integrate enhanced logging into API error handling - Add yellow warning banner when enhanced logging is enabled - Add comprehensive test coverage for logging functionality - Export disposeEnhancedLogging for proper cleanup Implements #8098 --- packages/types/src/global-settings.ts | 2 + src/core/task/Task.ts | 28 ++ src/core/webview/webviewMessageHandler.ts | 6 + src/shared/WebviewMessage.ts | 1 + src/utils/__tests__/enhancedLogging.spec.ts | 306 ++++++++++++++++++ src/utils/enhancedLogging.ts | 250 ++++++++++++++ webview-ui/src/components/chat/ChatView.tsx | 3 + .../common/EnhancedLoggingBanner.tsx | 53 +++ webview-ui/src/components/settings/About.tsx | 17 + .../src/context/ExtensionStateContext.tsx | 9 + webview-ui/src/i18n/locales/en/settings.json | 7 + 11 files changed, 682 insertions(+) create mode 100644 src/utils/__tests__/enhancedLogging.spec.ts create mode 100644 src/utils/enhancedLogging.ts create mode 100644 webview-ui/src/components/common/EnhancedLoggingBanner.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 7e79855f7e..08caea39da 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -136,6 +136,8 @@ export const globalSettingsSchema = z.object({ telemetrySetting: telemetrySettingsSchema.optional(), + enhancedLoggingEnabled: z.boolean().optional(), + mcpEnabled: z.boolean().optional(), enableMcpServerCreation: z.boolean().optional(), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cf16df8dcc..ab3979f405 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -73,6 +73,7 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" // utils import { calculateApiCostAnthropic } from "../../shared/cost" import { getWorkspacePath } from "../../utils/path" +import { logEnhancedError } from "../../utils/enhancedLogging" // prompts import { formatResponse } from "../prompts/responses" @@ -2182,6 +2183,19 @@ export class Task extends EventEmitter implements TaskLike { // Cline instance to finish aborting (error is thrown here when // any function in the for loop throws due to this.abort). if (!this.abandoned) { + // Log enhanced error details if enabled + const provider = this.providerRef.deref() + if (provider && !this.abort) { + logEnhancedError(provider.context, error, { + operation: "API Stream Processing", + provider: this.apiConfiguration.apiProvider, + model: this.api.getModel().id, + taskId: this.taskId, + streamPosition: "during streaming", + assistantMessageLength: assistantMessage.length, + }) + } + // If the stream failed, there's various states the task // could be in (i.e. could have streamed some tools the user // may have executed), so we just resort to replicating a @@ -2688,6 +2702,20 @@ export class Task extends EventEmitter implements TaskLike { this.isWaitingForFirstChunk = false const isContextWindowExceededError = checkContextWindowExceededError(error) + // Log enhanced error details if enabled + const provider = this.providerRef.deref() + if (provider) { + logEnhancedError(provider.context, error, { + operation: "API Request (First Chunk)", + provider: this.apiConfiguration.apiProvider, + model: this.api.getModel().id, + taskId: this.taskId, + retryAttempt, + isContextWindowError: isContextWindowExceededError, + contextTokens: this.getTokenUsage().contextTokens, + }) + } + // If it's a context window error and we haven't exceeded max retries for this error type if (isContextWindowExceededError && retryAttempt < MAX_CONTEXT_WINDOW_RETRIES) { console.warn( diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index accb66f6e9..6b28064f4f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2286,6 +2286,12 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break } + case "enhancedLoggingEnabled": { + const enabled = message.bool ?? false + await updateGlobalState("enhancedLoggingEnabled", enabled) + await provider.postStateToWebview() + break + } case "cloudButtonClicked": { // Navigate to the cloud tab. provider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" }) diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 93d0b9bc45..89a312e3ef 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -86,6 +86,7 @@ export interface WebviewMessage { | "allowedMaxCost" | "alwaysAllowSubtasks" | "alwaysAllowUpdateTodoList" + | "enhancedLoggingEnabled" | "autoCondenseContext" | "autoCondenseContextPercent" | "condensingApiConfigId" diff --git a/src/utils/__tests__/enhancedLogging.spec.ts b/src/utils/__tests__/enhancedLogging.spec.ts new file mode 100644 index 0000000000..6633057045 --- /dev/null +++ b/src/utils/__tests__/enhancedLogging.spec.ts @@ -0,0 +1,306 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import * as vscode from "vscode" +import { logEnhancedError, logEnhanced, disposeEnhancedLogging } from "../enhancedLogging" + +// Mock the Package module +vi.mock("../shared/package", () => ({ + Package: { + outputChannel: "Roo-Code", + }, +})) + +// Mock vscode module +vi.mock("vscode", () => ({ + window: { + createOutputChannel: vi.fn(), + }, + OutputChannel: vi.fn(), +})) + +describe("enhancedLogging", () => { + let mockContext: vscode.ExtensionContext + let mockOutputChannel: any + let mockGlobalState: any + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks() + + // Create mock output channel + mockOutputChannel = { + appendLine: vi.fn(), + append: vi.fn(), + clear: vi.fn(), + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + replace: vi.fn(), + name: "Roo-Code", + } + + // Mock window.createOutputChannel to return our mock + vi.mocked(vscode.window.createOutputChannel).mockReturnValue(mockOutputChannel as any) + + // Create mock global state + mockGlobalState = { + get: vi.fn(), + update: vi.fn(), + keys: vi.fn().mockReturnValue([]), + setKeysForSync: vi.fn(), + } + + // Create mock context + mockContext = { + globalState: mockGlobalState as any, + subscriptions: [], + workspaceState: {} as any, + extensionUri: {} as any, + extensionPath: "", + asAbsolutePath: vi.fn(), + storagePath: "", + globalStoragePath: "", + logPath: "", + extensionMode: 3, + extension: {} as any, + globalStorageUri: {} as any, + logUri: {} as any, + storageUri: {} as any, + secrets: {} as any, + environmentVariableCollection: {} as any, + languageModelAccessInformation: {} as any, + } + }) + + afterEach(() => { + // Dispose the output channel to reset module state + disposeEnhancedLogging() + vi.restoreAllMocks() + }) + + describe("logEnhancedError", () => { + it("should not log when enhanced logging is disabled", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return false + return defaultValue + }) + const error = new Error("Test error") + + // Act + logEnhancedError(mockContext, error) + + // Assert + expect(vscode.window.createOutputChannel).not.toHaveBeenCalled() + expect(mockOutputChannel.appendLine).not.toHaveBeenCalled() + }) + + it("should log error details when enhanced logging is enabled", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const error = new Error("Test error") + error.stack = "Error: Test error\n at test.js:1:1" + + // Act + logEnhancedError(mockContext, error, { + operation: "Test Operation", + provider: "test-provider", + }) + + // Assert + expect(vscode.window.createOutputChannel).toHaveBeenCalledWith("Roo-Code") + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("[ENHANCED LOGGING] Error occurred at"), + ) + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Operation: Test Operation") + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Provider: test-provider") + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Error Type: Error") + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Message: Test error") + expect(mockOutputChannel.show).toHaveBeenCalledWith(true) + }) + + it("should sanitize sensitive information", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const error = new Error("API key failed") + + // Act + logEnhancedError(mockContext, error, { + request: { + headers: { + authorization: "Bearer sk-abcdef123456", + apikey: "key-123456789", + }, + }, + }) + + // Assert + // Check that sensitive data is redacted in the output + const calls = mockOutputChannel.appendLine.mock.calls + + // The function should have been called multiple times + expect(calls.length).toBeGreaterThan(0) + + const allOutput = calls.map((call: any) => call[0]).join("\n") + expect(allOutput).toContain("[REDACTED") + expect(allOutput).not.toContain("sk-abcdef123456") + expect(allOutput).not.toContain("key-123456789") + }) + + it("should handle non-Error objects", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const error = { code: "NETWORK_ERROR", details: "Connection failed" } + + // Act + logEnhancedError(mockContext, error) + + // Assert + const calls = mockOutputChannel.appendLine.mock.calls + const allOutput = calls.map((call: any) => call[0]).join("\n") + expect(allOutput).toContain("NETWORK_ERROR") + expect(allOutput).toContain("Connection failed") + }) + + it("should handle string errors", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const error = "Simple error string" + + // Act + logEnhancedError(mockContext, error) + + // Assert + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("Error Message: Simple error string"), + ) + }) + + it("should include timestamp in logs", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const error = new Error("Test error") + const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z") + + // Act + logEnhancedError(mockContext, error) + + // Assert + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + expect.stringContaining("2024-01-01T12:00:00.000Z"), + ) + + dateSpy.mockRestore() + }) + }) + + describe("logEnhanced", () => { + it("should not log when enhanced logging is disabled", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return false + return defaultValue + }) + + // Act + logEnhanced(mockContext, "Test message") + + // Assert + expect(vscode.window.createOutputChannel).not.toHaveBeenCalled() + expect(mockOutputChannel.appendLine).not.toHaveBeenCalled() + }) + + it("should log message with INFO level by default", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z") + + // Act + logEnhanced(mockContext, "Test message") + + // Assert + expect(vscode.window.createOutputChannel).toHaveBeenCalledWith("Roo-Code") + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("[2024-01-01T12:00:00.000Z] [INFO] Test message") + + dateSpy.mockRestore() + }) + + it("should log message with specified level", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z") + + // Act + logEnhanced(mockContext, "Error occurred", "ERROR") + + // Assert + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[2024-01-01T12:00:00.000Z] [ERROR] Error occurred", + ) + + dateSpy.mockRestore() + }) + + it("should support different log levels", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z") + + // Act + logEnhanced(mockContext, "Debug info", "DEBUG") + logEnhanced(mockContext, "Warning message", "WARNING") + + // Assert + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("[2024-01-01T12:00:00.000Z] [DEBUG] Debug info") + expect(mockOutputChannel.appendLine).toHaveBeenCalledWith( + "[2024-01-01T12:00:00.000Z] [WARNING] Warning message", + ) + + dateSpy.mockRestore() + }) + }) + + describe("Output channel management", () => { + it("should reuse the same output channel for multiple logs", () => { + // Arrange + vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => { + if (key === "enhancedLoggingEnabled") return true + return defaultValue + }) + + // Act + logEnhanced(mockContext, "First message") + logEnhanced(mockContext, "Second message") + logEnhancedError(mockContext, new Error("Test error")) + + // Assert + expect(vscode.window.createOutputChannel).toHaveBeenCalledTimes(1) + expect(mockOutputChannel.appendLine).toHaveBeenCalled() + // logEnhanced doesn't call show(), only logEnhancedError does + expect(mockOutputChannel.show).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/src/utils/enhancedLogging.ts b/src/utils/enhancedLogging.ts new file mode 100644 index 0000000000..011074808f --- /dev/null +++ b/src/utils/enhancedLogging.ts @@ -0,0 +1,250 @@ +import * as vscode from "vscode" +import { Package } from "../shared/package" + +let outputChannel: vscode.OutputChannel | undefined + +/** + * Gets or creates the output channel for enhanced logging + */ +function getOutputChannel(): vscode.OutputChannel { + if (!outputChannel) { + outputChannel = vscode.window.createOutputChannel(Package.outputChannel) + } + return outputChannel +} + +/** + * Logs detailed error information to the VS Code Output channel when enhanced logging is enabled + * + * @param context - The vscode.ExtensionContext to access global state + * @param error - The error object or error information to log + * @param additionalContext - Optional additional context information + */ +export function logEnhancedError( + context: vscode.ExtensionContext, + error: unknown, + additionalContext?: { + operation?: string + provider?: string + model?: string + request?: any + response?: any + [key: string]: any + }, +): void { + // Check if enhanced logging is enabled + const enhancedLoggingEnabled = context.globalState.get("enhancedLoggingEnabled", false) + + if (!enhancedLoggingEnabled) { + return + } + + const channel = getOutputChannel() + const timestamp = new Date().toISOString() + + // Start logging the error + channel.appendLine("") + channel.appendLine("=".repeat(80)) + channel.appendLine(`[ENHANCED LOGGING] Error occurred at ${timestamp}`) + channel.appendLine("=".repeat(80)) + + // Log the operation context if provided + if (additionalContext?.operation) { + channel.appendLine(`Operation: ${additionalContext.operation}`) + } + + if (additionalContext?.provider) { + channel.appendLine(`Provider: ${additionalContext.provider}`) + } + + if (additionalContext?.model) { + channel.appendLine(`Model: ${additionalContext.model}`) + } + + // Log the error details + channel.appendLine("") + channel.appendLine("Error Details:") + channel.appendLine("-".repeat(40)) + + if (error instanceof Error) { + channel.appendLine(`Error Type: ${error.constructor.name}`) + channel.appendLine(`Message: ${error.message}`) + + if (error.stack) { + channel.appendLine("") + channel.appendLine("Stack Trace:") + channel.appendLine(error.stack) + } + + // Log any additional error properties + const errorObj = error as any + const standardProps = ["name", "message", "stack"] + const additionalProps = Object.keys(errorObj).filter((key) => !standardProps.includes(key)) + + if (additionalProps.length > 0) { + channel.appendLine("") + channel.appendLine("Additional Error Properties:") + for (const prop of additionalProps) { + try { + const value = errorObj[prop] + if (value !== undefined && value !== null) { + const displayValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value) + channel.appendLine(` ${prop}: ${displayValue}`) + } + } catch (e) { + channel.appendLine(` ${prop}: [Unable to serialize]`) + } + } + } + } else if (typeof error === "string") { + channel.appendLine(`Error Message: ${error}`) + } else if (error !== null && error !== undefined) { + try { + channel.appendLine(`Error Object: ${JSON.stringify(error, null, 2)}`) + } catch (e) { + channel.appendLine(`Error: ${String(error)}`) + } + } else { + channel.appendLine("Unknown error occurred (null or undefined)") + } + + // Log request details if provided + if (additionalContext?.request) { + channel.appendLine("") + channel.appendLine("Request Details:") + channel.appendLine("-".repeat(40)) + try { + // Sanitize sensitive information from request + const sanitizedRequest = sanitizeForLogging(additionalContext.request) + channel.appendLine(JSON.stringify(sanitizedRequest, null, 2)) + } catch (e) { + channel.appendLine("[Unable to serialize request]") + } + } + + // Log response details if provided + if (additionalContext?.response) { + channel.appendLine("") + channel.appendLine("Response Details:") + channel.appendLine("-".repeat(40)) + try { + // Sanitize sensitive information from response + const sanitizedResponse = sanitizeForLogging(additionalContext.response) + channel.appendLine(JSON.stringify(sanitizedResponse, null, 2)) + } catch (e) { + channel.appendLine("[Unable to serialize response]") + } + } + + // Log any other additional context + const excludedKeys = ["operation", "provider", "model", "request", "response"] + const otherContext = Object.keys(additionalContext || {}) + .filter((key) => !excludedKeys.includes(key)) + .reduce((acc, key) => { + acc[key] = additionalContext![key] + return acc + }, {} as any) + + if (Object.keys(otherContext).length > 0) { + channel.appendLine("") + channel.appendLine("Additional Context:") + channel.appendLine("-".repeat(40)) + try { + channel.appendLine(JSON.stringify(otherContext, null, 2)) + } catch (e) { + channel.appendLine("[Unable to serialize additional context]") + } + } + + channel.appendLine("") + channel.appendLine("=".repeat(80)) + channel.appendLine("") + + // Show the output channel to the user + channel.show(true) +} + +/** + * Sanitizes an object for logging by removing or masking sensitive information + */ +function sanitizeForLogging(obj: any): any { + if (obj === null || obj === undefined) { + return obj + } + + if (typeof obj !== "object") { + return obj + } + + if (Array.isArray(obj)) { + return obj.map((item) => sanitizeForLogging(item)) + } + + const sanitized: any = {} + const sensitiveKeys = [ + "apikey", + "api_key", + "apiKey", + "password", + "passwd", + "pwd", + "secret", + "token", + "auth", + "authorization", + "bearer", + "credential", + "credentials", + ] + + for (const [key, value] of Object.entries(obj)) { + const lowerKey = key.toLowerCase() + + // Check if this key contains sensitive information + if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) { + // Mask the value but show its type and partial info + if (typeof value === "string" && value.length > 0) { + sanitized[key] = `[REDACTED: ${value.slice(0, 4)}...${value.slice(-4)}]` + } else { + sanitized[key] = "[REDACTED]" + } + } else if (typeof value === "object" && value !== null) { + // Recursively sanitize nested objects + sanitized[key] = sanitizeForLogging(value) + } else { + sanitized[key] = value + } + } + + return sanitized +} + +/** + * Logs a simple enhanced message (not necessarily an error) + */ +export function logEnhanced( + context: vscode.ExtensionContext, + message: string, + level: "INFO" | "WARNING" | "ERROR" | "DEBUG" = "INFO", +): void { + const enhancedLoggingEnabled = context.globalState.get("enhancedLoggingEnabled", false) + + if (!enhancedLoggingEnabled) { + return + } + + const channel = getOutputChannel() + const timestamp = new Date().toISOString() + + channel.appendLine(`[${timestamp}] [${level}] ${message}`) +} + +/** + * Disposes the output channel if it exists + */ +export function disposeEnhancedLogging(): void { + if (outputChannel) { + outputChannel.dispose() + outputChannel = undefined + } +} diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index d358c68f1c..e6d6f83299 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -43,6 +43,7 @@ import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles" import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog" import TelemetryBanner from "../common/TelemetryBanner" +import EnhancedLoggingBanner from "../common/EnhancedLoggingBanner" import VersionIndicator from "../common/VersionIndicator" import { useTaskSearch } from "../history/useTaskSearch" import HistoryPreview from "../history/HistoryPreview" @@ -117,6 +118,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction {telemetrySetting === "unset" && } + {enhancedLoggingEnabled && } {(showAnnouncement || showAnnouncementModal) && ( { diff --git a/webview-ui/src/components/common/EnhancedLoggingBanner.tsx b/webview-ui/src/components/common/EnhancedLoggingBanner.tsx new file mode 100644 index 0000000000..2001e009ba --- /dev/null +++ b/webview-ui/src/components/common/EnhancedLoggingBanner.tsx @@ -0,0 +1,53 @@ +import { memo } from "react" +import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" + +import { useExtensionState } from "@src/context/ExtensionStateContext" +import { vscode } from "@src/utils/vscode" +import { useAppTranslation } from "@src/i18n/TranslationContext" + +const EnhancedLoggingBanner = () => { + const { t } = useAppTranslation() + const { enhancedLoggingEnabled, setEnhancedLoggingEnabled } = useExtensionState() + + const handleDisable = () => { + setEnhancedLoggingEnabled(false) + vscode.postMessage({ type: "enhancedLoggingEnabled", bool: false }) + } + + const handleOpenSettings = () => { + window.postMessage({ + type: "action", + action: "settingsButtonClicked", + values: { section: "about" }, + }) + } + + if (!enhancedLoggingEnabled) { + return null + } + + return ( +
+ {/* Close button (X) */} + + +
+ ⚠️ + {t("settings:about.enhancedLogging.bannerTitle")} +
+
+ {t("settings:about.enhancedLogging.bannerMessage")}{" "} + + {t("settings:about.enhancedLogging.bannerSettings")} + +
+
+ ) +} + +export default memo(EnhancedLoggingBanner) diff --git a/webview-ui/src/components/settings/About.tsx b/webview-ui/src/components/settings/About.tsx index 9afee12d72..0b86da06da 100644 --- a/webview-ui/src/components/settings/About.tsx +++ b/webview-ui/src/components/settings/About.tsx @@ -11,6 +11,7 @@ import { Package } from "@roo/package" import { vscode } from "@/utils/vscode" import { cn } from "@/lib/utils" import { Button } from "@/components/ui" +import { useExtensionState } from "@/context/ExtensionStateContext" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" @@ -22,6 +23,7 @@ type AboutProps = HTMLAttributes & { export const About = ({ telemetrySetting, setTelemetrySetting, className, ...props }: AboutProps) => { const { t } = useAppTranslation() + const { enhancedLoggingEnabled, setEnhancedLoggingEnabled } = useExtensionState() return (
@@ -57,6 +59,21 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro

+
+ { + const checked = e.target.checked === true + setEnhancedLoggingEnabled(checked) + vscode.postMessage({ type: "enhancedLoggingEnabled", bool: checked }) + }}> + {t("settings:footer.enhancedLogging.label")} + +

+ {t("settings:footer.enhancedLogging.description")} +

+
+
void includeTaskHistoryInEnhance?: boolean setIncludeTaskHistoryInEnhance: (value: boolean) => void + enhancedLoggingEnabled?: boolean + setEnhancedLoggingEnabled: (value: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -280,6 +282,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode global: {}, }) const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true) + const [enhancedLoggingEnabled, setEnhancedLoggingEnabled] = useState(false) const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), @@ -317,6 +320,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode if ((newState as any).includeTaskHistoryInEnhance !== undefined) { setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance) } + // Update enhancedLoggingEnabled if present in state message + if ((newState as any).enhancedLoggingEnabled !== undefined) { + setEnhancedLoggingEnabled((newState as any).enhancedLoggingEnabled) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -538,6 +545,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }, includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance, + enhancedLoggingEnabled, + setEnhancedLoggingEnabled, } return {children} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 1be824b37e..ff8999783a 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -801,6 +801,13 @@ "import": "Import", "export": "Export", "reset": "Reset" + }, + "enhancedLogging": { + "label": "Enhanced logging", + "description": "When enabled, writes detailed failure logs to the VS Code Output panel for troubleshooting. Logs remain local and are not uploaded.", + "bannerTitle": "Enhanced Logging Enabled", + "bannerMessage": "Detailed failure logs are being written to the VS Code Output panel. This may include sensitive information.", + "bannerSettings": "Disable in settings" } }, "thinkingBudget": {