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/dist/index.js CHANGED
@@ -1,27 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
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 { consumePendingPasteEndMarkerChunk, shouldSwallowPostPasteDebris, sanitizeInputArtifacts } from "./utils/paste.js";
9
- import { CodingAgent } from "./agent.js";
10
- import { loadConfig, saveConfig, detectLocalProvider, detectLocalProviderDetailed, parseCLIArgs, applyOverrides, listModels } from "./config.js";
11
- import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
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, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
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 { detectHardware, formatBytes } from "./utils/hardware.js";
23
- import { getRecommendationsWithLlmfit, getFitIcon, isLlmfitAvailable } from "./utils/models.js";
24
- import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel } from "./utils/ollama.js";
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: "list available models" },
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 [modelPicker, setModelPicker] = useState(null);
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
- const info = [];
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
- const cliArgs = parseCLIArgs();
239
- const rawConfig = loadConfig();
240
- const config = applyOverrides(rawConfig, cliArgs);
241
- let provider = config.provider;
242
- const info = [];
243
- if (isRetry) {
244
- info.push("Retrying connection...");
245
- setConnectionInfo([...info]);
246
- }
247
- if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !cliArgs.baseUrl)) {
248
- info.push("Detecting local LLM server...");
249
- setConnectionInfo([...info]);
250
- const detection = await detectLocalProviderDetailed();
251
- if (detection.status === "connected") {
252
- // Keep CLI model override if specified
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: msgId++, type, text }]);
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 === "/model" || selected.cmd === "/session delete" ||
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
- " /model — switch model mid-session",
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
- try {
639
- const ollamaModels = await listInstalledModelsDetailed();
640
- if (ollamaModels.length > 0) {
641
- setModelPicker(ollamaModels.map(m => m.name));
642
- setModelPickerIndex(0);
643
- return;
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
- catch (err) {
647
- // Ollama not available or failed, try provider
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 providerModels = await listModels(providerRef.current.baseUrl, providerRef.current.apiKey || "");
653
- if (providerModels.length > 0) {
654
- setModelPicker(providerModels);
655
- setModelPickerIndex(0);
656
- return;
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 (err) {
660
- // Provider fetch failed
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: /model <model-name>`);
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
- // Handle slash command navigation
802
- if (showSuggestionsRef.current) {
803
- const matches = cmdMatchesRef.current;
804
- if (key.upArrow) {
805
- setCmdIndex((prev) => (prev - 1 + matches.length) % matches.length);
806
- return;
807
- }
808
- if (key.downArrow) {
809
- setCmdIndex((prev) => (prev + 1) % matches.length);
810
- return;
811
- }
812
- if (key.tab) {
813
- const selected = matches[cmdIndexRef.current];
814
- if (selected) {
815
- setInput(selected.cmd + (selected.cmd === "/commit" ? " " : ""));
816
- setCmdIndex(0);
817
- setInputKey((k) => k + 1);
818
- }
819
- return;
820
- }
821
- }
822
- // Login method picker navigation (second level — pick auth method)
823
- if (loginMethodPicker) {
824
- const methods = loginMethodPicker.methods;
825
- if (key.upArrow) {
826
- setLoginMethodIndex((prev) => (prev - 1 + methods.length) % methods.length);
827
- return;
828
- }
829
- if (key.downArrow) {
830
- setLoginMethodIndex((prev) => (prev + 1) % methods.length);
831
- return;
832
- }
833
- if (key.escape) {
834
- setLoginMethodPicker(null);
835
- setLoginPicker(true); // go back to provider picker
836
- return;
837
- }
838
- if (key.return) {
839
- const method = methods[loginMethodIndex];
840
- const providerId = loginMethodPicker.provider;
841
- setLoginMethodPicker(null);
842
- if (method === "oauth" && providerId === "openrouter") {
843
- addMsg("info", "Starting OpenRouter OAuth — opening browser...");
844
- setLoading(true);
845
- setSpinnerMsg("Waiting for authorization...");
846
- openRouterOAuth((msg) => addMsg("info", msg))
847
- .then(() => {
848
- addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
849
- setLoading(false);
850
- })
851
- .catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
852
- }
853
- else if (method === "setup-token") {
854
- addMsg("info", "Starting setup-token flow — browser will open...");
855
- setLoading(true);
856
- setSpinnerMsg("Waiting for Claude Code auth...");
857
- anthropicSetupToken((msg) => addMsg("info", msg))
858
- .then((cred) => { addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); setLoading(false); })
859
- .catch((err) => { addMsg("error", `Auth failed: ${err.message}`); setLoading(false); });
860
- }
861
- else if (method === "cached-token" && providerId === "openai") {
862
- const imported = importCodexToken((msg) => addMsg("info", msg));
863
- if (imported) {
864
- addMsg("info", `✅ Imported Codex credentials! (${imported.label})`);
865
- }
866
- else {
867
- addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first.");
868
- }
869
- }
870
- else if (method === "cached-token" && providerId === "qwen") {
871
- const imported = importQwenToken((msg) => addMsg("info", msg));
872
- if (imported) {
873
- addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`);
874
- }
875
- else {
876
- addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first.");
877
- }
878
- }
879
- else if (method === "device-flow") {
880
- addMsg("info", "Starting GitHub Copilot device flow...");
881
- setLoading(true);
882
- setSpinnerMsg("Waiting for GitHub authorization...");
883
- copilotDeviceFlow((msg) => addMsg("info", msg))
884
- .then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
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
- }
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
- // CODE banner lines
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 && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: theme.colors.muted, children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: theme.colors.muted, children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.diff ? (_jsxs(Box, { flexDirection: "column", marginTop: 0, marginLeft: 2, children: [approval.diff.split("\n").slice(0, 40).map((line, i) => (_jsx(Text, { color: line.startsWith("+") ? theme.colors.success :
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
- // Paste event bus communicates between stdin interceptor and React
1747
- const pasteEvents = new EventEmitter();
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");