@parallel-cli/parallel 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/README.md +136 -131
- package/dist/commands.js +17 -4
- package/dist/config.js +218 -63
- package/dist/controller.js +13 -11
- package/dist/i18n.js +64 -20
- package/dist/index.js +5 -2
- package/dist/pricing.js +162 -54
- package/dist/ui/App.js +141 -56
- package/dist/ui/CommandInput.js +42 -17
- package/dist/ui/SettingsPanel.js +153 -31
- package/dist/ui/Wizard.js +33 -3
- package/dist/ui/views.js +15 -6
- package/package.json +10 -1
package/dist/ui/SettingsPanel.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createSkillTemplate, createSpecialistTemplate } from '../skills.js';
|
|
|
5
5
|
import { priceFor } from '../pricing.js';
|
|
6
6
|
import { SelectList } from './Wizard.js';
|
|
7
7
|
import { LANGS, getLang, setLang, t } from '../i18n.js';
|
|
8
|
+
import { providerNeedsApiKey, PROVIDER_PRESETS } from '../config.js';
|
|
8
9
|
function masked(key) {
|
|
9
10
|
if (!key)
|
|
10
11
|
return '—';
|
|
@@ -19,10 +20,9 @@ function nextApprovalMode(mode) {
|
|
|
19
20
|
}
|
|
20
21
|
/** Derive a status badge string for a provider in the submenu list. */
|
|
21
22
|
function providerStatus(p, defaultName) {
|
|
22
|
-
const isLocal = /localhost|127\.0\.0\.1|0\.0\.0\.0/.test(p.baseUrl);
|
|
23
23
|
if (p.name.toLowerCase() === defaultName.toLowerCase())
|
|
24
24
|
return t('set.status.default');
|
|
25
|
-
if (
|
|
25
|
+
if (!providerNeedsApiKey(p))
|
|
26
26
|
return t('set.status.local');
|
|
27
27
|
if (p.apiKey)
|
|
28
28
|
return masked(p.apiKey);
|
|
@@ -32,12 +32,13 @@ function providerStatus(p, defaultName) {
|
|
|
32
32
|
* /settings → scope 'global' : persisted in ~/.parallel/config.json
|
|
33
33
|
* /settings-session → scope 'session' : this session only, never persisted
|
|
34
34
|
*/
|
|
35
|
-
export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
35
|
+
export function SettingsPanel({ ctl, scope, height, onClose, }) {
|
|
36
36
|
const [step, setStep] = useState({ id: 'root' });
|
|
37
37
|
const [returnStep, setReturnStep] = useState(null);
|
|
38
38
|
const [flash, setFlash] = useState('');
|
|
39
39
|
const saved = () => setFlash(t('set.saved'));
|
|
40
40
|
const cfg = ctl.config;
|
|
41
|
+
const listHeight = height ? Math.max(3, height - 5) : undefined;
|
|
41
42
|
// ---- root menu items ----
|
|
42
43
|
const rootItems = scope === 'global'
|
|
43
44
|
? [
|
|
@@ -102,6 +103,13 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
102
103
|
};
|
|
103
104
|
// ---- shared helpers ----
|
|
104
105
|
const pickModel = (provider, model) => {
|
|
106
|
+
if (step.id === 'model' && step.setup) {
|
|
107
|
+
provider.defaultModel = model;
|
|
108
|
+
if (!provider.models.includes(model))
|
|
109
|
+
provider.models.push(model);
|
|
110
|
+
setStep({ id: 'endpoint', provider, setup: true });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
105
113
|
if (scope === 'global') {
|
|
106
114
|
provider.defaultModel = model;
|
|
107
115
|
if (!provider.models.includes(model))
|
|
@@ -116,9 +124,20 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
116
124
|
setStep(returnStep ?? { id: 'root' });
|
|
117
125
|
setReturnStep(null);
|
|
118
126
|
};
|
|
127
|
+
const finishProviderSetup = (provider) => {
|
|
128
|
+
ctl.saveProvider(provider);
|
|
129
|
+
if (scope === 'global') {
|
|
130
|
+
ctl.setDefaultProvider(provider.name);
|
|
131
|
+
saved();
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
ctl.setSessionModel(`${provider.name}:${provider.defaultModel || provider.models[0] || ''}`);
|
|
135
|
+
}
|
|
136
|
+
setStep(returnStep ?? { id: 'providers', scope });
|
|
137
|
+
setReturnStep(null);
|
|
138
|
+
};
|
|
119
139
|
const finishNewProvider = (name, url, model, key) => {
|
|
120
|
-
|
|
121
|
-
saved();
|
|
140
|
+
finishProviderSetup({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
|
|
122
141
|
setStep(returnStep ?? { id: 'root' });
|
|
123
142
|
setReturnStep(null);
|
|
124
143
|
};
|
|
@@ -128,7 +147,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
128
147
|
setStep(next);
|
|
129
148
|
};
|
|
130
149
|
// ---- render ----
|
|
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) => {
|
|
150
|
+
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, height: listHeight, onSelect: chooseRoot }), step.id === 'lang' && (_jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: listHeight, onSelect: (code) => {
|
|
132
151
|
setLang(code);
|
|
133
152
|
ctl.setLanguage(code);
|
|
134
153
|
saved();
|
|
@@ -136,7 +155,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
136
155
|
} })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SelectList, { items: [
|
|
137
156
|
...cfg.providers.map((p) => ({ label: p.name, value: p.name, hint: `(${p.baseUrl})` })),
|
|
138
157
|
{ label: t('set.back'), value: '__back__' },
|
|
139
|
-
], onSelect: (v) => {
|
|
158
|
+
], height: listHeight, onSelect: (v) => {
|
|
140
159
|
if (v === '__back__')
|
|
141
160
|
return setStep({ id: 'root' });
|
|
142
161
|
const p = cfg.providers.find((x) => x.name === v);
|
|
@@ -154,21 +173,45 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
154
173
|
hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
|
|
155
174
|
})),
|
|
156
175
|
{ label: t('set.back'), value: '__back__' },
|
|
157
|
-
], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
|
|
176
|
+
], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
|
|
158
177
|
if (v === '__back__') {
|
|
159
178
|
setStep(returnStep ?? { id: 'root' });
|
|
160
179
|
setReturnStep(null);
|
|
161
180
|
return;
|
|
162
181
|
}
|
|
163
182
|
pickModel(step.provider, v);
|
|
164
|
-
}, onInput: (m) => pickModel(step.provider, m) })] })), step.id === '
|
|
183
|
+
}, onInput: (m) => pickModel(step.provider, m) })] })), step.id === 'endpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.title', { name: step.provider.name }) }), _jsx(Text, { color: "gray", children: step.provider.baseUrl }), _jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.model', { model: step.provider.defaultModel || step.provider.models[0] || '—' }) }), _jsx(SelectList, { items: [
|
|
184
|
+
{ label: t('wiz.provider.endpoint.use'), value: 'use' },
|
|
185
|
+
{ label: t('wiz.provider.endpoint.edit'), value: 'edit' },
|
|
186
|
+
{ label: t('set.back'), value: '__back__' },
|
|
187
|
+
], height: listHeight, onSelect: (v) => {
|
|
188
|
+
if (v === '__back__') {
|
|
189
|
+
setStep({ id: 'model', provider: step.provider, setup: step.setup });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (v === 'edit')
|
|
193
|
+
return setStep({ id: 'editEndpoint', provider: step.provider, setup: step.setup });
|
|
194
|
+
if (step.setup) {
|
|
195
|
+
if (providerNeedsApiKey(step.provider))
|
|
196
|
+
return setStep({ id: 'key', provider: step.provider, setup: true });
|
|
197
|
+
finishProviderSetup(step.provider);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
ctl.saveProvider(step.provider);
|
|
201
|
+
saved();
|
|
202
|
+
setStep(returnStep ?? { id: 'providerDetail', provider: step.provider, scope });
|
|
203
|
+
setReturnStep(null);
|
|
204
|
+
} })] })), step.id === 'editEndpoint' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.url.title') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: step.provider.baseUrl, onInput: (url) => {
|
|
205
|
+
const provider = { ...step.provider, baseUrl: url.trim() };
|
|
206
|
+
setStep({ id: 'endpoint', provider, setup: step.setup });
|
|
207
|
+
} })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
165
208
|
...step.provider.models.map((m) => ({
|
|
166
209
|
label: m,
|
|
167
210
|
value: m,
|
|
168
211
|
hint: m === step.provider.defaultModel ? t('wiz.model.default') : t('set.makeDefault'),
|
|
169
212
|
})),
|
|
170
213
|
{ label: t('set.back'), value: '__back__' },
|
|
171
|
-
], allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
|
|
214
|
+
], height: listHeight, allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
|
|
172
215
|
if (v === '__back__') {
|
|
173
216
|
setStep(returnStep ?? { id: 'root' });
|
|
174
217
|
setReturnStep(null);
|
|
@@ -204,14 +247,14 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
204
247
|
};
|
|
205
248
|
}),
|
|
206
249
|
{ label: t('set.back'), value: '__back__' },
|
|
207
|
-
], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
|
|
250
|
+
], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
|
|
208
251
|
if (v === '__back__') {
|
|
209
252
|
setStep(returnStep ?? { id: 'root' });
|
|
210
253
|
setReturnStep(null);
|
|
211
254
|
return;
|
|
212
255
|
}
|
|
213
256
|
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) => {
|
|
257
|
+
}, 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: [], height: listHeight, allowInput: true, inputPlaceholder: "0.27, 1.10", onInput: (v) => {
|
|
215
258
|
const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
|
|
216
259
|
if (!m)
|
|
217
260
|
return setFlash(t('set.priceBad'));
|
|
@@ -223,13 +266,17 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
223
266
|
saved();
|
|
224
267
|
setStep(returnStep ?? { id: 'root' });
|
|
225
268
|
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
|
|
228
|
-
|
|
229
|
-
|
|
269
|
+
} })] })), step.id === 'key' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.key.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onInput: (k) => {
|
|
270
|
+
const provider = { ...step.provider, apiKey: k.trim() };
|
|
271
|
+
if (step.setup)
|
|
272
|
+
finishProviderSetup(provider);
|
|
273
|
+
else {
|
|
274
|
+
ctl.saveProvider(provider);
|
|
275
|
+
saved();
|
|
276
|
+
}
|
|
230
277
|
setStep(returnStep ?? { id: 'root' });
|
|
231
278
|
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) => {
|
|
279
|
+
} })] })), step.id === 'newName' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('wiz.provider.name.title') }), _jsx(SelectList, { items: [], height: listHeight, 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: [], height: listHeight, 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: [], height: listHeight, 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: [], height: listHeight, 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: [], height: listHeight, allowInput: true, inputPlaceholder: "review, deploy, tests\u2026", onInput: (name) => {
|
|
233
280
|
try {
|
|
234
281
|
const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
|
|
235
282
|
setFlash(t('m.skillCreated', { file }));
|
|
@@ -238,7 +285,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
238
285
|
setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
|
|
239
286
|
}
|
|
240
287
|
setStep({ id: 'root' });
|
|
241
|
-
} })] })), step.id === 'newSpecialist' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSpecialistName') }), _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: "reviewer, architect, tester\u2026", onInput: (name) => {
|
|
288
|
+
} })] })), step.id === 'newSpecialist' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.newSpecialistName') }), _jsx(SelectList, { items: [], height: listHeight, allowInput: true, inputPlaceholder: "reviewer, architect, tester\u2026", onInput: (name) => {
|
|
242
289
|
try {
|
|
243
290
|
const file = createSpecialistTemplate(name.trim(), '', 'global', ctl.projectRoot);
|
|
244
291
|
setFlash(t('m.specCreated', { file }));
|
|
@@ -247,31 +294,84 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
247
294
|
setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
|
|
248
295
|
}
|
|
249
296
|
setStep({ id: 'root' });
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
297
|
+
} })] })), 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: (() => {
|
|
298
|
+
const configuredNames = new Set(cfg.providers.map((p) => p.name.toLowerCase()));
|
|
299
|
+
const items = [];
|
|
300
|
+
// Section: Configured
|
|
301
|
+
if (cfg.providers.length > 0) {
|
|
302
|
+
items.push({ label: t('wiz.provider.section.configured'), value: '', section: true });
|
|
303
|
+
for (const p of cfg.providers) {
|
|
304
|
+
items.push({
|
|
305
|
+
label: p.name,
|
|
306
|
+
value: p.name,
|
|
307
|
+
detail: providerStatus(p, cfg.defaultProvider),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Sections per category: western, chinese, gateways, inference, local
|
|
312
|
+
const catOrder = ['western', 'chinese', 'gateways', 'inference', 'local'];
|
|
313
|
+
const emoji = {
|
|
314
|
+
western: '\u{1F1FA}\u{1F1F8} ',
|
|
315
|
+
chinese: '\u{1F1E8}\u{1F1F3} ',
|
|
316
|
+
gateways: '\u{1F310} ',
|
|
317
|
+
inference: '\u26A1 ',
|
|
318
|
+
local: '\u{1F3E0} ',
|
|
319
|
+
};
|
|
320
|
+
for (const cat of catOrder) {
|
|
321
|
+
const presetsInCat = PROVIDER_PRESETS.filter((p) => p.category === cat && !configuredNames.has(p.name.toLowerCase()));
|
|
322
|
+
if (presetsInCat.length === 0)
|
|
323
|
+
continue;
|
|
324
|
+
const key = `wiz.provider.section.${cat}`;
|
|
325
|
+
const sectionLabel = emoji[cat] + t(key);
|
|
326
|
+
items.push({ value: '', label: sectionLabel, section: true });
|
|
327
|
+
for (const preset of presetsInCat) {
|
|
328
|
+
const detail = preset.models.length > 0
|
|
329
|
+
? `${preset.models.length} model${preset.models.length > 1 ? 's' : ''}`
|
|
330
|
+
: undefined;
|
|
331
|
+
items.push({
|
|
332
|
+
label: preset.name,
|
|
333
|
+
value: `__preset__${preset.name}`,
|
|
334
|
+
detail,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Custom provider
|
|
339
|
+
items.push({ label: t('wiz.provider.custom'), value: '__add__' });
|
|
340
|
+
items.push({
|
|
341
|
+
label: step.scope === 'global' ? t('set.providers.back') : t('sset.providers.back'),
|
|
342
|
+
value: '__back__',
|
|
343
|
+
});
|
|
344
|
+
return items;
|
|
345
|
+
})(), height: listHeight, onSelect: (v) => {
|
|
259
346
|
if (v === '__back__')
|
|
260
347
|
return setStep({ id: 'root' });
|
|
261
348
|
if (v === '__add__') {
|
|
262
349
|
setReturnStep({ id: 'providers', scope: step.scope });
|
|
263
350
|
return setStep({ id: 'newName' });
|
|
264
351
|
}
|
|
352
|
+
// Preset selection (e.g. __preset__Anthropic)
|
|
353
|
+
if (v.startsWith('__preset__')) {
|
|
354
|
+
const presetName = v.slice('__preset__'.length);
|
|
355
|
+
const preset = PROVIDER_PRESETS.find((p) => p.name === presetName);
|
|
356
|
+
if (!preset)
|
|
357
|
+
return;
|
|
358
|
+
if (presetName.toLowerCase() === 'ollama') {
|
|
359
|
+
setReturnStep({ id: 'providers', scope: step.scope });
|
|
360
|
+
return setStep({ id: 'model', provider: { ...preset, apiKey: 'ollama-local' }, setup: true });
|
|
361
|
+
}
|
|
362
|
+
// Preset setup: choose model, review endpoint, then ask for API key if needed.
|
|
363
|
+
setReturnStep({ id: 'providers', scope: step.scope });
|
|
364
|
+
return setStep({ id: 'model', provider: { ...preset, models: [...preset.models] }, setup: true });
|
|
365
|
+
}
|
|
366
|
+
// Existing configured provider
|
|
265
367
|
const p = cfg.providers.find((x) => x.name === v);
|
|
266
368
|
if (!p)
|
|
267
369
|
return;
|
|
268
370
|
if (step.scope === 'session') {
|
|
269
|
-
// Session scope: pick a model for this session
|
|
270
371
|
setReturnStep({ id: 'root' });
|
|
271
372
|
setStep({ id: 'model', provider: p });
|
|
272
373
|
}
|
|
273
374
|
else {
|
|
274
|
-
// Global scope: go to provider detail
|
|
275
375
|
setStep({ id: 'providerDetail', provider: p, scope: 'global' });
|
|
276
376
|
}
|
|
277
377
|
} })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
|
|
@@ -280,6 +380,19 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
280
380
|
value: 'key',
|
|
281
381
|
hint: masked(step.provider.apiKey),
|
|
282
382
|
},
|
|
383
|
+
...(step.provider.apiKey
|
|
384
|
+
? [
|
|
385
|
+
{
|
|
386
|
+
label: t('set.providerDetail.clearKey'),
|
|
387
|
+
value: 'clearKey',
|
|
388
|
+
},
|
|
389
|
+
]
|
|
390
|
+
: []),
|
|
391
|
+
{
|
|
392
|
+
label: t('set.providerDetail.endpoint'),
|
|
393
|
+
value: 'endpoint',
|
|
394
|
+
hint: `(${step.provider.baseUrl})`,
|
|
395
|
+
},
|
|
283
396
|
{
|
|
284
397
|
label: t('set.providerDetail.models'),
|
|
285
398
|
value: 'models',
|
|
@@ -295,13 +408,22 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
295
408
|
},
|
|
296
409
|
{ label: t('set.providerDetail.remove'), value: 'remove' },
|
|
297
410
|
{ label: t('set.providerDetail.back'), value: '__back__' },
|
|
298
|
-
], onSelect: (v) => {
|
|
411
|
+
], height: listHeight, onSelect: (v) => {
|
|
299
412
|
if (v === '__back__')
|
|
300
413
|
return setStep({ id: 'providers', scope: step.scope });
|
|
301
414
|
if (v === 'key') {
|
|
302
415
|
setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
303
416
|
return setStep({ id: 'key', provider: step.provider });
|
|
304
417
|
}
|
|
418
|
+
if (v === 'clearKey') {
|
|
419
|
+
ctl.saveProvider({ ...step.provider, apiKey: '' });
|
|
420
|
+
saved();
|
|
421
|
+
return setStep({ id: 'providerDetail', provider: { ...step.provider, apiKey: '' }, scope: step.scope });
|
|
422
|
+
}
|
|
423
|
+
if (v === 'endpoint') {
|
|
424
|
+
setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
425
|
+
return setStep({ id: 'endpoint', provider: step.provider });
|
|
426
|
+
}
|
|
305
427
|
if (v === 'models') {
|
|
306
428
|
setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
307
429
|
return setStep({ id: 'modelList', provider: step.provider });
|
|
@@ -320,7 +442,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
|
|
|
320
442
|
} })] })), 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
443
|
{ label: t('set.removeProvider.yes'), value: 'yes' },
|
|
322
444
|
{ label: t('set.removeProvider.no'), value: 'no' },
|
|
323
|
-
], onSelect: (v) => {
|
|
445
|
+
], height: listHeight, onSelect: (v) => {
|
|
324
446
|
if (v === 'no')
|
|
325
447
|
return setStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
|
|
326
448
|
if (v === 'yes') {
|
package/dist/ui/Wizard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } 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';
|
|
@@ -7,7 +7,7 @@ import { t } from '../i18n.js';
|
|
|
7
7
|
* type a free value (e.g. a folder path or a custom model name) — typing
|
|
8
8
|
* switches to input mode, Esc comes back to the list.
|
|
9
9
|
*/
|
|
10
|
-
export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack, onSelect, onInput, }) {
|
|
10
|
+
export function SelectList({ items, allowInput, inputPlaceholder, mask, height, onBack, onSelect, onInput, }) {
|
|
11
11
|
const [idx, setIdx] = useState(0);
|
|
12
12
|
const [typed, setTyped] = useState('');
|
|
13
13
|
const typing = allowInput && typed.length > 0;
|
|
@@ -49,6 +49,26 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
49
49
|
setIdx((i) => (i + 1) % Math.max(1, selectable.length));
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
if (key.pageUp) {
|
|
53
|
+
if (!typing)
|
|
54
|
+
setIdx((i) => Math.max(0, i - Math.max(1, Math.floor((height ?? 8) / 2))));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.pageDown) {
|
|
58
|
+
if (!typing)
|
|
59
|
+
setIdx((i) => Math.min(Math.max(0, selectable.length - 1), i + Math.max(1, Math.floor((height ?? 8) / 2))));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.home) {
|
|
63
|
+
if (!typing)
|
|
64
|
+
setIdx(0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.end) {
|
|
68
|
+
if (!typing)
|
|
69
|
+
setIdx(Math.max(0, selectable.length - 1));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
52
72
|
if (key.tab || key.ctrl || key.meta)
|
|
53
73
|
return;
|
|
54
74
|
if (!allowInput || !input)
|
|
@@ -66,7 +86,17 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
66
86
|
// Build a separate index map so up/down skip section headers.
|
|
67
87
|
const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
|
|
68
88
|
const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
|
|
69
|
-
|
|
89
|
+
const maxVisible = height ? Math.max(1, height - (allowInput ? 2 : 0)) : items.length;
|
|
90
|
+
const start = items.length > maxVisible && safeIdx >= 0
|
|
91
|
+
? Math.max(0, Math.min(safeIdx - Math.floor(maxVisible / 2), items.length - maxVisible))
|
|
92
|
+
: 0;
|
|
93
|
+
const visibleItems = items.slice(start, start + maxVisible);
|
|
94
|
+
const above = start;
|
|
95
|
+
const below = Math.max(0, items.length - start - visibleItems.length);
|
|
96
|
+
return (_jsxs(Box, { flexDirection: "column", children: [above > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", above] }) : null, visibleItems.map((it, localIdx) => {
|
|
97
|
+
const i = start + localIdx;
|
|
98
|
+
return (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)));
|
|
99
|
+
}), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, 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] }) }))] }));
|
|
70
100
|
}
|
|
71
101
|
export function WizardStep({ step, total, title, children, footer, }) {
|
|
72
102
|
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/dist/ui/views.js
CHANGED
|
@@ -18,19 +18,27 @@ function useScrollWindow(items, visible, anchor = 'top') {
|
|
|
18
18
|
const s = Math.min(scroll, max);
|
|
19
19
|
const step = Math.max(1, visible - 1);
|
|
20
20
|
useInput((_input, key) => {
|
|
21
|
-
const towardsAnchor = (v) => Math.max(0, Math.min(v, max) -
|
|
22
|
-
const awayFromAnchor = (v) => Math.min(Math.min(v, max) +
|
|
21
|
+
const towardsAnchor = (v, amount = step) => Math.max(0, Math.min(v, max) - amount);
|
|
22
|
+
const awayFromAnchor = (v, amount = step) => Math.min(Math.min(v, max) + amount, max);
|
|
23
23
|
if (anchor === 'top') {
|
|
24
24
|
if (key.pageDown)
|
|
25
25
|
setScroll(awayFromAnchor);
|
|
26
26
|
if (key.pageUp)
|
|
27
27
|
setScroll(towardsAnchor);
|
|
28
|
+
if (key.downArrow)
|
|
29
|
+
setScroll((v) => awayFromAnchor(v, 1));
|
|
30
|
+
if (key.upArrow)
|
|
31
|
+
setScroll((v) => towardsAnchor(v, 1));
|
|
28
32
|
}
|
|
29
33
|
else {
|
|
30
34
|
if (key.pageUp)
|
|
31
35
|
setScroll(awayFromAnchor);
|
|
32
36
|
if (key.pageDown)
|
|
33
37
|
setScroll(towardsAnchor);
|
|
38
|
+
if (key.upArrow)
|
|
39
|
+
setScroll((v) => awayFromAnchor(v, 1));
|
|
40
|
+
if (key.downArrow)
|
|
41
|
+
setScroll((v) => towardsAnchor(v, 1));
|
|
34
42
|
}
|
|
35
43
|
});
|
|
36
44
|
const start = anchor === 'top' ? s : Math.max(0, items.length - visible - s);
|
|
@@ -86,9 +94,10 @@ export function SessionsView({ projectRoot }) {
|
|
|
86
94
|
const sessions = Controller.listSessions(projectRoot);
|
|
87
95
|
return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "yellow", children: t('sessions.title') }), sessions.length === 0 ? (_jsx(Text, { color: "gray", children: t('sessions.empty') })) : (sessions.map((s, i) => (_jsxs(Text, { wrap: "truncate-end", children: [' ', _jsxs(Text, { color: "yellow", bold: true, children: [String(i + 1).padStart(2), "."] }), ' ', _jsx(Text, { children: t('sessions.item', { date: new Date(s.data.savedAt).toLocaleString(), agents: s.data.agents.length }) }), _jsxs(Text, { color: "gray", children: [" ", s.data.agents.map((a) => a.name).join(', ').slice(0, 80)] })] }, s.file)))), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", children: t('sessions.hint') })] }));
|
|
88
96
|
}
|
|
89
|
-
export function HelpView() {
|
|
90
|
-
//
|
|
91
|
-
const
|
|
97
|
+
export function HelpView({ bodyHeight }) {
|
|
98
|
+
// Fixed intro/highlight/footer rows consume about 12 lines inside the already-sized body.
|
|
99
|
+
const fallbackVisible = useVisibleRows(16);
|
|
100
|
+
const visible = bodyHeight ? Math.max(3, bodyHeight - 12) : fallbackVisible;
|
|
92
101
|
const commands = visibleCommands();
|
|
93
102
|
const { slice, above, below } = useScrollWindow(commands, visible, 'top');
|
|
94
103
|
const highlights = [
|
|
@@ -96,5 +105,5 @@ export function HelpView() {
|
|
|
96
105
|
['Shell approvals', ['/approvals ask', '/approvals auto', '/approvals yolo']],
|
|
97
106
|
['Navigation', ['/focus', '/attach', '/raw', '/send']],
|
|
98
107
|
];
|
|
99
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: t('help.title') }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l1a') }), t('help.l1b'), _jsx(Text, { bold: true, children: t('help.l1c') }), "."] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l2a') }), t('help.l2b'), _jsx(Text, { bold: true, children: t('help.l2c') }), t('help.l2d')] }), _jsx(Text, { wrap: "truncate-end", children: t('help.l3') }), _jsx(Text, { children: " " }), highlights.map(([label, names]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "cyan", bold: true, children: [label, ": "] }), _jsx(Text, { color: "gray", children: names.join(' ') })] }, label))), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Keyboard: Tab/\u2192 autocomplete \u00B7 Esc back/clear \u00B7
|
|
108
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: t('help.title') }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l1a') }), t('help.l1b'), _jsx(Text, { bold: true, children: t('help.l1c') }), "."] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, children: t('help.l2a') }), t('help.l2b'), _jsx(Text, { bold: true, children: t('help.l2c') }), t('help.l2d')] }), _jsx(Text, { wrap: "truncate-end", children: t('help.l3') }), _jsx(Text, { children: " " }), highlights.map(([label, names]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: "cyan", bold: true, children: [label, ": "] }), _jsx(Text, { color: "gray", children: names.join(' ') })] }, label))), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "Keyboard: \u2191/\u2193 or PgUp/PgDn scroll \u00B7 Tab/\u2192 autocomplete \u00B7 Esc back/clear \u00B7 Ctrl+U clear \u00B7 Ctrl+V image" }), _jsx(Text, { children: " " }), _jsx(Above, { n: above }), slice.map((c) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(18) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(24) }), _jsxs(Text, { color: "gray", children: [t(c.descKey), c.aliases?.length ? ` (= ${c.aliases.join(', ')})` : ''] })] }, c.name))), _jsx(Below, { n: below }), _jsx(Text, { children: " " }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.states') }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: t('help.keys') })] }));
|
|
100
109
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallel-cli/parallel",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Real-time multi-agent coding CLI with shared context, adaptive co-editing, dedicated agent terminals, and headless CI runs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"real-time"
|
|
16
16
|
],
|
|
17
17
|
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Nil06/parallel-cli.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/Nil06/parallel-cli#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Nil06/parallel-cli/issues"
|
|
25
|
+
},
|
|
18
26
|
"author": "Nil Amara",
|
|
19
27
|
"type": "module",
|
|
20
28
|
"bin": {
|
|
@@ -24,6 +32,7 @@
|
|
|
24
32
|
"files": [
|
|
25
33
|
"dist",
|
|
26
34
|
"README.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
27
36
|
"LICENSE"
|
|
28
37
|
],
|
|
29
38
|
"publishConfig": {
|