ux: add downloadable error diagnostics from chat errors (#10188)

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: daniel-lxs <ricciodaniel98@gmail.com>
Co-authored-by: Chris Estreich <cestreich@gmail.com>
This commit is contained in:
Bruno Bergher 2025-12-19 19:50:40 +00:00 committed by GitHub
parent 6e2b85214e
commit bb358fb8f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 525 additions and 30 deletions

View file

@ -0,0 +1,188 @@
// npx vitest src/core/webview/__tests__/diagnosticsHandler.spec.ts
import * as path from "path"
// Mock vscode first
vi.mock("vscode", () => {
const showErrorMessage = vi.fn()
const openTextDocument = vi.fn().mockResolvedValue({})
const showTextDocument = vi.fn().mockResolvedValue(undefined)
return {
window: {
showErrorMessage,
showTextDocument,
},
workspace: {
openTextDocument,
},
}
})
// Mock storage utilities
vi.mock("../../../utils/storage", () => ({
getTaskDirectoryPath: vi.fn(async () => "/mock/task-dir"),
}))
// Mock fs utilities
vi.mock("../../../utils/fs", () => ({
fileExistsAtPath: vi.fn(),
}))
// Mock fs/promises
vi.mock("fs/promises", () => {
const mockReadFile = vi.fn()
const mockWriteFile = vi.fn().mockResolvedValue(undefined)
return {
default: {
readFile: mockReadFile,
writeFile: mockWriteFile,
},
readFile: mockReadFile,
writeFile: mockWriteFile,
}
})
import * as vscode from "vscode"
import * as fs from "fs/promises"
import * as fsUtils from "../../../utils/fs"
import { generateErrorDiagnostics } from "../diagnosticsHandler"
describe("generateErrorDiagnostics", () => {
const mockLog = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
it("generates a diagnostics file with error metadata and history", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
vi.mocked(fs.readFile).mockResolvedValue('[{"role": "user", "content": "test"}]' as any)
const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.2.3",
provider: "test-provider",
model: "test-model",
details: "Sample error details",
},
log: mockLog,
})
expect(result.success).toBe(true)
expect(result.filePath).toContain("roo-diagnostics-")
// Verify we attempted to read API history
expect(fs.readFile).toHaveBeenCalledWith(path.join("/mock/task-dir", "api_conversation_history.json"), "utf8")
// Verify we wrote a diagnostics file with the expected content
expect(fs.writeFile).toHaveBeenCalledTimes(1)
const [writtenPath, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
// taskId.slice(0, 8) = "test-tas" from "test-task-id"
expect(String(writtenPath)).toContain("roo-diagnostics-test-tas")
expect(String(writtenContent)).toContain(
"// Please share this file with Roo Code Support (support@roocode.com) to diagnose the issue faster",
)
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"')
// Verify VS Code APIs were used to open the generated file
expect(vscode.workspace.openTextDocument).toHaveBeenCalledTimes(1)
expect(vscode.window.showTextDocument).toHaveBeenCalledTimes(1)
})
it("uses empty history when API history file does not exist", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.0.0",
provider: "test",
model: "test",
details: "error",
},
log: mockLog,
})
expect(result.success).toBe(true)
// Should not attempt to read file when it doesn't exist
expect(fs.readFile).not.toHaveBeenCalled()
// Verify empty history in output
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
expect(String(writtenContent)).toContain('"history": []')
})
it("uses default values when values are not provided", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
log: mockLog,
})
expect(result.success).toBe(true)
// Verify defaults in output
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
expect(String(writtenContent)).toContain('"version": ""')
expect(String(writtenContent)).toContain('"provider": ""')
expect(String(writtenContent)).toContain('"model": ""')
expect(String(writtenContent)).toContain('"details": ""')
})
it("handles JSON parse error gracefully", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(true as any)
vi.mocked(fs.readFile).mockResolvedValue("invalid json" as any)
const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.0.0",
provider: "test",
model: "test",
details: "error",
},
log: mockLog,
})
// Should still succeed but with empty history
expect(result.success).toBe(true)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to parse api_conversation_history.json")
// Verify empty history in output
const [, writtenContent] = vi.mocked(fs.writeFile).mock.calls[0]
expect(String(writtenContent)).toContain('"history": []')
})
it("returns error result when file write fails", async () => {
vi.mocked(fsUtils.fileExistsAtPath).mockResolvedValue(false as any)
vi.mocked(fs.writeFile).mockRejectedValue(new Error("Write failed"))
const result = await generateErrorDiagnostics({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
log: mockLog,
})
expect(result.success).toBe(false)
expect(result.error).toBe("Write failed")
expect(mockLog).toHaveBeenCalledWith("Error generating diagnostics: Write failed")
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Failed to generate diagnostics: Write failed")
})
})

View file

@ -5,6 +5,11 @@ import type { Mock } from "vitest"
// Mock dependencies - must come before imports
vi.mock("../../../api/providers/fetchers/modelCache")
// Mock the diagnosticsHandler module
vi.mock("../diagnosticsHandler", () => ({
generateErrorDiagnostics: vi.fn().mockResolvedValue({ success: true, filePath: "/tmp/diagnostics.json" }),
}))
import { webviewMessageHandler } from "../webviewMessageHandler"
import type { ClineProvider } from "../ClineProvider"
import { getModels } from "../../../api/providers/fetchers/modelCache"
@ -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>) => {
@ -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,
}
})
@ -90,6 +110,7 @@ import * as path from "path"
import * as fsUtils from "../../../utils/fs"
import { getWorkspacePath } from "../../../utils/path"
import { ensureSettingsDirectoryExists } from "../../../utils/globalContext"
import { generateErrorDiagnostics } from "../diagnosticsHandler"
import type { ModeConfig } from "@roo-code/types"
vi.mock("../../../utils/fs")
@ -739,3 +760,57 @@ 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)
})
it("calls generateErrorDiagnostics with correct parameters", async () => {
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)
// Verify generateErrorDiagnostics was called with the correct parameters
expect(generateErrorDiagnostics).toHaveBeenCalledTimes(1)
expect(generateErrorDiagnostics).toHaveBeenCalledWith({
taskId: "test-task-id",
globalStoragePath: "/mock/global/storage",
values: {
timestamp: "2025-01-01T00:00:00.000Z",
version: "1.2.3",
provider: "test-provider",
model: "test-model",
details: "Sample error details",
},
log: expect.any(Function),
})
})
it("shows error when no active task", async () => {
vi.mocked(mockClineProvider.getCurrentTask).mockReturnValue(null as any)
await webviewMessageHandler(mockClineProvider, {
type: "downloadErrorDiagnostics",
values: {},
} as any)
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("No active task to generate diagnostics for")
expect(generateErrorDiagnostics).not.toHaveBeenCalled()
})
})

View file

@ -0,0 +1,92 @@
import * as path from "path"
import * as os from "os"
import * as fs from "fs/promises"
import * as vscode from "vscode"
import { getTaskDirectoryPath } from "../../utils/storage"
import { fileExistsAtPath } from "../../utils/fs"
export interface ErrorDiagnosticsValues {
timestamp?: string
version?: string
provider?: string
model?: string
details?: string
}
export interface GenerateDiagnosticsParams {
taskId: string
globalStoragePath: string
values?: ErrorDiagnosticsValues
log: (message: string) => void
}
export interface GenerateDiagnosticsResult {
success: boolean
filePath?: string
error?: string
}
/**
* Generates an error diagnostics file containing error metadata and API conversation history.
* The file is created in the system temp directory and opened in VS Code for the user to review
* before sharing with support.
*/
export async function generateErrorDiagnostics(params: GenerateDiagnosticsParams): Promise<GenerateDiagnosticsResult> {
const { taskId, globalStoragePath, values, log } = params
try {
const taskDirPath = await getTaskDirectoryPath(globalStoragePath, 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: values?.timestamp ?? new Date().toISOString(),
version: values?.version ?? "",
provider: values?.provider ?? "",
model: values?.model ?? "",
details: 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-${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 })
return { success: true, filePath: tempFilePath }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
log(`Error generating diagnostics: ${errorMessage}`)
vscode.window.showErrorMessage(`Failed to generate diagnostics: ${errorMessage}`)
return { success: false, error: errorMessage }
}
}

View file

@ -25,6 +25,7 @@ import { saveTaskMessages } from "../task-persistence"
import { ClineProvider } from "./ClineProvider"
import { BrowserSessionPanelManager } from "./BrowserSessionPanelManager"
import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler"
import { generateErrorDiagnostics } from "./diagnosticsHandler"
import { changeLanguage, t } from "../../i18n"
import { Package } from "../../shared/package"
import { type RouterName, type ModelRecord, toRouterName } from "../../shared/api"
@ -3173,6 +3174,22 @@ export const webviewMessageHandler = async (
break
}
case "downloadErrorDiagnostics": {
const currentTask = provider.getCurrentTask()
if (!currentTask) {
vscode.window.showErrorMessage("No active task to generate diagnostics for")
break
}
await generateErrorDiagnostics({
taskId: currentTask.taskId,
globalStoragePath: provider.contextProxy.globalStorageUri.fsPath,
values: message.values,
log: (msg) => provider.log(msg),
})
break
}
default: {
// console.log(`Unhandled message type: ${message.type}`)
//

View file

@ -178,6 +178,7 @@ export interface WebviewMessage {
| "browserPanelDidLaunch"
| "openDebugApiHistory"
| "openDebugUiHistory"
| "downloadErrorDiagnostics"
| "requestClaudeCodeRateLimits"
text?: string
editedMessageContent?: string

View file

@ -1,7 +1,8 @@
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 } from "lucide-react"
import { BookOpenText, MessageCircleWarning, 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 +113,23 @@ export const ErrorRow = memo(
return metadata + errorDetails
}, [errorDetails, version, provider, modelId])
const handleDownloadDiagnostics = useCallback(
(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
@ -278,7 +296,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" />
@ -291,6 +309,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>

View file

@ -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.diagnostics": "Get detailed error 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 diagnostics button
const downloadButton = screen.getByRole("button", { name: "Get detailed error 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")
})
})

View file

@ -267,7 +267,8 @@
"link": "Details",
"title": "Detalls de l'error",
"copyToClipboard": "Copiar al porta-retalls",
"copied": "Copiat!"
"copied": "Copiat!",
"diagnostics": "Obtenir informació d'error detallada"
},
"powershell": {
"issues": "Sembla que estàs tenint problemes amb Windows PowerShell, si us plau consulta aquesta documentació per a més informació."

View file

@ -267,7 +267,8 @@
"link": "Details",
"title": "Fehlerdetails",
"copyToClipboard": "In Zwischenablage kopieren",
"copied": "Kopiert!"
"copied": "Kopiert!",
"diagnostics": "Detaillierte Fehlerinformationen abrufen"
},
"powershell": {
"issues": "Es scheint, dass du Probleme mit Windows PowerShell hast, bitte sieh dir dies an"

View file

@ -293,8 +293,9 @@
"errorDetails": {
"link": "Details",
"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"

View file

@ -267,7 +267,8 @@
"link": "Detalles",
"title": "Detalles del error",
"copyToClipboard": "Copiar al portapapeles",
"copied": "¡Copiado!"
"copied": "¡Copiado!",
"diagnostics": "Obtener información de error detallada"
},
"powershell": {
"issues": "Parece que estás teniendo problemas con Windows PowerShell, por favor consulta esta"

View file

@ -267,7 +267,8 @@
"link": "Détails",
"title": "Détails de l'erreur",
"copyToClipboard": "Copier dans le presse-papiers",
"copied": "Copié !"
"copied": "Copié !",
"diagnostics": "Obtenir les détails de l'erreur"
},
"powershell": {
"issues": "Il semble que vous rencontriez des problèmes avec Windows PowerShell, veuillez consulter ce"

View file

@ -267,7 +267,8 @@
"link": "विवरण",
"title": "त्रुटि विवरण",
"copyToClipboard": "क्लिपबोर्ड पर कॉपी करें",
"copied": "कॉपी किया गया!"
"copied": "कॉपी किया गया!",
"diagnostics": "विस्तृत त्रुटि जानकारी प्राप्त करें"
},
"powershell": {
"issues": "ऐसा लगता है कि आपको Windows PowerShell के साथ समस्याएँ हो रही हैं, कृपया इसे देखें"

View file

@ -303,7 +303,8 @@
"link": "Details",
"title": "Detail Kesalahan",
"copyToClipboard": "Salin ke Clipboard",
"copied": "Disalin!"
"copied": "Disalin!",
"diagnostics": "Dapatkan informasi kesalahan terperinci"
},
"powershell": {
"issues": "Sepertinya kamu mengalami masalah Windows PowerShell, silakan lihat ini"

View file

@ -267,7 +267,8 @@
"link": "Dettagli",
"title": "Dettagli errore",
"copyToClipboard": "Copia negli appunti",
"copied": "Copiato!"
"copied": "Copiato!",
"diagnostics": "Ottieni informazioni errore dettagliate"
},
"powershell": {
"issues": "Sembra che tu stia avendo problemi con Windows PowerShell, consulta questa"

View file

@ -267,7 +267,8 @@
"link": "詳細",
"title": "エラー詳細",
"copyToClipboard": "クリップボードにコピー",
"copied": "コピーしました!"
"copied": "コピーしました!",
"diagnostics": "詳細なエラー情報を取得"
},
"powershell": {
"issues": "Windows PowerShellに問題があるようです。こちらを参照してください"

View file

@ -267,7 +267,8 @@
"link": "자세히",
"title": "오류 세부 정보",
"copyToClipboard": "클립보드에 복사",
"copied": "복사됨!"
"copied": "복사됨!",
"diagnostics": "상세한 오류 정보 가져오기"
},
"powershell": {
"issues": "Windows PowerShell에 문제가 있는 것 같습니다. 다음을 참조하세요"

View file

@ -267,7 +267,8 @@
"link": "Details",
"title": "Foutdetails",
"copyToClipboard": "Naar klembord kopiëren",
"copied": "Gekopieerd!"
"copied": "Gekopieerd!",
"diagnostics": "Gedetailleerde foutinformatie ophalen"
},
"powershell": {
"issues": "Het lijkt erop dat je problemen hebt met Windows PowerShell, zie deze"

View file

@ -267,7 +267,8 @@
"link": "Details",
"title": "Szczegóły błędu",
"copyToClipboard": "Kopiuj do schowka",
"copied": "Skopiowano!"
"copied": "Skopiowano!",
"diagnostics": "Uzyskaj szczegółowe informacje o błędzie"
},
"powershell": {
"issues": "Wygląda na to, że masz problemy z Windows PowerShell, proszę zapoznaj się z tym"

View file

@ -267,7 +267,8 @@
"link": "Details",
"title": "Detalhes do erro",
"copyToClipboard": "Copiar para área de transferência",
"copied": "Copiado!"
"copied": "Copiado!",
"diagnostics": "Obter informações detalhadas do erro"
},
"powershell": {
"issues": "Parece que você está tendo problemas com o Windows PowerShell, por favor veja este"

View file

@ -268,7 +268,8 @@
"link": "Подробности",
"title": "Детали ошибки",
"copyToClipboard": "Скопировать в буфер обмена",
"copied": "Скопировано!"
"copied": "Скопировано!",
"diagnostics": "Получить подробную информацию об ошибке"
},
"powershell": {
"issues": "Похоже, у вас проблемы с Windows PowerShell, пожалуйста, ознакомьтесь с этим"

View file

@ -268,7 +268,8 @@
"link": "Details",
"title": "Hata Detayları",
"copyToClipboard": "Panoya Kopyala",
"copied": "Kopyalandı!"
"copied": "Kopyalandı!",
"diagnostics": "Ayrıntılı hata bilgisi al"
},
"powershell": {
"issues": "Windows PowerShell ile ilgili sorunlar yaşıyor gibi görünüyorsunuz, lütfen şu konuya bakın"

View file

@ -268,7 +268,8 @@
"link": "Chi tiết",
"title": "Chi tiết lỗi",
"copyToClipboard": "Sao chép vào clipboard",
"copied": "Đã sao chép!"
"copied": "Đã sao chép!",
"diagnostics": "Nhận thông tin lỗi chi tiết"
},
"powershell": {
"issues": "Có vẻ như bạn đang gặp vấn đề với Windows PowerShell, vui lòng xem"

View file

@ -268,7 +268,8 @@
"link": "详情",
"title": "错误详情",
"copyToClipboard": "复制到剪贴板",
"copied": "已复制!"
"copied": "已复制!",
"diagnostics": "获取详细错误信息"
},
"powershell": {
"issues": "看起来您遇到了Windows PowerShell问题请参阅此"

View file

@ -301,7 +301,8 @@
"link": "詳情",
"title": "錯誤詳細資訊",
"copyToClipboard": "複製到剪貼簿",
"copied": "已複製!"
"copied": "已複製!",
"diagnostics": "取得詳細錯誤資訊"
},
"powershell": {
"issues": "您似乎遇到了 Windows PowerShell 的問題,請參閱此說明文件"