diff --git a/install-windows.ps1 b/install-windows.ps1
index ee8b927..89e4b5f 100644
--- a/install-windows.ps1
+++ b/install-windows.ps1
@@ -305,33 +305,16 @@ Pop-Location
if ($seedOk) { Write-OK "Default model set to $ChatModel" }
else { Write-Warn "Could not persist default model - pick it at the top of the chat instead." }
-Write-Step "Pulling models ($ChatModel for chat, $MemModel for memory, $EmbedModel for recall)"
-# A prompt, not "press Ctrl+C to skip": Ctrl+C in PowerShell 5.1 terminates the
-# whole script, so the escape hatch the installer advertised was also the one
-# thing that stopped it finishing - no Ollama cleanup, no summary, no window
-# close. Answering "n" declines the download and the installer carries on.
-Write-Host " These are several GB. You can skip and pull them later from the Models tab." -ForegroundColor DarkGray
-$pullAnswer = Read-Host " Download them now? [Y/n]"
-if ($pullAnswer -match '^\s*(n|no)\s*$') {
- Write-Warn "Model download skipped - get them from the Models tab when you are ready."
-} else {
-# No pipe: 'ollama pull' draws a progress bar with cursor control, and piping it
-# (to Out-Host or anything else) buffers the redraws - the download then shows no
-# output for minutes and reads as a hang. Let it own the console.
-# No try/catch either: a native command that exits non-zero does not throw, so
-# the catch never fired and a failed pull was reported as success.
- ollama pull $ChatModel
- if ($LASTEXITCODE -eq 0) { Write-OK "$ChatModel ready (default chat model)" }
- else { Write-Warn "$ChatModel pull skipped/failed - pull it from the Models tab later." }
-
- ollama pull $MemModel
- if ($LASTEXITCODE -eq 0) { Write-OK "$MemModel ready (memory curator)" }
- else { Write-Warn "$MemModel pull skipped/failed - the memory service will fall back to the chat model." }
-
- ollama pull $EmbedModel
- if ($LASTEXITCODE -eq 0) { Write-OK "$EmbedModel ready (conversation recall)" }
- else { Write-Warn "$EmbedModel pull skipped/failed - recall will fall back to lexical search." }
-}
+# No auto-download: the right models depend on the machine (a 4GB GPU can't fit
+# an 8B model). The Models tab detects VRAM/RAM and marks which models fit, so
+# the user pulls the right ones there instead of us guessing several GB.
+Write-Step "Skipping model download (pick hardware-appropriate models in the app)"
+Write-Host " No models were downloaded. Open NexusOS -> Models: it detects your" -ForegroundColor DarkGray
+Write-Host " VRAM/RAM and flags which models fit (green = GPU, yellow = CPU/RAM)." -ForegroundColor DarkGray
+Write-Host " Pull at least:" -ForegroundColor DarkGray
+Write-Host " - $EmbedModel (required for recall / document search)" -ForegroundColor Gray
+Write-Host " - a chat model the Models tab marks as fitting your GPU (or $ChatModel on a big one)" -ForegroundColor Gray
+Write-Host " - $MemModel for the memory curator (optional)" -ForegroundColor Gray
# -- Make Ollama manual-start (NexusOS owns the lifecycle) ----------------------
Write-Step "Setting Ollama to manual start"
diff --git a/interface/web/src/Models.jsx b/interface/web/src/Models.jsx
index b66ef7e..7e4e9d1 100644
--- a/interface/web/src/Models.jsx
+++ b/interface/web/src/Models.jsx
@@ -8,6 +8,7 @@ export function Models({ onPullStateChange }) {
const [pulling, setPulling] = useState(false);
const [modelName, setModelName] = useState("");
const [pullProgress, setPullProgress] = useState("");
+ const [recommended, setRecommended] = useState(null); // {hardware, models}
const checkOllamaStatus = useCallback(async () => {
try {
@@ -42,8 +43,9 @@ export function Models({ onPullStateChange }) {
}
}, [checkOllamaStatus]);
- const pullModel = async () => {
- if (!modelName.trim()) {
+ const pullModel = async (nameArg) => {
+ const name = (typeof nameArg === "string" ? nameArg : modelName).trim();
+ if (!name) {
setError("Please enter a model name");
return;
}
@@ -51,13 +53,13 @@ export function Models({ onPullStateChange }) {
setPulling(true);
onPullStateChange?.(true);
setError("");
- setPullProgress("");
+ setPullProgress(`Pulling ${name}…`);
try {
const response = await fetch(`${API_BASE}/models/pull`, {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name: modelName.trim() }),
+ body: JSON.stringify({ name }),
});
if (!response.ok) throw new Error("Failed to pull model");
@@ -122,11 +124,19 @@ export function Models({ onPullStateChange }) {
useEffect(() => {
loadModels();
+ fetch(`${API_BASE}/models/recommended`).then(r => r.ok ? r.json() : null).then(setRecommended).catch(() => {});
const interval = setInterval(loadModels, 30000);
return () => clearInterval(interval);
}, [loadModels]);
+ const installedNames = new Set(models.map(m => m.name.toLowerCase()));
+ const FIT = {
+ gpu: { label: "🟢 fits GPU", color: "#8aff8a" },
+ ram: { label: "🟡 runs on RAM (CPU)", color: "#e8c65a" },
+ no: { label: "🔴 too big", color: "#ff8a80" },
+ };
+
return (