33c554c29f
Games page gains an "Import crash dump…" button (shown when an AI provider is configured) that parses a Proton/Wine minidump and explains it via the opt-in AI assistant. New stdlib core/minidump.py reads the MDMP streams (crash reason, faulting module, OS/CPU, module list), optionally enriched by minidump_stackwalk if installed. Adds ai_knowledge facts for exception codes + faulting-module signatures, a MinidumpDialog, and CLI parity via `rigdoctor ai dump <file>`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
6.9 KiB
Python
183 lines
6.9 KiB
Python
"""Results view for an imported crash dump (.dmp, M14): parsed summary + AI explanation.
|
|
|
|
Mirrors :class:`DiagnosticDialog` — the same opt-in, streamed "Explain with AI" flow (D24),
|
|
applied to a Windows minidump parsed by :mod:`core.minidump` instead of a sensor capture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import Qt, Signal
|
|
from PySide6.QtGui import QFont, QTextCursor
|
|
from PySide6.QtWidgets import (
|
|
QDialog,
|
|
QFrame,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QTextEdit,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from ..core import minidump
|
|
from .widgets import finding_card
|
|
|
|
|
|
class MinidumpDialog(QDialog):
|
|
_chunk = Signal(str) # streamed token delta (worker thread -> GUI)
|
|
_explained = Signal(object) # (ok, full_text) when the AI stream finishes
|
|
|
|
def __init__(self, report: minidump.MinidumpReport, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self._report = report
|
|
self._stream_view = None
|
|
self._stream_status = None
|
|
self._chunk.connect(self._on_chunk)
|
|
self._explained.connect(self._on_explained)
|
|
name = Path(report.path).name
|
|
self.setWindowTitle(f"Crash dump — {name}")
|
|
self.resize(660, 680)
|
|
|
|
root = QVBoxLayout(self)
|
|
root.setContentsMargins(20, 18, 20, 16)
|
|
root.setSpacing(14)
|
|
|
|
title = QLabel(f"Crash dump — {name}")
|
|
title.setObjectName("PageTitle")
|
|
root.addWidget(title)
|
|
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
scroll.setStyleSheet("background: transparent;")
|
|
body = QWidget()
|
|
col = QVBoxLayout(body)
|
|
col.setContentsMargins(0, 0, 0, 0)
|
|
col.setSpacing(10)
|
|
col.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
|
|
# Parsed summary (crash reason / faulting module / OS / CPU / modules) — monospace.
|
|
summary_head = QLabel("Dump summary")
|
|
summary_head.setStyleSheet("font-weight: 700; background: transparent;")
|
|
col.addWidget(summary_head)
|
|
summary = QLabel(minidump.to_text(report))
|
|
summary.setObjectName("Report")
|
|
summary.setFont(QFont("monospace"))
|
|
summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
summary.setWordWrap(False)
|
|
summary.setStyleSheet(
|
|
"background: #0d0f13; color: #cfd3da; border: 1px solid #2a2f39; "
|
|
"border-radius: 8px; padding: 10px;"
|
|
)
|
|
col.addWidget(summary)
|
|
|
|
findings = minidump.to_findings(report)
|
|
find_head = QLabel(f"Findings ({len(findings)})")
|
|
find_head.setStyleSheet("font-weight: 700; background: transparent;")
|
|
col.addWidget(find_head)
|
|
for finding in findings:
|
|
col.addWidget(finding_card(finding))
|
|
|
|
if report.stackwalk: # only when an external stackwalker was available
|
|
sw_head = QLabel("minidump_stackwalk output")
|
|
sw_head.setStyleSheet("font-weight: 700; background: transparent;")
|
|
col.addWidget(sw_head)
|
|
sw = QTextEdit()
|
|
sw.setObjectName("Report")
|
|
sw.setReadOnly(True)
|
|
sw.setFont(QFont("monospace"))
|
|
sw.setPlainText(report.stackwalk)
|
|
sw.setMinimumHeight(160)
|
|
col.addWidget(sw)
|
|
|
|
scroll.setWidget(body)
|
|
root.addWidget(scroll, 1)
|
|
|
|
buttons = QHBoxLayout()
|
|
self._explain_btn = QPushButton("Explain with AI")
|
|
self._explain_btn.clicked.connect(self._explain_with_ai)
|
|
from ..core import ai
|
|
self._explain_btn.setVisible(ai.is_configured()) # opt-in only; hidden if not set up
|
|
buttons.addWidget(self._explain_btn)
|
|
buttons.addStretch(1)
|
|
close = QPushButton("Close")
|
|
close.setObjectName("PrimaryButton")
|
|
close.clicked.connect(self.accept)
|
|
buttons.addWidget(close)
|
|
root.addLayout(buttons)
|
|
|
|
# --- AI explanation (M14, D24) — streamed; runs only on this button press ----------
|
|
def _explain_with_ai(self) -> None:
|
|
from ..core import ai
|
|
|
|
if not ai.is_local(): # cloud provider → explicit consent before sending data
|
|
confirm = QMessageBox.question(
|
|
self, "Send to AI provider",
|
|
f"This sends the parsed crash dump to {ai.provider_label()}.\n\nContinue?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No,
|
|
)
|
|
if confirm != QMessageBox.StandardButton.Yes:
|
|
return
|
|
self._explain_btn.setEnabled(False)
|
|
dialog = self._open_stream_dialog()
|
|
threading.Thread(target=self._work_explain, daemon=True).start()
|
|
dialog.exec() # streaming fills the view live via signals during this nested loop
|
|
self._stream_view = self._stream_status = None
|
|
self._explain_btn.setEnabled(True)
|
|
|
|
def _work_explain(self) -> None:
|
|
from ..core import ai
|
|
|
|
text = minidump.to_ai_text(self._report)
|
|
ok, reply = ai.explain_stream(text, on_chunk=lambda d: self._chunk.emit(d))
|
|
self._explained.emit((ok, reply))
|
|
|
|
def _on_chunk(self, delta: str) -> None:
|
|
if self._stream_view is None:
|
|
return
|
|
self._stream_view.moveCursor(QTextCursor.MoveOperation.End)
|
|
self._stream_view.insertPlainText(delta) # live plain text as tokens arrive
|
|
self._stream_view.ensureCursorVisible()
|
|
|
|
def _on_explained(self, result) -> None:
|
|
ok, text = result
|
|
if self._stream_view is not None:
|
|
if ok:
|
|
self._stream_view.setMarkdown(text) # re-render the finished answer as Markdown
|
|
else:
|
|
self._stream_view.setPlainText(f"AI explanation failed:\n\n{text}")
|
|
if self._stream_status is not None:
|
|
self._stream_status.setText(
|
|
"AI-generated suggestions — verify before acting, especially anything that changes "
|
|
"settings or data." if ok else "The request failed.")
|
|
|
|
def _open_stream_dialog(self) -> QDialog:
|
|
"""A live dialog the AI streams into; finalized to rendered Markdown when done."""
|
|
from ..core import ai
|
|
|
|
dlg = QDialog(self)
|
|
dlg.setWindowTitle(f"AI explanation — {ai.provider_label()}")
|
|
dlg.resize(620, 520)
|
|
lay = QVBoxLayout(dlg)
|
|
view = QTextEdit()
|
|
view.setObjectName("Report")
|
|
view.setReadOnly(True)
|
|
lay.addWidget(view)
|
|
status = QLabel("Streaming from the model…")
|
|
status.setObjectName("Muted")
|
|
status.setWordWrap(True)
|
|
lay.addWidget(status)
|
|
close = QPushButton("Close")
|
|
close.setObjectName("PrimaryButton")
|
|
close.clicked.connect(dlg.accept)
|
|
lay.addWidget(close, alignment=Qt.AlignmentFlag.AlignRight)
|
|
self._stream_view = view
|
|
self._stream_status = status
|
|
return dlg
|