codemaxxing 0.4.14 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,11 @@ curl -fsSL -o $env:TEMP\install-codemaxxing.bat https://raw.githubusercontent.co
46
46
  npm update -g codemaxxing
47
47
  ```
48
48
 
49
+ If that doesn't get the latest version:
50
+ ```bash
51
+ npm install -g codemaxxing@latest
52
+ ```
53
+
49
54
  ## Quick Start
50
55
 
51
56
  ### 1. Start Your LLM
package/dist/index.js CHANGED
@@ -156,6 +156,8 @@ function App() {
156
156
  const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
157
157
  const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
158
158
  const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
159
+ const [modelPicker, setModelPicker] = useState(null);
160
+ const [modelPickerIndex, setModelPickerIndex] = useState(0);
159
161
  const [wizardScreen, setWizardScreen] = useState(null);
160
162
  const [wizardIndex, setWizardIndex] = useState(0);
161
163
  const [wizardHardware, setWizardHardware] = useState(null);
@@ -175,6 +177,35 @@ function App() {
175
177
  pasteEvents.on("paste", handler);
176
178
  return () => { pasteEvents.off("paste", handler); };
177
179
  }, []);
180
+ // Refresh the connection banner to reflect current provider status
181
+ const refreshConnectionBanner = useCallback(async () => {
182
+ const info = [];
183
+ const cliArgs = parseCLIArgs();
184
+ const rawConfig = loadConfig();
185
+ const config = applyOverrides(rawConfig, cliArgs);
186
+ const provider = config.provider;
187
+ if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
188
+ const detected = await detectLocalProvider();
189
+ if (detected) {
190
+ info.push(`✔ Connected to ${detected.baseUrl} → ${detected.model}`);
191
+ }
192
+ else {
193
+ const ollamaUp = await isOllamaRunning();
194
+ info.push(ollamaUp ? "Ollama running (no model loaded)" : "✗ No local LLM server found");
195
+ }
196
+ }
197
+ else {
198
+ info.push(`Provider: ${provider.baseUrl}`);
199
+ info.push(`Model: ${provider.model}`);
200
+ }
201
+ const cwd = process.cwd();
202
+ if (isGitRepo(cwd)) {
203
+ const branch = getBranch(cwd);
204
+ const status = getStatus(cwd);
205
+ info.push(`Git: ${branch} (${status})`);
206
+ }
207
+ setConnectionInfo(info);
208
+ }, []);
178
209
  // Connect/reconnect to LLM provider
179
210
  const connectToProvider = useCallback(async (isRetry = false) => {
180
211
  const cliArgs = parseCLIArgs();
@@ -664,6 +695,7 @@ function App() {
664
695
  await new Promise(r => setTimeout(r, 1000));
665
696
  if (await isOllamaRunning()) {
666
697
  addMsg("info", "Ollama is running.");
698
+ await refreshConnectionBanner();
667
699
  return;
668
700
  }
669
701
  }
@@ -679,6 +711,8 @@ function App() {
679
711
  addMsg("info", "Stopping Ollama...");
680
712
  const result = await stopOllama();
681
713
  addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
714
+ if (result.ok)
715
+ await refreshConnectionBanner();
682
716
  return;
683
717
  }
684
718
  if (trimmed === "/ollama pull") {
@@ -869,10 +903,34 @@ function App() {
869
903
  }
870
904
  return;
871
905
  }
872
- if (trimmed.startsWith("/model")) {
873
- const newModel = trimmed.replace("/model", "").trim();
906
+ if (trimmed === "/model" || trimmed === "/models") {
907
+ // Show picker of available models
908
+ try {
909
+ const ollamaModels = await listInstalledModelsDetailed();
910
+ if (ollamaModels.length > 0) {
911
+ setModelPicker(ollamaModels.map(m => m.name));
912
+ setModelPickerIndex(0);
913
+ return;
914
+ }
915
+ }
916
+ catch { }
917
+ // Fallback: try provider's model list
918
+ try {
919
+ const providerModels = await listModels(providerRef.current?.baseUrl || "", providerRef.current?.apiKey || "");
920
+ if (providerModels.length > 0) {
921
+ setModelPicker(providerModels.map((m) => m.id || m));
922
+ setModelPickerIndex(0);
923
+ return;
924
+ }
925
+ }
926
+ catch { }
927
+ addMsg("info", `Current model: ${modelName}\n Usage: /model <model-name>`);
928
+ return;
929
+ }
930
+ if (trimmed.startsWith("/model ")) {
931
+ const newModel = trimmed.replace("/model ", "").trim();
874
932
  if (!newModel) {
875
- addMsg("info", `Current model: ${agent.getModel()}\n Usage: /model <model-name>`);
933
+ addMsg("info", `Current model: ${modelName}\n Usage: /model <model-name>`);
876
934
  return;
877
935
  }
878
936
  agent.switchModel(newModel);
@@ -1037,7 +1095,7 @@ function App() {
1037
1095
  }
1038
1096
  setLoading(false);
1039
1097
  setStreaming(false);
1040
- }, [agent, exit]);
1098
+ }, [agent, exit, refreshConnectionBanner]);
1041
1099
  useInput((inputChar, key) => {
1042
1100
  // Handle slash command navigation
1043
1101
  if (showSuggestionsRef.current) {
@@ -1338,6 +1396,33 @@ function App() {
1338
1396
  }
1339
1397
  return;
1340
1398
  }
1399
+ // ── Model picker ──
1400
+ if (modelPicker) {
1401
+ if (key.upArrow) {
1402
+ setModelPickerIndex((prev) => (prev - 1 + modelPicker.length) % modelPicker.length);
1403
+ return;
1404
+ }
1405
+ if (key.downArrow) {
1406
+ setModelPickerIndex((prev) => (prev + 1) % modelPicker.length);
1407
+ return;
1408
+ }
1409
+ if (key.escape) {
1410
+ setModelPicker(null);
1411
+ return;
1412
+ }
1413
+ if (key.return) {
1414
+ const selected = modelPicker[modelPickerIndex];
1415
+ if (selected && agent) {
1416
+ agent.switchModel(selected);
1417
+ setModelName(selected);
1418
+ addMsg("info", `✅ Switched to: ${selected}`);
1419
+ refreshConnectionBanner();
1420
+ }
1421
+ setModelPicker(null);
1422
+ return;
1423
+ }
1424
+ return;
1425
+ }
1341
1426
  // ── Ollama delete picker ──
1342
1427
  if (ollamaDeletePicker) {
1343
1428
  if (key.upArrow) {
@@ -1367,7 +1452,7 @@ function App() {
1367
1452
  const pullModels = [
1368
1453
  { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
1369
1454
  { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
1370
- { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
1455
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
1371
1456
  { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
1372
1457
  { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
1373
1458
  { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
@@ -1928,10 +2013,10 @@ function App() {
1928
2013
  })(), skillsPicker === "remove" && (() => {
1929
2014
  const installed = listInstalledSkills();
1930
2015
  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" })] }));
1931
- })(), 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: "" }), [
2016
+ })(), 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" })] })] })), modelPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Switch model:" }), _jsx(Text, { children: "" }), modelPicker.map((m, i) => (_jsxs(Text, { children: [" ", i === modelPickerIndex ? _jsx(Text, { color: theme.colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === modelPickerIndex ? theme.colors.primary : undefined, children: m }), m === modelName ? _jsx(Text, { color: theme.colors.success, children: " (active)" }) : null] }, m))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to switch · Esc cancel" })] })), 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: "" }), [
1932
2017
  { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
1933
2018
  { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
1934
- { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
2019
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
1935
2020
  { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
1936
2021
  { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
1937
2022
  { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
@@ -7,7 +7,7 @@ export interface RecommendedModel {
7
7
  vramOptimal: number;
8
8
  description: string;
9
9
  speed: string;
10
- quality: "good" | "great" | "best";
10
+ quality: "limited" | "good" | "great" | "best";
11
11
  }
12
12
  export type ModelFit = "perfect" | "good" | "tight" | "skip";
13
13
  export interface ScoredModel extends RecommendedModel {
@@ -6,9 +6,9 @@ const MODELS = [
6
6
  size: 2,
7
7
  ramRequired: 8,
8
8
  vramOptimal: 4,
9
- description: "Lightweight, fast coding model",
9
+ description: "\u26A0\uFE0F May not support tool calling well",
10
10
  speed: "~60 tok/s on M1",
11
- quality: "good",
11
+ quality: "limited",
12
12
  },
13
13
  {
14
14
  name: "Qwen 2.5 Coder 7B",
@@ -84,7 +84,7 @@ function scoreModel(model, ramGB, vramGB) {
84
84
  return "tight";
85
85
  return "skip";
86
86
  }
87
- const qualityOrder = { best: 3, great: 2, good: 1 };
87
+ const qualityOrder = { best: 3, great: 2, good: 1, limited: 0 };
88
88
  const fitOrder = { perfect: 4, good: 3, tight: 2, skip: 1 };
89
89
  export function getRecommendations(hardware) {
90
90
  const ramGB = hardware.ram / (1024 * 1024 * 1024);
@@ -136,7 +136,9 @@ function mapLlmfitQuality(params_b) {
136
136
  return "best";
137
137
  if (params_b >= 7)
138
138
  return "great";
139
- return "good";
139
+ if (params_b >= 5)
140
+ return "good";
141
+ return "limited";
140
142
  }
141
143
  /** Get recommendations using llmfit if available, otherwise fall back to hardcoded list */
142
144
  export function getRecommendationsWithLlmfit(hardware) {
@@ -296,7 +296,8 @@ export async function stopOllama() {
296
296
  /** Delete a model from disk */
297
297
  export function deleteModel(modelId) {
298
298
  try {
299
- execSync(`ollama rm ${modelId}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 30000 });
299
+ const bin = findOllamaBinary();
300
+ execSync(`"${bin}" rm ${modelId}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 30000 });
300
301
  return { ok: true, message: `Deleted ${modelId}` };
301
302
  }
302
303
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.4.14",
3
+ "version": "0.4.16",
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
@@ -191,6 +191,8 @@ function App() {
191
191
  const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
192
192
  const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
193
193
  const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
194
+ const [modelPicker, setModelPicker] = useState<string[] | null>(null);
195
+ const [modelPickerIndex, setModelPickerIndex] = useState(0);
194
196
 
195
197
  // ── Setup Wizard State ──
196
198
  type WizardScreen = "connection" | "models" | "install-ollama" | "pulling" | null;
@@ -215,6 +217,37 @@ function App() {
215
217
  return () => { pasteEvents.off("paste", handler); };
216
218
  }, []);
217
219
 
220
+ // Refresh the connection banner to reflect current provider status
221
+ const refreshConnectionBanner = useCallback(async () => {
222
+ const info: string[] = [];
223
+ const cliArgs = parseCLIArgs();
224
+ const rawConfig = loadConfig();
225
+ const config = applyOverrides(rawConfig, cliArgs);
226
+ const provider = config.provider;
227
+
228
+ if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
229
+ const detected = await detectLocalProvider();
230
+ if (detected) {
231
+ info.push(`✔ Connected to ${detected.baseUrl} → ${detected.model}`);
232
+ } else {
233
+ const ollamaUp = await isOllamaRunning();
234
+ info.push(ollamaUp ? "Ollama running (no model loaded)" : "✗ No local LLM server found");
235
+ }
236
+ } else {
237
+ info.push(`Provider: ${provider.baseUrl}`);
238
+ info.push(`Model: ${provider.model}`);
239
+ }
240
+
241
+ const cwd = process.cwd();
242
+ if (isGitRepo(cwd)) {
243
+ const branch = getBranch(cwd);
244
+ const status = getStatus(cwd);
245
+ info.push(`Git: ${branch} (${status})`);
246
+ }
247
+
248
+ setConnectionInfo(info);
249
+ }, []);
250
+
218
251
  // Connect/reconnect to LLM provider
219
252
  const connectToProvider = useCallback(async (isRetry = false) => {
220
253
  const cliArgs = parseCLIArgs();
@@ -708,6 +741,7 @@ function App() {
708
741
  await new Promise(r => setTimeout(r, 1000));
709
742
  if (await isOllamaRunning()) {
710
743
  addMsg("info", "Ollama is running.");
744
+ await refreshConnectionBanner();
711
745
  return;
712
746
  }
713
747
  }
@@ -723,6 +757,7 @@ function App() {
723
757
  addMsg("info", "Stopping Ollama...");
724
758
  const result = await stopOllama();
725
759
  addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
760
+ if (result.ok) await refreshConnectionBanner();
726
761
  return;
727
762
  }
728
763
  if (trimmed === "/ollama pull") {
@@ -906,10 +941,32 @@ function App() {
906
941
  }
907
942
  return;
908
943
  }
909
- if (trimmed.startsWith("/model")) {
910
- const newModel = trimmed.replace("/model", "").trim();
944
+ if (trimmed === "/model" || trimmed === "/models") {
945
+ // Show picker of available models
946
+ try {
947
+ const ollamaModels = await listInstalledModelsDetailed();
948
+ if (ollamaModels.length > 0) {
949
+ setModelPicker(ollamaModels.map(m => m.name));
950
+ setModelPickerIndex(0);
951
+ return;
952
+ }
953
+ } catch {}
954
+ // Fallback: try provider's model list
955
+ try {
956
+ const providerModels = await listModels(providerRef.current?.baseUrl || "", providerRef.current?.apiKey || "");
957
+ if (providerModels.length > 0) {
958
+ setModelPicker(providerModels.map((m: any) => m.id || m));
959
+ setModelPickerIndex(0);
960
+ return;
961
+ }
962
+ } catch {}
963
+ addMsg("info", `Current model: ${modelName}\n Usage: /model <model-name>`);
964
+ return;
965
+ }
966
+ if (trimmed.startsWith("/model ")) {
967
+ const newModel = trimmed.replace("/model ", "").trim();
911
968
  if (!newModel) {
912
- addMsg("info", `Current model: ${agent.getModel()}\n Usage: /model <model-name>`);
969
+ addMsg("info", `Current model: ${modelName}\n Usage: /model <model-name>`);
913
970
  return;
914
971
  }
915
972
  agent.switchModel(newModel);
@@ -1071,7 +1128,7 @@ function App() {
1071
1128
 
1072
1129
  setLoading(false);
1073
1130
  setStreaming(false);
1074
- }, [agent, exit]);
1131
+ }, [agent, exit, refreshConnectionBanner]);
1075
1132
 
1076
1133
  useInput((inputChar, key) => {
1077
1134
  // Handle slash command navigation
@@ -1357,6 +1414,34 @@ function App() {
1357
1414
  return;
1358
1415
  }
1359
1416
 
1417
+ // ── Model picker ──
1418
+ if (modelPicker) {
1419
+ if (key.upArrow) {
1420
+ setModelPickerIndex((prev) => (prev - 1 + modelPicker.length) % modelPicker.length);
1421
+ return;
1422
+ }
1423
+ if (key.downArrow) {
1424
+ setModelPickerIndex((prev) => (prev + 1) % modelPicker.length);
1425
+ return;
1426
+ }
1427
+ if (key.escape) {
1428
+ setModelPicker(null);
1429
+ return;
1430
+ }
1431
+ if (key.return) {
1432
+ const selected = modelPicker[modelPickerIndex];
1433
+ if (selected && agent) {
1434
+ agent.switchModel(selected);
1435
+ setModelName(selected);
1436
+ addMsg("info", `✅ Switched to: ${selected}`);
1437
+ refreshConnectionBanner();
1438
+ }
1439
+ setModelPicker(null);
1440
+ return;
1441
+ }
1442
+ return;
1443
+ }
1444
+
1360
1445
  // ── Ollama delete picker ──
1361
1446
  if (ollamaDeletePicker) {
1362
1447
  if (key.upArrow) {
@@ -1387,7 +1472,7 @@ function App() {
1387
1472
  const pullModels = [
1388
1473
  { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
1389
1474
  { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
1390
- { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
1475
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
1391
1476
  { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
1392
1477
  { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
1393
1478
  { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
@@ -2179,6 +2264,23 @@ function App() {
2179
2264
  </Box>
2180
2265
  )}
2181
2266
 
2267
+ {/* ═══ MODEL PICKER ═══ */}
2268
+ {modelPicker && (
2269
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
2270
+ <Text bold color={theme.colors.secondary}>Switch model:</Text>
2271
+ <Text>{""}</Text>
2272
+ {modelPicker.map((m, i) => (
2273
+ <Text key={m}>
2274
+ {" "}{i === modelPickerIndex ? <Text color={theme.colors.primary} bold>{"▸ "}</Text> : " "}
2275
+ <Text color={i === modelPickerIndex ? theme.colors.primary : undefined}>{m}</Text>
2276
+ {m === modelName ? <Text color={theme.colors.success}>{" (active)"}</Text> : null}
2277
+ </Text>
2278
+ ))}
2279
+ <Text>{""}</Text>
2280
+ <Text dimColor>{" ↑↓ navigate · Enter to switch · Esc cancel"}</Text>
2281
+ </Box>
2282
+ )}
2283
+
2182
2284
  {/* ═══ OLLAMA DELETE PICKER ═══ */}
2183
2285
  {ollamaDeletePicker && (
2184
2286
  <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
@@ -2204,7 +2306,7 @@ function App() {
2204
2306
  {[
2205
2307
  { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
2206
2308
  { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
2207
- { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "Fast but limited quality" },
2309
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
2208
2310
  { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
2209
2311
  { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
2210
2312
  { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
@@ -9,7 +9,7 @@ export interface RecommendedModel {
9
9
  vramOptimal: number; // Optimal VRAM in GB (0 = CPU fine)
10
10
  description: string; // One-liner
11
11
  speed: string; // e.g., "~45 tok/s on M1"
12
- quality: "good" | "great" | "best";
12
+ quality: "limited" | "good" | "great" | "best";
13
13
  }
14
14
 
15
15
  export type ModelFit = "perfect" | "good" | "tight" | "skip";
@@ -25,9 +25,9 @@ const MODELS: RecommendedModel[] = [
25
25
  size: 2,
26
26
  ramRequired: 8,
27
27
  vramOptimal: 4,
28
- description: "Lightweight, fast coding model",
28
+ description: "\u26A0\uFE0F May not support tool calling well",
29
29
  speed: "~60 tok/s on M1",
30
- quality: "good",
30
+ quality: "limited",
31
31
  },
32
32
  {
33
33
  name: "Qwen 2.5 Coder 7B",
@@ -103,7 +103,7 @@ function scoreModel(model: RecommendedModel, ramGB: number, vramGB: number): Mod
103
103
  return "skip";
104
104
  }
105
105
 
106
- const qualityOrder: Record<string, number> = { best: 3, great: 2, good: 1 };
106
+ const qualityOrder: Record<string, number> = { best: 3, great: 2, good: 1, limited: 0 };
107
107
  const fitOrder: Record<string, number> = { perfect: 4, good: 3, tight: 2, skip: 1 };
108
108
 
109
109
  export function getRecommendations(hardware: HardwareInfo): ScoredModel[] {
@@ -168,10 +168,11 @@ function mapLlmfitFit(fit: string): ModelFit {
168
168
  }
169
169
  }
170
170
 
171
- function mapLlmfitQuality(params_b: number): "good" | "great" | "best" {
171
+ function mapLlmfitQuality(params_b: number): "limited" | "good" | "great" | "best" {
172
172
  if (params_b >= 14) return "best";
173
173
  if (params_b >= 7) return "great";
174
- return "good";
174
+ if (params_b >= 5) return "good";
175
+ return "limited";
175
176
  }
176
177
 
177
178
  /** Get recommendations using llmfit if available, otherwise fall back to hardcoded list */
@@ -319,7 +319,8 @@ export async function stopOllama(): Promise<{ ok: boolean; message: string }> {
319
319
  /** Delete a model from disk */
320
320
  export function deleteModel(modelId: string): { ok: boolean; message: string } {
321
321
  try {
322
- execSync(`ollama rm ${modelId}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 30000 });
322
+ const bin = findOllamaBinary();
323
+ execSync(`"${bin}" rm ${modelId}`, { stdio: ["pipe", "pipe", "pipe"], timeout: 30000 });
323
324
  return { ok: true, message: `Deleted ${modelId}` };
324
325
  } catch (err: any) {
325
326
  return { ok: false, message: `Failed to delete ${modelId}: ${err.stderr?.toString().trim() || err.message}` };