- .gitea/workflows/tests.yml: run `unittest discover` on push + pull_request.
`core` job (stdlib install, GUI tests skip) is bulletproof; `gui-smoke` job
installs the GUI extra + offscreen Qt libs and runs the suite headless.
- release.yml: add a `test` job and `release: needs: test` so a push to main
can't publish if the tests fail.
No version bump — CI config only; nothing in the shipped app changed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ai.explain_stream(findings_text, on_chunk) streams token deltas and returns
(ok, full_text). Ollama: stream=True NDJSON; Claude: stream=True SSE (parse
content_block_delta text deltas). The diagnostic dialog opens an explanation
window immediately and fills it token-by-token via a _chunk signal, then
re-renders the finished answer as Markdown — no more multi-second freeze on a
local model. Non-streaming explain() kept for the CLI. Tests for both parsers;
verified live against qwen2.5:7b.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand diagnostic/report collection (all stored per-diagnostic, in the Report zip;
logs also fed to the AI on "Explain"):
- syslogs: nvidia-smi -q snapshot (driver/throttle/clocks/power/temps/PCIe/ECC/
retired pages) + display-server log auto-detected — Xorg.0.log on X11, or the
compositor user-journal slice (gnome-shell/kwin/sway/gamescope) on Wayland.
- diagstore: include the full M5 inventory (inventory.txt + .json) — invaluable
for larger/shared debugging. inventory.collect() degrades gracefully (no root
prompt). Best-effort throughout.
- Tests for nvidia/display + inventory in store; docs (M15/SPEC).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
core/syslogs.py gathers, scoped to the diagnostic window:
- kernel-log slice (journalctl -k): Xid, OOM, MCE, PCIe AER, thermal, hung tasks
- crashed-process records (coredumpctl): exe, signal, when
Stored as syslogs.txt in the diagnostic dir, included in the Report bundle, and
fed to the AI on "Explain" alongside the game logs. Best-effort (degrades if the
tools are missing/denied); treats journalctl's "-- No entries --" as empty.
Tests + docs (M15/SPEC).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One `logging_enabled` toggle (default off) gates everything (D25):
- core/applog.py: rotating app.log (no-op unless enabled); setup() at GUI/CLI start.
- core/diagstore.py: each diagnostic stored in DATA_DIR/diagnostics/<id>/ (capture,
result.json, report.txt, scoped gamelogs, ai/ records of exactly what was sent to
the model + which model + the reply). make_report() zips a diagnostic (+ app.log)
into DATA_DIR/reports/.
- diagnostic.finish()/analyze_crash() store when enabled; DiagnosticResult.dir.
- GUI: Settings → Logging toggle; "Report" button on the diagnostic dialog; AI
interactions recorded into the diagnostic dir on "Explain with AI".
- CLI: `rigdoctor bundle` (report is taken by the M4 health report).
- Tests for store/record_ai/make_report + applog gating; docs (D25, M15, Phase 8).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The model guessed "Rainbow Six Siege" for appID 2694490 (Path of Exile 2). We
already know the names locally, so ground it: steam.appid_names() maps appid→name
from the scanned library, and ai.build_prompt scans the text for app IDs and
injects a resolved glossary. Only locally-known IDs are listed; no network, no
fine-tuning. Tests + verified live (2694490 = Path of Exile 2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The user ran a game ~20s with no crash but the AI dredged up old log lines,
guessed the wrong game, and gave Windows advice. Fixes:
- Prompt now includes the real game name + capture duration + outcome (clean vs
crash), so the model uses the known game instead of guessing from log paths.
- gamelogs.collect(since=…): scope Steam-console lines by timestamp and skip a
stale per-app Proton log (mtime before the session) — no unrelated past run.
- ai_knowledge: flag benign Steam/Proton lines (libnvidia-ml.so.1 assertion,
routine minidumps, "fork without exec") as non-causal.
- System prompt: Linux-only steps (no "run as administrator"); don't manufacture
a problem on a clean run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Selecting the Ollama provider pre-fills the model field with the suggested
qwen2.5:7b (fits an 8 GB GPU at Q4; grounding makes a 7B sufficient). Won't
overwrite a model the user already typed. Constant ai.OLLAMA_SUGGESTED_MODEL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New optional module (D24): explains the collected findings in plain language,
contacted ONLY on an explicit user action (never automatic).
- core/ai.py: provider chosen explicitly (no default) — ollama (local) or claude
(Anthropic Messages API via stdlib urllib; key in keyring). Grounded prompt;
HTTP error parsing; one-shot (no thinking/caching — snappy).
- core/ai_knowledge.py: curated reference KB (Xid/SMART/Proton/tunables),
exact keyword/code match ("RAG-lite", no embeddings) injected into the prompt —
lifts local models, sharpens Claude. No fine-tuning.
- config: ai_provider/ai_model/ai_endpoint + keyring-backed AI key (generalized
the token keyring helpers).
- GUI: Settings → AI assistant (provider radios, model/endpoint/key, Save/Test);
"Explain with AI" button on the diagnostic dialog (consent prompt for cloud).
- CLI: `rigdoctor ai status|test|explain`.
- Docs: D24, SPEC/MODULES/ROADMAP (Phase 7); tests for providers/grounding/parse.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QRadioButton was unstyled, so the selected trigger option was invisible on the
dark theme. Added QRadioButton::indicator styling (accent ring + center dot when
checked) and explicit QCheckBox :checked/:disabled states. Bundle checkboxes stay
selectable even when already installed so the page isn't dead when all deps are
present.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The full installer experience as a GUI wizard (gui/setup_wizard.py): environment
summary → pick dependency bundles (from the catalog, grouped) → install missing
apt packages → choose recording trigger → readiness summary.
- Shown on first launch (config setup_done) and via `rigdoctor-gui --setup`;
re-runnable from Settings → Run setup wizard.
- install.sh launches it after a fresh install when a desktop session is present.
- catalog.by_bundle() groups components; config gains setup_done.
- Tests: by_bundle grouping + wizard construction smoke.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scope M12 down to a single shared-terminal mode (D23, amends D16):
- Share page rewritten terminal-only: host shares their PTY/shell; guest watches
and may type only if the host ticks "Allow the guest to type" (read-only
otherwise — the D9 consent exception). Terminal is larger; either side can pop
it full-screen (Esc to exit).
- Removed the read-only stats view + HTTP server (core/share.py) and the
`rigdoctor share serve` CLI; deleted their tests.
- Docs: D23 added; SPEC/MODULES/ROADMAP updated (M12 → done, terminal-only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The terminal view rendered monochrome (QPlainTextEdit.setPlainText), dropping
pyte's per-cell attributes. Rewritten as a QTextEdit that renders fg/bg/bold/
reverse per cell (block cursor = inverted cell), preserving scrollback. The
session already runs the host's $SHELL + config with TERM=xterm-256color, so
fish/ls/git/prompts now look the same as locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M6 leftovers (the watcher defers to M9's trigger-mode work):
- gameenv: check_gpu_powermizer (NVIDIA, X; degrades when the gpu target won't
resolve), check_wine (wine --version), check_steam_client (dpkg package version);
steam.client_version() helper.
- core/launchers.py: detect Lutris (read-only SQLite pga.db) and Heroic (Epic
legendary + GOG JSON) installed games; Game gained a `launcher` field.
- Games page + `rigdoctor games` list non-Steam games alongside Steam, tagged by
launcher; Run Diagnostic works on them (auto-launch stays Steam-only).
- Tests for launchers (synthetic Lutris db + Heroic json).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upgrade `rigdoctor monitor` from a basic redraw to a stdlib curses dashboard
(tui.py): current / session-min / session-max per sensor, grouped by subsystem,
with temperature & utilization color bands (GPU-lost flagged red). q quits,
r resets min/max. Plain full-screen redraw fallback on a non-TTY (--plain forces
it). Pure track()/band() helpers are unit-tested; curses path verified in a pty.
Completes the Monitoring bundle (M2 + M8).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape the IA so it reads by intent instead of a flat pile of pages.
- Grouped sidebar: Monitor / Diagnose / System / App (section headers).
- Renames: Health → System Health, Environment → Tuning, Logs → Recordings,
Setup → Settings.
- Settings absorbs Notifications (alerts) as a section; Notifications dropped as a
separate page (notifications_page.py removed; SetupPage gains the alerts card +
`changed` signal wired to the live alert monitor).
- Recordings is now a hub: a source dropdown to view any captured log (always-on /
last diagnostic / preserved crash) + Analyze-crash in place, plus the recorder
controls; status line now shows the captured game.
- main_window nav is data-driven (_NAV groups → _PAGES order → stack); show_page,
badges, and tray flows updated. GUI smoke test asserts the new page set.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QSystemTrayIcon applet (gui/tray.py, D13): menu with live CPU/GPU temp + memory
used/total, a status line, a Run Diagnostic submenu per detected game, plus Open
dashboard / Start-Stop recording / Snapshot-copy / Quit. Reuses the dashboard's
sample stream; drives existing MainWindow flows.
- MainWindow creates the tray when one is available; closing the window hides to
tray (Quit exits); setQuitOnLastWindowClosed(False) so dialogs don't quit it.
- app: `--tray` starts hidden for autostart.
- tests/test_gui_smoke.py: construct MainWindow headless + exercise the tray, so
a startup crash (like the 0.18.0 import bug) fails the build. Skips if no PySide6.
- docs: M10/M11 marked done in MODULES/ROADMAP.
Completes the Desktop UI bundle (M10 + M11).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recording indicator (0.18.0) used `from .core import diagnostic`, which
resolves to the non-existent rigdoctor.gui.core and crashed MainWindow on launch.
Fixed to `from ..core import diagnostic`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The live sample count wasn't useful at a glance. The sidebar badge now shows
just ● Recording + the game, plus a ⚠ GPU-lost line when detected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
While a capture runs, the sidebar shows a red "● Recording" badge on every page
with the game and live sample count (+ GPU-lost flag). A 1.5s poll of the
recorder status reflects captures started any way — manual record, a guided
diagnostic, or the Steam launch wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restore the GUI Inventory page (removed in 0.7.2 for the CLI). Sidebar Inventory
→ System/CPU/Firmware/Memory/GPU/Storage/Display cards, Copy Markdown / Save… /
Refresh; root-only dmidecode details (motherboard/BIOS/RAM) fill in after launch
elevation. Reuses the existing M5 core/inventory.py; CLI unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
D12 "build first" wrapper: `rigdoctor wrap %command%` (Steam launch option /
Lutris/Heroic wrapper field) auto-brackets a focused diagnostic around a game —
start a game-tagged capture on launch, clean stop on exit; a hard freeze leaves
it unterminated → flagged as a crash next launch.
- core/wrap.py: game name from SteamAppId, PATH-proof launch_option(), run()
that doesn't disturb an existing capture and returns the game's exit code.
- diagnostic.start() preserves an unanalyzed crash to diagnostic-crash.jsonl
before clearing, so auto-relaunch can't wipe an unseen crash; pending_crash/
analyze_crash check the archive first.
- GUI: "Auto-capture…" helper dialog (copyable launch-option string).
- Tests for wrap (name resolution, exit-code passthrough, no-double-start).
- docs: fix stale MODULES.md status column (M1/M3/M4/M5/M8/M10/M13 → done),
update ROADMAP/MODULES for the wrapper + crash detection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A focused capture that ends without a clean stop (no session-stop, no live
recorder) is treated as a likely hard freeze.
- core/diagnostic.py: pending_crash() detects the unterminated session;
acknowledge_crash() dismisses it; analyze_crash() combines the captured window
(final readings + GPU-lost) with a focused scan of the PREVIOUS (crashed) boot
+ SMART/driver/persistence/temps.
- health.check_previous_boot() scans `journalctl -k -b -1`; run_health_checks
gained include_journal to avoid double-scanning for the crash path.
- GUI: Games page shows a warning banner on launch for an interrupted diagnostic
with Analyze crash / Dismiss → results dialog.
- Tests for crash detection / clean-stop / acknowledge / in-progress.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the four headline gauges (GPU temp, GPU load, CPU temp, memory) with
HistoryGraph trend tiles: each plots its session history with the current value,
window min/max, a dashed warn-threshold line, and a kind-colored line (temp band
/ usage / accent). QPainter-drawn, no new dependency. Seeing changes over time is
more useful than the live-only snapshot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recording banner gave no guidance, so it wasn't clear what to do after
clicking Run Diagnostic.
- Start dialog now spells out the flow: play the game, reproduce the crash, then
Finish & analyze (data survives a hard freeze + reboot), with "Launch game &
start" (steam.launch_game via steam:// appid URL) or "Start without launching".
- Recording banner now states the next step, not just a sample count.
- steam.launch_game(appid): best-effort Steam launch (steam / xdg-open).
- Fix: escape "&" in button labels (Qt mnemonic) so "Finish & analyze" shows
correctly instead of "Finish _analyze".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the guided diagnostic (0.11.0 core/CLI) into the GUI:
- Each game row gets a "Run Diagnostic" button → starts a focused, game-tagged
capture and shows a recording banner (live sample count + GPU-lost indicator)
with Finish & analyze / Discard.
- Finishing runs core.diagnostic.finish() off the UI thread and opens a results
dialog (gui/diagnostic_dialog.py): window-scoped capture summary + findings
cards (reusing render_summary + finding_card).
- Banner restores on showEvent if a capture is still running (navigate away/back).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The seed use case end to end, orchestrating M3 + M4 (ARCHITECTURE §7.1).
- core/diagnostic.py: start(game) runs a focused, game-tagged capture into a
dedicated diagnostic log (window-scoped report, separate from the always-on
crash log); finish() stops it and combines the capture summary (M3) with the
health findings (M4). Game recorded as a log event so it survives crash+reboot.
- CLI: rigdoctor diagnose start --game/--appid | status | finish.
- recorder/record run gained an optional --game tag; reccontrol passes it through.
- Tests for game recovery + the finish() combination.
GUI/tray "Run Diagnostic" button and auto start/stop (D12 wrapper) come next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>