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 +1 -1
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +73 -1
- package/dist/index.js +9 -4
- package/dist/ui/connection.js +25 -1
- package/dist/ui/input-router.d.ts +2 -0
- package/dist/ui/input-router.js +31 -7
- package/dist/ui/paste-interceptor.js +9 -0
- package/dist/utils/auth.js +6 -1
- package/dist/utils/responses-api.js +1 -0
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
|
|
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");
|
package/dist/ui/connection.js
CHANGED
|
@@ -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;
|
package/dist/ui/input-router.js
CHANGED
|
@@ -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) => {
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
429
|
+
// First-time: save model selection to config, then reconnect
|
|
420
430
|
ctx.addMsg("info", `Initializing with ${selected.name}...`);
|
|
421
|
-
|
|
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 ──
|
package/dist/utils/auth.js
CHANGED
|
@@ -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
|