-
Notifications
You must be signed in to change notification settings - Fork 3.4k
ux: add downloadable error diagnostics from chat errors #10188
Changes from 2 commits
8f46bc9
512f592
c7b9522
85bd021
59e77aa
73d4587
47408a5
ab41d9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3168,6 +3168,69 @@ export const webviewMessageHandler = async ( | |
| break | ||
| } | ||
|
|
||
| case "downloadErrorDiagnostics": { | ||
|
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 | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Diagnostics may capture wrong task's conversation historyThe Additional Locations (1) |
||
|
|
||
| default: { | ||
| // console.log(`Unhandled message type: ${message.type}`) | ||
| // | ||
|
|
||
| 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" | ||
|
|
@@ -112,6 +112,23 @@ export const ErrorRow = memo( | |
| return metadata + errorDetails | ||
| }, [errorDetails, version, provider, modelId]) | ||
|
|
||
| const handleDownloadDiagnostics = useCallback( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Error metadata captures current config, not error-time configThe Additional Locations (1) |
||
| (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 | ||
|
|
@@ -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" /> | ||
|
|
@@ -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> | ||
|
|
||
| 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") | ||
| }) | ||
| }) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.