@parallel-cli/parallel 0.4.0 → 0.4.1
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 +13 -2
- package/dist/config.js +70 -0
- package/dist/controller.js +20 -0
- package/dist/i18n.js +144 -0
- package/dist/pricing.js +27 -0
- package/dist/ui/App.js +94 -25
- package/dist/ui/SettingsPanel.js +161 -53
- package/dist/ui/Wizard.js +13 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ The philosophy is simple: agents can move in parallel, but the human keeps the c
|
|
|
34
34
|
- Avoid silent overwrites with adaptive merge-on-write for file edits.
|
|
35
35
|
- Pause, resume, stop, focus, restore, and steer agents while they are running.
|
|
36
36
|
- Review live diffs, notes, costs, sessions, skills, and specialists from built-in views.
|
|
37
|
-
- Use any OpenAI-compatible provider
|
|
37
|
+
- Use any OpenAI-compatible provider — 18 pre-configured cloud providers (DeepSeek, xAI/Grok, Perplexity, Cohere, DeepInfra, Fireworks, Cerebras, Novita, Hyperbolic, SambaNova, and more), plus Ollama for local models, and any custom OpenAI-compatible endpoint.
|
|
38
38
|
- Choose shell approval behavior: `ask`, `auto-safe`, or `yolo`.
|
|
39
39
|
- Track token usage and estimated cost per agent and per session.
|
|
40
40
|
|
|
@@ -281,7 +281,9 @@ When there is exactly one agent, commands such as `/undo`, `/focus`, `/pause`, `
|
|
|
281
281
|
|
|
282
282
|
## Providers
|
|
283
283
|
|
|
284
|
-
Parallel
|
|
284
|
+
Parallel ships with **18 pre-configured cloud providers** with verified endpoints and curated model lists, plus **Ollama** for local models with automatic model detection. All providers use OpenAI-compatible chat completions with tool calling. You can also add any custom OpenAI-compatible endpoint.
|
|
285
|
+
|
|
286
|
+
The built-in DeepSeek preset works out of the box once an API key is configured. Additional providers like xAI/Grok, Perplexity, Cohere, DeepInfra, Fireworks, Cerebras, Novita, Hyperbolic, and SambaNova are available for selection during setup or from the Providers settings submenu.
|
|
285
287
|
|
|
286
288
|
Environment variables:
|
|
287
289
|
|
|
@@ -346,6 +348,15 @@ The runtime is intentionally small:
|
|
|
346
348
|
|
|
347
349
|
## Changelog
|
|
348
350
|
|
|
351
|
+
### 0.4.1
|
|
352
|
+
|
|
353
|
+
- **18 pre-configured cloud providers** with verified endpoints and curated model lists (up from 8). Added: xAI/Grok, Perplexity, Cohere, DeepInfra, Fireworks, Cerebras, Novita, Hyperbolic, SambaNova.
|
|
354
|
+
- **Ollama (local models)** as a first-class preset with automatic connectivity check and model detection.
|
|
355
|
+
- **Wizard redesign** — provider selection now grouped by category (Configured, Cloud, Local, Custom) instead of a flat list.
|
|
356
|
+
- **Settings reorganization** — all provider actions (keys, models, pricing, add/remove) consolidated under a "Providers" submenu; settings root reduced from 13 to 8 items.
|
|
357
|
+
- **Provider removal** — can now remove configured providers from settings.
|
|
358
|
+
- Custom provider option always available for any OpenAI-compatible endpoint.
|
|
359
|
+
|
|
349
360
|
### 0.4.0
|
|
350
361
|
|
|
351
362
|
- **Removed `/spawn`** — use `/task` instead. The alias was redundant and its removal simplified the command registry.
|
package/dist/config.js
CHANGED
|
@@ -69,6 +69,76 @@ export const PROVIDER_PRESETS = [
|
|
|
69
69
|
models: ['openai/gpt-oss-120b', 'openai/gpt-oss-20b'],
|
|
70
70
|
defaultModel: 'openai/gpt-oss-120b',
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'xAI',
|
|
74
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
75
|
+
apiKey: '',
|
|
76
|
+
models: ['grok-4', 'grok-3-beta', 'grok-3-mini'],
|
|
77
|
+
defaultModel: 'grok-3-beta',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Perplexity',
|
|
81
|
+
baseUrl: 'https://api.perplexity.ai',
|
|
82
|
+
apiKey: '',
|
|
83
|
+
models: ['sonar-pro', 'sonar', 'sonar-reasoning'],
|
|
84
|
+
defaultModel: 'sonar-pro',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Cohere',
|
|
88
|
+
baseUrl: 'https://api.cohere.com/v2',
|
|
89
|
+
apiKey: '',
|
|
90
|
+
models: ['command-a', 'command-r-plus', 'command-r'],
|
|
91
|
+
defaultModel: 'command-a',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'DeepInfra',
|
|
95
|
+
baseUrl: 'https://api.deepinfra.com/v1/openai',
|
|
96
|
+
apiKey: '',
|
|
97
|
+
models: ['meta-llama/llama-4-maverick', 'deepseek-ai/deepseek-chat', 'microsoft/wizardlm-2-8x22b'],
|
|
98
|
+
defaultModel: 'meta-llama/llama-4-maverick',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'Fireworks',
|
|
102
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
103
|
+
apiKey: '',
|
|
104
|
+
models: ['llama-4-maverick', 'llama-4-scout', 'mixtral-8x22b'],
|
|
105
|
+
defaultModel: 'llama-4-maverick',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'Cerebras',
|
|
109
|
+
baseUrl: 'https://api.cerebras.ai/v1',
|
|
110
|
+
apiKey: '',
|
|
111
|
+
models: ['llama-3.3-70b', 'llama-3.1-8b'],
|
|
112
|
+
defaultModel: 'llama-3.3-70b',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'Novita',
|
|
116
|
+
baseUrl: 'https://api.novita.ai/v3/openai',
|
|
117
|
+
apiKey: '',
|
|
118
|
+
models: ['deepseek-r1', 'deepseek-v3', 'llama-3.1-70b'],
|
|
119
|
+
defaultModel: 'deepseek-v3',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Hyperbolic',
|
|
123
|
+
baseUrl: 'https://api.hyperbolic.xyz/v1',
|
|
124
|
+
apiKey: '',
|
|
125
|
+
models: ['deepseek-v3', 'llama-4-maverick', 'qwen3-235b'],
|
|
126
|
+
defaultModel: 'deepseek-v3',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'SambaNova',
|
|
130
|
+
baseUrl: 'https://api.sambanova.ai/v1',
|
|
131
|
+
apiKey: '',
|
|
132
|
+
models: ['llama-4-maverick', 'llama-4-scout', 'deepseek-r1'],
|
|
133
|
+
defaultModel: 'llama-4-maverick',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'Ollama',
|
|
137
|
+
baseUrl: 'http://localhost:11434/v1',
|
|
138
|
+
apiKey: '',
|
|
139
|
+
models: ['llama3', 'codellama', 'mistral'],
|
|
140
|
+
defaultModel: 'llama3',
|
|
141
|
+
},
|
|
72
142
|
];
|
|
73
143
|
export const DEFAULTS = {
|
|
74
144
|
providers: [],
|
package/dist/controller.js
CHANGED
|
@@ -718,6 +718,26 @@ export class Controller extends EventEmitter {
|
|
|
718
718
|
this.emit('update');
|
|
719
719
|
return true;
|
|
720
720
|
}
|
|
721
|
+
/** Remove a provider by name. Clears the default if it was the removed one. */
|
|
722
|
+
removeProvider(name) {
|
|
723
|
+
const idx = this.config.providers.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
|
|
724
|
+
if (idx < 0)
|
|
725
|
+
return false;
|
|
726
|
+
this.config.providers.splice(idx, 1);
|
|
727
|
+
if (this.config.defaultProvider.toLowerCase() === name.toLowerCase()) {
|
|
728
|
+
this.config.defaultProvider = this.config.providers[0]?.name ?? '';
|
|
729
|
+
}
|
|
730
|
+
// If the session was using the removed provider, reset it
|
|
731
|
+
if (this.session.providerName.toLowerCase() === name.toLowerCase()) {
|
|
732
|
+
const fallback = this.config.providers[0];
|
|
733
|
+
this.session.providerName = fallback?.name ?? '';
|
|
734
|
+
this.session.model = fallback?.defaultModel ?? '';
|
|
735
|
+
}
|
|
736
|
+
saveConfig(this.config);
|
|
737
|
+
this.llmCache.clear();
|
|
738
|
+
this.emit('update');
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
721
741
|
setGlobalApprovalMode(mode) {
|
|
722
742
|
this.config.approvalMode = mode;
|
|
723
743
|
saveConfig(this.config);
|
package/dist/i18n.js
CHANGED
|
@@ -276,6 +276,8 @@ const en = {
|
|
|
276
276
|
'sset.model': 'Session model: {pm}',
|
|
277
277
|
'sset.approvals': 'Shell approvals (session): {mode}',
|
|
278
278
|
'sset.sound': 'Sound (session): {state}',
|
|
279
|
+
'sset.providers.title': 'Change provider & model',
|
|
280
|
+
'sset.providers.back': 'Back',
|
|
279
281
|
'set.esc': 'Esc: close',
|
|
280
282
|
// agent questions (auto-run)
|
|
281
283
|
'q.title': '❓ AGENT QUESTION',
|
|
@@ -327,6 +329,40 @@ const en = {
|
|
|
327
329
|
'set.priceBad': 'Invalid format — expected: input, output (e.g. 0.27, 1.10)',
|
|
328
330
|
'set.newSkillName': 'Skill name (a .md template will be created in ~/.parallel/skills)',
|
|
329
331
|
'set.newSpecialistName': 'Specialist name (a .md template will be created in ~/.parallel/specialists)',
|
|
332
|
+
// provider-pick section headers
|
|
333
|
+
'wiz.provider.section.configured': 'Configured',
|
|
334
|
+
'wiz.provider.section.cloud': 'Cloud Providers',
|
|
335
|
+
'wiz.provider.section.local': 'Local',
|
|
336
|
+
'wiz.provider.customDetail': 'Configure manually',
|
|
337
|
+
'wiz.provider.ollamaDetail': 'Local — no API key needed',
|
|
338
|
+
'wiz.provider.ollama.checking': 'Checking Ollama at localhost:11434...',
|
|
339
|
+
'wiz.provider.ollama.found': 'Found {n} models',
|
|
340
|
+
'wiz.provider.ollama.notFound': 'Could not reach Ollama at localhost:11434. Is it running?',
|
|
341
|
+
'wiz.provider.ollama.continueDefaults': 'Continue with defaults',
|
|
342
|
+
'wiz.provider.ollama.goBack': 'Go back',
|
|
343
|
+
// provider settings (future use)
|
|
344
|
+
'set.providers': 'Providers',
|
|
345
|
+
'set.providers.title': 'Providers',
|
|
346
|
+
'set.providers.add': 'Add Provider...',
|
|
347
|
+
'set.providers.back': 'Back',
|
|
348
|
+
'set.providerDetail.title': '{name}',
|
|
349
|
+
'set.providerDetail.key': 'Set API Key',
|
|
350
|
+
'set.providerDetail.models': 'Manage Models',
|
|
351
|
+
'set.providerDetail.pricing': 'Pricing',
|
|
352
|
+
'set.providerDetail.setDefault': 'Set as Default',
|
|
353
|
+
'set.providerDetail.remove': 'Remove Provider',
|
|
354
|
+
'set.providerDetail.back': 'Back',
|
|
355
|
+
'set.defaultProvider': 'Default Provider',
|
|
356
|
+
'set.defaultProvider.title': 'Default Provider',
|
|
357
|
+
'set.removeProvider.title': 'Remove {name}?',
|
|
358
|
+
'set.removeProvider.confirm': 'This cannot be undone.',
|
|
359
|
+
'set.removeProvider.yes': 'Yes, remove',
|
|
360
|
+
'set.removeProvider.no': 'Cancel',
|
|
361
|
+
'set.status.configured': 'configured',
|
|
362
|
+
'set.status.noKey': 'no key',
|
|
363
|
+
'set.status.local': 'local',
|
|
364
|
+
'set.status.default': 'default',
|
|
365
|
+
'set.key.masked': '{masked}',
|
|
330
366
|
};
|
|
331
367
|
const fr = {
|
|
332
368
|
tagline: 'Des agents de code en parallèle, en temps réel — sans verrous, sans attente.',
|
|
@@ -574,6 +610,8 @@ const fr = {
|
|
|
574
610
|
'sset.model': 'Modèle de session : {pm}',
|
|
575
611
|
'sset.approvals': 'Approbations shell (session) : {mode}',
|
|
576
612
|
'sset.sound': 'Son (session) : {state}',
|
|
613
|
+
'sset.providers.title': 'Changer de fournisseur et de modèle',
|
|
614
|
+
'sset.providers.back': 'Retour',
|
|
577
615
|
'set.esc': 'Esc : fermer',
|
|
578
616
|
'q.title': '❓ QUESTION D’UN AGENT',
|
|
579
617
|
'q.pending': ' ({n} en attente)',
|
|
@@ -619,6 +657,40 @@ const fr = {
|
|
|
619
657
|
'set.priceBad': 'Format invalide — attendu : input, output (ex : 0.27, 1.10)',
|
|
620
658
|
'set.newSkillName': 'Nom du skill (un modèle .md sera créé dans ~/.parallel/skills)',
|
|
621
659
|
'set.newSpecialistName': 'Nom du spécialiste (un modèle .md sera créé dans ~/.parallel/specialists)',
|
|
660
|
+
// provider-pick section headers
|
|
661
|
+
'wiz.provider.section.configured': 'Configurés',
|
|
662
|
+
'wiz.provider.section.cloud': 'Fournisseurs cloud',
|
|
663
|
+
'wiz.provider.section.local': 'Local',
|
|
664
|
+
'wiz.provider.customDetail': 'Configurer manuellement',
|
|
665
|
+
'wiz.provider.ollamaDetail': 'Local — pas de clef API requise',
|
|
666
|
+
'wiz.provider.ollama.checking': 'Vérification de Ollama sur localhost:11434…',
|
|
667
|
+
'wiz.provider.ollama.found': '{n} modèles trouvés',
|
|
668
|
+
'wiz.provider.ollama.notFound': 'Ollama est injoignable sur localhost:11434. Est-il lancé ?',
|
|
669
|
+
'wiz.provider.ollama.continueDefaults': 'Continuer avec les valeurs par défaut',
|
|
670
|
+
'wiz.provider.ollama.goBack': 'Retour',
|
|
671
|
+
// provider settings (future use)
|
|
672
|
+
'set.providers': 'Fournisseurs',
|
|
673
|
+
'set.providers.title': 'Fournisseurs',
|
|
674
|
+
'set.providers.add': 'Ajouter un fournisseur…',
|
|
675
|
+
'set.providers.back': 'Retour',
|
|
676
|
+
'set.providerDetail.title': '{name}',
|
|
677
|
+
'set.providerDetail.key': 'Définir la clef API',
|
|
678
|
+
'set.providerDetail.models': 'Gérer les modèles',
|
|
679
|
+
'set.providerDetail.pricing': 'Tarifs',
|
|
680
|
+
'set.providerDetail.setDefault': 'Définir par défaut',
|
|
681
|
+
'set.providerDetail.remove': 'Supprimer le fournisseur',
|
|
682
|
+
'set.providerDetail.back': 'Retour',
|
|
683
|
+
'set.defaultProvider': 'Fournisseur par défaut',
|
|
684
|
+
'set.defaultProvider.title': 'Fournisseur par défaut',
|
|
685
|
+
'set.removeProvider.title': 'Supprimer {name} ?',
|
|
686
|
+
'set.removeProvider.confirm': 'Cette action est irréversible.',
|
|
687
|
+
'set.removeProvider.yes': 'Oui, supprimer',
|
|
688
|
+
'set.removeProvider.no': 'Annuler',
|
|
689
|
+
'set.status.configured': 'configuré',
|
|
690
|
+
'set.status.noKey': 'pas de clef',
|
|
691
|
+
'set.status.local': 'local',
|
|
692
|
+
'set.status.default': 'défaut',
|
|
693
|
+
'set.key.masked': '{masked}',
|
|
622
694
|
};
|
|
623
695
|
const es = {
|
|
624
696
|
tagline: 'Agentes de código en paralelo, en tiempo real — sin bloqueos, sin esperas.',
|
|
@@ -866,6 +938,8 @@ const es = {
|
|
|
866
938
|
'sset.model': 'Modelo de sesión: {pm}',
|
|
867
939
|
'sset.approvals': 'Aprobaciones shell (sesión): {mode}',
|
|
868
940
|
'sset.sound': 'Sonido (sesión): {state}',
|
|
941
|
+
'sset.providers.title': 'Cambiar proveedor y modelo',
|
|
942
|
+
'sset.providers.back': 'Volver',
|
|
869
943
|
'set.esc': 'Esc: cerrar',
|
|
870
944
|
'q.title': '❓ PREGUNTA DE UN AGENTE',
|
|
871
945
|
'q.pending': ' ({n} pendientes)',
|
|
@@ -911,6 +985,40 @@ const es = {
|
|
|
911
985
|
'set.priceBad': 'Formato inválido — esperado: input, output (ej.: 0.27, 1.10)',
|
|
912
986
|
'set.newSkillName': 'Nombre del skill (se creará una plantilla .md en ~/.parallel/skills)',
|
|
913
987
|
'set.newSpecialistName': 'Nombre del especialista (se creará una plantilla .md en ~/.parallel/specialists)',
|
|
988
|
+
// provider-pick section headers
|
|
989
|
+
'wiz.provider.section.configured': 'Configurados',
|
|
990
|
+
'wiz.provider.section.cloud': 'Proveedores en la nube',
|
|
991
|
+
'wiz.provider.section.local': 'Local',
|
|
992
|
+
'wiz.provider.customDetail': 'Configurar manualmente',
|
|
993
|
+
'wiz.provider.ollamaDetail': 'Local — no requiere clave API',
|
|
994
|
+
'wiz.provider.ollama.checking': 'Comprobando Ollama en localhost:11434…',
|
|
995
|
+
'wiz.provider.ollama.found': '{n} modelos encontrados',
|
|
996
|
+
'wiz.provider.ollama.notFound': 'No se pudo contactar con Ollama en localhost:11434. ¿Está en ejecución?',
|
|
997
|
+
'wiz.provider.ollama.continueDefaults': 'Continuar con valores predeterminados',
|
|
998
|
+
'wiz.provider.ollama.goBack': 'Volver',
|
|
999
|
+
// provider settings (future use)
|
|
1000
|
+
'set.providers': 'Proveedores',
|
|
1001
|
+
'set.providers.title': 'Proveedores',
|
|
1002
|
+
'set.providers.add': 'Añadir proveedor…',
|
|
1003
|
+
'set.providers.back': 'Volver',
|
|
1004
|
+
'set.providerDetail.title': '{name}',
|
|
1005
|
+
'set.providerDetail.key': 'Establecer clave API',
|
|
1006
|
+
'set.providerDetail.models': 'Gestionar modelos',
|
|
1007
|
+
'set.providerDetail.pricing': 'Precios',
|
|
1008
|
+
'set.providerDetail.setDefault': 'Establecer como predeterminado',
|
|
1009
|
+
'set.providerDetail.remove': 'Eliminar proveedor',
|
|
1010
|
+
'set.providerDetail.back': 'Volver',
|
|
1011
|
+
'set.defaultProvider': 'Proveedor predeterminado',
|
|
1012
|
+
'set.defaultProvider.title': 'Proveedor predeterminado',
|
|
1013
|
+
'set.removeProvider.title': '¿Eliminar {name}?',
|
|
1014
|
+
'set.removeProvider.confirm': 'Esta acción no se puede deshacer.',
|
|
1015
|
+
'set.removeProvider.yes': 'Sí, eliminar',
|
|
1016
|
+
'set.removeProvider.no': 'Cancelar',
|
|
1017
|
+
'set.status.configured': 'configurado',
|
|
1018
|
+
'set.status.noKey': 'sin clave',
|
|
1019
|
+
'set.status.local': 'local',
|
|
1020
|
+
'set.status.default': 'predeterminado',
|
|
1021
|
+
'set.key.masked': '{masked}',
|
|
914
1022
|
};
|
|
915
1023
|
const zh = {
|
|
916
1024
|
tagline: '并行实时代码智能体 — 无锁定,无等待。',
|
|
@@ -1158,6 +1266,8 @@ const zh = {
|
|
|
1158
1266
|
'sset.model': '会话模型:{pm}',
|
|
1159
1267
|
'sset.approvals': 'Shell 批准(会话):{mode}',
|
|
1160
1268
|
'sset.sound': '声音(会话):{state}',
|
|
1269
|
+
'sset.providers.title': '更改提供商和模型',
|
|
1270
|
+
'sset.providers.back': '返回',
|
|
1161
1271
|
'set.esc': 'Esc:关闭',
|
|
1162
1272
|
'q.title': '❓ 智能体提问',
|
|
1163
1273
|
'q.pending': '({n} 个待处理)',
|
|
@@ -1203,5 +1313,39 @@ const zh = {
|
|
|
1203
1313
|
'set.priceBad': '格式无效 — 应为:input, output(例:0.27, 1.10)',
|
|
1204
1314
|
'set.newSkillName': '技能名称(将在 ~/.parallel/skills 创建 .md 模板)',
|
|
1205
1315
|
'set.newSpecialistName': '专家名称(将在 ~/.parallel/specialists 创建 .md 模板)',
|
|
1316
|
+
// provider-pick section headers
|
|
1317
|
+
'wiz.provider.section.configured': '已配置',
|
|
1318
|
+
'wiz.provider.section.cloud': '云提供商',
|
|
1319
|
+
'wiz.provider.section.local': '本地',
|
|
1320
|
+
'wiz.provider.customDetail': '手动配置',
|
|
1321
|
+
'wiz.provider.ollamaDetail': '本地 — 无需 API 密钥',
|
|
1322
|
+
'wiz.provider.ollama.checking': '正在检查 Ollama(localhost:11434)…',
|
|
1323
|
+
'wiz.provider.ollama.found': '找到 {n} 个模型',
|
|
1324
|
+
'wiz.provider.ollama.notFound': '无法连接到 localhost:11434 的 Ollama。它在运行吗?',
|
|
1325
|
+
'wiz.provider.ollama.continueDefaults': '继续使用默认值',
|
|
1326
|
+
'wiz.provider.ollama.goBack': '返回',
|
|
1327
|
+
// provider settings (future use)
|
|
1328
|
+
'set.providers': '提供商',
|
|
1329
|
+
'set.providers.title': '提供商',
|
|
1330
|
+
'set.providers.add': '添加提供商…',
|
|
1331
|
+
'set.providers.back': '返回',
|
|
1332
|
+
'set.providerDetail.title': '{name}',
|
|
1333
|
+
'set.providerDetail.key': '设置 API 密钥',
|
|
1334
|
+
'set.providerDetail.models': '管理模型',
|
|
1335
|
+
'set.providerDetail.pricing': '定价',
|
|
1336
|
+
'set.providerDetail.setDefault': '设为默认',
|
|
1337
|
+
'set.providerDetail.remove': '移除提供商',
|
|
1338
|
+
'set.providerDetail.back': '返回',
|
|
1339
|
+
'set.defaultProvider': '默认提供商',
|
|
1340
|
+
'set.defaultProvider.title': '默认提供商',
|
|
1341
|
+
'set.removeProvider.title': '移除 {name}?',
|
|
1342
|
+
'set.removeProvider.confirm': '此操作无法撤销。',
|
|
1343
|
+
'set.removeProvider.yes': '是,移除',
|
|
1344
|
+
'set.removeProvider.no': '取消',
|
|
1345
|
+
'set.status.configured': '已配置',
|
|
1346
|
+
'set.status.noKey': '无密钥',
|
|
1347
|
+
'set.status.local': '本地',
|
|
1348
|
+
'set.status.default': '默认',
|
|
1349
|
+
'set.key.masked': '{masked}',
|
|
1206
1350
|
};
|
|
1207
1351
|
const STRINGS = { en, fr, es, zh };
|
package/dist/pricing.js
CHANGED
|
@@ -30,6 +30,33 @@ const BUILTIN = {
|
|
|
30
30
|
// Alibaba
|
|
31
31
|
'qwen2.5-coder': { input: 0.09, output: 0.09 },
|
|
32
32
|
'qwen-max': { input: 1.6, output: 6.4 },
|
|
33
|
+
// xAI (Grok)
|
|
34
|
+
'grok-4': { input: 4.0, output: 16.0 },
|
|
35
|
+
'grok-3-beta': { input: 3.0, output: 12.0 },
|
|
36
|
+
'grok-3-mini': { input: 0.55, output: 2.2 },
|
|
37
|
+
// Perplexity
|
|
38
|
+
'sonar-pro': { input: 3.0, output: 15.0 },
|
|
39
|
+
'sonar': { input: 1.0, output: 1.0 },
|
|
40
|
+
'sonar-reasoning': { input: 2.0, output: 16.0 },
|
|
41
|
+
// Cohere
|
|
42
|
+
'command-a': { input: 2.5, output: 10.0 },
|
|
43
|
+
'command-r-plus': { input: 2.5, output: 10.0 },
|
|
44
|
+
'command-r': { input: 0.5, output: 1.5 },
|
|
45
|
+
// DeepInfra
|
|
46
|
+
'llama-4-maverick': { input: 0.2, output: 0.6 }, // approximate
|
|
47
|
+
'wizardlm-2-8x22b': { input: 0.5, output: 0.5 }, // approximate
|
|
48
|
+
// Fireworks
|
|
49
|
+
'llama-4-scout': { input: 0.1, output: 0.3 }, // approximate
|
|
50
|
+
'mixtral-8x22b': { input: 0.9, output: 0.9 }, // approximate
|
|
51
|
+
// Cerebras
|
|
52
|
+
'llama-3.3-70b @cerebras': { input: 0.5, output: 1.5 }, // approximate
|
|
53
|
+
'llama-3.1-8b': { input: 0.05, output: 0.1 }, // approximate
|
|
54
|
+
// Novita
|
|
55
|
+
'deepseek-r1': { input: 2.0, output: 8.0 },
|
|
56
|
+
'deepseek-v3': { input: 1.25, output: 5.0 },
|
|
57
|
+
'llama-3.1-70b': { input: 0.35, output: 0.4 }, // approximate
|
|
58
|
+
// Hyperbolic
|
|
59
|
+
'qwen3-235b': { input: 0.5, output: 1.5 }, // approximate
|
|
33
60
|
// Local endpoints are free
|
|
34
61
|
'ollama': { input: 0, output: 0 },
|
|
35
62
|
};
|
package/dist/ui/App.js
CHANGED
|
@@ -274,37 +274,106 @@ export function App({ config, initialFolder }) {
|
|
|
274
274
|
.join(', ')
|
|
275
275
|
.slice(0, 60)})`,
|
|
276
276
|
})),
|
|
277
|
-
], onBack: wizardBack, onSelect: chooseSession }) })), phase === 'provider' && providerStep.id === 'pick' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.title'), children: _jsx(SelectList, { items:
|
|
278
|
-
|
|
277
|
+
], onBack: wizardBack, onSelect: chooseSession }) })), phase === 'provider' && providerStep.id === 'pick' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.title'), children: _jsx(SelectList, { items: (() => {
|
|
278
|
+
const items = [];
|
|
279
|
+
// Section: Configured
|
|
280
|
+
const configured = config.providers.map((p) => ({
|
|
279
281
|
label: p.name,
|
|
280
|
-
value:
|
|
281
|
-
|
|
282
|
-
}))
|
|
283
|
-
|
|
284
|
-
label:
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
value: p.name,
|
|
283
|
+
detail: p.apiKey ? undefined : t('wiz.provider.needsKey'),
|
|
284
|
+
}));
|
|
285
|
+
if (configured.length > 0) {
|
|
286
|
+
items.push({ label: t('wiz.provider.section.configured'), value: '', section: true });
|
|
287
|
+
items.push(...configured);
|
|
288
|
+
}
|
|
289
|
+
// Section: Cloud Providers (presets except Ollama, not already configured)
|
|
290
|
+
const cloudPresets = PROVIDER_PRESETS.filter((preset) => preset.name !== 'Ollama' &&
|
|
291
|
+
!config.providers.some((p) => p.name.toLowerCase() === preset.name.toLowerCase()));
|
|
292
|
+
if (cloudPresets.length > 0) {
|
|
293
|
+
items.push({ label: t('wiz.provider.section.cloud'), value: '', section: true });
|
|
294
|
+
items.push(...cloudPresets.map((preset) => ({
|
|
295
|
+
label: preset.name,
|
|
296
|
+
value: preset.name,
|
|
297
|
+
detail: preset.defaultModel,
|
|
298
|
+
})));
|
|
299
|
+
}
|
|
300
|
+
// Section: Local (Ollama only, if not already configured)
|
|
301
|
+
const ollamaPreset = PROVIDER_PRESETS.find((p) => p.name === 'Ollama');
|
|
302
|
+
const ollamaConfigured = config.providers.some((p) => p.name.toLowerCase() === 'ollama');
|
|
303
|
+
if (ollamaPreset && !ollamaConfigured) {
|
|
304
|
+
items.push({ label: t('wiz.provider.section.local'), value: '', section: true });
|
|
305
|
+
items.push({
|
|
306
|
+
label: ollamaPreset.name,
|
|
307
|
+
value: ollamaPreset.name,
|
|
308
|
+
detail: t('wiz.provider.ollamaDetail'),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
// Custom always last
|
|
312
|
+
items.push({
|
|
313
|
+
label: t('wiz.provider.custom'),
|
|
314
|
+
value: '__custom__',
|
|
315
|
+
detail: t('wiz.provider.customDetail'),
|
|
316
|
+
});
|
|
317
|
+
return items;
|
|
318
|
+
})(), onBack: wizardBack, onSelect: async (v) => {
|
|
290
319
|
if (v === '__custom__')
|
|
291
320
|
return setProviderStep({ id: 'name' });
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
321
|
+
// Already-configured provider?
|
|
322
|
+
const existing = config.providers.find((x) => x.name === v);
|
|
323
|
+
if (existing) {
|
|
324
|
+
if (existing.apiKey) {
|
|
325
|
+
ctlRef.current?.setDefaultProvider(v);
|
|
326
|
+
ctlRef.current?.setSessionProvider(v);
|
|
327
|
+
enterMain();
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
setProviderStep({ id: 'key', preset: existing });
|
|
331
|
+
}
|
|
299
332
|
return;
|
|
300
|
-
if (p.apiKey) {
|
|
301
|
-
ctlRef.current?.setDefaultProvider(p.name);
|
|
302
|
-
ctlRef.current?.setSessionProvider(p.name);
|
|
303
|
-
enterMain();
|
|
304
333
|
}
|
|
305
|
-
|
|
306
|
-
|
|
334
|
+
// Must be a preset
|
|
335
|
+
const preset = PROVIDER_PRESETS.find((p) => p.name === v);
|
|
336
|
+
if (!preset)
|
|
337
|
+
return;
|
|
338
|
+
// Ollama: connectivity check with 2s timeout
|
|
339
|
+
if (preset.name.toLowerCase() === 'ollama') {
|
|
340
|
+
setSystemLines((ls) => [
|
|
341
|
+
...ls.slice(-5),
|
|
342
|
+
{ text: t('wiz.provider.ollama.checking', { url: preset.baseUrl }), level: 'info' },
|
|
343
|
+
]);
|
|
344
|
+
let models = [...preset.models];
|
|
345
|
+
let defaultModel = preset.defaultModel;
|
|
346
|
+
try {
|
|
347
|
+
const controller = new AbortController();
|
|
348
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
349
|
+
const resp = await fetch(preset.baseUrl + '/models', { signal: controller.signal });
|
|
350
|
+
clearTimeout(timeout);
|
|
351
|
+
if (resp.ok) {
|
|
352
|
+
const data = (await resp.json());
|
|
353
|
+
const detected = data?.data?.map((m) => m.id).filter(Boolean) ?? [];
|
|
354
|
+
if (detected.length > 0) {
|
|
355
|
+
models = detected;
|
|
356
|
+
defaultModel = detected[0];
|
|
357
|
+
setSystemLines((ls) => [
|
|
358
|
+
...ls.slice(-5),
|
|
359
|
+
{ text: t('wiz.provider.ollama.found', { n: detected.length }), level: 'ok' },
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
setSystemLines((ls) => [
|
|
366
|
+
...ls.slice(-5),
|
|
367
|
+
{ text: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }), level: 'warn' },
|
|
368
|
+
]);
|
|
369
|
+
}
|
|
370
|
+
const ollamaProvider = { ...preset, apiKey: 'ollama-local', models, defaultModel };
|
|
371
|
+
ctlRef.current?.saveProvider(ollamaProvider);
|
|
372
|
+
ctlRef.current?.setSessionProvider(ollamaProvider.name);
|
|
373
|
+
setPhase('model');
|
|
374
|
+
return;
|
|
307
375
|
}
|
|
376
|
+
setProviderStep({ id: 'key', preset: { ...preset, models: [...preset.models] } });
|
|
308
377
|
} }) })), phase === 'provider' && providerStep.id === 'key' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.preset.name }), footer: t('wiz.provider.key.footer'), children: [_jsx(Text, { color: "gray", children: providerStep.preset.baseUrl }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (k) => finishProvider({ ...providerStep.preset, apiKey: k.trim() }) })] })), phase === 'provider' && providerStep.id === 'name' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.name.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onBack: wizardBack, onInput: (name) => setProviderStep({ id: 'url', name }) }) })), phase === 'provider' && providerStep.id === 'url' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.url.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onBack: wizardBack, onInput: (url) => setProviderStep({ id: 'model', name: providerStep.name, url }) }) })), phase === 'provider' && providerStep.id === 'model' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.model.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (model) => setProviderStep({ id: 'newKey', name: providerStep.name, url: providerStep.url, model }) }) })), phase === 'provider' && providerStep.id === 'newKey' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.name }), footer: t('wiz.provider.key.footer'), children: _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (key) => finishProvider({
|
|
309
378
|
name: providerStep.name,
|
|
310
379
|
baseUrl: providerStep.url,
|
package/dist/ui/SettingsPanel.js
CHANGED
|
@@ -17,66 +17,66 @@ function nextApprovalMode(mode) {
|
|
|
17
17
|
return 'yolo';
|
|
18
18
|
return 'ask';
|
|
19
19
|
}
|
|
20
|
+
/** Derive a status badge string for a provider in the submenu list. */
|
|
21
|
+
function providerStatus(p, defaultName) {
|
|
22
|
+
const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(p.baseUrl);
|
|
23
|
+
if (p.name.toLowerCase() === defaultName.toLowerCase())
|
|
24
|
+
return t('set.status.default');
|
|
25
|
+
if (isLocal)
|
|
26
|
+
return t('set.status.local');
|
|
27
|
+
if (p.apiKey)
|
|
28
|
+
return masked(p.apiKey);
|
|
29
|
+
return t('set.status.noKey');
|
|
30
|
+
}
|
|
20
31
|
/**
|
|
21
32
|
* /settings → scope 'global' : persisted in ~/.parallel/config.json
|
|
22
33
|
* /settings-session → scope 'session' : this session only, never persisted
|
|
23
34
|
*/
|
|
24
35
|
export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
25
36
|
const [step, setStep] = useState({ id: 'root' });
|
|
37
|
+
const [returnStep, setReturnStep] = useState(null);
|
|
26
38
|
const [flash, setFlash] = useState('');
|
|
27
39
|
const saved = () => setFlash(t('set.saved'));
|
|
28
40
|
const cfg = ctl.config;
|
|
41
|
+
// ---- root menu items ----
|
|
29
42
|
const rootItems = scope === 'global'
|
|
30
43
|
? [
|
|
31
44
|
{ label: t('set.language', { lang: LANGS.find((l) => l.code === getLang())?.label ?? getLang() }), value: 'lang' },
|
|
32
45
|
{
|
|
33
46
|
label: t('set.defaultPM', {
|
|
34
|
-
pm: cfg.defaultProvider
|
|
47
|
+
pm: cfg.defaultProvider
|
|
48
|
+
? `${cfg.defaultProvider}:${cfg.providers.find((p) => p.name === cfg.defaultProvider)?.defaultModel ?? '?'}`
|
|
49
|
+
: '—',
|
|
35
50
|
}),
|
|
36
51
|
value: 'defaultPM',
|
|
37
52
|
},
|
|
38
|
-
|
|
39
|
-
label: t('set.key', { name: p.name, masked: masked(p.apiKey) }),
|
|
40
|
-
value: `key:${p.name}`,
|
|
41
|
-
})),
|
|
42
|
-
{ label: t('set.addProvider'), value: 'add' },
|
|
43
|
-
{ label: t('set.models'), value: 'models' },
|
|
44
|
-
{ label: t('set.prices'), value: 'prices' },
|
|
45
|
-
{ label: t('set.newSkill'), value: 'newSkill' },
|
|
46
|
-
{ label: t('set.newSpecialist'), value: 'newSpecialist' },
|
|
53
|
+
{ label: t('set.providers'), value: 'providers' },
|
|
47
54
|
{ label: t('set.approvals', { mode: cfg.approvalMode }), value: 'approvals' },
|
|
48
55
|
{ label: t('set.sound', { state: cfg.soundEnabled ? 'on' : 'off' }), value: 'sound' },
|
|
56
|
+
{ label: t('set.newSkill'), value: 'newSkill' },
|
|
57
|
+
{ label: t('set.newSpecialist'), value: 'newSpecialist' },
|
|
49
58
|
{ label: t('set.back'), value: 'back' },
|
|
50
59
|
]
|
|
51
60
|
: [
|
|
52
61
|
{
|
|
53
62
|
label: t('sset.model', { pm: `${ctl.session.providerName || '—'}:${ctl.session.model || '—'}` }),
|
|
54
|
-
value: '
|
|
63
|
+
value: 'providers',
|
|
55
64
|
},
|
|
56
65
|
{ label: t('sset.approvals', { mode: ctl.session.approvalMode }), value: 'approvals' },
|
|
57
66
|
{ label: t('sset.sound', { state: ctl.session.soundEnabled ? 'on' : 'off' }), value: 'sound' },
|
|
58
67
|
{ label: t('set.back'), value: 'back' },
|
|
59
68
|
];
|
|
69
|
+
// ---- root menu handler ----
|
|
60
70
|
const chooseRoot = (v) => {
|
|
61
71
|
setFlash('');
|
|
62
72
|
if (v === 'back')
|
|
63
73
|
return onClose();
|
|
64
74
|
if (v === 'lang')
|
|
65
75
|
return setStep({ id: 'lang' });
|
|
66
|
-
if (v === 'defaultPM'
|
|
76
|
+
if (v === 'defaultPM')
|
|
67
77
|
return setStep({ id: 'pickProvider', next: 'model' });
|
|
68
|
-
if (v
|
|
69
|
-
|
|
70
|
-
if (p)
|
|
71
|
-
setStep({ id: 'key', provider: p });
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (v === 'add')
|
|
75
|
-
return setStep({ id: 'newName' });
|
|
76
|
-
if (v === 'models')
|
|
77
|
-
return setStep({ id: 'pickProvider', next: 'models' });
|
|
78
|
-
if (v === 'prices')
|
|
79
|
-
return setStep({ id: 'pickProvider', next: 'prices' });
|
|
78
|
+
if (v === 'providers')
|
|
79
|
+
return setStep({ id: 'providers', scope });
|
|
80
80
|
if (v === 'newSkill')
|
|
81
81
|
return setStep({ id: 'newSkill' });
|
|
82
82
|
if (v === 'newSpecialist')
|
|
@@ -100,6 +100,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
|
+
// ---- shared helpers ----
|
|
103
104
|
const pickModel = (provider, model) => {
|
|
104
105
|
if (scope === 'global') {
|
|
105
106
|
provider.defaultModel = model;
|
|
@@ -112,13 +113,21 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
112
113
|
else {
|
|
113
114
|
ctl.setSessionModel(`${provider.name}:${model}`);
|
|
114
115
|
}
|
|
115
|
-
setStep({ id: 'root' });
|
|
116
|
+
setStep(returnStep ?? { id: 'root' });
|
|
117
|
+
setReturnStep(null);
|
|
116
118
|
};
|
|
117
119
|
const finishNewProvider = (name, url, model, key) => {
|
|
118
120
|
ctl.saveProvider({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
|
|
119
121
|
saved();
|
|
120
|
-
setStep({ id: 'root' });
|
|
122
|
+
setStep(returnStep ?? { id: 'root' });
|
|
123
|
+
setReturnStep(null);
|
|
124
|
+
};
|
|
125
|
+
// ---- navigate into a sub-step, remembering where to return ----
|
|
126
|
+
const goSub = (next) => {
|
|
127
|
+
setReturnStep(step);
|
|
128
|
+
setStep(next);
|
|
121
129
|
};
|
|
130
|
+
// ---- render ----
|
|
122
131
|
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: scope === 'global' ? t('set.title') : t('sset.title') }), flash ? _jsx(Text, { color: "green", children: flash }) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [step.id === 'root' && _jsx(SelectList, { items: rootItems, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), onSelect: (code) => {
|
|
123
132
|
setLang(code);
|
|
124
133
|
ctl.setLanguage(code);
|
|
@@ -138,7 +147,21 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
138
147
|
if (step.next === 'models')
|
|
139
148
|
return setStep({ id: 'modelList', provider: p });
|
|
140
149
|
setStep({ id: 'model', provider: p });
|
|
141
|
-
} })] })), step.id === '
|
|
150
|
+
} })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
151
|
+
...step.provider.models.map((m) => ({
|
|
152
|
+
label: m,
|
|
153
|
+
value: m,
|
|
154
|
+
hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
|
|
155
|
+
})),
|
|
156
|
+
{ label: t('set.back'), value: '__back__' },
|
|
157
|
+
], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
|
|
158
|
+
if (v === '__back__') {
|
|
159
|
+
setStep(returnStep ?? { id: 'root' });
|
|
160
|
+
setReturnStep(null);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
pickModel(step.provider, v);
|
|
164
|
+
}, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
142
165
|
...step.provider.models.map((m) => ({
|
|
143
166
|
label: m,
|
|
144
167
|
value: m,
|
|
@@ -146,13 +169,17 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
146
169
|
})),
|
|
147
170
|
{ label: t('set.back'), value: '__back__' },
|
|
148
171
|
], allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
|
|
149
|
-
if (v === '__back__')
|
|
150
|
-
|
|
172
|
+
if (v === '__back__') {
|
|
173
|
+
setStep(returnStep ?? { id: 'root' });
|
|
174
|
+
setReturnStep(null);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
151
177
|
step.provider.defaultModel = v;
|
|
152
178
|
ctl.saveProvider(step.provider);
|
|
153
179
|
ctl.setDefaultProvider(step.provider.name);
|
|
154
180
|
saved();
|
|
155
|
-
setStep({ id: 'root' });
|
|
181
|
+
setStep(returnStep ?? { id: 'root' });
|
|
182
|
+
setReturnStep(null);
|
|
156
183
|
}, onInput: (m) => {
|
|
157
184
|
const model = m.trim();
|
|
158
185
|
if (!model)
|
|
@@ -163,30 +190,46 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
163
190
|
ctl.saveProvider(step.provider);
|
|
164
191
|
ctl.setDefaultProvider(step.provider.name);
|
|
165
192
|
saved();
|
|
166
|
-
setStep({ id: 'root' });
|
|
193
|
+
setStep(returnStep ?? { id: 'root' });
|
|
194
|
+
setReturnStep(null);
|
|
167
195
|
} })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
168
196
|
...step.provider.models.map((m) => {
|
|
169
197
|
const pr = priceFor(step.provider, m);
|
|
170
198
|
return {
|
|
171
199
|
label: m,
|
|
172
200
|
value: m,
|
|
173
|
-
hint: pr
|
|
201
|
+
hint: pr
|
|
202
|
+
? `($${pr.input}/M in · $${pr.output}/M out${step.provider.prices?.[m] ? ' — override' : ''})`
|
|
203
|
+
: `(${t('set.priceUnknown')})`,
|
|
174
204
|
};
|
|
175
205
|
}),
|
|
176
206
|
{ label: t('set.back'), value: '__back__' },
|
|
177
207
|
], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
|
|
178
|
-
if (v === '__back__')
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
208
|
+
if (v === '__back__') {
|
|
209
|
+
setStep(returnStep ?? { id: 'root' });
|
|
210
|
+
setReturnStep(null);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
goSub({ id: 'priceValue', provider: step.provider, model: v });
|
|
214
|
+
}, onInput: (m) => goSub({ id: 'priceValue', provider: step.provider, model: m.trim() }) })] })), step.id === 'priceValue' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceValue', { model: step.model }) }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
|
|
182
215
|
const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
|
|
183
216
|
if (!m)
|
|
184
217
|
return setFlash(t('set.priceBad'));
|
|
185
|
-
step.provider.prices = {
|
|
218
|
+
step.provider.prices = {
|
|
219
|
+
...step.provider.prices,
|
|
220
|
+
[step.model]: { input: parseFloat(m[1]), output: parseFloat(m[2]) },
|
|
221
|
+
};
|
|
186
222
|
ctl.saveProvider(step.provider);
|
|
187
223
|
saved();
|
|
188
|
-
setStep({ id: 'root' });
|
|
189
|
-
|
|
224
|
+
setStep(returnStep ?? { id: 'root' });
|
|
225
|
+
setReturnStep(null);
|
|
226
|
+
} })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
|
|
227
|
+
step.provider.apiKey = k.trim();
|
|
228
|
+
ctl.saveProvider(step.provider);
|
|
229
|
+
saved();
|
|
230
|
+
setStep(returnStep ?? { id: 'root' });
|
|
231
|
+
setReturnStep(null);
|
|
232
|
+
} })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onInput: (name) => setStep({ id: 'newUrl', name }) })] })), step.id === 'newUrl' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onInput: (url) => setStep({ id: 'newModel', name: step.name, url }) })] })), step.id === 'newModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.model.title') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onInput: (model) => setStep({ id: 'newKey', name: step.name, url: step.url, model }) })] })), step.id === 'newKey' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.name }) }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (key) => finishNewProvider(step.name, step.url, step.model, key.trim()) })] })), step.id === 'newSkill' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSkillName') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
|
|
190
233
|
try {
|
|
191
234
|
const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
|
|
192
235
|
setFlash(t('m.skillCreated', { file }));
|
|
@@ -204,21 +247,86 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
204
247
|
setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
|
|
205
248
|
}
|
|
206
249
|
setStep({ id: 'root' });
|
|
207
|
-
} })] })), step.id === '
|
|
208
|
-
...
|
|
209
|
-
label:
|
|
210
|
-
value:
|
|
211
|
-
hint:
|
|
250
|
+
} })] })), step.id === 'providers' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: step.scope === 'global' ? t('set.providers.title') : t('sset.providers.title') }), _jsx(SelectList, { items: [
|
|
251
|
+
...cfg.providers.map((p) => ({
|
|
252
|
+
label: p.name,
|
|
253
|
+
value: p.name,
|
|
254
|
+
hint: providerStatus(p, cfg.defaultProvider),
|
|
212
255
|
})),
|
|
213
|
-
{ label: t('set.
|
|
214
|
-
|
|
256
|
+
{ label: t('set.providers.add'), value: '__add__' },
|
|
257
|
+
{ label: step.scope === 'global' ? t('set.providers.back') : t('sset.providers.back'), value: '__back__' },
|
|
258
|
+
], onSelect: (v) => {
|
|
215
259
|
if (v === '__back__')
|
|
216
260
|
return setStep({ id: 'root' });
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
261
|
+
if (v === '__add__') {
|
|
262
|
+
setReturnStep({ id: 'providers', scope: step.scope });
|
|
263
|
+
return setStep({ id: 'newName' });
|
|
264
|
+
}
|
|
265
|
+
const p = cfg.providers.find((x) => x.name === v);
|
|
266
|
+
if (!p)
|
|
267
|
+
return;
|
|
268
|
+
if (step.scope === 'session') {
|
|
269
|
+
// Session scope: pick a model for this session
|
|
270
|
+
setReturnStep({ id: 'root' });
|
|
271
|
+
setStep({ id: 'model', provider: p });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Global scope: go to provider detail
|
|
275
|
+
setStep({ id: 'providerDetail', provider: p, scope: 'global' });
|
|
276
|
+
}
|
|
277
|
+
} })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
278
|
+
{
|
|
279
|
+
label: t('set.providerDetail.key'),
|
|
280
|
+
value: 'key',
|
|
281
|
+
hint: masked(step.provider.apiKey),
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
label: t('set.providerDetail.models'),
|
|
285
|
+
value: 'models',
|
|
286
|
+
hint: `(${step.provider.models.length})`,
|
|
287
|
+
},
|
|
288
|
+
{ label: t('set.providerDetail.pricing'), value: 'pricing' },
|
|
289
|
+
{
|
|
290
|
+
label: t('set.providerDetail.setDefault'),
|
|
291
|
+
value: 'setDefault',
|
|
292
|
+
hint: step.provider.name.toLowerCase() === cfg.defaultProvider.toLowerCase()
|
|
293
|
+
? `(${t('set.status.default')})`
|
|
294
|
+
: undefined,
|
|
295
|
+
},
|
|
296
|
+
{ label: t('set.providerDetail.remove'), value: 'remove' },
|
|
297
|
+
{ label: t('set.providerDetail.back'), value: '__back__' },
|
|
298
|
+
], onSelect: (v) => {
|
|
299
|
+
if (v === '__back__')
|
|
300
|
+
return setStep({ id: 'providers', scope: step.scope });
|
|
301
|
+
if (v === 'key') {
|
|
302
|
+
setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
303
|
+
return setStep({ id: 'key', provider: step.provider });
|
|
304
|
+
}
|
|
305
|
+
if (v === 'models') {
|
|
306
|
+
setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
307
|
+
return setStep({ id: 'modelList', provider: step.provider });
|
|
308
|
+
}
|
|
309
|
+
if (v === 'pricing') {
|
|
310
|
+
setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
311
|
+
return setStep({ id: 'priceModel', provider: step.provider });
|
|
312
|
+
}
|
|
313
|
+
if (v === 'setDefault') {
|
|
314
|
+
ctl.setDefaultProvider(step.provider.name);
|
|
315
|
+
saved();
|
|
316
|
+
return setStep({ id: 'providers', scope: step.scope });
|
|
317
|
+
}
|
|
318
|
+
if (v === 'remove')
|
|
319
|
+
return setStep({ id: 'removeProvider', provider: step.provider, scope: step.scope });
|
|
320
|
+
} })] })), step.id === 'removeProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.removeProvider.title', { name: step.provider.name }) }), _jsx(Text, { color: "yellow", children: t('set.removeProvider.confirm') }), _jsx(SelectList, { items: [
|
|
321
|
+
{ label: t('set.removeProvider.yes'), value: 'yes' },
|
|
322
|
+
{ label: t('set.removeProvider.no'), value: 'no' },
|
|
323
|
+
], onSelect: (v) => {
|
|
324
|
+
if (v === 'no')
|
|
325
|
+
return setStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
326
|
+
if (v === 'yes') {
|
|
327
|
+
ctl.removeProvider(step.provider.name);
|
|
328
|
+
saved();
|
|
329
|
+
setStep({ id: 'providers', scope: step.scope });
|
|
330
|
+
}
|
|
331
|
+
} })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: t('set.esc') }) })] }));
|
|
224
332
|
}
|
package/dist/ui/Wizard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { t } from '../i18n.js';
|
|
@@ -12,6 +12,8 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
12
12
|
const [typed, setTyped] = useState('');
|
|
13
13
|
const typing = allowInput && typed.length > 0;
|
|
14
14
|
useInput((input, key) => {
|
|
15
|
+
// Build selectable index list each render (cheap — items is small).
|
|
16
|
+
const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
|
|
15
17
|
if (key.escape) {
|
|
16
18
|
if (typed)
|
|
17
19
|
setTyped('');
|
|
@@ -26,8 +28,10 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
26
28
|
if (v)
|
|
27
29
|
onInput?.(v);
|
|
28
30
|
}
|
|
29
|
-
else
|
|
30
|
-
|
|
31
|
+
else {
|
|
32
|
+
const realIdx = selectable[idx];
|
|
33
|
+
if (realIdx !== undefined && items[realIdx])
|
|
34
|
+
onSelect?.(items[realIdx].value);
|
|
31
35
|
}
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
@@ -37,12 +41,12 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
37
41
|
}
|
|
38
42
|
if (key.upArrow) {
|
|
39
43
|
if (!typing)
|
|
40
|
-
setIdx((i) => (i - 1 +
|
|
44
|
+
setIdx((i) => (i - 1 + selectable.length) % Math.max(1, selectable.length));
|
|
41
45
|
return;
|
|
42
46
|
}
|
|
43
47
|
if (key.downArrow) {
|
|
44
48
|
if (!typing)
|
|
45
|
-
setIdx((i) => (i + 1) % Math.max(1,
|
|
49
|
+
setIdx((i) => (i + 1) % Math.max(1, selectable.length));
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
52
|
if (key.tab || key.ctrl || key.meta)
|
|
@@ -59,7 +63,10 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
59
63
|
}
|
|
60
64
|
setTyped((v) => v + input);
|
|
61
65
|
});
|
|
62
|
-
|
|
66
|
+
// Build a separate index map so up/down skip section headers.
|
|
67
|
+
const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
|
|
68
|
+
const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
|
|
69
|
+
return (_jsxs(Box, { flexDirection: "column", children: [items.map((it, i) => it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? 'cyanBright' : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i))), allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? 'cyanBright' : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: "cyanBright", children: "\u2588" }) : null] }) }))] }));
|
|
63
70
|
}
|
|
64
71
|
export function WizardStep({ step, total, title, children, footer, }) {
|
|
65
72
|
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["[", step, "/", total, "] ", title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: footer ?? t('wiz.footer.select') }) })] }));
|
package/package.json
CHANGED