Skip to content

Commit 60b87ce

Browse files
committed
Add an app activity & resource usage view to Settings
Surface what's currently loaded/connected (Ollama and llama.cpp loaded models, MCP server connections) plus this process's memory footprint, via a new system:getActivity IPC call, so users can see what the app is actually doing without digging through logs.
1 parent 1fe7f43 commit 60b87ce

8 files changed

Lines changed: 159 additions & 2 deletions

File tree

‎app/src/llamacpp-manager.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ export function listModels(modelsDir: string): LocalGgufModel[] {
8888
});
8989
}
9090

91+
// Model paths currently kept warm in modelCache — used for the
92+
// activity/resource usage view. Doesn't report VRAM/RAM footprint since
93+
// node-llama-cpp doesn't expose per-model memory usage.
94+
export function listLoadedModels(): string[] {
95+
return [...modelCache.keys()];
96+
}
97+
9198
export function deleteModel(modelsDir: string, name: string): void {
9299
const root = path.resolve(modelsDir);
93100
const target = path.resolve(root, name);

‎app/src/main.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,20 @@ function registerIpcHandlers(): void {
344344
const specs = await systemSpecs.getSpecs();
345345
return systemSpecs.recommendModels(specs);
346346
});
347+
ipcMain.handle("system:getActivity", async () => {
348+
const ollamaRunning = await ollama.isRunning();
349+
const ollamaLoadedModels = ollamaRunning
350+
? await ollama.listRunningModels().catch(() => [])
351+
: [];
352+
const mem = process.memoryUsage();
353+
return {
354+
ollamaRunning,
355+
ollamaLoadedModels,
356+
llamacppLoadedModels: llamacpp.listLoadedModels(),
357+
mcpServers: mcpClient.getServerStatuses(),
358+
memory: { rssMB: +(mem.rss / 1e6).toFixed(1), heapUsedMB: +(mem.heapUsed / 1e6).toFixed(1) },
359+
};
360+
});
347361

348362
ipcMain.handle("settings:get", () => settingsStore.getSettings());
349363
ipcMain.handle("settings:save", (_event: IpcMainInvokeEvent, partial) => {

‎app/src/ollama-manager.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,27 @@ export async function listModels(): Promise<OllamaModel[]> {
172172
return data.models || [];
173173
}
174174

175+
export interface OllamaRunningModel {
176+
name: string;
177+
size: number;
178+
size_vram: number;
179+
expires_at: string;
180+
}
181+
182+
// /api/ps lists models currently loaded in memory, distinct from /api/tags
183+
// (everything ever pulled) — used for the activity/resource usage view.
184+
export async function listRunningModels(): Promise<OllamaRunningModel[]> {
185+
let res: Response;
186+
try {
187+
res = await fetch(`${HOST}/api/ps`);
188+
} catch (err) {
189+
throw describeNetworkError(err);
190+
}
191+
if (!res.ok) throw new Error(await describeError(res, "Failed to list running models"));
192+
const data = await res.json();
193+
return data.models || [];
194+
}
195+
175196
export async function deleteModel(name: string): Promise<{ deleted: boolean }> {
176197
let res: Response;
177198
try {

‎app/src/preload.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ contextBridge.exposeInMainWorld("api", {
118118
system: {
119119
getSpecs: () => ipcRenderer.invoke("system:getSpecs"),
120120
getRecommendations: () => ipcRenderer.invoke("system:getRecommendations"),
121+
getActivity: () => ipcRenderer.invoke("system:getActivity"),
121122
},
122123

123124
settings: {

‎frontend/src/components/settings-ui.tsx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@ export function SettingsSection({
55
description,
66
children,
77
className,
8+
action,
89
}: {
910
title: string;
1011
description?: string;
1112
children: React.ReactNode;
1213
className?: string;
14+
action?: React.ReactNode;
1315
}) {
1416
return (
1517
<section className={cn("mb-8 last:mb-0", className)}>
16-
<h2 className="text-sm font-semibold">{title}</h2>
17-
{description && <p className="mt-0.5 text-xs text-muted-foreground">{description}</p>}
18+
<div className="flex items-start justify-between gap-4">
19+
<div>
20+
<h2 className="text-sm font-semibold">{title}</h2>
21+
{description && <p className="mt-0.5 text-xs text-muted-foreground">{description}</p>}
22+
</div>
23+
{action}
24+
</div>
1825
<div className="mt-3 divide-y divide-border overflow-hidden rounded-lg border border-border bg-card">
1926
{children}
2027
</div>

‎frontend/src/lib/translations.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ export interface Dictionary {
232232
diagnostics: string;
233233
diagnosticsDescription: string;
234234
copyDiagnosticInfo: string;
235+
appActivity: string;
236+
appActivityDescription: string;
237+
refresh: string;
238+
noModelsLoaded: string;
239+
notRunning: string;
240+
mcpServersLabel: string;
241+
tools: string;
242+
noneConnected: string;
243+
appMemoryUsage: string;
235244
copied: string;
236245
openLogsFolder: string;
237246
checkForUpdates: string;
@@ -484,6 +493,15 @@ export const en: Dictionary = {
484493
open: "Open",
485494
diagnostics: "Diagnostics",
486495
diagnosticsDescription: "Useful when reporting a bug: app/system versions, Ollama connection status, and recent log output.",
496+
appActivity: "Activity & resource usage",
497+
appActivityDescription: "What's currently loaded and connected, plus this app's memory footprint.",
498+
refresh: "Refresh",
499+
noModelsLoaded: "No models loaded",
500+
notRunning: "Not running",
501+
mcpServersLabel: "MCP servers",
502+
tools: "tools",
503+
noneConnected: "None connected",
504+
appMemoryUsage: "App memory usage",
487505
copyDiagnosticInfo: "Copy diagnostic info",
488506
copied: "Copied",
489507
openLogsFolder: "Open logs folder",
@@ -746,6 +764,15 @@ export const tr: Dictionary = {
746764
diagnostics: "Tanılama",
747765
diagnosticsDescription: "Hata bildirirken faydalıdır: uygulama/sistem sürümleri, Ollama bağlantı durumu ve son günlük çıktısı.",
748766
copyDiagnosticInfo: "Tanılama bilgisini kopyala",
767+
appActivity: "Etkinlik ve kaynak kullanımı",
768+
appActivityDescription: "Şu anda yüklü ve bağlı olanlar, ayrıca bu uygulamanın bellek kullanımı.",
769+
refresh: "Yenile",
770+
noModelsLoaded: "Yüklü model yok",
771+
notRunning: "Çalışmıyor",
772+
mcpServersLabel: "MCP sunucuları",
773+
tools: "araç",
774+
noneConnected: "Bağlı yok",
775+
appMemoryUsage: "Uygulama bellek kullanımı",
749776
copied: "Kopyalandı",
750777
openLogsFolder: "Günlük klasörünü aç",
751778
checkForUpdates: "Güncellemeleri denetle",

‎frontend/src/pages/Settings.tsx‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
MessageSquare,
2727
Volume2,
2828
Database,
29+
MemoryStick,
2930
} from "lucide-react";
3031
import { Button } from "@/components/ui/button";
3132
import { Badge } from "@/components/ui/badge";
@@ -59,6 +60,7 @@ import type {
5960
HfModelSummary,
6061
HfGgufFile,
6162
ScheduledTask,
63+
AppActivity,
6264
} from "@/types/electron";
6365
import { EXTRA_MODELS } from "@/lib/model-catalog";
6466
import { OPENAI_MODELS, ANTHROPIC_MODELS, GEMINI_MODELS, formatModelRef, CUSTOM_PROVIDER_PRESETS } from "@/lib/providers";
@@ -136,6 +138,8 @@ export default function Settings() {
136138
const [appVersion, setAppVersion] = useState<string | null>(null);
137139
const [diagnosticsCopied, setDiagnosticsCopied] = useState(false);
138140
const [userDataPath, setUserDataPath] = useState<string | null>(null);
141+
const [activity, setActivity] = useState<AppActivity | null>(null);
142+
const [activityLoading, setActivityLoading] = useState(false);
139143
const [importMessage, setImportMessage] = useState<string | null>(null);
140144
const [ollamaHostInput, setOllamaHostInput] = useState("");
141145
const [modelsDirStatus, setModelsDirStatus] = useState<string | null>(null);
@@ -191,6 +195,7 @@ export default function Settings() {
191195
window.api.ollama.status().then(setRunning);
192196
window.api.system.getSpecs().then(setSpecs);
193197
window.api.system.getRecommendations().then(setRecommendations);
198+
window.api.system.getActivity().then(setActivity);
194199
window.api.settings.get().then((s) => {
195200
setSettings(s);
196201
setOllamaHostInput(s.ollamaHost);
@@ -338,6 +343,15 @@ export default function Settings() {
338343
await refreshSessions();
339344
}
340345

346+
async function refreshActivity() {
347+
setActivityLoading(true);
348+
try {
349+
setActivity(await window.api.system.getActivity());
350+
} finally {
351+
setActivityLoading(false);
352+
}
353+
}
354+
341355
async function handleCopyDiagnostics() {
342356
const d = await window.api.app.getDiagnostics();
343357
const text = [
@@ -2043,6 +2057,56 @@ export default function Settings() {
20432057
)}
20442058
</SettingsSection>
20452059

2060+
<SettingsSection
2061+
title={t.appActivity}
2062+
description={t.appActivityDescription}
2063+
action={
2064+
<Button size="sm" variant="outline" onClick={refreshActivity} className="gap-1.5" disabled={activityLoading}>
2065+
<RefreshCw className={`size-3.5 ${activityLoading ? "animate-spin" : ""}`} /> {t.refresh}
2066+
</Button>
2067+
}
2068+
>
2069+
{activity && (
2070+
<>
2071+
<SettingsRow label="Ollama">
2072+
<span className="text-sm text-muted-foreground">
2073+
{activity.ollamaRunning
2074+
? activity.ollamaLoadedModels.length > 0
2075+
? activity.ollamaLoadedModels.map((m) => m.name).join(", ")
2076+
: t.noModelsLoaded
2077+
: t.notRunning}
2078+
</span>
2079+
</SettingsRow>
2080+
<SettingsRow label="llama.cpp">
2081+
<span className="text-sm text-muted-foreground">
2082+
{activity.llamacppLoadedModels.length > 0
2083+
? activity.llamacppLoadedModels.map((p) => p.split(/[/\\]/).pop()).join(", ")
2084+
: t.noModelsLoaded}
2085+
</span>
2086+
</SettingsRow>
2087+
<SettingsRow label={t.mcpServersLabel}>
2088+
{Object.keys(activity.mcpServers).length > 0 ? (
2089+
<div className="flex flex-wrap justify-end gap-1.5">
2090+
{Object.entries(activity.mcpServers).map(([id, status]) => (
2091+
<Badge key={id} variant={status.connected ? "secondary" : "outline"}>
2092+
{id} · {status.toolCount} {t.tools}
2093+
</Badge>
2094+
))}
2095+
</div>
2096+
) : (
2097+
<span className="text-sm text-muted-foreground">{t.noneConnected}</span>
2098+
)}
2099+
</SettingsRow>
2100+
<SettingsRow label={t.appMemoryUsage}>
2101+
<span className="flex items-center gap-1.5 text-sm text-muted-foreground">
2102+
<MemoryStick className="size-3.5" />
2103+
{activity.memory.rssMB.toLocaleString()} MB
2104+
</span>
2105+
</SettingsRow>
2106+
</>
2107+
)}
2108+
</SettingsSection>
2109+
20462110
<SettingsSection title={t.diagnostics} description={t.diagnosticsDescription}>
20472111
<SettingsRow label={t.copyDiagnosticInfo}>
20482112
<Button size="sm" variant="outline" onClick={handleCopyDiagnostics} className="gap-1.5">

‎frontend/src/types/electron.d.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ export interface McpServerStatus {
119119
error?: string;
120120
}
121121

122+
export interface OllamaRunningModel {
123+
name: string;
124+
size: number;
125+
size_vram: number;
126+
expires_at: string;
127+
}
128+
129+
export interface AppActivity {
130+
ollamaRunning: boolean;
131+
ollamaLoadedModels: OllamaRunningModel[];
132+
llamacppLoadedModels: string[];
133+
mcpServers: Record<string, McpServerStatus>;
134+
memory: { rssMB: number; heapUsedMB: number };
135+
}
136+
122137
export interface RollbackResult {
123138
path: string;
124139
restoredContent: boolean;
@@ -352,6 +367,7 @@ export interface ElectronApi {
352367
system: {
353368
getSpecs: () => Promise<SystemSpecs>;
354369
getRecommendations: () => Promise<ModelRecommendations>;
370+
getActivity: () => Promise<AppActivity>;
355371
};
356372
settings: {
357373
get: () => Promise<AppSettings>;

0 commit comments

Comments
 (0)