feat(ai): render Markdown + feed game/Proton/Steam logs to the AI — 0.28.0

1) The explanation popup rendered raw Markdown (### / **). Switched to
   QTextEdit.setMarkdown and told the model to answer in Markdown.
2) On "Explain with AI", also collect recent Proton (~/steam-*.log) and Steam
   console logs (core/gamelogs.py — tail-read, size-bounded) and include them in
   the prompt so the model can correlate log errors with findings and pinpoint
   when things went wrong. Reference-fact matching runs over the logs too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 13:32:51 +02:00
parent e6d94fbd59
commit b59f202891
7 changed files with 141 additions and 10 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
__version__ = "0.27.1"
__version__ = "0.28.0"
+8 -6
View File
@@ -34,12 +34,14 @@ ANTHROPIC_VERSION = "2023-06-01"
SYSTEM_PROMPT = (
"You are RigDoctor's hardware-diagnostics assistant for Linux gamers. You are given the "
"structured findings RigDoctor collected from this machine, and a set of reference facts. "
"Explain in plain language what the findings mean, identify the most likely root cause of "
"any problem, and give concrete, ordered next steps (exact commands where useful). Base "
"your reasoning ONLY on the findings and reference facts provided — do not invent readings, "
"hardware, or log lines. Be concise and practical. Present fixes as suggestions, and clearly "
"warn before any step that could cause data loss or instability."
"structured findings RigDoctor collected from this machine — which may include recent game, "
"Proton, and system log excerpts — plus a set of reference facts. Explain in plain language "
"what they mean, correlate any log errors with the findings to pinpoint WHEN and WHY things "
"went wrong, identify the most likely root cause, and give concrete, ordered next steps "
"(exact commands where useful). Base your reasoning ONLY on the data and reference facts "
"provided — do not invent readings, hardware, or log lines. Be concise and practical. "
"Present fixes as suggestions, and clearly warn before any step that could cause data loss "
"or instability. Format your answer in Markdown."
)
+67
View File
@@ -0,0 +1,67 @@
"""Collect recent game / Proton / Steam logs to enrich an AI diagnostic (M14).
Reads logs that already exist on disk — no change to how the game is launched. Two reliable
sources: Proton's per-app log (``~/steam-<appid>.log``, written when ``PROTON_LOG=1``) and
Steam's own console log. Each is tail-read and size-bounded so the AI prompt stays small. The
text is fed to the AI alongside the findings so it can see *when* something went wrong (a
vkd3d/DXVK error, a crash line, the exit code) rather than only the sensor summary.
"""
from __future__ import annotations
import os
from pathlib import Path
# Steam keeps logs under its install root; ~/.steam/steam usually symlinks to the real one.
_STEAM_LOG_DIRS = ("~/.steam/steam/logs", "~/.local/share/Steam/logs", "~/.steam/root/logs")
_STEAM_LOG_FILES = ("console-linux.txt", "console_log.txt", "stderr.txt")
def _tail(path: Path, max_bytes: int) -> str:
"""Last ``max_bytes`` of a file, decoded leniently (empty string on error)."""
try:
size = path.stat().st_size
with path.open("rb") as fh:
if size > max_bytes:
fh.seek(size - max_bytes)
return fh.read().decode("utf-8", "replace")
except OSError:
return ""
def _proton_logs() -> list[Path]:
try:
logs = list(Path.home().glob("steam-*.log"))
except OSError:
return []
return sorted(logs, key=lambda p: p.stat().st_mtime, reverse=True)
def _steam_console() -> Path | None:
for directory in _STEAM_LOG_DIRS:
base = Path(os.path.expanduser(directory))
for name in _STEAM_LOG_FILES:
candidate = base / name
if candidate.exists():
return candidate
return None
def available() -> bool:
return bool(_proton_logs() or _steam_console())
def collect(max_bytes: int = 6000) -> str:
"""Recent Proton + Steam log tails as one labelled text block ('' if none)."""
sections: list[str] = []
protons = _proton_logs()
if protons:
tail = _tail(protons[0], max_bytes).strip()
if tail:
sections.append(f"--- Proton log ({protons[0].name}) ---\n{tail}")
console = _steam_console()
if console:
tail = _tail(console, max_bytes).strip()
if tail:
sections.append(f"--- Steam log ({console.name}) ---\n{tail}")
return "\n\n".join(sections)
+5 -2
View File
@@ -111,10 +111,13 @@ class DiagnosticDialog(QDialog):
threading.Thread(target=self._work_explain, daemon=True).start()
def _work_explain(self) -> None:
from ..core import ai
from ..core import ai, gamelogs
text = ai.format_findings(self._result.findings, header="Diagnostic findings:")
text += "\n\nCapture summary:\n" + render_summary(self._result.summary)
logs = gamelogs.collect()
if logs:
text += "\n\nRecent game/Proton/Steam logs (newest at the end):\n" + logs
self._explained.emit(ai.explain(text))
def _on_explained(self, result) -> None:
@@ -133,7 +136,7 @@ class DiagnosticDialog(QDialog):
view = QTextEdit()
view.setObjectName("Report")
view.setReadOnly(True)
view.setPlainText(text)
view.setMarkdown(text) # the model replies in Markdown — render it
lay.addWidget(view)
note = QLabel("AI-generated suggestions — verify before acting, especially anything that changes settings or data.")
note.setObjectName("Muted")