@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.
- package/CHANGELOG.md +149 -0
- package/README.md +183 -195
- package/dist/agents/agent.js +2 -2
- package/dist/agents/tools.js +47 -5
- package/dist/commands.js +117 -18
- package/dist/config.js +248 -63
- package/dist/controller.js +48 -17
- package/dist/i18n.js +192 -44
- package/dist/index.js +8 -5
- package/dist/pricing.js +162 -54
- package/dist/ui/App.js +208 -102
- package/dist/ui/CommandInput.js +42 -17
- package/dist/ui/SettingsPanel.js +224 -34
- package/dist/ui/Wizard.js +33 -3
- package/dist/ui/views.js +53 -21
- package/package.json +10 -1
package/dist/ui/CommandInput.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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 (
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/ui/SettingsPanel.js
CHANGED
|
@@ -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 (
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 === '
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
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) =>
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
4
|
import { t } from '../i18n.js';
|
|
@@ -7,7 +7,7 @@ import { t } from '../i18n.js';
|
|
|
7
7
|
* type a free value (e.g. a folder path or a custom model name) — typing
|
|
8
8
|
* switches to input mode, Esc comes back to the list.
|
|
9
9
|
*/
|
|
10
|
-
export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack, onSelect, onInput, }) {
|
|
10
|
+
export function SelectList({ items, allowInput, inputPlaceholder, mask, height, onBack, onSelect, onInput, }) {
|
|
11
11
|
const [idx, setIdx] = useState(0);
|
|
12
12
|
const [typed, setTyped] = useState('');
|
|
13
13
|
const typing = allowInput && typed.length > 0;
|
|
@@ -49,6 +49,26 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
49
49
|
setIdx((i) => (i + 1) % Math.max(1, selectable.length));
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
if (key.pageUp) {
|
|
53
|
+
if (!typing)
|
|
54
|
+
setIdx((i) => Math.max(0, i - Math.max(1, Math.floor((height ?? 8) / 2))));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.pageDown) {
|
|
58
|
+
if (!typing)
|
|
59
|
+
setIdx((i) => Math.min(Math.max(0, selectable.length - 1), i + Math.max(1, Math.floor((height ?? 8) / 2))));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.home) {
|
|
63
|
+
if (!typing)
|
|
64
|
+
setIdx(0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (key.end) {
|
|
68
|
+
if (!typing)
|
|
69
|
+
setIdx(Math.max(0, selectable.length - 1));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
52
72
|
if (key.tab || key.ctrl || key.meta)
|
|
53
73
|
return;
|
|
54
74
|
if (!allowInput || !input)
|
|
@@ -66,7 +86,17 @@ export function SelectList({ items, allowInput, inputPlaceholder, mask, onBack,
|
|
|
66
86
|
// Build a separate index map so up/down skip section headers.
|
|
67
87
|
const selectable = items.map((it, i) => (it.section ? -1 : i)).filter((i) => i >= 0);
|
|
68
88
|
const safeIdx = selectable.length > 0 ? selectable[Math.min(idx, selectable.length - 1)] : -1;
|
|
69
|
-
|
|
89
|
+
const maxVisible = height ? Math.max(1, height - (allowInput ? 2 : 0)) : items.length;
|
|
90
|
+
const start = items.length > maxVisible && safeIdx >= 0
|
|
91
|
+
? Math.max(0, Math.min(safeIdx - Math.floor(maxVisible / 2), items.length - maxVisible))
|
|
92
|
+
: 0;
|
|
93
|
+
const visibleItems = items.slice(start, start + maxVisible);
|
|
94
|
+
const above = start;
|
|
95
|
+
const below = Math.max(0, items.length - start - visibleItems.length);
|
|
96
|
+
return (_jsxs(Box, { flexDirection: "column", children: [above > 0 ? _jsxs(Text, { color: "gray", children: ["\u25B2 ", above] }) : null, visibleItems.map((it, localIdx) => {
|
|
97
|
+
const i = start + localIdx;
|
|
98
|
+
return (it.section ? (_jsx(Box, { marginTop: i > 0 ? 1 : 0, children: _jsx(Text, { bold: true, color: "white", children: it.label }) }, it.label)) : (_jsxs(Text, { children: [_jsxs(Text, { color: !typing && i === safeIdx ? 'cyanBright' : 'gray', bold: !typing && i === safeIdx, children: [!typing && i === safeIdx ? '❯ ' : ' ', it.label] }), it.hint ? _jsxs(Text, { color: "gray", children: [" ", it.hint] }) : null, it.detail ? _jsxs(Text, { color: "gray", children: [" \u2014 ", it.detail] }) : null] }, it.value + i)));
|
|
99
|
+
}), below > 0 ? _jsxs(Text, { color: "gray", children: ["\u25BC ", below] }) : null, allowInput && (_jsx(Box, { marginTop: items.length > 0 ? 1 : 0, children: _jsxs(Text, { color: typing ? 'cyanBright' : 'gray', children: ["\u270E", ' ', typing ? (_jsx(Text, { color: "white", children: mask ? '•'.repeat(typed.length) : typed })) : (_jsx(Text, { color: "gray", children: inputPlaceholder ?? '…' })), typing ? _jsx(Text, { color: "cyanBright", children: "\u2588" }) : null] }) }))] }));
|
|
70
100
|
}
|
|
71
101
|
export function WizardStep({ step, total, title, children, footer, }) {
|
|
72
102
|
return (_jsxs(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { bold: true, color: "cyan", children: ["[", step, "/", total, "] ", title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: children }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", children: footer ?? t('wiz.footer.select') }) })] }));
|