Conversation
Implements a complete launch-from-MCED feature allowing users to start Minecraft directly without switching to their launcher. Key changes: - New LaunchService.ts: direct Java launch with per-launcher support (Modrinth, Prism/MultiMC, CurseForge, vanilla fallback) - Auth tokens read from launcher storage (Prism accounts.json, Modrinth app.db, vanilla launcher_accounts.json); offline fallback - Java detection: launcher-bundled → JAVA_HOME → PATH → common paths - Version JSON + classpath resolution per launcher type - Cross-platform classpath separator fix (: on Linux/Mac, ; on Windows) - 4 IPC handlers: game:launch, game:kill, game:isRunning, game:getRunning - Preload API: launchGame, killGame, isGameRunning, getRunningGames - Header.tsx: Play/Stop button with 3s running-state poll - App.tsx landing page: Play button overlay on recent instance cards - LandingPage.tsx: onLaunchInstance prop + Play button overlay https://claude.ai/code/session_01G1cMMQeJbAj2BYdK1pP8LC
- Stream stdout/stderr from the Minecraft process to the renderer via IPC (LaunchService emits 'log' events → mainWindow.webContents.send) - Add onGameLog / removeGameLogListener to preload contextBridge API - New GameConsole component: scrollable log view with color-coded stdout (green), stderr (red), system (yellow) lines, timestamps, clear + download actions, auto-scroll with manual override - Header: ℹ info icon next to Play button shows tooltip warning that the instance must be launched in the original launcher at least once before using MCED launch (with offline-fallback note) - Header: Terminal button appears once game output arrives, opens the GameConsole modal https://claude.ai/code/session_01G1cMMQeJbAj2BYdK1pP8LC
## Backup System (ZIP-based) - backup:create now zips config/, kubejs/ and defaultconfigs/ with proper folder structure so restores work correctly and KubeJS scripts no longer appear as loose files in the backup dir - backup:restore extracts to instance root so all folders are restored - Added missing backup:rename IPC handler ## Game Launcher - JVM heap memory (max/min) is now configurable in Settings (Game Launch section) - Added jvmMaxMemory/jvmMinMemory to settingsStore (defaults: 4096/1024 MB) - LaunchService, IPC handler and preload all updated to pass -Xmx/-Xms from settings ## UI/UX - Sidebar collapse button: click the chevron on the sidebar edge to collapse/expand the mod list - SmartSearch filter pills: filter results by type (boolean/integer/float/string/enum/array) or "Changed only" - "Recently Changed" widget on landing page shows last 5 modified settings with mod name ## Config Editor - Reset to Defaults button: resets all settings in current config to their defaultValue - Presets: save/load named config presets via localStorage, accessible from toolbar dropdown ## New Format Support - HOCON parser added (HoconParser.ts) for .conf files used by older Forge mods - ConfigService.ts updated to detect .conf files as cfg format ## Mod Update Checker - UpdateCheckerService checks Modrinth for newer mod versions in the background after instance loads - updateStore (Zustand) caches results; update badges appear in ModListItem ## Crash Log Analyzer - New CrashAnalyzer.tsx component: drag & drop crash log .txt/.log - IPC handler crash:analyze extracts main cause, error patterns and mentioned mod IDs - Button in Header to open analyzer ## Modpack Export (.mrpack) - IPC handler export:modpack creates mrpack ZIP with modrinth.index.json manifest + config/ and kubejs/ as overrides - Export button in Header with prompt for pack name https://claude.ai/code/session_01G1cMMQeJbAj2BYdK1pP8LC
- preload.ts: add launcherType to detectInstance return type - App.tsx: add lastOpened timestamp to RecentInstance, cast launcherType - Header.tsx: cast launcher comparisons to string to fix type narrowing errors - SmithingEditor.tsx: remove extraneous type/amount fields, rename onRemove -> onClear - index.ts: mark resolved merge conflict
There was a problem hiding this comment.
Pull request overview
Re-implements end-to-end game launching and observability in the Electron app, adding crash analysis, improved backups, and mod update checking, plus associated UI/UX enhancements and new settings.
Changes:
- Added a new main-process
LaunchServicewith IPC wiring and renderer UI controls (launch/stop, console log streaming). - Introduced crash log analysis UI + IPC handler, and expanded backup creation/restore (incl. kubejs/defaultconfigs) with new rename support.
- Added Modrinth-based mod update checking with renderer state/store integration and UI indicators; extended config support to include
.conf/HOCON parsing.
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/renderer/store/updateStore.ts | New Zustand store to track per-mod update metadata and check state. |
| src/renderer/store/settingsStore.ts | Adds JVM memory settings (min/max) to app settings defaults/types. |
| src/renderer/services/parsers/HoconParser.ts | New HOCON parser + stringify support for .conf / HOCON-like configs. |
| src/renderer/services/api/ModrinthAPI.ts | Adds getLatestVersion helper for update checking. |
| src/renderer/services/UpdateCheckerService.ts | New service to batch-check Modrinth versions and populate update store. |
| src/renderer/services/ConfigService.ts | Adds .conf support and maps it to cfg/HOCON parsing. |
| src/renderer/components/StatusBar.tsx | Simplifies status bar visibility/logic around unsaved changes. |
| src/renderer/components/StatsModal.css | Moves styling to CSS variables for theme consistency. |
| src/renderer/components/SmartSearch.tsx | Adds filter pills, “changed only” filtering, and UI refinements. |
| src/renderer/components/Sidebar.tsx | Adds collapsible sidebar UI. |
| src/renderer/components/Settings.tsx | Adds “Game Launch” section for JVM memory configuration. |
| src/renderer/components/Settings.css | Converts settings modal styling to CSS variables and refines UI. |
| src/renderer/components/ModListItem.tsx | Adds update badge indicator on mods with available updates. |
| src/renderer/components/LandingPage/LandingPage.tsx | Adds “launch” affordance for recent instances and prop wiring. |
| src/renderer/components/KubeJS/RecipeEditor/SmithingEditor.tsx | Aligns slot clearing API (onClear) and item shape. |
| src/renderer/components/Header.tsx | Adds launch/stop UI, console/crash analyzer entry points, export button, and running/log tracking. |
| src/renderer/components/GameConsole.tsx | New real-time log console modal with filtering and download. |
| src/renderer/components/CrashAnalyzer.tsx | New crash analysis modal with drag/drop upload and results display. |
| src/renderer/components/ConfigEditor/ConfigEditor.tsx | Adds config presets + reset-to-defaults actions. |
| src/renderer/components/ChangelogViewer.css | Moves styling to CSS variables for theme consistency. |
| src/renderer/components/Backup/BackupModal.css | Minor UI polish (rounded header). |
| src/renderer/App.tsx | Triggers background update checks and adds instance “play” actions + toast UI. |
| src/main/tsconfig.main.json | Adds ignoreDeprecations configuration. |
| src/main/preload.ts | Exposes new IPC APIs for launching, logs, crash analysis, export, and backup rename. |
| src/main/index.ts | Adds backup directory isolation, backup rename, crash analysis handler, export handler, and game launch IPC handlers. |
| src/main/LaunchService.ts | New main-process launcher implementation (auth, Java detection, classpath/natives, process management, log emission). |
| package.json | Adds node-gyp dev dependency. |
| package-lock.json | Locks updated dependency tree (incl. node-gyp and transitive updates). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const match = backupId.match(/^backup-(\d+)-/); | ||
| const timestamp = match ? match[1] : String(Date.now()); | ||
|
|
||
| const newId = `backup-${timestamp}-${newName.replace(/ /g, "-")}.zip`; | ||
| const newFile = path.join(backupDir, newId); | ||
|
|
||
| await fs.rename(oldFile, newFile); | ||
| return { success: true, newId }; |
There was a problem hiding this comment.
newName is used to construct the new backup filename with only spaces replaced. This allows path separators and other characters that can create confusing paths or invalid filenames, and may also overwrite an existing backup name. Sanitize newName similarly to getInstanceBackupDir and consider rejecting/handling name collisions before calling fs.rename.
| const zip = new AdmZip(backupFile); | ||
| zip.extractAllTo(configDir, true); | ||
| // Extract to instance root so config/, kubejs/, defaultconfigs/ are all restored correctly | ||
| zip.extractAllTo(instancePath, true); |
There was a problem hiding this comment.
backup:restore now extracts the entire ZIP to instancePath. Since backups can be manipulated on disk, this allows restoring arbitrary files into the instance root (potentially overwriting unrelated instance data). Consider restricting extraction to expected top-level folders (config/, kubejs/, defaultconfigs/) and validating each entry path before extraction.
| className="p-1.5 text-blue-400/60 hover:text-blue-400 transition-colors rounded-lg hover:bg-blue-500/10" | ||
| tabIndex={-1} | ||
| aria-label="First-time launch info" | ||
| > | ||
| <Info className="w-3.5 h-3.5" /> | ||
| </button> | ||
| <div className="absolute right-0 top-full mt-2 w-80 z-50 pointer-events-none opacity-0 group-hover/info:opacity-100 transition-opacity"> |
There was a problem hiding this comment.
The info button is removed from the tab order via tabIndex={-1}, making the launch hint inaccessible to keyboard users. Consider keeping it focusable (default tabIndex) and showing the tooltip on focus as well as hover, or replacing it with non-interactive text if it shouldn't be reachable.
| className="p-1.5 text-blue-400/60 hover:text-blue-400 transition-colors rounded-lg hover:bg-blue-500/10" | |
| tabIndex={-1} | |
| aria-label="First-time launch info" | |
| > | |
| <Info className="w-3.5 h-3.5" /> | |
| </button> | |
| <div className="absolute right-0 top-full mt-2 w-80 z-50 pointer-events-none opacity-0 group-hover/info:opacity-100 transition-opacity"> | |
| type="button" | |
| className="p-1.5 text-blue-400/60 hover:text-blue-400 transition-colors rounded-lg hover:bg-blue-500/10 focus-visible:text-blue-400 focus-visible:bg-blue-500/10" | |
| aria-label="First-time launch info" | |
| aria-describedby="launch-info-tooltip" | |
| > | |
| <Info className="w-3.5 h-3.5" /> | |
| </button> | |
| <div | |
| id="launch-info-tooltip" | |
| role="tooltip" | |
| className="absolute right-0 top-full mt-2 w-80 z-50 pointer-events-none opacity-0 group-hover/info:opacity-100 group-focus-within/info:opacity-100 transition-opacity" | |
| > |
| const getUpdate = useUpdateStore((s) => s.getUpdate); | ||
| const hasUpdate = !!getUpdate(mod.modId); | ||
|
|
There was a problem hiding this comment.
useUpdateStore((s) => s.getUpdate) subscribes only to the function reference, which is stable; this component will not re-render when updates changes, so the update badge may never appear/disappear. Subscribe to the derived value instead (e.g. select s.updates.get(mod.modId) or s.getUpdate(mod.modId) inside the selector) so Zustand can trigger re-renders on update changes.
|
|
||
| useEffect(() => { | ||
| if (autoScroll) { | ||
| bottomRef.current?.scrollIntoView({ behavior: "instant" }); |
There was a problem hiding this comment.
scrollIntoView does not support behavior: "instant" (standard values are "auto" and "smooth"); this will fail TypeScript type-checking (and may be ignored at runtime). Use behavior: "auto" or omit the option for an immediate jump.
| bottomRef.current?.scrollIntoView({ behavior: "instant" }); | |
| bottomRef.current?.scrollIntoView({ behavior: "auto" }); |
| export function LandingPage({ onSelectInstance, onLaunchInstance, recentInstances = [] }: LandingPageProps) { | ||
| const [hoveredCard, setHoveredCard] = useState<string | null>(null); | ||
|
|
||
| console.log("LandingPage recentInstances:", recentInstances); |
There was a problem hiding this comment.
Leftover debug logging (console.log("LandingPage recentInstances:", ...)) will spam the console in production. Please remove or guard it behind a development flag.
| const running = await window.api.isGameRunning(currentInstance.path); | ||
| if (!cancelled) setIsRunning(running); |
There was a problem hiding this comment.
The check function awaits window.api.isGameRunning(...) without a try/catch. If the IPC call rejects (e.g. main process error), this will create an unhandled rejection every 3 seconds. Wrap the await in a try/catch and decide on a safe fallback (e.g. treat as not running and stop polling).
| const running = await window.api.isGameRunning(currentInstance.path); | |
| if (!cancelled) setIsRunning(running); | |
| try { | |
| const running = await window.api.isGameRunning(currentInstance.path); | |
| if (!cancelled) setIsRunning(running); | |
| } catch { | |
| if (!cancelled) setIsRunning(false); | |
| } |
| // Compare versions - simple string comparison | ||
| if (latest.version !== mod.version && latest.version > mod.version) { | ||
| updates.set(mod.modId, { | ||
| modId: mod.modId, | ||
| currentVersion: mod.version, | ||
| latestVersion: latest.version, | ||
| latestVersionId: latest.versionId, | ||
| changelog: latest.changelog, | ||
| source: "modrinth", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Update detection compares versions using plain string ordering (latest.version > mod.version). This produces incorrect results for common version formats (e.g. "10.0.0" vs "2.0.0", or Forge-style build numbers). Consider using a real semver comparator when possible, or simply flag an update when the version differs (and optionally prefer the newest by date_published from Modrinth).
| private async findJava(launcher: string, mcVersion: string): Promise<string> { | ||
| const requiredMajor = this.requiredJavaMajor(mcVersion); | ||
| const javaExe = os.platform() === 'win32' ? 'java.exe' : 'java'; | ||
|
|
||
| // 1. Launcher-bundled Java | ||
| const launcherJava = await this.findLauncherJava(launcher, javaExe, requiredMajor); | ||
| if (launcherJava) return launcherJava; | ||
|
|
||
| // 2. JAVA_HOME | ||
| if (process.env.JAVA_HOME) { | ||
| const candidate = path.join(process.env.JAVA_HOME, 'bin', javaExe); | ||
| if (await this.fileExists(candidate)) return candidate; | ||
| } | ||
|
|
||
| // 3. System PATH | ||
| try { | ||
| const cmd = os.platform() === 'win32' ? 'where java' : 'which java'; | ||
| const { stdout } = await execAsync(cmd); | ||
| const javaPath = stdout.trim().split('\n')[0].trim(); | ||
| if (javaPath && (await this.fileExists(javaPath))) return javaPath; | ||
| } catch {} | ||
|
|
||
| // 4. Common installation paths | ||
| for (const candidate of this.getCommonJavaPaths(javaExe)) { | ||
| if (await this.fileExists(candidate)) return candidate; | ||
| } |
There was a problem hiding this comment.
findJava computes requiredMajor but only enforces it for launcher-bundled Java. For JAVA_HOME / PATH / common paths, it returns the first java found without checking the major version, which can reliably break launching (e.g. Java 8 found first for MC 1.20+). Consider validating candidates by running java -version (or reading the release file when possible) and ensuring major >= requiredMajor before accepting.
| const name = `Backup-${new Date(timestamp).toISOString().split("T")[0]}`; | ||
| const backupFile = path.join(backupDir, `backup-${timestamp}-${name.replace(/ /g, "-")}.zip`); | ||
| const backupName = name || `Backup-${new Date(timestamp).toISOString().split("T")[0]}`; | ||
| const backupFile = path.join(backupDir, `backup-${timestamp}-${backupName.replace(/ /g, "-")}.zip`); |
There was a problem hiding this comment.
backupName is incorporated into the output filename with only spaces replaced. If the caller supplies path separators or other special characters, this can create unintended nested paths or invalid filenames. Sanitize backupName (e.g., allow only [a-zA-Z0-9_-], or run through path.basename and replace other characters) before building backupFile.
| const backupFile = path.join(backupDir, `backup-${timestamp}-${backupName.replace(/ /g, "-")}.zip`); | |
| const sanitizedBackupName = path.basename(backupName) | |
| .replace(/[^a-zA-Z0-9_-]/g, "-") | |
| .replace(/-+/g, "-") | |
| .replace(/^-|-$/g, "") || "backup"; | |
| const backupFile = path.join(backupDir, `backup-${timestamp}-${sanitizedBackupName}.zip`); |
Summary
This PR re-implements the game launcher functionality that was previously removed, adds crash log analysis capabilities, improves backup management, and introduces update checking for mods. The launcher now supports multiple launcher types (Prism, MultiMC, Modrinth, Vanilla) with automatic authentication and Java detection.
Key Changes
Game Launcher (
LaunchService)LaunchServiceclass that handles Minecraft game launching with:Game Console UI
GameConsolecomponent for real-time game output monitoringCrash Analysis
CrashAnalyzercomponent for analyzing crash logsBackup Improvements
config/(main configuration folder)kubejs/(KubeJS scripts)defaultconfigs/(default configurations)backup:renameIPC handlerUpdate Checking
UpdateCheckerServicefor checking mod updatesupdateStorefor managing update stateConfiguration & Settings
UI Enhancements
IPC Handlers
game:launch- Launch a game instancegame:kill- Stop a running gamegame:isRunning- Check if instance is runninggame:getRunning- Get all running instancesgame:log- Event for game log outputbackup:rename- Rename a backupanalyzeCrashLog- Analyze crash log filesImplementation Details
releasefile in JRE roothttps://claude.ai/code/session_01G1cMMQeJbAj2BYdK1pP8LC