@parallel-cli/parallel 0.4.0 → 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.
@@ -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 '—';
@@ -17,66 +18,66 @@ function nextApprovalMode(mode) {
17
18
  return 'yolo';
18
19
  return 'ask';
19
20
  }
21
+ /** Derive a status badge string for a provider in the submenu list. */
22
+ function providerStatus(p, defaultName) {
23
+ if (p.name.toLowerCase() === defaultName.toLowerCase())
24
+ return t('set.status.default');
25
+ if (!providerNeedsApiKey(p))
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
- export function SettingsPanel({ ctl, scope, onClose, }) {
35
+ export function SettingsPanel({ ctl, scope, height, 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
+ const listHeight = height ? Math.max(3, height - 5) : undefined;
42
+ // ---- root menu items ----
29
43
  const rootItems = scope === 'global'
30
44
  ? [
31
45
  { label: t('set.language', { lang: LANGS.find((l) => l.code === getLang())?.label ?? getLang() }), value: 'lang' },
32
46
  {
33
47
  label: t('set.defaultPM', {
34
- pm: cfg.defaultProvider ? `${cfg.defaultProvider}:${cfg.providers.find((p) => p.name === cfg.defaultProvider)?.defaultModel ?? '?'}` : '—',
48
+ pm: cfg.defaultProvider
49
+ ? `${cfg.defaultProvider}:${cfg.providers.find((p) => p.name === cfg.defaultProvider)?.defaultModel ?? '?'}`
50
+ : '—',
35
51
  }),
36
52
  value: 'defaultPM',
37
53
  },
38
- ...cfg.providers.map((p) => ({
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' },
54
+ { label: t('set.providers'), value: 'providers' },
47
55
  { label: t('set.approvals', { mode: cfg.approvalMode }), value: 'approvals' },
48
56
  { label: t('set.sound', { state: cfg.soundEnabled ? 'on' : 'off' }), value: 'sound' },
57
+ { label: t('set.newSkill'), value: 'newSkill' },
58
+ { label: t('set.newSpecialist'), value: 'newSpecialist' },
49
59
  { label: t('set.back'), value: 'back' },
50
60
  ]
51
61
  : [
52
62
  {
53
63
  label: t('sset.model', { pm: `${ctl.session.providerName || '—'}:${ctl.session.model || '—'}` }),
54
- value: 'model',
64
+ value: 'providers',
55
65
  },
56
66
  { label: t('sset.approvals', { mode: ctl.session.approvalMode }), value: 'approvals' },
57
67
  { label: t('sset.sound', { state: ctl.session.soundEnabled ? 'on' : 'off' }), value: 'sound' },
58
68
  { label: t('set.back'), value: 'back' },
59
69
  ];
70
+ // ---- root menu handler ----
60
71
  const chooseRoot = (v) => {
61
72
  setFlash('');
62
73
  if (v === 'back')
63
74
  return onClose();
64
75
  if (v === 'lang')
65
76
  return setStep({ id: 'lang' });
66
- if (v === 'defaultPM' || v === 'model')
77
+ if (v === 'defaultPM')
67
78
  return setStep({ id: 'pickProvider', next: 'model' });
68
- if (v.startsWith('key:')) {
69
- const p = cfg.providers.find((x) => x.name === v.slice(4));
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' });
79
+ if (v === 'providers')
80
+ return setStep({ id: 'providers', scope });
80
81
  if (v === 'newSkill')
81
82
  return setStep({ id: 'newSkill' });
82
83
  if (v === 'newSpecialist')
@@ -100,7 +101,15 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
100
101
  return;
101
102
  }
102
103
  };
104
+ // ---- shared helpers ----
103
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
+ }
104
113
  if (scope === 'global') {
105
114
  provider.defaultModel = model;
106
115
  if (!provider.models.includes(model))
@@ -112,14 +121,33 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
112
121
  else {
113
122
  ctl.setSessionModel(`${provider.name}:${model}`);
114
123
  }
115
- setStep({ id: 'root' });
124
+ setStep(returnStep ?? { id: 'root' });
125
+ setReturnStep(null);
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);
116
138
  };
117
139
  const finishNewProvider = (name, url, model, key) => {
118
- ctl.saveProvider({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
119
- saved();
120
- setStep({ id: 'root' });
140
+ finishProviderSetup({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
141
+ setStep(returnStep ?? { id: 'root' });
142
+ setReturnStep(null);
143
+ };
144
+ // ---- navigate into a sub-step, remembering where to return ----
145
+ const goSub = (next) => {
146
+ setReturnStep(step);
147
+ setStep(next);
121
148
  };
122
- 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) => {
149
+ // ---- render ----
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) => {
123
151
  setLang(code);
124
152
  ctl.setLanguage(code);
125
153
  saved();
@@ -127,7 +155,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
127
155
  } })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SelectList, { items: [
128
156
  ...cfg.providers.map((p) => ({ label: p.name, value: p.name, hint: `(${p.baseUrl})` })),
129
157
  { label: t('set.back'), value: '__back__' },
130
- ], onSelect: (v) => {
158
+ ], height: listHeight, onSelect: (v) => {
131
159
  if (v === '__back__')
132
160
  return setStep({ id: 'root' });
133
161
  const p = cfg.providers.find((x) => x.name === v);
@@ -138,6 +166,44 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
138
166
  if (step.next === 'models')
139
167
  return setStep({ id: 'modelList', provider: p });
140
168
  setStep({ id: 'model', provider: p });
169
+ } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
170
+ ...step.provider.models.map((m) => ({
171
+ label: m,
172
+ value: m,
173
+ hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
174
+ })),
175
+ { label: t('set.back'), value: '__back__' },
176
+ ], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
177
+ if (v === '__back__') {
178
+ setStep(returnStep ?? { id: 'root' });
179
+ setReturnStep(null);
180
+ return;
181
+ }
182
+ pickModel(step.provider, v);
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 });
141
207
  } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
142
208
  ...step.provider.models.map((m) => ({
143
209
  label: m,
@@ -145,14 +211,18 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
145
211
  hint: m === step.provider.defaultModel ? t('wiz.model.default') : t('set.makeDefault'),
146
212
  })),
147
213
  { label: t('set.back'), value: '__back__' },
148
- ], allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
149
- if (v === '__back__')
150
- return setStep({ id: 'root' });
214
+ ], height: listHeight, allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
215
+ if (v === '__back__') {
216
+ setStep(returnStep ?? { id: 'root' });
217
+ setReturnStep(null);
218
+ return;
219
+ }
151
220
  step.provider.defaultModel = v;
152
221
  ctl.saveProvider(step.provider);
153
222
  ctl.setDefaultProvider(step.provider.name);
154
223
  saved();
155
- setStep({ id: 'root' });
224
+ setStep(returnStep ?? { id: 'root' });
225
+ setReturnStep(null);
156
226
  }, onInput: (m) => {
157
227
  const model = m.trim();
158
228
  if (!model)
@@ -163,30 +233,50 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
163
233
  ctl.saveProvider(step.provider);
164
234
  ctl.setDefaultProvider(step.provider.name);
165
235
  saved();
166
- setStep({ id: 'root' });
236
+ setStep(returnStep ?? { id: 'root' });
237
+ setReturnStep(null);
167
238
  } })] })), step.id === 'priceModel' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.priceModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
168
239
  ...step.provider.models.map((m) => {
169
240
  const pr = priceFor(step.provider, m);
170
241
  return {
171
242
  label: m,
172
243
  value: m,
173
- hint: pr ? `($${pr.input}/M in · $${pr.output}/M out${step.provider.prices?.[m] ? ' — override' : ''})` : `(${t('set.priceUnknown')})`,
244
+ hint: pr
245
+ ? `($${pr.input}/M in · $${pr.output}/M out${step.provider.prices?.[m] ? ' — override' : ''})`
246
+ : `(${t('set.priceUnknown')})`,
174
247
  };
175
248
  }),
176
249
  { label: t('set.back'), value: '__back__' },
177
- ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
178
- if (v === '__back__')
179
- return setStep({ id: 'root' });
180
- setStep({ id: 'priceValue', provider: step.provider, model: v });
181
- }, onInput: (m) => setStep({ 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) => {
250
+ ], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
251
+ if (v === '__back__') {
252
+ setStep(returnStep ?? { id: 'root' });
253
+ setReturnStep(null);
254
+ return;
255
+ }
256
+ goSub({ id: 'priceValue', provider: step.provider, model: 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) => {
182
258
  const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
183
259
  if (!m)
184
260
  return setFlash(t('set.priceBad'));
185
- step.provider.prices = { ...step.provider.prices, [step.model]: { input: parseFloat(m[1]), output: parseFloat(m[2]) } };
261
+ step.provider.prices = {
262
+ ...step.provider.prices,
263
+ [step.model]: { input: parseFloat(m[1]), output: parseFloat(m[2]) },
264
+ };
186
265
  ctl.saveProvider(step.provider);
187
266
  saved();
188
- setStep({ id: 'root' });
189
- } })] })), 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) => {
267
+ setStep(returnStep ?? { id: 'root' });
268
+ setReturnStep(null);
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
+ }
277
+ setStep(returnStep ?? { id: 'root' });
278
+ setReturnStep(null);
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) => {
190
280
  try {
191
281
  const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
192
282
  setFlash(t('m.skillCreated', { file }));
@@ -195,7 +285,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
195
285
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
196
286
  }
197
287
  setStep({ id: 'root' });
198
- } })] })), 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) => {
199
289
  try {
200
290
  const file = createSpecialistTemplate(name.trim(), '', 'global', ctl.projectRoot);
201
291
  setFlash(t('m.specCreated', { file }));
@@ -204,21 +294,161 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
204
294
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
205
295
  }
206
296
  setStep({ id: 'root' });
207
- } })] })), step.id === 'model' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseModel', { name: step.provider.name }) }), _jsx(SelectList, { items: [
208
- ...step.provider.models.map((m) => ({
209
- label: m,
210
- value: m,
211
- hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
212
- })),
213
- { label: t('set.back'), value: '__back__' },
214
- ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
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) => {
215
346
  if (v === '__back__')
216
347
  return setStep({ id: 'root' });
217
- pickModel(step.provider, v);
218
- }, onInput: (m) => pickModel(step.provider, m) })] })), 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) => {
219
- step.provider.apiKey = k.trim();
220
- ctl.saveProvider(step.provider);
221
- saved();
222
- setStep({ id: 'root' });
223
- } })] })), 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()) })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: t('set.esc') }) })] }));
348
+ if (v === '__add__') {
349
+ setReturnStep({ id: 'providers', scope: step.scope });
350
+ return setStep({ id: 'newName' });
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
367
+ const p = cfg.providers.find((x) => x.name === v);
368
+ if (!p)
369
+ return;
370
+ if (step.scope === 'session') {
371
+ setReturnStep({ id: 'root' });
372
+ setStep({ id: 'model', provider: p });
373
+ }
374
+ else {
375
+ setStep({ id: 'providerDetail', provider: p, scope: 'global' });
376
+ }
377
+ } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
378
+ {
379
+ label: t('set.providerDetail.key'),
380
+ value: 'key',
381
+ hint: masked(step.provider.apiKey),
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
+ },
396
+ {
397
+ label: t('set.providerDetail.models'),
398
+ value: 'models',
399
+ hint: `(${step.provider.models.length})`,
400
+ },
401
+ { label: t('set.providerDetail.pricing'), value: 'pricing' },
402
+ {
403
+ label: t('set.providerDetail.setDefault'),
404
+ value: 'setDefault',
405
+ hint: step.provider.name.toLowerCase() === cfg.defaultProvider.toLowerCase()
406
+ ? `(${t('set.status.default')})`
407
+ : undefined,
408
+ },
409
+ { label: t('set.providerDetail.remove'), value: 'remove' },
410
+ { label: t('set.providerDetail.back'), value: '__back__' },
411
+ ], height: listHeight, onSelect: (v) => {
412
+ if (v === '__back__')
413
+ return setStep({ id: 'providers', scope: step.scope });
414
+ if (v === 'key') {
415
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
416
+ return setStep({ id: 'key', provider: step.provider });
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
+ }
427
+ if (v === 'models') {
428
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
429
+ return setStep({ id: 'modelList', provider: step.provider });
430
+ }
431
+ if (v === 'pricing') {
432
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
433
+ return setStep({ id: 'priceModel', provider: step.provider });
434
+ }
435
+ if (v === 'setDefault') {
436
+ ctl.setDefaultProvider(step.provider.name);
437
+ saved();
438
+ return setStep({ id: 'providers', scope: step.scope });
439
+ }
440
+ if (v === 'remove')
441
+ return setStep({ id: 'removeProvider', provider: step.provider, scope: step.scope });
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: [
443
+ { label: t('set.removeProvider.yes'), value: 'yes' },
444
+ { label: t('set.removeProvider.no'), value: 'no' },
445
+ ], height: listHeight, onSelect: (v) => {
446
+ if (v === 'no')
447
+ return setStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
448
+ if (v === 'yes') {
449
+ ctl.removeProvider(step.provider.name);
450
+ saved();
451
+ setStep({ id: 'providers', scope: step.scope });
452
+ }
453
+ } })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: t('set.esc') }) })] }));
224
454
  }
package/dist/ui/Wizard.js CHANGED
@@ -7,11 +7,13 @@ 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;
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 if (items[idx]) {
30
- onSelect?.(items[idx].value);
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,32 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
37
41
  }
38
42
  if (key.upArrow) {
39
43
  if (!typing)
40
- setIdx((i) => (i - 1 + items.length) % Math.max(1, items.length));
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, items.length));
49
+ setIdx((i) => (i + 1) % Math.max(1, selectable.length));
50
+ return;
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));
46
70
  return;
47
71
  }
48
72
  if (key.tab || key.ctrl || key.meta)
@@ -59,7 +83,20 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
59
83
  }
60
84
  setTyped((v) => v + input);
61
85
  });
62
- return (_jsxs(Box, { flexDirection: "column", children: [items.map((it, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === idx ? 'cyanBright' : 'gray', bold: !typing && i === idx, children: [!typing && i === idx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : 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] }) }))] }));
86
+ // Build a separate index map so up/down skip section headers.
87
+ const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
88
+ const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
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] }) }))] }));
63
100
  }
64
101
  export function WizardStep({ step, total, title, children, footer, }) {
65
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') }) })] }));