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 2 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
95 changes: 86 additions & 9 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import type { Mock } from "vitest"
// Mock dependencies - must come before imports
vi.mock("../../../api/providers/fetchers/modelCache")

// Mock storage utilities used by debug/diagnostics handlers
vi.mock("../../../utils/storage", () => ({
getTaskDirectoryPath: vi.fn(async () => "/mock/task-dir"),
}))

import { webviewMessageHandler } from "../webviewMessageHandler"
import type { ClineProvider } from "../ClineProvider"
import { getModels } from "../../../api/providers/fetchers/modelCache"
Expand Down Expand Up @@ -41,15 +46,24 @@ const mockClineProvider = {

import { t } from "../../../i18n"

vi.mock("vscode", () => ({
window: {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
workspace: {
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
},
}))
vi.mock("vscode", () => {
const showInformationMessage = vi.fn()
const showErrorMessage = vi.fn()
const openTextDocument = vi.fn().mockResolvedValue({})
const showTextDocument = vi.fn().mockResolvedValue(undefined)

return {
window: {
showInformationMessage,
showErrorMessage,
showTextDocument,
},
workspace: {
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
openTextDocument,
},
}
})

vi.mock("../../../i18n", () => ({
t: vi.fn((key: string, args?: Record<string, any>) => {
Expand All @@ -72,14 +86,20 @@ vi.mock("../../../i18n", () => ({
vi.mock("fs/promises", () => {
const mockRm = vi.fn().mockResolvedValue(undefined)
const mockMkdir = vi.fn().mockResolvedValue(undefined)
const mockReadFile = vi.fn().mockResolvedValue("[]")
const mockWriteFile = vi.fn().mockResolvedValue(undefined)

return {
default: {
rm: mockRm,
mkdir: mockMkdir,
readFile: mockReadFile,
writeFile: mockWriteFile,
},
rm: mockRm,
mkdir: mockMkdir,
readFile: mockReadFile,
writeFile: mockWriteFile,
}
})

Expand Down Expand Up @@ -739,3 +759,60 @@ describe("webviewMessageHandler - mcpEnabled", () => {
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
})
})

describe("webviewMessageHandler - downloadErrorDiagnostics", () => {
beforeEach(() => {
vi.clearAllMocks()

// Ensure contextProxy has a globalStorageUri for the handler
;(mockClineProvider as any).contextProxy.globalStorageUri = { fsPath: "/mock/global/storage" }

// Provide a current task with a stable ID
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({
taskId: "test-task-id",
} as any)

// fileExistsAtPath should report that the history file exists
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
})

it("generates a diagnostics file with error metadata and history", async () => {
const readFileSpy = vi.spyOn(fs, "readFile").mockResolvedValue("[{}]" as any)
const writeFileSpy = vi.spyOn(fs, "writeFile").mockResolvedValue(undefined as any)

const openTextDocumentSpy = vi.spyOn(vscode.workspace, "openTextDocument")
const showTextDocumentSpy = vi.spyOn(vscode.window, "showTextDocument")

await webviewMessageHandler(mockClineProvider, {
type: "downloadErrorDiagnostics",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.2.3",
provider: "test-provider",
model: "test-model",
details: "Sample error details",
},
} as any)

// Ensure we attempted to read API history
expect(readFileSpy).toHaveBeenCalledWith(path.join("/mock/task-dir", "api_conversation_history.json"), "utf8")

// Ensure we wrote a diagnostics file with the expected header and JSON content
expect(writeFileSpy).toHaveBeenCalledTimes(1)
const [writtenPath, writtenContent] = writeFileSpy.mock.calls[0]
expect(String(writtenPath)).toContain("roo-diagnostics-")
expect(String(writtenContent)).toContain(
"// You can share this with with Roo Code Support to diagnose the issue faster",
)
Comment thread
brunobergher marked this conversation as resolved.
Outdated
expect(String(writtenContent)).toContain('"error":')
expect(String(writtenContent)).toContain('"history":')
expect(String(writtenContent)).toContain('"version": "1.2.3"')
expect(String(writtenContent)).toContain('"provider": "test-provider"')
expect(String(writtenContent)).toContain('"model": "test-model"')
expect(String(writtenContent)).toContain('"details": "Sample error details"')

// Ensure VS Code APIs were used to open the generated file
expect(openTextDocumentSpy).toHaveBeenCalledTimes(1)
expect(showTextDocumentSpy).toHaveBeenCalledTimes(1)
})
})
63 changes: 63 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3168,6 +3168,69 @@ export const webviewMessageHandler = async (
break
}

case "downloadErrorDiagnostics": {
Comment thread
daniel-lxs marked this conversation as resolved.
const currentTask = provider.getCurrentTask()
if (!currentTask) {
vscode.window.showErrorMessage("No active task to generate diagnostics for")
break
}

try {
const { getTaskDirectoryPath } = await import("../../utils/storage")
const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath
const taskDirPath = await getTaskDirectoryPath(globalStoragePath, currentTask.taskId)

// Load API conversation history from the same file used by openDebugApiHistory
const apiHistoryPath = path.join(taskDirPath, "api_conversation_history.json")
let history: unknown = []

if (await fileExistsAtPath(apiHistoryPath)) {
const content = await fs.readFile(apiHistoryPath, "utf8")
try {
history = JSON.parse(content)
} catch {
// If parsing fails, fall back to empty history but still generate diagnostics file
vscode.window.showErrorMessage("Failed to parse api_conversation_history.json")
}
}

const diagnostics = {
error: {
timestamp: message.values?.timestamp ?? new Date().toISOString(),
version: message.values?.version ?? "",
provider: message.values?.provider ?? "",
model: message.values?.model ?? "",
details: message.values?.details ?? "",
},
history,
}

// Prepend human-readable guidance comments before the JSON payload
const headerComment =
"// Please share this file with Roo Code Support (support@roocode.com) to diagnose the issue faster\n" +
"// Just make sure you're OK sharing the contents of the conversation below.\n\n"
const jsonContent = JSON.stringify(diagnostics, null, 2)
const fullContent = headerComment + jsonContent

// Create a temporary diagnostics file
const tmpDir = os.tmpdir()
const timestamp = Date.now()
const tempFileName = `roo-diagnostics-${currentTask.taskId.slice(0, 8)}-${timestamp}.json`
const tempFilePath = path.join(tmpDir, tempFileName)

await fs.writeFile(tempFilePath, fullContent, "utf8")

// Open the diagnostics file in VS Code
const doc = await vscode.workspace.openTextDocument(tempFilePath)
await vscode.window.showTextDocument(doc, { preview: true })
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
provider.log(`Error generating diagnostics: ${errorMessage}`)
vscode.window.showErrorMessage(`Failed to generate diagnostics: ${errorMessage}`)
}
break
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: Diagnostics may capture wrong task's conversation history

The downloadErrorDiagnostics handler uses getCurrentTask() to obtain the task ID for reading API conversation history. However, the ErrorRow component doesn't pass a task ID with the error, so if a user views an error from a completed/historical task while a different task is currently active, the diagnostics bundle will contain the API conversation history from the wrong task. This could lead to support receiving misleading diagnostic information where the error metadata doesn't match the conversation history.

Additional Locations (1)

Fix in Cursor Fix in Web


default: {
// console.log(`Unhandled message type: ${message.type}`)
//
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export interface WebviewMessage {
| "browserPanelDidLaunch"
| "openDebugApiHistory"
| "openDebugUiHistory"
| "downloadErrorDiagnostics"
| "requestClaudeCodeRateLimits"
text?: string
editedMessageContent?: string
Expand Down
25 changes: 23 additions & 2 deletions webview-ui/src/components/chat/ErrorRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useState, useCallback, memo, useMemo } from "react"
import { useTranslation } from "react-i18next"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { BookOpenText, MessageCircleWarning, Info, Copy, Check } from "lucide-react"
import { BookOpenText, MessageCircleWarning, Info, Copy, Check, Microscope } from "lucide-react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { vscode } from "@src/utils/vscode"
import CodeBlock from "../common/CodeBlock"
Expand Down Expand Up @@ -112,6 +112,23 @@ export const ErrorRow = memo(
return metadata + errorDetails
}, [errorDetails, version, provider, modelId])

const handleDownloadDiagnostics = useCallback(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: Error metadata captures current config, not error-time config

The formattedErrorDetails memo and handleDownloadDiagnostics callback use provider and modelId from the current extension state via useExtensionState() and useSelectedModel(). If the user changes their API provider/model after an error occurs and then views the old error's details dialog or downloads diagnostics, the metadata will reflect the current configuration rather than the configuration that was active when the error occurred. The error message text itself is preserved, but the metadata (provider, model, timestamp) will be incorrect, potentially confusing support when diagnosing the issue.

Additional Locations (1)

Fix in Cursor Fix in Web

(e: React.MouseEvent) => {
e.stopPropagation()
vscode.postMessage({
type: "downloadErrorDiagnostics",
values: {
timestamp: new Date().toISOString(),
version,
provider,
model: modelId,
details: errorDetails || "",
},
})
},
[version, provider, modelId, errorDetails],
)

// Default titles for different error types
const getDefaultTitle = () => {
if (title) return title
Expand Down Expand Up @@ -283,7 +300,7 @@ export const ErrorRow = memo(
</pre>
</div>
<DialogFooter>
<Button variant="secondary" onClick={handleCopyDetails}>
<Button variant="secondary" className="w-full" onClick={handleCopyDetails}>
{showDetailsCopySuccess ? (
<>
<Check className="size-3" />
Expand All @@ -296,6 +313,10 @@ export const ErrorRow = memo(
</>
)}
</Button>
<Button variant="secondary" className="w-full" onClick={handleDownloadDiagnostics}>
<Microscope className="size-3" />
{t("chat:errorDetails.diagnostics")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Expand Down
82 changes: 82 additions & 0 deletions webview-ui/src/components/chat/__tests__/ErrorRow.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react"

import { render, screen, fireEvent } from "@/utils/test-utils"
import { vscode } from "@/utils/vscode"

import { ErrorRow } from "../ErrorRow"

// Mock vscode webview messaging
vi.mock("@/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))

// Mock ExtensionState context
vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
version: "1.0.0",
apiConfiguration: {},
}),
}))

// Mock selected model hook
vi.mock("@/components/ui/hooks/useSelectedModel", () => ({
useSelectedModel: () => ({
provider: "test-provider",
id: "test-model",
}),
}))

// Mock i18n
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const map: Record<string, string> = {
"chat:error": "Error",
"chat:errorDetails.title": "Error Details",
"chat:errorDetails.copyToClipboard": "Copy to Clipboard",
"chat:errorDetails.copied": "Copied!",
"chat:errorDetails.downloadDiagnostics": "Download diagnostics info",
}
return map[key] ?? key
},
}),
initReactI18next: {
type: "3rdParty",
init: vi.fn(),
},
}))

describe("ErrorRow diagnostics download", () => {
it("sends downloadErrorDiagnostics message with error metadata", () => {
const mockPostMessage = vi.mocked(vscode.postMessage)

render(<ErrorRow type="error" message="Something went wrong" errorDetails="Detailed error body" />)

// Open the Error Details dialog via the info button
const infoButton = screen.getByRole("button", { name: "Error Details" })
fireEvent.click(infoButton)

// Click the Download diagnostics button
const downloadButton = screen.getByRole("button", { name: "Download diagnostics info" })
fireEvent.click(downloadButton)

expect(mockPostMessage).toHaveBeenCalled()
const call = mockPostMessage.mock.calls.find(([arg]) => arg.type === "downloadErrorDiagnostics")
expect(call).toBeTruthy()
if (!call) return

const payload = call[0] as { type: string; values?: any }
expect(payload.values).toBeTruthy()
if (!payload.values) return

expect(payload.values).toMatchObject({
version: "1.0.0",
provider: "test-provider",
model: "test-model",
})
// Timestamp is generated at runtime, but should be a string
expect(typeof payload.values.timestamp).toBe("string")
})
})
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/ca/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/de/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,9 @@
"error": "Error",
"errorDetails": {
"title": "Error Details",
"copyToClipboard": "Copy to Clipboard",
"copied": "Copied!"
"copyToClipboard": "Copy basic error info",
"copied": "Copied!",
"diagnostics": "Get detailed error info"
},
"diffError": {
"title": "Edit Unsuccessful"
Expand Down
3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/es/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion webview-ui/src/i18n/locales/fr/chat.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading