@parallel-cli/parallel 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { matchCommands } from '../commands.js';
5
5
  import { t } from '../i18n.js';
@@ -43,6 +43,7 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
43
43
  const [attachments, setAttachments] = useState([]);
44
44
  const [history, setHistory] = useState([]);
45
45
  const [histIdx, setHistIdx] = useState(-1);
46
+ const [selectedSuggestion, setSelectedSuggestion] = useState(0);
46
47
  const attSeq = useRef(0);
47
48
  const reset = () => {
48
49
  setValue('');
@@ -62,7 +63,8 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
62
63
  const images = attachments.filter((a) => a.kind === 'image');
63
64
  if (!full && images.length === 0)
64
65
  return;
65
- setHistory((h) => [...h.slice(-49), v]);
66
+ if (!full.toLowerCase().startsWith('/key '))
67
+ setHistory((h) => [...h.slice(-49), v]);
66
68
  setHistIdx(-1);
67
69
  reset();
68
70
  onSubmit(full, images.length > 0 ? images.map((i) => i.dataUri) : undefined);
@@ -85,19 +87,30 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
85
87
  setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
86
88
  notify?.(t('input.imageAdded'));
87
89
  };
90
+ const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 10) : [];
91
+ const agentSuggestions = value.startsWith('@') && !value.includes(' ')
92
+ ? ['all', ...agentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
93
+ : [];
94
+ const suggestionCount = cmdSuggestions.length > 0 ? cmdSuggestions.length : agentSuggestions.length;
95
+ const hasSuggestions = suggestionCount > 0;
96
+ const exactCommand = cmdSuggestions.some((c) => c.name === value.toLowerCase() || c.aliases?.some((a) => a === value.toLowerCase()));
97
+ useEffect(() => {
98
+ setSelectedSuggestion(0);
99
+ }, [value]);
100
+ useEffect(() => {
101
+ if (selectedSuggestion >= suggestionCount)
102
+ setSelectedSuggestion(Math.max(0, suggestionCount - 1));
103
+ }, [selectedSuggestion, suggestionCount]);
88
104
  const completeBest = () => {
89
- const cmd = bestCommandCompletion(value);
90
- if (cmd) {
91
- setValue(cmd);
105
+ if (cmdSuggestions.length > 0) {
106
+ const cmd = cmdSuggestions[Math.min(selectedSuggestion, cmdSuggestions.length - 1)];
107
+ setValue(`${cmd.name} `);
92
108
  return true;
93
109
  }
94
- if (value.startsWith('@')) {
95
- const frag = value.slice(1).toLowerCase();
96
- const m = ['all', ...agentNames].find((n) => n.toLowerCase().startsWith(frag));
97
- if (m) {
98
- setValue('@' + m + ' ');
99
- return true;
100
- }
110
+ if (agentSuggestions.length > 0) {
111
+ const agent = agentSuggestions[Math.min(selectedSuggestion, agentSuggestions.length - 1)];
112
+ setValue('@' + agent + ' ');
113
+ return true;
101
114
  }
102
115
  return false;
103
116
  };
@@ -110,6 +123,10 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
110
123
  return;
111
124
  }
112
125
  if (key.return) {
126
+ if (hasSuggestions && !exactCommand) {
127
+ completeBest();
128
+ return;
129
+ }
113
130
  submit(value);
114
131
  return;
115
132
  }
@@ -126,6 +143,10 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
126
143
  return;
127
144
  }
128
145
  if (key.upArrow) {
146
+ if (hasSuggestions) {
147
+ setSelectedSuggestion((i) => (i - 1 + suggestionCount) % suggestionCount);
148
+ return;
149
+ }
129
150
  setHistIdx((i) => {
130
151
  const ni = i === -1 ? history.length - 1 : Math.max(0, i - 1);
131
152
  if (history[ni] !== undefined)
@@ -135,6 +156,10 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
135
156
  return;
136
157
  }
137
158
  if (key.downArrow) {
159
+ if (hasSuggestions) {
160
+ setSelectedSuggestion((i) => (i + 1) % suggestionCount);
161
+ return;
162
+ }
138
163
  setHistIdx((i) => {
139
164
  if (i === -1)
140
165
  return -1;
@@ -183,13 +208,13 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
183
208
  }
184
209
  setValue((v) => v + input);
185
210
  }, { isActive: active });
186
- const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 10) : [];
187
- const agentSuggestions = value.startsWith('@') && !value.includes(' ')
188
- ? ['all', ...agentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
189
- : [];
190
211
  const shown = mask ? '•'.repeat(value.length) : value;
191
212
  const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
192
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(14) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name)))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
213
+ const commandIndexes = new Map(cmdSuggestions.map((c, i) => [c.name, i]));
214
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => ((() => {
215
+ const selected = commandIndexes.get(c.name) === selectedSuggestion;
216
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? 'cyanBright' : 'cyan', bold: true, children: [selected ? '› ' : ' ', c.name.padEnd(14)] }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name));
217
+ })()))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === selectedSuggestion ? 'cyanBright' : 'cyan', bold: true, children: [i === selectedSuggestion ? '› ' : ' ', "@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
193
218
  ? t('input.atAll')
194
219
  : `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color: "cyan", backgroundColor: "gray", children: [' ', a.kind === 'paste' ? t('input.attPaste', { n: a.n, lines: a.lines }) : t('input.attImage', { n: a.n, file: a.label }), ' '] }, a.n))) })), _jsxs(Box, { borderStyle: "single", borderColor: active ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u203A", ' '] }), shown ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: shown }), active && _jsx(Text, { color: "cyanBright", children: "\u2588" })] })) : (_jsxs(_Fragment, { children: [active && _jsx(Text, { color: "cyanBright", children: "\u2588" }), _jsx(Text, { color: "gray", children: placeholder })] }))] })] }));
195
220
  }
@@ -3,8 +3,9 @@ import { useState } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import { createSkillTemplate, createSpecialistTemplate } from '../skills.js';
5
5
  import { priceFor } from '../pricing.js';
6
- import { SelectList } from './Wizard.js';
6
+ import { SelectList as BaseSelectList } from './Wizard.js';
7
7
  import { LANGS, getLang, setLang, t } from '../i18n.js';
8
+ import { detectProviderModels, isLocalProvider, isPlaceholderModel, 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,23 @@ 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;
42
+ const goBack = () => {
43
+ if (step.id === 'root')
44
+ return onClose();
45
+ setStep(returnStep ?? { id: 'root' });
46
+ setReturnStep(null);
47
+ };
48
+ const SelectList = (props) => {
49
+ const { onBack, ...rest } = props;
50
+ return _jsx(BaseSelectList, { ...rest, onBack: onBack ?? goBack });
51
+ };
41
52
  // ---- root menu items ----
42
53
  const rootItems = scope === 'global'
43
54
  ? [
@@ -102,6 +113,17 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
102
113
  };
103
114
  // ---- shared helpers ----
104
115
  const pickModel = (provider, model) => {
116
+ if (isPlaceholderModel(model)) {
117
+ setFlash(t('set.modelPlaceholder'));
118
+ return;
119
+ }
120
+ if (step.id === 'model' && step.setup) {
121
+ provider.defaultModel = model;
122
+ if (!provider.models.includes(model))
123
+ provider.models.push(model);
124
+ setStep({ id: 'endpoint', provider, setup: true });
125
+ return;
126
+ }
105
127
  if (scope === 'global') {
106
128
  provider.defaultModel = model;
107
129
  if (!provider.models.includes(model))
@@ -116,10 +138,23 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
116
138
  setStep(returnStep ?? { id: 'root' });
117
139
  setReturnStep(null);
118
140
  };
119
- const finishNewProvider = (name, url, model, key) => {
120
- ctl.saveProvider({ name, baseUrl: url, apiKey: key, models: [model], defaultModel: model });
121
- saved();
122
- setStep(returnStep ?? { id: 'root' });
141
+ const finishProviderSetup = (provider, persist = scope === 'global') => {
142
+ if (persist) {
143
+ ctl.saveProvider(provider);
144
+ if (scope === 'global') {
145
+ ctl.setDefaultProvider(provider.name);
146
+ saved();
147
+ }
148
+ else {
149
+ ctl.setSessionModel(`${provider.name}:${provider.defaultModel || provider.models[0] || ''}`);
150
+ setFlash(t('set.saved'));
151
+ }
152
+ }
153
+ else {
154
+ ctl.setSessionProviderConfig(provider);
155
+ setFlash(t('set.sessionProviderReady', { name: provider.name }));
156
+ }
157
+ setStep(returnStep ?? { id: 'providers', scope });
123
158
  setReturnStep(null);
124
159
  };
125
160
  // ---- navigate into a sub-step, remembering where to return ----
@@ -128,7 +163,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
128
163
  setStep(next);
129
164
  };
130
165
  // ---- 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) => {
166
+ 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
167
  setLang(code);
133
168
  ctl.setLanguage(code);
134
169
  saved();
@@ -136,7 +171,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
136
171
  } })), step.id === 'pickProvider' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.chooseProvider') }), _jsx(SelectList, { items: [
137
172
  ...cfg.providers.map((p) => ({ label: p.name, value: p.name, hint: `(${p.baseUrl})` })),
138
173
  { label: t('set.back'), value: '__back__' },
139
- ], onSelect: (v) => {
174
+ ], height: listHeight, onSelect: (v) => {
140
175
  if (v === '__back__')
141
176
  return setStep({ id: 'root' });
142
177
  const p = cfg.providers.find((x) => x.name === v);
@@ -154,21 +189,55 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
154
189
  hint: m === step.provider.defaultModel ? t('wiz.model.default') : undefined,
155
190
  })),
156
191
  { label: t('set.back'), value: '__back__' },
157
- ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
192
+ ], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
158
193
  if (v === '__back__') {
159
194
  setStep(returnStep ?? { id: 'root' });
160
195
  setReturnStep(null);
161
196
  return;
162
197
  }
163
198
  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: [
199
+ }, 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: [
200
+ { label: t('wiz.provider.endpoint.use'), value: 'use' },
201
+ { label: t('wiz.provider.endpoint.edit'), value: 'edit' },
202
+ { label: t('set.back'), value: '__back__' },
203
+ ], height: listHeight, onSelect: (v) => {
204
+ if (v === '__back__') {
205
+ setStep({ id: 'model', provider: step.provider, setup: step.setup });
206
+ return;
207
+ }
208
+ if (v === 'edit')
209
+ return setStep({ id: 'editEndpoint', provider: step.provider, setup: step.setup });
210
+ if (step.setup) {
211
+ if (providerNeedsApiKey(step.provider))
212
+ return setStep({ id: 'key', provider: step.provider, setup: true });
213
+ if (scope === 'session')
214
+ return setStep({ id: 'setupScope', provider: step.provider });
215
+ finishProviderSetup(step.provider);
216
+ return;
217
+ }
218
+ ctl.saveProvider(step.provider);
219
+ saved();
220
+ setStep(returnStep ?? { id: 'providerDetail', provider: step.provider, scope });
221
+ setReturnStep(null);
222
+ } })] })), step.id === 'setupScope' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.setupScope.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
223
+ { label: t('set.setupScope.session'), value: 'session' },
224
+ { label: t('set.setupScope.global'), value: 'global' },
225
+ { label: t('set.back'), value: '__back__' },
226
+ ], height: listHeight, onSelect: (v) => {
227
+ if (v === '__back__')
228
+ return setStep({ id: 'endpoint', provider: step.provider, setup: true });
229
+ finishProviderSetup(step.provider, v === 'global');
230
+ } })] })), 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) => {
231
+ const provider = { ...step.provider, baseUrl: url.trim() };
232
+ setStep({ id: 'endpoint', provider, setup: step.setup });
233
+ } })] })), step.id === 'modelList' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.modelsFor', { name: step.provider.name }) }), _jsx(SelectList, { items: [
165
234
  ...step.provider.models.map((m) => ({
166
235
  label: m,
167
236
  value: m,
168
237
  hint: m === step.provider.defaultModel ? t('wiz.model.default') : t('set.makeDefault'),
169
238
  })),
170
239
  { label: t('set.back'), value: '__back__' },
171
- ], allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
240
+ ], height: listHeight, allowInput: true, inputPlaceholder: t('set.addModelName'), onSelect: (v) => {
172
241
  if (v === '__back__') {
173
242
  setStep(returnStep ?? { id: 'root' });
174
243
  setReturnStep(null);
@@ -204,14 +273,14 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
204
273
  };
205
274
  }),
206
275
  { label: t('set.back'), value: '__back__' },
207
- ], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
276
+ ], height: listHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onSelect: (v) => {
208
277
  if (v === '__back__') {
209
278
  setStep(returnStep ?? { id: 'root' });
210
279
  setReturnStep(null);
211
280
  return;
212
281
  }
213
282
  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) => {
283
+ }, 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
284
  const m = v.match(/^\s*([\d.]+)\s*[,;\s]\s*([\d.]+)\s*$/);
216
285
  if (!m)
217
286
  return setFlash(t('set.priceBad'));
@@ -223,13 +292,42 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
223
292
  saved();
224
293
  setStep(returnStep ?? { id: 'root' });
225
294
  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();
295
+ } })] })), 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) => {
296
+ const provider = { ...step.provider, apiKey: k.trim() };
297
+ if (step.setup) {
298
+ if (scope === 'session') {
299
+ setStep({ id: 'setupScope', provider });
300
+ return;
301
+ }
302
+ finishProviderSetup(provider);
303
+ return;
304
+ }
305
+ else {
306
+ ctl.saveProvider(provider);
307
+ saved();
308
+ }
230
309
  setStep(returnStep ?? { id: 'root' });
231
310
  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) => {
311
+ } })] })), 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) => {
312
+ const trimmed = model.trim();
313
+ if (isPlaceholderModel(trimmed)) {
314
+ setFlash(t('set.modelPlaceholder'));
315
+ return;
316
+ }
317
+ const local = isLocalProvider({ baseUrl: step.url });
318
+ setStep({
319
+ id: 'endpoint',
320
+ setup: true,
321
+ provider: {
322
+ name: step.name,
323
+ baseUrl: step.url,
324
+ apiKey: '',
325
+ models: [trimmed],
326
+ defaultModel: trimmed,
327
+ requiresApiKey: !local,
328
+ },
329
+ });
330
+ } })] })), 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) => finishProviderSetup({ name: step.name, baseUrl: step.url, apiKey: key.trim(), models: [step.model], defaultModel: step.model }) })] })), 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
331
  try {
234
332
  const file = createSkillTemplate(name.trim(), '', 'global', ctl.projectRoot);
235
333
  setFlash(t('m.skillCreated', { file }));
@@ -238,7 +336,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
238
336
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
239
337
  }
240
338
  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) => {
339
+ } })] })), 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
340
  try {
243
341
  const file = createSpecialistTemplate(name.trim(), '', 'global', ctl.projectRoot);
244
342
  setFlash(t('m.specCreated', { file }));
@@ -247,31 +345,101 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
247
345
  setFlash(t('m.alreadyExists', { msg: e?.message ?? '' }));
248
346
  }
249
347
  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) => {
348
+ } })] })), 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: (() => {
349
+ const configuredNames = new Set(cfg.providers.map((p) => p.name.toLowerCase()));
350
+ const items = [];
351
+ // Section: Configured
352
+ if (cfg.providers.length > 0) {
353
+ items.push({ label: t('wiz.provider.section.configured'), value: '', section: true });
354
+ for (const p of cfg.providers) {
355
+ items.push({
356
+ label: p.name,
357
+ value: p.name,
358
+ detail: providerStatus(p, cfg.defaultProvider),
359
+ });
360
+ }
361
+ }
362
+ // Sections per category: western, chinese, gateways, inference, local
363
+ const catOrder = ['western', 'chinese', 'gateways', 'inference', 'local'];
364
+ const emoji = {
365
+ western: '\u{1F1FA}\u{1F1F8} ',
366
+ chinese: '\u{1F1E8}\u{1F1F3} ',
367
+ gateways: '\u{1F310} ',
368
+ inference: '\u26A1 ',
369
+ local: '\u{1F3E0} ',
370
+ };
371
+ for (const cat of catOrder) {
372
+ const presetsInCat = PROVIDER_PRESETS.filter((p) => p.category === cat && !configuredNames.has(p.name.toLowerCase()));
373
+ if (presetsInCat.length === 0)
374
+ continue;
375
+ const key = `wiz.provider.section.${cat}`;
376
+ const sectionLabel = emoji[cat] + t(key);
377
+ items.push({ value: '', label: sectionLabel, section: true });
378
+ for (const preset of presetsInCat) {
379
+ const detail = preset.models.length > 0
380
+ ? `${preset.models.length} model${preset.models.length > 1 ? 's' : ''}`
381
+ : undefined;
382
+ items.push({
383
+ label: preset.name,
384
+ value: `__preset__${preset.name}`,
385
+ detail,
386
+ });
387
+ }
388
+ }
389
+ // Custom provider
390
+ items.push({ label: t('wiz.provider.custom'), value: '__add__' });
391
+ items.push({
392
+ label: step.scope === 'global' ? t('set.providers.back') : t('sset.providers.back'),
393
+ value: '__back__',
394
+ });
395
+ return items;
396
+ })(), height: listHeight, onSelect: (v) => {
259
397
  if (v === '__back__')
260
398
  return setStep({ id: 'root' });
261
399
  if (v === '__add__') {
262
400
  setReturnStep({ id: 'providers', scope: step.scope });
263
401
  return setStep({ id: 'newName' });
264
402
  }
403
+ // Preset selection (e.g. __preset__Anthropic)
404
+ if (v.startsWith('__preset__')) {
405
+ const presetName = v.slice('__preset__'.length);
406
+ const preset = PROVIDER_PRESETS.find((p) => p.name === presetName);
407
+ if (!preset)
408
+ return;
409
+ if (preset.category === 'local') {
410
+ setFlash(t('wiz.provider.ollama.checking', { url: preset.baseUrl }));
411
+ void (async () => {
412
+ const detected = await detectProviderModels(preset);
413
+ setFlash(detected
414
+ ? t('wiz.provider.ollama.found', { n: detected.models.length })
415
+ : t('wiz.provider.ollama.notFound', { url: preset.baseUrl }));
416
+ setReturnStep({ id: 'providers', scope: step.scope });
417
+ setStep({
418
+ id: 'model',
419
+ provider: {
420
+ ...preset,
421
+ apiKey: 'local',
422
+ models: detected?.models ?? [...preset.models],
423
+ defaultModel: detected?.defaultModel ?? preset.defaultModel,
424
+ },
425
+ setup: true,
426
+ });
427
+ })();
428
+ return;
429
+ }
430
+ // Preset setup: choose model, review endpoint, then ask for API key if needed.
431
+ setReturnStep({ id: 'providers', scope: step.scope });
432
+ return setStep({ id: 'model', provider: { ...preset, models: [...preset.models] }, setup: true });
433
+ }
434
+ // Existing configured provider
265
435
  const p = cfg.providers.find((x) => x.name === v);
266
436
  if (!p)
267
437
  return;
268
438
  if (step.scope === 'session') {
269
- // Session scope: pick a model for this session
270
439
  setReturnStep({ id: 'root' });
271
440
  setStep({ id: 'model', provider: p });
272
441
  }
273
442
  else {
274
- // Global scope: go to provider detail
275
443
  setStep({ id: 'providerDetail', provider: p, scope: 'global' });
276
444
  }
277
445
  } })] })), step.id === 'providerDetail' && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "gray", children: t('set.providerDetail.title', { name: step.provider.name }) }), _jsx(SelectList, { items: [
@@ -280,6 +448,19 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
280
448
  value: 'key',
281
449
  hint: masked(step.provider.apiKey),
282
450
  },
451
+ ...(step.provider.apiKey
452
+ ? [
453
+ {
454
+ label: t('set.providerDetail.clearKey'),
455
+ value: 'clearKey',
456
+ },
457
+ ]
458
+ : []),
459
+ {
460
+ label: t('set.providerDetail.endpoint'),
461
+ value: 'endpoint',
462
+ hint: `(${step.provider.baseUrl})`,
463
+ },
283
464
  {
284
465
  label: t('set.providerDetail.models'),
285
466
  value: 'models',
@@ -295,13 +476,22 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
295
476
  },
296
477
  { label: t('set.providerDetail.remove'), value: 'remove' },
297
478
  { label: t('set.providerDetail.back'), value: '__back__' },
298
- ], onSelect: (v) => {
479
+ ], height: listHeight, onSelect: (v) => {
299
480
  if (v === '__back__')
300
481
  return setStep({ id: 'providers', scope: step.scope });
301
482
  if (v === 'key') {
302
483
  setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
303
484
  return setStep({ id: 'key', provider: step.provider });
304
485
  }
486
+ if (v === 'clearKey') {
487
+ ctl.saveProvider({ ...step.provider, apiKey: '' });
488
+ saved();
489
+ return setStep({ id: 'providerDetail', provider: { ...step.provider, apiKey: '' }, scope: step.scope });
490
+ }
491
+ if (v === 'endpoint') {
492
+ setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
493
+ return setStep({ id: 'endpoint', provider: step.provider });
494
+ }
305
495
  if (v === 'models') {
306
496
  setReturnStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
307
497
  return setStep({ id: 'modelList', provider: step.provider });
@@ -320,7 +510,7 @@ export function SettingsPanel({ ctl, scope, onClose, }) {
320
510
  } })] })), 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
511
  { label: t('set.removeProvider.yes'), value: 'yes' },
322
512
  { label: t('set.removeProvider.no'), value: 'no' },
323
- ], onSelect: (v) => {
513
+ ], height: listHeight, onSelect: (v) => {
324
514
  if (v === 'no')
325
515
  return setStep({ id: 'providerDetail', provider: step.provider, scope: step.scope });
326
516
  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') }) })] }));