codemaxxing 0.4.11 → 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: "" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.4.11",
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}>