codemaxxing 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.tsx CHANGED
@@ -5,7 +5,7 @@ import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5
5
  import { EventEmitter } from "events";
6
6
  import TextInput from "ink-text-input";
7
7
  import { CodingAgent } from "./agent.js";
8
- import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
8
+ import { loadConfig, saveConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
9
9
  import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
10
10
  import { execSync } from "child_process";
11
11
  import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
@@ -13,6 +13,9 @@ import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./theme
13
13
  import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow, saveApiKey } from "./utils/auth.js";
14
14
  import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
15
15
  import { listServers, addServer, removeServer, getAllMCPTools, getConnectedServers } from "./utils/mcp.js";
16
+ import { detectHardware, formatBytes, type HardwareInfo } from "./utils/hardware.js";
17
+ import { getRecommendations, getRecommendationsWithLlmfit, getFitIcon, isLlmfitAvailable, type ScoredModel } from "./utils/models.js";
18
+ import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel, getGPUMemoryUsage, type PullProgress } from "./utils/ollama.js";
16
19
 
17
20
  const VERSION = "0.1.9";
18
21
 
@@ -64,6 +67,12 @@ const SLASH_COMMANDS = [
64
67
  { cmd: "/mcp add", desc: "add MCP server" },
65
68
  { cmd: "/mcp remove", desc: "remove MCP server" },
66
69
  { cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
70
+ { cmd: "/ollama", desc: "Ollama status & models" },
71
+ { cmd: "/ollama list", desc: "list installed models" },
72
+ { cmd: "/ollama start", desc: "start Ollama server" },
73
+ { cmd: "/ollama stop", desc: "stop Ollama server" },
74
+ { cmd: "/ollama pull", desc: "download a model" },
75
+ { cmd: "/ollama delete", desc: "delete a model" },
67
76
  { cmd: "/quit", desc: "exit" },
68
77
  ];
69
78
 
@@ -172,6 +181,21 @@ function App() {
172
181
  resolve: (decision: "yes" | "no" | "always") => void;
173
182
  } | null>(null);
174
183
 
184
+ // ── Ollama Management State ──
185
+ const [ollamaDeleteConfirm, setOllamaDeleteConfirm] = useState<{ model: string; size: number } | null>(null);
186
+ const [ollamaPulling, setOllamaPulling] = useState<{ model: string; progress: PullProgress } | null>(null);
187
+ const [ollamaExitPrompt, setOllamaExitPrompt] = useState(false);
188
+
189
+ // ── Setup Wizard State ──
190
+ type WizardScreen = "connection" | "models" | "install-ollama" | "pulling" | null;
191
+ const [wizardScreen, setWizardScreen] = useState<WizardScreen>(null);
192
+ const [wizardIndex, setWizardIndex] = useState(0);
193
+ const [wizardHardware, setWizardHardware] = useState<HardwareInfo | null>(null);
194
+ const [wizardModels, setWizardModels] = useState<ScoredModel[]>([]);
195
+ const [wizardPullProgress, setWizardPullProgress] = useState<PullProgress | null>(null);
196
+ const [wizardPullError, setWizardPullError] = useState<string | null>(null);
197
+ const [wizardSelectedModel, setWizardSelectedModel] = useState<ScoredModel | null>(null);
198
+
175
199
  // Listen for paste events from stdin interceptor
176
200
  useEffect(() => {
177
201
  const handler = ({ content, lines }: { content: string; lines: number }) => {
@@ -210,10 +234,11 @@ function App() {
210
234
  setConnectionInfo([...info]);
211
235
  } else {
212
236
  info.push("✗ No local LLM server found.");
213
- info.push(" /connect — retry after starting LM Studio or Ollama");
214
- info.push(" /login — authenticate with a cloud provider");
215
237
  setConnectionInfo([...info]);
216
238
  setReady(true);
239
+ // Show the setup wizard on first run
240
+ setWizardScreen("connection");
241
+ setWizardIndex(0);
217
242
  return;
218
243
  }
219
244
  } else {
@@ -369,7 +394,8 @@ function App() {
369
394
  // Commands that need args (like /commit, /model) — fill input instead of executing
370
395
  if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
371
396
  selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
372
- selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
397
+ selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect" ||
398
+ selected.cmd === "/ollama pull" || selected.cmd === "/ollama delete") {
373
399
  setInput(selected.cmd + " ");
374
400
  setCmdIndex(0);
375
401
  setInputKey((k) => k + 1);
@@ -397,7 +423,20 @@ function App() {
397
423
  addMsg("user", trimmed);
398
424
 
399
425
  if (trimmed === "/quit" || trimmed === "/exit") {
400
- exit();
426
+ // Check if Ollama is running and offer to stop it
427
+ const ollamaUp = await isOllamaRunning();
428
+ if (ollamaUp) {
429
+ const config = loadConfig();
430
+ if (config.defaults.stopOllamaOnExit) {
431
+ addMsg("info", "Stopping Ollama...");
432
+ await stopOllama();
433
+ exit();
434
+ } else {
435
+ setOllamaExitPrompt(true);
436
+ }
437
+ } else {
438
+ exit();
439
+ }
401
440
  return;
402
441
  }
403
442
  if (trimmed === "/login" || trimmed === "/auth") {
@@ -440,6 +479,12 @@ function App() {
440
479
  " /mcp add — add MCP server to global config",
441
480
  " /mcp remove — remove MCP server",
442
481
  " /mcp reconnect — reconnect all MCP servers",
482
+ " /ollama — Ollama status, models & GPU usage",
483
+ " /ollama list — list installed models with sizes",
484
+ " /ollama start — start Ollama server",
485
+ " /ollama stop — stop Ollama server (frees GPU RAM)",
486
+ " /ollama pull <model> — download a model",
487
+ " /ollama delete <model> — delete a model from disk",
443
488
  " /quit — exit",
444
489
  ].join("\n"));
445
490
  return;
@@ -600,6 +645,135 @@ function App() {
600
645
  return;
601
646
  }
602
647
 
648
+ // ── Ollama commands (work without agent) ──
649
+ if (trimmed === "/ollama" || trimmed === "/ollama status") {
650
+ const running = await isOllamaRunning();
651
+ const lines: string[] = [`Ollama: ${running ? "running" : "stopped"}`];
652
+ if (running) {
653
+ const models = await listInstalledModelsDetailed();
654
+ if (models.length > 0) {
655
+ lines.push(`Installed models (${models.length}):`);
656
+ for (const m of models) {
657
+ const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
658
+ lines.push(` ${m.name} (${sizeGB} GB)`);
659
+ }
660
+ } else {
661
+ lines.push("No models installed.");
662
+ }
663
+ const gpuMem = getGPUMemoryUsage();
664
+ if (gpuMem) lines.push(`GPU: ${gpuMem}`);
665
+ } else {
666
+ lines.push("Start with: /ollama start");
667
+ }
668
+ addMsg("info", lines.join("\n"));
669
+ return;
670
+ }
671
+ if (trimmed === "/ollama list") {
672
+ const running = await isOllamaRunning();
673
+ if (!running) {
674
+ addMsg("info", "Ollama is not running. Start with /ollama start");
675
+ return;
676
+ }
677
+ const models = await listInstalledModelsDetailed();
678
+ if (models.length === 0) {
679
+ addMsg("info", "No models installed. Pull one with /ollama pull <model>");
680
+ } else {
681
+ const lines = models.map((m) => {
682
+ const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
683
+ return ` ${m.name} (${sizeGB} GB)`;
684
+ });
685
+ addMsg("info", `Installed models:\n${lines.join("\n")}`);
686
+ }
687
+ return;
688
+ }
689
+ if (trimmed === "/ollama start") {
690
+ const running = await isOllamaRunning();
691
+ if (running) {
692
+ addMsg("info", "Ollama is already running.");
693
+ return;
694
+ }
695
+ if (!isOllamaInstalled()) {
696
+ addMsg("error", `Ollama is not installed. Install with: ${getOllamaInstallCommand(process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")}`);
697
+ return;
698
+ }
699
+ startOllama();
700
+ addMsg("info", "Starting Ollama server...");
701
+ // Wait for it to come up
702
+ for (let i = 0; i < 10; i++) {
703
+ await new Promise(r => setTimeout(r, 1000));
704
+ if (await isOllamaRunning()) {
705
+ addMsg("info", "Ollama is running.");
706
+ return;
707
+ }
708
+ }
709
+ addMsg("error", "Ollama did not start in time. Try running 'ollama serve' manually.");
710
+ return;
711
+ }
712
+ if (trimmed === "/ollama stop") {
713
+ const running = await isOllamaRunning();
714
+ if (!running) {
715
+ addMsg("info", "Ollama is not running.");
716
+ return;
717
+ }
718
+ addMsg("info", "Stopping Ollama...");
719
+ const result = await stopOllama();
720
+ addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
721
+ return;
722
+ }
723
+ if (trimmed.startsWith("/ollama pull ")) {
724
+ const modelId = trimmed.replace("/ollama pull ", "").trim();
725
+ if (!modelId) {
726
+ addMsg("info", "Usage: /ollama pull <model>\n Example: /ollama pull qwen2.5-coder:7b");
727
+ return;
728
+ }
729
+ if (!isOllamaInstalled()) {
730
+ addMsg("error", "Ollama is not installed.");
731
+ return;
732
+ }
733
+ // Ensure ollama is running
734
+ let running = await isOllamaRunning();
735
+ if (!running) {
736
+ startOllama();
737
+ addMsg("info", "Starting Ollama server...");
738
+ for (let i = 0; i < 10; i++) {
739
+ await new Promise(r => setTimeout(r, 1000));
740
+ if (await isOllamaRunning()) { running = true; break; }
741
+ }
742
+ if (!running) {
743
+ addMsg("error", "Could not start Ollama. Run 'ollama serve' manually.");
744
+ return;
745
+ }
746
+ }
747
+ setOllamaPulling({ model: modelId, progress: { status: "starting", percent: 0 } });
748
+ try {
749
+ await pullModel(modelId, (p) => {
750
+ setOllamaPulling({ model: modelId, progress: p });
751
+ });
752
+ setOllamaPulling(null);
753
+ addMsg("info", `\u2705 Downloaded ${modelId}`);
754
+ } catch (err: any) {
755
+ setOllamaPulling(null);
756
+ addMsg("error", `Failed to pull ${modelId}: ${err.message}`);
757
+ }
758
+ return;
759
+ }
760
+ if (trimmed.startsWith("/ollama delete ")) {
761
+ const modelId = trimmed.replace("/ollama delete ", "").trim();
762
+ if (!modelId) {
763
+ addMsg("info", "Usage: /ollama delete <model>");
764
+ return;
765
+ }
766
+ // Look up size for confirmation
767
+ const models = await listInstalledModelsDetailed();
768
+ const found = models.find((m) => m.name === modelId || m.name.startsWith(modelId));
769
+ if (!found) {
770
+ addMsg("error", `Model "${modelId}" not found. Use /ollama list to see installed models.`);
771
+ return;
772
+ }
773
+ setOllamaDeleteConfirm({ model: found.name, size: found.size });
774
+ return;
775
+ }
776
+
603
777
  // ── MCP commands (partially work without agent) ──
604
778
  if (trimmed === "/mcp" || trimmed === "/mcp list") {
605
779
  const servers = listServers(process.cwd());
@@ -1141,6 +1315,265 @@ function App() {
1141
1315
  return;
1142
1316
  }
1143
1317
 
1318
+ // ── Ollama delete confirmation ──
1319
+ if (ollamaDeleteConfirm) {
1320
+ if (inputChar === "y" || inputChar === "Y") {
1321
+ const model = ollamaDeleteConfirm.model;
1322
+ setOllamaDeleteConfirm(null);
1323
+ const result = deleteModel(model);
1324
+ addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
1325
+ return;
1326
+ }
1327
+ if (inputChar === "n" || inputChar === "N" || key.escape) {
1328
+ setOllamaDeleteConfirm(null);
1329
+ addMsg("info", "Delete cancelled.");
1330
+ return;
1331
+ }
1332
+ return;
1333
+ }
1334
+
1335
+ // ── Ollama exit prompt ──
1336
+ if (ollamaExitPrompt) {
1337
+ if (inputChar === "y" || inputChar === "Y") {
1338
+ setOllamaExitPrompt(false);
1339
+ stopOllama().then(() => exit());
1340
+ return;
1341
+ }
1342
+ if (inputChar === "n" || inputChar === "N") {
1343
+ setOllamaExitPrompt(false);
1344
+ exit();
1345
+ return;
1346
+ }
1347
+ if (inputChar === "a" || inputChar === "A") {
1348
+ setOllamaExitPrompt(false);
1349
+ saveConfig({ defaults: { ...loadConfig().defaults, stopOllamaOnExit: true } });
1350
+ addMsg("info", "Saved preference: always stop Ollama on exit.");
1351
+ stopOllama().then(() => exit());
1352
+ return;
1353
+ }
1354
+ if (key.escape) {
1355
+ setOllamaExitPrompt(false);
1356
+ return;
1357
+ }
1358
+ return;
1359
+ }
1360
+
1361
+ // ── Setup Wizard Navigation ──
1362
+ if (wizardScreen) {
1363
+ if (wizardScreen === "connection") {
1364
+ const items = ["local", "openrouter", "apikey", "existing"];
1365
+ if (key.upArrow) {
1366
+ setWizardIndex((prev) => (prev - 1 + items.length) % items.length);
1367
+ return;
1368
+ }
1369
+ if (key.downArrow) {
1370
+ setWizardIndex((prev) => (prev + 1) % items.length);
1371
+ return;
1372
+ }
1373
+ if (key.escape) {
1374
+ setWizardScreen(null);
1375
+ return;
1376
+ }
1377
+ if (key.return) {
1378
+ const selected = items[wizardIndex];
1379
+ if (selected === "local") {
1380
+ // Scan hardware and show model picker (use llmfit if available)
1381
+ const hw = detectHardware();
1382
+ setWizardHardware(hw);
1383
+ const { models: recs } = getRecommendationsWithLlmfit(hw);
1384
+ setWizardModels(recs.filter(m => m.fit !== "skip"));
1385
+ setWizardScreen("models");
1386
+ setWizardIndex(0);
1387
+ } else if (selected === "openrouter") {
1388
+ setWizardScreen(null);
1389
+ addMsg("info", "Starting OpenRouter OAuth — opening browser...");
1390
+ setLoading(true);
1391
+ setSpinnerMsg("Waiting for authorization...");
1392
+ openRouterOAuth((msg: string) => addMsg("info", msg))
1393
+ .then(() => {
1394
+ addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
1395
+ setLoading(false);
1396
+ })
1397
+ .catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
1398
+ } else if (selected === "apikey") {
1399
+ setWizardScreen(null);
1400
+ setLoginPicker(true);
1401
+ setLoginPickerIndex(0);
1402
+ } else if (selected === "existing") {
1403
+ setWizardScreen(null);
1404
+ addMsg("info", "Start your LLM server, then type /connect to retry.");
1405
+ }
1406
+ return;
1407
+ }
1408
+ return;
1409
+ }
1410
+
1411
+ if (wizardScreen === "models") {
1412
+ const models = wizardModels;
1413
+ if (key.upArrow) {
1414
+ setWizardIndex((prev) => (prev - 1 + models.length) % models.length);
1415
+ return;
1416
+ }
1417
+ if (key.downArrow) {
1418
+ setWizardIndex((prev) => (prev + 1) % models.length);
1419
+ return;
1420
+ }
1421
+ if (key.escape) {
1422
+ setWizardScreen("connection");
1423
+ setWizardIndex(0);
1424
+ return;
1425
+ }
1426
+ if (key.return) {
1427
+ const selected = models[wizardIndex];
1428
+ if (selected) {
1429
+ setWizardSelectedModel(selected);
1430
+ // Check if Ollama is installed
1431
+ if (!isOllamaInstalled()) {
1432
+ setWizardScreen("install-ollama");
1433
+ } else {
1434
+ // Start pulling the model
1435
+ setWizardScreen("pulling");
1436
+ setWizardPullProgress({ status: "starting", percent: 0 });
1437
+ setWizardPullError(null);
1438
+
1439
+ (async () => {
1440
+ try {
1441
+ // Ensure ollama is running
1442
+ const running = await isOllamaRunning();
1443
+ if (!running) {
1444
+ setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
1445
+ startOllama();
1446
+ // Wait for it to come up
1447
+ for (let i = 0; i < 15; i++) {
1448
+ await new Promise(r => setTimeout(r, 1000));
1449
+ if (await isOllamaRunning()) break;
1450
+ }
1451
+ if (!(await isOllamaRunning())) {
1452
+ setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
1453
+ return;
1454
+ }
1455
+ }
1456
+
1457
+ await pullModel(selected.ollamaId, (p) => {
1458
+ setWizardPullProgress(p);
1459
+ });
1460
+
1461
+ setWizardPullProgress({ status: "success", percent: 100 });
1462
+
1463
+ // Wait briefly then connect
1464
+ await new Promise(r => setTimeout(r, 500));
1465
+ setWizardScreen(null);
1466
+ setWizardPullProgress(null);
1467
+ setWizardSelectedModel(null);
1468
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1469
+ await connectToProvider(true);
1470
+ } catch (err: any) {
1471
+ setWizardPullError(err.message);
1472
+ }
1473
+ })();
1474
+ }
1475
+ }
1476
+ return;
1477
+ }
1478
+ return;
1479
+ }
1480
+
1481
+ if (wizardScreen === "install-ollama") {
1482
+ if (key.escape) {
1483
+ setWizardScreen("models");
1484
+ setWizardIndex(0);
1485
+ return;
1486
+ }
1487
+ if (key.return) {
1488
+ // User says they installed it — check and proceed
1489
+ if (isOllamaInstalled()) {
1490
+ const selected = wizardSelectedModel;
1491
+ if (selected) {
1492
+ setWizardScreen("pulling");
1493
+ setWizardPullProgress({ status: "starting", percent: 0 });
1494
+ setWizardPullError(null);
1495
+
1496
+ (async () => {
1497
+ try {
1498
+ const running = await isOllamaRunning();
1499
+ if (!running) {
1500
+ setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
1501
+ startOllama();
1502
+ for (let i = 0; i < 15; i++) {
1503
+ await new Promise(r => setTimeout(r, 1000));
1504
+ if (await isOllamaRunning()) break;
1505
+ }
1506
+ if (!(await isOllamaRunning())) {
1507
+ setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
1508
+ return;
1509
+ }
1510
+ }
1511
+ await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
1512
+ setWizardPullProgress({ status: "success", percent: 100 });
1513
+ await new Promise(r => setTimeout(r, 500));
1514
+ setWizardScreen(null);
1515
+ setWizardPullProgress(null);
1516
+ setWizardSelectedModel(null);
1517
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1518
+ await connectToProvider(true);
1519
+ } catch (err: any) {
1520
+ setWizardPullError(err.message);
1521
+ }
1522
+ })();
1523
+ }
1524
+ } else {
1525
+ addMsg("info", "Ollama not found yet. Install it and press Enter again.");
1526
+ }
1527
+ return;
1528
+ }
1529
+ return;
1530
+ }
1531
+
1532
+ if (wizardScreen === "pulling") {
1533
+ // Allow retry on error
1534
+ if (wizardPullError && key.return) {
1535
+ const selected = wizardSelectedModel;
1536
+ if (selected) {
1537
+ setWizardPullError(null);
1538
+ setWizardPullProgress({ status: "retrying", percent: 0 });
1539
+ (async () => {
1540
+ try {
1541
+ const running = await isOllamaRunning();
1542
+ if (!running) {
1543
+ startOllama();
1544
+ for (let i = 0; i < 15; i++) {
1545
+ await new Promise(r => setTimeout(r, 1000));
1546
+ if (await isOllamaRunning()) break;
1547
+ }
1548
+ }
1549
+ await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
1550
+ setWizardPullProgress({ status: "success", percent: 100 });
1551
+ await new Promise(r => setTimeout(r, 500));
1552
+ setWizardScreen(null);
1553
+ setWizardPullProgress(null);
1554
+ setWizardSelectedModel(null);
1555
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1556
+ await connectToProvider(true);
1557
+ } catch (err: any) {
1558
+ setWizardPullError(err.message);
1559
+ }
1560
+ })();
1561
+ }
1562
+ return;
1563
+ }
1564
+ if (wizardPullError && key.escape) {
1565
+ setWizardScreen("models");
1566
+ setWizardIndex(0);
1567
+ setWizardPullError(null);
1568
+ setWizardPullProgress(null);
1569
+ return;
1570
+ }
1571
+ return; // Ignore keys while pulling
1572
+ }
1573
+
1574
+ return;
1575
+ }
1576
+
1144
1577
  // Theme picker navigation
1145
1578
  if (themePicker) {
1146
1579
  const themeKeys = listThemes();
@@ -1297,7 +1730,13 @@ function App() {
1297
1730
 
1298
1731
  if (key.ctrl && inputChar === "c") {
1299
1732
  if (ctrlCPressed) {
1300
- exit();
1733
+ // Force quit on second Ctrl+C — don't block
1734
+ const config = loadConfig();
1735
+ if (config.defaults.stopOllamaOnExit) {
1736
+ stopOllama().finally(() => exit());
1737
+ } else {
1738
+ exit();
1739
+ }
1301
1740
  } else {
1302
1741
  setCtrlCPressed(true);
1303
1742
  addMsg("info", "Press Ctrl+C again to exit.");
@@ -1602,6 +2041,150 @@ function App() {
1602
2041
  </Box>
1603
2042
  )}
1604
2043
 
2044
+ {/* ═══ OLLAMA DELETE CONFIRM ═══ */}
2045
+ {ollamaDeleteConfirm && (
2046
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
2047
+ <Text bold color={theme.colors.warning}>Delete {ollamaDeleteConfirm.model} ({(ollamaDeleteConfirm.size / (1024 * 1024 * 1024)).toFixed(1)} GB)?</Text>
2048
+ <Text>
2049
+ <Text color={theme.colors.error} bold> [y]</Text><Text>es </Text>
2050
+ <Text color={theme.colors.success} bold>[n]</Text><Text>o</Text>
2051
+ </Text>
2052
+ </Box>
2053
+ )}
2054
+
2055
+ {/* ═══ OLLAMA PULL PROGRESS ═══ */}
2056
+ {ollamaPulling && (
2057
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2058
+ <Text bold color={theme.colors.secondary}>{" Downloading "}{ollamaPulling.model}{"..."}</Text>
2059
+ {ollamaPulling.progress.status === "downloading" || ollamaPulling.progress.percent > 0 ? (
2060
+ <Text>
2061
+ {" "}
2062
+ <Text color={theme.colors.primary}>
2063
+ {"\u2588".repeat(Math.floor(ollamaPulling.progress.percent / 5))}
2064
+ {"\u2591".repeat(20 - Math.floor(ollamaPulling.progress.percent / 5))}
2065
+ </Text>
2066
+ {" "}<Text bold>{ollamaPulling.progress.percent}%</Text>
2067
+ {ollamaPulling.progress.completed != null && ollamaPulling.progress.total != null ? (
2068
+ <Text color={theme.colors.muted}>{" \u00B7 "}{formatBytes(ollamaPulling.progress.completed)}{" / "}{formatBytes(ollamaPulling.progress.total)}</Text>
2069
+ ) : null}
2070
+ </Text>
2071
+ ) : (
2072
+ <Text color={theme.colors.muted}>{" "}{ollamaPulling.progress.status}...</Text>
2073
+ )}
2074
+ </Box>
2075
+ )}
2076
+
2077
+ {/* ═══ OLLAMA EXIT PROMPT ═══ */}
2078
+ {ollamaExitPrompt && (
2079
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
2080
+ <Text bold color={theme.colors.warning}>Ollama is still running.</Text>
2081
+ <Text color={theme.colors.muted}>Stop it to free GPU memory?</Text>
2082
+ <Text>
2083
+ <Text color={theme.colors.success} bold> [y]</Text><Text>es </Text>
2084
+ <Text color={theme.colors.error} bold>[n]</Text><Text>o </Text>
2085
+ <Text color={theme.colors.primary} bold>[a]</Text><Text>lways</Text>
2086
+ </Text>
2087
+ </Box>
2088
+ )}
2089
+
2090
+ {/* ═══ SETUP WIZARD ═══ */}
2091
+ {wizardScreen === "connection" && (
2092
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2093
+ <Text bold color={theme.colors.secondary}>No LLM detected. How do you want to connect?</Text>
2094
+ <Text>{""}</Text>
2095
+ {[
2096
+ { key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
2097
+ { key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
2098
+ { key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
2099
+ { key: "existing", icon: "\u2699\uFE0F", label: "I already have a server running", desc: "" },
2100
+ ].map((item, i) => (
2101
+ <Text key={item.key}>
2102
+ {i === wizardIndex ? <Text color={theme.colors.suggestion} bold>{" \u25B8 "}</Text> : <Text>{" "}</Text>}
2103
+ <Text color={i === wizardIndex ? theme.colors.suggestion : theme.colors.primary} bold>{item.icon} {item.label}</Text>
2104
+ {item.desc ? <Text color={theme.colors.muted}>{" ("}{item.desc}{")"}</Text> : null}
2105
+ </Text>
2106
+ ))}
2107
+ <Text>{""}</Text>
2108
+ <Text dimColor>{" \u2191\u2193 navigate \u00B7 Enter to select"}</Text>
2109
+ </Box>
2110
+ )}
2111
+
2112
+ {wizardScreen === "models" && wizardHardware && (
2113
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2114
+ <Text bold color={theme.colors.secondary}>Your hardware:</Text>
2115
+ <Text color={theme.colors.muted}>{" CPU: "}{wizardHardware.cpu.name}{" ("}{wizardHardware.cpu.cores}{" cores)"}</Text>
2116
+ <Text color={theme.colors.muted}>{" RAM: "}{formatBytes(wizardHardware.ram)}</Text>
2117
+ {wizardHardware.gpu ? (
2118
+ <Text color={theme.colors.muted}>{" GPU: "}{wizardHardware.gpu.name}{wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""}</Text>
2119
+ ) : (
2120
+ <Text color={theme.colors.muted}>{" GPU: none detected"}</Text>
2121
+ )}
2122
+ {!isLlmfitAvailable() && (
2123
+ <Text dimColor>{" Tip: Install llmfit for smarter recommendations: brew install llmfit"}</Text>
2124
+ )}
2125
+ <Text>{""}</Text>
2126
+ <Text bold color={theme.colors.secondary}>Recommended models:</Text>
2127
+ <Text>{""}</Text>
2128
+ {wizardModels.map((m, i) => (
2129
+ <Text key={m.ollamaId}>
2130
+ {i === wizardIndex ? <Text color={theme.colors.suggestion} bold>{" \u25B8 "}</Text> : <Text>{" "}</Text>}
2131
+ <Text>{getFitIcon(m.fit)} </Text>
2132
+ <Text color={i === wizardIndex ? theme.colors.suggestion : theme.colors.primary} bold>{m.name}</Text>
2133
+ <Text color={theme.colors.muted}>{" ~"}{m.size}{" GB \u00B7 "}{m.quality === "best" ? "Best" : m.quality === "great" ? "Great" : "Good"}{" quality \u00B7 "}{m.speed}</Text>
2134
+ </Text>
2135
+ ))}
2136
+ {wizardModels.length === 0 && (
2137
+ <Text color={theme.colors.error}>{" No suitable models found for your hardware."}</Text>
2138
+ )}
2139
+ <Text>{""}</Text>
2140
+ <Text dimColor>{" \u2191\u2193 navigate \u00B7 Enter to install \u00B7 Esc back"}</Text>
2141
+ </Box>
2142
+ )}
2143
+
2144
+ {wizardScreen === "install-ollama" && (
2145
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
2146
+ <Text bold color={theme.colors.warning}>Ollama is required for local models.</Text>
2147
+ <Text>{""}</Text>
2148
+ <Text color={theme.colors.primary}>{" Install with: "}<Text bold>{getOllamaInstallCommand(wizardHardware?.os ?? "linux")}</Text></Text>
2149
+ <Text>{""}</Text>
2150
+ <Text dimColor>{" Run the command above, then press Enter to continue..."}</Text>
2151
+ <Text dimColor>{" Esc to go back"}</Text>
2152
+ </Box>
2153
+ )}
2154
+
2155
+ {wizardScreen === "pulling" && wizardSelectedModel && (
2156
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2157
+ {wizardPullError ? (
2158
+ <>
2159
+ <Text color={theme.colors.error} bold>{" \u274C Error: "}{wizardPullError}</Text>
2160
+ <Text>{""}</Text>
2161
+ <Text dimColor>{" Press Enter to retry \u00B7 Esc to go back"}</Text>
2162
+ </>
2163
+ ) : wizardPullProgress ? (
2164
+ <>
2165
+ <Text bold color={theme.colors.secondary}>{" Downloading "}{wizardSelectedModel.name}{"..."}</Text>
2166
+ {wizardPullProgress.status === "downloading" || wizardPullProgress.percent > 0 ? (
2167
+ <>
2168
+ <Text>
2169
+ {" "}
2170
+ <Text color={theme.colors.primary}>
2171
+ {"\u2588".repeat(Math.floor(wizardPullProgress.percent / 5))}
2172
+ {"\u2591".repeat(20 - Math.floor(wizardPullProgress.percent / 5))}
2173
+ </Text>
2174
+ {" "}<Text bold>{wizardPullProgress.percent}%</Text>
2175
+ {wizardPullProgress.completed != null && wizardPullProgress.total != null ? (
2176
+ <Text color={theme.colors.muted}>{" \u00B7 "}{formatBytes(wizardPullProgress.completed)}{" / "}{formatBytes(wizardPullProgress.total)}</Text>
2177
+ ) : null}
2178
+ </Text>
2179
+ </>
2180
+ ) : (
2181
+ <Text color={theme.colors.muted}>{" "}{wizardPullProgress.status}...</Text>
2182
+ )}
2183
+ </>
2184
+ ) : null}
2185
+ </Box>
2186
+ )}
2187
+
1605
2188
  {/* ═══ COMMAND SUGGESTIONS ═══ */}
1606
2189
  {showSuggestions && (
1607
2190
  <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.muted} paddingX={1} marginBottom={0}>