@parallel-cli/parallel 0.4.0 → 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/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 (v0.3.3). Hardcoded — rootDir: "src" prevents importing ../../package.json.
21
- const VERSION = '0.3.3';
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.apiKey && (p.defaultModel || p.models[0]) ? p : undefined;
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 === 'key') {
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 === 'model') {
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: 'model', name: providerStep.name, url: providerStep.url });
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,43 +317,153 @@ 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: [
278
- ...config.providers.map((p) => ({
279
- label: p.name,
280
- value: `existing:${p.name}`,
281
- hint: `(${p.baseUrl}${p.apiKey ? '' : ' — ' + t('wiz.provider.needsKey')})`,
282
- })),
283
- ...PROVIDER_PRESETS.filter((preset) => !config.providers.some((p) => p.name.toLowerCase() === preset.name.toLowerCase())).map((preset) => ({
284
- label: preset.name,
285
- value: `preset:${preset.name}`,
286
- hint: t('wiz.provider.presetHint', { url: preset.baseUrl, model: preset.defaultModel }),
287
- })),
288
- { label: t('wiz.provider.custom'), value: '__custom__', hint: t('wiz.provider.customHint') },
289
- ], onBack: wizardBack, onSelect: (v) => {
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: (() => {
321
+ const items = [];
322
+ const configuredNames = new Set(config.providers.map((p) => p.name.toLowerCase()));
323
+ // Section: Configured
324
+ if (config.providers.length > 0) {
325
+ items.push({ label: t('wiz.provider.section.configured'), value: '', section: true });
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
+ }
333
+ }
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
+ }
360
+ }
361
+ // Custom always last
362
+ items.push({
363
+ label: t('wiz.provider.custom'),
364
+ value: '__custom__',
365
+ detail: t('wiz.provider.customDetail'),
366
+ });
367
+ return items;
368
+ })(), height: wizardListHeight, onBack: wizardBack, onSelect: async (v) => {
290
369
  if (v === '__custom__')
291
370
  return setProviderStep({ id: 'name' });
292
- if (v.startsWith('preset:')) {
293
- const preset = PROVIDER_PRESETS.find((p) => p.name === v.slice('preset:'.length));
294
- if (preset)
295
- return setProviderStep({ id: 'key', preset: { ...preset, models: [...preset.models] } });
296
- }
297
- const p = config.providers.find((x) => x.name === v.slice('existing:'.length));
298
- if (!p)
371
+ // Already-configured provider?
372
+ const existing = config.providers.find((x) => x.name === v);
373
+ if (existing) {
374
+ if (providerReady(existing)) {
375
+ ctlRef.current?.setDefaultProvider(v);
376
+ ctlRef.current?.setSessionProvider(v);
377
+ setPhase('model');
378
+ }
379
+ else {
380
+ setProviderStep({ id: 'presetModel', provider: existing });
381
+ }
299
382
  return;
300
- if (p.apiKey) {
301
- ctlRef.current?.setDefaultProvider(p.name);
302
- ctlRef.current?.setSessionProvider(p.name);
303
- enterMain();
304
383
  }
305
- else {
306
- setProviderStep({ id: 'key', preset: p });
384
+ // Must be a preset
385
+ const preset = PROVIDER_PRESETS.find((p) => p.name === v);
386
+ if (!preset)
387
+ return;
388
+ // Ollama: connectivity check with 2s timeout
389
+ if (preset.name.toLowerCase() === 'ollama') {
390
+ setSystemLines((ls) => [
391
+ ...ls.slice(-5),
392
+ { text: t('wiz.provider.ollama.checking', { url: preset.baseUrl }), level: 'info' },
393
+ ]);
394
+ let models = [...preset.models];
395
+ let defaultModel = preset.defaultModel;
396
+ try {
397
+ const controller = new AbortController();
398
+ const timeout = setTimeout(() => controller.abort(), 2000);
399
+ const resp = await fetch(preset.baseUrl + '/models', { signal: controller.signal });
400
+ clearTimeout(timeout);
401
+ if (resp.ok) {
402
+ const data = (await resp.json());
403
+ const detected = data?.data?.map((m) => m.id).filter(Boolean) ?? [];
404
+ if (detected.length > 0) {
405
+ models = detected;
406
+ defaultModel = detected[0];
407
+ setSystemLines((ls) => [
408
+ ...ls.slice(-5),
409
+ { text: t('wiz.provider.ollama.found', { n: detected.length }), level: 'ok' },
410
+ ]);
411
+ }
412
+ }
413
+ }
414
+ catch {
415
+ setSystemLines((ls) => [
416
+ ...ls.slice(-5),
417
+ { text: t('wiz.provider.ollama.notFound', { url: preset.baseUrl }), level: 'warn' },
418
+ ]);
419
+ }
420
+ const ollamaProvider = { ...preset, apiKey: 'ollama-local', models, defaultModel };
421
+ setProviderStep({ id: 'presetModel', provider: ollamaProvider });
422
+ return;
307
423
  }
308
- } }) })), phase === 'provider' && providerStep.id === 'key' && (_jsxs(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.preset.name }), footer: t('wiz.provider.key.footer'), children: [_jsx(Text, { color: "gray", children: providerStep.preset.baseUrl }), _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (k) => finishProvider({ ...providerStep.preset, 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: [], 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: [], allowInput: true, inputPlaceholder: t('wiz.provider.url.ph'), onBack: wizardBack, onInput: (url) => setProviderStep({ id: 'model', name: providerStep.name, url }) }) })), phase === 'provider' && providerStep.id === 'model' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.model.title'), footer: t('wiz.footer.type'), children: _jsx(SelectList, { items: [], allowInput: true, inputPlaceholder: t('wiz.provider.model.ph'), onBack: wizardBack, onInput: (model) => setProviderStep({ id: 'newKey', name: providerStep.name, url: providerStep.url, model }) }) })), phase === 'provider' && providerStep.id === 'newKey' && (_jsx(WizardStep, { step: 4, total: totalSteps, title: t('wiz.provider.key.title', { name: providerStep.name }), footer: t('wiz.provider.key.footer'), children: _jsx(SelectList, { items: [], allowInput: true, mask: true, inputPlaceholder: "sk-\u2026", onBack: wizardBack, onInput: (key) => finishProvider({
309
- name: providerStep.name,
310
- baseUrl: providerStep.url,
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,
311
466
  apiKey: key.trim(),
312
- models: [providerStep.model],
313
- defaultModel: providerStep.model,
314
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: [
315
468
  ...sessionProvider.models.map((m) => ({
316
469
  label: m,
@@ -319,7 +472,7 @@ export function App({ config, initialFolder }) {
319
472
  })),
320
473
  { label: t('wiz.model.custom'), value: '__custom__', hint: t('wiz.model.customHint') },
321
474
  { label: t('wiz.model.addProvider'), value: '__provider__' },
322
- ], 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) => {
323
476
  setModelCustom(false);
324
477
  chooseModel(m);
325
478
  } }) }))] })] }));
@@ -333,7 +486,8 @@ export function App({ config, initialFolder }) {
333
486
  const approval = ctl.approvals[0];
334
487
  const question = approval ? undefined : ctl.questions[0]; // approvals take priority
335
488
  const settingsOpen = view === 'settings' || view === 'settings-session';
336
- const inputActive = inputReady && !approval && !question && !settingsOpen;
489
+ const viewOwnsKeyboard = view !== 'agents' && !settingsOpen;
490
+ const inputActive = inputReady && !approval && !question && !settingsOpen && !viewOwnsKeyboard;
337
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) => {
338
492
  const v = value.trim();
339
493
  // Focus mode: plain text goes straight to the focused agent.
@@ -356,8 +510,8 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
356
510
  const agents = [...ctl.board.agents.values()];
357
511
  // Adapt the layout to the REAL terminal size (never resize the user's terminal).
358
512
  const { stdout } = useStdout();
359
- const cols = stdout?.columns ?? 100;
360
- const rows = stdout?.rows ?? 30;
513
+ const cols = Math.max(20, stdout?.columns ?? 100);
514
+ const rows = Math.max(12, stdout?.rows ?? 30);
361
515
  const settingsOpen = view === 'settings' || view === 'settings-session';
362
516
  // Height budget: fixed sections → body gets the remainder.
363
517
  const headerLines = 4; // border-box header (top border + 2 content lines + bottom border)
@@ -473,7 +627,7 @@ function MainScreen({ ctl, folder, view, focus, rawLogs, systemLines, agentNames
473
627
  specialists: 'specialists',
474
628
  };
475
629
  const viewLabel = VIEW_LABEL[view] ?? 'control room';
476
- 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
477
631
  ? systemLines
478
632
  .filter((l) => !/^Ready|^Type a task|^⚡ Ready|^Default \/task|^Agent .* launched/.test(l.text))
479
633
  .slice(-2)
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useRef, useState } from 'react';
2
+ import { useEffect, useRef, useState } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { matchCommands } from '../commands.js';
5
5
  import { t } from '../i18n.js';
@@ -43,6 +43,7 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
43
43
  const [attachments, setAttachments] = useState([]);
44
44
  const [history, setHistory] = useState([]);
45
45
  const [histIdx, setHistIdx] = useState(-1);
46
+ const [selectedSuggestion, setSelectedSuggestion] = useState(0);
46
47
  const attSeq = useRef(0);
47
48
  const reset = () => {
48
49
  setValue('');
@@ -62,7 +63,8 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
62
63
  const images = attachments.filter((a) => a.kind === 'image');
63
64
  if (!full && images.length === 0)
64
65
  return;
65
- setHistory((h) => [...h.slice(-49), v]);
66
+ if (!full.toLowerCase().startsWith('/key '))
67
+ setHistory((h) => [...h.slice(-49), v]);
66
68
  setHistIdx(-1);
67
69
  reset();
68
70
  onSubmit(full, images.length > 0 ? images.map((i) => i.dataUri) : undefined);
@@ -85,19 +87,30 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
85
87
  setAttachments((arr) => [...arr, { kind: 'image', n, dataUri: img.dataUri, label: img.label }]);
86
88
  notify?.(t('input.imageAdded'));
87
89
  };
90
+ const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 10) : [];
91
+ const agentSuggestions = value.startsWith('@') && !value.includes(' ')
92
+ ? ['all', ...agentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
93
+ : [];
94
+ const suggestionCount = cmdSuggestions.length > 0 ? cmdSuggestions.length : agentSuggestions.length;
95
+ const hasSuggestions = suggestionCount > 0;
96
+ const exactCommand = cmdSuggestions.some((c) => c.name === value.toLowerCase() || c.aliases?.some((a) => a === value.toLowerCase()));
97
+ useEffect(() => {
98
+ setSelectedSuggestion(0);
99
+ }, [value]);
100
+ useEffect(() => {
101
+ if (selectedSuggestion >= suggestionCount)
102
+ setSelectedSuggestion(Math.max(0, suggestionCount - 1));
103
+ }, [selectedSuggestion, suggestionCount]);
88
104
  const completeBest = () => {
89
- const cmd = bestCommandCompletion(value);
90
- if (cmd) {
91
- setValue(cmd);
105
+ if (cmdSuggestions.length > 0) {
106
+ const cmd = cmdSuggestions[Math.min(selectedSuggestion, cmdSuggestions.length - 1)];
107
+ setValue(`${cmd.name} `);
92
108
  return true;
93
109
  }
94
- if (value.startsWith('@')) {
95
- const frag = value.slice(1).toLowerCase();
96
- const m = ['all', ...agentNames].find((n) => n.toLowerCase().startsWith(frag));
97
- if (m) {
98
- setValue('@' + m + ' ');
99
- return true;
100
- }
110
+ if (agentSuggestions.length > 0) {
111
+ const agent = agentSuggestions[Math.min(selectedSuggestion, agentSuggestions.length - 1)];
112
+ setValue('@' + agent + ' ');
113
+ return true;
101
114
  }
102
115
  return false;
103
116
  };
@@ -110,6 +123,10 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
110
123
  return;
111
124
  }
112
125
  if (key.return) {
126
+ if (hasSuggestions && !exactCommand) {
127
+ completeBest();
128
+ return;
129
+ }
113
130
  submit(value);
114
131
  return;
115
132
  }
@@ -126,6 +143,10 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
126
143
  return;
127
144
  }
128
145
  if (key.upArrow) {
146
+ if (hasSuggestions) {
147
+ setSelectedSuggestion((i) => (i - 1 + suggestionCount) % suggestionCount);
148
+ return;
149
+ }
129
150
  setHistIdx((i) => {
130
151
  const ni = i === -1 ? history.length - 1 : Math.max(0, i - 1);
131
152
  if (history[ni] !== undefined)
@@ -135,6 +156,10 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
135
156
  return;
136
157
  }
137
158
  if (key.downArrow) {
159
+ if (hasSuggestions) {
160
+ setSelectedSuggestion((i) => (i + 1) % suggestionCount);
161
+ return;
162
+ }
138
163
  setHistIdx((i) => {
139
164
  if (i === -1)
140
165
  return -1;
@@ -183,13 +208,13 @@ export function CommandInput({ active, placeholder, mask, agentNames = [], agent
183
208
  }
184
209
  setValue((v) => v + input);
185
210
  }, { isActive: active });
186
- const cmdSuggestions = value.startsWith('/') && !value.includes(' ') ? matchCommands(value).slice(0, 10) : [];
187
- const agentSuggestions = value.startsWith('@') && !value.includes(' ')
188
- ? ['all', ...agentNames].filter((n) => n.toLowerCase().startsWith(value.slice(1).toLowerCase())).slice(0, 8)
189
- : [];
190
211
  const shown = mask ? '•'.repeat(value.length) : value;
191
212
  const byName = new Map(agents.flatMap((a) => [[a.name, a], [a.alias, a]]));
192
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: c.name.padEnd(14) }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name)))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", bold: true, children: ["@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
213
+ const commandIndexes = new Map(cmdSuggestions.map((c, i) => [c.name, i]));
214
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", wrap: "truncate-end", children: modeHint(value) }), cmdSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: groupedCommands(cmdSuggestions).map(([group, commands]) => (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", bold: true, children: GROUP_LABEL[group] ?? group }), commands.map((c) => ((() => {
215
+ const selected = commandIndexes.get(c.name) === selectedSuggestion;
216
+ return (_jsxs(Text, { children: [_jsxs(Text, { color: selected ? 'cyanBright' : 'cyan', bold: true, children: [selected ? '› ' : ' ', c.name.padEnd(14)] }), _jsx(Text, { color: "yellow", children: c.args.padEnd(22) }), _jsx(Text, { color: "gray", children: t(c.descKey) })] }, c.name));
217
+ })()))] }, group))) })), agentSuggestions.length > 0 && (_jsx(Box, { borderStyle: "single", borderColor: "gray", flexDirection: "column", paddingX: 1, children: agentSuggestions.map((n, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: i === selectedSuggestion ? 'cyanBright' : 'cyan', bold: true, children: [i === selectedSuggestion ? '› ' : ' ', "@", n.padEnd(10)] }), _jsx(Text, { color: "gray", children: n === 'all'
193
218
  ? t('input.atAll')
194
219
  : `${byName.get(n)?.state ?? ''} ${byName.get(n)?.mode ? `/${byName.get(n)?.mode}` : ''}` })] }, n))) })), attachments.length > 0 && (_jsx(Box, { flexDirection: "row", gap: 1, paddingX: 1, children: attachments.map((a) => (_jsxs(Text, { color: "cyan", backgroundColor: "gray", children: [' ', a.kind === 'paste' ? t('input.attPaste', { n: a.n, lines: a.lines }) : t('input.attImage', { n: a.n, file: a.label }), ' '] }, a.n))) })), _jsxs(Box, { borderStyle: "single", borderColor: active ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { color: "cyanBright", bold: true, children: ["\u203A", ' '] }), shown ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: shown }), active && _jsx(Text, { color: "cyanBright", children: "\u2588" })] })) : (_jsxs(_Fragment, { children: [active && _jsx(Text, { color: "cyanBright", children: "\u2588" }), _jsx(Text, { color: "gray", children: placeholder })] }))] })] }));
195
220
  }