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/README.md +16 -16
- package/dist/config.d.ts +3 -0
- package/dist/config.js +13 -0
- package/dist/index.js +457 -8
- package/dist/utils/hardware.d.ts +17 -0
- package/dist/utils/hardware.js +120 -0
- package/dist/utils/models.d.ts +24 -0
- package/dist/utils/models.js +177 -0
- package/dist/utils/ollama.d.ts +42 -0
- package/dist/utils/ollama.js +213 -0
- package/package.json +1 -1
- package/src/config.ts +15 -0
- package/src/index.tsx +589 -6
- package/src/utils/hardware.ts +131 -0
- package/src/utils/models.ts +217 -0
- package/src/utils/ollama.ts +232 -0
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
|
-
|
|
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
|
-
|
|
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}>
|