codemaxxing 0.4.10 → 0.4.12

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/dist/index.js CHANGED
@@ -152,6 +152,10 @@ function App() {
152
152
  const [ollamaDeleteConfirm, setOllamaDeleteConfirm] = useState(null);
153
153
  const [ollamaPulling, setOllamaPulling] = useState(null);
154
154
  const [ollamaExitPrompt, setOllamaExitPrompt] = useState(false);
155
+ const [ollamaDeletePicker, setOllamaDeletePicker] = useState(null);
156
+ const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
157
+ const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
158
+ const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
155
159
  const [wizardScreen, setWizardScreen] = useState(null);
156
160
  const [wizardIndex, setWizardIndex] = useState(0);
157
161
  const [wizardHardware, setWizardHardware] = useState(null);
@@ -678,10 +682,17 @@ function App() {
678
682
  addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
679
683
  return;
680
684
  }
685
+ if (trimmed === "/ollama pull") {
686
+ // No model specified — show picker
687
+ setOllamaPullPicker(true);
688
+ setOllamaPullPickerIndex(0);
689
+ return;
690
+ }
681
691
  if (trimmed.startsWith("/ollama pull ")) {
682
692
  const modelId = trimmed.replace("/ollama pull ", "").trim();
683
693
  if (!modelId) {
684
- addMsg("info", "Usage: /ollama pull <model>\n Example: /ollama pull qwen2.5-coder:7b");
694
+ setOllamaPullPicker(true);
695
+ setOllamaPullPickerIndex(0);
685
696
  return;
686
697
  }
687
698
  if (!isOllamaInstalled()) {
@@ -719,10 +730,27 @@ function App() {
719
730
  }
720
731
  return;
721
732
  }
733
+ if (trimmed === "/ollama delete") {
734
+ // No model specified — show picker of installed models
735
+ const models = await listInstalledModelsDetailed();
736
+ if (models.length === 0) {
737
+ addMsg("info", "No models installed.");
738
+ return;
739
+ }
740
+ setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
741
+ setOllamaDeletePickerIndex(0);
742
+ return;
743
+ }
722
744
  if (trimmed.startsWith("/ollama delete ")) {
723
745
  const modelId = trimmed.replace("/ollama delete ", "").trim();
724
746
  if (!modelId) {
725
- addMsg("info", "Usage: /ollama delete <model>");
747
+ const models = await listInstalledModelsDetailed();
748
+ if (models.length === 0) {
749
+ addMsg("info", "No models installed.");
750
+ return;
751
+ }
752
+ setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
753
+ setOllamaDeletePickerIndex(0);
726
754
  return;
727
755
  }
728
756
  // Look up size for confirmation
@@ -1295,6 +1323,71 @@ function App() {
1295
1323
  }
1296
1324
  return;
1297
1325
  }
1326
+ // ── Ollama delete picker ──
1327
+ if (ollamaDeletePicker) {
1328
+ if (key.upArrow) {
1329
+ setOllamaDeletePickerIndex((prev) => (prev - 1 + ollamaDeletePicker.models.length) % ollamaDeletePicker.models.length);
1330
+ return;
1331
+ }
1332
+ if (key.downArrow) {
1333
+ setOllamaDeletePickerIndex((prev) => (prev + 1) % ollamaDeletePicker.models.length);
1334
+ return;
1335
+ }
1336
+ if (key.escape) {
1337
+ setOllamaDeletePicker(null);
1338
+ return;
1339
+ }
1340
+ if (key.return) {
1341
+ const selected = ollamaDeletePicker.models[ollamaDeletePickerIndex];
1342
+ if (selected) {
1343
+ setOllamaDeletePicker(null);
1344
+ setOllamaDeleteConfirm({ model: selected.name, size: selected.size });
1345
+ }
1346
+ return;
1347
+ }
1348
+ return;
1349
+ }
1350
+ // ── Ollama pull picker ──
1351
+ if (ollamaPullPicker) {
1352
+ const pullModels = [
1353
+ { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
1354
+ { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
1355
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
1356
+ { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
1357
+ { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
1358
+ { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
1359
+ { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
1360
+ ];
1361
+ if (key.upArrow) {
1362
+ setOllamaPullPickerIndex((prev) => (prev - 1 + pullModels.length) % pullModels.length);
1363
+ return;
1364
+ }
1365
+ if (key.downArrow) {
1366
+ setOllamaPullPickerIndex((prev) => (prev + 1) % pullModels.length);
1367
+ return;
1368
+ }
1369
+ if (key.escape) {
1370
+ setOllamaPullPicker(false);
1371
+ return;
1372
+ }
1373
+ if (key.return) {
1374
+ const selected = pullModels[ollamaPullPickerIndex];
1375
+ if (selected) {
1376
+ setOllamaPullPicker(false);
1377
+ // Trigger the pull
1378
+ setInput(`/ollama pull ${selected.id}`);
1379
+ setInputKey((k) => k + 1);
1380
+ // Submit it
1381
+ setTimeout(() => {
1382
+ const submitInput = `/ollama pull ${selected.id}`;
1383
+ setInput("");
1384
+ handleSubmit(submitInput);
1385
+ }, 50);
1386
+ }
1387
+ return;
1388
+ }
1389
+ return;
1390
+ }
1298
1391
  // ── Ollama delete confirmation ──
1299
1392
  if (ollamaDeleteConfirm) {
1300
1393
  if (inputChar === "y" || inputChar === "Y") {
@@ -1820,7 +1913,15 @@ function App() {
1820
1913
  })(), skillsPicker === "remove" && (() => {
1821
1914
  const installed = listInstalledSkills();
1822
1915
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Remove a skill:" }), installed.map((s, i) => (_jsxs(Text, { children: [i === skillsPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: [s.name, " \u2014 ", s.description] })] }, s.name))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter remove · Esc back" })] }));
1823
- })(), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Delete a session:" }), deleteSessionPicker.map((s, i) => (_jsxs(Text, { children: [i === deleteSessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete session ", deleteSessionConfirm.id, "?"] }), _jsxs(Text, { color: theme.colors.muted, children: [" ", deleteSessionConfirm.display] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaDeleteConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete ", ollamaDeleteConfirm.model, " (", (ollamaDeleteConfirm.size / (1024 * 1024 * 1024)).toFixed(1), " GB)?"] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaPulling && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" Downloading ", ollamaPulling.model, "..."] }), ollamaPulling.progress.status === "downloading" || ollamaPulling.progress.percent > 0 ? (_jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(ollamaPulling.progress.percent / 5)), "\u2591".repeat(20 - Math.floor(ollamaPulling.progress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [ollamaPulling.progress.percent, "%"] }), ollamaPulling.progress.completed != null && ollamaPulling.progress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(ollamaPulling.progress.completed), " / ", formatBytes(ollamaPulling.progress.total)] })) : null] })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", ollamaPulling.progress.status, "..."] }))] })), ollamaExitPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.warning, children: "Ollama is still running." }), _jsx(Text, { color: theme.colors.muted, children: "Stop it to free GPU memory?" }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), wizardScreen === "connection" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "No LLM detected. How do you want to connect?" }), _jsx(Text, { children: "" }), [
1916
+ })(), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Delete a session:" }), deleteSessionPicker.map((s, i) => (_jsxs(Text, { children: [i === deleteSessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete session ", deleteSessionConfirm.id, "?"] }), _jsxs(Text, { color: theme.colors.muted, children: [" ", deleteSessionConfirm.display] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaDeletePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Delete which model?" }), _jsx(Text, { children: "" }), ollamaDeletePicker.models.map((m, i) => (_jsxs(Text, { children: [" ", i === ollamaDeletePickerIndex ? _jsx(Text, { color: theme.colors.primary, bold: true, children: " " }) : " ", _jsx(Text, { color: i === ollamaDeletePickerIndex ? theme.colors.primary : undefined, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" (", (m.size / (1024 * 1024 * 1024)).toFixed(1), " GB)"] })] }, m.name))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to delete · Esc cancel" })] })), ollamaPullPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Download which model?" }), _jsx(Text, { children: "" }), [
1917
+ { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
1918
+ { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
1919
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
1920
+ { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
1921
+ { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
1922
+ { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
1923
+ { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
1924
+ ].map((m, i) => (_jsxs(Text, { children: [" ", i === ollamaPullPickerIndex ? _jsx(Text, { color: theme.colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === ollamaPullPickerIndex ? theme.colors.primary : undefined, bold: true, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" · ", m.size, " · ", m.desc] })] }, m.id))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to download · Esc cancel" })] })), ollamaDeleteConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete ", ollamaDeleteConfirm.model, " (", (ollamaDeleteConfirm.size / (1024 * 1024 * 1024)).toFixed(1), " GB)?"] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaPulling && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" Downloading ", ollamaPulling.model, "..."] }), ollamaPulling.progress.status === "downloading" || ollamaPulling.progress.percent > 0 ? (_jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(ollamaPulling.progress.percent / 5)), "\u2591".repeat(20 - Math.floor(ollamaPulling.progress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [ollamaPulling.progress.percent, "%"] }), ollamaPulling.progress.completed != null && ollamaPulling.progress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(ollamaPulling.progress.completed), " / ", formatBytes(ollamaPulling.progress.total)] })) : null] })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", ollamaPulling.progress.status, "..."] }))] })), ollamaExitPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.warning, children: "Ollama is still running." }), _jsx(Text, { color: theme.colors.muted, children: "Stop it to free GPU memory?" }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), wizardScreen === "connection" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "No LLM detected. How do you want to connect?" }), _jsx(Text, { children: "" }), [
1824
1925
  { key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
1825
1926
  { key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
1826
1927
  { key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
@@ -13,9 +13,9 @@ export interface PullProgress {
13
13
  percent: number;
14
14
  }
15
15
  /**
16
- * Pull a model from Ollama registry.
16
+ * Pull a model from Ollama registry via HTTP API.
17
+ * Falls back to CLI if API fails.
17
18
  * Calls onProgress with download updates.
18
- * Returns a promise that resolves when complete.
19
19
  */
20
20
  export declare function pullModel(modelId: string, onProgress?: (progress: PullProgress) => void): Promise<void>;
21
21
  export interface OllamaModelInfo {
@@ -69,22 +69,98 @@ export function getOllamaInstallCommand(os) {
69
69
  case "windows": return "winget install Ollama.Ollama";
70
70
  }
71
71
  }
72
+ /** Find the ollama binary path */
73
+ function findOllamaBinary() {
74
+ // Try PATH first
75
+ try {
76
+ const cmd = process.platform === "win32" ? "where ollama" : "which ollama";
77
+ return execSync(cmd, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 }).toString().trim().split("\n")[0];
78
+ }
79
+ catch { }
80
+ // Check known Windows paths
81
+ if (process.platform === "win32") {
82
+ for (const p of getWindowsOllamaPaths()) {
83
+ if (existsSync(p))
84
+ return p;
85
+ }
86
+ }
87
+ return "ollama"; // fallback, hope for the best
88
+ }
72
89
  /** Start ollama serve in background */
73
90
  export function startOllama() {
74
- const child = spawn("ollama", ["serve"], {
91
+ const bin = findOllamaBinary();
92
+ const child = spawn(bin, ["serve"], {
75
93
  detached: true,
76
94
  stdio: "ignore",
77
95
  });
78
96
  child.unref();
79
97
  }
80
98
  /**
81
- * Pull a model from Ollama registry.
99
+ * Pull a model from Ollama registry via HTTP API.
100
+ * Falls back to CLI if API fails.
82
101
  * Calls onProgress with download updates.
83
- * Returns a promise that resolves when complete.
84
102
  */
85
103
  export function pullModel(modelId, onProgress) {
104
+ // Try HTTP API first (works even when CLI isn't on PATH)
105
+ return pullModelViaAPI(modelId, onProgress).catch(() => {
106
+ // Fallback to CLI
107
+ return pullModelViaCLI(modelId, onProgress);
108
+ });
109
+ }
110
+ function pullModelViaAPI(modelId, onProgress) {
111
+ return new Promise(async (resolve, reject) => {
112
+ try {
113
+ const res = await fetch("http://localhost:11434/api/pull", {
114
+ method: "POST",
115
+ headers: { "Content-Type": "application/json" },
116
+ body: JSON.stringify({ name: modelId, stream: true }),
117
+ });
118
+ if (!res.ok || !res.body) {
119
+ reject(new Error(`Ollama API returned ${res.status}`));
120
+ return;
121
+ }
122
+ const reader = res.body.getReader();
123
+ const decoder = new TextDecoder();
124
+ let buffer = "";
125
+ while (true) {
126
+ const { done, value } = await reader.read();
127
+ if (done)
128
+ break;
129
+ buffer += decoder.decode(value, { stream: true });
130
+ const lines = buffer.split("\n");
131
+ buffer = lines.pop() || "";
132
+ for (const line of lines) {
133
+ if (!line.trim())
134
+ continue;
135
+ try {
136
+ const data = JSON.parse(line);
137
+ if (data.status === "success") {
138
+ onProgress?.({ status: "success", percent: 100 });
139
+ resolve();
140
+ return;
141
+ }
142
+ if (data.total && data.completed) {
143
+ const percent = Math.round((data.completed / data.total) * 100);
144
+ onProgress?.({ status: "downloading", total: data.total, completed: data.completed, percent });
145
+ }
146
+ else {
147
+ onProgress?.({ status: data.status || "working...", percent: 0 });
148
+ }
149
+ }
150
+ catch { }
151
+ }
152
+ }
153
+ resolve();
154
+ }
155
+ catch (err) {
156
+ reject(err);
157
+ }
158
+ });
159
+ }
160
+ function pullModelViaCLI(modelId, onProgress) {
86
161
  return new Promise((resolve, reject) => {
87
- const child = spawn("ollama", ["pull", modelId], {
162
+ const bin = findOllamaBinary();
163
+ const child = spawn(bin, ["pull", modelId], {
88
164
  stdio: ["pipe", "pipe", "pipe"],
89
165
  });
90
166
  let lastOutput = "";
@@ -172,7 +248,9 @@ export async function stopOllama() {
172
248
  try {
173
249
  // First unload all models from memory
174
250
  try {
175
- execSync("ollama stop", { stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
251
+ const bin = findOllamaBinary();
252
+ const { spawnSync } = require("child_process");
253
+ spawnSync(bin, ["stop"], { stdio: "pipe", timeout: 5000 });
176
254
  }
177
255
  catch { /* may fail if no models loaded */ }
178
256
  // Kill the server process
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.tsx CHANGED
@@ -187,6 +187,10 @@ function App() {
187
187
  const [ollamaDeleteConfirm, setOllamaDeleteConfirm] = useState<{ model: string; size: number } | null>(null);
188
188
  const [ollamaPulling, setOllamaPulling] = useState<{ model: string; progress: PullProgress } | null>(null);
189
189
  const [ollamaExitPrompt, setOllamaExitPrompt] = useState(false);
190
+ const [ollamaDeletePicker, setOllamaDeletePicker] = useState<{ models: { name: string; size: number }[] } | null>(null);
191
+ const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
192
+ const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
193
+ const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
190
194
 
191
195
  // ── Setup Wizard State ──
192
196
  type WizardScreen = "connection" | "models" | "install-ollama" | "pulling" | null;
@@ -722,10 +726,17 @@ function App() {
722
726
  addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
723
727
  return;
724
728
  }
729
+ if (trimmed === "/ollama pull") {
730
+ // No model specified — show picker
731
+ setOllamaPullPicker(true);
732
+ setOllamaPullPickerIndex(0);
733
+ return;
734
+ }
725
735
  if (trimmed.startsWith("/ollama pull ")) {
726
736
  const modelId = trimmed.replace("/ollama pull ", "").trim();
727
737
  if (!modelId) {
728
- addMsg("info", "Usage: /ollama pull <model>\n Example: /ollama pull qwen2.5-coder:7b");
738
+ setOllamaPullPicker(true);
739
+ setOllamaPullPickerIndex(0);
729
740
  return;
730
741
  }
731
742
  if (!isOllamaInstalled()) {
@@ -759,10 +770,27 @@ function App() {
759
770
  }
760
771
  return;
761
772
  }
773
+ if (trimmed === "/ollama delete") {
774
+ // No model specified — show picker of installed models
775
+ const models = await listInstalledModelsDetailed();
776
+ if (models.length === 0) {
777
+ addMsg("info", "No models installed.");
778
+ return;
779
+ }
780
+ setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
781
+ setOllamaDeletePickerIndex(0);
782
+ return;
783
+ }
762
784
  if (trimmed.startsWith("/ollama delete ")) {
763
785
  const modelId = trimmed.replace("/ollama delete ", "").trim();
764
786
  if (!modelId) {
765
- addMsg("info", "Usage: /ollama delete <model>");
787
+ const models = await listInstalledModelsDetailed();
788
+ if (models.length === 0) {
789
+ addMsg("info", "No models installed.");
790
+ return;
791
+ }
792
+ setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
793
+ setOllamaDeletePickerIndex(0);
766
794
  return;
767
795
  }
768
796
  // Look up size for confirmation
@@ -1317,6 +1345,73 @@ function App() {
1317
1345
  return;
1318
1346
  }
1319
1347
 
1348
+ // ── Ollama delete picker ──
1349
+ if (ollamaDeletePicker) {
1350
+ if (key.upArrow) {
1351
+ setOllamaDeletePickerIndex((prev) => (prev - 1 + ollamaDeletePicker.models.length) % ollamaDeletePicker.models.length);
1352
+ return;
1353
+ }
1354
+ if (key.downArrow) {
1355
+ setOllamaDeletePickerIndex((prev) => (prev + 1) % ollamaDeletePicker.models.length);
1356
+ return;
1357
+ }
1358
+ if (key.escape) {
1359
+ setOllamaDeletePicker(null);
1360
+ return;
1361
+ }
1362
+ if (key.return) {
1363
+ const selected = ollamaDeletePicker.models[ollamaDeletePickerIndex];
1364
+ if (selected) {
1365
+ setOllamaDeletePicker(null);
1366
+ setOllamaDeleteConfirm({ model: selected.name, size: selected.size });
1367
+ }
1368
+ return;
1369
+ }
1370
+ return;
1371
+ }
1372
+
1373
+ // ── Ollama pull picker ──
1374
+ if (ollamaPullPicker) {
1375
+ const pullModels = [
1376
+ { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
1377
+ { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
1378
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
1379
+ { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
1380
+ { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
1381
+ { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
1382
+ { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
1383
+ ];
1384
+ if (key.upArrow) {
1385
+ setOllamaPullPickerIndex((prev) => (prev - 1 + pullModels.length) % pullModels.length);
1386
+ return;
1387
+ }
1388
+ if (key.downArrow) {
1389
+ setOllamaPullPickerIndex((prev) => (prev + 1) % pullModels.length);
1390
+ return;
1391
+ }
1392
+ if (key.escape) {
1393
+ setOllamaPullPicker(false);
1394
+ return;
1395
+ }
1396
+ if (key.return) {
1397
+ const selected = pullModels[ollamaPullPickerIndex];
1398
+ if (selected) {
1399
+ setOllamaPullPicker(false);
1400
+ // Trigger the pull
1401
+ setInput(`/ollama pull ${selected.id}`);
1402
+ setInputKey((k) => k + 1);
1403
+ // Submit it
1404
+ setTimeout(() => {
1405
+ const submitInput = `/ollama pull ${selected.id}`;
1406
+ setInput("");
1407
+ handleSubmit(submitInput);
1408
+ }, 50);
1409
+ }
1410
+ return;
1411
+ }
1412
+ return;
1413
+ }
1414
+
1320
1415
  // ── Ollama delete confirmation ──
1321
1416
  if (ollamaDeleteConfirm) {
1322
1417
  if (inputChar === "y" || inputChar === "Y") {
@@ -2072,6 +2167,48 @@ function App() {
2072
2167
  </Box>
2073
2168
  )}
2074
2169
 
2170
+ {/* ═══ OLLAMA DELETE PICKER ═══ */}
2171
+ {ollamaDeletePicker && (
2172
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2173
+ <Text bold color={theme.colors.secondary}>Delete which model?</Text>
2174
+ <Text>{""}</Text>
2175
+ {ollamaDeletePicker.models.map((m, i) => (
2176
+ <Text key={m.name}>
2177
+ {" "}{i === ollamaDeletePickerIndex ? <Text color={theme.colors.primary} bold>{"▸ "}</Text> : " "}
2178
+ <Text color={i === ollamaDeletePickerIndex ? theme.colors.primary : undefined}>{m.name}</Text>
2179
+ <Text color={theme.colors.muted}>{" ("}{(m.size / (1024 * 1024 * 1024)).toFixed(1)}{" GB)"}</Text>
2180
+ </Text>
2181
+ ))}
2182
+ <Text>{""}</Text>
2183
+ <Text dimColor>{" ↑↓ navigate · Enter to delete · Esc cancel"}</Text>
2184
+ </Box>
2185
+ )}
2186
+
2187
+ {/* ═══ OLLAMA PULL PICKER ═══ */}
2188
+ {ollamaPullPicker && (
2189
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2190
+ <Text bold color={theme.colors.secondary}>Download which model?</Text>
2191
+ <Text>{""}</Text>
2192
+ {[
2193
+ { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
2194
+ { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
2195
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
2196
+ { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
2197
+ { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
2198
+ { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
2199
+ { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
2200
+ ].map((m, i) => (
2201
+ <Text key={m.id}>
2202
+ {" "}{i === ollamaPullPickerIndex ? <Text color={theme.colors.primary} bold>{"▸ "}</Text> : " "}
2203
+ <Text color={i === ollamaPullPickerIndex ? theme.colors.primary : undefined} bold>{m.name}</Text>
2204
+ <Text color={theme.colors.muted}>{" · "}{m.size}{" · "}{m.desc}</Text>
2205
+ </Text>
2206
+ ))}
2207
+ <Text>{""}</Text>
2208
+ <Text dimColor>{" ↑↓ navigate · Enter to download · Esc cancel"}</Text>
2209
+ </Box>
2210
+ )}
2211
+
2075
2212
  {/* ═══ OLLAMA DELETE CONFIRM ═══ */}
2076
2213
  {ollamaDeleteConfirm && (
2077
2214
  <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
@@ -72,9 +72,26 @@ export function getOllamaInstallCommand(os: "macos" | "linux" | "windows"): stri
72
72
  }
73
73
  }
74
74
 
75
+ /** Find the ollama binary path */
76
+ function findOllamaBinary(): string {
77
+ // Try PATH first
78
+ try {
79
+ const cmd = process.platform === "win32" ? "where ollama" : "which ollama";
80
+ return execSync(cmd, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 }).toString().trim().split("\n")[0];
81
+ } catch {}
82
+ // Check known Windows paths
83
+ if (process.platform === "win32") {
84
+ for (const p of getWindowsOllamaPaths()) {
85
+ if (existsSync(p)) return p;
86
+ }
87
+ }
88
+ return "ollama"; // fallback, hope for the best
89
+ }
90
+
75
91
  /** Start ollama serve in background */
76
92
  export function startOllama(): void {
77
- const child = spawn("ollama", ["serve"], {
93
+ const bin = findOllamaBinary();
94
+ const child = spawn(bin, ["serve"], {
78
95
  detached: true,
79
96
  stdio: "ignore",
80
97
  });
@@ -89,16 +106,77 @@ export interface PullProgress {
89
106
  }
90
107
 
91
108
  /**
92
- * Pull a model from Ollama registry.
109
+ * Pull a model from Ollama registry via HTTP API.
110
+ * Falls back to CLI if API fails.
93
111
  * Calls onProgress with download updates.
94
- * Returns a promise that resolves when complete.
95
112
  */
96
113
  export function pullModel(
97
114
  modelId: string,
98
115
  onProgress?: (progress: PullProgress) => void
116
+ ): Promise<void> {
117
+ // Try HTTP API first (works even when CLI isn't on PATH)
118
+ return pullModelViaAPI(modelId, onProgress).catch(() => {
119
+ // Fallback to CLI
120
+ return pullModelViaCLI(modelId, onProgress);
121
+ });
122
+ }
123
+
124
+ function pullModelViaAPI(
125
+ modelId: string,
126
+ onProgress?: (progress: PullProgress) => void
127
+ ): Promise<void> {
128
+ return new Promise(async (resolve, reject) => {
129
+ try {
130
+ const res = await fetch("http://localhost:11434/api/pull", {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ name: modelId, stream: true }),
134
+ });
135
+ if (!res.ok || !res.body) {
136
+ reject(new Error(`Ollama API returned ${res.status}`));
137
+ return;
138
+ }
139
+ const reader = res.body.getReader();
140
+ const decoder = new TextDecoder();
141
+ let buffer = "";
142
+ while (true) {
143
+ const { done, value } = await reader.read();
144
+ if (done) break;
145
+ buffer += decoder.decode(value, { stream: true });
146
+ const lines = buffer.split("\n");
147
+ buffer = lines.pop() || "";
148
+ for (const line of lines) {
149
+ if (!line.trim()) continue;
150
+ try {
151
+ const data = JSON.parse(line);
152
+ if (data.status === "success") {
153
+ onProgress?.({ status: "success", percent: 100 });
154
+ resolve();
155
+ return;
156
+ }
157
+ if (data.total && data.completed) {
158
+ const percent = Math.round((data.completed / data.total) * 100);
159
+ onProgress?.({ status: "downloading", total: data.total, completed: data.completed, percent });
160
+ } else {
161
+ onProgress?.({ status: data.status || "working...", percent: 0 });
162
+ }
163
+ } catch {}
164
+ }
165
+ }
166
+ resolve();
167
+ } catch (err: any) {
168
+ reject(err);
169
+ }
170
+ });
171
+ }
172
+
173
+ function pullModelViaCLI(
174
+ modelId: string,
175
+ onProgress?: (progress: PullProgress) => void
99
176
  ): Promise<void> {
100
177
  return new Promise((resolve, reject) => {
101
- const child = spawn("ollama", ["pull", modelId], {
178
+ const bin = findOllamaBinary();
179
+ const child = spawn(bin, ["pull", modelId], {
102
180
  stdio: ["pipe", "pipe", "pipe"],
103
181
  });
104
182
 
@@ -198,7 +276,9 @@ export async function stopOllama(): Promise<{ ok: boolean; message: string }> {
198
276
  try {
199
277
  // First unload all models from memory
200
278
  try {
201
- execSync("ollama stop", { stdio: ["pipe", "pipe", "pipe"], timeout: 5000 });
279
+ const bin = findOllamaBinary();
280
+ const { spawnSync } = require("child_process");
281
+ spawnSync(bin, ["stop"], { stdio: "pipe", timeout: 5000 });
202
282
  } catch { /* may fail if no models loaded */ }
203
283
 
204
284
  // Kill the server process