@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.
@@ -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 (isLocal)
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
- ctl.saveProvider({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
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 === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
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.apiKey = k.trim();
228
- ctl.saveProvider(step.provider);
229
- saved();
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
- ...cfg.providers.map((p) => ({
252
- label: p.name,
253
- value: p.name,
254
- hint: providerStatus(p, cfg.defaultProvider),
255
- })),
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) => {
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
- 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] }) }))] }));
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) - step);
22
- const awayFromAnchor = (v) => Math.min(Math.min(v, max) + step, 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
- // Intro (4) + title + blank lines (2) + footer (2) + border/input/status ≈ 16 rows of overhead.
91
- const visible = useVisibleRows(16);
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 PgUp/PgDn scroll \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') })] }));
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.1",
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": {