codemaxxing 1.0.0 → 1.0.2
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 +18 -12
- package/dist/agent.d.ts +4 -0
- package/dist/agent.js +91 -16
- package/dist/commands/git.d.ts +2 -0
- package/dist/commands/git.js +50 -0
- package/dist/commands/ollama.d.ts +27 -0
- package/dist/commands/ollama.js +171 -0
- package/dist/commands/output.d.ts +2 -0
- package/dist/commands/output.js +18 -0
- package/dist/commands/registry.d.ts +2 -0
- package/dist/commands/registry.js +8 -0
- package/dist/commands/skills.d.ts +18 -0
- package/dist/commands/skills.js +121 -0
- package/dist/commands/types.d.ts +5 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/ui.d.ts +16 -0
- package/dist/commands/ui.js +79 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -3
- package/dist/exec.js +4 -1
- package/dist/index.js +75 -401
- package/dist/tools/files.js +58 -3
- package/dist/utils/context.js +6 -0
- package/dist/utils/mcp.d.ts +7 -2
- package/dist/utils/mcp.js +34 -6
- package/package.json +8 -5
- package/src/agent.ts +0 -894
- package/src/auth-cli.ts +0 -287
- package/src/cli.ts +0 -37
- package/src/config.ts +0 -352
- package/src/exec.ts +0 -183
- package/src/index.tsx +0 -2647
- package/src/skills/registry.ts +0 -1436
- package/src/themes.ts +0 -335
- package/src/tools/files.ts +0 -374
- package/src/utils/auth.ts +0 -606
- package/src/utils/context.ts +0 -174
- package/src/utils/git.ts +0 -117
- package/src/utils/hardware.ts +0 -131
- package/src/utils/lint.ts +0 -116
- package/src/utils/mcp.ts +0 -307
- package/src/utils/models.ts +0 -218
- package/src/utils/ollama.ts +0 -352
- package/src/utils/repomap.ts +0 -220
- package/src/utils/sessions.ts +0 -254
- package/src/utils/skills.ts +0 -241
- package/tsconfig.json +0 -16
package/src/index.tsx
DELETED
|
@@ -1,2647 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import React, { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
5
|
-
import { EventEmitter } from "events";
|
|
6
|
-
import TextInput from "ink-text-input";
|
|
7
|
-
import { CodingAgent } from "./agent.js";
|
|
8
|
-
import { loadConfig, saveConfig, detectLocalProvider, detectLocalProviderDetailed, parseCLIArgs, applyOverrides, listModels } from "./config.js";
|
|
9
|
-
import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
|
|
10
|
-
import { execSync } from "child_process";
|
|
11
|
-
import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
|
|
12
|
-
import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./themes.js";
|
|
13
|
-
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow, saveApiKey } from "./utils/auth.js";
|
|
14
|
-
import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
|
|
15
|
-
import { listServers, addServer, removeServer, getAllMCPTools, getConnectedServers } from "./utils/mcp.js";
|
|
16
|
-
import { detectHardware, formatBytes, type HardwareInfo } from "./utils/hardware.js";
|
|
17
|
-
import { getRecommendations, getRecommendationsWithLlmfit, getFitIcon, isLlmfitAvailable, type ScoredModel } from "./utils/models.js";
|
|
18
|
-
import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel, getGPUMemoryUsage, type PullProgress } from "./utils/ollama.js";
|
|
19
|
-
|
|
20
|
-
import { createRequire } from "module";
|
|
21
|
-
const _require = createRequire(import.meta.url);
|
|
22
|
-
const VERSION = _require("../package.json").version;
|
|
23
|
-
|
|
24
|
-
// ── Helpers ──
|
|
25
|
-
function formatTimeAgo(date: Date): string {
|
|
26
|
-
const secs = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
27
|
-
if (secs < 60) return `${secs}s ago`;
|
|
28
|
-
const mins = Math.floor(secs / 60);
|
|
29
|
-
if (mins < 60) return `${mins}m ago`;
|
|
30
|
-
const hours = Math.floor(mins / 60);
|
|
31
|
-
if (hours < 24) return `${hours}h ago`;
|
|
32
|
-
const days = Math.floor(hours / 24);
|
|
33
|
-
return `${days}d ago`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── Slash Commands ──
|
|
37
|
-
const SLASH_COMMANDS = [
|
|
38
|
-
{ cmd: "/help", desc: "show commands" },
|
|
39
|
-
{ cmd: "/connect", desc: "retry LLM connection" },
|
|
40
|
-
{ cmd: "/login", desc: "set up authentication" },
|
|
41
|
-
{ cmd: "/map", desc: "show repository map" },
|
|
42
|
-
{ cmd: "/reset", desc: "clear conversation" },
|
|
43
|
-
{ cmd: "/context", desc: "show message count" },
|
|
44
|
-
{ cmd: "/diff", desc: "show git changes" },
|
|
45
|
-
{ cmd: "/undo", desc: "revert last codemaxxing commit" },
|
|
46
|
-
{ cmd: "/commit", desc: "commit all changes" },
|
|
47
|
-
{ cmd: "/push", desc: "push to remote" },
|
|
48
|
-
{ cmd: "/git on", desc: "enable auto-commits" },
|
|
49
|
-
{ cmd: "/git off", desc: "disable auto-commits" },
|
|
50
|
-
{ cmd: "/models", desc: "list available models" },
|
|
51
|
-
{ cmd: "/theme", desc: "switch color theme" },
|
|
52
|
-
{ cmd: "/model", desc: "switch model mid-session" },
|
|
53
|
-
{ cmd: "/sessions", desc: "list past sessions" },
|
|
54
|
-
{ cmd: "/session delete", desc: "delete a session" },
|
|
55
|
-
{ cmd: "/resume", desc: "resume a past session" },
|
|
56
|
-
{ cmd: "/skills", desc: "manage skill packs" },
|
|
57
|
-
{ cmd: "/skills install", desc: "install a skill" },
|
|
58
|
-
{ cmd: "/skills remove", desc: "remove a skill" },
|
|
59
|
-
{ cmd: "/skills list", desc: "show installed skills" },
|
|
60
|
-
{ cmd: "/skills search", desc: "search registry" },
|
|
61
|
-
{ cmd: "/skills on", desc: "enable skill for session" },
|
|
62
|
-
{ cmd: "/skills off", desc: "disable skill for session" },
|
|
63
|
-
{ cmd: "/architect", desc: "toggle architect mode" },
|
|
64
|
-
{ cmd: "/lint", desc: "show auto-lint status" },
|
|
65
|
-
{ cmd: "/lint on", desc: "enable auto-lint" },
|
|
66
|
-
{ cmd: "/lint off", desc: "disable auto-lint" },
|
|
67
|
-
{ cmd: "/mcp", desc: "show MCP servers" },
|
|
68
|
-
{ cmd: "/mcp tools", desc: "list MCP tools" },
|
|
69
|
-
{ cmd: "/mcp add", desc: "add MCP server" },
|
|
70
|
-
{ cmd: "/mcp remove", desc: "remove MCP server" },
|
|
71
|
-
{ cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
|
|
72
|
-
{ cmd: "/ollama", desc: "Ollama status & models" },
|
|
73
|
-
{ cmd: "/ollama list", desc: "list installed models" },
|
|
74
|
-
{ cmd: "/ollama start", desc: "start Ollama server" },
|
|
75
|
-
{ cmd: "/ollama stop", desc: "stop Ollama server" },
|
|
76
|
-
{ cmd: "/ollama pull", desc: "download a model" },
|
|
77
|
-
{ cmd: "/ollama delete", desc: "delete a model" },
|
|
78
|
-
{ cmd: "/quit", desc: "exit" },
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
82
|
-
|
|
83
|
-
const SPINNER_MESSAGES = [
|
|
84
|
-
// OG
|
|
85
|
-
"Locking in...", "Cooking...", "Maxxing...", "In the zone...",
|
|
86
|
-
"Yapping...", "Frame mogging...", "Jester gooning...", "Gooning...",
|
|
87
|
-
"Doing back flips...", "Jester maxxing...", "Getting baked...",
|
|
88
|
-
"Blasting tren...", "Pumping...", "Wondering if I should actually do this...",
|
|
89
|
-
"Hacking the main frame...", "Codemaxxing...", "Vibe coding...", "Running a marathon...",
|
|
90
|
-
// Gym/Looksmaxxing
|
|
91
|
-
"Mewing aggressively...", "Looksmaxxing your codebase...", "Hitting a PR on this function...",
|
|
92
|
-
"Eating 4000 calories of code...", "Creatine loading...", "On my bulk arc...",
|
|
93
|
-
"Warming up the deadlift...",
|
|
94
|
-
// Brainrot/Skibidi
|
|
95
|
-
"Going full skibidi...", "Sigma grinding...", "Rizzing up the compiler...",
|
|
96
|
-
"No cap processing...", "Main character coding...", "It's giving implementation...",
|
|
97
|
-
"This code is bussin fr fr...", "Aura check in progress...", "Erm what the sigma...",
|
|
98
|
-
// Deranged/Unhinged
|
|
99
|
-
"Ascending to a higher plane...", "Achieving final form...", "Third eye compiling...",
|
|
100
|
-
"Astral projecting through your repo...", "Becoming one with the codebase...",
|
|
101
|
-
"Having a spiritual awakening...", "Entering the shadow realm...", "Going goblin mode...",
|
|
102
|
-
"Deleting System32... jk...", "Sacrificing tokens to the GPU gods...",
|
|
103
|
-
"Summoning the machine spirit...",
|
|
104
|
-
// Self-aware/Meta
|
|
105
|
-
"Pretending to think really hard...", "Staring at your code judgmentally...",
|
|
106
|
-
"Rethinking my career choices...", "Having an existential crisis...",
|
|
107
|
-
"Hoping this actually works...", "Praying to the stack overflow gods...",
|
|
108
|
-
"Copying from the internet with dignity...",
|
|
109
|
-
// Pure Chaos
|
|
110
|
-
"Doing hot yoga in the terminal...", "Microdosing your dependencies...",
|
|
111
|
-
"Running on 3 hours of sleep...", "Speedrunning your deadline...",
|
|
112
|
-
"Built different rn...", "That's crazy let me cook...",
|
|
113
|
-
"Absolutely feral right now...", "Ong no cap fr fr...",
|
|
114
|
-
"Living rent free in your RAM...", "Ate and left no crumbs...",
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
// ── Neon Spinner ──
|
|
118
|
-
function NeonSpinner({ message, colors }: { message: string; colors: Theme['colors'] }) {
|
|
119
|
-
const [frame, setFrame] = useState(0);
|
|
120
|
-
const [elapsed, setElapsed] = useState(0);
|
|
121
|
-
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
const start = Date.now();
|
|
124
|
-
const interval = setInterval(() => {
|
|
125
|
-
setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
|
|
126
|
-
setElapsed(Math.floor((Date.now() - start) / 1000));
|
|
127
|
-
}, 80);
|
|
128
|
-
return () => clearInterval(interval);
|
|
129
|
-
}, []);
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<Text>
|
|
133
|
-
{" "}<Text color={colors.spinner}>{SPINNER_FRAMES[frame]}</Text>
|
|
134
|
-
{" "}<Text bold color={colors.secondary}>{message}</Text>
|
|
135
|
-
{" "}<Text color={colors.muted}>[{elapsed}s]</Text>
|
|
136
|
-
</Text>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ── Streaming Indicator (subtle, shows model is still working) ──
|
|
141
|
-
const STREAM_DOTS = ["· ", "·· ", "···", " ··", " ·", " "];
|
|
142
|
-
function StreamingIndicator({ colors }: { colors: Theme['colors'] }) {
|
|
143
|
-
const [frame, setFrame] = useState(0);
|
|
144
|
-
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
const interval = setInterval(() => {
|
|
147
|
-
setFrame((f) => (f + 1) % STREAM_DOTS.length);
|
|
148
|
-
}, 300);
|
|
149
|
-
return () => clearInterval(interval);
|
|
150
|
-
}, []);
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<Text dimColor>
|
|
154
|
-
{" "}<Text color={colors.spinner}>{STREAM_DOTS[frame]}</Text>
|
|
155
|
-
{" "}<Text color={colors.muted}>streaming</Text>
|
|
156
|
-
</Text>
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ── Message Types ──
|
|
161
|
-
interface ChatMessage {
|
|
162
|
-
id: number;
|
|
163
|
-
type: "user" | "response" | "tool" | "tool-result" | "error" | "info";
|
|
164
|
-
text: string;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let msgId = 0;
|
|
168
|
-
|
|
169
|
-
// ── Main App ──
|
|
170
|
-
function App() {
|
|
171
|
-
const { exit } = useApp();
|
|
172
|
-
const { stdout } = useStdout();
|
|
173
|
-
const termWidth = stdout?.columns ?? 80;
|
|
174
|
-
|
|
175
|
-
const [input, setInput] = useState("");
|
|
176
|
-
const [pastedChunks, setPastedChunks] = useState<Array<{ id: number; lines: number; content: string }>>([]);
|
|
177
|
-
const [pasteCount, setPasteCount] = useState(0);
|
|
178
|
-
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
179
|
-
const [loading, setLoading] = useState(false);
|
|
180
|
-
const [streaming, setStreaming] = useState(false);
|
|
181
|
-
const [spinnerMsg, setSpinnerMsg] = useState("");
|
|
182
|
-
const [agent, setAgent] = useState<CodingAgent | null>(null);
|
|
183
|
-
const [modelName, setModelName] = useState("");
|
|
184
|
-
const [theme, setTheme] = useState<Theme>(getTheme(DEFAULT_THEME));
|
|
185
|
-
const providerRef = React.useRef<{ baseUrl: string; apiKey: string }>({ baseUrl: "", apiKey: "" });
|
|
186
|
-
const [ready, setReady] = useState(false);
|
|
187
|
-
const [connectionInfo, setConnectionInfo] = useState<string[]>([]);
|
|
188
|
-
const [ctrlCPressed, setCtrlCPressed] = useState(false);
|
|
189
|
-
const [cmdIndex, setCmdIndex] = useState(0);
|
|
190
|
-
const [inputKey, setInputKey] = useState(0);
|
|
191
|
-
const [sessionPicker, setSessionPicker] = useState<Array<{ id: string; display: string }> | null>(null);
|
|
192
|
-
const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
|
|
193
|
-
const [themePicker, setThemePicker] = useState(false);
|
|
194
|
-
const [themePickerIndex, setThemePickerIndex] = useState(0);
|
|
195
|
-
const [deleteSessionPicker, setDeleteSessionPicker] = useState<Array<{ id: string; display: string }> | null>(null);
|
|
196
|
-
const [deleteSessionPickerIndex, setDeleteSessionPickerIndex] = useState(0);
|
|
197
|
-
const [deleteSessionConfirm, setDeleteSessionConfirm] = useState<{ id: string; display: string } | null>(null);
|
|
198
|
-
const [loginPicker, setLoginPicker] = useState(false);
|
|
199
|
-
const [loginPickerIndex, setLoginPickerIndex] = useState(0);
|
|
200
|
-
const [loginMethodPicker, setLoginMethodPicker] = useState<{ provider: string; methods: string[] } | null>(null);
|
|
201
|
-
const [loginMethodIndex, setLoginMethodIndex] = useState(0);
|
|
202
|
-
const [skillsPicker, setSkillsPicker] = useState<"menu" | "browse" | "installed" | "remove" | null>(null);
|
|
203
|
-
const [skillsPickerIndex, setSkillsPickerIndex] = useState(0);
|
|
204
|
-
const [sessionDisabledSkills, setSessionDisabledSkills] = useState<Set<string>>(new Set());
|
|
205
|
-
const [approval, setApproval] = useState<{
|
|
206
|
-
tool: string;
|
|
207
|
-
args: Record<string, unknown>;
|
|
208
|
-
diff?: string;
|
|
209
|
-
resolve: (decision: "yes" | "no" | "always") => void;
|
|
210
|
-
} | null>(null);
|
|
211
|
-
|
|
212
|
-
// ── Ollama Management State ──
|
|
213
|
-
const [ollamaDeleteConfirm, setOllamaDeleteConfirm] = useState<{ model: string; size: number } | null>(null);
|
|
214
|
-
const [ollamaPulling, setOllamaPulling] = useState<{ model: string; progress: PullProgress } | null>(null);
|
|
215
|
-
const [ollamaExitPrompt, setOllamaExitPrompt] = useState(false);
|
|
216
|
-
const [ollamaDeletePicker, setOllamaDeletePicker] = useState<{ models: { name: string; size: number }[] } | null>(null);
|
|
217
|
-
const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
|
|
218
|
-
const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
|
|
219
|
-
const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
|
|
220
|
-
const [modelPicker, setModelPicker] = useState<string[] | null>(null);
|
|
221
|
-
const [modelPickerIndex, setModelPickerIndex] = useState(0);
|
|
222
|
-
|
|
223
|
-
// ── Setup Wizard State ──
|
|
224
|
-
type WizardScreen = "connection" | "models" | "install-ollama" | "pulling" | null;
|
|
225
|
-
const [wizardScreen, setWizardScreen] = useState<WizardScreen>(null);
|
|
226
|
-
const [wizardIndex, setWizardIndex] = useState(0);
|
|
227
|
-
const [wizardHardware, setWizardHardware] = useState<HardwareInfo | null>(null);
|
|
228
|
-
const [wizardModels, setWizardModels] = useState<ScoredModel[]>([]);
|
|
229
|
-
const [wizardPullProgress, setWizardPullProgress] = useState<PullProgress | null>(null);
|
|
230
|
-
const [wizardPullError, setWizardPullError] = useState<string | null>(null);
|
|
231
|
-
const [wizardSelectedModel, setWizardSelectedModel] = useState<ScoredModel | null>(null);
|
|
232
|
-
|
|
233
|
-
// Listen for paste events from stdin interceptor
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
const handler = ({ content, lines }: { content: string; lines: number }) => {
|
|
236
|
-
setPasteCount((c) => {
|
|
237
|
-
const newId = c + 1;
|
|
238
|
-
setPastedChunks((prev) => [...prev, { id: newId, lines, content }]);
|
|
239
|
-
return newId;
|
|
240
|
-
});
|
|
241
|
-
};
|
|
242
|
-
pasteEvents.on("paste", handler);
|
|
243
|
-
return () => { pasteEvents.off("paste", handler); };
|
|
244
|
-
}, []);
|
|
245
|
-
|
|
246
|
-
// Refresh the connection banner to reflect current provider status
|
|
247
|
-
const refreshConnectionBanner = useCallback(async () => {
|
|
248
|
-
const info: string[] = [];
|
|
249
|
-
const cliArgs = parseCLIArgs();
|
|
250
|
-
const rawConfig = loadConfig();
|
|
251
|
-
const config = applyOverrides(rawConfig, cliArgs);
|
|
252
|
-
const provider = config.provider;
|
|
253
|
-
|
|
254
|
-
if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
|
|
255
|
-
const detected = await detectLocalProvider();
|
|
256
|
-
if (detected) {
|
|
257
|
-
info.push(`✔ Connected to ${detected.baseUrl} → ${detected.model}`);
|
|
258
|
-
} else {
|
|
259
|
-
const ollamaUp = await isOllamaRunning();
|
|
260
|
-
info.push(ollamaUp ? "Ollama running (no model loaded)" : "✗ No local LLM server found");
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
info.push(`Provider: ${provider.baseUrl}`);
|
|
264
|
-
info.push(`Model: ${provider.model}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const cwd = process.cwd();
|
|
268
|
-
if (isGitRepo(cwd)) {
|
|
269
|
-
const branch = getBranch(cwd);
|
|
270
|
-
const status = getStatus(cwd);
|
|
271
|
-
info.push(`Git: ${branch} (${status})`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
setConnectionInfo(info);
|
|
275
|
-
}, []);
|
|
276
|
-
|
|
277
|
-
// Connect/reconnect to LLM provider
|
|
278
|
-
const connectToProvider = useCallback(async (isRetry = false) => {
|
|
279
|
-
const cliArgs = parseCLIArgs();
|
|
280
|
-
const rawConfig = loadConfig();
|
|
281
|
-
const config = applyOverrides(rawConfig, cliArgs);
|
|
282
|
-
let provider = config.provider;
|
|
283
|
-
const info: string[] = [];
|
|
284
|
-
|
|
285
|
-
if (isRetry) {
|
|
286
|
-
info.push("Retrying connection...");
|
|
287
|
-
setConnectionInfo([...info]);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
|
|
291
|
-
info.push("Detecting local LLM server...");
|
|
292
|
-
setConnectionInfo([...info]);
|
|
293
|
-
const detection = await detectLocalProviderDetailed();
|
|
294
|
-
if (detection.status === "connected") {
|
|
295
|
-
// Keep CLI model override if specified
|
|
296
|
-
if (cliArgs.model) detection.provider.model = cliArgs.model;
|
|
297
|
-
provider = detection.provider;
|
|
298
|
-
info.push(`✔ Connected to ${provider.baseUrl} → ${provider.model}`);
|
|
299
|
-
setConnectionInfo([...info]);
|
|
300
|
-
} else if (detection.status === "no-models") {
|
|
301
|
-
info.push(`⚠ ${detection.serverName} is running but has no models. Use /ollama pull to download one.`);
|
|
302
|
-
setConnectionInfo([...info]);
|
|
303
|
-
setReady(true);
|
|
304
|
-
return;
|
|
305
|
-
} else {
|
|
306
|
-
info.push("✗ No local LLM server found.");
|
|
307
|
-
setConnectionInfo([...info]);
|
|
308
|
-
setReady(true);
|
|
309
|
-
// Show the setup wizard on first run
|
|
310
|
-
setWizardScreen("connection");
|
|
311
|
-
setWizardIndex(0);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
info.push(`Provider: ${provider.baseUrl}`);
|
|
316
|
-
info.push(`Model: ${provider.model}`);
|
|
317
|
-
setConnectionInfo([...info]);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const cwd = process.cwd();
|
|
321
|
-
|
|
322
|
-
// Git info
|
|
323
|
-
if (isGitRepo(cwd)) {
|
|
324
|
-
const branch = getBranch(cwd);
|
|
325
|
-
const status = getStatus(cwd);
|
|
326
|
-
info.push(`Git: ${branch} (${status})`);
|
|
327
|
-
setConnectionInfo([...info]);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const a = new CodingAgent({
|
|
331
|
-
provider,
|
|
332
|
-
cwd,
|
|
333
|
-
maxTokens: config.defaults.maxTokens,
|
|
334
|
-
autoApprove: config.defaults.autoApprove,
|
|
335
|
-
onToken: (token) => {
|
|
336
|
-
// Switch from big spinner to streaming mode
|
|
337
|
-
setLoading(false);
|
|
338
|
-
setStreaming(true);
|
|
339
|
-
|
|
340
|
-
// Update the current streaming response in-place
|
|
341
|
-
setMessages((prev) => {
|
|
342
|
-
const lastIdx = prev.length - 1;
|
|
343
|
-
const last = prev[lastIdx];
|
|
344
|
-
|
|
345
|
-
if (last && last.type === "response" && (last as any)._streaming) {
|
|
346
|
-
return [
|
|
347
|
-
...prev.slice(0, lastIdx),
|
|
348
|
-
{ ...last, text: last.text + token },
|
|
349
|
-
];
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// First token of a new response
|
|
353
|
-
return [...prev, { id: msgId++, type: "response" as const, text: token, _streaming: true } as any];
|
|
354
|
-
});
|
|
355
|
-
},
|
|
356
|
-
onToolCall: (name, args) => {
|
|
357
|
-
setLoading(true);
|
|
358
|
-
setSpinnerMsg("Executing tools...");
|
|
359
|
-
const argStr = Object.entries(args)
|
|
360
|
-
.map(([k, v]) => {
|
|
361
|
-
const val = String(v);
|
|
362
|
-
return val.length > 60 ? val.slice(0, 60) + "..." : val;
|
|
363
|
-
})
|
|
364
|
-
.join(", ");
|
|
365
|
-
addMsg("tool", `${name}(${argStr})`);
|
|
366
|
-
},
|
|
367
|
-
onToolResult: (_name, result) => {
|
|
368
|
-
const numLines = result.split("\n").length;
|
|
369
|
-
const size = result.length > 1024 ? `${(result.length / 1024).toFixed(1)}KB` : `${result.length}B`;
|
|
370
|
-
addMsg("tool-result", `└ ${numLines} lines (${size})`);
|
|
371
|
-
},
|
|
372
|
-
onThinking: (text) => {
|
|
373
|
-
if (text.length > 0) {
|
|
374
|
-
addMsg("info", `💭 Thought for ${text.split(/\s+/).length} words`);
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
onGitCommit: (message) => {
|
|
378
|
-
addMsg("info", `📝 Auto-committed: ${message}`);
|
|
379
|
-
},
|
|
380
|
-
onContextCompressed: (oldTokens, newTokens) => {
|
|
381
|
-
const saved = oldTokens - newTokens;
|
|
382
|
-
const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
|
|
383
|
-
addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
|
|
384
|
-
},
|
|
385
|
-
onArchitectPlan: (plan) => {
|
|
386
|
-
addMsg("info", `🏗️ Architect Plan:\n${plan}`);
|
|
387
|
-
},
|
|
388
|
-
onLintResult: (file, errors) => {
|
|
389
|
-
addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
|
|
390
|
-
},
|
|
391
|
-
onMCPStatus: (server, status) => {
|
|
392
|
-
addMsg("info", `🔌 MCP ${server}: ${status}`);
|
|
393
|
-
},
|
|
394
|
-
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
395
|
-
onToolApproval: (name, args, diff) => {
|
|
396
|
-
return new Promise((resolve) => {
|
|
397
|
-
setApproval({ tool: name, args, diff, resolve });
|
|
398
|
-
setLoading(false);
|
|
399
|
-
});
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
// Initialize async context (repo map)
|
|
404
|
-
await a.init();
|
|
405
|
-
|
|
406
|
-
// Show project rules in banner
|
|
407
|
-
const rulesSource = a.getProjectRulesSource();
|
|
408
|
-
if (rulesSource) {
|
|
409
|
-
info.push(`📋 ${rulesSource} loaded`);
|
|
410
|
-
setConnectionInfo([...info]);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Show MCP server count
|
|
414
|
-
const mcpCount = a.getMCPServerCount();
|
|
415
|
-
if (mcpCount > 0) {
|
|
416
|
-
info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
|
|
417
|
-
setConnectionInfo([...info]);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
setAgent(a);
|
|
421
|
-
setModelName(provider.model);
|
|
422
|
-
providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
|
|
423
|
-
setReady(true);
|
|
424
|
-
if (isRetry) {
|
|
425
|
-
addMsg("info", `✅ Connected to ${provider.model}`);
|
|
426
|
-
}
|
|
427
|
-
}, []);
|
|
428
|
-
|
|
429
|
-
// Initialize agent on mount
|
|
430
|
-
useEffect(() => {
|
|
431
|
-
connectToProvider(false);
|
|
432
|
-
}, []);
|
|
433
|
-
|
|
434
|
-
function addMsg(type: ChatMessage["type"], text: string) {
|
|
435
|
-
setMessages((prev) => [...prev, { id: msgId++, type, text }]);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Compute matching commands for suggestions
|
|
439
|
-
const cmdMatches = input.startsWith("/")
|
|
440
|
-
? SLASH_COMMANDS.filter(c => c.cmd.startsWith(input.toLowerCase()))
|
|
441
|
-
: [];
|
|
442
|
-
const showSuggestions = cmdMatches.length > 0 && !loading && !approval && input !== cmdMatches[0]?.cmd;
|
|
443
|
-
|
|
444
|
-
// Refs to avoid stale closures in handleSubmit
|
|
445
|
-
const cmdIndexRef = React.useRef(cmdIndex);
|
|
446
|
-
cmdIndexRef.current = cmdIndex;
|
|
447
|
-
const cmdMatchesRef = React.useRef(cmdMatches);
|
|
448
|
-
cmdMatchesRef.current = cmdMatches;
|
|
449
|
-
const showSuggestionsRef = React.useRef(showSuggestions);
|
|
450
|
-
showSuggestionsRef.current = showSuggestions;
|
|
451
|
-
const pastedChunksRef = React.useRef(pastedChunks);
|
|
452
|
-
pastedChunksRef.current = pastedChunks;
|
|
453
|
-
|
|
454
|
-
const handleSubmit = useCallback(async (value: string) => {
|
|
455
|
-
// Skip autocomplete if input exactly matches a command (e.g. /models vs /model)
|
|
456
|
-
const isExactCommand = SLASH_COMMANDS.some(c => c.cmd === value.trim());
|
|
457
|
-
|
|
458
|
-
// If suggestions are showing and input isn't already an exact command, use autocomplete
|
|
459
|
-
if (showSuggestionsRef.current && !isExactCommand) {
|
|
460
|
-
const matches = cmdMatchesRef.current;
|
|
461
|
-
const idx = cmdIndexRef.current;
|
|
462
|
-
const selected = matches[idx];
|
|
463
|
-
if (selected) {
|
|
464
|
-
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
465
|
-
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
|
|
466
|
-
selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
|
|
467
|
-
selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
|
|
468
|
-
setInput(selected.cmd + " ");
|
|
469
|
-
setCmdIndex(0);
|
|
470
|
-
setInputKey((k) => k + 1);
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
// Execute the selected command directly
|
|
474
|
-
value = selected.cmd;
|
|
475
|
-
setCmdIndex(0);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Combine typed text with any pasted chunks
|
|
480
|
-
const chunks = pastedChunksRef.current;
|
|
481
|
-
let fullValue = value;
|
|
482
|
-
if (chunks.length > 0) {
|
|
483
|
-
const pasteText = chunks.map(p => p.content).join("\n\n");
|
|
484
|
-
fullValue = value ? `${value}\n\n${pasteText}` : pasteText;
|
|
485
|
-
}
|
|
486
|
-
const trimmed = fullValue.trim();
|
|
487
|
-
setInput("");
|
|
488
|
-
setPastedChunks([]);
|
|
489
|
-
setPasteCount(0);
|
|
490
|
-
if (!trimmed) return;
|
|
491
|
-
|
|
492
|
-
addMsg("user", trimmed);
|
|
493
|
-
|
|
494
|
-
if (trimmed === "/quit" || trimmed === "/exit") {
|
|
495
|
-
// Check if Ollama is running and offer to stop it
|
|
496
|
-
const ollamaUp = await isOllamaRunning();
|
|
497
|
-
if (ollamaUp) {
|
|
498
|
-
const config = loadConfig();
|
|
499
|
-
if (config.defaults.stopOllamaOnExit) {
|
|
500
|
-
addMsg("info", "Stopping Ollama...");
|
|
501
|
-
await stopOllama();
|
|
502
|
-
exit();
|
|
503
|
-
} else {
|
|
504
|
-
setOllamaExitPrompt(true);
|
|
505
|
-
}
|
|
506
|
-
} else {
|
|
507
|
-
exit();
|
|
508
|
-
}
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
if (trimmed === "/login" || trimmed === "/auth") {
|
|
512
|
-
setLoginPicker(true);
|
|
513
|
-
setLoginPickerIndex(0);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
if (trimmed === "/connect") {
|
|
517
|
-
addMsg("info", "🔄 Reconnecting...");
|
|
518
|
-
await connectToProvider(true);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
if (trimmed === "/help") {
|
|
522
|
-
addMsg("info", [
|
|
523
|
-
"Commands:",
|
|
524
|
-
" /help — show this",
|
|
525
|
-
" /connect — retry LLM connection",
|
|
526
|
-
" /login — authentication setup (run codemaxxing login in terminal)",
|
|
527
|
-
" /model — switch model mid-session",
|
|
528
|
-
" /models — list available models",
|
|
529
|
-
" /map — show repository map",
|
|
530
|
-
" /sessions — list past sessions",
|
|
531
|
-
" /session delete — delete a session",
|
|
532
|
-
" /resume — resume a past session",
|
|
533
|
-
" /reset — clear conversation",
|
|
534
|
-
" /context — show message count",
|
|
535
|
-
" /diff — show git changes",
|
|
536
|
-
" /undo — revert last codemaxxing commit",
|
|
537
|
-
" /commit — commit all changes",
|
|
538
|
-
" /push — push to remote",
|
|
539
|
-
" /git on — enable auto-commits",
|
|
540
|
-
" /git off — disable auto-commits",
|
|
541
|
-
" /skills — manage skill packs",
|
|
542
|
-
" /architect — toggle architect mode (plan then execute)",
|
|
543
|
-
" /lint — show auto-lint status & detected linter",
|
|
544
|
-
" /lint on — enable auto-lint",
|
|
545
|
-
" /lint off — disable auto-lint",
|
|
546
|
-
" /mcp — show MCP servers & status",
|
|
547
|
-
" /mcp tools — list all MCP tools",
|
|
548
|
-
" /mcp add — add MCP server to global config",
|
|
549
|
-
" /mcp remove — remove MCP server",
|
|
550
|
-
" /mcp reconnect — reconnect all MCP servers",
|
|
551
|
-
" /ollama — Ollama status, models & GPU usage",
|
|
552
|
-
" /ollama list — list installed models with sizes",
|
|
553
|
-
" /ollama start — start Ollama server",
|
|
554
|
-
" /ollama stop — stop Ollama server (frees GPU RAM)",
|
|
555
|
-
" /ollama pull <model> — download a model",
|
|
556
|
-
" /ollama delete <model> — delete a model from disk",
|
|
557
|
-
" /quit — exit",
|
|
558
|
-
].join("\n"));
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
// ── Skills commands (work without agent) ──
|
|
562
|
-
if (trimmed === "/skills") {
|
|
563
|
-
setSkillsPicker("menu");
|
|
564
|
-
setSkillsPickerIndex(0);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
if (trimmed.startsWith("/skills install ")) {
|
|
568
|
-
const name = trimmed.replace("/skills install ", "").trim();
|
|
569
|
-
const result = installSkill(name);
|
|
570
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
if (trimmed.startsWith("/skills remove ")) {
|
|
574
|
-
const name = trimmed.replace("/skills remove ", "").trim();
|
|
575
|
-
const result = removeSkill(name);
|
|
576
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (trimmed === "/skills list") {
|
|
580
|
-
const installed = listInstalledSkills();
|
|
581
|
-
if (installed.length === 0) {
|
|
582
|
-
addMsg("info", "No skills installed. Use /skills to browse & install.");
|
|
583
|
-
} else {
|
|
584
|
-
const active = getActiveSkills(process.cwd(), sessionDisabledSkills);
|
|
585
|
-
const lines = installed.map((s) => {
|
|
586
|
-
const isActive = active.includes(s.name);
|
|
587
|
-
const disabledBySession = sessionDisabledSkills.has(s.name);
|
|
588
|
-
const status = disabledBySession ? " (off)" : isActive ? " (on)" : "";
|
|
589
|
-
return ` ${isActive ? "●" : "○"} ${s.name} — ${s.description}${status}`;
|
|
590
|
-
});
|
|
591
|
-
addMsg("info", `Installed skills:\n${lines.join("\n")}`);
|
|
592
|
-
}
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (trimmed.startsWith("/skills search ")) {
|
|
596
|
-
const query = trimmed.replace("/skills search ", "").trim();
|
|
597
|
-
const results = searchRegistry(query);
|
|
598
|
-
if (results.length === 0) {
|
|
599
|
-
addMsg("info", `No skills found matching "${query}".`);
|
|
600
|
-
} else {
|
|
601
|
-
const installed = listInstalledSkills().map((s) => s.name);
|
|
602
|
-
const lines = results.map((s) => {
|
|
603
|
-
const mark = installed.includes(s.name) ? " ✓" : "";
|
|
604
|
-
return ` ${s.name} — ${s.description}${mark}`;
|
|
605
|
-
});
|
|
606
|
-
addMsg("info", `Registry matches:\n${lines.join("\n")}`);
|
|
607
|
-
}
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
if (trimmed.startsWith("/skills create ")) {
|
|
611
|
-
const name = trimmed.replace("/skills create ", "").trim();
|
|
612
|
-
if (!name) {
|
|
613
|
-
addMsg("info", "Usage: /skills create <name>");
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
const result = createSkillScaffold(name);
|
|
617
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}\n Edit: ${result.path}/prompt.md` : `✗ ${result.message}`);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
if (trimmed.startsWith("/skills on ")) {
|
|
621
|
-
const name = trimmed.replace("/skills on ", "").trim();
|
|
622
|
-
const installed = listInstalledSkills().map((s) => s.name);
|
|
623
|
-
if (!installed.includes(name)) {
|
|
624
|
-
addMsg("error", `Skill "${name}" is not installed.`);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
setSessionDisabledSkills((prev) => { const next = new Set(prev); next.delete(name); return next; });
|
|
628
|
-
if (agent) agent.enableSkill(name);
|
|
629
|
-
addMsg("info", `✅ Enabled skill: ${name}`);
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
if (trimmed.startsWith("/skills off ")) {
|
|
633
|
-
const name = trimmed.replace("/skills off ", "").trim();
|
|
634
|
-
const installed = listInstalledSkills().map((s) => s.name);
|
|
635
|
-
if (!installed.includes(name)) {
|
|
636
|
-
addMsg("error", `Skill "${name}" is not installed.`);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
setSessionDisabledSkills((prev) => { const next = new Set(prev); next.add(name); return next; });
|
|
640
|
-
if (agent) agent.disableSkill(name);
|
|
641
|
-
addMsg("info", `✅ Disabled skill: ${name} (session only)`);
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
if (trimmed.startsWith("/theme")) {
|
|
645
|
-
const themeName = trimmed.replace("/theme", "").trim();
|
|
646
|
-
if (!themeName) {
|
|
647
|
-
const themeKeys = listThemes();
|
|
648
|
-
const currentIdx = themeKeys.indexOf(theme.name.toLowerCase());
|
|
649
|
-
setThemePicker(true);
|
|
650
|
-
setThemePickerIndex(currentIdx >= 0 ? currentIdx : 0);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (!THEMES[themeName]) {
|
|
654
|
-
addMsg("error", `Theme "${themeName}" not found. Use /theme to see available themes.`);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
setTheme(getTheme(themeName));
|
|
658
|
-
addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
// ── Architect commands (work without agent) ──
|
|
662
|
-
if (trimmed === "/architect") {
|
|
663
|
-
if (!agent) {
|
|
664
|
-
addMsg("info", "🏗️ Architect mode: no agent connected. Connect first with /login or /connect.");
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
const current = agent.getArchitectModel();
|
|
668
|
-
if (current) {
|
|
669
|
-
agent.setArchitectModel(null);
|
|
670
|
-
addMsg("info", "🏗️ Architect mode OFF");
|
|
671
|
-
} else {
|
|
672
|
-
// Use config default or a sensible default
|
|
673
|
-
const defaultModel = loadConfig().defaults.architectModel || agent.getModel();
|
|
674
|
-
agent.setArchitectModel(defaultModel);
|
|
675
|
-
addMsg("info", `🏗️ Architect mode ON (planner: ${defaultModel})`);
|
|
676
|
-
}
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (trimmed.startsWith("/architect ")) {
|
|
680
|
-
const model = trimmed.replace("/architect ", "").trim();
|
|
681
|
-
if (!model) {
|
|
682
|
-
addMsg("info", "Usage: /architect <model> or /architect to toggle");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
if (agent) {
|
|
686
|
-
agent.setArchitectModel(model);
|
|
687
|
-
addMsg("info", `🏗️ Architect mode ON (planner: ${model})`);
|
|
688
|
-
} else {
|
|
689
|
-
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
690
|
-
}
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// ── Lint commands (work without agent) ──
|
|
695
|
-
if (trimmed === "/lint") {
|
|
696
|
-
const { detectLinter } = await import("./utils/lint.js");
|
|
697
|
-
const linter = detectLinter(process.cwd());
|
|
698
|
-
const enabled = agent ? agent.isAutoLintEnabled() : true;
|
|
699
|
-
if (linter) {
|
|
700
|
-
addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n Detected: ${linter.name}\n Command: ${linter.command} <file>`);
|
|
701
|
-
} else {
|
|
702
|
-
addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n No linter detected in this project.`);
|
|
703
|
-
}
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (trimmed === "/lint on") {
|
|
707
|
-
if (agent) agent.setAutoLint(true);
|
|
708
|
-
addMsg("info", "🔍 Auto-lint ON");
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (trimmed === "/lint off") {
|
|
712
|
-
if (agent) agent.setAutoLint(false);
|
|
713
|
-
addMsg("info", "🔍 Auto-lint OFF");
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// ── Ollama commands (work without agent) ──
|
|
718
|
-
if (trimmed === "/ollama" || trimmed === "/ollama status") {
|
|
719
|
-
const running = await isOllamaRunning();
|
|
720
|
-
const lines: string[] = [`Ollama: ${running ? "running" : "stopped"}`];
|
|
721
|
-
if (running) {
|
|
722
|
-
const models = await listInstalledModelsDetailed();
|
|
723
|
-
if (models.length > 0) {
|
|
724
|
-
lines.push(`Installed models (${models.length}):`);
|
|
725
|
-
for (const m of models) {
|
|
726
|
-
const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
|
|
727
|
-
lines.push(` ${m.name} (${sizeGB} GB)`);
|
|
728
|
-
}
|
|
729
|
-
} else {
|
|
730
|
-
lines.push("No models installed.");
|
|
731
|
-
}
|
|
732
|
-
const gpuMem = getGPUMemoryUsage();
|
|
733
|
-
if (gpuMem) lines.push(`GPU: ${gpuMem}`);
|
|
734
|
-
} else {
|
|
735
|
-
lines.push("Start with: /ollama start");
|
|
736
|
-
}
|
|
737
|
-
addMsg("info", lines.join("\n"));
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
if (trimmed === "/ollama list") {
|
|
741
|
-
const running = await isOllamaRunning();
|
|
742
|
-
if (!running) {
|
|
743
|
-
addMsg("info", "Ollama is not running. Start with /ollama start");
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
const models = await listInstalledModelsDetailed();
|
|
747
|
-
if (models.length === 0) {
|
|
748
|
-
addMsg("info", "No models installed. Pull one with /ollama pull <model>");
|
|
749
|
-
} else {
|
|
750
|
-
const lines = models.map((m) => {
|
|
751
|
-
const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
|
|
752
|
-
return ` ${m.name} (${sizeGB} GB)`;
|
|
753
|
-
});
|
|
754
|
-
addMsg("info", `Installed models:\n${lines.join("\n")}`);
|
|
755
|
-
}
|
|
756
|
-
return;
|
|
757
|
-
}
|
|
758
|
-
if (trimmed === "/ollama start") {
|
|
759
|
-
const running = await isOllamaRunning();
|
|
760
|
-
if (running) {
|
|
761
|
-
addMsg("info", "Ollama is already running.");
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
if (!isOllamaInstalled()) {
|
|
765
|
-
addMsg("error", `Ollama is not installed. Install with: ${getOllamaInstallCommand(process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")}`);
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
startOllama();
|
|
769
|
-
addMsg("info", "Starting Ollama server...");
|
|
770
|
-
// Wait for it to come up
|
|
771
|
-
for (let i = 0; i < 10; i++) {
|
|
772
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
773
|
-
if (await isOllamaRunning()) {
|
|
774
|
-
addMsg("info", "Ollama is running.");
|
|
775
|
-
await refreshConnectionBanner();
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
addMsg("error", "Ollama did not start in time. Try running 'ollama serve' manually.");
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
if (trimmed === "/ollama stop") {
|
|
783
|
-
const running = await isOllamaRunning();
|
|
784
|
-
if (!running) {
|
|
785
|
-
addMsg("info", "Ollama is not running.");
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
addMsg("info", "Stopping Ollama...");
|
|
789
|
-
const result = await stopOllama();
|
|
790
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
|
|
791
|
-
if (result.ok) await refreshConnectionBanner();
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
if (trimmed === "/ollama pull") {
|
|
795
|
-
// No model specified — show picker
|
|
796
|
-
setOllamaPullPicker(true);
|
|
797
|
-
setOllamaPullPickerIndex(0);
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
if (trimmed.startsWith("/ollama pull ")) {
|
|
801
|
-
const modelId = trimmed.replace("/ollama pull ", "").trim();
|
|
802
|
-
if (!modelId) {
|
|
803
|
-
setOllamaPullPicker(true);
|
|
804
|
-
setOllamaPullPickerIndex(0);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
if (!isOllamaInstalled()) {
|
|
808
|
-
addMsg("error", "Ollama is not installed.");
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
// Ensure ollama is running
|
|
812
|
-
let running = await isOllamaRunning();
|
|
813
|
-
if (!running) {
|
|
814
|
-
startOllama();
|
|
815
|
-
addMsg("info", "Starting Ollama server...");
|
|
816
|
-
for (let i = 0; i < 10; i++) {
|
|
817
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
818
|
-
if (await isOllamaRunning()) { running = true; break; }
|
|
819
|
-
}
|
|
820
|
-
if (!running) {
|
|
821
|
-
addMsg("error", "Could not start Ollama. Run 'ollama serve' manually.");
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
setOllamaPulling({ model: modelId, progress: { status: "starting", percent: 0 } });
|
|
826
|
-
try {
|
|
827
|
-
await pullModel(modelId, (p) => {
|
|
828
|
-
setOllamaPulling({ model: modelId, progress: p });
|
|
829
|
-
});
|
|
830
|
-
setOllamaPulling(null);
|
|
831
|
-
addMsg("info", `\u2705 Downloaded ${modelId}`);
|
|
832
|
-
} catch (err: any) {
|
|
833
|
-
setOllamaPulling(null);
|
|
834
|
-
addMsg("error", `Failed to pull ${modelId}: ${err.message}`);
|
|
835
|
-
}
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
if (trimmed === "/ollama delete") {
|
|
839
|
-
// Ensure Ollama is running so we can list models
|
|
840
|
-
let running = await isOllamaRunning();
|
|
841
|
-
if (!running) {
|
|
842
|
-
addMsg("info", "Starting Ollama to list models...");
|
|
843
|
-
startOllama();
|
|
844
|
-
for (let i = 0; i < 10; i++) {
|
|
845
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
846
|
-
if (await isOllamaRunning()) { running = true; break; }
|
|
847
|
-
}
|
|
848
|
-
if (!running) {
|
|
849
|
-
addMsg("error", "Could not start Ollama. Start it manually first.");
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
const models = await listInstalledModelsDetailed();
|
|
854
|
-
if (models.length === 0) {
|
|
855
|
-
addMsg("info", "No models installed.");
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
|
|
859
|
-
setOllamaDeletePickerIndex(0);
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
if (trimmed.startsWith("/ollama delete ")) {
|
|
863
|
-
const modelId = trimmed.replace("/ollama delete ", "").trim();
|
|
864
|
-
if (!modelId) {
|
|
865
|
-
const models = await listInstalledModelsDetailed();
|
|
866
|
-
if (models.length === 0) {
|
|
867
|
-
addMsg("info", "No models installed.");
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
setOllamaDeletePicker({ models: models.map(m => ({ name: m.name, size: m.size })) });
|
|
871
|
-
setOllamaDeletePickerIndex(0);
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
// Look up size for confirmation
|
|
875
|
-
const models = await listInstalledModelsDetailed();
|
|
876
|
-
const found = models.find((m) => m.name === modelId || m.name.startsWith(modelId));
|
|
877
|
-
if (!found) {
|
|
878
|
-
addMsg("error", `Model "${modelId}" not found. Use /ollama list to see installed models.`);
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
setOllamaDeleteConfirm({ model: found.name, size: found.size });
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
// ── MCP commands (partially work without agent) ──
|
|
886
|
-
if (trimmed === "/mcp" || trimmed === "/mcp list") {
|
|
887
|
-
const servers = listServers(process.cwd());
|
|
888
|
-
if (servers.length === 0) {
|
|
889
|
-
addMsg("info", "🔌 No MCP servers configured.\n Add one: /mcp add <name> <command> [args...]");
|
|
890
|
-
} else {
|
|
891
|
-
const lines = servers.map((s) => {
|
|
892
|
-
const status = s.connected ? `✔ connected (${s.toolCount} tools)` : "✗ not connected";
|
|
893
|
-
return ` ${s.connected ? "●" : "○"} ${s.name} [${s.source}] — ${s.command}\n ${status}`;
|
|
894
|
-
});
|
|
895
|
-
addMsg("info", `🔌 MCP Servers:\n${lines.join("\n")}`);
|
|
896
|
-
}
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
if (trimmed === "/mcp tools") {
|
|
900
|
-
const servers = getConnectedServers();
|
|
901
|
-
if (servers.length === 0) {
|
|
902
|
-
addMsg("info", "🔌 No MCP servers connected.");
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
const lines: string[] = [];
|
|
906
|
-
for (const server of servers) {
|
|
907
|
-
lines.push(`${server.name} (${server.tools.length} tools):`);
|
|
908
|
-
for (const tool of server.tools) {
|
|
909
|
-
lines.push(` • ${tool.name} — ${tool.description ?? "(no description)"}`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
addMsg("info", `🔌 MCP Tools:\n${lines.join("\n")}`);
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (trimmed.startsWith("/mcp add ")) {
|
|
916
|
-
const parts = trimmed.replace("/mcp add ", "").trim().split(/\s+/);
|
|
917
|
-
if (parts.length < 2) {
|
|
918
|
-
addMsg("info", "Usage: /mcp add <name> <command> [args...]\n Example: /mcp add github npx -y @modelcontextprotocol/server-github");
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
const [name, command, ...cmdArgs] = parts;
|
|
922
|
-
const result = addServer(name, { command, args: cmdArgs.length > 0 ? cmdArgs : undefined });
|
|
923
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
924
|
-
return;
|
|
925
|
-
}
|
|
926
|
-
if (trimmed.startsWith("/mcp remove ")) {
|
|
927
|
-
const name = trimmed.replace("/mcp remove ", "").trim();
|
|
928
|
-
if (!name) {
|
|
929
|
-
addMsg("info", "Usage: /mcp remove <name>");
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
const result = removeServer(name);
|
|
933
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
if (trimmed === "/mcp reconnect") {
|
|
937
|
-
if (!agent) {
|
|
938
|
-
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
939
|
-
return;
|
|
940
|
-
}
|
|
941
|
-
addMsg("info", "🔌 Reconnecting MCP servers...");
|
|
942
|
-
await agent.reconnectMCP();
|
|
943
|
-
const count = agent.getMCPServerCount();
|
|
944
|
-
addMsg("info", count > 0
|
|
945
|
-
? `✅ ${count} MCP server${count > 1 ? "s" : ""} reconnected.`
|
|
946
|
-
: "No MCP servers connected.");
|
|
947
|
-
return;
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Commands below require an active LLM connection
|
|
951
|
-
if (!agent) {
|
|
952
|
-
addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
if (trimmed === "/reset") {
|
|
956
|
-
agent.reset();
|
|
957
|
-
addMsg("info", "✅ Conversation reset.");
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
if (trimmed === "/context") {
|
|
961
|
-
addMsg("info", `Messages in context: ${agent.getContextLength()}`);
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
if (trimmed === "/models") {
|
|
965
|
-
addMsg("info", "Fetching available models...");
|
|
966
|
-
const { baseUrl, apiKey } = providerRef.current;
|
|
967
|
-
const models = await listModels(baseUrl, apiKey);
|
|
968
|
-
if (models.length === 0) {
|
|
969
|
-
addMsg("info", "No models found or couldn't reach provider.");
|
|
970
|
-
} else {
|
|
971
|
-
addMsg("info", "Available models:\n" + models.map(m => ` ${m}`).join("\n"));
|
|
972
|
-
}
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
if (trimmed === "/model") {
|
|
976
|
-
// Show picker of available models
|
|
977
|
-
addMsg("info", "Fetching available models...");
|
|
978
|
-
try {
|
|
979
|
-
const ollamaModels = await listInstalledModelsDetailed();
|
|
980
|
-
if (ollamaModels.length > 0) {
|
|
981
|
-
setModelPicker(ollamaModels.map(m => m.name));
|
|
982
|
-
setModelPickerIndex(0);
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
} catch (err) {
|
|
986
|
-
// Ollama not available or failed, try provider
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Fallback: try provider's model list
|
|
990
|
-
if (providerRef.current?.baseUrl && providerRef.current.baseUrl !== "auto") {
|
|
991
|
-
try {
|
|
992
|
-
const providerModels = await listModels(providerRef.current.baseUrl, providerRef.current.apiKey || "");
|
|
993
|
-
if (providerModels.length > 0) {
|
|
994
|
-
setModelPicker(providerModels);
|
|
995
|
-
setModelPickerIndex(0);
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
} catch (err) {
|
|
999
|
-
// Provider fetch failed
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
// No models found anywhere
|
|
1004
|
-
addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
if (trimmed.startsWith("/model ")) {
|
|
1008
|
-
const newModel = trimmed.replace("/model ", "").trim();
|
|
1009
|
-
if (!newModel) {
|
|
1010
|
-
addMsg("info", `Current model: ${modelName}\n Usage: /model <model-name>`);
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
agent.switchModel(newModel);
|
|
1014
|
-
setModelName(newModel);
|
|
1015
|
-
addMsg("info", `✅ Switched to model: ${newModel}`);
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
if (trimmed === "/map") {
|
|
1019
|
-
const map = agent.getRepoMap();
|
|
1020
|
-
if (map) {
|
|
1021
|
-
addMsg("info", map);
|
|
1022
|
-
} else {
|
|
1023
|
-
// Map hasn't been built yet, refresh it
|
|
1024
|
-
setLoading(true);
|
|
1025
|
-
const newMap = await agent.refreshRepoMap();
|
|
1026
|
-
addMsg("info", newMap || "No repository map available.");
|
|
1027
|
-
setLoading(false);
|
|
1028
|
-
}
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
if (trimmed === "/diff") {
|
|
1032
|
-
const diff = getDiff(process.cwd());
|
|
1033
|
-
addMsg("info", diff);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (trimmed === "/undo") {
|
|
1037
|
-
const result = undoLastCommit(process.cwd());
|
|
1038
|
-
addMsg("info", result.success ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
if (trimmed === "/git on") {
|
|
1042
|
-
if (!agent.isGitEnabled()) {
|
|
1043
|
-
addMsg("info", "✗ Not a git repository");
|
|
1044
|
-
} else {
|
|
1045
|
-
agent.setAutoCommit(true);
|
|
1046
|
-
addMsg("info", "✅ Auto-commits enabled for this session");
|
|
1047
|
-
}
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
if (trimmed === "/git off") {
|
|
1051
|
-
agent.setAutoCommit(false);
|
|
1052
|
-
addMsg("info", "✅ Auto-commits disabled");
|
|
1053
|
-
return;
|
|
1054
|
-
}
|
|
1055
|
-
if (trimmed === "/sessions") {
|
|
1056
|
-
const sessions = listSessions(10);
|
|
1057
|
-
if (sessions.length === 0) {
|
|
1058
|
-
addMsg("info", "No past sessions found.");
|
|
1059
|
-
} else {
|
|
1060
|
-
const lines = sessions.map((s, i) => {
|
|
1061
|
-
const date = new Date(s.updated_at + "Z");
|
|
1062
|
-
const ago = formatTimeAgo(date);
|
|
1063
|
-
const dir = s.cwd.split("/").pop() || s.cwd;
|
|
1064
|
-
const tokens = s.token_estimate >= 1000
|
|
1065
|
-
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
1066
|
-
: String(s.token_estimate);
|
|
1067
|
-
const cost = s.estimated_cost > 0
|
|
1068
|
-
? `$${s.estimated_cost < 0.01 ? s.estimated_cost.toFixed(4) : s.estimated_cost.toFixed(2)}`
|
|
1069
|
-
: "";
|
|
1070
|
-
return ` ${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok${cost ? ` ${cost}` : ""} ${ago} ${s.model}`;
|
|
1071
|
-
});
|
|
1072
|
-
addMsg("info", "Recent sessions:\n" + lines.join("\n") + "\n\n Use /resume <id> to continue a session");
|
|
1073
|
-
}
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
if (trimmed.startsWith("/session delete")) {
|
|
1077
|
-
const idArg = trimmed.replace("/session delete", "").trim();
|
|
1078
|
-
if (idArg) {
|
|
1079
|
-
// Direct delete by ID
|
|
1080
|
-
const session = getSession(idArg);
|
|
1081
|
-
if (!session) {
|
|
1082
|
-
addMsg("error", `Session "${idArg}" not found.`);
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
const dir = session.cwd.split("/").pop() || session.cwd;
|
|
1086
|
-
setDeleteSessionConfirm({ id: idArg, display: `${idArg} ${dir}/ ${session.message_count} msgs ${session.model}` });
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
// Show picker
|
|
1090
|
-
const sessions = listSessions(10);
|
|
1091
|
-
if (sessions.length === 0) {
|
|
1092
|
-
addMsg("info", "No sessions to delete.");
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
const items = sessions.map((s) => {
|
|
1096
|
-
const date = new Date(s.updated_at + "Z");
|
|
1097
|
-
const ago = formatTimeAgo(date);
|
|
1098
|
-
const dir = s.cwd.split("/").pop() || s.cwd;
|
|
1099
|
-
const tokens = s.token_estimate >= 1000
|
|
1100
|
-
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
1101
|
-
: String(s.token_estimate);
|
|
1102
|
-
return {
|
|
1103
|
-
id: s.id,
|
|
1104
|
-
display: `${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`,
|
|
1105
|
-
};
|
|
1106
|
-
});
|
|
1107
|
-
setDeleteSessionPicker(items);
|
|
1108
|
-
setDeleteSessionPickerIndex(0);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
if (trimmed === "/resume") {
|
|
1112
|
-
const sessions = listSessions(10);
|
|
1113
|
-
if (sessions.length === 0) {
|
|
1114
|
-
addMsg("info", "No past sessions to resume.");
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
const items = sessions.map((s) => {
|
|
1118
|
-
const date = new Date(s.updated_at + "Z");
|
|
1119
|
-
const ago = formatTimeAgo(date);
|
|
1120
|
-
const dir = s.cwd.split("/").pop() || s.cwd;
|
|
1121
|
-
const tokens = s.token_estimate >= 1000
|
|
1122
|
-
? `${(s.token_estimate / 1000).toFixed(1)}k`
|
|
1123
|
-
: String(s.token_estimate);
|
|
1124
|
-
return {
|
|
1125
|
-
id: s.id,
|
|
1126
|
-
display: `${s.id} ${dir}/ ${s.message_count} msgs ~${tokens} tok ${ago} ${s.model}`,
|
|
1127
|
-
};
|
|
1128
|
-
});
|
|
1129
|
-
setSessionPicker(items);
|
|
1130
|
-
setSessionPickerIndex(0);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
if (trimmed === "/push") {
|
|
1134
|
-
try {
|
|
1135
|
-
const output = execSync("git push", { cwd: process.cwd(), encoding: "utf-8", stdio: "pipe" });
|
|
1136
|
-
addMsg("info", `✅ Pushed to remote${output.trim() ? "\n" + output.trim() : ""}`);
|
|
1137
|
-
} catch (e: any) {
|
|
1138
|
-
addMsg("error", `Push failed: ${e.stderr || e.message}`);
|
|
1139
|
-
}
|
|
1140
|
-
return;
|
|
1141
|
-
}
|
|
1142
|
-
if (trimmed.startsWith("/commit")) {
|
|
1143
|
-
const msg = trimmed.replace("/commit", "").trim();
|
|
1144
|
-
if (!msg) {
|
|
1145
|
-
addMsg("info", "Usage: /commit your commit message here");
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
try {
|
|
1149
|
-
execSync("git add -A", { cwd: process.cwd(), stdio: "pipe" });
|
|
1150
|
-
execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, { cwd: process.cwd(), stdio: "pipe" });
|
|
1151
|
-
addMsg("info", `✅ Committed: ${msg}`);
|
|
1152
|
-
} catch (e: any) {
|
|
1153
|
-
addMsg("error", `Commit failed: ${e.stderr || e.message}`);
|
|
1154
|
-
}
|
|
1155
|
-
return;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
setLoading(true);
|
|
1159
|
-
setStreaming(false);
|
|
1160
|
-
setSpinnerMsg(SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)]);
|
|
1161
|
-
|
|
1162
|
-
try {
|
|
1163
|
-
// Response is built incrementally via onToken callback
|
|
1164
|
-
// send() routes through architect if enabled, otherwise direct chat
|
|
1165
|
-
await agent.send(trimmed);
|
|
1166
|
-
} catch (err: any) {
|
|
1167
|
-
addMsg("error", `Error: ${err.message}`);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
setLoading(false);
|
|
1171
|
-
setStreaming(false);
|
|
1172
|
-
}, [agent, exit, refreshConnectionBanner]);
|
|
1173
|
-
|
|
1174
|
-
useInput((inputChar, key) => {
|
|
1175
|
-
// Handle slash command navigation
|
|
1176
|
-
if (showSuggestionsRef.current) {
|
|
1177
|
-
const matches = cmdMatchesRef.current;
|
|
1178
|
-
if (key.upArrow) {
|
|
1179
|
-
setCmdIndex((prev) => (prev - 1 + matches.length) % matches.length);
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
if (key.downArrow) {
|
|
1183
|
-
setCmdIndex((prev) => (prev + 1) % matches.length);
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
if (key.tab) {
|
|
1187
|
-
const selected = matches[cmdIndexRef.current];
|
|
1188
|
-
if (selected) {
|
|
1189
|
-
setInput(selected.cmd + (selected.cmd === "/commit" ? " " : ""));
|
|
1190
|
-
setCmdIndex(0);
|
|
1191
|
-
setInputKey((k) => k + 1);
|
|
1192
|
-
}
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
// Login method picker navigation (second level — pick auth method)
|
|
1198
|
-
if (loginMethodPicker) {
|
|
1199
|
-
const methods = loginMethodPicker.methods;
|
|
1200
|
-
if (key.upArrow) {
|
|
1201
|
-
setLoginMethodIndex((prev: number) => (prev - 1 + methods.length) % methods.length);
|
|
1202
|
-
return;
|
|
1203
|
-
}
|
|
1204
|
-
if (key.downArrow) {
|
|
1205
|
-
setLoginMethodIndex((prev: number) => (prev + 1) % methods.length);
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
if (key.escape) {
|
|
1209
|
-
setLoginMethodPicker(null);
|
|
1210
|
-
setLoginPicker(true); // go back to provider picker
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
if (key.return) {
|
|
1214
|
-
const method = methods[loginMethodIndex];
|
|
1215
|
-
const providerId = loginMethodPicker.provider;
|
|
1216
|
-
setLoginMethodPicker(null);
|
|
1217
|
-
|
|
1218
|
-
if (method === "oauth" && providerId === "openrouter") {
|
|
1219
|
-
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
1220
|
-
setLoading(true);
|
|
1221
|
-
setSpinnerMsg("Waiting for authorization...");
|
|
1222
|
-
openRouterOAuth((msg: string) => addMsg("info", msg))
|
|
1223
|
-
.then(() => {
|
|
1224
|
-
addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
|
|
1225
|
-
setLoading(false);
|
|
1226
|
-
})
|
|
1227
|
-
.catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
1228
|
-
} else if (method === "setup-token") {
|
|
1229
|
-
addMsg("info", "Starting setup-token flow — browser will open...");
|
|
1230
|
-
setLoading(true);
|
|
1231
|
-
setSpinnerMsg("Waiting for Claude Code auth...");
|
|
1232
|
-
anthropicSetupToken((msg: string) => addMsg("info", msg))
|
|
1233
|
-
.then((cred) => { addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); setLoading(false); })
|
|
1234
|
-
.catch((err: any) => { addMsg("error", `Auth failed: ${err.message}`); setLoading(false); });
|
|
1235
|
-
} else if (method === "cached-token" && providerId === "openai") {
|
|
1236
|
-
const imported = importCodexToken((msg: string) => addMsg("info", msg));
|
|
1237
|
-
if (imported) { addMsg("info", `✅ Imported Codex credentials! (${imported.label})`); }
|
|
1238
|
-
else { addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first."); }
|
|
1239
|
-
} else if (method === "cached-token" && providerId === "qwen") {
|
|
1240
|
-
const imported = importQwenToken((msg: string) => addMsg("info", msg));
|
|
1241
|
-
if (imported) { addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`); }
|
|
1242
|
-
else { addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first."); }
|
|
1243
|
-
} else if (method === "device-flow") {
|
|
1244
|
-
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
1245
|
-
setLoading(true);
|
|
1246
|
-
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
1247
|
-
copilotDeviceFlow((msg: string) => addMsg("info", msg))
|
|
1248
|
-
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
1249
|
-
.catch((err: any) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
1250
|
-
} else if (method === "api-key") {
|
|
1251
|
-
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
1252
|
-
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${providerId} <your-key>\n Get key at: ${provider?.consoleUrl ?? "your provider's dashboard"}`);
|
|
1253
|
-
}
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// Login picker navigation (first level — pick provider)
|
|
1260
|
-
if (loginPicker) {
|
|
1261
|
-
const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
|
|
1262
|
-
if (key.upArrow) {
|
|
1263
|
-
setLoginPickerIndex((prev: number) => (prev - 1 + loginProviders.length) % loginProviders.length);
|
|
1264
|
-
return;
|
|
1265
|
-
}
|
|
1266
|
-
if (key.downArrow) {
|
|
1267
|
-
setLoginPickerIndex((prev: number) => (prev + 1) % loginProviders.length);
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
if (key.return) {
|
|
1271
|
-
const selected = loginProviders[loginPickerIndex];
|
|
1272
|
-
setLoginPicker(false);
|
|
1273
|
-
|
|
1274
|
-
// Get available methods for this provider (filter out 'none')
|
|
1275
|
-
const methods = selected.methods.filter((m) => m !== "none");
|
|
1276
|
-
|
|
1277
|
-
if (methods.length === 1) {
|
|
1278
|
-
// Only one method — execute it directly
|
|
1279
|
-
setLoginMethodPicker({ provider: selected.id, methods });
|
|
1280
|
-
setLoginMethodIndex(0);
|
|
1281
|
-
// Simulate Enter press on the single method
|
|
1282
|
-
if (methods[0] === "oauth" && selected.id === "openrouter") {
|
|
1283
|
-
setLoginMethodPicker(null);
|
|
1284
|
-
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
1285
|
-
setLoading(true);
|
|
1286
|
-
setSpinnerMsg("Waiting for authorization...");
|
|
1287
|
-
openRouterOAuth((msg: string) => addMsg("info", msg))
|
|
1288
|
-
.then(() => { addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); setLoading(false); })
|
|
1289
|
-
.catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
1290
|
-
} else if (methods[0] === "device-flow") {
|
|
1291
|
-
setLoginMethodPicker(null);
|
|
1292
|
-
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
1293
|
-
setLoading(true);
|
|
1294
|
-
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
1295
|
-
copilotDeviceFlow((msg: string) => addMsg("info", msg))
|
|
1296
|
-
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
1297
|
-
.catch((err: any) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
1298
|
-
} else if (methods[0] === "api-key") {
|
|
1299
|
-
setLoginMethodPicker(null);
|
|
1300
|
-
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? "your provider's dashboard"}`);
|
|
1301
|
-
}
|
|
1302
|
-
} else {
|
|
1303
|
-
// Multiple methods — show submenu
|
|
1304
|
-
setLoginMethodPicker({ provider: selected.id, methods });
|
|
1305
|
-
setLoginMethodIndex(0);
|
|
1306
|
-
}
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
if (key.escape) {
|
|
1310
|
-
setLoginPicker(false);
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
// Skills picker navigation
|
|
1317
|
-
if (skillsPicker) {
|
|
1318
|
-
if (skillsPicker === "menu") {
|
|
1319
|
-
const menuItems = ["browse", "installed", "create", "remove"];
|
|
1320
|
-
if (key.upArrow) {
|
|
1321
|
-
setSkillsPickerIndex((prev) => (prev - 1 + menuItems.length) % menuItems.length);
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
if (key.downArrow) {
|
|
1325
|
-
setSkillsPickerIndex((prev) => (prev + 1) % menuItems.length);
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
if (key.escape) {
|
|
1329
|
-
setSkillsPicker(null);
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
if (key.return) {
|
|
1333
|
-
const selected = menuItems[skillsPickerIndex];
|
|
1334
|
-
if (selected === "browse") {
|
|
1335
|
-
setSkillsPicker("browse");
|
|
1336
|
-
setSkillsPickerIndex(0);
|
|
1337
|
-
} else if (selected === "installed") {
|
|
1338
|
-
setSkillsPicker("installed");
|
|
1339
|
-
setSkillsPickerIndex(0);
|
|
1340
|
-
} else if (selected === "create") {
|
|
1341
|
-
setSkillsPicker(null);
|
|
1342
|
-
setInput("/skills create ");
|
|
1343
|
-
setInputKey((k) => k + 1);
|
|
1344
|
-
} else if (selected === "remove") {
|
|
1345
|
-
const installed = listInstalledSkills();
|
|
1346
|
-
if (installed.length === 0) {
|
|
1347
|
-
setSkillsPicker(null);
|
|
1348
|
-
addMsg("info", "No skills installed to remove.");
|
|
1349
|
-
} else {
|
|
1350
|
-
setSkillsPicker("remove");
|
|
1351
|
-
setSkillsPickerIndex(0);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
|
-
return;
|
|
1357
|
-
}
|
|
1358
|
-
if (skillsPicker === "browse") {
|
|
1359
|
-
const registry = getRegistrySkills();
|
|
1360
|
-
if (key.upArrow) {
|
|
1361
|
-
setSkillsPickerIndex((prev) => (prev - 1 + registry.length) % registry.length);
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
if (key.downArrow) {
|
|
1365
|
-
setSkillsPickerIndex((prev) => (prev + 1) % registry.length);
|
|
1366
|
-
return;
|
|
1367
|
-
}
|
|
1368
|
-
if (key.escape) {
|
|
1369
|
-
setSkillsPicker("menu");
|
|
1370
|
-
setSkillsPickerIndex(0);
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
if (key.return) {
|
|
1374
|
-
const selected = registry[skillsPickerIndex];
|
|
1375
|
-
if (selected) {
|
|
1376
|
-
const result = installSkill(selected.name);
|
|
1377
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
1378
|
-
}
|
|
1379
|
-
setSkillsPicker(null);
|
|
1380
|
-
return;
|
|
1381
|
-
}
|
|
1382
|
-
return;
|
|
1383
|
-
}
|
|
1384
|
-
if (skillsPicker === "installed") {
|
|
1385
|
-
const installed = listInstalledSkills();
|
|
1386
|
-
if (installed.length === 0) {
|
|
1387
|
-
setSkillsPicker("menu");
|
|
1388
|
-
setSkillsPickerIndex(0);
|
|
1389
|
-
addMsg("info", "No skills installed.");
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
if (key.upArrow) {
|
|
1393
|
-
setSkillsPickerIndex((prev) => (prev - 1 + installed.length) % installed.length);
|
|
1394
|
-
return;
|
|
1395
|
-
}
|
|
1396
|
-
if (key.downArrow) {
|
|
1397
|
-
setSkillsPickerIndex((prev) => (prev + 1) % installed.length);
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
if (key.escape) {
|
|
1401
|
-
setSkillsPicker("menu");
|
|
1402
|
-
setSkillsPickerIndex(0);
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
if (key.return) {
|
|
1406
|
-
// Toggle on/off for session
|
|
1407
|
-
const selected = installed[skillsPickerIndex];
|
|
1408
|
-
if (selected) {
|
|
1409
|
-
const isDisabled = sessionDisabledSkills.has(selected.name);
|
|
1410
|
-
if (isDisabled) {
|
|
1411
|
-
setSessionDisabledSkills((prev) => { const next = new Set(prev); next.delete(selected.name); return next; });
|
|
1412
|
-
if (agent) agent.enableSkill(selected.name);
|
|
1413
|
-
addMsg("info", `✅ Enabled: ${selected.name}`);
|
|
1414
|
-
} else {
|
|
1415
|
-
setSessionDisabledSkills((prev) => { const next = new Set(prev); next.add(selected.name); return next; });
|
|
1416
|
-
if (agent) agent.disableSkill(selected.name);
|
|
1417
|
-
addMsg("info", `✅ Disabled: ${selected.name} (session only)`);
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
setSkillsPicker(null);
|
|
1421
|
-
return;
|
|
1422
|
-
}
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
if (skillsPicker === "remove") {
|
|
1426
|
-
const installed = listInstalledSkills();
|
|
1427
|
-
if (installed.length === 0) {
|
|
1428
|
-
setSkillsPicker(null);
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
if (key.upArrow) {
|
|
1432
|
-
setSkillsPickerIndex((prev) => (prev - 1 + installed.length) % installed.length);
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
if (key.downArrow) {
|
|
1436
|
-
setSkillsPickerIndex((prev) => (prev + 1) % installed.length);
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
if (key.escape) {
|
|
1440
|
-
setSkillsPicker("menu");
|
|
1441
|
-
setSkillsPickerIndex(0);
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
|
-
if (key.return) {
|
|
1445
|
-
const selected = installed[skillsPickerIndex];
|
|
1446
|
-
if (selected) {
|
|
1447
|
-
const result = removeSkill(selected.name);
|
|
1448
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
1449
|
-
}
|
|
1450
|
-
setSkillsPicker(null);
|
|
1451
|
-
return;
|
|
1452
|
-
}
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
// ── Model picker ──
|
|
1459
|
-
if (modelPicker) {
|
|
1460
|
-
if (key.upArrow) {
|
|
1461
|
-
setModelPickerIndex((prev) => (prev - 1 + modelPicker.length) % modelPicker.length);
|
|
1462
|
-
return;
|
|
1463
|
-
}
|
|
1464
|
-
if (key.downArrow) {
|
|
1465
|
-
setModelPickerIndex((prev) => (prev + 1) % modelPicker.length);
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
if (key.escape) {
|
|
1469
|
-
setModelPicker(null);
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
if (key.return) {
|
|
1473
|
-
const selected = modelPicker[modelPickerIndex];
|
|
1474
|
-
if (selected && agent) {
|
|
1475
|
-
agent.switchModel(selected);
|
|
1476
|
-
setModelName(selected);
|
|
1477
|
-
addMsg("info", `✅ Switched to: ${selected}`);
|
|
1478
|
-
refreshConnectionBanner();
|
|
1479
|
-
}
|
|
1480
|
-
setModelPicker(null);
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
|
-
return;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
// ── Ollama delete picker ──
|
|
1487
|
-
if (ollamaDeletePicker) {
|
|
1488
|
-
if (key.upArrow) {
|
|
1489
|
-
setOllamaDeletePickerIndex((prev) => (prev - 1 + ollamaDeletePicker.models.length) % ollamaDeletePicker.models.length);
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
if (key.downArrow) {
|
|
1493
|
-
setOllamaDeletePickerIndex((prev) => (prev + 1) % ollamaDeletePicker.models.length);
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
if (key.escape) {
|
|
1497
|
-
setOllamaDeletePicker(null);
|
|
1498
|
-
return;
|
|
1499
|
-
}
|
|
1500
|
-
if (key.return) {
|
|
1501
|
-
const selected = ollamaDeletePicker.models[ollamaDeletePickerIndex];
|
|
1502
|
-
if (selected) {
|
|
1503
|
-
setOllamaDeletePicker(null);
|
|
1504
|
-
setOllamaDeleteConfirm({ model: selected.name, size: selected.size });
|
|
1505
|
-
}
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// ── Ollama pull picker ──
|
|
1512
|
-
if (ollamaPullPicker) {
|
|
1513
|
-
const pullModels = [
|
|
1514
|
-
{ id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
|
|
1515
|
-
{ id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
|
|
1516
|
-
{ id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
|
|
1517
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
|
|
1518
|
-
{ id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
|
|
1519
|
-
{ id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
|
|
1520
|
-
{ id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
|
|
1521
|
-
];
|
|
1522
|
-
if (key.upArrow) {
|
|
1523
|
-
setOllamaPullPickerIndex((prev) => (prev - 1 + pullModels.length) % pullModels.length);
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
if (key.downArrow) {
|
|
1527
|
-
setOllamaPullPickerIndex((prev) => (prev + 1) % pullModels.length);
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
if (key.escape) {
|
|
1531
|
-
setOllamaPullPicker(false);
|
|
1532
|
-
return;
|
|
1533
|
-
}
|
|
1534
|
-
if (key.return) {
|
|
1535
|
-
const selected = pullModels[ollamaPullPickerIndex];
|
|
1536
|
-
if (selected) {
|
|
1537
|
-
setOllamaPullPicker(false);
|
|
1538
|
-
// Trigger the pull
|
|
1539
|
-
setInput(`/ollama pull ${selected.id}`);
|
|
1540
|
-
setInputKey((k) => k + 1);
|
|
1541
|
-
// Submit it
|
|
1542
|
-
setTimeout(() => {
|
|
1543
|
-
const submitInput = `/ollama pull ${selected.id}`;
|
|
1544
|
-
setInput("");
|
|
1545
|
-
handleSubmit(submitInput);
|
|
1546
|
-
}, 50);
|
|
1547
|
-
}
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
return;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
// ── Ollama delete confirmation ──
|
|
1554
|
-
if (ollamaDeleteConfirm) {
|
|
1555
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1556
|
-
const model = ollamaDeleteConfirm.model;
|
|
1557
|
-
setOllamaDeleteConfirm(null);
|
|
1558
|
-
const result = deleteModel(model);
|
|
1559
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
|
|
1560
|
-
return;
|
|
1561
|
-
}
|
|
1562
|
-
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
1563
|
-
setOllamaDeleteConfirm(null);
|
|
1564
|
-
addMsg("info", "Delete cancelled.");
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
return;
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
// ── Ollama exit prompt ──
|
|
1571
|
-
if (ollamaExitPrompt) {
|
|
1572
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1573
|
-
setOllamaExitPrompt(false);
|
|
1574
|
-
stopOllama().then(() => exit());
|
|
1575
|
-
return;
|
|
1576
|
-
}
|
|
1577
|
-
if (inputChar === "n" || inputChar === "N") {
|
|
1578
|
-
setOllamaExitPrompt(false);
|
|
1579
|
-
exit();
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
if (inputChar === "a" || inputChar === "A") {
|
|
1583
|
-
setOllamaExitPrompt(false);
|
|
1584
|
-
saveConfig({ defaults: { ...loadConfig().defaults, stopOllamaOnExit: true } });
|
|
1585
|
-
addMsg("info", "Saved preference: always stop Ollama on exit.");
|
|
1586
|
-
stopOllama().then(() => exit());
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
if (key.escape) {
|
|
1590
|
-
setOllamaExitPrompt(false);
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
// ── Setup Wizard Navigation ──
|
|
1597
|
-
if (wizardScreen) {
|
|
1598
|
-
if (wizardScreen === "connection") {
|
|
1599
|
-
const items = ["local", "openrouter", "apikey", "existing"];
|
|
1600
|
-
if (key.upArrow) {
|
|
1601
|
-
setWizardIndex((prev) => (prev - 1 + items.length) % items.length);
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
if (key.downArrow) {
|
|
1605
|
-
setWizardIndex((prev) => (prev + 1) % items.length);
|
|
1606
|
-
return;
|
|
1607
|
-
}
|
|
1608
|
-
if (key.escape) {
|
|
1609
|
-
setWizardScreen(null);
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
if (key.return) {
|
|
1613
|
-
const selected = items[wizardIndex];
|
|
1614
|
-
if (selected === "local") {
|
|
1615
|
-
// Scan hardware and show model picker (use llmfit if available)
|
|
1616
|
-
const hw = detectHardware();
|
|
1617
|
-
setWizardHardware(hw);
|
|
1618
|
-
const { models: recs } = getRecommendationsWithLlmfit(hw);
|
|
1619
|
-
setWizardModels(recs.filter(m => m.fit !== "skip"));
|
|
1620
|
-
setWizardScreen("models");
|
|
1621
|
-
setWizardIndex(0);
|
|
1622
|
-
} else if (selected === "openrouter") {
|
|
1623
|
-
setWizardScreen(null);
|
|
1624
|
-
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
1625
|
-
setLoading(true);
|
|
1626
|
-
setSpinnerMsg("Waiting for authorization...");
|
|
1627
|
-
openRouterOAuth((msg: string) => addMsg("info", msg))
|
|
1628
|
-
.then(() => {
|
|
1629
|
-
addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
|
|
1630
|
-
setLoading(false);
|
|
1631
|
-
})
|
|
1632
|
-
.catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
1633
|
-
} else if (selected === "apikey") {
|
|
1634
|
-
setWizardScreen(null);
|
|
1635
|
-
setLoginPicker(true);
|
|
1636
|
-
setLoginPickerIndex(0);
|
|
1637
|
-
} else if (selected === "existing") {
|
|
1638
|
-
setWizardScreen(null);
|
|
1639
|
-
addMsg("info", "Start your LLM server, then type /connect to retry.");
|
|
1640
|
-
}
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
if (wizardScreen === "models") {
|
|
1647
|
-
const models = wizardModels;
|
|
1648
|
-
if (key.upArrow) {
|
|
1649
|
-
setWizardIndex((prev) => (prev - 1 + models.length) % models.length);
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
if (key.downArrow) {
|
|
1653
|
-
setWizardIndex((prev) => (prev + 1) % models.length);
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
if (key.escape) {
|
|
1657
|
-
setWizardScreen("connection");
|
|
1658
|
-
setWizardIndex(0);
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
if (key.return) {
|
|
1662
|
-
const selected = models[wizardIndex];
|
|
1663
|
-
if (selected) {
|
|
1664
|
-
setWizardSelectedModel(selected);
|
|
1665
|
-
// Check if Ollama is installed
|
|
1666
|
-
if (!isOllamaInstalled()) {
|
|
1667
|
-
setWizardScreen("install-ollama");
|
|
1668
|
-
} else {
|
|
1669
|
-
// Start pulling the model
|
|
1670
|
-
setWizardScreen("pulling");
|
|
1671
|
-
setWizardPullProgress({ status: "starting", percent: 0 });
|
|
1672
|
-
setWizardPullError(null);
|
|
1673
|
-
|
|
1674
|
-
(async () => {
|
|
1675
|
-
try {
|
|
1676
|
-
// Ensure ollama is running
|
|
1677
|
-
const running = await isOllamaRunning();
|
|
1678
|
-
if (!running) {
|
|
1679
|
-
setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
|
|
1680
|
-
startOllama();
|
|
1681
|
-
// Wait for it to come up
|
|
1682
|
-
for (let i = 0; i < 15; i++) {
|
|
1683
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1684
|
-
if (await isOllamaRunning()) break;
|
|
1685
|
-
}
|
|
1686
|
-
if (!(await isOllamaRunning())) {
|
|
1687
|
-
setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
|
|
1688
|
-
return;
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
await pullModel(selected.ollamaId, (p) => {
|
|
1693
|
-
setWizardPullProgress(p);
|
|
1694
|
-
});
|
|
1695
|
-
|
|
1696
|
-
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1697
|
-
|
|
1698
|
-
// Wait briefly then connect
|
|
1699
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1700
|
-
setWizardScreen(null);
|
|
1701
|
-
setWizardPullProgress(null);
|
|
1702
|
-
setWizardSelectedModel(null);
|
|
1703
|
-
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1704
|
-
await connectToProvider(true);
|
|
1705
|
-
} catch (err: any) {
|
|
1706
|
-
setWizardPullError(err.message);
|
|
1707
|
-
}
|
|
1708
|
-
})();
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
if (wizardScreen === "install-ollama") {
|
|
1717
|
-
if (key.escape) {
|
|
1718
|
-
setWizardScreen("models");
|
|
1719
|
-
setWizardIndex(0);
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
if (key.return) {
|
|
1723
|
-
// Auto-install Ollama if not present
|
|
1724
|
-
if (!isOllamaInstalled()) {
|
|
1725
|
-
setLoading(true);
|
|
1726
|
-
setSpinnerMsg("Installing Ollama... this may take a minute");
|
|
1727
|
-
|
|
1728
|
-
// Run install async so the UI can update
|
|
1729
|
-
const installCmd = getOllamaInstallCommand(wizardHardware?.os ?? "linux");
|
|
1730
|
-
(async () => {
|
|
1731
|
-
try {
|
|
1732
|
-
const { exec } = _require("child_process");
|
|
1733
|
-
await new Promise<void>((resolve, reject) => {
|
|
1734
|
-
exec(installCmd, { timeout: 180000 }, (err: any, _stdout: string, stderr: string) => {
|
|
1735
|
-
if (err) reject(new Error(stderr || err.message));
|
|
1736
|
-
else resolve();
|
|
1737
|
-
});
|
|
1738
|
-
});
|
|
1739
|
-
addMsg("info", "✅ Ollama installed! Proceeding to model download...");
|
|
1740
|
-
setLoading(false);
|
|
1741
|
-
// Small delay for PATH to update on Windows
|
|
1742
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
1743
|
-
// Go back to models screen so user can pick and it'll proceed to pull
|
|
1744
|
-
setWizardScreen("models");
|
|
1745
|
-
} catch (e: any) {
|
|
1746
|
-
addMsg("error", `Install failed: ${e.message}`);
|
|
1747
|
-
addMsg("info", `Try manually in a separate terminal: ${installCmd}`);
|
|
1748
|
-
setLoading(false);
|
|
1749
|
-
setWizardScreen("install-ollama");
|
|
1750
|
-
}
|
|
1751
|
-
})();
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
|
-
// Ollama already installed — proceed to pull
|
|
1755
|
-
{
|
|
1756
|
-
const selected = wizardSelectedModel;
|
|
1757
|
-
if (selected) {
|
|
1758
|
-
setWizardScreen("pulling");
|
|
1759
|
-
setWizardPullProgress({ status: "starting", percent: 0 });
|
|
1760
|
-
setWizardPullError(null);
|
|
1761
|
-
|
|
1762
|
-
(async () => {
|
|
1763
|
-
try {
|
|
1764
|
-
const running = await isOllamaRunning();
|
|
1765
|
-
if (!running) {
|
|
1766
|
-
setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
|
|
1767
|
-
startOllama();
|
|
1768
|
-
for (let i = 0; i < 15; i++) {
|
|
1769
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1770
|
-
if (await isOllamaRunning()) break;
|
|
1771
|
-
}
|
|
1772
|
-
if (!(await isOllamaRunning())) {
|
|
1773
|
-
setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
|
|
1774
|
-
return;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
|
|
1778
|
-
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1779
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1780
|
-
setWizardScreen(null);
|
|
1781
|
-
setWizardPullProgress(null);
|
|
1782
|
-
setWizardSelectedModel(null);
|
|
1783
|
-
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1784
|
-
await connectToProvider(true);
|
|
1785
|
-
} catch (err: any) {
|
|
1786
|
-
setWizardPullError(err.message);
|
|
1787
|
-
}
|
|
1788
|
-
})();
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
if (wizardScreen === "pulling") {
|
|
1797
|
-
// Allow retry on error
|
|
1798
|
-
if (wizardPullError && key.return) {
|
|
1799
|
-
const selected = wizardSelectedModel;
|
|
1800
|
-
if (selected) {
|
|
1801
|
-
setWizardPullError(null);
|
|
1802
|
-
setWizardPullProgress({ status: "retrying", percent: 0 });
|
|
1803
|
-
(async () => {
|
|
1804
|
-
try {
|
|
1805
|
-
const running = await isOllamaRunning();
|
|
1806
|
-
if (!running) {
|
|
1807
|
-
startOllama();
|
|
1808
|
-
for (let i = 0; i < 15; i++) {
|
|
1809
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1810
|
-
if (await isOllamaRunning()) break;
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
|
|
1814
|
-
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1815
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1816
|
-
setWizardScreen(null);
|
|
1817
|
-
setWizardPullProgress(null);
|
|
1818
|
-
setWizardSelectedModel(null);
|
|
1819
|
-
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1820
|
-
await connectToProvider(true);
|
|
1821
|
-
} catch (err: any) {
|
|
1822
|
-
setWizardPullError(err.message);
|
|
1823
|
-
}
|
|
1824
|
-
})();
|
|
1825
|
-
}
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
if (wizardPullError && key.escape) {
|
|
1829
|
-
setWizardScreen("models");
|
|
1830
|
-
setWizardIndex(0);
|
|
1831
|
-
setWizardPullError(null);
|
|
1832
|
-
setWizardPullProgress(null);
|
|
1833
|
-
return;
|
|
1834
|
-
}
|
|
1835
|
-
return; // Ignore keys while pulling
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
// Theme picker navigation
|
|
1842
|
-
if (themePicker) {
|
|
1843
|
-
const themeKeys = listThemes();
|
|
1844
|
-
if (key.upArrow) {
|
|
1845
|
-
setThemePickerIndex((prev) => (prev - 1 + themeKeys.length) % themeKeys.length);
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
if (key.downArrow) {
|
|
1849
|
-
setThemePickerIndex((prev) => (prev + 1) % themeKeys.length);
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
if (key.return) {
|
|
1853
|
-
const selected = themeKeys[themePickerIndex];
|
|
1854
|
-
setTheme(getTheme(selected));
|
|
1855
|
-
setThemePicker(false);
|
|
1856
|
-
addMsg("info", `✅ Switched to theme: ${THEMES[selected].name}`);
|
|
1857
|
-
return;
|
|
1858
|
-
}
|
|
1859
|
-
if (key.escape) {
|
|
1860
|
-
setThemePicker(false);
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
return;
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// Session picker navigation
|
|
1867
|
-
if (sessionPicker) {
|
|
1868
|
-
if (key.upArrow) {
|
|
1869
|
-
setSessionPickerIndex((prev) => (prev - 1 + sessionPicker.length) % sessionPicker.length);
|
|
1870
|
-
return;
|
|
1871
|
-
}
|
|
1872
|
-
if (key.downArrow) {
|
|
1873
|
-
setSessionPickerIndex((prev) => (prev + 1) % sessionPicker.length);
|
|
1874
|
-
return;
|
|
1875
|
-
}
|
|
1876
|
-
if (key.return) {
|
|
1877
|
-
const selected = sessionPicker[sessionPickerIndex];
|
|
1878
|
-
if (selected && agent) {
|
|
1879
|
-
const session = getSession(selected.id);
|
|
1880
|
-
if (session) {
|
|
1881
|
-
agent.resume(selected.id).then(() => {
|
|
1882
|
-
const dir = session.cwd.split("/").pop() || session.cwd;
|
|
1883
|
-
// Find last user message for context
|
|
1884
|
-
const msgs = loadMessages(selected.id);
|
|
1885
|
-
const lastUserMsg = [...msgs].reverse().find(m => m.role === "user");
|
|
1886
|
-
const lastText = lastUserMsg && typeof lastUserMsg.content === "string"
|
|
1887
|
-
? lastUserMsg.content.slice(0, 80) + (lastUserMsg.content.length > 80 ? "..." : "")
|
|
1888
|
-
: null;
|
|
1889
|
-
let info = `✅ Resumed session ${selected.id} (${dir}/, ${session.message_count} messages)`;
|
|
1890
|
-
if (lastText) info += `\n Last: "${lastText}"`;
|
|
1891
|
-
addMsg("info", info);
|
|
1892
|
-
}).catch((e: any) => {
|
|
1893
|
-
addMsg("error", `Failed to resume: ${e.message}`);
|
|
1894
|
-
});
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
setSessionPicker(null);
|
|
1898
|
-
setSessionPickerIndex(0);
|
|
1899
|
-
return;
|
|
1900
|
-
}
|
|
1901
|
-
if (key.escape) {
|
|
1902
|
-
setSessionPicker(null);
|
|
1903
|
-
setSessionPickerIndex(0);
|
|
1904
|
-
addMsg("info", "Resume cancelled.");
|
|
1905
|
-
return;
|
|
1906
|
-
}
|
|
1907
|
-
return; // Ignore other keys during session picker
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
// Delete session confirmation (y/n)
|
|
1911
|
-
if (deleteSessionConfirm) {
|
|
1912
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1913
|
-
const deleted = deleteSession(deleteSessionConfirm.id);
|
|
1914
|
-
if (deleted) {
|
|
1915
|
-
addMsg("info", `✅ Deleted session ${deleteSessionConfirm.id}`);
|
|
1916
|
-
} else {
|
|
1917
|
-
addMsg("error", `Failed to delete session ${deleteSessionConfirm.id}`);
|
|
1918
|
-
}
|
|
1919
|
-
setDeleteSessionConfirm(null);
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
1923
|
-
addMsg("info", "Delete cancelled.");
|
|
1924
|
-
setDeleteSessionConfirm(null);
|
|
1925
|
-
return;
|
|
1926
|
-
}
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// Delete session picker navigation
|
|
1931
|
-
if (deleteSessionPicker) {
|
|
1932
|
-
if (key.upArrow) {
|
|
1933
|
-
setDeleteSessionPickerIndex((prev) => (prev - 1 + deleteSessionPicker.length) % deleteSessionPicker.length);
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
if (key.downArrow) {
|
|
1937
|
-
setDeleteSessionPickerIndex((prev) => (prev + 1) % deleteSessionPicker.length);
|
|
1938
|
-
return;
|
|
1939
|
-
}
|
|
1940
|
-
if (key.return) {
|
|
1941
|
-
const selected = deleteSessionPicker[deleteSessionPickerIndex];
|
|
1942
|
-
if (selected) {
|
|
1943
|
-
setDeleteSessionPicker(null);
|
|
1944
|
-
setDeleteSessionPickerIndex(0);
|
|
1945
|
-
setDeleteSessionConfirm(selected);
|
|
1946
|
-
}
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
if (key.escape) {
|
|
1950
|
-
setDeleteSessionPicker(null);
|
|
1951
|
-
setDeleteSessionPickerIndex(0);
|
|
1952
|
-
addMsg("info", "Delete cancelled.");
|
|
1953
|
-
return;
|
|
1954
|
-
}
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// Backspace with empty input → remove last paste chunk
|
|
1959
|
-
if (key.backspace || key.delete) {
|
|
1960
|
-
if (input === "" && pastedChunksRef.current.length > 0) {
|
|
1961
|
-
setPastedChunks((prev) => prev.slice(0, -1));
|
|
1962
|
-
return;
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
// Handle approval prompts
|
|
1967
|
-
if (approval) {
|
|
1968
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1969
|
-
const r = approval.resolve;
|
|
1970
|
-
setApproval(null);
|
|
1971
|
-
setLoading(true);
|
|
1972
|
-
setSpinnerMsg("Executing...");
|
|
1973
|
-
r("yes");
|
|
1974
|
-
return;
|
|
1975
|
-
}
|
|
1976
|
-
if (inputChar === "n" || inputChar === "N") {
|
|
1977
|
-
const r = approval.resolve;
|
|
1978
|
-
setApproval(null);
|
|
1979
|
-
addMsg("info", "✗ Denied");
|
|
1980
|
-
r("no");
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
1983
|
-
if (inputChar === "a" || inputChar === "A") {
|
|
1984
|
-
const r = approval.resolve;
|
|
1985
|
-
setApproval(null);
|
|
1986
|
-
setLoading(true);
|
|
1987
|
-
setSpinnerMsg("Executing...");
|
|
1988
|
-
addMsg("info", `✔ Always allow ${approval.tool} for this session`);
|
|
1989
|
-
r("always");
|
|
1990
|
-
return;
|
|
1991
|
-
}
|
|
1992
|
-
return; // Ignore other keys during approval
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
if (key.ctrl && inputChar === "c") {
|
|
1996
|
-
if (ctrlCPressed) {
|
|
1997
|
-
// Force quit on second Ctrl+C — don't block
|
|
1998
|
-
const config = loadConfig();
|
|
1999
|
-
if (config.defaults.stopOllamaOnExit) {
|
|
2000
|
-
stopOllama().finally(() => exit());
|
|
2001
|
-
} else {
|
|
2002
|
-
exit();
|
|
2003
|
-
}
|
|
2004
|
-
} else {
|
|
2005
|
-
setCtrlCPressed(true);
|
|
2006
|
-
addMsg("info", "Press Ctrl+C again to exit.");
|
|
2007
|
-
setTimeout(() => setCtrlCPressed(false), 3000);
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
});
|
|
2011
|
-
|
|
2012
|
-
// CODE banner lines
|
|
2013
|
-
const codeLines = [
|
|
2014
|
-
" _(`-') (`-') _ ",
|
|
2015
|
-
" _ .-> ( (OO ).-> ( OO).-/ ",
|
|
2016
|
-
" \\-,-----.(`-')----. \\ .'_ (,------. ",
|
|
2017
|
-
" | .--./( OO).-. ''`'-..__) | .---' ",
|
|
2018
|
-
" /_) (`-')( _) | | || | ' |(| '--. ",
|
|
2019
|
-
" || |OO ) \\| |)| || | / : | .--' ",
|
|
2020
|
-
"(_' '--'\\ ' '-' '| '-' / | `---. ",
|
|
2021
|
-
" `-----' `-----' `------' `------' ",
|
|
2022
|
-
];
|
|
2023
|
-
const maxxingLines = [
|
|
2024
|
-
"<-. (`-') (`-') _ (`-') (`-') _ <-. (`-')_ ",
|
|
2025
|
-
" \\(OO )_ (OO ).-/ (OO )_.-> (OO )_.-> (_) \\( OO) ) .-> ",
|
|
2026
|
-
",--./ ,-.) / ,---. (_| \\_)--. (_| \\_)--.,-(`-'),--./ ,--/ ,---(`-') ",
|
|
2027
|
-
"| `.' | | \\ /`.\\ \\ `.' / \\ `.' / | ( OO)| \\ | | ' .-(OO ) ",
|
|
2028
|
-
"| |'.'| | '-'|_.' | \\ .') \\ .') | | )| . '| |)| | .-, \\ ",
|
|
2029
|
-
"| | | |(| .-. | .' \\ .' \\ (| |_/ | |\\ | | | '.(_/ ",
|
|
2030
|
-
"| | | | | | | | / .'. \\ / .'. \\ | |'->| | \\ | | '-' | ",
|
|
2031
|
-
"`--' `--' `--' `--'`--' '--'`--' '--'`--' `--' `--' `-----' ",
|
|
2032
|
-
];
|
|
2033
|
-
|
|
2034
|
-
return (
|
|
2035
|
-
<Box flexDirection="column">
|
|
2036
|
-
{/* ═══ BANNER BOX ═══ */}
|
|
2037
|
-
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.border} paddingX={1}>
|
|
2038
|
-
{codeLines.map((line, i) => (
|
|
2039
|
-
<Text key={`c${i}`} color={theme.colors.primary}>{line}</Text>
|
|
2040
|
-
))}
|
|
2041
|
-
{maxxingLines.map((line, i) => (
|
|
2042
|
-
<Text key={`m${i}`} color={theme.colors.secondary}>{line}</Text>
|
|
2043
|
-
))}
|
|
2044
|
-
<Text>
|
|
2045
|
-
<Text color={theme.colors.muted}>{" v" + VERSION}</Text>
|
|
2046
|
-
{" "}<Text color={theme.colors.primary}>💪</Text>
|
|
2047
|
-
{" "}<Text dimColor>your code. your model. no excuses.</Text>
|
|
2048
|
-
</Text>
|
|
2049
|
-
<Text dimColor>{" Type "}<Text color={theme.colors.muted}>/help</Text>{" for commands · "}<Text color={theme.colors.muted}>Ctrl+C</Text>{" twice to exit"}</Text>
|
|
2050
|
-
</Box>
|
|
2051
|
-
|
|
2052
|
-
{/* ═══ CONNECTION INFO BOX ═══ */}
|
|
2053
|
-
{connectionInfo.length > 0 && (
|
|
2054
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.muted} paddingX={1} marginBottom={1}>
|
|
2055
|
-
{connectionInfo.map((line, i) => (
|
|
2056
|
-
<Text key={i} color={line.startsWith("✔") ? theme.colors.primary : line.startsWith("✗") ? theme.colors.error : theme.colors.muted}>{line}</Text>
|
|
2057
|
-
))}
|
|
2058
|
-
</Box>
|
|
2059
|
-
)}
|
|
2060
|
-
|
|
2061
|
-
{/* ═══ CHAT MESSAGES ═══ */}
|
|
2062
|
-
{messages.map((msg) => {
|
|
2063
|
-
switch (msg.type) {
|
|
2064
|
-
case "user":
|
|
2065
|
-
return (
|
|
2066
|
-
<Box key={msg.id} marginTop={1}>
|
|
2067
|
-
<Text color={theme.colors.userInput}>{" > "}{msg.text}</Text>
|
|
2068
|
-
</Box>
|
|
2069
|
-
);
|
|
2070
|
-
case "response":
|
|
2071
|
-
return (
|
|
2072
|
-
<Box key={msg.id} flexDirection="column" marginLeft={2} marginBottom={1}>
|
|
2073
|
-
{msg.text.split("\n").map((l, i) => (
|
|
2074
|
-
<Text key={i} wrap="wrap">
|
|
2075
|
-
{i === 0 ? <Text color={theme.colors.response}>● </Text> : <Text> </Text>}
|
|
2076
|
-
{l.startsWith("```") ? <Text color={theme.colors.muted}>{l}</Text> :
|
|
2077
|
-
l.startsWith("# ") || l.startsWith("## ") ? <Text bold color={theme.colors.secondary}>{l}</Text> :
|
|
2078
|
-
l.startsWith("**") ? <Text bold>{l}</Text> :
|
|
2079
|
-
<Text>{l}</Text>}
|
|
2080
|
-
</Text>
|
|
2081
|
-
))}
|
|
2082
|
-
</Box>
|
|
2083
|
-
);
|
|
2084
|
-
case "tool":
|
|
2085
|
-
return (
|
|
2086
|
-
<Box key={msg.id}>
|
|
2087
|
-
<Text><Text color={theme.colors.response}> ● </Text><Text bold color={theme.colors.tool}>{msg.text}</Text></Text>
|
|
2088
|
-
</Box>
|
|
2089
|
-
);
|
|
2090
|
-
case "tool-result":
|
|
2091
|
-
return <Text key={msg.id} color={theme.colors.toolResult}> {msg.text}</Text>;
|
|
2092
|
-
case "error":
|
|
2093
|
-
return <Text key={msg.id} color={theme.colors.error}> {msg.text}</Text>;
|
|
2094
|
-
case "info":
|
|
2095
|
-
return <Text key={msg.id} color={theme.colors.muted}> {msg.text}</Text>;
|
|
2096
|
-
default:
|
|
2097
|
-
return <Text key={msg.id}>{msg.text}</Text>;
|
|
2098
|
-
}
|
|
2099
|
-
})}
|
|
2100
|
-
|
|
2101
|
-
{/* ═══ SPINNER ═══ */}
|
|
2102
|
-
{loading && !approval && !streaming && <NeonSpinner message={spinnerMsg} colors={theme.colors} />}
|
|
2103
|
-
{streaming && !loading && <StreamingIndicator colors={theme.colors} />}
|
|
2104
|
-
|
|
2105
|
-
{/* ═══ APPROVAL PROMPT ═══ */}
|
|
2106
|
-
{approval && (
|
|
2107
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginTop={1}>
|
|
2108
|
-
<Text bold color={theme.colors.warning}>⚠ Approve {approval.tool}?</Text>
|
|
2109
|
-
{approval.tool === "write_file" && approval.args.path ? (
|
|
2110
|
-
<Text color={theme.colors.muted}>{" 📄 "}{String(approval.args.path)}</Text>
|
|
2111
|
-
) : null}
|
|
2112
|
-
{approval.tool === "write_file" && approval.args.content ? (
|
|
2113
|
-
<Text color={theme.colors.muted}>{" "}{String(approval.args.content).split("\n").length}{" lines, "}{String(approval.args.content).length}{"B"}</Text>
|
|
2114
|
-
) : null}
|
|
2115
|
-
{approval.diff ? (
|
|
2116
|
-
<Box flexDirection="column" marginTop={0} marginLeft={2}>
|
|
2117
|
-
{approval.diff.split("\n").slice(0, 40).map((line, i) => (
|
|
2118
|
-
<Text key={i} color={
|
|
2119
|
-
line.startsWith("+") ? theme.colors.success :
|
|
2120
|
-
line.startsWith("-") ? theme.colors.error :
|
|
2121
|
-
line.startsWith("@@") ? theme.colors.primary :
|
|
2122
|
-
theme.colors.muted
|
|
2123
|
-
}>{line}</Text>
|
|
2124
|
-
))}
|
|
2125
|
-
{approval.diff.split("\n").length > 40 ? (
|
|
2126
|
-
<Text color={theme.colors.muted}>... ({approval.diff.split("\n").length - 40} more lines)</Text>
|
|
2127
|
-
) : null}
|
|
2128
|
-
</Box>
|
|
2129
|
-
) : null}
|
|
2130
|
-
{approval.tool === "run_command" && approval.args.command ? (
|
|
2131
|
-
<Text color={theme.colors.muted}>{" $ "}{String(approval.args.command)}</Text>
|
|
2132
|
-
) : null}
|
|
2133
|
-
<Text>
|
|
2134
|
-
<Text color={theme.colors.success} bold> [y]</Text><Text>es </Text>
|
|
2135
|
-
<Text color={theme.colors.error} bold>[n]</Text><Text>o </Text>
|
|
2136
|
-
<Text color={theme.colors.primary} bold>[a]</Text><Text>lways</Text>
|
|
2137
|
-
</Text>
|
|
2138
|
-
</Box>
|
|
2139
|
-
)}
|
|
2140
|
-
|
|
2141
|
-
{/* ═══ LOGIN PICKER ═══ */}
|
|
2142
|
-
{loginPicker && (
|
|
2143
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2144
|
-
<Text bold color={theme.colors.secondary}>💪 Choose a provider:</Text>
|
|
2145
|
-
{PROVIDERS.filter((p) => p.id !== "local").map((p, i) => (
|
|
2146
|
-
<Text key={p.id}>
|
|
2147
|
-
{i === loginPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2148
|
-
<Text color={i === loginPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{p.name}</Text>
|
|
2149
|
-
<Text color={theme.colors.muted}>{" — "}{p.description}</Text>
|
|
2150
|
-
{getCredentials().some((c) => c.provider === p.id) ? <Text color={theme.colors.success}> ✓</Text> : null}
|
|
2151
|
-
</Text>
|
|
2152
|
-
))}
|
|
2153
|
-
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
2154
|
-
</Box>
|
|
2155
|
-
)}
|
|
2156
|
-
|
|
2157
|
-
{/* ═══ LOGIN METHOD PICKER ═══ */}
|
|
2158
|
-
{loginMethodPicker && (
|
|
2159
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2160
|
-
<Text bold color={theme.colors.secondary}>How do you want to authenticate?</Text>
|
|
2161
|
-
{loginMethodPicker.methods.map((method, i) => {
|
|
2162
|
-
const labels: Record<string, string> = {
|
|
2163
|
-
"oauth": "🌐 Browser login (OAuth)",
|
|
2164
|
-
"setup-token": "🔑 Link subscription (via Claude Code CLI)",
|
|
2165
|
-
"cached-token": "📦 Import from existing CLI",
|
|
2166
|
-
"api-key": "🔒 Enter API key manually",
|
|
2167
|
-
"device-flow": "📱 Device flow (GitHub)",
|
|
2168
|
-
};
|
|
2169
|
-
return (
|
|
2170
|
-
<Text key={method}>
|
|
2171
|
-
{i === loginMethodIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2172
|
-
<Text color={i === loginMethodIndex ? theme.colors.suggestion : theme.colors.primary} bold>{labels[method] ?? method}</Text>
|
|
2173
|
-
</Text>
|
|
2174
|
-
);
|
|
2175
|
-
})}
|
|
2176
|
-
<Text dimColor>{" ↑↓ navigate · Enter select · Esc back"}</Text>
|
|
2177
|
-
</Box>
|
|
2178
|
-
)}
|
|
2179
|
-
|
|
2180
|
-
{/* ═══ SKILLS PICKER ═══ */}
|
|
2181
|
-
{skillsPicker === "menu" && (
|
|
2182
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2183
|
-
<Text bold color={theme.colors.secondary}>Skills:</Text>
|
|
2184
|
-
{[
|
|
2185
|
-
{ key: "browse", label: "Browse & Install", icon: "📦" },
|
|
2186
|
-
{ key: "installed", label: "Installed Skills", icon: "📋" },
|
|
2187
|
-
{ key: "create", label: "Create Custom Skill", icon: "➕" },
|
|
2188
|
-
{ key: "remove", label: "Remove Skill", icon: "🗑️" },
|
|
2189
|
-
].map((item, i) => (
|
|
2190
|
-
<Text key={item.key}>
|
|
2191
|
-
{i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2192
|
-
<Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{item.icon} {item.label}</Text>
|
|
2193
|
-
</Text>
|
|
2194
|
-
))}
|
|
2195
|
-
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
2196
|
-
</Box>
|
|
2197
|
-
)}
|
|
2198
|
-
{skillsPicker === "browse" && (() => {
|
|
2199
|
-
const registry = getRegistrySkills();
|
|
2200
|
-
const installed = listInstalledSkills().map((s) => s.name);
|
|
2201
|
-
return (
|
|
2202
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2203
|
-
<Text bold color={theme.colors.secondary}>Browse Skills Registry:</Text>
|
|
2204
|
-
{registry.map((s, i) => (
|
|
2205
|
-
<Text key={s.name}>
|
|
2206
|
-
{i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2207
|
-
<Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{s.name}</Text>
|
|
2208
|
-
<Text color={theme.colors.muted}>{" — "}{s.description}</Text>
|
|
2209
|
-
{installed.includes(s.name) ? <Text color={theme.colors.success}> ✓</Text> : null}
|
|
2210
|
-
</Text>
|
|
2211
|
-
))}
|
|
2212
|
-
<Text dimColor>{" ↑↓ navigate · Enter install · Esc back"}</Text>
|
|
2213
|
-
</Box>
|
|
2214
|
-
);
|
|
2215
|
-
})()}
|
|
2216
|
-
{skillsPicker === "installed" && (() => {
|
|
2217
|
-
const installed = listInstalledSkills();
|
|
2218
|
-
const active = getActiveSkills(process.cwd(), sessionDisabledSkills);
|
|
2219
|
-
return (
|
|
2220
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2221
|
-
<Text bold color={theme.colors.secondary}>Installed Skills:</Text>
|
|
2222
|
-
{installed.length === 0 ? (
|
|
2223
|
-
<Text color={theme.colors.muted}> No skills installed. Use Browse & Install.</Text>
|
|
2224
|
-
) : installed.map((s, i) => (
|
|
2225
|
-
<Text key={s.name}>
|
|
2226
|
-
{i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2227
|
-
<Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{s.name}</Text>
|
|
2228
|
-
<Text color={theme.colors.muted}>{" — "}{s.description}</Text>
|
|
2229
|
-
{active.includes(s.name) ? <Text color={theme.colors.success}> (on)</Text> : <Text color={theme.colors.muted}> (off)</Text>}
|
|
2230
|
-
</Text>
|
|
2231
|
-
))}
|
|
2232
|
-
<Text dimColor>{" ↑↓ navigate · Enter toggle · Esc back"}</Text>
|
|
2233
|
-
</Box>
|
|
2234
|
-
);
|
|
2235
|
-
})()}
|
|
2236
|
-
{skillsPicker === "remove" && (() => {
|
|
2237
|
-
const installed = listInstalledSkills();
|
|
2238
|
-
return (
|
|
2239
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.error} paddingX={1} marginBottom={0}>
|
|
2240
|
-
<Text bold color={theme.colors.error}>Remove a skill:</Text>
|
|
2241
|
-
{installed.map((s, i) => (
|
|
2242
|
-
<Text key={s.name}>
|
|
2243
|
-
{i === skillsPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2244
|
-
<Text color={i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.muted}>{s.name} — {s.description}</Text>
|
|
2245
|
-
</Text>
|
|
2246
|
-
))}
|
|
2247
|
-
<Text dimColor>{" ↑↓ navigate · Enter remove · Esc back"}</Text>
|
|
2248
|
-
</Box>
|
|
2249
|
-
);
|
|
2250
|
-
})()}
|
|
2251
|
-
|
|
2252
|
-
{/* ═══ THEME PICKER ═══ */}
|
|
2253
|
-
{themePicker && (
|
|
2254
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2255
|
-
<Text bold color={theme.colors.secondary}>Choose a theme:</Text>
|
|
2256
|
-
{listThemes().map((key, i) => (
|
|
2257
|
-
<Text key={key}>
|
|
2258
|
-
{i === themePickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2259
|
-
<Text color={i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{key}</Text>
|
|
2260
|
-
<Text color={theme.colors.muted}>{" — "}{THEMES[key].description}</Text>
|
|
2261
|
-
{key === theme.name.toLowerCase() ? <Text color={theme.colors.muted}> (current)</Text> : null}
|
|
2262
|
-
</Text>
|
|
2263
|
-
))}
|
|
2264
|
-
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
2265
|
-
</Box>
|
|
2266
|
-
)}
|
|
2267
|
-
|
|
2268
|
-
{/* ═══ SESSION PICKER ═══ */}
|
|
2269
|
-
{sessionPicker && (
|
|
2270
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.secondary} paddingX={1} marginBottom={0}>
|
|
2271
|
-
<Text bold color={theme.colors.secondary}>Resume a session:</Text>
|
|
2272
|
-
{sessionPicker.map((s, i) => (
|
|
2273
|
-
<Text key={s.id}>
|
|
2274
|
-
{i === sessionPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2275
|
-
<Text color={i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted}>{s.display}</Text>
|
|
2276
|
-
</Text>
|
|
2277
|
-
))}
|
|
2278
|
-
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
2279
|
-
</Box>
|
|
2280
|
-
)}
|
|
2281
|
-
|
|
2282
|
-
{/* ═══ DELETE SESSION PICKER ═══ */}
|
|
2283
|
-
{deleteSessionPicker && (
|
|
2284
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.error} paddingX={1} marginBottom={0}>
|
|
2285
|
-
<Text bold color={theme.colors.error}>Delete a session:</Text>
|
|
2286
|
-
{deleteSessionPicker.map((s, i) => (
|
|
2287
|
-
<Text key={s.id}>
|
|
2288
|
-
{i === deleteSessionPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2289
|
-
<Text color={i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted}>{s.display}</Text>
|
|
2290
|
-
</Text>
|
|
2291
|
-
))}
|
|
2292
|
-
<Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
|
|
2293
|
-
</Box>
|
|
2294
|
-
)}
|
|
2295
|
-
|
|
2296
|
-
{/* ═══ DELETE SESSION CONFIRM ═══ */}
|
|
2297
|
-
{deleteSessionConfirm && (
|
|
2298
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
|
|
2299
|
-
<Text bold color={theme.colors.warning}>Delete session {deleteSessionConfirm.id}?</Text>
|
|
2300
|
-
<Text color={theme.colors.muted}>{" "}{deleteSessionConfirm.display}</Text>
|
|
2301
|
-
<Text>
|
|
2302
|
-
<Text color={theme.colors.error} bold> [y]</Text><Text>es </Text>
|
|
2303
|
-
<Text color={theme.colors.success} bold>[n]</Text><Text>o</Text>
|
|
2304
|
-
</Text>
|
|
2305
|
-
</Box>
|
|
2306
|
-
)}
|
|
2307
|
-
|
|
2308
|
-
{/* ═══ MODEL PICKER ═══ */}
|
|
2309
|
-
{modelPicker && (
|
|
2310
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2311
|
-
<Text bold color={theme.colors.secondary}>Switch model:</Text>
|
|
2312
|
-
<Text>{""}</Text>
|
|
2313
|
-
{modelPicker.map((m, i) => (
|
|
2314
|
-
<Text key={m}>
|
|
2315
|
-
{" "}{i === modelPickerIndex ? <Text color={theme.colors.primary} bold>{"▸ "}</Text> : " "}
|
|
2316
|
-
<Text color={i === modelPickerIndex ? theme.colors.primary : undefined}>{m}</Text>
|
|
2317
|
-
{m === modelName ? <Text color={theme.colors.success}>{" (active)"}</Text> : null}
|
|
2318
|
-
</Text>
|
|
2319
|
-
))}
|
|
2320
|
-
<Text>{""}</Text>
|
|
2321
|
-
<Text dimColor>{" ↑↓ navigate · Enter to switch · Esc cancel"}</Text>
|
|
2322
|
-
</Box>
|
|
2323
|
-
)}
|
|
2324
|
-
|
|
2325
|
-
{/* ═══ OLLAMA DELETE PICKER ═══ */}
|
|
2326
|
-
{ollamaDeletePicker && (
|
|
2327
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2328
|
-
<Text bold color={theme.colors.secondary}>Delete which model?</Text>
|
|
2329
|
-
<Text>{""}</Text>
|
|
2330
|
-
{ollamaDeletePicker.models.map((m, i) => (
|
|
2331
|
-
<Text key={m.name}>
|
|
2332
|
-
{" "}{i === ollamaDeletePickerIndex ? <Text color={theme.colors.primary} bold>{"▸ "}</Text> : " "}
|
|
2333
|
-
<Text color={i === ollamaDeletePickerIndex ? theme.colors.primary : undefined}>{m.name}</Text>
|
|
2334
|
-
<Text color={theme.colors.muted}>{" ("}{(m.size / (1024 * 1024 * 1024)).toFixed(1)}{" GB)"}</Text>
|
|
2335
|
-
</Text>
|
|
2336
|
-
))}
|
|
2337
|
-
<Text>{""}</Text>
|
|
2338
|
-
<Text dimColor>{" ↑↓ navigate · Enter to delete · Esc cancel"}</Text>
|
|
2339
|
-
</Box>
|
|
2340
|
-
)}
|
|
2341
|
-
|
|
2342
|
-
{/* ═══ OLLAMA PULL PICKER ═══ */}
|
|
2343
|
-
{ollamaPullPicker && (
|
|
2344
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2345
|
-
<Text bold color={theme.colors.secondary}>Download which model?</Text>
|
|
2346
|
-
<Text>{""}</Text>
|
|
2347
|
-
{[
|
|
2348
|
-
{ id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
|
|
2349
|
-
{ id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
|
|
2350
|
-
{ id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
|
|
2351
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
|
|
2352
|
-
{ id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
|
|
2353
|
-
{ id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
|
|
2354
|
-
{ id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
|
|
2355
|
-
].map((m, i) => (
|
|
2356
|
-
<Text key={m.id}>
|
|
2357
|
-
{" "}{i === ollamaPullPickerIndex ? <Text color={theme.colors.primary} bold>{"▸ "}</Text> : " "}
|
|
2358
|
-
<Text color={i === ollamaPullPickerIndex ? theme.colors.primary : undefined} bold>{m.name}</Text>
|
|
2359
|
-
<Text color={theme.colors.muted}>{" · "}{m.size}{" · "}{m.desc}</Text>
|
|
2360
|
-
</Text>
|
|
2361
|
-
))}
|
|
2362
|
-
<Text>{""}</Text>
|
|
2363
|
-
<Text dimColor>{" ↑↓ navigate · Enter to download · Esc cancel"}</Text>
|
|
2364
|
-
</Box>
|
|
2365
|
-
)}
|
|
2366
|
-
|
|
2367
|
-
{/* ═══ OLLAMA DELETE CONFIRM ═══ */}
|
|
2368
|
-
{ollamaDeleteConfirm && (
|
|
2369
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
|
|
2370
|
-
<Text bold color={theme.colors.warning}>Delete {ollamaDeleteConfirm.model} ({(ollamaDeleteConfirm.size / (1024 * 1024 * 1024)).toFixed(1)} GB)?</Text>
|
|
2371
|
-
<Text>
|
|
2372
|
-
<Text color={theme.colors.error} bold> [y]</Text><Text>es </Text>
|
|
2373
|
-
<Text color={theme.colors.success} bold>[n]</Text><Text>o</Text>
|
|
2374
|
-
</Text>
|
|
2375
|
-
</Box>
|
|
2376
|
-
)}
|
|
2377
|
-
|
|
2378
|
-
{/* ═══ OLLAMA PULL PROGRESS ═══ */}
|
|
2379
|
-
{ollamaPulling && (
|
|
2380
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2381
|
-
<Text bold color={theme.colors.secondary}>{" Downloading "}{ollamaPulling.model}{"..."}</Text>
|
|
2382
|
-
{ollamaPulling.progress.status === "downloading" || ollamaPulling.progress.percent > 0 ? (
|
|
2383
|
-
<Text>
|
|
2384
|
-
{" "}
|
|
2385
|
-
<Text color={theme.colors.primary}>
|
|
2386
|
-
{"\u2588".repeat(Math.floor(ollamaPulling.progress.percent / 5))}
|
|
2387
|
-
{"\u2591".repeat(20 - Math.floor(ollamaPulling.progress.percent / 5))}
|
|
2388
|
-
</Text>
|
|
2389
|
-
{" "}<Text bold>{ollamaPulling.progress.percent}%</Text>
|
|
2390
|
-
{ollamaPulling.progress.completed != null && ollamaPulling.progress.total != null ? (
|
|
2391
|
-
<Text color={theme.colors.muted}>{" \u00B7 "}{formatBytes(ollamaPulling.progress.completed)}{" / "}{formatBytes(ollamaPulling.progress.total)}</Text>
|
|
2392
|
-
) : null}
|
|
2393
|
-
</Text>
|
|
2394
|
-
) : (
|
|
2395
|
-
<Text color={theme.colors.muted}>{" "}{ollamaPulling.progress.status}...</Text>
|
|
2396
|
-
)}
|
|
2397
|
-
</Box>
|
|
2398
|
-
)}
|
|
2399
|
-
|
|
2400
|
-
{/* ═══ OLLAMA EXIT PROMPT ═══ */}
|
|
2401
|
-
{ollamaExitPrompt && (
|
|
2402
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
|
|
2403
|
-
<Text bold color={theme.colors.warning}>Ollama is still running.</Text>
|
|
2404
|
-
<Text color={theme.colors.muted}>Stop it to free GPU memory?</Text>
|
|
2405
|
-
<Text>
|
|
2406
|
-
<Text color={theme.colors.success} bold> [y]</Text><Text>es </Text>
|
|
2407
|
-
<Text color={theme.colors.error} bold>[n]</Text><Text>o </Text>
|
|
2408
|
-
<Text color={theme.colors.primary} bold>[a]</Text><Text>lways</Text>
|
|
2409
|
-
</Text>
|
|
2410
|
-
</Box>
|
|
2411
|
-
)}
|
|
2412
|
-
|
|
2413
|
-
{/* ═══ SETUP WIZARD ═══ */}
|
|
2414
|
-
{wizardScreen === "connection" && (
|
|
2415
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2416
|
-
<Text bold color={theme.colors.secondary}>No LLM detected. How do you want to connect?</Text>
|
|
2417
|
-
<Text>{""}</Text>
|
|
2418
|
-
{[
|
|
2419
|
-
{ key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
|
|
2420
|
-
{ key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
|
|
2421
|
-
{ key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
|
|
2422
|
-
{ key: "existing", icon: "\u2699\uFE0F", label: "I already have a server running", desc: "" },
|
|
2423
|
-
].map((item, i) => (
|
|
2424
|
-
<Text key={item.key}>
|
|
2425
|
-
{i === wizardIndex ? <Text color={theme.colors.suggestion} bold>{" \u25B8 "}</Text> : <Text>{" "}</Text>}
|
|
2426
|
-
<Text color={i === wizardIndex ? theme.colors.suggestion : theme.colors.primary} bold>{item.icon} {item.label}</Text>
|
|
2427
|
-
{item.desc ? <Text color={theme.colors.muted}>{" ("}{item.desc}{")"}</Text> : null}
|
|
2428
|
-
</Text>
|
|
2429
|
-
))}
|
|
2430
|
-
<Text>{""}</Text>
|
|
2431
|
-
<Text dimColor>{" \u2191\u2193 navigate \u00B7 Enter to select"}</Text>
|
|
2432
|
-
</Box>
|
|
2433
|
-
)}
|
|
2434
|
-
|
|
2435
|
-
{wizardScreen === "models" && wizardHardware && (
|
|
2436
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2437
|
-
<Text bold color={theme.colors.secondary}>Your hardware:</Text>
|
|
2438
|
-
<Text color={theme.colors.muted}>{" CPU: "}{wizardHardware.cpu.name}{" ("}{wizardHardware.cpu.cores}{" cores)"}</Text>
|
|
2439
|
-
<Text color={theme.colors.muted}>{" RAM: "}{formatBytes(wizardHardware.ram)}</Text>
|
|
2440
|
-
{wizardHardware.gpu ? (
|
|
2441
|
-
<Text color={theme.colors.muted}>{" GPU: "}{wizardHardware.gpu.name}{wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""}</Text>
|
|
2442
|
-
) : (
|
|
2443
|
-
<Text color={theme.colors.muted}>{" GPU: none detected"}</Text>
|
|
2444
|
-
)}
|
|
2445
|
-
{!isLlmfitAvailable() && (
|
|
2446
|
-
<Text dimColor>{" Tip: Install llmfit for smarter recommendations: brew install llmfit"}</Text>
|
|
2447
|
-
)}
|
|
2448
|
-
<Text>{""}</Text>
|
|
2449
|
-
<Text bold color={theme.colors.secondary}>Recommended models:</Text>
|
|
2450
|
-
<Text>{""}</Text>
|
|
2451
|
-
{wizardModels.map((m, i) => (
|
|
2452
|
-
<Text key={m.ollamaId}>
|
|
2453
|
-
{i === wizardIndex ? <Text color={theme.colors.suggestion} bold>{" \u25B8 "}</Text> : <Text>{" "}</Text>}
|
|
2454
|
-
<Text>{getFitIcon(m.fit)} </Text>
|
|
2455
|
-
<Text color={i === wizardIndex ? theme.colors.suggestion : theme.colors.primary} bold>{m.name}</Text>
|
|
2456
|
-
<Text color={theme.colors.muted}>{" ~"}{m.size}{" GB \u00B7 "}{m.quality === "best" ? "Best" : m.quality === "great" ? "Great" : "Good"}{" quality \u00B7 "}{m.speed}</Text>
|
|
2457
|
-
</Text>
|
|
2458
|
-
))}
|
|
2459
|
-
{wizardModels.length === 0 && (
|
|
2460
|
-
<Text color={theme.colors.error}>{" No suitable models found for your hardware."}</Text>
|
|
2461
|
-
)}
|
|
2462
|
-
<Text>{""}</Text>
|
|
2463
|
-
<Text dimColor>{" \u2191\u2193 navigate \u00B7 Enter to install \u00B7 Esc back"}</Text>
|
|
2464
|
-
</Box>
|
|
2465
|
-
)}
|
|
2466
|
-
|
|
2467
|
-
{wizardScreen === "install-ollama" && (
|
|
2468
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.warning} paddingX={1} marginBottom={0}>
|
|
2469
|
-
<Text bold color={theme.colors.warning}>Ollama is required for local models.</Text>
|
|
2470
|
-
<Text>{""}</Text>
|
|
2471
|
-
<Text color={theme.colors.primary}>{" Press Enter to install Ollama automatically"}</Text>
|
|
2472
|
-
<Text dimColor>{" Or install manually: "}<Text>{getOllamaInstallCommand(wizardHardware?.os ?? "linux")}</Text></Text>
|
|
2473
|
-
<Text>{""}</Text>
|
|
2474
|
-
<Text dimColor>{" Enter to install · Esc to go back"}</Text>
|
|
2475
|
-
</Box>
|
|
2476
|
-
)}
|
|
2477
|
-
|
|
2478
|
-
{wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (
|
|
2479
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
|
|
2480
|
-
{wizardPullError ? (
|
|
2481
|
-
<>
|
|
2482
|
-
<Text color={theme.colors.error} bold>{" \u274C Error: "}{wizardPullError}</Text>
|
|
2483
|
-
<Text>{""}</Text>
|
|
2484
|
-
<Text dimColor>{" Press Enter to retry \u00B7 Esc to go back"}</Text>
|
|
2485
|
-
</>
|
|
2486
|
-
) : wizardPullProgress ? (
|
|
2487
|
-
<>
|
|
2488
|
-
<Text bold color={theme.colors.secondary}>{" "}{wizardSelectedModel ? `Downloading ${wizardSelectedModel.name}...` : wizardPullProgress?.status || "Working..."}</Text>
|
|
2489
|
-
{wizardPullProgress.status === "downloading" || wizardPullProgress.percent > 0 ? (
|
|
2490
|
-
<>
|
|
2491
|
-
<Text>
|
|
2492
|
-
{" "}
|
|
2493
|
-
<Text color={theme.colors.primary}>
|
|
2494
|
-
{"\u2588".repeat(Math.floor(wizardPullProgress.percent / 5))}
|
|
2495
|
-
{"\u2591".repeat(20 - Math.floor(wizardPullProgress.percent / 5))}
|
|
2496
|
-
</Text>
|
|
2497
|
-
{" "}<Text bold>{wizardPullProgress.percent}%</Text>
|
|
2498
|
-
{wizardPullProgress.completed != null && wizardPullProgress.total != null ? (
|
|
2499
|
-
<Text color={theme.colors.muted}>{" \u00B7 "}{formatBytes(wizardPullProgress.completed)}{" / "}{formatBytes(wizardPullProgress.total)}</Text>
|
|
2500
|
-
) : null}
|
|
2501
|
-
</Text>
|
|
2502
|
-
</>
|
|
2503
|
-
) : (
|
|
2504
|
-
<Text color={theme.colors.muted}>{" "}{wizardPullProgress.status}...</Text>
|
|
2505
|
-
)}
|
|
2506
|
-
</>
|
|
2507
|
-
) : null}
|
|
2508
|
-
</Box>
|
|
2509
|
-
)}
|
|
2510
|
-
|
|
2511
|
-
{/* ═══ COMMAND SUGGESTIONS ═══ */}
|
|
2512
|
-
{showSuggestions && (
|
|
2513
|
-
<Box flexDirection="column" borderStyle="single" borderColor={theme.colors.muted} paddingX={1} marginBottom={0}>
|
|
2514
|
-
{cmdMatches.slice(0, 6).map((c, i) => (
|
|
2515
|
-
<Text key={i}>
|
|
2516
|
-
{i === cmdIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
|
|
2517
|
-
<Text color={i === cmdIndex ? theme.colors.suggestion : theme.colors.primary} bold>{c.cmd}</Text>
|
|
2518
|
-
<Text color={theme.colors.muted}>{" — "}{c.desc}</Text>
|
|
2519
|
-
</Text>
|
|
2520
|
-
))}
|
|
2521
|
-
<Text dimColor>{" ↑↓ navigate · Tab select"}</Text>
|
|
2522
|
-
</Box>
|
|
2523
|
-
)}
|
|
2524
|
-
|
|
2525
|
-
{/* ═══ INPUT BOX (always at bottom) ═══ */}
|
|
2526
|
-
<Box borderStyle="single" borderColor={approval ? theme.colors.warning : theme.colors.border} paddingX={1}>
|
|
2527
|
-
<Text color={theme.colors.secondary} bold>{"> "}</Text>
|
|
2528
|
-
{approval ? (
|
|
2529
|
-
<Text color={theme.colors.warning}>waiting for approval...</Text>
|
|
2530
|
-
) : ready && !loading && !wizardScreen ? (
|
|
2531
|
-
<Box>
|
|
2532
|
-
{pastedChunks.map((p) => (
|
|
2533
|
-
<Text key={p.id} color={theme.colors.muted}>[Pasted text #{p.id} +{p.lines} lines]</Text>
|
|
2534
|
-
))}
|
|
2535
|
-
<TextInput
|
|
2536
|
-
key={inputKey}
|
|
2537
|
-
value={input}
|
|
2538
|
-
onChange={(v) => { setInput(v); setCmdIndex(0); }}
|
|
2539
|
-
onSubmit={handleSubmit}
|
|
2540
|
-
/>
|
|
2541
|
-
</Box>
|
|
2542
|
-
) : (
|
|
2543
|
-
<Text dimColor>{loading ? "waiting for response..." : "initializing..."}</Text>
|
|
2544
|
-
)}
|
|
2545
|
-
</Box>
|
|
2546
|
-
|
|
2547
|
-
{/* ═══ STATUS BAR ═══ */}
|
|
2548
|
-
{agent && (
|
|
2549
|
-
<Box paddingX={2}>
|
|
2550
|
-
<Text dimColor>
|
|
2551
|
-
{"💬 "}{agent.getContextLength()}{" messages · ~"}
|
|
2552
|
-
{(() => {
|
|
2553
|
-
const tokens = agent.estimateTokens();
|
|
2554
|
-
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
2555
|
-
})()}
|
|
2556
|
-
{" tokens"}
|
|
2557
|
-
{(() => {
|
|
2558
|
-
const { totalCost } = agent.getCostInfo();
|
|
2559
|
-
if (totalCost > 0) {
|
|
2560
|
-
return ` · 💰 $${totalCost < 0.01 ? totalCost.toFixed(4) : totalCost.toFixed(2)}`;
|
|
2561
|
-
}
|
|
2562
|
-
return "";
|
|
2563
|
-
})()}
|
|
2564
|
-
{modelName ? ` · 🤖 ${modelName}` : ""}
|
|
2565
|
-
{(() => {
|
|
2566
|
-
const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
|
|
2567
|
-
return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
|
|
2568
|
-
})()}
|
|
2569
|
-
{agent.getArchitectModel() ? " · 🏗️ architect" : ""}
|
|
2570
|
-
</Text>
|
|
2571
|
-
</Box>
|
|
2572
|
-
)}
|
|
2573
|
-
</Box>
|
|
2574
|
-
);
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
// Clear screen before render
|
|
2578
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
2579
|
-
|
|
2580
|
-
// Paste event bus — communicates between stdin interceptor and React
|
|
2581
|
-
const pasteEvents = new EventEmitter();
|
|
2582
|
-
|
|
2583
|
-
// Enable bracketed paste mode — terminal wraps pastes in escape sequences
|
|
2584
|
-
process.stdout.write("\x1b[?2004h");
|
|
2585
|
-
|
|
2586
|
-
// Intercept stdin to handle pasted content
|
|
2587
|
-
// Bracketed paste: \x1b[200~ ... \x1b[201~
|
|
2588
|
-
let pasteBuffer = "";
|
|
2589
|
-
let inPaste = false;
|
|
2590
|
-
|
|
2591
|
-
const origPush = process.stdin.push.bind(process.stdin);
|
|
2592
|
-
(process.stdin as any).push = function (chunk: any, encoding?: any) {
|
|
2593
|
-
if (chunk === null) return origPush(chunk, encoding);
|
|
2594
|
-
|
|
2595
|
-
let data = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf-8") : String(chunk);
|
|
2596
|
-
|
|
2597
|
-
const hasStart = data.includes("\x1b[200~");
|
|
2598
|
-
const hasEnd = data.includes("\x1b[201~");
|
|
2599
|
-
|
|
2600
|
-
if (hasStart) {
|
|
2601
|
-
inPaste = true;
|
|
2602
|
-
data = data.replace(/\x1b\[200~/g, "");
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
if (hasEnd) {
|
|
2606
|
-
data = data.replace(/\x1b\[201~/g, "");
|
|
2607
|
-
pasteBuffer += data;
|
|
2608
|
-
inPaste = false;
|
|
2609
|
-
|
|
2610
|
-
const content = pasteBuffer.trim();
|
|
2611
|
-
pasteBuffer = "";
|
|
2612
|
-
const lineCount = content.split("\n").length;
|
|
2613
|
-
|
|
2614
|
-
if (lineCount > 2) {
|
|
2615
|
-
// Multi-line paste → store as chunk, don't send to input
|
|
2616
|
-
pasteEvents.emit("paste", { content, lines: lineCount });
|
|
2617
|
-
return true;
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
// Short paste (1-2 lines) — send as normal input
|
|
2621
|
-
const sanitized = content.replace(/\r?\n/g, " ");
|
|
2622
|
-
if (sanitized) {
|
|
2623
|
-
return origPush(sanitized, "utf-8" as any);
|
|
2624
|
-
}
|
|
2625
|
-
return true;
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
if (inPaste) {
|
|
2629
|
-
pasteBuffer += data;
|
|
2630
|
-
return true;
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
data = data.replace(/\x1b\[20[01]~/g, "");
|
|
2634
|
-
return origPush(typeof chunk === "string" ? data : Buffer.from(data), encoding);
|
|
2635
|
-
};
|
|
2636
|
-
|
|
2637
|
-
// Disable bracketed paste on exit
|
|
2638
|
-
process.on("exit", () => {
|
|
2639
|
-
process.stdout.write("\x1b[?2004l");
|
|
2640
|
-
});
|
|
2641
|
-
|
|
2642
|
-
// Handle terminal resize — clear ghost artifacts
|
|
2643
|
-
process.stdout.on("resize", () => {
|
|
2644
|
-
process.stdout.write("\x1B[2J\x1B[H");
|
|
2645
|
-
});
|
|
2646
|
-
|
|
2647
|
-
render(<App />, { exitOnCtrlC: false });
|