codemaxxing 1.1.1 → 1.1.3

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
 
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
@@ -460,9 +460,12 @@ function App() {
460
460
  : "No MCP servers connected.");
461
461
  return;
462
462
  }
463
- // Commands below require an active LLM connection
464
- if (!agent) {
465
- addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
463
+ // Commands that work without an agent (needed for first-time setup)
464
+ // /models and /login are handled below and don't need an agent
465
+ const agentlessCommands = ["/models", "/model", "/login"];
466
+ const isAgentlessCmd = agentlessCommands.some(cmd => trimmed === cmd || trimmed.startsWith(cmd + " "));
467
+ if (!agent && !isAgentlessCmd) {
468
+ addMsg("info", "⚠ No LLM connected.\n Use /login to authenticate, then /models to pick a model.");
466
469
  return;
467
470
  }
468
471
  if (trimmed === "/reset") {
@@ -822,6 +825,8 @@ function App() {
822
825
  setLoading,
823
826
  setSpinnerMsg,
824
827
  agent,
828
+ streaming,
829
+ loading,
825
830
  setModelName,
826
831
  addMsg,
827
832
  exit,
@@ -851,7 +856,7 @@ function App() {
851
856
  default:
852
857
  return _jsx(Text, { children: msg.text }, msg.id);
853
858
  }
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 }))] }));
859
+ }), 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 }))] }));
855
860
  }
856
861
  // Clear screen before render
857
862
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
@@ -69,8 +69,21 @@ export async function connectToProvider(isRetry, ctx) {
69
69
  else {
70
70
  info.push("✗ No local LLM server found.");
71
71
  ctx.setConnectionInfo([...info]);
72
+ // Check if user has saved credentials — if so, auto-show model picker
73
+ const { getCredential } = await import("../utils/auth.js");
74
+ const hasAnyCreds = !!getCredential("anthropic") || !!getCredential("openai") ||
75
+ !!getCredential("openrouter") || !!getCredential("qwen") ||
76
+ !!getCredential("copilot");
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.");
80
+ ctx.setConnectionInfo([...info]);
81
+ ctx.setReady(true);
82
+ // The user will run /models, which now works without an agent
83
+ return;
84
+ }
85
+ // No creds found — show the setup wizard
72
86
  ctx.setReady(true);
73
- // Show the setup wizard on first run
74
87
  ctx.setWizardScreen("connection");
75
88
  ctx.setWizardIndex(0);
76
89
  return;
@@ -179,4 +192,15 @@ export async function connectToProvider(isRetry, ctx) {
179
192
  if (isRetry) {
180
193
  ctx.addMsg("info", `✅ Connected to ${provider.model}`);
181
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
+ }
182
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;
@@ -108,7 +115,10 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
108
115
  ctx.setLoading(true);
109
116
  ctx.setSpinnerMsg("Waiting for Anthropic authorization...");
110
117
  loginAnthropicOAuth((msg) => ctx.addMsg("info", msg))
111
- .then((cred) => { ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); ctx.setLoading(false); })
118
+ .then((cred) => {
119
+ ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})\n Next: type /models to pick a model`);
120
+ ctx.setLoading(false);
121
+ })
112
122
  .catch((err) => {
113
123
  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`);
114
124
  ctx.setLoading(false);
@@ -127,7 +137,7 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
127
137
  ctx.setSpinnerMsg("Waiting for OpenAI authorization...");
128
138
  loginOpenAICodexOAuth((msg) => ctx.addMsg("info", msg))
129
139
  .then((cred) => {
130
- ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})`);
140
+ ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})\n Next: type /models to pick a model`);
131
141
  ctx.setLoading(false);
132
142
  })
133
143
  .catch((err) => {
@@ -150,7 +160,7 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
150
160
  ctx.setLoading(true);
151
161
  ctx.setSpinnerMsg("Waiting for GitHub authorization...");
152
162
  copilotDeviceFlow((msg) => ctx.addMsg("info", msg))
153
- .then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!`); ctx.setLoading(false); })
163
+ .then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Next: type /models to pick a model`); ctx.setLoading(false); })
154
164
  .catch((err) => { ctx.addMsg("error", `Copilot auth failed: ${err.message}`); ctx.setLoading(false); });
155
165
  }
156
166
  else if (method === "api-key") {
@@ -186,7 +196,7 @@ function handleLoginPicker(_inputChar, key, ctx) {
186
196
  ctx.setLoading(true);
187
197
  ctx.setSpinnerMsg("Waiting for authorization...");
188
198
  openRouterOAuth((msg) => ctx.addMsg("info", msg))
189
- .then(() => { ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); ctx.setLoading(false); })
199
+ .then(() => { ctx.addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.\n Next: type /models to pick a model`); ctx.setLoading(false); })
190
200
  .catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
191
201
  }
192
202
  else if (methods[0] === "device-flow") {
@@ -195,7 +205,7 @@ function handleLoginPicker(_inputChar, key, ctx) {
195
205
  ctx.setLoading(true);
196
206
  ctx.setSpinnerMsg("Waiting for GitHub authorization...");
197
207
  copilotDeviceFlow((msg) => ctx.addMsg("info", msg))
198
- .then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!`); ctx.setLoading(false); })
208
+ .then(() => { ctx.addMsg("info", `✅ GitHub Copilot authenticated!\n Next: type /models to pick a model`); ctx.setLoading(false); })
199
209
  .catch((err) => { ctx.addMsg("error", `Copilot auth failed: ${err.message}`); ctx.setLoading(false); });
200
210
  }
201
211
  else if (methods[0] === "api-key") {
@@ -416,9 +426,23 @@ function handleModelPicker(_inputChar, key, ctx) {
416
426
  ctx.refreshConnectionBanner();
417
427
  }
418
428
  else if (selected && !ctx.agent) {
419
- // First-time: trigger reconnect which will create the agent
429
+ // First-time: save model selection to config, then reconnect
420
430
  ctx.addMsg("info", `Initializing with ${selected.name}...`);
421
- ctx.connectToProvider?.(false);
431
+ // Save selected model to config so connectToProvider picks it up
432
+ import("../config.js").then(({ loadConfig, saveConfig }) => {
433
+ const config = loadConfig();
434
+ config.provider = {
435
+ baseUrl: selected.baseUrl,
436
+ apiKey: selected.apiKey,
437
+ model: selected.name,
438
+ type: selected.providerType === "anthropic" ? "anthropic" : "openai",
439
+ };
440
+ saveConfig(config);
441
+ // Now reconnect with the saved config
442
+ ctx.connectToProvider?.(false);
443
+ }).catch((err) => {
444
+ ctx.addMsg("error", `Failed to initialize: ${err.message}`);
445
+ });
422
446
  }
423
447
  ctx.setModelPickerGroups(null);
424
448
  ctx.setProviderPicker(null);
@@ -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 ──
@@ -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());
@@ -38,6 +38,7 @@ export async function chatWithResponsesAPI(options) {
38
38
  inputItems.push({
39
39
  type: "function_call",
40
40
  id: tc.id,
41
+ call_id: tc.id,
41
42
  name: tc.function?.name || tc.name || "",
42
43
  arguments: typeof tc.function?.arguments === "string"
43
44
  ? tc.function.arguments
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {