codemaxxing 1.1.2 → 1.1.4

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 CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  Open-source terminal coding agent. Connect **any** LLM — local or remote — and start building. Like Claude Code, but you bring your own model.
12
12
 
13
- **🆕 v1.1.0:** Use GPT-5.4 with your ChatGPT Plus subscription no API key needed. Just `/login` OpenAI OAuth. Same access as Codex CLI.
13
+ **🆕 v1.1.3:** GPT-5.4 via ChatGPT Plus OAuth, Anthropic OAuth auto-refresh, better Windows terminal behavior, Escape-to-cancel, model picker fixes, and smoother first-run auth/model selection.
14
14
 
15
15
  ## Why?
16
16
 
@@ -71,7 +71,7 @@ If you already have a local server running, Codemaxxing auto-detects common defa
71
71
 
72
72
  For LM Studio:
73
73
  1. Download [LM Studio](https://lmstudio.ai)
74
- 2. Load a coding model (for example **Qwen 2.5 Coder 7B** for a lightweight test)
74
+ 2. Load a coding model (ideally **Qwen 2.5 Coder 14B** or **DeepSeek Coder V2 16B** if your machine can handle it; use 7B only as a fallback)
75
75
  3. Start the local server
76
76
  4. Run:
77
77
 
@@ -89,10 +89,11 @@ codemaxxing
89
89
 
90
90
  If no LLM is available, Codemaxxing can guide you through:
91
91
  - detecting your hardware
92
- - recommending a model
92
+ - recommending a model (with stronger coding defaults first)
93
93
  - installing Ollama
94
94
  - downloading the model
95
95
  - connecting automatically
96
+ - opening the model picker automatically after cloud auth
96
97
 
97
98
  ### Option C — ChatGPT Plus (GPT-5.4, easiest cloud option)
98
99
 
package/dist/agent.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ChatCompletionTool } from "openai/resources/chat/completions";
1
2
  import { type ConnectedServer } from "./utils/mcp.js";
2
3
  import type { ProviderConfig } from "./config.js";
3
4
  export declare function getModelCost(model: string): {
@@ -28,6 +29,7 @@ export declare class CodingAgent {
28
29
  private providerType;
29
30
  private currentApiKey;
30
31
  private currentBaseUrl;
32
+ private aborted;
31
33
  private messages;
32
34
  private tools;
33
35
  private cwd;
@@ -97,6 +99,19 @@ export declare class CodingAgent {
97
99
  /**
98
100
  * Switch to a different model mid-session
99
101
  */
102
+ /**
103
+ * Get available tools (for UI hints, capabilities display, etc.)
104
+ */
105
+ getTools(): ChatCompletionTool[];
106
+ /**
107
+ * Abort the current generation. Safe to call from any thread.
108
+ */
109
+ abort(): void;
110
+ /**
111
+ * Check if generation was aborted, and reset the flag.
112
+ */
113
+ isAborted(): boolean;
114
+ private resetAbort;
100
115
  switchModel(model: string, baseUrl?: string, apiKey?: string, providerType?: "openai" | "anthropic"): void;
101
116
  /**
102
117
  * Attempt to refresh an expired Anthropic OAuth token.
package/dist/agent.js CHANGED
@@ -153,6 +153,7 @@ export class CodingAgent {
153
153
  providerType;
154
154
  currentApiKey = null;
155
155
  currentBaseUrl = "";
156
+ aborted = false;
156
157
  messages = [];
157
158
  tools = FILE_TOOLS;
158
159
  cwd;
@@ -268,6 +269,7 @@ export class CodingAgent {
268
269
  * and loops until the model responds with text (no more tool calls).
269
270
  */
270
271
  async chat(userMessage) {
272
+ this.resetAbort();
271
273
  const userMsg = { role: "user", content: userMessage };
272
274
  this.messages.push(userMsg);
273
275
  saveMessage(this.sessionId, userMsg);
@@ -312,6 +314,14 @@ export class CodingAgent {
312
314
  let chunkPromptTokens = 0;
313
315
  let chunkCompletionTokens = 0;
314
316
  for await (const chunk of stream) {
317
+ // Check for abort
318
+ if (this.aborted) {
319
+ try {
320
+ stream.controller?.abort();
321
+ }
322
+ catch { }
323
+ break;
324
+ }
315
325
  // Capture usage from the final chunk
316
326
  if (chunk.usage) {
317
327
  chunkPromptTokens = chunk.usage.prompt_tokens ?? 0;
@@ -382,6 +392,11 @@ export class CodingAgent {
382
392
  (this.totalCompletionTokens / 1_000_000) * costs.output;
383
393
  updateSessionCost(this.sessionId, this.totalPromptTokens, this.totalCompletionTokens, this.totalCost);
384
394
  }
395
+ // If aborted, return what we have so far
396
+ if (this.aborted) {
397
+ updateTokenEstimate(this.sessionId, this.estimateTokens());
398
+ return contentText ? contentText + "\n\n_(cancelled)_" : "_(cancelled)_";
399
+ }
385
400
  // If no tool calls, we're done — return the text
386
401
  if (toolCalls.size === 0) {
387
402
  updateTokenEstimate(this.sessionId, this.estimateTokens());
@@ -542,12 +557,33 @@ export class CodingAgent {
542
557
  });
543
558
  }
544
559
  }
545
- return msgs;
560
+ // Sanitize: remove tool_result messages that don't have a matching tool_use
561
+ const validToolUseIds = new Set();
562
+ for (const m of msgs) {
563
+ if (m.role === "assistant" && Array.isArray(m.content)) {
564
+ for (const block of m.content) {
565
+ if (block.type === "tool_use") {
566
+ validToolUseIds.add(block.id);
567
+ }
568
+ }
569
+ }
570
+ }
571
+ return msgs.filter((m) => {
572
+ if (m.role === "user" && Array.isArray(m.content)) {
573
+ const toolResults = m.content.filter((b) => b.type === "tool_result");
574
+ if (toolResults.length > 0) {
575
+ // Only keep if ALL tool_results have matching tool_use
576
+ return toolResults.every((tr) => validToolUseIds.has(tr.tool_use_id));
577
+ }
578
+ }
579
+ return true;
580
+ });
546
581
  }
547
582
  /**
548
583
  * Anthropic-native streaming chat
549
584
  */
550
585
  async chatAnthropic(_userMessage) {
586
+ this.resetAbort();
551
587
  let iterations = 0;
552
588
  const MAX_ITERATIONS = 20;
553
589
  while (iterations < MAX_ITERATIONS) {
@@ -587,6 +623,10 @@ export class CodingAgent {
587
623
  tools: anthropicTools,
588
624
  });
589
625
  stream.on("text", (text) => {
626
+ if (this.aborted) {
627
+ stream.abort();
628
+ return;
629
+ }
590
630
  contentText += text;
591
631
  this.options.onToken?.(text);
592
632
  });
@@ -607,6 +647,11 @@ export class CodingAgent {
607
647
  // Re-throw if we can't handle it
608
648
  throw err;
609
649
  }
650
+ // If aborted, return what we have
651
+ if (this.aborted) {
652
+ updateTokenEstimate(this.sessionId, this.estimateTokens());
653
+ return contentText ? contentText + "\n\n_(cancelled)_" : "_(cancelled)_";
654
+ }
610
655
  // Track usage
611
656
  if (finalMessage.usage) {
612
657
  const promptTokens = finalMessage.usage.input_tokens;
@@ -735,6 +780,7 @@ export class CodingAgent {
735
780
  * OpenAI Responses API chat (for Codex OAuth tokens + GPT-5.4)
736
781
  */
737
782
  async chatOpenAIResponses(userMessage) {
783
+ this.resetAbort();
738
784
  let iterations = 0;
739
785
  const MAX_ITERATIONS = 20;
740
786
  while (iterations < MAX_ITERATIONS) {
@@ -771,6 +817,11 @@ export class CodingAgent {
771
817
  }
772
818
  this.messages.push(assistantMessage);
773
819
  saveMessage(this.sessionId, assistantMessage);
820
+ // If aborted, return what we have
821
+ if (this.aborted) {
822
+ updateTokenEstimate(this.sessionId, this.estimateTokens());
823
+ return contentText ? contentText + "\n\n_(cancelled)_" : "_(cancelled)_";
824
+ }
774
825
  // If no tool calls, we're done
775
826
  if (toolCalls.length === 0) {
776
827
  updateTokenEstimate(this.sessionId, this.estimateTokens());
@@ -868,6 +919,27 @@ export class CodingAgent {
868
919
  /**
869
920
  * Switch to a different model mid-session
870
921
  */
922
+ /**
923
+ * Get available tools (for UI hints, capabilities display, etc.)
924
+ */
925
+ getTools() {
926
+ return this.tools;
927
+ }
928
+ /**
929
+ * Abort the current generation. Safe to call from any thread.
930
+ */
931
+ abort() {
932
+ this.aborted = true;
933
+ }
934
+ /**
935
+ * Check if generation was aborted, and reset the flag.
936
+ */
937
+ isAborted() {
938
+ return this.aborted;
939
+ }
940
+ resetAbort() {
941
+ this.aborted = false;
942
+ }
871
943
  switchModel(model, baseUrl, apiKey, providerType) {
872
944
  this.model = model;
873
945
  if (apiKey)
package/dist/index.js CHANGED
@@ -229,6 +229,7 @@ function App() {
229
229
  setApproval,
230
230
  setWizardScreen,
231
231
  setWizardIndex,
232
+ openModelPicker,
232
233
  });
233
234
  }, []);
234
235
  // Initialize agent on mount
@@ -252,6 +253,125 @@ function App() {
252
253
  showSuggestionsRef.current = showSuggestions;
253
254
  const pastedChunksRef = React.useRef(pastedChunks);
254
255
  pastedChunksRef.current = pastedChunks;
256
+ const openModelPicker = useCallback(async () => {
257
+ addMsg("info", "Fetching available models...");
258
+ const groups = {};
259
+ const providerEntries = [];
260
+ let localFound = false;
261
+ const localEndpoints = [
262
+ { name: "LM Studio", port: 1234 },
263
+ { name: "Ollama", port: 11434 },
264
+ { name: "vLLM", port: 8000 },
265
+ { name: "LocalAI", port: 8080 },
266
+ ];
267
+ for (const endpoint of localEndpoints) {
268
+ if (localFound)
269
+ break;
270
+ try {
271
+ const url = `http://localhost:${endpoint.port}/v1`;
272
+ const models = await listModels(url, "local");
273
+ if (models.length > 0) {
274
+ groups["Local LLM"] = models.map(m => ({
275
+ name: m,
276
+ baseUrl: url,
277
+ apiKey: "local",
278
+ providerType: "openai",
279
+ }));
280
+ localFound = true;
281
+ }
282
+ }
283
+ catch { /* not running */ }
284
+ }
285
+ if (!localFound) {
286
+ try {
287
+ const ollamaModels = await listInstalledModelsDetailed();
288
+ if (ollamaModels.length > 0) {
289
+ groups["Local LLM"] = ollamaModels.map(m => ({
290
+ name: m.name,
291
+ baseUrl: "http://localhost:11434/v1",
292
+ apiKey: "ollama",
293
+ providerType: "openai",
294
+ }));
295
+ localFound = true;
296
+ }
297
+ }
298
+ catch { /* Ollama not running */ }
299
+ }
300
+ if (localFound) {
301
+ providerEntries.push({ name: "Local LLM", description: "No auth needed — auto-detected", authed: true });
302
+ }
303
+ const anthropicCred = getCredential("anthropic");
304
+ const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"];
305
+ if (anthropicCred) {
306
+ groups["Anthropic (Claude)"] = claudeModels.map(m => ({
307
+ name: m,
308
+ baseUrl: "https://api.anthropic.com",
309
+ apiKey: anthropicCred.apiKey,
310
+ providerType: "anthropic",
311
+ }));
312
+ }
313
+ providerEntries.push({ name: "Anthropic (Claude)", description: "Claude Opus, Sonnet, Haiku — use your subscription or API key", authed: !!anthropicCred });
314
+ const openaiCred = getCredential("openai");
315
+ 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"];
316
+ if (openaiCred) {
317
+ const isOAuthToken = openaiCred.method === "oauth" || openaiCred.method === "cached-token" ||
318
+ (!openaiCred.apiKey.startsWith("sk-") && !openaiCred.apiKey.startsWith("sess-"));
319
+ const baseUrl = isOAuthToken
320
+ ? "https://chatgpt.com/backend-api"
321
+ : (openaiCred.baseUrl || "https://api.openai.com/v1");
322
+ groups["OpenAI (ChatGPT)"] = openaiModels.map(m => ({
323
+ name: m,
324
+ baseUrl,
325
+ apiKey: openaiCred.apiKey,
326
+ providerType: "openai",
327
+ }));
328
+ }
329
+ providerEntries.push({ name: "OpenAI (ChatGPT)", description: "GPT-5, GPT-4.1, o3 — use your ChatGPT subscription or API key", authed: !!openaiCred });
330
+ const openrouterCred = getCredential("openrouter");
331
+ if (openrouterCred) {
332
+ try {
333
+ const orModels = await listModels(openrouterCred.baseUrl || "https://openrouter.ai/api/v1", openrouterCred.apiKey);
334
+ if (orModels.length > 0) {
335
+ groups["OpenRouter"] = orModels.slice(0, 20).map(m => ({
336
+ name: m,
337
+ baseUrl: openrouterCred.baseUrl || "https://openrouter.ai/api/v1",
338
+ apiKey: openrouterCred.apiKey,
339
+ providerType: "openai",
340
+ }));
341
+ }
342
+ }
343
+ catch { /* skip */ }
344
+ }
345
+ providerEntries.push({ name: "OpenRouter", description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login", authed: !!openrouterCred });
346
+ const qwenCred = getCredential("qwen");
347
+ if (qwenCred) {
348
+ groups["Qwen"] = ["qwen-max", "qwen-plus", "qwen-turbo"].map(m => ({
349
+ name: m,
350
+ baseUrl: qwenCred.baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1",
351
+ apiKey: qwenCred.apiKey,
352
+ providerType: "openai",
353
+ }));
354
+ }
355
+ providerEntries.push({ name: "Qwen", description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key", authed: !!qwenCred });
356
+ const copilotCred = getCredential("copilot");
357
+ if (copilotCred) {
358
+ groups["GitHub Copilot"] = ["gpt-4o", "claude-3.5-sonnet"].map(m => ({
359
+ name: m,
360
+ baseUrl: copilotCred.baseUrl || "https://api.githubcopilot.com",
361
+ apiKey: copilotCred.apiKey,
362
+ providerType: "openai",
363
+ }));
364
+ }
365
+ providerEntries.push({ name: "GitHub Copilot", description: "Use your GitHub Copilot subscription", authed: !!copilotCred });
366
+ if (providerEntries.length > 0) {
367
+ setModelPickerGroups(groups);
368
+ setProviderPicker(providerEntries);
369
+ setProviderPickerIndex(0);
370
+ setSelectedProvider(null);
371
+ return;
372
+ }
373
+ addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
374
+ }, [addMsg]);
255
375
  const handleSubmit = useCallback(async (value) => {
256
376
  value = sanitizeInputArtifacts(value);
257
377
  // Skip autocomplete if input exactly matches a command (e.g. /models vs /model)
@@ -478,133 +598,7 @@ function App() {
478
598
  return;
479
599
  }
480
600
  if (trimmed === "/models" || trimmed === "/model") {
481
- addMsg("info", "Fetching available models...");
482
- const groups = {};
483
- const providerEntries = [];
484
- // Local LLM (Ollama/LM Studio) — always show, auto-detect
485
- let localFound = false;
486
- // Check common local LLM endpoints
487
- const localEndpoints = [
488
- { name: "LM Studio", port: 1234 },
489
- { name: "Ollama", port: 11434 },
490
- { name: "vLLM", port: 8000 },
491
- { name: "LocalAI", port: 8080 },
492
- ];
493
- for (const endpoint of localEndpoints) {
494
- if (localFound)
495
- break;
496
- try {
497
- const url = `http://localhost:${endpoint.port}/v1`;
498
- const models = await listModels(url, "local");
499
- if (models.length > 0) {
500
- groups["Local LLM"] = models.map(m => ({
501
- name: m,
502
- baseUrl: url,
503
- apiKey: "local",
504
- providerType: "openai",
505
- }));
506
- localFound = true;
507
- }
508
- }
509
- catch { /* not running */ }
510
- }
511
- // Also check Ollama native API
512
- if (!localFound) {
513
- try {
514
- const ollamaModels = await listInstalledModelsDetailed();
515
- if (ollamaModels.length > 0) {
516
- groups["Local LLM"] = ollamaModels.map(m => ({
517
- name: m.name,
518
- baseUrl: "http://localhost:11434/v1",
519
- apiKey: "ollama",
520
- providerType: "openai",
521
- }));
522
- localFound = true;
523
- }
524
- }
525
- catch { /* Ollama not running */ }
526
- }
527
- if (localFound) {
528
- providerEntries.push({ name: "Local LLM", description: "No auth needed — auto-detected", authed: true });
529
- }
530
- // Anthropic
531
- const anthropicCred = getCredential("anthropic");
532
- const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"];
533
- if (anthropicCred) {
534
- groups["Anthropic (Claude)"] = claudeModels.map(m => ({
535
- name: m,
536
- baseUrl: "https://api.anthropic.com",
537
- apiKey: anthropicCred.apiKey,
538
- providerType: "anthropic",
539
- }));
540
- }
541
- providerEntries.push({ name: "Anthropic (Claude)", description: "Claude Opus, Sonnet, Haiku — use your subscription or API key", authed: !!anthropicCred });
542
- // OpenAI
543
- const openaiCred = getCredential("openai");
544
- 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"];
545
- if (openaiCred) {
546
- // OAuth tokens (non sk- keys) must use ChatGPT backend, not api.openai.com
547
- const isOAuthToken = openaiCred.method === "oauth" || openaiCred.method === "cached-token" ||
548
- (!openaiCred.apiKey.startsWith("sk-") && !openaiCred.apiKey.startsWith("sess-"));
549
- const baseUrl = isOAuthToken
550
- ? "https://chatgpt.com/backend-api"
551
- : (openaiCred.baseUrl || "https://api.openai.com/v1");
552
- groups["OpenAI (ChatGPT)"] = openaiModels.map(m => ({
553
- name: m,
554
- baseUrl,
555
- apiKey: openaiCred.apiKey,
556
- providerType: "openai",
557
- }));
558
- }
559
- providerEntries.push({ name: "OpenAI (ChatGPT)", description: "GPT-5, GPT-4.1, o3 — use your ChatGPT subscription or API key", authed: !!openaiCred });
560
- // OpenRouter
561
- const openrouterCred = getCredential("openrouter");
562
- if (openrouterCred) {
563
- try {
564
- const orModels = await listModels(openrouterCred.baseUrl || "https://openrouter.ai/api/v1", openrouterCred.apiKey);
565
- if (orModels.length > 0) {
566
- groups["OpenRouter"] = orModels.slice(0, 20).map(m => ({
567
- name: m,
568
- baseUrl: openrouterCred.baseUrl || "https://openrouter.ai/api/v1",
569
- apiKey: openrouterCred.apiKey,
570
- providerType: "openai",
571
- }));
572
- }
573
- }
574
- catch { /* skip */ }
575
- }
576
- providerEntries.push({ name: "OpenRouter", description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login", authed: !!openrouterCred });
577
- // Qwen
578
- const qwenCred = getCredential("qwen");
579
- if (qwenCred) {
580
- groups["Qwen"] = ["qwen-max", "qwen-plus", "qwen-turbo"].map(m => ({
581
- name: m,
582
- baseUrl: qwenCred.baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1",
583
- apiKey: qwenCred.apiKey,
584
- providerType: "openai",
585
- }));
586
- }
587
- providerEntries.push({ name: "Qwen", description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key", authed: !!qwenCred });
588
- // GitHub Copilot
589
- const copilotCred = getCredential("copilot");
590
- if (copilotCred) {
591
- groups["GitHub Copilot"] = ["gpt-4o", "claude-3.5-sonnet"].map(m => ({
592
- name: m,
593
- baseUrl: copilotCred.baseUrl || "https://api.githubcopilot.com",
594
- apiKey: copilotCred.apiKey,
595
- providerType: "openai",
596
- }));
597
- }
598
- providerEntries.push({ name: "GitHub Copilot", description: "Use your GitHub Copilot subscription", authed: !!copilotCred });
599
- // Show provider picker (step 1)
600
- if (providerEntries.length > 0) {
601
- setModelPickerGroups(groups);
602
- setProviderPicker(providerEntries);
603
- setProviderPickerIndex(0);
604
- setSelectedProvider(null);
605
- return;
606
- }
607
- addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
601
+ await openModelPicker();
608
602
  return;
609
603
  }
610
604
  if (trimmed.startsWith("/model ")) {
@@ -825,11 +819,14 @@ function App() {
825
819
  setLoading,
826
820
  setSpinnerMsg,
827
821
  agent,
822
+ streaming,
823
+ loading,
828
824
  setModelName,
829
825
  addMsg,
830
826
  exit,
831
827
  refreshConnectionBanner,
832
828
  connectToProvider,
829
+ openModelPicker,
833
830
  handleSubmit,
834
831
  _require,
835
832
  });
@@ -854,7 +851,7 @@ function App() {
854
851
  default:
855
852
  return _jsx(Text, { children: msg.text }, msg.id);
856
853
  }
857
- }), 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 }))] }));
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: process.platform === "win32" && !process.env.WT_SESSION ? "classic" : "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 }))] }));
858
855
  }
859
856
  // Clear screen before render
860
857
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
@@ -30,4 +30,5 @@ export interface ConnectionContext {
30
30
  } | null) => void;
31
31
  setWizardScreen: (val: WizardScreen) => void;
32
32
  setWizardIndex: (val: number) => void;
33
+ openModelPicker: () => Promise<void>;
33
34
  }
@@ -75,11 +75,11 @@ export async function connectToProvider(isRetry, ctx) {
75
75
  !!getCredential("openrouter") || !!getCredential("qwen") ||
76
76
  !!getCredential("copilot");
77
77
  if (hasAnyCreds) {
78
- // User has auth'd before — skip wizard, go straight to /models picker
79
- info.push("✔ Found saved credentials. Use /models to pick a model and start coding.");
78
+ // User has auth'd before — skip wizard and go straight to the model picker
79
+ info.push("✔ Found saved credentials. Opening model picker...");
80
80
  ctx.setConnectionInfo([...info]);
81
81
  ctx.setReady(true);
82
- // The user will run /models, which now works without an agent
82
+ await ctx.openModelPicker();
83
83
  return;
84
84
  }
85
85
  // No creds found — show the setup wizard
@@ -192,4 +192,15 @@ export async function connectToProvider(isRetry, ctx) {
192
192
  if (isRetry) {
193
193
  ctx.addMsg("info", `✅ Connected to ${provider.model}`);
194
194
  }
195
+ else {
196
+ // First-time connection — show capabilities hint
197
+ const tools = a.getTools();
198
+ const toolCount = tools.length;
199
+ const toolNames = tools
200
+ .map((t) => t.function.name.replace(/_/g, " "))
201
+ .slice(0, 3)
202
+ .join(", ");
203
+ ctx.addMsg("info", `💡 You can: ${toolNames}${toolCount > 3 ? `, +${toolCount - 3} more` : ""}\n` +
204
+ ` Try: "list files in src/" or "read main.ts"`);
205
+ }
195
206
  }
@@ -169,6 +169,8 @@ export interface InputRouterContext extends WizardContext {
169
169
  setCtrlCPressed: (val: boolean) => void;
170
170
  agent: CodingAgent | null;
171
171
  setModelName: (val: string) => void;
172
+ streaming: boolean;
173
+ loading: boolean;
172
174
  exit: () => void;
173
175
  refreshConnectionBanner: () => Promise<void>;
174
176
  handleSubmit: (value: string) => void;
@@ -43,6 +43,13 @@ export function routeKeyPress(inputChar, key, ctx) {
43
43
  return true;
44
44
  if (handleApprovalPrompts(inputChar, key, ctx))
45
45
  return true;
46
+ // Escape to abort generation (when loading or streaming)
47
+ if (key.escape && (ctx.streaming || ctx.loading) && ctx.agent) {
48
+ ctx.agent.abort();
49
+ ctx.setLoading(false);
50
+ ctx.addMsg("info", "⏹ Generation cancelled.");
51
+ return true;
52
+ }
46
53
  if (handleCtrlCExit(inputChar, key, ctx))
47
54
  return true;
48
55
  return false;
@@ -97,9 +104,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
97
104
  ctx.setLoading(true);
98
105
  ctx.setSpinnerMsg("Waiting for authorization...");
99
106
  openRouterOAuth((msg) => ctx.addMsg("info", msg))
100
- .then(() => {
101
- ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
107
+ .then(async () => {
108
+ ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n Opening model picker...`);
102
109
  ctx.setLoading(false);
110
+ await ctx.openModelPicker();
103
111
  })
104
112
  .catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
105
113
  }
@@ -108,9 +116,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
108
116
  ctx.setLoading(true);
109
117
  ctx.setSpinnerMsg("Waiting for Anthropic authorization...");
110
118
  loginAnthropicOAuth((msg) => ctx.addMsg("info", msg))
111
- .then((cred) => {
112
- ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})\n Next: type /models to pick a model`);
119
+ .then(async (cred) => {
120
+ ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})\n Opening model picker...`);
113
121
  ctx.setLoading(false);
122
+ await ctx.openModelPicker();
114
123
  })
115
124
  .catch((err) => {
116
125
  ctx.addMsg("error", `OAuth failed: ${err.message}\n Fallback: set your key via CLI: codemaxxing auth api-key anthropic <your-key>\n Or set ANTHROPIC_API_KEY env var and restart.\n Get key at: console.anthropic.com/settings/keys`);
@@ -121,7 +130,8 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
121
130
  // Try cached Codex token first as a quick path
122
131
  const imported = importCodexToken((msg) => ctx.addMsg("info", msg));
123
132
  if (imported) {
124
- ctx.addMsg("info", `✅ Imported Codex credentials! (${imported.label})`);
133
+ ctx.addMsg("info", `✅ Imported Codex credentials! (${imported.label})\n Opening model picker...`);
134
+ void ctx.openModelPicker();
125
135
  }
126
136
  else {
127
137
  // Primary flow: browser OAuth
@@ -129,9 +139,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
129
139
  ctx.setLoading(true);
130
140
  ctx.setSpinnerMsg("Waiting for OpenAI authorization...");
131
141
  loginOpenAICodexOAuth((msg) => ctx.addMsg("info", msg))
132
- .then((cred) => {
133
- ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})\n Next: type /models to pick a model`);
142
+ .then(async (cred) => {
143
+ ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})\n Opening model picker...`);
134
144
  ctx.setLoading(false);
145
+ await ctx.openModelPicker();
135
146
  })
136
147
  .catch((err) => {
137
148
  ctx.addMsg("error", `OAuth failed: ${err.message}\n Fallback: set your key via CLI: codemaxxing auth api-key openai <your-key>\n Or set OPENAI_API_KEY env var and restart.\n Get key at: platform.openai.com/api-keys`);
@@ -142,7 +153,8 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
142
153
  else if (method === "cached-token" && providerId === "qwen") {
143
154
  const imported = importQwenToken((msg) => ctx.addMsg("info", msg));
144
155
  if (imported) {
145
- ctx.addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`);
156
+ ctx.addMsg("info", `✅ Imported Qwen credentials! (${imported.label})\n Opening model picker...`);
157
+ void ctx.openModelPicker();
146
158
  }
147
159
  else {
148
160
  ctx.addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first.");
@@ -153,7 +165,7 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
153
165
  ctx.setLoading(true);
154
166
  ctx.setSpinnerMsg("Waiting for GitHub authorization...");
155
167
  copilotDeviceFlow((msg) => ctx.addMsg("info", msg))
156
- .then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Next: type /models to pick a model`); ctx.setLoading(false); })
168
+ .then(async () => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Opening model picker...`); ctx.setLoading(false); await ctx.openModelPicker(); })
157
169
  .catch((err) => { ctx.addMsg("error", `Copilot auth failed: ${err.message}`); ctx.setLoading(false); });
158
170
  }
159
171
  else if (method === "api-key") {
@@ -189,7 +201,7 @@ function handleLoginPicker(_inputChar, key, ctx) {
189
201
  ctx.setLoading(true);
190
202
  ctx.setSpinnerMsg("Waiting for authorization...");
191
203
  openRouterOAuth((msg) => ctx.addMsg("info", msg))
192
- .then(() => { ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n Next: type /models to pick a model`); ctx.setLoading(false); })
204
+ .then(async () => { ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n Opening model picker...`); ctx.setLoading(false); await ctx.openModelPicker(); })
193
205
  .catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
194
206
  }
195
207
  else if (methods[0] === "device-flow") {
@@ -198,7 +210,7 @@ function handleLoginPicker(_inputChar, key, ctx) {
198
210
  ctx.setLoading(true);
199
211
  ctx.setSpinnerMsg("Waiting for GitHub authorization...");
200
212
  copilotDeviceFlow((msg) => ctx.addMsg("info", msg))
201
- .then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Next: type /models to pick a model`); ctx.setLoading(false); })
213
+ .then(async () => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Opening model picker...`); ctx.setLoading(false); await ctx.openModelPicker(); })
202
214
  .catch((err) => { ctx.addMsg("error", `Copilot auth failed: ${err.message}`); ctx.setLoading(false); });
203
215
  }
204
216
  else if (methods[0] === "api-key") {
@@ -380,6 +392,9 @@ function handleProviderPicker(_inputChar, key, ctx) {
380
392
  }
381
393
  if (key.escape) {
382
394
  ctx.setProviderPicker(null);
395
+ if (!ctx.agent) {
396
+ ctx.addMsg("info", "Model selection cancelled. Use /login for cloud providers or choose local setup from the startup menu.");
397
+ }
383
398
  return true;
384
399
  }
385
400
  if (key.return) {
@@ -475,13 +490,13 @@ function handleOllamaPullPicker(_inputChar, key, ctx) {
475
490
  if (!ctx.ollamaPullPicker)
476
491
  return false;
477
492
  const pullModels = [
478
- { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
479
- { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
480
- { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
481
- { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs 48GB+" },
482
- { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
483
- { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
484
- { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
493
+ { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Recommended default for coding if your machine can handle it" },
494
+ { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2 16B", size: "9 GB", desc: "Strong higher-quality alternative" },
495
+ { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Fallback for mid-range machines" },
496
+ { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs lots of RAM" },
497
+ { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Older fallback coding model" },
498
+ { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Completion-focused fallback" },
499
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "⚠️ Last resort — may struggle with tool calls" },
485
500
  ];
486
501
  if (key.upArrow) {
487
502
  ctx.setOllamaPullPickerIndex((prev) => (prev - 1 + pullModels.length) % pullModels.length);
@@ -17,6 +17,15 @@ import { consumePendingPasteEndMarkerChunk, shouldSwallowPostPasteDebris } from
17
17
  */
18
18
  export function setupPasteInterceptor() {
19
19
  const pasteEvents = new EventEmitter();
20
+ // Detect Windows CMD/conhost — these don't support bracketed paste or ANSI sequences well.
21
+ // On these terminals, skip paste interception entirely to avoid eating keystrokes.
22
+ const isWindowsLegacyTerminal = process.platform === "win32" && (!process.env.WT_SESSION && // Not Windows Terminal
23
+ !process.env.TERM_PROGRAM // Not a modern terminal emulator
24
+ );
25
+ if (isWindowsLegacyTerminal) {
26
+ // Just return a dummy event bus — no interception, no burst buffering
27
+ return pasteEvents;
28
+ }
20
29
  // Enable bracketed paste mode — terminal wraps pastes in escape sequences
21
30
  process.stdout.write("\x1b[?2004h");
22
31
  // ── Internal state ──
@@ -74,13 +74,13 @@ export function OllamaDeletePicker({ models, selectedIndex, colors }) {
74
74
  }
75
75
  // ── Ollama Pull Picker ──
76
76
  const PULL_MODELS = [
77
- { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Best balance of speed & quality" },
78
- { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Higher quality, needs 16GB+ RAM" },
79
- { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "\u26A0\uFE0F Basic \u2014 may struggle with tool calls" },
80
- { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs 48GB+" },
81
- { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2", size: "9 GB", desc: "Strong alternative" },
82
- { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Meta's coding model" },
83
- { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Code completion focused" },
77
+ { id: "qwen2.5-coder:14b", name: "Qwen 2.5 Coder 14B", size: "9 GB", desc: "Recommended default for coding if your machine can handle it" },
78
+ { id: "deepseek-coder-v2:16b", name: "DeepSeek Coder V2 16B", size: "9 GB", desc: "Strong higher-quality alternative" },
79
+ { id: "qwen2.5-coder:7b", name: "Qwen 2.5 Coder 7B", size: "5 GB", desc: "Fallback for mid-range machines" },
80
+ { id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs lots of RAM" },
81
+ { id: "codellama:7b", name: "CodeLlama 7B", size: "4 GB", desc: "Older fallback coding model" },
82
+ { id: "starcoder2:7b", name: "StarCoder2 7B", size: "4 GB", desc: "Completion-focused fallback" },
83
+ { id: "qwen2.5-coder:3b", name: "Qwen 2.5 Coder 3B", size: "2 GB", desc: "⚠️ Last resort — may struggle with tool calls" },
84
84
  ];
85
85
  export function OllamaPullPicker({ selectedIndex, colors }) {
86
86
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Download which model?" }), _jsx(Text, { children: "" }), PULL_MODELS.map((m, i) => (_jsxs(Text, { children: [" ", i === selectedIndex ? _jsx(Text, { color: colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === selectedIndex ? colors.primary : undefined, bold: true, children: m.name }), _jsxs(Text, { color: colors.muted, children: [" · ", m.size, " · ", m.desc] })] }, m.id))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to download · Esc cancel" })] }));
@@ -113,7 +113,7 @@ export function WizardModels({ wizardIndex, wizardHardware, wizardModels, colors
113
113
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Your hardware:" }), _jsxs(Text, { color: colors.muted, children: [" CPU: ", wizardHardware.cpu.name, " (", wizardHardware.cpu.cores, " cores)"] }), _jsxs(Text, { color: colors.muted, children: [" RAM: ", formatBytes(wizardHardware.ram)] }), wizardHardware.gpu ? (_jsxs(Text, { color: colors.muted, children: [" GPU: ", wizardHardware.gpu.name, wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""] })) : (_jsx(Text, { color: 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: colors.secondary, children: "Recommended models:" }), _jsx(Text, { children: "" }), wizardModels.map((m, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { children: [getFitIcon(m.fit), " "] }), _jsx(Text, { color: i === wizardIndex ? colors.suggestion : colors.primary, bold: true, children: m.name }), _jsxs(Text, { color: 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: 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" })] }));
114
114
  }
115
115
  export function WizardInstallOllama({ wizardHardware, colors }) {
116
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.warning, children: "Ollama is required for local models." }), _jsx(Text, { children: "" }), _jsx(Text, { color: 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" })] }));
116
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.warning, children: "Ollama is required for local models." }), _jsx(Text, { children: "" }), _jsx(Text, { color: 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 back to model list" })] }));
117
117
  }
118
118
  export function WizardPulling({ wizardSelectedModel, wizardPullProgress, wizardPullError, colors }) {
119
119
  return (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: wizardPullError ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: 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: 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: 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: colors.muted, children: [" \u00B7 ", formatBytes(wizardPullProgress.completed), " / ", formatBytes(wizardPullProgress.total)] })) : null] }) })) : (_jsxs(Text, { color: colors.muted, children: [" ", wizardPullProgress.status, "..."] }))] })) : null }));
@@ -23,5 +23,6 @@ export interface WizardContext {
23
23
  setSpinnerMsg: (val: string) => void;
24
24
  addMsg: (type: "user" | "response" | "tool" | "tool-result" | "error" | "info", text: string) => void;
25
25
  connectToProvider: (isRetry: boolean) => Promise<void>;
26
+ openModelPicker: () => Promise<void>;
26
27
  _require: NodeRequire;
27
28
  }
package/dist/ui/wizard.js CHANGED
@@ -35,9 +35,10 @@ export function handleWizardScreen(_inputChar, key, ctx) {
35
35
  ctx.setLoading(true);
36
36
  ctx.setSpinnerMsg("Waiting for authorization...");
37
37
  openRouterOAuth((msg) => ctx.addMsg("info", msg))
38
- .then(() => {
39
- ctx.addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
38
+ .then(async () => {
39
+ ctx.addMsg("info", "✅ OpenRouter authenticated! Opening model picker...");
40
40
  ctx.setLoading(false);
41
+ await ctx.openModelPicker();
41
42
  })
42
43
  .catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
43
44
  }
@@ -45,10 +46,12 @@ export function handleWizardScreen(_inputChar, key, ctx) {
45
46
  ctx.setWizardScreen(null);
46
47
  ctx.setLoginPicker(true);
47
48
  ctx.setLoginPickerIndex(() => 0);
49
+ ctx.addMsg("info", "Choose a cloud provider to continue.");
48
50
  }
49
51
  else if (selected === "existing") {
50
52
  ctx.setWizardScreen(null);
51
- ctx.addMsg("info", "Start your LLM server, then type /connect to retry.");
53
+ ctx.addMsg("info", "Checking for your running server...");
54
+ void ctx.connectToProvider(true);
52
55
  }
53
56
  return true;
54
57
  }
@@ -196,6 +199,7 @@ function startPullFlow(ctx, selected) {
196
199
  return;
197
200
  }
198
201
  }
202
+ ctx.addMsg("info", `Downloading ${selected.name}. This can take a while on first run depending on your internet and disk speed.`);
199
203
  await pullModel(selected.ollamaId, (p) => {
200
204
  ctx.setWizardPullProgress(p);
201
205
  });
@@ -149,10 +149,12 @@ function generatePKCE() {
149
149
  export async function openRouterOAuth(onStatus) {
150
150
  const { verifier, challenge } = generatePKCE();
151
151
  return new Promise((resolve, reject) => {
152
+ let handled = false; // Guard against duplicate callbacks
152
153
  // Start local callback server
153
154
  const server = createServer(async (req, res) => {
154
155
  const url = new URL(req.url ?? "/", `http://localhost`);
155
- if (url.pathname === "/callback") {
156
+ if (url.pathname === "/callback" && !handled) {
157
+ handled = true;
156
158
  const code = url.searchParams.get("code");
157
159
  if (!code) {
158
160
  res.writeHead(400, { "Content-Type": "text/html" });
@@ -175,6 +177,9 @@ export async function openRouterOAuth(onStatus) {
175
177
  });
176
178
  if (!exchangeRes.ok) {
177
179
  const errText = await exchangeRes.text();
180
+ if (exchangeRes.status === 409) {
181
+ throw new Error(`OpenRouter returned 409 (Conflict) — this usually means the auth code was already used. Please try /login again.`);
182
+ }
178
183
  throw new Error(`Exchange failed (${exchangeRes.status}): ${errText}`);
179
184
  }
180
185
  const data = (await exchangeRes.json());
@@ -1,14 +1,24 @@
1
1
  import { execSync } from "child_process";
2
2
  const MODELS = [
3
3
  {
4
- name: "Qwen 2.5 Coder 3B",
5
- ollamaId: "qwen2.5-coder:3b",
6
- size: 2,
7
- ramRequired: 8,
8
- vramOptimal: 4,
9
- description: "\u26A0\uFE0F May not support tool calling well",
10
- speed: "~60 tok/s on M1",
11
- quality: "limited",
4
+ name: "Qwen 2.5 Coder 14B",
5
+ ollamaId: "qwen2.5-coder:14b",
6
+ size: 9,
7
+ ramRequired: 24,
8
+ vramOptimal: 12,
9
+ description: "Recommended default for coding when your machine can handle it",
10
+ speed: "~25 tok/s on M1 Pro",
11
+ quality: "best",
12
+ },
13
+ {
14
+ name: "DeepSeek Coder V2 16B",
15
+ ollamaId: "deepseek-coder-v2:16b",
16
+ size: 9,
17
+ ramRequired: 24,
18
+ vramOptimal: 12,
19
+ description: "Strong higher-quality alternative for coding",
20
+ speed: "~30 tok/s on M1 Pro",
21
+ quality: "great",
12
22
  },
13
23
  {
14
24
  name: "Qwen 2.5 Coder 7B",
@@ -16,20 +26,10 @@ const MODELS = [
16
26
  size: 5,
17
27
  ramRequired: 16,
18
28
  vramOptimal: 8,
19
- description: "Sweet spot for most machines",
29
+ description: "Fallback for mid-range machines when 14B is too heavy",
20
30
  speed: "~45 tok/s on M1",
21
31
  quality: "great",
22
32
  },
23
- {
24
- name: "Qwen 2.5 Coder 14B",
25
- ollamaId: "qwen2.5-coder:14b",
26
- size: 9,
27
- ramRequired: 32,
28
- vramOptimal: 16,
29
- description: "High quality coding",
30
- speed: "~25 tok/s on M1 Pro",
31
- quality: "best",
32
- },
33
33
  {
34
34
  name: "Qwen 2.5 Coder 32B",
35
35
  ollamaId: "qwen2.5-coder:32b",
@@ -40,23 +40,13 @@ const MODELS = [
40
40
  speed: "~12 tok/s on M1 Max",
41
41
  quality: "best",
42
42
  },
43
- {
44
- name: "DeepSeek Coder V2 16B",
45
- ollamaId: "deepseek-coder-v2:16b",
46
- size: 9,
47
- ramRequired: 32,
48
- vramOptimal: 16,
49
- description: "Strong alternative for coding",
50
- speed: "~30 tok/s on M1 Pro",
51
- quality: "great",
52
- },
53
43
  {
54
44
  name: "CodeLlama 7B",
55
45
  ollamaId: "codellama:7b",
56
46
  size: 4,
57
47
  ramRequired: 16,
58
48
  vramOptimal: 8,
59
- description: "Meta's coding model",
49
+ description: "Older fallback coding model",
60
50
  speed: "~40 tok/s on M1",
61
51
  quality: "good",
62
52
  },
@@ -66,10 +56,20 @@ const MODELS = [
66
56
  size: 4,
67
57
  ramRequired: 16,
68
58
  vramOptimal: 8,
69
- description: "Good for code completion",
59
+ description: "Completion-focused fallback",
70
60
  speed: "~40 tok/s on M1",
71
61
  quality: "good",
72
62
  },
63
+ {
64
+ name: "Qwen 2.5 Coder 3B",
65
+ ollamaId: "qwen2.5-coder:3b",
66
+ size: 2,
67
+ ramRequired: 8,
68
+ vramOptimal: 4,
69
+ description: "⚠️ Last-resort fallback — may struggle with tool calling",
70
+ speed: "~60 tok/s on M1",
71
+ quality: "limited",
72
+ },
73
73
  ];
74
74
  function scoreModel(model, ramGB, vramGB) {
75
75
  if (ramGB < model.ramRequired)
@@ -86,6 +86,23 @@ function scoreModel(model, ramGB, vramGB) {
86
86
  }
87
87
  const qualityOrder = { best: 3, great: 2, good: 1, limited: 0 };
88
88
  const fitOrder = { perfect: 4, good: 3, tight: 2, skip: 1 };
89
+ function recommendationPriority(model) {
90
+ if (model.ollamaId === "qwen2.5-coder:14b")
91
+ return 100;
92
+ if (model.ollamaId === "deepseek-coder-v2:16b")
93
+ return 95;
94
+ if (model.ollamaId === "qwen2.5-coder:7b")
95
+ return 80;
96
+ if (model.ollamaId === "qwen2.5-coder:32b")
97
+ return 75;
98
+ if (model.ollamaId === "codellama:7b")
99
+ return 60;
100
+ if (model.ollamaId === "starcoder2:7b")
101
+ return 55;
102
+ if (model.ollamaId === "qwen2.5-coder:3b")
103
+ return 10;
104
+ return 0;
105
+ }
89
106
  export function getRecommendations(hardware) {
90
107
  const ramGB = hardware.ram / (1024 * 1024 * 1024);
91
108
  const vramGB = hardware.gpu?.vram ? hardware.gpu.vram / (1024 * 1024 * 1024) : 0;
@@ -95,11 +112,14 @@ export function getRecommendations(hardware) {
95
112
  ...m,
96
113
  fit: scoreModel(m, ramGB, effectiveVRAM),
97
114
  }));
98
- // Sort: perfect first, then by quality descending
115
+ // Sort: fit first, then strongly prefer better coding defaults over tiny fallback models
99
116
  scored.sort((a, b) => {
100
117
  const fitDiff = (fitOrder[b.fit] ?? 0) - (fitOrder[a.fit] ?? 0);
101
118
  if (fitDiff !== 0)
102
119
  return fitDiff;
120
+ const priorityDiff = recommendationPriority(b) - recommendationPriority(a);
121
+ if (priorityDiff !== 0)
122
+ return priorityDiff;
103
123
  return (qualityOrder[b.quality] ?? 0) - (qualityOrder[a.quality] ?? 0);
104
124
  });
105
125
  return scored;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {