codemaxxing 1.0.16 → 1.1.0
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 +72 -21
- package/dist/agent.d.ts +12 -1
- package/dist/agent.js +296 -31
- package/dist/index.js +256 -1303
- package/dist/ui/banner.d.ts +12 -0
- package/dist/ui/banner.js +28 -0
- package/dist/ui/connection-types.d.ts +33 -0
- package/dist/ui/connection-types.js +1 -0
- package/dist/ui/connection.d.ts +11 -0
- package/dist/ui/connection.js +182 -0
- package/dist/ui/input-router.d.ts +176 -0
- package/dist/ui/input-router.js +710 -0
- package/dist/ui/paste-interceptor.d.ts +21 -0
- package/dist/ui/paste-interceptor.js +179 -0
- package/dist/ui/pickers.d.ts +171 -0
- package/dist/ui/pickers.js +120 -0
- package/dist/ui/status-bar.d.ts +8 -0
- package/dist/ui/status-bar.js +15 -0
- package/dist/ui/wizard-types.d.ts +27 -0
- package/dist/ui/wizard-types.js +1 -0
- package/dist/ui/wizard.d.ts +3 -0
- package/dist/ui/wizard.js +214 -0
- package/dist/utils/anthropic-oauth.d.ts +13 -0
- package/dist/utils/anthropic-oauth.js +171 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +42 -3
- package/dist/utils/ollama.js +6 -1
- package/dist/utils/openai-oauth.d.ts +19 -0
- package/dist/utils/openai-oauth.js +233 -0
- package/dist/utils/responses-api.d.ts +40 -0
- package/dist/utils/responses-api.js +264 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useEffect, useCallback } from "react";
|
|
4
4
|
import { render, Box, Text, useInput, useApp, useStdout } from "ink";
|
|
5
|
-
import { EventEmitter } from "events";
|
|
6
|
-
import { appendFileSync } from "node:fs";
|
|
7
5
|
import TextInput from "ink-text-input";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { loadConfig,
|
|
11
|
-
import { listSessions, getSession
|
|
12
|
-
import { isGitRepo, getBranch, getStatus } from "./utils/git.js";
|
|
6
|
+
import { sanitizeInputArtifacts } from "./utils/paste.js";
|
|
7
|
+
import { setupPasteInterceptor } from "./ui/paste-interceptor.js";
|
|
8
|
+
import { loadConfig, listModels } from "./config.js";
|
|
9
|
+
import { listSessions, getSession } from "./utils/sessions.js";
|
|
13
10
|
import { tryHandleGitCommand } from "./commands/git.js";
|
|
14
11
|
import { tryHandleOllamaCommand } from "./commands/ollama.js";
|
|
15
12
|
import { dispatchRegisteredCommands } from "./commands/registry.js";
|
|
16
|
-
import { getTheme,
|
|
13
|
+
import { getTheme, DEFAULT_THEME } from "./themes.js";
|
|
17
14
|
import { tryHandleUiCommand } from "./commands/ui.js";
|
|
18
|
-
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
|
|
19
|
-
import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
|
|
20
15
|
import { listServers, addServer, removeServer, getConnectedServers } from "./utils/mcp.js";
|
|
21
16
|
import { tryHandleSkillsCommand } from "./commands/skills.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
17
|
+
import { isOllamaRunning, stopOllama, listInstalledModelsDetailed } from "./utils/ollama.js";
|
|
18
|
+
import { routeKeyPress } from "./ui/input-router.js";
|
|
19
|
+
import { getCredential } from "./utils/auth.js";
|
|
20
|
+
import { Banner, ConnectionInfo } from "./ui/banner.js";
|
|
21
|
+
import { StatusBar } from "./ui/status-bar.js";
|
|
22
|
+
import { refreshConnectionBanner as refreshConnectionBannerImpl, connectToProvider as connectToProviderImpl, } from "./ui/connection.js";
|
|
23
|
+
import { CommandSuggestions, LoginPicker, LoginMethodPickerUI, SkillsMenu, SkillsBrowse, SkillsInstalled, SkillsRemove, ThemePickerUI, SessionPicker, DeleteSessionPicker, DeleteSessionConfirm, ProviderPicker, ModelPicker, OllamaDeletePicker, OllamaPullPicker, OllamaDeleteConfirm, OllamaPullProgress, OllamaExitPrompt, ApprovalPrompt, WizardConnection, WizardModels, WizardInstallOllama, WizardPulling, } from "./ui/pickers.js";
|
|
25
24
|
import { createRequire } from "module";
|
|
26
25
|
const _require = createRequire(import.meta.url);
|
|
27
26
|
const VERSION = _require("../package.json").version;
|
|
@@ -53,9 +52,8 @@ const SLASH_COMMANDS = [
|
|
|
53
52
|
{ cmd: "/push", desc: "push to remote" },
|
|
54
53
|
{ cmd: "/git on", desc: "enable auto-commits" },
|
|
55
54
|
{ cmd: "/git off", desc: "disable auto-commits" },
|
|
56
|
-
{ cmd: "/models", desc: "
|
|
55
|
+
{ cmd: "/models", desc: "switch model" },
|
|
57
56
|
{ cmd: "/theme", desc: "switch color theme" },
|
|
58
|
-
{ cmd: "/model", desc: "switch model mid-session" },
|
|
59
57
|
{ cmd: "/sessions", desc: "list past sessions" },
|
|
60
58
|
{ cmd: "/session delete", desc: "delete a session" },
|
|
61
59
|
{ cmd: "/resume", desc: "resume a past session" },
|
|
@@ -139,6 +137,7 @@ function StreamingIndicator({ colors }) {
|
|
|
139
137
|
return (_jsxs(Text, { dimColor: true, children: [" ", _jsx(Text, { color: colors.spinner, children: STREAM_DOTS[frame] }), " ", _jsx(Text, { color: colors.muted, children: "streaming" })] }));
|
|
140
138
|
}
|
|
141
139
|
let msgId = 0;
|
|
140
|
+
function nextMsgId() { return msgId++; }
|
|
142
141
|
// ── Main App ──
|
|
143
142
|
function App() {
|
|
144
143
|
const { exit } = useApp();
|
|
@@ -183,8 +182,13 @@ function App() {
|
|
|
183
182
|
const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
|
|
184
183
|
const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
|
|
185
184
|
const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
|
|
186
|
-
const [
|
|
185
|
+
const [modelPickerGroups, setModelPickerGroups] = useState(null);
|
|
187
186
|
const [modelPickerIndex, setModelPickerIndex] = useState(0);
|
|
187
|
+
const [flatModelList, setFlatModelList] = useState([]);
|
|
188
|
+
const [providerPicker, setProviderPicker] = useState(null);
|
|
189
|
+
const [providerPickerIndex, setProviderPickerIndex] = useState(0);
|
|
190
|
+
const [selectedProvider, setSelectedProvider] = useState(null);
|
|
191
|
+
// ── Setup Wizard State ──
|
|
188
192
|
const [wizardScreen, setWizardScreen] = useState(null);
|
|
189
193
|
const [wizardIndex, setWizardIndex] = useState(0);
|
|
190
194
|
const [wizardHardware, setWizardHardware] = useState(null);
|
|
@@ -206,182 +210,33 @@ function App() {
|
|
|
206
210
|
}, []);
|
|
207
211
|
// Refresh the connection banner to reflect current provider status
|
|
208
212
|
const refreshConnectionBanner = useCallback(async () => {
|
|
209
|
-
|
|
210
|
-
const cliArgs = parseCLIArgs();
|
|
211
|
-
const rawConfig = loadConfig();
|
|
212
|
-
const config = applyOverrides(rawConfig, cliArgs);
|
|
213
|
-
const provider = config.provider;
|
|
214
|
-
if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
|
|
215
|
-
const detected = await detectLocalProvider();
|
|
216
|
-
if (detected) {
|
|
217
|
-
info.push(`✔ Connected to ${detected.baseUrl} → ${detected.model}`);
|
|
218
|
-
}
|
|
219
|
-
else {
|
|
220
|
-
const ollamaUp = await isOllamaRunning();
|
|
221
|
-
info.push(ollamaUp ? "Ollama running (no model loaded)" : "✗ No local LLM server found");
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else {
|
|
225
|
-
info.push(`Provider: ${provider.baseUrl}`);
|
|
226
|
-
info.push(`Model: ${provider.model}`);
|
|
227
|
-
}
|
|
228
|
-
const cwd = process.cwd();
|
|
229
|
-
if (isGitRepo(cwd)) {
|
|
230
|
-
const branch = getBranch(cwd);
|
|
231
|
-
const status = getStatus(cwd);
|
|
232
|
-
info.push(`Git: ${branch} (${status})`);
|
|
233
|
-
}
|
|
234
|
-
setConnectionInfo(info);
|
|
213
|
+
await refreshConnectionBannerImpl(setConnectionInfo);
|
|
235
214
|
}, []);
|
|
236
215
|
// Connect/reconnect to LLM provider
|
|
237
216
|
const connectToProvider = useCallback(async (isRetry = false) => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (cliArgs.model)
|
|
254
|
-
detection.provider.model = cliArgs.model;
|
|
255
|
-
provider = detection.provider;
|
|
256
|
-
info.push(`✔ Connected to ${provider.baseUrl} → ${provider.model}`);
|
|
257
|
-
setConnectionInfo([...info]);
|
|
258
|
-
}
|
|
259
|
-
else if (detection.status === "no-models") {
|
|
260
|
-
info.push(`⚠ ${detection.serverName} is running but has no models. Use /ollama pull to download one.`);
|
|
261
|
-
setConnectionInfo([...info]);
|
|
262
|
-
setReady(true);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
else {
|
|
266
|
-
info.push("✗ No local LLM server found.");
|
|
267
|
-
setConnectionInfo([...info]);
|
|
268
|
-
setReady(true);
|
|
269
|
-
// Show the setup wizard on first run
|
|
270
|
-
setWizardScreen("connection");
|
|
271
|
-
setWizardIndex(0);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
else {
|
|
276
|
-
info.push(`Provider: ${provider.baseUrl}`);
|
|
277
|
-
info.push(`Model: ${provider.model}`);
|
|
278
|
-
setConnectionInfo([...info]);
|
|
279
|
-
}
|
|
280
|
-
const cwd = process.cwd();
|
|
281
|
-
// Git info
|
|
282
|
-
if (isGitRepo(cwd)) {
|
|
283
|
-
const branch = getBranch(cwd);
|
|
284
|
-
const status = getStatus(cwd);
|
|
285
|
-
info.push(`Git: ${branch} (${status})`);
|
|
286
|
-
setConnectionInfo([...info]);
|
|
287
|
-
}
|
|
288
|
-
const a = new CodingAgent({
|
|
289
|
-
provider,
|
|
290
|
-
cwd,
|
|
291
|
-
maxTokens: config.defaults.maxTokens,
|
|
292
|
-
autoApprove: config.defaults.autoApprove,
|
|
293
|
-
onToken: (token) => {
|
|
294
|
-
// Switch from big spinner to streaming mode
|
|
295
|
-
setLoading(false);
|
|
296
|
-
setStreaming(true);
|
|
297
|
-
// Update the current streaming response in-place
|
|
298
|
-
setMessages((prev) => {
|
|
299
|
-
const lastIdx = prev.length - 1;
|
|
300
|
-
const last = prev[lastIdx];
|
|
301
|
-
if (last && last.type === "response" && last._streaming) {
|
|
302
|
-
return [
|
|
303
|
-
...prev.slice(0, lastIdx),
|
|
304
|
-
{ ...last, text: last.text + token },
|
|
305
|
-
];
|
|
306
|
-
}
|
|
307
|
-
// First token of a new response
|
|
308
|
-
return [...prev, { id: msgId++, type: "response", text: token, _streaming: true }];
|
|
309
|
-
});
|
|
310
|
-
},
|
|
311
|
-
onToolCall: (name, args) => {
|
|
312
|
-
setLoading(true);
|
|
313
|
-
setSpinnerMsg("Executing tools...");
|
|
314
|
-
const argStr = Object.entries(args)
|
|
315
|
-
.map(([k, v]) => {
|
|
316
|
-
const val = String(v);
|
|
317
|
-
return val.length > 60 ? val.slice(0, 60) + "..." : val;
|
|
318
|
-
})
|
|
319
|
-
.join(", ");
|
|
320
|
-
addMsg("tool", `${name}(${argStr})`);
|
|
321
|
-
},
|
|
322
|
-
onToolResult: (_name, result) => {
|
|
323
|
-
const numLines = result.split("\n").length;
|
|
324
|
-
const size = result.length > 1024 ? `${(result.length / 1024).toFixed(1)}KB` : `${result.length}B`;
|
|
325
|
-
addMsg("tool-result", `└ ${numLines} lines (${size})`);
|
|
326
|
-
},
|
|
327
|
-
onThinking: (text) => {
|
|
328
|
-
if (text.length > 0) {
|
|
329
|
-
addMsg("info", `💭 Thought for ${text.split(/\s+/).length} words`);
|
|
330
|
-
}
|
|
331
|
-
},
|
|
332
|
-
onGitCommit: (message) => {
|
|
333
|
-
addMsg("info", `📝 Auto-committed: ${message}`);
|
|
334
|
-
},
|
|
335
|
-
onContextCompressed: (oldTokens, newTokens) => {
|
|
336
|
-
const saved = oldTokens - newTokens;
|
|
337
|
-
const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
|
|
338
|
-
addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
|
|
339
|
-
},
|
|
340
|
-
onArchitectPlan: (plan) => {
|
|
341
|
-
addMsg("info", `🏗️ Architect Plan:\n${plan}`);
|
|
342
|
-
},
|
|
343
|
-
onLintResult: (file, errors) => {
|
|
344
|
-
addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
|
|
345
|
-
},
|
|
346
|
-
onMCPStatus: (server, status) => {
|
|
347
|
-
addMsg("info", `🔌 MCP ${server}: ${status}`);
|
|
348
|
-
},
|
|
349
|
-
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
350
|
-
onToolApproval: (name, args, diff) => {
|
|
351
|
-
return new Promise((resolve) => {
|
|
352
|
-
setApproval({ tool: name, args, diff, resolve });
|
|
353
|
-
setLoading(false);
|
|
354
|
-
});
|
|
355
|
-
},
|
|
217
|
+
await connectToProviderImpl(isRetry, {
|
|
218
|
+
setConnectionInfo,
|
|
219
|
+
setReady,
|
|
220
|
+
setAgent,
|
|
221
|
+
setModelName,
|
|
222
|
+
providerRef,
|
|
223
|
+
setLoading,
|
|
224
|
+
setStreaming,
|
|
225
|
+
setSpinnerMsg,
|
|
226
|
+
setMessages,
|
|
227
|
+
addMsg,
|
|
228
|
+
nextMsgId,
|
|
229
|
+
setApproval,
|
|
230
|
+
setWizardScreen,
|
|
231
|
+
setWizardIndex,
|
|
356
232
|
});
|
|
357
|
-
// Initialize async context (repo map)
|
|
358
|
-
await a.init();
|
|
359
|
-
// Show project rules in banner
|
|
360
|
-
const rulesSource = a.getProjectRulesSource();
|
|
361
|
-
if (rulesSource) {
|
|
362
|
-
info.push(`📋 ${rulesSource} loaded`);
|
|
363
|
-
setConnectionInfo([...info]);
|
|
364
|
-
}
|
|
365
|
-
// Show MCP server count
|
|
366
|
-
const mcpCount = a.getMCPServerCount();
|
|
367
|
-
if (mcpCount > 0) {
|
|
368
|
-
info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
|
|
369
|
-
setConnectionInfo([...info]);
|
|
370
|
-
}
|
|
371
|
-
setAgent(a);
|
|
372
|
-
setModelName(provider.model);
|
|
373
|
-
providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
|
|
374
|
-
setReady(true);
|
|
375
|
-
if (isRetry) {
|
|
376
|
-
addMsg("info", `✅ Connected to ${provider.model}`);
|
|
377
|
-
}
|
|
378
233
|
}, []);
|
|
379
234
|
// Initialize agent on mount
|
|
380
235
|
useEffect(() => {
|
|
381
236
|
connectToProvider(false);
|
|
382
237
|
}, []);
|
|
383
238
|
function addMsg(type, text) {
|
|
384
|
-
setMessages((prev) => [...prev, { id:
|
|
239
|
+
setMessages((prev) => [...prev, { id: nextMsgId(), type, text }]);
|
|
385
240
|
}
|
|
386
241
|
// Compute matching commands for suggestions
|
|
387
242
|
const cmdMatches = input.startsWith("/")
|
|
@@ -408,7 +263,7 @@ function App() {
|
|
|
408
263
|
const selected = matches[idx];
|
|
409
264
|
if (selected) {
|
|
410
265
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
411
|
-
if (selected.cmd === "/commit" || selected.cmd === "/
|
|
266
|
+
if (selected.cmd === "/commit" || selected.cmd === "/session delete" ||
|
|
412
267
|
selected.cmd === "/architect") {
|
|
413
268
|
setInput(selected.cmd + " ");
|
|
414
269
|
setCmdIndex(0);
|
|
@@ -469,8 +324,7 @@ function App() {
|
|
|
469
324
|
" /help — show this",
|
|
470
325
|
" /connect — retry LLM connection",
|
|
471
326
|
" /login — authentication setup (run codemaxxing login in terminal)",
|
|
472
|
-
" /
|
|
473
|
-
" /models — list available models",
|
|
327
|
+
" /models — switch model",
|
|
474
328
|
" /map — show repository map",
|
|
475
329
|
" /sessions — list past sessions",
|
|
476
330
|
" /session delete — delete a session",
|
|
@@ -620,54 +474,140 @@ function App() {
|
|
|
620
474
|
addMsg("info", `Messages in context: ${agent.getContextLength()}`);
|
|
621
475
|
return;
|
|
622
476
|
}
|
|
623
|
-
if (trimmed === "/models") {
|
|
624
|
-
addMsg("info", "Fetching available models...");
|
|
625
|
-
const { baseUrl, apiKey } = providerRef.current;
|
|
626
|
-
const models = await listModels(baseUrl, apiKey);
|
|
627
|
-
if (models.length === 0) {
|
|
628
|
-
addMsg("info", "No models found or couldn't reach provider.");
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
addMsg("info", "Available models:\n" + models.map(m => ` ${m}`).join("\n"));
|
|
632
|
-
}
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
if (trimmed === "/model") {
|
|
636
|
-
// Show picker of available models
|
|
477
|
+
if (trimmed === "/models" || trimmed === "/model") {
|
|
637
478
|
addMsg("info", "Fetching available models...");
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
479
|
+
const groups = {};
|
|
480
|
+
const providerEntries = [];
|
|
481
|
+
// Local LLM (Ollama/LM Studio) — always show, auto-detect
|
|
482
|
+
let localFound = false;
|
|
483
|
+
// Check common local LLM endpoints
|
|
484
|
+
const localEndpoints = [
|
|
485
|
+
{ name: "LM Studio", port: 1234 },
|
|
486
|
+
{ name: "Ollama", port: 11434 },
|
|
487
|
+
{ name: "vLLM", port: 8000 },
|
|
488
|
+
{ name: "LocalAI", port: 8080 },
|
|
489
|
+
];
|
|
490
|
+
for (const endpoint of localEndpoints) {
|
|
491
|
+
if (localFound)
|
|
492
|
+
break;
|
|
493
|
+
try {
|
|
494
|
+
const url = `http://localhost:${endpoint.port}/v1`;
|
|
495
|
+
const models = await listModels(url, "local");
|
|
496
|
+
if (models.length > 0) {
|
|
497
|
+
groups["Local LLM"] = models.map(m => ({
|
|
498
|
+
name: m,
|
|
499
|
+
baseUrl: url,
|
|
500
|
+
apiKey: "local",
|
|
501
|
+
providerType: "openai",
|
|
502
|
+
}));
|
|
503
|
+
localFound = true;
|
|
504
|
+
}
|
|
644
505
|
}
|
|
506
|
+
catch { /* not running */ }
|
|
645
507
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
// Fallback: try provider's model list
|
|
650
|
-
if (providerRef.current?.baseUrl && providerRef.current.baseUrl !== "auto") {
|
|
508
|
+
// Also check Ollama native API
|
|
509
|
+
if (!localFound) {
|
|
651
510
|
try {
|
|
652
|
-
const
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
511
|
+
const ollamaModels = await listInstalledModelsDetailed();
|
|
512
|
+
if (ollamaModels.length > 0) {
|
|
513
|
+
groups["Local LLM"] = ollamaModels.map(m => ({
|
|
514
|
+
name: m.name,
|
|
515
|
+
baseUrl: "http://localhost:11434/v1",
|
|
516
|
+
apiKey: "ollama",
|
|
517
|
+
providerType: "openai",
|
|
518
|
+
}));
|
|
519
|
+
localFound = true;
|
|
657
520
|
}
|
|
658
521
|
}
|
|
659
|
-
catch
|
|
660
|
-
|
|
522
|
+
catch { /* Ollama not running */ }
|
|
523
|
+
}
|
|
524
|
+
if (localFound) {
|
|
525
|
+
providerEntries.push({ name: "Local LLM", description: "No auth needed — auto-detected", authed: true });
|
|
526
|
+
}
|
|
527
|
+
// Anthropic
|
|
528
|
+
const anthropicCred = getCredential("anthropic");
|
|
529
|
+
const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"];
|
|
530
|
+
if (anthropicCred) {
|
|
531
|
+
groups["Anthropic (Claude)"] = claudeModels.map(m => ({
|
|
532
|
+
name: m,
|
|
533
|
+
baseUrl: "https://api.anthropic.com",
|
|
534
|
+
apiKey: anthropicCred.apiKey,
|
|
535
|
+
providerType: "anthropic",
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
providerEntries.push({ name: "Anthropic (Claude)", description: "Claude Opus, Sonnet, Haiku — use your subscription or API key", authed: !!anthropicCred });
|
|
539
|
+
// OpenAI
|
|
540
|
+
const openaiCred = getCredential("openai");
|
|
541
|
+
const openaiModels = ["gpt-5.4", "gpt-5.4-pro", "gpt-5", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "o3", "o4-mini", "gpt-4o"];
|
|
542
|
+
if (openaiCred) {
|
|
543
|
+
// OAuth tokens (non sk- keys) must use ChatGPT backend, not api.openai.com
|
|
544
|
+
const isOAuthToken = openaiCred.method === "oauth" || openaiCred.method === "cached-token" ||
|
|
545
|
+
(!openaiCred.apiKey.startsWith("sk-") && !openaiCred.apiKey.startsWith("sess-"));
|
|
546
|
+
const baseUrl = isOAuthToken
|
|
547
|
+
? "https://chatgpt.com/backend-api"
|
|
548
|
+
: (openaiCred.baseUrl || "https://api.openai.com/v1");
|
|
549
|
+
groups["OpenAI (ChatGPT)"] = openaiModels.map(m => ({
|
|
550
|
+
name: m,
|
|
551
|
+
baseUrl,
|
|
552
|
+
apiKey: openaiCred.apiKey,
|
|
553
|
+
providerType: "openai",
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
providerEntries.push({ name: "OpenAI (ChatGPT)", description: "GPT-5, GPT-4.1, o3 — use your ChatGPT subscription or API key", authed: !!openaiCred });
|
|
557
|
+
// OpenRouter
|
|
558
|
+
const openrouterCred = getCredential("openrouter");
|
|
559
|
+
if (openrouterCred) {
|
|
560
|
+
try {
|
|
561
|
+
const orModels = await listModels(openrouterCred.baseUrl || "https://openrouter.ai/api/v1", openrouterCred.apiKey);
|
|
562
|
+
if (orModels.length > 0) {
|
|
563
|
+
groups["OpenRouter"] = orModels.slice(0, 20).map(m => ({
|
|
564
|
+
name: m,
|
|
565
|
+
baseUrl: openrouterCred.baseUrl || "https://openrouter.ai/api/v1",
|
|
566
|
+
apiKey: openrouterCred.apiKey,
|
|
567
|
+
providerType: "openai",
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
661
570
|
}
|
|
571
|
+
catch { /* skip */ }
|
|
572
|
+
}
|
|
573
|
+
providerEntries.push({ name: "OpenRouter", description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login", authed: !!openrouterCred });
|
|
574
|
+
// Qwen
|
|
575
|
+
const qwenCred = getCredential("qwen");
|
|
576
|
+
if (qwenCred) {
|
|
577
|
+
groups["Qwen"] = ["qwen-max", "qwen-plus", "qwen-turbo"].map(m => ({
|
|
578
|
+
name: m,
|
|
579
|
+
baseUrl: qwenCred.baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
580
|
+
apiKey: qwenCred.apiKey,
|
|
581
|
+
providerType: "openai",
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
providerEntries.push({ name: "Qwen", description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key", authed: !!qwenCred });
|
|
585
|
+
// GitHub Copilot
|
|
586
|
+
const copilotCred = getCredential("copilot");
|
|
587
|
+
if (copilotCred) {
|
|
588
|
+
groups["GitHub Copilot"] = ["gpt-4o", "claude-3.5-sonnet"].map(m => ({
|
|
589
|
+
name: m,
|
|
590
|
+
baseUrl: copilotCred.baseUrl || "https://api.githubcopilot.com",
|
|
591
|
+
apiKey: copilotCred.apiKey,
|
|
592
|
+
providerType: "openai",
|
|
593
|
+
}));
|
|
594
|
+
}
|
|
595
|
+
providerEntries.push({ name: "GitHub Copilot", description: "Use your GitHub Copilot subscription", authed: !!copilotCred });
|
|
596
|
+
// Show provider picker (step 1)
|
|
597
|
+
if (providerEntries.length > 0) {
|
|
598
|
+
setModelPickerGroups(groups);
|
|
599
|
+
setProviderPicker(providerEntries);
|
|
600
|
+
setProviderPickerIndex(0);
|
|
601
|
+
setSelectedProvider(null);
|
|
602
|
+
return;
|
|
662
603
|
}
|
|
663
|
-
// No models found anywhere
|
|
664
604
|
addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
|
|
665
605
|
return;
|
|
666
606
|
}
|
|
667
607
|
if (trimmed.startsWith("/model ")) {
|
|
668
608
|
const newModel = trimmed.replace("/model ", "").trim();
|
|
669
609
|
if (!newModel) {
|
|
670
|
-
addMsg("info", `Current model: ${modelName}\n Usage: /
|
|
610
|
+
addMsg("info", `Current model: ${modelName}\n Usage: /models`);
|
|
671
611
|
return;
|
|
672
612
|
}
|
|
673
613
|
agent.switchModel(newModel);
|
|
@@ -798,875 +738,100 @@ function App() {
|
|
|
798
738
|
setStreaming(false);
|
|
799
739
|
}, [agent, exit, refreshConnectionBanner]);
|
|
800
740
|
useInput((inputChar, key) => {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
// Login picker navigation (first level — pick provider)
|
|
896
|
-
if (loginPicker) {
|
|
897
|
-
const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
|
|
898
|
-
if (key.upArrow) {
|
|
899
|
-
setLoginPickerIndex((prev) => (prev - 1 + loginProviders.length) % loginProviders.length);
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
if (key.downArrow) {
|
|
903
|
-
setLoginPickerIndex((prev) => (prev + 1) % loginProviders.length);
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
if (key.return) {
|
|
907
|
-
const selected = loginProviders[loginPickerIndex];
|
|
908
|
-
setLoginPicker(false);
|
|
909
|
-
// Get available methods for this provider (filter out 'none')
|
|
910
|
-
const methods = selected.methods.filter((m) => m !== "none");
|
|
911
|
-
if (methods.length === 1) {
|
|
912
|
-
// Only one method — execute it directly
|
|
913
|
-
setLoginMethodPicker({ provider: selected.id, methods });
|
|
914
|
-
setLoginMethodIndex(0);
|
|
915
|
-
// Simulate Enter press on the single method
|
|
916
|
-
if (methods[0] === "oauth" && selected.id === "openrouter") {
|
|
917
|
-
setLoginMethodPicker(null);
|
|
918
|
-
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
919
|
-
setLoading(true);
|
|
920
|
-
setSpinnerMsg("Waiting for authorization...");
|
|
921
|
-
openRouterOAuth((msg) => addMsg("info", msg))
|
|
922
|
-
.then(() => { addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); setLoading(false); })
|
|
923
|
-
.catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
924
|
-
}
|
|
925
|
-
else if (methods[0] === "device-flow") {
|
|
926
|
-
setLoginMethodPicker(null);
|
|
927
|
-
addMsg("info", "Starting GitHub Copilot device flow...");
|
|
928
|
-
setLoading(true);
|
|
929
|
-
setSpinnerMsg("Waiting for GitHub authorization...");
|
|
930
|
-
copilotDeviceFlow((msg) => addMsg("info", msg))
|
|
931
|
-
.then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
|
|
932
|
-
.catch((err) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
933
|
-
}
|
|
934
|
-
else if (methods[0] === "api-key") {
|
|
935
|
-
setLoginMethodPicker(null);
|
|
936
|
-
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"}`);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
else {
|
|
940
|
-
// Multiple methods — show submenu
|
|
941
|
-
setLoginMethodPicker({ provider: selected.id, methods });
|
|
942
|
-
setLoginMethodIndex(0);
|
|
943
|
-
}
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
if (key.escape) {
|
|
947
|
-
setLoginPicker(false);
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
// Skills picker navigation
|
|
953
|
-
if (skillsPicker) {
|
|
954
|
-
if (skillsPicker === "menu") {
|
|
955
|
-
const menuItems = ["browse", "installed", "create", "remove"];
|
|
956
|
-
if (key.upArrow) {
|
|
957
|
-
setSkillsPickerIndex((prev) => (prev - 1 + menuItems.length) % menuItems.length);
|
|
958
|
-
return;
|
|
959
|
-
}
|
|
960
|
-
if (key.downArrow) {
|
|
961
|
-
setSkillsPickerIndex((prev) => (prev + 1) % menuItems.length);
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
if (key.escape) {
|
|
965
|
-
setSkillsPicker(null);
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
if (key.return) {
|
|
969
|
-
const selected = menuItems[skillsPickerIndex];
|
|
970
|
-
if (selected === "browse") {
|
|
971
|
-
setSkillsPicker("browse");
|
|
972
|
-
setSkillsPickerIndex(0);
|
|
973
|
-
}
|
|
974
|
-
else if (selected === "installed") {
|
|
975
|
-
setSkillsPicker("installed");
|
|
976
|
-
setSkillsPickerIndex(0);
|
|
977
|
-
}
|
|
978
|
-
else if (selected === "create") {
|
|
979
|
-
setSkillsPicker(null);
|
|
980
|
-
setInput("/skills create ");
|
|
981
|
-
setInputKey((k) => k + 1);
|
|
982
|
-
}
|
|
983
|
-
else if (selected === "remove") {
|
|
984
|
-
const installed = listInstalledSkills();
|
|
985
|
-
if (installed.length === 0) {
|
|
986
|
-
setSkillsPicker(null);
|
|
987
|
-
addMsg("info", "No skills installed to remove.");
|
|
988
|
-
}
|
|
989
|
-
else {
|
|
990
|
-
setSkillsPicker("remove");
|
|
991
|
-
setSkillsPickerIndex(0);
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
return;
|
|
995
|
-
}
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
if (skillsPicker === "browse") {
|
|
999
|
-
const registry = getRegistrySkills();
|
|
1000
|
-
if (key.upArrow) {
|
|
1001
|
-
setSkillsPickerIndex((prev) => (prev - 1 + registry.length) % registry.length);
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (key.downArrow) {
|
|
1005
|
-
setSkillsPickerIndex((prev) => (prev + 1) % registry.length);
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
if (key.escape) {
|
|
1009
|
-
setSkillsPicker("menu");
|
|
1010
|
-
setSkillsPickerIndex(0);
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
if (key.return) {
|
|
1014
|
-
const selected = registry[skillsPickerIndex];
|
|
1015
|
-
if (selected) {
|
|
1016
|
-
const result = installSkill(selected.name);
|
|
1017
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
1018
|
-
}
|
|
1019
|
-
setSkillsPicker(null);
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (skillsPicker === "installed") {
|
|
1025
|
-
const installed = listInstalledSkills();
|
|
1026
|
-
if (installed.length === 0) {
|
|
1027
|
-
setSkillsPicker("menu");
|
|
1028
|
-
setSkillsPickerIndex(0);
|
|
1029
|
-
addMsg("info", "No skills installed.");
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
|
-
if (key.upArrow) {
|
|
1033
|
-
setSkillsPickerIndex((prev) => (prev - 1 + installed.length) % installed.length);
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
if (key.downArrow) {
|
|
1037
|
-
setSkillsPickerIndex((prev) => (prev + 1) % installed.length);
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
if (key.escape) {
|
|
1041
|
-
setSkillsPicker("menu");
|
|
1042
|
-
setSkillsPickerIndex(0);
|
|
1043
|
-
return;
|
|
1044
|
-
}
|
|
1045
|
-
if (key.return) {
|
|
1046
|
-
// Toggle on/off for session
|
|
1047
|
-
const selected = installed[skillsPickerIndex];
|
|
1048
|
-
if (selected) {
|
|
1049
|
-
const isDisabled = sessionDisabledSkills.has(selected.name);
|
|
1050
|
-
if (isDisabled) {
|
|
1051
|
-
setSessionDisabledSkills((prev) => { const next = new Set(prev); next.delete(selected.name); return next; });
|
|
1052
|
-
if (agent)
|
|
1053
|
-
agent.enableSkill(selected.name);
|
|
1054
|
-
addMsg("info", `✅ Enabled: ${selected.name}`);
|
|
1055
|
-
}
|
|
1056
|
-
else {
|
|
1057
|
-
setSessionDisabledSkills((prev) => { const next = new Set(prev); next.add(selected.name); return next; });
|
|
1058
|
-
if (agent)
|
|
1059
|
-
agent.disableSkill(selected.name);
|
|
1060
|
-
addMsg("info", `✅ Disabled: ${selected.name} (session only)`);
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
setSkillsPicker(null);
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
if (skillsPicker === "remove") {
|
|
1069
|
-
const installed = listInstalledSkills();
|
|
1070
|
-
if (installed.length === 0) {
|
|
1071
|
-
setSkillsPicker(null);
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
if (key.upArrow) {
|
|
1075
|
-
setSkillsPickerIndex((prev) => (prev - 1 + installed.length) % installed.length);
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (key.downArrow) {
|
|
1079
|
-
setSkillsPickerIndex((prev) => (prev + 1) % installed.length);
|
|
1080
|
-
return;
|
|
1081
|
-
}
|
|
1082
|
-
if (key.escape) {
|
|
1083
|
-
setSkillsPicker("menu");
|
|
1084
|
-
setSkillsPickerIndex(0);
|
|
1085
|
-
return;
|
|
1086
|
-
}
|
|
1087
|
-
if (key.return) {
|
|
1088
|
-
const selected = installed[skillsPickerIndex];
|
|
1089
|
-
if (selected) {
|
|
1090
|
-
const result = removeSkill(selected.name);
|
|
1091
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
1092
|
-
}
|
|
1093
|
-
setSkillsPicker(null);
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
return;
|
|
1099
|
-
}
|
|
1100
|
-
// ── Model picker ──
|
|
1101
|
-
if (modelPicker) {
|
|
1102
|
-
if (key.upArrow) {
|
|
1103
|
-
setModelPickerIndex((prev) => (prev - 1 + modelPicker.length) % modelPicker.length);
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
if (key.downArrow) {
|
|
1107
|
-
setModelPickerIndex((prev) => (prev + 1) % modelPicker.length);
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
if (key.escape) {
|
|
1111
|
-
setModelPicker(null);
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
if (key.return) {
|
|
1115
|
-
const selected = modelPicker[modelPickerIndex];
|
|
1116
|
-
if (selected && agent) {
|
|
1117
|
-
agent.switchModel(selected);
|
|
1118
|
-
setModelName(selected);
|
|
1119
|
-
addMsg("info", `✅ Switched to: ${selected}`);
|
|
1120
|
-
refreshConnectionBanner();
|
|
1121
|
-
}
|
|
1122
|
-
setModelPicker(null);
|
|
1123
|
-
return;
|
|
1124
|
-
}
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
// ── Ollama delete picker ──
|
|
1128
|
-
if (ollamaDeletePicker) {
|
|
1129
|
-
if (key.upArrow) {
|
|
1130
|
-
setOllamaDeletePickerIndex((prev) => (prev - 1 + ollamaDeletePicker.models.length) % ollamaDeletePicker.models.length);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
if (key.downArrow) {
|
|
1134
|
-
setOllamaDeletePickerIndex((prev) => (prev + 1) % ollamaDeletePicker.models.length);
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
if (key.escape) {
|
|
1138
|
-
setOllamaDeletePicker(null);
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
if (key.return) {
|
|
1142
|
-
const selected = ollamaDeletePicker.models[ollamaDeletePickerIndex];
|
|
1143
|
-
if (selected) {
|
|
1144
|
-
setOllamaDeletePicker(null);
|
|
1145
|
-
setOllamaDeleteConfirm({ model: selected.name, size: selected.size });
|
|
1146
|
-
}
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
// ── Ollama pull picker ──
|
|
1152
|
-
if (ollamaPullPicker) {
|
|
1153
|
-
const pullModels = [
|
|
1154
|
-
{ id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
|
|
1155
|
-
{ id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
|
|
1156
|
-
{ id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
|
|
1157
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
|
|
1158
|
-
{ id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
|
|
1159
|
-
{ id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
|
|
1160
|
-
{ id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
|
|
1161
|
-
];
|
|
1162
|
-
if (key.upArrow) {
|
|
1163
|
-
setOllamaPullPickerIndex((prev) => (prev - 1 + pullModels.length) % pullModels.length);
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
if (key.downArrow) {
|
|
1167
|
-
setOllamaPullPickerIndex((prev) => (prev + 1) % pullModels.length);
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
if (key.escape) {
|
|
1171
|
-
setOllamaPullPicker(false);
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
if (key.return) {
|
|
1175
|
-
const selected = pullModels[ollamaPullPickerIndex];
|
|
1176
|
-
if (selected) {
|
|
1177
|
-
setOllamaPullPicker(false);
|
|
1178
|
-
// Trigger the pull
|
|
1179
|
-
setInput(`/ollama pull ${selected.id}`);
|
|
1180
|
-
setInputKey((k) => k + 1);
|
|
1181
|
-
// Submit it
|
|
1182
|
-
setTimeout(() => {
|
|
1183
|
-
const submitInput = `/ollama pull ${selected.id}`;
|
|
1184
|
-
setInput("");
|
|
1185
|
-
handleSubmit(submitInput);
|
|
1186
|
-
}, 50);
|
|
1187
|
-
}
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
// ── Ollama delete confirmation ──
|
|
1193
|
-
if (ollamaDeleteConfirm) {
|
|
1194
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1195
|
-
const model = ollamaDeleteConfirm.model;
|
|
1196
|
-
setOllamaDeleteConfirm(null);
|
|
1197
|
-
const result = deleteModel(model);
|
|
1198
|
-
addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
1202
|
-
setOllamaDeleteConfirm(null);
|
|
1203
|
-
addMsg("info", "Delete cancelled.");
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
// ── Ollama exit prompt ──
|
|
1209
|
-
if (ollamaExitPrompt) {
|
|
1210
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1211
|
-
setOllamaExitPrompt(false);
|
|
1212
|
-
stopOllama().then(() => exit());
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
if (inputChar === "n" || inputChar === "N") {
|
|
1216
|
-
setOllamaExitPrompt(false);
|
|
1217
|
-
exit();
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
if (inputChar === "a" || inputChar === "A") {
|
|
1221
|
-
setOllamaExitPrompt(false);
|
|
1222
|
-
saveConfig({ defaults: { ...loadConfig().defaults, stopOllamaOnExit: true } });
|
|
1223
|
-
addMsg("info", "Saved preference: always stop Ollama on exit.");
|
|
1224
|
-
stopOllama().then(() => exit());
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
if (key.escape) {
|
|
1228
|
-
setOllamaExitPrompt(false);
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
// ── Setup Wizard Navigation ──
|
|
1234
|
-
if (wizardScreen) {
|
|
1235
|
-
if (wizardScreen === "connection") {
|
|
1236
|
-
const items = ["local", "openrouter", "apikey", "existing"];
|
|
1237
|
-
if (key.upArrow) {
|
|
1238
|
-
setWizardIndex((prev) => (prev - 1 + items.length) % items.length);
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
if (key.downArrow) {
|
|
1242
|
-
setWizardIndex((prev) => (prev + 1) % items.length);
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
if (key.escape) {
|
|
1246
|
-
setWizardScreen(null);
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
if (key.return) {
|
|
1250
|
-
const selected = items[wizardIndex];
|
|
1251
|
-
if (selected === "local") {
|
|
1252
|
-
// Scan hardware and show model picker (use llmfit if available)
|
|
1253
|
-
const hw = detectHardware();
|
|
1254
|
-
setWizardHardware(hw);
|
|
1255
|
-
const { models: recs } = getRecommendationsWithLlmfit(hw);
|
|
1256
|
-
setWizardModels(recs.filter(m => m.fit !== "skip"));
|
|
1257
|
-
setWizardScreen("models");
|
|
1258
|
-
setWizardIndex(0);
|
|
1259
|
-
}
|
|
1260
|
-
else if (selected === "openrouter") {
|
|
1261
|
-
setWizardScreen(null);
|
|
1262
|
-
addMsg("info", "Starting OpenRouter OAuth — opening browser...");
|
|
1263
|
-
setLoading(true);
|
|
1264
|
-
setSpinnerMsg("Waiting for authorization...");
|
|
1265
|
-
openRouterOAuth((msg) => addMsg("info", msg))
|
|
1266
|
-
.then(() => {
|
|
1267
|
-
addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
|
|
1268
|
-
setLoading(false);
|
|
1269
|
-
})
|
|
1270
|
-
.catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
|
|
1271
|
-
}
|
|
1272
|
-
else if (selected === "apikey") {
|
|
1273
|
-
setWizardScreen(null);
|
|
1274
|
-
setLoginPicker(true);
|
|
1275
|
-
setLoginPickerIndex(0);
|
|
1276
|
-
}
|
|
1277
|
-
else if (selected === "existing") {
|
|
1278
|
-
setWizardScreen(null);
|
|
1279
|
-
addMsg("info", "Start your LLM server, then type /connect to retry.");
|
|
1280
|
-
}
|
|
1281
|
-
return;
|
|
1282
|
-
}
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
if (wizardScreen === "models") {
|
|
1286
|
-
const models = wizardModels;
|
|
1287
|
-
if (key.upArrow) {
|
|
1288
|
-
setWizardIndex((prev) => (prev - 1 + models.length) % models.length);
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
if (key.downArrow) {
|
|
1292
|
-
setWizardIndex((prev) => (prev + 1) % models.length);
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
if (key.escape) {
|
|
1296
|
-
setWizardScreen("connection");
|
|
1297
|
-
setWizardIndex(0);
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
if (key.return) {
|
|
1301
|
-
const selected = models[wizardIndex];
|
|
1302
|
-
if (selected) {
|
|
1303
|
-
setWizardSelectedModel(selected);
|
|
1304
|
-
// Check if Ollama is installed
|
|
1305
|
-
if (!isOllamaInstalled()) {
|
|
1306
|
-
setWizardScreen("install-ollama");
|
|
1307
|
-
}
|
|
1308
|
-
else {
|
|
1309
|
-
// Start pulling the model
|
|
1310
|
-
setWizardScreen("pulling");
|
|
1311
|
-
setWizardPullProgress({ status: "starting", percent: 0 });
|
|
1312
|
-
setWizardPullError(null);
|
|
1313
|
-
(async () => {
|
|
1314
|
-
try {
|
|
1315
|
-
// Ensure ollama is running
|
|
1316
|
-
const running = await isOllamaRunning();
|
|
1317
|
-
if (!running) {
|
|
1318
|
-
setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
|
|
1319
|
-
startOllama();
|
|
1320
|
-
// Wait for it to come up
|
|
1321
|
-
for (let i = 0; i < 15; i++) {
|
|
1322
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1323
|
-
if (await isOllamaRunning())
|
|
1324
|
-
break;
|
|
1325
|
-
}
|
|
1326
|
-
if (!(await isOllamaRunning())) {
|
|
1327
|
-
setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
await pullModel(selected.ollamaId, (p) => {
|
|
1332
|
-
setWizardPullProgress(p);
|
|
1333
|
-
});
|
|
1334
|
-
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1335
|
-
// Wait briefly then connect
|
|
1336
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1337
|
-
setWizardScreen(null);
|
|
1338
|
-
setWizardPullProgress(null);
|
|
1339
|
-
setWizardSelectedModel(null);
|
|
1340
|
-
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1341
|
-
await connectToProvider(true);
|
|
1342
|
-
}
|
|
1343
|
-
catch (err) {
|
|
1344
|
-
setWizardPullError(err.message);
|
|
1345
|
-
}
|
|
1346
|
-
})();
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
return;
|
|
1352
|
-
}
|
|
1353
|
-
if (wizardScreen === "install-ollama") {
|
|
1354
|
-
if (key.escape) {
|
|
1355
|
-
setWizardScreen("models");
|
|
1356
|
-
setWizardIndex(0);
|
|
1357
|
-
return;
|
|
1358
|
-
}
|
|
1359
|
-
if (key.return) {
|
|
1360
|
-
// Auto-install Ollama if not present
|
|
1361
|
-
if (!isOllamaInstalled()) {
|
|
1362
|
-
setLoading(true);
|
|
1363
|
-
setSpinnerMsg("Installing Ollama... this may take a minute");
|
|
1364
|
-
// Run install async so the UI can update
|
|
1365
|
-
const installCmd = getOllamaInstallCommand(wizardHardware?.os ?? "linux");
|
|
1366
|
-
(async () => {
|
|
1367
|
-
try {
|
|
1368
|
-
const { exec } = _require("child_process");
|
|
1369
|
-
await new Promise((resolve, reject) => {
|
|
1370
|
-
exec(installCmd, { timeout: 180000 }, (err, _stdout, stderr) => {
|
|
1371
|
-
if (err)
|
|
1372
|
-
reject(new Error(stderr || err.message));
|
|
1373
|
-
else
|
|
1374
|
-
resolve();
|
|
1375
|
-
});
|
|
1376
|
-
});
|
|
1377
|
-
addMsg("info", "✅ Ollama installed! Proceeding to model download...");
|
|
1378
|
-
setLoading(false);
|
|
1379
|
-
// Small delay for PATH to update on Windows
|
|
1380
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
1381
|
-
// Go back to models screen so user can pick and it'll proceed to pull
|
|
1382
|
-
setWizardScreen("models");
|
|
1383
|
-
}
|
|
1384
|
-
catch (e) {
|
|
1385
|
-
addMsg("error", `Install failed: ${e.message}`);
|
|
1386
|
-
addMsg("info", `Try manually in a separate terminal: ${installCmd}`);
|
|
1387
|
-
setLoading(false);
|
|
1388
|
-
setWizardScreen("install-ollama");
|
|
1389
|
-
}
|
|
1390
|
-
})();
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
// Ollama already installed — proceed to pull
|
|
1394
|
-
{
|
|
1395
|
-
const selected = wizardSelectedModel;
|
|
1396
|
-
if (selected) {
|
|
1397
|
-
setWizardScreen("pulling");
|
|
1398
|
-
setWizardPullProgress({ status: "starting", percent: 0 });
|
|
1399
|
-
setWizardPullError(null);
|
|
1400
|
-
(async () => {
|
|
1401
|
-
try {
|
|
1402
|
-
const running = await isOllamaRunning();
|
|
1403
|
-
if (!running) {
|
|
1404
|
-
setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
|
|
1405
|
-
startOllama();
|
|
1406
|
-
for (let i = 0; i < 15; i++) {
|
|
1407
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1408
|
-
if (await isOllamaRunning())
|
|
1409
|
-
break;
|
|
1410
|
-
}
|
|
1411
|
-
if (!(await isOllamaRunning())) {
|
|
1412
|
-
setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
|
|
1417
|
-
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1418
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1419
|
-
setWizardScreen(null);
|
|
1420
|
-
setWizardPullProgress(null);
|
|
1421
|
-
setWizardSelectedModel(null);
|
|
1422
|
-
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1423
|
-
await connectToProvider(true);
|
|
1424
|
-
}
|
|
1425
|
-
catch (err) {
|
|
1426
|
-
setWizardPullError(err.message);
|
|
1427
|
-
}
|
|
1428
|
-
})();
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
return;
|
|
1432
|
-
}
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
|
-
if (wizardScreen === "pulling") {
|
|
1436
|
-
// Allow retry on error
|
|
1437
|
-
if (wizardPullError && key.return) {
|
|
1438
|
-
const selected = wizardSelectedModel;
|
|
1439
|
-
if (selected) {
|
|
1440
|
-
setWizardPullError(null);
|
|
1441
|
-
setWizardPullProgress({ status: "retrying", percent: 0 });
|
|
1442
|
-
(async () => {
|
|
1443
|
-
try {
|
|
1444
|
-
const running = await isOllamaRunning();
|
|
1445
|
-
if (!running) {
|
|
1446
|
-
startOllama();
|
|
1447
|
-
for (let i = 0; i < 15; i++) {
|
|
1448
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1449
|
-
if (await isOllamaRunning())
|
|
1450
|
-
break;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
|
|
1454
|
-
setWizardPullProgress({ status: "success", percent: 100 });
|
|
1455
|
-
await new Promise(r => setTimeout(r, 500));
|
|
1456
|
-
setWizardScreen(null);
|
|
1457
|
-
setWizardPullProgress(null);
|
|
1458
|
-
setWizardSelectedModel(null);
|
|
1459
|
-
addMsg("info", `✅ ${selected.name} installed! Connecting...`);
|
|
1460
|
-
await connectToProvider(true);
|
|
1461
|
-
}
|
|
1462
|
-
catch (err) {
|
|
1463
|
-
setWizardPullError(err.message);
|
|
1464
|
-
}
|
|
1465
|
-
})();
|
|
1466
|
-
}
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
if (wizardPullError && key.escape) {
|
|
1470
|
-
setWizardScreen("models");
|
|
1471
|
-
setWizardIndex(0);
|
|
1472
|
-
setWizardPullError(null);
|
|
1473
|
-
setWizardPullProgress(null);
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
return; // Ignore keys while pulling
|
|
1477
|
-
}
|
|
1478
|
-
return;
|
|
1479
|
-
}
|
|
1480
|
-
// Theme picker navigation
|
|
1481
|
-
if (themePicker) {
|
|
1482
|
-
const themeKeys = listThemes();
|
|
1483
|
-
if (key.upArrow) {
|
|
1484
|
-
setThemePickerIndex((prev) => (prev - 1 + themeKeys.length) % themeKeys.length);
|
|
1485
|
-
return;
|
|
1486
|
-
}
|
|
1487
|
-
if (key.downArrow) {
|
|
1488
|
-
setThemePickerIndex((prev) => (prev + 1) % themeKeys.length);
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
if (key.return) {
|
|
1492
|
-
const selected = themeKeys[themePickerIndex];
|
|
1493
|
-
setTheme(getTheme(selected));
|
|
1494
|
-
setThemePicker(false);
|
|
1495
|
-
addMsg("info", `✅ Switched to theme: ${THEMES[selected].name}`);
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
if (key.escape) {
|
|
1499
|
-
setThemePicker(false);
|
|
1500
|
-
return;
|
|
1501
|
-
}
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
// Session picker navigation
|
|
1505
|
-
if (sessionPicker) {
|
|
1506
|
-
if (key.upArrow) {
|
|
1507
|
-
setSessionPickerIndex((prev) => (prev - 1 + sessionPicker.length) % sessionPicker.length);
|
|
1508
|
-
return;
|
|
1509
|
-
}
|
|
1510
|
-
if (key.downArrow) {
|
|
1511
|
-
setSessionPickerIndex((prev) => (prev + 1) % sessionPicker.length);
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
if (key.return) {
|
|
1515
|
-
const selected = sessionPicker[sessionPickerIndex];
|
|
1516
|
-
if (selected && agent) {
|
|
1517
|
-
const session = getSession(selected.id);
|
|
1518
|
-
if (session) {
|
|
1519
|
-
agent.resume(selected.id).then(() => {
|
|
1520
|
-
const dir = session.cwd.split("/").pop() || session.cwd;
|
|
1521
|
-
// Find last user message for context
|
|
1522
|
-
const msgs = loadMessages(selected.id);
|
|
1523
|
-
const lastUserMsg = [...msgs].reverse().find(m => m.role === "user");
|
|
1524
|
-
const lastText = lastUserMsg && typeof lastUserMsg.content === "string"
|
|
1525
|
-
? lastUserMsg.content.slice(0, 80) + (lastUserMsg.content.length > 80 ? "..." : "")
|
|
1526
|
-
: null;
|
|
1527
|
-
let info = `✅ Resumed session ${selected.id} (${dir}/, ${session.message_count} messages)`;
|
|
1528
|
-
if (lastText)
|
|
1529
|
-
info += `\n Last: "${lastText}"`;
|
|
1530
|
-
addMsg("info", info);
|
|
1531
|
-
}).catch((e) => {
|
|
1532
|
-
addMsg("error", `Failed to resume: ${e.message}`);
|
|
1533
|
-
});
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
setSessionPicker(null);
|
|
1537
|
-
setSessionPickerIndex(0);
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
if (key.escape) {
|
|
1541
|
-
setSessionPicker(null);
|
|
1542
|
-
setSessionPickerIndex(0);
|
|
1543
|
-
addMsg("info", "Resume cancelled.");
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
return; // Ignore other keys during session picker
|
|
1547
|
-
}
|
|
1548
|
-
// Delete session confirmation (y/n)
|
|
1549
|
-
if (deleteSessionConfirm) {
|
|
1550
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1551
|
-
const deleted = deleteSession(deleteSessionConfirm.id);
|
|
1552
|
-
if (deleted) {
|
|
1553
|
-
addMsg("info", `✅ Deleted session ${deleteSessionConfirm.id}`);
|
|
1554
|
-
}
|
|
1555
|
-
else {
|
|
1556
|
-
addMsg("error", `Failed to delete session ${deleteSessionConfirm.id}`);
|
|
1557
|
-
}
|
|
1558
|
-
setDeleteSessionConfirm(null);
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
if (inputChar === "n" || inputChar === "N" || key.escape) {
|
|
1562
|
-
addMsg("info", "Delete cancelled.");
|
|
1563
|
-
setDeleteSessionConfirm(null);
|
|
1564
|
-
return;
|
|
1565
|
-
}
|
|
1566
|
-
return;
|
|
1567
|
-
}
|
|
1568
|
-
// Delete session picker navigation
|
|
1569
|
-
if (deleteSessionPicker) {
|
|
1570
|
-
if (key.upArrow) {
|
|
1571
|
-
setDeleteSessionPickerIndex((prev) => (prev - 1 + deleteSessionPicker.length) % deleteSessionPicker.length);
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
if (key.downArrow) {
|
|
1575
|
-
setDeleteSessionPickerIndex((prev) => (prev + 1) % deleteSessionPicker.length);
|
|
1576
|
-
return;
|
|
1577
|
-
}
|
|
1578
|
-
if (key.return) {
|
|
1579
|
-
const selected = deleteSessionPicker[deleteSessionPickerIndex];
|
|
1580
|
-
if (selected) {
|
|
1581
|
-
setDeleteSessionPicker(null);
|
|
1582
|
-
setDeleteSessionPickerIndex(0);
|
|
1583
|
-
setDeleteSessionConfirm(selected);
|
|
1584
|
-
}
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
if (key.escape) {
|
|
1588
|
-
setDeleteSessionPicker(null);
|
|
1589
|
-
setDeleteSessionPickerIndex(0);
|
|
1590
|
-
addMsg("info", "Delete cancelled.");
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
// Backspace with empty input → remove last paste chunk
|
|
1596
|
-
if (key.backspace || key.delete) {
|
|
1597
|
-
if (input === "" && pastedChunksRef.current.length > 0) {
|
|
1598
|
-
setPastedChunks((prev) => prev.slice(0, -1));
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1602
|
-
// Handle approval prompts
|
|
1603
|
-
if (approval) {
|
|
1604
|
-
if (inputChar === "y" || inputChar === "Y") {
|
|
1605
|
-
const r = approval.resolve;
|
|
1606
|
-
setApproval(null);
|
|
1607
|
-
setLoading(true);
|
|
1608
|
-
setSpinnerMsg("Executing...");
|
|
1609
|
-
r("yes");
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
if (inputChar === "n" || inputChar === "N") {
|
|
1613
|
-
const r = approval.resolve;
|
|
1614
|
-
setApproval(null);
|
|
1615
|
-
addMsg("info", "✗ Denied");
|
|
1616
|
-
r("no");
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
if (inputChar === "a" || inputChar === "A") {
|
|
1620
|
-
const r = approval.resolve;
|
|
1621
|
-
setApproval(null);
|
|
1622
|
-
setLoading(true);
|
|
1623
|
-
setSpinnerMsg("Executing...");
|
|
1624
|
-
addMsg("info", `✔ Always allow ${approval.tool} for this session`);
|
|
1625
|
-
r("always");
|
|
1626
|
-
return;
|
|
1627
|
-
}
|
|
1628
|
-
return; // Ignore other keys during approval
|
|
1629
|
-
}
|
|
1630
|
-
if (key.ctrl && inputChar === "c") {
|
|
1631
|
-
if (ctrlCPressed) {
|
|
1632
|
-
// Force quit on second Ctrl+C — don't block
|
|
1633
|
-
const config = loadConfig();
|
|
1634
|
-
if (config.defaults.stopOllamaOnExit) {
|
|
1635
|
-
stopOllama().finally(() => exit());
|
|
1636
|
-
}
|
|
1637
|
-
else {
|
|
1638
|
-
exit();
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
else {
|
|
1642
|
-
setCtrlCPressed(true);
|
|
1643
|
-
addMsg("info", "Press Ctrl+C again to exit.");
|
|
1644
|
-
setTimeout(() => setCtrlCPressed(false), 3000);
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
741
|
+
routeKeyPress(inputChar, key, {
|
|
742
|
+
showSuggestionsRef,
|
|
743
|
+
cmdMatchesRef,
|
|
744
|
+
cmdIndexRef,
|
|
745
|
+
setCmdIndex,
|
|
746
|
+
setInput,
|
|
747
|
+
setInputKey,
|
|
748
|
+
loginMethodPicker,
|
|
749
|
+
loginMethodIndex,
|
|
750
|
+
setLoginMethodIndex,
|
|
751
|
+
setLoginMethodPicker,
|
|
752
|
+
loginPicker,
|
|
753
|
+
loginPickerIndex,
|
|
754
|
+
setLoginPickerIndex,
|
|
755
|
+
setLoginPicker,
|
|
756
|
+
skillsPicker,
|
|
757
|
+
skillsPickerIndex,
|
|
758
|
+
setSkillsPickerIndex,
|
|
759
|
+
setSkillsPicker,
|
|
760
|
+
sessionDisabledSkills,
|
|
761
|
+
setSessionDisabledSkills,
|
|
762
|
+
modelPickerGroups,
|
|
763
|
+
modelPickerIndex,
|
|
764
|
+
setModelPickerIndex,
|
|
765
|
+
setModelPickerGroups,
|
|
766
|
+
flatModelList,
|
|
767
|
+
setFlatModelList,
|
|
768
|
+
providerPicker,
|
|
769
|
+
providerPickerIndex,
|
|
770
|
+
setProviderPickerIndex,
|
|
771
|
+
setProviderPicker,
|
|
772
|
+
selectedProvider,
|
|
773
|
+
setSelectedProvider,
|
|
774
|
+
ollamaDeletePicker,
|
|
775
|
+
ollamaDeletePickerIndex,
|
|
776
|
+
setOllamaDeletePickerIndex,
|
|
777
|
+
setOllamaDeletePicker,
|
|
778
|
+
ollamaPullPicker,
|
|
779
|
+
ollamaPullPickerIndex,
|
|
780
|
+
setOllamaPullPickerIndex,
|
|
781
|
+
setOllamaPullPicker,
|
|
782
|
+
ollamaDeleteConfirm,
|
|
783
|
+
setOllamaDeleteConfirm,
|
|
784
|
+
ollamaExitPrompt,
|
|
785
|
+
setOllamaExitPrompt,
|
|
786
|
+
wizardScreen,
|
|
787
|
+
wizardIndex,
|
|
788
|
+
wizardModels,
|
|
789
|
+
wizardHardware,
|
|
790
|
+
wizardPullProgress,
|
|
791
|
+
wizardPullError,
|
|
792
|
+
wizardSelectedModel,
|
|
793
|
+
setWizardScreen,
|
|
794
|
+
setWizardIndex,
|
|
795
|
+
setWizardHardware,
|
|
796
|
+
setWizardModels,
|
|
797
|
+
setWizardPullProgress,
|
|
798
|
+
setWizardPullError,
|
|
799
|
+
setWizardSelectedModel,
|
|
800
|
+
themePicker,
|
|
801
|
+
themePickerIndex,
|
|
802
|
+
setThemePickerIndex,
|
|
803
|
+
setThemePicker,
|
|
804
|
+
setTheme,
|
|
805
|
+
sessionPicker,
|
|
806
|
+
sessionPickerIndex,
|
|
807
|
+
setSessionPickerIndex,
|
|
808
|
+
setSessionPicker,
|
|
809
|
+
deleteSessionConfirm,
|
|
810
|
+
setDeleteSessionConfirm,
|
|
811
|
+
deleteSessionPicker,
|
|
812
|
+
deleteSessionPickerIndex,
|
|
813
|
+
setDeleteSessionPickerIndex,
|
|
814
|
+
setDeleteSessionPicker,
|
|
815
|
+
input,
|
|
816
|
+
pastedChunksRef,
|
|
817
|
+
setPastedChunks,
|
|
818
|
+
approval,
|
|
819
|
+
setApproval,
|
|
820
|
+
ctrlCPressed,
|
|
821
|
+
setCtrlCPressed,
|
|
822
|
+
setLoading,
|
|
823
|
+
setSpinnerMsg,
|
|
824
|
+
agent,
|
|
825
|
+
setModelName,
|
|
826
|
+
addMsg,
|
|
827
|
+
exit,
|
|
828
|
+
refreshConnectionBanner,
|
|
829
|
+
connectToProvider,
|
|
830
|
+
handleSubmit,
|
|
831
|
+
_require,
|
|
832
|
+
});
|
|
1647
833
|
});
|
|
1648
|
-
|
|
1649
|
-
const codeLines = [
|
|
1650
|
-
" _(`-') (`-') _ ",
|
|
1651
|
-
" _ .-> ( (OO ).-> ( OO).-/ ",
|
|
1652
|
-
" \\-,-----.(`-')----. \\ .'_ (,------. ",
|
|
1653
|
-
" | .--./( OO).-. ''`'-..__) | .---' ",
|
|
1654
|
-
" /_) (`-')( _) | | || | ' |(| '--. ",
|
|
1655
|
-
" || |OO ) \\| |)| || | / : | .--' ",
|
|
1656
|
-
"(_' '--'\\ ' '-' '| '-' / | `---. ",
|
|
1657
|
-
" `-----' `-----' `------' `------' ",
|
|
1658
|
-
];
|
|
1659
|
-
const maxxingLines = [
|
|
1660
|
-
"<-. (`-') (`-') _ (`-') (`-') _ <-. (`-')_ ",
|
|
1661
|
-
" \\(OO )_ (OO ).-/ (OO )_.-> (OO )_.-> (_) \\( OO) ) .-> ",
|
|
1662
|
-
",--./ ,-.) / ,---. (_| \\_)--. (_| \\_)--.,-(`-'),--./ ,--/ ,---(`-') ",
|
|
1663
|
-
"| `.' | | \\ /`.\\ \\ `.' / \\ `.' / | ( OO)| \\ | | ' .-(OO ) ",
|
|
1664
|
-
"| |'.'| | '-'|_.' | \\ .') \\ .') | | )| . '| |)| | .-, \\ ",
|
|
1665
|
-
"| | | |(| .-. | .' \\ .' \\ (| |_/ | |\\ | | | '.(_/ ",
|
|
1666
|
-
"| | | | | | | | / .'. \\ / .'. \\ | |'->| | \\ | | '-' | ",
|
|
1667
|
-
"`--' `--' `--' `--'`--' '--'`--' '--'`--' `--' `--' `-----' ",
|
|
1668
|
-
];
|
|
1669
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.colors.border, paddingX: 1, children: [codeLines.map((line, i) => (_jsx(Text, { color: theme.colors.primary, children: line }, `c${i}`))), maxxingLines.map((line, i) => (_jsx(Text, { color: theme.colors.secondary, children: line }, `m${i}`))), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.muted, children: " v" + VERSION }), " ", _jsx(Text, { color: theme.colors.primary, children: "\uD83D\uDCAA" }), " ", _jsx(Text, { dimColor: true, children: "your code. your model. no excuses." })] }), _jsxs(Text, { dimColor: true, children: [" Type ", _jsx(Text, { color: theme.colors.muted, children: "/help" }), " for commands · ", _jsx(Text, { color: theme.colors.muted, children: "Ctrl+C" }), " twice to exit"] })] }), connectionInfo.length > 0 && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 1, children: connectionInfo.map((line, i) => (_jsx(Text, { color: line.startsWith("✔") ? theme.colors.primary : line.startsWith("✗") ? theme.colors.error : theme.colors.muted, children: line }, i))) })), messages.map((msg) => {
|
|
834
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Banner, { version: VERSION, colors: theme.colors }), connectionInfo.length > 0 && (_jsx(ConnectionInfo, { connectionInfo: connectionInfo, colors: theme.colors })), messages.map((msg) => {
|
|
1670
835
|
switch (msg.type) {
|
|
1671
836
|
case "user":
|
|
1672
837
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.userInput, children: [" > ", msg.text] }) }, msg.id));
|
|
@@ -1686,224 +851,12 @@ function App() {
|
|
|
1686
851
|
default:
|
|
1687
852
|
return _jsx(Text, { children: msg.text }, msg.id);
|
|
1688
853
|
}
|
|
1689
|
-
}), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (
|
|
1690
|
-
line.startsWith("-") ? theme.colors.error :
|
|
1691
|
-
line.startsWith("@@") ? theme.colors.primary :
|
|
1692
|
-
theme.colors.muted, children: line }, i))), approval.diff.split("\n").length > 40 ? (_jsxs(Text, { color: theme.colors.muted, children: ["... (", approval.diff.split("\n").length - 40, " more lines)"] })) : null] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: theme.colors.muted, children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), loginPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "\uD83D\uDCAA Choose a provider:" }), PROVIDERS.filter((p) => p.id !== "local").map((p, i) => (_jsxs(Text, { children: [i === loginPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginPickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: p.name }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", p.description] }), getCredentials().some((c) => c.provider === p.id) ? _jsx(Text, { color: theme.colors.success, children: " \u2713" }) : null] }, p.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), loginMethodPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "How do you want to authenticate?" }), loginMethodPicker.methods.map((method, i) => {
|
|
1693
|
-
const labels = {
|
|
1694
|
-
"oauth": "🌐 Browser login (OAuth)",
|
|
1695
|
-
"setup-token": "🔑 Link subscription (via Claude Code CLI)",
|
|
1696
|
-
"cached-token": "📦 Import from existing CLI",
|
|
1697
|
-
"api-key": "🔒 Enter API key manually",
|
|
1698
|
-
"device-flow": "📱 Device flow (GitHub)",
|
|
1699
|
-
};
|
|
1700
|
-
return (_jsxs(Text, { children: [i === loginMethodIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginMethodIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: labels[method] ?? method })] }, method));
|
|
1701
|
-
}), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc back" })] })), skillsPicker === "menu" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Skills:" }), [
|
|
1702
|
-
{ key: "browse", label: "Browse & Install", icon: "📦" },
|
|
1703
|
-
{ key: "installed", label: "Installed Skills", icon: "📋" },
|
|
1704
|
-
{ key: "create", label: "Create Custom Skill", icon: "➕" },
|
|
1705
|
-
{ key: "remove", label: "Remove Skill", icon: "🗑️" },
|
|
1706
|
-
].map((item, i) => (_jsxs(Text, { children: [i === skillsPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: [item.icon, " ", item.label] })] }, item.key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), skillsPicker === "browse" && (() => {
|
|
1707
|
-
const registry = getRegistrySkills();
|
|
1708
|
-
const installed = listInstalledSkills().map((s) => s.name);
|
|
1709
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Browse Skills Registry:" }), registry.map((s, i) => (_jsxs(Text, { children: [i === skillsPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: s.name }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", s.description] }), installed.includes(s.name) ? _jsx(Text, { color: theme.colors.success, children: " \u2713" }) : null] }, s.name))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter install · Esc back" })] }));
|
|
1710
|
-
})(), skillsPicker === "installed" && (() => {
|
|
1711
|
-
const installed = listInstalledSkills();
|
|
1712
|
-
const active = getActiveSkills(process.cwd(), sessionDisabledSkills);
|
|
1713
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Installed Skills:" }), installed.length === 0 ? (_jsx(Text, { color: theme.colors.muted, children: " No skills installed. Use Browse & Install." })) : installed.map((s, i) => (_jsxs(Text, { children: [i === skillsPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: s.name }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", s.description] }), active.includes(s.name) ? _jsx(Text, { color: theme.colors.success, children: " (on)" }) : _jsx(Text, { color: theme.colors.muted, children: " (off)" })] }, s.name))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter toggle · Esc back" })] }));
|
|
1714
|
-
})(), skillsPicker === "remove" && (() => {
|
|
1715
|
-
const installed = listInstalledSkills();
|
|
1716
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Remove a skill:" }), installed.map((s, i) => (_jsxs(Text, { children: [i === skillsPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: [s.name, " \u2014 ", s.description] })] }, s.name))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter remove · Esc back" })] }));
|
|
1717
|
-
})(), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Delete a session:" }), deleteSessionPicker.map((s, i) => (_jsxs(Text, { children: [i === deleteSessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete session ", deleteSessionConfirm.id, "?"] }), _jsxs(Text, { color: theme.colors.muted, children: [" ", deleteSessionConfirm.display] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), modelPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Switch model:" }), _jsx(Text, { children: "" }), modelPicker.map((m, i) => (_jsxs(Text, { children: [" ", i === modelPickerIndex ? _jsx(Text, { color: theme.colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === modelPickerIndex ? theme.colors.primary : undefined, children: m }), m === modelName ? _jsx(Text, { color: theme.colors.success, children: " (active)" }) : null] }, m))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to switch · Esc cancel" })] })), ollamaDeletePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Delete which model?" }), _jsx(Text, { children: "" }), ollamaDeletePicker.models.map((m, i) => (_jsxs(Text, { children: [" ", i === ollamaDeletePickerIndex ? _jsx(Text, { color: theme.colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === ollamaDeletePickerIndex ? theme.colors.primary : undefined, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" (", (m.size / (1024 * 1024 * 1024)).toFixed(1), " GB)"] })] }, m.name))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to delete · Esc cancel" })] })), ollamaPullPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Download which model?" }), _jsx(Text, { children: "" }), [
|
|
1718
|
-
{ id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
|
|
1719
|
-
{ id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
|
|
1720
|
-
{ id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
|
|
1721
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
|
|
1722
|
-
{ id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
|
|
1723
|
-
{ id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
|
|
1724
|
-
{ id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
|
|
1725
|
-
].map((m, i) => (_jsxs(Text, { children: [" ", i === ollamaPullPickerIndex ? _jsx(Text, { color: theme.colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === ollamaPullPickerIndex ? theme.colors.primary : undefined, bold: true, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" · ", m.size, " · ", m.desc] })] }, m.id))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to download · Esc cancel" })] })), ollamaDeleteConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete ", ollamaDeleteConfirm.model, " (", (ollamaDeleteConfirm.size / (1024 * 1024 * 1024)).toFixed(1), " GB)?"] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaPulling && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" Downloading ", ollamaPulling.model, "..."] }), ollamaPulling.progress.status === "downloading" || ollamaPulling.progress.percent > 0 ? (_jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(ollamaPulling.progress.percent / 5)), "\u2591".repeat(20 - Math.floor(ollamaPulling.progress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [ollamaPulling.progress.percent, "%"] }), ollamaPulling.progress.completed != null && ollamaPulling.progress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(ollamaPulling.progress.completed), " / ", formatBytes(ollamaPulling.progress.total)] })) : null] })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", ollamaPulling.progress.status, "..."] }))] })), ollamaExitPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.warning, children: "Ollama is still running." }), _jsx(Text, { color: theme.colors.muted, children: "Stop it to free GPU memory?" }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), wizardScreen === "connection" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "No LLM detected. How do you want to connect?" }), _jsx(Text, { children: "" }), [
|
|
1726
|
-
{ key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
|
|
1727
|
-
{ key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
|
|
1728
|
-
{ key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
|
|
1729
|
-
{ key: "existing", icon: "\u2699\uFE0F", label: "I already have a server running", desc: "" },
|
|
1730
|
-
].map((item, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === wizardIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: [item.icon, " ", item.label] }), item.desc ? _jsxs(Text, { color: theme.colors.muted, children: [" (", item.desc, ")"] }) : null] }, item.key))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to select" })] })), wizardScreen === "models" && wizardHardware && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Your hardware:" }), _jsxs(Text, { color: theme.colors.muted, children: [" CPU: ", wizardHardware.cpu.name, " (", wizardHardware.cpu.cores, " cores)"] }), _jsxs(Text, { color: theme.colors.muted, children: [" RAM: ", formatBytes(wizardHardware.ram)] }), wizardHardware.gpu ? (_jsxs(Text, { color: theme.colors.muted, children: [" GPU: ", wizardHardware.gpu.name, wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""] })) : (_jsx(Text, { color: theme.colors.muted, children: " GPU: none detected" })), !isLlmfitAvailable() && (_jsx(Text, { dimColor: true, children: " Tip: Install llmfit for smarter recommendations: brew install llmfit" })), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Recommended models:" }), _jsx(Text, { children: "" }), wizardModels.map((m, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { children: [getFitIcon(m.fit), " "] }), _jsx(Text, { color: i === wizardIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" ~", m.size, " GB \u00B7 ", m.quality === "best" ? "Best" : m.quality === "great" ? "Great" : "Good", " quality \u00B7 ", m.speed] })] }, m.ollamaId))), wizardModels.length === 0 && (_jsx(Text, { color: theme.colors.error, children: " No suitable models found for your hardware." })), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to install \u00B7 Esc back" })] })), wizardScreen === "install-ollama" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.warning, children: "Ollama is required for local models." }), _jsx(Text, { children: "" }), _jsx(Text, { color: theme.colors.primary, children: " Press Enter to install Ollama automatically" }), _jsxs(Text, { dimColor: true, children: [" Or install manually: ", _jsx(Text, { children: getOllamaInstallCommand(wizardHardware?.os ?? "linux") })] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Enter to install · Esc to go back" })] })), wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: wizardPullError ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.colors.error, bold: true, children: [" \u274C Error: ", wizardPullError] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Press Enter to retry \u00B7 Esc to go back" })] })) : wizardPullProgress ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" ", wizardSelectedModel ? `Downloading ${wizardSelectedModel.name}...` : wizardPullProgress?.status || "Working..."] }), wizardPullProgress.status === "downloading" || wizardPullProgress.percent > 0 ? (_jsx(_Fragment, { children: _jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(wizardPullProgress.percent / 5)), "\u2591".repeat(20 - Math.floor(wizardPullProgress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [wizardPullProgress.percent, "%"] }), wizardPullProgress.completed != null && wizardPullProgress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(wizardPullProgress.completed), " / ", formatBytes(wizardPullProgress.total)] })) : null] }) })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", wizardPullProgress.status, "..."] }))] })) : null })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading && !wizardScreen ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(sanitizeInputArtifacts(v)); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
|
|
1731
|
-
const tokens = agent.estimateTokens();
|
|
1732
|
-
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
|
|
1733
|
-
})(), " tokens", (() => {
|
|
1734
|
-
const { totalCost } = agent.getCostInfo();
|
|
1735
|
-
if (totalCost > 0) {
|
|
1736
|
-
return ` · 💰 $${totalCost < 0.01 ? totalCost.toFixed(4) : totalCost.toFixed(2)}`;
|
|
1737
|
-
}
|
|
1738
|
-
return "";
|
|
1739
|
-
})(), modelName ? ` · 🤖 ${modelName}` : "", (() => {
|
|
1740
|
-
const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
|
|
1741
|
-
return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
|
|
1742
|
-
})(), agent.getArchitectModel() ? " · 🏗️ architect" : ""] }) }))] }));
|
|
854
|
+
}), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsx(ApprovalPrompt, { approval: approval, colors: theme.colors })), loginPicker && (_jsx(LoginPicker, { loginPickerIndex: loginPickerIndex, colors: theme.colors })), loginMethodPicker && (_jsx(LoginMethodPickerUI, { loginMethodPicker: loginMethodPicker, loginMethodIndex: loginMethodIndex, colors: theme.colors })), skillsPicker === "menu" && (_jsx(SkillsMenu, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "browse" && (_jsx(SkillsBrowse, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "installed" && (_jsx(SkillsInstalled, { skillsPickerIndex: skillsPickerIndex, sessionDisabledSkills: sessionDisabledSkills, colors: theme.colors })), skillsPicker === "remove" && (_jsx(SkillsRemove, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), themePicker && (_jsx(ThemePickerUI, { themePickerIndex: themePickerIndex, theme: theme })), sessionPicker && (_jsx(SessionPicker, { sessions: sessionPicker, selectedIndex: sessionPickerIndex, colors: theme.colors })), deleteSessionPicker && (_jsx(DeleteSessionPicker, { sessions: deleteSessionPicker, selectedIndex: deleteSessionPickerIndex, colors: theme.colors })), deleteSessionConfirm && (_jsx(DeleteSessionConfirm, { session: deleteSessionConfirm, colors: theme.colors })), providerPicker && !selectedProvider && (_jsx(ProviderPicker, { providers: providerPicker, selectedIndex: providerPickerIndex, colors: theme.colors })), selectedProvider && modelPickerGroups && modelPickerGroups[selectedProvider] && (_jsx(ModelPicker, { providerName: selectedProvider, models: modelPickerGroups[selectedProvider], selectedIndex: modelPickerIndex, activeModel: modelName, colors: theme.colors })), ollamaDeletePicker && (_jsx(OllamaDeletePicker, { models: ollamaDeletePicker.models, selectedIndex: ollamaDeletePickerIndex, colors: theme.colors })), ollamaPullPicker && (_jsx(OllamaPullPicker, { selectedIndex: ollamaPullPickerIndex, colors: theme.colors })), ollamaDeleteConfirm && (_jsx(OllamaDeleteConfirm, { model: ollamaDeleteConfirm.model, size: ollamaDeleteConfirm.size, colors: theme.colors })), ollamaPulling && (_jsx(OllamaPullProgress, { model: ollamaPulling.model, progress: ollamaPulling.progress, colors: theme.colors })), ollamaExitPrompt && (_jsx(OllamaExitPrompt, { colors: theme.colors })), wizardScreen === "connection" && (_jsx(WizardConnection, { wizardIndex: wizardIndex, colors: theme.colors })), wizardScreen === "models" && wizardHardware && (_jsx(WizardModels, { wizardIndex: wizardIndex, wizardHardware: wizardHardware, wizardModels: wizardModels, colors: theme.colors })), wizardScreen === "install-ollama" && (_jsx(WizardInstallOllama, { wizardHardware: wizardHardware, colors: theme.colors })), wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (_jsx(WizardPulling, { wizardSelectedModel: wizardSelectedModel, wizardPullProgress: wizardPullProgress, wizardPullError: wizardPullError, colors: theme.colors })), showSuggestions && (_jsx(CommandSuggestions, { cmdMatches: cmdMatches, cmdIndex: cmdIndex, colors: theme.colors })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading && !wizardScreen ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(sanitizeInputArtifacts(v)); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(StatusBar, { agent: agent, modelName: modelName, sessionDisabledSkills: sessionDisabledSkills }))] }));
|
|
1743
855
|
}
|
|
1744
856
|
// Clear screen before render
|
|
1745
857
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
1746
|
-
//
|
|
1747
|
-
const pasteEvents =
|
|
1748
|
-
// Enable bracketed paste mode — terminal wraps pastes in escape sequences
|
|
1749
|
-
process.stdout.write("\x1b[?2004h");
|
|
1750
|
-
// Intercept stdin to handle pasted content
|
|
1751
|
-
// Strategy: patch emit('data') instead of push() — this is the ONE path all data
|
|
1752
|
-
// must travel through to reach Ink's listeners, regardless of how the TTY/stream
|
|
1753
|
-
// delivers it internally.
|
|
1754
|
-
//
|
|
1755
|
-
// Two detection layers:
|
|
1756
|
-
// 1. Bracketed paste escape sequences (\x1b[200~ ... \x1b[201~)
|
|
1757
|
-
// 2. Burst buffering — accumulate rapid-fire chunks over a short window and check
|
|
1758
|
-
// whether the combined content looks like a multiline paste.
|
|
1759
|
-
let bracketedBuffer = "";
|
|
1760
|
-
let inBracketedPaste = false;
|
|
1761
|
-
let burstBuffer = "";
|
|
1762
|
-
let burstTimer = null;
|
|
1763
|
-
let pendingPasteEndMarker = { active: false, buffer: "" };
|
|
1764
|
-
let swallowPostPasteDebrisUntil = 0;
|
|
1765
|
-
const BURST_WINDOW_MS = 50; // Long enough for slow terminals to finish delivering paste
|
|
1766
|
-
const POST_PASTE_DEBRIS_WINDOW_MS = 1200;
|
|
1767
|
-
// Debug paste: set CODEMAXXING_DEBUG_PASTE=1 to log all stdin chunks to /tmp/codemaxxing-paste-debug.log
|
|
1768
|
-
const PASTE_DEBUG = process.env.CODEMAXXING_DEBUG_PASTE === "1";
|
|
1769
|
-
function pasteLog(msg) {
|
|
1770
|
-
if (!PASTE_DEBUG)
|
|
1771
|
-
return;
|
|
1772
|
-
const escaped = msg.replace(/\x1b/g, "\\x1b").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
|
|
1773
|
-
try {
|
|
1774
|
-
appendFileSync("/tmp/codemaxxing-paste-debug.log", `[${Date.now()}] ${escaped}\n`);
|
|
1775
|
-
}
|
|
1776
|
-
catch { }
|
|
1777
|
-
}
|
|
1778
|
-
const origEmit = process.stdin.emit.bind(process.stdin);
|
|
1779
|
-
function handlePasteContent(content) {
|
|
1780
|
-
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").trim();
|
|
1781
|
-
if (!normalized)
|
|
1782
|
-
return;
|
|
1783
|
-
const lineCount = normalized.split("\n").length;
|
|
1784
|
-
if (lineCount > 2) {
|
|
1785
|
-
// Real multiline paste → badge it
|
|
1786
|
-
// Some terminals dribble the closing bracketed-paste marker (`[201~`)
|
|
1787
|
-
// one character at a time *after* the paste payload. Arm a tiny
|
|
1788
|
-
// swallow-state so those trailing fragments never leak into the input.
|
|
1789
|
-
pendingPasteEndMarker = { active: true, buffer: "" };
|
|
1790
|
-
swallowPostPasteDebrisUntil = Date.now() + POST_PASTE_DEBRIS_WINDOW_MS;
|
|
1791
|
-
pasteEvents.emit("paste", { content: normalized, lines: lineCount });
|
|
1792
|
-
return;
|
|
1793
|
-
}
|
|
1794
|
-
// Short paste (1-2 lines) → collapse to single line and forward as normal input
|
|
1795
|
-
const sanitized = normalized.replace(/\n/g, " ");
|
|
1796
|
-
if (sanitized) {
|
|
1797
|
-
origEmit("data", sanitized);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
function looksLikeMultilinePaste(data) {
|
|
1801
|
-
const clean = data.replace(/\x1b\[[0-9;]*[A-Za-z]/g, ""); // Strip all ANSI escapes
|
|
1802
|
-
// Count \r\n, \n, and bare \r as line breaks (macOS terminals often use bare \r)
|
|
1803
|
-
const normalized = clean.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1804
|
-
const newlines = (normalized.match(/\n/g) ?? []).length;
|
|
1805
|
-
const printable = normalized.replace(/\n/g, "").trim().length;
|
|
1806
|
-
return newlines >= 2 || (newlines >= 1 && printable >= 40);
|
|
1807
|
-
}
|
|
1808
|
-
function flushBurst() {
|
|
1809
|
-
if (!burstBuffer)
|
|
1810
|
-
return;
|
|
1811
|
-
let buffered = burstBuffer;
|
|
1812
|
-
burstBuffer = "";
|
|
1813
|
-
// Strip any bracketed paste marker fragments that accumulated across
|
|
1814
|
-
// individual character chunks (terminal sends [, 2, 0, 1, ~ separately)
|
|
1815
|
-
buffered = buffered.replace(/\x1b?\[?20[01]~/g, "");
|
|
1816
|
-
buffered = buffered.replace(/20[01]~/g, "");
|
|
1817
|
-
if (!buffered || !buffered.trim()) {
|
|
1818
|
-
pasteLog("BURST FLUSH stripped to empty — swallowed marker");
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
const isMultiline = looksLikeMultilinePaste(buffered);
|
|
1822
|
-
pasteLog(`BURST FLUSH len=${buffered.length} multiline=${isMultiline}`);
|
|
1823
|
-
if (isMultiline) {
|
|
1824
|
-
handlePasteContent(buffered);
|
|
1825
|
-
}
|
|
1826
|
-
else {
|
|
1827
|
-
// Normal typing — forward to Ink
|
|
1828
|
-
origEmit("data", buffered);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
process.stdin.emit = function (event, ...args) {
|
|
1832
|
-
// Pass through non-data events untouched
|
|
1833
|
-
if (event !== "data") {
|
|
1834
|
-
return origEmit(event, ...args);
|
|
1835
|
-
}
|
|
1836
|
-
const chunk = args[0];
|
|
1837
|
-
let data = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf-8") : String(chunk);
|
|
1838
|
-
pasteLog(`CHUNK len=${data.length} raw=${data.substring(0, 200)}`);
|
|
1839
|
-
const pendingResult = consumePendingPasteEndMarkerChunk(data, pendingPasteEndMarker);
|
|
1840
|
-
pendingPasteEndMarker = pendingResult.nextState;
|
|
1841
|
-
data = pendingResult.remaining;
|
|
1842
|
-
if (!data) {
|
|
1843
|
-
pasteLog("PENDING END MARKER swallowed chunk");
|
|
1844
|
-
return true;
|
|
1845
|
-
}
|
|
1846
|
-
if (Date.now() < swallowPostPasteDebrisUntil && shouldSwallowPostPasteDebris(data)) {
|
|
1847
|
-
pasteLog(`POST-PASTE DEBRIS swallowed raw=${data}`);
|
|
1848
|
-
return true;
|
|
1849
|
-
}
|
|
1850
|
-
// Aggressively strip ALL bracketed paste escape sequences from every chunk,
|
|
1851
|
-
// regardless of context. Some terminals split markers across chunks or send
|
|
1852
|
-
// them in unexpected positions. We never want \x1b[200~ or \x1b[201~ (or
|
|
1853
|
-
// partial fragments like [200~ / [201~) to reach the input component.
|
|
1854
|
-
const hadStart = data.includes("\x1b[200~") || data.includes("[200~") || data.includes("200~");
|
|
1855
|
-
const hadEnd = data.includes("\x1b[201~") || data.includes("[201~") || data.includes("201~");
|
|
1856
|
-
pasteLog(`MARKERS start=${hadStart} end=${hadEnd} inBracketed=${inBracketedPaste}`);
|
|
1857
|
-
// Strip full and partial bracketed paste markers — catch every possible fragment
|
|
1858
|
-
// Full: \x1b[200~ / \x1b[201~ Partial: [200~ / [201~ Bare: 200~ / 201~
|
|
1859
|
-
data = data.replace(/\x1b?\[?20[01]~/g, "");
|
|
1860
|
-
// Belt-and-suspenders: catch any residual marker fragments with multiple passes
|
|
1861
|
-
data = data.replace(/\[20[01]~/g, ""); // [200~ or [201~
|
|
1862
|
-
data = data.replace(/20[01]~/g, ""); // 200~ or 201~
|
|
1863
|
-
data = data.replace(/\[\d01~/g, ""); // any [Xdigit01~
|
|
1864
|
-
// Final paranoia pass: remove anything that looks like a closing bracket-tilde
|
|
1865
|
-
if (data.includes("[201") || data.includes("[200")) {
|
|
1866
|
-
data = data.replace(/\[[0-9]*0?[01]~?/g, "");
|
|
1867
|
-
}
|
|
1868
|
-
// ── Bracketed paste handling ──
|
|
1869
|
-
if (hadStart) {
|
|
1870
|
-
// Flush any pending burst before entering bracketed mode
|
|
1871
|
-
if (burstTimer) {
|
|
1872
|
-
clearTimeout(burstTimer);
|
|
1873
|
-
burstTimer = null;
|
|
1874
|
-
}
|
|
1875
|
-
flushBurst();
|
|
1876
|
-
inBracketedPaste = true;
|
|
1877
|
-
pasteLog("ENTERED bracketed paste mode");
|
|
1878
|
-
}
|
|
1879
|
-
if (hadEnd) {
|
|
1880
|
-
bracketedBuffer += data;
|
|
1881
|
-
inBracketedPaste = false;
|
|
1882
|
-
const content = bracketedBuffer;
|
|
1883
|
-
bracketedBuffer = "";
|
|
1884
|
-
pasteLog(`BRACKETED COMPLETE len=${content.length} lines=${content.split("\\n").length}`);
|
|
1885
|
-
handlePasteContent(content);
|
|
1886
|
-
return true;
|
|
1887
|
-
}
|
|
1888
|
-
if (inBracketedPaste) {
|
|
1889
|
-
bracketedBuffer += data;
|
|
1890
|
-
pasteLog(`BRACKETED BUFFERING total=${bracketedBuffer.length}`);
|
|
1891
|
-
return true;
|
|
1892
|
-
}
|
|
1893
|
-
// ── Burst buffering for non-bracketed paste ──
|
|
1894
|
-
burstBuffer += data;
|
|
1895
|
-
if (burstTimer)
|
|
1896
|
-
clearTimeout(burstTimer);
|
|
1897
|
-
burstTimer = setTimeout(() => {
|
|
1898
|
-
burstTimer = null;
|
|
1899
|
-
flushBurst();
|
|
1900
|
-
}, BURST_WINDOW_MS);
|
|
1901
|
-
return true;
|
|
1902
|
-
};
|
|
1903
|
-
// Disable bracketed paste on exit
|
|
1904
|
-
process.on("exit", () => {
|
|
1905
|
-
process.stdout.write("\x1b[?2004l");
|
|
1906
|
-
});
|
|
858
|
+
// Set up paste interception (bracketed paste, burst buffering, debris swallowing)
|
|
859
|
+
const pasteEvents = setupPasteInterceptor();
|
|
1907
860
|
// Handle terminal resize — clear ghost artifacts
|
|
1908
861
|
process.stdout.on("resize", () => {
|
|
1909
862
|
process.stdout.write("\x1B[2J\x1B[H");
|