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,