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:
@@ -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)
|
||||
Reference in New Issue
Block a user