@parallel-cli/parallel 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/README.md +136 -131
- package/dist/commands.js +17 -4
- package/dist/config.js +218 -63
- package/dist/controller.js +13 -11
- package/dist/i18n.js +64 -20
- package/dist/index.js +5 -2
- package/dist/pricing.js +162 -54
- package/dist/ui/App.js +141 -56
- package/dist/ui/CommandInput.js +42 -17
- package/dist/ui/SettingsPanel.js +153 -31
- package/dist/ui/Wizard.js +33 -3
- package/dist/ui/views.js +15 -6
- package/package.json +10 -1
package/dist/ui/App.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
|
6
6
|
import { Controller } from '../controller.js';
|
|
7
7
|
import { startSessionServer } from '../server.js';
|
|
8
8
|
import { executeInput } from '../commands.js';
|
|
9
|
-
import { PROVIDER_PRESETS, getProvider, rememberFolder, saveConfig } from '../config.js';
|
|
9
|
+
import { PROVIDER_PRESETS, getProvider, providerNeedsApiKey, providerReady, rememberFolder, saveConfig } from '../config.js';
|
|
10
10
|
import { LANGS, setLang, t } from '../i18n.js';
|
|
11
11
|
import { AgentRow, AgentTranscript } from './AgentPanel.js';
|
|
12
12
|
import { ApprovalPrompt } from './ApprovalPrompt.js';
|
|
@@ -17,11 +17,11 @@ import { BoardView, CostView, DiffView, HelpView, NotesView, SessionsView, Skill
|
|
|
17
17
|
import { SelectList, WizardStep } from './Wizard.js';
|
|
18
18
|
import { BRAND, CHROME, STATE, STATE_META, UI, middleTruncate } from './tokens.js';
|
|
19
19
|
const LOGO = 'Parallel';
|
|
20
|
-
// Version from package.json
|
|
21
|
-
const VERSION = '0.
|
|
20
|
+
// Version from package.json. Hardcoded — rootDir: "src" prevents importing ../../package.json.
|
|
21
|
+
const VERSION = '0.4.3';
|
|
22
22
|
function usableProvider(config) {
|
|
23
23
|
const p = getProvider(config);
|
|
24
|
-
return p && p
|
|
24
|
+
return p && providerReady(p) && (p.defaultModel || p.models[0]) ? p : undefined;
|
|
25
25
|
}
|
|
26
26
|
function normalizeFolder(p) {
|
|
27
27
|
return path.resolve(p.replace(/^~(?=$|\/)/, process.env.HOME ?? '~'));
|
|
@@ -47,6 +47,8 @@ function startupFolder(config, initialFolder) {
|
|
|
47
47
|
}
|
|
48
48
|
export function App({ config, initialFolder }) {
|
|
49
49
|
const { exit } = useApp();
|
|
50
|
+
const { stdout } = useStdout();
|
|
51
|
+
const wizardListHeight = Math.max(4, (stdout?.rows ?? 30) - 10);
|
|
50
52
|
const initialUsableProvider = usableProvider(config);
|
|
51
53
|
const directFolder = config.language && initialUsableProvider ? startupFolder(config, initialFolder) : null;
|
|
52
54
|
// ---------- wizard state ----------
|
|
@@ -71,6 +73,19 @@ export function App({ config, initialFolder }) {
|
|
|
71
73
|
: []);
|
|
72
74
|
const [inputReady, setInputReady] = useState(Boolean(directFolder));
|
|
73
75
|
const ctl = ctlRef.current;
|
|
76
|
+
const leaveCurrentProject = () => {
|
|
77
|
+
const current = ctlRef.current;
|
|
78
|
+
current?.saveSession();
|
|
79
|
+
current?.stopAll();
|
|
80
|
+
setFocus(null);
|
|
81
|
+
setView('agents');
|
|
82
|
+
setRawLogs(false);
|
|
83
|
+
setProviderStep({ id: 'pick' });
|
|
84
|
+
setModelCustom(false);
|
|
85
|
+
setWizardError('');
|
|
86
|
+
setSessions([]);
|
|
87
|
+
setInputReady(false);
|
|
88
|
+
};
|
|
74
89
|
// Re-render (throttled) on every blackboard/controller update.
|
|
75
90
|
useEffect(() => {
|
|
76
91
|
if (!ctl)
|
|
@@ -132,6 +147,22 @@ export function App({ config, initialFolder }) {
|
|
|
132
147
|
{ text: t('m.copyDone', { name: latest.name }), level: 'ok' },
|
|
133
148
|
]);
|
|
134
149
|
},
|
|
150
|
+
openProject: (nextFolder) => {
|
|
151
|
+
leaveCurrentProject();
|
|
152
|
+
if (nextFolder) {
|
|
153
|
+
chooseFolder(nextFolder);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
ctlRef.current = null;
|
|
157
|
+
setFolder('');
|
|
158
|
+
setPhase('folder');
|
|
159
|
+
},
|
|
160
|
+
openWizard: () => {
|
|
161
|
+
leaveCurrentProject();
|
|
162
|
+
ctlRef.current = null;
|
|
163
|
+
setFolder('');
|
|
164
|
+
setPhase('lang');
|
|
165
|
+
},
|
|
135
166
|
}), [exit]);
|
|
136
167
|
// ---------- wizard transitions ----------
|
|
137
168
|
// In normal launches, a complete config goes straight to the main TUI.
|
|
@@ -193,20 +224,29 @@ export function App({ config, initialFolder }) {
|
|
|
193
224
|
if (providerStep.id === 'pick') {
|
|
194
225
|
setPhase(sessions.length > 0 ? 'session' : 'folder');
|
|
195
226
|
}
|
|
196
|
-
else if (providerStep.id === '
|
|
227
|
+
else if (providerStep.id === 'presetModel') {
|
|
197
228
|
setProviderStep({ id: 'pick' });
|
|
198
229
|
}
|
|
230
|
+
else if (providerStep.id === 'endpoint') {
|
|
231
|
+
setProviderStep({ id: 'presetModel', provider: providerStep.provider });
|
|
232
|
+
}
|
|
233
|
+
else if (providerStep.id === 'editEndpoint') {
|
|
234
|
+
setProviderStep({ id: 'endpoint', provider: providerStep.provider });
|
|
235
|
+
}
|
|
236
|
+
else if (providerStep.id === 'key') {
|
|
237
|
+
setProviderStep({ id: 'endpoint', provider: providerStep.provider });
|
|
238
|
+
}
|
|
199
239
|
else if (providerStep.id === 'name') {
|
|
200
240
|
setProviderStep({ id: 'pick' });
|
|
201
241
|
}
|
|
202
242
|
else if (providerStep.id === 'url') {
|
|
203
243
|
setProviderStep({ id: 'name' });
|
|
204
244
|
}
|
|
205
|
-
else if (providerStep.id === '
|
|
245
|
+
else if (providerStep.id === 'customModel') {
|
|
206
246
|
setProviderStep({ id: 'url', name: providerStep.name });
|
|
207
247
|
}
|
|
208
248
|
else if (providerStep.id === 'newKey') {
|
|
209
|
-
setProviderStep({ id: '
|
|
249
|
+
setProviderStep({ id: 'customModel', name: providerStep.provider.name, url: providerStep.provider.baseUrl });
|
|
210
250
|
}
|
|
211
251
|
}
|
|
212
252
|
};
|
|
@@ -221,6 +261,9 @@ export function App({ config, initialFolder }) {
|
|
|
221
261
|
const finishProvider = (p) => {
|
|
222
262
|
ctlRef.current?.saveProvider(p);
|
|
223
263
|
ctlRef.current?.setDefaultProvider(p.name);
|
|
264
|
+
ctlRef.current?.setSessionProvider(p.name);
|
|
265
|
+
if (p.defaultModel || p.models[0])
|
|
266
|
+
ctlRef.current?.setSessionModel(`${p.name}:${p.defaultModel || p.models[0]}`);
|
|
224
267
|
setProviderStep({ id: 'pick' });
|
|
225
268
|
enterMain();
|
|
226
269
|
};
|
|
@@ -259,12 +302,12 @@ export function App({ config, initialFolder }) {
|
|
|
259
302
|
if (phase !== 'main') {
|
|
260
303
|
const totalSteps = 5;
|
|
261
304
|
const sessionProvider = ctl ? ctl.sessionProvider() : getProvider(config);
|
|
262
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyanBright", children: LOGO }), _jsx(Text, { color: "gray", children: t('tagline') }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === 'lang' && (_jsx(WizardStep, { step: 1, total: totalSteps, title: t('wiz.lang.title'), children: _jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), onSelect: chooseLang }) })), phase === 'folder' && (_jsxs(WizardStep, { step: 2, total: totalSteps, title: t('wiz.folder.title'), footer: t('wiz.folder.footer'), children: [wizardError ? _jsx(Text, { color: "red", children: wizardError }) : null, _jsx(SelectList, { items: [
|
|
305
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyanBright", children: LOGO }), _jsx(Text, { color: "gray", children: t('tagline') }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [phase === 'lang' && (_jsx(WizardStep, { step: 1, total: totalSteps, title: t('wiz.lang.title'), children: _jsx(SelectList, { items: LANGS.map((l) => ({ label: l.label, value: l.code })), height: wizardListHeight, onSelect: chooseLang }) })), phase === 'folder' && (_jsxs(WizardStep, { step: 2, total: totalSteps, title: t('wiz.folder.title'), footer: t('wiz.folder.footer'), children: [wizardError ? _jsx(Text, { color: "red", children: wizardError }) : null, _jsx(SelectList, { items: [
|
|
263
306
|
{ label: process.cwd(), value: process.cwd(), hint: t('wiz.folder.current') },
|
|
264
307
|
...config.recentFolders
|
|
265
308
|
.filter((f) => f !== process.cwd())
|
|
266
309
|
.map((f) => ({ label: f, value: f, hint: t('wiz.folder.recent') })),
|
|
267
|
-
], allowInput: true, inputPlaceholder: t('wiz.folder.input'), onBack: wizardBack, onSelect: chooseFolder, onInput: chooseFolder })] })), phase === 'session' && (_jsx(WizardStep, { step: 3, total: totalSteps, title: t('wiz.session.title'), children: _jsx(SelectList, { items: [
|
|
310
|
+
], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.folder.input'), onBack: wizardBack, onSelect: chooseFolder, onInput: chooseFolder })] })), phase === 'session' && (_jsx(WizardStep, { step: 3, total: totalSteps, title: t('wiz.session.title'), children: _jsx(SelectList, { items: [
|
|
268
311
|
{ label: t('wiz.session.new'), value: '__new__', hint: t('wiz.session.newHint') },
|
|
269
312
|
...sessions.map((s) => ({
|
|
270
313
|
label: t('wiz.session.item', { date: new Date(s.data.savedAt).toLocaleString() }),
|
|
@@ -274,39 +317,46 @@ export function App({ config, initialFolder }) {
|
|
|
274
317
|
.join(', ')
|
|
275
318
|
.slice(0, 60)})`,
|
|
276
319
|
})),
|
|
277
|
-
], onBack: wizardBack, onSelect: chooseSession }) })), phase === 'provider' && providerStep.id === 'pick' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.title'), children: _jsx(SelectList, { items: (() => {
|
|
320
|
+
], height: wizardListHeight, onBack: wizardBack, onSelect: chooseSession }) })), phase === 'provider' && providerStep.id === 'pick' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.title'), children: _jsx(SelectList, { items: (() => {
|
|
278
321
|
const items = [];
|
|
322
|
+
const configuredNames = new Set(config.providers.map((p) => p.name.toLowerCase()));
|
|
279
323
|
// Section: Configured
|
|
280
|
-
|
|
281
|
-
label: p.name,
|
|
282
|
-
value: p.name,
|
|
283
|
-
detail: p.apiKey ? undefined : t('wiz.provider.needsKey'),
|
|
284
|
-
}));
|
|
285
|
-
if (configured.length > 0) {
|
|
324
|
+
if (config.providers.length > 0) {
|
|
286
325
|
items.push({ label: t('wiz.provider.section.configured'), value: '', section: true });
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
items.push(...cloudPresets.map((preset) => ({
|
|
295
|
-
label: preset.name,
|
|
296
|
-
value: preset.name,
|
|
297
|
-
detail: preset.defaultModel,
|
|
298
|
-
})));
|
|
326
|
+
for (const p of config.providers) {
|
|
327
|
+
items.push({
|
|
328
|
+
label: p.name,
|
|
329
|
+
value: p.name,
|
|
330
|
+
detail: p.apiKey ? undefined : t('wiz.provider.needsKey'),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
299
333
|
}
|
|
300
|
-
//
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
// Sections per category: western, chinese, gateways, inference, local
|
|
335
|
+
const catOrder = ['western', 'chinese', 'gateways', 'inference', 'local'];
|
|
336
|
+
const emoji = {
|
|
337
|
+
western: '\u{1F1FA}\u{1F1F8} ',
|
|
338
|
+
chinese: '\u{1F1E8}\u{1F1F3} ',
|
|
339
|
+
gateways: '\u{1F310} ',
|
|
340
|
+
inference: '\u26A1 ',
|
|
341
|
+
local: '\u{1F3E0} ',
|
|
342
|
+
};
|
|
343
|
+
for (const cat of catOrder) {
|
|
344
|
+
const presetsInCat = PROVIDER_PRESETS.filter((p) => p.category === cat && !configuredNames.has(p.name.toLowerCase()));
|
|
345
|
+
if (presetsInCat.length === 0)
|
|
346
|
+
continue;
|
|
347
|
+
const key = `wiz.provider.section.${cat}`;
|
|
348
|
+
const sectionLabel = emoji[cat] + t(key);
|
|
349
|
+
items.push({ value: '', label: sectionLabel, section: true });
|
|
350
|
+
for (const preset of presetsInCat) {
|
|
351
|
+
const detail = preset.models.length > 0
|
|
352
|
+
? `${preset.models.length} model${preset.models.length > 1 ? 's' : ''}`
|
|
353
|
+
: undefined;
|
|
354
|
+
items.push({
|
|
355
|
+
value: preset.name,
|
|
356
|
+
label: preset.name,
|
|
357
|
+
detail,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
310
360
|
}
|
|
311
361
|
// Custom always last
|
|
312
362
|
items.push({
|
|
@@ -315,19 +365,19 @@ export function App({ config, initialFolder }) {
|
|
|
315
365
|
detail: t('wiz.provider.customDetail'),
|
|
316
366
|
});
|
|
317
367
|
return items;
|
|
318
|
-
})(), onBack: wizardBack, onSelect: async (v) => {
|
|
368
|
+
})(), height: wizardListHeight, onBack: wizardBack, onSelect: async (v) => {
|
|
319
369
|
if (v === '__custom__')
|
|
320
370
|
return setProviderStep({ id: 'name' });
|
|
321
371
|
// Already-configured provider?
|
|
322
372
|
const existing = config.providers.find((x) => x.name === v);
|
|
323
373
|
if (existing) {
|
|
324
|
-
if (existing
|
|
374
|
+
if (providerReady(existing)) {
|
|
325
375
|
ctlRef.current?.setDefaultProvider(v);
|
|
326
376
|
ctlRef.current?.setSessionProvider(v);
|
|
327
|
-
|
|
377
|
+
setPhase('model');
|
|
328
378
|
}
|
|
329
379
|
else {
|
|
330
|
-
setProviderStep({ id: '
|
|
380
|
+
setProviderStep({ id: 'presetModel', provider: existing });
|
|
331
381
|
}
|
|
332
382
|
return;
|
|
333
383
|
}
|
|
@@ -368,18 +418,52 @@ export function App({ config, initialFolder }) {
|
|
|
368
418
|
]);
|
|
369
419
|
}
|
|
370
420
|
const ollamaProvider = { ...preset, apiKey: 'ollama-local', models, defaultModel };
|
|
371
|
-
|
|
372
|
-
ctlRef.current?.setSessionProvider(ollamaProvider.name);
|
|
373
|
-
setPhase('model');
|
|
421
|
+
setProviderStep({ id: 'presetModel', provider: ollamaProvider });
|
|
374
422
|
return;
|
|
375
423
|
}
|
|
376
|
-
setProviderStep({ id: '
|
|
377
|
-
} }) })), phase === 'provider' && providerStep.id === '
|
|
378
|
-
|
|
379
|
-
|
|
424
|
+
setProviderStep({ id: 'presetModel', provider: { ...preset, models: [...preset.models] } });
|
|
425
|
+
} }) })), phase === 'provider' && providerStep.id === 'presetModel' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.model.title'), children: [_jsx(Text, { color: "gray", children: t('wiz.model.provider', { name: providerStep.provider.name, url: providerStep.provider.baseUrl }) }), _jsx(SelectList, { items: [
|
|
426
|
+
...providerStep.provider.models.map((m) => ({
|
|
427
|
+
label: m,
|
|
428
|
+
value: m,
|
|
429
|
+
hint: m === providerStep.provider.defaultModel ? t('wiz.model.default') : undefined,
|
|
430
|
+
})),
|
|
431
|
+
], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onSelect: (v) => {
|
|
432
|
+
const provider = { ...providerStep.provider };
|
|
433
|
+
provider.defaultModel = v;
|
|
434
|
+
if (!provider.models.includes(v))
|
|
435
|
+
provider.models.push(v);
|
|
436
|
+
setProviderStep({ id: 'endpoint', provider });
|
|
437
|
+
}, onInput: (m) => {
|
|
438
|
+
const model = m.trim();
|
|
439
|
+
if (!model)
|
|
440
|
+
return;
|
|
441
|
+
const provider = { ...providerStep.provider, models: [...providerStep.provider.models] };
|
|
442
|
+
provider.defaultModel = model;
|
|
443
|
+
if (!provider.models.includes(model))
|
|
444
|
+
provider.models.push(model);
|
|
445
|
+
setProviderStep({ id: 'endpoint', provider });
|
|
446
|
+
} })] })), phase === 'provider' && providerStep.id === 'endpoint' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.endpoint.title', { name: providerStep.provider.name }), children: [_jsx(Text, { color: "gray", children: providerStep.provider.baseUrl }), _jsx(Text, { color: "gray", children: t('wiz.provider.endpoint.model', { model: providerStep.provider.defaultModel || providerStep.provider.models[0] || '—' }) }), _jsx(SelectList, { items: [
|
|
447
|
+
{ label: t('wiz.provider.endpoint.use'), value: 'use' },
|
|
448
|
+
{ label: t('wiz.provider.endpoint.edit'), value: 'edit' },
|
|
449
|
+
], height: wizardListHeight, onBack: wizardBack, onSelect: (v) => {
|
|
450
|
+
if (v === 'edit')
|
|
451
|
+
return setProviderStep({ id: 'editEndpoint', provider: providerStep.provider });
|
|
452
|
+
if (providerNeedsApiKey(providerStep.provider))
|
|
453
|
+
return setProviderStep({ id: 'key', provider: providerStep.provider });
|
|
454
|
+
finishProvider(providerStep.provider);
|
|
455
|
+
} })] })), phase === 'provider' && providerStep.id === 'editEndpoint' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.url.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: providerStep.provider.baseUrl, onBack: wizardBack, onInput: (url) => setProviderStep({ id: 'endpoint', provider: { ...providerStep.provider, baseUrl: url.trim() } }) }) })), phase === 'provider' && providerStep.id === 'key' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.provider.name }), footer: t('wiz.provider.key.footer'), children: [_jsx(Text, { color: "gray", children: providerStep.provider.baseUrl }), _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (k) => finishProvider({ ...providerStep.provider, apiKey: k.trim() }) })] })), phase === 'provider' && providerStep.id === 'name' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.name.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.name.ph'), onBack: wizardBack, onInput: (name) => setProviderStep({ id: 'url', name }) }) })), phase === 'provider' && providerStep.id === 'url' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.url.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onBack: wizardBack, onInput: (url) => setProviderStep({ id: 'customModel', name: providerStep.name, url }) }) })), phase === 'provider' && providerStep.id === 'customModel' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.model.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (model) => setProviderStep({
|
|
456
|
+
id: 'newKey',
|
|
457
|
+
provider: {
|
|
458
|
+
name: providerStep.name,
|
|
459
|
+
baseUrl: providerStep.url,
|
|
460
|
+
apiKey: '',
|
|
461
|
+
models: [model.trim()],
|
|
462
|
+
defaultModel: model.trim(),
|
|
463
|
+
},
|
|
464
|
+
}) }) })), phase === 'provider' && providerStep.id === 'newKey' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.provider.name }), footer: t('wiz.provider.key.footer'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (key) => finishProvider({
|
|
465
|
+
...providerStep.provider,
|
|
380
466
|
apiKey: key.trim(),
|
|
381
|
-
models: [providerStep.model],
|
|
382
|
-
defaultModel: providerStep.model,
|
|
383
467
|
}) }) })), phase === 'model' && !modelCustom && sessionProvider && (_jsxs(WizardStep, { step: 5, total: totalSteps, title: t('wiz.model.title'), children: [_jsx(Text, { color: "gray", children: t('wiz.model.provider', { name: sessionProvider.name, url: sessionProvider.baseUrl }) }), _jsx(SelectList, { items: [
|
|
384
468
|
...sessionProvider.models.map((m) => ({
|
|
385
469
|
label: m,
|
|
@@ -388,7 +472,7 @@ export function App({ config, initialFolder }) {
|
|
|
388
472
|
})),
|
|
389
473
|
{ label: t('wiz.model.custom'), value: '__custom__', hint: t('wiz.model.customHint') },
|
|
390
474
|
{ label: t('wiz.model.addProvider'), value: '__provider__' },
|
|
391
|
-
], onBack: wizardBack, onSelect: chooseModel })] })), phase === 'model' && modelCustom && (_jsx(WizardStep, { step: 5, total: totalSteps, title: t('wiz.model.customTitle'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (m) => {
|
|
475
|
+
], height: wizardListHeight, onBack: wizardBack, onSelect: chooseModel })] })), phase === 'model' && modelCustom && (_jsx(WizardStep, { step: 5, total: totalSteps, title: t('wiz.model.customTitle'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (m) => {
|
|
392
476
|
setModelCustom(false);
|
|
393
477
|
chooseModel(m);
|
|
394
478
|
} }) }))] })] }));
|
|
@@ -402,7 +486,8 @@ export function App({ config, initialFolder }) {
|
|
|
402
486
|
const approval = ctl.approvals[0];
|
|
403
487
|
const question = approval ? undefined : ctl.questions[0]; // approvals take priority
|
|
404
488
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
405
|
-
const
|
|
489
|
+
const viewOwnsKeyboard = view !== 'agents' && !settingsOpen;
|
|
490
|
+
const inputActive = inputReady && !approval && !question && !settingsOpen && !viewOwnsKeyboard;
|
|
406
491
|
return (_jsx(MainScreen, { ctl: ctl, folder: folder, view: view, focus: focus, rawLogs: rawLogs, systemLines: systemLines, agentNames: agentNames, approval: approval, question: question, inputActive: inputActive, onInput: (value, images) => {
|
|
407
492
|
const v = value.trim();
|
|
408
493
|
// Focus mode: plain text goes straight to the focused agent.
|
|
@@ -425,8 +510,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
425
510
|
const agents = [...ctl.board.agents.values()];
|
|
426
511
|
// Adapt the layout to the REAL terminal size (never resize the user's terminal).
|
|
427
512
|
const { stdout } = useStdout();
|
|
428
|
-
const cols = stdout?.columns ?? 100;
|
|
429
|
-
const rows = stdout?.rows ?? 30;
|
|
513
|
+
const cols = Math.max(20, stdout?.columns ?? 100);
|
|
514
|
+
const rows = Math.max(12, stdout?.rows ?? 30);
|
|
430
515
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
431
516
|
// Height budget: fixed sections → body gets the remainder.
|
|
432
517
|
const headerLines = 4; // border-box header (top border + 2 content lines + bottom border)
|
|
@@ -542,7 +627,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
542
627
|
specialists: 'specialists',
|
|
543
628
|
};
|
|
544
629
|
const viewLabel = VIEW_LABEL[view] ?? 'control room';
|
|
545
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills() })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists() })) : view === 'help' ? (_jsx(HelpView, {})) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
630
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: CHROME.muted, children: ["\u256D", '─'.repeat(cols - 2), "\u256E"] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { bold: true, color: BRAND.primary, children: "PARALLEL" }), _jsx(Text, { color: globalDotColor, children: " \u25CF" }), _jsxs(Text, { color: view === 'agents' ? CHROME.muted : BRAND.muted, children: [" ", viewLabel] }), rawLogs ? _jsx(Text, { color: UI.warn, children: " [RAW]" }) : null] }), _jsx(Text, { color: CHROME.muted, children: middleTruncate(folder, folderMax) })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Box, { flexDirection: "row", width: cols, children: [_jsx(Text, { color: CHROME.muted, children: "\u2502 " }), _jsxs(Box, { flexDirection: "row", width: cols - 4, justifyContent: agents.length > 0 ? 'space-between' : 'flex-end', children: [agents.length > 0 ? (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { children: [_jsxs(Text, { color: CHROME.muted, children: ["\u25C7 ", idleCount, " idle"] }), ' · ', _jsxs(Text, { color: workingCount > 0 ? STATE.working : CHROME.muted, children: ["\u25CF ", workingCount, " active"] }), ' · ', _jsxs(Text, { color: doneCount > 0 ? STATE.done : CHROME.muted, children: ["\u2713 ", doneCount, " done"] }), ' · ', _jsxs(Text, { color: errorCount > 0 ? STATE.error : CHROME.muted, children: ["\u2717 ", errorCount, " err"] })] }) })) : null, _jsxs(Text, { color: CHROME.muted, children: ["v", VERSION] })] }), _jsx(Text, { color: CHROME.muted, children: " \u2502" })] }), _jsxs(Text, { color: CHROME.muted, children: ["\u2570", '─'.repeat(cols - 2), "\u256F"] })] }), _jsx(Text, { children: " " }), _jsx(Box, { height: bodyHeight, overflow: "hidden", flexDirection: "column", children: view === 'settings' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "global", height: bodyHeight, onClose: onEscape })) : view === 'settings-session' ? (_jsx(SettingsPanel, { ctl: ctl, scope: "session", height: bodyHeight, onClose: onEscape })) : view === 'board' ? (_jsx(BoardView, { board: ctl.board })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills() })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists() })) : view === 'help' ? (_jsx(HelpView, { bodyHeight: bodyHeight })) : agents.length === 0 ? (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: _jsx(Text, { color: "gray", children: t('main.empty') }) })) : focused ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(AgentTranscript, { agent: focused, logs: visibleLogs, raw: rawLogs, scrolled: clampedScroll }), !focusFollowTail ? _jsx(Text, { color: UI.warn, children: "Viewing older \u00B7 PgDn to latest" }) : null] })) : (_jsx(AgentHub, { agents: agents, ctl: ctl, cols: cols, scroll: clampedHub, visibleRows: hubRows })) }), systemLines.length > 0 && !settingsOpen && (_jsxs(Box, { flexDirection: "column", children: [agents.length > 0 ? _jsx(Text, { color: UI.muted, bold: true, children: "Session" }) : null, (agents.length > 0
|
|
546
631
|
? systemLines
|
|
547
632
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
548
633
|
.slice(-2)
|
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
|
}
|