codemaxxing 0.3.1 → 0.4.1
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 +16 -16
- package/dist/config.d.ts +3 -0
- package/dist/config.js +13 -0
- package/dist/index.js +457 -8
- package/dist/utils/hardware.d.ts +17 -0
- package/dist/utils/hardware.js +120 -0
- package/dist/utils/models.d.ts +24 -0
- package/dist/utils/models.js +177 -0
- package/dist/utils/ollama.d.ts +42 -0
- package/dist/utils/ollama.js +213 -0
- package/package.json +1 -1
- package/src/config.ts +15 -0
- package/src/index.tsx +589 -6
- package/src/utils/hardware.ts +131 -0
- package/src/utils/models.ts +217 -0
- package/src/utils/ollama.ts +232 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useCallback } from "react";
|
|
4
4
|
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
5
5
|
import { EventEmitter } from "events";
|
|
6
6
|
import TextInput from "ink-text-input";
|
|
7
7
|
import { CodingAgent } from "./agent.js";
|
|
8
|
-
import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
|
|
8
|
+
import { loadConfig, saveConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
|
|
9
9
|
import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
|
|
10
10
|
import { execSync } from "child_process";
|
|
11
11
|
import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
|
|
@@ -13,6 +13,9 @@ import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
|
|
|
13
13
|
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
|
|
14
14
|
import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
|
|
15
15
|
import { listServers, addServer, removeServer, getConnectedServers } from "./utils/mcp.js";
|
|
16
|
+
import { detectHardware, formatBytes } from "./utils/hardware.js";
|
|
17
|
+
import { getRecommendationsWithLlmfit, getFitIcon, isLlmfitAvailable } from "./utils/models.js";
|
|
18
|
+
import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel, getGPUMemoryUsage } from "./utils/ollama.js";
|
|
16
19
|
const VERSION = "0.1.9";
|
|
17
20
|
// ── Helpers ──
|
|
18
21
|
function formatTimeAgo(date) {
|
|
@@ -64,6 +67,12 @@ const SLASH_COMMANDS = [
|
|
|
64
67
|
{ cmd: "/mcp add", desc: "add MCP server" },
|
|
65
68
|
{ cmd: "/mcp remove", desc: "remove MCP server" },
|
|
66
69
|
{ cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
|
|
70
|
+
{ cmd: "/ollama", desc: "Ollama status & models" },
|
|
71
|
+
{ cmd: "/ollama list", desc: "list installed models" },
|
|
72
|
+
{ cmd: "/ollama start", desc: "start Ollama server" },
|
|
73
|
+
{ cmd: "/ollama stop", desc: "stop Ollama server" },
|
|
74
|
+
{ cmd: "/ollama pull", desc: "download a model" },
|
|
75
|
+
{ cmd: "/ollama delete", desc: "delete a model" },
|
|
67
76
|
{ cmd: "/quit", desc: "exit" },
|
|
68
77
|
];
|
|
69
78
|
const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
@@ -137,6 +146,17 @@ function App() {
|
|
|
137
146
|
const [skillsPickerIndex, setSkillsPickerIndex] = useState(0);
|
|
138
147
|
const [sessionDisabledSkills, setSessionDisabledSkills] = useState(new Set());
|
|
139
148
|
const [approval, setApproval] = useState(null);
|
|
149
|
+
// ── Ollama Management State ──
|
|
150
|
+
const [ollamaDeleteConfirm, setOllamaDeleteConfirm] = useState(null);
|
|
151
|
+
const [ollamaPulling, setOllamaPulling] = useState(null);
|
|
152
|
+
const [ollamaExitPrompt, setOllamaExitPrompt] = useState(false);
|
|
153
|
+
const [wizardScreen, setWizardScreen] = useState(null);
|
|
154
|
+
const [wizardIndex, setWizardIndex] = useState(0);
|
|
155
|
+
const [wizardHardware, setWizardHardware] = useState(null);
|
|
156
|
+
const [wizardModels, setWizardModels] = useState([]);
|
|
157
|
+
const [wizardPullProgress, setWizardPullProgress] = useState(null);
|
|
158
|
+
const [wizardPullError, setWizardPullError] = useState(null);
|
|
159
|
+
const [wizardSelectedModel, setWizardSelectedModel] = useState(null);
|
|
140
160
|
// Listen for paste events from stdin interceptor
|
|
141
161
|
useEffect(() => {
|
|
142
162
|
const handler = ({ content, lines }) => {
|
|
@@ -174,10 +194,11 @@ function App() {
|
|
|
174
194
|
}
|
|
175
195
|
else {
|
|
176
196
|
info.push("✗ No local LLM server found.");
|
|
177
|
-
info.push(" /connect — retry after starting LM Studio or Ollama");
|
|
178
|
-
info.push(" /login — authenticate with a cloud provider");
|
|
179
197
|
setConnectionInfo([...info]);
|
|
180
198
|
setReady(true);
|
|
199
|
+
// Show the setup wizard on first run
|
|
200
|
+
setWizardScreen("connection");
|
|
201
|
+
setWizardIndex(0);
|
|
181
202
|
return;
|
|
182
203
|
}
|
|
183
204
|
}
|
|
@@ -318,7 +339,8 @@ function App() {
|
|
|
318
339
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
319
340
|
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
|
|
320
341
|
selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
|
|
321
|
-
selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect"
|
|
342
|
+
selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect" ||
|
|
343
|
+
selected.cmd === "/ollama pull" || selected.cmd === "/ollama delete") {
|
|
322
344
|
setInput(selected.cmd + " ");
|
|
323
345
|
setCmdIndex(0);
|
|
324
346
|
setInputKey((k) => k + 1);
|
|
@@ -344,7 +366,22 @@ function App() {
|
|
|
344
366
|
return;
|
|
345
367
|
addMsg("user", trimmed);
|
|
346
368
|
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
347
|
-
|
|
369
|
+
// Check if Ollama is running and offer to stop it
|
|
370
|
+
const ollamaUp = await isOllamaRunning();
|
|
371
|
+
if (ollamaUp) {
|
|
372
|
+
const config = loadConfig();
|
|
373
|
+
if (config.defaults.stopOllamaOnExit) {
|
|
374
|
+
addMsg("info", "Stopping Ollama...");
|
|
375
|
+
await stopOllama();
|
|
376
|
+
exit();
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
setOllamaExitPrompt(true);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
exit();
|
|
384
|
+
}
|
|
348
385
|
return;
|
|
349
386
|
}
|
|
350
387
|
if (trimmed === "/login" || trimmed === "/auth") {
|
|
@@ -387,6 +424,12 @@ function App() {
|
|
|
387
424
|
" /mcp add — add MCP server to global config",
|
|
388
425
|
" /mcp remove — remove MCP server",
|
|
389
426
|
" /mcp reconnect — reconnect all MCP servers",
|
|
427
|
+
" /ollama — Ollama status, models & GPU usage",
|
|
428
|
+
" /ollama list — list installed models with sizes",
|
|
429
|
+
" /ollama start — start Ollama server",
|
|
430
|
+
" /ollama stop — stop Ollama server (frees GPU RAM)",
|
|
431
|
+
" /ollama pull <model> — download a model",
|
|
432
|
+
" /ollama delete <model> — delete a model from disk",
|
|
390
433
|
" /quit — exit",
|
|
391
434
|
].join("\n"));
|
|
392
435
|
return;
|
|
@@ -554,6 +597,142 @@ function App() {
|
|
|
554
597
|
addMsg("info", "🔍 Auto-lint OFF");
|
|
555
598
|
return;
|
|
556
599
|
}
|
|
600
|
+
// ── Ollama commands (work without agent) ──
|
|
601
|
+
if (trimmed === "/ollama" || trimmed === "/ollama status") {
|
|
602
|
+
const running = await isOllamaRunning();
|
|
603
|
+
const lines = [`Ollama: ${running ? "running" : "stopped"}`];
|
|
604
|
+
if (running) {
|
|
605
|
+
const models = await listInstalledModelsDetailed();
|
|
606
|
+
if (models.length > 0) {
|
|
607
|
+
lines.push(`Installed models (${models.length}):`);
|
|
608
|
+
for (const m of models) {
|
|
609
|
+
const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
|
|
610
|
+
lines.push(` ${m.name} (${sizeGB} GB)`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
lines.push("No models installed.");
|
|
615
|
+
}
|
|
616
|
+
const gpuMem = getGPUMemoryUsage();
|
|
617
|
+
if (gpuMem)
|
|
618
|
+
lines.push(`GPU: ${gpuMem}`);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
lines.push("Start with: /ollama start");
|
|
622
|
+
}
|
|
623
|
+
addMsg("info", lines.join("\n"));
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (trimmed === "/ollama list") {
|
|
627
|
+
const running = await isOllamaRunning();
|
|
628
|
+
if (!running) {
|
|
629
|
+
addMsg("info", "Ollama is not running. Start with /ollama start");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const models = await listInstalledModelsDetailed();
|
|
633
|
+
if (models.length === 0) {
|
|
634
|
+
addMsg("info", "No models installed. Pull one with /ollama pull <model>");
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
const lines = models.map((m) => {
|
|
638
|
+
const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
|
|
639
|
+
return ` ${m.name} (${sizeGB} GB)`;
|
|
640
|
+
});
|
|
641
|
+
addMsg("info", `Installed models:\n${lines.join("\n")}`);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (trimmed === "/ollama start") {
|
|
646
|
+
const running = await isOllamaRunning();
|
|
647
|
+
if (running) {
|
|
648
|
+
addMsg("info", "Ollama is already running.");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (!isOllamaInstalled()) {
|
|
652
|
+
addMsg("error", `Ollama is not installed. Install with: ${getOllamaInstallCommand(process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")}`);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
startOllama();
|
|
656
|
+
addMsg("info", "Starting Ollama server...");
|
|
657
|
+
// Wait for it to come up
|
|
658
|
+
for (let i = 0; i < 10; i++) {
|
|
659
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
660
|
+
if (await isOllamaRunning()) {
|
|
661
|
+
addMsg("info", "Ollama is running.");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
addMsg("error", "Ollama did not start in time. Try running 'ollama serve' manually.");
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (trimmed === "/ollama stop") {
|
|
669
|
+
const running = await isOllamaRunning();
|
|
670
|
+
if (!running) {
|
|
671
|
+
addMsg("info", "Ollama is not running.");
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
addMsg("info", "Stopping Ollama...");
|
|
675
|
+
const result = await stopOllama();
|
|
676
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (trimmed.startsWith("/ollama pull ")) {
|
|
680
|
+
const modelId = trimmed.replace("/ollama pull ", "").trim();
|
|
681
|
+
if (!modelId) {
|
|
682
|
+
addMsg("info", "Usage: /ollama pull <model>\n Example: /ollama pull qwen2.5-coder:7b");
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!isOllamaInstalled()) {
|
|
686
|
+
addMsg("error", "Ollama is not installed.");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
// Ensure ollama is running
|
|
690
|
+
let running = await isOllamaRunning();
|
|
691
|
+
if (!running) {
|
|
692
|
+
startOllama();
|
|
693
|
+
addMsg("info", "Starting Ollama server...");
|
|
694
|
+
for (let i = 0; i < 10; i++) {
|
|
695
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
696
|
+
if (await isOllamaRunning()) {
|
|
697
|
+
running = true;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!running) {
|
|
702
|
+
addMsg("error", "Could not start Ollama. Run 'ollama serve' manually.");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
setOllamaPulling({ model: modelId, progress: { status: "starting", percent: 0 } });
|
|
707
|
+
try {
|
|
708
|
+
await pullModel(modelId, (p) => {
|
|
709
|
+
setOllamaPulling({ model: modelId, progress: p });
|
|
710
|
+
});
|
|
711
|
+
setOllamaPulling(null);
|
|
712
|
+
addMsg("info", `\u2705 Downloaded ${modelId}`);
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
setOllamaPulling(null);
|
|
716
|
+
addMsg("error", `Failed to pull ${modelId}: ${err.message}`);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (trimmed.startsWith("/ollama delete ")) {
|
|
721
|
+
const modelId = trimmed.replace("/ollama delete ", "").trim();
|
|
722
|
+
if (!modelId) {
|
|
723
|
+
addMsg("info", "Usage: /ollama delete <model>");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
// Look up size for confirmation
|
|
727
|
+
const models = await listInstalledModelsDetailed();
|
|
728
|
+
const found = models.find((m) => m.name === modelId || m.name.startsWith(modelId));
|
|
729
|
+
if (!found) {
|
|
730
|
+
addMsg("error", `Model "${modelId}" not found. Use /ollama list to see installed models.`);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
setOllamaDeleteConfirm({ model: found.name, size: found.size });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
557
736
|
// ── MCP commands (partially work without agent) ──
|
|
558
737
|
if (trimmed === "/mcp" || trimmed === "/mcp list") {
|
|
559
738
|
const servers = listServers(process.cwd());
|
|
@@ -1114,6 +1293,264 @@ function App() {
|
|
|
1114
1293
|
}
|
|
1115
1294
|
return;
|
|
1116
1295
|
}
|
|
1296
|
+
// ── Ollama delete confirmation ──
|
|
1297
|
+
if (ollamaDeleteConfirm) {
|
|
1298
|
+
if (inputChar === "y" || inputChar === "Y") {
|
|
1299
|
+
const model = ollamaDeleteConfirm.model;
|
|
1300
|
+
setOllamaDeleteConfirm(null);
|
|
1301
|
+
const result = deleteModel(model);
|
|
1302
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
1306
|
+
setOllamaDeleteConfirm(null);
|
|
1307
|
+
addMsg("info", "Delete cancelled.");
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
// ── Ollama exit prompt ──
|
|
1313
|
+
if (ollamaExitPrompt) {
|
|
1314
|
+
if (inputChar === "y" || inputChar === "Y") {
|
|
1315
|
+
setOllamaExitPrompt(false);
|
|
1316
|
+
stopOllama().then(() => exit());
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (inputChar === "n" || inputChar === "N") {
|
|
1320
|
+
setOllamaExitPrompt(false);
|
|
1321
|
+
exit();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (inputChar === "a" || inputChar === "A") {
|
|
1325
|
+
setOllamaExitPrompt(false);
|
|
1326
|
+
saveConfig({ defaults: { ...loadConfig().defaults, stopOllamaOnExit: true } });
|
|
1327
|
+
addMsg("info", "Saved preference: always stop Ollama on exit.");
|
|
1328
|
+
stopOllama().then(() => exit());
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (key.escape) {
|
|
1332
|
+
setOllamaExitPrompt(false);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
// ── Setup Wizard Navigation ──
|
|
1338
|
+
if (wizardScreen) {
|
|
1339
|
+
if (wizardScreen === "connection") {
|
|
1340
|
+
const items = ["local", "openrouter", "apikey", "existing"];
|
|
1341
|
+
if (key.upArrow) {
|
|
1342
|
+
setWizardIndex((prev) => (prev - 1 + items.length) % items.length);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (key.downArrow) {
|
|
1346
|
+
setWizardIndex((prev) => (prev + 1) % items.length);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (key.escape) {
|
|
1350
|
+
setWizardScreen(null);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
if (key.return) {
|
|
1354
|
+
const selected = items[wizardIndex];
|
|
1355
|
+
if (selected === "local") {
|
|
1356
|
+
// Scan hardware and show model picker (use llmfit if available)
|
|
1357
|
+
const hw = detectHardware();
|
|
1358
|
+
setWizardHardware(hw);
|
|
1359
|
+
const { models: recs } = getRecommendationsWithLlmfit(hw);
|
|
1360
|
+
setWizardModels(recs.filter(m => m.fit !== "skip"));
|
|
1361
|
+
setWizardScreen("models");
|
|
1362
|
+
setWizardIndex(0);
|
|
1363
|
+
}
|
|
1364
|
+
else if (selected === "openrouter") {
|
|
1365
|
+
setWizardScreen(null);
|
|
1366
|
+
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
1367
|
+
setLoading(true);
|
|
1368
|
+
setSpinnerMsg("Waiting for authorization...");
|
|
1369
|
+
openRouterOAuth((msg) => addMsg("info", msg))
|
|
1370
|
+
.then(() => {
|
|
1371
|
+
addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
|
|
1372
|
+
setLoading(false);
|
|
1373
|
+
})
|
|
1374
|
+
.catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
1375
|
+
}
|
|
1376
|
+
else if (selected === "apikey") {
|
|
1377
|
+
setWizardScreen(null);
|
|
1378
|
+
setLoginPicker(true);
|
|
1379
|
+
setLoginPickerIndex(0);
|
|
1380
|
+
}
|
|
1381
|
+
else if (selected === "existing") {
|
|
1382
|
+
setWizardScreen(null);
|
|
1383
|
+
addMsg("info", "Start your LLM server, then type /connect to retry.");
|
|
1384
|
+
}
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (wizardScreen === "models") {
|
|
1390
|
+
const models = wizardModels;
|
|
1391
|
+
if (key.upArrow) {
|
|
1392
|
+
setWizardIndex((prev) => (prev - 1 + models.length) % models.length);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (key.downArrow) {
|
|
1396
|
+
setWizardIndex((prev) => (prev + 1) % models.length);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (key.escape) {
|
|
1400
|
+
setWizardScreen("connection");
|
|
1401
|
+
setWizardIndex(0);
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (key.return) {
|
|
1405
|
+
const selected = models[wizardIndex];
|
|
1406
|
+
if (selected) {
|
|
1407
|
+
setWizardSelectedModel(selected);
|
|
1408
|
+
// Check if Ollama is installed
|
|
1409
|
+
if (!isOllamaInstalled()) {
|
|
1410
|
+
setWizardScreen("install-ollama");
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
// Start pulling the model
|
|
1414
|
+
setWizardScreen("pulling");
|
|
1415
|
+
setWizardPullProgress({ status: "starting", percent: 0 });
|
|
1416
|
+
setWizardPullError(null);
|
|
1417
|
+
(async () => {
|
|
1418
|
+
try {
|
|
1419
|
+
// Ensure ollama is running
|
|
1420
|
+
const running = await isOllamaRunning();
|
|
1421
|
+
if (!running) {
|
|
1422
|
+
setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
|
|
1423
|
+
startOllama();
|
|
1424
|
+
// Wait for it to come up
|
|
1425
|
+
for (let i = 0; i < 15; i++) {
|
|
1426
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1427
|
+
if (await isOllamaRunning())
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
if (!(await isOllamaRunning())) {
|
|
1431
|
+
setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
await pullModel(selected.ollamaId, (p) => {
|
|
1436
|
+
setWizardPullProgress(p);
|
|
1437
|
+
});
|
|
1438
|
+
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1439
|
+
// Wait briefly then connect
|
|
1440
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1441
|
+
setWizardScreen(null);
|
|
1442
|
+
setWizardPullProgress(null);
|
|
1443
|
+
setWizardSelectedModel(null);
|
|
1444
|
+
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1445
|
+
await connectToProvider(true);
|
|
1446
|
+
}
|
|
1447
|
+
catch (err) {
|
|
1448
|
+
setWizardPullError(err.message);
|
|
1449
|
+
}
|
|
1450
|
+
})();
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (wizardScreen === "install-ollama") {
|
|
1458
|
+
if (key.escape) {
|
|
1459
|
+
setWizardScreen("models");
|
|
1460
|
+
setWizardIndex(0);
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
if (key.return) {
|
|
1464
|
+
// User says they installed it — check and proceed
|
|
1465
|
+
if (isOllamaInstalled()) {
|
|
1466
|
+
const selected = wizardSelectedModel;
|
|
1467
|
+
if (selected) {
|
|
1468
|
+
setWizardScreen("pulling");
|
|
1469
|
+
setWizardPullProgress({ status: "starting", percent: 0 });
|
|
1470
|
+
setWizardPullError(null);
|
|
1471
|
+
(async () => {
|
|
1472
|
+
try {
|
|
1473
|
+
const running = await isOllamaRunning();
|
|
1474
|
+
if (!running) {
|
|
1475
|
+
setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
|
|
1476
|
+
startOllama();
|
|
1477
|
+
for (let i = 0; i < 15; i++) {
|
|
1478
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1479
|
+
if (await isOllamaRunning())
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
if (!(await isOllamaRunning())) {
|
|
1483
|
+
setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
|
|
1488
|
+
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1489
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1490
|
+
setWizardScreen(null);
|
|
1491
|
+
setWizardPullProgress(null);
|
|
1492
|
+
setWizardSelectedModel(null);
|
|
1493
|
+
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1494
|
+
await connectToProvider(true);
|
|
1495
|
+
}
|
|
1496
|
+
catch (err) {
|
|
1497
|
+
setWizardPullError(err.message);
|
|
1498
|
+
}
|
|
1499
|
+
})();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
addMsg("info", "Ollama not found yet. Install it and press Enter again.");
|
|
1504
|
+
}
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
if (wizardScreen === "pulling") {
|
|
1510
|
+
// Allow retry on error
|
|
1511
|
+
if (wizardPullError && key.return) {
|
|
1512
|
+
const selected = wizardSelectedModel;
|
|
1513
|
+
if (selected) {
|
|
1514
|
+
setWizardPullError(null);
|
|
1515
|
+
setWizardPullProgress({ status: "retrying", percent: 0 });
|
|
1516
|
+
(async () => {
|
|
1517
|
+
try {
|
|
1518
|
+
const running = await isOllamaRunning();
|
|
1519
|
+
if (!running) {
|
|
1520
|
+
startOllama();
|
|
1521
|
+
for (let i = 0; i < 15; i++) {
|
|
1522
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1523
|
+
if (await isOllamaRunning())
|
|
1524
|
+
break;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
|
|
1528
|
+
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1529
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1530
|
+
setWizardScreen(null);
|
|
1531
|
+
setWizardPullProgress(null);
|
|
1532
|
+
setWizardSelectedModel(null);
|
|
1533
|
+
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1534
|
+
await connectToProvider(true);
|
|
1535
|
+
}
|
|
1536
|
+
catch (err) {
|
|
1537
|
+
setWizardPullError(err.message);
|
|
1538
|
+
}
|
|
1539
|
+
})();
|
|
1540
|
+
}
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (wizardPullError && key.escape) {
|
|
1544
|
+
setWizardScreen("models");
|
|
1545
|
+
setWizardIndex(0);
|
|
1546
|
+
setWizardPullError(null);
|
|
1547
|
+
setWizardPullProgress(null);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
return; // Ignore keys while pulling
|
|
1551
|
+
}
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1117
1554
|
// Theme picker navigation
|
|
1118
1555
|
if (themePicker) {
|
|
1119
1556
|
const themeKeys = listThemes();
|
|
@@ -1266,7 +1703,14 @@ function App() {
|
|
|
1266
1703
|
}
|
|
1267
1704
|
if (key.ctrl && inputChar === "c") {
|
|
1268
1705
|
if (ctrlCPressed) {
|
|
1269
|
-
|
|
1706
|
+
// Force quit on second Ctrl+C — don't block
|
|
1707
|
+
const config = loadConfig();
|
|
1708
|
+
if (config.defaults.stopOllamaOnExit) {
|
|
1709
|
+
stopOllama().finally(() => exit());
|
|
1710
|
+
}
|
|
1711
|
+
else {
|
|
1712
|
+
exit();
|
|
1713
|
+
}
|
|
1270
1714
|
}
|
|
1271
1715
|
else {
|
|
1272
1716
|
setCtrlCPressed(true);
|
|
@@ -1344,7 +1788,12 @@ function App() {
|
|
|
1344
1788
|
})(), skillsPicker === "remove" && (() => {
|
|
1345
1789
|
const installed = listInstalledSkills();
|
|
1346
1790
|
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" })] }));
|
|
1347
|
-
})(), 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" })] })] })),
|
|
1791
|
+
})(), 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: "" }), [
|
|
1792
|
+
{ key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
|
|
1793
|
+
{ key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
|
|
1794
|
+
{ key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
|
|
1795
|
+
{ key: "existing", icon: "\u2699\uFE0F", label: "I already have a server running", desc: "" },
|
|
1796
|
+
].map((item, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === wizardIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: [item.icon, " ", item.label] }), item.desc ? _jsxs(Text, { color: theme.colors.muted, children: [" (", item.desc, ")"] }) : null] }, item.key))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to select" })] })), wizardScreen === "models" && wizardHardware && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Your hardware:" }), _jsxs(Text, { color: theme.colors.muted, children: [" CPU: ", wizardHardware.cpu.name, " (", wizardHardware.cpu.cores, " cores)"] }), _jsxs(Text, { color: theme.colors.muted, children: [" RAM: ", formatBytes(wizardHardware.ram)] }), wizardHardware.gpu ? (_jsxs(Text, { color: theme.colors.muted, children: [" GPU: ", wizardHardware.gpu.name, wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""] })) : (_jsx(Text, { color: theme.colors.muted, children: " GPU: none detected" })), !isLlmfitAvailable() && (_jsx(Text, { dimColor: true, children: " Tip: Install llmfit for smarter recommendations: brew install llmfit" })), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Recommended models:" }), _jsx(Text, { children: "" }), wizardModels.map((m, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { children: [getFitIcon(m.fit), " "] }), _jsx(Text, { color: i === wizardIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" ~", m.size, " GB \u00B7 ", m.quality === "best" ? "Best" : m.quality === "great" ? "Great" : "Good", " quality \u00B7 ", m.speed] })] }, m.ollamaId))), wizardModels.length === 0 && (_jsx(Text, { color: theme.colors.error, children: " No suitable models found for your hardware." })), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to install \u00B7 Esc back" })] })), wizardScreen === "install-ollama" && (_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 required for local models." }), _jsx(Text, { children: "" }), _jsxs(Text, { color: theme.colors.primary, children: [" Install with: ", _jsx(Text, { bold: true, children: getOllamaInstallCommand(wizardHardware?.os ?? "linux") })] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Run the command above, then press Enter to continue..." }), _jsx(Text, { dimColor: true, children: " Esc to go back" })] })), wizardScreen === "pulling" && wizardSelectedModel && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: wizardPullError ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.colors.error, bold: true, children: [" \u274C Error: ", wizardPullError] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Press Enter to retry \u00B7 Esc to go back" })] })) : wizardPullProgress ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" Downloading ", wizardSelectedModel.name, "..."] }), wizardPullProgress.status === "downloading" || wizardPullProgress.percent > 0 ? (_jsx(_Fragment, { children: _jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(wizardPullProgress.percent / 5)), "\u2591".repeat(20 - Math.floor(wizardPullProgress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [wizardPullProgress.percent, "%"] }), wizardPullProgress.completed != null && wizardPullProgress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(wizardPullProgress.completed), " / ", formatBytes(wizardPullProgress.total)] })) : null] }) })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", wizardPullProgress.status, "..."] }))] })) : null })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(v); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
|
|
1348
1797
|
const tokens = agent.estimateTokens();
|
|
1349
1798
|
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
1350
1799
|
})(), " tokens", (() => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface HardwareInfo {
|
|
2
|
+
cpu: {
|
|
3
|
+
name: string;
|
|
4
|
+
cores: number;
|
|
5
|
+
speed: number;
|
|
6
|
+
};
|
|
7
|
+
ram: number;
|
|
8
|
+
gpu: {
|
|
9
|
+
name: string;
|
|
10
|
+
vram: number;
|
|
11
|
+
} | null;
|
|
12
|
+
os: "macos" | "linux" | "windows";
|
|
13
|
+
appleSilicon: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function detectHardware(): HardwareInfo;
|
|
16
|
+
/** Format bytes to human-readable string */
|
|
17
|
+
export declare function formatBytes(bytes: number): string;
|