@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/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, detectProviderModels, getProvider, isLocalProvider, isPlaceholderModel, 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.4';
|
|
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 ----------
|
|
@@ -58,7 +60,7 @@ export function App({ config, initialFolder }) {
|
|
|
58
60
|
const [modelCustom, setModelCustom] = useState(false);
|
|
59
61
|
const ctlRef = useRef(directFolder ? new Controller(config, directFolder) : null);
|
|
60
62
|
// ---------- main state ----------
|
|
61
|
-
const [, setTick] = useState(0);
|
|
63
|
+
const [tick, setTick] = useState(0);
|
|
62
64
|
const [view, setView] = useState('agents');
|
|
63
65
|
// Focus mode (/focus <agent>): plain input is routed to that agent.
|
|
64
66
|
const [focus, setFocus] = useState(null);
|
|
@@ -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)
|
|
@@ -92,6 +107,12 @@ export function App({ config, initialFolder }) {
|
|
|
92
107
|
clearInterval(timer);
|
|
93
108
|
};
|
|
94
109
|
}, [ctl]);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!ctl || !focus)
|
|
112
|
+
return;
|
|
113
|
+
if (!ctl.board.getAgentByName(focus))
|
|
114
|
+
setFocus(null);
|
|
115
|
+
}, [ctl, focus, tick]);
|
|
95
116
|
// Session server: lets `parallel attach <agent>` open per-agent terminals.
|
|
96
117
|
useEffect(() => {
|
|
97
118
|
if (!ctl)
|
|
@@ -132,6 +153,22 @@ export function App({ config, initialFolder }) {
|
|
|
132
153
|
{ text: t('m.copyDone', { name: latest.name }), level: 'ok' },
|
|
133
154
|
]);
|
|
134
155
|
},
|
|
156
|
+
openProject: (nextFolder) => {
|
|
157
|
+
leaveCurrentProject();
|
|
158
|
+
if (nextFolder) {
|
|
159
|
+
chooseFolder(nextFolder);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
ctlRef.current = null;
|
|
163
|
+
setFolder('');
|
|
164
|
+
setPhase('folder');
|
|
165
|
+
},
|
|
166
|
+
openWizard: () => {
|
|
167
|
+
leaveCurrentProject();
|
|
168
|
+
ctlRef.current = null;
|
|
169
|
+
setFolder('');
|
|
170
|
+
setPhase('lang');
|
|
171
|
+
},
|
|
135
172
|
}), [exit]);
|
|
136
173
|
// ---------- wizard transitions ----------
|
|
137
174
|
// In normal launches, a complete config goes straight to the main TUI.
|
|
@@ -193,20 +230,32 @@ export function App({ config, initialFolder }) {
|
|
|
193
230
|
if (providerStep.id === 'pick') {
|
|
194
231
|
setPhase(sessions.length > 0 ? 'session' : 'folder');
|
|
195
232
|
}
|
|
196
|
-
else if (providerStep.id === '
|
|
233
|
+
else if (providerStep.id === 'presetModel') {
|
|
197
234
|
setProviderStep({ id: 'pick' });
|
|
198
235
|
}
|
|
236
|
+
else if (providerStep.id === 'endpoint') {
|
|
237
|
+
const isPreset = PROVIDER_PRESETS.some((p) => p.name.toLowerCase() === providerStep.provider.name.toLowerCase());
|
|
238
|
+
setProviderStep(isPreset
|
|
239
|
+
? { id: 'presetModel', provider: providerStep.provider }
|
|
240
|
+
: { id: 'customModel', name: providerStep.provider.name, url: providerStep.provider.baseUrl });
|
|
241
|
+
}
|
|
242
|
+
else if (providerStep.id === 'editEndpoint') {
|
|
243
|
+
setProviderStep({ id: 'endpoint', provider: providerStep.provider });
|
|
244
|
+
}
|
|
245
|
+
else if (providerStep.id === 'key') {
|
|
246
|
+
setProviderStep({ id: 'endpoint', provider: providerStep.provider });
|
|
247
|
+
}
|
|
199
248
|
else if (providerStep.id === 'name') {
|
|
200
249
|
setProviderStep({ id: 'pick' });
|
|
201
250
|
}
|
|
202
251
|
else if (providerStep.id === 'url') {
|
|
203
252
|
setProviderStep({ id: 'name' });
|
|
204
253
|
}
|
|
205
|
-
else if (providerStep.id === '
|
|
254
|
+
else if (providerStep.id === 'customModel') {
|
|
206
255
|
setProviderStep({ id: 'url', name: providerStep.name });
|
|
207
256
|
}
|
|
208
257
|
else if (providerStep.id === 'newKey') {
|
|
209
|
-
setProviderStep({ id: '
|
|
258
|
+
setProviderStep({ id: 'customModel', name: providerStep.provider.name, url: providerStep.provider.baseUrl });
|
|
210
259
|
}
|
|
211
260
|
}
|
|
212
261
|
};
|
|
@@ -221,6 +270,9 @@ export function App({ config, initialFolder }) {
|
|
|
221
270
|
const finishProvider = (p) => {
|
|
222
271
|
ctlRef.current?.saveProvider(p);
|
|
223
272
|
ctlRef.current?.setDefaultProvider(p.name);
|
|
273
|
+
ctlRef.current?.setSessionProvider(p.name);
|
|
274
|
+
if (p.defaultModel || p.models[0])
|
|
275
|
+
ctlRef.current?.setSessionModel(`${p.name}:${p.defaultModel || p.models[0]}`);
|
|
224
276
|
setProviderStep({ id: 'pick' });
|
|
225
277
|
enterMain();
|
|
226
278
|
};
|
|
@@ -259,12 +311,12 @@ export function App({ config, initialFolder }) {
|
|
|
259
311
|
if (phase !== 'main') {
|
|
260
312
|
const totalSteps = 5;
|
|
261
313
|
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: [
|
|
314
|
+
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
315
|
{ label: process.cwd(), value: process.cwd(), hint: t('wiz.folder.current') },
|
|
264
316
|
...config.recentFolders
|
|
265
317
|
.filter((f) => f !== process.cwd())
|
|
266
318
|
.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: [
|
|
319
|
+
], 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
320
|
{ label: t('wiz.session.new'), value: '__new__', hint: t('wiz.session.newHint') },
|
|
269
321
|
...sessions.map((s) => ({
|
|
270
322
|
label: t('wiz.session.item', { date: new Date(s.data.savedAt).toLocaleString() }),
|
|
@@ -274,39 +326,46 @@ export function App({ config, initialFolder }) {
|
|
|
274
326
|
.join(', ')
|
|
275
327
|
.slice(0, 60)})`,
|
|
276
328
|
})),
|
|
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: (() => {
|
|
329
|
+
], 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
330
|
const items = [];
|
|
331
|
+
const configuredNames = new Set(config.providers.map((p) => p.name.toLowerCase()));
|
|
279
332
|
// 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) {
|
|
333
|
+
if (config.providers.length > 0) {
|
|
286
334
|
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
|
-
})));
|
|
335
|
+
for (const p of config.providers) {
|
|
336
|
+
items.push({
|
|
337
|
+
label: p.name,
|
|
338
|
+
value: p.name,
|
|
339
|
+
detail: p.apiKey ? undefined : t('wiz.provider.needsKey'),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
299
342
|
}
|
|
300
|
-
//
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
343
|
+
// Sections per category: western, chinese, gateways, inference, local
|
|
344
|
+
const catOrder = ['western', 'chinese', 'gateways', 'inference', 'local'];
|
|
345
|
+
const emoji = {
|
|
346
|
+
western: '\u{1F1FA}\u{1F1F8} ',
|
|
347
|
+
chinese: '\u{1F1E8}\u{1F1F3} ',
|
|
348
|
+
gateways: '\u{1F310} ',
|
|
349
|
+
inference: '\u26A1 ',
|
|
350
|
+
local: '\u{1F3E0} ',
|
|
351
|
+
};
|
|
352
|
+
for (const cat of catOrder) {
|
|
353
|
+
const presetsInCat = PROVIDER_PRESETS.filter((p) => p.category === cat && !configuredNames.has(p.name.toLowerCase()));
|
|
354
|
+
if (presetsInCat.length === 0)
|
|
355
|
+
continue;
|
|
356
|
+
const key = `wiz.provider.section.${cat}`;
|
|
357
|
+
const sectionLabel = emoji[cat] + t(key);
|
|
358
|
+
items.push({ value: '', label: sectionLabel, section: true });
|
|
359
|
+
for (const preset of presetsInCat) {
|
|
360
|
+
const detail = preset.models.length > 0
|
|
361
|
+
? `${preset.models.length} model${preset.models.length > 1 ? 's' : ''}`
|
|
362
|
+
: undefined;
|
|
363
|
+
items.push({
|
|
364
|
+
value: preset.name,
|
|
365
|
+
label: preset.name,
|
|
366
|
+
detail,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
310
369
|
}
|
|
311
370
|
// Custom always last
|
|
312
371
|
items.push({
|
|
@@ -315,19 +374,19 @@ export function App({ config, initialFolder }) {
|
|
|
315
374
|
detail: t('wiz.provider.customDetail'),
|
|
316
375
|
});
|
|
317
376
|
return items;
|
|
318
|
-
})(), onBack: wizardBack, onSelect: async (v) => {
|
|
377
|
+
})(), height: wizardListHeight, onBack: wizardBack, onSelect: async (v) => {
|
|
319
378
|
if (v === '__custom__')
|
|
320
379
|
return setProviderStep({ id: 'name' });
|
|
321
380
|
// Already-configured provider?
|
|
322
381
|
const existing = config.providers.find((x) => x.name === v);
|
|
323
382
|
if (existing) {
|
|
324
|
-
if (existing
|
|
383
|
+
if (providerReady(existing)) {
|
|
325
384
|
ctlRef.current?.setDefaultProvider(v);
|
|
326
385
|
ctlRef.current?.setSessionProvider(v);
|
|
327
|
-
|
|
386
|
+
setPhase('model');
|
|
328
387
|
}
|
|
329
388
|
else {
|
|
330
|
-
setProviderStep({ id: '
|
|
389
|
+
setProviderStep({ id: 'presetModel', provider: existing });
|
|
331
390
|
}
|
|
332
391
|
return;
|
|
333
392
|
}
|
|
@@ -341,45 +400,87 @@ export function App({ config, initialFolder }) {
|
|
|
341
400
|
...ls.slice(-5),
|
|
342
401
|
{ text: t('wiz.provider.ollama.checking', { url: preset.baseUrl }), level: 'info' },
|
|
343
402
|
]);
|
|
344
|
-
|
|
345
|
-
let
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const data = (await resp.json());
|
|
353
|
-
const detected = data?.data?.map((m) => m.id).filter(Boolean) ?? [];
|
|
354
|
-
if (detected.length > 0) {
|
|
355
|
-
models = detected;
|
|
356
|
-
defaultModel = detected[0];
|
|
357
|
-
setSystemLines((ls) => [
|
|
358
|
-
...ls.slice(-5),
|
|
359
|
-
{ text: t('wiz.provider.ollama.found', { n: detected.length }), level: 'ok' },
|
|
360
|
-
]);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
403
|
+
const detected = await detectProviderModels(preset);
|
|
404
|
+
let models = detected?.models ?? [...preset.models];
|
|
405
|
+
let defaultModel = detected?.defaultModel ?? preset.defaultModel;
|
|
406
|
+
if (detected) {
|
|
407
|
+
setSystemLines((ls) => [
|
|
408
|
+
...ls.slice(-5),
|
|
409
|
+
{ text: t('wiz.provider.ollama.found', { n: detected.models.length }), level: 'ok' },
|
|
410
|
+
]);
|
|
363
411
|
}
|
|
364
|
-
|
|
412
|
+
else {
|
|
365
413
|
setSystemLines((ls) => [
|
|
366
414
|
...ls.slice(-5),
|
|
367
415
|
{ text: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }), level: 'warn' },
|
|
368
416
|
]);
|
|
369
417
|
}
|
|
370
418
|
const ollamaProvider = { ...preset, apiKey: 'ollama-local', models, defaultModel };
|
|
371
|
-
|
|
372
|
-
ctlRef.current?.setSessionProvider(ollamaProvider.name);
|
|
373
|
-
setPhase('model');
|
|
419
|
+
setProviderStep({ id: 'presetModel', provider: ollamaProvider });
|
|
374
420
|
return;
|
|
375
421
|
}
|
|
376
|
-
setProviderStep({ id: '
|
|
377
|
-
} }) })), phase === 'provider' && providerStep.id === '
|
|
378
|
-
|
|
379
|
-
|
|
422
|
+
setProviderStep({ id: 'presetModel', provider: { ...preset, models: [...preset.models] } });
|
|
423
|
+
} }) })), 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: [
|
|
424
|
+
...providerStep.provider.models.map((m) => ({
|
|
425
|
+
label: m,
|
|
426
|
+
value: m,
|
|
427
|
+
hint: m === providerStep.provider.defaultModel ? t('wiz.model.default') : undefined,
|
|
428
|
+
})),
|
|
429
|
+
], height: wizardListHeight, allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onSelect: (v) => {
|
|
430
|
+
const provider = { ...providerStep.provider };
|
|
431
|
+
provider.defaultModel = v;
|
|
432
|
+
if (!provider.models.includes(v))
|
|
433
|
+
provider.models.push(v);
|
|
434
|
+
setProviderStep({ id: 'endpoint', provider });
|
|
435
|
+
}, onInput: (m) => {
|
|
436
|
+
const model = m.trim();
|
|
437
|
+
if (!model)
|
|
438
|
+
return;
|
|
439
|
+
const provider = { ...providerStep.provider, models: [...providerStep.provider.models] };
|
|
440
|
+
provider.defaultModel = model;
|
|
441
|
+
if (!provider.models.includes(model))
|
|
442
|
+
provider.models.push(model);
|
|
443
|
+
setProviderStep({ id: 'endpoint', provider });
|
|
444
|
+
} })] })), 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: [
|
|
445
|
+
{ label: t('wiz.provider.endpoint.use'), value: 'use' },
|
|
446
|
+
{ label: t('wiz.provider.endpoint.edit'), value: 'edit' },
|
|
447
|
+
], height: wizardListHeight, onBack: wizardBack, onSelect: (v) => {
|
|
448
|
+
if (v === 'edit')
|
|
449
|
+
return setProviderStep({ id: 'editEndpoint', provider: providerStep.provider });
|
|
450
|
+
if (providerNeedsApiKey(providerStep.provider))
|
|
451
|
+
return setProviderStep({ id: 'key', provider: providerStep.provider });
|
|
452
|
+
finishProvider(providerStep.provider);
|
|
453
|
+
} })] })), 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) => {
|
|
454
|
+
const baseUrl = url.trim();
|
|
455
|
+
setProviderStep({
|
|
456
|
+
id: 'endpoint',
|
|
457
|
+
provider: {
|
|
458
|
+
...providerStep.provider,
|
|
459
|
+
baseUrl,
|
|
460
|
+
requiresApiKey: !isLocalProvider({ baseUrl }),
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
} }) })), 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) => {
|
|
464
|
+
const trimmed = model.trim();
|
|
465
|
+
if (isPlaceholderModel(trimmed)) {
|
|
466
|
+
setSystemLines((ls) => [...ls.slice(-5), { text: t('set.modelPlaceholder'), level: 'warn' }]);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const local = isLocalProvider({ baseUrl: providerStep.url });
|
|
470
|
+
setProviderStep({
|
|
471
|
+
id: 'endpoint',
|
|
472
|
+
provider: {
|
|
473
|
+
name: providerStep.name,
|
|
474
|
+
baseUrl: providerStep.url,
|
|
475
|
+
apiKey: '',
|
|
476
|
+
models: [trimmed],
|
|
477
|
+
defaultModel: trimmed,
|
|
478
|
+
requiresApiKey: !local,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
} }) })), 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({
|
|
482
|
+
...providerStep.provider,
|
|
380
483
|
apiKey: key.trim(),
|
|
381
|
-
models: [providerStep.model],
|
|
382
|
-
defaultModel: providerStep.model,
|
|
383
484
|
}) }) })), 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
485
|
...sessionProvider.models.map((m) => ({
|
|
385
486
|
label: m,
|
|
@@ -388,7 +489,7 @@ export function App({ config, initialFolder }) {
|
|
|
388
489
|
})),
|
|
389
490
|
{ label: t('wiz.model.custom'), value: '__custom__', hint: t('wiz.model.customHint') },
|
|
390
491
|
{ 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) => {
|
|
492
|
+
], 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
493
|
setModelCustom(false);
|
|
393
494
|
chooseModel(m);
|
|
394
495
|
} }) }))] })] }));
|
|
@@ -402,7 +503,8 @@ export function App({ config, initialFolder }) {
|
|
|
402
503
|
const approval = ctl.approvals[0];
|
|
403
504
|
const question = approval ? undefined : ctl.questions[0]; // approvals take priority
|
|
404
505
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
405
|
-
const
|
|
506
|
+
const viewOwnsKeyboard = view !== 'agents' && !settingsOpen;
|
|
507
|
+
const inputActive = inputReady && !approval && !question && !settingsOpen && !viewOwnsKeyboard;
|
|
406
508
|
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
509
|
const v = value.trim();
|
|
408
510
|
// Focus mode: plain text goes straight to the focused agent.
|
|
@@ -425,8 +527,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
425
527
|
const agents = [...ctl.board.agents.values()];
|
|
426
528
|
// Adapt the layout to the REAL terminal size (never resize the user's terminal).
|
|
427
529
|
const { stdout } = useStdout();
|
|
428
|
-
const cols = stdout?.columns ?? 100;
|
|
429
|
-
const rows = stdout?.rows ?? 30;
|
|
530
|
+
const cols = Math.max(20, stdout?.columns ?? 100);
|
|
531
|
+
const rows = Math.max(12, stdout?.rows ?? 30);
|
|
430
532
|
const settingsOpen = view === 'settings' || view === 'settings-session';
|
|
431
533
|
// Height budget: fixed sections → body gets the remainder.
|
|
432
534
|
const headerLines = 4; // border-box header (top border + 2 content lines + bottom border)
|
|
@@ -463,8 +565,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
463
565
|
: [];
|
|
464
566
|
const [hubScroll, setHubScroll] = useState(0);
|
|
465
567
|
const [hubFollowTail, setHubFollowTail] = useState(true);
|
|
466
|
-
const hubRows = Math.max(
|
|
467
|
-
const maxHubScroll = Math.max(0, agents.length - hubRows);
|
|
568
|
+
const hubRows = Math.max(3, bodyHeight - 2);
|
|
569
|
+
const maxHubScroll = Math.max(0, agents.length - Math.max(1, Math.floor(hubRows / 2)));
|
|
468
570
|
const clampedHub = Math.min(hubScroll, maxHubScroll);
|
|
469
571
|
const logSeq = ctl.board.logs.length > 0 ? ctl.board.logs[ctl.board.logs.length - 1].seq ?? ctl.board.logs.length : 0;
|
|
470
572
|
useEffect(() => {
|
|
@@ -475,7 +577,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
475
577
|
if (hubFollowTail)
|
|
476
578
|
setHubScroll(0);
|
|
477
579
|
}, [logSeq, agents.length, hubFollowTail]);
|
|
478
|
-
// Scroll helpers
|
|
580
|
+
// Scroll helpers.
|
|
479
581
|
const scrollFocusUp = () => {
|
|
480
582
|
setFocusFollowTail(false);
|
|
481
583
|
setScroll((s) => Math.min(s + 1, maxScroll));
|
|
@@ -500,25 +602,23 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
500
602
|
return next;
|
|
501
603
|
});
|
|
502
604
|
};
|
|
503
|
-
// Keyboard:
|
|
504
|
-
// When CommandInput is NOT focused, Up/Down scroll the hub or focused agent;
|
|
505
|
-
// when it IS focused, CommandInput's own useInput sees them first (history nav).
|
|
605
|
+
// Keyboard: PgUp/PgDn always scroll hub/focus. Up/Down only scroll when input is inactive.
|
|
506
606
|
useInput((_input, key) => {
|
|
507
|
-
if (key.escape)
|
|
607
|
+
if (key.escape && !settingsOpen)
|
|
508
608
|
onEscape();
|
|
509
609
|
if (focused) {
|
|
510
|
-
if (key.pageUp || key.upArrow)
|
|
610
|
+
if (key.pageUp || (!inputActive && key.upArrow))
|
|
511
611
|
scrollFocusUp();
|
|
512
|
-
if (key.pageDown || key.downArrow)
|
|
612
|
+
if (key.pageDown || (!inputActive && key.downArrow))
|
|
513
613
|
scrollFocusDown();
|
|
514
614
|
}
|
|
515
615
|
else if (view === 'agents') {
|
|
516
|
-
if (key.pageUp || key.upArrow)
|
|
616
|
+
if (key.pageUp || (!inputActive && key.upArrow))
|
|
517
617
|
scrollHubUp();
|
|
518
|
-
if (key.pageDown || key.downArrow)
|
|
618
|
+
if (key.pageDown || (!inputActive && key.downArrow))
|
|
519
619
|
scrollHubDown();
|
|
520
620
|
}
|
|
521
|
-
}, { isActive: !
|
|
621
|
+
}, { isActive: !approval && !question });
|
|
522
622
|
const idleCount = agents.filter((a) => a.state === 'idle').length;
|
|
523
623
|
const workingCount = agents.filter((a) => ['working', 'thinking', 'listening'].includes(a.state)).length;
|
|
524
624
|
const doneCount = agents.filter((a) => a.state === 'done').length;
|
|
@@ -542,7 +642,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
|
|
|
542
642
|
specialists: 'specialists',
|
|
543
643
|
};
|
|
544
644
|
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
|
|
645
|
+
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, bodyHeight: bodyHeight })) : view === 'notes' ? (_jsx(NotesView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'sessions' ? (_jsx(SessionsView, { projectRoot: ctl.projectRoot, bodyHeight: bodyHeight })) : view === 'diff' ? (_jsx(DiffView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'cost' ? (_jsx(CostView, { board: ctl.board, bodyHeight: bodyHeight })) : view === 'skills' ? (_jsx(SkillsView, { skills: ctl.getSkills(), bodyHeight: bodyHeight })) : view === 'specialists' ? (_jsx(SpecialistsView, { specialists: ctl.getSpecialists(), bodyHeight: bodyHeight })) : 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
646
|
? systemLines
|
|
547
647
|
.filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
|
|
548
648
|
.slice(-2)
|
|
@@ -573,27 +673,33 @@ function groupAgents(agents) {
|
|
|
573
673
|
function AgentHub({ agents, ctl, cols, scroll, visibleRows, }) {
|
|
574
674
|
const groups = groupAgents([...agents].sort((a, b) => STATE_META[a.state].rank - STATE_META[b.state].rank || a.startedAt - b.startedAt));
|
|
575
675
|
let skipped = scroll;
|
|
576
|
-
let
|
|
676
|
+
let renderedAgents = 0;
|
|
677
|
+
let renderedLines = 0;
|
|
577
678
|
const rows = [];
|
|
679
|
+
let full = false;
|
|
578
680
|
for (const group of groups) {
|
|
579
|
-
const groupRows = [];
|
|
580
681
|
for (const agent of group.agents) {
|
|
581
682
|
if (skipped > 0) {
|
|
582
683
|
skipped--;
|
|
583
684
|
continue;
|
|
584
685
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
686
|
+
const needsSeparator = rows.length > 0;
|
|
687
|
+
const neededLines = 2 + (needsSeparator ? 1 : 0);
|
|
688
|
+
if (renderedLines + neededLines > visibleRows) {
|
|
689
|
+
full = true;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
if (needsSeparator) {
|
|
693
|
+
rows.push(_jsx(Text, { color: CHROME.separator, children: '─'.repeat(cols - 2) }, `sep-${group.title}-${agent.id}`));
|
|
694
|
+
renderedLines++;
|
|
695
|
+
}
|
|
696
|
+
renderedAgents++;
|
|
697
|
+
renderedLines += 2;
|
|
698
|
+
rows.push(_jsx(AgentRow, { agent: agent, logs: ctl.board.logsFor(agent.id, 8), cols: cols }, agent.id));
|
|
594
699
|
}
|
|
595
|
-
|
|
700
|
+
if (full)
|
|
701
|
+
break;
|
|
596
702
|
}
|
|
597
|
-
const below = Math.max(0, agents.length - scroll -
|
|
703
|
+
const below = Math.max(0, agents.length - scroll - renderedAgents);
|
|
598
704
|
return (_jsxs(Box, { flexDirection: "column", children: [scroll > 0 ? _jsxs(Text, { color: CHROME.muted, children: ["\u25B2 ", scroll, " older \u00B7 PgDn to latest"] }) : null, rows, below > 0 ? _jsxs(Text, { color: CHROME.muted, children: ["\u25BC ", below, " more \u00B7 PgUp"] }) : null] }));
|
|
599
705
|
}
|