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 +4 -3
- package/dist/agent.d.ts +15 -0
- package/dist/agent.js +73 -1
- package/dist/index.js +125 -128
- package/dist/ui/connection-types.d.ts +1 -0
- package/dist/ui/connection.js +14 -3
- package/dist/ui/input-router.d.ts +2 -0
- package/dist/ui/input-router.js +33 -18
- package/dist/ui/paste-interceptor.js +9 -0
- package/dist/ui/pickers.js +8 -8
- package/dist/ui/wizard-types.d.ts +1 -0
- package/dist/ui/wizard.js +7 -3
- package/dist/utils/auth.js +6 -1
- package/dist/utils/models.js +52 -32
- 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
|
|
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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");
|
package/dist/ui/connection.js
CHANGED
|
@@ -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
|
|
79
|
-
info.push("✔ Found saved credentials.
|
|
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
|
-
|
|
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;
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
479
|
-
{ id: "
|
|
480
|
-
{ id: "qwen2.5-coder:
|
|
481
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium quality, needs
|
|
482
|
-
{ id: "
|
|
483
|
-
{ id: "
|
|
484
|
-
{ id: "
|
|
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 ──
|
package/dist/ui/pickers.js
CHANGED
|
@@ -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:
|
|
78
|
-
{ id: "
|
|
79
|
-
{ id: "qwen2.5-coder:
|
|
80
|
-
{ id: "qwen2.5-coder:32b", name: "Qwen 2.5 Coder 32B", size: "20 GB", desc: "Premium, needs
|
|
81
|
-
{ id: "
|
|
82
|
-
{ id: "
|
|
83
|
-
{ id: "
|
|
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
|
|
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!
|
|
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", "
|
|
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
|
});
|
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());
|
package/dist/utils/models.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
import { execSync } from "child_process";
|
|
2
2
|
const MODELS = [
|
|
3
3
|
{
|
|
4
|
-
name: "Qwen 2.5 Coder
|
|
5
|
-
ollamaId: "qwen2.5-coder:
|
|
6
|
-
size:
|
|
7
|
-
ramRequired:
|
|
8
|
-
vramOptimal:
|
|
9
|
-
description: "
|
|
10
|
-
speed: "~
|
|
11
|
-
quality: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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:
|
|
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;
|