Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit 8f46bc9

Browse files
committed
Adds diagnostics functionality
1 parent d376869 commit 8f46bc9

6 files changed

Lines changed: 258 additions & 13 deletions

File tree

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import type { Mock } from "vitest"
55
// Mock dependencies - must come before imports
66
vi.mock("../../../api/providers/fetchers/modelCache")
77

8+
// Mock storage utilities used by debug/diagnostics handlers
9+
vi.mock("../../../utils/storage", () => ({
10+
getTaskDirectoryPath: vi.fn(async () => "/mock/task-dir"),
11+
}))
12+
813
import { webviewMessageHandler } from "../webviewMessageHandler"
914
import type { ClineProvider } from "../ClineProvider"
1015
import { getModels } from "../../../api/providers/fetchers/modelCache"
@@ -41,15 +46,24 @@ const mockClineProvider = {
4146

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

44-
vi.mock("vscode", () => ({
45-
window: {
46-
showInformationMessage: vi.fn(),
47-
showErrorMessage: vi.fn(),
48-
},
49-
workspace: {
50-
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
51-
},
52-
}))
49+
vi.mock("vscode", () => {
50+
const showInformationMessage = vi.fn()
51+
const showErrorMessage = vi.fn()
52+
const openTextDocument = vi.fn().mockResolvedValue({})
53+
const showTextDocument = vi.fn().mockResolvedValue(undefined)
54+
55+
return {
56+
window: {
57+
showInformationMessage,
58+
showErrorMessage,
59+
showTextDocument,
60+
},
61+
workspace: {
62+
workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
63+
openTextDocument,
64+
},
65+
}
66+
})
5367

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

7692
return {
7793
default: {
7894
rm: mockRm,
7995
mkdir: mockMkdir,
96+
readFile: mockReadFile,
97+
writeFile: mockWriteFile,
8098
},
8199
rm: mockRm,
82100
mkdir: mockMkdir,
101+
readFile: mockReadFile,
102+
writeFile: mockWriteFile,
83103
}
84104
})
85105

@@ -739,3 +759,60 @@ describe("webviewMessageHandler - mcpEnabled", () => {
739759
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
740760
})
741761
})
762+
763+
describe("webviewMessageHandler - downloadErrorDiagnostics", () => {
764+
beforeEach(() => {
765+
vi.clearAllMocks()
766+
767+
// Ensure contextProxy has a globalStorageUri for the handler
768+
;(mockClineProvider as any).contextProxy.globalStorageUri = { fsPath: "/mock/global/storage" }
769+
770+
// Provide a current task with a stable ID
771+
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue({
772+
taskId: "test-task-id",
773+
} as any)
774+
775+
// fileExistsAtPath should report that the history file exists
776+
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
777+
})
778+
779+
it("generates a diagnostics file with error metadata and history", async () => {
780+
const readFileSpy = vi.spyOn(fs, "readFile").mockResolvedValue("[{}]" as any)
781+
const writeFileSpy = vi.spyOn(fs, "writeFile").mockResolvedValue(undefined as any)
782+
783+
const openTextDocumentSpy = vi.spyOn(vscode.workspace, "openTextDocument")
784+
const showTextDocumentSpy = vi.spyOn(vscode.window, "showTextDocument")
785+
786+
await webviewMessageHandler(mockClineProvider, {
787+
type: "downloadErrorDiagnostics",
788+
values: {
789+
timestamp: "2025-01-01T00:00:00.000Z",
790+
version: "1.2.3",
791+
provider: "test-provider",
792+
model: "test-model",
793+
details: "Sample error details",
794+
},
795+
} as any)
796+
797+
// Ensure we attempted to read API history
798+
expect(readFileSpy).toHaveBeenCalledWith(path.join("/mock/task-dir", "api_conversation_history.json"), "utf8")
799+
800+
// Ensure we wrote a diagnostics file with the expected header and JSON content
801+
expect(writeFileSpy).toHaveBeenCalledTimes(1)
802+
const [writtenPath, writtenContent] = writeFileSpy.mock.calls[0]
803+
expect(String(writtenPath)).toContain("roo-diagnostics-")
804+
expect(String(writtenContent)).toContain(
805+
"// You can share this with with Roo Code Support to diagnose the issue faster",
806+
)
807+
expect(String(writtenContent)).toContain('"error":')
808+
expect(String(writtenContent)).toContain('"history":')
809+
expect(String(writtenContent)).toContain('"version": "1.2.3"')
810+
expect(String(writtenContent)).toContain('"provider": "test-provider"')
811+
expect(String(writtenContent)).toContain('"model": "test-model"')
812+
expect(String(writtenContent)).toContain('"details": "Sample error details"')
813+
814+
// Ensure VS Code APIs were used to open the generated file
815+
expect(openTextDocumentSpy).toHaveBeenCalledTimes(1)
816+
expect(showTextDocumentSpy).toHaveBeenCalledTimes(1)
817+
})
818+
})

src/core/webview/webviewMessageHandler.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3168,6 +3168,69 @@ export const webviewMessageHandler = async (
31683168
break
31693169
}
31703170

3171+
case "downloadErrorDiagnostics": {
3172+
const currentTask = provider.getCurrentTask()
3173+
if (!currentTask) {
3174+
vscode.window.showErrorMessage("No active task to generate diagnostics for")
3175+
break
3176+
}
3177+
3178+
try {
3179+
const { getTaskDirectoryPath } = await import("../../utils/storage")
3180+
const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath
3181+
const taskDirPath = await getTaskDirectoryPath(globalStoragePath, currentTask.taskId)
3182+
3183+
// Load API conversation history from the same file used by openDebugApiHistory
3184+
const apiHistoryPath = path.join(taskDirPath, "api_conversation_history.json")
3185+
let history: unknown = []
3186+
3187+
if (await fileExistsAtPath(apiHistoryPath)) {
3188+
const content = await fs.readFile(apiHistoryPath, "utf8")
3189+
try {
3190+
history = JSON.parse(content)
3191+
} catch {
3192+
// If parsing fails, fall back to empty history but still generate diagnostics file
3193+
vscode.window.showErrorMessage("Failed to parse api_conversation_history.json")
3194+
}
3195+
}
3196+
3197+
const diagnostics = {
3198+
error: {
3199+
timestamp: message.values?.timestamp ?? new Date().toISOString(),
3200+
version: message.values?.version ?? "",
3201+
provider: message.values?.provider ?? "",
3202+
model: message.values?.model ?? "",
3203+
details: message.values?.details ?? "",
3204+
},
3205+
history,
3206+
}
3207+
3208+
// Prepend human-readable guidance comments before the JSON payload
3209+
const headerComment =
3210+
"// You can share this with with Roo Code Support (support@roocode.com) to diagnose the issue faster\n" +
3211+
"// Make sure you're OK sharing the contents of the conversation below\n\n"
3212+
const jsonContent = JSON.stringify(diagnostics, null, 2)
3213+
const fullContent = headerComment + jsonContent
3214+
3215+
// Create a temporary diagnostics file
3216+
const tmpDir = os.tmpdir()
3217+
const timestamp = Date.now()
3218+
const tempFileName = `roo-diagnostics-${currentTask.taskId.slice(0, 8)}-${timestamp}.json`
3219+
const tempFilePath = path.join(tmpDir, tempFileName)
3220+
3221+
await fs.writeFile(tempFilePath, fullContent, "utf8")
3222+
3223+
// Open the diagnostics file in VS Code
3224+
const doc = await vscode.workspace.openTextDocument(tempFilePath)
3225+
await vscode.window.showTextDocument(doc, { preview: true })
3226+
} catch (error) {
3227+
const errorMessage = error instanceof Error ? error.message : String(error)
3228+
provider.log(`Error generating diagnostics: ${errorMessage}`)
3229+
vscode.window.showErrorMessage(`Failed to generate diagnostics: ${errorMessage}`)
3230+
}
3231+
break
3232+
}
3233+
31713234
default: {
31723235
// console.log(`Unhandled message type: ${message.type}`)
31733236
//

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export interface WebviewMessage {
178178
| "browserPanelDidLaunch"
179179
| "openDebugApiHistory"
180180
| "openDebugUiHistory"
181+
| "downloadErrorDiagnostics"
181182
| "requestClaudeCodeRateLimits"
182183
text?: string
183184
editedMessageContent?: string

webview-ui/src/components/chat/ErrorRow.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useState, useCallback, memo, useMemo } from "react"
22
import { useTranslation } from "react-i18next"
33
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
4-
import { BookOpenText, MessageCircleWarning, Info, Copy, Check } from "lucide-react"
4+
import { BookOpenText, MessageCircleWarning, Info, Copy, Check, Microscope } from "lucide-react"
55
import { useCopyToClipboard } from "@src/utils/clipboard"
66
import { vscode } from "@src/utils/vscode"
77
import CodeBlock from "../common/CodeBlock"
@@ -112,6 +112,23 @@ export const ErrorRow = memo(
112112
return metadata + errorDetails
113113
}, [errorDetails, version, provider, modelId])
114114

115+
const handleDownloadDiagnostics = useCallback(
116+
(e: React.MouseEvent) => {
117+
e.stopPropagation()
118+
vscode.postMessage({
119+
type: "downloadErrorDiagnostics",
120+
values: {
121+
timestamp: new Date().toISOString(),
122+
version,
123+
provider,
124+
model: modelId,
125+
details: errorDetails || "",
126+
},
127+
})
128+
},
129+
[version, provider, modelId, errorDetails],
130+
)
131+
115132
// Default titles for different error types
116133
const getDefaultTitle = () => {
117134
if (title) return title
@@ -283,7 +300,7 @@ export const ErrorRow = memo(
283300
</pre>
284301
</div>
285302
<DialogFooter>
286-
<Button variant="secondary" onClick={handleCopyDetails}>
303+
<Button variant="secondary" className="w-full" onClick={handleCopyDetails}>
287304
{showDetailsCopySuccess ? (
288305
<>
289306
<Check className="size-3" />
@@ -296,6 +313,10 @@ export const ErrorRow = memo(
296313
</>
297314
)}
298315
</Button>
316+
<Button variant="secondary" className="w-full" onClick={handleDownloadDiagnostics}>
317+
<Microscope className="size-3" />
318+
{t("chat:errorDetails.diagnostics")}
319+
</Button>
299320
</DialogFooter>
300321
</DialogContent>
301322
</Dialog>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React from "react"
2+
3+
import { render, screen, fireEvent } from "@/utils/test-utils"
4+
import { vscode } from "@/utils/vscode"
5+
6+
import { ErrorRow } from "../ErrorRow"
7+
8+
// Mock vscode webview messaging
9+
vi.mock("@/utils/vscode", () => ({
10+
vscode: {
11+
postMessage: vi.fn(),
12+
},
13+
}))
14+
15+
// Mock ExtensionState context
16+
vi.mock("@/context/ExtensionStateContext", () => ({
17+
useExtensionState: () => ({
18+
version: "1.0.0",
19+
apiConfiguration: {},
20+
}),
21+
}))
22+
23+
// Mock selected model hook
24+
vi.mock("@/components/ui/hooks/useSelectedModel", () => ({
25+
useSelectedModel: () => ({
26+
provider: "test-provider",
27+
id: "test-model",
28+
}),
29+
}))
30+
31+
// Mock i18n
32+
vi.mock("react-i18next", () => ({
33+
useTranslation: () => ({
34+
t: (key: string) => {
35+
const map: Record<string, string> = {
36+
"chat:error": "Error",
37+
"chat:errorDetails.title": "Error Details",
38+
"chat:errorDetails.copyToClipboard": "Copy to Clipboard",
39+
"chat:errorDetails.copied": "Copied!",
40+
"chat:errorDetails.downloadDiagnostics": "Download diagnostics info",
41+
}
42+
return map[key] ?? key
43+
},
44+
}),
45+
initReactI18next: {
46+
type: "3rdParty",
47+
init: vi.fn(),
48+
},
49+
}))
50+
51+
describe("ErrorRow diagnostics download", () => {
52+
it("sends downloadErrorDiagnostics message with error metadata", () => {
53+
const mockPostMessage = vi.mocked(vscode.postMessage)
54+
55+
render(<ErrorRow type="error" message="Something went wrong" errorDetails="Detailed error body" />)
56+
57+
// Open the Error Details dialog via the info button
58+
const infoButton = screen.getByRole("button", { name: "Error Details" })
59+
fireEvent.click(infoButton)
60+
61+
// Click the Download diagnostics button
62+
const downloadButton = screen.getByRole("button", { name: "Download diagnostics info" })
63+
fireEvent.click(downloadButton)
64+
65+
expect(mockPostMessage).toHaveBeenCalled()
66+
const call = mockPostMessage.mock.calls.find(([arg]) => arg.type === "downloadErrorDiagnostics")
67+
expect(call).toBeTruthy()
68+
if (!call) return
69+
70+
const payload = call[0] as { type: string; values?: any }
71+
expect(payload.values).toBeTruthy()
72+
if (!payload.values) return
73+
74+
expect(payload.values).toMatchObject({
75+
version: "1.0.0",
76+
provider: "test-provider",
77+
model: "test-model",
78+
})
79+
// Timestamp is generated at runtime, but should be a string
80+
expect(typeof payload.values.timestamp).toBe("string")
81+
})
82+
})

webview-ui/src/i18n/locales/en/chat.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,9 @@
287287
"error": "Error",
288288
"errorDetails": {
289289
"title": "Error Details",
290-
"copyToClipboard": "Copy to Clipboard",
291-
"copied": "Copied!"
290+
"copyToClipboard": "Copy basic error info",
291+
"copied": "Copied!",
292+
"diagnostics": "Get detailed error info"
292293
},
293294
"diffError": {
294295
"title": "Edit Unsuccessful"

0 commit comments

Comments
 (0)