feat(ROO-538): add send error to support button for authenticated users

This commit is contained in:
Roo Code 2026-01-15 11:07:18 +00:00
parent 4ee494b08c
commit 4fc418c130
5 changed files with 186 additions and 6 deletions

View file

@ -507,6 +507,7 @@ export interface WebviewMessage {
| "openDebugApiHistory"
| "openDebugUiHistory"
| "downloadErrorDiagnostics"
| "sendErrorToSupport"
| "requestClaudeCodeRateLimits"
| "refreshCustomTools"
| "requestModes"

View file

@ -3310,6 +3310,78 @@ export const webviewMessageHandler = async (
break
}
case "sendErrorToSupport": {
const currentTask = provider.getCurrentTask()
if (!currentTask) {
vscode.window.showErrorMessage("No active task to send error details")
break
}
try {
// Get the cloud API URL from the message values
const cloudApiUrl =
message.values?.cloudApiUrl || process.env.ROO_CODE_CLOUD_URL || "https://app.roocode.com"
const contactEndpoint = `${cloudApiUrl}/api/contact/issue`
// Prepare the error details JSON
const errorData = {
timestamp: message.values?.timestamp || new Date().toISOString(),
version: message.values?.version || "",
provider: message.values?.provider || "",
model: message.values?.model || "",
details: message.values?.details || "",
taskId: currentTask.taskId,
}
// Create a FormData-like structure
const errorJson = JSON.stringify(errorData, null, 2)
const boundary = `----RooCodeBoundary${Date.now()}`
const body = [
`--${boundary}`,
'Content-Disposition: form-data; name="file"; filename="error-details.json"',
"Content-Type: application/json",
"",
errorJson,
`--${boundary}--`,
].join("\r\n")
// Get the session token from CloudService
const sessionToken = CloudService.hasInstance()
? CloudService.instance.authService?.getSessionToken()
: undefined
if (!sessionToken) {
vscode.window.showErrorMessage("You must be logged in to send error details to support")
break
}
// Make the authenticated POST request
const response = await fetch(contactEndpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${sessionToken}`,
"Content-Type": `multipart/form-data; boundary=${boundary}`,
},
body: body,
})
if (response.ok) {
vscode.window.showInformationMessage("Error details sent to Roo Code support successfully")
} else {
const errorText = await response.text()
provider.log(`Failed to send error details to support: ${response.status} ${errorText}`)
vscode.window.showErrorMessage("Failed to send error details to support. Please try again later.")
}
} catch (error) {
provider.log(
`Error sending error details to support: ${error instanceof Error ? error.message : String(error)}`,
)
vscode.window.showErrorMessage("Failed to send error details to support. Please try again later.")
}
break
}
default: {
// console.log(`Unhandled message type: ${message.type}`)
//

View file

@ -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, Copy, Check, Microscope, Info } from "lucide-react"
import { BookOpenText, MessageCircleWarning, Copy, Check, Microscope, Info, Send } from "lucide-react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { vscode } from "@src/utils/vscode"
@ -94,8 +94,9 @@ export const ErrorRow = memo(
const [showCopySuccess, setShowCopySuccess] = useState(false)
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false)
const [showDetailsCopySuccess, setShowDetailsCopySuccess] = useState(false)
const [showSendSuccess, setShowSendSuccess] = useState(false)
const { copyWithFeedback } = useCopyToClipboard()
const { version, apiConfiguration } = useExtensionState()
const { version, apiConfiguration, cloudIsAuthenticated, cloudApiUrl } = useExtensionState()
const { provider, id: modelId } = useSelectedModel(apiConfiguration)
const usesProxy = PROVIDERS.find((p) => p.value === provider)?.proxy ?? false
@ -133,6 +134,28 @@ export const ErrorRow = memo(
[version, provider, modelId, errorDetails],
)
const handleSendToSupport = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
vscode.postMessage({
type: "sendErrorToSupport",
values: {
timestamp: new Date().toISOString(),
version,
provider,
model: modelId,
details: errorDetails || "",
cloudApiUrl,
},
})
setShowSendSuccess(true)
setTimeout(() => {
setShowSendSuccess(false)
}, 2000)
},
[version, provider, modelId, errorDetails, cloudApiUrl],
)
// Default titles for different error types
const getDefaultTitle = () => {
if (title) return title
@ -307,6 +330,21 @@ export const ErrorRow = memo(
)}
</div>
<DialogFooter>
{cloudIsAuthenticated && (
<Button variant="secondary" className="w-full" onClick={handleSendToSupport}>
{showSendSuccess ? (
<>
<Check className="size-3" />
{t("chat:errorDetails.sent")}
</>
) : (
<>
<Send className="size-3" />
{t("chat:errorDetails.sendToSupport")}
</>
)}
</Button>
)}
<Button variant="secondary" className="w-full" onClick={handleCopyDetails}>
{showDetailsCopySuccess ? (
<>

View file

@ -13,11 +13,15 @@ vi.mock("@/utils/vscode", () => ({
}))
// Mock ExtensionState context
const mockExtensionState = {
version: "1.0.0",
apiConfiguration: {},
cloudIsAuthenticated: false,
cloudApiUrl: "https://app.roocode.com",
}
vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
version: "1.0.0",
apiConfiguration: {},
}),
useExtensionState: () => mockExtensionState,
}))
// Mock selected model hook
@ -38,6 +42,8 @@ vi.mock("react-i18next", () => ({
"chat:errorDetails.copyToClipboard": "Copy to Clipboard",
"chat:errorDetails.copied": "Copied!",
"chat:errorDetails.diagnostics": "Get detailed error info",
"chat:errorDetails.sendToSupport": "Send details to Roo Code support",
"chat:errorDetails.sent": "Sent!",
}
return map[key] ?? key
},
@ -79,4 +85,65 @@ describe("ErrorRow diagnostics download", () => {
// Timestamp is generated at runtime, but should be a string
expect(typeof payload.values.timestamp).toBe("string")
})
it("does not show send to support button when user is not authenticated", () => {
mockExtensionState.cloudIsAuthenticated = false
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)
// The send to support button should not be present
const sendButton = screen.queryByRole("button", { name: "Send details to Roo Code support" })
expect(sendButton).not.toBeInTheDocument()
})
it("shows send to support button when user is authenticated", () => {
mockExtensionState.cloudIsAuthenticated = true
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)
// The send to support button should be present
const sendButton = screen.getByRole("button", { name: "Send details to Roo Code support" })
expect(sendButton).toBeInTheDocument()
})
it("sends sendErrorToSupport message when authenticated user clicks send to support button", () => {
mockExtensionState.cloudIsAuthenticated = true
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 send to support button
const sendButton = screen.getByRole("button", { name: "Send details to Roo Code support" })
fireEvent.click(sendButton)
expect(mockPostMessage).toHaveBeenCalled()
const call = mockPostMessage.mock.calls.find(([arg]) => arg.type === "sendErrorToSupport")
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",
cloudApiUrl: "https://app.roocode.com",
})
// Timestamp is generated at runtime, but should be a string
expect(typeof payload.values.timestamp).toBe("string")
})
})

View file

@ -304,6 +304,8 @@
"copyToClipboard": "Copy basic error info",
"copied": "Copied!",
"diagnostics": "Get detailed error info",
"sendToSupport": "Send details to Roo Code support",
"sent": "Sent!",
"proxyProvider": "You seem to be using a proxy-based provider. Make sure to check its logs and to ensure it's not rewriting Roo's requests."
},
"diffError": {