diff --git a/electron/main/ipc-handlers.ts b/electron/main/ipc-handlers.ts index 005f1f78..71294cd4 100644 --- a/electron/main/ipc-handlers.ts +++ b/electron/main/ipc-handlers.ts @@ -629,6 +629,29 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe return { total, used: total - free, available: free } }) + // GPU memory (VRAM) — NVIDIA only via nvidia-smi + ipcMain.handle('system:gpuMemory', async () => { + if (process.platform === 'darwin' && process.arch === 'arm64') { + // Apple Silicon: unified memory, no separate VRAM + return null + } + try { + const { stdout } = await pExecFile('nvidia-smi', [ + '--query-gpu=memory.total,memory.used', + '--format=csv,noheader,nounits' + ]) + const line = stdout.trim().split('\n')[0].trim() + const [totalMiB, usedMiB] = line.split(',').map(s => parseInt(s.trim(), 10)) + if (isNaN(totalMiB) || isNaN(usedMiB) || totalMiB === 0) return null + const total = totalMiB * 1024 * 1024 + const used = usedMiB * 1024 * 1024 + const available = total - used + return { total, used, available } + } catch { + return null + } + }) + ipcMain.handle('app:info', () => ({ version: app.getVersion(), userData: app.getPath('userData'), diff --git a/electron/preload/electron-api.ts b/electron/preload/electron-api.ts index 89fa69ec..b4552549 100644 --- a/electron/preload/electron-api.ts +++ b/electron/preload/electron-api.ts @@ -47,6 +47,8 @@ export function createElectronApi(ipcRenderer: IpcRendererLike, webFrame: WebFra system: { memory: (): Promise<{ total: number; used: number; available: number }> => ipcRenderer.invoke('system:memory') as Promise<{ total: number; used: number; available: number }>, + gpuMemory: (): Promise<{ total: number; used: number; available: number } | null> => + ipcRenderer.invoke('system:gpuMemory') as Promise<{ total: number; used: number; available: number } | null>, }, // Python / FastAPI bridge diff --git a/src/areas/settings/components/ApplicationSection.tsx b/src/areas/settings/components/ApplicationSection.tsx index 63984cb7..462bb618 100644 --- a/src/areas/settings/components/ApplicationSection.tsx +++ b/src/areas/settings/components/ApplicationSection.tsx @@ -2,7 +2,7 @@ import { useAppStore } from '@shared/stores/appStore' import { Section, Card, Row, Toggle } from '@shared/ui' export function ApplicationSection(): JSX.Element { - const { showRamIndicator, setShowRamIndicator } = useAppStore() + const { showRamIndicator, setShowRamIndicator, showVramIndicator, setShowVramIndicator } = useAppStore() return (
@@ -13,6 +13,12 @@ export function ApplicationSection(): JSX.Element { > + + +
) diff --git a/src/shared/components/layout/MemoryIndicator.tsx b/src/shared/components/layout/MemoryIndicator.tsx index fe09eac1..f30f8978 100644 --- a/src/shared/components/layout/MemoryIndicator.tsx +++ b/src/shared/components/layout/MemoryIndicator.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { useAppStore } from '@shared/stores/appStore' const GB = 1024 ** 3 @@ -6,15 +7,31 @@ function fmtGB(bytes: number): string { return (bytes / GB).toFixed(1) } +function getBarColors(pct: number): { barColor: string; textColor: string } { + if (pct >= 90) return { barColor: 'bg-red-500', textColor: 'text-red-300' } + if (pct >= 75) return { barColor: 'bg-amber-500', textColor: 'text-amber-300' } + return { barColor: 'bg-emerald-500', textColor: 'text-zinc-300' } +} + export default function MemoryIndicator(): JSX.Element | null { - const [mem, setMem] = useState<{ total: number; used: number; available: number } | null>(null) + const [ram, setRam] = useState<{ total: number; used: number; available: number } | null>(null) + const [vram, setVram] = useState<{ total: number; used: number; available: number } | null>(null) + const platform = useAppStore(s => s.platform) + const isMac = platform === 'darwin' + const showVramIndicator = useAppStore(s => s.showVramIndicator) useEffect(() => { let cancelled = false const tick = async () => { try { - const next = await window.electron.system.memory() - if (!cancelled) setMem(next) + const [ramNext, vramNext] = await Promise.all([ + window.electron.system.memory(), + isMac ? Promise.resolve(null) : window.electron.system.gpuMemory(), + ]) + if (!cancelled) { + setRam(ramNext) + setVram(vramNext) + } } catch { // Renderer should not break if memory sampling fails. } @@ -25,42 +42,59 @@ export default function MemoryIndicator(): JSX.Element | null { cancelled = true clearInterval(id) } - }, []) + }, [isMac]) - if (!mem) return null + if (!ram) return null - const pct = mem.total > 0 ? Math.min(100, Math.round((mem.used / mem.total) * 100)) : 0 + const ramPct = ram.total > 0 ? Math.min(100, Math.round((ram.used / ram.total) * 100)) : 0 + const ramColors = getBarColors(ramPct) - let barColor = 'bg-emerald-500' - let textColor = 'text-zinc-300' - if (pct >= 90) { - barColor = 'bg-red-500' - textColor = 'text-red-300' - } else if (pct >= 75) { - barColor = 'bg-amber-500' - textColor = 'text-amber-300' - } + const ramTooltip = + `RAM:\n` + + ` Used: ${fmtGB(ram.used)} GB\n` + + ` Available: ${fmtGB(ram.available)} GB\n` + + ` Total: ${fmtGB(ram.total)} GB` - const tooltip = - `Used: ${fmtGB(mem.used)} GB\n` + - `Available: ${fmtGB(mem.available)} GB\n` + - `Total: ${fmtGB(mem.total)} GB` + const vramBar = showVramIndicator && !isMac && vram && vram.total > 0 ? (() => { + const vramPct = Math.min(100, Math.round((vram.used / vram.total) * 100)) + const vramColors = getBarColors(vramPct) + const vramTooltip = + `VRAM:\n` + + ` Used: ${fmtGB(vram.used)} GB\n` + + ` Available: ${fmtGB(vram.available)} GB\n` + + ` Total: ${fmtGB(vram.total)} GB` + return ( +
+ VRAM +
+
+
+ + {fmtGB(vram.used)} / {fmtGB(vram.total)} GB + +
+ ) + })() : null return (
RAM
- - {fmtGB(mem.used)} / {fmtGB(mem.total)} GB + + {fmtGB(ram.used)} / {fmtGB(ram.total)} GB + {vramBar}
) -} +} \ No newline at end of file diff --git a/src/shared/stores/appStore.ts b/src/shared/stores/appStore.ts index d06a5211..05281eb2 100644 --- a/src/shared/stores/appStore.ts +++ b/src/shared/stores/appStore.ts @@ -137,6 +137,8 @@ interface AppState { // UI preferences showRamIndicator: boolean setShowRamIndicator: (v: boolean) => void + showVramIndicator: boolean + setShowVramIndicator: (v: boolean) => void // Accessibility useAtkinsonFont: boolean @@ -241,6 +243,8 @@ export const useAppStore = create()( showRamIndicator: true, setShowRamIndicator: (v) => set({ showRamIndicator: v }), + showVramIndicator: true, + setShowVramIndicator: (v) => set({ showVramIndicator: v }), useAtkinsonFont: false, setUseAtkinsonFont: (v) => set({ useAtkinsonFont: v }), @@ -301,6 +305,7 @@ export const useAppStore = create()( partialize: (state) => ({ generationOptions: state.generationOptions, showRamIndicator: state.showRamIndicator, + showVramIndicator: state.showVramIndicator, useAtkinsonFont: state.useAtkinsonFont, uiScale: state.uiScale, lightSettings: state.lightSettings,