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 +5 -0
- package/dist/index.js +92 -7
- package/dist/utils/models.d.ts +1 -1
- package/dist/utils/models.js +6 -4
- package/dist/utils/ollama.js +2 -1
- package/package.json +1 -1
- package/src/index.tsx +108 -6
- package/src/utils/models.ts +7 -6
- package/src/utils/ollama.ts +2 -1
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
|
|
873
|
-
|
|
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: ${
|
|
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: "
|
|
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: "
|
|
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" },
|
package/dist/utils/models.d.ts
CHANGED
|
@@ -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 {
|
package/dist/utils/models.js
CHANGED
|
@@ -6,9 +6,9 @@ const MODELS = [
|
|
|
6
6
|
size: 2,
|
|
7
7
|
ramRequired: 8,
|
|
8
8
|
vramOptimal: 4,
|
|
9
|
-
description: "
|
|
9
|
+
description: "\u26A0\uFE0F May not support tool calling well",
|
|
10
10
|
speed: "~60 tok/s on M1",
|
|
11
|
-
quality: "
|
|
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
|
-
|
|
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) {
|
package/dist/utils/ollama.js
CHANGED
|
@@ -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
|
-
|
|
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
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
|
|
910
|
-
|
|
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: ${
|
|
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: "
|
|
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: "
|
|
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" },
|
package/src/utils/models.ts
CHANGED
|
@@ -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: "
|
|
28
|
+
description: "\u26A0\uFE0F May not support tool calling well",
|
|
29
29
|
speed: "~60 tok/s on M1",
|
|
30
|
-
quality: "
|
|
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 */
|
package/src/utils/ollama.ts
CHANGED
|
@@ -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
|
-
|
|
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}` };
|