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 +104 -3
- package/dist/utils/ollama.d.ts +2 -2
- package/dist/utils/ollama.js +83 -5
- package/package.json +1 -1
- package/src/index.tsx +139 -2
- package/src/utils/ollama.ts +85 -5
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
|
-
|
|
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
|
-
|
|
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" })] })] })),
|
|
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: "" },
|
package/dist/utils/ollama.d.ts
CHANGED
|
@@ -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 {
|
package/dist/utils/ollama.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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}>
|
package/src/utils/ollama.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|