codemaxxing 1.0.16 → 1.0.17
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 +58 -15
- package/dist/index.js +119 -1263
- 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 +136 -0
- package/dist/ui/input-router.js +657 -0
- package/dist/ui/paste-interceptor.d.ts +21 -0
- package/dist/ui/paste-interceptor.js +179 -0
- package/dist/ui/pickers.d.ts +150 -0
- package/dist/ui/pickers.js +111 -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/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,27 +1,25 @@
|
|
|
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 { Banner, ConnectionInfo } from "./ui/banner.js";
|
|
20
|
+
import { StatusBar } from "./ui/status-bar.js";
|
|
21
|
+
import { refreshConnectionBanner as refreshConnectionBannerImpl, connectToProvider as connectToProviderImpl, } from "./ui/connection.js";
|
|
22
|
+
import { CommandSuggestions, LoginPicker, LoginMethodPickerUI, SkillsMenu, SkillsBrowse, SkillsInstalled, SkillsRemove, ThemePickerUI, SessionPicker, DeleteSessionPicker, DeleteSessionConfirm, ModelPicker, OllamaDeletePicker, OllamaPullPicker, OllamaDeleteConfirm, OllamaPullProgress, OllamaExitPrompt, ApprovalPrompt, WizardConnection, WizardModels, WizardInstallOllama, WizardPulling, } from "./ui/pickers.js";
|
|
25
23
|
import { createRequire } from "module";
|
|
26
24
|
const _require = createRequire(import.meta.url);
|
|
27
25
|
const VERSION = _require("../package.json").version;
|
|
@@ -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();
|
|
@@ -185,6 +184,7 @@ function App() {
|
|
|
185
184
|
const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
|
|
186
185
|
const [modelPicker, setModelPicker] = useState(null);
|
|
187
186
|
const [modelPickerIndex, setModelPickerIndex] = useState(0);
|
|
187
|
+
// ── Setup Wizard State ──
|
|
188
188
|
const [wizardScreen, setWizardScreen] = useState(null);
|
|
189
189
|
const [wizardIndex, setWizardIndex] = useState(0);
|
|
190
190
|
const [wizardHardware, setWizardHardware] = useState(null);
|
|
@@ -206,182 +206,33 @@ function App() {
|
|
|
206
206
|
}, []);
|
|
207
207
|
// Refresh the connection banner to reflect current provider status
|
|
208
208
|
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);
|
|
209
|
+
await refreshConnectionBannerImpl(setConnectionInfo);
|
|
235
210
|
}, []);
|
|
236
211
|
// Connect/reconnect to LLM provider
|
|
237
212
|
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
|
-
},
|
|
213
|
+
await connectToProviderImpl(isRetry, {
|
|
214
|
+
setConnectionInfo,
|
|
215
|
+
setReady,
|
|
216
|
+
setAgent,
|
|
217
|
+
setModelName,
|
|
218
|
+
providerRef,
|
|
219
|
+
setLoading,
|
|
220
|
+
setStreaming,
|
|
221
|
+
setSpinnerMsg,
|
|
222
|
+
setMessages,
|
|
223
|
+
addMsg,
|
|
224
|
+
nextMsgId,
|
|
225
|
+
setApproval,
|
|
226
|
+
setWizardScreen,
|
|
227
|
+
setWizardIndex,
|
|
356
228
|
});
|
|
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
229
|
}, []);
|
|
379
230
|
// Initialize agent on mount
|
|
380
231
|
useEffect(() => {
|
|
381
232
|
connectToProvider(false);
|
|
382
233
|
}, []);
|
|
383
234
|
function addMsg(type, text) {
|
|
384
|
-
setMessages((prev) => [...prev, { id:
|
|
235
|
+
setMessages((prev) => [...prev, { id: nextMsgId(), type, text }]);
|
|
385
236
|
}
|
|
386
237
|
// Compute matching commands for suggestions
|
|
387
238
|
const cmdMatches = input.startsWith("/")
|
|
@@ -798,875 +649,92 @@ function App() {
|
|
|
798
649
|
setStreaming(false);
|
|
799
650
|
}, [agent, exit, refreshConnectionBanner]);
|
|
800
651
|
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
|
-
.catch((err) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
|
|
886
|
-
}
|
|
887
|
-
else if (method === "api-key") {
|
|
888
|
-
const provider = PROVIDERS.find((p) => p.id === providerId);
|
|
889
|
-
addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${providerId} <your-key>\n Get key at: ${provider?.consoleUrl ?? "your provider's dashboard"}`);
|
|
890
|
-
}
|
|
891
|
-
return;
|
|
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
|
-
}
|
|
652
|
+
routeKeyPress(inputChar, key, {
|
|
653
|
+
showSuggestionsRef,
|
|
654
|
+
cmdMatchesRef,
|
|
655
|
+
cmdIndexRef,
|
|
656
|
+
setCmdIndex,
|
|
657
|
+
setInput,
|
|
658
|
+
setInputKey,
|
|
659
|
+
loginMethodPicker,
|
|
660
|
+
loginMethodIndex,
|
|
661
|
+
setLoginMethodIndex,
|
|
662
|
+
setLoginMethodPicker,
|
|
663
|
+
loginPicker,
|
|
664
|
+
loginPickerIndex,
|
|
665
|
+
setLoginPickerIndex,
|
|
666
|
+
setLoginPicker,
|
|
667
|
+
skillsPicker,
|
|
668
|
+
skillsPickerIndex,
|
|
669
|
+
setSkillsPickerIndex,
|
|
670
|
+
setSkillsPicker,
|
|
671
|
+
sessionDisabledSkills,
|
|
672
|
+
setSessionDisabledSkills,
|
|
673
|
+
modelPicker,
|
|
674
|
+
modelPickerIndex,
|
|
675
|
+
setModelPickerIndex,
|
|
676
|
+
setModelPicker,
|
|
677
|
+
ollamaDeletePicker,
|
|
678
|
+
ollamaDeletePickerIndex,
|
|
679
|
+
setOllamaDeletePickerIndex,
|
|
680
|
+
setOllamaDeletePicker,
|
|
681
|
+
ollamaPullPicker,
|
|
682
|
+
ollamaPullPickerIndex,
|
|
683
|
+
setOllamaPullPickerIndex,
|
|
684
|
+
setOllamaPullPicker,
|
|
685
|
+
ollamaDeleteConfirm,
|
|
686
|
+
setOllamaDeleteConfirm,
|
|
687
|
+
ollamaExitPrompt,
|
|
688
|
+
setOllamaExitPrompt,
|
|
689
|
+
wizardScreen,
|
|
690
|
+
wizardIndex,
|
|
691
|
+
wizardModels,
|
|
692
|
+
wizardHardware,
|
|
693
|
+
wizardPullProgress,
|
|
694
|
+
wizardPullError,
|
|
695
|
+
wizardSelectedModel,
|
|
696
|
+
setWizardScreen,
|
|
697
|
+
setWizardIndex,
|
|
698
|
+
setWizardHardware,
|
|
699
|
+
setWizardModels,
|
|
700
|
+
setWizardPullProgress,
|
|
701
|
+
setWizardPullError,
|
|
702
|
+
setWizardSelectedModel,
|
|
703
|
+
themePicker,
|
|
704
|
+
themePickerIndex,
|
|
705
|
+
setThemePickerIndex,
|
|
706
|
+
setThemePicker,
|
|
707
|
+
setTheme,
|
|
708
|
+
sessionPicker,
|
|
709
|
+
sessionPickerIndex,
|
|
710
|
+
setSessionPickerIndex,
|
|
711
|
+
setSessionPicker,
|
|
712
|
+
deleteSessionConfirm,
|
|
713
|
+
setDeleteSessionConfirm,
|
|
714
|
+
deleteSessionPicker,
|
|
715
|
+
deleteSessionPickerIndex,
|
|
716
|
+
setDeleteSessionPickerIndex,
|
|
717
|
+
setDeleteSessionPicker,
|
|
718
|
+
input,
|
|
719
|
+
pastedChunksRef,
|
|
720
|
+
setPastedChunks,
|
|
721
|
+
approval,
|
|
722
|
+
setApproval,
|
|
723
|
+
ctrlCPressed,
|
|
724
|
+
setCtrlCPressed,
|
|
725
|
+
setLoading,
|
|
726
|
+
setSpinnerMsg,
|
|
727
|
+
agent,
|
|
728
|
+
setModelName,
|
|
729
|
+
addMsg,
|
|
730
|
+
exit,
|
|
731
|
+
refreshConnectionBanner,
|
|
732
|
+
connectToProvider,
|
|
733
|
+
handleSubmit,
|
|
734
|
+
_require,
|
|
735
|
+
});
|
|
1647
736
|
});
|
|
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) => {
|
|
737
|
+
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
738
|
switch (msg.type) {
|
|
1671
739
|
case "user":
|
|
1672
740
|
return (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.colors.userInput, children: [" > ", msg.text] }) }, msg.id));
|
|
@@ -1686,224 +754,12 @@ function App() {
|
|
|
1686
754
|
default:
|
|
1687
755
|
return _jsx(Text, { children: msg.text }, msg.id);
|
|
1688
756
|
}
|
|
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" : ""] }) }))] }));
|
|
757
|
+
}), 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 })), modelPicker && (_jsx(ModelPicker, { models: modelPicker, 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
758
|
}
|
|
1744
759
|
// Clear screen before render
|
|
1745
760
|
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
|
-
});
|
|
761
|
+
// Set up paste interception (bracketed paste, burst buffering, debris swallowing)
|
|
762
|
+
const pasteEvents = setupPasteInterceptor();
|
|
1907
763
|
// Handle terminal resize — clear ghost artifacts
|
|
1908
764
|
process.stdout.on("resize", () => {
|
|
1909
765
|
process.stdout.write("\x1B[2J\x1B[H");
|