diff --git a/apps/web/lib/api/games-performance.ts b/apps/web/lib/api/games-performance.ts index e3304b9..e1e9a8b 100644 --- a/apps/web/lib/api/games-performance.ts +++ b/apps/web/lib/api/games-performance.ts @@ -8,7 +8,7 @@ import { gamePlatformSupport, entryScreenshots, } from "@/lib/db/schema" -import { and, desc, eq, sql } from "drizzle-orm" +import { and, desc, eq } from "drizzle-orm" import { getR2PublicUrl } from "@/lib/storage" /** @@ -86,7 +86,6 @@ export const gamesPerformanceRoutes = new Elysia({ and( eq(gameVersions.gameId, gameId), eq(performanceEntries.isRemoved, false), - sql`${performanceEntries.settingsJson} IS NOT NULL`, ), ) .orderBy( diff --git a/plugins/decky-vault/main.py b/plugins/decky-vault/main.py index 0e562b2..15cf6c5 100644 --- a/plugins/decky-vault/main.py +++ b/plugins/decky-vault/main.py @@ -34,6 +34,7 @@ def parse_mangohud_log(log_content: str) -> dict: fps_col = 0 frametime_col = None gpu_power_col = None + cpu_power_col = None for i, line in enumerate(lines): stripped = line.strip() @@ -47,6 +48,8 @@ def parse_mangohud_log(log_content: str) -> dict: frametime_col = columns.index('frametime') if 'gpu_power' in columns: gpu_power_col = columns.index('gpu_power') + if 'cpu_power' in columns: + cpu_power_col = columns.index('cpu_power') header_idx = i break @@ -56,6 +59,7 @@ def parse_mangohud_log(log_content: str) -> dict: fps_values = [] frametime_values = [] gpu_power_values = [] + cpu_power_values = [] for line in lines[header_idx + 1:]: stripped = line.strip() @@ -71,6 +75,8 @@ def parse_mangohud_log(log_content: str) -> dict: frametime_values.append(float(parts[frametime_col])) if gpu_power_col is not None and gpu_power_col < len(parts): gpu_power_values.append(float(parts[gpu_power_col])) + if cpu_power_col is not None and cpu_power_col < len(parts): + cpu_power_values.append(float(parts[cpu_power_col])) except (ValueError, IndexError): continue @@ -96,8 +102,17 @@ def parse_mangohud_log(log_content: str) -> dict: fps_one_percent_low = round(sorted_fps[one_percent_idx], 1) tdp_watts = None - if gpu_power_values: - tdp_watts = round(sum(gpu_power_values) / len(gpu_power_values), 1) + total_power_values = [] + if gpu_power_values and cpu_power_values: + # Sum GPU and CPU power for total APU power + for gp, cp in zip(gpu_power_values, cpu_power_values): + total_power_values.append(gp + cp) + elif gpu_power_values: + total_power_values = gpu_power_values + elif cpu_power_values: + total_power_values = cpu_power_values + if total_power_values: + tdp_watts = round(sum(total_power_values) / len(total_power_values), 1) return { "fpsAvg": fps_avg, @@ -155,44 +170,46 @@ class Plugin: return self._settings async def check_mangohud(self) -> dict: - """RPC: Check if MangoHud is installed. Returns {installed: bool, path: str, version: str}.""" - import subprocess + """RPC: Check if MangoHud is installed.""" + import os + import re try: - result = subprocess.run( - ["which", "mangohud"], - capture_output=True, text=True, timeout=5 - ) - if result.returncode == 0: - mangohud_path = result.stdout.strip() - # Get version - version_result = subprocess.run( - ["mangohud", "--version"], - capture_output=True, text=True, timeout=5 - ) - version = version_result.stdout.strip() if version_result.returncode == 0 else "unknown" - return {"installed": True, "path": mangohud_path, "version": version} - else: + path = "/usr/bin/mangohud" + exists = os.path.exists(path) + if not exists: return {"installed": False, "path": "", "version": ""} + + # Read version from the shell script itself + version = "" + try: + with open(path, 'r') as f: + content = f.read() + # Look for the version line: echo v0.8.3-rc1-24-g33c2c7dd+ + m = re.search(r'echo\s+(v?[\d.]+[^\s]*)', content) + if m: + version = m.group(1) + if "-" in version: + version = version.split("-")[0] + except: + pass + + return {"installed": True, "path": path, "version": version} except Exception as e: return {"installed": False, "path": "", "version": "", "error": str(e)} async def write_mangohud_config(self) -> dict: - """RPC: Write the MangoHud logging config to ~/.config/MangoHud/MangoHud.conf. - Returns {success: bool, path: str, error: str?}.""" + """RPC: Write the MangoHud logging config and a wrapper script.""" try: home = os.path.expanduser("~") config_dir = os.path.join(home, ".config", "MangoHud") config_path = os.path.join(config_dir, "MangoHud.conf") os.makedirs(config_dir, exist_ok=True) - # MangoHud config that enables logging with the metrics we need. - # output_folder is required for logging to work. - # We log to /tmp so the plugin can read it after the session. config_content = """\ # DeckyVault MangoHud logging config output_folder=/tmp -output_file=deckyvault-mangohud.log -log_duration=0 +control=mangohud +autostart_log=1 fps frame_timing cpu_power @@ -204,7 +221,18 @@ benchmark_percentiles=97,AVG,1,0.1 with open(config_path, 'w') as f: f.write(config_content) - return {"success": True, "path": config_path} + # Write a wrapper script that forces MangoHud to use our config + wrapper_path = os.path.join(home, "deckyvault-mangohud.sh") + wrapper_content = """\ +#!/bin/bash +export MANGOHUD_CONFIGFILE="$HOME/.config/MangoHud/MangoHud.conf" +exec mangohud "$@" +""" + with open(wrapper_path, 'w') as f: + f.write(wrapper_content) + os.chmod(wrapper_path, 0o755) + + return {"success": True, "path": config_path, "wrapper": wrapper_path} except Exception as e: return {"success": False, "path": "", "error": str(e)} @@ -217,11 +245,111 @@ benchmark_percentiles=97,AVG,1,0.1 return {"exists": True, "content": f.read(), "path": config_path} return {"exists": False, "content": "", "path": config_path} - async def read_and_parse_mangohud_log(self, log_path: str = "/tmp/deckyvault-mangohud.log") -> dict: + async def start_mangohud_logging(self) -> dict: + """RPC: Start MangoHud logging via mangohudctl.""" + import subprocess + try: + result = subprocess.run( + ["mangohudctl", "set", "log_session", "true"], + capture_output=True, text=True, timeout=5 + ) + if result.returncode == 0: + return {"success": True} + else: + return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} + except FileNotFoundError: + return {"success": False, "error": "mangohudctl not found. Is MangoHud running?"} + except Exception as e: + return {"success": False, "error": str(e)} + + async def start_mangohud_logging(self) -> dict: + """RPC: Start MangoHud logging via mangohudctl. + Retries a few times in case MangoHud hasn't started yet.""" + import subprocess + import time + for attempt in range(5): + try: + result = subprocess.run( + ["mangohudctl", "set", "log_session", "true"], + capture_output=True, text=True, timeout=2 + ) + if result.returncode == 0: + return {"success": True} + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + if attempt < 4: + await asyncio.sleep(2) + return {"success": False, "error": "Could not connect to MangoHud. Is the game running?"} + + async def stop_mangohud_logging(self) -> dict: + """RPC: Stop MangoHud logging via mangohudctl. Best-effort.""" + import subprocess + try: + result = subprocess.run( + ["mangohudctl", "set", "log_session", "false"], + capture_output=True, text=True, timeout=2 + ) + if result.returncode == 0: + return {"success": True} + return {"success": False, "error": result.stderr.strip() or "mangohudctl failed"} + except Exception as e: + return {"success": False, "error": str(e)} + + async def _find_mangohud_log(self) -> str | None: + """Find the most recent MangoHud log file in /tmp/.""" + import glob + import time + candidates = [] + now = time.time() + for pattern in ["/tmp/*MangoHud*", "/tmp/*.csv", "/tmp/*.log"]: + for f in glob.glob(pattern): + if os.path.isdir(f): + continue + # Only consider files modified in the last hour + try: + mtime = os.path.getmtime(f) + if now - mtime > 3600: + continue + except: + pass + # Check if it looks like a MangoHud log + try: + with open(f, 'r') as fh: + first_lines = "".join(fh.readline() for _ in range(5)) + if 'fps' in first_lines.lower() or 'MangoHud' in first_lines: + candidates.append(f) + except: + candidates.append(f) # Add anyway if we can't read it + if not candidates: + return None + candidates.sort(key=lambda f: os.path.getmtime(f), reverse=True) + return candidates[0] + + async def debug_list_tmp(self) -> dict: + """RPC: List all files in /tmp/ for debugging.""" + import glob + files = [] + for f in glob.glob("/tmp/*"): + if os.path.isfile(f): + try: + mtime = os.path.getmtime(f) + size = os.path.getsize(f) + files.append({"name": os.path.basename(f), "size": size, "mtime": mtime}) + except: + pass + files.sort(key=lambda x: x["mtime"], reverse=True) + return {"files": files[:30]} + + async def read_and_parse_mangohud_log(self, log_path: str | None = None) -> dict: """RPC: Read the MangoHud log file and return parsed FPS stats. + If no log_path given, searches /tmp/ for the most recent MangoHud log. Returns parsed stats dict or {error: str}.""" + if log_path is None: + log_path = await self._find_mangohud_log() + if log_path is None: + return {"error": "No MangoHud log found in /tmp/. Make sure MangoHud is enabled and logging."} if not os.path.exists(log_path): - return {"error": f"MangoHud log not found at {log_path}. Make sure MangoHud is enabled and logging."} + return {"error": f"MangoHud log not found at {log_path}."} try: with open(log_path, 'r') as f: content = f.read() @@ -231,11 +359,17 @@ benchmark_percentiles=97,AVG,1,0.1 except Exception as e: return {"error": f"Failed to read log: {str(e)}"} - async def clear_mangohud_log(self, log_path: str = "/tmp/deckyvault-mangohud.log") -> dict: - """RPC: Delete the MangoHud log file so the next recording starts fresh.""" + async def clear_mangohud_log(self) -> dict: + """RPC: Delete all MangoHud log files in /tmp/ so the next recording starts fresh.""" + import glob try: - if os.path.exists(log_path): - os.remove(log_path) + for pattern in ["/tmp/*MangoHud*", "/tmp/*.csv", "/tmp/*.log"]: + for f in glob.glob(pattern): + if os.path.isfile(f): + try: + os.remove(f) + except (IOError, PermissionError): + pass return {"success": True} except Exception as e: return {"success": False, "error": str(e)} @@ -277,20 +411,80 @@ benchmark_percentiles=97,AVG,1,0.1 return "unknown" async def get_proton_version(self, app_id: int) -> str: - """RPC: Attempt to read the Proton version for a Steam app. - Reads from the Steam compatdata directory.""" + """RPC: Read the Proton version for a Steam app from config_info.""" try: home = os.path.expanduser("~") - # Steam compat data lives in ~/.steam/steam/steamapps/compatdata// - compat_path = os.path.join(home, ".steam", "steam", "steamapps", "compatdata", str(app_id)) - version_file = os.path.join(compat_path, "version") - if os.path.exists(version_file): - with open(version_file, 'r') as f: - return f.read().strip() + config_path = os.path.join(home, ".steam", "steam", "steamapps", "compatdata", str(app_id), "config_info") + if os.path.exists(config_path): + with open(config_path, 'r') as f: + lines = f.readlines() + if len(lines) >= 2: + proton_path = lines[1].strip() + import re + m = re.search(r'Proton[\s]+([\d.]+)', proton_path) + if m: + return m.group(1) + return proton_path.split("/")[-1] if proton_path else "" return "" except (IOError, FileNotFoundError): return "" + async def detect_current_game(self) -> dict: + """RPC: Detect the currently running game by checking processes. + Returns {appId: int?, name: str}.""" + import subprocess + import re + + home = os.path.expanduser("~") + steam_path = os.path.join(home, ".steam", "steam") + compat_dir = os.path.join(steam_path, "steamapps", "compatdata") + + # Get all running PIDs and their cmdlines + try: + r = subprocess.run( + ["ps", "-eo", "pid,args", "--no-headers"], + capture_output=True, text=True, timeout=3 + ) + if r.returncode != 0: + return {"appId": None, "name": ""} + all_procs = r.stdout + except: + return {"appId": None, "name": ""} + + # Check each compatdata directory for running processes + if os.path.exists(compat_dir): + for app_id_str in sorted(os.listdir(compat_dir), reverse=True): + if not app_id_str.isdigit(): + continue + # Check if this app has a running process by searching for the app ID + # in the process tree (Steam runtime includes app ID in some form) + try: + r = subprocess.run( + ["pgrep", "-f", app_id_str], + capture_output=True, timeout=2 + ) + if r.returncode == 0: + # Found a running game! Get its proper name from appmanifest + manifest_path = os.path.join(steam_path, "steamapps", f"appmanifest_{app_id_str}.acf") + if os.path.exists(manifest_path): + with open(manifest_path, 'r') as f: + content = f.read() + m = re.search(r'"name"\s+"([^"]+)"', content) + if m: + return {"appId": int(app_id_str), "name": m.group(1)} + return {"appId": int(app_id_str), "name": f"App {app_id_str}"} + except: + continue + + # Fallback: extract name from .exe path + for line in all_procs.split('\n'): + if '.exe' in line.lower() and 'proton' in line.lower(): + m = re.search(r'/([^/]+)\.exe', line, re.IGNORECASE) + if m: + return {"appId": None, "name": m.group(1)} + + return {"appId": None, "name": ""} + async def get_launch_options(self, app_id: int) -> str: """RPC: Read launch options for a Steam app from localconfig.vdf. This is best-effort — the VDF format is not officially documented.""" @@ -359,6 +553,8 @@ benchmark_percentiles=97,AVG,1,0.1 headers={ "Content-Type": "application/json", "x-api-key": api_key, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0", + "Accept": "application/json", }, method="POST" ) @@ -396,7 +592,10 @@ benchmark_percentiles=97,AVG,1,0.1 url = f"{base_url}/api/games/lookup?steamAppId=0" req = urllib.request.Request( url, - headers={"x-api-key": api_key}, + headers={ + "x-api-key": api_key, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; rv:136.0) Gecko/20100101 Firefox/136.0", + }, method="GET" ) context = _get_ssl_context() diff --git a/plugins/decky-vault/src/components/main-panel.tsx b/plugins/decky-vault/src/components/main-panel.tsx index 3636445..7be8d4b 100644 --- a/plugins/decky-vault/src/components/main-panel.tsx +++ b/plugins/decky-vault/src/components/main-panel.tsx @@ -3,14 +3,33 @@ import { ButtonItem, PanelSection, PanelSectionRow, + TextField, + DropdownItem, staticClasses, } from "@decky/ui" import { FaPlay, FaStop, FaClock, + FaCopy, + FaCheck, + FaTimes, + FaDownload, + FaCog, + FaFileExport, + FaFileImport, + FaSearch, } from "react-icons/fa" import type { RecordingState, SessionData, RecentSession, PluginSettings } from "../lib/store" +import { KNOWN_HARDWARE_SLUGS } from "@deckyvault/shared" +import { + testApiKey, + checkMangohud, + writeMangohudConfig, + getMangohudConfig, + exportConfig, + importConfig, +} from "../lib/api" import SessionForm from "./session-form" interface MainPanelProps { @@ -25,8 +44,15 @@ interface MainPanelProps { onAddToRecent: (sess: SessionData) => void onReset: () => void setError: (msg: string) => void + setGameName: (name: string, appId?: number) => void + onUpdateSetting: (key: K, value: string | null) => void } +const HARDWARE_OPTIONS = [ + { label: "Auto-detect", data: "" }, + ...KNOWN_HARDWARE_SLUGS.map((slug) => ({ label: slug, data: slug })), +] + export default function MainPanel({ recordingState, session, @@ -39,8 +65,26 @@ export default function MainPanel({ onAddToRecent, onReset, setError, + setGameName, + onUpdateSetting, }: MainPanelProps) { const [elapsed, setElapsed] = useState(0) + const [mangohudStatus, setMangohudStatus] = useState<{ + checked: boolean + installed: boolean + path: string + version: string + }>({ checked: false, installed: false, path: "", version: "" }) + const [keyTestStatus, setKeyTestStatus] = useState<"idle" | "testing" | "valid" | "invalid">("idle") + const [keyTestMessage, setKeyTestMessage] = useState("") + const [copiedLaunchOpt, setCopiedLaunchOpt] = useState(false) + const [configWritten, setConfigWritten] = useState(false) + const [configVerified, setConfigVerified] = useState<{ + checked: boolean + valid: boolean + message: string + }>({ checked: false, valid: false, message: "" }) + const [configStatus, setConfigStatus] = useState<{ message: string; isError: boolean } | null>(null) // Timer for recording state useEffect(() => { @@ -60,6 +104,100 @@ export default function MainPanel({ return `${m}:${s.toString().padStart(2, "0")}` } + async function handleCheckMangohud() { + const result = await checkMangohud() + setMangohudStatus({ + checked: true, + installed: result.installed, + path: result.path, + version: result.version, + }) + if (result.debug) { + console.log("[DeckyVault] MangoHud debug:", result.debug) + } + } + + async function handleTestKey() { + if (!settings.apiKey) { + setKeyTestStatus("invalid") + setKeyTestMessage("Enter an API key first") + return + } + setKeyTestStatus("testing") + setKeyTestMessage("") + const result = await testApiKey(settings.apiKey, settings.baseUrl) + if (result.valid) { + setKeyTestStatus("valid") + setKeyTestMessage("API key is valid") + } else { + setKeyTestStatus("invalid") + setKeyTestMessage(result.error || "Invalid API key") + } + } + + async function handleCopyLaunchOption() { + try { + await navigator.clipboard.writeText("~/deckyvault-mangohud.sh %command%") + setCopiedLaunchOpt(true) + setTimeout(() => setCopiedLaunchOpt(false), 2000) + } catch { + const ta = document.createElement("textarea") + ta.value = "~/deckyvault-mangohud.sh %command%" + document.body.appendChild(ta) + ta.select() + document.execCommand("copy") + document.body.removeChild(ta) + setCopiedLaunchOpt(true) + setTimeout(() => setCopiedLaunchOpt(false), 2000) + } + } + + async function handleWriteConfig() { + const result = await writeMangohudConfig() + setConfigWritten(result.success) + } + + async function handleVerifyConfig() { + const result = await getMangohudConfig() + if (!result.exists) { + setConfigVerified({ checked: true, valid: false, message: "No MangoHud config found. Write one first." }) + return + } + const content = result.content + const hasOutputFolder = content.includes("output_folder=/tmp") + const hasFps = content.includes("fps") + const hasFrameTiming = content.includes("frame_timing") + if (hasOutputFolder && hasFps) { + setConfigVerified({ checked: true, valid: true, message: "Config looks good" }) + } else { + setConfigVerified({ checked: true, valid: false, message: "Config is missing required settings. Write it again." }) + } + } + + async function handleExportConfig() { + setConfigStatus(null) + const result = await exportConfig(settings) + if (result.success) { + setConfigStatus({ message: `Config saved to ${result.path}`, isError: false }) + } else { + setConfigStatus({ message: result.error || "Export failed", isError: true }) + } + } + + async function handleImportConfig() { + setConfigStatus(null) + const result = await importConfig() + if (result.success && result.settings) { + onUpdateSetting("apiKey", result.settings.apiKey || "") + onUpdateSetting("exportPath", result.settings.exportPath || "/home/deck/Downloads") + onUpdateSetting("baseUrl", result.settings.baseUrl || "https://deckyvault.xyz") + onUpdateSetting("hardwareSlug", result.settings.hardwareSlug || null) + setConfigStatus({ message: "Config imported from Downloads", isError: false }) + } else { + setConfigStatus({ message: result.error || "No config file found in Downloads", isError: true }) + } + } + // ── Stopped state: show the session form ────────────────────── if (recordingState === "stopped") { return ( @@ -75,78 +213,270 @@ export default function MainPanel({ ) } - // ── Idle or Recording state ─────────────────────────────────── return ( - - {error && ( - -
- {error} -
-
- )} - - - {recordingState === "idle" ? ( - -
- - Start Recording -
-
- ) : ( - -
- - Stop Recording -
-
- )} -
- - {recordingState === "recording" && ( - <> + <> + {/* ── Recording ──────────────────────────────────────────── */} + + {error && ( -
-
- - {formatTime(elapsed)} -
-
- {session.gameName - ? `Recording: ${session.gameName}` - : "No game detected — recording anyway"} -
-
+
{error}
- - )} + )} - {recordingState === "idle" && ( - -
- Enable MangoHud for your game, then press Start Recording before launching. - Configure MangoHud in the Settings tab. -
-
- )} + {recordingState === "idle" && ( + <> + + setGameName(e.target.value)} + placeholder="e.g. Cyberpunk 2077" + /> + + + +
+ + Start Recording +
+
+
+ + )} - {recentSessions.length > 0 && recordingState === "idle" && ( - - {recentSessions.map((rs, i) => ( - -
- {rs.gameName || "Unknown game"} -
- - {rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} ·{" "} - {new Date(rs.date).toLocaleDateString()} - + {recordingState === "recording" && ( + <> + +
+
+ + {formatTime(elapsed)} +
+
+ {session.gameName ? `Recording: ${session.gameName}` : "Recording..."} +
- ))} - - )} - + + +
+ + Stop Recording +
+
+
+ + )} + + {recentSessions.length > 0 && recordingState === "idle" && ( + + {recentSessions.map((rs, i) => ( + +
+ {rs.gameName || "Unknown game"} +
+ + {rs.fpsAvg ? `${rs.fpsAvg} FPS avg` : "No data"} · {new Date(rs.date).toLocaleDateString()} + +
+
+ ))} +
+ )} + + + {/* ── Status ──────────────────────────────────────────────── */} + + + +
+ + Check MangoHud Status +
+
+
+ {mangohudStatus.checked && ( + +
+ {mangohudStatus.installed ? ( + <> MangoHud {mangohudStatus.version} + ) : ( + <> MangoHud not found + )} +
+
+ )} + {session.gameName && ( + +
+ Game: {session.gameName} + {session.appId && <> App ID: {session.appId}} +
+
+ )} + + + {keyTestStatus === "testing" ? "Testing..." : "Test API Key"} + {keyTestStatus === "valid" && } + {keyTestStatus === "invalid" && } + + + {keyTestMessage && ( + +
+ {keyTestMessage} +
+
+ )} +
+ + {/* ── Usage Instructions ──────────────────────────────────── */} + + +
+ Add this to your game's Steam launch options, then launch the game. Press Start Recording once you're in-game and ready to benchmark. +
+
+ +
+ ~/deckyvault-mangohud.sh %command% +
+
+ + +
+ + {copiedLaunchOpt ? "Copied to clipboard" : "Copy Launch Option"} +
+
+
+ +
+ Config stored in ~/.config/MangoHud/MangoHud.conf +
+
+
+ + {/* ── MangoHud Config ────────────────────────────────────── */} + + + +
+ + Write Config +
+
+
+ {configWritten && ( + +
+ Config written +
+
+ )} + + +
+ + Verify Config +
+
+
+ {configVerified.checked && ( + +
+ {configVerified.valid ? : } {configVerified.message} +
+
+ )} +
+ + {/* ── Configuration ────────────────────────────────────────── */} + + + onUpdateSetting("apiKey", e.target.value)} + placeholder="dv_..." + bIsPassword + /> + + + onUpdateSetting("exportPath", e.target.value)} + placeholder="/home/deck/Downloads" + /> + + + onUpdateSetting("baseUrl", e.target.value)} + placeholder="https://deckyvault.xyz" + /> + + + onUpdateSetting("hardwareSlug", opt.data as string || null)} + /> + + + +
+ + Export Config to Downloads +
+
+
+ + +
+ + Import Config from Downloads +
+
+
+ {configStatus && ( + +
+ {configStatus.isError ? : } {configStatus.message} +
+
+ )} +
+ + {/* ── MangoHud Setup Guide ────────────────────────────────── */} + + +
+ Steam Deck (SteamOS): MangoHud is pre-installed. Add mangohud %command% to your game's Steam launch options (right-click → Properties → Launch Options). +
+
+ +
+ Other Linux: Install via sudo apt install mangohud or flatpak install ...VulkanLayer.MangoHud. See{" "} + github.com/flightlessmango/MangoHud. +
+
+ +
+ Troubleshooting: Log empty? Check MangoHud is enabled. Not attaching? Add mangohud %command% to launch options explicitly. +
+
+
+ ) } \ No newline at end of file diff --git a/plugins/decky-vault/src/components/session-form.tsx b/plugins/decky-vault/src/components/session-form.tsx index 5a7d39b..a2954bb 100644 --- a/plugins/decky-vault/src/components/session-form.tsx +++ b/plugins/decky-vault/src/components/session-form.tsx @@ -5,6 +5,7 @@ import { PanelSectionRow, DropdownItem, TextField, + Field, staticClasses, } from "@decky/ui" import { @@ -155,18 +156,6 @@ export default function SessionForm({ /> - - -